最近接了个工业现场巡检项目,要给几千张设备照片标注三类目标:仪表盘、阀门、渗漏点。团队三个人手动标了三天,一人一天三百张,眼睛都快瞎了,更麻烦的是三个人画的框风格还不一样,有人框得紧,有人框得松,连类别都偶尔混。我意识到不能再纯手动搞了,开始研究怎么把自动标注工具串成一条流水线。最后定下来的组合就是 X-AnyLabeling、autodistill 和 Grounded-SAM 三件套:Grounded-SAM 负责从文本描述直接出分割掩码,autodistill 负责把基础模型的标注能力转成目标模型的训练数据,X-AnyLabeling 则作为人工复核和格式管理的枢纽。这套配置跑下来,同样的数据量,时间压缩到原来的三分之一左右,而且标注一致性明显比纯手动好。下面把完整的选型逻辑、环境搭建、实操步骤和踩过的坑都写出来,适合正在被数据标注折磨的目标检测/分割项目团队。
1. 选型思考:为什么是 X-AnyLabeling、autodistill、Grounded-SAM 三件套
1.1 手动标注的瓶颈到底在哪里
先用数据说事。目标检测场景里,一张干净的单目标图片画个框大概十到二十秒,但如果是拥挤场景,比如工厂里一堆表计阀门互相遮挡,一张图三分钟都未必标得干净。实例分割更夸张,沿着边缘描一个精细的 mask,慢的时候一张图要十分钟往上。按这个速度算,五万张图的生产级数据集,一个人全职要标大半年,这还没算审核和返工的时间。
更隐蔽的成本是质量漂移。标注员上午精神好,框画得紧;下午累了,框随手拉大一圈;新来的标注员对"渗漏点"理解不到位,把水管接头的阴影也框进去。这种不一致进了训练集,模型学到的边界就是错的。所以单纯堆人力解决不了问题,关键是要有一条能产出初稿、再由人来兜底的流程,把标注员从"从零画框"里解放出来,只做判断和修正。
1.2 三个工具各自的分工逻辑
选择这三种工具有明确的分工考虑,不是跟风凑数:
- Grounded-SAM 负责"从无到有"。它在零样本的情况下,只要给一句文本提示,就能在图片里找出对应目标,生成检测框和分割掩码。适合在没有任何标注数据时先产出预标注。
- autodistill 负责"从有到练"。它把 Grounding DINO、Grounded-SAM 这类基础模型封装成组件,自动把所有图片转成 YOLO 格式的数据集,再直接拉起 YOLOv8 等目标模型训练,整个过程是"教师-学生"模式:大模型当老师标数据,小模型当学生学这份数据。
- X-AnyLabeling 负责"质量兜底与格式枢纽"。它有完整的图形界面,能快速修正检测框、多边形和 mask,还能导入导出 YOLO、COCO、VOC 三种主流格式。自动标注产出的东西,最终都要进这里人工过一遍。
简单说:Grounded-SAM 出初稿,autodistill 把初稿变成训练数据并驱动模型迭代,X-AnyLabeling 做人工审核和格式转换。三者组合就是一条半自动标注流水线,自动的部分解决效率,人工的部分守住质量。
| 工具/链路 | 自动化程度 | 人工依赖 | 主要产出 |
|---|---|---|---|
| 纯手动标注 | 接近 0% | 非常高 | 小规模高精度数据 |
| X-AnyLabeling + 内置模型 | 30% 到 50% | 高 | 精标数据集 |
| Grounded-SAM 批量推理 | 80% 左右 | 中,需抽检 | 全图 mask 预标注 |
| autodistill 全流程 | 90% 左右 | 低,训练前需审核 | 可直接训练的数据集 |
提示:不要期待任何自动标注工具能完全替代人。训练集里哪怕只有 3% 的错误框,模型表现都会明显抖动。自动化的目标是减少人工工作量,不是把人工清零。
2. 环境搭建与模型准备:让我少走弯路的清单
2.1 X-AnyLabeling 的获取与启动
X-AnyLabeling 是 AnyLabeling 的增强分支,界面基于 PyQt5,内置了 SAM、YOLOv8、PaddleOCR 等一系列模型,安装方式有两种。Windows 下最省事的办法是直接用官方 release 里的打包版,解压后双击主程序就能打开;如果你需要改源码,或者要往工具里塞自定义模型,建议用源码方式跑:
git clone https://github.com/CVHub520/X-AnyLabeling.git cd X-AnyLabeling conda create -n anylabel python=3.9 conda activate anylabel pip install -r requirements.txt python main.pyLinux 下第一次跑大概率会遇到两个坑:一是 PyQt 的 xcb 插件相关报错,Ubuntu 下需要先执行sudo apt install libxcb-cursor0;二是路径里有中文或空格会导致模型加载失败,建议项目目录保持纯英文路径。装好之后每次启动只需要激活 conda 环境再执行python main.py,看到主界面弹出就说明打开成功了。第一次打开如果界面卡顿,多半是在后台准备内置模型权重,属于正常现象,模型权重默认放在项目内的 model 目录,下载中断时手动把权重文件放进去、命名和配置文件保持一致即可。
为什么用 conda 3.9 而不是最新版?X-AnyLabeling 依赖的 PyQt 与 onnxruntime 在 3.7 和 3.11/3.12 上都出现过兼容性问题,3.9 是最稳的,实测很少因为 Python 版本本身报错。
2.2 autodistill 的安装与依赖
autodistill 安装本身不难,麻烦的是它背后的 torch、CUDA 环境要事先就绪。建议在同一个 conda 环境里继续装:
pip install autodistill autodistill-grounding-dino autodistill-grounded-sam autodistill-yolov8如果你的显卡支持 CUDA,提前把 PyTorch 装成 GPU 版本,再装上面的包,然后确认torch.cuda.is_available()返回 True。有个常见误区:很多人先装 autodistill,后补 torch,结果 pip 把之前的 GPU 版覆盖成了 CPU 版,Grounding DINO 推理全程走 CPU,一张 640 分辨率的图要等好几秒,批量跑几千张图完全不现实。所以我的固定顺序是先把 torch GPU 环境确认好,再装 autodistill 系列包。
权重文件同样要注意。第一次运行 autodistill 会自动下载 Grounding DINO 权重,公司内网或网络不稳定的环境经常卡在下载阶段。我的做法是找一台网络通畅的机器把权重提前下好,放到用户目录缓存或模型目录,再运行脚本就不会卡。如果代码里指定了绝对路径,多台机器协作时也不会出现找不着模型的问题。
2.3 Grounded-SAM 独立部署与快速验证
autodistill 里已经有 grounded-sam 的封装,但我仍然单独部署了一份 Grounded-SAM。原因有两个:一是官方 demo 的参数暴露更完整,想调 box_threshold、text_threshold 这类细节时更方便;二是 autodistill 底层跑的是固定流程,出问题很难定位是模型问题还是脚本问题,独立部署一份方便对照排查。
部署步骤:
git clone https://github.com/IDEA-Research/Grounded-SAM.git cd Grounded-SAM pip install -r requirements.txt然后下载两个权重文件放到模型目录:GroundingDINO 的groundingdino_swint_ogc.pth和 SAM 的sam_vit_h_4b8939.pth。SAM 的大模型权重接近 2.5GB,8G 显存跑起来很吃力,只是快速验证效果的话,换sam_vit_b_01ec64.pth这类小权重更合适。跑一次官方 demo:
python grounding_sam_demo.py \ --config ../GroundingDINO/groundingdino/config/GroundingDINO_SwinT_OGC.py \ --grounded_checkpoint ./models/groundingdino_swint_ogc.pth \ --sam_checkpoint ./models/sam_vit_h_4b8939.pth \ --input_image demo.jpg \ --text_prompt "cat" \ --box_threshold 0.3 \ --text_threshold 0.25 \ --output_dir ./output能在 output 目录看到可视化结果,就说明这一路是通的。后续做批量推理时,把这段命令拆成 Python 循环脚本,按产品需求逐张处理即可。
3. X-AnyLabeling 实操:人工标注与内建模型辅助
3.1 界面结构与基本标注动作
打开 X-AnyLabeling 后,左侧是标签列表,中间是图像画布,右侧是属性面板,底部有图像导航条,整体布局和主流标注工具相近。第一次使用先把标签加好,在左侧逐条添加类别,比如 meter、valve、leak,每个类别可以单独设定颜色。快捷键方面,画矩形框按 R,画多边形按 P,切换到移动工具按 V,保存按 Ctrl+S,这些快捷键用顺后标注效率提升非常明显。
画框的基本流程是:打开一张图片,按 R,在目标上拉一个框,选对类别,然后下一张。如果目标有遮挡,或者目标形状是弧形区域,用多边形工具贴边描更靠谱。实例分割任务就不要用矩形框了,直接在图像上描多边形,导出时就是 polygon 格式。X-AnyLabeling 的撤销、复制标签、合并标签都在右键菜单里,遇到重复目标时很省事。
3.2 内置模型辅助标注的用法
X-AnyLabeling 最实用的地方在于把模型推理放进了标注流程。右侧 AI 推理面板里选择要加载的模型,比如 YOLOv8 或 SAM,设置类别列表和置信度阈值,点运行,当前图片上就会自动出现预标注框或 mask。这些结果默认是"待确认"状态,标注员只需要把 AI 画错的框删掉、漏掉的补上,再修正一下框的贴合度,比从零开始手动画快得多。
实际用下来,内置 SAM 辅助对分割任务的帮助尤其明显。在目标物体上点一个前景点,再点一下背景点,SAM 就会生成一个贴合边缘的 mask,一些难处理的东西——比如弯曲管道上的渗漏点——看得准了很多。SAM 也不是万能的,密集排列的同类物体会被合并成一个 mask,这种时候要手动分割后再修正边缘。
3.3 为什么人工兜底不可省
我见过不少团队第一次跑自动标注,看到 Grounding DINO 框得挺准,就直接把标注结果扔进训练,最后模型效果一塌糊涂。原因很简单:自动标注的"准"是统计意义上的准,单张图总会有漏检、误检、框偏移。而训练集里每张图都会被模型反复学习,哪怕只有 5% 的错框,也足够让模型的预测边界变得模糊。所以整套流程里我一直坚持一个原则:自动标注结果必须经过 X-AnyLabeling 的人工复核,复核通过后才能进训练集。
复核不是一张张重新标,而是把自动标注的初稿当作"待修正稿",人只关注明显的问题:类别贴错、框没贴边、遮挡目标漏了、相邻目标被并框。熟练的标注员用这种方式处理一张复杂图,大概只要三十到四十秒,比完全手动快很多。这个"机器初稿 + 人工修正"的模式,就是整套流水线的质量闸口。
4. autodistill:从基础模型到目标模型的自动闭环
4.1 核心概念与执行逻辑
autodistill 的设计思路一句话可以概括:让一个大模型负责标注,再让一个小模型学习这份标注。对应到代码里有两个核心角色,一个是 Base model,通常是体积大、零样本能力强的模型,比如 Grounding DINO;另一个是 Target model,是真正要部署到业务里的轻量模型,比如 YOLOv8n。
两者之间通过 CaptionOntology 连接。CaptionOntology 本质上是"算法最终要识别的类别名"与"喂给大模型的自然语言提示词"之间的映射表。比如希望模型识别瓶子和穿反光背心的工人,ontology 可以这样定义:
from autodistill.detection import CaptionOntology ontology = CaptionOntology({ "bottle": "bottle", "worker": "person wearing reflective vest" })左边是训练数据的最终类别名,右边是给 Grounding DINO 的文字提示。基础模型拿文字提示去图片里找目标,找到后生成检测框,落盘成 YOLO 格式。
4.2 一个可运行的标注与训练脚本
完整脚本大概是这样:
from autodistill_grounding_dino import GroundingDINO from autodistill.detection import CaptionOntology from autodistill_yolov8 import YOLOv8 ontology = CaptionOntology({ "meter": "pressure gauge", "valve": "industrial valve", "leak": "water leakage" }) base_model = GroundingDINO(ontology) dataset = base_model.label( input_folder="./raw_images", output_folder="./auto_labeled" ) print(dataset)跑完这一步去输出目录看,正常情况下会看到按 train/valid 划分的数据集,每个划分里都有 images 和 labels 两个子目录,labels 里的 txt 文件与图片一一对应,这就是一份可以直接丢给 YOLO 系模型训练的数据。具体目录层级会随 autodistill 版本略有差异,跑完先ls看一下再写后续路径。
接着训练目标模型:
target_model = YOLOv8("yolov8n.pt") target_model.train( dataset, epochs=50, imgsz=640 )dataset 是上一步 label() 返回的对象,里面记录了划分路径,autodistill 会自己生成训练所需的配置文件,不需要手工写 YAML。如果你的 autodistill 版本比较早,train 的第一个参数直接传输出目录字符串也可以,跑之前先看一眼当前版本的接口说明。
4.3 提示词设计是自动标注质量的上限
用 autodistill 过程中最深的体会是:提示词写得好不好,直接决定标注质量,这部分没有任何参数能替你兜底。Grounding DINO 的文本提示对措辞非常敏感。我把提示词写成长句,比如 "a person walking on the road at noon",漏检率明显上升;把多个概念塞进一个提示词,比如 "person and dog",模型会把两个目标当成一个整体框出来。现在的经验是:每个类别只放一个清晰的名词短语,场景词能去掉就去掉,类别之间互相干扰的,在 ontology 里换词或加细节来区分。
提示:提示词是自动标注质量的上限。先拿 20 到 30 张代表性图片跑一轮 label(),肉眼检查预标注,确认提示词对应目标都找得到,再放开到全量数据。否则几千张图一次性标完,回头发现一半框是错的,重跑的时间成本很亏。
5. Grounded-SAM 批量推理:从文本提示到分割掩码
5.1 Grounding DINO 出框,SAM 出掩码
Grounded-SAM 的组合原理说起来不复杂:第一步,Grounding DINO 读入文本提示和图像,通过跨模态注意力机制找出与文本语义匹配的区域,输出候选框和置信度;第二步,把候选框作为 prompt 传给 SAM,SAM 在框内生成精细的分割掩码。这样就把"文本到检测框"和"框到掩码"接成了一条链,等于用一句话做实例分割。
为什么要绕这一步,而不是直接用 SAM 自动分割所有区域?因为 SAM 本身没有语义理解能力,它知道"哪里是物体",但不知道"哪个是仪表盘"。Grounded-SAM 的文本提示补上了语义这一环,让它只分割指定类别。这也是它能做零样本标注的原因,不需要任何训练样本,就能对陌生类别的图像输出掩码。
5.2 官方 Demo 运行与参数含义
前面第二章已经给了运行命令,这里重点讲参数怎么调。box_threshold控制检测框的置信度阈值,调低会让更多潜在目标被框出来,但噪声也会增加;text_threshold控制文本与视觉匹配的置信度阈值,调低会显著提高召回率,代价是误检变多。精度优先的场景,两个阈值都设在 0.3 左右;漏检严重的场景,先把text_threshold降到 0.2,观察输出再决定要不要继续降。
输出目录里会有两类关键结果:一类是可视化图片,框和 mask 直接叠在原图上,方便肉眼确认;另一类是包含掩码信息的 JSON 文件,里面用 COCO 风格的标注格式记录了每个目标的位置和 mask 像素位置,这份 JSON 是后续转成 YOLO 格式或者导入 X-AnyLabeling 复核的重要中间文件。
5.3 批处理的显存与效率经验
批量推理时最容易翻车的是显存。SAM 的 ViT-H 权重非常吃显存,8G 显卡处理 1280 分辨率的图,几张就会出现 CUDA out of memory。我的处理办法有两招:第一,把输入图片先缩放到宽边 1280 以内再推理,很多业务场景的精度损失可以接受;第二,在循环脚本里每处理完一张图就释放中间变量,必要时加torch.cuda.empty_cache()。如果依然溢出,把 SAM 权重换成 ViT-B 版本,速度提升明显,掩码质量在大多数场景下够用。
另一个隐性问题是小目标。目标在整张图里占比很小时,Grounding DINO 容易漏。解决思路是切图推理:把大图切成若干有重叠的 tile,分别推理后再把结果合并回原图坐标。代价是推理时间变长,我一般在主要类别确实以中小目标为主时才启用。
6. 三条标注数据的汇合:格式转换与复标闭环
6.1 工具之间的格式差异
三种工具或脚本产出的数据格式各不相同,这是整个流程里最繁琐也最容易出错的一环,先把常用格式理清楚:
| 数据来源 | 输出格式 | 特点 |
|---|---|---|
| X-AnyLabeling | YOLO txt / COCO json / VOC xml,也支持内部 workdir | 导入导出灵活,类别顺序由标签列表决定 |
| autodistill | YOLO txt,按 train/valid 划分 | 每行class x_center y_center width height,数值归一化到 0~1 |
| Grounded-SAM 官方 demo | COCO json + mask png | 掩码以 polygon/segmentation 形式记录 |
最容易出问题的点在于坐标换算。COCO 的 bbox 是绝对像素坐标的[left, top, width, height];YOLO txt 是归一化的[x_center, y_center, width, height]。很多人直接把 COCO 的 left/top 当 YOLO 的 x_center/y_center 用,结果训练时所有框都偏到图像角落。
注意:每次转换后抽几张图把框画回原图上确认一遍。坐标问题用肉眼检查十分钟,比训练到一半才发现错误省事得多。
6.2 COCO JSON 转 YOLO TXT 的核心脚本
下面是我项目里一直在用的转换脚本,只针对检测框场景,分割 mask 的转换逻辑类似,区别是多一步把 polygon 转成归一化坐标:
import json from pathlib import Path def coco_to_yolo(coco_path, image_dir, output_dir): output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) with open(coco_path, "r", encoding="utf-8") as f: data = json.load(f) image_id_to_name = {img["id"]: img["file_name"] for img in data["images"]} image_id_to_size = {img["id"]: (img["width"], img["height"]) for img in data["images"]} cat_id_to_label = {cat["id"]: i for i, cat in enumerate(data["categories"])} for ann in data["annotations"]: img_name = image_id_to_name[ann["image_id"]] width, height = image_id_to_size[ann["image_id"]] cat_id = ann["category_id"] x, y, w, h = ann["bbox"] x_center = (x + w / 2) / width y_center = (y + h / 2) / height norm_w = w / width norm_h = h / height label_path = output_dir / (Path(img_name).stem + ".txt") with open(label_path, "a", encoding="utf-8") as f: f.write(f"{cat_id_to_label[cat_id]} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}\n")脚本里继承了原图的宽高信息,每个标注写的都是归一化之后的坐标。类别 id 的映射顺序需要固定下来,训练时的类别顺序和这里保持一致,否则模型输出的类别就全乱了。
6.3 从自动初稿到人工复标再回训练的闭环
我现在跑项目的标准闭环是这样:第一批图片,先用 autodistill 或 Grounded-SAM 自动标注,产出预标注数据;把这些预标注统一转成 X-AnyLabeling 能打开的格式,标注员在界面上逐类复核修正;复核通过后的数据重新导成 YOLO 格式,拿去训练目标模型;目标模型训练好后,把它对剩余海量图片的推理结果再一次作为预标注,交给人复核,形成"自动初稿 → 人工修正 → 模型迭代 → 再自动初稿"的循环。
这个循环里最值钱的部分是:模型精度越高,人工修正工作量越小。第一轮可能一张图要改三十秒,跑过两轮之后很多图只需要翻一下就能确认,单张耗时降到十秒以内。最终目标不是把人工降到零,而是让人工专注处理模型搞不定的硬样本,这才是自动标注流程的核心价值。
7. 踩坑实录:模型加载、显存与提示词的七个大坑
7.1 环境与启动类
第一个坑是 X-AnyLabeling 在 Linux 上报 xcb 相关错误。当时我以为是 PyQt5 装坏了,反复重建环境,最后查出来是缺libxcb-cursor0系统库,执行sudo apt install libxcb-cursor0后问题消失。遇到类似报错先搜系统库缺失,不要一上来就怀疑 Python 包。
第二个坑是 onnxruntime GPU 版本和 CUDA 版本不匹配,导致 X-AnyLabeling 内置模型始终在 CPU 上推理,图一多就卡成幻灯片。解决方法是先卸载 onnxruntime,按自己 CUDA 版本装对应版本的onnxruntime-gpu,再在模型推理面板里确认设备已变成 GPU。
7.2 推理与显存类
第三个坑是自动下载权重中断。Grounding DINO 的权重文件比较大,网络代理环境下经常下到一半失败。我的处理方式是提前手动下载好权重,放到代码指定路径,并把模型路径写成绝对路径,避免不同机器上相对路径解析不一致。
第四个坑是批量推理显存溢出。前面讲过,优先降输入分辨率、换 SAM 小权重、逐张释放。另外一个容易被忽略的点:不要同时开多个进程跑 Grounded-SAM,每个进程都会把模型完整加载进显存,四个进程就能把 24G 卡吃满。要加速就增大单进程内部 batch,而不是开多个进程。
7.3 提示词与数据类
第五个坑是提示词写得太文学。想标无人机,写 "a drone flying in the sky",检测率和只写 "drone" 相比有明显下降。我现在统一用最短的类别名词,容易出现误检的近义类别在 ontology 里做区分,比如 meter 和 gauge 同时使用时,给不同文本提示并搭配样本检查。
第六个坑是类别中文名问题。训练环节对中文类别名支持不好,而 X-AnyLabeling 导出的标签列表可能是中文。我吃过一次亏,训练中途报错才发现 classes.txt 里全是中文。现在的做法是工具里就用英文类别名,展示层再映射成中文,或者转换脚本里统一做映射。
第七个坑是重叠框。自动标注结果经常出现两个模型对同一个目标各输出一个框,或者同一目标被相邻提示词重复召回。让标注员手工删太累,我加了后处理步骤:对整张图跑一次 NMS,把 IoU 大于 0.5 的重复框合并,只保留置信度高的那一个。Grounded-SAM 的 JSON 输出可以在转换脚本里顺手做 NMS,autodistill 的结果要看版本,老版本一般没做,需要在进入训练前自己过滤。
最后说一点个人体会。这套自动标注组合真正适合的场景是"数据量中等到大、类别相对固定、目标形态不是特别极端"的项目。它不会让标注这件事消失,而是把人的工作从"画框描边"挪到"判断与修正"。我实际跑下来最大的收获不是省了多少小时,而是标注质量稳定了——机器初稿的框虽然不完美,但至少不会像不同标注员那样风格漂移。如果你也在为标注效率发愁,我的建议是别一上来全量自动化,先拿三五十张图把每个环节的格式打通,确认提示词和阈值靠谱,再慢慢放开规模。数据管道这种事,越早理顺,后面越省心。