🤖 roboto_origin_03 Wiki
首页 / 推理子模块 / 动作参考加载与 MotionLoader

在基于强化学习与模仿学习的机器人控制中,策略网络往往需要一段"参考动作"作为输入,以指导当前步态向预设轨迹靠拢。本页聚焦 MotionLoader 模块的设计原理、NPZ 动作文件的格式规范,以及动作参考在实时推理循环中的注入与帧推进机制。阅读本页前,建议先理解 观测布局配置与动态解析obs_layout 的注册与解析逻辑,因为 motion_posmotion_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_posjoint_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_obsget_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_posmotion_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:23motion_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())按以下顺序处理动作参考:

  1. 更新观测段:调用 update_obs_segments,触发 get_motion_pos_obsget_motion_vel_obs,从当前 policy.motion_frame 拷贝数据到 segments
  2. 堆叠与裁剪:将各观测段展平、裁剪后写入 ONNX 输入张量。
  3. 推进帧指针:调用 step_motion_frame(),将 motion_frame 自增 1;若到达最后一帧则钳位保持,不做循环播放。
  4. 执行推理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 非空,将执行严格的启动前校验:

  1. 非空帧校验motion_loader->get_num_frames() == 0 时直接报错。
  2. 关节数匹配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_posmotion_vel 即可,无需修改 C++ 源码。