从标注到训练:手把手教你将Labelme的JSON文件转为YOLO/COCO数据集格式
当你完成图像标注工作后,面对Labelme生成的JSON文件,下一步该如何处理?本文将带你深入理解标注数据的转换逻辑,并提供可复用的Python代码,将Labelme标注无缝对接YOLO、MMDetection等主流框架。
1. 理解Labelme的JSON数据结构
Labelme生成的JSON文件包含完整的标注元数据。通过解析这个结构,我们能提取出训练所需的关键信息。以下是一个典型JSON文件的简化结构:
{ "version": "5.3.0", "flags": {}, "shapes": [ { "label": "cat", "points": [[102, 205], [320, 207], [298, 369], [94, 370]], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "image_001.jpg", "imageData": null, "imageHeight": 480, "imageWidth": 640 }关键字段解析:
shapes:包含所有标注对象的数组points:标注形状的顶点坐标(多边形)或对角点坐标(矩形)shape_type:标注类型(polygon/rectangle/circle等)imageHeight/imageWidth:原始图像尺寸
注意:Labelme的坐标采用像素单位,原点(0,0)位于图像左上角,这与大多数深度学习框架的坐标系统一致。
2. 转换逻辑与数学原理
2.1 坐标系统转换
不同框架对标注数据的要求存在差异:
| 框架类型 | 坐标格式 | 归一化要求 | 文件结构 |
|---|---|---|---|
| YOLO | 中心点+宽高 | 需要(0-1范围) | 每图对应.txt文件 |
| COCO | 多边形/bbox | 保持像素值 | 集中式.json文件 |
| PascalVOC | 对角坐标 | 保持像素值 | XML文件 |
多边形转YOLO bbox的数学过程:
- 提取多边形所有顶点的x、y坐标
- 计算最小外接矩形:
x_min = min(x_points) x_max = max(x_points) y_min = min(y_points) y_max = max(y_points) - 转换为YOLO格式:
x_center = (x_min + x_max) / 2 / image_width y_center = (y_min + y_max) / 2 / image_height width = (x_max - x_min) / image_width height = (y_max - y_min) / image_height
2.2 标签映射处理
建立类别ID映射表是转换的关键步骤。例如:
class_mapping = { "person": 0, "car": 1, "dog": 2, "cat": 3 }提示:建议将映射关系保存为单独的文件(如
classes.txt),方便后续模型训练时引用。
3. 完整转换代码实现
3.1 Labelme转YOLO格式
import json import os from pathlib import Path def labelme_to_yolo(json_path, output_dir, class_mapping): with open(json_path) as f: data = json.load(f) image_width = data['imageWidth'] image_height = data['imageHeight'] txt_path = Path(output_dir) / (Path(json_path).stem + '.txt') with open(txt_path, 'w') as f: for shape in data['shapes']: label = shape['label'] points = shape['points'] # 多边形转边界框 x_coords = [p[0] for p in points] y_coords = [p[1] for p in points] x_min, x_max = min(x_coords), max(x_coords) y_min, y_max = min(y_coords), max(y_coords) # 计算YOLO格式 x_center = (x_min + x_max) / 2 / image_width y_center = (y_min + y_max) / 2 / image_height width = (x_max - x_min) / image_width height = (y_max - y_min) / image_height # 写入文件 class_id = class_mapping[label] f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n")3.2 Labelme转COCO格式
COCO格式需要构建更复杂的数据结构:
import datetime def create_coco_template(): return { "info": { "description": "COCO dataset converted from Labelme", "version": "1.0", "year": datetime.datetime.now().year, "contributor": "", "date_created": datetime.datetime.now().isoformat() }, "licenses": [{"id": 1, "name": "Unknown"}], "images": [], "annotations": [], "categories": [] } def labelme_to_coco(json_files, output_path, class_mapping): coco_data = create_coco_template() # 构建categories for name, id in class_mapping.items(): coco_data["categories"].append({ "id": id, "name": name, "supercategory": "object" }) image_id = 1 annotation_id = 1 for json_file in json_files: with open(json_file) as f: data = json.load(f) # 添加image信息 image_info = { "id": image_id, "file_name": data["imagePath"], "width": data["imageWidth"], "height": data["imageHeight"], "license": 1, "date_captured": "" } coco_data["images"].append(image_info) # 处理每个标注 for shape in data["shapes"]: label = shape["label"] points = shape["points"] # 计算边界框 x_coords = [p[0] for p in points] y_coords = [p[1] for p in points] x_min, x_max = min(x_coords), max(x_coords) y_min, y_max = min(y_coords), max(y_coords) width = x_max - x_min height = y_max - y_min # 多边形转COCO格式 segmentation = [] for x, y in points: segmentation.extend([x, y]) annotation = { "id": annotation_id, "image_id": image_id, "category_id": class_mapping[label], "segmentation": [segmentation], "area": width * height, "bbox": [x_min, y_min, width, height], "iscrowd": 0 } coco_data["annotations"].append(annotation) annotation_id += 1 image_id += 1 # 保存结果 with open(output_path, 'w') as f: json.dump(coco_data, f, indent=2)4. 实战中的常见问题处理
4.1 多图批量处理方案
使用Python的glob模块批量处理JSON文件:
import glob json_files = glob.glob('annotations/*.json') class_mapping = {"person": 0, "car": 1} # 根据实际情况修改 # YOLO转换 for json_file in json_files: labelme_to_yolo(json_file, 'yolo_labels', class_mapping) # COCO转换 labelme_to_coco(json_files, 'coco/annotations.json', class_mapping)4.2 特殊形状处理策略
不同标注形状需要特殊处理:
| 形状类型 | 处理方案 | 适用框架 |
|---|---|---|
| 多边形 | 保留原始顶点或转bbox | COCO/YOLO |
| 矩形 | 直接转换对角点 | 全部框架 |
| 圆形 | 转为外接矩形 | 目标检测 |
| 点 | 扩展为小矩形 | 关键点检测 |
对于圆形标注的转换示例:
def circle_to_bbox(points): """将圆形标注转为边界框""" center_x, center_y = points[0] edge_x, edge_y = points[1] radius = ((edge_x - center_x)**2 + (edge_y - center_y)**2)**0.5 return [ center_x - radius, center_y - radius, center_x + radius, center_y + radius ]4.3 数据集划分技巧
合理的数据划分对模型训练至关重要:
import random from sklearn.model_selection import train_test_split # 划分训练集/验证集/测试集 all_images = [f for f in os.listdir('images') if f.endswith('.jpg')] train, test = train_test_split(all_images, test_size=0.2, random_state=42) train, val = train_test_split(train, test_size=0.125, random_state=42) # 70/10/20 # 创建数据集结构 def create_dataset_structure(images, subset_name): os.makedirs(f'dataset/{subset_name}/images', exist_ok=True) os.makedirs(f'dataset/{subset_name}/labels', exist_ok=True) for img in images: # 移动图像文件 os.rename(f'images/{img}', f'dataset/{subset_name}/images/{img}') # 移动标注文件 json_file = img.replace('.jpg', '.json') txt_file = img.replace('.jpg', '.txt') if os.path.exists(f'annotations/{json_file}'): labelme_to_yolo(f'annotations/{json_file}', f'dataset/{subset_name}/labels', class_mapping)5. 质量检查与验证
转换完成后,必须验证数据的正确性:
5.1 可视化验证工具
使用OpenCV绘制标注框验证YOLO格式:
import cv2 def visualize_yolo_label(image_path, label_path, class_names): image = cv2.imread(image_path) h, w = image.shape[:2] with open(label_path) as f: lines = f.readlines() for line in lines: class_id, xc, yc, bw, bh = map(float, line.strip().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(image, (x1, y1), (x2, y2), (0,255,0), 2) cv2.putText(image, class_names[int(class_id)], (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (36,255,12), 2) cv2.imshow('Validation', image) cv2.waitKey(0)5.2 常见错误排查表
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 标注框偏移 | 坐标未归一化/图像尺寸错误 | 检查imageWidth/Height是否正确 |
| 类别ID错误 | 映射表不匹配 | 验证class_mapping的一致性 |
| 缺失标注 | 文件名不匹配 | 确保图像和标注文件前缀一致 |
| 坐标超出范围 | 归一化计算错误 | 确认所有坐标值在0-1范围内(YOLO) |
5.3 自动化检查脚本
def validate_yolo_labels(label_dir, class_count): """检查YOLO标注文件的完整性""" errors = [] for label_file in os.listdir(label_dir): with open(os.path.join(label_dir, label_file)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: errors.append(f"{label_file}: 格式错误") continue class_id = int(parts[0]) if class_id >= class_count: errors.append(f"{label_file}: 无效类别ID {class_id}") for coord in map(float, parts[1:]): if not (0 <= coord <= 1): errors.append(f"{label_file}: 坐标超出范围 {coord}") if errors: print(f"发现{len(errors)}个错误:") for error in errors[:5]: # 只显示前5个错误 print(error) else: print("所有标注文件验证通过")