☰
X-AnyLabeling标注JSON转YOLO-POSE格式txt全流程详解与可视化验证
2026/10/8 3:08:19 网站建设 项目流程

干姿态估计这一行的人,十有八九都被数据标注格式转换折腾过。X-AnyLabeling标注完一套关键点数据,导出是JSON,YOLO-POSE训练要的是txt,中间还牵扯到标签筛选、坐标归一化、关键点可见性标记。今天这篇就是来把这个过程彻底讲透:JSON转txt(YOLO-POSE关键点预测模型)及其可视化,支持指定标签转换,把脚本、原理、踩坑记录一次说清楚,适合正在做姿态估计训练、被数据格式卡住的朋友直接抄作业。

1. 为什么绕不开“JSON转txt”这一步

1.1 X-AnyLabeling导出格式与训练格式的天然差异

X-AnyLabeling是个非常好用的开源标注工具,尤其是做关键点标注的时候,交互体验比纯CVAT轻量,也比LabelMe更顺手。它导出的标注文件是JSON结构,里面记录了每个目标的类别名、关键点名称、关键点坐标、框的顶点坐标,还有图片名、图片尺寸等元信息。这个结构对“人”来说是友好的——一眼看过去就知道哪个点是左肩、哪个点是右肘。

但YOLO-POSE训练时吃的不是这种结构化数据,而是纯文本的txt格式:每行一个目标,格式大致是class_id x_center y_center width height接上17个(或你定义数量)关键点的x_n y_n vis_n。所有的坐标都必须归一化到0到1之间,关键点可见性用0或1(有些任务用0/1/2)表示。这套格式的设计目标只有一个——让训练时的数据加载器能以最少的IO开销、最快的字符串解析速度把标注读进来。

所以只要你想用X-AnyLabeling标注的数据跑YOLO-POSE,就必须写一个转换程序。这活儿看起来机械,实际上门道不少:关键点顺序怎么对齐?哪些标签要转哪些不要?COCO骨架定义和自有骨架定义怎么映射?坐标归一化除宽度还是高度?这些问题不搞清楚,转换完的txt就是错的,训练出来的模型关键点全飘。

1.2 一次转换要解决的核心问题

我总结下来,一份健壮的转换脚本至少要回答这么几个问题:

  • 怎么定位JSON里的“关键点”和“框”?不同标注版本导出结构不完全一样,有的keypoints字段是数组,有的直接存在points里。
  • 怎么按需指定标签?项目里可能同时标注了person和dog,但训练只做person姿态估计,那dog的数据就不能进训练集。
  • 关键点顺序怎么保证稳定?YOLO-POSE的txt里关键点顺序是固定的,而X-AnyLabeling里每个目标的keypoints字典顺序可能因为标注顺序变化而变化。不显式排序,转换结果就不可复现。
  • 可见性怎么算?从来没标出来的点、被遮挡的点、工具自动填充的点,都要有明确的处理策略。

这些点看起来小,却是转换脚本从“能跑”走向“能用”的关键分水岭。我在第二部分会逐个拆解。

2. 转换核心设计与关键参数解析

2.1 JSON标注数据内部长什么样

做转换之前,先得把X-AnyLabeling导出的JSON结构摸清楚。每个人的标注习惯可能不同,但典型的keypoints标注导出长这样(我做脱敏和简化处理):

{ "version": "1.0", "flags": {}, "shapes": [ { "label": "person", "points": [[45, 67], [90, 121], [34, 200]], "group_id": null, "shape_type": "keypoints", "flags": {}, "keypoints": { "nose": [45, 67], "left_eye": [90, 121], "right_eye": [34, 200], "left_shoulder": [160, 80] } } ], "imagePath": "frame_0001.jpg", "imageData": null }

注意声明的shape_type为keypoints时,points数组和keypoints字典往往保存的是同一批坐标,只是组织方式不同。这里最容易踩的坑是:keypoints字典里字段顺序不稳定,而且可能缺关键点——比如某个目标没标右眼,字典里就完全没有right_eye这个键。写转换脚本时必须按照预定义的关键点顺序列表去取,取不到就填(0, 0, 0)。

