GVHMR输出的.pt文件最全面分析
GVHMR / HMR4D 输出 .pt 文件规范化全量解读
适用对象:GVHMR Demo 推理生成的结果文件(例如:
hmr4d_results.pt)
目标:把字段结构、维度、语义、来源代码位置、关键公式、坐标系/骨架约定、后处理逻辑一次性讲清楚,便于后续稳定转换到 BVH / VMD / 其他动作格式。
本文信息来源:
- 你给的样例文件:
/mnt/data/hmr4d_results.pt(本例序列长度L=1392)- 你给的最新代码库:
/mnt/data/GVHMR-main/(以下均给出精确文件路径与函数名)
以下所有内容来自 GPT5.2 thinking
0. 重要结论速览(给后续“直接上手”用)
.pt顶层最关键的是两套 SMPL 参数:
smpl_params_global(重力对齐的世界/轨迹坐标)smpl_params_incam(相机坐标)
它们字段完全一致:global_orient / body_pose / betas / transl。
body_pose是 21 个身体关节(不含 pelvis 根),每个关节 axis-angle 3 维,所以63=21*3。
根关节旋转在global_orient (L,3)。net_outputs/model_output/pred_x的 151 维含义是确定的,且在代码里写死(不是猜的):
pred_x是 归一化后的运动向量 x_norm,由 5 个块拼接:
- 0:126
body_pose_r6d(21×6) - 126:136
betas(10) - 136:142
global_orient_r6d(6,incam) - 142:148
global_orient_gv_r6d(6,gv) - 148:151
local_transl_vel(3,SMPL局部速度)
static_conf_logits的 6 维顺序也是确定的(代码写死):
[left_ankle, left_foot, right_ankle, right_foot, left_wrist, right_wrist]
它用于修正全局平移(抑制脚滑/手滑)并进行 IK 约束。- Demo 保存
.pt的入口在:
hmr4d/model/gvhmr/gvhmr_pl_demo.py::DemoPL.predict()
它把outputs["pred_smpl_params_*"][0]拿出来形成顶层smpl_params_*,并把完整outputs存进net_outputs。
1. .pt 文件如何生成(字段结构的根源)
文件: hmr4d/model/gvhmr/gvhmr_pl_demo.py
DemoPL.predict() 会返回并保存一个 dict:
smpl_params_global={k: v[0] for k, v in outputs["pred_smpl_params_global"].items()}smpl_params_incam={k: v[0] for k, v in outputs["pred_smpl_params_incam"].items()}K_fullimg=data["K_fullimg"]net_outputs=outputs(把网络/后处理的所有中间结果都保存)
因此:
顶层smpl_params_*与net_outputs/pred_smpl_params_*[0]在数值上应完全一致(本例已验证差值为 0)。
2. 顶层结构与维度(通用 + 本例)
2.1 顶层 keys(通用)
pt (dict)
├── K_fullimg
├── smpl_params_incam
├── smpl_params_global
└── net_outputs
2.2 本例 hmr4d_results.pt 的实际维度(L=1392)
K_fullimg:(1392, 3, 3)(本例每帧完全相同)smpl_params_incam(dict)body_pose:(1392, 63)betas:(1392, 10)(本例为常数)global_orient:(1392, 3)transl:(1392, 3)
smpl_params_global(dict)同上net_outputs(dict)model_output(dict)pred_context:(1, 1392, 512)pred_x:(1, 1392, 151)pred_cam:(1, 1392, 3)static_conf_logits:(1, 1392, 6)
decode_dict(dict)body_pose:(1, 1392, 63)betas:(1, 1392, 10)global_orient:(1, 1392, 3)global_orient_gv:(1, 1392, 3)local_transl_vel:(1, 1392, 3)
pred_smpl_params_incam(dict, 形状均为(1,1392,*))pred_smpl_params_global(dict, 形状均为(1,1392,*))static_conf_logits:(1, 1392, 6)(与model_output冗余存一份)
注意:
net_outputs内大多带 batch 维(B=1),而顶层smpl_params_*已 squeeze 掉 batch。
3. K_fullimg:相机内参(确定含义 + 代码来源 + 本例数值)
文件: hmr4d/utils/geo/hmr_cam.py
3.1 含义
K_fullimg 为 pinhole 相机内参矩阵(每帧一份):
用于 3D→2D 投影、bbox 信息归一化、以及由 pred_cam 反推 transl。
3.2 本例 K_fullimg[0]
本例每帧相同(已验证差值最大为 0):
[[977.90796, 0. , 426. ],
[ 0. , 977.90796, 240. ],
[ 0. , 0. , 1. ]]
4. smpl_params_incam / smpl_params_global:最终可用的 SMPL 参数(导出动作最核心)
这两套 dict 字段相同:
betas (L,10):体型参数(shape)global_orient (L,3):pelvis 根旋转(axis-angle)body_pose (L,63):21 个身体关节旋转(axis-angle)transl (L,3):pelvis 根平移
4.1 body_pose 的 21 关节含义与顺序(确定)
body_pose` 对应 **SMPLH_JOINT_NAMES[1:22]**(不含 pelvis 根,含到 right_wrist 为止)。
**文件:** `hmr4d/utils/body_model/utils.py
- SMPLH 22 关节(含根)为:
| idx | joint_name |
|---|---|
| 0 | pelvis |
| 1 | left_hip |
| 2 | right_hip |
| 3 | spine1 |
| 4 | left_knee |
| 5 | right_knee |
| 6 | spine2 |
| 7 | left_ankle |
| 8 | right_ankle |
| 9 | spine3 |
| 10 | left_foot |
| 11 | right_foot |
| 12 | neck |
| 13 | left_collar |
| 14 | right_collar |
| 15 | head |
| 16 | left_shoulder |
| 17 | right_shoulder |
| 18 | left_elbow |
| 19 | right_elbow |
| 20 | left_wrist |
| 21 | right_wrist |
因此:
global_orient= idx 0(pelvis)的 axis-anglebody_pose= idx 1~21(21 个关节)的 axis-angle 依序拼接(21×3=63)
4.2 父子层级 parents(长度 22,确定)
文件: hmr4d/model/gvhmr/utils/postprocess.py(process_ik() 内写死)
父节点数组:
parents = [-1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19]
链条结构(便于 BVH/VMD 构骨架):
- 左腿:0-1-4-7-10
- 右腿:0-2-5-8-11
- 躯干:0-3-6-9-12-15
- 左臂:9-13-16-18-20
- 右臂:9-14-17-19-21
4.3 本例中 incam/global 的一致性(已验证)
对本例 hmr4d_results.pt:
smpl_params_incam.body_pose == smpl_params_global.body_pose(完全相等)smpl_params_incam.betas == smpl_params_global.betas(完全相等,且为常数)- 主要差异在:
global_orient(坐标系不同)transl(坐标系与生成方式不同)
5. net_outputs:网络输出与后处理全链路(理解 pt 的关键)
net_outputs 来自 Pipeline:
文件: hmr4d/model/gvhmr/pipeline/gvhmr_pipeline.py::Pipeline.forward()
整体流程:
- 网络预测:
model_output = denoiser3d(...) - 解码:
decode_dict = endecoder.decode(model_output["pred_x"]) - 组装 incam SMPL:
pred_smpl_params_incam - 推出 global 轨迹:
get_smpl_params_w_Rt_v2(...)得到 globalglobal_orient/transl
5)(可选)后处理:静止关节约束 + IK,使轨迹更稳
6. model_output(网络原始输出头)——字段含义全部确定
文件: hmr4d/network/gvhmr/relative_transformer.py
6.1 pred_context (B,L,512)
Transformer 最终特征序列(中间表征)。
一般不直接用于导出动作,但可用于调试/可视化/再训练。
6.2 pred_x (B,L,151):151 维运动向量(关键、确定)
文件: hmr4d/model/gvhmr/utils/endecoder.py::EnDecoder.encode()/decode()
pred_x 训练监督目标是 endecoder.encode(inputs) 的输出 x_norm,即:
[
x_{norm}=\frac{x-\mu}{\sigma}
]
其中 x 的拼接定义在代码注释中写死:
| slice | 维度 | 名称 | 含义 |
|---|---|---|---|
[0:126] |
126 | body_pose_r6d |
21 关节 Rotation6D(21×6) |
[126:136] |
10 | betas |
体型 |
[136:142] |
6 | global_orient_r6d |
根旋转 Rotation6D(incam) |
[142:148] |
6 | global_orient_gv_r6d |
根旋转 Rotation6D(GV) |
[148:151] |
3 | local_transl_vel |
根平移速度(SMPL 局部坐标) |
非常重要:
.pt里的pred_x是 归一化空间(x_norm),不是 axis-angle,也不是直接可用的平移。- 需要通过
endecoder.decode(pred_x)才能得到decode_dict的 axis-angle 与速度。
6.3 pred_cam (B,L,3):弱透视/尺度相机头(确定)
文件: hmr4d/utils/geo/hmr_cam.py::compute_transl_full_cam()
pred_cam = (s, tx, ty),用于反推出相机坐标 transl:
设:
f = K_fullimg[...,0,0]icx = K_fullimg[...,0,2],icy = K_fullimg[...,1,2]- bbox 表示
bbx_xys = (bbx_center_x, bbx_center_y, bbx_size) sb = s * bbx_size
则:
最终:
这解释了为何
smpl_params_incam.transl来自pred_cam + bbox + K_fullimg,而不是来自pred_x。
6.4 static_conf_logits (B,L,6):静止关节 logits(关键、确定)
静止 logits 的 6 个通道并非泛指“hands/toes/heels”,而是代码里明确选定的 6 个关节:
文件: hmr4d/model/gvhmr/utils/postprocess.py(多处一致)
joint_ids = [7, 10, 8, 11, 20, 21]
对应关节名(用第 4.1 节 joint 表可直接映射):
- left_ankle (7)
- left_foot (10)
- right_ankle (8)
- right_foot (11)
- left_wrist (20)
- right_wrist (21)
7. decode_dict:把 pred_x decode 后得到的可解释变量(确定)
文件: hmr4d/model/gvhmr/utils/endecoder.py::EnDecoder.decode()
decode_dict 的字段:
body_pose (B,L,63):21 关节 axis-anglebetas (B,L,10)global_orient (B,L,3):根 axis-angle(incam)global_orient_gv (B,L,3):根 axis-angle(GV)local_transl_vel (B,L,3):根平移速度(SMPL局部坐标)
命名纠正:
你最初那份分析里出现global_orient_qv(看起来像“quaternion vector part”猜测)。
最新代码/实际.pt字段是global_orient_gv(GV 坐标系根朝向,axis-angle),并且来源明确。
8. pred_smpl_params_incam/global:最终 SMPL 参数如何组装(确定)
文件: hmr4d/model/gvhmr/pipeline/gvhmr_pipeline.py::Pipeline.forward()
8.1 incam 参数(相机坐标)
pred_smpl_params_incam = {
body_pose = decode_dict["body_pose"]
betas = decode_dict["betas"]
global_orient = decode_dict["global_orient"]
transl = compute_transl_full_cam(pred_cam, bbx_xys, K_fullimg)
}
8.2 global 参数(轨迹/重力对齐坐标)
先调用:
函数: get_smpl_params_w_Rt_v2(global_orient_gv, local_transl_vel, global_orient_c, cam_angvel)
输出:
global_orient(global/ay)transl(global/ay)
然后组装:
pred_smpl_params_global = {
body_pose = decode_dict["body_pose"]
betas = decode_dict["betas"]
global_orient, transl # 来自 get_smpl_params_w_Rt_v2
}
9. global 轨迹是怎么从 GV/速度/相机角速度推出来的(确定,含关键数学含义)
文件: hmr4d/model/gvhmr/pipeline/gvhmr_pipeline.py::get_smpl_params_w_Rt_v2()
相关坐标工具:hmr4d/utils/geo/hmr_global.py
9.1 关键输入解释
global_orient_gv:每帧根朝向(GV 坐标系,axis-angle)local_transl_vel:每帧根速度(SMPL 局部坐标)global_orient_c:每帧根朝向(相机坐标,axis-angle)cam_angvel:每帧相机相对旋转(Rotation6D),注释:
cam_angvel定义为相机旋转随时间的相对变化(用于估计 yaw 漂移并 roll-out 到第 0 帧参考)
9.2 输出逻辑(概念版)
- 将
cam_angvel转回旋转矩阵R_t_to_tp1 - 根据
R_gv与R_c构造R_c2gv = R_gv @ R_c^T - 用视线方向在 GV 中的变化估计逐帧“绕重力轴”的相对旋转,并累乘得到
R_t_to_0 - 得到 global 根旋转:
global_orient = aa( R_t_to_0 @ R_gv ) - 用
local_transl_vel+global_orientroll-out 得到 global 平移轨迹:rollout_local_transl_vel():
先把局部速度转回全局速度:v_global = R * v_local
再做累加cumsum得到transl
- 最后做坐标轴变换到
ay(重力对齐):get_tgtcoord_rootparam(..., tsf="any->ay")- 在
hmr_global.py里any->ay被固定为 axis-angle[0,0,π](绕 z 旋转 180°)
10. 后处理(postproc=True)到底做了什么(确定)
Pipeline 在 Demo 中调用:
outputs = pipeline.forward(..., postproc=True, static_cam=static_cam)
后处理在:
文件: hmr4d/model/gvhmr/utils/postprocess.py
包含三块核心:
10.1 pp_static_joint():用静止末端约束修正 global transl(默认)
- 对 6 个关节(脚踝/脚/手腕)做 FK,计算逐帧位移
- 用
static_conf_logits(去掉最后一帧)做 softmax 权重,估计“应该抵消的位移” - 通过累积方式修正
transl,并对 x/z 做平滑 - 最后把轨迹整体放到地面:
transl[...,1] -= min_y
10.2 pp_static_joint_cam():在“静态相机”假设下更强的修正
当 static_cam=True 时使用:
- 从第 0 帧估计一个固定
T_c2w,把 incam 关节变换到 world - 先让 global transl 更接近该静态相机推出来的结果(只修正很差的帧)
- 再用静止约束(sigmoid > 0.8 阈值)抵消漂移
- y 不修改(避免高度乱飘),最后同样落地
10.3 process_ik():用 CCD IK 把姿态拉回“静止目标”
- 先用静止概率(sigmoid)把目标末端位置做时序融合
- 对四条链分别做 CCD IK:
- 左腿链
[0,1,4,7,10] - 右腿链
[0,2,5,8,11] - 左手链
[9,13,16,18,20] - 右手链
[9,14,17,19,21]
- 左腿链
- 输出新的
body_pose覆盖decode_dict["body_pose"] - 同时覆盖
pred_smpl_params_global/body_pose与pred_smpl_params_incam/body_pose
因此: Demo 输出的最终
body_pose往往已经是 “IK 后更稳”的版本。
11. betas 为什么常数(确定)
文件: hmr4d/network/gvhmr/relative_transformer.py
当 avgbeta=True(配置里默认开启):
- 网络输出
sample = final_layer(x)后 - 对 betas slice
[126:136]在有效帧上求平均 - 再把每帧的 betas 替换为同一个平均值(repeat 到每帧)
所以 .pt 中 betas 常数是预期行为,不是保存错误。
12. EnDecoder 的 FK 计算方式(确定,方便你做骨架验证)
文件: hmr4d/model/gvhmr/utils/endecoder.py::EnDecoder.fk_v2()
要点:
- 把
aa = concat([global_orient, body_pose]).reshape(...,22,3) - 转 rotmat:
axis_angle_to_matrix(aa) - skeleton(关节 rest 位置)由
smplx_model.get_skeleton(betas)[...,:22,:]得到 - 通过父子层级做 forward kinematics,输出
(B,L,22,3)joints
这意味着:只要你有 global_orient/body_pose/betas/transl,你就能在本仓库里用 FK 得到 22 关节轨迹,用来校验 BVH/VMD 导出是否正确。
13. “最小导出子集”建议(给 BVH/VMD 转换用,不做转换,只列依赖)
如果你要导出骨骼动画(旋转+根平移),通常只需要:
13.1 选择参考系
- 更适合“落地/轨迹”的:
smpl_params_global - 更贴“相机观察”的:
smpl_params_incam
13.2 必要字段
global_orient (L,3):根旋转(axis-angle)body_pose (L,63):21 关节旋转(axis-angle)transl (L,3):根平移betas (10):体型(常数即可)
13.3 必要约定(必须固定写进转换器/导出器)
-
22 关节顺序与名字(第 4.1 节)
-
父子层级 parents(第 4.2 节)
-
目标格式的坐标系轴向(例如 BVH/MMD 的 up/forward/right 定义)
这一项与
.pt无关,但一旦确定就必须写死,否则会出现“朝向/镜像/左右手颠倒”等典型问题。
14. 常见误解与纠正(把坑一次填平)
pred_x (151)不是“51×3 关节坐标”
它是 归一化后的拼接向量 x_norm,定义在EnDecoder.encode(),切片范围完全确定(第 6.2 节)。global_orient_gv不是四元数虚部
它是 GV 坐标系下根旋转的 axis-angle(由decode()得到)。static_conf_logits(6)的通道顺序不是模糊的
代码明确固定为:脚踝/脚/手腕(左右)6 个关节,顺序确定(第 6.4 节)。- incam/global 的
body_pose一样是正常的
差异主要在根global_orient/transl(参考系不同、transl 生成方式不同)。
15. 一段可复用的自检脚本(建议复制进你的工具链)
import torch
pt = torch.load("hmr4d_results.pt", map_location="cpu")
# 基本结构
print("top keys:", pt.keys())
L = pt["K_fullimg"].shape[0]
print("L =", L)
# K 是否恒定(可能恒定,也可能逐帧不同;以实际为准)
K = pt["K_fullimg"]
print("K max diff to first:", (K - K[0]).abs().max().item())
print("K[0]:\n", K[0])
# incam/global 的一致性
bp_in = pt["smpl_params_incam"]["body_pose"]
bp_gl = pt["smpl_params_global"]["body_pose"]
print("body_pose incam==global maxdiff:", (bp_in - bp_gl).abs().max().item())
bet = pt["smpl_params_global"]["betas"]
print("betas std max:", bet.std(0).max().item()) # avgbeta=True 时通常为 0
# 顶层与 net_outputs 对齐检查
out = pt["net_outputs"]
for space in ["incam", "global"]:
top = pt[f"smpl_params_{space}"]
net = out[f"pred_smpl_params_{space}"]
for k in ["body_pose","betas","global_orient","transl"]:
d = (top[k] - net[k][0]).abs().max().item()
print(f"{space}:{k} top==net[0] maxdiff:", d)
# static_conf_logits 顺序(代码固定)
static_order = ["left_ankle","left_foot","right_ankle","right_foot","left_wrist","right_wrist"]
print("static_conf_logits order:", static_order)
16. 与你最初那份“312 帧示例分析”的兼容说明
你最初那份 md 里出现过:
global_orient_qv(推测四元数虚部)pred_x(猜测 51×3 坐标)
在最新代码库与实际 .pt 中,这两点已经被完全确定并纠正为:
global_orient_gv(GV 根旋转,axis-angle)pred_x(151 维 x_norm,严格切片定义见第 6.2 节)
同时你那份 md 中对:
K_fullimgsmpl_params_incam/global的字段解释local_transl_vel与时序建模的理解
这些方向是对的,只是缺少“严格定义与通道顺序”。本文已补齐并写死到可执行级别。
✅ 你现在拥有的“全确定知识库”包含
.pt字段树与维度(含本例实测)- 每个字段的严格语义与生成来源(精确到文件/函数)
pred_x(151)逐段切片定义(写死)static_conf_logits(6)通道顺序与用途(写死)- global 轨迹推导与后处理逻辑(写死)
- 22 关节顺序 + parents(写死,可直接做 BVH/VMD 骨架)
- incam transl 的几何公式(写死)
如果你下一步开始“转 BVH 或转 VMD”,我们就只需要再确定:
导出目标的坐标轴约定(up/forward/right)与单位(通常按米/厘米),然后即可把 smpl_params_global 或 smpl_params_incam 稳定落地到目标格式。

浙公网安备 33010602011771号