☰
VOC、COCO、YOLO格式互转实战:检测数据集制作与避坑指南
2026/10/1 5:37:14 网站建设 项目流程

1. 数据集制作的整体思路与格式选型逻辑

做检测任务的人都有一个共识:模型结构再花哨,数据拉胯,一切都是白搭。我见过太多人把 YOLO 的配置文件改得天花乱坠,结果训练出来的模型连基本的框都飘,回头一查,标注文件里类别索引对不上、坐标归一化算错、图片和标签文件名不匹配。所以这篇内容我想把“检测数据集制作”这件事从头到尾讲透,尤其是 VOC、COCO、YOLO 三种格式之间的互转,这块是坑最多的地方。

先明确一下这三种格式各自是什么定位。VOC 格式(Pascal VOC)是早期检测任务的事实标准,用 XML 文件存储每张图的标注信息,结构清晰、可读性强,适合人工检查和调试。COCO 格式用单个 JSON 文件管理整个数据集的标注,支持检测、分割、关键点等多种任务,是学术界和大型竞赛的主流格式。YOLO 格式则是为 YOLO 系列模型量身定制的轻量格式,每张图对应一个 txt 文件,每行一个目标,坐标是归一化后的中心点加宽高,训练时读取效率极高。

为什么必须搞懂互转?因为实际工作中,你拿到的数据来源五花八门。从开源数据集下载的可能是 COCO JSON,从标注工具导出的可能是 VOC XML,而你要训练的 YOLO 模型只认 txt。如果每次都重新标注,成本高到离谱。掌握互转能力,等于把任意来源的数据都能盘活。

提示:格式互转的核心难点不在代码本身,而在于类别映射、坐标归一化、以及边界情况的处理。后面我会逐一拆解。

整个数据集制作流程可以拆成四个阶段:数据收集与清洗、标注、格式转换、质量校验。这四个阶段环环相扣,任何一个环节出问题,后面都要返工。我个人的习惯是,在标注之前就把格式转换的脚本准备好,标注完立刻转一遍做校验,这样能尽早发现标注规范的问题。

2. 数据收集与清洗的实操要点

2.1 数据来源与采集策略

数据收集这一步,很多人上来就开始爬图或者拍图,结果收集了几万张,真正能用的不到一半。我的经验是,先明确你的检测目标是什么,然后按“场景覆盖优先、数量其次”的原则来收集。

常见的数据来源有几类:公开数据集(如 COCO、VOC 官方数据)、网络爬取、自己拍摄、以及业务系统沉淀的历史数据。公开数据集适合做预训练或者补充通用类别,但往往和你的实际场景有域差异。自己拍摄的数据最贴合场景,但要注意采集时的多样性——光照、角度、遮挡、背景都要覆盖到。

举个例子,如果你做的是电力设备红外检测,那数据收集时就要考虑不同季节、不同时间段、不同拍摄距离下的红外图像。我见过一个项目,训练集全是晴天拍的,结果阴天部署时模型直接失效。这种问题不是模型能解决的,是数据收集阶段就埋下的雷。

2.2 数据清洗的关键动作

收集完的数据不能直接用,必须过一遍清洗。清洗的核心动作包括:

  • 去重:用感知哈希(pHash)或者简单的 MD5 去重,避免相似图片过多导致训练集分布偏斜。
  • 剔除损坏文件:有些图片下载下来是残缺的,用 PIL 或 OpenCV 批量读取一遍,读失败的直接删。
  • 分辨率过滤:太小的图(比如小于 100x100)标注意义不大,太大的图(比如超过 4K)训练时显存吃不消,建议统一缩放到合理范围。
  • 格式统一:把 jpg、png、bmp、webp 统一转成 jpg,减少后续读取的兼容性问题。
import os from PIL import Image from tqdm import tqdm def clean_images(img_dir, min_size=100, max_size=4096): valid_ext = {'.jpg', '.jpeg', '.png', '.bmp'} removed = 0 for fname in tqdm(os.listdir(img_dir)): ext = os.path.splitext(fname)[1].lower() if ext not in valid_ext: continue fpath = os.path.join(img_dir, fname) try: with Image.open(fpath) as im: w, h = im.size if w < min_size or h < min_size or w > max_size or h > max_size: os.remove(fpath) removed += 1 except Exception: os.remove(fpath) removed += 1 print(f"清洗完成,移除 {removed} 张无效图片")