2.2 YOLO-POSE的txt格式规范对照

YOLO-POSE的txt每一行格式如下(以COCO 17关键点为例):

<cls_id> <xc> <yc> <w> <h> <k1_x> <k1_y> <k1_v> <k2_x> <k2_y> <k2_v> ... <k17_x> <k17_y> <k17_v>

cls_id是类别编号,从0开始。xc、yc是目标框中心的归一化坐标,w、h是框的归一化宽高。关键点后面每个点三个数:归一化x、归一化y、可见性。

可见性在YOLO-POSE的训练实现里,常见有两种约定:一种是0表示该点缺失、不可用,1表示可见;另一种是COCO风格,0表示未标注、1表示有标注但被遮挡、2表示可见。如果你是用Ultralytics的YOLOv8-pose或者YOLO11-pose,代码里默认会读vis值,大于等于1的才参与损失计算的点。所以我建议统一用0/1:缺失填0,存在填1。这样最直观,也不容易在训练时引入奇怪行为。

坐标归一化有个细节必须说清楚:关键点的x、y是用图片宽高分别归一化,不是用目标框的宽高。目标框自身只用中心点坐标和宽高这四个参数归一化。有人贪图省事,把关键点相对框归一化,训练出来的模型关键点定位会完全乱掉,因为推理时模型输出的关键点坐标本来就是像素坐标(或归一化到图的坐标),和框没关系。

2.3 关键点顺序与骨架映射设计

这是整个转换脚本里最能体现工程质量的地方。YOLO-POSE训练时,txt里第几个关键点代表身体哪个部位,完全由你训练时传入的数据配置决定——模型不关心你的关键点叫什么名字,只关心索引顺序是否一致。

所以最佳做法是:在项目里维护一份常量列表,定义关键点顺序和骨架连接关系。比如我常用的是COCO 17点顺序:

NOSE = 0 LEFT_EYE = 1 RIGHT_EYE = 2 LEFT_EAR = 3 RIGHT_EAR = 4 LEFT_SHOULDER = 5 RIGHT_SHOULDER = 6 LEFT_ELBOW = 7 RIGHT_ELBOW = 8 LEFT_WRIST = 9 RIGHT_WRIST = 10 LEFT_HIP = 11 RIGHT_HIP = 12 LEFT_KNEE = 13 RIGHT_KNEE = 14 LEFT_ANKLE = 15 RIGHT_ANKLE = 16

X-AnyLabeling导出时关键点名称可能叫l_shoulder、r_shoulder,那就需要建立一个从工具命名到标准索引的映射表。脚本里用一个字典即可:

KEYPOINT_ALIAS_MAP = { "nose": 0, "left_eye": 1, "l_eye": 1, "right_eye": 2, "r_eye": 2, "left_ear": 3, "l_ear": 3, "right_ear": 4, "r_ear": 4, "left_shoulder": 5, "l_shoulder": 5, "right_shoulder": 6, "r_shoulder": 6, "left_elbow": 7, "l_elbow": 7, "right_elbow": 8, "r_elbow": 8, "left_wrist": 9, "l_wrist": 9, "right_wrist": 10, "r_wrist": 10, "left_hip": 11, "l_hip": 11, "right_hip": 12, "r_hip": 12, "left_knee": 13, "l_knee": 13, "right_knee": 14, "r_knee": 14, "left_ankle": 15, "l_ankle": 15, "right_ankle": 16, "r_ankle": 16, }

骨架连接关系(用于可视化)也放在常量里,这样后面画图的时候直接索引即可。骨架定义建议参考COCO:(0,1)鼻子-左眼、(0,2)鼻子-右眼、(1,3)左眼-左耳、(2,4)右眼-右耳、(5,6)双肩、(5,7)左肩-左肘、(7,9)左肘-左腕、(6,8)右肩-右肘、(8,10)右肘-右腕、(5,11)左肩-左髋、(6,12)右肩-右髋、(11,13)左髋-左膝、(13,15)左膝-左踝、(12,14)右髋-右膝、(14,16)右膝-右踝。

