YOLO 数据集转换:3种JSON格式解析与Python脚本适配实战
📅 2026/7/8 22:59:40
👁️ 阅读次数
📝 编程学习
YOLO 数据集转换:3种JSON格式解析与Python脚本适配实战
在计算机视觉项目中,数据格式的兼容性往往是第一个需要攻克的堡垒。当你从不同来源获取标注数据时,可能会遇到COCO、Labelme和自定义JSON等多种格式,而YOLO训练却要求统一的TXT格式。本文将深入解析这三种主流JSON格式的结构差异,并提供可复用的Python转换方案,助你打通数据预处理的关键环节。
1. 理解YOLO标注格式的核心要求
YOLO所需的TXT标注文件遵循严格的规范化标准。每个图像对应一个同名文本文件,其中每行表示一个对象的标注信息,格式为:
<class_id> <x_center> <y_center> <width> <height>所有坐标值必须是归一化的(0到1之间),计算方式如下:
x_center= 边界框中心x坐标 / 图像宽度y_center= 边界框中心y坐标 / 图像高度width= 边界框宽度 / 图像宽度height= 边界框高度 / 图像高度
关键检查点:
- 确认坐标值是否已完成归一化
- 验证class_id是否从0开始连续编号
- 检查图像尺寸与标注是否匹配
2. COCO JSON格式解析与转换
COCO(Common Objects in Context)是当前最通用的目标检测数据集格式,其标注文件包含以下核心结构:
{ "images": [{"id": 0, "width": 640, "height": 480, "file_name": "001.jpg"}], "annotations": [ { "id": 1, "image_id": 0, "category_id": 2, "bbox": [x,y,width,height], # 左上角坐标+宽高 "area": 1200, "iscrowd": 0 } ], "categories": [{"id": 0, "name": "person"}, {"id": 1, "name": "car"}] }转换时需要特别注意:
- COCO使用绝对像素坐标(非归一化)
- bbox格式为[x_top_left, y_top_left, width, height]
- 需要建立category_id到class_id的映射关系
完整转换脚本:
import json from pathlib import Path def coco2yolo(coco_path, output_dir): with open(coco_path) as f: data = json.load(f) # 创建类别映射表 cat_map = {cat["id"]: idx for idx, cat in enumerate(data["categories"])} # 按图像ID分组标注 img_anns = {} for ann in data["annotations"]: img_id = ann["image_id"] img_anns.setdefault(img_id, []).append(ann) # 处理每张图像 for img in data["images"]: img_id = img["id"] img_w, img_h = img["width"], img["height"] txt_path = Path(output_dir) / f"{Path(img['file_name']).stem}.txt" with open(txt_path, "w") as f_txt: for ann in img_anns.get(img_id, []): # 转换bbox格式 x, y, w, h = ann["bbox"] x_center = (x + w/2) / img_w y_center = (y + h/2) / img_h w_norm = w / img_w h_norm = h / img_h # 写入YOLO格式 class_id = cat_map[ann["category_id"]] line = f"{class_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n" f_txt.write(line)3. Labelme JSON格式深度处理
Labelme是流行的图像标注工具,其生成的JSON文件结构如下:
{ "version": "5.1.1", "flags": {}, "shapes": [ { "label": "dog", "points": [[x1,y1], [x2,y2]], # 多边形点或矩形对角点 "shape_type": "rectangle", "flags": {} } ], "imagePath": "image.jpg", "imageData": null, "imageHeight": 600, "imageWidth": 800 }特殊处理要点:
- 支持多边形和矩形两种标注方式
- 需要将多边形转换为外接矩形(对于非矩形标注)
- 点坐标可能是浮点数或整数
转换代码实现:
import json import numpy as np from pathlib import Path def labelme2yolo(labelme_path, output_dir, class_map): with open(labelme_path) as f: data = json.load(f) img_h, img_w = data["imageHeight"], data["imageWidth"] txt_path = Path(output_dir) / f"{Path(data['imagePath']).stem}.txt" with open(txt_path, "w") as f_txt: for shape in data["shapes"]: label = shape["label"] points = np.array(shape["points"]) # 获取边界框坐标 if shape["shape_type"] == "rectangle": x_min, y_min = points.min(axis=0) x_max, y_max = points.max(axis=0) else: # 多边形转外接矩形 x_min, y_min = points.min(axis=0) x_max, y_max = points.max(axis=0) # 计算YOLO格式坐标 width = x_max - x_min height = y_max - y_min x_center = (x_min + width/2) / img_w y_center = (y_min + height/2) / img_h width /= img_w height /= img_h # 写入文件 class_id = class_map[label] line = f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n" f_txt.write(line)4. 自定义JSON格式的灵活适配
自定义JSON格式千变万化,我们需要先分析其结构。以下是一个典型示例:
[ { "image": "001.jpg", "annotations": [ { "label": "cat", "bbox": { "x": 0.25, # 中心x (已归一化) "y": 0.35, # 中心y "w": 0.1, # 宽度 "h": 0.15 # 高度 } } ] } ]适配策略:
- 确认坐标是否已归一化
- 检查bbox表示方式(中心点+宽高 或 角点坐标)
- 验证标签命名一致性
通用转换函数:
def custom_json2yolo(json_path, output_dir, class_map): with open(json_path) as f: data = json.load(f) for item in data: img_name = Path(item["image"]).stem txt_path = Path(output_dir) / f"{img_name}.txt" with open(txt_path, "w") as f_txt: for ann in item["annotations"]: label = ann["label"] bbox = ann["bbox"] # 假设已经是YOLO格式 if all(key in bbox for key in ["x", "y", "w", "h"]): x, y, w, h = bbox["x"], bbox["y"], bbox["w"], bbox["h"] # 处理角点格式 elif all(key in bbox for key in ["x1", "y1", "x2", "y2"]): x1, y1, x2, y2 = bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"] w = x2 - x1 h = y2 - y1 x = x1 + w/2 y = y1 + h/2 class_id = class_map[label] line = f"{class_id} {x:.6f} {y:.6f} {w:.6f} {h:.6f}\n" f_txt.write(line)5. 实战:构建通用转换工具链
将上述模块整合为可配置的转换流水线:
import argparse from pathlib import Path def auto_convert(input_path, output_dir, format_type, class_map=None): """智能转换入口函数""" Path(output_dir).mkdir(exist_ok=True) if format_type == "coco": coco2yolo(input_path, output_dir) elif format_type == "labelme": if not class_map: raise ValueError("class_map required for Labelme format") labelme2yolo(input_path, output_dir, class_map) elif format_type == "custom": if not class_map: raise ValueError("class_map required for custom format") custom_json2yolo(input_path, output_dir, class_map) else: raise ValueError(f"Unsupported format: {format_type}") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("input", help="Input JSON file path") parser.add_argument("output_dir", help="Output directory for TXT files") parser.add_argument("--format", choices=["coco", "labelme", "custom"], required=True) parser.add_argument("--class_map", help="JSON file mapping labels to class IDs") args = parser.parse_args() class_map = json.loads(Path(args.class_map).read_text()) if args.class_map else None auto_convert(args.input, args.output_dir, args.format, class_map)使用示例:
# 转换COCO格式 python converter.py annotations.json output_coco --format coco # 转换Labelme格式(需提供类别映射) python converter.py labelme.json output_labelme --format labelme --class_map classes.json6. 验证与调试技巧
转换后务必进行数据验证:
- 可视化检查:使用OpenCV绘制边界框
import cv2 def visualize_yolo(img_path, txt_path, class_names): img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: class_id, x, y, w_box, h_box = map(float, line.split()) # 转换回像素坐标 x1 = int((x - w_box/2) * w) y1 = int((y - h_box/2) * h) x2 = int((x + w_box/2) * w) y2 = int((y + h_box/2) * h) # 绘制矩形和标签 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)- 统计验证:检查类别分布和框尺寸分布
import matplotlib.pyplot as plt def analyze_annotations(txt_dir): class_counts = {} box_ratios = [] for txt_path in Path(txt_dir).glob("*.txt"): with open(txt_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue class_id = int(parts[0]) w = float(parts[3]) h = float(parts[4]) # 统计类别 class_counts[class_id] = class_counts.get(class_id, 0) + 1 # 计算宽高比 if h > 0: box_ratios.append(w/h) # 绘制统计图 plt.figure(figsize=(12,5)) plt.subplot(121) plt.bar(class_counts.keys(), class_counts.values()) plt.title("Class Distribution") plt.subplot(122) plt.hist(box_ratios, bins=50) plt.title("Width/Height Ratio Distribution") plt.show()7. 高级应用:处理复杂场景
多任务数据转换:同时处理检测和分割标注
def coco2yolo_seg(coco_path, output_dir): """转换COCO实例分割标注""" with open(coco_path) as f: data = json.load(f) cat_map = {cat["id"]: idx for idx, cat in enumerate(data["categories"])} img_anns = defaultdict(list) for ann in data["annotations"]: img_anns[ann["image_id"]].append(ann) for img in data["images"]: txt_path = Path(output_dir) / f"{Path(img['file_name']).stem}.txt" with open(txt_path, "w") as f_txt: for ann in img_anns.get(img["id"], []): if not ann["segmentation"]: continue class_id = cat_map[ann["category_id"]] # 获取所有多边形点(COCO允许多个多边形) segments = [] for seg in ann["segmentation"]: points = np.array(seg).reshape(-1,2) # 归一化 points[:,0] /= img["width"] points[:,1] /= img["height"] segments.append(points.flatten().tolist()) # YOLO分割格式:class_id x1 y1 x2 y2 ... xn yn line = f"{class_id} " + " ".join(map(str, sum(segments, []))) + "\n" f_txt.write(line)性能优化技巧:
- 使用多进程处理大型数据集
from multiprocessing import Pool def process_file(args): """包装函数供多进程调用""" file_path, format_type, output_dir, class_map = args try: auto_convert(file_path, output_dir, format_type, class_map) return True except Exception as e: print(f"Error processing {file_path}: {str(e)}") return False def batch_convert(file_list, format_type, output_dir, class_map=None, workers=4): """批量转换入口""" tasks = [(f, format_type, output_dir, class_map) for f in file_list] with Pool(workers) as pool: results = pool.map(process_file, tasks) print(f"Successfully processed {sum(results)}/{len(file_list)} files")
编程学习
技术分享
实战经验