ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Cartographer踩坑全记录:从环境编译到建图定位的ROS调优与排障指南

Cartographer踩坑全记录:从环境编译到建图定位的ROS调优与排障指南

跑Cartographer跑得头痛?如果你接触过机器人导航,大概率对这句话有共鸣。Cartographer是目前应用最广的激光雷达SLAM方案之一,相比gmapping在回环检测和地图一致性上的表现确实更稳,但代价是依赖链长、配置项多、数据类型复杂,稍不留神就陷进某个“小问题”里出不来。这篇博文就是我实际运行Cartographer时踩坑、排雷、绕远路的全过程记录,覆盖从环境编译、传感器配置、在线建图到定位调试的主要问题。适合正在折腾Cartographer、想快速定位问题的开发者。内容以问题维度组织,每条都可以对照你自己的环境去复现和查找。

1. 环境搭建与依赖编译的坑

我个人体验,Cartographer安装阶段就把一多半的人劝退了。它不像gmapping那样一个二进制包放进来就能用,涉及Ceres Solver、Protobuf、Eigen、Abseil这些老熟人的版本博弈。很多报错看起来是编译参数问题,本质上都是依赖版本和系统自带库打架。

1.1 依赖版本不匹配的连锁问题

Cartographer对Protobuf的要求是3.4.x,而Ubuntu 18.04自带的Protobuf是2.6.1,Ubuntu 20.04自带的可能是3.6.1或者更高,但ROS相关组件往往又依赖系统自带的那份。问题就出在这里:当你把系统级Protobuf升级到4.x,所有依赖旧版Protobuf的ROS包会一起崩掉,连rosbag都可能跑不起来。我踩过一次之后,就再也不敢动系统Protobuf了。

我的做法是:源码编译指定版本的Protobuf,安装到一个独立目录,比如/opt/protobuf3.4,然后通过CMAKE_PREFIX_PATH让Cartographer找到它,而不是直接替换系统库。编译Protobuf时记得先执行autogen.sh,再执行./configure --prefix=/opt/protobuf3.4,然后make -j8和make install。之后编译Cartographer的CMake阶段,需要把/opt/protobuf3.4加进环境变量,还要确认libprotobuf.so链接的是这个目录下的版本,而不是系统目录的。

Ceres Solver也是重灾区。apt源里的Ceres版本通常老到和Cartographer要求的API对不上,编译时会碰到Ceres requires at least ...这类信息。建议直接编译官方的Ceres 1.13.0或更高版本。编译Ceres之前,最好先确认Eigen版本,因为Ceres和Cartographer都依赖Eigen。Ubuntu 16.04/18.04的apt源里Eigen可能还是3.2.x,但Cartographer需要Eigen 3.3以上,否则一堆模板相关函数找不到。源码装Eigen很轻松,下载解压后直接把整个Eigen目录放到/usr/include/eigen3下,或者用CMake指定CMAKE_PREFIX_PATH。

另外还有Abseil,很多人在这一步翻车。Cartographer的公共代码里大量使用absl库,旧版本没有这个库。如果你早前编译过Cartographer,新代码拉下来后忘记同步更新Abseil,就会出现Could not find a package configuration file provided by "absl"。官方提供了install脚本,在cartographer源码的/scripts/install_abseil.sh,直接运行它即可。但注意,该脚本默认把Abseil安装到/usr/local,有可能会和系统其他库冲突。如果机器环境比较乱,更推荐把Abseil编译到私有目录,比如/opt/absl,然后同样用CMAKE_PREFIX_PATH指向它,这样不会污染全局。

把这些第三方依赖理顺之后,再回到catkin workspace编译Cartographer本体和cartographer_ros。如果顺序乱了,后面编译错误会指数上升。我有一个小习惯:第一次配置时先用catkin_make -DCMAKE_BUILD_TYPE=Release,不用Debug模式,后面跑建图时速度差别很大。编译时建议加-j4或-j8,但不要一下拉满核数,不然内存小的机器会在链接阶段直接OOM,反而拖慢。

1.2 源码编译时常见错误清单

这些年反复出现、搜索量最高的编译问题,我整理成了一张速查表:

错误信息(摘录)根本原因处理办法
fatal error: google/protobuf/message.h: No such file...系统Protobuf版本过低或路径不对编译3.4.x版本,设置CMAKE_PREFIX_PATH
Could not find a package configuration file provided by "absl"Abseil未安装或版本过旧运行install_abseil.sh,注意安装路径
Eigen supports ... but ... requestedEigen版本过旧源码编译Eigen 3.3.x,放到CMAKE_PREFIX_PATH
undefined reference to absl::...Abseil和Cartographer编译时间不一致重新编译Abseil,然后干净重编Cartographer
Ceres version too oldapt源中Ceres过旧编译安装Ceres 1.13以上
Using deprecated gflagsgflags版本不统一统一使用源码安装版本,避免混用

