ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLOv5半自动标注实战:txt转xml格式转换全流程解析

YOLOv5半自动标注实战:txt转xml格式转换全流程解析

干目标检测的人,迟早会被数据标注绊一跤。几千张图,一张一张拿鼠标画框,画到后半夜眼睛发花,框还没画完。我身边有朋友就是这么熬过来的——后来他学乖了,先训练一个不算太差的YOLOv5模型,让模型把几千张图“预标注”一遍,再拉人去检查、改错、补漏。这条路走到最后,卡在一个很实在的问题上:模型吐出来的是YOLO格式的txt,标注工具认的是VOC格式的xml。这篇就把我用YOLOv5做半自动标注/预标注的完整方案拆开讲,重点放在txt转xml这一步,顺带把推理参数、人工校验、迭代闭环这些周边细节一并说清楚。想少熬夜画框、又想保住标注质量的,可以直接参考这套流程。

1. 先想清楚:半自动标注到底要解决什么问题

1.1 标注这件事的账,得这么算

做检测项目,第一个绕不过去的坑就是训练数据的量。以工业检测场景为例,起步两千张图很正常,复杂场景加到五千张也不夸张。纯人工标注,一张图平均四十秒到一分钟,两千张就是二十多个小时,这还没算重复劳动带来的眼疲劳和手误。一个班里三十个学生同时举手,老师挨个点名回答,一节课下来嗓子哑了,还没答完一半——人工标注就是这种既费时又费神的活。

半自动标注/预标注的思路,本质上是让模型先当一次“代课老师”。用已经训练好的权重对新图跑一遍推理,把疑似目标的框和类别提前打出来,人工只需要对着预标注结果做确认、微调和补充。人工从一个字一个字“写作文”变成“改作文”,速度自然就上来了。我实测的数据是:预标注后,单张图片的校对时间压到八到十五秒,效率提升至少七成,眼睛负担小了一大截。

这里要说清楚,半自动标注不是“全自动标注”,它的核心价值从来不是完全替代人工,而是把人工从低价值的拉框动作里解放出来,集中精力处理模型漏检、误检和边界修正。这也是它比全自动方案更稳的原因——错框有人兜底,不会直接带坏训练集。

1.2 为什么偏偏是YOLOv5而不是其他模型

做预标注,模型选型有三个硬指标:推理速度要快、丢进新数据里要能跑、生态要成熟到出了问题能找到答案。YOLOv5在这三点上一直很稳。

推理速度方面,YOLOv5s在消费级显卡上单张图片跑几十毫秒是常态,几千张图个把小时就能全部过完。相比两阶段检测器,YOLO这种一阶段架构在速度上天然占优,预标注场景是高吞吐、低延迟,它天生就是干这活的。

生态方面就更不用说了,网上关于YOLOv5的教程和工具链多到溢出来,无论是部署、导出还是踩坑,基本搜一下就有答案。而且它在公共数据集上的权重是开放的,就算不训练自己的数据,直接拿官方预训练模型跑通用检测也能凑合用。对刚起步的小团队来说,这是一条成本极低的上手路径。

值得提醒的是,现在YOLOv8也出来了,但在我做的半自动标注流程里,YOLOv5的推理接口和输出格式我已经非常熟悉,改起来顺手,稳定性也验证过很多轮,所以这篇还是以它为主线。工具是死的,流程是活的,你完全可以把脚本里的模型换掉,核心思想不变。

1.3 整条流程拆开看,就这五步

我在做预标注的时候,习惯把整个流程拆成固定的几步,每一步的输出都会落到硬盘上,这样即使中间哪一步出了问题,也不用从头再来。

第一步,准备好一定量的人工标注数据,先训练一个初版YOLOv5模型。这个初版模型不需要多强,能用就行,它的任务不是上线,而是给后面的预标注当“工人的手”。第二步,用这个模型对剩余未标注图片批量推理,输出YOLO格式的txt文件。第三步,把txt转成VOC格式的xml文件。第四步,用标注工具打开xml,人工检查和修正。第五步,把修正后的xml再转回YOLO格式,合并进训练集,进入下一轮迭代。

五是五步,听起来简单,实际操作里每一步都有不少细节。比如第二步的置信度阈值怎么设,设高了漏检多、人工补框累,设低了假框多、人工删框累。又比如第三步的坐标转换,YOLO存的是归一化的中心点和宽高,XML要的是绝对像素坐标的左上角和右下角,两边数据结构完全不一样,写脚本时一个粗心就全乱了。下面我会一个一个拆开讲。

