8. 30 Hz 统一导出¶
8.1 为什么要统一导出¶
原始录制里没有两路信号的速率相同:tracker 65 或 90 Hz,手套 120 或 220 Hz,视频 30 或 60 Hz。投影得到的 SMPL-X 用的是 tracker 自己的时间戳,而且从 0 开始计时,其它信号用的却是主机的绝对时间。直接按下标把这些数组对在一起,结果是错的,而且不会报错。
export_unified_30hz.py 为每条录制输出一个 npz,所有逐帧数组的第 i 个元素对应同一时刻。时间栅格对齐到绝对时间的 1/30 秒整数倍,同一时刻在任何录制里都落在同一个位置。
8.2 运行¶
python scripts/export_unified_30hz.py --projected-root $WORK/out/projected \
--output-root $WORK/out/unified_30hz --rate 30 --skip-existing
- 有手部融合结果时读
smplx_with_manus_hands.npz,没有时读smplx_profile_driven.npz(手部为中性姿态)。 --session <录制名>只导出指定录制;--keep-raw-tactile额外保存 744 个触点的原始值(每只手每条约 17 MB)。- Wuji 触觉需要
tactile/下的处理模块(wuji_correct.py、wuji_layout.py)和手套健康表(health/health_db.npz)。路径可以用环境变量EGOEXPLORER_TACTILE_DIR改。缺少这些文件时只导出原始值,并在报告里注明。
投影失败的录制可以单独导出视频和触觉,供第 9 章的校验页说明失败原因。输出目录必须和上面分开,因为这个脚本会删掉输出目录里已投影录制的导出:
python scripts/export_unified_nobody.py --sync-root $WORK/input/ego_data_sync \
--batch-root $WORK/out --output-root $WORK/out/unified_30hz_nobody --skip-existing
8.3 重采样方式¶
| 信号 | 方法 | 原因 |
|---|---|---|
| 旋转 | Slerp | 在轴角或旋转矩阵上直接线性插值是错的 |
| 位置 | 线性插值 | |
| 有效标记 | 窗口内取"与" | |
| 触觉 | 窗口内取最大值;接触取"或" | 以 30 Hz 采样 120 Hz 的接触信号,会漏掉大部分短暂接触 |
| 视频 | 取最近一帧的帧号,并记录与栅格时刻的偏差 | 视频帧不能插值 |
每个 tracker 单独重采样,只用它自己的有效样本。超出某个 tracker 覆盖范围的栅格点记为 tracker_valid=False,不硬插值(长度为 0 的四元数会让 Slerp 直接报错)。每路信号还有一个 *_interpolated 标记:栅格点离最近的真实样本超过一个原生采样周期时为 True,用来区分测量值和跨越丢帧插出来的值。
往返误差(先重采样到 30 Hz,再插回原始时间戳)在 0.1° / 0.5 mm 以内,触觉峰值全部保留,都远小于拟合误差本身。
8.4 输出内容:unified_30hz/<录制名>.npz¶
时间
| 键 | 形状 | 含义 |
|---|---|---|
timestamps |
(T,) float64 | 主机绝对时间(秒) |
rate_hz |
() | 30.0 |
upsample_ratio |
() | 30 ÷ tracker 原生速率,例如 Wuji 约 0.46。大于 1 说明有一部分值是插值得到的 |
身体与手部(中性 SMPL-X,num_betas=10,use_pca=False)
| 键 | 形状 | 含义 |
|---|---|---|
betas、scale |
(10,)、() | 来自标定 |
global_orient、body_pose、transl |
(T,3)、(T,63)、(T,3) | 轴角(弧度);transl 的含义见第 11.4 节 |
left_hand_pose、right_hand_pose |
(T,45) | MANO 15 个关节的轴角 |
hand_pose_source |
字符串 | glove_fused 为手套测量;none_neutral_zero 为占位值 |
attachment_position、attachment_rotation |
(5,3)、(5,3,3) | 五个 tracker 相对关节的安装平移和旋转 |
body_interpolated |
(T,) bool | |
has_body |
() bool | 投影失败的导出为 False |
ground_y_m |
() | 地面在原始坐标中的 Y 值 |
coordinate_frame |
字符串 | raw_capture_y_up |
tracker
| 键 | 形状 | 含义 |
|---|---|---|
tracker_roles |
(5,) | camera, left_wrist, right_wrist, left_ankle, right_ankle,与原始文件的顺序不同 |
tracker_position、tracker_rotation |
(T,5,3)、(T,5,3,3) | 位置(米);旋转矩阵,各列是 tracker 自身坐标轴在世界中的方向 |
tracker_valid、tracker_interpolated、tracker_native_hz |
手套
| 键 | 形状 | 含义 |
|---|---|---|
glove_layout |
字符串 | 21 节点为 wuji21(MANUS 和 Wuji 都是这个值) |
glove_left_points、glove_right_points |
(T,21,3) | 手腕局部坐标系下的关节点 |
glove_*_native_hz、glove_*_interpolated |
触觉(仅 Wuji),{S} 为 L 或 R:
| 键 | 形状 | 含义 |
|---|---|---|
tactile_{S}_L4_contact |
(T,17) | 17 个区域的接触(0/1),窗口内取"或" |
tactile_{S}_L2_score |
(T,17) | 去基线后除以稳健标准差得到的分数 |
tactile_{S}_L1_residual |
(T,17) | 去掉滚动 10% 分位基线后的残差 |
tactile_{S}_coarse_contact、_coarse_score |
(T,6) | 拇指、食指、中指、无名指、小指、手掌 |
tactile_{S}_coverage、_sigma |
各区域可用触点的比例、噪声尺度 | |
tactile_{S}_L0_raw |
(T,744) | 仅在 --keep-raw-tactile 或缺少健康表时导出 |
tactile_region_names、tactile_coarse_names、tactile_levels |
区域名称与各层定义 |
17 个区域:thumb_distal, thumb_proximal, index_{distal,middle,proximal}, middle_{…}, ring_{…}, pinky_{…}, palm_radial, palm_central, palm_ulnar。
触觉分层的定义和阈值写在 wuji_correct.py 开头:L0 原始值;L1 去掉滚动基线;L2 除以稳健标准差;L3 增益补偿(只用于消融实验,不导出);L4 按区域 80% 分位并带迟滞判断,得到接触。手套老化后,靠的是"看变化量而不是绝对值"(L1、L2)恢复数据,增益补偿帮助不大。手套健康系数按录制估计,不能按天平均,因为很多线路时好时坏。
视频
| 键 | 形状 | 含义 |
|---|---|---|
video_frame_index |
(T,) int32 | 最近一帧的帧号 |
video_time_offset_s |
(T,) | 该帧与栅格时刻的实际偏差,实测最大约 22 ms |
video_native_hz |
() |
同名的 .json 是摘要:帧数、各路原生速率、body_interpolated_frac、tracker_valid_frac、hand_pose_source、video_max_offset_ms 等。
8.5 注意事项¶
smplx_profile_driven.npz里的timestamps从 0 开始,要加上tracker_t_host[0]才是绝对时间。统一导出里已经换算好。- 触觉的左右按设备名取:
glove_0为 L,glove_1为 R,没有读修正副本里的slot。目前所有 Wuji 设备都是glove_0戴在左手,已有的手套左右修正也都发生在没有触觉的 MANUS 数据上,所以没有影响。如果以后 Wuji 出现左右标反,这里要改成读slot。