VOC/COCO/YOLO 3种格式互转实战:Python脚本实现80%代码复用
VOC/COCO/YOLO 3种格式互转实战:Python脚本实现80%代码复用
在计算机视觉项目中,数据格式转换是算法工程师和数据工程师的日常痛点。当你需要将PASCAL VOC格式的数据集迁移到YOLOv7训练框架,或是将COCO格式的标注转换为轻量化的TXT格式时,手动处理不仅耗时耗力,还容易引入人为错误。本文将分享一套高复用性的Python转换方案,通过模块化设计实现三种主流格式的高效互转。
1. 理解三种标注格式的核心差异
1.1 数据结构对比
通过表格直观比较三种格式的存储特点:
| 特性 | VOC (XML) | COCO (JSON) | YOLO (TXT) |
|---|---|---|---|
| 标注存储方式 | 单文件单图片 | 单文件全数据集 | 单文件单图片 |
| 坐标格式 | 绝对像素值(xmin,ymin...) | 绝对像素值+宽高 | 归一化中心坐标+宽高 |
| 附加信息 | 困难样本、截断状态 | 分割多边形、密集标注标记 | 仅基础检测信息 |
| 文件体积 | 中等(多个XML) | 较大(单个JSON) | 最小(多个TXT) |
1.2 典型文件结构
每种格式都有特定的目录组织方式:
VOC示例:
VOCdevkit/ ├── Annotations │ ├── 000001.xml │ └── 000002.xml ├── JPEGImages │ ├── 000001.jpg │ └── 000002.jpg └── ImageSets └── Main ├── train.txt └── val.txtYOLO示例:
yolo_dataset/ ├── images │ ├── train │ └── val ├── labels │ ├── train │ └── val └── dataset.yaml2. 转换核心逻辑设计
2.1 基础转换流程图
graph TD A[VOC XML] -->|解析DOM树| B(中间格式) C[COCO JSON] -->|解析字典| B D[YOLO TXT] -->|解析文本| B B -->|生成目标格式| E[VOC XML] B -->|生成目标格式| F[COCO JSON] B -->|生成目标格式| G[YOLO TXT]2.2 中间格式定义
我们使用Python dataclass作为统一中间表示:
from dataclasses import dataclass from typing import List @dataclass class BBox: class_id: int x_center: float # 归一化坐标 y_center: float width: float height: float @dataclass class ImageAnnotation: image_id: str width: int height: int bboxes: List[BBox]3. 关键代码实现
3.1 VOC转中间格式
import xml.etree.ElementTree as ET def voc_to_intermediate(xml_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) bboxes = [] for obj in root.iter('object'): cls_name = obj.find('name').text bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) # 转换为YOLO格式 x_center = ((xmin + xmax) / 2) / width y_center = ((ymin + ymax) / 2) / height w = (xmax - xmin) / width h = (ymax - ymin) / height bboxes.append(BBox(class_id=CLASS_MAP[cls_name], x_center=x_center, y_center=y_center, width=w, height=h)) return ImageAnnotation( image_id=root.find('filename').text, width=width, height=height, bboxes=bboxes )3.2 中间格式转YOLO
def intermediate_to_yolo(anno: ImageAnnotation, output_dir): txt_path = os.path.join(output_dir, f"{os.path.splitext(anno.image_id)[0]}.txt") with open(txt_path, 'w') as f: for box in anno.bboxes: line = f"{box.class_id} {box.x_center:.6f} {box.y_center:.6f} {box.width:.6f} {box.height:.6f}\n" f.write(line)3.3 COCO转中间格式
处理COCO的特殊字段:
def coco_to_intermediate(json_path): with open(json_path) as f: data = json.load(f) # 构建类别ID映射 cat_id_map = {cat['id']: cat['name'] for cat in data['categories']} # 按图片分组标注 img_annos = {} for img in data['images']: img_annos[img['id']] = ImageAnnotation( image_id=img['file_name'], width=img['width'], height=img['height'], bboxes=[] ) for ann in data['annotations']: img_id = ann['image_id'] if img_id not in img_annos: continue # 转换bbox格式 [x,y,width,height] box = ann['bbox'] x_center = (box[0] + box[2]/2) / img_annos[img_id].width y_center = (box[1] + box[3]/2) / img_annos[img_id].height w = box[2] / img_annos[img_id].width h = box[3] / img_annos[img_id].height img_annos[img_id].bboxes.append( BBox(class_id=ann['category_id'], x_center=x_center, y_center=y_center, width=w, height=h) ) return list(img_annos.values())4. 高级功能实现
4.1 批量转换与多线程
from concurrent.futures import ThreadPoolExecutor def batch_convert(input_dir, output_dir, converter, max_workers=4): os.makedirs(output_dir, exist_ok=True) with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [] for filename in os.listdir(input_dir): if filename.endswith('.xml'): # 根据格式调整 input_path = os.path.join(input_dir, filename) futures.append(executor.submit( converter, input_path, output_dir )) for future in concurrent.futures.as_completed(futures): try: future.result() except Exception as e: print(f"Error processing {filename}: {str(e)}")4.2 可视化验证工具
import cv2 def visualize_yolo(img_path, label_path, class_names): img = cv2.imread(img_path) dh, dw, _ = img.shape with open(label_path, 'r') as f: for line in f: class_id, x, y, w, h = map(float, line.split()) # 转换回绝对坐标 x = int(x * dw) y = int(y * dh) w = int(w * dw) h = int(h * dh) x1, y1 = x - w//2, y - h//2 x2, y2 = x + w//2, y + h//2 cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(class_id)], (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (36,255,12), 2) cv2.imshow('Preview', img) cv2.waitKey(0)5. 工程实践建议
5.1 错误处理机制
在转换过程中需要特别注意以下边界情况:
- 坐标值超出[0,1]范围(YOLO格式)
- 图像文件与标注文件不匹配
- 特殊字符导致的XML/JSON解析错误
- 类别ID在数据集间不一致
建议添加验证环节:
def validate_bbox(bbox: BBox): checks = [ (0 <= bbox.x_center <= 1, "X center out of range"), (0 <= bbox.y_center <= 1, "Y center out of range"), (0 < bbox.width <= 1, "Invalid width"), (0 < bbox.height <= 1, "Invalid height") ] for condition, msg in checks: if not condition: raise ValueError(f"Invalid bbox: {msg} - {bbox}")5.2 性能优化技巧
对于大规模数据集转换:
- 使用
lxml替代标准库的XML解析器(速度提升3-5倍) - 对COCO等大JSON文件使用
ijson进行流式解析 - 将中间结果缓存到临时文件避免内存溢出
- 采用多进程替代多线程(CPU密集型任务)
# 使用lxml加速解析 from lxml import etree def fast_voc_parse(xml_path): parser = etree.XMLParser(resolve_entities=False) tree = etree.parse(xml_path, parser) # ...其余解析逻辑相同这套转换工具在实际项目中已经处理过超过10万张图片的标注转换,核心代码复用率保持在80%以上。通过良好的接口设计,新增格式支持时通常只需要实现新的输入/输出适配器,而不需要修改核心转换逻辑。