2. 准备工作:环境、权重、推理参数

2.1 环境怎么装最快,别绕路

YOLOv5的依赖安装网上教程一堆,但很多教程喜欢让人一步一步手动装,纯属浪费时间。我一般直接用项目里的requirements.txt一把梭。装完再验证一下关键依赖的版本对齐,尤其是torch和torchvision,版本不匹配的坑后面很难排查。

git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt

装完之后做一次快速冒烟测试,跑一张测试图看看能不能正常出结果。这一步很多人会跳过,结果等到真正批量跑数据的时候才发现环境有问题,几万张图跑到一半崩了,那才叫崩溃。我个人的建议是,装完环境先拿官方权重随便跑通一次完整流程,确认没报错再进入下一步。

如果你要训练自己的数据,还需要在data目录下准备好yaml配置文件,把训练集、验证集路径和类别名写清楚。这个YAML文件的格式网上资料很多,这里就不展开说了,但有一件事必须提醒:类别顺序一旦定了,中途不要改,否则预标注生成txt的时候类别ID全乱了,后面的xml也会跟着错。

2.2 权重选哪个版本,我的取舍标准

YOLOv5官方给了n、s、m、l、x几个尺寸,不同尺寸对应不同的精度和速度。做预标注,我通常推荐从YOLOv5s起步。

s尺寸在速度和精度的平衡上最合适,入门项目用它足够了。要是你的场景里小目标特别多,比如远处的小零件、小行人,可以换成m甚至l,精度会上去一点,但推理时间也会拉长,预标注几千张图的时候时间成本会明显增加。遇到这种情况,我会先跑一批小样本对比一下效果,再决定要不要升级。

还有一个容易被忽略的选项——复用公共数据集预训练权重。拿官方在COCO上训练好的权重作为初始权重来微调,比从头训练收敛快得多。哪怕是完全不同的业务场景,COCO预训练学到的基础特征对迁移学习也有实实在在的帮助。我在几个不同的项目里都验证过,这招能省下至少四分之一的训练时间。

2.3 推理参数里最容易忽略的三个点

用YOLOv5做批量预标注时,detect.py里有几个参数直接影响标注质量,很多人第一次跑就忽略掉它们。

第一个是--conf-thres,置信度阈值。这个参数决定模型输出哪些框,太低会漏标,太高会错标。我建议先按默认值0.25跑一两百张图,人工抽查一下效果再微调。第二个是--iou-thres,NMS的IoU阈值,默认0.45基本不用动,除非目标特别密集,否则不需要调。

第三个是最容易被忽略的--save-txt,这个参数不打开的话,模型只会保存带框的图片,不会输出txt标注文件。预标注全流程都依赖txt文件,这个开关漏了,后面全白跑。我第一回跑批量预标注的时候就忘了开,等图片全部跑完才发现没有标注文件,只能重来。

3. 核心环节:用模型批量生成txt预标注

3.1 推理脚本的改造思路

直接用官方detect.py跑预标注其实没问题,只是它的输出目录结构是按exp、exp2这种递增的命名来的,跑多了目录又乱又难找。而且它默认会把每个类的label文件合在一起,存成和图片同名但后缀为txt的文件,这个倒是我们想要的。

我习惯在detect.py的基础上做一点小改造:固定输出目录名为runs/detect/prelabel_xxx,把--save-txt和--save-conf都打开。save-conf会把置信度也写进txt,这个信息后面导入标注工具时可以用来排序,先处理置信度低的框,人工校验效率会高很多。

还有个细节是--project和--name这两个参数,指定了就再也不会出现exp、exp2这种乱糟糟的目录了。脚本跑完,每个图片对应的txt文件会生成在相同的子目录结构里,和图片一一对应,后面转xml的时候遍历起来非常方便。

3.2 conf阈值到底怎么设,给出取值范围

置信度阈值的选择,本质是在“漏检”和“误检”之间找平衡。我做了好几轮实验,发现一个规律:预标注场景下,置信度阈值一般设在0.2到0.3之间,比上线部署时的阈值要低不少。

为什么?因为预标注后面还有人工审核这道工序,误检的框删掉也就一秒的事,但如果漏检了,人工就得重新画框,那才是真正耗时间的操作。所以我会故意把阈值压低一点,让模型多给一些“候选答案”,宁可人工删错,也不要人工补框。

具体数值上,我用的是0.2。如果你用的是YOLOv5m或更大的模型,可以适当提到0.3,因为大模型的输出通常更干净,误检率更低。当然这个数字不是死的,每换一批新数据,我都建议先跑一百张图人工看一下效果再定,别一上来就全量跑。

