从Labelme到YOLO:关键点标注数据格式转换实战指南
在计算机视觉领域,数据标注是模型训练的基础环节。许多研究者和工程师在使用Labelme完成关键点标注后,常常面临如何将JSON格式转换为YOLO所需TXT格式的难题。本文将深入解析两种数据格式的结构差异,提供可复用的Python转换脚本,并分享关键点标注与转换中的实用技巧。
1. 理解Labelme与YOLO数据格式的核心差异
Labelme生成的JSON文件和YOLO所需的TXT文件在数据结构上存在本质区别。Labelme的JSON采用树状结构存储标注信息,而YOLO的TXT则是基于行的扁平化格式。
Labelme JSON典型结构:
{ "version": "5.1.1", "flags": {}, "shapes": [ { "label": "person", "points": [[x1,y1], [x2,y2]], "group_id": 1, "shape_type": "rectangle" }, { "label": "nose", "points": [[x,y]], "group_id": null, "shape_type": "point" } ], "imagePath": "image.jpg", "imageData": null, "imageHeight": 1080, "imageWidth": 1920 }YOLO TXT格式要求:
<class_id> <x_center> <y_center> <width> <height> <x1> <y1> <vis1> ... <xn> <yn> <visn>表:关键字段对应关系
| Labelme字段 | YOLO字段 | 说明 |
|---|---|---|
| shapes[n].points[0] | x_center, y_center | 边界框中心坐标 |
| shapes[n].points[1] | width, height | 边界框宽高 |
| imageWidth/imageHeight | - | 用于坐标归一化 |
| shapes[n].group_id | vis | 关键点可见性标识 |
2. 关键点标注的三种状态处理策略
在姿态估计任务中,关键点的可见性状态直接影响模型训练效果。YOLO格式将关键点分为三类:
- 可见关键点(状态值2):完全可见且位置明确
- 遮挡关键点(状态值1):位置可推测但被遮挡
- 缺失关键点(状态值0):完全不可见或位置未知
处理建议:
- 对于遮挡关键点,应在Labelme中标注时设置
group_id=1 - 保持关键点标注顺序一致,便于后续处理
- 复杂场景下可考虑使用
shape_attributes记录额外信息
3. 完整Python转换脚本解析
以下增强版脚本不仅实现格式转换,还包含异常处理和详细注释:
import json import os from pathlib import Path def convert_labelme_to_yolo(json_dir, txt_dir, class_mapping): """ 将Labelme JSON标注转换为YOLO格式TXT文件 参数: json_dir: JSON文件目录路径 txt_dir: 输出TXT目录路径 class_mapping: 类别名称到ID的映射字典 """ # 确保输出目录存在 Path(txt_dir).mkdir(parents=True, exist_ok=True) # 关键点顺序定义(示例为COCO17关键点) keypoints_order = [ 'nose', 'left_eye', 'right_eye', 'left_ear', 'right_ear', 'left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow', 'left_wrist', 'right_wrist', 'left_hip', 'right_hip', 'left_knee', 'right_knee', 'left_ankle', 'right_ankle' ] for json_file in Path(json_dir).glob('*.json'): with open(json_file, 'r', encoding='utf-8') as f: try: data = json.load(f) except json.JSONDecodeError: print(f"警告: {json_file} 不是有效的JSON文件,已跳过") continue # 准备输出文件路径 txt_path = Path(txt_dir) / f"{json_file.stem}.txt" with open(txt_path, 'w', encoding='utf-8') as txt_file: img_width = data['imageWidth'] img_height = data['imageHeight'] # 分离边界框和关键点 bboxes = [s for s in data['shapes'] if s['shape_type'] == 'rectangle'] points = {s['label']: s for s in data['shapes'] if s['shape_type'] == 'point'} for bbox in bboxes: # 处理类别和边界框 class_id = class_mapping.get(bbox['label'], -1) if class_id == -1: continue # 计算归一化坐标 x_min, y_min = bbox['points'][0] x_max, y_max = bbox['points'][1] x_center = ((x_min + x_max) / 2) / img_width y_center = ((y_min + y_max) / 2) / img_height width = (x_max - x_min) / img_width height = (y_max - y_min) / img_height # 写入基础信息 line = [ str(class_id), f"{x_center:.6f}", f"{y_center:.6f}", f"{width:.6f}", f"{height:.6f}" ] # 处理关键点 for kp_name in keypoints_order: if kp_name in points: kp = points[kp_name] x = kp['points'][0][0] / img_width y = kp['points'][0][1] / img_height vis = 1 if kp.get('group_id') == 1 else 2 line.extend([f"{x:.6f}", f"{y:.6f}", str(vis)]) else: line.extend(['0.000000', '0.000000', '0']) txt_file.write(' '.join(line) + '\n') # 使用示例 class_map = {'person': 0, 'cat': 1} # 定义类别映射 convert_labelme_to_yolo( json_dir='path/to/json_files', txt_dir='path/to/output_txt', class_mapping=class_map )4. 常见问题与解决方案
编码问题处理:
# 处理可能的中文路径问题 def safe_open(path, mode='r', encoding='utf-8', errors='ignore'): return open(path, mode=mode, encoding=encoding, errors=errors)典型错误场景:
坐标越界:添加边界检查
def normalize_coord(value, max_val): return max(0.0, min(1.0, value / max_val))关键点顺序不一致:使用OrderedDict维护顺序
多对象处理:按group_id分组处理
表:常见错误及解决方法
| 错误类型 | 现象 | 解决方案 |
|---|---|---|
| 编码错误 | 文件读取失败 | 指定encoding='utf-8' |
| 坐标异常 | 数值超出[0,1]范围 | 添加归一化边界检查 |
| 关键点缺失 | 部分关键点未标注 | 提供默认值填充 |
| 顺序混乱 | 关键点顺序不一致 | 使用预设顺序强制对齐 |
5. 高级技巧与性能优化
对于大规模数据集转换,可以考虑以下优化策略:
多进程处理:
from multiprocessing import Pool def process_file(json_path): # 单个文件处理逻辑 pass with Pool(processes=4) as pool: pool.map(process_file, Path(json_dir).glob('*.json'))增量处理:
# 记录已处理文件,支持断点续传 processed = set() log_file = 'processed.log' if Path(log_file).exists(): with open(log_file) as f: processed.update(f.read().splitlines())内存优化:
# 流式处理大文件 def iter_json_objects(file_path): with open(file_path) as f: for line in f: yield json.loads(line)在实际项目中,建议先将小批量数据转换后验证效果,再扩展到整个数据集。转换完成后,可使用以下命令快速检查结果文件:
# 检查前10个转换后的文件 head -n 10 path/to/output/*.txt # 统计各类别数量 awk '{print $1}' path/to/output/*.txt | sort | uniq -c