做姿态估计训练的时候,最烦的一件事就是标注数据和训练格式之间来回倒腾。尤其是用X-anylabing这类工具标注完,导出的JSON和YOLO-POSE要的txt长得完全不一样,还得写脚本转。更别说转换完到底对不对,光看数字很难看出来,搞不好训练的时候才发现关键点全错位,那真是浪费好几天。
这篇文章就聊透这件事:X-anylabing标注的JSON怎么转成YOLO-POSE训练用的txt,怎么支持自定义标签映射,转换结果如何可视化验证。我自己用Python写了一套完整方案,实测跑通,附带踩坑记录,给后面做关键点检测的兄弟们一个能直接抄作业的参考。
1. 项目背景与整体设计思路
1.1 为什么需要JSON转txt转换脚本
做YOLO-POSE关键点预测,训练数据的格式是固定的:每张图片对应一个txt文件,每行描述一个目标,格式是class_id x_center y_center width height kx1 ky1 v1 kx2 ky2 v2 ...。这里x_center、y_center、width、height都是归一化到0~1的边界框参数,后面跟着的是17个关键点的归一化坐标和可见性标志。
而标注工具导出的是JSON,里面保存的是分层结构:图片信息、尺寸、对象列表,每个对象里有bounding_box或polygon,还有keypoints列表,每个关键点有名称、坐标和可见状态。
一边是专为机器训练设计的紧凑文本格式,一边是给人阅读设计的富信息JSON结构。如果每次手动改或者拿Excel自己拼,不仅效率低,而且极易出错。写脚本转换是唯一的正道,而要想转换得不错位,必须先搞清楚两种格式的映射关系。
这里有个核心痛点:X-anylabing的JSON中关键点坐标是绝对值(像素坐标),且坐标系可能涉及不同缩放,比如标注时的视图缩放和实际图像尺寸不一致。转换时如果只拿视觉数据的宽高直接归一化,极容易出现坐标漂移。这也是为什么必须把图像尺寸和标注坐标在同一个基准坐标下计算,后面我详细说。
1.2 项目技术选型与实现思路
实现语言我选了Python。原因很直接:YOLO生态基本都在Python里,OpenCV、NumPy、Pyyaml这些库随手就能装,而且处理JSON是Python的看家本领。
整个转换脚本的设计分成四层:
- 输入层:解析X-anylabing导出的JSON文件,支持单个文件和整个目录批量处理。
- 转换层:从JSON里提取边界框和关键点信息,做坐标归一化,根据传入的标签映射表把字符串标签转成YOLO类别ID。
- 输出层:为每张图片生成同名txt文件,写入指定目录。
- 验证层:用OpenCV将转换后的关键点和边界框画回原图,人工核对。
这里我要强调一个提前想清楚的点:标签映射必须解耦。X-anylabing里标签可能是中文名(如“人”、“狗”、“猫”),也可能是英文名(如“person”、“dog”)甚至数字,而YOLO训练时只认类别ID。所以脚本必须支持传入一个映射字典或映射文件,而不是写死标签名。这样做的好处是,同一个转换脚本可以复用到其他数据集,不用每次改代码。
设计标签映射时还考虑了一种特殊情况:如果JSON里的标签在映射字典里找不到,怎么办?我的方案是直接跳过该对象并打印警告。如果因为漏标导致某个类别一张图都没有,训练时该类别的loss会异常,所以宁可少一个对象,也不能把类别ID配错。
“可指定标签转换”这个需求,说白了就是给用户几个映射选项:可以是手动输入的字典,也可以是一个YAML文件。我更推荐YAML文件,因为标注项目多了以后,命名习惯千奇百怪,有个文件可维护性好很多。后面代码里两种方式都会支持。
1.3 这个方案解决了什么问题
我举个具体场景。之前做了一个人体姿态估计项目,X-anylabing里标注了5000张图片,每张平均2~3个人,共标注了17个关键点。如果手动导出转换,至少一天。用脚本批量转换,不到一分钟搞定,而且可以随时重新生成。
更重要的是,我踩过一次坑:转换完后直接丢进训练,跑了三天,发现loss下不去。后来画出来一看,边界框是对的,但关键点全偏了一个身位,原因就是JSON里的关键点坐标不是相对于图片原图的,而是相对于某个裁剪区域。后来我改了脚本,加入了“基准坐标对齐”逻辑,才真正解决。
这个问题不解决,后面的训练全白搞。
2. 数据格式深度解析:从JSON到txt的映射规则
2.1 X-anylabing导出的JSON结构长什么样
先看一个典型的X-anylabing导出JSON结构。为了直观,我把它整理成伪代码形式,方便对照:
{ "info": { "name": "dataset_001", "created": "2024-05-10T14:33:00" }, "images": [ { "id": 0, "file_name": "img_0001.jpg", "width": 1280, "height": 720 } ], "annotations": [ { "id": 0, "image_id": 0, "category_name": "person", "bbox": [310, 52, 562, 671], "keypoints": [ {"name": "nose", "x": 402, "y": 158, "visible": 2}, {"name": "left_shoulder", "x": 339, "y": 203, "visible": 2} ] } ] }注意几点:
images数组里存图片基本信息,核心字段是file_name、width、height。annotations里的bbox是[x, y, w, h],x和y是左上角坐标,w、h是宽高。keypoints是对象数组,每个关键点有名称、坐标、可见性。visible字段:2表示可见,1表示遮挡但能推断,0表示完全不可见。YOLO-POSE也支持这种三态,但很多训练配置里只区分0和非0,这点要注意。
实际用X-anylabing导出的JSON可能字段名不完全一样,比如category_name可能叫label,keypoints里可能是x和y分开的。写转换脚本时,字段名一定要先打印几份原始数据看看,不要想当然。
2.2 YOLO-POSE的txt格式规则
YOLO-POSE和YOLO检测最大的区别在于行尾多了关键点。一个标准的YOLO-POSE txt行格式:
class_id x_center y_center width height px1 py1 pv1 px2 py2 pv2 ... px17 py17 pv17其中:
class_id:整数,从0开始。x_center y_center width height:归一化到0~1的边界框。px_i py_i pv_i:第i个关键点的x、y归一化坐标和可见性标志。
这里有个极易出错的地方:关键点的顺序必须固定。YOLO-POSE在训练时是依赖关键点顺序的,比如官方coco权重顺序是鼻子、左眼、右眼、左耳、右耳、左肩、右肩、左肘、右肘、左腕、右腕、左髋、右髋、左膝、右膝、左踝、右踝。如果你的txt里顺序不统一,训练出来的模型关键点会张冠李戴。
所以在转换脚本里,我要求用户传入一个关键点顺序列表,脚本会按照这个列表的顺序输出,而不是按照JSON里出现的顺序输出。这算是我踩过很多坑之后总结出来的硬性要求。
2.3 标签与可见性的映射约定
X-anylabing里的标签是字符串,YOLO训练要整数ID,转换脚本必须在中间加一层字典映射。
我常用的映射方式有两种:一种是直接手动指定对应表,比如{"person": 0, "dog": 1};另一种是根据一个标签列表的顺序自动生成ID,比如["person", "dog"],那person就是0,dog就是1。
这里有个坑:如果JSON里出现了映射字典之外的标签,脚本应该怎么办。直接报错会让整个转换中断,忽略会导致这个目标丢失。我的方案是:打印警告,跳过该对象,并统计总数。转换完了看一眼统计输出,如果跳过的数量异常,说明标签映射表不完整,事后补上再重新转换即可。
关于可见性,我完全保留X-anylabing的visible值,不去做任何归一化。因为有的训练配置会区分“被遮挡但可见”和“完全不可见”,如果你擅自把1改成2,等于篡改标注信息。保留原始值最稳妥。
3. 转换脚本的设计与实现
3.1 环境准备与核心依赖
写这个脚本不需要太多第三方库,但有几个是必须的:
- Python 3.7+,我测试用的是3.9。
opencv-python,用于可视化验证。numpy,用于坐标计算和数组操作。pyyaml,用于读取外部标签映射文件。
安装命令:
pip install opencv-python numpy pyyaml如果只是转换,不可视化,那只需要用到Python自带的json和os模块就够了。但我强烈建议可视化,原因后面说。OpenCV装的时候注意版本,新版OpenCV的circle函数参数不变,但老版本和新版本对字体类型、线宽的常量定义有区别,代码里我统一用整数。
3.2 核心转换函数实现
先写一个最核心的单个JSON文件转换函数。它的作用是:解析一个JSON文件,提取所有标注,按固定关键点顺序输出YOLO格式的txt行。
import json import os import numpy as np def convert_json_to_txt(json_path, output_dir, keypoint_names, label_map, image_id=0): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_info = data["images"][image_id] img_width = img_info["width"] img_height = img_info["height"] img_name = os.path.splitext(img_info["file_name"])[0] output_txt = os.path.join(output_dir, img_name + ".txt") lines = [] skipped = 0 for ann in data["annotations"]: if ann.get("image_id") != image_id: continue label_name = ann.get("category_name") if label_name not in label_map: print(f"[Warning] label '{label_name}' not in label_map, skipped.") skipped += 1 continue bbox = ann["bbox"] # [x, y, w, h] x, y, w, h = bbox[0], bbox[1], bbox[2], bbox[3] # 边界框归一化 x_center = (x + w / 2.0) / img_width y_center = (y + h / 2.0) / img_height w_norm = w / img_width h_norm = h / img_height # 关键点处理 kp_list = [] for name in keypoint_names: found = False for kp in ann["keypoints"]: if kp["name"] == name: kx = kp["x"] / img_width ky = kp["y"] / img_height kv = kp["visible"] kp_list.extend([round(kx, 6), round(ky, 6), kv]) found = True break if not found: # 缺失的关键点,用0填充并标记不可见 kp_list.extend([0.0, 0.0, 0]) class_id = label_map[label_name] line = [class_id, round(x_center, 6), round(y_center, 6), round(w_norm, 6), round(h_norm, 6)] + kp_list lines.append(" ".join(map(str, line))) with open(output_txt, "w", encoding="utf-8") as f: f.write("\n".join(lines)) print(f"[OK] {json_path} -> {output_txt}, objects: {len(lines)}, skipped: {skipped}")这里我用了round(x, 6)保留六位小数。为什么是六位?因为YOLO训练时坐标精度只要达到1e-6,就足够覆盖常见的4096x4096这种超大图。再多的位数只是浪费存储空间,而且在一些极端情况下浮点数过长还会引发txt解析错误。
值得注意的是,当某个关键点在JSON中不存在时,我填充了[0, 0, 0]。位置填0而不是某个默认坐标,避免模型学到错误的位置先验。
3.3 支持指定标签转换
“可指定标签转换”是这个项目的核心需求之一。我支持两种标签映射方式。
第一种是直接在命令行里用字典指定:
python convert.py --json_dir ./jsons --output_dir ./labels --label_map '{"person": 0, "dog": 1}'当你有多个目标类别时,这种写法一眼就能看明白每个类别的ID。
第二种是使用YAML映射文件。这种方式更适合大型项目,因为标签可能经常调整,改YAML比改命令行方便得多:
# label_map.yaml person: 0 dog: 1 cat: 2然后在脚本里用yaml.safe_load加载:
import yaml def load_label_map(path): with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f)有了标签映射,转换脚本才能适应各种数据集,而不是写死标签名。
这里要说一个我踩过的坑:类别ID必须从0开始且连续递增,中间不要留空洞。YOLO的类别数是通过最大ID+1推断的,如果你把person映射成0、dog映射成2(中间没有1),那么实际训练时类别数会变成3,dog的数组索引是2,但很多时候网络结构是ch_out = nc,这个空洞就会导致维度不匹配,训练直接报错。当然,现在很多框架会自动修正,但数据层面尽量规范。
3.4 可视化验证的实现
转换完成不等于万事大吉,必须画出来看一眼。可视化脚本的作用是:读取txt文件,把边界框和关键点画回原图,保存为新的图片,然后用肉眼看位置是否合理。
核心函数如下,我把关键点按连接线分组,画起来更直观:
import cv2 import numpy as np # COCO 17关键点的连接关系(简化示例) skeleton = [ (0, 1), (0, 2), (1, 3), (2, 4), # 鼻子到眼睛和耳朵 (5, 6), (5, 7), (7, 9), (6, 8), (8, 10), # 手臂 (5, 11), (6, 12), (11, 12), # 躯干 (11, 13), (13, 15), (12, 14), (14, 16) # 腿 ] def draw_yolo_pose_txt(image_path, txt_path, output_path, keypoint_names, class_names=None): img = cv2.imread(image_path) h, w = img.shape[:2] with open(txt_path, "r", encoding="utf-8") as f: lines = f.readlines() for line in lines: parts = list(map(float, line.strip().split())) class_id = int(parts[0]) x_center, y_center, box_w, box_h = parts[1], parts[2], parts[3], parts[4] # 反归一化边界框 x1 = int((x_center - box_w / 2) * w) y1 = int((y_center - box_h / 2) * h) x2 = int((x_center + box_w / 2) * w) y2 = int((y_center + box_h / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) # 提取关键点 kp = [] for i in range(5, len(parts), 3): px, py, pv = parts[i], parts[i + 1], parts[i + 2] if pv > 0: # 只画可见或遮挡的关键点 cv2.circle(img, (int(px * w), int(py * h)), 3, (0, 0, 255), -1) kp.append((int(px * w), int(py * h))) else: kp.append(None) # 连线 for s in skeleton: p1 = kp[s[0]] p2 = kp[s[1]] if p1 is not None and p2 is not None: cv2.line(img, p1, p2, (255, 0, 0), 1) if class_names: label_text = class_names[class_id] else: label_text = str(class_id) cv2.putText(img, label_text, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1) cv2.imwrite(output_path, img)这个可视化函数里有个细节:pv > 0才画点。因为在COCO约定里,0是完全不可见,1是遮挡但有位置,2是可见。但我发现有些标注工具的visible字段含义完全相反,比如0表示可见,1表示不可见。如果你发现画出来的结果点和可见性完全反着,优先检查这里。
3.5 脚本的扩展与批量处理
量产项目不可能只转一个文件,所以我封装了一个批量处理函数,遍历目录里所有JSON文件,逐一转换,并统计总体的处理情况。
def batch_convert(json_dir, output_dir, keypoint_names, label_map): os.makedirs(output_dir, exist_ok=True) total_json = 0 total_objects = 0 total_skipped = 0 for filename in os.listdir(json_dir): if not filename.endswith(".json"): continue json_path = os.path.join(json_dir, filename) # 这里假设每个JSON对应一张图 lines_count, skipped = convert_json_to_txt_by_lines(json_path, output_dir, keypoint_names, label_map) total_json += 1 total_objects += lines_count total_skipped += skipped print(f"\n[Summary] Converted {total_json} JSON files, {total_objects} objects, skipped {total_skipped} due to label mapping issues.")注意:批量转换时我按照文件名一一对应,要求JSON的文件名和图片名一致。如果实际数据集里图片名是0001.jpg,JSON名是0001.json,那没问题。如果对不上,要先统一改文件名或写一个映射关系,否则结果txt会找不到对应图片,训练时会报错。
3.6 参数解析与命令行入口
完整的脚本肯定不能把参数写死在代码里,我用argparse构建了一个命令行入口,支持指定JSON目录、输出目录、关键点顺序文件、标签映射文件、是否可视化等参数。
import argparse def main(): parser = argparse.ArgumentParser(description="X-anylabing JSON to YOLO-POSE txt converter") parser.add_argument("--json_dir", type=str, required=True, help="Directory containing JSON files") parser.add_argument("--output_dir", type=str, required=True, help="Directory to save txt files") parser.add_argument("--keypoint_order", type=str, required=True, help="Path to keypoint order file") parser.add_argument("--label_map", type=str, required=True, help="Path to label map YAML file") parser.add_argument("--visualize", action="store_true", help="Whether to draw verification images") parser.add_argument("--image_dir", type=str, default="", help="Image directory for visualization") parser.add_argument("--vis_output_dir", type=str, default="", help="Output directory for visualization images") args = parser.parse_args() keypoint_names = load_keypoint_order(args.keypoint_order) label_map = load_label_map(args.label_map) batch_convert(args.json_dir, args.output_dir, keypoint_names, label_map) if args.visualize: # 这里可以调用上面写的draw_yolo_pose_txt,遍历目录 pass这里要强调一下为什么需要独立的keypoint_order文件。因为不同数据集可能关键点名称不同,比如COCO是17个,MPII是16个,自定义数据集可能是20个。如果写死在脚本里,每次换数据集都得改代码,不优雅。把它做成一个文本文件,每行一个关键点名称,脚本读了就知道顺序了,这才是“工程级”的处理方式。
4. 实操全过程记录:从JSON到可训练的txt
4.1 准备关键点顺序文件和标签映射文件
先创建一个keypoint_order.txt,内容按序排列:
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再创建一个label_map.yaml:
person: 0注意这里我用了person: 0而不是person: 1。类别ID从0开始是YOLO的标准约定,如果从1开始,训练时类别数会多算一个,而且背景类还单独占一个位置,非常容易出错。
4.2 执行转换命令
假设我的JSON文件放在./any_data/jsons,图片放在./any_data/images,输出目录是./any_data/labels,可视化图片放到./any_data/vis。执行:
python convert.py \ --json_dir ./any_data/jsons \ --output_dir ./any_data/labels \ --keypoint_order keypoint_order.txt \ --label_map label_map.yaml \ --visualize \ --image_dir ./any_data/images \ --vis_output_dir ./any_data/vis跑完之后,控制台输出每一张图的转换结果。我当时的输出是:
[OK] img_0001.json -> ./any_data/labels/img_0001.txt, objects: 3, skipped: 0 [OK] img_0002.json -> ./any_data/labels/img_0002.txt, objects: 2, skipped: 0 ... [Summary] Converted 1280 JSON files, 3521 objects, skipped 0 due to label mapping issues.这个输出非常关键。如果skipped数很大,说明标签映射文件有遗漏,赶紧回去补。如果不看这个统计,你都不知道数据已经丢了。
4.3 可视化验证的两种方式
我实际工作中会做两层可视化验证。
第一层是样本抽检。随机抽20~30张图片,把txt和图片对应上,用上面的draw_yolo_pose_txt函数画出来,然后人眼检查。重点看三件事:
- 边界框是否贴合目标主体,不要半截框。
- 关键点是否落在语义正确的位置,比如鼻子在脸中央、手腕在手腕处。
- 遮挡关键点是否被正确标为1,而不是凭空瞎画。
第二层是自动检查。写一个简单的脚本,扫描所有txt,统计有没有出界的归一化坐标(小于0或大于1),有没有负数的宽高,有没有关键点顺序位数不对的行。这些是训练时最容易崩的地方。
def check_txt_health(txt_dir): bad_lines = 0 for txt_file in os.listdir(txt_dir): if not txt_file.endswith(".txt"): continue with open(os.path.join(txt_dir, txt_file), "r", encoding="utf-8") as f: for line in f: parts = list(map(float, line.strip().split())) # YOLO-POSE: 1 class + 4 bbox + 17*3 kp = 56 if len(parts) != 56: print(f"[Bad length] {txt_file}: {len(parts)}") bad_lines += 1 continue # 检查边界框坐标 for i in range(1, 5): if parts[i] < 0 or parts[i] > 1: print(f"[Out of range] {txt_file}: {line.strip()}") bad_lines += 1 break print(f"[Check] Done, {bad_lines} bad lines.") check_txt_health("./any_data/labels")这里有个想法:如果某个关键点的坐标因为标注遗漏而填了0,恰好目标在图片左上角,这个0也不一定越界,所以自动检查只能查出格式问题,语义问题还得靠可视化。
4.4 参数计算过程举例
我用一张具体图片说明转换时坐标是怎么算的。
假设原图尺寸是1280x720,标注框是[310, 52, 562, 671],即左上角x=310,y=52,宽562,高671。那么:
- 中心点x = 310 + 562/2 = 591
- 中心点y = 52 + 671/2 = 387.5
- 归一化x_center = 591 / 1280 = 0.46171875,取六位小数约0.461719
- 归一化y_center = 387.5 / 720 = 0.53819444,约0.538194
- 归一化宽 = 562 / 1280 = 0.4390625,约0.439062
- 归一化高 = 671 / 720 = 0.93194444,约0.931944
如果某个关键点,比如鼻子坐标是(402, 158),那么:
- 归一化x = 402 / 1280 = 0.3140625,约0.314062
- 归一化y = 158 / 720 = 0.21944444,约0.219444
输出行就是:
0 0.461719 0.538194 0.439062 0.931944 0.314062 0.219444 2 ...看见没,所有数字都在0~1之间,这就是YOLO训练需要的标准格式。
5. 常见问题与排查技巧实录
5.1 关键点坐标偏移,但边界框正确
这是最容易踩的坑,我开头提到的那个case。表现为:边界框贴得很完美,但关键点全往左上方偏了一截。
原因通常是X-anylabing在标注时图片经过了缩放或裁剪,导出的JSON中关键点坐标不是相对于原图的,而是相对于标注视图的。比如标注时图片显示宽度是800,实际原图宽度是1280,如果不做比例还原,所有x坐标都会偏小。
排查方法:取一张图,手动找一个关键点的像素坐标,除以原图宽,再对比txt里的归一化值。如果差异明显,说明坐标基准不对。
解决办法:在转换前,找到X-anylabing标注视图的尺寸(一般在JSON的info里有记录,或者标注时的界面比例),先把关键点坐标按比例映射回原图坐标,再进行归一化。
# 示例:如果标注视图宽是800,原图宽是1280 scale_x = img_width / 800 scale_y = img_height / 600 # 假设高也是缩放过的 real_x = kp["x"] * scale_x real_y = kp["y"] * scale_y5.2 可见性标志全部是0,导致关键点没画出来
有粉丝问过我一个现象:转换后的txt关键点位置都对,但训练效果很差,loss震荡。一查发现,所有关键点的可见性都是0,等于模型根本没学到关键点。
我排查后发现,X-anylabing在标注时如果没手动修改,默认可见性可能是0或是某个特殊值,而转换脚本如果把这个值当成了“完全不可见”,训练时就会忽略这些关键点。
解决方法是:在转换前先统计一下JSON里visible字段的取值分布。如果全是0,说明标注时可见性没被正确写入,需要在转换时做一次默认值修正,把0改为2(可见)。如果分布正常(2/1/0混合),就不要乱改。
python -c " import json data = json.load(open('annotations.json')) vis_values = set() for ann in data['annotations']: for kp in ann['keypoints']: vis_values.add(kp['visible']) print(vis_values) "5.3 每个JSON对应多张图,image_id怎么对齐
X-anylabing支持一次导出多个图片的标注,经常出现一个JSON文件里包含几十张图片和几百个对象的标注。如果你的数据集结构是这样的,就不能简单假设JSON文件名和图片名一致。
这时候需要遍历images数组,再遍历annotations,按image_id过滤出属于当前图片的对象,分别生成每张图片的txt文件。
我在上面的示例代码里实际上已经给了image_id参数,但批量场景下需要循环所有图片ID。还有,如果一张图没有标注,txt文件应该留空还是跳过?我的建议是:跳过生成,但要打印提示。因为YOLO训练时,空txt文件会让数据加载器卡住或产生未定义行为。
5.4 标签ID不连续,模型训练报维度错误
这个问题在上面提过一嘴,但值得展开说。假设你的数据集有“person”和“helmet”两类,你映射成了{"person": 0, "helmet": 2},中间跳过了1。那么数据加载器看到的最大ID是2,类别数nc=3,但第二类实际缺失,损失函数计算时索引1永远不会被激活,模型最终输出3个通道。看起来很合理,但当数据集里只有两种目标时,这3个通道的第2个通道对应一个不存在的类,如果测试时预测出这类,就白白浪费了。
更糟糕的是,如果在训练过程中加了类别采样或数据增强,这种空洞的ID可能会导致内存问题或奇怪的分错。遵守从0开始连续递增,是数据集转换的最基本纪律。
5.5 图片尺寸和JSON尺寸不一致
有时候JSON里记录的width和height和实际图片的像素尺寸不一致。这种情况常见于:标注时图片被软件压缩,但JSON里的信息还是压缩前的,或者标注人员修图替换了原图,但没有更新标注。
这个问题的检测方法也很简单:在可视化阶段,如果发现所有框和点整体向左上或右下偏移,且偏移量随位置线性变化,那就是尺寸不一致。解决方法是:以实际图片尺寸为准,重新计算归一化坐标。
我在转换脚本里特意留了一个--use_actual_size参数,开启后会用cv2.imread读取图片实际尺寸来替换JSON里的宽高。
5.6 关键点顺序错误导致关键点错乱
这个坑非常隐蔽,因为它不影响训练跑通,但模型预测时鼻子会显示在肩膀上。原因是:X-anylabing里的关键点顺序和YOLO-POSE需要的顺序不一致,如果你的转换脚本直接按JSON里的出现顺序输出,那顺序就乱了。
解决方案就是我前面说的keypoint_order文件。转换前先确认关键点顺序是真的和你训练配置一致,尤其是COCO预训练权重,它的顺序是固定的。如果顺序不对,必须调整keypoint_order文件内容,而不是改代码。
6. 可视化大屏与应用的一点点延伸思考
说句实话,纯做转换和可视化已经能解决训练前的数据准备需求了,但“可视化”这个需求在网络热词里出现了很多次,比如“可视化大屏”、“数据可视化”、“python数据分析与可视化”。这说明很多人做标注转换不只是为了自己看,还要向上汇报、展示数据集质量、判断标注一致性。如果你的场景也是要给团队或客户看标注质量,那可以直接把可视化结果做成一个HTML报告,每张图片一个缩略图,汇总转换统计信息和标签分布。
这个延伸思路不难实现:用Pandas统计每个类别的目标数量、每张图的目标数量、关键点可见性分布,然后用简单的HTML模板生成一个报告页面。这样就不需要一张张去翻图片了,汇报时打开网页一目了然。
不过我在实际项目里发现,可视化验证和Auto QA(自动质量检查)才是真正的生产力,大屏展示反而是次要的。数据准备阶段,画出来的图能帮你快速发现上节说的那几个坑;自动检查可以批量扫描异常文件。两者结合,数据集的置信度就上来了。
7. 实操总结与我的几点个人体会
这个项目本身不复杂,但涉及到的知识点很杂:JSON解析、YOLO格式规范、关键点顺序约定、标签映射策略、图像坐标变换、OpenCV可视化。任何一个环节出错,都会直接影响到后续训练效果。
结合我这几次做姿态估计的真实经历,总结几条硬经验:
- 转换脚本写完之后,先拿20张图手工验证,确认边界框、关键点、可见性全部正确,再全量转换。别贪快,一贪快就出幺蛾子。
- 保留原始JSON文件,永远不要删。因为转换脚本可能有bug,训练发现数据错误时可以回退重转,不用重新标注。
- 标签映射文件用YAML管理,版本控制里也纳管。项目迭代时标签增删是常态,有个文件历史记录才不会乱。
- 自动检查脚本一定要写,每次转完跑一遍,把坐标越界、维度错误、标签超范围的行全找出来。
- 可视化时把关键点画大一点,3像素半径在720p图上能看清,但在4K图上几乎隐形。我习惯根据图片宽高动态调整半径和线宽,比如
r = max(2, int(w/500))。
我做这个转换脚本踩过最深的坑就是基准坐标问题。当时赶进度,没验证就直接训练,结果关键点全偏。后来重新设计了转换流程,才彻底根治。现在每次新项目,我第一件事就是做数据格式验证,而不是急着写模型,这大概就是所谓“数据决定了模型的上限”的真正含义吧。
最后再分享一个小技巧:转换后的txt可以用numpy数组读回内存,做一次统计性检查,比如每个关键点的平均可见性比例、每个坐标的均值方差。这些统计特征能帮你判断标注是否整体偏移、是否存在系统性问题。我遇到过一次,某个batch的txt所有关键点x坐标都小于0.3,一查发现标注时只标了目标的左半边,这种问题光靠眼抽检根本发现不了。