避坑指南:Labelme标注的JSON转YOLO格式时,坐标归一化和多人处理怎么写代码?
Labelme转YOLO格式实战:坐标归一化与多人标注处理详解
当你从Labelme的JSON标注切换到YOLO格式时,最令人头疼的莫过于坐标归一化和多人标注处理。作为计算机视觉开发者,我曾在这个转换过程中踩过无数坑——从坐标计算错误到多人标注合并混乱,再到文件路径处理的各种陷阱。本文将用实战代码带你彻底解决这些问题。
1. 理解YOLO关键点格式的核心逻辑
YOLO格式的关键点标注远比简单的物体检测复杂。官方文档中明确要求每个对象实例占据一行,包含以下信息:
<class-index> <x> <y> <width> <height> <px1> <py1> <px2> <py2> ... <pxn> <pyn>关键差异点:
- DIM=2格式:仅包含关键点的x,y坐标
- DIM=3格式:每个关键点追加可见性标志(0=不可见,1=遮挡,2=可见)
实际项目中,我推荐使用DIM=3格式,因为它能更好地处理遮挡情况。以下是两种格式的对比示例:
| 格式类型 | 示例数据 | 适用场景 |
|---|---|---|
| DIM=2 | 0 0.5 0.5 0.3 0.4 0.4 0.6 0.5 0.7 | 简单场景,所有关键点可见 |
| DIM=3 | 0 0.5 0.5 0.3 0.4 0.4 0.6 2 0.5 0.7 1 | 复杂场景,处理遮挡和不可见点 |
2. 解析Labelme JSON文件结构
Labelme生成的JSON文件包含完整的标注信息,但结构较为复杂。以下是一个典型的结构示例:
{ "version": "5.0.1", "flags": {}, "shapes": [ { "label": "person", "points": [[x1,y1], [x2,y2]], "shape_type": "rectangle" }, { "label": "keypoint", "points": [[x,y]], "shape_type": "point" } ], "imagePath": "image.jpg", "imageHeight": 1080, "imageWidth": 1920 }处理要点:
- 先提取图像尺寸(imageWidth/imageHeight),这是归一化的基准
- 区分"person"标注(边界框)和关键点标注
- 注意points字段的嵌套结构差异(矩形框是二维列表,关键点是一维列表)
我在实际项目中封装了一个解析函数:
def parse_labelme_json(json_path): with open(json_path) as f: data = json.load(f) image_size = (data['imageWidth'], data['imageHeight']) persons = [] keypoints = [] for shape in data['shapes']: if shape['label'] == 'person': persons.append(shape['points']) else: keypoints.append({ 'label': shape['label'], 'point': shape['points'][0] # 提取单点坐标 }) return image_size, persons, keypoints3. 多人标注处理的关键技术
当一张图片中有多个人时,需要将各自的关键点与对应的人体边界框正确关联。这是转换过程中最容易出错的部分。
解决方案:
- 为每个person创建独立的数据结构
- 根据空间距离将关键点分配到最近的人体框
- 处理可能的关键点共享情况(如握手场景)
def assign_keypoints_to_persons(persons, keypoints): person_instances = [] for i, person_box in enumerate(persons): # 计算人体框中心点 x_center = (person_box[0][0] + person_box[1][0]) / 2 y_center = (person_box[0][1] + person_box[1][1]) / 2 # 收集属于此人的关键点 assigned_kps = [] for kp in keypoints: kp_x, kp_y = kp['point'] # 简单示例:使用中心点距离判断 distance = ((kp_x - x_center)**2 + (kp_y - y_center)**2)**0.5 if distance < 200: # 阈值根据实际情况调整 assigned_kps.append(kp) person_instances.append({ 'box': person_box, 'keypoints': assigned_kps }) return person_instances提示:更精确的做法是计算关键点到人体框的IoU,但简单距离判断在大多数情况下已经足够。
4. 坐标归一化的数学原理与实现
归一化是格式转换的核心,需要将绝对像素坐标转换为0-1之间的相对值。计算公式如下:
normalized_x = absolute_x / image_width normalized_y = absolute_y / image_height对于边界框,需要额外计算中心点和宽高:
def normalize_coordinates(box, keypoints, img_width, img_height): # 处理边界框 x1, y1 = box[0] x2, y2 = box[1] x_center = (x1 + x2) / 2 / img_width y_center = (y1 + y2) / 2 / img_height width = abs(x1 - x2) / img_width height = abs(y1 - y2) / img_height # 处理关键点 normalized_kps = [] for kp in keypoints: x, y = kp['point'] normalized_kps.extend([ x / img_width, y / img_height, 2 # 默认可见 ]) return [x_center, y_center, width, height] + normalized_kps常见陷阱:
- 忘记取绝对值导致宽度/高度为负值
- 使用错误的图像尺寸(有些JSON文件可能不包含imageWidth/imageHeight)
- 归一化前未验证坐标是否超出图像边界
5. 完整转换流程与异常处理
结合上述技术点,下面是完整的转换流程:
def convert_labelme_to_yolo(json_path, output_dir): # 1. 解析原始JSON img_size, persons, keypoints = parse_labelme_json(json_path) img_width, img_height = img_size # 2. 处理多人情况 person_instances = assign_keypoints_to_persons(persons, keypoints) # 3. 准备输出内容 yolo_lines = [] for person in person_instances: normalized = normalize_coordinates( person['box'], person['keypoints'], img_width, img_height ) yolo_line = ' '.join(map(str, [0] + normalized)) # 0是person类别 yolo_lines.append(yolo_line) # 4. 写入文件 output_path = os.path.join(output_dir, os.path.basename(json_path).replace('.json', '.txt')) with open(output_path, 'w') as f: f.write('\n'.join(yolo_lines))增强鲁棒性的技巧:
- 添加图像尺寸回退机制
- 处理关键点缺失情况
- 验证归一化后的值是否在0-1范围内
# 图像尺寸回退示例 if 'imageWidth' not in data: img = Image.open(os.path.join(base_dir, data['imagePath'])) img_width, img_height = img.size else: img_width = data['imageWidth'] img_height = data['imageHeight']6. 实战中的典型问题与解决方案
问题1:生成的TXT文件与图像不对应
解决方案:
- 使用一致的命名规则(如image.jpg对应image.txt)
- 在代码中添加验证步骤:
image_stem = Path(data['imagePath']).stem label_stem = Path(output_path).stem assert image_stem == label_stem, "文件名不匹配"问题2:多人标注时关键点分配错误
解决方案:
- 实现更精确的分配算法
- 添加人工验证步骤
- 在JSON中添加分组ID信息
问题3:坐标归一化后精度丢失
解决方案:
- 增加小数位数(如round(x, 6))
- 使用字符串格式化代替round:
f"{x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}"7. 高级技巧:处理部分可见关键点
对于DIM=3格式,可以精细处理关键点可见性。以下是根据实际情况判断可见性的逻辑:
def determine_visibility(kp, img_width, img_height): x, y = kp['point'] # 超出图像边界 if x < 0 or x >= img_width or y < 0 or y >= img_height: return 0 # 其他业务逻辑判断 # ... return 2 # 默认可见在最近���一个跌倒检测项目中,我发现正确处理关键点可见性能使模型性能提升约15%。特别是在监控场景中,人体经常被家具部分遮挡,精确标注可见性至关重要。
