☰
Cartographer激光SLAM入门:原理、架构与工程实践
2026/10/3 13:12:48 网站建设 项目流程

如果你最近在折腾移动机器人、扫地机、园区配送车这类带实体移动的东西,大概率绕不开 Cartographer 这个名字。它是 Google 在 2016 年开源的激光 SLAM 库,主打 2D/3D 实时建图与定位,这几年几乎成了工业级激光 SLAM 的默认选项之一。这个系列我打算从头到尾拆一遍:第一篇先把 Cartographer 是什么、为什么选它、整体架构长什么样讲清楚,让还没上手的人有一个完整的地图认知;后面几篇再钻代码、调参数、记录实战踩坑。

如果你是有 SLAM 基础但一直没系统读过 Cartographer 源码的朋友,或者刚接触机器人导航、想给自己小车加一个靠谱建图方案的新手,这篇都值得好好看看。我不打算给你堆术语,尽量把每个概念还原到“为什么这么设计”的层面上。

1. Cartographer到底是什么

1.1 起源与定位

Cartographer 是 Google 在 2016 年 10 月开源的实时 SLAM 库,对应的论文是《Real-Time Loop Closure in 2D LIDAR SLAM》,当年发在 ICRA 上。开源之后很快在 ROS 社区火起来,因为它解决了当时很多开源 SLAM 方案的真实痛点:长时间建图累积漂移、回环检测不稳定、对传感器要求苛刻。

严格说,Cartographer 不是单纯“画地图”的工具,而是一整套轨迹估计与建图系统。它把激光数据、IMU、里程计、甚至顶帽标记点(Landmark)都融合进来,一边估计机器人轨迹,一边构建栅格地图,并且在检测到机器人回到曾经去过的地方时,会主动“回头修正”历史和轨迹。这种闭环能力是它区别于 Gmapping、Hector 这类轻量方案的核心标志。

很多人第一次听到“SLAM”会以为是个黑盒子:激光进去,地图出来。Cartographer 给我的感觉更像是一条生产流水线,前端负责把每一帧激光精确“贴”到当前局部地图上,后端负责定期检查“整条轨迹有没有闭环错误”,发现问题就全局优化一遍。两个环节互相配合,一个管局部准,一个管全局稳。

1.2 它和“建图工具”的区别

在 ROS 生态里,常见的建图工具不少,但 Cartographer 的设计定位有两个明显不同。

第一个不同:它对传感器融合的态度是“最好全都要”。Cartographer 不会因为你是单线激光雷达就拒绝工作,但如果你想让它长时间稳定跑,它更希望拿到 IMU 的角速度和线加速度,拿到轮式里程计或视觉里程计,甚至拿到路标点数据。多一份约束,轨迹的漂移就少一点。你可以把它理解为“一个非常愿意吸收各种信息来降低不确定性的系统”。

第二个不同:地图不是一次性输出的结果,而是伴随轨迹持续维护的状态。Cartographer 内部有很多被称为 Submap(子图)的局部地图,轨迹则由一连串位姿构成。回环检测发生时,它修正的不是当前帧,而是把整串位姿和相关子图做一次大调整。这也是为什么它在回环场景下表现明显强于普通 scan-to-scan 匹配方案。如果一个机器人只走直线、从不回头,任何 SLAM 都会表现不错;一旦绕圈、回到原点附近,才是真正见功底的地方。

2. 为什么选择Cartographer

2.1 与主流开源方案横向对比

每次聊 SLAM,免不了拿经典方案做对比。我直接给一张表,把几个常见 2D 激光 SLAM 方案放在一起,方便你按场景挑。

方案核心思想优点主要痛点
GmappingRBPF粒子滤波轻量、适合小场景、对激光频率要求不算高依赖里程计,大场景粒子数爆炸,回环能力弱
HectorSLAMscan-to-map梯度匹配不需要里程计,实现简洁对激光帧率和噪声敏感,无回环,打滑场景容易废
Karto图优化SLAM有后端,能做闭环早期工程整合成本高,社区维护不够活跃
Cartographer子图+图优化+分支定界回环回环强、支持多传感器融合、2D/3D统一配置复杂,CPU占用偏高,纯激光场景也需细心调参

注意,这张表不是说其他方案一无是处。如果你的机器人只在几十平房间里低速跑、环境又比较空旷,Gmapping 完全可以胜任;如果传感器很差、没有轮式里程计,可能 Hector 反而更容易跑起来。但如果你需要长时间、大范围、反复往返的工作场景,比如商场清洁机器人、仓储盘点机器人、园区巡检车,Cartographer 的抗漂移能力就是实打实的优势。