还有一类是CMake过程中提示找不到gflags或者glog。这时别急着装系统库,看一下版本。Cartographer需要gflags 2.2.0以上,glog 0.3.5以上。用pkg-config --modversion gflags可以查版本,不满足就源码装。我遇到过一种奇怪情况,系统里同时存在2.1和2.2两个版本的gflags,CMake默认选了旧的那个,导致后面链接时一堆undefined reference。把旧版本彻底卸载或单独指定路径,问题马上消失。

注意:编译之前最好把之前失败的build目录清理干净,不然一些缓存的CMake文件会导致奇怪的中断。rm -rf build devel之后再重新编译,能解决很多玄学问题。

另外,如果你使用ROS Melodic或Noetic,catkin本身也可能出错。例如Cartographer_ros编译时提示找不到tf2_eigen这类依赖,你需要提前安装ros-$ROS_DISTRO-tf2-eigen和ros-$ROS_DISTRO-rosbag等。把所有ROS核心包都齐全了再开始,能省很多事。这里强烈建议用官方Wstool方式拉取cartographer和cartographer_ros,保持版本对应,而不是随意从分支拉代码,否则很容易出现API对不上的情况。

2. 配置文件与数据话题的对接

编译通过只是万里长征第一步,真正让人头痛的是数据没接对。Cartographer对数据格式很挑剔,传感器话题错了,它连报错都报得让人看不懂。下面是我最常遇到的两大类问题。

2.1 TF树与坐标系问题

Cartographer启动时最典型的报错是:Could not get transform from ... to ...或者Timed out waiting for transform from base_link to laser...。遇到这个,先去查TF树。一个完整的2D建图TF树必须是这样:

map -> odom -> base_link -> laser

其中map -> odom是由Cartographer在建图过程中不断发布的,odom -> base_link由你的里程计节点发布,base_link -> laser通常由robot_state_publisher或static_transform_publisher发布。如果缺了base_link -> laser,Cartographer就拿不到雷达相对于机器人的姿态,扫描匹配自然算不出来。

很多人在URDF里定义了雷达坐标系,但忘了把laser直接挂在base_link下,而是挂在更深的子坐标系,也没写TF发布。Cartographer只关心tracking_frame和laser_frame之间的变换,并不会自动从URDF里推导。所以要在launch里加一个静态变换:

<node pkg="tf2_ros" type="static_transform_publisher" name="laser_broadcaster" args="0.0 0.0 0.15 0.0 0.0 0.0 base_link laser" />

另一个坑是时间戳。Cartographer要求雷达点云的时间戳与TF的发布时间在合理范围内。默认的transform_timeout只有0.3秒,如果你的TF频率很低,或者用rosbag回放时没有加--clock,就会报timed out。我一般会把transform_timeout调到0.5秒,同时在回放bag时加--clock参数,让roscore的仿真时间与bag内时间同步。

如果你用的雷达是多帧累积型,比如某些固态雷达会发布sensor_msgs/LaserScan,但每条消息时间跨度很大,Cartographer会出现比较奇怪的轨迹抖动。这时候可以看看num_subdivisions_per_laser_scan,默认是1,如果每条LaserScan消息里的点云跨越了不同时刻,TF时间会乱掉。这个参数在点云较稀疏时尤其关键,调成8到10能减少抖动。

2.2 Lua参数调整时最常踩的坑

Lua配置文件看起来很短,每一行参数都能影响结果。我踩得最深的坑,是直接把官方demo里的lua拿过来用,完全没看雷达量程。我雷达最远测距只有5米,lua里的max_range还保持在30米,结果就是建出来的地图边缘全是放射状线条,像太阳光芒一样往外发散,因为每次扫描匹配都产生了大量无效外点。

正确做法是先看雷达实际能测多远,把max_range设置成略小于有效量程,比如5米雷达就写4.5米。这样既避免无效点干扰,也能减少计算量。min_range同理,一般雷达都有近场盲区,特别容易受到超声干扰,设成0.05~0.1米比较合理。如果雷达紧贴底盘,min_range太大会把机器人本体壳子也算进地图,那就要调整安装位置或手动过滤。

