目标检测数据集制作全流程:VOC、COCO、YOLO格式解析与互转实战

发布时间:2026/10/3 5:50:54
目标检测数据集制作全流程:VOC、COCO、YOLO格式解析与互转实战 1. 检测数据集制作全流程拆解从原始图片到模型可读格式搞目标检测的人都有一个共识模型结构再花哨数据拉胯全白搭。我见过太多人拿着 YOLOv8 或者最新的 DEIM 预训练权重兴冲冲地跑自己的数据结果 mAP 死活上不去最后排查一圈发现是标注格式转错了——类别索引对不上、坐标归一化搞反了、VOC 的 xmin/ymin 和 COCO 的 xywh 混着用。这类问题不复杂但极其消耗时间而且网上的教程往往只讲一个格式很少有人把 VOC、COCO、YOLO 三者的关系和互转逻辑一次讲透。这篇内容就是来解决这个问题的。我会从数据收集开始经过标注工具选型、标注规范制定、三种主流格式的结构解析一直到格式互转的完整代码实现把整条链路串起来。适合刚入门目标检测、准备训练自己数据集的朋友也适合已经跑过几个项目但每次转格式还要翻旧代码的从业者。读完你至少能搞清楚三件事VOC 的 XML 里每个字段到底什么意思、COCO 的 JSON 为什么那么绕、YOLO 的 txt 归一化坐标怎么算才不会错。1.1 为什么格式问题值得单独拿出来讲目标检测的数据集格式本质上是在描述同一件事一张图里有哪些目标、每个目标的类别是什么、位置在哪里。但不同框架对“位置”的表达方式不同对“类别”的索引方式也不同。VOC 用绝对像素坐标的左上角和右下角COCO 用绝对像素坐标的左上角加宽高YOLO 用归一化后的中心点加宽高。这三种表达之间是可以无损互转的前提是你得知道每个数值对应的参照系是什么。我踩过最典型的坑是用 labelImg 标注完导出 YOLO 格式训练时发现所有框都偏了。排查半天才意识到labelImg 在 YOLO 模式下保存的 txt 是归一化坐标但我中途换了一台机器重新标注时工具默认切回了 VOC 模式导出的 XML 被我用脚本强行转 YOLO脚本里忘了做归一化。这种问题不会报错模型也能跑但学出来的东西完全是错的。所以下面我会把每个格式的字段含义、转换公式、代码实现都写清楚你照着抄就能用。2. 数据收集与标注动手之前先想清楚这几件事2.1 数据收集的渠道与筛选标准数据来源无非几种自己拍、网上爬、公开数据集筛选、合成数据。自己拍的质量最可控但成本高网上爬要注意版权和隐私问题涉及人脸、车牌的内容必须做脱敏处理公开数据集像 COCO、CrowdHuman、CCPD、HRSC2016 这些可以直接下载使用但要注意类别定义是否和你的任务匹配。筛选图片时我一般遵循几个原则。第一分辨率不要低于 640×640太小的图放大后模糊特征提取效果差。第二单张图里的目标数量不要过于极端一张图几百个目标的场景需要特殊处理。第三光照、角度、遮挡情况要尽量覆盖实际部署时的场景否则模型上线后容易翻车。第四训练集、验证集、测试集要按 8:1:1 或 7:2:1 划分且同一场景的图片不要跨集合分布避免数据泄漏。注意如果做的是工业检测类任务比如开关闭合检测负样本没有目标的图也要收集比例控制在正样本的 10% 到 20% 左右能有效降低误检率。2.2 标注工具选型labelImg、CVAT、Labelme 怎么选labelImg 是最轻量的选择支持 VOC 和 YOLO 两种导出格式适合小规模单人或小团队标注。缺点是多人协作麻烦没有审核机制。CVAT 功能强大支持多人协作、自动标注、视频标注适合团队使用但部署和维护成本高。Labelme 更偏向分割任务检测任务也能用但导出格式需要额外转换。我个人的习惯是小于 5000 张图用 labelImg大于 5000 张或者需要多人协作就上 CVAT。标注之前一定要写好标注规范文档明确每个类别的边界怎么画、遮挡怎么处理、最小目标尺寸是多少。比如标注“行人”时被遮挡 70% 以上的要不要标不同标注员的理解不一致最后数据质量一定出问题。2.3 标注规范中的几个关键决策类别定义要互斥且完备。互斥是指一个目标只能属于一个类别不能出现“轿车”和“汽车”这种重叠定义。完备是指所有需要检测的目标都有对应类别不要用“其他”这种模糊类别。边界框的绘制标准要统一。目标被遮挡时框是画可见部分还是预估完整部分我的建议是画可见部分因为模型能学到的只有可见特征。但如果遮挡比例超过 80%建议直接忽略该目标否则会引入大量噪声。最小目标尺寸要设阈值。COCO 的定义是面积小于 32×32 像素为小目标小于 96×96 为中等目标。如果你的任务里小目标很多标注时就要格外仔细必要时放大图片再标。3. 三种格式深度解析VOC、COCO、YOLO 的结构与字段含义3.1 VOC 格式XML 里的每个字段都要搞明白VOC 格式的核心是一个 XML 文件对应一张图片。根节点是annotation下面包含folder、filename、size、object等子节点。size里有width、height、depth分别对应图片的宽、高、通道数。每个object里有name类别名、pose姿态一般填 Unspecified、truncated是否被截断、difficult是否难以识别、bndbox边界框。bndbox里有四个值xmin、ymin、xmax、ymax都是绝对像素坐标原点在图片左上角。注意 VOC 的坐标是 1-based 还是 0-based 在不同工具里可能不一致labelImg 导出的是 1-based但很多解析代码按 0-based 处理差一个像素通常不影响训练但如果你做的是高精度测量类任务这个细节要注意。annotation folderimages/folder filename000001.jpg/filename size width640/width height480/height depth3/depth /size object nameperson/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin100/xmin ymin200/ymin xmax300/xmax ymax400/ymax /bndbox /object /annotationVOC 格式的优点是直观、易读、易解析缺点是文件数量多每张图一个 XML大规模数据集管理起来比较麻烦。3.2 COCO 格式一个 JSON 管所有COCO 格式把所有信息塞进一个 JSON 文件顶层有images、annotations、categories三个关键字段。images是图片列表每张图有id、file_name、width、height。annotations是标注列表每条标注有id、image_id、category_id、bbox、area、iscrowd。categories是类别列表每个类别有id、name、supercategory。bbox的格式是[x, y, width, height]x 和 y 是左上角绝对像素坐标width 和 height 是宽高。注意 COCO 的category_id不要求从 0 开始连续但 YOLO 要求从 0 开始连续转换时要做映射。area是目标面积一般等于 width×heightiscrowd表示是否密集目标0 表示单个目标1 表示密集区域。{ images: [ {id: 1, file_name: 000001.jpg, width: 640, height: 480} ], annotations: [ {id: 1, image_id: 1, category_id: 1, bbox: [100, 200, 200, 200], area: 40000, iscrowd: 0} ], categories: [ {id: 1, name: person, supercategory: none} ] }COCO 格式的优点是一个文件搞定所有方便数据集划分和加载缺点是 JSON 文件可能非常大解析时占内存。3.3 YOLO 格式归一化坐标的 txtYOLO 格式每张图对应一个 txt 文件每行一个目标格式是class_id x_center y_center width height所有数值都是归一化到 0 到 1 之间的浮点数。class_id从 0 开始连续编号对应类别列表的索引。x_center和y_center是边界框中心点的归一化坐标width和height是边界框的归一化宽高。归一化公式很简单x_center (xmin xmax) / 2 / image_widthy_center (ymin ymax) / 2 / image_heightwidth (xmax - xmin) / image_widthheight (ymax - ymin) / image_height。反归一化就是反过来算。0 0.3125 0.625 0.3125 0.4167YOLO 格式的优点是简洁、加载快、适合大规模训练缺点是可读性差没有图片尺寸信息单独看 txt 不知道框的实际位置。3.4 三种格式的对照表特性VOCCOCOYOLO存储方式每图一个 XML单个 JSON每图一个 txt坐标类型绝对像素绝对像素归一化框表达xmin,ymin,xmax,ymaxx,y,width,heightxc,yc,width,height类别索引类别名数字 id可不连续数字 id从 0 连续图片尺寸XML 内包含JSON 内包含不包含适用框架早期检测框架COCO 系列、DEIM 等YOLO 系列4. 格式互转实战代码实现与参数计算4.1 VOC 转 YOLO归一化是核心VOC 转 YOLO 的流程是解析 XML 拿到图片宽高和所有 bndbox对每个 bndbox 计算归一化中心点和宽高按类别名映射到类别索引写入 txt。类别映射表要提前建好建议用有序字典保证索引从 0 开始连续。import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, output_txt_path, class_map): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_map: continue cls_id class_map[cls_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) xc (xmin xmax) / 2.0 / img_w yc (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) with open(output_txt_path, w) as f: f.write(\n.join(lines))注意归一化后的值必须裁剪到 0 到 1 之间标注时如果框超出了图片边界转换后会出现负值或大于 1 的值训练时会报错或产生异常梯度。4.2 YOLO 转 VOC反归一化要乘对尺寸YOLO 转 VOC 需要知道图片尺寸因为 txt 里没有。流程是读取图片拿到宽高解析 txt 每行反归一化得到绝对坐标写入 XML。from PIL import Image import xml.etree.ElementTree as ET def yolo_to_voc(txt_path, img_path, output_xml_path, class_names): img Image.open(img_path) img_w, img_h img.size annotation ET.Element(annotation) ET.SubElement(annotation, filename).text os.path.basename(img_path) size ET.SubElement(annotation, size) ET.SubElement(size, width).text str(img_w) ET.SubElement(size, height).text str(img_h) ET.SubElement(size, depth).text 3 with open(txt_path, r) as f: for line in f.readlines(): parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) xc, yc, w, h map(float, parts[1:]) xmin int((xc - w / 2) * img_w) ymin int((yc - h / 2) * img_h) xmax int((xc w / 2) * img_w) ymax int((yc h / 2) * img_h) obj ET.SubElement(annotation, object) ET.SubElement(obj, name).text class_names[cls_id] bndbox ET.SubElement(obj, bndbox) ET.SubElement(bndbox, xmin).text str(xmin) ET.SubElement(bndbox, ymin).text str(ymin) ET.SubElement(bndbox, xmax).text str(xmax) ET.SubElement(bndbox, ymax).text str(ymax) tree ET.ElementTree(annotation) tree.write(output_xml_path)4.3 VOC 转 COCO注意 category_id 的映射VOC 转 COCO 需要把所有 XML 的信息汇总到一个 JSON。关键是category_id的映射COCO 官方类别 id 不连续但自定义数据集建议从 1 开始连续编号方便后续转换。import json from collections import defaultdict def voc_to_coco(xml_dir, output_json, class_names): coco { images: [], annotations: [], categories: [] } for i, name in enumerate(class_names): coco[categories].append({id: i 1, name: name, supercategory: none}) ann_id 1 img_id 1 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() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) filename root.find(filename).text coco[images].append({ id: img_id, file_name: filename, width: img_w, height: img_h }) for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_names: continue cls_id class_names.index(cls_name) 1 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) w xmax - xmin h ymax - ymin coco[annotations].append({ id: ann_id, image_id: img_id, category_id: cls_id, bbox: [xmin, ymin, w, h], area: w * h, iscrowd: 0 }) ann_id 1 img_id 1 with open(output_json, w) as f: json.dump(coco, f, indent2)4.4 COCO 转 YOLO批量处理与类别映射COCO 转 YOLO 需要遍历 annotations按 image_id 分组对每条标注反算归一化坐标。注意 COCO 的 category_id 可能不连续要建立 id 到连续索引的映射。def coco_to_yolo(coco_json, output_dir, img_dir): with open(coco_json, r) as f: coco json.load(f) cat_id_to_idx {} for idx, cat in enumerate(coco[categories]): cat_id_to_idx[cat[id]] idx img_id_to_info {} for img in coco[images]: img_id_to_info[img[id]] img anns_by_img defaultdict(list) for ann in coco[annotations]: anns_by_img[ann[image_id]].append(ann) for img_id, anns in anns_by_img.items(): img_info img_id_to_info[img_id] img_w img_info[width] img_h img_info[height] txt_name os.path.splitext(img_info[file_name])[0] .txt txt_path os.path.join(output_dir, txt_name) lines [] for ann in anns: cls_idx cat_id_to_idx[ann[category_id]] x, y, w, h ann[bbox] xc (x w / 2) / img_w yc (y h / 2) / img_h nw w / img_w nh h / img_h lines.append(f{cls_idx} {xc:.6f} {yc:.6f} {nw:.6f} {nh:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines))4.5 转换后的校验别等训练报错才发现问题转换完成后一定要做校验。我一般写一个简单的可视化脚本把转换后的框画回原图随机抽几十张看一眼。如果框的位置明显偏移、大小不对、类别颜色混乱说明转换有问题。另一个校验点是统计各类别的目标数量和转换前对比。如果某个类别数量对不上大概率是类别映射出了问题。还有归一化值的范围检查出现负值或大于 1 的值要立即排查。def check_yolo_txt(txt_path): with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f格式错误: {line}) continue vals list(map(float, parts[1:])) for v in vals: if v 0 or v 1: print(f归一化越界: {line})5. 常见问题与排查技巧实录5.1 类别索引错位最常见也最隐蔽的坑类别索引错位表现为模型训练 loss 正常下降但推理时类别全乱。原因通常是转换时类别映射表不一致比如 VOC 转 YOLO 时用了字母序COCO 转 YOLO 时用了 JSON 里的原始顺序。解决办法是全局维护一份类别列表所有转换脚本都从这份列表读取索引。5.2 坐标越界与负值标注不规范的后遗症标注时框超出图片边界转换后会出现负值或大于 1 的值。YOLO 训练时这些值会被裁剪但裁剪后框的形状变了相当于引入了错误标注。建议在转换脚本里加裁剪逻辑同时输出警告日志方便回溯哪些图片标注有问题。5.3 图片与标注文件不匹配文件名大小写和扩展名陷阱Windows 系统不区分大小写Linux 区分。标注时图片是.jpg转换后写成.JPG训练时找不到文件。还有文件名里的空格、特殊字符在不同系统间传输时容易出问题。建议统一用小写字母加下划线命名扩展名统一为.jpg。5.4 常见问题速查表问题现象可能原因排查方法解决方案训练 loss 不下降标注格式错误可视化标注框重新转换并校验推理类别全乱类别索引错位对比类别映射表统一类别列表框位置偏移归一化算错检查图片尺寸重新计算归一化找不到图片文件名不匹配对比文件名列表统一命名规范某些类别学不到样本不均衡统计类别分布补充样本或加权小目标检测差小目标标注粗糙放大检查标注重新精细标注5.5 实操心得几个能省半天时间的技巧第一转换脚本一定要加日志记录处理了多少张图、多少个目标、跳过了哪些异常。出问题时日志比调试器好用。第二转换前先备份原始标注转换脚本写错了可以重来。第三小规模数据先跑通全流程再上大规模别一上来就处理几万张图。第四用 Git 管理标注文件和转换脚本每次改动可追溯。6. 数据集版本管理与后续扩展数据集不是做完就完了后续增删改是常态。我建议用版本号管理比如dataset_v1.0、dataset_v1.1每次改动记录变更日志。图片和标注分开存储用软链接关联方便切换不同版本。如果后续要扩展到实例分割YOLO 的 txt 格式需要增加多边形点坐标COCO 的 JSON 需要增加segmentation字段。如果要做旋转框检测比如 DOTA 数据集那种格式又不一样需要额外的角度信息。这些扩展都建立在当前格式理解的基础上把基础打牢后面换任务时迁移成本会低很多。我在实际项目里最深的体会是数据集的功夫花在前期后期训练和调参就轻松。标注规范定得细、转换脚本写得稳、校验流程走得全模型效果自然差不了。反过来前期图省事后期就是无尽的排查和返工。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询