-
Notifications
You must be signed in to change notification settings - Fork 12
Hands On Depth Policy State
本文按当前实现说明 com.bxi.normal_depth 怎样消费仿真或真机深度相机。相机采集与
机器人控制已经解耦:bxi_depth_camera 管理真机,MuJoCo 发布仿真图像,深度行走 Mod
只订阅统一的 ROS 2 图像和 CameraInfo。
真机:bxi_depth_camera
仿真:MuJoCo camera sensor
│
│ Image + CameraInfo
▼
NormalDepthState
├─ 解码完整深度图
├─ 根据真实内参裁剪策略 FOV
├─ 缩放、距离限幅和旋转
├─ 按策略频率选择深度帧
└─ RobotControlState 输出 MotorFrame
当前边界如下:
- 相机包负责发现设备、序列号映射、SDK 滤波和标准 ROS 流发布。
- 深度行走 Mod 不导入
pyrealsense2,也不打开相机设备。 - 仿真与真机使用相同逻辑相机名和流级目录。
-
simulation与hardware只由/topic_prefix区分。 - 策略裁剪依赖每一帧配置对应的真实
CameraInfo,不硬编码输入相机内参。
真机可单独启动相机管理器:
ros2 launch bxi_depth_camera cameras.launch.py硬件示例启动文件已经默认包含它:
ros2 launch bxi_example_py_elf3 example_demo_hw.launch.pyMuJoCo 不需要启动真机相机包。仿真包会发布 XML 中声明的 camera-backed user sensor。
逻辑相机名默认为 body_depth_camera。状态实际使用两条深度流:
/simulation/body_depth_camera/depth/image_rect_raw
/simulation/body_depth_camera/depth/camera_info
/hardware/body_depth_camera/depth/image_rect_raw
/hardware/body_depth_camera/depth/camera_info
图像要求:
- 类型:
sensor_msgs/msg/Image; - 编码:
16UC1、mono16或32FC1; -
16UC1/mono16默认按毫米解释; -
32FC1按米解释; - 允许
step包含行尾 padding; - 无效深度保持为
0。
相机信息要求:
- 类型:
sensor_msgs/msg/CameraInfo; -
width、height为正数; -
k含 9 个有限值; -
fx=k[0]、fy=k[4]为正数; - 必须与当前深度流的分辨率和内参对应。
状态不会订阅旧的 /camera/depth/image_36x48 或
/camera/depth/image_64x36。策略尺寸由状态内部生成。
策略配置只写安装位置的逻辑名称:
camera_name: body_depth_camera每台真机在 bxi_depth_camera 配置中把该名称映射到实际设备序列号:
/depth_camera_manager:
ros__parameters:
single_camera_name: body_depth_camera
cameras:
body_depth_camera:
serial_no: "实际序列号"显式序列号映射始终优先。如果只连接一台未映射的受支持相机,它会使用
single_camera_name(默认 body_depth_camera);连接多台未映射相机时,各设备使用
SN_<serial>,因此多相机部署应显式配置逻辑名称,不能依赖 USB 枚举顺序。
当前 mod.yaml 的核心配置是:
states:
normal_depth:
params:
mode: origin_camera
camera_name: body_depth_camera
topic: ""
camera_info_topic: ""
depth_uint16_scale: 0.001
depth_timeout_sec: 0.5
horizontal_fov: 45.2
vertical_fov: 58.0616969
min_dist: 0.2
max_dist: 3.0当 topic 和 camera_info_topic 都为空时,状态使用 /topic_prefix 和
camera_name 自动解析话题。两者必须同时为空或同时填写,不能只覆盖一条:
camera_name: ""
topic: /recorded_camera/depth/image_rect_raw
camera_info_topic: /recorded_camera/depth/camera_info支持的策略模式:
| mode | 投影输出(旋转前) | 状态旋转后 shape | 默认策略 FOV | 默认距离 |
|---|---|---|---|---|
origin_camera |
36 × 48 |
(36, 48) |
45.2° × 58.0616969° |
0.2–3.0 m |
depth_walk |
64 × 36 |
(64, 36) |
89.24° × 58.06° |
0.2–2.5 m |
这些是策略输入要求,不是物理相机的完整 FOV。状态根据物理相机的 CameraInfo 计算中心
ROI,再缩放到策略尺寸。
两套策略使用不同的全局 Resource key:
LEGACY_POLICY = ResourceKey[HumanoidGaitDepthPolicyIsaaclab](
"com.bxi.normal_depth/legacy_policy"
)
ORIGIN_POLICY = ResourceKey[HumanoidGaitOriginCameraPolicyIsaaclab](
"com.bxi.normal_depth/origin_policy"
)当前内置 Mod 在启动控制循环前准备两个模型:
context.register_resource(
LEGACY_POLICY,
_load_legacy_policy,
policy="startup",
)
context.register_resource(
ORIGIN_POLICY,
_load_origin_policy,
policy="startup",
)如果客户动作希望首次进入时才加载,可改为 policy="on_demand"。此时状态必须声明
资源依赖;NormalDepthState 已按 API 4 这样实现:
class NormalDepthState(
RobotControlState,
EntryFrameProvider,
RunningFrameProvider,
):
def __init__(self, name, state_id, policy, **config):
super().__init__(name, state_id, resources=(policy,))
self._policy = policyResourceHandle.get() 只读取已经就绪的模型。它不会在 ROS 回调或控制周期中同步加载。
状态在 on_bind() 中创建长期订阅,因为进入状态前就必须收到新鲜图像:
def on_bind(self, ctx):
if not self.depth_image_topic:
prefix = str(
ctx.ros_node.get_parameter("/topic_prefix").value
).strip("/")
self.depth_image_topic, self.camera_info_topic = resolve_camera_topics(
prefix,
self.camera_name,
)
self._depth_subscription = ctx.ros_node.create_subscription(
Image,
self.depth_image_topic,
self.depth_image_callback,
qos_profile_sensor_data,
)
self._camera_info_subscription = ctx.ros_node.create_subscription(
CameraInfo,
self.camera_info_topic,
self.camera_info_callback,
qos_profile_sensor_data,
)实际实现使用深度为 1 的 sensor-data QoS。on_unbind() 对称销毁两个 subscription。
不要在 on_enter() 反复创建订阅,否则刚请求状态时还没有图像,健康检查也无法通过。
回调流程是:
- 按
encoding、is_bigendian和step解码深度图; - 将深度统一转换为米;
- 读取对应
CameraInfo的fx、fy; - 按策略水平/垂直 FOV 和输出宽高比选择中心 ROI;
- 使用最近邻缩放,避免为深度制造不存在的插值表面;
- 只对有效像素做距离限幅,无效像素继续保持
0; - 按原策略方向顺时针旋转 90°;
- 在锁内替换最新完整数组和递增的 frame id。
核心调用如下:
projected, crop = project_depth(
depth_meters,
camera_info,
self.projection,
)
depth_rotated = np.ascontiguousarray(
np.rot90(projected, k=-1).astype(np.float32)
)相机分辨率变化时,只要 Image 与 CameraInfo 同步更新,裁剪会按新内参重新计算;不需要
为每一种分辨率硬编码一套 K/P 数组。
is_available() 必须非阻塞,只读取回调维护的快照:
def is_available(self, ctx):
with self._depth_lock:
depth = self._depth_rotated
stamp = self._last_depth_time
camera_info = self._camera_info
return (
self.depth_configured
and camera_info is not None
and depth is not None
and stamp is not None
and time.monotonic() - stamp <= self.depth_timeout_sec
)没有 CameraInfo、没有有效图像或图像已超时,状态请求会返回 False,机器人保持当前
状态。force=True 只绕过健康检查,不会补出缺失的深度数据,普通业务不要使用。
进入 normal_depth 使用 first_frame_switch,所以目标状态必须实现
EntryFrameProvider:
def get_entry_frame(self, ctx):
return self._motor_frame_from_target(ctx, self.policy.output.joints)切回普通行走使用 dual_running_blend。深度状态作为来源侧提供
RunningFrameProvider:
def sample_running_frame(self, ctx, dt, *, advance):
if not advance:
return self._last_running_frame or self.get_entry_frame(ctx)
depth, frame_id = self._get_depth_for_inference()
self.get_cmd_vel(ctx)
ctx.inference_frame.depth = depth
ctx.inference_frame.depth_frame_id = frame_id
output = self.policy.step(ctx.inference_frame, dt, advance=True)
return self._motor_frame_from_target(ctx, output.joints)advance=False 不能推进策略 history、时间或动作缓存。当前实现返回最近一次真实推理帧,
避免 Transition 预览重复消费深度数据。
控制循环和相机不需要同频。回调随相机更新完整图像;控制周期按
policy.depth_update_period 选择是否接收最新 frame id。策略可在多个控制周期复用同一张
深度图,但不会把同一帧误认为新的传感器采样。
不要在控制周期里等待下一张图,也不要在持有 _depth_lock 时执行推理。锁只保护引用、
时间戳和 frame id 的短暂复制。
状态每周期检查:
- 姿态超出安全范围:请求
com.bxi.basic_actions/zero_torque; - 深度超过
depth_timeout_sec未更新:请求com.bxi.basic_actions/normal; - 新深度暂不可用:本周期不发布新的策略帧;
- 输入编码、尺寸或内参非法:记录一次警告并丢弃该帧。
安全请求只发起状态切换,不在回调线程直接发布电机命令。
先确认相机契约:
ros2 topic info /simulation/body_depth_camera/depth/image_rect_raw --verbose
ros2 topic echo /simulation/body_depth_camera/depth/camera_info --once
ros2 topic hz /simulation/body_depth_camera/depth/image_rect_raw真机把 simulation 换成 hardware。然后检查:
ros2 topic echo /simulation/state_machine_info --once推荐顺序:
- 相机图和
CameraInfo都持续发布; - 日志中的 image、camera_info 和 post-rotation shape 正确;
- 静止仿真进入深度状态;
- 遮挡或停止相机,确认自动退回 normal;
- 验证 normal 与 normal_depth 的进入、退出 Transition;
- 最后在吊架和低速条件下验证真机。
| 现象 | 优先检查 |
|---|---|
| 状态不可进入 |
CameraInfo、图像新鲜度、逻辑相机名 |
| 一直等待 CameraInfo | 相机是否发布 /depth/camera_info,话题前缀是否正确 |
| shape 不符合策略 | mode、FOV 参数、图像与 CameraInfo 是否属于同一 profile |
| 深度全零 | 编码、单位、设备量程和 SDK 滤波 |
| 真机找不到相机 |
bxi_depth_camera 配置中的序列号映射 |
| 仿真正常、真机失败 | 比较两端 CameraInfo、安装姿态、深度单位和 frame rate |
| 切换时突跳 | 检查 entry/running frame 和 Transition profile |
| 首次进入等待模型 | 资源使用了 policy="on_demand",查看 preparing 状态 |
- 状态不直接依赖 RealSense 或 Orbbec SDK。
- 仿真与真机只替换
simulation/hardware前缀。 - 图像与
CameraInfo成对配置。 - Resource 使用 API 4 的
startup/on_demand策略。 - 状态通过
resources=(policy,)声明模型依赖。 -
advance=False不推进策略。 - 回调不执行模型推理,控制周期不等待相机。
- 图像失联和姿态异常都有明确退出路径。
当前实现以以下文件为准:
src/bxi_example_py_elf3/mods/com.bxi.normal_depth/mod.yaml
src/bxi_example_py_elf3/mods/com.bxi.normal_depth/plugin.py
src/bxi_example_py_elf3/mods/com.bxi.normal_depth/state.py
src/bxi_example_py_elf3/mods/com.bxi.normal_depth/depth_projection.py
src/bxi_example_py_elf3/mods/com.bxi.normal_depth/README.md