当你第一次接触目标检测任务时,可能会被各种数据格式搞得晕头转向。COCO和YOLO是两种最常见的格式,但它们的设计理念完全不同。COCO JSON就像是一个精心整理的档案柜,把所有图片和标注信息都打包在一个大文件里;而YOLO TXT更像是便利贴,每张图片对应一个简单的文本文件,只记录最关键的检测框信息。
我在实际项目中遇到过这样的场景:好不容易下载了一个COCO格式的数据集,但发现自己的YOLOv5模型无法直接使用。这时候就需要进行格式转换。COCO JSON文件包含了太多训练时用不到的元数据(比如图片拍摄时间、授权信息等),而YOLO只需要知道每个物体是什么类别、在图片的什么位置就够了。
最让人头疼的是坐标系的差异。COCO使用绝对像素坐标,标注的是左上角点坐标和宽高;而YOLO使用相对坐标,需要的是中心点坐标和归一化后的宽高。这就好比一个是说"从客厅东北角开始,往南3米、往西2米",另一个是说"在整个房子的正中间偏右30%、偏下20%的位置"。
打开一个典型的COCO JSON文件,你会看到一个巨大的字典结构。我把它比喻成一个五层楼的建筑:
info层:相当于大楼的门牌,记录着数据集的基本信息。这里通常有数据集版本、创建日期等,但对我们的转换工作影响不大。
licenses层:版权声明区。虽然法律上很重要,但技术上我们可以暂时忽略。
images层:图片档案室。每张图片在这里都有一个身份证,包括:
json复制{
"id": 0,
"file_name": "0000001.jpg",
"width": 640,
"height": 480
}
这个宽度和高度信息至关重要,因为后续的坐标转换都需要它。
annotations层:真正的金矿所在。每个标注对象都包含:
json复制{
"id": 1,
"image_id": 0,
"category_id": 2,
"bbox": [100, 200, 50, 80],
"iscrowd": 0
}
这里的bbox就是我们要重点处理的对象。
categories层:类别目录。把数字ID映射到具体类别名,比如:
json复制{
"id": 1,
"name": "person",
"supercategory": "animal"
}
在实际处理中,有几个坑我踩过多次:
iscrowd标记:当这个值为1时,表示多个物体挤在一起难以区分。有些数据集会用这个标记来跳过困难样本,我们需要决定是否保留这些标注。
segmentation字段:虽然我们主要关注bbox,但有些标注只有多边形分割信息。这时需要计算外接矩形:
python复制import numpy as np
points = np.array(segmentation).reshape(-1,2)
x_min, y_min = points.min(axis=0)
x_max, y_max = points.max(axis=0)
bbox = [x_min, y_min, x_max-x_min, y_max-y_min]
无效标注:偶尔会遇到宽度或高度为0的bbox,这种标注需要过滤掉。
YOLO的标签文件简单得令人感动。每行代表一个检测对象,格式如下:
code复制<class_id> <x_center> <y_center> <width> <height>
所有坐标值都是相对于图片宽高的比例值,范围在0到1之间。举个例子,如果一个500x400的图片上有个中心在(250,200),宽高为100x80的物体,那么对应的YOLO标注应该是:
code复制0 0.5 0.5 0.2 0.2
需要注意的是,YOLOv5和YOLOv8的官方实现都使用上述格式。但有些老版本或者变种可能会有所不同:
建议在转换前先用labelImg等工具手动标注几张图片,确认你的模型具体需要什么格式。
下面这个Python脚本是我在实际项目中反复打磨过的版本,比原始文章中的更健壮:
python复制import json
from pathlib import Path
def coco2yolo(coco_json_path, output_dir):
# 创建输出目录
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# 加载COCO数据
with open(coco_json_path) as f:
coco_data = json.load(f)
# 建立image_id到文件名的映射
image_info = {img['id']: img for img in coco_data['images']}
# 按image_id分组annotations
from collections import defaultdict
img_to_anns = defaultdict(list)
for ann in coco_data['annotations']:
if ann['iscrowd']: # 跳过拥挤区域
continue
img_to_anns[ann['image_id']].append(ann)
# 处理每张图片
for img_id, anns in img_to_anns.items():
img = image_info[img_id]
img_w, img_h = img['width'], img['height']
# 生成对应的YOLO标签文件
txt_path = output_dir / f"{Path(img['file_name']).stem}.txt"
with open(txt_path, 'w') as f:
for ann in anns:
# 转换bbox格式
x, y, w, h = ann['bbox']
x_center = x + w/2
y_center = y + h/2
# 归一化
x_center /= img_w
y_center /= img_h
w /= img_w
h /= img_h
# 写入文件
line = f"{ann['category_id']} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n"
f.write(line)
if __name__ == '__main__':
coco2yolo('train.json', 'labels')
相比基础版本,这个脚本做了以下改进:
在实际项目中,我还遇到过这些特殊情况:
图片找不到的情况:有些JSON里的图片可能在你的本地不存在,需要添加检查:
python复制if not (img_dir / img['file_name']).exists():
print(f"Warning: {img['file_name']} not found")
continue
类别ID重映射:有时候需要把COCO的类别ID重新编号:
python复制# 假设我们只关心person和car两类
class_map = {1:0, 2:1} # COCO中person是1,car是2
if ann['category_id'] not in class_map:
continue
class_id = class_map[ann['category_id']]
验证转换结果:可以用这个函数快速检查转换是否正确:
python复制def visualize_annotation(img_path, txt_path):
import cv2
img = cv2.imread(str(img_path))
h, w = img.shape[:2]
with open(txt_path) as f:
for line in f:
cls_id, xc, yc, bw, bh = map(float, line.split())
x1 = int((xc - bw/2) * w)
y1 = int((yc - bh/2) * h)
x2 = int((xc + bw/2) * w)
y2 = int((yc + bh/2) * h)
cv2.rectangle(img, (x1,y1), (x2,y2), (0,255,0), 2)
cv2.imshow('check', img)
cv2.waitKey(0)
转换完成后,千万别直接开始训练!我吃过这个亏,浪费了好几天时间。以下是必做的检查:
随机抽样检查:选5-10张图片,用OpenCV画出转换后的标注框,肉眼确认位置是否正确。
统计类别分布:检查每个类别的样本数量是否合理。有时候转换过程可能会意外过滤掉某些类别。
检查坐标范围:所有坐标值应该在0到1之间。如果出现负数或大于1的值,说明转换逻辑有问题。
验证文件对应关系:确保每个图片文件都有对应的标签文件,且文件名能正确匹配。
根据我的踩坑经验,以下是几个典型问题及解决方法:
问题1:转换后的标注框位置偏移
问题2:类别ID全部为0
问题3:转换后图片和标注对不上
Path.stem而不是简单替换后缀当处理大规模数据集时,原始的方法可能会非常慢。以下是几个提升效率的技巧:
多进程处理:对于数万张图片的数据集,可以使用Python的multiprocessing:
python复制from multiprocessing import Pool
def process_image(args):
img_id, anns = args
# 转换逻辑...
if __name__ == '__main__':
with Pool(8) as p: # 使用8个进程
p.map(process_image, img_to_anns.items())
增量式处理:如果内存不足,可以逐图片处理:
python复制for img in coco_data['images']:
img_anns = [a for a in coco_data['annotations'] if a['image_id'] == img['id']]
# 处理单张图片...
进度显示:对于长时间运行的任务,添加进度条:
python复制from tqdm import tqdm
for img_id, anns in tqdm(img_to_anns.items(), desc="Processing"):
# 转换逻辑...
结果缓存:如果可能中途中断,可以保存处理状态:
python复制processed = set()
if (output_dir / 'processed.txt').exists():
with open(output_dir / 'processed.txt') as f:
processed = set(f.read().splitlines())
for img_id, anns in img_to_anns.items():
if img_id in processed:
continue
# 处理图片...
with open(output_dir / 'processed.txt', 'a') as f:
f.write(f"{img_id}\n")
在实际项目中,我们通常需要把转换流程嵌入到更大的工作流中。以下是几种常见场景:
可以直接在训练前添加转换步骤:
python复制def prepare_training_data(coco_path, yolo_dir):
if not list(yolo_dir.glob('*.txt')):
coco2yolo(coco_path, yolo_dir)
# 生成data.yaml
with open(yolo_dir/'data.yaml', 'w') as f:
yaml.dump({
'train': 'images/train',
'val': 'images/val',
'nc': len(categories),
'names': [c['name'] for c in categories]
}, f)
可以创建一个转换专用的Docker镜像:
dockerfile复制FROM python:3.8
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY coco2yolo.py .
ENTRYPOINT ["python", "coco2yolo.py"]
然后这样使用:
bash复制docker run -v $(pwd)/data:/data coco2yolo /data/train.json /data/labels
如果需要频繁转换,可以做成Web服务:
python复制from fastapi import FastAPI, UploadFile
import tempfile
app = FastAPI()
@app.post("/convert")
async def convert(file: UploadFile):
with tempfile.NamedTemporaryFile() as tmp:
content = await file.read()
tmp.write(content)
tmp.flush()
coco2yolo(tmp.name, "output")
return {"status": "success"}
如果JSON文件同时包含检测和分割标注,我们可以扩展脚本:
python复制if 'segmentation' in ann and ann['segmentation']:
# 保存分割信息到单独文件
seg_path = output_dir / f"{Path(img['file_name']).stem}_seg.txt"
with open(seg_path, 'w') as f:
json.dump(ann['segmentation'], f)
同样的思路可以应用于其他格式转换。比如Pascal VOC转YOLO:
python复制def voc2yolo(xml_path, output_dir):
from xml.etree import ElementTree as ET
tree = ET.parse(xml_path)
root = tree.getroot()
size = root.find('size')
img_w = int(size.find('width').text)
img_h = int(size.find('height').text)
with open(output_dir / f"{Path(xml_path).stem}.txt", 'w') as f:
for obj in root.iter('object'):
cls_name = obj.find('name').text
bbox = obj.find('bndbox')
x1 = int(bbox.find('xmin').text)
y1 = int(bbox.find('ymin').text)
x2 = int(bbox.find('xmax').text)
y2 = int(bbox.find('ymax').text)
# 转换逻辑...
可以在转换过程中直接应用一些简单的增强:
python复制import random
def random_flip(bbox, img_w, img_h, p=0.5):
if random.random() < p:
x_center = 1.0 - bbox[0]
bbox[0] = x_center
return bbox
# 在转换循环中
bbox = random_flip(bbox, img_w, img_h)
在多个实际项目中应用这个转换流程后,我总结出以下几点经验:
保持原始数据备份:永远不要直接修改原始COCO JSON文件。我习惯在转换前先复制一份:
bash复制cp train.json train_backup.json
版本控制转换脚本:每次改进转换逻辑时,都保存一个新版本:
code复制coco2yolo_v1.py
coco2yolo_v2_fix_bbox_order.py
记录转换日志:特别是处理大型数据集时,记录下过滤掉了哪些标注:
python复制with open('conversion.log', 'a') as log:
if w == 0 or h == 0:
log.write(f"Invalid bbox in image {img_id}\n")
考虑使用现成工具:对于标准COCO数据集,可以考虑使用官方工具:
python复制from pycocotools.coco import COCO
coco = COCO('train.json')
团队协作时的注意事项:如果多人共同处理,要明确约定:
让我们通过一个具体例子把前面讲的内容串起来。假设我们有一个自定义的COCO格式数据集,包含以下特点:
我们的转换脚本需要处理所有这些情况。以下是完整解决方案的关键部分:
python复制def custom_coco2yolo(coco_path, output_dir):
# 初始化
output_dir = Path(output_dir)
output_dir.mkdir(exist_ok=True)
# 自定义类别映射
CLASS_MAP = {10:0, 11:1, 12:2, 13:3, 14:4}
# 加载数据
with open(coco_path) as f:
data = json.load(f)
# 处理每张图片
for img in data['images']:
# 清理文件名
clean_name = Path(img['file_name']).name.replace(" ", "_")
txt_path = output_dir / f"{clean_name}.txt"
# 收集该图片的所有标注
anns = [a for a in data['annotations']
if a['image_id'] == img['id'] and not a['iscrowd']]
with open(txt_path, 'w') as f:
for ann in anns:
# 检查类别是否在映射中
if ann['category_id'] not in CLASS_MAP:
continue
# 获取图片尺寸
img_w, img_h = img['width'], img['height']
# 转换bbox
x, y, w, h = ann['bbox']
# 处理无效标注
if w <= 0 or h <= 0:
continue
# 计算YOLO格式坐标
x_center = (x + w/2) / img_w
y_center = (y + h/2) / img_h
w /= img_w
h /= img_h
# 写入文件
line = f"{CLASS_MAP[ann['category_id']]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n"
f.write(line)
这个案例展示了如何根据实际需求调整基础转换逻辑。关键在于理解原理后灵活应用,而不是死记硬背代码。