Files
ros_contorl/devices/sim/gazebo/config/cameras.yaml
T
arm_dev 47ad9d9a8b P5c-4 相机出图联调:桥接 d405/d435 到 ROS,并修掉两处静默的几何错误
相机链路(devices/sim/gazebo/config/cameras.yaml 是唯一真相)
  - sim_cameras.py        纯逻辑:话题映射 / 深度换算 / 内参 / 视场一致性比对
  - sim_camera_adapter.py 改光学系 frame_id、32FC1 米 → 16UC1 毫米、覆盖内参
  - gazebo_sim.launch.py  桥接 + 适配器 + 世界相机的静态 TF(位姿从 world 解析)

本轮修掉两个**静默**的几何错误(都能出图、都能查到 TF,但点位是错的)

  1. gz 发的 camera_info 是错的:恒为 fx=fy=277, cx=160, cy=120
     (等于它的默认 60° 视场 + 半分辨率主点),而渲染用的是配置的 87° 视场。
     照抄它做三维反投影,点会偏半米量级;单目预览完全正常。
     用已知地面平面做最小二乘反解,得 fy=337.2 / cy=239.5,与
     640/(2·tan(87°/2))=337.2、480/2=240 吻合(残差中位 0.2mm)。
     改为由 sim_cameras.intrinsics() 按 cameras.yaml 的视场重算并覆盖。
     代价:cameras.yaml 的 horizontal_fov 必须与 SDF 一致,启动时自动比对告警。

  2. assembly.world 里 D435 的 rpy 写成 (0, 50°, 180°),
     而 URDF 相机的视线是**本体 X 轴**、rpy 复合顺序又是 Rz·Ry·Rx,
     于是"先俯 50° 再偏航 180°"把视线翻到了背面:相机背对台面、只看地面。
     它不报错,而且几何自洽(点确实落在真实地面上),只查数值发现不了。
     改为 (0, 50°, 0),并加回归测试锁住。

新增验收工具 + 测试
  - scripts/check_sim_cameras.py  反投影深度、要求命中地面/台面才算通过。
    "有图像"不是验收标准;实测 d435 台面 11.9%/地面 75.3%、d405 台面 37.8%。
  - 97 项单测(新增 sim_cameras 22 项、sim_site 16 项)

顺带修掉一处"改了配置静默无效"
  - sim.world 此前硬编码在 robot_description._WORLD_REL,而 site.yaml 与
    manifest.yaml 都宣称改它就生效。现在真的读了:
    优先级 --world > site.yaml:sim.world > 内置默认(相对路径按仓库根解析)。
  - physics_enable / joint_prefix 仍未接线,但若其值与实际状态冲突,
    启动时会告警(sim_site.unwired_warnings),不再静默忽略。

文档
  - devices/sim/gazebo/README.md 重写:原文还写着"仿真不是 Gazebo"、
    相机插件用 libgazebo_ros_camera.so,与实现早已不符。现分三部分:
    哪个"仿真"是真的 / 现状与缺什么 / 五条静默失效的坑及怎么验。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 15:19:16 +08:00