这段脚本我用了很多次,基本能解决 90% 的脏数据问题。注意max_size不要设得太小,否则会把一些有价值的高分辨率图误删。如果你的场景确实需要高分辨率,可以单独保留一个高分辨率子集。

2.3 数据集划分的注意事项

清洗完之后要划分训练集、验证集、测试集。常见的比例是 7:2:1 或 8:1:1。但这里有个坑:划分必须按场景或按采集批次来分,不能随机分。如果你随机分,同一场景的相似图片可能同时出现在训练集和验证集里,导致验证指标虚高,实际部署时性能暴跌。

我的做法是,先给每张图打上场景标签(比如“室内-白天”“室外-夜晚”),然后按场景分层抽样。这样能保证验证集和测试集的分布与训练集一致,同时避免数据泄漏。

3. 标注工具选型与标注规范制定

3.1 标注工具怎么选

标注工具的选择直接影响到后续格式转换的顺畅程度。市面上主流的工具有 LabelImg、Labelme、CVAT、Roboflow 等。我简单对比一下:

工具输出格式适用场景优缺点
LabelImgVOC XML / YOLO txt矩形框检测轻量、离线、操作简单;不支持分割
LabelmeCOCO JSON / VOC XML检测+分割支持多边形标注;界面稍重
CVAT多种格式团队协作功能强大、支持视频;部署复杂
Roboflow多种格式在线协作一键导出多格式;免费版有额度限制

如果你只做矩形框检测,LabelImg 足够了,而且它可以直接导出 YOLO txt 和 VOC XML 两种格式,省去转换步骤。如果需要做实例分割,那就上 Labelme 或 CVAT。

注意:不管用什么工具,标注前一定要先制定标注规范文档,明确“什么算一个目标”“遮挡到什么程度不标”“边界框贴边还是留白”等细节。否则多人标注时一致性极差。

3.2 标注规范的核心条款

我整理了一份标注规范模板,基本覆盖了常见争议点:

  1. 目标定义:明确每个类别的定义,比如“行人”是否包含骑自行车的人,“车辆”是否包含摩托车。
  2. 遮挡处理:遮挡超过 50% 的目标不标,遮挡 20%-50% 的目标标可见部分,遮挡小于 20% 的正常标。
  3. 边界框精度:框要贴紧目标边缘,但不要切掉目标的一部分。对于模糊目标,框到肉眼可辨的边界即可。
  4. 小目标处理:小于 10x10 像素的目标不标,因为标注误差太大,训练时也是噪声。
  5. 类别互斥:一个目标只能属于一个类别,不允许出现“既算 A 又算 B”的情况。

这份规范看起来简单,但实际标注时能省掉大量返工。我见过一个团队,标注了 5000 张图之后才发现两个人对“遮挡”的理解不一致,结果全部重标。

3.3 标注质量抽检方法

标注完成后不能直接用来训练,必须抽检。抽检的方法是:随机抽取 5%-10% 的图片,人工核对标注框和类别是否正确。如果错误率超过 5%,说明标注规范执行不到位,需要扩大抽检比例甚至全检。

另外,可以用脚本做一些自动化校验,比如检查是否有空标注文件、坐标是否越界、类别索引是否超出范围等。这些校验能在格式转换之前就发现问题。

4. VOC、COCO、YOLO 三种格式的结构解析

4.1 VOC 格式详解

VOC 格式的核心是每张图对应一个 XML 文件,文件名与图片名一致,存放在Annotations目录下。图片存放在JPEGImages目录。XML 的结构如下:

<annotation> <folder>VOC2007</folder> <filename>000001.jpg</filename> <size> <width>500</width> <height>375</height> <depth>3</depth> </size> <object> <name>person</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>100</xmin> <ymin>50</ymin> <xmax>300</xmax> <ymax>350</ymax> </bndbox> </object> </annotation>

关键字段说明:bndbox里的坐标是绝对像素值,左上角为(xmin, ymin),右下角为(xmax, ymax)。difficult字段标记难样本,训练时可以选择忽略。truncated标记是否被截断。

VOC 格式的优点是直观、易调试,缺点是文件数量多(每张图一个 XML),数据集大了之后管理起来很繁琐。

4.2 COCO 格式详解

COCO 格式用一个 JSON 文件管理所有标注,结构分为images、annotations、categories三个主要部分:

{ "images": [ {"id": 1, "file_name": "000001.jpg", "width": 500, "height": 375} ], "annotations": [ { "id": 1, "image_id": 1, "category_id": 1, "bbox": [100, 50, 200, 300], "area": 60000, "iscrowd": 0 } ], "categories": [ {"id": 1, "name": "person", "supercategory": "none"} ] }

