YOLOv5半自动标注:高效目标检测数据集生产实践
2026/9/13 14:01:22 网站建设 项目流程

简介:面向需要高效扩充目标检测数据集的开发者和算法工程师,这套yolov系列半自动标注代码提供了一条“人工预标注少量样本→训练初版检测模型→模型批量预测生成XML→人工检查修正”的实用工作流。压缩包体积仅7KB,共5个文件,以两个Python脚本为核心,一个负责读取待标注图片并调用模型权重生成XML标注文件,另一个用于单独查看预测效果;同时附带YAML数据集配置文件和使用步骤说明文本,文件结构紧凑。按说明修改图片路径、权重路径和配置文件路径即可运行,作者还给出了分阶段落地方案,适合从零搭建半自动标注流程的读者参考,尤其能解决标注数据量大、人工成本高的痛点。目前已有521人学习下载,配合轻量级脚本与清晰说明,能大幅提高数据集制作效率,适合有一定目标检测基础的开发者使用。

1. 为什么说YOLOv5半自动标注是数据集生产的最优解

手动标注目标检测数据集的成本曲线是线性的:一张1280×720的图,如果密集目标有好几个,画框加类别可能要折腾三分钟。而YOLOv系列做推理的成本几乎可以忽略,单张图在普通显卡上几十毫秒就能出框。因此“先人工标一批、再训练初版模型、再用模型大量预标注、最后人工修正”这个流程,能把人力从“画框”中释放出来,只留“确认/调整”的活。

这个zip里的auto_label.py脚本就是为此设计的:把图片丢进auto_label/images,改一下模型权重路径和数据集配置,运行后就会输出Pascal VOC格式XML;之后用detect_image_only.py做可视化预览,再在LabelImg里人工修正。适合正在做垂直领域目标检测(比如证件识别、桥墩病害检测、小目标作业场景)又缺公开数据集的团队。

关键点在于,这套方法不会因为你初始模型精度低而失效。只要迭代逻辑闭环,模型的量级优势就能弥补初期的漏检和误检,最终把数据集生产速度整体拉高一个量级。

2. 半自动标注的前置工作:数据整理与custom_data.yaml配置

2.1 整体流程:先有人工标注,再有预标注迭代

半自动标注不是一个孤立脚本,而是一条生产链路。包里的使用步骤说明.txt只写了“将待标注图像放入auto_label/images,修改auto_label.py的第62至65行,运行auto_label.py”,但实际落地时必须先满足两个前提:一是已经有一批人工标注过的图像,二是基于这批图像训练了YOLO权重。整体闭环如下:

  • 人工标注第一批样本:目标覆盖场景多样化,不追求数量,只追求类别和形态的完整性。比如证件照片,要包含倾斜、遮挡、光线不均等版本。
  • 训练初版模型:使用yolov训练自己的数据集,例如yolov5s,迭代100轮左右,验证集mAP曲线开始平缓即可。
  • 模型预标注:将训练好的权重填入auto_label.py,对未标注图片批量推理,生成XML。
  • 人工复核:用detect_image_only.py或LabelImg打开预标注结果,修正错框、补充漏框。
  • 数据回流:把修正后的标注合并进训练集,重新训练,下一轮预标注质量会更高。

这里的核心是“迭代”。第一次预标注可能错漏较多,但每轮训练后模型学到的视觉特征会更有针对性。收敛标志是:按固定验证集统计的mAP提升幅度低于0.5个百分点,且人工修正率低于10%。尤其要注意,预标注的结果如果直接回灌训练集而不人工检查,错误会指数级累积,所以“人工复核”一步不能省略。

实际操作时,我会在auto_label目录下建四个子目录:images存放原始图片,xmls存放脚本生成的预标注XML,preview存放带检测框的可视化结果,runs/labels存放检测置信度记录。这样每次迭代可以快速对比“模型上一次的预测”和“人工修正后最终标注”的差异,出现问题时也能定位是哪一步污染了数据。