3. 实操:JSON转txt完整脚本实现

3.1 环境准备与目录结构

我的建议是任何转换任务都单独建一个文件夹,不要和训练目录混在一起。项目目录结构如下:

pose_data_converter/ ├── convert.py ├── visualize.py ├── labels.txt ├── annotations/ # X-AnyLabeling导出的JSON │ ├── frame_0001.json │ ├── frame_0002.json │ └── ... ├── images/ # 与JSON同名的原图 ├── labels/ # 输出txt存放目录 └── viz_output/ # 可视化验证输出

文件安排之所以要分离convert.py和visualize.py,是因为开发调试阶段你大概率需要反复来回切:先转换一批,再可视化检查,发现问题改映射,再重新转换。分离脚本避免每次重复执行不必要的IO。

环境只需要Python 3.8+和OpenCV-python、Pillow两个库。如果你要把可视化结果拼成大图批量看,建议顺手装numpy。安装一行:

pip install opencv-python pillow numpy

3.2 转换脚本主体代码

下面是我整理的一套可以直接跑通的核心代码,去掉了过于工程化的封装,保留主干逻辑方便你理解。核心思路是从配置读取标签筛选规则,遍历JSON,解析每个shapes目标,按预定义的关键点顺序表取值,归一化,写出txt。脚本开头我建议用argparse接收参数,命令行可以灵活指定标签筛选:

import json import argparse import os from pathlib import Path # 标签转换映射:需要转换哪些标签到哪个类别ID # 这里表示只转换person类别,类别ID为0;dog会被忽略 LABEL_TO_ID = { "person": 0, # "dog": 1, # 想转狗就把注释打开 } KEYPOINT_ALIAS_MAP = { "nose": 0, "left_eye": 1, "l_eye": 1, "right_eye": 2, "r_eye": 2, "left_ear": 3, "l_ear": 3, "right_ear": 4, "r_ear": 4, "left_shoulder": 5, "l_shoulder": 5, "right_shoulder": 6, "r_shoulder": 6, "left_elbow": 7, "l_elbow": 7, "right_elbow": 8, "r_elbow": 8, "left_wrist": 9, "l_wrist": 9, "right_wrist": 10, "r_wrist": 10, "left_hip": 11, "l_hip": 11, "right_hip": 12, "r_hip": 12, "left_knee": 13, "l_knee": 13, "right_knee": 14, "r_knee": 14, "left_ankle": 15, "l_ankle": 15, "right_ankle": 16, "r_ankle": 16, } NUM_KEYPOINTS = 17 def convert_json_to_yolo_pose(json_path, img_width, img_height, output_label_path): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) lines = [] shapes = data.get("shapes", []) for shape in shapes: label = shape.get("label", "") if label not in LABEL_TO_ID: continue cls_id = LABEL_TO_ID[label] # 关键点字典:兼容不同版本字段名 keypoints_raw = shape.get("keypoints", {}) if not keypoints_raw: # 有的版本用points存关键点,且第一个点可能是bbox左上角等 pts = shape.get("points", []) # 这里假设points数组就是按关键点顺序排列的 for idx, pt in enumerate(pts): keypoints_raw[f"kpt_{idx}"] = pt # 生成初始关键点数组 kpt_arr = [0.0, 0.0, 0] * NUM_KEYPOINTS for name, idx in KEYPOINT_ALIAS_MAP.items(): if name not in keypoints_raw: continue x, y = keypoints_raw[name][:2] kpt_arr[idx * 3] = x / img_width kpt_arr[idx * 3 + 1] = y / img_height kpt_arr[idx * 3 + 2] = 1 # 可见 # 计算目标框:请根据你的标注实际结构来取。常见情况是单独的bbox字段。 # 如果用关键点坐标推算bbox,取所有可见点的最小外接矩形加一定边距 xs = [keypoints_raw[name][0] for name in keypoints_raw if name in KEYPOINT_ALIAS_MAP and keypoints_raw[name]] ys = [keypoints_raw[name][1] for name in keypoints_raw if name in KEYPOINT_ALIAS_MAP and keypoints_raw[name]] if not xs: continue x1, x2 = min(xs), max(xs) y1, y2 = min(ys), max(ys) bw = max(x2 - x1, 1) bh = max(y2 - y1, 1) xc = (x1 + x2) / 2.0 / img_width yc = (y1 + y2) / 2.0 / img_height line = f"{cls_id} {xc:.6f} {yc:.6f} {bw / img_width:.6f} {bh / img_height:.6f}" for i in range(NUM_KEYPOINTS): line += f" {kpt_arr[i * 3]:.6f} {kpt_arr[i * 3 + 1]:.6f} {kpt_arr[i * 3 + 2]}" lines.append(line) with open(output_label_path, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") def main(): parser = argparse.ArgumentParser(description="X-AnyLabeling JSON -> YOLO-POSE txt") parser.add_argument("--json_dir", type=str, required=True) parser.add_argument("--label_out_dir", type=str, required=True) parser.add_argument("--img_dir", type=str, required=True) args = parser.parse_args() os.makedirs(args.label_out_dir, exist_ok=True) for json_file in sorted(Path(args.json_dir).glob("*.json")): img_stem = json_file.stem img_path = Path(args.img_dir) / f"{img_stem}.jpg" if not img_path.exists(): img_path = Path(args.img_dir) / f"{img_stem}.png" if not img_path.exists(): print(f"[跳过] 找不到图片: {img_stem}") continue from PIL import Image with Image.open(img_path) as im: w, h = im.size out_txt = Path(args.label_out_dir) / f"{img_stem}.txt" convert_json_to_yolo_pose(str(json_file), w, h, str(out_txt)) print(f"[已转换] {img_stem}") if __name__ == "__main__": main()

