简介:这是一套面向目标检测入门与进阶开发者的数据集制作及格式转换工具包,覆盖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> <folder>JPEGImages</folder> <filename>000001.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>car</name> <bndbox> <xmin>100</xmin> <ymin>150</ymin> <xmax>400</xmax> <ymax>350</ymax> </bndbox> </object> </annotation>VOC 的优点是结构清晰、肉眼可读,缺点是文件数量多(一张图一个 XML),而且对畸形 XML 的容错性差。很多老检测框架如 SSD、EfficientDet 的早期实现都直接用 VOC 目录结构,因此它至今仍是数据交接的“通用语言”。
2.2 COCO:以 JSON 为核心的“机器友好”标准
COCO 格式把所有标注集中在单个 JSON 文件里,顶层字段是images、annotations、categories。images只存图片 ID、文件名、宽高;annotations存segmentations、bbox和category_id;categories存类别 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都遵循该规则,这点在下文写转换脚本时必须盯紧。
三种格式对比:
| 维度 | VOC | COCO | YOLO |
|---|---|---|---|
| 文件载体 | XML | 单个 JSON | TXT |
| 坐标形式 | xmin、ymin、xmax、ymax | x、y、width、height | 中心点 cx、cy、w、h |
| 归一化 | 否 | 否 | 是 |
| 类别管理 | 元素类别名 | category_id 映射 | 类 ID 整数 |
| 典型用途 | 数据交换 | 训练与评测 | 推理与训练 |
3. 从零做一份可用数据集:标注工具与校验命令
格式理解了,接下来就是把图片变成标注。这里的任务是“做数据集”,因此工具选择不能只看能不能画框,还要看导出格式和后续扩展性。
3.1 选标注工具:labelImg 与 labelme 的边界
最常用的标注工具是 labelImg(矩形框标注)和 labelme(多边形/分割标注)。做目标检测只需矩形框时优先选 labelImg,原因在于它对 VOC/YOLO 格式的原生支持让标注完就能直接用;labelme 适合实例分割和语义分割,生成的是 JSON 而非 VOC XML,若要做检测还需要二次轮廓转换框。其他选项包括 CVAT(Web 服务,适合团队协作)和 X-AnyLabeling(集成了一键部署脚本的融合工具),但团队规模和标注任务量决定选择,不必追新。
labelImg 的安装方式在 GitHub 仓库有现成描述,常见做法是用 pip 安装后命令行启动:
pip install labelImg labelImg JPEGImages classes.txt参数说明:JPEGImages是存放图片的目录,classes.txt是预置类别文件(每行一个类名)。启动后快捷键分别为:W画框、D切下一张、A切上一张、Ctrl+S保存。这里值得强调:标注前先把类别文件写好,避免中途改类名导致 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.txt |
4.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 连续 ID;YOLO 不能接受稀疏 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 的映射规则。把上面三类验证做成一条命令串起来,每次数据集转换后自动执行一次,把输出截图或日志存档,整个数据链路才算闭环。
本文还有配套的精品资源,点击获取