从标注到训练:手把手教你将Labelme的JSON文件转为YOLO/COCO数据集格式

从标注到训练:手把手教你将Labelme的JSON文件转为YOLO/COCO数据集格式 从标注到训练手把手教你将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_height2.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, indent2)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 特殊形状处理策略不同标注形状需要特殊处理形状类型处理方案适用框架多边形保留原始顶点或转bboxCOCO/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_size0.2, random_state42) train, val train_test_split(train, test_size0.125, random_state42) # 70/10/20 # 创建数据集结构 def create_dataset_structure(images, subset_name): os.makedirs(fdataset/{subset_name}/images, exist_okTrue) os.makedirs(fdataset/{subset_name}/labels, exist_okTrue) for img in images: # 移动图像文件 os.rename(fimages/{img}, fdataset/{subset_name}/images/{img}) # 移动标注文件 json_file img.replace(.jpg, .json) txt_file img.replace(.jpg, .txt) if os.path.exists(fannotations/{json_file}): labelme_to_yolo(fannotations/{json_file}, fdataset/{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(所有标注文件验证通过)