细节说明:这个脚本里我故意保留了兼容逻辑——如果X-AnyLabeling的某个导出版本里没有keypoints字典,就尝试从points数组里按顺序取。实际项目里如果发现字段名不同,需要把脚本里的字段名改成你导出的实际字段名。不要迷信网上任何一份现成脚本能直接跑通,标注工具的版本更新速度远超想象。

另一个关键点是bbox取值逻辑。我这里用所有可见关键点坐标的最小外接矩形作为目标框,这样对“只有人体部件标注”的数据集较稳妥。但如果你的标注里本来就有独立的框(X-AnyLabeling的rectangle标注或human_pose模式导出带了bbox),优先读框字段而不是自己算。自己算框的缺点是:如果一个人只标了上半身关键点,框就只有上半身,训练时模型对下半身的定位会受影响。

注意:如果X-AnyLabeling的JSON里同时存在bbox字段且坐标是顶点格式,比如[x1, y1, x2, y2],优先用这个框数据,这才是标注者真正想框住的区域。用关键点外接框只是没有框数据时的兜底方案。

3.3 指定标签转换的实际应用

上面代码里LABEL_TO_ID就是标签筛选的关键配置。实战中常见场景是:标注文件里什么都有,人、车、狗、猫,但你的任务只关心人,而且人的关键点定义和狗完全不一样。这时候你只需要在LABEL_TO_ID里写{"person": 0},非person的shape全部被跳过。

还有一类更隐蔽的场景:同一个目标标了两套关键点,比如“person”和“person_face”。如果训练YOLO-POSE模型只需要身体17点,那就要在KEYPOINT_ALIAS_MAP里不要放脸部的额外关键点名。否则脚本会把多余的颈部、髋部等也当成关键点填进去,导致txt出现超过17组的浮点数,训练时直接报维度错误。

