简介:面向YOLO系列目标检测模型训练的数据自动标注工具,主要服务于需要高效构建训练数据集的算法工程师、研究人员与学生。压缩包内置labelImg-master完整源代码,共115个文件,涵盖Python源码、界面图标、启动脚本及说明文档等类型,整体仅有6.3MB,轻量易部署,也便于按需进行二次开发。工具支持多边形精确标注、快捷编辑、类别管理、批量处理、预览校验以及YOLO格式导出,并能借助自动化辅助减少手动框选时间,适用于自动驾驶、视频监控、机器人导航等实时检测场景。包内目录结构清晰,核心模块与设计资源相互独立,源码注释和配置示例可帮助快速定位修改点。已有382人学习下载,特别适合希望定制标注流程、提升数据集制作效率的YOLO用户,既能直接使用现成工具,也能扩展出适合自己的标注功能。
1. 自动标注工具在 YOLO 数据集流程里的真实位置:不是替代人工,是压缩人工
第一次用 YOLO 训练自己的数据集时,我花了两周手动框了 3000 张车辆图,手腕酸到拿不住鼠标。后来我把自动标注工具接进流程,发现它解决的从来不是「让 AI 全自动标完」,而是「把 80% 的重复框选工作交给模型,人工只做确认和修正」。这套做法适用于 yolo 系列所有数据集,尤其是一个人维护数据集、做项目预研、或者从公开数据集(比如 CCPD 车牌数据集、BDD100K 车辆检测数据集)清洗出自己业务子集的时候。它不挑 YOLOv5、YOLOv8 还是 YOLO11,因为标注文件归根到底就是class x_center y_center width height一行行文本。这个标题听起来像某个现成软件的广告,但真正能落地的方案不在 UI 里,而在你手上的推理脚本、坐标转换函数和置信度过滤逻辑里。下文就把这套链路完整拆开。
2. 跑通最小自动标注链路:预标注 + 人工修正的完整流程
2.1 为什么「预标注 + 人工校验」是可靠路径,而不是端到端全自动
自动标注工具在 YOLO 生态里有三种常见形态:第一种是 LabelImg 这类手动工具加了「用预训练模型自动预打标签」的插件;第二种是 CVAT 这类标注平台内置的模型辅助标注;第三种是自己写推理脚本,把模型输出的 txt 文件直接改写成 YOLO 训练格式。我踩过一轮坑后的结论是:第三种最可靠,前两种适合有人维护平台的团队。
原因很实际。自动标注的价值在于「预标注」,而不在于「自动完成」。YOLO 模型给出的边框不会自己判断「这个框压到了车道线会不会影响检测」,它只会在置信度维度上给你一个数。你要做的是把人工从「划线」中解放出来,而不是把人工从「决策」中解放出来。一个被我反复使用的比例是:预标注能把一张图的标注时间从 40 秒压到 8 秒,但前提是后面有人工确认环节。完全不加确认的直接训练,轻则 mAP 掉 3 到 5 个点,重则 FP(误检)数据把整个验证集污染掉。后者的危害比前者大得多,因为你会拿一个不干净的验证集去判断模型好坏,整个迭代就失去参照系了。
2.2 用 YOLOv8 做首轮预标注:最小推理命令与输出文件说明
我一般用 YOLOv8 的 CLI 做首轮预标注,因为它开箱即用,且输出格式直接对齐 YOLO 训练格式。下面这个命令是我在单张 4090 上批量标注 5000 张 JPEG 图的标准动作:
yolo predict model=yolov8n.pt source=./raw_images save_txt=True save_conf=True conf=0.25 imgsz=1280 project=./auto_label name=first_pass命令拆开看:model=yolov8n.pt是预训练权重,n 是 nano,速度最快,但精度只够做「预标注草稿」;如果你的场景类别和 COCO 80 类重合度低,后面要换成自己之前训过的模型,这个参数是第一个要动的。save_txt=True决定把框写进 txt 文件,这是自动标注链路里唯一必须为真的开关。save_conf=True会把置信度写进 txt 每行末尾,后续过滤低质量框全靠它。conf=0.25是初始置信度阈值,这个数值选 0.2 到 0.3 之间比较稳妥——太高漏标多,太低了标注结果里全是噪声框,人工修起来反而烦。imgsz=1280很关键,YOLOv8 默认推理尺寸是 640,但小目标在 640 下经常丢,预标注场景里宁愿慢一点,也要用 1280 把召回率提上来。
跑完以后,结果目录./auto_label/first_pass/labels下每个 txt 文件的内容长这样:
2 0.4321 0.5678 0.1234 0.0892 0.87 5 0.8123 0.2345 0.0567 0.1234 0.52每一行的含义分别是:类别 id、归一化后的框中心 x、框中心 y、框宽、框高、置信度。这个 txt 格式和 YOLO 训练格式唯一的差别就是最后一列置信度,训练时会自动忽略它。所以这一步产出的文件可以直接作为训练的起点,只是质量还不行。
提示:如果原始图片尺寸不是正方形,YOLO 推理时会做 letterbox,也就是在边缘补灰边。
save_txt=True输出的坐标是相对于补边之后的图像的,不是相对于原始图像的。坐标转换逻辑在 2.3 节给出。
2.3 把预标注结果清洗成干净数据集:置信度过滤与坐标校验脚本
首轮预标注出来的 txt 不能直接用,要过一次清洗。我通常写一个短脚本,做三件事:按置信度阈值过滤整行、删除空标注文件、输出一份标注统计表。置信度阈值过滤是第一步,因为conf=0.25只是推理时用的初始阈值,清洗时我一般用 0.35 到 0.45,看场景复杂度浮动。
import os from pathlib import Path label_dir = Path("./auto_label/first_pass/labels") keep_conf = 0.35 drop_list = [] total_lines = 0 for txt_path in sorted(label_dir.glob("*.txt")): new_lines = [] with open(txt_path, "r", encoding="utf-8") as f: for line in f.readlines(): parts = line.strip().split() if len(parts) != 6: continue conf = float(parts[5]) total_lines += 1 if conf >= keep_conf: new_lines.append(parts[:5]) if len(new_lines) == 0: drop_list.append(txt_path.name) else: with open(txt_path, "w", encoding="utf-8") as f: for lbl in new_lines: f.write(" ".join(lbl) + "\n") print(f"total boxes: {total_lines}") print(f"keep conf >= {keep_conf}") print(f"empty files (removed from dataset): {len(drop_list)}") for name in drop_list[:20]: print(" remove:", name)脚本背后的逻辑是:parts[:5]只保留前五列,把置信度列直接丢掉,输出文件就变成标准 YOLO 训练格式。空 txt 文件对应的图片要么没目标,要么被漏标,把它们单独列出来,人工决定删除还是进第二轮复核。统计出来的total_boxes和drop_list数量能帮你判断这轮预标注的质量——如果 5000 张图只剩 2000 张有框,说明阈值设高了,或者模型和场景根本不匹配。
坐标校验这步很多人会跳过,但我吃过亏:有一次目标在边缘,自动标注出的框中心坐标超过 1.0,训练时 OpenCV 读图直接报边界错误。所以脚本里应该加上坐标范围检查,超过[0, 1]的行直接删掉或截断。这类脏数据不清理,后面训练时 Loss 值会出现 NaN,排查起来极其痛苦。
2.4 把清洗后的结果交给人工校验:用 LabelImg 打开并修正的具体操作方法
清洗后的 txt 以及对应图片,我建议直接丢给 LabelImg 做人工复核。为什么不是 CVAT?如果你只有几千张图,启动一个 Web 服务端的成本比用桌面工具高太多,杀鸡不用牛刀。LabelImg 默认支持 YOLO 格式,把 txt 和 jpg 放在同一目录下打开就能看到已经打好的框,人工只需要做「确认 + 拖拽修正」。
具体节奏是:不逐张看,按目录跳着看,每 5 张确认 1 张,重点看两类图——置信度刚过阈值的图(框大概率歪)和一屏目标超过 20 个的密集图(漏标高发区)。在 LabelImg 里修正时,用快捷键d翻下一张,碰到框压得不准的直接拖角点,碰到漏标的用w键补一个框。一个人一天能复核 1500 张左右,比纯手动标注快出三到四倍。
这里插一个操作要点:LabelImg 会把同名 txt 里的内容加载为已有框,你修正后点保存会覆盖原文件;所以人工复核环节前,一定要把原图复制一份留底。别问我为什么强调这个——连续复核 800 张图后手指肌肉记忆会失灵,会误删一整批框,没有后悔药。
3. 不同数据源怎么喂给自动标注链路:BDD100K、CCPD、VOC 的转换与类别映射
3.1 自动标注工具能处理的数据格式边界:是 YOLO 风格 txt,还是带根目录的 zip
标题写「适用于 yolo 系列所有数据集」,这句话要打个折扣理解:YOLO 系列的训练脚本只认两类东西——一类是images/xxx.jpg与labels/xxx.txt的镜像目录结构,另一类是单类别文件夹加 txt 标注。所以「适用于所有数据集」的真实含义是:只要你能把任意公开数据集的标注格式转成上面两种结构之一,这条自动标注链路就能跑。反过来,任何自动标注工具都不会替你完成「BDD100K 的 JSON 标注」到「YOLO txt 标注」之间的转换,这步永远是你的工作。
我梳理一下常见数据集的原始标注格式:BDD100K 提供train.json文件,里面是带categories和boxes的嵌套结构;CCPD 车牌数据集不提供传统标注文件,而是把标注信息编码在文件名里,比如025-95_91-220&483_516&537-516&473_503&537_488&540_405&536-36_33_22_28_28_34_33_25-108-22.jpg,车牌四个角的坐标就藏在文件名里;VOC 系数据集是 XML 文件,每个<object>节点一个框。自动标注工具要能落地,第一步就是统一这些格式。
3.2 把 BDD100K 的 JSON 标注转成 YOLO txt:完整转换脚本
BDD100K 是车辆检测场景很常用的数据集,它的 JSON 结构是[{"name": "xxx.jpg", "labels": [{"category": "car", "box2d": {"x1": ..., "y1": ..., "x2": ..., "y2": ...}}]}]。转成 YOLO txt 只需要做一个比例换算:把 x1、y1、x2、y2 从像素坐标转成相对于图片宽高的小数。
import json from pathlib import Path from PIL import Image # 类别到 id 的映射,必须和训练配置中的 class names 顺序一致 category_map = {"car": 0, "bus": 1, "person": 2, "truck": 3} json_path = Path("./bdd100k/labels/val.json") img_dir = Path("./bdd100k/images/val") out_dir = Path("./bdd100k_yolo/labels/val") out_dir.mkdir(parents=True, exist_ok=True) with open(json_path, "r", encoding="utf-8") as f: records = json.load(f) for rec in records: img_name = Path(rec["name"]).name img_path = img_dir / img_name if not img_path.exists(): continue w, h = Image.open(img_path).size lines = [] for lab in rec["labels"]: cat = lab.get("category") box = lab.get("box2d") if not box or cat not in category_map: continue x1, y1, x2, y2 = box["x1"], box["y1"], box["x2"], box["y2"] x_center = ((x1 + x2) / 2) / w y_center = ((y1 + y2) / 2) / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{category_map[cat]} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") out_path = out_dir / (img_path.stem + ".txt") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) print(f"completed, output size: {len(list(out_dir.glob('*.txt')))} txt files")这里的关键参数是category_map,它必须和你训练 YAML 配置文件里的names严格一致。比如你的data.yaml里names: ['car', 'bus', 'person', 'truck'],那么category_map里的值就得是 0、1、2、3。不一致的话,预标注模型的输出 id 和你业务 id 可能完全错位,训练的时候会出现「模型以为在标 bus,实际 label 文件里写的 car」这种诡异情况。
还有一个参数容易被忽略:图片宽高w, h,这里用的是原始图片尺寸,不需要经过 letterbox 预处理的尺寸。BDD100K 图片大多接近 1280x720,不会有 int 溢出问题;但如果你处理的是超大航拍图,Image.open可能占用内存过多,建议改成用图片元信息直接取尺寸,避免全部解码。
3.3 CCPD 车牌数据集:从文件名解析坐标并生成标注 txt
CCPD 数据集是车牌检测场景的标配,它的特殊之处在于不需要模型预标注,解析文件名即可得到框坐标。文件名中的220&483_516&537_516&473_503&537_488&540_405&536部分就是四个角点坐标。车牌标注里最重要的是两点:不按最小外接矩形画框,而是按倾斜四边形的角点顺序解析;车牌有蓝牌、绿牌、黄牌之分,类别字段需要业务决定。
import re from pathlib import Path dataset_dir = Path("./CCPD/ccpd_base") out_dir = Path("./CCPD/yolo_labels") out_dir.mkdir(parents=True, exist_ok=True) for img_path in sorted((dataset_dir).glob("*.jpg")): filename = img_path.stem parts = filename.split("-") if len(parts) < 3: continue # 角点字符串,如 220&483_516&537_516&473_503&537 corners_str = parts[2] corners = re.findall(r"(\d+)&(\d+)", corners_str) if len(corners) != 4: continue xs = [int(c[0]) for c in corners] ys = [int(c[1]) for c in corners] x1, y1, x2, y2 = min(xs), min(ys), max(xs), max(ys) width = 1160 # 常用 CCPD 图像宽 height = 720 x_center = ((x1 + x2) / 2) / width y_center = ((y1 + y2) / 2) / height bw = (x2 - x1) / width bh = (y2 - y1) / height out_path = out_dir / (filename + ".txt") with open(out_path, "w", encoding="utf-8") as f: # 车牌类别假设为 0,按自己的业务需要修改 f.write(f"0 {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}\n") print(f"CCPD labels generated: {len(list(out_dir.glob('*.txt')))} files")这段脚本的表层逻辑是正则提取坐标,里层要理解的是:CCPD 角点顺序是左下、右下、左上、右上,直接用min/max取外接矩形会丢失倾斜信息;如果业务要求严格检测倾斜车牌,脚本里应该存四角坐标,而不是文字框。宽度和高度用了硬编码1160和720,这是 CCPD 公开子集的常见尺寸;如果你下的是ccpd_weather这类分辨率不同的子集,这两行需要改成从图片读取。这类细节就是手动转格式最常见的翻车点:数据集官方说 1160x720,但你手里实际数据可能是 1152x720,一个像素的偏差在归一化后影响不大,但在可视化调试时框总是偏移一点点,非常折磨。
3.4 VOC XML 转 YOLO:节点解析与无标签图片的过滤
VOC 格式在很多旧的检测数据集里仍然常见,YOLO 官方仓库也一直保留voc2yolo脚本。自己写解析器的核心是处理<object>节点里<bndbox>下<xmin>、<ymin>、<xmax>、<ymax>四个字段。有一个细节很多人没注意:VOC 的 XML 里可能包含difficult和truncated字段,不处理它们等于把困难样本全当正常样本塞进训练集。
import xml.etree.ElementTree as ET from pathlib import Path xml_dir = Path("./VOC2007/Annotations") img_dir = Path("./VOC2007/JPEGImages") out_dir = Path("./VOC2007_yolo/labels") out_dir.mkdir(parents=True, exist_ok=True) from PIL import Image for xml_path in sorted(xml_dir.glob("*.xml")): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.find("filename").text img_path = img_dir / img_name if not img_path.exists(): continue w = int(root.find("size/width").text) h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): difficult = obj.find("difficult") if difficult is not None and int(difficult.text) == 1: continue cls_name = obj.find("name").text if cls_name not in category_map: continue bndbox = obj.find("bndbox") x1 = float(bndbox.find("xmin").text) y1 = float(bndbox.find("ymin").text) x2 = float(bndbox.find("xmax").text) y2 = float(bndbox.find("ymax").text) x_center = ((x1 + x2) / 2) / w y_center = ((y1 + y2) / 2) / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{category_map[cls_name]} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") if lines: out_path = out_dir / (img_path.stem + ".txt") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n")这里的category_map不是脚本内置的,是一个外部字典,在不同数据集里值不一样。实践上我建议你把所有数据集的类别映射统一维护在一个category_map.py文件里,而不是每个转换脚本里重复定义。否则你同时用了 BDD100K(car=0)、CCPD(plate=0)和 VOC(person=0)做多数据集融合时,id 冲突问题会直接把你劝退。
4. 避坑:自动标注的 4 个高频翻车点与排查办法
4.1 坑一:推理尺寸变了,归一化坐标整体偏移
现象:自动标注生成的框在 LabelImg 里看,都往右下角偏了几个像素,且越靠近边缘偏移越明显,越靠近中心越准。
原因:YOLO 推理时会按imgsz对原图做 letterbox 缩放,输出坐标是相对缩放后图像的归一化值。而 LabelImg 打开原图时,坐标系是原始像素;两者如果没有做反向映射,边缘区域就会出现系统性的偏差。这不是模型的锅,是坐标参考系不一致。
解决:在生成 txt 前对坐标反算 letterbox。常见做法是记录推理时的缩放比例ratio和填充偏移dw/dh,还原到原始坐标后再归一化。具体公式为:原始 x = (归一化 x × 推理图宽 - dw) / ratio,y 同理。你的代码拿到 txt 后,用imgsz和原图大小计算ratio = min(new_w / w, new_h / h),可以一次性还原再存成 YOLO 格式。
4.2 坑二:预训练模型不认识你的业务类别,却硬要标
现象:用yolov8n.pt(COCO 80 类)预标注「玻璃瓶裂纹」数据集,结果 80% 的框都是错的,很多裂纹根本没框,反而把人、车、桌子框了不少。
原因:COCO 类别里没有「裂纹」这个类,模型只会把类似边缘结构误判成已知类。这是选型失误,不是工具问题。
解决:给预标注链路加一个「类别对齐」前置检查。做法是:先用目标场景的小批量图片(比如 500 张)做一次预标注,人工看一遍输出的类别 id 分布和框质量,要求可见目标的标注率超过 60%、误检率低于 15%,再决定是否扩大标注量。如果你手上没有自己训过的模型,可以尝试用带推理能力的实例分割模型辅助打轮廓,再转成框,但边界精度不会更好,只适合做弱特征轮廓场景。
4.3 坑三:输出目录不清理,标签文件重复叠加
现象:第二次对同一批图片跑yolo predict,输出 txt 比第一次多了一倍的框,用肉眼检查发现同目标有两个框位置几乎一样。
原因:YOLO 的project参数指向同一个目录,name也相同,第二次运行直接沿用上一次的输出目录,同名的 txt 被追加写或者旧文件残留。这在我们连续调参时经常发生。
解决:每次跑预标注用独立目录名,比如按时间戳命名。看一眼命令:
yolo predict model=./best.pt source=./raw_images save_txt=True save_conf=True project=./auto_label name=$(date +%Y%m%d_%H%M%S)这样每次的输出目录都不同,永远不会和上次的结果混在一起。如果你需要和之前的对比,runs/下天然保留历史。
4.4 坑四:类别 id 映射错位,训练时 loss 异常且 mAP 为零
现象:转换后的 txt 里类别 id 是 0,但训练 YAML 里的names第一个是plate(车牌),模型训练 10 个 epoch 后 mAP 一直是 0,验证集可视化时发现框全部标在正确位置但类别全部不对。
原因:预标注模型的类别 id(COCO 里 car=2,person=0)和业务数据集的类别 id(业务里 car=0,person=1)不一致。转换脚本做数值替换时的规则写反了。
解决:把 id 映射表做成显式字典并打印出来核验,不要依赖「顺序一致」的假设。最稳妥的做法是在转换脚本末尾加一段校验代码:随机读 5 个 txt,打印类别 id 和对应的 class name,人工扫一眼是否合理。自动化工具再快,这一步不建议省,因为它能拦住大部分数据污染类错误。
5. 让自动标注质量可验证:置信度回退与错题集抽检机制
自动标注做完、人工也复核完一遍之后,还需要一个可量化的质量出口,否则你永远不知道这批数据到底能不能喂给训练。我现在的习惯是在正式训练前做两次抽检,两轮加起来能拦住 90% 以上的脏数据。
第一轮是「置信度回退抽检」。把人工修正后的标注文件全部按图片分组,统计每张图的平均置信度(人工复核时保留置信度列,或者复核后从原始预标注结果回填)。取置信度最低的 5% 图片,人工再二刷一遍。原理是:某个模型对一张图的所有目标都给低置信度,说明这张图大概率在目标尺度、光照或遮挡上偏离了训练分布,人工修正时也容易漏。这个回退机制相当于给了标注质量一个兜底,不只是看单框准不准,而是看整张图是否值得保留。
第二轮是「错题集对比」。全量标注完成后,跑一个 50 epoch 的短训练,取验证集里预测的错题——也就是conf高于 0.5 但目标完全落空的框,和标注真实值重合度极低的框,可视化出来。看这些错题框对应的原图,如果发现它们的标注框位置都是同一个方向偏移,那说明是自动标注时的坐标转换 bug;如果错误框集中出现在某个类别,说明该类别样本不够或标注质量普遍差,但不会因为一两个错框去全量重标。
我自己的教训是:不要在「数据集已经标完」这个节点上过于自信。自动标注省下来的时间,至少留出 15% 用于抽检和回退,这个比例对应的是训练稳定性的预算。我见过很多人用自动标注一小时生成 3000 张图,训练后 Loss 曲线稳定但 mAP 一直上不去,排查到最后发现是验证集里混了 200 张空标签图。用错的数据集做判断,比没有数据集更危险。
如果条件允许,可以为每个数据集维护一个stats.json,记录图片总数、框总数、类别分布、平均置信度、空标签图数量。这个文件在下一批自动标注跑完后对比一下比例变化,就能知道新的预标注模型有没有进步。这套验证方法不复杂,但它是从「能用自动标注」过渡到「信任自动标注」的分水岭。我也是在这个步骤补上之后,才敢把自动标注跑出来的数据直接作为训练集的主体的。希望帮到你。
本文还有配套的精品资源,点击获取