Loading

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. 重要结论速览(给后续“直接上手”用)

  1. .pt 顶层最关键的是两套 SMPL 参数:

    • smpl_params_global(重力对齐的轨迹坐标,不是外部标定的绝对世界坐标)
    • smpl_params_incam(相机坐标)

    它们字段完全一致:global_orient / body_pose / betas / transl

  2. body_pose21 个身体关节(不含 pelvis 根)的局部旋转,每个关节使用 3 维 axis-angle 旋转向量,所以 63=21*3;根旋转在 global_orient (L,3)。axis-angle 的单位是弧度,它不是 XYZ Euler,不能直接写入 BVH 旋转通道。

  3. net_outputs/model_output/pred_x151 维含义是确定的,且在代码里写死(不是猜的):
    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 局部坐标中的逐帧位移,米/帧)
  4. static_conf_logits 的 6 维顺序也是确定的(代码写死):
    [left_ankle, left_foot, right_ankle, right_foot, left_wrist, right_wrist]
    它用于修正全局平移(抑制脚滑/手滑)并进行 IK 约束。

  5. transl 是施加到整个 SMPL-X 模型上的平移参数,不严格等于 pelvis 关节坐标。精确的 pelvis FK 位置还包含由 betas 决定的静态 pelvis 偏移。

  6. .pt 本身没有保存 FPS。当前 tools/demo/demo.py 会把模型处理的视频副本写为 30 FPS,所以这条 Demo 路径生成的结果使用 Frame Time=1/30;其他来源的 .pt 不能只根据张量推断帧率。

  7. 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_xyscam_angvel。最终两套 smpl_params_* 已经可直接使用,但仅凭主 .pt 内的 pred_x/pred_cam 不能从头完整重建 incam/global 根轨迹:incam 重建还需要 preprocess/bbx.pt 中的 bbox,global 重建还需要相机相对旋转。

1.1 序列化格式与“非自描述”边界

hmr4d_results.pttorch.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_camuse_dpvof_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 相机内参矩阵(每帧一份):

\[K = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix} \]

用于 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-angle
  • body_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.pyprocess_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 只定义骨架拓扑,不包含骨长或 BVH OFFSETOFFSET 必须由相同 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()

整体流程:

  1. 网络预测:model_output = denoiser3d(...)
  2. 解码:decode_dict = endecoder.decode(model_output["pred_x"])
  3. 组装 incam SMPL:pred_smpl_params_incam
  4. 推出 global 轨迹:get_smpl_params_w_Rt_v2(...) 得到 global global_orient/transl
  5. 推理 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_{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 与逐帧位移增量。

当前 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_matrixmatrix_to_rotation_6dmatrix_to_axis_angleaxis_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 单独解释为以米为单位的相机平移。

则:

\[c_x = 2 \frac{bbx\_center\_x - icx}{sb}, \quad c_y = 2 \frac{bbx\_center\_y - icy}{sb}, \quad t_z = 2 \frac{f}{sb} \]

最终:

\[transl_cam = (tx + c_x, ty + c_y, tz) \]

这解释了为何 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 仍不足以单独重算 incam transl

网络会把 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 表可直接映射):

  1. left_ankle (7)
  2. left_foot (10)
  3. right_ankle (8)
  4. right_foot (11)
  5. left_wrist (20)
  6. 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-angle
  • betas (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 输出逻辑(概念版)

  1. cam_angvel 转回旋转矩阵 R_t_to_tp1
  2. 根据 R_gvR_c 构造 R_c2gv = R_gv @ R_c^T
  3. 用视线方向在 GV 中的变化估计逐帧“绕重力轴”的相对旋转,并累乘得到 R_t_to_0
  4. 得到 global 根旋转:global_orient = aa( R_t_to_0 @ R_gv )
  5. local_transl_vel + global_orient roll-out 得到 global 平移轨迹:
    • rollout_local_transl_vel()
      先把局部位移转回全局位移:delta_global = R * delta_local
      默认把第 0 帧平移起点设为 (0,0,0),再累加前 L-1 个位移得到 transl
      最后一个 local_transl_vel 只用于对齐长度,在 rollout 中不会产生第 L
  6. 最后做坐标轴变换到 ay(重力对齐):
    • get_tgtcoord_rootparam(..., tsf="any->ay")
    • hmr_global.pyany->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_posepred_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 到每帧)

