GVHMR输出的.pt文件最全面分析
GVHMR / HMR4D 输出 .pt 文件规范化全量解读(第二版)
所有内容由 GPT 5.6 sol 最大强度思考并反复验证,可用于研究GVHMR时节省提示词或者想对.pt文件学习的朋友
适用对象:GVHMR Demo 推理生成的结果文件(例如:
hmr4d_results.pt)
目标:把字段结构、维度、语义、来源代码位置、关键公式、坐标系/骨架约定、后处理逻辑一次性讲清楚,便于后续稳定转换到 BVH / VMD / 其他动作格式。
本文信息来源:
- 当前项目样例:
outputs/demo/a1/hmr4d_results.pt(本例序列长度L=1392)- 当前 GVHMR 项目代码(以下均使用项目相对路径给出文件与函数名)
outputs/demo下现有的 18 个hmr4d_results.pt(用于核对通用字段结构)
以下内容已按当前项目代码和实际输出样例重新核查;文末列出了相较第一版的升级与改进。
0. 重要结论速览(给后续“直接上手”用)
-
.pt顶层最关键的是两套 SMPL 参数:smpl_params_global(重力对齐的轨迹坐标,不是外部标定的绝对世界坐标)smpl_params_incam(相机坐标)
它们字段完全一致:
global_orient / body_pose / betas / transl。 -
body_pose是 21 个身体关节(不含 pelvis 根)的局部旋转,每个关节使用 3 维 axis-angle 旋转向量,所以63=21*3;根旋转在global_orient (L,3)。axis-angle 的单位是弧度,它不是 XYZ Euler,不能直接写入 BVH 旋转通道。 -
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 局部坐标中的逐帧位移,米/帧)
- 0:126
-
static_conf_logits的 6 维顺序也是确定的(代码写死):
[left_ankle, left_foot, right_ankle, right_foot, left_wrist, right_wrist]
它用于修正全局平移(抑制脚滑/手滑)并进行 IK 约束。 -
transl是施加到整个 SMPL-X 模型上的平移参数,不严格等于 pelvis 关节坐标。精确的 pelvis FK 位置还包含由betas决定的静态 pelvis 偏移。 -
.pt本身没有保存 FPS。当前tools/demo/demo.py会把模型处理的视频副本写为 30 FPS,所以这条 Demo 路径生成的结果使用Frame Time=1/30;其他来源的.pt不能只根据张量推断帧率。 -
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)。
保存范围注意: 主
hmr4d_results.pt没有保存预处理输入bbx_xys和cam_angvel。最终两套smpl_params_*已经可直接使用,但仅凭主.pt内的pred_x/pred_cam不能从头完整重建 incam/global 根轨迹:incam 重建还需要preprocess/bbx.pt中的 bbox,global 重建还需要相机相对旋转。
1.1 序列化格式与“非自描述”边界
hmr4d_results.pt 是 torch.save() 写出的 Python dict,内部主要是嵌套字典和 Tensor。它不是 TorchScript、不是模型 checkpoint,也不是带正式 schema/version 的通用交换格式。
当前 tools/demo/demo.py 在保存前调用 detach_to_cpu(pred)。实测 outputs/demo 中现有 18 个结果文件的 Tensor 均为:
dtype = torch.float32
device = cpu
这是当前写出流程和现有样本的事实,不应被通用读取器当成永远不变的格式承诺;其他版本可能使用不同 dtype,读取后应按需显式转换。
更重要的是,主 .pt 没有保存以下生成元数据:
- schema/version 或代码 commit;
- checkpoint 路径和 EnDecoder 统计量版本;
static_cam、use_dpvo、f_mm等运行选项;- 原视频路径、FPS、时间戳和视频尺寸;
- bbox、相机相对旋转及完整预处理配置;
- SMPL-X 模型资产版本或目标导出坐标约定。
当前 hmr4d/configs/demo.yaml 的默认值为:
checkpoint = inputs/checkpoints/gvhmr/gvhmr_siga24_release.ckpt
endecoder = gvhmr/v1_amass_local_bedlam_cam
static_cam = False
use_dpvo = False
f_mm = null
命令行和代码可以覆盖这些选项,而且保存文件不会记录覆盖后的值。因此,要把 .pt 作为长期稳定的交换格式,建议转换流程另外保存一个 JSON/YAML sidecar,至少记录代码版本、checkpoint、decoder 配置、FPS、视频尺寸、运行选项、模型资产和目标坐标约定。
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。
当前 18 个样本的帧数范围为 382 <= L <= 7072,顶层 key 集合和本节一致,所有逐帧 Tensor 的时间维长度均为 L。文件不包含时间戳或有效帧 mask:第 t 行表示处理后视频的第 t 帧,一般是一输入帧对应一输出帧。
主 .pt 也没有单独的 length 字段;应从 smpl_params_global.body_pose.shape[0](并与其他逐帧字段交叉核对)取得 L,不要依赖文件名或视频时长反推。
当前结果是单个被跟踪人物的一条序列,没有 person 维、person id 或多人物列表。net_outputs 中的 B=1 是推理 batch 维,不是“一个人物维”。若处理多人,当前 Demo 通常需要选择不同 track/person 分别运行并分别保存,而不是在同一 hmr4d_results.pt 中得到 (P,L,...)。
当前时间差分实现要求 L>=2。代码探针表明 compute_cam_angvel() 在 L=0/1 时只返回空序列,get_local_transl_vel() 在 L=0/1 时会索引失败;现有最短样本为 382 帧,不能据此宣称单帧或空序列受支持。
对转换器而言,顶层 smpl_params_global/incam 是应优先依赖的最终结果。net_outputs 属于当前模型实现的调试/中间输出,未来版本可能增删字段或改变维度;不要把它当作比顶层最终参数更稳定的公共接口。
主文件也不直接保存 22 关节世界坐标、mesh vertices、faces、静态骨长或 BVH offsets。这些都应通过 body_pose/betas/global_orient/transl 和相同人体模型做 FK/蒙皮重新计算。
2.3 Tensor 连续性与安全变形
形状正确不代表内存一定连续。18/18 个主结果中的 smpl_params_global.transl (L,3) 都是非 contiguous Tensor,实测 stride 为 (1,L);对应 net_outputs.pred_smpl_params_global.transl 同样非连续。其他最终主参数在当前样本中为 contiguous,但通用读取器不应依赖这一偶然实现细节。
因此转换代码应优先使用:
transl = pt["smpl_params_global"]["transl"].contiguous()
body_pose = pt["smpl_params_global"]["body_pose"].reshape(L, 21, 3)
不要直接对未知 stride 的 Tensor 使用只接受连续布局的 .view(...),也不要假设 .numpy() 得到的一定是 C-contiguous 数组;交给只接受连续内存的第三方库前,应显式调用 .contiguous() 或 np.ascontiguousarray(...)。
3. K_fullimg:相机内参(确定含义 + 代码来源 + 本例数值)
文件: hmr4d/utils/geo/hmr_cam.py
3.1 含义
K_fullimg 为 pinhole 相机内参矩阵(每帧一份):
用于 3D→2D 投影、bbox 信息归一化、以及由 pred_cam 反推 transl。
fx/fy/cx/cy 的单位都是处理后图像的像素;它们对应 Demo 输出目录中的 0_input_video.mp4,不是归一化到 [0,1] 的数值。
主文件没有保存径向/切向畸变系数,当前投影与相机平移恢复按理想 pinhole 模型处理。
当前 Demo 通过图像尺寸估计一份 K 并沿帧复制;提供 f_mm 时则由相机传感器模型生成。因此它通常是估计内参,不一定是真实标定值。使用最终 smpl_params_global 导出 BVH 时不需要 K_fullimg。
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):施加到整个 SMPL-X 模型的平移参数,单位为米
axis-angle 的 3 个数构成旋转向量 r:||r|| 是弧度制旋转角,r/||r|| 是旋转轴。它不是按 X/Y/Z 顺序执行的 Euler 角。转换 BVH 时必须先转为旋转矩阵或四元数,再按目标 CHANNELS 顺序分解为 Euler。
虽然字段名沿用 smpl_params,当前 GVHMR Demo 实际使用的是 neutral SMPL-X 人体模型约定,体型为 10 个 betas;这里仅保存前 22 个身体关节的根旋转与 21 个局部姿态,不包含 SMPL-X 的手指、表情、下颌和眼球参数。FK/骨长计算应使用本仓库 make_smplx("supermotion_v437coco17") 对应的相同模型资产和 joint regressor。
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)
对 joint index j=1..21,其切片为:
body_pose[..., 3*(j-1) : 3*j]
该输出只包含这 22 个带姿态参数的身体关节,不包含手指、脸部和眼球动画。若目标 BVH 骨架包含这些额外关节,需要保留静态姿态、使用其他数据源补充,或在重定向阶段处理,不能从当前 body_pose 中恢复不存在的旋转通道。
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
重要:
parents只定义骨架拓扑,不包含骨长或 BVHOFFSET。OFFSET必须由相同 SMPL-X 模型和betas计算静态关节位置后,再求父子关节差;详见第 12 节。
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(坐标系与生成方式不同)
body_pose 相等是因为它表示运动学树内的局部姿态,参考系差异主要由根旋转和平移承担;当前 Demo 的 IK 还会用同一份结果同时覆盖 incam/global 的 body_pose。
4.4 transl 与 pelvis FK 位置的区别
体型相关静态骨架记为:
J(beta) = J_template + J_shapedirs × beta
在本仓库 EnDecoder.fk_v2() 中,pelvis 的实际 FK 位置是:
pelvis_position[t] = J(beta)[0] + transl[t]
因此 BVH ROOT 可采用两种等价方案之一:
方案 A:ROOT OFFSET = J(beta)[0],ROOT Position = transl[t]
方案 B:ROOT OFFSET = 0, ROOT Position = J(beta)[0] + transl[t]
两种方案不能混用。坐标换基和米/厘米缩放也必须同时应用到 ROOT OFFSET 与 ROOT Position。
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 - 推理 Demo 开启后处理:静止关节约束 + IK,使轨迹更稳
Pipeline.forward()的postproc参数本身可选,但DemoPL.predict()固定以postproc=True调用。因此本文讨论的标准 Demo.pt已经经过后处理。
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 的拼接定义在代码注释中写死:
| 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 与逐帧位移增量。
当前 Demo 使用 endecoder=gvhmr/v1_amass_local_bedlam_cam,对应统计量 MM_V1_AMASS_LOCAL_BEDLAM_CAM(定义于 hmr4d/model/gvhmr/utils/stats_compose.py)。均值和标准差没有保存在 .pt 中,因此不能用任意 mean/std 或不同版本的 EnDecoder 解码 pred_x。如果目标只是导出最终动作,应直接读取顶层 smpl_params_global,无需重新解码 pred_x。
Rotation6D 与 axis-angle 的互转使用 pytorch3d.transforms.rotation_6d_to_matrix、matrix_to_rotation_6d、matrix_to_axis_angle 和 axis_angle_to_matrix。若不调用本仓库/PyTorch3D 的实现,就必须严格复现其 6D 排列和正交化约定,不能假设所有名为 Rotation6D 的实现都有相同分量布局。
名称中的 vel 容易产生误解:代码没有除以帧间隔,local_transl_vel[t] 实际是从第 t 帧到第 t+1 帧的位移增量,单位为米/帧,而不是米/秒。若要得到近似物理速度,应在确认 FPS 后再乘以 FPS。
delta_global[t] = transl[t+1] - transl[t]
delta_local[t] = transpose(R_root[t]) @ delta_global[t]
训练目标会重复最后一个有效位移,把 L-1 个帧间隔补齐为长度 L;轨迹 rollout 只使用前 L-1 项。
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
其中 bbox center 和 size 的单位是处理后图像像素;pred_cam 是供该公式使用的网络相机参数,不应脱离 bbox/K 单独解释为以米为单位的相机平移。
则:
最终:
这解释了为何
smpl_params_incam.transl来自pred_cam + bbox + K_fullimg,而不是来自pred_x。
重建依赖:
bbx_xys不在主hmr4d_results.pt中,而在outputs/demo/<video_name>/preprocess/bbx.pt。所以主.pt中的pred_cam + K_fullimg仍不足以单独重算 incamtransl。
网络会把 pred_cam[...,0] 的尺度 s 下限截到 0.25。公式分母虽然加了 1e-9 防止数值除零,但这不会让 bbox_size<=0 变得具有物理意义;转换/验证时仍应要求 bbox size、焦距和 incam Z 为合理值。
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)
六个通道是彼此独立的二分类 logits,通常应逐通道使用 sigmoid(logit) 得到静止概率,而不是把六通道整体解释成一个 softmax 概率分布。普通 pp_static_joint() 内部的 softmax 是后处理加权策略:先选出 logit > 0(即 sigmoid 概率大于 0.5)的候选静止关节,再只在候选之间归一化权重。
它只表示六个指定末端“是否静止”的置信度,不是 22 个关节的姿态精度、遮挡概率或通用质量分数。主 .pt 没有为最终 body_pose/global_orient/transl 提供逐关节误差条、协方差或统一质量评分。
静止标签和位移约束本质上对应相邻帧区间。第 L-1 帧没有下一帧,训练目标通过 repeat_last=True 重复最后一个有效标签来对齐长度 L;网络仍会输出长度为 L 的 logits。实际平移后处理使用 static_conf_logits[:, :-1],所以最后一个预测 logit 不参与这一步的帧间位移修正。
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):根平移的逐帧局部位移(名称叫 vel,但不是米/秒)
后处理后的字段语义: 上述列表是
EnDecoder.decode()的直接输出定义,但当前 Demo 随后运行process_ik(),并用 IK 结果覆盖decode_dict["body_pose"]、pred_smpl_params_global["body_pose"]和pred_smpl_params_incam["body_pose"]。因此保存后的decode_dict.body_pose已是 IK 后姿态,不再是pred_x的原始直接解码值;model_output.pred_x本身没有被改写。若需要 IK 前姿态,必须用相同配置和归一化统计量的EnDecoder重新解码pred_x。
命名纠正:
你最初那份分析里出现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 参数(相机坐标)
投影代码采用 pinhole 相机约定,incam 结果可按 X 向右、Y 向下、Z 向前理解。它适合复现人体在原视频相机中的位置,但通常不能不经换基就写入采用 Y-up 的 BVH 场景。
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 参数(轨迹/重力对齐坐标)
global 结果最终采用 Y-up 的重力对齐轨迹坐标,更适合作为 BVH 动作导出的起点。
先调用:
函数: 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
}
后处理会继续修改 global transl,并用同一份 IK 结果覆盖 incam/global body_pose;incam transl 不会被静止关节平移后处理覆盖。
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 帧参考)
cam_angvel 同样不是以弧度/秒表示的物理角速度,而是相邻帧相对旋转矩阵的 Rotation6D 表示:
R_rel[t] = R_w2c[t+1] @ transpose(R_w2c[t])
它原本只有 L-1 个帧间隔,compute_cam_angvel() 会重复最后一个相对旋转补齐到 L。主 .pt 不保存该输入。
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():
先把局部位移转回全局位移:delta_global = R * delta_local
默认把第 0 帧平移起点设为(0,0,0),再累加前L-1个位移得到transl
最后一个local_transl_vel只用于对齐长度,在 rollout 中不会产生第L帧
- 最后做坐标轴变换到
ay(重力对齐):get_tgtcoord_rootparam(..., tsf="any->ay")- 在
hmr_global.py里any->ay被固定为 axis-angle[0,0,π](绕 z 旋转 180°)
最终 global 结果使用 Y 作为竖直轴,并由后处理放到地面。但它的 X/Z 绝对朝向和原点来自首帧参考与估计过程,不是经过外部标定、带真实地理方向和绝对原点的世界坐标。
因此,global transl 在轨迹恢复开始时以零点为参考,经过 X/Z 平滑、静止约束和落地后,保存值的首帧不再保证三个分量都严格为 0。不要把它解释成真实场地中的绝对坐标。
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,计算逐帧位移
- 先选择
logit > 0的候选静止关节,再在候选之间对 logits 做 softmax 权重,估计“应该抵消的位移”;若该帧没有候选则不修正 - 通过累积方式修正
transl,并对 x/z 做平滑 - 最后按 22 个 FK 关节的最低 Y 把轨迹整体放到地面:
transl[...,1] -= min_y
这里使用的是关节中心最低点,不是脚底 mesh vertex、鞋底厚度或目标 BVH 的 End Site,因此“关节最低点为零”不等于可视模型的真实脚底平面恰好为零。
10.2 pp_static_joint_cam():在“静态相机”假设下更强的修正
当 static_cam=True 时使用:
- 从第 0 帧估计一个固定
T_c2w,把 incam 关节变换到 world - 先让 global transl 更接近该静态相机推出来的结果(只修正很差的帧)
- 再用静止约束(sigmoid > 0.8 阈值)抵消漂移
- 静止末端抵消步骤不修改 y;此前用于拉近 incam/global 的纠偏步骤仍可能修正 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 后更稳”的版本。
落地平移是在 IK 之前根据 pre-IK 的 22 个 FK 关节计算的,随后 IK 可以再次抬高或压低末端,所以最终保存姿态的最低关节不保证严格等于 y=0。18 个现有样本中,pre-IK 最低 Y 的数值误差不超过约 4.1e-7 m;IK 后最低 Y 落在约 -0.00169 m 到 +0.01911 m。若目标格式要求脚底严格贴地,需要在导出/重定向完成后再次按目标骨架检测和修正,而不能简单再把 transl.y 减去一个未经 FK 验证的常数。
IK 也不一定只是很小的姿态微调。现有样本中,相对 raw pred_x 解码姿态的逐关节旋转变化,按每个样本统计的 99 分位约为 3.92°–20.61°,观察到的单个关节/帧最大变化约为 87.03°。需要原始网络姿态时必须重新解码 pred_x,不能把最终 body_pose 当作 pre-IK 值。
11. betas 为什么常数(确定)
文件: hmr4d/network/gvhmr/relative_transformer.py
当 avgbeta=True(配置里默认开启):
- 网络输出
sample = final_layer(x)后 - 对 betas slice
[126:136]在有效帧上求平均 - 再把每帧的 betas 替换为同一个平均值(repeat 到每帧)
所以 .pt 中 betas 常数是预期行为,不是保存错误。
对 BVH 来说,betas 不是逐帧动画通道,而是用于生成体型相关的固定骨长。如果某个非标准结果中的 betas 确实随帧变化,标准 BVH 无法表达逐帧变化的 OFFSET,转换器必须明确选用首帧或时间平均体型构建一套固定骨架。
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 导出是否正确。
12.1 从 betas 生成 BVH 静态 OFFSET
.pt 不直接保存 BVH 骨长。应使用与 GVHMR 相同的 SMPL-X 模型计算静态关节:
import torch
from hmr4d.utils.smplx_utils import make_smplx
params = pt["smpl_params_global"]
beta = params["betas"].mean(dim=0, keepdim=True)
model = make_smplx("supermotion_v437coco17")
joints_rest = model.get_skeleton(beta)[0, :22] # (22, 3), meter
parents = model.parents[:22].cpu().long()
offsets = joints_rest.clone()
offsets[1:] = joints_rest[1:] - joints_rest[parents[1:]]
对非根节点:
OFFSET[j] = J(beta)[j] - J(beta)[parent[j]]
只使用第 4.2 节的 parents 或按名称手写统一骨长,无法严格复现 GVHMR 的 FK。head、left_foot、right_foot、双腕等末端的 BVH End Site 也不在 .pt 中,可使用目标骨架的末端偏移、SMPL-X 额外静态关节,或仅用于显示的合理短偏移。
13. “最小导出子集”建议(给 BVH/VMD 转换用,不做转换,只列依赖)
如果你要导出骨骼动画(旋转+根平移),从 .pt 动作张量本身通常只需要下列字段;静态模型资产、帧率和导出约定仍需按后续小节提供:
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):模型整体平移;按第 4.4 节处理 ROOT 位置betas (L,10):用于生成体型相关静态OFFSET,当前 Demo 中通常为时间常数
13.3 必要约定(必须固定写进转换器/导出器)
-
22 关节顺序与名字(第 4.1 节)
-
父子层级 parents(第 4.2 节)
-
使用相同 SMPL-X 模型和
betas得到的静态OFFSET(第 12.1 节) -
目标格式的坐标系轴向与手性(例如 up/forward/right 定义)
-
位置单位(SMPL-X/GVHMR 为米,部分 BVH 消费端使用厘米)
-
BVH
CHANNELS的 Euler 顺序,以及与之严格一致的矩阵到 Euler 分解顺序 -
FPS /
Frame Time、末端End Site规则和旋转连续性处理这些导出约定有一部分不在
.pt中,但一旦确定就必须固定,否则会出现朝向错误、镜像、左右颠倒、速度不对或旋转跳变等问题。
13.4 坐标换基不能只交换 axis-angle 分量
设矩阵 C 把源坐标向量映射到目标坐标:
v_target = C @ v_source
正确转换为:
offset_target = scale * C @ offset_source
transl_target = scale * C @ transl_source
R_target = C @ R_source @ inverse(C)
其中 R_source 应先由 axis-angle 转成旋转矩阵。根旋转和 21 个局部关节旋转都要做共轭换基。只交换或取反 axis-angle 的三个分量通常是错误的。
13.5 FPS、Euler 顺序与连续性
主 .pt 没有 FPS 字段。当前 tools/demo/demo.py 用 fps=30 写入模型处理的视频副本,因此标准 Demo 输出应使用:
Frames = L
Frame Time = 1 / 30 = 0.0333333333
当前复制逻辑会逐帧读取原视频,再无条件以 30 FPS 写出 0_input_video.mp4;它不会把原视频 FPS 保存进主 .pt,也不会按原时间戳重采样帧。因此当原视频不是 30 FPS 时,处理后视频和 .pt 的播放时长可能相对原视频改变:转换结果应与处理后的 0_input_video.mp4 对齐并使用 30 FPS,而不能擅自沿用原文件 FPS。若还要同步原音频,应先明确是否需要按时长重新采样动作或音频。
同理,local_transl_vel 是基于处理后相邻帧的米/帧位移;把它解释为米/秒时,应乘处理后 FPS,而不是未经确认的原视频 FPS。
来自其他脚本或外部来源的 .pt 必须从对应视频或额外元数据读取真实 FPS。
BVH 写出的 Euler 数值必须与 CHANNELS 声明顺序完全一致,例如不能用 XYZ 分解却按 ZXY 通道写入。逐帧转换还应处理四元数符号连续和 Euler unwrap,否则同一连续动作可能出现 180°/360° 数值跳变。
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 生成方式不同)。 - axis-angle 的三个分量不是 XYZ Euler
body_pose/global_orient必须先转旋转矩阵或四元数,再换基并按 BVHCHANNELS顺序分解。 transl不严格等于 pelvis 关节坐标
在本仓库 FK 中,pelvis 位置为J(beta)[0] + transl;BVH ROOT OFFSET 与 Position 必须采用一致方案。parents不能单独构成可复现的 BVH 骨架
它只定义拓扑;骨长和OFFSET必须由相同 SMPL-X 模型与betas生成。.pt中没有 FPS
当前 Demo 路径是 30 FPS,但通用转换器必须允许从视频或外部参数传入帧率。smpl_params_global不是外部标定的绝对世界坐标
它是重力对齐并以首帧附近为参考的轨迹坐标,绝对原点和 X/Z 朝向不带真实地理含义。- 主
.pt的内部输出不足以从头重建全部根参数
pred_cam还依赖未保存在主文件中的bbx_xys,global 轨迹恢复还依赖cam_angvel;导出最终动作应直接使用顶层smpl_params_global。
15. 一段可复用的自检脚本(建议复制进你的工具链)
以下示例仅用于加载本项目自己生成的可信 .pt;不要对不可信来源直接使用 torch.load。
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)
# 最终参数形状与有限值
for space in ["incam", "global"]:
params = pt[f"smpl_params_{space}"]
assert params["body_pose"].shape == (L, 63)
assert params["betas"].shape == (L, 10)
assert params["global_orient"].shape == (L, 3)
assert params["transl"].shape == (L, 3)
for key, value in params.items():
assert torch.isfinite(value).all(), f"non-finite: {space}.{key}"
global_transl = pt["smpl_params_global"]["transl"]
print("global transl contiguous:", global_transl.is_contiguous())
print("global transl stride:", global_transl.stride())
# 交给要求连续内存的库前:global_transl = global_transl.contiguous()
# 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 temporal maxdiff:", (bet - bet[:1]).abs().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)
# 当前标准 Demo 的帧时间;其他来源必须读取真实 FPS
print("current Demo Frame Time:", 1 / 30)
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(拓扑固定,但骨长仍需由
betas和模型生成) - incam transl 的几何公式(写死)
- BVH 所需的静态
OFFSET来源、ROOT 平移约定、坐标换基公式和帧率来源
如果下一步开始转 BVH 或 VMD,还必须明确目标坐标轴与手性、位置单位、Euler/通道顺序、FPS、静态骨架与 End Site 规则,并在写出后用 FK 对比验证。仅确定坐标轴和单位还不足以保证转换正确。
16.1 能否把本文直接作为其他模型的提示词
可以。本文现在足以让其他模型理解当前项目版本 hmr4d_results.pt 的结构、字段语义、生成链路和 BVH 转换约束。但它描述的是 schema 和生成规则,不包含某个具体文件的全部 Tensor 数值,也不能替代目标格式/目标软件的导出要求。
不同任务还应附带以下上下文:
| 交给其他模型的任务 | 除本文外仍需提供 |
|---|---|
解释 .pt 字段 |
具体文件路径或实际 key/shape 输出;说明它是否由当前 Demo 生成 |
| 生成当前文件的 BVH 转换器 | 目标坐标轴、手性、单位、Euler CHANNELS 顺序、ROOT 规则、End Site 规则、FPS、目标软件 |
| 精确复现 22 关节 FK | 当前仓库及相同 SMPL-X 模型资产/joint regressor |
从 pred_x 重新解码 |
v1_amass_local_bedlam_cam EnDecoder 及 MM_V1_AMASS_LOCAL_BEDLAM_CAM 统计量 |
| 从内部输出重建 incam/global 根轨迹 | 还需 bbox、cam_angvel、运行配置以及相应预处理结果 |
| 判断动作质量 | 原视频、渲染或 FK 可视化;.pt 没有完整的姿态误差/遮挡置信度 |
推荐把本文连同下面这段任务前提一起交给其他模型:
以下文档描述当前 GVHMR 项目生成的 hmr4d_results.pt。
请把顶层 smpl_params_global 视为最终动作结果,把 net_outputs 视为实现相关的中间信息。
不要把 axis-angle 直接当 Euler,不要把 transl 直接等同于 pelvis 坐标,
不要从 .pt 猜 FPS、模型版本或目标坐标约定。
如果实际文件的 key/shape 与文档不一致,请先报告差异,不要静默套用本文假设。
本次具体任务:<填写任务>
实际 .pt 路径:<填写路径>
目标软件/格式:<填写>
目标坐标轴与手性:<填写>
位置单位:<填写>
Euler CHANNELS 顺序:<填写>
FPS 或 Frame Time:<填写>
ROOT 与 End Site 规则:<填写>
16.2 信息完整性的最终边界
本文对当前代码能够确定的 schema 已较全面,但任何单独的 hmr4d_results.pt 都无法自证其生产版本和配置。以下内容无法从主文件可靠反推:
- 它是否确实由当前 commit、默认 checkpoint 和默认命令行选项生成;
- 原始视频的真实 FPS、时间戳、尺寸和被选择的人物 track;
- 未保存的 bbox、相机运动输入和预处理缓存版本;
- SMPL-X 模型文件是否被替换过;
- 目标 BVH 软件对坐标、Euler、单位和根节点的具体约定;
- 网络预测的真实误差。姿态、尺度、轨迹和接触仍可能受遮挡、运动模糊、跟踪错误、相机估计和模型误差影响。
因此,“把本文交给其他模型”可以完整传达当前格式知识,但若要求生成可直接投入生产的转换器,仍应同时提供实际样本、目标格式合同,并进行源 FK 与导出 FK 的数值回归测试。
16.3 outputs 全量 PT 案例盘点
本次不是只检查 a1。当前工作区 outputs 下共找到 72 个 .pt,恰好组成 18 套完整 Demo 样本:
| 文件类型 | 数量 | 实测结构 | 用途 |
|---|---|---|---|
hmr4d_results.pt |
18 | 本文第 2 节所列顶层结构 | 最终动作与网络中间输出 |
preprocess/bbx.pt |
18 | bbx_xyxy (L,4)、bbx_xys (L,3) |
跟踪框和 incam 平移恢复 |
preprocess/vitpose.pt |
18 | (L,17,3) |
COCO17 二维关键点/置信度 |
preprocess/vit_features.pt |
18 | (L,1024) |
图像特征条件 |
preprocess/slam_results.pt |
0 | 当前输出目录没有保留 | 无法从现有 outputs 复算 cam_angvel |
18 个主结果合计 73,643 帧,单个序列长度从 382 到 7072 帧;覆盖横屏、竖屏、文件名含空格/连字符等情况,以及 14 种 (width,height) 分辨率。所有目录都有 0_input_video.mp4、bbox、VitPose 和视觉特征,且它们的帧数/时间维与对应主 .pt 完全相等。
16.4 18 个主结果的深度验证证据
下表中的“全部通过”只表示当前工作区 18 个样本通过,不等于证明所有未来输入都不会失败。
| 验证项 | 全量结果 |
|---|---|
| 顶层 key、嵌套 key 和形状 | 18/18 与第 2 节完全一致 |
| dtype/device/有限值 | 18/18 全部为 CPU float32,所有主参数和配套 Tensor 均无 NaN/Inf |
| Tensor 内存布局 | 18/18 的 global transl 非 contiguous、stride 为 (1,L);其余顶层主参数在当前样本中 contiguous |
顶层与 net_outputs/pred_smpl_params_*[0] |
所有字段逐元素完全相等,最大差为 0 |
| 顶层与内部冗余 Tensor 的 storage | 18/18 均未共享底层 storage,不能依赖修改联动 |
incam/global 的 body_pose 和 betas |
18/18 逐元素完全相等;所有 betas 沿时间最大差为 0 |
| 静止 logits 两份冗余值 | 18/18 逐元素完全相等 |
| 视频 | 18/18 均为 30 FPS,声明帧数与 L 完全一致 |
| bbox/VitPose/视觉特征 | 18/18 形状正确、值有限、bbox size 全为正 |
| K | 18/18 每帧恒定、fx=fy=sqrt(W²+H²)、主点为 (W/2,H/2)、底行为 [0,0,1] |
pred_cam + bbox + K -> incam transl |
18/18 重算最大绝对误差为 0 |
当前 EnDecoder 重解码 pred_x |
除被 IK 覆盖的 body_pose 外,其余 decode 字段最大绝对误差为 4.77e-7–9.54e-7 |
| pelvis ROOT 公式 | FK_pelvis == J(beta)[0] + transl,18/18 最大绝对误差为 0 |
| pre-IK 落地 | 全局最低关节 Y 在约 [-2.46e-7, 4.03e-7] m,即 float32 数值零点 |
| 最终 IK 后最低关节 | 约 [-0.00169, 0.01911] m,证明最终结果不保证严格贴 y=0 |
| IK 对 raw pose 的影响 | 各样本逐关节变化的 99 分位为 3.92°–20.61°;观察到最大值 87.03° |
| incam 深度 | 所有样本为正;按每个文件的最小 Z 统计,范围约 1.24–4.19 m |
pred_cam 尺度 |
所有样本大于代码下限 0.25;各文件最小值范围约 0.718–0.901 |
| global 相邻帧根平移 | 各文件 99 分位约 0.0271–0.0572 m/frame,观察到最大值约 0.1702 m/frame |
最后三项是观测分布,不是合法性硬阈值。不同动作、尺度错误或跟踪失败都可能超出这些范围;验证器只报告它们,不应仅凭超出当前范围就判定文件损坏。
由于 18 个目录都没有 slam_results.pt,并且主 .pt 不保存 cam_angvel,本次无法从原始相机运动输入完整重放 get_smpl_params_w_Rt_v2()。已经验证的是其保存后的根旋转/平移结构、incam 几何、decoder 一致性、ROOT 公式和后处理落地结果;“从相机输入开始逐算 global 轨迹”仍属于缺少原始输入的不可验证环节。
16.5 真实 PT 案例补充出的实现注意事项
只考虑 .pt 后,18 套案例还补充确认了以下容易在转换代码中遗漏的事实:
- 主结果没有
length/fps/schema_version等元数据;L必须由逐帧 Tensor 形状取得,FPS 必须来自处理后视频或 sidecar。 smpl_params_global.transl在 18/18 个主.pt中都是非 contiguous,stride 为(1,L)。数值完全正确,但直接.view()或交给要求 C-contiguous 内存的库可能失败,必须显式连续化。- 顶层最终参数与
net_outputs中的冗余参数数值相等,但加载后并不共享底层 storage。修改其中一份不会自动更新另一份;转换器应选定顶层最终参数作为唯一数据源,不要依赖别名同步。 - 18 套
bbx.pt、vitpose.pt和vit_features.pt与主结果逐帧对齐;但它们是重建/验证依赖,不是 BVH 最小动作字段。缺失时仍能导出已经保存好的 global 最终动作,只是无法完整重算 incam 几何和预处理链路。 - 18 套输出都没有
slam_results.pt,主结果也没有cam_angvel。因此当前案例能验证保存后的 global 参数和后处理结果,却不能从相机运动输入开始完整重放 global 轨迹推导。 - 当前所有 K 都是默认估计、逐帧恒定;这是现有案例分布,不是 schema 强制。读取器仍必须保留
(L,3,3)并允许未来每帧 K 不同。 - 当前所有 betas 都沿时间恒定、incam Z 都为正、pred_cam scale 都高于 0.25,但这些只是观察结果。验证器应报告未来偏离,而不能把当前观测范围写成通用硬编码。
16.6 边界覆盖矩阵
“所有边界都成功覆盖”无法由有限的 18 个样本严格证明。更可靠的说法是:已覆盖项有实测证据,未覆盖项被显式列出并由验证器拒绝或警告。
| 边界 | 当前状态 | 正确处理方式 |
|---|---|---|
382 <= L <= 7072 |
已覆盖 | 18 个样本全部通过 |
L=0 或 L=1 |
代码探针确认不支持 | 在进入 Pipeline 前拒绝;当前时间差分至少需要 2 帧 |
2 <= L < 382、L > 7072 |
无现有样本 | 不能保证性能/显存;用验证器和实际推理测试 |
| 横屏/竖屏、14 种分辨率、路径含空格/连字符 | 已覆盖 | 全部通过 |
| 30 FPS | 已覆盖 | 当前 18 个处理视频全部通过 |
| 非 30 FPS | 未覆盖 | 不要改张量;应携带真实 FPS 并正确写 Frame Time |
| 恒定默认估计 K | 已覆盖 | 18/18 与 estimate_K 一致 |
f_mm 自定义 K 或逐帧变化 K |
schema/公式支持,但无样本 | 按每帧读取,禁止强行取 K[0] 代替全序列 |
| bbox size 正值 | 已覆盖 | 验证器将 <=0 视为错误 |
pred_cam 尺度触及 0.25 下限 |
代码边界已知,样本未触及 | 保留 clamp;检查由此产生的深度异常 |
| incam Z 正值 | 当前样本已覆盖 | 非正值需警告并检查 bbox/K/跟踪,不把当前正值范围当硬阈值 |
| 时间常数 betas | 已覆盖 | 若未来随帧变化,BVH 必须选一套固定体型并报警 |
postproc=True 的当前 Demo |
已覆盖 | raw pose 与最终 IK pose 必须区分 |
postproc=False |
未覆盖 | pre-IK 不保证落地,验证器应警告而非误报 schema 损坏 |
static_cam=True / DPVO / SimpleVO 分支 |
.pt 无元数据,现有缓存不足以判别 |
保存 sidecar 和相机输入;静态相机后处理还明确要求 B=1 |
单人物、内部 B=1 |
已覆盖 | 多人物应拆分文件或定义新 schema,不要复用 B 维冒充 person 维 |
| 其他 checkpoint/decoder/stats/model asset | 未覆盖 | 保存版本元数据;deep 验证不一致时停止而不是静默继续 |
非 float32/CPU、额外 key、缺少 net_outputs |
无现有样本 | 顶层最终参数可定义兼容读取;内部字段检查按版本降级为警告/显式错误 |
NaN/Inf、截断或不可信 torch.save 文件 |
无有效样本,也不应制造生产案例 | 只加载可信 .pt、检查有限值;损坏文件立即失败 |
| 目标 BVH 的坐标/单位/Euler/End Site | 完全由目标合同决定 | 必须由调用方提供,并做源 FK 与导出 FK 回归 |
| 遮挡、跟踪错人、极端运动、尺度漂移 | 当前案例不能穷尽 | 结合原视频、渲染、接触和轨迹诊断,不能只验证 schema |
16.7 可重复验证工具
本仓库新增只读验证器:tools/validate_hmr4d_results.py。对当前全部输出执行最完整检查:
.\python.exe tools\validate_hmr4d_results.py outputs\demo --require-companions --deep
当前执行结果:
SUMMARY files=18 passed=18 warned=0 failed=0
基本模式检查 schema、形状、dtype、有限值、顶层/内部冗余关系、bbox/VitPose/特征、视频帧数/FPS、K 和 incam 平移重建。--deep 还会使用当前 EnDecoder 重解码 pred_x、比较 IK 改变量,并验证 22 关节 FK、pelvis ROOT 公式和 pre-/post-IK 最低高度。
如果验证非 30 FPS 的其他生产流程,可传 --expect-fps -1 关闭固定 FPS 断言,再由调用方依据 sidecar/视频决定 Frame Time。工具返回非零退出码表示至少一个文件存在错误,适合放入后续转换脚本或 CI 的前置检查。
17. 相较第一版的升级与改进
本次修改保留了第一版从“文件生成 → 顶层结构 → 字段语义 → 轨迹恢复 → 后处理 → 导出建议”的整体阅读顺序,主要进行了以下修复和升级:
- 更新核查来源:把失效的
/mnt/data/...临时路径改为当前项目相对路径,并补充对outputs/demo现有结果文件的结构核对。 - 明确旋转语义:补充
body_pose是相对父节点的局部 axis-angle、单位为弧度,并说明它不能直接当作 BVH Euler 通道。 - 修正
transl定义:不再把它简单称为 pelvis 根坐标,增加pelvis_position = J(beta)[0] + transl以及两种一致的 BVH ROOT 写法。 - 补齐静态骨架来源:说明
parents只有拓扑没有骨长,新增用相同 SMPL-X 模型和betas生成OFFSET的代码与公式。 - 修正后处理后的字段解释:明确 IK 会覆盖保存后的
decode_dict.body_pose和两套最终body_pose,而pred_x保持不变。 - 细化静止置信度逻辑:区分逐通道 sigmoid 概率、普通分支中候选关节内的 softmax,以及静态相机分支的 0.8 阈值。
- 澄清 global 坐标性质:将其定义为重力对齐、首帧参考的轨迹坐标,而不是外部标定的绝对世界坐标。
- 补充缺失依赖:指出主
.pt不保存bbx_xys、cam_angvel和 FPS,避免误以为可以只靠内部预测字段从头恢复全部结果。 - 补齐 BVH 导出约定:新增完整坐标换基公式、米/厘米统一缩放、Euler
CHANNELS顺序、30 FPS 来源、End Site、四元数符号连续和 Euler unwrap。 - 增强自检脚本:增加字段形状、有限值、时间 beta 一致性和 Frame Time 检查,并避免单帧情况下
std()产生无效结果。 - 修复文档表达与格式:修正了错误编号、损坏的 Markdown 代码块、数学公式定界符以及“只需坐标轴和单位即可”等过度结论。
- 补充序列化与版本边界:明确
.pt是无正式 schema/version 的torch.save嵌套字典,列出当前 float32/CPU 实测结果以及文件未保存的生产元数据。 - 纠正伪 velocity 语义:明确
local_transl_vel是米/帧的相邻帧位移、cam_angvel是相邻帧相对旋转,两者都不是按秒计量的物理速度,并说明末帧 padding/rollout 行为。 - 记录当前解码配置:补充默认 checkpoint、
v1_amass_local_bedlam_cam和MM_V1_AMASS_LOCAL_BEDLAM_CAM,避免其他模型用错误统计量解码pred_x。 - 明确单人物与时间对齐:说明
B=1是 batch 而非人物维,文件无 person id、时间戳和 valid mask,所有逐帧字段按处理后视频帧索引对齐。 - 增加跨模型提示词说明:新增本文可以直接作为提示词的适用范围、不同任务仍需附带的信息、推荐提示词前缀和无法从主
.pt反推的最终边界。 - 补充未保存的几何数据:明确主文件不含 joints、vertices、faces、骨长和 offsets,必须通过相同人体模型重建;同时标明 K/bbox 的像素单位。
- 固定 Rotation6D 实现约定:记录当前代码使用的 PyTorch3D 旋转转换函数,避免其他模型用分量布局不同的 6D 实现错误解码。
- 扩大到 outputs 全量审计:盘点 72 个
.pt、18 套主结果/预处理、73,643 帧、14 种分辨率,并把每一项实测证据写入第 16.3–16.4 节。 - 增加深度数值回归:对全部样本重解码
pred_x、重算 incam 平移和 22 关节 FK,量化 decoder 误差、ROOT 公式、落地误差及 IK 实际改变量。 - 补充 PT 内存与时间边界:记录 global
transl的非连续 stride、冗余 Tensor 加载后不共享 storage,并明确当前 30 FPS 重编码可能改变非 30 FPS 原视频的播放时长。 - 建立边界覆盖矩阵和验证器:明确区分已覆盖、代码确认不支持、schema 支持但无样本和目标格式相关边界,并新增
tools/validate_hmr4d_results.py供未来输出和 CI 重复检查。

浙公网安备 33010602011771号