1. 从一堆散乱点云到一张能用的地图,中间到底发生了什么
扫地机器人能自己规划路线、绕开拖鞋、钻进沙发底下再自己出来,靠的不是什么黑魔法,而是背后一整套SLAM(同步定位与建图)加Nav2 导航的链路在支撑。我前后折腾过好几台基于 ROS2 的移动机器人,从最便宜的二维雷达小车到带三维激光的底盘,踩过的坑能写满一个笔记本。这篇就把“从点云到地图”这条链路完整拆一遍,讲清楚 SLAM 怎么把一帧帧点云拼成地图,Nav2 又怎么拿着这张地图把机器人从 A 点送到 B 点,中间 LiDAR、IMU、Gazebo 仿真、RViz2 可视化这些环节各自扮演什么角色。
如果你刚开始接触 ROS2,或者已经能跑通 TurtleBot3 的官方 demo 但一到自己搭车就懵,那这篇基本就是给你写的。我会尽量把每个参数为什么这么设、每个坐标系为什么这么连讲透,而不是甩一堆命令让你照抄。毕竟照抄能跑通一次,但换个雷达、换个底盘就又歇菜了,只有搞懂链路才敢接私活。
整条链路我习惯分成两大段来看:前端是 SLAM,负责“我在哪、周围长啥样”;后端是 Nav2,负责“我该怎么走过去”。中间靠一张地图、一套 TF 坐标变换、一堆话题和服务把它们缝在一起。下面按这个思路一层层剥。
2. 整体链路设计与方案选型:为什么是这套组合
2.1 先想清楚:二维 SLAM 还是三维 SLAM
很多人一上来就问“用哪个 SLAM 算法好”,其实这个问题问反了。应该先问:你的雷达是二维还是三维,你的场景是单层还是多层。
二维激光雷达(比如常见的 RPLIDAR、思岚系列)扫出来的是一个平面上的距离点,SLAM 出来的是一张二维栅格地图,每个格子标记为“占用/空闲/未知”。这种方案算力需求低,一张地图几 MB,Nav2 直接就能用,扫地机器人绝大多数走的就是这条路。
三维激光雷达(比如 16 线、32 线)扫出来的是空间点云,能感知高度信息,适合多层建筑、有坡道、有悬空障碍的场景。但三维 SLAM 出来的地图要么是点云地图(几 GB 起步),要么得投影成二维栅格或者八叉树地图才能给 Nav2 用。八叉树地图(OctoMap)就是把三维空间切成一个个小立方体,每个立方体记录被占用的概率,既能表达三维结构又能压缩体积,是三维导航里很常见的一环。
我的建议很直接:扫地机器人这种贴地跑的,老老实实二维 SLAM 加二维 Nav2,别给自己找麻烦。三维雷达留给无人机、机械臂、多层巡检机器人。热搜里那个“nav2导航使用3d雷达”确实能做,但通常是把三维点云压成二维代价地图再用,本质还是二维导航。
2.2 SLAM 算法怎么挑:Cartographer、SLAM Toolbox 还是别的
ROS2 生态里能直接用的二维 SLAM 主要有两个:Cartographer和SLAM Toolbox。
Cartographer 是谷歌开源的,回环检测强,建大场景地图不容易漂,但配置项多、调参门槛高,而且 ROS2 版本维护节奏不如以前活跃。SLAM Toolbox 是 ROS2 原生生态里更主流的选择,支持在线建图、离线建图、地图续建,和 Nav2 配合得也顺,配置文件相对好懂。
我现在的默认选择是SLAM Toolbox,理由有三个:一是它原生支持 ROS2 的生命周期节点管理,启停干净;二是它能把建好的地图直接序列化成.posegraph和.data文件,下次可以接着建,不用从头来;三是它的参数文档清楚,遇到问题好排查。Cartographer 我一般只在客户明确要求、或者场景特别大(几百米以上走廊)的时候才上。
2.3 仿真先行:为什么一定要在 Gazebo 里先跑通
真机调试 SLAM 有个致命问题:你分不清是算法错了还是硬件错了。雷达装歪了、IMU 没标定、轮子打滑、时间戳对不上,任何一个都能让建图飘得一塌糊涂,而你在真机上排查这些成本极高。
Gazebo 仿真的价值就在这:它给你一个理想但可配置的环境。雷达数据是干净的,轮子不打滑,时间戳是同步的,你可以先把算法链路跑通,确认 TF 树、话题、参数都没问题,再上真机。真机上再出问题,那基本就是硬件和标定的锅,排查范围一下子缩小了。
我一般的流程是:Gazebo 里跑通建图 → Gazebo 里跑通导航 → 导出地图 → 真机复现。热搜里那一堆“gazebo仿真环境模型”“gazebo 参数”“gazebo教程”说明大家卡在仿真环境搭建上的不少,后面我会专门讲。
2.4 整条链路的数据流
先把数据流画清楚,后面每个环节才好对号入座:
- LiDAR发布
/scan(二维)或/points(三维)话题,内容是距离点。 - IMU发布
/imu/data,提供角速度和加速度,帮 SLAM 判断转向。 - 里程计发布
/odom,来自轮子编码器,提供粗略位移。 - SLAM 节点订阅上面这些,输出
/map(栅格地图)和map→odom的 TF 变换。 - Nav2订阅
/map,结合/scan生成代价地图,规划路径,输出/cmd_vel控制轮子。 - RViz2订阅所有话题,把点云、地图、路径、机器人模型画出来给你看。
关键就在TF 树:map → odom → base_link → laser这条链必须完整且不断裂,断了 RViz2 里机器人就飘,Nav2 直接罢工。这是新手最容易翻车的地方,后面单独讲。
3. 核心细节解析:TF、标定与那些要命的参数
3.1 TF 坐标树:整条链路的骨架
TF 是 ROS 里描述“谁在谁的坐标系下、什么姿态”的机制。整条 SLAM 导航链路里,至少要有这几层:
| 坐标系 | 含义 | 谁发布 |
|---|---|---|
| map | 全局地图坐标系,原点固定 | SLAM 节点 |
| odom | 里程计坐标系,随机器人漂移 | 里程计节点 |
| base_link | 机器人本体中心 | 静态 TF 或 URDF |
| laser | 雷达安装位置 | 静态 TF |
| imu_link | IMU 安装位置 | 静态 TF |
map → odom由 SLAM 发布,odom → base_link由里程计发布,base_link → laser是固定的安装关系,用静态 TF 发布。为什么要有 odom 这一层?因为里程计短期准、长期漂,SLAM 短期可能跳、长期准,两者结合才能既平滑又不漂。SLAM 通过不断修正map → odom来抵消里程计的累积误差,这就是“定位”的本质。
新手最常见的错误是:雷达的静态 TF 没发,或者发错了方向。记住一个口诀:父坐标系在前,子坐标系在后。base_link是父,laser是子,因为雷达装在机器人身上,机器人动雷达跟着动。
3.2 LiDAR 与 IMU 标定:别跳过这一步
真机上,雷达和 IMU 的安装位置、朝向必须标定准,否则 SLAM 出来的地图会扭曲。标定分两部分:
外参标定:确定雷达、IMU 相对base_link的平移和旋转。平移好量,拿尺子量安装位置就行;旋转麻烦,因为雷达的 0 度朝向、IMU 的轴向不一定和机器人正前方一致。我一般用tf2的静态发布工具先填一个估计值,然后在 RViz2 里看激光扫描和地图是否对齐,不对就微调 yaw 角。
IMU 内参标定:主要是零偏和噪声。IMU 静止时会输出一个非零的角速度,这就是零偏,不扣掉的话 SLAM 会以为机器人在慢慢转。标定方法很简单:把机器人放平静止几分钟,采集数据算平均值,写进配置。噪声参数影响滤波权重,一般用厂商给的 datasheet 值起步。
提示:二维 SLAM 对 IMU 的依赖其实没那么强,很多纯激光 SLAM 不用 IMU 也能跑。但一旦场景里有长走廊、特征少,IMU 能显著提升鲁棒性。三维 SLAM 则基本离不开 IMU。
3.3 SLAM Toolbox 关键参数怎么设
SLAM Toolbox 的配置我挑几个最影响效果的讲:
max_laser_range:雷达最大有效距离。设太大,远处噪声点会污染地图;设太小,地图建不全。一般设成雷达标称量程的 80% 左右。resolution:地图分辨率,单位米/格。0.05 是默认值,也就是 5 厘米一格。扫地机器人用 0.05 够了,太小地图文件爆炸,太大细节丢失。minimum_travel_distance/minimum_travel_heading:机器人移动多少距离/转多少角度才插入一帧新数据。设太小地图糊,设太大细节丢。一般 0.5 米和 0.5 弧度起步。transform_timeout:等 TF 的超时时间。仿真里可以设小,真机上雷达和里程计有延迟,得设大点,否则频繁报 TF 超时。
这些参数没有万能值,得根据你的雷达频率、机器人速度、场景大小调。我的经验是:先按默认跑一遍,看地图哪里糊、哪里飘,再针对性调。
3.4 Nav2 的代价地图:导航的“路况图”
Nav2 不是直接拿 SLAM 的栅格地图规划路径的,它要先转成代价地图(Costmap)。代价地图分两层:
- 全局代价地图:基于整张地图,用于全局路径规划,算得慢但看得远。
- 局部代价地图:基于雷达实时数据,用于局部避障,算得快但只看眼前。
每个格子有个代价值,0 是自由通行,255 是致命障碍,中间值表示“离障碍多近”。机器人规划路径时会尽量走代价值低的格子,这样自然就远离了墙壁和障碍物。
代价地图里有个关键参数叫inflation_radius(膨胀半径),它把障碍物往外“撑大”一圈,让机器人不会贴着墙走。这个值一般设成机器人半径加一点余量。设太小机器人会蹭墙,设太大窄门过不去。扫地机器人半径通常 15-20 厘米,膨胀半径设 0.25-0.3 米比较合适。
4. 实操过程:从零把链路跑起来
4.1 环境准备与 ROS2 安装
我用的环境是 Ubuntu 22.04 加 ROS2 Humble,这是目前最稳的组合。热搜里那个“ubuntu26.04安装ros2”目前还不现实,26.04 还没正式发布,别折腾。ROS2 安装走官方 apt 源最省心:
sudo apt update && sudo apt install curl gnupg lsb-release sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null sudo apt update sudo apt install ros-humble-desktop装完记得 source 环境,写进.bashrc省得每次手动:
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc source ~/.bashrc热搜里那个“获取:1 http://packages.ros.org/ros2/ubuntu jammy inrelease 错误”是典型的源没配好或者网络问题,检查一下lsb_release -cs输出是不是jammy,以及 key 有没有导入成功。
4.2 Gazebo 仿真环境搭建
Gazebo 和 ROS2 的集成现在叫gazebo_ros,Humble 里装ros-humble-gazebo-ros-pkgs就行。仿真环境我一般自己搭,因为官方 world 文件太简单,测不出问题。
一个能用的仿真 world 至少要有:地面、几面墙、几个障碍物(箱子、柱子)、一个能跑的机器人模型。机器人模型用 URDF 或 SDF 描述,里面要挂上雷达和 IMU 的插件,让它们能发布话题。
雷达插件配置里几个关键参数:
<plugin name="laser" filename="libgazebo_ros_ray_sensor.so"> <ros> <remapping>~/out:=scan</remapping> </ros> <output_type>sensor_msgs/LaserScan</output_type> <frame_name>laser</frame_name> <min_range>0.1</min_range> <max_range>12.0</max_range> <samples>360</samples> </plugin>samples是每圈采样点数,360 就是每度一个点,够用了。max_range要和 SLAM 配置里的max_laser_range对得上,否则地图边缘会出问题。
注意:Gazebo 里雷达的
frame_name必须和 URDF 里定义的 link 名一致,否则 TF 树接不上,RViz2 里啥都看不到。
4.3 启动 SLAM 建图
SLAM Toolbox 的启动分在线异步和在线同步两种模式。异步模式对算力要求低,适合实时建图;同步模式精度高但吃 CPU,适合离线建图。扫地机器人用异步就行。
启动命令:
ros2 launch slam_toolbox online_async_launch.py params_file:=./my_slam_params.yaml启动后打开 RViz2,添加 LaserScan、Map、TF 三个显示项。用键盘遥控机器人(ros2 run teleop_twist_keyboard teleop_twist_keyboard)在环境里走一圈,你会看到地图一点点长出来。
建图时有个技巧:走慢点,转弯时更慢。走太快雷达帧之间位移大,匹配容易失败,地图就糊了。转弯时角速度大,如果 IMU 数据跟不上,地图会扭。我一般把线速度控制在 0.2 米/秒,角速度 0.5 弧度/秒以内。
4.4 保存地图并切换到导航模式
建完图先保存:
ros2 run nav2_map_server map_saver_cli -f ./my_map这会生成my_map.pgm(图片)和my_map.yaml(元数据)两个文件。yaml 里记录了分辨率、原点、占用阈值,Nav2 靠它加载地图。
然后关掉 SLAM 节点,启动 Nav2:
ros2 launch nav2_bringup bringup_launch.py map:=./my_map.yamlNav2 启动后会发布代价地图、路径、控制指令。在 RViz2 里用“2D Pose Estimate”给机器人一个初始位置,再用“2D Goal Pose”点一个目标点,机器人就应该自己规划路径走过去了。
4.5 参数调优的实操记录
第一次跑 Nav2 大概率不完美,我记录几个典型调参过程:
问题一:机器人贴着墙走,蹭墙。这是膨胀半径太小。把inflation_radius从 0.2 调到 0.3,机器人就离墙远了。
问题二:窄门过不去。膨胀半径太大,门被“堵死”了。这时候要么减小膨胀半径,要么调cost_scaling_factor(代价衰减因子),让膨胀代价衰减快一点,门中间还能留出低代价通道。
问题三:机器人原地打转。多半是局部规划器参数问题,或者 TF 有延迟。检查controller_frequency和transform_tolerance,前者是控制频率,后者是 TF 容忍时间。
问题四:路径规划失败。检查全局代价地图里目标点是不是被标记成障碍了,或者机器人当前位置在代价地图里是未知区域。用 RViz2 的代价地图显示项一看便知。
5. 常见问题与排查技巧实录
5.1 TF 相关问题的排查套路
TF 问题占了新手故障的一大半。排查顺序:
ros2 run tf2_tools view_frames生成 TF 树图,看链路全不全。ros2 run tf2_ros tf2_echo map base_link看两个坐标系之间的变换能不能查到。- RViz2 里 Fixed Frame 设成
map,看机器人模型和激光是否对齐。
常见症状对照表:
| 症状 | 可能原因 | 解决 |
|---|---|---|
| RViz2 里机器人飘 | map→odom 没发布 | 检查 SLAM 节点是否启动 |
| 激光和机器人错位 | 静态 TF 方向反了 | 检查父子坐标系顺序 |
| 报 TF timeout | 时间戳不同步 | 检查 use_sim_time 设置 |
| 地图扭曲 | 雷达外参不准 | 重新标定安装角度 |
提示:仿真里一定要把所有节点的
use_sim_time设成true,否则 Gazebo 时间和系统时间对不上,TF 直接乱套。这是仿真环境最隐蔽的坑之一。
5.2 建图飘移的几种典型原因
建图飘移表现为地图上墙壁重影、走廊弯曲、回环对不上。原因通常有:
- 里程计标定不准:轮子直径、轮距参数错了,走直线实际走成弧线。用卷尺量实际走的距离和里程计读数对比,算比例修正。
- 雷达安装角度偏:雷达 0 度没对准机器人正前方,建图整体旋转。在静态 TF 里调 yaw。
- 场景特征太少:长走廊、大白墙,激光匹配没特征可抓。这种场景要么加反光板,要么靠 IMU 撑。
- 移动太快:前面说过,慢点走。
5.3 Nav2 导航失败的排查清单
导航失败时,按这个清单过一遍:
- 地图加载了吗?
ros2 topic echo /map --once看有没有数据。 - 代价地图正常吗?RViz2 里看代价地图显示项,障碍物位置对不对。
- 初始位置设了吗?没设初始位置,AMCL 不知道机器人在哪,规划必失败。
- 目标点可达吗?目标点在障碍物里或者地图外,规划器直接放弃。
- 控制指令发了吗?
ros2 topic echo /cmd_vel看有没有速度输出。
5.4 仿真与真机的差异坑
仿真跑通不代表真机跑通,几个典型差异:
- 雷达噪声:仿真雷达是理想的,真机有噪声、有盲区、有反射干扰。真机上要把 SLAM 的
max_laser_range调小,滤掉远处噪声。 - 轮子打滑:仿真轮子不打滑,真机在地毯、门槛上会打滑,里程计漂移大。这时候 IMU 的权重就要调高。
- 计算延迟:真机算力有限,SLAM 和 Nav2 抢 CPU,导致控制频率下降。该降分辨率降分辨率,该关可视化关可视化。
6. 几个能省下大量时间的实操心得
折腾这套链路,我最大的体会是:别一上来就追求完美地图和丝滑导航,先把最小闭环跑通。Gazebo 里一个方块机器人、一个简单房间、一张糊地图,能跑通建图和导航,就说明链路是通的。剩下的都是调参和优化,是体力活不是脑力活。
第二个心得是善用 ros2 bag 录数据。真机调试时,把雷达、IMU、里程计话题全录下来,出问题可以反复回放分析,不用每次都重新跑一遍机器人。录数据命令:
ros2 bag record /scan /imu/data /odom /tf /tf_static回放的时候注意用--clock参数,让 bag 里的时间驱动整个系统。
第三个心得是参数文件一定要版本管理。SLAM 和 Nav2 的参数动辄几十个,改来改去很容易忘了哪个版本好用。我习惯每个能跑通的配置存一个 yaml,命名带上日期和场景,比如slam_room_20240115.yaml。下次换场景直接复制一份改,比从头调快得多。
最后一个,RViz2 的配置也存下来。每次重新配显示项很烦,RViz2 可以把当前配置存成.rviz文件,下次直接加载。把常用的显示项——地图、激光、代价地图、路径、TF——都配好存一份,能省下大量重复劳动。
这套链路我从第一次跑通到现在,前后迭代了七八个版本,从二维雷达小车到三维雷达底盘都趟过。核心逻辑其实一直没变:SLAM 负责把点云变成地图和位姿,Nav2 负责拿地图和位姿规划控制,TF 把两者缝在一起。把这三件事的关系理清楚,剩下的就是耐心调参。真机上的问题永远比仿真多,但每解决一个,你对整条链路的理解就深一层。