4、数据标注与格式转换:COCO格式详解、YOLO格式详解、格式互转脚本编写、标注质量检查

数据标注这件事,说实话,是工业检测项目里最磨人、也最容易出问题的环节。我见过太多团队,模型结构调得花里胡哨,最后发现是标注格式搞错了,白白浪费几周时间。今天咱们就把COCO和YOLO这两种主流格式彻底聊透,再手写一个转换脚本,最后聊聊怎么检查标注质量——这些可都是我在产线上踩过的坑。

4.1 COCO格式详解

COCO格式,全称是Common Objects in Context。它用JSON文件来存储标注信息。你想想看,一个JSON文件里要装下图片路径、类别、目标框、甚至分割多边形,结构自然复杂一些。

核心结构长这样:

{
    "images": [
        {
            "id": 1,
            "file_name": "img_001.jpg",
            "width": 1920,
            "height": 1080
        }
    ],
    "annotations": [
        {
            "id": 1,
            "image_id": 1,
            "category_id": 1,
            "bbox": [100, 200, 300, 400],
            "area": 120000,
            "iscrowd": 0
        }
    ],
    "categories": [
        {
            "id": 1,
            "name": "defect",
            "supercategory": "object"
        }
    ]
}

这里有个细节——bbox的格式是 [x, y, width, height],也就是左上角坐标加宽高。我在项目中遇到过有人把它和YOLO格式搞混,结果训练出来的模型检测框全偏了。嗯,这里要注意。

关键字段说明:

  • image_id:必须和images里的id对应,否则数据就串了
  • category_id:从1开始编号,0通常留给背景
  • area:bbox的面积,计算方式就是width * height
  • iscrowd:0表示单个目标,1表示密集人群/物体群

4.2 YOLO格式详解

YOLO格式就简单多了。每个图片对应一个同名的txt文件。每行代表一个目标,格式是:

<class_id> <x_center> <y_center> <width> <height>

注意,这里的坐标都是归一化的。说白了,就是把像素坐标除以图片的宽和高,变成0到1之间的小数。

举个例子:

0 0.5 0.5 0.2 0.3
1 0.8 0.6 0.1 0.15

第一行表示类别0的目标,中心点在图片正中央,宽占20%,高占30%。

我曾经踩过的坑:

YOLO格式的坐标是归一化的,但很多人会忘记除以图片尺寸。结果就是训练时loss直接炸掉。我建议你在转换后,随机挑几张图,把框画出来看看,一眼就能发现问题。

4.3 格式互转脚本编写

下面是我自己一直在用的转换脚本。它能把COCO格式转成YOLO格式。反过来也类似,只是方向不同。

import json
import os

def coco_to_yolo(coco_json_path, output_dir):
    with open(coco_json_path, 'r') as f:
        coco_data = json.load(f)
    
    # 建立类别映射
    categories = {cat['id']: idx for idx, cat in enumerate(coco_data['categories'])}
    
    # 建立图片信息映射
    images = {img['id']: img for img in coco_data['images']}
    
    # 按图片分组标注
    annotations_by_image = {}
    for ann in coco_data['annotations']:
        img_id = ann['image_id']
        if img_id not in annotations_by_image:
            annotations_by_image[img_id] = []
        annotations_by_image[img_id].append(ann)
    
    # 逐图片生成YOLO标注
    for img_id, anns in annotations_by_image.items():
        img_info = images[img_id]
        img_w = img_info['width']
        img_h = img_info['height']
        
        txt_name = os.path.splitext(img_info['file_name'])[0] + '.txt'
        txt_path = os.path.join(output_dir, txt_name)
        
        with open(txt_path, 'w') as f:
            for ann in anns:
                # COCO格式: [x, y, w, h]
                x, y, w, h = ann['bbox']
                # 转成YOLO格式: 中心点 + 宽高,归一化
                x_center = (x + w / 2) / img_w
                y_center = (y + h / 2) / img_h
                w_norm = w / img_w
                h_norm = h / img_h
                
                class_id = categories[ann['category_id']]
                f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n")
    
    print(f"转换完成,共处理 {len(annotations_by_image)} 张图片")

# 使用示例
coco_to_yolo('annotations.json', 'labels/')

个人经验:

我习惯在转换脚本里加一个校验函数。比如检查归一化后的坐标是否在0到1之间,如果超出范围就报错。这能帮你提前发现标注框超出图片边界的问题。

4.4 标注质量检查

标注质量直接决定模型上限。我见过一个项目,标注员把缺陷框画得比实际大一圈,结果模型学出来的检测框也偏大,漏检率居高不下。

我常用的检查方法有这几个:

  1. 可视化检查:把标注框画在原图上,肉眼扫一遍。重点关注框是否贴合目标边缘。
  2. 统计分布检查:统计所有标注框的宽高比、面积分布。如果出现异常值(比如宽高比超过10),大概率是标注错了。
  3. 类别平衡检查:统计每个类别的目标数量。如果某个类别样本太少,需要补充标注。

这里给一个简单的统计脚本:

import os

def check_yolo_labels(label_dir, img_dir):
    stats = {'total_boxes': 0, 'classes': {}}
    
    for txt_file in os.listdir(label_dir):
        if not txt_file.endswith('.txt'):
            continue
        
        with open(os.path.join(label_dir, txt_file), 'r') as f:
            lines = f.readlines()
        
        for line in lines:
            parts = line.strip().split()
            class_id = int(parts[0])
            x, y, w, h = map(float, parts[1:])
            
            stats['total_boxes'] += 1
            stats['classes'][class_id] = stats['classes'].get(class_id, 0) + 1
            
            # 检查坐标是否合理
            if not (0 <= x <= 1 and 0 <= y <= 1 and 0 <= w <= 1 and 0 <= h <= 1):
                print(f"警告:{txt_file} 中存在异常坐标")
    
    print(f"总标注框数:{stats['total_boxes']}")
    print("各类别数量:", stats['classes'])

避坑指南:

我曾经因为标注文件里混入了空行,导致训练时数据加载报错。后来我在脚本里加了空行过滤,再也没出过这个问题。建议你也加上。

最后说一句,标注质量检查不是一次性工作。我建议在训练过程中,每隔一段时间就随机抽一批标注结果看看。因为标注员可能会疲劳,后期标注质量下降是常有的事。及时发现、及时纠正,才能保证模型效果。