2.2 从工程角度谈选型

除了算法能力,工程因素也很重要。Cartographer 背后是 Google 维护的开源项目,代码风格统一,文档、issue 讨论活跃,Ceres Solver、Abseil、glog 等依赖也都是成熟库。相比某些只有论文、没有可用代码的方案,它更接近“可以拿去落地”的工业级项目。

选型时你还要想清楚一个问题:团队里后续有没有人能读懂、改得动代码。Cartographer 的代码起初对新手不太友好,抽象层级多,模板多,但要改回环搜索策略、匹配核函数、栅格更新方式,它的扩展点其实非常清晰。这一点我在后面系列文章里会重点拆。假如你们只是临时要一张地图,那确实没必要啃 Cartographer 源码;但如果是做产品,长期要维护地图质量、定位稳定性,那就值得在它上面投入时间去学透。

纯激光小车的场景要特别提醒:Cartographer 虽然融合能力很强,但对纯 2D 激光、没有 IMU、没有里程计的小车并不是零成本跑起来。你需要给它补充运动预测信息,或者通过参数调整放宽匹配搜索范围,否则很容易出现“地图莫名扭曲”的状况。这个我放在第四节详细说。

3. 核心原理初探:不懂代码也能理解的设计

3.1 两段式架构:局部SLAM与全局SLAM

Cartographer 整体可以分为两个大的模块:局部 SLAM(Local SLAM)和全局 SLAM(Global SLAM)。你可以把它们想象成两个配合工作的员工:一个人手里拿着草稿纸,负责把最新看到的激光点以最快速度画到当前这页草图上,要求手稳、速度快;另一个人拿着整本笔记,定期检查前面的草稿页之间是不是拼错了,一旦发现某页应该和另一页相邻,就回头把所有相关页重新排列。

局部 SLAM 的核心任务是维护“当前子图”。每来一帧激光,它先用当前估计位姿把激光点投影到子图坐标系下,再通过 scan-to-map 匹配对位姿做一次更精细的修正,最后把修正后的激光点插入子图,更新栅格概率。这个过程以帧为单位实时执行,频率高、延迟低,但只考虑局部一致性,时间久了必然有累积误差。

全局 SLAM 的核心任务是维护位姿图(Pose Graph)。节点就是不同时刻的机器人在某个坐标系下的位姿,边代表这些位姿之间的约束。约束来自两类:一类是相邻帧之间“短时间内的位姿关系”,另一类是回环检测发现“并不相邻、但从数据结构上看应当重合”的长程关系。全局 SLAM 在收到新的约束后,会运行一次基于 Ceres Solver 的图优化,把所有节点和边的误差最小化,得到一版全局更一致的轨迹。

3.2 Submap和“打草稿再定稿”的巧妙之处

Submap(子图)是理解 Cartographer 的一把钥匙。它不是一整张大地图,而是一段局部地图。默认情况下,Cartographer 会攒一定数量的激光帧就生成一个新的子图,比如 2D 模式里常见的是几十帧一个。子图可以理解成“被打草稿的半成品地图”,新来的激光帧会先贴到最新子图上,子图累积到足够帧数后就冻结,变成不可变的地图块。

为什么不能直接把所有帧都往一整张大图上贴?因为存在累积误差。如果把所有帧都强制放到同一个全局栅格上,前面几帧效果还好,后面误差会越来越大,地图越来越糊。分成子图以后,局部 SLAM 只需要保证“当前帧贴到当前子图”是准确的;至于子图和子图之间是否“拼得严丝合缝”,交给全局 SLAM 去检查。这样一来,局部匹配的压力小,全局修正的范围又可控,整个系统实时性和准确性都能兼顾。

用拼拼图来类比:先按时间顺序拼出很多小块(子图),每块内部的拼图是紧贴的;然后退后一步,看哪几块其实挨着、哪几块拼反了,再统一调整所有小块的相对位置。Cartographer 的回环检测就是那个“退后一步看全貌”的动作。

3.3 回环检测:怎么知道机器人回到了原来的地方

Gmapping 这类方案很少做回环,因为它的地图表示和粒子滤波结构很难高效回溯修正。Cartographer 则把回环检测当成头等大事。