我建议在脚本里加一个硬性检查:生成完一行数据后,统计后半段的关键点组数是否等于NUM_KEYPOINTS,不等于就抛异常并打印是哪个JSON、哪个label出了问题。这种防御性写法的价值,在数据集有几百上千张图时尤其明显——你不会想等训练跑了一半才发现某张图的txt格式不对。

3.4 完整可视化验证流程

转换完成并不代表万事大吉。我就有过惨痛教训:JSON解析逻辑写错了,坐标顺序没对齐,转换出来的txt关键点横七竖八,训练出来的YOLO-POSE模型关键点直接飞边。所以转换之后必须可视化逐张检查。下面是一个实用的可视化脚本核心代码:

import cv2 import numpy as np from pathlib import Path # 骨架连接定义 SKELETON = [ (0, 1), (0, 2), (1, 3), (2, 4), (5, 6), (5, 7), (7, 9), (6, 8), (8, 10), (5, 11), (6, 12), (11, 13), (13, 15), (12, 14), (14, 16) ] def draw_pose_yolo_txt(img_path, txt_path, out_path): img = cv2.imread(str(img_path)) h, w = img.shape[:2] with open(txt_path, "r", encoding="utf-8") as f: lines = f.read().strip().splitlines() for line in lines: parts = list(map(float, line.strip().split())) if len(parts) < 7: continue cls_id = int(parts[0]) xc, yc, bw, bh = parts[1:5] 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(img, (x1, y1), (x2, y2), (0, 255, 0), 2) kpts = parts[5:] points = [] for i in range(0, len(kpts), 3): kx = int(kpts[i] * w) ky = int(kpts[i + 1] * h) vis = int(kpts[i + 2]) points.append((kx, ky, vis)) if vis > 0: cv2.circle(img, (kx, ky), 3, (0, 0, 255), -1) for idx1, idx2 in SKELETON: if idx1 < len(points) and idx2 < len(points): p1, p2 = points[idx1], points[idx2] if p1[2] > 0 and p2[2] > 0: cv2.line(img, (p1[0], p1[1]), (p2[0], p2[1]), (255, 0, 0), 2) cv2.imwrite(str(out_path), img) print(f"[已生成] {out_path}") # 批量跑全部txt img_dir = Path("images") label_dir = Path("labels") viz_dir = Path("viz_output") viz_dir.mkdir(exist_ok=True) for txt_path in sorted(label_dir.glob("*.txt")): img_path = img_dir / f"{txt_path.stem}.jpg" if not img_path.exists(): img_path = img_dir / f"{txt_path.stem}.png" if img_path.exists(): draw_pose_yolo_txt(img_path, txt_path, viz_dir / f"{txt_path.stem}_viz.jpg")

这个可视化脚本做的事其实非常朴素:从txt里读坐标、还原成像素坐标、画框、画点、画骨架线。但它把训练数据的真实面貌直接呈现出来,任何解析错位都逃不过眼睛。我习惯把viz_output里的图拼成九宫格或做成视频逐帧扫一遍,一分钟就能筛出几百张图里的异常。

注意一个细节:画点的半径和线段粗细,如果是在1080P以上分辨率的图上建议把半径调到4、线宽调到3,不然小目标的关键点糊成一片,看不出来对齐问题。如果图片数量大,还可以把可视化结果缩略图输出,反正目的只是检查标注质量,不需要原图精度。

4. 常见问题与排查技巧实录

4.1 典型错误清单

我整理了转换过程中最常踩的几类坑,每条都有对应的排查思路:

错误表现根本原因排查方法
训练时报关键点维度不匹配txt行里关键点组数不等于模型定义数用脚本统计异常行数,打印该行长度
所有关键点都堆在图片左上角归一化时用的图像尺寸不对,或JSON坐标值本来就是相对坐标打印第一行txt原始float值,和图像尺寸对比
只有框没有关键点解析的字段名不对,keypoints键名变了打印单个JSON的JSON缩进结构,确认字段
某些目标没被转换标签筛选字典漏了对应标签检查LABEL_TO_ID是否包含目标标签
可视化时骨架线乱连关键点索引顺序与骨架定义不一致单独打印某个点的像素坐标,对照原图确认是哪个部位
大量目标框宽高为0被过滤框数据解析用了错误的字段或轴序检查bbox字段是xyxy还是xywh,是顶点还是中心点表示