所以 .ptbetas 常数是预期行为,不是保存错误。

对 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。headleft_footright_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.pyfps=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. 常见误解与纠正(把坑一次填平)

  1. pred_x (151) 不是“51×3 关节坐标”
    它是 归一化后的拼接向量 x_norm,定义在 EnDecoder.encode(),切片范围完全确定(第 6.2 节)。
  2. global_orient_gv 不是四元数虚部
    它是 GV 坐标系下根旋转的 axis-angle(由 decode() 得到)。
  3. static_conf_logits(6) 的通道顺序不是模糊的
    代码明确固定为:脚踝/脚/手腕(左右)6 个关节,顺序确定(第 6.4 节)。
  4. incam/global 的 body_pose 一样是正常的
    差异主要在根 global_orient/transl(参考系不同、transl 生成方式不同)。
  5. axis-angle 的三个分量不是 XYZ Euler
    body_pose/global_orient 必须先转旋转矩阵或四元数,再换基并按 BVH CHANNELS 顺序分解。
  6. transl 不严格等于 pelvis 关节坐标
    在本仓库 FK 中,pelvis 位置为 J(beta)[0] + transl;BVH ROOT OFFSET 与 Position 必须采用一致方案。
  7. parents 不能单独构成可复现的 BVH 骨架
    它只定义拓扑;骨长和 OFFSET 必须由相同 SMPL-X 模型与 betas 生成。
  8. .pt 中没有 FPS
    当前 Demo 路径是 30 FPS,但通用转换器必须允许从视频或外部参数传入帧率。
  9. smpl_params_global 不是外部标定的绝对世界坐标
    它是重力对齐并以首帧附近为参考的轨迹坐标,绝对原点和 X/Z 朝向不带真实地理含义。
  10. .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_fullimg
  • smpl_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_posebetas 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 套案例还补充确认了以下容易在转换代码中遗漏的事实:

  1. 主结果没有 length/fps/schema_version 等元数据;L 必须由逐帧 Tensor 形状取得,FPS 必须来自处理后视频或 sidecar。
  2. smpl_params_global.transl 在 18/18 个主 .pt 中都是非 contiguous,stride 为 (1,L)。数值完全正确,但直接 .view() 或交给要求 C-contiguous 内存的库可能失败,必须显式连续化。
  3. 顶层最终参数与 net_outputs 中的冗余参数数值相等,但加载后并不共享底层 storage。修改其中一份不会自动更新另一份;转换器应选定顶层最终参数作为唯一数据源,不要依赖别名同步。
  4. 18 套 bbx.ptvitpose.ptvit_features.pt 与主结果逐帧对齐;但它们是重建/验证依赖,不是 BVH 最小动作字段。缺失时仍能导出已经保存好的 global 最终动作,只是无法完整重算 incam 几何和预处理链路。
  5. 18 套输出都没有 slam_results.pt,主结果也没有 cam_angvel。因此当前案例能验证保存后的 global 参数和后处理结果,却不能从相机运动输入开始完整重放 global 轨迹推导。
  6. 当前所有 K 都是默认估计、逐帧恒定;这是现有案例分布,不是 schema 强制。读取器仍必须保留 (L,3,3) 并允许未来每帧 K 不同。
  7. 当前所有 betas 都沿时间恒定、incam Z 都为正、pred_cam scale 都高于 0.25,但这些只是观察结果。验证器应报告未来偏离,而不能把当前观测范围写成通用硬编码。

16.6 边界覆盖矩阵

“所有边界都成功覆盖”无法由有限的 18 个样本严格证明。更可靠的说法是:已覆盖项有实测证据,未覆盖项被显式列出并由验证器拒绝或警告。

边界 当前状态 正确处理方式
382 <= L <= 7072 已覆盖 18 个样本全部通过
L=0L=1 代码探针确认不支持 在进入 Pipeline 前拒绝;当前时间差分至少需要 2 帧
2 <= L < 382L > 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. 相较第一版的升级与改进