3.3 批量跑图时的目录与命名规范

批量处理大量图片之前,先把图片的命名和目录结构理顺,这是我从教训里学来的。最合理的做法是:把所有待标注图片统一放在一个文件夹,命名规范统一,比如按照DSC_0001.jpg、DSC_0002.jpg这种规律递增的格式。千万不要把不同批次、不同来源的图片混在一个文件夹里,不然后面xml和图片对不上,排查起来想哭。

图片和txt文件的对应关系也是一个大坑。YOLOv5保存txt文件时,会保持和输入图片相同的文件名前缀。比如DSC_0001.jpg对应DSC_0001.txt。如果图片名有重复或者特殊字符,txt和图片就容易错位。我遇到过一轮,图片名里带空格,结果转换脚本遍历的时候全乱了。从那以后我定了个规矩:进标注流程的图片,名字里一律不允许有空格、中文和特殊符号。

4. 硬核转换:txt转xml的完整实现

4.1 YOLO和VOC的坐标系到底差在哪

这一步是整个流程里最核心也最容易出错的,我们必须先把两种格式的数据结构理解清楚。

YOLO的txt文件,每一行代表一个目标框,有五个数值,格式是:class_id x_center y_center width height。注意,这五个数里,除了class_id是整数,后面四个全是归一化的小数,范围在0到1之间。比如0 0.5123 0.4421 0.1234 0.2345,表示类别0的框,中心点在图片水平方向的51.23%、垂直方向的44.21%处,框宽度占图片宽度的12.34%,高度占图片高度的23.45%。

VOC的xml文件就不一样了,它要求的是绝对像素坐标。框的描述是用左上角xmin, ymin和右下角xmax, ymax来确定的。所以从txt转成xml,本质上要做两件事:第一,把归一化的中心点坐标和宽高还原成像素值;第二,把中心点加宽高的表示法,换算成左上角和右下角。

换算公式是固定的,我来写一遍:

x_center_pixel = x_center * image_width y_center_pixel = y_center * image_height width_pixel = width * image_width height_pixel = height * image_height xmin = int(x_center_pixel - width_pixel / 2) ymin = int(y_center_pixel - height_pixel / 2) xmax = int(x_center_pixel + width_pixel / 2) ymax = int(y_center_pixel + height_pixel / 2)

这套公式就是txt转xml的灵魂,理解了它,脚本写起来就顺手了。

4.2 一个能直接用的txt转xml脚本

下面这个脚本是我自己一直在用的,支持指定图片目录、txt目录和输出xml目录,批量转换。用了中文注释,方便理解。我把图片尺寸读取放在了脚本里,用PIL获取每张图片的宽和高,这样就不需要额外维护尺寸表了。

import os from PIL import Image import xml.etree.ElementTree as ET def yolo_txt_to_voc_xml(txt_path, img_path, xml_save_path, class_list): """ txt_path: YOLO格式的txt文件路径 img_path: 对应图片文件路径 xml_save_path: 输出xml文件保存路径 class_list: 类别名称列表,索引对应class_id """ img = Image.open(img_path) img_width, img_height = img.size root = ET.Element("annotation") filename_tag = ET.SubElement(root, "filename") filename_tag.text = os.path.basename(img_path) path_tag = ET.SubElement(root, "path") path_tag.text = img_path size_tag = ET.SubElement(root, "size") width_tag = ET.SubElement(size_tag, "width") width_tag.text = str(img_width) height_tag = ET.SubElement(size_tag, "height") height_tag.text = str(img_height) depth_tag = ET.SubElement(size_tag, "depth") depth_tag.text = "3" with open(txt_path, "r") as f: lines = f.readlines() for line in lines: line = line.strip() if not line: continue parts = line.split() class_id = int(parts[0]) x_center = float(parts[1]) y_center = float(parts[2]) box_width = float(parts[3]) box_height = float(parts[4]) xmin = int(x_center * img_width - box_width * img_width / 2) ymin = int(y_center * img_height - box_height * img_height / 2) xmax = int(x_center * img_width + box_width * img_width / 2) ymax = int(y_center * img_height + box_height * img_height / 2) xmin = max(0, xmin) ymin = max(0, ymin) xmax = min(img_width, xmax) ymax = min(img_height, ymax) object_tag = ET.SubElement(root, "object") name_tag = ET.SubElement(object_tag, "name") name_tag.text = class_list[class_id] pose_tag = ET.SubElement(object_tag, "pose") pose_tag.text = "Unspecified" truncated_tag = ET.SubElement(object_tag, "truncated") truncated_tag.text = "0" difficult_tag = ET.SubElement(object_tag, "difficult") difficult_tag.text = "0" bndbox_tag = ET.SubElement(object_tag, "bndbox") xmin_tag = ET.SubElement(bndbox_tag, "xmin") xmin_tag.text = str(xmin) ymin_tag = ET.SubElement(bndbox_tag, "ymin") ymin_tag.text = str(ymin) xmax_tag = ET.SubElement(bndbox_tag, "xmax") xmax_tag.text = str(xmax) ymax_tag = ET.SubElement(bndbox_tag, "ymax") ymax_tag.text = str(ymax) tree = ET.ElementTree(root) tree.write(xml_save_path, encoding="utf-8", xml_declaration=True) if __name__ == "__main__": # 使用前改这三个路径 image_dir = "./images" label_dir = "./labels" output_dir = "./xmls" class_list = ["cat", "dog", "person"] # 改你的类别 os.makedirs(output_dir, exist_ok=True) for filename in os.listdir(image_dir): if not filename.lower().endswith((".jpg", ".jpeg", ".png")): continue basename = os.path.splitext(filename)[0] img_path = os.path.join(image_dir, filename) txt_path = os.path.join(label_dir, basename + ".txt") xml_path = os.path.join(output_dir, basename + ".xml") if not os.path.exists(txt_path): print(f"跳过 {filename},找不到对应的txt文件") continue yolo_txt_to_voc_xml(txt_path, img_path, xml_path, class_list) print("转换完成")

