尧图精选

目标检测数据集制作全流程:从图像收集到VOC/COCO/YOLO格式转换

🕒 发布时间:2026/10/1 23:35:52 📁 来源:尧图网络
做目标检测项目最容易被低估的一个环节就是数据集制作。模型效果不行很多人第一时间去调网络结构、改损失函数但排查到最后往往是数据格式、类别映射、标注坐标出了问题。我前前后后手工做过好几套检测数据集从几百张的小样本到上万张的正式训练集都走过一遍踩过的坑包括标注完才发现坐标归一化反了、类别ID对不上、COCO的bbox宽高被写成了右下角坐标、YOLO训练时突然报标签越界……这些坑大多不是模型的问题而是数据集从收集到标注、再到格式互转的环节没搞对。这篇文章就把整个流程串一遍图像怎么收、标注规范怎么定、VOC/COCO/YOLO三种格式的本质差异在哪、互转脚本怎么写、以及转完之后怎么验。适合正在准备检测数据集、或者已经被各种格式转换搞到头大的朋友尤其是刚入门准备自己训YOLO、跑MMDetection或Detectron2的人。我会把每一步为什么这么做也讲清楚尽量让你不光能照着做还能知道出了问题往哪儿查。1. 图像收集的三个渠道公开数据集、定向采集、日常素材沉淀1.1 公开数据集怎么选、怎么用工业界做检测很少从零开始拍图。最省力的方式是在公开数据集里找基座然后叠加自己的场景数据。VOC 2007/2012、COCO、OpenImages都是很常用的来源。VOC的类别比较老主要是人、车、猫、狗这类日常物体适合做通用检测的预训练和调试COCO类别更细有80类而且带了实例分割标注很多预训练模型的类别ID就是按COCO的顺序定的OpenImages胜在数据量大、类别极度丰富但标注质量参差不齐用之前必须抽检。用公开数据集时有几个隐性成本要注意。第一是许可证有的数据集只能用于学术研究商用会有法律风险如果是给公司项目做数据最好查清楚数据集的使用条款。第二是类别定义差异同样是“person”这个类别COCO和VOC的标注风格就不一样——VOC倾向于把人从头到脚完整框出来COCO在严重遮挡时只标可见部分这两个标准混在一起训练会让模型学得无所适从。我习惯的做法是同一种场景、同一种标注规范的数据放在一起不跨标准硬揉。1.2 定向采集爬取、录像抽帧、摄像头轮拍如果公开数据集覆盖不了你的目标就得自己收图。最常见的定向采集有三种方式网络爬图按关键词批量搜索图片适合收集“某类物体”的大量样貌变体。这个方式胜在便宜、量大但姿态、场景、光照分布不可控而且图片质量参差需要大量人工筛图。视频抽帧从实际业务录像、监控视频、产品演示视频里按帧率抽样。抽帧比单张拍图高效得多因为同一场景连续帧里目标形态连续变化能比较自然地覆盖遮挡、模糊、视角变化。现场轮拍把相机固定在不同高度和角度对目标物体进行批量成像。工业质检项目里常用这种方式因为场景和部署环境强相关网络爬来的图反而没有代表性。不管用哪种方式我在收图阶段就会同步记录一个“数据来源清单”——每张图来自哪个批次、什么设备、大致拍摄条件。这不是可有可无的元信息后续划分训练集/验证集时如果同一视频里抽出来的连续帧被同时塞进训练集和验证集模型验证分数会虚高。等部署到真实环境才发现泛化不行那时再回溯是哪批图造成的就会非常痛苦。1.3 收图阶段就要做的质量门槛很多新手收到图就直接丢给标注工具这是错误的。收图阶段应该做一道“预筛”分辨率过低的图删掉画面模糊到看不清目标轮廓的删掉目标占比小于1%的图放进“难例池”而不是直接删——难例池留着以后做模型的对抗性测试很有用。我一般按三个维度给图片质量打分清晰度、目标可辨识度、背景干扰程度。清晰度看的是目标边缘是否锐利可辨识度看的是人工标注时能否不借助语义联想就判断出类别背景干扰看的是目标与背景的纹理对比。三个维度都不达标的图直接丢弃两个维度不达标但剩一个维度特别有价值的放进难例池。这样前置过滤后标注成本能省下至少20%训练时的无效学习也少很多。2. 标注不是画框而是定规范工具选型与质检流程2.1 标注工具怎么选单机版与团队版标注工具这块我前后用过不少简单对比一下适用场景工具适用场景优点痛点LabelImg百张级小样本、个人项目轻量、开箱即用、输出VOC/ YOLO格式多人协作几乎没有、无法管理复杂标注任务labelme需要多边形标注能标多边形而不是矩形框检测任务用矩形框反而绕CVAT团队协作、中大规模数据线上标注、任务分配、自动保存、支持AI辅助预标注部署有门槛需要服务器Label Studio多模态标注、复杂流程支持检测/分类/分割等混合任务接口完善界面相对重数据量大时有点卡X-AnyLabeling个人快速标注内置YOLO、SAM等模型辅助标注效率高依赖GPU推理弱机器体验一般如果你的项目自己一个人干、数据量在两三千张以内LabelImg完全够用到了团队作战、多个人同时标一万张图强烈建议直接上CVAT。还有一个经验工具的输出格式不是首选考虑因素反正我们最终都要做格式互转重要的是标注体验和协作能力。我见过有人为了避开格式转换强迫所有人用YOLO原生的txt格式标注结果坐标归一化、类别ID一旦有人标错排查起来非常费劲。2.2 标注规则比点框本身更重要一个检测数据集能不能用很大程度上取决于标注规则写得够不够细。只说“把目标框出来”是不够的。我手里的一份标注规范通常包含这些条款目标定义什么算目标、什么不算。比如“车辆”这个类自行车算不算“人”这个类只露一只手算不算框的边界矩形框应该紧贴目标可见部分还是包含因透视而变形的整体范围框要包含目标所有像素但背景尽量少。遮挡处理目标被其他物体遮挡框只框可见部分还是人工“脑补”完全轮廓我采用的原则是只框可见部分目标准确率80%以上才算标注小于50%可见度且无法明确类别的不标。截断目标目标在图像边缘被截断要不要标如果测试场景里大量出现截断目标那就应该标如果只是偶然情况可以不标但要在规范里写明。类别边界几个类别之间容易混淆时比如“裂纹”和“划痕”要在规范中给出图文示例正反面各举几个。这些规则看起来啰嗦但多人协作时它就是避免返工的保命条款。我自己标注时会定期把标注结果导出用脚本统计“每类的平均框面积”“框中目标占比”等指标检查有没有人把框画得过大或过小这种数据层面的异常比肉眼抽查更快暴露问题。2.3 多人协作时的质检怎么做多人标注最大的风险是标准漂移——同一个物体不同人画出的框差很多甚至同一个人标了500张之后手感和早上都不一样了。我常用两个办法来控标准重复标注对比随机抽5%-10%的图片让两个不同的人独立各标一遍然后计算两个框之间的IOU。同类别框的IOU低于0.7就要回溯沟通找出是谁的理解偏了。交叉抽检项目负责人每天抽检已完成的标注检查是否有漏标、错标、框偏移。发现问题不要只改那一张而是把同类问题图片全部筛出来重新标。另外要注意标注的“难例集中处理”。把所有人都觉得犹豫的图片单独建一个文件夹在例会上统一讨论定标准然后把讨论结果写进规范文档。我见过很多团队忽略这一步结果就是每个人都在按自己的想法标难例数据集内部的噪声很大训练出来的模型对难例的鲁棒性很差。3. 三种格式的本质差异VOC是文件夹、COCO是字典、YOLO是归一化坐标3.1 VOC格式目录即数据库先普及一下VOCPascal VOC的目录结构最常见的长这样VOCdevkit/ ├── VOC2007/ │ ├── JPEGImages/ # 存放jpg原图 │ ├── Annotations/ # 存放xml标注文件 │ ├── ImageSets/ │ │ └── Main/ # train.txt、val.txt、test.txt每个XML文件对应一张图片文件名字和图片名一一对应。XML里的关键结构是这样的annotation filename000001.jpg/filename size width500/width height375/height depth3/depth /size object namecat/name difficult0/difficult bndbox xmin100/xmin ymin100/ymin xmax200/xmax ymax200/ymax /bndbox /object /annotationVOC的重点在于坐标是绝对像素坐标格式为左上角(xmin, ymin)和右下角(xmax, ymax)类别名称是字符串而不是数字ID图像尺寸写在XML里方便做校验。这种格式直观、人类可读但缺点是每张图一个文件数据量大了后文件数过多读取速度慢而且没有统一的“类别总表”类别列表需要自己去全部XML里扫一遍容易漏。3.2 COCO格式JSON组织起整张图COCO标注是一个巨大的JSON文件顶层结构包含几个关键字段info、licenses、images、annotations、categories。核心关联关系是images数组里每个元素包含id、file_name、width、height、licenseannotations数组里每个元素包含id、image_id、category_id、bbox、area、iscrowd、segmentationcategories数组里每个元素包含id、supercategory、name一个典型的annotation对象长这样{ id: 1, image_id: 1, category_id: 3, bbox: [100, 100, 100, 100], area: 10000, iscrowd: 0, segmentation: [] }注意COCO里bbox的格式是[x, y, width, height]x和y是左上角坐标width和height是框的宽高都是像素值。这与VOC的[xmin, ymin, xmax, ymax]不同也是新手最容易转错的地方。另外COCO的category_id是整数ID必须和categories数组里的定义对应起来它本身不存类别名字符串。iscrowd字段表示该标注是否属于“密集人群/物体”标注很多框架训练时会忽略iscrowd1的标注转格式时如果忘了把这个字段处理好可能训练时莫名其妙少了很多样本。3.3 YOLO格式每张图一个小文件YOLO系列使用的标注格式与VOC/COCO差别最大。它为每张图片生成一个同名txt文件放在labels目录下每一行表示一个检测框class_id x_center y_center width height其中x_center、y_center、width、height全部是归一化到0~1之间的相对值用比例表示而不是像素值。例如一张宽1000、高800的图某个框的左上角在像素(100, 200)宽300、高200那么归一化后的中心坐标为((100300/2)/1000, (200200/2)/800)归一化后的宽高为(300/1000, 200/800)。YOLO的目录结构也很有特点训练时用一个data.yaml文件统一描述数据集的路径和类别train: ./images/train val: ./images/val nc: 3 names: [cat, dog, person]nc是类别总数names的索引就是class_id。必须始终记住YOLO的标注文件本身不记录类别名称只记录数字ID数字ID与names列表的索引一一绑定。一旦names顺序变了整个标注就全乱了。4. 格式互转实战手写转换脚本的正确姿势与高频踩坑点4.1 明确你手里的“源格式”和“目标格式”做互转时我建议先画一张“数据流图”在脑子里过一遍当前标注在什么格式、训练框架吃进什么格式、二者之间差的字段是什么。比如从VOC转COCO类别从字符串变成数字坐标从[xmin,ymin,xmax,ymax]变成[x,y,w,h]从COCO转YOLO像素坐标变成归一化坐标、类别ID从整型直接映射过去、文件组织从单个JSON变成按图片拆分的txt。这几条转换规则本身不难难的是正确管理类别ID。类别ID在不同格式里可能完全不同。比如VOC里person的name是字符串“person”COCO里person的category_id是1而如果你用某些YOLO预训练权重person的class_id又是0。转换脚本必须维护一张“类别名 ↔ 全局ID”的映射表绝不能靠硬编码。4.2 VOC转COCO手写一版最小实现写VOC转COCO时我经常用lxml解析XML。核心步骤是这样import os import json import xml.etree.ElementTree as ET def voc_to_coco(xml_dir, img_dir, output_json): images [] annotations [] categories [] cat_id_map {} ann_id 1 # 第一步扫描所有XML建立类别名到ID的映射 for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() for obj in root.findall(object): name obj.find(name).text if name not in cat_id_map: cat_id_map[name] len(cat_id_map) 1 categories.append({ id: len(cat_id_map), name: name, supercategory: name }) # 第二步遍历每个XML填充images和annotations for xml_file in sorted(os.listdir(xml_dir)): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() filename root.find(filename).text width int(root.find(size/width).text) height int(root.find(size/height).text) image_id len(images) 1 images.append({ id: image_id, file_name: filename, width: width, height: height }) for obj in root.findall(object): name obj.find(name).text category_id cat_id_map[name] bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) width_box xmax - xmin height_box ymax - ymin annotations.append({ id: ann_id, image_id: image_id, category_id: category_id, bbox: [xmin, ymin, width_box, height_box], area: width_box * height_box, iscrowd: 0, }) ann_id 1 coco_data { images: images, annotations: annotations, categories: categories } with open(output_json, w) as f: json.dump(coco_data, f, indent2)这段代码有个前提标注文件里的filename字段在JPEGImages目录里真实存在。有些VOC数据集因为文件经过改名XML里的filename和实际文件名不一致建议在脚本里加一步根目录拼接检查。4.3 COCO转YOLO最容易犯的归一化错误COCO转YOLO的脚本同样不复杂关键就是把像素坐标归一化import json import os def coco_to_yolo(coco_json, output_dir): with open(coco_json) as f: data json.load(f) # 构建image_id到图片宽高的索引 image_info {} for img in data[images]: image_info[img[id]] img # 构建category_id到0~N-1的索引 cat_map {} for i, cat in enumerate(data[categories]): cat_map[cat[id]] i os.makedirs(output_dir, exist_okTrue) # 把annotations按image_id分组 from collections import defaultdict anns_by_img defaultdict(list) for ann in data[annotations]: anns_by_img[ann[image_id]].append(ann) for image_id, anns in anns_by_img.items(): img image_info[image_id] img_w img[width] img_h img[height] file_stem os.path.splitext(img[file_name])[0] txt_path os.path.join(output_dir, file_stem .txt) with open(txt_path, w) as f: for ann in anns: cat_id cat_map[ann[category_id]] bbox ann[bbox] # [x, y, width, height] x, y, w, h bbox[0], bbox[1], bbox[2], bbox[3] x_center (x w / 2) / img_w y_center (y h / 2) / img_h norm_w w / img_w norm_h h / img_h # 防止浮点误差导致坐标大于1 x_center min(max(x_center, 0), 1) y_center min(max(y_center, 0), 1) norm_w min(max(norm_w, 0), 1) norm_h min(max(norm_h, 0), 1) f.write(f{cat_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}\n)两个最值得强调的坑第一COCO的bbox有可能是浮点数也可能是int都按float处理就好但转YOLO后的txt大概率会被YOLO训练框架当作float读取并不会有问题。真正的问题是w/2时如果w是int在Python3中除法结果本身是float没问题但如果用//就会出现中心坐标错误。第二归一化后的值理论上必须在0~1之间但浮点运算和bbox越界会导致x_center norm_w/2 1即框出了图像边界。很多YOLO训练框架对此直接报错或者忽略该标签所以我在写文件前会用min/max把值截断到[0,1]区间内。这个截断看起来是“脏操作”但对真实数据集非常有效因为人工标注时框的边缘经常超出图像边界这在像素坐标里合法在归一化坐标里就会越界。4.4 YOLO转VOC/COCO反向转换的坑反向转换同样经常发生比如你下载到一个YOLO格式的检测数据集想转成COCO跑到MMDetection里。这个时候最大的问题是YOLO的txt文件里没有图像宽高信息只有归一化坐标。你要么从目录里读取每张图片的实际尺寸用PIL或OpenCV)要么从data.yaml对应的images目录里读取。否则无法把归一化坐标还原成像素坐标。我写过一段反向转换的通用逻辑核心就是先从图片文件读出宽高from PIL import Image import os def yolo_to_voc_txt(txt_file, img_file): img Image.open(img_file) img_w, img_h img.size with open(txt_file) as f: lines f.readlines() objects [] for line in lines: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) x_center float(parts[1]) y_center float(parts[2]) w float(parts[3]) h float(parts[4]) xmin (x_center - w / 2) * img_w ymin (y_center - h / 2) * img_h xmax (x_center w / 2) * img_w ymax (y_center h / 2) * img_h objects.append((cls_id, xmin, ymin, xmax, ymax)) return objects这里注意一点当x_center - w/2是负数时xmin会变成负数听起来它和COCO转YOLO时的越界问题是对称的。但区别在于处理这种情况时不要盲目截断。因为负的xmin本身意味着人工在标注时可能画出了图外有些框架能容忍有些会直接报错。我的做法是转换后统一做一次clip把坐标限制在[0, img_w]和[0, img_h]内然后重新计算w和h同时检查w/h是否会变成0比如目标完全在图外如果变成0就把该标注行删掉。4.5 类别映射顺序的统一管理做任何一次转换我建议都写一个独立的“类别映射配置文件”例如# category_map.yaml VOC_CAT: person: 1 car: 2 COCO_CAT: person: 1 car: 3 YOLO_CAT: person: 0 car: 1转换脚本不要直接硬编码映射而是从这个配置里读取。这样最大的好处是当你从其他项目拿到一个数据集对方给的类别顺序和你的项目不同你只需要改配置文件而不用改转换脚本。我见过太多项目为了图省事在脚本里写死映射结果换一个数据集就得改一次代码改完又引入新的bug。5. 转完格式先别急着训可视化验证和数据集体检5.1 画框可视化是最直接的验证手段格式互转完成后第一步不是急着跑训练而是随机抽20-30张图片把标注框画在原图上可视化检查。原因很简单数字上完全合规的坐标视觉上可能是偏移的、尺度是错的、类别是乱的。可视化能让你一眼看出问题。我常用的画框脚本很简单基于PIL或OpenCV都能做import cv2 def draw_boxes_yolo(img_path, txt_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) color (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[int(cls_id)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) return img如果是VOC或COCO格式就先按前面的转换逻辑把标注读出来再基于同一套画框函数可视化。这里我特别提醒可视化用的一定要是“转换后”的标注文件而不是转换前。有人习惯直接画原始XML能看到框正常就觉得转换没问题等训练时才暴雷。画转换后的文件能让偏移、尺度、类别ID错乱这类问题在上训练前就暴露。5.2 数据集体检这些统计指标比人眼更早发现问题除了可视化我还会跑一套统计脚本对数据集做“体检”主要看这几个指标类别分布各类别的目标数量是否严重失衡。如果某个类只有另一个类的1/10模型很可能偏向样本多的类。需要做类别均衡比如复制扩充、使用loss权重或者增加该类别的采样概率。单张图片的框数量分布如果大量图片有20个框少数图片有1个框说明模型训练时每个batch里目标数量变化很大会影响loss的稳定性。极端情况下有些图因为目标太多、太小模型难以收敛。目标面积分布把目标按像素面积分成小目标(小于32x32)、中目标(32x32~96x96)、大目标(大于96x96)统计三类占比。小目标占比过高且与其他目标类别混杂时检测器很容易漏检。空标注图片转换过程中很容易出现某些图被漏写txt/XML/annotation记录导致图片无标签。YOLO训练框架一般会忽略无标签图片但如果你在分类损失里使用了负样本挖掘图片的语义信息是背景还是漏标就会严重误导模型。5.3 数据集划分随机划分不是万能答案最后把整套数据集划分为train/val/test时我建议按照“来源隔离”而不是纯随机。前面提到的视频抽帧数据如果同一段视频的连续帧既出现在训练集又出现在验证集验证集就失真了。正确做法是按视频片段或采集批次划分一个视频的帧全部归到某一个集合里不在多个集合间穿插。我经常用hasattr的方式给每张图片打上来源批次字段然后对批次做划分而不是对图片直接随机切分。对于纯网络爬取的图片也要尽量考虑“场景重复”问题——同一网页上连着下载的图往往背景和摄影风格高度相似要先把页面URL去重再按域名或采集任务分组。这一步做对了模型在真实场景中的表现才和验证分数对得上。写在最后的一点心得数据集制作这个环节真正拉开差距的往往不是工具多先进而是流程是否严密、规范是否清楚、转换脚本是否可控。我自己花了很长时间才意识到标注和转换不应该是“一次性工作”而是一个可以随时回溯、审计、重跑的过程。所以我现在做数据集项目时都会先把目录结构、命名规则、映射配置和转换脚本固化下来每个版本都保留一份快照。这样后续数据扩充、格式再变、或者是别人接手时大家都能很快进入状态而不是靠个人记忆和一串不知道谁写的脚本在支撑。如果你也正在做检测数据集我的建议是先别追求一步到位拿二三十张图把从收集到标注再到格式转换、可视化的全链路走一遍确认每个环节无误后再放大到全量数据。这个“小规模试点”的方法帮我省下过好几次全量返工的力气你也可以试试。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →