干机器人SLAM这一行,建图和定位从来都是两件事。很多团队用Cartographer把地图建出来了,结果到了正式上线的环节,反而不会用了:要么让机器人在已知环境里重新跑一遍建图,要么干脆每次开机都重新建一次,浪费时间还容易在重复区域里漂。实际上Cartographer早就内置了纯定位模式,尤其对基于3D地图的纯定位,它能直接加载之前保存的.pbstream地图,在冻结地图的前提下只做实时点云到地图的匹配,稳定输出机器人当前位姿。这篇内容就围绕cartographer基于3d地图的纯定位模式,从原理、地图准备、配置、实操到排查,按我做项目时的思路完整讲一遍。适合正在搞自主导航、巡检机器人、AGV和无人车的朋友,特别是准备从2D定位往3D方向升级的工程师。
1. 项目核心拆解:纯定位模式到底在做什么
1.1 建图与定位是两码事,别再混着干
我见过不少项目组,算法工程师张口就是“我们用的Cartographer”,但细问之下,要么只跑了建图demo,要么在建图模式下硬跑定位。这两种状态的本质区别很多人没想清楚:建图是边探索边构建全局一致的submap集合,需要持续维护图优化、回环闭合、累积误差修正,计算量大、实时性压力也大;而纯定位模式是“地图已经存在,且默认为真”,系统不再新建submap,也不做回环,只做一件事——把当前传感器采集到的3D点云和已有地图做匹配,迭代求解出机器人相对地图坐标系的位姿。
纯定位模式下,Cartographer的Local SLAM前端依然在跑,包括点云预处理、自适应体素滤波、实时相关扫描匹配、Ceres位姿优化等;但Global SLAM后端的图优化被大幅限制,不再对新生成的节点做大规模全局调整。这意味着定位的计算开销远低于建图,可以在车载主控甚至嵌入式计算平台上跑,同时定位结果相对于已有地图是稳定的,不会因为后端优化把地图坐标体系改掉。
1.2 3D纯定位的完整数据链路
我们看一个典型的3D纯定位环节,传感器假设是Velodyne VLP-16这类3D激光雷达,加上IMU和轮式里程计。链路大致是:
- 3D激光雷达原始点云进入节点,先做时间同步,把多帧点云累积成一次range data。这个累积由
num_accumulated_range_data控制,设成1意味着每帧原始点云单独处理,设成更大的值则可以降噪、补密。 - 对累积后的点云进行自适应体素滤波,降低点云密度,同时保留环境几何特征。这个过程很关键,3D雷达一帧几十万甚至上百万点,不降采样直接送入匹配器,计算量会非常夸张。
- 以IMU和里程计的输出作为预测初值,先跑实时相关扫描匹配(Real-Time Correlative Scan Matching,RTCSM),在局部窗口内给出一个粗略但可靠的位姿估计。
- 再把这个估计作为初值,送入Ceres扫描匹配器,对位姿做精修,让当前点云与已有地图的吻合度最大化。
- 最终输出的是一个从map坐标系到odom坐标系的变换,等价于定位系统对里程计累积误差的修正量;机器人自身在odom坐标系下的位姿由底盘里程计或IMU积分提供。
这条链路和建图模式最大的差异在于第4步之后:建图模式会把匹配好的位姿插入到PoseGraph中,参与后端优化,甚至触发回环检测;纯定位模式则只更新当前节点在已加载submap中的匹配结果,不再对已经存在的submap做增量修正。
1.3 为什么非要基于3D地图,2D地图不香吗
在开阔平地或简单仓库场景,2D激光定位确实够用,计算量小、调试也简单。但一旦环境变成多层厂房、坡道、码头、矿井、半室外园区,问题就来了:2D SLAM把激光扫描压缩到一个水平平面上,overlap不足时匹配退化严重。
我举一个实际踩过的例子:某园区巡检项目,路面有坡度,2D SLAM在建图时把坡道强行投影到平面,结果坡顶和坡底在2D地图上几乎重叠,机器人走到坡道中段时,2D定位结果开始在前后两个位置之间来回跳。后来换成Cartographer的3D建图+3D纯定位,坡道在高度维上自然区分开,点云匹配约束从2个自由度扩展到了6个自由度,定位稳定性大幅提升。
3D的另一个优势是特征丰富。3D点云里柱子、墙棱、地面、天花板、标识牌都成为约束,哪怕在空旷大厅这种2D定位容易“迷路”的地方,3D匹配也能借助地面、墙角等几何元素把位姿锁住。
当然,3D也不是没代价:传感器更贵、数据量更大、配置更复杂、IMU几乎是必需品。所以选型时要看场景,如果环境确实比较“平”,2D是更务实的选择;但只要环境在高度维上有信息,就别吝啬上3D,否则后面调定位会调到怀疑人生。
1.4 纯定位模式适合哪些落地场景
把概念理清楚之后,落地场景其实很清晰:
- 仓储AGV:高架库货架固定,地图基本不变,AGV开机后只需快速定位,就能立刻进入导航调度。
- 园区巡检机器人:路线固定但环境范围大,靠纯定位+全局路径规划,不需要每次重新建图。
- 无人配送车:固定园区、固定楼宇的配送路线,车辆跨天运行,纯定位模式保证每次开机都在同一坐标系下工作。
- 多机协同定位:一台机器建图,多台机器加载同一张地图做纯定位,避免每台机器都消耗建图算力。
这些场景的共同点就是“地图长期有效、环境相对稳定”。如果环境一直在变、布局经常调整,那就没有纯定位的生存空间,得走动态SLAM或者定期重绘地图的路线了。
2. 3D地图的准备:没有一张好地图,定位无从谈起
2.1 用Cartographer先建一张高质量的3D地图
纯定位模式的地图来源,基本都来自Cartographer自身的3D建图流程。前期建图质量直接决定后期定位的成败,这个环节值得花最多的时间。
建图流程一般分四步:配置传感器、标定外参、采集数据、离线或在线优化。
传感器配置上,3D建图一般需要3D激光雷达+IMU。Cartographer 3D模式对IMU依赖度很高,因为它需要借助IMU的重力向量来约束z轴的姿态,没有可靠的IMU,3D定位就是空中楼阁。外参标定方面,雷达与IMU、雷达与底盘之间的tum齐(坐标变换)必须精确到厘米级和度级,否则建出来的地图会有虚影,后边定位也会出现系统性的偏移。
采集数据时要注意轨迹设计:尽量让机器人走“回字形”或“8字形”路线,保证各个区域被多次覆盖,并制造足够的回环。回环是消除累积误差的关键,我见过太多建图翻车案例,都是因为轨迹太直、回环太少,导致地图首尾对不上。另外,建图时要清空动态物体,推车、人员、停放的车辆都会在地图里留下“鬼影”,定位时这些鬼影是灾难级别的干扰。
3D建图跑完后,如果用的是在线模式,可以先跑一次离线优化(cartographer_offline_node),对采集的数据反复优化,得到更平滑的轨迹和更一致的地图。离线优化对算力要求更高,但效果通常比在线建图好不少。
2.2 pbstream文件才是真正的“地图”
很多同学对3D地图的理解还停留在PNG栅格图或者PCD点云文件。但Cartographer的3D地图,核心载体是.pbstream文件,它保存的不仅仅是点云,而是完整的SLAM状态,包括:
- 所有submap的激光数据(range data)和位姿;
- PoseGraph中所有节点(pose node)的位姿;
- 约束关系(包括scan-to-submap约束和回环约束);
- 传感器元数据、轨迹信息等。
也就是说,pbstream文件里存的不只是“长得像地图的几何数据”,而是“可以让Cartographer重新恢复SLAM完整上下文的数据结构”。这个特性对纯定位模式至关重要,因为Cartographer在加载pbstream之后,不需要重新构建submap,而是直接把已有的submap加载进PoseGraph,机器人在这个“已经固定好的地图”里做匹配。
如果只是想要一个可视化的3D点云地图,可以直接用Cartographer的Python脚本把pbstream导出成xyz点云文件,或者用cartographer_ros的相关工具转成其他格式。但要注意,定位用的地图始终应该是原始的pbstream,而不是导出后的点云,因为一旦导出,submap之间的关联和位姿信息就丢了。
2.3 地图质量检查:点云密度、闭合差与鬼影
建完图先别急着上定位,先做三项检查:
第一,点云密度是否覆盖充分。把地图导成xyz后,用CloudCompare之类的工具打开,观察点云在墙面、柱体、地面处是否连续。如果某些区域点云稀疏甚至空洞,说明建图时该区域覆盖不足,要么补采,要么重新规划轨迹。
第二,闭合差是否在可接受范围。具体做法是看地图里同一面墙在不同轨迹下是否有双重墙、错位重影。3D建图常见的“口红效应”就是起点和终点没有完全对齐,导致整个地图呈微弧状,这在长走廊场景里特别明显。如果里面只有轻微重影,可以通过离线优化修正;如果严重错位,只能回去补采。
第三,鬼影静态化。建图期间任何移动的东西都会在地图里留下痕迹。定位的时候,这些鬼影会对扫描匹配造成错误约束。一个比较笨但很有效的办法是:在地图里人工检查一遍明显动态区域(比如门厅、过道),如果发现鬼影太多,重新建图比后期清洗更省事。
另外,对于可视化展示的需求,我习惯把导出的xyz点云通过Potree转成Web端可浏览的3D点云页面。这样不仅自己调试方便,给甲方做方案演示时也很直观。毕竟3D地图不像2D栅格图,直接在浏览器里就能旋转查看,这对“3d地区地图可视化”类的展示场景特别加分。
3. 纯定位模式的关键配置与坐标体系
3.1 坐标系与TF树:最容易翻车的地方
Cartographer纯定位模式对坐标系的要求是非常严格的,很多定位问题排查到最后,根因往往是TF树断链或者frame定义错误,而不是算法本身的问题。
标准的TF树应该是这样的:
map(地图坐标系):全局坐标系,也就是pbstream中submap所在的坐标系,固定不变。odom(里程计坐标系):由机器人底盘里程计或Cartographer自己发布的中间坐标,作为前端预测和局部位姿的参考。base_link(机器人本体坐标系):机器人底盘的载体坐标系。imu_link、激光雷达坐标系:通过URDF或TF static发布器固定在base_link下。
在纯定位模式下,map到odom的变换就是定位系统的最终输出,它表示“里程计的估计”和“在地图中的真实位姿”之间的差异。odom到base_link由底盘/里程计发布。base_link到各传感器则由静态TF发布。
有一个参数需要特别注意:provide_odom_frame。如果设置为true,Cartographer会自己发布odom坐标系;如果外部里程计已经提供了odom,建议设置为false,避免TF树冲突。
我在实际项目中一般让底盘里程计发布odom到base_link,Cartographer只负责map到odom,这样定位系统只是一个“修正器”,不干扰底盘自身的运动控制。
3.2 lua配置文件逐项解读
纯定位模式需要一份独立的lua配置,不能直接拿建图配置硬套。下面是一份基于3D传感器、适合VLP-16级别雷达的定位配置,逐项注释一下关键参数的含义,方便参考调参。
这份配置对应localization_3d.lua文件:
include "map_builder.lua" include "trajectory_builder.lua" options = { map_builder = MAP_BUILDER, trajectory_builder = TRAJECTORY_BUILDER, map_frame = "map", tracking_frame = "imu_link", published_frame = "base_link", odom_frame = "odom", provide_odom_frame = false, publish_frame_projected_to_2d = false, use_pose_extrapolator = true, use_odometry = true, num_point_clouds = 1, use_laser_scan = false, use_multi_echo_laser_scan = false, num_subdivisions_per_laser_scan = 1, } MAP_BUILDER.use_trajectory_builder_3d = true MAP_BUILDER.num_background_threads = 4 MAP_BUILDER.collate_landmarks = false TRAJECTORY_BUILDER_3D.num_accumulated_range_data = 1 TRAJECTORY_BUILDER_3D.min_range = 0.3 TRAJECTORY_BUILDER_3D.max_range = 50. TRAJECTORY_BUILDER_3D.voxel_filter_size = 0.05 TRAJECTORY_BUILDER_3D.use_online_correlative_scan_matching = true TRAJECTORY_BUILDER_3D.ceres_scan_matcher.translation_weight = 0.1 TRAJECTORY_BUILDER_3D.ceres_scan_matcher.rotation_weight = 1.0 TRAJECTORY_BUILDER_3D.imu_gravity_time_constant = 10. TRAJECTORY_BUILDER_3D.adaptive_resolution = 0.05挨个说几个重点:
map_frame、odom_frame、tracking_frame、published_frame这四个就是整个TF树的骨架。tracking_frame是传感器参考系,Cartographer会在这个坐标系下做位姿估计,一般用IMU所在的坐标系;published_frame是我们要对外输出位姿的坐标系,一般就是底盘坐标系。
use_odometry建议在底盘可靠时打开。Cartographer会把里程计作为位姿预测的输入之一,能明显提升动态场景下的鲁棒性;如果底盘里程计质量很差,比如轮胎打滑严重的场景,反而关掉,靠IMU和激光匹配。
num_accumulated_range_data:3D激光雷达一般设成1或2。设成更多的帧累积,相当于做了一次“时间积分”,可以把单帧点云稀疏的问题缓解,但会增加延迟,动态环境里请注意延迟过大带来的匹配滞后。
use_online_correlative_scan_matching:建议打开。它能在正式Ceres匹配之前快速给一个粗略初值,避免Ceres直接陷入局部最优。
ceres_scan_matcher.translation_weight和ceres_scan_matcher.rotation_weight:这两个参数决定优化时平移和旋转的权重比例。3D定位里旋转漂移往往比平移更致命,所以rotation_weight一般可以高于translation_weight,我常用0.1和1.0的组合。如果实际环境里平移误差大,再尝试把translation_weight调到0.2~0.3。
3.3 launch文件与话题remap
配置好lua之后,launch文件负责把实际传感器话题和Cartographer节点接起来。下面是一个比较标准的3D纯定位launch写法:
<launch> <param name="/use_sim_time" value="false" /> <node name="cartographer_node" pkg="cartographer_ros" type="cartographer_node" output="screen" args="-configuration_directory $(find cartographer_ros)/configuration_files -configuration_basename localization_3d.lua -load_state_filename $(find your_package)/maps/your_map.pbstream"> <remap from="points2" to="/velodyne_points" /> <remap from="imu" to="/imu/data" /> </node> <node name="cartographer_occupancy_grid_node" pkg="cartographer_ros" type="cartographer_occupancy_grid_node" output="screen"> <remap from="map" to="/localization_map" /> </node> </launch>注意-load_state_filename参数,它接收的就是我们提前写好的pbstream文件路径。每次更换地图,只需要改这个路径,不需要改动节点逻辑。points2和imu的remap要和机器人实际的话题名对应,否则Cartographer收不到数据,节点虽然起来但根本没有输入。
如果还想在rviz里看到栅格化后的定位地图,可以额外启动cartographer_occupancy_grid_node,不过要注意这只是用于可视化,纯定位的主力匹配数据始终是3D点云和pbstream中的submap。
4. 实操实录:从启动到验证纯定位
4.1 先决条件:保证底盘、雷达、IMU都在线
启动纯定位之前,我的习惯是先启动底盘驱动和传感器驱动,然后检查三个东西:能正常听到点云话题吗?IMU话题有数据且重力向量方向合理吗(静态时加速度计读数大概在z轴方向为9.8m/s²)?TF树发布正常吗?
这几个检查可以用简单的命令行完成:
roslaunch your_robot robot_base.launch roslaunch your_robot sensors.launch # 检查频率 rostopic hz /velodyne_points rostopic hz /imu/data # 检查TF树 rosrun tf view_frames4.2 定位前的“证据”:必须给初始位姿
这是3D纯定位最容易踩的坑,也是和建图模式最不一样的地方。建图模式下,Cartographer可以从原点开始逐渐构建地图;但纯定位模式下,系统加载地图后,它并不知道机器人此刻在地图的哪个位置。如果初始位姿给错了,后面所有匹配都是在错误区域找相似的几何结构,轻则定位收敛慢,重则直接发散。
所以,启动定位节点的第一件事,是在rviz里用2D Pose Estimate按钮,在地图对应位置给一个大概的初始位姿。不用非常精确,方向别差太多、位置别偏出几米就基本能收敛。给完初始位姿后,观察点云和地图的对齐程度,如果基本重合,说明初始估计是靠谱的;如果差得很远,需要重新给。
4.3 启动定位节点并观察收敛
初始位姿发布后,再启动定位节点,或者调换顺序也行——先启动节点,再立刻发布初始位姿。实际操作中,我一般先启动节点、确认话题有数据,再在rviz里给位姿,这样能避免节点一启动就带着错误初值乱匹配。
在rviz里添加map和map/PointCloud2显示,观察两个关键信号:
map到odom的TF变换是否在持续平滑变化。刚开始可能会有一小段跳变,那是系统在从初始位姿往精确位姿收敛;如果几秒后还在持续大幅跳动,说明匹配不稳定。- 实时点云和地图点云是否重叠。重叠越高,定位越准。经验上,收敛后两者偏差一般能在10cm以内(取决于雷达精度和环境特征)。
如果想量化定位结果,可以看Cartographer节点输出的位姿话题,比如/tf里的map->base_link变换,把它录成bag,后处理的时候和真值对比,评估误差。
4.4 长时间运行与重定位
机器人长时间运行,定位误差可能存在缓慢漂移,尤其是环境发生微小变化(比如货架挪了、门开了)。纯定位模式下Cartographer会持续做scan-to-map匹配,正常情况下能保持定位稳定。如果出现漂移增大的趋势,可以考虑定期用后端优化或人工重定位修正,但这是更进阶的话题。
遇到定位彻底丢失的情况,最快的恢复手段不是重启节点,而是在rviz里重新发布一个接近真实位置的初始位姿。Cartographer在收到新的初始位姿后会重新初始化局部SLAM,不需要重启进程。
5. 常见问题与排查经验:一次把坑踩平
5.1 定位结果跳变、漂移严重
跳变一般有三个来源:初始位姿给得太差、传感器时间没有对齐、环境特征退化。
初始位姿的问题前面说过,在rviz里校正到合理范围即可。时间对齐问题比较隐蔽,雷达点云和IMU数据如果时间戳不一致,扫描匹配时会引入系统误差,表现为定位结果周期性抖动。排查方法是录制bag,回放时检查点云话题和IMU话题的时间戳偏差,如果偏差超过几十毫秒,就需要在launch里做时间同步,或者检查驱动的时间戳发布逻辑。
环境特征退化指机器人走到长走廊、大面积平地这类自重复区域,匹配约束不足,定位误差会沿着某个方向膨胀。解决办法是尽可能让扫描匹配利用更多维度的信息,比如3D模式下保留地面的约束;如果依然退化,只能考虑在地图中增加特征锚点或者调整运行路线避免长距离直线穿越。
5.2 点云匹配完全不收敛
如果定位节点启动后,实时点云始终无法与地图对齐,多半是pbstream加载的地图坐标系和当前传感器坐标系不匹配。最常见的原因有:
- 建图时的传感器外参和当前运行时的外参不一致。比如换过雷达支架、重新标定过IMU,但launch里的TF没有同步更新。
tracking_frame设置错误。如果设的是base_link导致没有跟随传感器运动,匹配结果会一塌糊涂。- 建图使用的传感器配置和定位时的配置不同。比如建图时用了32线雷达,定位时换成了16线雷达,点云密度差异太大,匹配器需要重新调参。
遇到匹配不收敛,先别急着调算法参数,把外参、frame之类的基础问题查清楚,90%的“不收敛”其实是配置错误。
5.3 TF树错乱导致地图错位
TF问题太常见了。定位节点刚启动时,如果TF树不完整,rviz里会直接报找不到坐标变换,或者地图显示得歪七扭八。
排查步骤很固定:先用rosrun tf view_frames生成TF树图,确认map、odom、base_link、tracking_frame在一棵树上;再检查各frame之间的变换是否持续更新;最后检查是否有两个节点同时发布同一个frame的TF,这会导致跳变和错乱。
一个小技巧:关闭除底盘驱动之外所有可能发布TF的节点,只留Cartographer和底盘驱动,然后逐步加节点,定位问题基本能锁定。
5.4 动态环境误匹配
仓库里有人推车经过,定位结果瞬间被“拉走”,过一会儿才恢复。这是动态环境下的经典问题。Cartographer的纯定位模式本质上假设环境是静止的,动态物体会给扫描匹配引入异常约束。
实际项目里,我常用的减轻措施包括:提高min_range、调小max_range,避开雷达近处行人和远处大型车辆;开启并使用use_online_correlative_scan_matching增加匹配初值的鲁棒性;如果动态物体会长期存在(比如某个区域总有叉车作业),最好的办法是重新建图,把该区域在低频时段采集,或者后期在点云里手动剔除。
5.5 常见问题速查表
整理一个速查表,方便现场排查:
| 问题现象 | 可能原因 | 快速处理 |
|---|---|---|
| 定位完全不收敛 | pbstream未加载、初始位姿错误、TF断链 | 先确认-load_state_filename,再检查TF树,重新发布初始位姿 |
| 定位跳变 | 时间同步差、外参错、特征退化 | 检查时间戳偏差,复核外参 |
| 地图错位 | TF重复发布、frame不一致 | view_frames检查TF,清理重复发布 |
| 漂移增大 | 环境变化、动态物体干扰 | 提高匹配权重、重映射,必要时重定位 |
| 点云与地图分层 | 雷达安装高度/角度变化 | 重新标定传感器外参 |
6. 一点私人心得,关于调试习惯
最后再多说一句个人习惯。我在做Cartographer纯定位项目时,一定会保存两个版本的配置文件:一个是“保守版”,参数尽量稳定,适合长时间运行;另一个是“激进版”,匹配权重更激进、滤波更小,适合短时间快速收敛验证。调试的时候先用激进版把初始位姿调准,等定位稳定了再切回保守版,这样既能保证调试效率,又不影响正式运行稳定性。
另外,别把纯定位模式当作“建图模式的简单衍生品”。它和建图是完全不同的工程思路:建图允许犯错,反正后端能修;定位则必须战战兢兢,地图一错,后边全是错的。所以每次换场地、换传感器、换地图,我都会强制自己重新走一遍从地图质检到初始位姿验证的完整流程,宁可多花半小时,也不愿意在真机上碰运气。这套方法实测下来很稳,希望对你有用。