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 * heightiscrowd: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 标注质量检查
标注质量直接决定模型上限。我见过一个项目,标注员把缺陷框画得比实际大一圈,结果模型学出来的检测框也偏大,漏检率居高不下。
我常用的检查方法有这几个:
- 可视化检查:把标注框画在原图上,肉眼扫一遍。重点关注框是否贴合目标边缘。
- 统计分布检查:统计所有标注框的宽高比、面积分布。如果出现异常值(比如宽高比超过10),大概率是标注错了。
- 类别平衡检查:统计每个类别的目标数量。如果某个类别样本太少,需要补充标注。
这里给一个简单的统计脚本:
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'])
避坑指南:
我曾经因为标注文件里混入了空行,导致训练时数据加载报错。后来我在脚本里加了空行过滤,再也没出过这个问题。建议你也加上。
最后说一句,标注质量检查不是一次性工作。我建议在训练过程中,每隔一段时间就随机抽一批标注结果看看。因为标注员可能会疲劳,后期标注质量下降是常有的事。及时发现、及时纠正,才能保证模型效果。