3. 标注格式详解:COCO、VOC、YOLO 格式的差异与转换
做数据标注,说白了就是在教机器「看」东西。但你教它的「语言」不一样,它理解的方式也就不一样。
我刚开始带项目时,团队里有人用 VOC,有人用 YOLO,还有人直接丢给我一堆 JSON。结果模型训练时,数据加载器报错报了一整天。嗯,从那以后我定了个规矩:项目启动前,先把标注格式统一了。
这一章,咱们就把三种主流格式掰开揉碎讲清楚。你搞懂了它们的差异,转换起来自然顺手。
3.1 三种格式的核心差异
先看一张总览表,心里有个底:
| 维度 | COCO | VOC | YOLO |
|---|---|---|---|
| 文件类型 | 单个 JSON | 每张图一个 XML | 每张图一个 TXT |
| 坐标格式 | 左上角 (x, y) + 宽高 (w, h) | 左上角 (xmin, ymin) + 右下角 (xmax, ymax) | 归一化的中心点 (cx, cy) + 宽高 (w, h) |
| 类别管理 | JSON 内嵌 categories 数组 | XML 内嵌 name 标签 | 类别 ID 从 0 开始,需额外映射文件 |
| 适用场景 | 检测、分割、关键点 | 检测、分割 | 检测(YOLO 系列) |
| 文件数量 | 1 个 JSON 管全部 | N 张图 = N 个 XML | N 张图 = N 个 TXT |
你想想看,COCO 把所有信息塞进一个 JSON,管理起来方便,但文件大了加载慢。VOC 每个 XML 独立,好读好改,但文件多了找起来头疼。YOLO 最轻量,一个 TXT 几行数字,训练时读取速度飞快——这也是它能在边缘设备上跑的原因之一。
我的经验:如果你做的是通用目标检测,YOLO 格式最省事。但如果你要做实例分割或关键点检测,老老实实用 COCO。VOC 格式现在用得少了,不过很多老数据集还是它,转换工具得备着。
3.2 COCO 格式详解
COCO 格式的核心是一个 JSON 字典。结构大概是这样的:
{
"images": [
{"id": 1, "file_name": "001.jpg", "width": 640, "height": 480}
],
"annotations": [
{
"id": 1,
"image_id": 1,
"category_id": 3,
"bbox": [100, 150, 200, 300],
"area": 60000,
"segmentation": [...],
"iscrowd": 0
}
],
"categories": [
{"id": 3, "name": "car", "supercategory": "vehicle"}
]
}
这里要注意几个点:
- bbox 是 [x, y, width, height],x 和 y 是左上角坐标。
- area 是 bbox 的面积,有些工具会校验这个值。
- iscrowd 表示目标是否密集重叠,0 是单目标,1 是群体。
小技巧:我习惯用 pycocotools 来读写 COCO 格式。它自带可视化工具,能帮你快速检查标注有没有画歪。
3.3 VOC 格式详解
VOC 格式每个 XML 文件对应一张图片。结构清晰,但写起来啰嗦:
<annotation>
<folder>images</folder>
<filename>001.jpg</filename>
<size>
<width>640</width>
<height>480</height>
<depth>3</depth>
</size>
<object>
<name>car</name>
<bndbox>
<xmin>100</xmin>
<ymin>150</ymin>
<xmax>300</xmax>
<ymax>450</ymax>
</bndbox>
</object>
</annotation>
VOC 的坐标是 绝对坐标,直接标出框的四个角。好处是直观,坏处是图片尺寸一变,坐标就得重新算。
我曾经踩过的坑:有一次从 VOC 转 YOLO,忘了把 xmax 和 ymax 转成宽高,结果训练出来的模型框全偏了。记住:VOC 是两点,YOLO 是中心点+宽高,转换时千万别搞混。
3.4 YOLO 格式详解
YOLO 格式最简单,一个 TXT 文件里每行一个目标:
3 0.546875 0.468750 0.312500 0.625000
1 0.156250 0.312500 0.125000 0.250000
每行格式:class_id cx cy width height。
- class_id 从 0 开始,需要额外一个
classes.txt文件做映射。 - cx, cy, width, height 都是归一化到 [0, 1] 的浮点数。
- 归一化公式:
cx = (xmin + xmax) / 2 / image_width。
为什么 YOLO 要用归一化坐标?说白了,就是为了让模型不关心图片原始尺寸。你训练时用 640x640,部署时用 1280x1280,坐标值不用改。
3.5 格式转换实战
下面是我自己写的一个转换脚本片段,把 VOC 转成 YOLO。核心逻辑就几步:
import xml.etree.ElementTree as ET
def voc_to_yolo(xml_path, classes):
tree = ET.parse(xml_path)
root = tree.getroot()
# 获取图片尺寸
width = int(root.find('size/width').text)
height = int(root.find('size/height').text)
yolo_lines = []
for obj in root.findall('object'):
name = obj.find('name').text
class_id = classes.index(name)
# 读取 VOC 坐标
xmin = int(obj.find('bndbox/xmin').text)
ymin = int(obj.find('bndbox/ymin').text)
xmax = int(obj.find('bndbox/xmax').text)
ymax = int(obj.find('bndbox/ymax').text)
# 转成 YOLO 归一化坐标
cx = (xmin + xmax) / 2.0 / width
cy = (ymin + ymax) / 2.0 / height
w = (xmax - xmin) / width
h = (ymax - ymin) / height
yolo_lines.append(f"{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}")
return yolo_lines
这段代码我用了好几年,基本没出过问题。你只需要准备一个 classes 列表,比如 ['person', 'car', 'dog'],脚本会自动匹配类别 ID。
3.6 知识结构图
下面这张图帮你理清三种格式的转换关系:
3.7 避坑指南
最后,分享几个我实际项目中遇到的坑:
- 坐标越界:YOLO 的归一化坐标理论上在 [0, 1] 之间,但标注时偶尔会标出图片边界。我习惯在转换后加一个
clip操作,把坐标限制在 [0, 1] 内。 - 类别 ID 不一致:不同数据集的类别编号可能不同。比如 COCO 的「car」是 3,你的项目里可能是 0。转换时一定要做一次映射。
- 空标注文件:有些图片没有目标,YOLO 格式会生成一个空 TXT。训练时记得检查,否则数据加载器会报错。
我的建议:不管用什么格式,都先写一个可视化脚本,把标注框画到图片上看看。肉眼过一遍,比跑十次训练都管用。