在基于强化学习与模仿学习的机器人控制中,策略网络往往需要一段"参考动作"作为输入,以指导当前步态向预设轨迹靠拢。本页聚焦 MotionLoader 模块的设计原理、NPZ 动作文件的格式规范,以及动作参考在实时推理循环中的注入与帧推进机制。阅读本页前,建议先理解 观测布局配置与动态解析 中 obs_layout 的注册与解析逻辑,因为 motion_pos 与 motion_vel 本质上是两个特殊的观测源。
Sources: motion_loader.hpp, motion_loader.cpp
整体架构与数据流
MotionLoader 并非独立线程,而是作为 PolicyRuntime 的成员对象存在。其生命周期与策略上下文绑定:启动时从磁盘加载 .npz 文件到内存,运行时按推理节拍逐帧取出关节位置与速度,供观测组装器拼接为 ONNX 输入张量。
以下 Mermaid 图展示了动作参考在推理管线中的位置:
flowchart LR
subgraph 磁盘
NPZ["motions/xxx.npz<br/>(fps, joint_pos, joint_vel)"]
end
subgraph 初始化阶段
ML["MotionLoader"]
end
subgraph 运行时
OR["观测注册表<br/>motion_pos / motion_vel"]
OS["观测组装"]
INF["ONNX 推理"]
end
NPZ -->|cnpy 解析| ML
ML -->|get_pos / get_vel| OR
OR -->|update_obs_segments| OS
OS -->|input_tensor| INF
在多策略系统中(如 beyondmimic 模式),基础行走策略无需动作参考,而舞蹈、挥手、出拳等策略则各自由独立的 MotionLoader 实例驱动。PolicyRuntime 通过 std::shared_ptr<MotionLoader> 管理该可选依赖,未配置动作文件时指针保持为空。
Sources: inference_node.hpp, inference_node.cpp
NPZ 动作文件格式
系统使用 NumPy NPZ 格式存储预录制的参考动作,依赖第三方库 cnpy 在 C++ 端完成零拷贝解析。一个合法的 motion 文件必须包含三个数组:
| 数组名 | 数据类型 | 形状 | 说明 |
|---|---|---|---|
fps |
int32 |
标量 | 动作录制帧率 |
joint_pos |
float32 |
[num_frames, num_joints] |
每帧关节位置(弧度) |
joint_vel |
float32 |
[num_frames, num_joints] |
每帧关节速度(弧度/秒) |
加载时,MotionLoader 构造函数会执行三项校验:首先确认三个关键字均存在;随后将二维数组扁平化索引转存为 std::vector<std::vector<float>>,外层索引为帧序号,内层为关节序号;最后在标准输出打印 FPS、总帧数与关节数,便于启动时排错。若 joint_pos 与 joint_vel 的帧数或关节数不一致,或文件缺失关键字,构造将抛出 std::runtime_error,阻止节点继续启动。
Sources: motion_loader.cpp
MotionLoader 类设计
该类遵循最小可用原则,仅暴露只读访问接口,无运行时修改能力。
classDiagram
class MotionLoader {
-int fps_
-size_t num_frames_
-size_t num_joints_
-vector~vector~float~~ joint_pos_
-vector~vector~float~~ joint_vel_
+MotionLoader(motion_file: string)
+get_fps() int
+get_num_frames() size_t
+get_num_joints() size_t
+get_pos(frame: size_t) vector~float~&
+get_vel(frame: size_t) vector~float~&
}
核心 API 说明:
| 方法 | 返回值 | 语义 |
|---|---|---|
get_fps() |
int |
录制帧率,当前仅用于日志打印 |
get_num_frames() |
size_t |
总帧数,决定动作播放长度 |
get_num_joints() |
size_t |
关节维度,必须与 joint_num 一致 |
get_pos(frame) |
const vector<float>& |
返回第 frame 帧的位置向量 |
get_vel(frame) |
const vector<float>& |
返回第 frame 帧的速度向量 |
值得注意的是,类内部未实现线程安全锁;其线程安全性由调用方保证——在本文架构中,get_motion_pos_obs 与 get_motion_vel_obs 仅在推理线程(inference thread)内被调用,而 step_motion_frame 也在同一线程同一节拍内顺序执行,因此不存在竞态条件。
Sources: motion_loader.hpp, obs_manager.cpp
配置参数与策略绑定
动作参考的启用完全由 YAML 参数驱动,通过 ROS 2 load_config 在节点构造时完成绑定。关键参数有两组:
1. motion_names(与 model_names 等长)
在 config/inference_beyondmimic.yaml 中,基础策略留空,其余策略分别绑定到 motions 目录下的 NPZ 文件:
model_names: ["policy.onnx", "policy_wave.onnx", "policy_dance.onnx", "policy_punch.onnx"]
motion_names: ["", "wave.npz", "dance.npz", "punch.npz"]
load_config 遍历策略列表时,将非空的 motion_names[i] 映射为 ROOT_DIR/motions/<name> 并写入 policy.motion_path,留空则表示该策略不加载 MotionLoader。
Sources: ros_interface.cpp
2. obs_layouts 中的 motion_pos 与 motion_vel
仅配置动作文件并不足以让策略"看到"参考轨迹;还必须在观测布局中显式声明这两个源。以 beyondmimic 为例:
obs_layouts:
- "ang_vel:3, gravity_b:3, cmd_vel:3, dof_pos:23, dof_vel:23, last_action:23"
- "motion_pos:23, motion_vel:23, ang_vel:3, gravity_b:3, dof_pos:23, dof_vel:23, last_action:23"
第二项布局将 motion_pos:23 与 motion_vel:23 置于观测向量前端,尺寸必须与 joint_num 一致。parse_obs_layout 在解析字符串时,会通过 obs_source_definitions() 查找名称匹配的观测源,并将成员函数指针 &InferenceNode::get_motion_pos_obs 注册到该策略的 obs_layout 中。
Sources: obs_manager.cpp, config/inference_beyondmimic.yaml
运行时帧推进与观测提取
推理循环的每一轮(inference())按以下顺序处理动作参考:
- 更新观测段:调用
update_obs_segments,触发get_motion_pos_obs与get_motion_vel_obs,从当前policy.motion_frame拷贝数据到segments。 - 堆叠与裁剪:将各观测段展平、裁剪后写入 ONNX 输入张量。
- 推进帧指针:调用
step_motion_frame(),将motion_frame自增 1;若到达最后一帧则钳位保持,不做循环播放。 - 执行推理:
session->Run生成动作输出。
sequenceDiagram
participant INF as inference()
participant OBS as update_obs_segments
participant MP as get_motion_pos_obs
participant MV as get_motion_vel_obs
participant STEP as step_motion_frame
participant ONNX as ONNX Runtime
INF->>OBS: 按 obs_layout 更新
OBS->>MP: motion_pos segment
MP->>MP: motion_loader->get_pos(motion_frame)
OBS->>MV: motion_vel segment
MV->>MV: motion_loader->get_vel(motion_frame)
INF->>STEP: 推理后推进
STEP->>STEP: motion_frame++
alt 到达末尾
STEP->>STEP: clamp 到最后一帧
end
INF->>ONNX: Run(input_tensor)
step_motion_frame 的钳位设计意味着动作序列只播放一次。例如一个 300 帧的 wave.npz,在第 300 个推理步后,motion_frame 将永远停在 299,策略会持续以最后一帧为参考继续运行,而非从头循环。这一行为与模仿学习中"先示范、后保持"的常见设定一致。
Sources: inference_node.cpp, obs_manager.cpp
运动策略的切换机制
在多策略运行时中,附带 MotionLoader 的策略被归类为 motion policy,其索引存入 motion_policy_indices_。手柄映射提供两层控制:
| 按键 | 条件 | 行为 |
|---|---|---|
| LB | 系统存在 motion policy | 在"基础策略"与"当前选中的 motion policy"之间切换;切换前自动暂停推理、重置该策略的运行时状态(reset_policy_runtime),确保帧指针归零。 |
| RB | 系统存在 motion policy 且当前不在 motion policy 模式 | 轮询 motion_policy_indices_ 队列,预选下一个 motion policy,仅日志提示,不立即切换。 |
该设计避免了在 motion policy 运行过程中切文件导致的帧号混乱。若用户希望在 wave 播放中途切换到 dance,必须先按 LB 退出 motion 模式回到基础策略,再按 RB 预选 dance,最后按 LB 进入 dance 模式。
Sources: ros_interface.cpp
关节维度校验与启动安全
在 InferenceNode 构造函数中,若检测到 motion_path 非空,将执行严格的启动前校验:
- 非空帧校验:
motion_loader->get_num_frames() == 0时直接报错。 - 关节数匹配:
motion_loader->get_num_joints()必须等于参数joint_num_。若 NPZ 文件来自不同 URDF 配置或关节顺序不一致,此检查可阻止后续索引越界。
这些校验位于模型初始化(setup_model)之前,确保在分配 ONNX 输入输出缓冲区前即暴露配置错误,减少运行时崩溃风险。
Sources: inference_node.hpp
故障排查速查
| 现象 | 可能原因 | 排查建议 |
|---|---|---|
| 启动报错 "Joint count mismatch" | NPZ 的关节维度与 joint_num 不符 |
检查生成 NPZ 时使用的机器人模型与当前 robot.yaml 是否一致 |
| 启动报错 "FPS not found" | NPZ 缺少 fps 字段 |
确保保存 NPZ 时包含 fps 数组,即使当前未在逻辑中使用 |
| 动作始终停留在第一帧 | motion_frame 未推进 |
确认 obs_layout 中正确注册了 motion_pos/motion_vel,否则 step_motion_frame 不会被调用 |
| 动作播放过快/过慢 | 推理周期与录制帧率不匹配 | MotionLoader 当前按推理节拍(dt * decimation)逐帧推进,而非按 fps 插值;若需时间对齐,应保证推理周期等于 1/fps |
小结与延伸阅读
MotionLoader 以极小的接口面积完成了动作参考的加载与帧管理:初始化阶段一次性解析 NPZ 到内存,运行时以 O(1) 索引提供当前帧数据。它与 观测布局配置与动态解析 中的动态观测源注册机制深度耦合,又与 多策略运行时与热切换机制 中的策略切换逻辑共享 PolicyRuntime 状态。若需新增一条自定义动作序列,只需准备符合格式的 NPZ 文件、在 YAML 的 motion_names 中追加条目,并在对应 obs_layouts 项中插入 motion_pos 与 motion_vel 即可,无需修改 C++ 源码。