本次修改保留了第一版从“文件生成 → 顶层结构 → 字段语义 → 轨迹恢复 → 后处理 → 导出建议”的整体阅读顺序,主要进行了以下修复和升级:

  1. 更新核查来源:把失效的 /mnt/data/... 临时路径改为当前项目相对路径,并补充对 outputs/demo 现有结果文件的结构核对。
  2. 明确旋转语义:补充 body_pose 是相对父节点的局部 axis-angle、单位为弧度,并说明它不能直接当作 BVH Euler 通道。
  3. 修正 transl 定义:不再把它简单称为 pelvis 根坐标,增加 pelvis_position = J(beta)[0] + transl 以及两种一致的 BVH ROOT 写法。
  4. 补齐静态骨架来源:说明 parents 只有拓扑没有骨长,新增用相同 SMPL-X 模型和 betas 生成 OFFSET 的代码与公式。
  5. 修正后处理后的字段解释:明确 IK 会覆盖保存后的 decode_dict.body_pose 和两套最终 body_pose,而 pred_x 保持不变。
  6. 细化静止置信度逻辑:区分逐通道 sigmoid 概率、普通分支中候选关节内的 softmax,以及静态相机分支的 0.8 阈值。
  7. 澄清 global 坐标性质:将其定义为重力对齐、首帧参考的轨迹坐标,而不是外部标定的绝对世界坐标。
  8. 补充缺失依赖:指出主 .pt 不保存 bbx_xyscam_angvel 和 FPS,避免误以为可以只靠内部预测字段从头恢复全部结果。
  9. 补齐 BVH 导出约定:新增完整坐标换基公式、米/厘米统一缩放、Euler CHANNELS 顺序、30 FPS 来源、End Site、四元数符号连续和 Euler unwrap。
  10. 增强自检脚本:增加字段形状、有限值、时间 beta 一致性和 Frame Time 检查,并避免单帧情况下 std() 产生无效结果。
  11. 修复文档表达与格式:修正了错误编号、损坏的 Markdown 代码块、数学公式定界符以及“只需坐标轴和单位即可”等过度结论。
  12. 补充序列化与版本边界:明确 .pt 是无正式 schema/version 的 torch.save 嵌套字典,列出当前 float32/CPU 实测结果以及文件未保存的生产元数据。
  13. 纠正伪 velocity 语义:明确 local_transl_vel 是米/帧的相邻帧位移、cam_angvel 是相邻帧相对旋转,两者都不是按秒计量的物理速度,并说明末帧 padding/rollout 行为。
  14. 记录当前解码配置:补充默认 checkpoint、v1_amass_local_bedlam_camMM_V1_AMASS_LOCAL_BEDLAM_CAM,避免其他模型用错误统计量解码 pred_x
  15. 明确单人物与时间对齐:说明 B=1 是 batch 而非人物维,文件无 person id、时间戳和 valid mask,所有逐帧字段按处理后视频帧索引对齐。
  16. 增加跨模型提示词说明:新增本文可以直接作为提示词的适用范围、不同任务仍需附带的信息、推荐提示词前缀和无法从主 .pt 反推的最终边界。
  17. 补充未保存的几何数据:明确主文件不含 joints、vertices、faces、骨长和 offsets,必须通过相同人体模型重建;同时标明 K/bbox 的像素单位。
  18. 固定 Rotation6D 实现约定:记录当前代码使用的 PyTorch3D 旋转转换函数,避免其他模型用分量布局不同的 6D 实现错误解码。
  19. 扩大到 outputs 全量审计:盘点 72 个 .pt、18 套主结果/预处理、73,643 帧、14 种分辨率,并把每一项实测证据写入第 16.3–16.4 节。
  20. 增加深度数值回归:对全部样本重解码 pred_x、重算 incam 平移和 22 关节 FK,量化 decoder 误差、ROOT 公式、落地误差及 IK 实际改变量。
  21. 补充 PT 内存与时间边界:记录 global transl 的非连续 stride、冗余 Tensor 加载后不共享 storage,并明确当前 30 FPS 重编码可能改变非 30 FPS 原视频的播放时长。
  22. 建立边界覆盖矩阵和验证器:明确区分已覆盖、代码确认不支持、schema 支持但无样本和目标格式相关边界,并新增 tools/validate_hmr4d_results.py 供未来输出和 CI 重复检查。
posted @ 2026-01-23 18:33  271374667  阅读(154)  评论(0)    收藏  举报