这个脚本核心逻辑清晰,用的时候只需要改三个路径和类别列表,别的不用动。我故意把边界做了clip处理,防止模型输出的框超出图片边界,这在边缘目标很常见,不处理的话标注工具打开会报错。

4.3 转换时最容易翻车的三个细节

第一个坑是类别列表的顺序。class_list里第0个元素对应class_id 0,第1个对应1。如果类别顺序和训练时的yaml文件不一致,转换出来的xml里标签名就会和框想表达的类别对不上。这个错误非常隐蔽,因为脚本不会报错,直到人工校对时才发现“person”的框标成了“dog”。

第二个坑是图片尺寸。xml里的width和height必须要和实际图片的尺寸一致,不然标注工具打开时框的位置会整体偏移。我的脚本直接从图片本身读取宽高,这是最保险的做法。不推荐用什么预先写好的txt说明文件,万一图片被压缩过一轮,尺寸变了,xml就全废了。

第三个坑是xml编码。有些标注工具对xml的编码格式很敏感,保存时unsigned不管,读取时就乱码。我上面脚本里用了xml_declaration=True和encoding="utf-8",就是为了避免这种编码问题。生成完xml之后,推荐用文本编辑器随机抽查几个,确认没有乱码再进入人工校对环节。

5. 人机配合:人工校验和迭代闭环

5.1 用xml之后,校验工具怎么选

txt转成xml之后,就可以交给标注工具进行人工校验了。这里有个选择题:用传统的LabelImg还是用更现代的LabelStudio。

LabelImg是老牌工具,轻量、启动快、对VOC xml支持最完整。如果你只是要打开xml,快速改几个框的位置,然后保存,那LabelImg完全够用。我最早期的项目都是用它,稳定,功能简单,没有学习成本。

LabelStudio功能更强,支持多人协同、任务分配,而且可以直接预览图片和标注的对应关系。如果你的标注量很大,需要几个标注员同时干活,用LabelStudio效率会高很多。它同样支持导入VOC xml格式。有个朋友的公司团队分工是LabelImg预校验第一遍,LabelStudio做第二遍抽检,两层防线下来数据质量很有保障。

工具选型没有唯一定论,我的建议是:“一个人改框就用LabelImg,小组协同就用LabelStudio”。

5.2 badcase落地:重新训练应该用哪些数据

人工校对完之后,会形成一批修正后的标注。这些xml文件千万不能直接扔进训练集就完事,我在项目里总结了一套“三分类回收法”。

第一类是模型完全漏检的图,也就是xml里人工补了很多框的图片。这类图是所有数据里最值钱的,它代表了模型的知识盲区,必须重点加入下一轮训练。第二类是模型框错了但没补框的图,通常是误检,这类数据用于纠正模型“过于自信”的问题,也值得加入训练。第三类是模型本身预测正确、人工没怎么改动的图,这类数据价值最低,可以只抽样加入,不必全量收进训练集,否则训练集会过于冗余。

