目标检测数据集格式转换指南:VOC/COCO/YOLO互转与校验
简介这是一套面向目标检测入门与进阶开发者的数据集制作及格式转换工具包覆盖VOC、COCO、YOLO三类主流格式的构建思路与互相转换方案适用于需要自行标注数据、统一多格式训练集或复现检测项目的场景。压缩包共139个文件包含60张jpg样例图像、45个txt标注文件、20个xml标注文件、4个json注解文件、9个Python转换脚本及1份说明文档整体仅3.79MB轻量实用。包内脚本借助xml.etree、json与pandas等库完成XML/JSON解析、坐标换算和类别映射并兼顾数据增强与转换后校验可帮助读者避开手动转换的常见坑点。目前已有2754人学习下载适合正在构建自定义检测数据集、或需在VOC/COCO/YOLO格式间灵活切换的开发者参考使用。1. 目标检测数据集的起点为什么格式转换才是真正的坑很多人以为“目标检测数据集制作”就是把图片框几个框存成某种格式。但真正上手就会发现VOC 要 XML、COCO 要 JSON、YOLO 要每张图一个 TXT三种格式字段名不同、坐标归一化方式不同、类别注册方式更不同。你在标注工具里画好的框换个训练框架就“凭空消失”这类问题 90% 都出在格式边界上。这篇文章沿着 VOC、COCO、YOLO 三种最常见格式的字段结构、标注工具选型、转换脚本编写和校验方法展开适合既想自己做数据集又不想在格式转换上返工的检测算法工程师和数据标注同学。2. VOC/COCO/YOLO 三种格式的字段级拆解先把三种格式的“骨架”讲明白后面写脚本时才知道该从哪里取字段、往哪里写值。无论你用哪种标注工具导出时最终都会落到这三种结构之一。2.1 VOC以 XML 为载体的“人可读”标准VOC 格式源自 PASCAL VOC 挑战赛它的核心是每个图片对应一个同名 XML 文件。XML 里最关键的节点是size和object前者记录图片宽度、高度和通道数后者包含name类别、bndbox左上角和右下角坐标。一个容易忽略的点是VOC 的坐标是绝对像素值单位是整数不需要归一化也不允许超出图片边界。annotation folderJPEGImages/folder filename000001.jpg/filename size width1280/width height720/height depth3/depth /size object namecar/name bndbox xmin100/xmin ymin150/ymin xmax400/xmax ymax350/ymax /bndbox /object /annotationVOC 的优点是结构清晰、肉眼可读缺点是文件数量多一张图一个 XML而且对畸形 XML 的容错性差。很多老检测框架如 SSD、EfficientDet 的早期实现都直接用 VOC 目录结构因此它至今仍是数据交接的“通用语言”。2.2 COCO以 JSON 为核心的“机器友好”标准COCO 格式把所有标注集中在单个 JSON 文件里顶层字段是images、annotations、categories。images只存图片 ID、文件名、宽高annotations存segmentations、bbox和category_idcategories存类别 ID 到名称的映射。COCO 的bbox是[x, y, width, height]即左上角坐标加宽高与 VOC 的左上右下不同这是转换时最容易写错的地方。{ images: [{id: 1, file_name: 000001.jpg, width: 1280, height: 720}], annotations: [{ id: 1, image_id: 1, category_id: 1, bbox: [100, 150, 300, 200], area: 60000, iscrowd: 0 }], categories: [{id: 1, name: car}] }COCO 的 JSON 适合程序读取和训练加速也方便做数据划分train/val 往往一个 JSON 覆盖一个子集。缺点是手改困难哪怕加一个类别都要按 ID 重新映射所以实际操作中“JSON 生成”远比“手写 JSON”常见。2.3 YOLO每张图一个 TXT 的“归一化极简风”YOLO 把标注记录为“类 ID 中心点 x 中心点 y 宽度 w 高度 h”五组数每行一个目标按空格分隔存储在labels目录下与图片同名的 TXT 文件中。所有坐标都要除以图片宽高做归一化值域在 0 到 1 之间。这样的好处是不同分辨率图片共用同一套标注坏处是一旦图片分辨率写错所有框全部偏移。0 0.3125 0.3472 0.2344 0.2778 1 0.4219 0.6528 0.1562 0.1944YOLO 的目录副本还包括train.txt/val.txt每个路径对应一张图以及data.yaml类别列表。注意YOLO 的类别 ID 从 0 开始darknet和ultralytics都遵循该规则这点在下文写转换脚本时必须盯紧。三种格式对比维度VOCCOCOYOLO文件载体XML单个 JSONTXT坐标形式xmin、ymin、xmax、ymaxx、y、width、height中心点 cx、cy、w、h归一化否否是类别管理元素类别名category_id 映射类 ID 整数典型用途数据交换训练与评测推理与训练3. 从零做一份可用数据集标注工具与校验命令格式理解了接下来就是把图片变成标注。这里的任务是“做数据集”因此工具选择不能只看能不能画框还要看导出格式和后续扩展性。3.1 选标注工具labelImg 与 labelme 的边界最常用的标注工具是 labelImg矩形框标注和 labelme多边形/分割标注。做目标检测只需矩形框时优先选 labelImg原因在于它对 VOC/YOLO 格式的原生支持让标注完就能直接用labelme 适合实例分割和语义分割生成的是 JSON 而非 VOC XML若要做检测还需要二次轮廓转换框。其他选项包括 CVATWeb 服务适合团队协作和 X-AnyLabeling集成了一键部署脚本的融合工具但团队规模和标注任务量决定选择不必追新。labelImg 的安装方式在 GitHub 仓库有现成描述常见做法是用 pip 安装后命令行启动pip install labelImg labelImg JPEGImages classes.txt参数说明JPEGImages是存放图片的目录classes.txt是预置类别文件每行一个类名。启动后快捷键分别为W画框、D切下一张、A切上一张、CtrlS保存。这里值得强调标注前先把类别文件写好避免中途改类名导致 XML 里的类别和训练配置不一致。3.2 标注前的目录与命名规范标注工作一开始最常见的问题是文件命名混乱。推荐按以下目录结构组织数据这套结构也是后面转换脚本的输入假设dataset/ ├── images/ │ └── train/ # 原始图片 ├── annotations/ │ ├── xml/ # VOC 格式标注 │ └── json/ # COCO 格式标注 ├── labels/ │ └── train/ # YOLO 格式标注 └── classes.txt # 类别列表命名统一用 6 位以上数字或“前缀 日期 序号”禁止使用空格和中文。文件名会被写入 XML、JSON、TXT 多处一旦命名有空格解析脚本极容易把路径切碎。最忌讳的是把train.txt和train/同名同目录很多转格式的脚本会递归扫到自身导致死循环。3.3 用 Python 脚本校验损坏标注标注完成后要做机器校验不能靠人眼看图检查。脚本至少应覆盖三类问题图片打不开、XML 里坐标超出图片边界、YOLO TXT 中的归一化坐标 1。下面是一个针对 VOC XML 与 YOLO TXT 的快速校验脚本可直接保存运行import os import xml.etree.ElementTree as ET from PIL import Image # 校验 VOC XML def check_voc(xml_dir, img_dir): for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue root ET.parse(os.path.join(xml_dir, xml_file)).getroot() filename root.find(filename).text img_path os.path.join(img_dir, filename) w int(root.find(size/width).text) h int(root.find(size/height).text) for obj in root.findall(object): xmin int(float(obj.find(bndbox/xmin).text)) ymin int(float(obj.find(bndbox/ymin).text)) xmax int(float(obj.find(bndbox/xmax).text)) ymax int(float(obj.find(bndbox/ymax).text)) if xmin 0 or ymin 0 or xmax w or ymax h: print(f越界: {xml_file} - {filename}) if xmin xmax or ymin ymax: print(f坐标颠倒: {xml_file})这段校验逻辑的核心是三步解析 XML、取图片宽高、逐个目标对比边界。参数说明xml_dir和img_dir分别是 XML 与图片所在目录脚本会对每个 XML 检查是否存在越界或“框宽为负”的情况。至于 YOLO TXT则要读归一化值检查是否在 0~1 范围for txt_file in os.listdir(txt_dir): if not txt_file.endswith(.txt): continue with open(os.path.join(txt_dir, txt_file)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f字段数错误: {txt_file}) continue vals list(map(float, parts[1:])) if any(v 0 or v 1 for v in vals): print(f归一化越界: {txt_file} - {line.strip()})4. 格式互转脚本我能直接抄的 VOC/COCO/YOLO 转换代码格式转换的核心是坐标换算和文件组织。很多现成开源库能完成这批工作但自己写一遍才能理解中间细节也方便适配私有数据集。4.1 转换链路与脚本骨架VOC↔COCO↔YOLO常见做法是“以 VOC 为中间桥”。原因有三VOC 目录结构简单既有边界框又有文件名和图片尺寸由 VOC 转向 COCO 只需汇总一个 JSON由 VOC 转 YOLO 只需逐行写 TXT。反过来从 COCO 或 YOLO 转 VOC 也不难但要把类别 ID 反查成类别名。推荐链路如下VOC XML ⇄ COCO JSON → YOLO TXT ↑ 标注工具导出实际操作中优先写“VOC ↔ COCO”的单向脚本因为 COCO 方向常用于训练 Mask R-CNN、MMDetection 等框架而 YOLO 方向则服务于 YOLOv8、YOLOv9、YOLOv11 等模型。4.2 VOC XML 转 YOLO TXT 的完整脚本下面是一段可以直接落地的最小脚本它遍历xml_dir对每张图生成同名 TXT并自动生成classes.txt。注意它假设所有 XML 只属于同一类集合如果有多份子集 XML请先合并。import os import xml.etree.ElementTree as ET def voc2yolo(xml_dir, label_dir, class_list_path): class_names [] # 第一遍扫描建立类别索引 for f in os.listdir(xml_dir): if not f.endswith(.xml): continue root ET.parse(os.path.join(xml_dir, f)).getroot() for obj in root.findall(object): name obj.find(name).text if name not in class_names: class_names.append(name) with open(class_list_path, w) as f: f.write(\n.join(class_names)) # 第二遍转换坐标 for f in os.listdir(xml_dir): if not f.endswith(.xml): continue root ET.parse(os.path.join(xml_dir, f)).getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) base os.path.splitext(f)[0] with open(os.path.join(label_dir, base .txt), w) as out: for obj in root.findall(object): name obj.find(name).text cls_id class_names.index(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) # 中心点坐标与宽高 cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h out.write(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n)逻辑说明脚本做了两遍扫描。第一遍建立类别列表避免某一类在后续文件中才出现而漏建类别第二遍逐个取坐标并归一化。小技巧xml.find(bndbox/xmin)用了路径表达式比逐级 find 更稳float转换兼容整数和字符串两种写法。参数含义示例xml_dir输入 VOC XML 目录annotations/xml/label_dir输出 YOLO TXT 目录labels/train/class_list_path最终类别文件classes.txt4.3 COCO JSON 转 YOLO TXT 的命令行封装若手头已经是 COCO JSON比如公开的 COCO2017 子集或标注工具导出的 JSON可直接用脚本转 TXT。核心是建立category_id到连续整数的映射再读取每个annotation的bbox换算为 YOLO 值。import json def coco2yolo(json_path, output_dir): with open(json_path) as f: data json.load(f) # 类别映射表 cat_map {cat[id]: new_id for new_id, cat in enumerate(data[categories])} # 图片 id - 文件名 img_map {img[id]: img[file_name] for img in data[images]} # 按图片聚合标注 anns_by_img {} for ann in data[annotations]: anns_by_img.setdefault(ann[image_id], []).append(ann) for img_id, anns in anns_by_img.items(): info next(img for img in data[images] if img[id] img_id) w, h info[width], info[height] out_name os.path.splitext(img_map[img_id])[0] .txt with open(os.path.join(output_dir, out_name), w) as f: for ann in anns: x, y, bw, bh ann[bbox] cx, cy x bw/2, y bh/2 cls_id cat_map[ann[category_id]] f.write(f{cls_id} {cx/w:.6f} {cy/h:.6f} {bw/w:.6f} {bh/h:.6f}\n)常见问题COCO JSON 中category_id可能从 90 开始COCO 原版数据集必须映射为 0-based 连续 IDYOLO 不能接受稀疏 ID。另外COCO 的bbox宽度/高度是浮点数若标注值是[x, y, w, h]中的 x/y 为左上角实现时不要误当中心点。4.4 转换时的目录与类别顺序陷阱转换脚本最容易踩的坑不是数学运算而是目录层级和类别顺序。第一YOLO 训练时要求images和labels目录并行、同名一一对应路径层级写错会导致 “image not found”。第二不同脚本扫目录的顺序不同若两次运行之间文件顺序变化类别索引会漂移所以经典的稳妥做法是像上面的 VOC 脚本一样“先建类别表并写入classes.txt”之后的所有转换都读取该文件而不是重新扫描。第三个坑是背景类问题。COCO/VOC 通常没有“背景”类别但某些检测框架比如部分 YOLO 改进版本要求把背景补成0类导致所有目标类别往后顺延一位。遇到这种情况不要写进转换规则里正确做法是训练配置里单独设置nc len(classes)保持标注数据原始性。第四个坑是重复文件.xml与.jpeg大小写混用、.txt文件用的 CRLF 换行这些在高版本 Linux 上一般无碍但在 Windows 上训练时会出现解析错误建议转换后统一用dos2unix清洗一遍。5. 转换后必做的三项验证与平衡技巧转换脚本跑通不等于转换正确尤其当你是从标注工具直接导出再转 YOLO 时中间任何一次坐标换算错误都会让模型“静默”学习到错误信息。下面这三个验证步骤是我每次处理数据集都会执行的最低限度检查。5.1 用脚本对比坐标和类别先跑通一图再跑全量无论做哪种转换都建议先把单张图拿出来人工核对。写一个对比脚本读 VOC 的 bbox再读对应 YOLO TXT 的归一化坐标按原图比例换算回来检查数值是否一致。更直观的做法是直接在图上画框并保存对比图用 OpenCV 输出新图看框是否贴在目标上。python check_boxes.py --img 000001.jpg --xml 000001.xml --txt 000001.txt建议把“全量转换”与“抽样验证”拆成两步先跑 3 张图再批量跑。批量后统计每张图的平均框数、空标注比例若空标注占比超过 10%回溯标注工具导出配置。5.2 图片与标注的不对齐问题最常见的故障是“有图无标注”和“有标注无图”后者会导致目标检测训练进程直接卡在加载阶段。验证方法很简单统计images与labels目录下的文件名交集列出差集。差集里若包含.zip、.json这类非图片文件多半是脚本过滤不严若包含真实图片缺失则说明原始数据从源头就没对齐。此时不要直接删标注先查工具导出时的过滤规则有的工具会跳过损坏图片但不给提示。5.3 类别不平衡的平衡技巧与实用建议数据集中常出现“小目标多、大目标少”或“某些类标注量仅占 5%”的情况转换格式时虽然无需改坐标但建议在生成train.txt前做一次按类别的统计。我的经验平衡技巧是将小类别样本复制进另一份做拼图增强Mosaic或者直接在采样器里调整权重这比在格式转换后返工更高效。对于追求稳定训练的用户推荐使用开源数据集如 KITTI、DOTA、CWRU 相关数据集做预训练再叠加少量自标数据进行微调这样既能省标注成本又不会让坐标换算的潜在错误影响最终精度。记得所有转换脚本固定随机种子保证每次跑出的train.txt划分一致复现训练时才有可比性。最后提醒格式转换的脚本要长期留存并写清输入输出参数否则过了两周再看很可能忘了类别 ID 的映射规则。把上面三类验证做成一条命令串起来每次数据集转换后自动执行一次把输出截图或日志存档整个数据链路才算闭环。本文还有配套的精品资源点击获取