回环检测的核心问题:机器人走到某个位置,它怎么知道“这个场景我以前来过”?Cartographer 的做法是把当前激光扫描与附近所有已经冻结的子图做一一匹配,寻找一个分数最高的匹配结果。如果分数超过设定阈值,并且这个匹配对应的轨迹位置与当前位姿足够远(说明确实走出了一个大圈),就认为这是一个回环约束,把它加入位姿图。

匹配过程不是暴力搜索所有位置,因为那样太慢。Cartographer 论文里的一个亮点是使用分支定界(Branch and Bound)搜索策略。简单理解:先在一个很粗的搜索网格上做低精度匹配,快速剔除大量明显不行的候选位置,再在剩余的高分区域逐步细化,找到全局最优或接近全局最优的匹配。这样既保证搜索范围足够大,又控制计算量。

这条设计直接影响建图质量。实际测试中,一个 1000 平米以上的区域,纯靠前端的局部匹配走下来,轨迹误差经常会到几十厘米甚至几米;但触发几个靠谱的回环约束再做全局优化,误差能压回厘米级别。这就是 Cartographer 的“杀招”。

4. 传感器输入、TF树与数据流水线

4.1 前端需要哪些传感器

Cartographer 在纯 2D 模式下最少只需要 2D 激光扫描,但这不代表它喜欢这么干。从稳定性角度看,下面几种传感器各有分工:

  • 激光雷达:核心传感器,提供点到环境的距离观测。Cartographer 对激光数据会做体素滤波,把同一区域重复的点压掉,控制计算量。
  • IMU:提供高频姿态与加速度约束。特别在机器人快速旋转的时候,IMU 能告诉前端“这半秒内机器人转了多少度”,避免 scan-to-submap 匹配因为初值太差不收敛。
  • 里程计(Odometry):提供短时间内的位移估计,给 scan matcher 一个靠谱的初始位姿。轮式里程计、全向底盘编码器、视觉里程计都行。
  • Landmark(路标点):可选输入,比如顶帽、二维码、UWB 锚点。它们能提供绝对位置约束,对长时间建图尤其有用。

这段信息很重要:Cartographer 不是“越多传感器越好”,而是“约束越稳越好”。如果你给它的里程计本身打滑严重,就得适当降低里程计在初始估计里的权重;如果 IMU 安装角度没配好,反而会把姿态带偏。

4.2 TF树:map、odom、base_link三个世界

接触 Cartographer 之后,你一定会遇到一张 TF 树,核心是 map、odom、base_link 三个坐标系。

  • map:地图坐标系,全局回环修正之后的地图锚定在这里,是“真实世界”的参考系。
  • odom:里程计坐标系,由轮式里程计或惯导积分出来的轨迹位于这里,短时间局部可用,但长时间会漂移。
  • base_link:机器人本体坐标系,激光、IMU 都挂在它下面。

Cartographer 的工作可以描述成:前端主要在 odom 坐标系附近干活,用它和激光匹配保证局部轨迹平滑;后端一旦做出回环修正,就调整 map 到 odom 的变换,让整条轨迹在地图坐标系下重新对齐。所以你在 RViz 里如果看到地图偶尔“跳一下”,完全不用慌,大多数时候是后端收到回环约束后在做全局修正。

很多新手在这块犯迷糊:明明是激光雷达数据,为什么 TF 树不对就跑不出地图?因为 Cartographer 需要知道你每个传感器的空间位置和外参,坐标变换错了,数据融合全歪。我遇到过因为激光安装高度填错,地图建出来像被“压扁”的案例,查了半天最后发现是 TF 的问题。

4.3 激光数据如何一步步变成地图

拿 2D 场景举例,一次完整的数据流水线大概是这样的:

  1. 激光雷达发布原始点云或 LaserScan,经过自适应体素滤波,减少密集区域点数。
  2. 同一时刻收到 IMU 数据和里程计数据,通过外参把它们转换到 base_link 坐标系。
  3. 前端用上一个时刻的位姿加上 IMU/里程计预测,得到当前帧的初始估计。
  4. 把当前激光扫描与最新子图做 scan-to-map 匹配,优化位姿。
  5. 优化后的位姿作为轨迹节点,同时把激光点插入子图,更新栅格概率。
  6. 当子图达到指定帧数,冻结子图,开启下一个子图。
  7. 后端定期做回环搜索,发现闭环则加入约束,并触发全局优化。
  8. 最终通过 cartographer_ros 中的占据栅格转换逻辑,把子图转为 ROS 的 OccupancyGrid 发布出去。

