ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

目标检测数据集制作全流程:从采集标注到VOC/YOLO格式转换

目标检测数据集制作全流程:从采集标注到VOC/YOLO格式转换 1. 项目概述与核心思路做检测任务这些年最消磨耐心的不是调参而是做数据集。这篇文章就是把我的检测数据集制作全流程完整梳理一遍原始图像从哪来、怎么整理用什么工具标注标注结果落地成VOC、COCO、YOLO格式之后又怎么互相转换。适用对象是想自己从头造数据集或者手里已经有一批图片和标注文件、需要在不同训练框架之间搬运数据的工程师。我见过很多朋友直接从网上下载一个公开数据集扔进训练脚本就跑结果根本跑不起来。一查原因图像是JPG标注是VOC XML而训练脚本只认YOLO txt。这种问题在整个流程里太常见了。所以我会把格式互转单独拿出来拆透给出可以直接改来用的转换脚本而不是讲空泛的原理。你可以把这份流程当成一个模板拿到真实项目里按步骤执行基本能少踩一半的坑。1.1 为什么不能只靠公开数据集公开数据集适合做学术基准和算法对比但真实项目往往是定制场景。比如开关闭合检测、传送带异物检测、电力红外检测几乎不太可能从公开渠道找到带标注的完整版本。之前有人让我推荐鸟类目标检测的数据集公开的倒是有几个但类别、背景、光照跟他的监控相机完全不同直接迁移效果很差。于是自采加自标就成了唯一靠谱的路。做数据集的核心是“你要解决什么问题就围绕这个问题收集什么数据”。模型不会凭空认识世界它只能从你给的图像和标注里学规律。与其花大量时间在网上捞不相关数据不如先把需求拆解检测什么目标、什么环境、什么光照、什么遮挡情况、需要哪些类别。想清楚这些之后再去收集或拍摄数据效率会高很多。1.2 先搞懂三种格式再动手VOC、COCO、YOLO是目前最主流的三种检测标注格式。VOC格式来自Pascal VOC竞赛核心是每个图像对应一个XML文件里面记录文件名、图像尺寸以及每个目标的类别和边界框。COCO格式来自Microsoft COCO数据集所有标注集中在一个JSON文件里支持目标检测、实例分割、关键点等多种任务。YOLO格式来自Ultralytics等框架标注信息保存在每个图像对应的TXT文件中记录的是归一化后的中心点坐标和宽高。这三种格式各有各的适用场景。VOC格式直观适合手工检查COCO格式标准化程度高很多开源框架原生支持YOLO格式简洁高效是训练时最常用的输入格式。实际项目里经常需要在它们之间来回切换比如用LabelImg标注时默认导出VOC训练YOLO时需要转成YOLO格式提交评估或者做模型集成时又要转成COCO。这个切换过程看似简单里面却藏着不少细节坑后面我会一个个说清楚。2. 数据收集与规范整理先讲数据从哪来。虽然网上能找到很多现成的图像但真实场景的数据往往需要自己动手拍或者找合作方拿。这里最重要的并不是数量多而是质量可控、分布合理。2.1 公开数据集与自采数据的取舍公开数据集最大的价值是作为“预训练基座”或者“相似场景补充”。比如车辆检测BDD100K这类公开数据集里的城市道路场景非常丰富如果你的项目正好是车载摄像头视角那直接使用它的图像和标注就很有帮助。再比如通用目标检测COCO数据集提供的80个类别覆盖了大部分日常物体很多项目可以直接用COCO预训练权重做迁移学习。但如果你的场景比较特殊比如电力红外图像里的设备温度异常区域或者传送带上的异物公开数据集里基本找不到对应样本就必须自采。自采数据要注意拍摄多样性。不要只在同一天、同一个角度、同一光照下拍那样模型学到的是场景背景而不是目标本身。我一般建议至少覆盖多个时间段、多个角度、多个背景并且把相机分辨率、焦距、拍摄距离都记录下来。还有一种方式是从视频中抽帧这样能快速获得大量连续变化的样本但要注意抽帧间隔不要太短否则相邻帧高度相似会让训练集实际有效样本变少。公开数据和自采数据混合使用时要注意类别定义的统一。不同数据集的类别名称可能不一样比如“car”、“vehicle”、“automobile”其实是一个意思合并前必须先做映射表。这个映射表最好单独维护成一份CSV或JSON文件不要随手写在代码里否则后面改起来很痛苦。2.2 图像清洗与文件命名规范拿到原始图像后第一步是清洗。我习惯把图像分成三类可用、需处理、废弃。可用的是清晰、目标明确、构图正常的需处理的是模糊、过暗、过曝但还能靠简单增强补救的废弃的是严重失真、目标被完全遮挡、或者完全与项目无关的。清洗时不要心疼垃圾数据进模型只会拖累训练效果。清洗完之后要重新规范命名。很多相机拍出来的文件名是类似“IMG_20250101_123456.jpg”这种本身没问题但当你有多台设备、多个来源时文件名碰撞和混乱就开始了。我的做法是统一用“项目名_类别_日期_序号”的格式比如“switch_close_20250101_0001.jpg”。文件名里不要出现中文、空格、特殊符号尽量用英文小写和下划线。这样无论在Linux服务器还是Windows机器上处理都不会出现编码或路径问题。目录结构也要提前设计好。我通常建立三个一级目录images放原始图像annotations放标注文件splits放训练/验证/测试划分的列表。如果后续需要更新数据可以直接在images里追加新图像然后重新跑一遍划分脚本不用动其他目录。对于版本管理建议用Git做整体数据的记录但不要把动辄几十GB图像直接提交进Git仓库而是用DVC或手动归档的方式用文本文件记录文件名和哈希值方便追溯。2.3 数据增强与类别平衡数据增强不是标注环节但它直接影响标注数据的利用效率。尤其是当某个类别样本很少时增强几乎是必须的。常见的离在线增强包括随机翻转、旋转、缩放、亮度对比度调整、高斯噪声、马赛克拼接等。做检测项目时增强操作必须同步修改边界框坐标。如果只是翻转让框也跟着翻转那就需要按图像宽高做坐标变换如果做随机裁剪就要判断框是否被裁掉或者是否需要重算交点。实际增强时还要注意“增强过度”的问题。有些人喜欢把所有增强都开满结果训练出来的模型对真实场景反而不敏感。我一般遵循两个原则第一增强不能破坏目标的语义信息比如一个开关是否闭合旋转180度后可能视觉上就变了第二增强幅度要与真实部署场景一致如果你实际部署时只有白天光线就没必要大量增强成夜晚效果。先跑一轮基线再逐步增加增强强度看验证集表现这样最稳妥。类别不均衡也是常见问题。比如传送带异物检测正常物料样本可能有10000张异物样本只有200张。直接用原始分布去训练模型会倾向把所有东西都预测成正常物料。处理方法无非三种欠采样、过采样和加权损失。在数据集层面我会优先用“复制粘贴增强”和“过采样”提高少数类图像数量然后再配合损失函数的类别权重。但无论怎么处理都要单独分出一个少数类样本占比合理的验证集否则验证指标会虚高。3. 标注环节工具选型与标注规范标注工具决定了整个标注流程的效率和输出格式。很多同学上来就直接用LabelImg其实还有更合适的选择。我把常用的几个工具拉出来对比一下你可以按项目规模来决定用哪个。3.1 四款常见标注工具横评工具输出格式适合场景优缺点LabelImgVOC XML小项目、个人快速标注轻量、单机运行但功能简陋无自动保存LabelStudioCOCO、VOC、YOLO等团队协作、多种标注任务功能全面、支持在线多人标注但部署稍复杂X-AnyLabelingYOLO、VOC、COCO半自动辅助标注内置模型可以预标注能大幅减少手工操作CVATCOCO、YOLO、VOC等大规模标注团队工业级方案但需要服务器环境我自己最常用的组合是小批量数据用LabelImg大量数据用X-AnyLabeling先跑一次预标注再人工修正。X-AnyLabeling的自动标注功能在目标明确的情况下能节省大概一半时间但它依赖的预训练模型不一定认识你的类别所以只能把它当辅助工具最终判定还得靠人。如果是多人协作项目我强烈建议用LabelStudio或CVAT这类带后端管理的平台。它们能记录每个标注员的工作量还能设置审核员角色方便做二次质检。别小看这个功能团队标注时最重要的就是权限管理和过程审计否则出了问题很难追溯是谁的错误标注。3.2 标注规范从边界框到难点目标选好工具后还要定一套标注规范。所谓规范就是让不同标注员做出来的框尽可能一致否则模型会学到混乱的边界框。规范里最重要的一条是边界框要紧密贴合目标可见区域但不包含背景。如果目标整体可见就框整个目标如果目标被遮挡只框可见部分。尤其要注意同一类别的目标大小差异很大时标注员容易把框画得忽大忽小训练时难以收敛。另一个容易出错的是“目标太小是否要标注”。我一般设定一个策略目标在图像中占不超过20像素且难以辨认时可以忽略。否则标注后模型会因为学习大量小目标噪声而降低精度。但如果你做的是小目标检测专项那就要反过来把目标从多尺度聚焦出来这个策略要按项目需求提前定好。还要给每个类别定义好“判定边界”。例如“开关闭合检测”什么状态算“关”什么算“合”我的做法是写一份标注细则配上正例、反例和外语图片标注员先做一轮试标我检查一遍再正式开工。这份细则看起来繁琐但对数据集的一致性极其重要。标注细则可以是一页PDF也可以是一份Markdown文档但一定要包含示例图片和边界情况说明。3.3 质量复查不要相信第一遍标注第一遍标注之后一定要做质量复查。我自己通常是“三遍法”第一遍是标注员自查第二遍是另一个人交叉抽检第三遍是脚本自动校验检查格式、边界框是否越界、类别是否在预定列表内。交叉抽检时至少要覆盖所有类别不能只抽图像数量因为有些类别样本本来就少如果漏检就基本没救了。脚本自动校验非常必要哪怕简单到只有几行代码。比如检查XML或TXT中的坐标是否大于0、是否小于图像宽高、类别ID是否在范围内。还有一个常被忽略的检查图像和标注文件的数量必须对应而且文件名一一匹配。我就遇到过训练时不断报错最后发现是多了一堆无标注图片导致dataloader不知道如何处理。后面我会专门给出统计脚本这类问题十分钟就能查完。4. VOC、COCO、YOLO 格式互转全解4.1 三种格式的数据结构对比先说清三种格式的真实结构这也是后续写转换脚本的基础。VOC格式是每个图像对应一个XML文件典型结构如下annotation folderimages/folder filename001.jpg/filename size width640/width height480/height depth3/depth /size object namecat/name bndbox xmin100/xmin ymin80/ymin xmax200/xmax ymax300/ymax /bndbox /object /annotationCOCO格式是把整个数据集打包成一个JSON文件包含images、annotations、categories三个主要数组。images数组记录每张图片的id、文件名、宽高categories数组记录类别id到名称的映射annotations数组记录每个标注框所属的image_id、category_id以及bboxbbox格式是[x_min, y_min, width, height]。YOLO格式最简单每张图像对应一个TXT文件每一行代表一个目标格式是class_id x_center y_center width height坐标全部归一化到[0,1]区间。注意这里的宽高是目标框实际宽度和高度分别除以图像宽度、高度而不是除以同一个数。很多人在这里经常搞错。看这些结构会发现转换过程其实就是在不同的坐标表达方式之间切换同时把类别信息在名称和索引之间互相映射。核心逻辑并不复杂就是细节太多。4.2 坐标转换公式与核心脚本从VOC到YOLO的转换是实战中最常用的公式如下x_center (xmin (xmax - xmin) / 2) / image_width y_center (ymin (ymax - ymin) / 2) / image_height bbox_width (xmax - xmin) / image_width bbox_height (ymax - ymin) / image_height其实这里的x_center也可以写成(xmin xmax) / 2 / image_width但为了减少理解成本我建议统一用上面的写法。从YOLO转回VOC的逆向公式也很直接xmin (x_center - bbox_width / 2) * image_width xmax (x_center bbox_width / 2) * image_width ymin (y_center - bbox_height / 2) * image_height ymax (y_center bbox_height / 2) * image_height从VOC转COCO只需要把xmin, ymin, xmax, ymax换算成x_min, y_min, width, heightCOCO的bbox不需要归一化直接用像素值即可。此外COCO的坐标是浮点数但很多框架会要求整数实际写脚本时可以保留一位小数避免反复取整造成误差累积。下面放一个从VOC转YOLO的Python脚本去掉注释也就三十行不到。你需要先准备一个类名列表文件classes.txt顺序要和最终训练时保持一致。import os import xml.etree.ElementTree as ET from tqdm import tqdm def convert_voc_to_yolo(xml_dir, txt_dir, classes_file): os.makedirs(txt_dir, exist_okTrue) with open(classes_file) as f: classes [line.strip() for line in f.readlines()] class_to_id {name: idx for idx, name in enumerate(classes)} for xml_file in tqdm(os.listdir(xml_dir)): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) base os.path.splitext(xml_file)[0] out_txt os.path.join(txt_dir, base .txt) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_to_id: continue bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 裁剪防止越界 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_to_id[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt, w) as f: f.write(\n.join(lines)) # 用法 # convert_voc_to_yolo(annotations/voc, annotations/yolo, classes.txt)这段代码里我加了坐标裁剪因为有些标注框偶尔会超出图像边界保留原始值会导致训练时框的坐标出现大于1的情况最终让模型输出不稳定。写转换脚本时一定要把这种边界情况都处理掉。YOLO转VOC、COCO转YOLO的逻辑与此类似只是解析JSON或者反向计算篇幅原因就不全部贴出来了。4.3 转换时最容易翻车的几个细节转换时最隐蔽的问题是类别索引不一致。比如你训练时用的类别顺序是“background, cat, dog”但是转换脚本读的classes.txt却是“cat, dog”两者一错位模型就把猫学成了狗。我建议在项目根目录维护一份固定的classes.txt标注工具、转换脚本、训练配置都从这份文件读取绝对不要在不同地方手动写类别列表。第二个翻车点是图像尺寸不一致。VOC XML中的size会和实际图片尺寸不一致比如你标注时用的是缩小预览图XML记录的是原始图尺寸但训练时你读入了未缩放的图坐标就全部偏移了。解决办法是转换前先写一个异常检测脚本用PIL读取每张图的真实宽高和XML里的记录做对比不一致就输出警告。第三个细节是IO中的路径分隔符。Windows路径用反斜杠Linux用正斜杠标注工具保存的绝对路径经常是错的。有一个最简单的方式在转换脚本里只保留文件名不要用XML里的path或filename标签的完整路径而是用图像文件列表为主索引。这样无论项目搬到哪台机器只要images目录文件名不变都不会出错。转换后的验证也很关键。我的习惯是转换脚本跑完后随机抽几十张图像把转换后的坐标画在图上看看边界框是否贴合目标。这一步不能省因为人工检查和自动统计的关注点完全不同。自动统计只关注数值范围而人工检查能看出坐标偏移、框选错等肉眼可见的问题。5. 实操案例从图片目录到可直接训练的数据集理论讲了一堆还是要串起一个案例。我就用“开关闭合检测”这个场景做演示目标是判断开关处于闭合还是断开状态并输出边界框。这里只展示流程骨架类别和路径替换成你自己的项目就行。5.1 LabelImg 导出 VOC 的完整流程安装LabelImg很简单Python环境下直接执行pip install labelImg labelImg打开LabelImg后左侧先“Open Dir”选择图像目录右侧“Change Save Dir”选择标注输出目录。在“PascalVOC”模式下标注框会自动保存成XML。快捷键不多但有三个必须记住W画框、D下一张、CtrlS保存。很多人用LabelImg忘记保存结果标注了半天切换图像后全部丢失这是最常见的“无效劳动”。我的建议是每标完一张图立刻按CtrlS保存然后快速切换下一张。每完成20张左右手动去保存目录检查一下文件数量和内容。如果发现某张图里没有目标LabelImg仍然会生成XML这时候需要确认该XML是不是空标注有些训练框架会忽略空标注但有些会报错。空标注的处理方式也需要提前约定是保留还是删除看你的数据需求。标注过程中如果发现需要调整某个框可以用Ctrl鼠标滚轮缩放图像再按住框的边角拖拽。对于特别密集的目标我建议把图像放大到200%再标避免误标。标注完成后所有XML文件收进annotations/voc目录这就是VOC格式的数据集。5.2 Python 脚本转 YOLO 并划分训练验证集拿到了VOC XML后先用前面给的convert_voc_to_yolo函数转成YOLO格式。转换前先把classes.txt定义好比如switch_open switch_closed然后执行转换输出目录是annotations/yolo。接下来要做的是训练集和验证集划分。直接随机划分有可能导致某一类别的目标只出现在训练集或验证集中所以我习惯先按类别做分层采样。简单做法是用Python的train_test_split加stratify参数但这里要注意stratify的输入是图片的类别标签向量。如果一张图包含多个类别就需要把多标签转成一个组合标签比如“switch_openswitch_closed”。划分完成后YOLO框架通常还需要一个train.txt和val.txt每一行是图像文件的绝对路径。另外很多框架会约定图像和标注文件目录的组织方式比如images/train、labels/train所以我喜欢在划分时直接按图像文件复制到对应目录避免每次训练都去解析路径列表。当然如果要节省存储空间用路径列表也可以这个看项目的部署环境。5.3 转换后的可视化验证拿到YOLO标注后第一步就是用脚本把标注画回图。我用OpenCV简单写了一个调试脚本import cv2 import os def draw_yolo_boxes(image_path, label_path, classes): img cv2.imread(image_path) h, w img.shape[:2] with open(label_path) as f: lines f.readlines() for line in lines: parts line.strip().split() class_id int(parts[0]) x_c float(parts[1]) * w y_c float(parts[2]) * h box_w float(parts[3]) * w box_h float(parts[4]) * h x_min int(x_c - box_w / 2) y_min int(y_c - box_h / 2) x_max int(x_c box_w / 2) y_max int(y_c box_h / 2) color (0, 255, 0) cv2.rectangle(img, (x_min, y_min), (x_max, y_max), color, 2) cv2.putText(img, classes[class_id], (x_min, max(0, y_min - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) cv2.imshow(check, img) cv2.waitKey(0) cv2.destroyAllWindows()这个脚本会在屏幕上逐张展示画好框的图你按任意键切到下一张。手动看几十张比单纯的统计更直观。看到框明显随图像尺寸比例不协调时多半是转换公式里有问题要及时回头检查。6. 常见问题与排查技巧实录整个流程走下来总会在某个环节卡住。我把这几年实际遇到的问题整理成一份速查表按症状、原因、解决办法列出来。6.1 典型问题loss为NaN、类别错乱、框出界训练时loss变成NaN很多人以为是学习率太大其实数据集也有嫌疑。最常见的诱因是标注文件里有坐标超过图像边界或者有宽高为0的空框。这些异常值会让损失计算出现除以零或者log(0)的情况。排查方法很简单写一个脚本扫描所有TXT文件输出所有异常行重点关注x_center width/2 1或者width 0的记录。我在前面的转换脚本里已经做了裁剪但如果你拿到的是外部数据还是要自己再跑一遍检查。类别错乱往往不是训练问题而是类别列表顺序对不上。比如有人把classes.txt从[switch_open, switch_closed]改成了[switch_closed, switch_open]但没有重新转换标注文件。于是所有标注框的ID还是旧顺序模型学习时完全错乱。解决办法是重新转换所有标注或者在脚本中自动根据类别名转成ID而不是手动改。框出界的问题通常出现在“图像resize”前后。你训练时如果输入尺寸是640×640而标注坐标基于原始1920×1080的图没有做归一化或者没做变换框自然就飞了。一定要确保从数据读取到模型输入的整个pipeline里坐标始终跟随图像尺寸同步缩放。以YOLO格式的归一化坐标为基础在图像resize时只需要等比例缩放原图如果使用letterbox填充则还需要计算padding偏移这个细节最具迷惑性。6.2 用统计脚本快速发现标注异常除了可视化抽查我还习惯用统计脚本给数据集做“透视”。统计内容一般是四类每个类别的目标数量、每张图像的标注框数量分布、目标框宽高分布、类别共现矩阵。这些统计能快速暴露很多问题。比如某个类别目标数量明显偏少那就要考虑是否需要补充数据如果一张图里有几十个框但大部分图像只有一到两个框那模型的尺度适应性就会很差如果框宽高分布极端比如全是特别窄的长条那可能你的目标本身是细长的或者标注过程中出现了系统性的画框偏差。下面给一个简单的类别数量统计脚本用它可以快速生成分布报告from collections import Counter def count_labels(label_dir, classes): counts Counter() total_boxes 0 for txt_file in os.listdir(label_dir): with open(os.path.join(label_dir, txt_file)) as f: for line in f: class_id int(line.strip().split()[0]) counts[classes[class_id]] 1 total_boxes 1 print(Total boxes:, total_boxes) for name in classes: print(f{name}: {counts.get(name, 0)})这个脚本还能扩展成统计空标注文件的数量因为空标注文件意味着这张图没有目标如果你的任务是目标检测空图太多会影响评估逻辑。如果一张图没有目标最好单独放一个empty目录不要混在训练集里无限循环。6.3 我的几条指标级经验最后聊点实际体会。第一做数据集不要一次性投入全部时间标注万张图。我的习惯是先标注一百张跑通整个流程从原始图像到训练脚本全部验证没问题再大规模标注。如果流程有问题一百张图改起来很快等一万张图做完再发现问题就晚了。第二用版本控制管理数据集配置。很多人只把代码放Git数据集标注文件不放结果改了几次类别顺序后完全找不到当初用的哪一版。我的做法是把类别列表、标注细则、转换脚本以及每次数据集的变更说明都推进Git图像文件用外部存储或DVC管理。这样即使过了半年也能准确复现当时的训练数据集。第三小技巧做完格式转换后用一个很小的模型跑几分钟训练看loss曲线是否能降下去。这一步能快速验证数据格式和训练Pipeline是否衔接完美。不要直接用完整大数据集训练不然跑了一天后才发现格式错误浪费算力事小影响项目节奏事大。拿我自己来说光是因为类别顺序不一致导致重新转换标注就有过至少三次。后来我干脆把“数据集校验脚本”放到训练流程的入口处每次训练前自动跑一遍任何异常先报警再决定是否继续。这个习惯帮我省下了大量无意义的调试时间。如果你也想做好检测数据集最值得花时间的三个点就是采集时保证多样性标注时保证一致性转换后保证同步验证。这三点只要做到位后面训练模型基本就是水到渠成的事。
返回列表