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 知识结构图

下面这张图帮你理清三种格式的转换关系:

标注格式转换关系图 COCO (JSON) VOC (XML) YOLO (TXT) pycocotools 自定义脚本 反向转换(需还原绝对坐标) 💡 转换核心原则 1. 坐标格式转换:绝对 ↔ 归一化 2. 类别 ID 映射:名称 ↔ 数字索引 COCO:通用数据集标准 VOC:老牌数据集格式 YOLO:轻量训练格式

3.7 避坑指南

最后,分享几个我实际项目中遇到的坑:

  • 坐标越界:YOLO 的归一化坐标理论上在 [0, 1] 之间,但标注时偶尔会标出图片边界。我习惯在转换后加一个 clip 操作,把坐标限制在 [0, 1] 内。
  • 类别 ID 不一致:不同数据集的类别编号可能不同。比如 COCO 的「car」是 3,你的项目里可能是 0。转换时一定要做一次映射。
  • 空标注文件:有些图片没有目标,YOLO 格式会生成一个空 TXT。训练时记得检查,否则数据加载器会报错。

我的建议:不管用什么格式,都先写一个可视化脚本,把标注框画到图片上看看。肉眼过一遍,比跑十次训练都管用。