简介:自动标注工具包面向使用YOLO系列模型开展目标检测训练的研究者与开发者,重点解决图像标注耗时、格式转换繁琐的问题。它基于labelImg源代码整理,提供多边形选择、快捷键编辑、类别管理、批量处理、标注预览校验以及YOLO格式导出等能力,可用于自建数据集和课程实验中的数据预处理。压缩包共115个文件、约6.3MB,以42张png界面与示例图、29个py源码、9个svg图标、6个sh脚本及rst、md说明文档为主,另含配置、资源与授权文件,便于阅读源码、编译运行并按项目需求二次开发。已有386人学习下载。读者可据此快速搭建本地标注环境,理解标注工具从界面交互到标签文件生成的实现路径,并借助批量处理与导出功能缩短数据集制作周期,为后续模型训练和标注流程优化提供可复用的工具基础。
1. 几千张图 labels 目录是空的,这份 YOLO 自动标注工具能省掉多少拉框时间
现场图攒了几千张,labels 目录空空如也,靠人一张张拉框,一天撑死三百来张,眼睛先投降——做 YOLO 检测项目的人几乎都卡在这一步。这份自动标注工具盯的就是这段:拿预训练权重先把无标注图片跑一遍,输出 YOLO 系列数据集通用的 txt 标签,人只负责复核和补漏,不再从零画框。它的适用边界得先说清楚:类别定义稳定、目标在常见视觉域里、手头已经有一批无标注图,满足这三条,自动标注能把前期工作量压下去一大截;反过来,类别极偏门、预训练权重压根没见过的目标,它顶多帮你筛掉一批空白图,别指望一键出成品。这条流程真正的价值不在"自动"两个字,而在于把标注从"生产"变成"校对",人只需要判断框对不对、类别对不对,这一步比画框快得多。
2. 伪标签链路拆解:预训练检测器、置信度阈值与 YOLO txt 的归一化坐标
自动标注这件事,很多人第一次听会以为是某种智能识别算法,实际拆开看,它就是一次模型推理加一次格式转换。模型部分用的是已经训练好的检测器权重,格式部分是把模型输出的像素坐标换成 YOLO 训练器认识的归一化 txt。这两段任何一段出问题,后面训练出来的模型都是歪的。所以先把链路拆清楚,再谈工具怎么用,比上来就贴命令靠谱。
2.1 自动标注为什么绕不开回环微调
自动标注的质量上限,基本由预训练权重和你数据分布之间的距离决定。分三种情况看会更直观。第一种,目标属于 COCO 那八十类里常见的类别——人、车、瓶子、椅子这类,直接拿官方预训练权重推理,框的质量已经能用了,人工只需要处理遮挡和密集小目标。第二种,目标不在 COCO 里,但形态跟某些类别接近,比如某种工业零件、某种包装盒,这时候正确做法不是硬跑,而是先人工标两三百张,微调一轮得到一个能用的检测器,再用这个检测器去给剩下的图上标签。行业里管这叫自举,本质是让模型先学会你的数据长什么样,再去批量复制自己的判断。第三种,目标形态极其特殊,预训练权重完全没见过,自动标注基本退化成筛图工具,只能帮你把没有目标的空图挑出去,省掉一部分无效标注。
为什么不干脆用传统图像处理做自动标注?阈值分割、边缘检测、模板匹配这些方法在固定工位、固定光源、固定拍摄角度下确实能用,但一旦光照变了、物体转了个角度、背景里多出几样东西,参数就得重调。检测器的优势在于它对尺度、光照、部分遮挡有泛化能力,代价是需要一定量的标注来微调。所以实际工程里更常见的是一条闭环:少量人工标注 → 微调 → 自动标注一批 → 人工抽检修正 → 修正后的标签回灌再微调。第一轮自动标注的可用率通常在六到八成,第二轮能上九成,这个提升不是靠调参数调出来的,是靠把人工修正的结果喂回去。
这里还有一个容易被忽略的点:自动标注产出的是伪标签,伪标签里必然混着假阳性。假阳性进入训练集,模型会把错误当正确答案学,mAP 看上去涨了,实际部署时误检反而更多。所以不管自动标注跑得多顺,抽检这一环不能省,第五章会专门讲这件事。
2.2 YOLO txt 的五个字段与归一化坐标的换算
YOLO 系列数据集认的标签格式非常简单,每张图对应一个同名 txt 文件,每行一个目标,五个字段用空格隔开:
class_id x_center y_center width heightclass_id是从 0 开始的整数,顺序必须和data.yaml里names列表的下标严格对齐;后面四个是归一化到 [0, 1] 的浮点数,x_center、y_center是框中心点相对整图宽高的比例,width、height是框宽高相对整图宽高的比例。一张图里没有目标,就留一个空的 txt 文件,这个空文件不能省——YOLO 训练时靠它区分"这张图确实没有目标"和"这张图漏标了"。
检测器输出的是像素坐标x1 y1 x2 y2,换算成 YOLO 格式要过一道归一化,代码不长但边界处理不能马虎:
def xyxy_to_yolo(x1, y1, x2, y2, img_w, img_h): """把像素坐标的左上/右下角点换算成 YOLO 归一化中心点格式""" # 先裁剪到图像范围内,避免预测框越界导致坐标大于 1 x1 = max(0.0, min(float(x1), img_w)) x2 = max(0.0, min(float(x2), img_w)) y1 = max(0.0, min(float(y1), img_h)) y2 = max(0.0, min(float(y2), img_h)) # 退化框直接丢弃,宽或高为 0 的框训练时是噪声 if x2 - x1 < 1.0 or y2 - y1 < 1.0: return None x_center = (x1 + x2) / 2.0 / img_w y_center = (y1 + y2) / 2.0 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h # 保留 6 位小数足够,再多的位数对训练没有意义 return round(x_center, 6), round(y_center, 6), round(w, 6), round(h, 6)这段代码里有两个参数值得单独说。img_w、img_h必须是原始图像的宽高,不是模型输入尺寸,检测器内部做 letterbox 缩放后会把框映射回原图坐标,如果你拿到的是缩放后的坐标,归一化就全错了。第二个是裁剪顺序,先裁剪再判断退化,如果先判断退化再裁剪,越界框会以负宽高通过检查,写进 txt 后训练器读出来是畸形标签,报错信息还很难定位。
一个具体的换算例子:一张 1920×1080 的图,某个目标框是(100, 200, 500, 600),换算后是0.15625 0.37037 0.20833 0.37037。写进 txt 时前面再加上类别 id,一行就是0 0.15625 0.37037 0.20833 0.37037。手工验证时拿这一行反算回像素坐标,能对回原框,说明换算逻辑没问题。
2.3 置信度阈值与 NMS 参数的初值怎么定
参数表比经验之谈更好用,下面是这套工具默认的初值,以及每个参数动一下会带来什么后果:
| 参数 | 建议初值 | 调大之后 | 调小之后 |
|---|---|---|---|
| conf 置信度阈值 | 0.25 | 假阳性减少,漏标明显增加 | 召回上升,误检框大量涌入 |
| iou NMS 阈值 | 0.45 | 重叠目标保留更多,同一目标可能出双框 | 密集目标被合并,漏掉相邻物体 |
| imgsz 推理尺寸 | 640 | 小目标召回上升,显存和耗时同步涨 | 小目标直接消失,大目标影响不大 |
| max_det 单图最大框数 | 300 | 密集场景够用,稀疏场景浪费 | 密集场景截断,后半个图漏标 |
conf是最值得反复调的一个。设 0.25 起步,先跑两三百张看结果,如果人工复核时十张里有六七张需要删框,说明阈值太松,往上抬到 0.35 或 0.4;如果发现大量明显目标没被框出来,往下压到 0.15 再试。不同类别的检出难度差别很大,大类目标 0.4 都稳,小目标和遮挡目标 0.15 才勉强够,工程上可以按类别设不同阈值,代价是脚本要多一层判断。
iou这个参数在密集场景里最容易翻车。一排并排的零件、货架上挨着的商品,NMS 阈值设 0.45 会把相邻两件合成一件,人工复核时看着像模型瞎了,实际是后处理把它们吞了。这种情况把iou抬到 0.6 甚至 0.7,代价是同一目标偶尔出两个重叠框,但重叠框人工删起来比补漏快得多。反过来,稀疏的大目标场景,iou压到 0.4 能减少重复框。
imgsz必须和后续训练时用的尺寸一致,这一点经常被忽略。自动标注用 1280 跑出来的框,训练时用 640 输入,小目标的框在缩放后只剩几个像素,学习信号已经很弱了。工具里把推理尺寸和训练尺寸绑成同一个配置项,比分开设两个参数省心。
3. 工具落地:目录约定、data.yaml 对齐与批量推理的写回逻辑
拿到一个自动标注工具,第一件事不是跑命令,是看它假设的目录结构。目录对不上,后面全是路径报错;data.yaml的类别顺序对不上,标签全是对的但类别全错,这种错误比路径错误更难发现,因为训练照样能跑,只是 mAP 莫名其妙上不去。
3.1 目录结构与 data.yaml 的类别映射
这套工具沿用的是 YOLO 官方推荐的组织方式,图片和标签分目录平行放置,文件名主干一一对应:
dataset/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ └── 0002.jpg │ └── val/ │ └── 0101.jpg ├── labels/ │ ├── train/ │ │ ├── 0001.txt │ │ └── 0002.txt │ └── val/ │ └── 0101.txt └── data.yamldata.yaml三行内容,每一行都有坑:
path: /data/dataset train: images/train val: images/val names: 0: person 1: helmet 2: vestpath写绝对路径,train和val写相对于path的子路径,不要重复拼。names用字典形式写比列表形式更不容易错位,但两种写法 YOLO 都认,关键是键必须从 0 开始连续,不能跳号。很多人复制别人的 yaml 只改名字不改键,结果类别 0 是 person、类别 1 直接跳到 3,训练时读标签直接崩。
names的顺序和自动标注脚本里的类别映射表必须是同一份来源。工具里我一般把类别表单独抽成一个classes.txt,一行一个类名,data.yaml和推理脚本都从这个文件读,只有一处维护,改一次全链路生效。这种"单一事实来源"的做法在类别超过十个之后能省掉大量对齐工作。
3.2 批量推理脚本:扫描、推理、写回三段式
工具的核心是一个批量脚本,结构上分三段:扫描待处理图片、逐张推理、把结果写成 txt。下面这版是精简过的可运行骨架,用的是常见检测器接口:
from pathlib import Path from ultralytics import YOLO def batch_auto_label(img_dir, label_dir, weights, class_map, conf=0.25, iou=0.45, imgsz=640, skip_existing=True): """ img_dir 待标注图片目录 label_dir 标签输出目录,不存在会自动创建 weights 预训练或微调后的权重路径 class_map 模型类别 id -> 数据集类别 id 的映射字典 skip_existing 为 True 时跳过已有 txt 的图片,用于断点续跑 """ img_dir = Path(img_dir) label_dir = Path(label_dir) label_dir.mkdir(parents=True, exist_ok=True) model = YOLO(weights) exts = {".jpg", ".jpeg", ".png", ".bmp"} for img_path in sorted(img_dir.iterdir()): if img_path.suffix.lower() not in exts: continue txt_path = label_dir / f"{img_path.stem}.txt" if skip_existing and txt_path.exists(): continue # stream=False 时返回列表,单张图取第 0 个结果 results = model.predict( source=str(img_path), conf=conf, iou=iou, imgsz=imgsz, verbose=False, ) result = results[0] lines = [] if result.boxes is not None and len(result.boxes) > 0: # xyxy 是原图像素坐标,xywhn 是已归一化的中心点坐标 xyxy = result.boxes.xyxy.cpu().numpy() cls_ids = result.boxes.cls.cpu().numpy().astype(int) h, w = result.orig_shape for (x1, y1, x2, y2), cid in zip(xyxy, cls_ids): if cid not in class_map: continue # 模型输出但数据集不关心的类别直接丢弃 mapped = class_map[cid] box = xyxy_to_yolo(x1, y1, x2, y2, w, h) if box is None: continue lines.append( f"{mapped} {box[0]} {box[1]} {box[2]} {box[3]}" ) # 没有目标也要写空文件,训练器靠它区分漏标和真无目标 txt_path.write_text("\n".join(lines), encoding="utf-8")三个参数决定这段代码跑起来顺不顺。class_map是模型类别到数据集类别的桥,模型输出八十类,你的数据集只有三类,不做映射就会写出 id 为 47 的标签,训练器读的时候直接报类别越界。skip_existing是断点续跑的开关,几千张图跑到一半因为显存或者别的原因中断,重跑时打开它,只处理还没生成 txt 的图,不用从头再来。verbose=False关掉逐图日志,几千张图的日志刷屏会把真正的报错冲掉,排查问题时反而麻烦。
写回逻辑里最容易被改坏的是空文件那一行。有人觉得没目标就不写文件,省点磁盘,结果训练时 YOLO 把那些图当成漏标,要么报警告要么直接跳过,召回率怎么调都上不去。空 txt 是零字节但必须存在,这几百个文件占不了多少空间。
3.3 断点续跑与进度可观测
批量任务的可观测性比想象中重要。几千张图跑两三个小时,中途看一眼跑到哪了、当前处理到哪张、有没有异常图,靠print刷屏是看不出来的。工具里我习惯加两个东西:一个进度文件记录已处理文件名,一个失败清单记录报错的图,跑完之后直接拿失败清单重试。
import json def load_state(state_file): p = Path(state_file) if not p.exists(): return {"done": [], "failed": []} return json.loads(p.read_text(encoding="utf-8")) def save_state(state_file, state): Path(state_file).write_text( json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8" )done列表每处理完一张追加一次,failed记录异常和路径。这个状态文件还有第二个用途:跑完之后可以统计每张图的框数量,框数异常多的图往往是背景复杂、误检密集的图,优先抽检这些图,人工复核效率最高。我一般按框数从多到少排序,取前五十张过一遍,能发现大部分系统性问题。
4. 三类数据集接入:单类别、多类别与已有标签补标的差异化处理
同样一套工具,喂给不同类型的 YOLO 数据集,处理方式差别不小。单类别看起来最简单,但空标签策略和背景图处理有讲究;多类别的难点全在映射表;补标场景则要考虑怎么和已有标签共存,不能把人工标好的框覆盖掉。
4.1 单类别场景:类别恒为 0,重点在背景图
工业质检、单一物体检测这类项目,数据集只有一个类别,模型输出的所有目标映射到 0 即可,class_map就是{模型类别 id: 0}。这种情况下最该关注的不是类别映射,是背景图。产线上大量图片是"没有缺陷"的正常件,这些图必须进训练集,而且标签是空文件。如果自动标注只挑了有框的图放进数据集,训练出来的模型会把整张图都当成目标区域,误检率高得离谱。
我的做法是先把整个图片目录全部纳入,自动标注产出 txt,有框的写框、没框的写空文件,然后按框数量分桶:有框的一桶、空文件的一桶,两边按比例抽样进 train 和 val。背景图占比控制在两到三成,太少模型学不会"没有目标"长什么样,太多则正样本被稀释。这个比例不是固定值,缺陷本身越稀有,背景图占比越要控制。
4.2 多类别场景:映射表是唯一的对齐依据
多类别数据集的坑几乎全在映射表上。模型在 COCO 上训练,输出 id 0 到 79,你的数据集可能只有安全帽、反光背心、人三类,对应关系必须显式写出来:
# 键是模型输出的类别 id,值是数据集 data.yaml 里的类别 id CLASS_MAP = { 0: 2, # COCO person -> 数据集 2 号类 27: 0, # COCO tie 近似安全帽,按实际微调结果调整 28: 1, # COCO suitcase 近似工具箱,仅作示例 }用相近类别做映射是常见的应急手段,但必须清楚这只是让模型先动起来,映射出来的框类别不一定对,人工复核时要重点看类别。真正稳的做法还是先人工标一批目标类别的数据微调一轮,再用微调后的权重做自动标注,这时候模型的输出 id 已经是你的数据集 id,映射表可以直接写成恒等映射。
映射表还有一处容易漏:data.yaml里names的键改了顺序,映射表没跟着改。这种事情在多人协作时经常发生,一个人调了类别顺序,另一个人还在用旧映射跑,产出的标签类别全错但格式全对,训练不报错,mAP 就是上不去,排查起来非常耗时。工具里把映射表的哈希值写进产出目录的一个元信息文件,跑完能对一下用的是哪个版本。
4.3 补标场景:只处理缺标签的图,绝不覆盖人工成果
实际项目里更常见的情况不是从零开始,而是标了一半、还剩一半。这时候自动标注要解决的是补标,核心原则只有一条:已经有 txt 的图一律跳过,不管那个 txt 是空文件还是有内容。人工标过的框是最贵的资产,被自动标注覆盖掉是不可逆的损失。
skip_existing=True就是为这个场景准备的,但它判断的是文件存不存在,不判断文件内容。有些流程会先批量创建一个空 txt 占位,这种情况下所有文件都"存在",补标就全被跳过了。更稳的判断是先读文件,文件存在且非空才跳过,空文件视为待补标:
def needs_label(txt_path: Path) -> bool: """文件不存在,或存在但内容为空,都算需要补标""" if not txt_path.exists(): return True return txt_path.stat().st_size == 0这个判断逻辑会带来一个副作用:真正没有目标的背景图会被反复重跑。解决办法是在补标前先把背景图单独筛出来,标一个.done后缀的标记文件,或者在状态文件里记一笔,重跑时跳过。工具里我一般选后者,状态文件和断点续跑共用一套,逻辑更统一。
补标产出的框和人工框混在一个文件里,复核时要能区分哪些是机器产的。工具会在产出目录旁边生成一份auto_boxes.json,记录每张图机器新增了哪些框,人工复核时对着这份记录看,机器框重点检查,人工框基本不用动,复核速度能快一倍。
5. 自动标注避坑:从两千张图里攒出来的五条排查记录
这套流程我跑过几个不同规模的数据集,下面这五条是出现频率最高、排查最费时间的,每条按现象、原因、解决三段写,遇到同类问题可以直接对号。
5.1 训练报类别越界,标签里出现大于类别数的 id
现象:训练刚启动就报IndexError或者类别索引越界,打开某个 txt 看到 id 是 47,而data.yaml里只有三个类别。
原因:模型输出的是 COCO 八十类的 id,脚本没做类别映射就直接写进 txt 了,或者映射表里漏了某个类别 id,if cid not in class_map: continue这行被删掉了。
解决:先把映射表补全,确认所有模型可能输出的 id 都有对应;然后在写文件前加一道断言,mapped必须小于len(names),越界直接抛异常并打印图片路径,比写进文件后再排查快得多。
5.2 mAP 比纯手工标注还低,模型学得越久越差
现象:自动标注产出的数据集训练,验证集 mAP 比之前手工标的小数据集还低,训练 loss 正常下降,但精度上不去。
原因:conf设得太低,大量低置信度假阳性被当成真标签写进训练集,模型学会了"到处都有目标",召回上去了精度塌了。伪标签的错误会被模型当成正确答案放大,这是自动标注最典型的反噬。
解决:把conf抬到 0.35 以上重跑一遍,或者按框的置信度分层处理,高置信度直接写、中间置信度标记为待复核、低置信度丢弃。跑完必须抽样复核,不能只看 loss 曲线。
5.3 图像和标签文件名对不上,训练时大量图被跳过
现象:训练日志里反复出现找不到标签的警告,实际标注框都在,只是部分图没有参与训练。
原因:图片名是IMG_0001.JPG,脚本用img_path.stem生成 txt 名,大小写和扩展名处理不一致,或者原图有重名文件被覆盖,导致图片和 txt 的主干名对不上。
解决:跑完之后做一次配对校验,遍历图片目录,检查每张图是否有同名 txt,列出缺失清单。文件名统一转小写、统一去空格,这一步放在扫描阶段做,比事后补要省事。
5.4 密集目标被合并,人工复核时发现漏了一半箱子
现象:货架场景里一排挨着的商品,自动标注只框出了其中几个,剩下的明明在图上却一个框都没有。
原因:NMS 的iou阈值设得太低,相邻目标的框重叠度超过阈值后被当成重复框抑制掉了。密集排列的场景这个问题几乎是必然的。
解决:把iou抬到 0.6 到 0.7,允许重叠框存在,重复框人工删比漏框人工补快。同时把max_det调大,默认 300 在极端密集场景可能不够。
5.5 跑到一半进程被杀,重跑从头开始
现象:几千张图跑到百分之六七十,显存耗尽或者被系统杀掉,重新执行脚本又从第一张开始。
原因:脚本没有断点续跑机制,或者skip_existing判断的是文件存在但空文件也被当成已处理,导致要么全跳过要么全重跑。
解决:用状态文件记录已完成的文件名,重跑时先读状态再决定处理哪些;空文件判断按 4.3 节的needs_label逻辑走。另外把批量任务按目录分批,每批几百张,一批失败不影响其他批,排查也更容易定位。
6. 验收与提效:五十张抽检把伪标签从"能用"推到"敢用"
自动标注跑完不等于结束,产出能不能直接用于训练,得靠抽检验证。我一般的做法是从产出里随机抽五十张,人工逐张核对,统计三个数:框的准确率(画出来的框里有多少是真的目标)、召回率(图上的真目标有多少被框出来)、类别正确率。这三个数里准确率低于八成,说明conf太松,回去调;召回率低于七成,说明阈值太严或者模型没见过这类目标,考虑先微调;类别正确率低于九成,多半是映射表的问题。
抽检还有个提效用法:按框数量排序,优先看框最多的那批图。背景复杂、误检密集的图会集中出现在这个区间,看五十张能覆盖大部分系统性问题。反过来,框数极少的图也值得看几张,确认不是模型整体失灵。
| 抽检指标 | 及格线 | 不达标时的第一动作 |
|---|---|---|
| 框准确率 | ≥ 80% | 抬 conf,重跑受影响类别 |
| 目标召回率 | ≥ 70% | 降 conf 或先微调权重再跑 |
| 类别正确率 | ≥ 90% | 核对 class_map 与 names 顺序 |
| 空图比例 | 与真实分布接近 | 检查背景图是否被误纳入或漏纳入 |
抽检通过之后,还有一步能继续压人工成本:把自动标注的框当作预填结果导入标注工具,人工只做确认和微调,不做从零画框。实测下来,同样一批图,纯手工标注和"机器预填 + 人工确认"的耗时差距在三四倍,后者主要时间花在删误检和调边界上,前者花在找目标和画框上。标注工具如果支持导入 YOLO txt,这一步几乎是零成本。
最后说一个我踩过的坑。早期有一次为了赶进度,自动标注跑完没抽检就直接开训,训练集里混进了大量把背景纹理当成目标的假框,模型训出来在验证集上指标漂亮,一上现场误检率直接翻倍,回头查了两天才定位到是伪标签的问题。从那以后我每次跑完自动标注,不管多急,都强制走一遍五十张抽检和配对校验这两步,宁可多花半小时,也不愿意再经历一次推倒重训。希望帮到你。
本文还有配套的精品资源,点击获取