这些都处理完之后,需要把xml再转回YOLO格式。注意,是从“修正后的xml”转回yolo格式,而不是直接用原始模型输出的txt。我之前见过有人偷懒,以为转成了xml再转回txt,和原来的一样——大错特错,人工修正的内容全丢了。所以xml转yolo这步也要写脚本,格式对称,核心是读xml的bndbox坐标,反算成归一化的x_center、y_center、width、height。自己写一遍或者从网上找个现成脚本都行,逻辑不复杂。

5.3 三轮迭代后的效率变化数据

我拿一个实际项目举例,两千张未标注图片,场景是广场上的人流检测,五个类别:行人、自行车、电动车、汽车、公交车。

第一轮用时:先花两小时用两百张人工标注训练了一个初版模型,然后用模型批量预标注剩余一千八百张图,大概用时四十分钟。人工校对花费约六个小时。总耗时约八小时四十分钟。如果纯人工标注,两千张图按每张四十秒算,大约要二十二个小时。第一轮已经省了六成时间。

第一轮修正后的数据加入训练集,训出第二轮模型,预标注效果明显变好,置信度框准确率明显提升。第二轮人工校对时间降到了四个小时以内。第三轮的时候,初版模型在常见场景下已经相当稳了,校对时间进一步降到两小时左右。

三轮下来,总人工校对时间从六小时降到两小时,整个流程越跑越快。这个趋势是半自动标注最迷人的地方——标注工作不是一次性成本,它是在给模型“喂经验”,模型越跑越聪明,人工负担越来越轻。

6. 常见问题速查与避坑清单

6.1 典型问题一网打尽

问题现象解决方案
漏掉--save-txt参数图片跑完没有txt文件推理命令务必加--save-txt
类别顺序不一致xml里标签名和框不对应转换脚本的class_list顺序必须和训练yaml完全一致
图片名带空格中文txt和xml对应错位进流程前统一重命名,只留数字字母下划线
xml打开报错或框偏移标注工具打不开文件检查xml里width/height是否和实际图片一致
转回来的txt和原txt一样人工修正没生效必须从“修正后的xml”反转,而不是直接复制原txt
置信度过低或过高漏检多或误检多先在100张图上调参数,再全量跑

6.2 调试阶段先小样,别直接全量

很多第一次做预标注的人,喜欢一上来就把几千张图全部扔进去跑。这是很危险的行为。我给自己定了一条规矩:任何新场景、新模型、新参数组合,第一轮必须先跑100张图,人工检查一遍输出质量,确认没问题再跑全量。

这100张不是随便挑的,要尽量覆盖不同光照、不同角度、不同遮挡程度的图,最好在图片集里均匀抽样。检查的时候重点看三件事:框的位置准不准、类别对不对、小目标漏检率能不能接受。评估完再决定是调整阈值、换权重,还是先用更多人工标注训练初版模型。

6.3 我在实际项目中保留的几个习惯

第一个习惯是保留所有中间产物。每轮预标注的结果txt、转换后的xml、人工修正后的xml、重新生成的训练txt,都按版本分目录存好。这样做的好处是,一旦发现某轮数据有问题,可以精准回溯到出错的那一步,而不用重新跑整个流程。

第二个习惯是给每轮预标注打一个版本标签。比如prelabel_v1_conf020、prelabel_v2_conf025,这样不但方便追溯是哪轮的标注,还能对比不同阈值下模型的输出质量,为后续调参提供参考。

第三个习惯是每周用随机抽样的方式检查标注质量。从已标注数据里抽五十张,人工重新看一遍,统计框偏移和类别错误的比率。这个检测步骤看似多花时间,实际上是给后面的模型训练省时间——数据质量差,训练出来的模型就是垃圾进垃圾出,修模型的成本永远比修数据的成本高。

结尾

做了这么多轮预标注,我最深的体会是:半自动标注的核心不在模型,而在于流程设计。模型只是工具,真正决定整个方案好不好用的,是你把置信度阈值、格式转换、人工校验、迭代回传这些环节配合得有多顺畅。txt转xml这一步看起来只是一段简单的坐标换算脚本,但它把YOLO模型的输出和标注工具的输入打通了,是这个流程里承上启下的关键一环。强烈建议第一次做的人,别急着追求全自动标注,先把带人工校验的半自动流程跑通,等你把预标注的置信度调参、badcase回收这些手感都建立了,再往自动化方向演进也不迟。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进