简介:面向YOLO系列目标检测训练的数据标注工具,适合需要快速构建数据集的研究人员、学生与开发者,重点解决手工框选耗时、标注格式不统一的问题。压缩包共115个文件,约6.3MB,以42个png界面/示例图、29个py源码、9个svg图标、6个sh脚本及rst、md说明文档等为主,兼顾运行、定制与查阅。已有384人学习下载,说明其在数据预处理环节具有一定参考价值。工具围绕多边形选择、快速编辑、类别管理、批量处理、预览校验与YOLO格式导出展开,并保留源码结构,便于按项目需求修改标注流程或扩展自动化辅助能力。读者可据此搭建本地标注环境,理解标注文件生成逻辑,并用于目标检测数据集的整理与校验。
1. 自动标注工具到底替谁省了时间
标注这件事,真正消耗时间的地方有三处:第一遍画框、画错回改、格式导出后训练时才发现坐标不对。标题里这个自动标注工具针对的是 YOLO 系列数据集,核心价值不在于“点一下就把整张图标完”,而在于先把预标注跑出来,再用人工只做增删改这三件事。它适合手上已经有几百到上万张图、要跑目标检测训练、但不想从零拉框的人。
YOLO 生态里数据格式的细节很容易被忽略:txt 文本标签、类别编号、归一化坐标、图片与标签同名配对,这四点任一出错,训练脚本不会报错,只会影响模型收敛。工具如果能在导出阶段就把这些固定下来,比“画得快”更重要。第 1 章先把适用范围讲清,后面几章从环境、预标注、转格式一路写到踩坑和验证。
2. 工具定位与 YOLO 数据格式的对应关系
自动标注工具不是单一软件,通常由三块组成:预标注模型、可视化修改界面、格式转换脚本。很多现成工具箱(比如基于检测模型的半自动标注流程)把这三点做成一体化;也有人用手写脚本拆开。选择哪种取决于数据规模和团队协作方式,不是取决于哪个界面好看。
2.1 为什么先看格式再选工具
YOLO 的标签文件是与图片同名的.txt,每行形如class_id x_center y_center width height,后四个值都是相对原图宽高归一化到 0~1 的浮点数。这带来两个直接后果:一是图片尺寸变了标签不用重算,二是坐标一旦不是归一化值,训练侧不会报错,只会让框全部漂移。常见的翻车现象是“loss 不降、mAP 一直是 0”,排查半天才发现 txt 里写的是像素坐标。
不同 YOLO 版本对目录结构的要求大体一致:训练集和验证集各自一个images和一个labels子目录,文件名去掉后缀后必须一一对应。工具如果导出时按train/images/xxx.jpg和train/labels/xxx.txt分开放,就不用再手写脚本搬文件。我在选工具时第一眼看的是导出目录长什么样,而不是标注界面有多酷。
提示:先在几十张图上跑通“导出→训练脚本能读到标签”这条链路,再批量处理全量数据,否则错误会被放大到整个数据集。
2.2 三类落地方式与选型判断
按使用门槛从低到高,常见做法可以归成三类,适合的场景差别不小:
| 方式 | 典型组成 | 适合规模 | 主要代价 |
|---|---|---|---|
| 图形化标注软件内置模型 | 标注工具 + 本地检测权重 | 几百到几千张 | 模型与界面绑定,换类别要重配 |
| 预标注脚本 + 通用标注工具 | 推理脚本输出 txt,再人工修正 | 几千到几万张 | 需要自己写或改格式转换 |
| 全流程服务/平台 | 上传数据、云端预标注、导出 | 团队协作、短期项目 | 数据出本地,流程不透明 |
我一般推荐第二种:预标注脚本只负责“把模型结果写成 YOLO txt”,人工修正交给熟悉的标注工具,职责分离后出问题好定位。第一种胜在开箱即用,但模型和界面耦合,一旦要换检测头或改类别映射,改动面比想象中大。
2.3 类别映射表要提前定死
自动标注最常见的隐性成本是类别编号错位。模型输出的类别是它训练时的编号,你自己的数据集需要的是另一套编号,中间的映射如果不写成显式文件,就会散落在各个脚本里。建议在项目根目录放一个classes.txt或data.yaml,一行一个类名,顺序即编号,所有脚本都读它。
# data.yaml:类名顺序就是标签里的 class_id path: ./dataset train: images/train val: images/val names: 0: person 1: helmet 2: vest这个文件的names顺序必须和预标注模型的输出顺序区分开:模型给的是“它的类别编号”,你要做的是映射到“你的编号”。两者重合时最省事,不重合就必须显式写映射字典,不要靠猜。改完data.yaml后,训练前用一小段脚本统计标签里出现过的 class_id,看有没有超出names长度的值,能提前拦掉大部分低级错误。
3. 跑通预标注:环境、推理与标签生成
这一章是动手部分。整体链路是:准备图片目录 → 加载检测模型 → 逐张推理 → 把框写成 YOLO txt → 人工在标注工具里修正 → 导出训练格式。中间任何一步的输出格式不对,后面都要返工,所以每一步都建议先小批量验证。
3.1 环境准备与依赖固定
预标注依赖的无非是推理框架加图像库。用虚拟环境隔离,版本写死,避免“换台机器就跑不起来”。下面这套是最小依赖,检测模型换成自己手上的权重即可。
# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 固定版本,避免推理结果随版本漂移 pip install "ultralytics==8.2.0" "opencv-python==4.9.0.80" "numpy<2"ultralytics负责加载检测权重和推理,opencv-python用来读写图片并拿到宽高,numpy锁在 2.0 以下是因为部分图像库对 NumPy 2.x 的 ABI 还不兼容,装完 import 报错多半是这里。版本号不是越新越好,预标注只要求结果稳定可复现,不追求新特性。
3.2 逐张推理并直接写出 txt
下面的脚本做三件事:遍历图片目录、推理、按 YOLO 归一化格式写 txt。置信度阈值设 0.35 是经验值,太低会引入大量误检,人工删框的时间反而超过手画。
import os from pathlib import Path import cv2 from ultralytics import YOLO IMG_DIR = Path("dataset/images/raw") LBL_DIR = Path("dataset/labels/raw") LBL_DIR.mkdir(parents=True, exist_ok=True) model = YOLO("weights/detector.pt") # 换成自己的检测权重 CONF = 0.35 # 置信度阈值,先保守 IOU = 0.5 # NMS 的 IoU 阈值 for img_path in sorted(IMG_DIR.glob("*.jpg")): img = cv2.imread(str(img_path)) h, w = img.shape[:2] # 归一化必须用原图宽高 results = model.predict(img, conf=CONF, iou=IOU, verbose=False) lines = [] for box in results[0].boxes: cls_id = int(box.cls.item()) x1, y1, x2, y2 = box.xyxy[0].tolist() # 转成中心点 + 宽高,再除以宽高归一化 cx = (x1 + x2) / 2 / w cy = (y1 + y2) / 2 / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") txt_path = LBL_DIR / f"{img_path.stem}.txt" txt_path.write_text("\n".join(lines), encoding="utf-8")逻辑上有两个容易出错的地方。第一,归一化用的是原图h, w,不是模型输入尺寸,模型内部会做 letterbox 缩放,xyxy返回的已经是映射回原图的坐标,但如果你自己再做一次缩放就全错。第二,img_path.stem保证 txt 与图片同名,这是 YOLO 读取标签的硬性要求,改了命名规则训练侧就找不到。
还有一个现实问题:没有检测框的图片要不要写空 txt?要写。YOLO 训练时把“没有对应 txt”和“空 txt”都当背景图处理,但保留空文件便于统计哪些图确实没目标,也避免人工修正阶段漏看。脚本里加一行判断即可,"\n".join([])得到空字符串,write_text会生成 0 字节文件,正是需要的效果。
3.3 类别映射与置信度调参
模型输出编号和你的数据集编号不一致时,在写 txt 之前插一层映射,别在训练时再补救。
# 模型类别名 -> 你的数据集编号 NAME_TO_ID = {"person": 0, "helmet": 1, "vest": 2} for box in results[0].boxes: name = model.names[int(box.cls.item())] # 先拿到模型侧类名 if name not in NAME_TO_ID: continue # 不需要的类直接丢弃 cls_id = NAME_TO_ID[name]用类名做中间键比直接用编号稳,模型换版本后编号可能变,类名一般不变。置信度阈值建议分两轮跑:第一轮 0.35 覆盖大部分目标,人工只做删改;第二轮对漏检多的类别降到 0.2,再把新出的框合并进去。别一次性调到 0.1,误检框会让人工成本翻倍。
另外,iou参数控制 NMS 合并重叠框的力度。密集小目标场景调高到 0.6~0.7,否则相邻目标会被合并掉;大目标场景保持 0.5 即可。这两个参数不是玄学,改完在几十张图上肉眼对比框的数量就能判断方向。
3.4 人工修正环节怎么少返工
预标注只是把“从零画框”变成“改框”,修正环节的效率取决于标注工具的快捷键和 Classes 配置。把classes.txt直接导入标注工具,保证界面里的类别顺序与data.yaml一致;修正时只做四类操作:删误检、补漏检、调边界、改类别。不要在修正阶段顺手重命名文件,命名规则一旦变动,图片和 txt 的配对关系就要重新校验。
如果数据量上千,建议按目录分批修正,每批修完立刻跑一次格式校验(第 5 章会给脚本),校验通过再进入下一批。血泪经验是攒了几千张一起校验,报错时已经定位不到是哪一步引入的。
4. 避坑与排查:五类真实翻车记录
自动标注的坑集中在“不报错但结果不对”。下面五条按现象、原因、解决写,基本都是我在不同项目里踩过的。
4.1 训练 loss 正常但 mAP 接近 0
现象:训练能跑完,分类 loss 下降,验证集 mAP 一直是 0 或极低。原因:标签写成了像素坐标而非归一化坐标,模型学到的框全部超出图像范围。解决:抽查任意一个 txt,四个数值都应该在 0~1 之间;若出现几十、几百的值,回到 3.2 检查是否漏了除以宽高这一步,批量重跑预标注。
4.2 标签文件与图片对不上号
现象:训练日志里提示大量“missing labels”,或者明明标了却学不到。原因:图片是IMG_001.JPG大写后缀,txt 生成成了IMG_001.txt,某些环境对大小写敏感;或者图片放在子目录里,标签却平铺在一层。解决:统一转小写后缀,标签目录结构与图片目录一一镜像,转换前先跑一遍配对检查。
from pathlib import Path img_stems = {p.stem for p in Path("dataset/images/train").glob("*")} lbl_stems = {p.stem for p in Path("dataset/labels/train").glob("*.txt")} print("无标签的图片:", sorted(img_stems - lbl_stems)[:10]) print("无图片的标签:", sorted(lbl_stems - img_stems)[:10])两边差集为空才算配对完成,先看前 10 条就能判断是普遍问题还是个别脏数据。
4.3 框整体偏移固定像素
现象:所有框相对目标统一往一个方向偏,偏移量还差不多。原因:预标注时对图片做了 resize 或 padding,写标签却用了缩放后的尺寸,或者反过来。解决:确认归一化除数始终是原图宽高;如果流程里确有预处理,坐标必须先逆变换回原图再归一化。固定偏移是典型的坐标系错位,不是模型精度问题。
4.4 类别编号越界导致静默失败
现象:训练不报错,但某些类永远检测不到。原因:txt 里的 class_id 大于等于data.yaml中names的长度,部分训练脚本会直接忽略这类标签。解决:训练前统计所有 txt 中出现过的最大编号,与类名数量比对。
# 统计所有标签里出现过的最大 class_id cat dataset/labels/train/*.txt | awk '{print $1}' | sort -n | tail -1输出值必须小于names的条目数,否则就是映射表漏了类。
4.5 空标签文件被误删
现象:人工修正后部分背景图消失,模型对无目标场景误检变多。原因:清理脚本把 0 字节的 txt 当作垃圾文件删掉了。解决:保留空 txt,清理逻辑里只删“没有同名图片的 txt”,不按文件大小判断。背景图对抑制误检有实际作用,删了等于丢掉负样本。
5. 数据集校验与增量标注的稳定做法
批量预标注完成后,真正决定能不能直接开训的是校验环节。我习惯在导出后固定跑三段检查:坐标范围、类别分布、配对完整性。坐标范围用一行 Python 就能扫全量,比肉眼抽查可靠得多。
from pathlib import Path bad = [] for txt in Path("dataset/labels/train").glob("*.txt"): for i, line in enumerate(txt.read_text().splitlines(), 1): parts = line.split() if len(parts) != 5: bad.append((txt.name, i, "字段数不为5")); continue vals = list(map(float, parts[1:])) if any(v < 0 or v > 1 for v in vals): bad.append((txt.name, i, "坐标越界")) if vals[2] <= 0 or vals[3] <= 0: bad.append((txt.name, i, "宽高非正")) print(f"异常行数: {len(bad)}") for item in bad[:20]: print(item)这段脚本覆盖了三类高频错误:字段缺失、坐标越界、宽高为零或负。宽高为负通常来自x1 > x2的框,说明预标注阶段坐标顺序取反了。扫出来的前 20 条足够判断是系统性问题还是个别脏数据,系统性错误就回上一步重跑,别逐条手改。
类别分布则用计数看一眼长尾。某个类样本极少时,预标注模型对它本身就不可靠,这时候自动标注反而会引入噪声,建议这类样本退回纯手工。增量标注时我固定遵循一条规则:新一批数据先用同一套权重预标注,修完后与旧标签合并,合并后必须重跑一次配对和越界校验。后悔药就是校验脚本,每次合并前都强制走一遍,比事后查 loss 曲线猜问题快得多。
验证工具是否真的省时间,有个简单指标:随机抽 100 张预标注结果,统计需要人工改动的框占比。改动率低于 30% 说明阈值和模型选得合适,高于 50% 就该调置信度或换权重,而不是硬着头皮修。从那以后我每次接入新数据源,都先用 100 张做改动率评估,再决定要不要全量跑,这个习惯帮我省掉过好几次整批返工。希望帮到你。
本文还有配套的精品资源,点击获取