兄弟们,如果你已经有一张建好的二维栅格地图,想让机器人在里面稳定跑导航,但每次重启都要冒着重新建图的风险,那直接用Cartographer的纯定位模式就是最划算的路子。
我最早接触纯定位,是因为一个室内巡检项目:场地里已经用Cartographer建好图,但机器人每次关机重启后,AMCL在长走廊上疯狂粒子发散,动不动就跳到一个错误位置,导航直接原地罚站。后来我干脆把整个定位前端换成Cartographer的pure localization,跑了一段时间,发现只要初值和外参给对了,稳定性确实比AMCL强不少。这篇就把我从Gazebo仿真一路折腾到真机部署的完整过程记录下来,重点讲踩过的坑、绕过的弯,以及每步背后的原因。
这文章适合谁看?只要你想把Cartographer从建图模式切到纯定位模式、在仿真里做验证、或者正在被“真机定位飘”折磨,都可以参考。整篇思路是:先讲纯定位和建图的本质区别,再拆解配置和原理,然后分别给仿真和真机的完整流程,最后是高频故障排查。
1. 先把纯定位这件事想明白
1.1 纯定位到底解决什么问题
纯定位,英文常写成pure localization,意思是不再往地图里添加新的子图(submap),而是只靠已有的地图数据,持续估计机器人在地图中的位姿。换句话说:建图是“画地图”,纯定位是“看地图找自己”。
我见过不少人把纯定位理解成“把建图的launch跑起来,不保存地图就行”,这是不对的。建图模式下位姿也会估计,但那个估计会同时反馈到地图构建里,地图本身还在演化。纯定位模式的地图是固定的,所有观测都拿来和固定地图做匹配,误差只用来修正位姿,不再污染地图。
这带来两个直接好处:第一,地图不会因为某次传感器异常而被动“改坏”;第二,定位的实时性要求更高,因为每一帧数据都要和全局地图对齐,而不是在局部子图上凑合。代价就是:一张准确的、已经完成闭环的地图是纯定位的前提。地图质量不好,定位再努力也是白搭。
1.2 为什么我选了Cartographer而不是AMCL
在这个方案之前,团队里有人提议用AMCL,毕竟ROS里一套navigation stack现成能跑。我也承认AMCL在小场景、房间特征明显的地方挺好用,但它有两个让我头疼的地方:
第一,AMCL的粒子滤波器在对称环境或者长走廊里,容易维持多峰分布,机器人在某个时刻有两个相距很远的候选位置,粒子不收敛,定位结果来回跳。第二,AMCL对里程计精度很敏感,轮子打滑、地面湿滑、地毯上跑久了,粒子云的方差会变得很大,最后重置也要手动。
Cartographer的纯定位走的是scan-to-map匹配加位姿图优化,它对里程计的依赖没有AMCL那么强,尤其在传感器外参正确、IMU参与的情况下,即便是短时间里程计误差较大,也能靠激光匹配拉回来。实测在同一个回字形走廊里,AMCL偶尔会“瞬移”,Cartographer纯定位一次都没出现这种跳变。
当然Cartographer也不是没有代价:它CPU占用比AMCL高,配置项多,初值给得离谱时也可能全局匹配失败。但这些坑都有解,下面慢慢讲。
2. 纯定位的模式切换与配置拆解
2.1 Cartographer里的“定位”是怎么执行的
要理解纯定位,你得先知道Cartographer建图时内部在做什么。建图时,它维护两类东西:一个是按时间累积的局部轨迹(trajectory),由一系列子图组成;另一个是全局的位姿图(pose graph),负责闭环检测和全局误差优化。
纯定位模式下,这条轨迹依然存在,但子图不再被新的扫描数据更新。每一帧激光扫描会做两件事:先是scan-to-submap匹配,把新扫描对齐到最近的一个子图上,获得一个初值;然后是全局的scan-to-map匹配,在整张地图上搜索更准确的位置。实际上Cartographer做全局定位时,用的是分支定界搜索(branch and bound)在整个地图范围里找最匹配的位姿,这也是它在被“绑架”后还能找回自己的原因。
所以纯定位的核心不是“不做建图”,而是“关闭地图更新,只做位姿跟踪和全局修正”。理解了这一点,配置也不会瞎调了。
2.2 从建图配置改成纯定位,关键改哪几处
纯定位的配置文件,很多人直接复用建图的lua。这样做能跑,但不是最优,而且容易在运行一段时间后出现莫名其妙的漂移。我通常在原有配置基础上做这几处调整:
-- 纯定位时建议改动 PURE_LOCALIZATION = true -- 只是示意,实际通过launch参数传入 -- 关闭子图更新,也就是不再新增子图 TRAJECTORY_BUILDER_2D.use_online_correlative_scan_matching = true TRAJECTORY_BUILDER_2D.submaps.num_range_data = 1 -- 纯定位时,pose graph里的闭环约束没必要频繁计算,可以降低频率 POSE_GRAPH.optimize_every_n_nodes = 10 POSE_GRAPH.global_sampling_ratio = 0.002先说use_online_correlative_scan_matching,这行默认建图是true,纯定位一般也保持true。它决定每帧扫描是否用相关性扫描匹配来给匹配提供一个更好的初值。纯定位时初值差一点就容易锁到错误位置,所以这个最好开。
再说submaps.num_range_data,我在建图时设成一个子图8~10帧数据,纯定位时把它调成1。原因是纯定位不需要积累很多帧才生成子图,子图更新本身被冻结,这个值影响的是局部匹配时的子图大小,1帧数据就当一个子图边界来用,可以降低延迟,也能减少局部漂移累积。
optimize_every_n_nodes在纯定位里可以适当调小,我用的10,意思是每10个节点跑一次全局优化。纯定位不需要频繁闭环,频率太高只浪费CPU。但要注意,如果你在白天的动态环境里跑,节点之间误差累积快,优化频率太低,定位会慢慢飘走。这个值没有标准答案,我一般先按10跑,观察日志里的优化频率和误差指标再调。
2.3 加载地图的两种方式,别用错
Cartographer纯定位加载地图,本质是加载建图时保存的pbstream文件。这个文件里包含子图数据、轨迹节点、约束关系。启动时,Cartographer需要先加载这些数据,然后开启一个新的轨迹来跟踪当前位置。在cartographer_ros里,常见两种方式:一种是launch文件里通过load_state_filename参数加载,另一种是启动后调用服务接口加载。我习惯用launch加载,因为可以保证坐标系在启动时就对齐。
还有不少人问:我已经有一张PGM/PND格式地图,能不能直接给Cartographer纯定位用?说实话,原生Cartographer不推荐从PGM转过来用,因为纯定位需要的是子图和约束信息,不是一张平面图片。如果你想用已有栅格地图,最省事的方案是:拿着这个栅格地图,重新走一遍Cartographer的定位初始化——但Cartographer并没有直接“导入栅格图像生成地图数据”的高级工具。我实际遇到的情况是:之前团队留下了OccupancyGrid格式的地图,但没留pbstream。最后我用这栅格图反推建图路径,重新跑了一次建图,把pbstream留下来。如果你也没有pbstream,建议直接重新建一次图,反正Cartographer建图也不慢,省得后面绕弯。
3. 仿真先行:在Gazebo里把流程跑通
3.1 环境准备:Gazebo地图和机器人模型
仿真阶段,我用的还是经典的TurtleBot3。买不起真机的时候,TurtleBot3的Gazebo模型最省心,雷达、IMU、里程计模型都现成,还能模拟噪声。建议先做两件事:
第一,在Gazebo里搭一个和你实际场地格局相似的环境,重点是放几面长墙和几个柱子。长墙用来考验长走廊情况下的定位稳定性,柱子制造遮挡,能暴露匹配失败的问题。第二,先跑一轮Cartographer建图,得到一个pbstream。这一步务必等到建图的submap收敛了再保存,怎么判断收敛?看Rviz里机器人轨迹和地图边缘是否紧密贴合,闭合回环后地图无重影。
仿真阶段我用的是Ubuntu 20.04 + ROS Noetic + Cartographer的源码编译版本。不要用二进制安装的旧版cartographer_ros,很多纯定位相关的改动和bug修复都在源码版里,二进制版本太久容易踩旧坑。
3.2 纯定位launch的启动顺序和配置
仿真里启动纯定位,我会分成三个终端依次操作:
第一个终端启动仿真环境:
roslaunch turtlebot3_gazebo turtlebot3_world.launch第二个终端启动纯定位节点,关键是launch文件里要包含加载地图文件:
<launch> <param name="/use_sim_time" value="true"/> <node name="cartographer_node" pkg="cartographer_ros" type="cartographer_node" output="screen"> <remap from="scan" to="/scan"/> <remap from="odom" to="/odom"/> <param name="configuration_directory" value="$(find my_cartographer)/config"/> <param name="configuration_basename" value="localization.lua"/> <param name="load_state_filename" value="$(find my_cartographer)/maps/map.pbstream"/> </node> <node name="cartographer_occupancy_grid_node" pkg="cartographer_ros" type="cartographer_occupancy_grid_node" output="screen"/> </launch>注意这里有个细节:load_state_filename给的是pbstream路径,而没有给初始位姿。很多第一次跑的人到这里会很疑惑:加载地图后Cartographer怎么知道我在地图哪里?答案是:它不知道,所以必须给一个初始位姿。
初始位姿可以用三种方式给:launch里通过initial_pose参数给、启动后用Rviz的“2D Pose Estimate”按钮点一下、或者调用SetInitialPose服务。在仿真里我强烈建议用launch参数直接给,比如:
<param name="initial_pose" value="1.0 1.0 0.0" />这三个值分别是x、y、yaw。坐标系是map系。给初始位姿的本质是告诉Cartographer“你加载的地图里,机器人大概在这个地方,面朝这个方向”。如果给得和真实位置相差太远(比如超过一两米),全局匹配很有可能失败,定位直接飘到天上去。这也是纯定位第一个大坑。
启动完成后,第三个终端做验证:
rosrun teleop_twist_keyboard teleop_twist_keyboard.py控制机器人走一圈,观察Rviz里机器人在地图上的位置是否平稳贴合。
3.3 用“绑架测试”检验定位是否真的稳
仿真阶段我强烈建议做一次“绑架测试”,也就是把机器人强行搬到地图的另一个位置,看Cartographer能不能在几秒内重新定位回来。这个测试能直接检验全局匹配是否可靠。
TurtleBot3在Gazebo里怎么“搬”呢?直接改模型位置再重启仿真不行,那样地图也没了。我用的方法是:暂停Gazebo,手动修改机器人模型在world里的初始坐标,同时删除map→odom的坐标变换发布关系,模拟位姿突变。当然更省事的办法是,启动后用下面这个命令直接把TF树里的map→odom变换改掉:
rosrun tf2_ros static_transform_publisher 5.0 6.0 0 0 0 0 map odom这个操作会强迫Cartographer认为当前里程计起点在地图位置(5,6)处,等于把机器人瞬间搬走了。如果配置没问题,几秒内Rviz里的机器人应该被全局匹配拉回到正确位置附近,而不是继续在错误位置瞎跑。如果拉不回来,或者拉回来后又跳走,基本可以确定全局匹配参数有问题。在仿真里把绑架测试跑通,至少能筛掉一半真机上的坑。
3.4 这里我踩过的一个仿真大坑
仿真里最容易让人误判的一个问题,是用了use_sim_time true但时间戳对不齐。Cartographer对时间戳的敏感度很高,如果激光雷达发布的时间戳和IMU、里程计的时间戳差异超过几百毫秒,即便仿真里传感器模型没噪声,定位也可能缓慢漂移。
我遇到过的情况是:Gazebo里两个传感器的话题时间戳差了几百毫秒,Rviz看着没问题,但Cartographer日志里疯狂报“Timestamp of sensor data earlier than last timestamp”,定位看起来在动,实际位置就是不对齐。排查方式是看rostopic hz和rostopic delay,把各话题的实际时间戳打出来对比。如果发现时间差,先检查节点里是否有缓存导致的延迟,或者直接重启仿真环境。
4. 真机部署:从Gazebo搬到物理世界的差异
4.1 传感器时间同步和外参标定,这两关不过就是白给
仿真里传感器时间戳和精神都完美,真机就不一样了。我上真机遇到最常见的问题就是雷达和IMU时间同步。Cartographer内部对传感器数据有队列和时间戳排序,如果激光和IMU来自不同驱动节点,时间基准不一样,数据流的先后就乱了。
所以真机部署第一步,不是调参数,而是把时间同步做好。最稳的做法是:先用time_reference参数指定一个参考时间源,一般以雷达或主控时钟为基准。然后在驱动节点里把所有传感器的时间戳都统一回这个参考源。简单粗暴的办法是统一用机器人的主控时钟,所有传感器驱动发布消息时直接取当前系统时间。但要注意,如果传感器自带时间戳寄存器,驱动里要确认是软件时间戳还是设备时间戳,两者不一致就会出问题。
外参标定更是重灾区。Cartographer里激光雷达往往安装在和base_link有一定偏移的地方,IMU安装也会有角度偏差。如果你的雷达装在底盘前方10厘米、高度30厘米处,不在base_link原点,而lua配置里tracking_frame和base_link的TF没有正确发布,那么激光点云在“看到墙壁”时会产生固定偏差,定位会跑出一条稳定的弯曲线。
我建议用tf2_ros静态变换发布外参,并且一定要实测验证:把一个已知物体放到激光正前方1米处,看Rviz里点云位置是否真的在1米。如果差几厘米,就是外参不准确,别急着调Cartographer参数,先把外参调对了再说。
4.2 真机上的IMU和轮式里程计,谁优先谁次要
Cartographer纯定位能不能只靠激光和里程计跑?能,但很不稳。原因在于纯定位的全局匹配通常需要有效的初始猜测,而这个猜测主要来自里程计和IMU积分。轮式里程计在平坦地面还行,一旦地面湿滑、过减速带、转弯打滑,里程计就给出错误预测,全局匹配可能直接跳到错误位置。
我上真机后,第一件事就是把IMU接入Cartographer。Cartographer的位姿估计器会融合IMU的角速度和加速度,显著增强对底盘姿态变化的跟踪。配置里主要涉及这几个字段:
TRAJECTORY_BUILDER_2D.use_imu_data = true TRAJECTORY_BUILDER_2D.imu_gravity_time_constant = 10.use_imu_data设为true后,Cartographer会期望收到IMU消息,而且会用它来修正重力对齐。真机IMU安装时注意:IMU的z轴应该尽量朝上,如果装反了,Cartographer会认为重力方向变了,纯定位必然失败。我见过有同事把IMU装在了电池下面没固定,车辆一加速IMU就震,定位直接被震飞。
没有IMU的话,激光雷达的旋转匹配也能凑合,但前提是旋转不能太剧烈。如果你只是做平面轮式机器人,且旋转速度不快,可以在IMU不可用的情况下临时禁用IMU数据跑一段时间,但别指望长期稳定。
4.3 启动真机时的初始位姿处理
真机启动流程和仿真最大的区别是:真机没法保证机器人在每次开机时都停在已知位置上,所以初始位姿的处理要更讲究。
我现在的标准流程是这样的:
- 机器人上电后,先手动遥控到一个地图里辨识度高的位置,比如某个拐角、柱子的旁边。
- 打开Rviz,加载地图显示,看机器人当前实际位置。
- 用Rviz的“2D Pose Estimate”按钮,手动点一下机器人在地图里的位置和朝向。
- 确认Rviz里激光点云和地图边缘基本贴合后,再切换到自动导航。
如果一开始给的初始位姿偏差比较大,Cartographer通常会在全局匹配里发起一次“全图搜索”,它自己也可能拉回来。但从稳定性角度和工程效率角度,我建议一开始就给准,不要赌它自己拉回来。还有一个容易忽略的点:启动纯定位节点时,如果地图加载需要几十秒,机器人在此时已经开始打了点云,这期间机器人可能被遥控动了,那么初值必须要等地图加载完成后再给,否则初值和实际位置对不上。
另外,从定位节点启动到完成初值设置之间,尽量不要让机器人来回移动。我遇到过启动后机器人自己原地转了一圈,结果初始位姿设到了几分钟后的位置,全局匹配直接失败。后来干脆在启动脚本里加了一个检查:等待定位节点发出“已加载地图并等待初始位姿”的状态再允许遥控。
5. 高频故障:定位飘、跳变、加载失败怎么排查
5.1 定位越跑越偏,最常见的三个根因
定位漂移是最让人头疼的,也是最常见的。我遇到的漂移,本质上逃不出三个原因:
第一个是外参不对。雷达安装偏移没有实测,IMU和底盘之间的旋转标定不准,导致每次观测都带一个固定偏差,漂移是一条完美的弧线。这个在仿真里容易被掩盖,真机上一跑远就原形毕露。
第二个是时间戳混乱。传感器的时间戳如果出现倒退或跳变,Cartographer会对数据重新排序,局部匹配会出现鬼影一样的结果,地图上能看到机器人在原地打转但点云和地图越来越不贴合。
第三个是地图本身质量不行。如果闭环节点误差很大,子图之间有重叠错位,纯定位拿这张地图去匹配,机器人在不同子图区域来回走的时候,定位结果会在子图交界处发生跳变。这种问题最无解,只能重新建图。
排查顺序我建议是:先看时间戳,再看外参,最后考虑地图。因为前两个是工程问题,好修,地图重建成本高。
5.2 启动时报错/加载失败,常见错误速查
下面这个表是我遇到的典型报错和处理办法。
| 报错或现象 | 可能原因 | 解决思路 |
|---|---|---|
| “Failed to load state” | pbstream路径错误或文件损坏 | 检查文件是否存在,文件大小是否正常 |
| “Could not match scan” | 初始位姿给得离谱,全局匹配失败 | 重新给一个靠近实际位置的初值 |
| “Timed out waiting for transform” | TF树不完整,map→odom或odom→base_link缺失 | 检查是否有TF发布节点,常见于cartographer_node没启动成功 |
| 定位节点CPU 100% | 全局匹配搜索范围过大或优化次数过多 | 调低global_sampling_ratio,增大optimize_every_n_nodes |
| 机器人原地转但地图不动 | IMU数据异常或TF里base_link和tracking_frame不一致 | 验证IMU的数据方向,确认坐标变换 |
| 定位偶尔跳到地图另一侧 | 对称环境或点云稀疏引起全局匹配歧义 | 用初始位姿限制搜索范围,或增加激光点云密度 |
凡是看到“transform timeout”这类报错,先不要怀疑Cartographer,把rosrun tf2_ros tf2_echo map odom和rosrun tf2_ros tf2_echo odom base_link打一遍,看TF是否正常、频率是否够。很多时候启动顺序不对,导致cartographer_node先启动但地图还没加载完,TF会等很久才发布,误报成超时。
5.3 定位过程中如何快速判断“该相信它”还是“该停机”
真机跑起来之后,眼睛不能只盯着Rviz里的机器人模型,还要看两个东西:一是激光点云和地图边缘的贴合度,二是Cartographer的节点优化误差。
点云和地图边缘贴合,指的是Rviz里显示的红色点云会不会像“拖影”一样偏离地图边界。如果偏离但误差方向一致,多半是外参或者时间戳问题;如果误差忽左忽右,多考虑匹配质量问题。这时候暂停导航,手动遥控一下,看点云能不能拉回来。能拉回来说明还有救,拉不回来就赶紧重置,别让它继续跑。
日志方面,Cartographer会输出每次优化后的残差信息。残差不算特别大,但如果你看到残差值持续增大,说明匹配越来越差,这时候再跑下去就会出问题。我一般会在真机上做一个看门狗脚本,不断检测最近N帧的匹配残差,超过阈值就发警告。这个阈值需要在仿真里先统计一个正常范围,真机调试时再根据实际的传感器噪声调整。
5.4 调试时好用的排查手段
除了Rviz和日志,我强烈推荐两个调试工具。
第一个是cartographer_pbstream命令行工具。它可以查看pbstream里包含的轨迹信息和节点数量,还能用来给已有地图做轨迹分割、去掉某个轨迹甚至合并轨迹。我在排查地图问题时经常用它确认保存的地图到底存了几个轨迹、每个轨迹的节点数多少,如果节点数太少,说明建图时数据不够,纯定位自然容易失败。
第二个是ROS里的dynamic_reconfigure。Cartographer的很多运行时参数可以通过rosrun rqt_reconfigure rqt_reconfigure动态调整。比如你发现global_sampling_ratio太高导致CPU吃紧,可以运行中调低,不用重启节点。不过要小心:纯定位运行中动态调参,不要一次改动太大,否则匹配会突然崩掉。我习惯一次改一个参数,观察几分钟确认稳定了再改下一个。
写在最后
Cartographer纯定位这套方案,我跑仿真用了大概两周,真机调试又花了将近一个月。回头想想,真正能让你少熬夜的其实就那么几件事:先保证地图质量,再校准时序和外参,最后才谈参数调优。顺序反了,你会发现自己一遍遍调参,问题却总是原地踏步。
最后再分享一个小技巧:如果你需要频繁部署到多台机器人,建议把初始化位姿做成一个独立服务,启动时从调度系统下发坐标。这样既能保证初值可重复,也方便运维统一管控。纯定位这条路能走通之后,你会觉得比反复建图省心太多了。