num_accumulated_range_data决定累计几帧点云后再做一次匹配。雷达频率是20Hz时,累计10帧代表0.5秒一次匹配,对动态环境来说太慢了,地图会拖尾。我当时把雷达topic的频率确认后,把该值调到了4,实时效果明显改善。如果雷达频率只有10Hz,建议只在2到3之间选,否则CPU压力大,建图质量也会下降。

关于IMU,很多初学平台根本没有IMU,但lua里use_imu = true是默认值。如果你的雷达没有IMU,而你还开着这个选项,Cartographer启动时会一直等待IMU话题,导致建图完全停滞,或者进去以后地图不断旋转。解决方法是把lua改为use_imu = false,同时不要发布IMU话题。如果你有不太准的IMU,我一开始建议先不开,用纯雷达跑通了再加,这样更容易定位是哪部分引入的漂移。

还有voxel_filter_size。这个参数控制点云体素滤波的粒度,默认0.025米,对普通室内场景没问题。如果你的雷达噪声偏大,建议提到0.05米,减少计算量;如果是高精度雷达,0.015米能保留更多细节。体素越小,计算量越大,但地图细节多。调试时可以先从0.1起步,加快迭代速度,效果稳定后再逐步调小。

注意:修改lua后一定要重新启动节点才生效,Cartographer不会热加载参数。我最初改完lua,忘记重开节点,白调了好几分钟参数,还以为自己手气不好。

3. 在线建图与定位运行阶段的常见故障

编译、配置都通过,终于把包跑起来了,结果真实运行时还是会出幺蛾子。这一节主要说建图漂移和运行崩溃这两类最令人头大的问题。

3.1 建图漂移与地图错位的定位思路

建图最简单,也最烦人的现象是“走着走着地图歪了”。我有一次遇到的情况是:前半段墙体很平整,后半段门、走廊全部变成双线,地图上甚至出现几十厘米宽的“裂缝”。这种错位,基础原因是局部扫描匹配做得还行,但位姿轨迹的累计漂移没有被闭环检测及时纠正。

排查先从在线的TF下手。在建图过程中,map -> odom会不断更新,odom -> base_link由里程计节点发布。如果你在rviz中打开map、odom、base_link三个坐标系,并打开路径显示,会发现odom -> base_link那条路径一直稳定,但map -> odom发布有跳变,说明一旦闭环约束出现,整个轨迹会被重新拉回。如果map -> odom长时间不变,说明回环失效了。

回环失效的原因很多。第一个就是min_score设置过高。比如默认或者你调到0.65,但传感器噪声大,真正的回环匹配分数只有0.6以下,那么每次回环识别都会被拒绝,约束永远加不进去。我会把min_score降到0.5左右,再配合max_constraint_distance控制筛选范围。不要以为回环分数越高越安全,实际场景里高分数反而会让回环很难触发。

另一个原因是里程计权重过大。在很多差速底盘上,里程计可以提供很准确的短时运动估计,但长时间会累积漂移。如果lua里pose_graph.optimization_problem.local_slam_pose_rotation_weight给得太高,图优化就会过度信任局部slam的位姿,从而放过了累计误差。降低旋转权重和位移权重,让回环约束有更多话语权,地图一致性会显著变好。我常用的一组值是:rotation_weight = 0.2、translation_weight = 0.3,对大多数差速底盘都有效。

还有一个容易被忽略的点:IMU的消息是否包含足够的姿态角速度。Cartographer在2D模式下其实也会使用IMU的角速度积分约束。如果你的IMU发布频率太低,达不到100Hz,它在这一环节会引入大量“假约束”。用rostopic hz /imu看一下频率,不满足就换一个IMU节点,不要靠插值硬凑。

3.2 运行崩溃与资源占用过高的问题

运行中出现Segmentation fault是最难受的,大部分情况下不是Cartographer的公式算错,而是输入数据有问题。我遇到过两种情况:

第一种是雷达消息类型不匹配。Cartographer支持LaserScan和PointCloud2两种雷达数据。lua里配置了use_point_cloud = true表示用PointCloud2,但实际发布的话题类型还是LaserScan,或者反过来。这时节点在转换点云时会访问一个内存不确定的指针,直接段错误。解决办法很直接:rostopic type /laser_scan查看话题类型,确认和lua一致。

第二种是IMU数据时有时无。如果你的IMU节点没有发完整的数据,Cartographer在读取协方差矩阵时可能会崩溃。可以先用rostopic info /imu检查是否稳定发布,并在lua中先关闭IMU,如果能跑通,再慢慢加入IMU,逐步排查。我习惯在launch文件中用remap显式指定IMU话题,避免因为默认话题名不一致导致莫名其妙地读不到数据。