87 lines
5.5 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# devices/sim/gazebo/config/cameras.yaml
#
# 仿真相机的**唯一数据源**:gazebo_sim.launch.py 里的 ros_gz_bridge 参数、
# sim_camera_adapter 节点、以及为世界相机补的静态 TF,全部从这份文件派生。
# 想加一台仿真相机,改这里一处即可(前提是它在 URDF / world 里已经有 <sensor>)。
#
# 与 site.yaml 的分工:
# 这里只描述"Gazebo 里有什么、要桥到 ROS 的哪个名字"。
# site.yaml 描述"真机上有哪些相机、序列号是多少"。仿真不读序列号 ——
# Gazebo 里没有 USB,序列号无从绑定。
# 两者的**话题命名约定必须一致**,这是应用层能不改代码跑仿真的前提。
#
# 话题命名约定(与真机 realsense 完全对齐,见 arm_application/handeye_realsense.py
# 的 REALSENSE_TOPIC_DEFAULTS 与 realsense_color_pub.py):
# /<ros_ns>/color/image_raw rgb8
# /<ros_ns>/color/camera_info plumb_bob
# /<ros_ns>/aligned_depth_to_color/image_raw 16UC1,单位毫米,0 = 无读数
#
# ⚠️ 单位与编码是关键差异,不能只做话题改名就完事:
# Gazebo 的深度相机发的是 **32FC1、单位米**(本机实测 step=2560 即 4 字节/像素),
# 而真机 realsense 的 aligned_depth 是 **16UC1、单位毫米**。
# 直接把 32FC1 换个名字发出去,应用侧按毫米解释会整体差 1000 倍,
# 且 numpy 的 frombuffer 会因 dtype 不符报错或读出垃圾。
# 所以深度必须**真正转换**,由 sim_camera_adapter 负责。
#
# ⚠️⚠️ 更要紧的一条:**gz 发布的 camera_info 是错的,不能直接用。**
# 本机实测(Fortress 6.16 / RoboStack):无论怎么配,它都给
# fx=fy=277, cx=160, cy=120
# 这组值对应的是 **gz 的默认值** —— 60° 视场 + 半分辨率图幅(320x240),
# 而实际渲染用的是配置里的 87° 视场与 640x480。
# 用已知地面平面反解真实内参得到 fy=337.2、cy=239.5,
# 与 640/2/tan(1.5184/2)=337.2、480/2=240 完全吻合(残差中位 0.2mm)。
# 照抄 gz 的 camera_info 去做三维反投影,点位会错 0.5 米量级 ——
# 而单目预览里**完全看不出来**,只会让"仿真里调好的参数搬到真机"整个失效。
# 因此 sim_camera_adapter 用下面这份 horizontal_fov **重算**并覆盖内参。
#
# 所以每台相机必须声明 horizontal_fov,且**必须与 SDF 里真正渲染用的值一致**。
# 它有两个来源,必须同步(启动时会自动比对并告警,见 sim_cameras.check_fov_consistency):
# 腕上相机 → devices/sim/gazebo/urdf/arm.sim.sensors.xacro 的 horizontal_fov
# 世界相机 → devices/sim/gazebo/worlds/assembly.world 里该 <sensor> 的 horizontal_fov
# =============================================================================
cameras:
# --- 腕上相机(eye-in-hand),挂在机械臂法兰上 ------------------------------
- gz_ns: /d405 # Gazebo 传感器话题前缀(see arm.sim.sensors.xacro 的 <topic>)
ros_ns: d405 # ROS 侧命名空间。应用按 camera_name 参数用这个名字取话题,
# 所以它必须等于启动应用时给的 camera_name(默认 d405)
link_frame: sim_d405_link
# 图像上盖的坐标系必须是**光学系**,不能是本体系。
# gz 与 URDF 里的相机本体遵循"X 朝前 / Y 朝左 / Z 朝上"(相机机身约定),
# 而 ROS 的 image / camera_info 必须用光学约定"Z 朝前 / X 朝右 / Y 朝下"
# (REP-103/105)。两者差一个 rpy=(-90°, 0, -90°)。
# 若直接把 frame_id 盖成本体系,应用做 tf2 查询能查到、不报错,但
# 深度反投影出的三维点会整体转错 —— 这类错误在单目预览里完全看不出来。
optical_frame: sim_d405_color_optical_frame
depth: true
# 水平视场(弧度)。真机 D405 彩色的水平视场约 87° = 1.5184 rad,
# 仿真的传感器也按这个建,这样两边的像素尺度接近。
# 内参由它和图像尺寸算出:fx = fy = W / (2*tan(hfov/2)),cx=W/2,cy=H/2。
horizontal_fov: 1.5184
width: 640
height: 480
note: 仿真里由 sim_link7 上的 d405_rgbd 传感器提供
# --- 手外相机(eye-to-hand),固定在世界里看工作台 -------------------------
# ⚠️ 这台相机在 world 文件里,不在 URDF 里 —— 因为它的正确物理归属就是"世界",
# 而不是机器人本体。(曾经把它挂在 URDF 的 world link 上,结果 sdformat 把
# 无质量的根 link 连同整棵子树静默丢掉,相机与它的话题一起消失。详见
# devices/sim/gazebo/README.md。)
# 代价是:gz 不会为世界模型发布 TF,所以必须由启动方补一条静态 TF
# world → d435_link。位姿从 world 文件里**解析**得到(不在这里重复配置),
# 避免"改了一处忘了另一处"。
- gz_ns: /d435
ros_ns: d435
link_frame: d435_link
optical_frame: d435_color_optical_frame
depth: true
horizontal_fov: 1.5184
width: 640
height: 480
# 非空 = 这台相机挂在世界模型上,需要补静态 TF。
# world_model 是 world 文件里的 <model name>,world_link 是模型里的 <link name>。
world_model: d435
world_link: link
note: height 也与真机不同(真机 1280x720),同 D405 的说明