资讯详情

X-AnyLabeling标注JSON转YOLO-POSE格式txt全流程详解与可视化验证

📅 2026/10/8 3:08:20 | 华诺云谱 👁 阅读
X-AnyLabeling标注JSON转YOLO-POSE格式txt全流程详解与可视化验证
干姿态估计这一行的人十有八九都被数据标注格式转换折腾过。X-AnyLabeling标注完一套关键点数据导出是JSONYOLO-POSE训练要的是txt中间还牵扯到标签筛选、坐标归一化、关键点可见性标记。今天这篇就是来把这个过程彻底讲透JSON转txtYOLO-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_vcls_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 16X-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 numpy3.2 转换脚本主体代码下面是我整理的一套可以直接跑通的核心代码去掉了过于工程化的封装保留主干逻辑方便你理解。核心思路是从配置读取标签筛选规则遍历JSON解析每个shapes目标按预定义的关键点顺序表取值归一化写出txt。脚本开头我建议用argparse接收参数命令行可以灵活指定标签筛选import json import argparse import os from pathlib import Path # 标签转换映射需要转换哪些标签到哪个类别ID # 这里表示只转换person类别类别ID为0dog会被忽略 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, encodingutf-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[fkpt_{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, encodingutf-8) as f: f.write(\n.join(lines) \n) def main(): parser argparse.ArgumentParser(descriptionX-AnyLabeling JSON - YOLO-POSE txt) parser.add_argument(--json_dir, typestr, requiredTrue) parser.add_argument(--label_out_dir, typestr, requiredTrue) parser.add_argument(--img_dir, typestr, requiredTrue) args parser.parse_args() os.makedirs(args.label_out_dir, exist_okTrue) 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, encodingutf-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_okTrue) 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个角点等场景。但就当前这个需求来说上面的方案已经完全够用可以直接投入生产环境。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