CPU占用过高是更多机器人上真实遇到的。Cartographer虽然是SLAM方案中性能较好的,但在低算力平台(比如树莓派或没有GPU的工控机)上,如果参数激进,CPU会直接打满,甚至进程被OOM kill。我的经验是先把voxel_filter_size调到0.1~0.15,这个参数影响很大,0.1时地图细节已经够用,CPU能下降30%以上。再把max_range限制在雷达实际量程内,能减少点云数量。最后把全局优化的频率调低,比如optimize_every_n_nodes从默认3调到8,图优化不是每帧都跑的,这样CPU压力会小很多。

如果内存不停上涨,多半是submaps维护的节点太多。可以把num_range_data(每个子图的扫描数)降低,子图更频繁结束,不活跃的子图会被发送给回环检测去清理。还有一个偏门技巧:关闭地图的可视化,不要在rviz中订阅地图话题太久,地图分辨率很大,rviz会把整张图重算,占用会飙升。我一般会先跑一段数据,保存pbstream之后再回放生成地图,而不是边建图边开着rviz干等。

4. 问题排查思路与调试工具汇总

当问题已经出现,用对工具排查能省下大把时间。最后我整理一下日常排查流程,这些方法帮我解决过很多看起来无解的问题。

4.1 读懂Cartographer的日志与内部状态

使用Cartographer时,roslaunch终端里的INFO/ERROR日志就是第一手证据。我知道很多人看到大量INFO就忽略,但里面藏着关键信息。

启动过程里正常情况下会打印Initializing submap with ...和Node: tracker...,这些不用管。

如果出现Failed to compute local pose,说明当前帧点云和地图匹配失败。这不是崩溃,但意味着这一帧没有位姿插入,一般会伴随后续几帧的丢帧。频繁出现这个错误,通常是因为地图太干净,或者雷达被遮挡,短暂失去特征。

如果在运行日志中看到Global constraint added,这是好事,说明回环检测成功,当前位置和之前某块地图被约束在一起。如果这一条从未出现,回环基本没生效,可以回头调min_score和max_constraint_distance。

Dropped sensor data出现得比较多,表示某个传感器数据因为时间戳不满足TF转换而被丢弃。要去检查对应话题频率,以及transform_timeout是否太短。我见过有人把transform_timeout设成0.1秒,结果建图时一路都在丢数据,地图质量惨不忍睹。

4.2 常用调试命令和工具链

这组命令我每次排查都会跑一遍,工具不在多,关键要会用。

  • rosrun tf view_frames:生成TF树PDF文件,看坐标系是否完整。运行后会在当前目录生成frames.pdf,用浏览器打开即可一目了然。
  • rqt_tf_tree:实时查看TF树,如果某个坐标系out of date,红色高亮会立刻告诉你。
  • rostopic hz /scan、rostopic hz /odom、rostopic hz /imu:查看传感器频率。雷达一般要10Hz以上,IMU最好100Hz,里程计至少20Hz。
  • rostopic echo /tf -p:带时间戳输出所有TF变换,检查坐标更新是否稳定。

排查时间同步问题时,还要看下use_sim_time。如果回放bag,必须roscore里设置use_sim_time true,否则TF时间戳无法对起来。我经常忘了这一点,导致明明有数据却一直提示Timed out。

对于地图保存,通常先调用write_state保存内部状态,再用工具解析为pgm/yaml,也可以直接在节点运行时用map_saver订阅地图话题生成。需要注意,带宽图和最终最优化图之间要区分开。如果地图有明显错位,不要急着保存,先让轨迹跑完,让最后的全局优化把所有子图修正后再保存。我现在的习惯是先跑完整个场地,确认没有大面积重影后,再统一保存地图。

4.3 常见问题速查表

现象可能原因排查步骤
启动后建图静止不动没有收到scan/odom检查话题频率和TF树
地图旋转或发散缺少IMU且use_imu为true关闭IMU选项,或补IMU
TF timeout时间戳不同步检查use_sim_time和transform_timeout
地图边缘出现放射线max_range大于雷达量程调整lua的max_range
帧率低、CPU高点云密度太大或优化频率过高调大voxel_filter_size,调低优化频率
内存缓慢上涨子图未合并降低num_range_data,检查全局优化
Segfault消息类型不匹配rostopic type核对话题类型

个人经验再说一句:Cartographer的Lua配置就像一个旋钮面板,但每个旋钮都对应着具体的物理含义,而不是玄学。拿到一台新机器人,先别急着调参数,花半小时确认传感器话题类型、量程、频率和TF结构,往往比盲调一整晚更有效。这是我绕了很多弯路才明白的事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进