2.2 custom_data.yaml:类别映射是半自动标注的地基

auto_label.py在生成XML时,需要知道每个检测框的类别名,这个信息来自custom_data.yaml。一个典型的配置文件内容如下:

# custom_data.yaml train: /home/user/projects/auto_label/datasets/idcard/train val: /home/user/projects/auto_label/datasets/idcard/val nc: 2 names: ['idcard_front', 'idcard_back']

读取时用yaml.safe_load即可,names列表会按顺序映射到模型输出张量里每个anchor框的class id。auto_label.py拿到class id后,查表得到字符串,写入XML的<name>节点。这个文件必须和训练时完全一致,不能只改nc不改names,否则预标注的类别会整体错位。

修改时常见错误是路径写成相对路径,而脚本从auto_label子目录启动时,相对路径基准变了。我一般把train和val改成绝对路径,或者将所有数据集放在脚本同一级目录下。如果你手里的模型是从其他工程导出的,也要把names调整为模型训练时的类别顺序,否则XML里会出现类别标签互换。建议运行前先打印一行类别表,对一张已知图片做推理,确认类别顺序无误再批量跑。

2.3 模型权重选择:决定预标注的人工修正量

auto_label.py中yolo_model_weight参数指向的权重,决定了预标注的初始质量。这里有三条经验。第一,不要用未微调的COCO预训练权重去标注自定义类别,因为YOLO的head只会输出COCO的80类,如果你的类别不在其中,模型根本不会输出对应框。第二,不要用过拟合的最后一代权重,建议用验证集mAP较高的早期checkpoint,这类权重泛化能力更好,对未标注图像的预测结果更稳定。第三,如果目标尺寸很小或密集排列,模型选择要跟着目标尺度走,输入分辨率也要同步提高。

一个粗暴但有效的判断方法是对比不同权重在同一批100张图上的预标注修正耗时。先小批量测试,记录单张推理时间和需要人工调整的框数,再决定是否换更大模型。大致参考如下:

模型权重推理速度(单张640×640)适用场景
yolov5s约300fps数据量大、目标尺寸中等
yolov5m约150fps精度和速度平衡
v6x约50fps小目标或密集目标,人工修正量优先

这里的fps数值会因显卡不同而浮动,重点是权重越大,预标注质量越好,但耗时也越高。如果待标注图有上万张,我会先用小权重跑完第一批,把漏检多但容易补框的图处理掉,再用大权重专门处理高难度图。

3. auto_label.py:从图片到XML标注的脚本改造与参数调整

3.1 脚本结构与核心推理流程

解压zip后需要关心的Python文件只有两个:auto_label.py负责生成标注,detect_image_only.py负责可视化审核。auto_label.py内部结构与YOLOv5的detect.py很接近:加载权重、遍历图像、非极大值抑制、坐标缩放、结果输出。不同点在于,它把输出从“txt或图片”改成了Pascal VOC XML。以下是一个可以直接参考的脚本骨架:

import torch from pathlib import Path from xml.dom.minidom import Document model = torch.hub.load('ultralytics/yolov5', 'custom', path='./weight/IDCard_v6x_best.pt', force_reload=True) model.conf = 0.5 model.iou = 0.45 def write_xml(img_path, boxes, xml_path): doc = Document() annotation = doc.createElement('annotation') filename = doc.createElement('filename') filename.appendChild(doc.createTextNode(img_path.name)) annotation.appendChild(filename) for _, row in boxes.iterrows(): obj = doc.createElement('object') name = doc.createElement('name') name.appendChild(doc.createTextNode(row['name'])) obj.appendChild(name) bndbox = doc.createElement('bndbox') for tag, val in [('xmin', int(row['xmin'])), ('ymin', int(row['ymin'])), ('xmax', int(row['xmax'])), ('ymax', int(row['ymax']))]: node = doc.createElement(tag) node.appendChild(doc.createTextNode(str(val))) bndbox.appendChild(node) obj.appendChild(bndbox) annotation.appendChild(obj) doc.appendChild(annotation) with open(xml_path, 'w') as f: doc.writexml(f, indent='\t', addindent='\t', newl='\n') img_dir = Path(r"auto_label/images") for img_path in img_dir.glob("*.jpg"): results = model(str(img_path)) boxes = results.pandas().xyxy[0] write_xml(img_path, boxes, img_dir / (img_path.stem + ".xml")) print(f"{img_path.name}: {len(boxes)} objects")