可以看到,每一步都有明确目标。前端两步保证“准”,后端一步保证“稳”,栅格输出保证“能用”。理解这条流水线之后,你再看 Cartographer 的源码,就能对着文件找自己的位置了。

5. 环境搭建与快速跑通第一个Demo

5.1 安装方式:建议源码编译

Cartographer 最简单的安装方式是 apt 直接装ros-noetic-cartographer和ros-noetic-cartographer-ros,对于只想调包的用户确实省事。但我个人推荐源码编译,原因很简单:后面几篇要解析代码,你总得能随时改源码、加日志、打断点。而且源码编译能让你顺便摸清 Ceres、Abseil 这些依赖在系统里是怎么组织起来的。

源码编译基本步骤如下:

# 创建工作空间 mkdir -p ~/carto_ws/src cd ~/carto_ws/src git clone https://github.com/cartographer-project/cartographer_ros.git git clone https://github.com/cartographer-project/cartographer.git # 使用wstool合并rosdep依赖 sudo apt update rosdep update rosdep install --from-paths src --ignore-src -r -y # 安装protobuf、suitesparse等系统依赖 sudo apt install -y python3-wstool python3-rosdep ninja-build libceres-dev libsuitesparse-dev # 编译 cd ~/carto_ws catkin_make_isolated --use-ninja source devel_isolated/setup.bash

编译过程最常遇到的问题有两个:protobuf版本冲突,以及Ceres Solver版本过旧。建议你把系统里的 protobuf 版本与 Cartographer 要求的版本对一下,必要时单独编译一套libprotobuf和protoc,放到独立前缀下,避免污染系统环境。

5.2 跑通离线建图Demo

安装完成后,最快的体验方式是跑官方提供的离线 bag 包。Cartographer 官方仓库里放了几个示例数据包,比如b2-2016-04-05-14-44-52.bag,是背包式设备在室内环境中采集的真实数据。下载完 bag 后,执行:

roslaunch cartographer_ros demo_backpack_2d.launch \ bag_filename:=/your/path/to/b2-2016-04-05-14-44-52.bag

你会看到 RViz 窗口打开,机器人的轨迹和地图慢慢生长出来。值得注意的几个点:官方 demo 默认开了 3D 可视化,地图用的是/map话题;建图过程中轨迹会保持稳定,当 robot 在第几分钟内绕回起点附近时,你能明显观察到地图被“拉正”的过程,这就是回环闭合带来的全局修正效果。

第一次跑建议多做一件事:把 bag 播放速度调慢一点,例如rosbag play -r 0.5,观察后端优化发生时轨迹和地图的变化。这个体感比任何文档描述都直观。

5.3 认识第一份配置文件

跑通 demo 之后,你肯定会想:能不能调参数让我自己的机器人也跑起来?Cartographer 的参数配置集中在.lua文件里,2D 场景最常改的是这两个:

  • map_builder.lua:负责地图构建器的全局设置,比如是否启用 2D、3D,以及回环优化线程数。
  • trajectory_builder_2d.lua:负责 2D 轨迹构建器的详细参数,包括激光匹配、子图插入、传感器权重。

我挑几个新手绕不开的关键参数:

TRAJECTORY_BUILDER_2D.min_range = 0.3 TRAJECTORY_BUILDER_2D.max_range = 30. TRAJECTORY_BUILDER_2D.num_accumulated_range_data = 1 TRAJECTORY_BUILDER_2D.use_imu_data = true
  • min_range/max_range:过滤激光数据。雷达近处容易有飞点、遮挡,远处灰尘杂讯多,把它们设为合理值能显著提升匹配稳定性。比如室内你一般不需要看 30 米外,可以降下来省算力。
  • num_accumulated_range_data:把几帧激光累积起来再插入子图。值越大,单次插入的信息越密集,但实时性会变差,内存开销也高。
  • use_imu_data:如果你的设备真的没有 IMU,必须设为false,否则系统会因为等待 IMU 数据而不工作或崩溃。但一旦关了,建议给足里程计源,否则前端缺少预积分运动估计,纯靠 scan match 撑不久。

提示:改参数不是玄学,每次只改一个变量,对比前后地图差异,避免多个参数一起调导致无法定位问题来源。

6. 常见问题排查与实战心得

6.1 地图乱飘、轨迹发散怎么办

表现:地图越建越歪,或者机器人明明走直线,轨迹却画成圆弧。

