资讯详情

资讯详情

Python解析Human3.6M:从BVH到3D关键点坐标的完整指南

简介面向计算机视觉与机器学习研究者的Human3.6M三维人体姿态数据集获取工具包专门解决从官方渠道下载、解压到数据预处理的完整流程中遇到的常见问题适合需要批量使用该标准数据集的初学者或工程团队。资源共14个文件压缩包大小仅34KB主要包含Python脚本download_all.py、extract_all.py、process_all.py、metadata.py、依赖清单requirements.txt、Dockerfile与docker-compose.yml容器化配置以及config.ini.example配置示例、checksums.txt校验文件、README说明文档、metadata.xml等文件类型覆盖源码、配置、容器编排与文档。其中下载、解压、元数据解析、关节坐标处理等脚本可直接独立运行配合校验文件可确保数据完整性容器化配置则帮助快速搭建一致的环境。目前已有3628人学习使用。该工具包虽然体积小巧但完整覆盖了数据获取与预处理的关键环节能帮助读者省去大量踩坑时间将精力集中于后续的3D姿态估计模型训练与评估。1. Python 获取 Human3.6M 数据集问题不在下载在解析做过 3D 人体姿态估计的人都知道Human3.6M 是目前学术界用得最多的 benchmark 之一但也是新手最容易在数据获取阶段就翻车的一个数据集。很多人以为难点在模型训练实际上光是把数据从压缩包里变成能喂给网络的(N, J, 3)张量就能劝退一大半人。这份资源的核心价值不在于它给了你几个下载链接而在于它把「官方原始数据 → 可用训练数据」这条链路完整地拆开了从协议申请、文件目录解析、BVH 运动文件读取到关键点坐标提取和可视化验证。适合谁来用如果你正在复现 SRNet、HMR、VIBE 这类模型或者想用 Human3.6M 做跨数据集评估这篇文章正好帮你把数据准备这关过掉。我在拆这个资源的时候把源码包里每个脚本都跑了一遍这里把我验证过的流程和踩过的坑一并写出来。2. 下载前的门道协议申请、目录结构与 Python 依赖2.1 别急着写代码先过协议关Human3.6M 不是公开直下载的。你需要去官网填一份协议注明所在机构和用途。这一步看起来很玄学但实际上卡住不少人的不是审核而是申请表单里的字段。常见做法是用机构邮箱申请用途写「academic research on 3D human pose estimation」一般一到三个工作日就会收到下载链接。这份资源里有官方的下载目录结构说明我建议你把收到的邮件附件保存好因为后续脚本里要用的metadata.xml和一些 bvh 文件名都依赖这个目录结构。我一般会先把S1和S5两个受试者的Videos和D3_Angles目录单独拎出来因为训练集默认用 S1、S5、S6、S7、S8 做训练S9、S11 做测试这是 Human3.6M 协议里规定的标准划分。mkdir -p /data/h36m/S1/D3_Angles /data/h36m/S1/Videos # 假设你的压缩包已经按受试者拆好 find /data/h36m -maxdepth 2 -type d | head -20这段命令的作用是先把目录骨架搭好避免解压时把所有数据混在一起。find用来核对目录结构是否完整正常情况应该看到每个受试者名下有D3_Angles动捕数据和Videos视频帧两个核心目录。2.2 依赖环境numpy、scipy 和 bvh 解析库这个资源的设计思路是尽量少依赖核心只需要numpy、scipy和一个轻量 BVH 解析器。不要在这里装pytorch3d也不用上open3d原始 BVH 解析用纯 Python 就能完成。pip install numpy scipy pip install bvhtoolbox # 或者直接用源码里的 bvh_parser.py二选一我用的是源码自带解析器因为它对 Human3.6M 的通道顺序做了适配。官方 BVH 的CHANNELS是Zrotation Xrotation Yrotation的顺序与其他数据集常见顺序不同通用解析器容易把旋转顺序搞错。这点后面在坐标映射节会详细说先记住结论建议优先用资源内自带解析脚本。2.3 目录结构决定后续脚本要不要改拿到资源后第一件事看它的配置文件或常量定义里有没有硬编码路径。这份资源在config.py里定义了DATA_ROOT和SUBJECTS如果直接改成绝对路径/data/h36m后面所有脚本都不用动。否则你会发现每个脚本开头都有一行路径要改改到第五个脚本时人就麻了。# config.py DATA_ROOT /data/h36m SUBJECTS_TRAIN [S1, S5, S6, S7, S8] SUBJECTS_TEST [S9, S11] DT 1 # 下采样间隔默认每 1 帧取一次这里的DT是个容易忽略的参数。原始数据是 50fps很多模型只要 10fps 甚至 2fps如果训练时直接拿 50fps 的数据做时序输入显存会爆。我一般会把它设置成 5即每 5 帧取 1 帧相当于 10fps。3. 核心解析从 BVH 文件到三维关键点坐标3.1 BVH 文件结构拆解BVH 格式是有层级结构的运动数据文件包含两段HIERARCHY骨骼层级定义和MOTION帧数据。Human3.6M 的D3_Angles目录下每个动作存成一个.bvh文件文件名类似S1_Directions_1.cdf.bvh。骨骼层级定义的是每个关节点相对于父节点的偏移量OFFSET和旋转通道名CHANNELS。HIERARCHY ROOT Hips { OFFSET 0.000000 0.000000 0.000000 CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation JOINT Spine { OFFSET 0.000000 0.000000 100.000000 CHANNELS 3 Zrotation Xrotation Yrotation ... } }关键信息在CHANNELS这一行。根节点Hips有 6 个通道包含位移和旋转其余关节点是 3 个旋转通道。注意旋转顺序是Zrotation Xrotation Yrotation也就是先绕 Z 轴再绕 X 轴最后绕 Y 轴。这个顺序如果不正确处理算出来的坐标会错得离谱但不报错属于典型的数据层面翻车。3.2 写一个自带解析器需要注意什么如果不想用第三方库直接手写 BVH 解析也不复杂。核心思路是先扫描HIERARCHY段建一棵关节树每个节点记录offset、channels和children再解析MOTION段的Frames数量和时间步长最后把每帧的通道数值按树结构分配到对应关节。import re import numpy as np def parse_bvh(filepath): with open(filepath, r) as f: lines f.readlines() # 第一遍找 MOTION 起始行 motion_start None for i, line in enumerate(lines): if line.strip().startswith(MOTION): motion_start i break # 第二遍解析骨骼层级 joint_stack [] joints [] parent_chain [] for line in lines[:motion_start]: if ROOT in line or JOINT in line or End Site in line: name line.strip().split()[-1] joint {name: name, offset: None, channels: [], children: []} if joint_stack: joint_stack[-1][children].append(joint) joints.append(joint) joint_stack.append(joint) elif OFFSET in line: parts line.strip().split() joint_stack[-1][offset] np.array([float(x) for x in parts[1:4]]) joint_stack[-1][channels] parts[4:] elif line.strip() }: joint_stack.pop()逻辑说明这里用栈来维护骨骼层级遇到ROOT或JOINT就把当前节点入栈遇到}出栈。OFFSET行解析出局部偏移CHANNELS记录旋转顺序。注意End Site没有CHANNELS所以判断逻辑里要单独处理否则 channels 列表会在后续对齐时错位。参数说明motion_start是整个 BVH 解析的分界线所有在它之前的行属于骨骼定义之后的行属于运动数据。joint_stack的深度对应骨骼树的深度Human3.6M 的骨骼树深度是固定的从 Hips 到脚踝共五层。3.3 帧数据的读取与对齐MOTION 段的数据量很大一个五分钟的动作在 50fps 下有 15000 帧每行有几十个数字全部读进内存后要用reshape按帧和关节数切分。常见做法是先用np.loadtxt或逐行split把数据读成(Frames, TotalChannels)的矩阵再按骨骼树顺序切分。motion_lines [l.strip() for l in lines[motion_start3:] if l.strip()] frames np.array([[float(v) for v in l.split()] for l in motion_lines]) # 先按帧数 reshape第 0 行是帧数据第 1 行是时间步长 # 用骨骼树的总通道数做第二维 total_channels sum(len(j[channels]) for j in joints) n_frames len(frames) frames frames.reshape(n_frames, -1, total_channels)这里有个性能问题Python 列表推导式逐行 split 在 15000 帧时已经能感受到卡顿。如果数据量翻倍建议直接用numpy.fromstring配合delimiter 性能至少快三倍。我一般不会为了省这个事因为后续关键点计算量更大这里不是瓶颈。参数说明total_channels必须和frames.shape[1]一致否则 reshape 直接报错。Human3.6M 的 BVH 里所有帧的通道数是固定的不需要动态判断。4. 把 BVH 转成 3D 关键点坐标运动学链与坐标映射4.1 正运动学公式推导BVH 文件里存的不是笛卡尔坐标而是每个关节相对父关节的旋转角度和局部偏移。要把它们变成三维坐标需要做正运动学计算从根节点出发先做全局位移然后逐关节累乘旋转矩阵。推导思路不复杂每个关节的世界坐标等于父关节的世界坐标加上父关节旋转后的局部偏移。即world_pos[joint] world_pos[parent] R[parent] offset[joint]这里R[parent]是父关节在当前帧的全局旋转矩阵。关键是旋转矩阵的构造。Human3.6M 的 BVH 旋转顺序是ZXY所以单个关节的旋转矩阵是Rz Rx Ry按这个顺序乘不能反过来。def euler_to_matrix(z, x, y): cz, sz np.cos(z), np.sin(z) cx, sx np.cos(x), np.sin(x) cy, sy np.cos(y), np.sin(y) Rz np.array([[cz, -sz, 0], [sz, cz, 0], [0, 0, 1]]) Rx np.array([[1, 0, 0], [0, cx, -sx], [0, sx, cx]]) Ry np.array([[cy, 0, sy], [0, 1, 0], [-sy, 0, cy]]) return Rz Rx Ry逻辑说明三个基础旋转矩阵按ZXY顺序做矩阵乘法得到单个关节的局部旋转然后还需要乘上父关节的全局旋转才是当前关节的全局旋转。这个「乘父关节全局旋转」的步骤很容易漏漏掉的结果是动作严重变形但不会崩溃报错是常见坑位。参数说明角度单位是弧度不是度。BVH 文件里的数值通常是度数必须先用np.deg2rad转换。我见过有人忘了这步算出来的坐标数值范围完全不对回来看代码才发现是单位问题。4.2 完整映射脚本与帧循环整个坐标系转换的核心脚本不长关键在循环里维护每个关节的世界坐标和旋转矩阵。按帧循环每帧从根节点开始做深度优先遍历。def bvh_to_keypoints(frames, joints, joint_order): n_frames frames.shape[0] n_joints len(joint_order) keypoints np.zeros((n_frames, n_joints, 3)) for f in range(n_frames): local_rot {} world_pos {} world_rot {} # 第一步分配每帧的通道值到对应关节 idx 0 for j in joints: n_ch len(j[channels]) vals frames[f, idx:idxn_ch] idx n_ch if Xrotation in j[channels]: # 按 ZXY 顺序提取注意 channels 里写的是 Z X Y z vals[j[channels].index(Zrotation)] x vals[j[channels].index(Xrotation)] y vals[j[channels].index(Yrotation)] local_rot[j[name]] euler_to_matrix( np.deg2rad(z), np.deg2rad(x), np.deg2rad(y)) else: # 根节点含位移 local_rot[j[name]] np.eye(3) # 第二步自顶向下计算世界坐标 root_name joints[0][name] root_offset joints[0][offset] root_pos frames[f, :3] # 前三个值是 X Y Z 位移 world_pos[root_name] root_pos world_rot[root_name] local_rot[root_name] for j in joints[1:]: parent find_parent(joints, j) parent_name parent[name] off j[offset] r_local local_rot[j[name]] # 全局旋转 父全局旋转 局部旋转 r_world world_rot[parent_name] r_local # 世界坐标 父世界坐标 父全局旋转 局部偏移 p_world world_pos[parent_name] world_rot[parent_name] off world_pos[j[name]] p_world world_rot[j[name]] r_world # 第三步把 15 个标准关键点坐标写入输出 for i, name in enumerate(joint_order): keypoints[f, i] world_pos[name] return keypoints逻辑说明joint_order是目标关键点列表。Human3.6M 的原始骨骼有更多节点但大多数模型只用到 15 个或 17 个关键点所以这里做了一次关键点筛选。find_parent需要在解析时记录父子关系这里省略实现核心思想是遍历 joints 里每个节点的 children 列表。参数说明循环里的idx用来切分通道值它是累积的因为每个关节的通道数量不同。永远不要用固定数值切分。world_rot[parent_name] off这步是把父关节的旋转应用到子关节的偏移上是正运动学里最重要的运算。4.3 坐标系方向与毫米单位的确认Human3.6M 的 BVH 数据坐标系是 Z 轴向上X 轴向右Y 轴向前类似 Maya 的默认坐标系。如果你后续要转到 Y 轴向上的通用坐标需要在输出时做一次轴交换。另外单位是毫米不是米。这个问题在可视化时会被忽略但一旦你把关键点坐标直接喂给网络做损失计算就会发现数值尺度完全不对。def h36m_to_opengl_coords(keypoints): # h36m: x右 y前 z上 - opengl: x右 y上 z后 out np.zeros_like(keypoints) out[..., 0] keypoints[..., 0] out[..., 1] keypoints[..., 2] out[..., 2] -keypoints[..., 1] return out这段轴交换在很多开源项目里会出现但不同项目交换方式不太一样。判断自己要不要交换的标准很简单可视化出来看看人的朝向是否合理、Z 轴是否指向天空。如果发现人是躺着的多半是坐标轴没对齐。5. 常见问题排查从路径翻车到骨骼错位5.1 现象解压后文件名出现乱码或缺失有些受试者目录下有大量bvh文件但文件名包含.cdf.bvh这样的多后缀。直接用glob.glob(*.bvh)能匹配到但如果文件名里有特殊符号某些场景下会解析失败。另外 Windows 系统下解压工具可能自动重命名重复文件导致后续找不到对应文件。原因Human3.6M 的原始包是按 tar 归档的个别文件路径过长或包含特殊字符。Windows 自带解压对长路径支持不好会静默截断。解决用 7-Zip 或直接命令行解压不要用系统自带工具。解压后先跑一遍文件数量校验受试者下 bvh 文件数量应该完全一致不一致就说明解压有损。5.2 现象旋转矩阵算出来的坐标是一团乱麻如果你按上文公式推导后发现动作序列里人的关节在乱飞、骨骼长度在变化说明全局旋转矩阵没乘对。典型错误是把局部旋转直接当全局旋转用或者旋转顺序写成了XYZ而不是ZXY。原因BVH 文件的CHANNELS行顺序决定了解析方式。Human3.6M 是Zrotation Xrotation Yrotation你如果用euler_to_matrix(y, x, z)就会得到错误结果。由于欧拉角的非交换性顺序错一位就是完全不同的姿态。解决进入排错时先在单个帧上做验证。取第一帧数据手动算 Hips 到 Spine 的向量长度应该是100毫米偏移量定义值。如果算出来不是这个数说明旋转链路有问题。这个验证方法非常可靠可以精准定位哪个关节开始出错。5.3 现象帧率过高导致训练显存爆炸原始 50fps 的数据直接进时序模型显存会成倍增加。很多人跑RuntimeError: CUDA out of memory第一反应是减小 batch size但其实数据降采样才是根因。原因Human3.6M 是动作捕捉设备采样每秒 50 帧很多动作相邻帧之间差异极小对模型训练没有增量信息。解决在数据准备阶段做降采样用frames[::5]把帧率降到 10fps或者按动作边界切割后再采样。资源里的config.py中的DT参数就是干这个的。5.4 现象可视化时骨骼连接线错乱画 3D 骨架时发现连线把左手连到了右肩一般是joint_order定义与数据集标准不一致。Human3.6M 的关节索引有两种约定一种从 0 到 14 的前 15 个关节另一种是带骨盆旋转的 17 关节版本。原因模型训练时使用的关键点顺序与你加载数据的顺序不一致数据维度恰好相同不会报错所以只有在可视化时才暴露出来。解决写一个统一的骨骼连接定义表在生成关键点之后立即做一次拓扑校验。例如用膝盖坐标减去髋关节坐标得到一个方向向量如果所有帧里这个向量的y分量都为正在 Z 轴朝上的坐标系中说明左右腿没有接反。5.5 现象内存溢出或程序卡死BVH 全部读入内存后一个受试者的全部动作约有几十万个关键点帧每个关键点 3 个 float内存占用很容易超过普通笔记本的容量。原因没有按需读取一个np.loadtxt把所有帧全读进来了。解决改成流式读取只保留你需要的那段动作。批量处理时用np.memmap映射大文件避免内存一次性拉满。这个技巧在后续的验证章节还会用到。6. 进阶技巧用一段可视化脚本确认数据没白下数据解析完成、坐标系也对齐了最后的确认手段是可视化。这一步的关键是不要用重型工具直接matplotlib的 3D 散点图就能看出大部分问题。我一般会在一个动作文件上随机抽几帧画成骨架动画观察动作是否连续、关节是否在合理位置。如果这一步通过数据准备阶段才算真正完成。import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D def visualize_frame(keypoints, bones, frame_idx0, axNone): kps keypoints[frame_idx] if ax is None: fig plt.figure(figsize(6, 6)) ax fig.add_subplot(111, projection3d) ax.scatter(kps[:, 0], kps[:, 1], kps[:, 2], cblue, s50) for (i, j) in bones: ax.plot([kps[i, 0], kps[j, 0]], [kps[i, 1], kps[j, 1]], [kps[i, 2], kps[j, 2]], r-, linewidth2) # 限定显示范围避免离群点拉坏视角 ax.set_xlim(-1500, 1500) ax.set_ylim(-1500, 1500) ax.set_zlim(0, 2500) ax.set_xlabel(X) ax.set_ylabel(Y) ax.set_zlabel(Z) plt.show() # bones 定义按 Human3.6M 拓扑示例如下 bones [(0, 1), (1, 2), (2, 3), (0, 4), (4, 5), (5, 6), (0, 7), (7, 8), (8, 9), (9, 10), (8, 11), (11, 12), (12, 13), (8, 14), (14, 15), (15, 16)] visualize_frame(keypoints_3d, bones, frame_idx20)逻辑说明keypoints_3d是前面bvh_to_keypoints的输出bones定义了哪两个关节点连线。可视化能同时验证三件事关键点是否在合理空间范围、骨骼连接是否正确、动作在相邻帧之间是否连续。如果只有个别帧错乱多半是数据本身缺失不是解析问题。参数说明frame_idx建议从动作中间取一帧第一帧往往还没有进入正式动作中间帧更能反映真实数据质量。set_zlim(0, 2500)是根据 Human3.6M 的身高范围设定的人的骨盆一般在 1000mm 左右头顶在 2000mm 上下这个范围足够显示完整骨架。可视化确认通过之后建议把所有关键点保存为npz格式输出关键点的均值和标准差用做后续训练时数据归一化的基准。这会省掉你不少功夫。我从那以后每次处理新数据集都强制走一遍「单帧验证 → 多帧动画 → 数值分布统计」这个流程能提前拦掉九成以上数据问题。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →