第一次把 ZED 接上 RTABMAP 的时候,我天真地以为这就是一个"现成相机 + 现成 SLAM 库"的标准组合,装好驱动、写好 launch、跑起来就能出图。结果从上车那一刻起,我花了整整两个晚上在 debugging,光是把 TF 树理干净就用掉一半时间。这篇文章就是一次完整的 ZED + RTABMAP 排错记录,覆盖我从"话题接不通"到"地图稳下来"的全过程,包括坐标系约定、深度对齐、时间同步、里程计选型、性能调参。如果你正准备用手头的 ZED(ZED 2 系列同样适用)跑 RTABMAP,或者已经跑起来但地图天天跳、点云糊成一团,这篇应该能帮你少走不少弯路。
1. 启动链路:ZED话题重映射与坐标系约定
1.1 我最终的 launch 骨架
先把我跑通的 launch 原文贴出来,后面所有坑都围绕这份配置展开。我用的环境是 Ubuntu 20.04 + ROS Noetic + ZED SDK 4.x + zed-ros-wrapper + rtabmap_ros 0.21。
<launch> <!-- ZED 相机,关掉 TF 发布,理由见第 2 节 --> <include file="$(find zed_wrapper)/launch/zed.launch"> <arg name="publish_tf" value="false"/> </include> <!-- RTABMAP 主节点 --> <node name="rtabmap" pkg="rtabmap_ros" type="rtabmap" output="screen"> <param name="frame_id" type="string" value="zed_left_camera_optical_frame"/> <param name="odom_frame_id" type="string" value="odom"/> <param name="subscribe_depth" type="bool" value="true"/> <param name="approx_sync" type="bool" value="false"/> <param name="grid_3d" type="bool" value="true"/> <param name="rgbd_odometry/imu_topic" type="string" value="/zed/zed_node/imu/data"/> <remap from="imu" to="/zed/zed_node/imu/data"/> <remap from="rgb/image" to="/zed/zed_node/rgb/image_rect_color"/> <remap from="depth/image" to="/zed/zed_node/depth/depth_registered"/> <remap from="rgb/camera_info" to="/zed/zed_node/rgb/camera_info"/> <remap from="depth/camera_info" to="/zed/zed_node/rgb/camera_info"/> </node> </launch>很多教程会教你直接roslaunch zed_wrapper zed.launch然后再开一个 rtabmap 的 launch,但真正的问题从"怎么把两边接起来"就开始了。RTABMAP 的默认输入话题名,和 ZED 的话题名完全对不上,必须靠 remap 一条条接。我用到的映射关系汇总如下:
| RTABMAP 默认话题 | 接到 ZED 的话题 | 说明 |
|---|---|---|
| rgb/image | /zed/zed_node/rgb/image_rect_color | 左目去畸变彩色图 |
| depth/image | /zed/zed_node/depth/depth_registered | 已对齐左目的深度图 |
| rgb/camera_info | /zed/zed_node/rgb/camera_info | 左侧相机内参 |
| depth/camera_info | /zed/zed_node/rgb/camera_info | 注册深度复用同一内参 |
| odom | /zed/zed_node/odom | 选外部里程计时才需要 |
| imu | /zed/zed_node/imu/data | 需要 IMU 融合时才需要 |
1.2 为什么必须是 depth_registered,而不是 depth/depth
这是第一个我能想到的最隐蔽的坑。ZED 输出的depth/depth是原始视差图,坐标系在左目相机,但它和rgb/image_rect_color之间有一个固定的视差偏移,严格来说并不是逐像素对齐的。RTABMAP 的 RGB-D 前端默认假设你喂进来的彩色图和深度图已经完成像素级配准,它内部直接用depth(u,v)对应rgb(u,v)去生成点云、算视觉里程计。
如果你图省事,把 depth/image 重映射到/zed/zed_node/depth/depth,结果就是:RTABMAP 不会立刻报错,但建出来的地图会有明显的重影和撕裂感,视觉里程计在纹理丰富的地方也会频繁丢失。我最初就是栽在这里,看着地图像"图像和深度互相错位套印",排查半天才发现是 topic 接错了。ZED 专门提供了一条depth_registered话题,就是已经和左目彩色图对齐过的深度,SLAM 场景一律用这个。
同理,depth/camera_info也别接成 ZED 的depth/camera_info,注册后的深度图内参应该和 RGB 完全一致,所以两个 camera_info 都指向/zed/zed_node/rgb/camera_info即可。
1.3 坐标系约定:frame_id 必须和图像头一致
RTABMAP 的frame_id参数代表"相机所在坐标系",它的默认值是base_link,这是给底盘机器人用的。手持 ZED 或者直接把 ZED 当传感器的时候,最省事的做法是把frame_id直接设成图像话题 header 里的 frame_id,也就是zed_left_camera_optical_frame。
怎么确认?一条命令:
rostopic echo /zed/zed_node/rgb/image_rect_color/header/frame_id我一开始按惯性设了base_link,结果 RTABMAP 一直报 transform 找不到,日志反复出现类似Could not get transform from zed_left_camera_optical_frame to base_link。这句话的意思就是:TF 树里缺了从相机光学系到 base_link 的变换。如果你后面确实需要base_link(比如 ZED 装在机器人底盘上),那就得自己发布一个静态变换:
rosrun tf2_ros static_transform_publisher 0 0 0 0 0 0 zed_left_camera_optical_frame base_link否则,手持场景直接让 RTABMAP 工作在相机坐标系下,最干净。
2. TF 树里的两股力量:里程计坐标之争
2.1 症状:地图瞬移、相机位姿横跳
跑通数据流之后,我碰到的是最让人崩溃的现象:rviz 里整张地图和 ZED 点云每隔几秒就整体"瞬移"一下,有时候直接跳到走廊另一头;rtabmapviz 里显示的两个位姿互相打架。一开始我以为是回环检测在抽风,后来才发现是 TF 树乱了。
2.2 根因:有两个里程计来源在同时广播 TF
ZED wrapper 默认publish_tf=true,它认为自己是一个完整的里程计源,会广播从odom到相机系(或base_link)的 TF。而 RTABMAP 的 RGB-D 里程计也会广播odom到frame_id的 TF。
这里有两种崩溃方式:
- 如果你给 ZED 设了
base_frame=base_link,同时 RTABMAP 的frame_id也是base_link,那么两个节点抢同一条 TF 边,后广播的覆盖先广播的,坐标自然来回跳。 - 如果你没设 base_frame,ZED 广播的是
odom → zed_camera_center,RTABMAP 广播的是odom → zed_left_camera_optical_frame,TF 树本身不报错,但系统里出现了两个互不确认的"相机在 odom 系下的位姿",rviz 里表现为相机位置在两个坐标系之间横跳。
不管是哪种,结论都一样:ZED 和 RTABMAP 都把自己当成里程计源,TF 树里容不下两个老大。
2.3 排查链路
这个问题的排查顺序其实很固定,我建议按下面来:
# 1. 看 TF 树全貌 rosrun rqt_tf_tree rqt_tf_tree在树里找odom的子节点。正常情况下,odom下面应该只有一条通向相机系的链,而且最终由 RTABMAP 控制的map → odom是唯一的全局修正通道。如果看到odom下面同时挂着两个都"长得像相机"的坐标系,或者同一条边有两个广播者,基本就是它了。
# 2. 看具体的 TF 是否跳变 rosrun tf tf_echo base_link odom如果输出在两个位姿之间反复横跳,几乎可以实锤。
# 3. 看 /tf 话题是从哪个节点发出来的 rostopic info /tf这一步能直接看到广播者名单,确认是不是 ZED wrapper 和 rtabmap 同时在发。
2.4 修复:把 TF 的发布权完全交给 RTABMAP
我的做法是 ZED 只当传感器和图像源,不给它任何建图层面的身份:
<include file="$(find zed_wrapper)/launch/zed.launch"> <arg name="publish_tf" value="false"/> </include>这样 ZED 只发布图像、深度、内参,以及你需要的 IMU 数据,TF 树完全由 RTABMAP 构建:map → odom → zed_left_camera_optical_frame。回环检测触发时,也只有 RTABMAP 能去修正map → odom,不会出现另一个源头跑来捣乱。
注意:关掉
publish_tf之后,如果后面要做 IMU 融合,记得确认 ZED 的 IMU 相关 TF(zed_camera_center → zed_imu_link)仍然存在。ZED wrapper 里 IMU 的 TF 通常是独立开关控制的,比如publish_imu_tf,别一起误关了,否则 RTABMAP 会报找不到zed_imu_link的变换。
3. 数据流的两个隐形坑:时间同步与深度未对齐
3.1 症状:RTABMAP 界面空白,日志只有"等待同步"
TF 理顺之后,下一道坎是 RTABMAP 收不到数据。不是完全收不到,而是它一直在等。日志里反复刷:
Waiting for synchronization...或者 rgbd_odometry 压根不输出里程计话题。这是非常典型的"数据在、同步没成"的问题。
3.2 根因分析:exact sync 的严格匹配
RTABMAP 默认使用 exact time sync,要求 RGB 和深度话题的时间戳逐帧严格相等(允许一个很小的容差)。ZED 在默认配置下,RGB 和depth_registered确实是同帧率、同时间戳发布的,所以 exact sync 理论上没问题。但实际情况里,只要出现下面任一情况,同步就崩:
- 你在 ZED 里改了
depth_fps,比如彩色 30fps、深度 15fps; - 你订阅的是压缩话题(
compressed),解压和转发会引入额外延迟; - ZED 在 GPU 高负载时丢帧,两个话题丢帧节奏不一致。
我当时的现场就是 ZED 开了 2K + ULTRA 深度质量,RGB 和 depth 的发布节奏被打乱,rostopic hz 看到两个话题都有数据,但时间戳永远凑不到一对。
3.3 定位方法
先看频率:
rostopic hz /zed/zed_node/rgb/image_rect_color rostopic hz /zed/zed_node/depth/depth_registered两个频率应该完全一致。再看时间戳:
rostopic echo --noarr /zed/zed_node/rgb/image_rect_color/header/stamp rostopic echo --noarr /zed/zed_node/depth/depth_registered/header/stamp对比连续几帧的 stamp,如果 RGB 和 depth 的差值超过一个帧周期,exact sync 基本不可能成功。
3.4 两个解决办法
优先方案:让 RGB 和 depth 回到同帧率、同分辨率。在 ZED 的启动参数里,把camera_resolution和 fps 固定下来,深度不要单独降帧。这样能保持 exact sync,对视觉里程计最理想,因为深度和图像的曝光/位姿是同一时刻的。
兜底方案:把 RTABMAP 的approx_sync和rgbd_odometry/approx_sync都设为 true。近似同步的代价是深度和图像最多相差一帧,在建图精度上几乎感知不到,但对整体稳定性帮助很大。我最后在 2K 高负载场景下就开了 approx,实测没有问题。
3.5 camera_info 不对齐的连锁反应
还有一个和深度未对齐配套的坑:如果你误用了未注册的depth/depth,RTABMAP 虽然能跑,但日志里会出现 camera info 相关的 warning,因为深度的内参和 RGB 内参不一致(ZED 的depth/camera_info和rgb/camera_info本来就不一样)。这会导致点云比例尺错误,地图整体变形。所以再次强调:深度图用depth_registered,camera_info 全部指向rgb/camera_info。
4. 里程计路线选择:ZED自带odom还是RTABMAP的RGB-D VO
4.1 两条路线的本质区别
ZED 自己有一套相当成熟的视觉里程计,SDK 内部会融合 IMU 数据,输出 6DoF 的/zed/zed_node/odom。RTABMAP 也有自带的 RGB-D 里程计,可以从零开始算 frame-to-frame 运动,也可以接收 IMU 话题做融合。所以你有两条路线:
| 路线 | 里程计来源 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|---|
| A | ZED SDK odom | 移动底盘、需要多源里程计 | 省事、ZED 融合效果好 | 内部算法不可控,回环修正靠 map→odom 吸收漂移 |
| B | RTABMAP 自带 RGB-D VO | 手持 ZED 纯视觉建图 | 回环更自然,参数可调 | 需要自己调 VO 参数 |
4.2 我踩过的坑:路线混用
我最初的 launch 很"贪心":把/zed/zed_node/odomremap 给了odom,同时又把/zed/zed_node/imu/data也喂给了 RTABMAP。理论上这等于"ZED 出里程计,RTABMAP 又拿着 IMU 自己算一份",两个里程计源在 RTABMAP 内部打架,结果就是地图前端很漂,回环一触发整个图就跳。
后来我把思路理清楚:要么走 A,RTABMAP 只用 ZED 的 odom,不开启自己的 VO 也不喂 IMU;要么走 B,RTABMAP 自己算 VO,IMU 只作为 VO 内部的融合输入。混用等于同时开了两套里程计,别这么干。
4.3 手持 ZED 的推荐:走 B
我做的是手持 ZED 在室内走廊来回走、建闭环地图,最后选了路线 B。理由是:RTABMAP 的回环检测和图优化是它的强项,让它的算法从头到尾掌握里程计信息,回环修正更容易收敛;ZED 的 odom 在急转和快速移动时虽然也很稳,但那是 ZED"自己的世界观",RTABMAP 拿过来用还得额外处理两个里程计之间的尺度差。
路线 B 的关键配置就是前面 launch 里的两行:
<param name="rgbd_odometry/imu_topic" type="string" value="/zed/zed_node/imu/data"/> <remap from="imu" to="/zed/zed_node/imu/data"/>ZED 2 系列的 IMU 频率很高(400Hz),数据量看起来吓人,但 RTABMAP 内部会做降采样和缓存,正常不会成为瓶颈。真正要检查的,是 TF 树里有没有zed_imu_link的变换。如果你像我一样关掉了publish_tf,记得保留 IMU 相关 TF,否则会看到:
Could not get transform from zed_imu_link to ...4.4 IMU 融合的两个附带提醒
第一,RTABMAP 有个参数wait_imu_to_rgbd,默认 false。如果你开了 IMU 融合但 IMU 话题迟迟不来(比如 ZED 节点刚启动还在传感器预热),RTABMAP 可能会拿着没有 IMU 的 RGB-D 数据先跑一段,等 IMU 到了再融合,前后可能需要复位。建议在 launch 里显式设置wait_imu_to_rgbd为 true,让它等 IMU 稳定后再开始融合,代价只是启动慢一两秒。
第二,ZED 的 IMU 话题在 400Hz 下会占用一定带宽。如果你的机器网络环节薄弱,或者用无线传输 rosbag,优先把 IMU 降频或者只在建图时开启,别让 IMU 数据吃光链路。
5. 参数校准:从"能跑"到"建图稳定"
5.1 ZED 侧参数:深度质量和分辨率是跷跷板
ZED 的动态参数可以通过rqt_reconfigure在线调,也可以在启动参数里固定。我踩出来的经验表如下:
| 场景 | 分辨率 | 帧率 | 深度质量 | 说明 |
|---|---|---|---|---|
| 手持室内建图(推荐起步) | 1080p | 30 | QUALITY | 兼顾细节和帧率,GPU 压力可控 |
| 快速验证/低配机器 | 720p | 30 | PERFORMANCE | 先跑通流程再说 |
| 追求极致细节 | 2K | 15 | ULTRA | 仅限相机固定/三脚架,手持强烈不推荐 |
2K + ULTRA 看起来爽,但手持时运动模糊会让视觉里程计的特征匹配大面积失败,加上 GPU 占用率飙高,ZED 自身丢帧,最后建出来的图反而不如 1080p QUALITY。我的教训是:别把相机的极限参数当成 SLAM 的最优参数。
5.2 RTABMAP 侧参数:最值得动的几项
RTABMAP 参数很多,我实际调试中作用最明显的就几个:
Mem/STMSize(默认 40):短期记忆节点数。走廊类的长直场景,40 可能偏小,旧关键帧很快被移出短期记忆,回环检测找不到足够重叠的视角。我把它调到 60~80 后,回环质量明显改善。RGBD/MaxDepth(默认 0 表示用深度图自带范围):ZED 深度在远处噪声很大,建议显式设 8~12 米,把远处杂点排除。RGBD/ProjMaxDepth:生成占据网格/点云地图时同理,限制投影深度范围。rgbd_odometry/feature_based:0 表示基于稠密/半稠密的 RGB-D 里程计,1 表示基于特征点。室内走廊这种纹理重复、转角多的环境,特征点法容易丢,我最后长期用 0。但如果是纹理非常丰富的场景,1 可能更快。grid_3d:手持 ZED 会上下俯仰,建 3D 网格地图更合理,launch 里已设为 true。
5.3 性能监控:GPU 是共享资源
ZED SDK 的深度计算和视觉里程计都吃 GPU,RTABMAP 的特征提取、回环检测也会用 GPU 加速。两者共享一张显卡时,用nvidia-smi看占用率是个好习惯。我实测发现,1080p@30 QUALITY 下 ZED 约占 30%~40% GPU,RTABMAP 再占 20% 左右,整体平稳;一旦切到 2K ULTRA,ZED 直接吃掉 70%+,两个节点开始在显存上互相踩踏,表现为深度图掉帧和 RTABMAP 里程计中断。
如果你和我一样只有一张卡,建议先固定 ZED 分辨率,再调 RTABMAP 参数。别让两个节点同时饿肚子。
5.4 回环不闭环时的排查顺序
最磨人的是"明明走了一圈,回环就是不触发,或者触发了但地图还是跳"。按这个顺序查:
- 看 RTABMAP 日志里有没有
Loop closure detected!。没有,说明回环检测本身没成功,优先怀疑 STMSize 太小、路径缺少重叠视角、或者深度孔洞太多(depth_confidence阈值太高导致有效深度点太少)。 - 有回环但地图还跳,说明图优化没压住累积漂移,检查
map -> odom -> 相机系这条 TF 链是否有外部干扰(回到第 2 节的问题)。 - 用 rtabmapviz 看回环候选,确认候选关键帧和你当前视角确实有共同可见区域。走廊墙面粉刷成一片纯白、没有任何纹理的时候,特征匹配数不够,回环失败是正常的,物理世界的问题算法解决不了。
6. 排错清单:症状、命令、根因速查表
最后给一张我实际贴在工位上的速查表,按症状直接查命令和根因:
| 症状 | 首查命令/工具 | 最常见根因 | 处理 |
|---|---|---|---|
| 完全没数据 | rostopic list、rqt_graph | remap 写错或命名空间不对 | 对照第 1 节重映射表逐条核对 |
| 有话题但 RTABMAP 空白 | rostopic hz、echo 对比时间戳 | RGB 和 depth 帧率/时间戳不一致 | 同帧率或设approx_sync=true |
| TF 报错找不到变换 | rostopic echo .../header/frame_id、tf_echo | frame_id 和图像头不一致,缺静态变换 | 统一 frame_id 或补 static TF |
| 地图/相机位姿瞬移 | rqt_tf_tree、rostopic info /tf | ZED 和 RTABMAP 双里程计源 | 关 ZEDpublish_tf |
| 点云重影/地图撕裂 | 检查 depth/image 实际话题 | 用了未对齐的depth/depth | 改用depth_registered |
| 回环长期不触发 | rtabmap 日志、rtabmapviz | STMSize 太小/视角不足/深度孔洞 | 调 STMSize、降 confidence 阈值 |
| GPU 满载、频繁丢帧 | nvidia-smi、rostopic hz | ZED 参数过高 | 降分辨率/深度质量 |
我自己排查的习惯是固定顺序:先数据流,再 TF,再参数,最后性能。数据流不通,后面全是白费;TF 错了,参数调得再好也会跳;参数是锦上添花的东西;性能问题是最后压轴的。不要跳步,不要同时改两个变量。
另外提一句,如果你是在 ROS 2(比如 Humble)下跑 zed-ros2-wrapper 和 rtabmap_ros2,最常遇到的根本不是上面这些,而是 DDS QoS 不匹配:话题能看到、频率也能看到,但 receive 端就是收不到消息。需要把 RTABMAP 的 QoS 参数和 ZED 发布端的 QoS 设置对齐。这块内容足够单独写一篇了,这里先立个 flag。
最后说点实在的。跑通 ZED + RTABMAP 只是开始,真正让我把地图从"能看"变成"能用"的是两个习惯:一是每次调参前先用 rosbag 录一段固定路线,调参后放同一段数据回放对比,保证变量只有参数本身;二是永远只改一个参数、跑一次、记一次。别同时动 ZED 的深度质量和 RTABMAP 的 STMSize,出了问题你根本不知道是谁干的。另外,手持 ZED 时尽量慢速、少急转弯,视觉里程计和回环检测都是"数据喂出来的",你走得越稳,地图越干净。