这段代码的逻辑很清楚,但也藏着几个坑。model.conf和model.iou是YOLOv5推理接口提供的属性,前者控制置信度阈值,后者控制NMS的IoU阈值。预标注时conf可以设在0.4到0.6之间,如果设太低,会输出大量杂乱的误检框,人工光删框就很累。boxes.pandas().xyxy[0]返回的是一个DataFrame,包含xmin、ymin、xmax、ymax、confidence、class、name,脚本直接按列名取值,省去手动解析张量的麻烦。坐标已经经过YOLOv5内部从输入尺寸到原图尺寸的反算,正常情况下可以直接写入XML。

提示:如果脚本使用torch.hub.load,每次运行都可能联网检查YOLOv5仓库。离线环境要把force_reload设为False,并提前将ultralytics/yolov5仓库clone到本地,否则会在加载权重时卡住。

3.2 第62至65行:四个路径与模型参数

按原项目说明,只需修改第62至65行,这四个参数基本就是脚本的“业务入口”:

path = r"auto_label/images" # 待标注图片路径 xml_path = r"auto_label/images" # 输出的xml标注文件保存路径 yolo_model_weight = './weight/IDCard_v6x_best.pt' # 模型文件路径 data_conf = './data/custom_data.yaml' # 数据集配置文件路径

这里的path和xml_path可以是同一个目录,但我不建议这样做。批量生成XML时会写几千个小文件,放在同一目录会让后续人工筛查经常误碰。我习惯于把xml_path独立指向xmls子目录,人工复核完成后再把正式XML复制回images对应的labels目录。这样即使某次运行出错,也不会污染原始图片目录。

yolo_model_weight和data_conf建议改成绝对路径。常见错误是权重放在weight目录,但脚本从项目根目录运行时,./weight/...是相对根目录的;如果从别的目录执行python,就会找不到文件。稳妥做法是在脚本开头用Path(__file__).parent定位脚本所在目录,再拼接子目录:

BASE_DIR = Path(__file__).parent yolo_model_weight = str(BASE_DIR / 'weight' / 'IDCard_v6x_best.pt') data_conf = str(BASE_DIR / 'data' / 'custom_data.yaml')

改完之后不要急着全量跑。先取10张图像丢入auto_label/images,运行auto_label.py,再用detect_image_only.py检查这10张的预标注框位置。如果框体偏移,多半是模型推理预处理与XML写入时的坐标还原不一致;如果没有框,先检查conf阈值和权重路径。小批量验证通过后再跑全量,能省下大量无效计算。

如果运行时遇到错误,参考常见的排查表:

报错信息常见原因处理办法
FileNotFoundError权重路径或数据配置路径错误改成绝对路径,检查文件存在
No such file or directory相对路径基准不对用Path(file).parent定位
RuntimeError: CUDA out of memory输入分辨率或batch过大调低conf、一次只跑一张
XML生成但无框conf阈值过高或模型类别不匹配调低conf到0.3测试,检查names顺序

3.3 XML转YOLO训练的txt格式

这个方案输出XML是因为人工审核工具对XML格式支持成熟,但你最终训练yolov系列大概率需要YOLO txt格式。因此半自动标注管线里还应该包含一个转换脚本。转换的核心是根据XML里的<size>做归一化:

