深度学习标注避坑指南:YOLOPose关键点标注与json转txt全流程解析
深度学习标注避坑指南:YOLOPose关键点标注与json转txt全流程解析
如果你正在为YOLOPose模型准备训练数据,那么标注工作很可能已经让你感到头疼。关键点标注不像画个矩形框那么简单,它涉及到人体姿态的精细捕捉,更别提那些恼人的遮挡、模糊和格式转换问题了。我见过不少项目,模型架构选得挺先进,但最终效果却折在了数据标注这个“脏活累活”上。标注质量直接决定了模型性能的上限,而一个顺畅的标注到训练的数据流水线,则是高效迭代的基石。这篇文章,就是为你——那些已经熟悉深度学习基本流程,但希望在实际的YOLOPose项目标注中少走弯路的开发者——准备的。我们将抛开泛泛而谈,深入标注工具的选择、关键点处理的哲学、以及从json到txt转换中那些教科书里不会写的“坑”,目标是让你构建一套稳健、可复用的数据准备流程。
1. 标注前的战略规划与工具选型
在动手标注第一张图片之前,花点时间进行规划是绝对值得的。这能避免你在标注了几千张图片后,发现格式不对或者标准不统一,导致前功尽弃。
标注工具的选择,远不止“能用”那么简单。对于关键点标注,labelme 确实是社区的主流选择,因为它原生支持多边形、矩形和点的标注。但仅仅知道用它还不够,你需要根据项目规模来配置它。
- 小规模项目(<1000张图):直接使用
labelme的图形界面是可行的。务必在File -> Save Automatically打上勾,这个选项能最大程度避免软件意外崩溃导致标注丢失。我个人的习惯是,每标注完50-100张图,就手动备份一次整个标注文件夹。 - 中大规模项目:考虑使用
labelme的命令行模式进行半自动化标注。例如,你可以先用人脸检测器或姿态估计模型生成关键点的初始位置,再用labelme进行微调和修正。这能极大提升效率。
注意:无论使用何种工具,确保整个项目路径(从图片存储到标注文件输出)绝对不包含任何中文字符或空格。这是许多底层图像库和训练脚本的硬性要求,违反它可能会在后期引发难以排查的编码错误。
一个经常被忽视的要点是标注规范的文档化。在团队协作或个人长期项目中,你必须明确记录以下内容,并形成一份简单的 README_for_labeling.md:
- 关键点定义与顺序:明确每个关键点的名称和编号。例如,对于COCO格式的17个关键点,顺序固定为:0-鼻子,1-左眼,2-右眼……16-右脚踝。这个顺序必须在整个数据集中严格保持一致。
- 边界框(Bounding Box)标准:框应该画多大?是紧贴目标轮廓,还是留出一定余量?对于人体,通常框住整个人体即可。
- 遮挡与不可见处理规则:这是YOLOPose标注的核心难点,我们将在下一章详细展开。
- 图像质量要求:哪些模糊、过暗、严重畸变的图片应该被剔除?
把这些规则写下来,即使是你自己一个人干活,也能在几周后回头修改时,快速回忆起当时的决策依据。
2. 关键点标注的实战哲学:可见、遮挡与不可见
YOLOPose模型要求每个关键点都有三个状态标签,这直接对应了损失函数中的不同处理方式。理解这三个状态,是高质量标注的关键。
| 状态 | 在YOLO格式txt中的数值 | 物理含义 | 标注操作 |
|---|---|---|---|
| 可见 (Visible) | 2 | 关键点在图像中清晰可见,位置明确。 | 在准确的位置点击标注。 |
| 遮挡 (Occluded) | 1 | 关键点因被其他物体(如书本、另一个人)遮挡而不可见,但其大致位置可以推断。 | 在推断出的最可能位置点击标注,并标记为“遮挡”。在labelme中,可以通过设置该点的 group_id 来实现(例如设为1)。 |
| 不可见 (Not Labeled) | 0 | 关键点因图像裁剪、严重遮挡或其他原因完全无法确定位置。 | 不进行标注,在转换后的数据中,该点坐标和状态均记为0。 |
如何处理“遮挡”是关键中的关键。 这里没有放之四海而皆准的答案,取决于你的项目目标。
- 场景一:高精度姿态估计。如果你的应用对关节角度、肢体朝向极其敏感(如运动分析、医疗康复),那么对于位置推断不确定性高的遮挡点,建议标记为“不可见”(0)。因为一个错误的位置标注会给模型带来严重的噪声,不如直接告诉模型“这里我不知道”。
- 场景二:粗粒度姿态识别。如果你的目标是判断“举手”、“坐下”等大体姿态,那么对于大部分可推断的遮挡点(例如,一个人侧身站立,被遮挡的右肩位置相对明确),可以标记为“遮挡”(1)。这能为模型提供更多的上下文信息。
一个实用的技巧是:在 labelme 中,为所有标记为遮挡的点设置一个统一的 group_id(比如1)。这样,在后续的格式转换脚本中,你可以轻松地通过读取这个 group_id 来将其状态值设为1。
标注顺序的一致性也至关重要。我推荐的操作流程是:
- 为图片中的第一个目标绘制完整的边界框。
- 紧接着,按预定义顺序标注这个目标的所有关键点(可见的标位置,遮挡的标位置并设
group_id,不可见的不标)。 - 完成第一个目标后,再开始第二个目标的边界框和关键点标注。
切忌把所有目标的框都画完,再回过头来标所有关键点。
labelme生成的JSON文件是线性记录形状(shapes)的,混合顺序会给解析代码带来不必要的复杂性。
3. 从Labelme JSON到YOLO TXT:解析与转换的深层解析
当你完成标注,得到一堆 .json 文件后,真正的挑战开始了:将它们转化为YOLO训练所需的 .txt 格式。这个转换过程不仅仅是格式翻译,更是数据校验和规范化的过程。
一个典型的 labelme 生成的JSON结构如下所示(精简后):
{
"version": "5.0.1",
"flags": {},
"shapes": [
{
"label": "person",
"points": [[192.34, 98.45], [305.12, 250.67]],
"group_id": null,
"shape_type": "rectangle"
},
{
"label": "nose",
"points": [[248.21, 125.89]],
"group_id": null,
"shape_type": "point"
},
{
"label": "left_shoulder",
"points": [[210.54, 178.32]],
"group_id": 1,
"shape_type": "point"
}
],
"imagePath": "example.jpg",
"imageData": null,
"imageHeight": 480,
"imageWidth": 640
}
我们需要从中提取出:每个目标的类别ID、归一化的边界框坐标 (x_center, y_center, width, height),以及按顺序排列的每个关键点的归一化坐标 (x, y) 和状态 (v)。
下面是一个增强版的、更健壮的Python转换脚本。它包含了错误处理、更清晰的逻辑,并假设我们使用COCO的17关键点顺序。
import json
import os
from pathlib import Path
# 配置区域
JSON_DIR = Path("/path/to/your/json/files") # 替换为你的JSON文件夹路径
TXT_DIR = Path("/path/to/output/txt/files") # 替换为输出TXT文件夹路径
TXT_DIR.mkdir(parents=True, exist_ok=True) # 创建输出目录
# COCO 17关键点名称及顺序 (必须与标注时使用的名称完全一致)
KEYPOINT_NAMES = [
'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'
]
# 类别名称到ID的映射 (根据你的数据集修改)
CLASS_NAME_TO_ID = {"person": 0, "cat": 1} # 示例
def convert_bbox_to_yolo(x1, y1, x2, y2, img_w, img_h):
"""将边界框坐标(x1,y1,x2,y2)转换为YOLO格式的归一化中心坐标和宽高"""
x_center = (x1 + x2) / 2.0 / img_w
y_center = (y1 + y2) / 2.0 / img_h
width = abs(x2 - x1) / img_w
height = abs(y2 - y1) / img_h
# 确保坐标在[0,1]范围内
x_center = max(0, min(1, x_center))
y_center = max(0, min(1, y_center))
width = max(0, min(1, width))
height = max(0, min(1, height))
return x_center, y_center, width, height
def process_json_file(json_path, txt_path):
"""处理单个JSON文件"""
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
img_h = data['imageHeight']
img_w = data['imageWidth']
shapes = data['shapes']
# 按group_id或连续区域分组,关联边界框和关键点
# 这里采用简单策略:假设标注顺序是【框1,点1-1,点1-2...,框2,点2-1...】
objects = []
current_obj = None
for shape in shapes:
if shape['shape_type'] == 'rectangle':
# 遇到新的矩形框,保存上一个对象,开始新的对象
if current_obj:
objects.append(current_obj)
current_obj = {
'class_name': shape['label'],
'bbox': shape['points'], # [[x1,y1], [x2,y2]]
'keypoints': {} # 字典,键为关键点名,值为{'pos':[x,y], 'group_id': id}
}
elif shape['shape_type'] == 'point' and current_obj is not None:
# 将关键点关联到当前对象
kp_name = shape['label']
current_obj['keypoints'][kp_name] = {
'pos': shape['points'][0],
'group_id': shape.get('group_id')
}
# 添加最后一个对象
if current_obj:
objects.append(current_obj)
# 写入TXT文件
with open(txt_path, 'w') as f_txt:
for obj in objects:
# 1. 写入类别ID
class_id = CLASS_NAME_TO_ID.get(obj['class_name'])
if class_id is None:
print(f"警告: {json_path} 中发现未知类别 {obj['class_name']},已跳过。")
continue
f_txt.write(f"{class_id} ")
# 2. 写入归一化边界框
x1, y1 = obj['bbox'][0]
x2, y2 = obj['bbox'][1]
bbox_yolo = convert_bbox_to_yolo(x1, y1, x2, y2, img_w, img_h)
f_txt.write(' '.join(f"{coord:.6f}" for coord in bbox_yolo) + ' ')
# 3. 按固定顺序写入17个关键点
for kp_name in KEYPOINT_NAMES:
if kp_name in obj['keypoints']:
kp_info = obj['keypoints'][kp_name]
x, y = kp_info['pos']
# 归一化坐标
nx, ny = x / img_w, y / img_h
nx, ny = max(0, min(1, nx)), max(0, min(1, ny)) # 边界保护
# 判断状态:2-可见,1-遮挡,0-不可见
if kp_info['group_id'] == 1: # 假设group_id=1代表遮挡
v = 1
else:
v = 2 # 可见
f_txt.write(f"{nx:.6f} {ny:.6f} {v} ")
else:
# 关键点未标注
f_txt.write("0.000000 0.000000 0 ")
f_txt.write('\n') # 一个对象结束,换行
# 批量处理
json_files = list(JSON_DIR.glob('*.json'))
print(f"找到 {len(json_files)} 个JSON文件。")
for json_file in json_files:
txt_file = TXT_DIR / (json_file.stem + '.txt')
try:
process_json_file(json_file, txt_file)
except Exception as e:
print(f"处理文件 {json_file.name} 时出错: {e}")
print("转换完成!")
这个脚本的核心改进在于:
- 健壮的对象关联逻辑:它通过遍历
shapes数组,将连续的矩形框和关键点自动关联为一个目标对象,更贴合人工标注时的自然顺序。 - 全面的边界保护:对归一化后的坐标进行了
max(0, min(1, coord))处理,防止因标注框轻微超出图像边界导致坐标大于1或小于0。 - 清晰的错误提示:当遇到未在
CLASS_NAME_TO_ID中定义的类别时,会给出明确警告。
4. 转换后的校验与可视化:确保数据万无一失
生成TXT文件后,绝对不要直接扔给模型训练。必须进行校验,否则垃圾数据进去,垃圾模型出来。
第一步:格式校验。 写一个简单的脚本检查每个TXT文件:
- 每行是否都有
1 + 4 + 17*3 = 56个数值?(1个类别+4个框坐标+17个关键点*3个值) - 所有数值是否都在0到1之间(类别ID除外)?
- 关键点状态值是否只有0、1、2?
第二步:可视化校验。 这是最直观有效的方法。编写一个可视化脚本,将TXT文件中的标注画回原图上,检查边界框是否贴合,关键点位置和状态是否正确。
import cv2
import numpy as np
from pathlib import Path
def visualize_yolo_label(img_path, label_path, keypoint_names):
img = cv2.imread(str(img_path))
ih, iw, _ = img.shape
with open(label_path, 'r') as f:
lines = f.readlines()
for line in lines:
parts = list(map(float, line.strip().split()))
if len(parts) != 56: # 检查数据长度
print(f"数据长度错误于 {label_path}")
continue
class_id = int(parts[0])
# 绘制边界框
x_c, y_c, w, h = parts[1:5]
x1 = int((x_c - w/2) * iw)
y1 = int((y_c - h/2) * ih)
x2 = int((x_c + w/2) * iw)
y2 = int((y_c + h/2) * ih)
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
# 绘制关键点
kp_data = parts[5:]
for i in range(0, len(kp_data), 3):
x, y, v = kp_data[i], kp_data[i+1], int(kp_data[i+2])
px, py = int(x * iw), int(y * ih)
if v == 2: # 可见,画绿色实心圆
cv2.circle(img, (px, py), 3, (0, 255, 0), -1)
elif v == 1: # 遮挡,画黄色空心圆
cv2.circle(img, (px, py), 3, (0, 255, 255), 2)
# v==0 不绘制
cv2.imshow('Visualization', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 使用示例
img_file = Path("path/to/image.jpg")
label_file = Path("path/to/label.txt")
visualize_yolo_label(img_file, label_file, KEYPOINT_NAMES)
通过可视化,你能立刻发现标注错误:比如关键点标在了背景上、遮挡状态标记反了、或者边界框包含了过多背景。
第三步:数据集统计分析。 统计一下你的数据集中关键点状态的分布。如果“不可见(0)”的比例异常高,可能需要检查标注规范或图像质量;如果“遮挡(1)”的比例极低,也许你的场景中遮挡情况不多,或者标注员过于保守。这些统计信息对于理解数据集偏差和后续可能的数据增强策略(例如,针对性地模拟遮挡)非常有价值。
整个标注与转换流程,本质上是一个数据工程问题。它要求你在追求效率的同时,保持极致的严谨。磨刀不误砍柴工,在数据准备阶段多花一天时间排查和验证,很可能为你在模型调试阶段节省一周的时间。当你看到可视化脚本中,绿色的框和点准确地落在图像中的人体上时,那份确信感会让你觉得所有的繁琐工作都是值得的。
更多推荐

所有评论(0)