资讯动态

目标检测数据集制作全流程:收集、标注与VOC/COCO/YOLO格式互转

发布时间:2026/10/1 18:22:30 来源:尧图企业网站定制
做检测项目第一课不是搭模型而是先搞出一份能喂给模型的数据集。标题里这串流程——收集、标注、VOC、COCO、YOLO 格式互转——我做了不下十次踩过的坑比掉的头发还多。每次帮别人看训练失败八成问题不是网络结构而是数据集格式不对、标签漏标、坐标越界这种基本功问题。这篇就把我实际干活的全流程写下来从数据怎么攒到三种格式怎么互转一次讲清楚适合正在做目标检测毕业设计、工业项目、竞赛任务或者刚进算法岗需要自建数据集的同学。我的经验是这份工作看起来是“体力活”实际上最耗心力的地方反而是那些“看起来很简单”的细节。比如 YOLO 的归一化坐标到底是除 width 还是除以 img_widthCOCO 的 bbox 从哪一点开始算VOC 里的 difficult 字段要不要保留任何一个环节搞错轻则训练指标上不去重则整个数据集得推倒重标。下面按实际制作顺序来聊。1. 数据收集与清洗好数据集是规划出来的不是碰巧攒出来的很多人拿到任务第一反应是“先爬图”然后怀着“图越多越好”的心态随便攒了几千张图扔进标注工具。这样往往后期要返工。真正的第一步是花至少半天时间把数据方案想清楚。1.1 先想清楚场景和样本分布再开始找图收集之前先问自己几个问题目标在画面里占多大比例光线是室内还是室外有没有遮挡和截断相机视角固定还是移动以热词里出现的几类常见数据集为例鸟类目标检测鸟占画面比例通常很小背景是天空、树冠姿态变化大容易和小块杂物混在一起。收集时应该重点补充小目标样本。开关闭合检测固定机位、固定角度目标是开关面板上的小型元件难点在于反光、阴影和相似物区分。这种场景下场景多样性不用太高但类内细节差异必须标细。传送带异物检测动态模糊、灰尘遮挡、物品堆叠是常态收集时必须包含大量“正常物料”作为干扰背景否则模型会把传送带本身也当异物。每类目标建议最少准备 300 到 500 个实例。注意是“实例数”不是“图片数”。一张图里如果有 10 个开关那这张图就贡献了 10 个实例。实例太少时模型很容易过拟合验证集的 mAP 忽高忽低看起来“训练正常”实则没学到东西。数据来源无外乎三种自己拍/录视频抽帧、公开数据集筛选、互联网收集。自己拍最可控抽帧时最好隔几帧抽一次避免连续帧太相似。公开数据集比如热词里提到的车辆检测 BDD100K、遥感图像数据集可以直接拿一部分用但要注意原数据集自身的标注范围和类别划分是否符合你的需求。互联网收集则必须人工过一遍清晰度、水印、非目标内容都要清掉。1.2 数据清洗去重、去坏图、查版权攒完图之后做三件事第一是去重。大量相似场景会导致训练集和验证集高度重合mAP 虚高部署时立刻现原形。我一般用图片哈希或者感知哈希工具扫一遍阈值调高一点宁可误删不可放过。第二是去坏图。用代码批量检查文件是否损坏、分辨率是否过小、是否全黑或全白。顺手写一个 Python 脚本读取图片宽高和通道数过滤掉宽高小于 320 的图。小图放大后的标注框误差极大训练时也是负样本来源。第三是版权和隐私。从网上拿的图尽量选开源协议或者明确允许研究使用的数据集涉及人脸、车牌、人物这些信息时提前做模糊或者直接弃用。数据合规这件事不是“流程问题”是底线问题别等上线了再处理。清洗完的数据集中至少留出 10% 的图做验证集10% 做测试集。划分之前先充分打乱避免同一个场景的连续帧同时出现在训练集和验证集里。我在实际项目中遇到过最坑的事情就是验证集全是“晴天、正面、无遮挡”测试集全是“阴天、侧面、强遮挡”导致测试集 mAP 比验证集低了十几个点。2. 标注规范与工具选择一半的时间省在“标准”上标注这件事最反直觉的地方在于——花时间定标准反而能更早完成标注。没标准直接开干每个人对“框到哪”的理解都不一样后期统一意见的成本高得多。2.1 标签定义和边界规则要提前写下来类别编号顺序会直接影响 YOLO 的 class_id。比如你要做鸟和开关两个类建议从一开始就固定0 代表 bird1 代表 switch。中间不要留空项。这个顺序最好写进一个 categories.txt之后所有脚本都读这个文件不要手动改数字。边界规则必须明确尤其是这几个目标被遮挡超过一半要框完全身还是只框可见部分通常建议框可见部分并单独标记截断。目标在画面边缘保留完整目标还是裁掉按照 VOC 的习惯是保留整个目标哪怕超出边界也要把坐标算出来超出部分在转换时再 clip。目标极小连 10x10 像素都不到的目标要不要标我的看法是如果是主要检测对象就必须标如果只是背景里的零星目标可以不标。最怕的是时标时不标模型会迷。紧挨着的同类目标比如开关面板上挤在一起的几个开关必须严格沿着边缘分开宁可留 1 像素间隙也不能让两个框重叠。重叠的 GT 会让模型在 NMS 阶段无所适从。这些规范写成一个一页纸文档让参与标注的人先读再干。每标完 50 张图抽查一次看看有没有漏标、串框。2.2 标注工具选型单机用 LabelImg团队协作上 CVAT标框工具选型不用搞太复杂个人小项目或者临时验证LabelImg 最轻量Windows 上 pip install 或者直接下打包版就能跑支持 VOC 格式导出也能输出 YOLO 格式。缺点是多人协作基本靠文件传来传去不适合大项目。团队中大型项目强烈推荐 CVAT。它可以部署在服务器上多人同时标注内置自动保存、任务分配、质量审计还能导出 COCO、VOC、YOLO 多种格式。浮漂社区对它的支持也很多遇到问题几乎都能搜到。遥感大图和点云标注热词里提到的遥感图像标注相对特殊图像尺寸大动辄上万像素需要分瓦片标注。CVAT 有专门的遥感任务模式或者用 X-View 这类针对遥感设计的工具。我自己的习惯是无论最后要训练哪个框架标注阶段统一用 VOC 格式或者原始多边形格式存。VOC 基于单个 XML 文件结构清晰转换脚本到处都是出了问题也最容易排查。等标注全部验收完成再统一转成 COCO 或 YOLO 给训练用不要边标边转。2.3 标注实操中的细节习惯标注不是光点四个点就完了。实践中我总结出几条习惯第一紧贴目标边缘但不要截掉目标的一部分。第二一个实例一个框重叠的物体就差用高倍率抠图也不要做“一个框框两个物体”。第三标注完成后必须做二次核验至少要抽样看 20% 的图重点检查是否有小目标漏标。还要注意工具里的“半自动标注”功能。CVAT 有插值跟踪视频抽帧后可以做自动补全但自动框经常偏移或者漏掉小目标必须逐帧检查。我见过很多同学自动标注后完全不看结果训练时大量标签错位模型学出来的特征全是乱的。3. 三种格式背后VOC、COCO、YOLO 到底在存什么格式互转翻车翻的往往不是“转换代码”本身而是“不知道源格式每个字段到底表示什么”。这里把三种格式彻底讲透。3.1 VOC 格式图像检测的“老教材”VOC 格式源自 PASCAL VOC 挑战赛现在依然是最通用的中间格式。它的核心是每个图片对应一个同名的 XML 文件。结构大致是annotation根标签下包含了folder、filename、source、sizewidth、height、depth和object。每个object里有name类别名、pose、truncated、difficult以及bndbox里的xmin、ymin、xmax、ymax。最容易被忽略的是truncated和difficult两个字段。difficult表示这个目标本身就很难识别比如极小、极度模糊训练时通常会被忽略。如果转换脚本没处理difficult1的框它会进入训练集但又因为边界不清晰干扰模型。我在转 COCO 和 YOLO 时都会把difficult1的框单列出来或者直接跳过后面写脚本时也会专门做这个过滤。3.2 COCO 格式一切皆 JSONCOCO 是目前科研界使用最广的格式目标检测、实例分割、姿态估计都共用这一套 JSON 结构。整个文件是一棵嵌套的字典树顶层有三个大字段和一个可选字段images数组每个元素包含id、file_name、width、height、license、date_captured。annotations数组每个元素是id、image_id、category_id、bbox、area、segmentation、iscrowd。categories数组每个元素是id、name、supercategory。info可选通常是数据集描述和版本号。COCO 的bbox是直接像素坐标格式为[x, y, width, height]x和y是框左上角的绝对坐标不是中心点。area对于目标检测来说就是width * height如果是从多边形标注转过来的则应该用多边形面积而不是纯框面积。iscrowd默认填 0表示这个框是否包含密集人群/一组目标。COCO 格式最烦人的地方是它的id体系。image_id必须对应images里的idcategory_id必须对应categories里的id而且categories的id不一定是连续的可以自定义。写转换脚本时最容易错的就是这里——对着图片名去找 id结果图片名对不上。3.3 YOLO 格式简单到极致但也最容易错YOLO 的标注是扁平的一张图对应一个 txt 文件文件名和图片名相同txt 里每行描述一个目标。每行格式是class_id x_center y_center width height这五个值全部用归一化坐标即除以图片的宽或高。注意它既不是左上角坐标也不是未归一化坐标。举例来说一张 1280x720 的图目标左上角是 (320, 180)宽高是 (640, 360)那么x_center (320 640 / 2) / 1280 0.375y_center (180 360 / 2) / 720 0.375width 640 / 1280 0.5height 360 / 720 0.5所以一行是0 0.375 0.375 0.5 0.5类别为 0。YOLO 的核心坑有三个。第一是归一化很多人把 x_center 和 y_center 写成了像素值训练时 loss 直接爆掉或者框全部跑到角落。第二是 width 和 height 也要归一化这一步经常被遗漏。第三是类别索引必须以 darknet 的obj.names文件里的顺序为准和你标注时的序号必须完全一致。此外训练时还需要一个train.txt列出所有图片路径一个obj.data配置数据参数这些文件虽然不算标注但也是数据集的一部分容易因为路径不对而抓狂。4. 格式互转一次搞对坐标变换是关键坑全在这里VOC 到 YOLO、COCO 到 YOLO、COCO 到 VOC本质上处理的只有两种坐标形态直接像素坐标和归一化坐标。把这个搞明白转换脚本自己都能写。4.1 坐标形态和转换公式VOC 的bndbox绝对像素坐标左上角与右下角xmin, ymin, xmax, ymax。COCO 的bbox绝对像素坐标左上角与宽高x, y, w, h。YOLO 的每行反正都归一化需要的是中心点坐标与宽高cx, cy, w, h全部除以原图宽高。从 VOC 到 COCOx xmin y ymin w xmax - xmin h ymax - ymin从 VOC 到 YOLOx_center (xmin xmax) / 2 / img_width y_center (ymin ymax) / 2 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height从 COCO 到 YOLOx_center (x w / 2) / img_width y_center (y h / 2) / img_height width w / img_width height h / img_height反过来从 YOLO 到 COCO 就解方程x (x_center - width / 2) * img_width y (y_center - height / 2) * img_height w width * img_width h height * img_height所有转换公式就是这个程度。但实际项目中我见过的错误有不同图片分辨率不同转换时忘记读取每张图的宽高用的是数据集统一分辨率导致小尺寸图坐标偏得离谱。相减得到负宽高说明 VOC 标签里 xmax xmin 或者 ymax ymin这种标签要提前过滤或者修正。归一化后出现大于 1 或小于 0 的值说明目标本身伸出了图片边界最好单独统计出来人工决定 clip 还是弃框。4.2 我自己常用的批量转换脚本Python下面这段脚本实现 VOC 到 YOLO 的批量转换。核心思路是遍历某个文件夹里所有 XML逐个解析出 size 和 bndbox套公式转换写同名 txt。import os import xml.etree.ElementTree as ET voc_dir path/to/Annotations yolo_dir path/to/labels os.makedirs(yolo_dir, exist_okTrue) classes [bird, switch] # 必须和训练用的 obj.names 严格一致 def voc_to_yolo(xml_path, yolo_path): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: continue # 过滤掉困难样本 class_name obj.find(name).text if class_name not in classes: continue class_id classes.index(class_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) # 防止非法框 if xmax xmin or ymax ymin: print(f跳过非法框: {xml_path}, {class_name}) continue x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 归一化裁剪到 [0,1] 之间防止越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(yolo_path, w) as f: f.write(\n.join(lines)) for xml_file in os.listdir(voc_dir): if not xml_file.endswith(.xml): continue name os.path.splitext(xml_file)[0] voc_to_yolo( os.path.join(voc_dir, xml_file), os.path.join(yolo_dir, name .txt) ) print(转换完成)脚本里我特意做了三件事过滤 difficult1 的框、跳过非法框、把归一化坐标 clip 到 0 到 1 之间。前两个保证不让脏数据进训练第三个防止点超出图像边界导致 OpenCV 画框时越界或者检测层计算异常。这套逻辑同样适用于 COCO 转 YOLO只是换成了用 json 解析 fields。转换完成后一定要做反方向验证。我自己习惯写一个“画框可视化”脚本用 OpenCV 读取图片和转换后的 YOLO txt把框画出来看一遍重点看几个困难场景小目标、密集目标、边缘目标。这一步虽然花时间但比训练完发现框不对再回来排查快得多。4.3 工具与现有开源脚本的“拿来主义”如果不想自己写脚本可以用 GitHub 上的开源转换工具。我试过的有 labelme2coco、voc2yolo、coco2voc 等大部分能用。但用第三方脚本有三个检查点看它是否读取了每张图的原始尺寸而不是用固定尺寸。看它是否处理了 category 顺序最好直接用你的 categories.txt 做映射。看它是否保留了 difficult / iscrowd / segmentation 这些非核心信息。有一次我用一个 coco2voc 脚本它给 segmentation 字段的空列表直接跳过结果检测框全丢了。后面我学乖了先小批量转 20 张图验证再全量转。永远不要上来就转全量除非你想体验数据全部重来的酸爽。5. 数据集完成后训练前的最后检查清单数据集做完了格式对了不等于万事大吉。训练前用几分钟跑一遍检查能省下后面好几天的排障时间。5.1 五步快速自检第一步统计标签文件数量和图片数量是否一致有没有图片没有对应标签文件有没有标签文件对应的图片不存在。第二步统计每个类别的目标数量。YOLO 里可以直接解析所有 txt统计每行第一列的数字分布。如果某一类只有十来个实例训练基本不会收敛得很好要么补数据要么考虑类别权重。第三步检查标签文件里的归一化坐标是否有大于 1 或者小于 0 的异常值。上文已知这是越界的典型标志。第四步检查数据集划分。建议按照 8:1:1 或者 7:2:1 划分训练、验证、测试测试集应该是最接近真实部署场景的那部分数据而不是随便从所有数据里抽样。划分结果落盘后要在三个目录下分别保留一份避免训练时重复使用测试集调参。第五步验证集里至少挑 20 张图人工过一遍标签是否和视觉内容一致。这个“最后一眼”能挡住大量标注时漏下的低级错误。5.2 YOLO 训练阶段几个和数据集相关的常见坑热词里提到了 YOLO 训练中 BN 崩溃、损失函数、混淆矩阵这些概念。这些问题的根子往往也是数据集的质量问题。BN 崩溃比如 loss 变成 NaN很多时候是输入图片里有过多异常像素或者标签里出现了空文件。空文件对模型来说是一个“无目标”的负样本但如果一个标称有目标的图只有空标签文件模型会学到“这张图没目标”产生严重矛盾。检查时把空标签文件全部列出来逐个确认是不是漏标如果确实无目标建议直接从训练集剔除。混淆矩阵总和不是唯一看起来很神秘其实是因为数据集存在类别不平衡或者同一目标被重复标注。这两个问题一个靠补数据解决一个靠标注阶段严格保证“一实例一框”解决。预训练权重下载后第一次跑训练loss 第一天降得不错第二天反而反弹这种情况十有八九是验证集和训练集混了图或者验证集划分没打乱导致数据分布不收敛。建议从划分脚本开始重新走一遍。5.3 一个部署侧的提醒热词里还有类似“T4 1080P 25 帧每秒用 TensorRT YOLO 640 分辨率可以支持多少路”的疑问。严格说这属于部署性能问题但和数据集也有关系输入分辨率 640 意味着你的训练数据应该以 640 附近的尺度为主。如果训练好的模型部署时输入 640而你标注的图片尺寸五花八门模型的尺寸泛化能力就会削弱。统一训练尺度也是数据集准备的一部分整理数据时顺手做一次 resize 到统一尺度后面部署的坑会少很多。6. 常见问题速查这些毛病我全都遇到过下面整理了一个现实中高频出现的排查表都是我或者身边同事真实踩过的坑。症状一转出来的 YOLO 框全偏了但坐标看起来正常可能原因图片读的宽高顺序写反了。OpenCV 读到的shape是(height, width, channels)而 XML 里的 size 是width在前height在后。脚本里把两个值反过来用所有框横纵坐标就颠倒了。检查方法挑三张图手动对比原始框和转换框。症状二训练时某个类别完全没参与计算可能原因obj.names里的类别顺序和标注 txt 里的 class_id 不一致。特别常见于多人合作时有人用了不同的 categories.txt 做了标注。症状三COCO 转 VOC 后图像文件名后缀变了导致图片和 XML 对不上可能原因COCO 的file_name有时不带路径有时带train2017/xxx.jpg转成 VOC 时拼接路径逻辑写死了。处理方式不要直接用file_name建议用图片 ID 索引自己维护一份文件名映射表。症状四验证时 mAP 很高但部署出来框全是歪的可能原因数据集里的标注框本身就不准要么是标注时框选松散要么是标注工具导出的坐标有缩放误差。这时候检查原始标注而不是调模型代码。我见过很多人花了两周调 anchor、调 NMS最后发现是标注框整体偏了四五个像素。症状五标注好的图放进 YOLO 训练直接报“All labels empty”可能原因图片路径或者标签路径不对YOLO 找不到同名 txt。另一个常见原因是标签文件虽然存在但里面的所有行都被过滤掉了比如 difficult 全部 skip。检查路径的同时也看一眼过滤逻辑。症状六标注平台导出的压缩包损坏这个纯粹是工具层面问题建议使用 CVAT 的导出物时先解压验证解压后确认图片数量和标注数量一致再开始下一步。做一份完整数据集不容易导出后第一时间备份并且保存一版未压缩的原始标注目录。症状七教程脚本跑得飞起你的数据却各种报错因为教程的数据集很干净。真实数据里不仅有 difficult 框、非法框、空标注还有重复图片、坏图。所以你的脚本里必须内置过滤逻辑也就是前面 4.2 里那三段——过滤 difficult、过滤非法框、clip 归一化坐标。宁可在转换时严格一点也好过让脏数据流到训练阶段浪费 GP U 时间。7. 最后的实际体会把“母本数据集”留下来这里准备讲一个我觉得很值得执行的经验刚做第一个数据集时我犯过的最大错误是直接拿 YOLO txt 当原始标注后续要转 COCO 或者要增加类别时痛苦无比。后来我固定下来一个工作流程——保留一版 VOC 格式作为“母本数据集”。理由很简单VOC 格式信息最丰富truncated、difficult、pose这些字段可以留到后续做数据筛选而 COCO 和 YOLO 为了训练效率会丢掉部分信息。保留母本后任何一次格式转换都从母本重新生成再也不做“YOLO 转 COCO”这种容易丢失信息的二次转换。每次修改标注也只在母本上改然后重新导出才分发到各训练框架。另外数据集一定做版本管理。我习惯用 git 或者至少带日期的文件夹管理母本每轮迭代都留一份说明文档记录新增了什么数据、修正了什么标签。一次改动导致模型指标骤降时能快速回退到上一版母本重新试验。这份流程走完一遍以后你要再接手任何检测数据集基本就是流水线作业了。训练不出来、指标诡异、部署崩坏大概率是前面某一步数据没守住。数据集的制作是在为整个检测项目打好地基地基稳了模型怎么折腾都行。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