排查优先级我一般这样排:第一,检查TF树是否完整,map、odom、base_link、laser、imu 都在发布吗?外参对吗?第二,确认use_imu_data与真实传感器是否一致,没 IMU 却填 true,可能直接崩;有 IMU 但安装方向反了,轨迹会离谱。第三,看子图冻结频率,如果num_accumulated_range_data设得过大,系统响应慢,容易掉帧导致匹配错位。

还有一个容易忽略的点:激光雷达本身的频率和角分辨率。Cartographer 对低帧率雷达(比如 5Hz 以下)会力不从心,因为两帧之间机器人可能已经转动很大角度,匹配初值很难猜准。如果雷达只能提供 10Hz,尽量配合较高频率的 IMU,否则就调大loop_closure相关搜索范围。

6.2 CPU占用偏高怎么优化

Cartographer 计算开销主要集中在 scan-to-submap 匹配、栅格概率更新和回环搜索上。项目里如果 CPU 不够,优先做三件事:

  • 打开自适应体素滤波器,把每一帧激光点数限制在合理范围。2D 场景几千个点足够,不需要几万点全上。
  • 调大num_accumulated_range_data,减少激光插入子图的频率。
  • 降低回环检测频率或者限制搜索半径,例如限制max_constraint_distance,不让它搜索离当前轨迹太远的位置,省掉大量无效匹配。

调试 CPU 占用时,不要纠结单个匹配有多快,先看整体帧率能不能保持实时。SLAM 是流式系统,局部 SLAM 必须跟上传感器输入频率,否则后台积压数据,实时性直接崩。

6.3 回环出现明显瞬移或者错误闭合

正常回环发生时,后端优化会让轨迹“跳”到正确位置,地图也会有可见变化,这不算 bug。但如果你发现地图从某个位置开始大面积错位、或者明明两个不重叠的位置被强制拼到了一起,通常是回环误匹配导致的。

遇到这种情况,优先调高匹配阈值min_score,降低误匹配概率;再检查分支定界搜索的栅格分辨率submaps.grid_2d.resolution,分辨率太低容易匹配粗粝,导致误闭环。此外,landmark和odometry权重过高也可能让位姿图里长程约束失真,需要综合调整。

这里给一条调试习惯:每次跑完建图,保存一份pbstream,然后用cartographer_pbstream做离线分析。官方提供的工具链能帮你查看轨迹和约束的分布,你会发现很多只靠 RViz 看不出细节的问题。

7. 从“会用”到“读懂”的学习路线

7.1 四个阶段建议

以大家熟悉的 ROS 生态来划分,我把学习 Cartographer 分成四个阶段:

  • 能用:能编译、能跑官方 demo、能换自己的 bag 包建出像样的地图。
  • 会调:掌握.lua参数与真实传感器的对应关系,遇到地图问题知道往哪个方向试。
  • 能读:能从节点入口找到数据流,理清局部 SLAM、全局 SLAM 的边界和交互。
  • 能改:能自定义回环搜索策略、换匹配核、接入新的传感器类型,或者移植到非 ROS 系统。

大多数网上找资料的人会卡在“会调”到“能读”之间,因为不读源码就只能猜参数,而读源码又不知道从哪儿下手。后面几篇我会直接带着大家过核心源码,订正这条跳板。

7.2 源码结构地图

先给个整体地图,方便你有空自己翻。

  • cartographer/common:时间、配置、数学工具、线程池等底层组件。
  • cartographer/sensor:激光点云、IMU、里程计、Landmark 等数据结构的定义与采集逻辑。
  • cartographer/mapping:栅格图、子图、位姿图、轨迹构建器的核心实现。
  • cartographer_ros:ROS 适配层,包括节点入口、话题订阅、TF 发布、栅格地图输出。

个人建议先读cartographer_ros/node.cc,它是整个系统的入口,话题回调都在这里注册;接着读cartographer/mapping/internal/2d/pose_graph_2d.cc,这里会看到位姿图维护的精髓。读完这两个文件,你对 Cartographer 的理解会直接从“用了半年”跳升到“真正懂了一点”。

第一篇先到这儿。当年我第一次编译 Cartographer 的时候也被各种依赖折腾到怀疑人生,但跑通 demo 并亲眼看到回环把偏掉的地图拉回来的瞬间,确实觉得前面所有折腾都值了。这套系列后续会慢慢展开局部匹配、回环搜索、参数调优和代码解析,建议你先把环境搭好、demo 跑通,后面几篇就能边看边练。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询