注意 COCO 的bbox格式是[x, y, width, height],其中x, y是左上角坐标,width, height是宽高,都是绝对像素值。这和 VOC 的[xmin, ymin, xmax, ymax]不同,转换时需要计算。

COCO 格式的优点是单文件管理、支持多种任务、生态完善,缺点是 JSON 文件大了之后(比如几十万张图)读写很慢,而且人工查看不方便。

4.3 YOLO 格式详解

YOLO 格式每张图对应一个 txt 文件,每行表示一个目标,格式为:

class_id center_x center_y width height

其中所有坐标都是归一化到 0-1 之间的相对值。center_x和center_y是目标中心点相对于图片宽高的比例,width和height是目标宽高相对于图片宽高的比例。

举个例子,一张 500x375 的图,VOC 标注为[100, 50, 300, 350],转成 YOLO 格式的计算过程是:

  • 中心点 x = (100 + 300) / 2 / 500 = 0.4
  • 中心点 y = (50 + 350) / 2 / 375 = 0.5333
  • 宽 = (300 - 100) / 500 = 0.4
  • 高 = (350 - 50) / 375 = 0.8

所以 YOLO 行是:0 0.4 0.5333 0.4 0.8。

YOLO 格式的优点是读取快、存储小、直接适配 YOLO 训练,缺点是可读性差、不包含图片尺寸信息(所以必须保证图片和标签一一对应)。

5. 格式互转的完整实现与参数计算

5.1 VOC 转 YOLO 的完整脚本

这是最常用的转换方向。核心步骤是:读取 XML、解析图片尺寸、计算归一化坐标、写入 txt。

import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(voc_anno_dir, img_dir, out_dir, class_map): os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(voc_anno_dir): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(voc_anno_dir, xml_file)) root = tree.getroot() img_name = root.find('filename').text img_path = os.path.join(img_dir, img_name) with Image.open(img_path) as im: w, h = im.size 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] bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) cx = (xmin + xmax) / 2 / w cy = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") txt_name = os.path.splitext(xml_file)[0] + '.txt' with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(lines))

这段脚本有几个细节要注意:坐标保留 6 位小数足够,太多位数没必要;类别映射用字典管理,方便增删类别;空标注文件也要生成(空 txt),否则 YOLO 训练时可能报错。

5.2 YOLO 转 VOC 的反向转换

反向转换的核心是把归一化坐标还原成绝对像素坐标,然后写入 XML。