import xml.etree.ElementTree as ET def xml_to_yolo(xml_file, txt_file, class_names): tree = ET.parse(xml_file) root = tree.getroot() img_w = float(root.find('size/width').text) img_h = float(root.find('size/height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue cls = class_names.index(name) xmin = float(obj.find('bndbox/xmin').text) ymin = float(obj.find('bndbox/ymin').text) xmax = float(obj.find('bndbox/xmax').text) ymax = float(obj.find('bndbox/ymax').text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h bw = (xmax - xmin) / img_w bh = (ymax - ymin) / img_h lines.append(f"{cls} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") with open(txt_file, 'w') as f: f.write('\n'.join(lines))

转换脚本里的class_names必须与训练时一致。这里有个容易忽略的问题:半自动标注输出的XML中可能包含模型识别出的“难例”,也就是置信度不高但确实存在的目标。人工复核时如果保留了这些框,转换后的txt会包含它们,这是好事。但如果XML里有超出class_names范围的类别名,脚本会直接跳过,不会报错,这时需要回头检查custom_data.yaml的names是否和训练一致,否则会丢失大量有效标注。

4. 用detect_image_only.py做批量预览与漏检分析

4.1 只看图不写标注,审核效率反而更高

auto_label.py生成XML后,你不能相信每一个框都是准确的。要快速了解模型在某个批次的预标注质量,最好的方式是生成带框的可视化图片,人眼扫一遍比逐条查XML快很多。detect_image_only.py就是干这个的,它只做推理和画框,不写XML,适合在人工复核前先做个“预览”。用法与YOLOv5官方detect.py类似:

python detect_image_only.py --source auto_label/images \ --weights ./weight/IDCard_v6x_best.pt \ --conf-thres 0.4 \ --save-img runs/preview \ --save-txt runs/labels

参数说明:

  • --source:输入图像路径,可以是目录,也可以是单张图片。
  • --conf-thres:置信度阈值,预览时可以比生成XML时更低,便于发现那些被模型忽略但确实存在的目标。
  • --save-img:保存可视化结果,框上会显示类别和置信度。
  • --save-txt:保存检测结果的原始txt,用于后续统计分析,但不影响XML生成流程。

如果预览时发现某一批图的框普遍偏小或偏离目标,不要急着改脚本,先确认这些图是否来自与你训练数据分布差异过大的场景。比如训练数据全是证件扫描件,预标注时丢进来手机翻拍照片,模型的表现必然退化。这时应该将该批图像并入训练集,先人工标注,再重新训练,而不是依赖预标注硬标。

在LabelImg中打开预标注XML时,要把classes.txt配置正确,否则类别名无法识别。开启自动保存后,人工修正的框会立即覆盖原XML。修正后注意检查XML里的<size>节点是否完整,只要width、height、depth三个值别丢,后续转换成YOLO txt就不会出坐标错位的问题。

4.2 置信度分层:决定哪些框需要人工细致检查

预标注阶段,模型输出的置信度是一个非常有价值的筛选信号。我的做法是把检测框按置信度分成三档,对应不同的人工处理策略,便于排定复核优先级:

置信度区间预标注处理人工复核方式
0.5-1.0直接生成XML快速浏览,只调整明显偏移的框
0.25-0.5生成XML但标记为待确认目标较小或遮挡时重点检查
0-0.25不生成XML纳入困难样本池,人工单独框选

这个策略的本质是让模型处理它有把握的部分,把有限的人力集中在容易漏检的低置信度区域。很多团队一上来就把conf降到0.2,希望模型把所有可能目标都标出来,结果误检框太多,人工删框的时间比画框还长。正确做法是先用0.5以上阈值把高置信度数据覆盖掉,再用detect_image_only.py配合--conf-thres 0.25专门浏览低置信度区域,人工决定哪些要补充标注。

经过几轮迭代后,可以统计每个置信度区间内的精确率,反过来指导阈值设定。如果0.5~0.7区间的错误框很多,就把预标注阈值上调到0.6,避免无谓的人工删框。

4.3 统计漏检率:量化模型是否在进步

判断半自动标注是否越做越顺,不能靠感觉,需要可量化的指标。人工复核时,在LabelImg中新增的框就是“漏检框”,删除的框是“误检框”。随机抽取本轮已标注的100张图,统计漏检率和误检率:

  • 漏检率 = 人工新增框数 / (模型框数 + 人工新增框数)
  • 误检率 = 人工删除框数 / 模型框数

当漏检率和误检率都低于20%时,预标注结果可以直接进入“只修正不打框”的轻量复核流程。如果误检率居高不下,把conf阈值调高0.1往往能明显减少空框。如果漏检率居高不下,则说明模型还没学透当前类别的外观变化,应该把漏检图收集起来,单独训练几轮后再做预标注。

这个统计不用写复杂工具,一行Python就能算:

import glob import xml.etree.ElementTree as ET model_box = 0 human_added = 0 for xml_file in glob.glob('auto_label/xmls/*.xml'): root = ET.parse(xml_file).getroot() for obj in root.iter('object'): if obj.find('deleted') is not None: continue model_box += 1 human_added += len(glob.glob('auto_label/human_added/*.xml')) print(f"漏检率: {human_added/(model_box+human_added):.2f}")

这里的deleted标记和human_added目录,是你在LabelImg中人工操作后需要导出的信息。如果你的工具不支持导出“人工新增”,也可以把审核前后的XML做差,差异框数量即人工修正量。

5. 半自动标注的迭代收敛与小目标切图技巧

5.1 防止错误累积:设固定验证集

模型自动标注的框进入训练集后,会直接影响下一轮模型权重。为了防止错误标注被模型当作正样本加强,从第一批手动标注数据中留出200张固定验证集,永远不参与自动标注。每轮训练完成后,用该验证集计算mAP;如果mAP比上一轮下降超过0.5个百分点,立即暂停数据合并,回退训练数据到上一版本,人工复核新增预标注中置信度在0.5以下的框。这比依赖tensorboard曲线更直接。

5.2 小目标场景下的切图预标注

当目标像素面积小于32×32时,直接整图推理漏检很常见。一个有效做法是把原始图切成重叠小块,分别检测再合并坐标:

def predict_tile(image, model, tile=640, overlap=64): h, w = image.shape[:2] det_boxes = [] for y in range(0, h, tile - overlap): for x in range(0, w, tile - overlap): piece = image[y:y+tile, x:x+tile] results = model(piece) boxes = results.pandas().xyxy[0] for _, b in boxes.iterrows(): det_boxes.append([b['xmin']+x, b['ymin']+y, b['xmax']+x, b['ymax']+y, b['confidence'], b['name']]) return det_boxes

切图后的坐标偏移量需要在合并时加回原图坐标。另外,切图会产生大量重叠检测,比如同一个目标被两个tile同时框到,需要做IoU阈值0.5的NMS融合。边缘被切掉一半的目标在预标注中往往没有完整框,人工复核时要额外注意图片上下左右边缘,把它们补充成完整框,否则训练目标框会大量偏小。

5.3 用脚本统计类别分布,提前暴露数据偏斜

半自动标注的价值还在于提供一个全局视角。处理完一批图后,统计所有XML中的类别实例数:

from collections import Counter import glob, xml.etree.ElementTree as ET counter = Counter() for xml in glob.glob('auto_label/xmls/*.xml'): root = ET.parse(xml).getroot() counter.update(obj.find('name').text for obj in root.iter('object')) print(counter)

如果某个类别数量只有另一个的十分之一,说明数据集存在严重偏斜。下一轮不要继续均匀分配人工标注量,而是把所有低置信度预标注框按类别聚类,单独挑出稀有类别样本补充标注。这样能让目标检测算法在真实场景里不至于因类别不平衡而放弃小样本类别。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询