4.2 排查脚本的进阶用法

除了基础可视化,我还建议在转换脚本里加一个统一的校验模式:对每个生成的txt,检查所有坐标是否都在0到1之间、是否有NaN、每行元素数量是否为5 + 17 * 3、是否有重复的类别ID定义冲突。这类校验能自动揪出绝大多数隐藏问题。

有一个坑特别值得单独拿出来说:X-AnyLabeling在标注视频连续帧时,偶尔会在不存在的关键点上自动填充上一帧的坐标值。如果标注者没注意到这个填充行为,转换后关键点坐标看起来正常,但其实是“假可见”——标注者并未在该帧实际标记这个点。这种问题可视化很难看出来,需要结合任务逻辑判断。如果项目对关键点准确性要求高,建议在转换前用脚本筛查:连续多帧中某个关键点坐标完全没变化的情况,自动标记为疑似伪标注。

4.3 一个绕不开的陷阱:图像通道顺序

可视化时用OpenCV画图,读进来是BGR顺序。如果和原始的RGB显示混淆,输出的可视化图颜色会整体发蓝发红。这个问题虽然不影响txt数据本身,但在你检查可视化结果时会产生误导——尤其当你用matplotlib叠加显示时,更要注意通道转换。我的做法是固定统一用OpenCV的cv2.imread读图、cv2.imwrite写图,避免混用不同库。

还有一个容易忽视的点:因为YOLO-POSE训练是读取txt的归一化坐标,而原图尺寸在训练时会被resize,所以txt里的坐标必须是相对于原图的归一化值。如果你的JSON导出时自带了imageWidth和imageHeight字段,优先用JSON里的值,不要自己去读图算,因为有的视频抽帧流程里JSON图片路径对应的文件已经被压缩过,尺寸变了,归一化基准就对不上了。

4.4 批量转换时的工程化建议

当你要处理上千张图时,逐张转换的效率影响就体现出来了。这时建议给脚本加几个工程化特性:一是用tqdm显示进度条,二是支持多进程并行转换,三是对已转换的文件做幂等校验(如果输出txt已经存在且格式正确,可以跳过)。

多进程并行其实很简单,核心逻辑用一个worker函数接收JSON路径,返回成功或失败状态,然后用concurrent.futures.ProcessPoolExecutor跑。注意在Windows上跑多进程时要把主逻辑放到if __name__ == "__main__":下面,否则会无限递归。这个细节我在第一次写并行版本时就被坑过。

5. 个人实操总结与经验心得

这套转换+可视化流程我前后迭代了三个版本才稳定下来。最初版本只做了最基础的JSON解析和txt输出,结果在验证时发现关键点顺序完全对不上,返工改映射表又花费了大量时间。后来我养成了一个习惯:每次改完转换脚本,都先拿几张图做完整验证——转换、可视化、人工对照原图,确认无误后再批量跑数据集。这个过程虽然烦琐,但能让你在训练的起点就确保数据质量。

另外,如果你做的是自有关键点定义(比如工业场景里只标4个点、8个点),不要把上面代码里的NUM_KEYPOINTS和骨架定义死扣成COCO标准。改这两个常量即可,脚本逻辑完全通用。唯一要注意的是:YOLO-POSE预训练权重一般是基于COCO 17点训练的,如果你自定义关键点数量和语义彻底变了,千万别加载预训练权重,从头训练反而更干净。

以后再做类似数据集,我可能还会把转换脚本扩展成支持COCO JSON格式互转、支持多类别关键点并存(比如person用17点、car用4个角点)等场景。但就当前这个需求来说,上面的方案已经完全够用,可以直接投入生产环境。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询