def yolo_to_voc(yolo_dir, img_dir, out_dir, class_names): os.makedirs(out_dir, exist_ok=True) for txt_file in os.listdir(yolo_dir): if not txt_file.endswith('.txt'): continue img_name = os.path.splitext(txt_file)[0] + '.jpg' img_path = os.path.join(img_dir, img_name) with Image.open(img_path) as im: w, h = im.size root = ET.Element('annotation') ET.SubElement(root, 'filename').text = img_name size = ET.SubElement(root, 'size') ET.SubElement(size, 'width').text = str(w) ET.SubElement(size, 'height').text = str(h) ET.SubElement(size, 'depth').text = '3' with open(os.path.join(yolo_dir, txt_file)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id = int(parts[0]) cx, cy, bw, bh = map(float, parts[1:]) xmin = int((cx - bw / 2) * w) ymin = int((cy - bh / 2) * h) xmax = int((cx + bw / 2) * w) ymax = int((cy + bh / 2) * h) obj = ET.SubElement(root, '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(root) tree.write(os.path.join(out_dir, os.path.splitext(txt_file)[0] + '.xml'))

反向转换时要注意坐标取整后的边界问题。如果xmin算出来是负数,要截断到 0;如果xmax超过图片宽度,要截断到w。否则生成的 XML 在某些工具里会报错。

5.3 COCO 与 YOLO 互转的关键差异

COCO 转 YOLO 的难点在于 COCO 的bbox是[x, y, w, h],而 YOLO 需要中心点坐标。转换公式是:

  • cx = (x + w/2) / img_w
  • cy = (y + h/2) / img_h
  • bw = w / img_w
  • bh = h / img_h

反过来,YOLO 转 COCO 时:

  • x = (cx - bw/2) * img_w
  • y = (cy - bh/2) * img_h
  • w = bw * img_w
  • h = bh * img_h

另外,COCO 的category_id不一定是连续的,可能从 1 开始也可能从 0 开始,甚至跳号。转换时一定要建立category_id到 YOLOclass_id的映射表,不能想当然地直接用。

提示:COCO 的area字段是w * h,但有些数据集里area是分割面积,不一定等于 bbox 面积。转换时建议重新计算,不要直接复制。

6. 常见问题排查与避坑经验

6.1 格式转换中的典型报错与解决

问题现象可能原因解决方法
训练时 loss 为 nan坐标越界或宽高为负检查转换脚本,确保所有坐标在 0-1 之间
类别索引越界class_map 与训练配置不一致核对 data.yaml 中的 names 顺序
图片和标签不匹配文件名不一致或扩展名不同统一用图片名(不含扩展名)作为标签名
空标签文件导致报错YOLO 要求每张图都有对应 txt生成空 txt 文件
COCO 转 YOLO 后框偏移bbox 格式理解错误确认 COCO bbox 是 [x,y,w,h] 而非 [xmin,ymin,xmax,ymax]

6.2 坐标归一化的精度陷阱

归一化时如果用整数除法,会直接得到 0。Python 3 里/是浮点除法,没问题,但如果你从其他语言移植代码,要注意这一点。另外,坐标保留位数建议 6 位,太少会导致小目标框偏移明显,太多没必要且增加文件体积。

还有一个隐蔽的坑:图片尺寸读取不一致。标注时用的图片可能被缩放过了,但转换时读取的是原图,导致尺寸对不上。解决办法是,转换前统一用同一批图片,或者在标注文件里记录图片尺寸。

6.3 类别映射的一致性检查

类别映射是格式转换中最容易出错的地方。我建议在转换脚本里加一段校验逻辑:转换完成后,统计每个类别的实例数量,和原始数据集对比。如果数量对不上,说明有类别被漏掉或者映射错了。

def check_class_distribution(yolo_dir, num_classes): counts = {i: 0 for i in range(num_classes)} for txt_file in os.listdir(yolo_dir): with open(os.path.join(yolo_dir, txt_file)) as f: for line in f: parts = line.strip().split() if parts: counts[int(parts[0])] += 1 for cls_id, cnt in counts.items(): print(f"类别 {cls_id}: {cnt} 个实例")

这段代码跑一遍,心里就有底了。如果某个类别实例数为 0,要么是数据里确实没有,要么是映射错了。

6.4 数据集版本管理与复现

最后说一个容易被忽视的点:数据集版本管理。每次修改标注或转换格式后,都要保留一个版本快照。否则训练效果变差了,你都不知道是模型改了还是数据改了。我的做法是用dataset_v1、dataset_v2这样的目录命名,每次变更写一个简短的 changelog。

另外,转换脚本本身也要纳入版本管理。同一个数据集,用不同版本的脚本转换,结果可能不一样。把脚本和数据放在一起,才能保证实验可复现。

7. 从标注到训练的数据管线搭建建议

7.1 目录结构设计

一个清晰的数据集目录结构能省掉很多麻烦。我常用的结构是:

dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── annotations/ │ ├── voc/ │ └── coco/ ├── scripts/ │ ├── voc2yolo.py │ ├── yolo2voc.py │ └── check_dataset.py └── data.yaml

这样组织的好处是,YOLO 训练时直接指向images和labels目录即可,VOC 和 COCO 的原始标注保留在annotations里备查。

7.2 data.yaml 的配置要点

YOLO 训练依赖data.yaml文件,配置如下:

path: /path/to/dataset train: images/train val: images/val test: images/test nc: 3 names: 0: person 1: car 2: dog

注意names的顺序必须和转换时的class_map完全一致。我见过有人改了names顺序但没重新转换标签,结果模型把人和车搞反了。

7.3 训练前的最终校验清单

在启动训练之前,跑一遍这个校验清单:

  1. 图片和标签数量是否一致
  2. 每个类别的实例数量是否合理
  3. 坐标是否都在 0-1 范围内
  4. 是否有空标签文件(正常,但要知道有多少)
  5. data.yaml中的路径和类别是否与标签一致
  6. 随机抽 10 张图可视化标注框,肉眼确认

这六步做完,基本可以放心开训。如果训练过程中发现指标异常,回头查这六步,大概率能找到原因。

我个人在实际操作中的体会是,数据集制作这件事,花在清洗和校验上的时间永远不亏。标注阶段多花一小时定规范,转换阶段多花十分钟写校验,训练阶段就能少熬三个通宵。格式互转的代码网上到处都是,但真正决定成败的是对细节的把控——坐标精度、类别映射、边界处理,这些才是区分老手和新手的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询