SLAM建图原理速览与建图节点启动

SLAM 建图原理速览与建图节点启动

1. 先说结论

SLAM 是机器人在没有现成地图时,一边估计自身位置,一边建立环境地图的过程。英文全称是 Simultaneous Localization and Mapping,即“同时定位与建图”。

对于移动机器人来说,SLAM 不是单独启动一个程序就能完成的功能。它至少需要同时获得以下几类数据:

  1. 激光雷达发布的二维扫描数据,通常是 sensor_msgs/msg/LaserScan 类型的 /scan 话题。
  2. 底盘里程计或激光里程计,通常通过 /odom 话题提供机器人的连续运动估计。
  3. 完整的 TF 坐标变换,至少要能把 odom、机器人底盘坐标系和雷达坐标系连接起来。
  4. 一个可以处理这些数据的 SLAM 算法节点,例如 ROS 2 中常用的 slam_toolbox。

因此,建图时应该按下面这条链路理解:

机器人底盘与传感器
        -> /scan、/odom、TF
        -> SLAM 建图节点
        -> /map 与 map -> odom
        -> RViz2 检查地图
        -> map_saver_cli 保存地图

本文以 ROSLander多模态机器人(Orin Nano 版本) ROS2 工程为例,介绍二维激光 SLAM 的基本原理、建图节点的启动方式、启动前检查、实际建图流程以及常见故障排查。

适用范围说明:本文对应 ROSLander多模态机器人(Orin Nano 版本) 的 ROS2 工程。该工程已经按 slam、peripherals、controller 和 roslander_description 等功能包组织底盘、IMU、里程计、激光雷达、机器人模型和 SLAM 启动链路。若使用的是其他 ROSLander 版本,或当前系统没有安装二维激光雷达,请以本机实际功能包、环境变量和话题名称为准。

2. 示例产品与软件环境

2.1 示例产品

项目 示例配置
产品 ROSLander多模态机器人(Orin Nano 版本)
建图任务 二维激光 SLAM 建立室内栅格地图
传感器 二维激光雷达、IMU
底盘 麦克纳姆轮移动底盘
可视化工具 RViz2
地图格式 OccupancyGrid,保存为 .yaml + .pgm
默认算法 ROSLander 工程通过 slam.launch.py 启动同步 slam_toolbox

2.2 软件环境

下面是一组常见的 ROS2 环境示例。实际使用时,以设备已经安装的 ROS2 发行版和功能包为准。

项目 示例
操作系统 Ubuntu 22.04
ROS2 Humble
SLAM 软件包 slam_toolbox
地图保存工具 nav2_map_server
可视化工具 rviz2
工作空间 ~/ros2_ws
建图参数文件 ~/ros2_ws/src/slam/config/slam.yaml

本文以 ROS 2 Humble 为例。如果你的系统使用其他发行版,请把下面命令中的 humble 替换成实际发行版名称。

如果系统中还没有相关软件包,可以先检查环境变量:

echo $ROS_DISTRO

在确认 ROS2 已正确安装后,再根据发行版安装依赖:

sudo apt update
sudo apt install ros-humble-slam-toolbox \
  ros-humble-navigation2 \
  ros-humble-nav2-bringup \
  ros-humble-rviz2

安装完成后检查软件包是否可以被 ROS2 找到:

ros2 pkg prefix slam_toolbox
ros2 pkg prefix nav2_map_server
ros2 pkg prefix rviz2

如果命令返回了安装目录,说明对应软件包已经进入当前 ROS2 环境。若提示 Package not found,先不要启动建图节点,应先处理安装或环境加载问题。

3. SLAM 建图到底在解决什么问题

机器人要在陌生环境中移动,必须同时回答两个问题:

  1. 机器人此刻位于什么位置,朝向是什么方向。
  2. 周围哪些位置是空闲区域,哪些位置被墙体或障碍物占据。

第一个问题是定位,第二个问题是建图。SLAM 的难点在于,机器人一开始既没有完整地图,也没有完全准确的自身位置,只能利用传感器观测和运动估计不断互相校正。

可以把 SLAM 的工作过程概括为三个阶段。

3.1 传感器数据预处理

激光雷达每一帧都会输出一组距离值。距离值通常按照角度顺序排列,每个数据点表示雷达在某个方向上测得的距离。SLAM 节点首先要检查这些数据是否满足使用条件,例如:

  • 距离值是否在雷达的有效量程内。
  • 是否存在大量 NaN、无穷大或超量程数据。
  • 扫描时间戳是否连续。
  • 雷达坐标系是否已经通过 TF 连接到底盘。
  • 雷达安装方向是否与实际坐标轴约定一致。

如果输入数据本身不稳定,后面的算法再复杂也无法得到可靠地图。建图过程中地图突然拉伸、墙体变成重影或走廊逐渐弯曲,很多时候首先要检查传感器数据和 TF,而不是立即修改 SLAM 参数。

3.2 当前扫描与已有信息匹配

机器人移动后,当前帧扫描到的墙体和障碍物会与上一帧有所重叠。SLAM 算法会尝试把当前扫描与已有扫描或当前地图对齐,估计机器人在这段时间内发生了多少平移和旋转。

这一步可以理解为“扫描匹配”。里程计提供运动变化的初始猜测,激光扫描提供环境几何约束。两者配合时,算法既能知道机器人大概走了多远,也能利用墙体等稳定结构纠正轮子打滑带来的误差。

3.3 地图融合与回环优化

得到当前位姿后,SLAM 节点把这一次扫描投影到地图坐标系中,更新占据栅格。机器人沿着环境继续移动时,地图不断扩展。

当机器人重新回到之前经过的位置,算法可以识别出“当前位置和历史位置是同一个地方”,这就是回环约束。回环检测成功后,SLAM 后端会优化一组历史位姿,让之前累计的误差得到整体调整。大环境建图时,回环质量通常直接影响最终地图是否闭合。

slam_toolbox 的建图过程可以概括为:订阅激光和里程计,使用扫描匹配维护位姿图,在发现回环时优化位姿图,再根据优化后的位姿发布地图和 map -> odom 变换。

4. 建图节点在 ROS2 中扮演什么角色

4.1 ROSLander 工程中的 slam_toolbox 节点

ROSLander 工程不是直接启动 slam_toolbox 官方的 online_async_launch.py,而是使用工程自己的总入口:

ros2 launch slam slam.launch.py

这个总入口会先引入 slam/launch/include/robot.launch.py,启动底盘、IMU、里程计、雷达和相关外围节点;等待约 5 秒后,再引入 slam/launch/include/slam_base.launch.py。slam_base.launch.py 读取 slam/config/slam.yaml,最终启动的核心可执行程序是:

sync_slam_toolbox_node

节点名为 slam_toolbox。因此,本文后面的命令都以 ROSLander 工程的 slam.launch.py 为准,不再把 online_async_launch.py 当作 ROSLander 的默认入口。

ROSLander 的 SLAM 节点通常完成以下工作:

  • 订阅雷达过滤后的 LaserScan 数据。
  • 查询 odom、base_footprint 和 lidar_frame 之间的 TF。
  • 使用 /odom 作为运动估计,并结合激光扫描进行匹配。
  • 维护位姿图并进行回环检测。
  • 发布二维地图 /map 或当前命名空间下的地图话题。
  • 发布地图坐标系到里程计坐标系的 TF。

4.2 ROSLander 底盘、IMU 与里程计节点

底盘驱动节点不是 SLAM 节点,但它是建图能否运行的前提。ROSLander 的 controller.launch.py 会组织多条底盘数据链路,主要包括:

  • odom_publisher.launch.py:根据底盘运动和 IMU 数据生成里程计相关信息。
  • imu_filter.launch.py:处理 IMU 数据。
  • robot_localization/ekf_node:融合里程计和 IMU,并把 odometry/filtered 重映射为 /odom。
  • 舵机控制、运动学和底盘控制节点:接收速度指令并驱动机器人运动。

因此,ROSLander 建图前不能只确认“底盘能动”,还要确认 /odom 正在更新,并且 odom -> base_footprint 的 TF 连续、方向正确。里程计依赖 IMU,若要获得更好的建图效果,应先完成 IMU、线速度和角速度校准。

4.3 ROSLander 雷达与过滤节点

ROSLander 通过 peripherals/launch/lidar.launch.py 统一启动雷达。该 launch 根据环境变量 LIDAR_TYPE 选择雷达驱动,支持 LD19、A1、G4 和 S2L 等类型,然后通过 laser_filters 对原始扫描进行过滤。

其数据链路可以概括为:

雷达驱动 -> scan_raw -> laser_filters -> scan -> slam_toolbox

其中常见消息类型是:

sensor_msgs/msg/LaserScan

lidar.launch.py 暴露了 lidar_frame、scan_raw 和 scan_topic 参数。ROSLander 默认使用过滤后的 scan 作为 SLAM 输入;如果 robot_name 不为空,实际话题可能带机器人命名空间,例如 /<robot_name>/scan。不要只凭经验写死 /scan,应先用 ros2 topic list 查看当前系统真正发布的名称。

4.4 TF 发布节点

ROSLander 单机器人建图至少需要建立下面的坐标关系:

map -> odom -> base_footprint -> lidar_frame

其中:

坐标系 作用 常见发布者
map 全局地图坐标系 SLAM 节点
odom 连续但会漂移的里程计坐标系 底盘驱动或里程计节点
base_link / base_footprint 机器人底盘坐标系 底盘控制和机器人模型
lidar_frame 激光雷达坐标系 雷达 launch、URDF 或静态 TF

建图时通常由 SLAM 节点发布 map -> odom,底盘和 EKF 链路提供 odom -> base_footprint,雷达与机器人模型提供 base_footprint -> lidar_frame。ROSLander 的 slam.launch.py 会根据 robot_name 生成 map、odom 和 base_footprint 的前缀;若设置了 robot_name,实际 TF 可能显示为 <robot_name>/map、<robot_name>/odom 和 <robot_name>/base_footprint。坐标系名称必须以 tf2_echo 和 RViz 的实际结果为准。

5. 建图前必须确认的四个条件

5.1 激光雷达有稳定输出

先单独启动雷达驱动,再检查话题:

ros2 topic list | grep scan

查看 /scan 的消息类型:

ros2 topic type /scan

正常情况下应看到:

sensor_msgs/msg/LaserScan

检查扫描频率:

ros2 topic hz /scan

短时间查看一帧内容:

ros2 topic echo /scan --once

重点观察 header.frame_id、angle_min、angle_max、range_min、range_max 和 ranges。如果 header.frame_id 为空,或者所有 ranges 都是无效值,SLAM 节点无法完成有效匹配。

5.2 里程计正在更新

检查 /odom 是否存在:

ros2 topic list | grep odom
ros2 topic type /odom
ros2 topic hz /odom

让小车缓慢向前移动,观察里程计消息中的位置是否变化:

ros2 topic echo /odom --once

如果每次读到的消息时间戳在变化,但位置始终为零,说明底盘驱动可能只发布了空消息,或编码器数据没有接入。若前进时 x 方向变化、旋转时角度变化完全相反,也要在启动建图前修正底盘坐标约定。

5.3 TF 链路完整

查看 TF 结构:

ros2 run tf2_tools view_frames

也可以直接检查雷达和底盘之间的变换:

ros2 run tf2_ros tf2_echo base_footprint lidar_frame

如果当前 ROSLander 工程启用了机器人命名空间,实际坐标系可能带有 robot_name 前缀,应以 ros2 run tf2_tools view_frames 生成的 TF 图为准。命令能够持续输出变换,说明这两个坐标系之间至少存在可查询的 TF。

注意:在 SLAM 启动前,不要要求系统已经存在 map -> odom。这段变换通常正是由 SLAM 节点在建图过程中发布的。启动前需要确认的是 odom -> base_footprint 和 base_footprint -> lidar_frame,启动后再检查完整链路。

5.4 机器人时间源一致

真实机器人使用系统时间,仿真机器人使用 /clock。ROSLander 真实硬件启动时,slam.launch.py 的 sim 参数应保持为 false;源码会据此把 use_sim_time 传给底盘和 SLAM 节点。如果误设为 true,而系统没有 /clock,节点可能看似已经启动,但消息时间戳无法正常推进。

检查是否存在仿真时钟:

ros2 topic list | grep clock

真实硬件没有 /clock 并不代表系统有问题,关键是所有相关节点都要使用同一种时间模式。

6. 参数文件怎么读

使用 slam_toolbox 时,ROSLander 将参数集中放在 ~/ros2_ws/src/slam/config/slam.yaml。slam_base.launch.py 会根据 launch 参数重写时间源和坐标系。下面只展示阅读参数文件时需要关注的结构,不替代 ROSLander 工程中的完整 YAML:

slam_toolbox:
  ros__parameters:
    use_sim_time: false
    mode: mapping

    map_frame: map
    odom_frame: odom
    base_frame: base_footprint
    scan_topic: scan

    resolution: 0.05
    transform_publish_period: 0.02
    map_update_interval: 5.0
    minimum_time_interval: 0.5

    use_scan_matching: true
    use_scan_barycenter: true
    do_loop_closing: true
    minimum_travel_distance: 0.5
    minimum_travel_heading: 0.5

这份示例只列出理解启动链路所需的关键项,不能替代 ROSLander 工程中的完整 slam.yaml。如果 HOST 或 robot_name 非空,实际话题和 TF 可能带命名空间;不要仅凭这段示意 YAML 判断最终名称。

6.1 map_frame

地图全局坐标系,通常写成 map。它应与 RViz 的 Fixed Frame 以及导航系统的全局坐标系保持一致。

6.2 odom_frame

里程计坐标系,通常写成 odom。这个坐标系应该连续、平滑,但允许随着时间产生漂移。SLAM 节点通过发布 map -> odom 对漂移进行全局修正。

6.3 base_frame

机器人底盘坐标系。常见值是 base_link 或 base_footprint,必须以实际 TF 树为准。参数名填写正确但坐标系不存在时,日志里通常会出现 TF lookup 失败。

6.4 scan_topic

激光扫描话题名称。不要只根据雷达驱动的产品名称猜测话题名,应通过 ros2 topic list 和 ros2 topic type 实际确认。

6.5 use_sim_time

真实 ROSLander 设置 sim:=false,仿真环境才设置 sim:=true。ROSLander 的 slam.launch.py 默认 sim 为 false,并会把对应的 use_sim_time 传给 robot.launch.py 和 slam_base.launch.py。

7. 建图节点的标准启动顺序

启动顺序的核心原则是:先让输入数据可用,再启动依赖这些数据的 SLAM 节点,最后启动可视化和保存工具。

7.1 第一步:打开终端并加载 ROS2 环境

每个终端都要加载 ROS2 环境。若使用自建工作空间,还要加载工作空间的 install 目录。

source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash

如果工作空间还没有编译过,先进入工作空间编译:

cd ~/ros2_ws
colcon build --symlink-install
source install/setup.bash

7.2 第二步:启动底盘、雷达和机器人模型

ROSLander 建图时应先关闭 APP 自启服务,避免自启程序重复占用底盘、雷达或相关话题:

sudo systemctl stop start_app_node.service

然后启动 ROSLander 的 SLAM 总入口:

ros2 launch slam slam.launch.py

这个 launch 文件会先引入 slam/launch/include/robot.launch.py,启动底盘控制、IMU、里程计、雷达和机器人模型等外围节点;等待约 5 秒后,再引入 slam/launch/include/slam_base.launch.py,启动同步 slam_toolbox 节点。键盘控制通过终端单独启动,正常使用时不需要另外手动启动 controller 和 peripherals lidar,否则容易重复启动节点。

ROSLander 的总 launch 支持以下常用参数:

ros2 launch slam slam.launch.py \
  sim:=false \
  slam_method:=slam_toolbox \
  enable_save:=true

其中 sim:=false 表示真实机器人,slam_method:=slam_toolbox 表示使用默认的二维建图算法,enable_save:=true 保留地图保存服务。

启动后再确认输入数据:

ros2 node list
ros2 topic list
ros2 topic hz /scan
ros2 topic hz /odom

如果需要单独调试雷达或底盘,才使用对应的子 launch。例如雷达调试入口是:

ros2 launch peripherals lidar.launch.py

底盘调试入口是:

ros2 launch controller controller.launch.py

单独启动这些子 launch 前,应先结束正在运行的 slam.launch.py,避免同一节点被启动两次。

7.3 第三步:确认 launch 文件参数

查看 ROSLander 工程总 launch 暴露的参数:

ros2 launch slam slam.launch.py --show-args

重点确认是否包含 sim、slam_method、enable_save、master_name 和 robot_name。其中 slam_method 默认是 slam_toolbox,sim 默认是 false。

7.4 第四步:确认同步建图节点已经启动

执行 ros2 launch slam slam.launch.py 后,slam_base.launch.py 会读取下面的参数文件:

~/ros2_ws/src/slam/config/slam.yaml

源码中真正启动的节点为:

package: slam_toolbox
executable: sync_slam_toolbox_node
name: slam_toolbox

ROSLander 的默认建图方式是同步模式。它会按扫描顺序处理有效传感器测量,适合稳定的建图流程。启动成功后,终端中应能看到 /slam_toolbox 或当前机器人命名空间下的 slam_toolbox 节点。

ROSLander 工程也保留了直接启动官方 slam_toolbox launch 的可能性,但这不是工程的默认操作。除非你明确知道参数文件、话题 remap 和坐标系已经与工程一致,否则不要用下面这种通用命令替代 ros2 launch slam slam.launch.py:

ros2 launch slam_toolbox online_sync_launch.py

这条命令只用于理解官方工具接口,不能作为 ROSLander 的首选启动命令。

7.5 第五步:启动 RViz2

可以直接启动 RViz2:

rviz2

ROSLander 工程提供了对应的 RViz 启动文件:

ros2 launch slam rviz_slam.launch.py

在 RViz2 中至少检查以下内容:

RViz 项目 建议设置
Fixed Frame map 或当前机器人命名空间下的地图坐标系
Map 当前 SLAM 节点发布的 map 话题
LaserScan 过滤后的 scan 话题
TF 开启显示
RobotModel 如果工程提供 URDF,则开启
Grid 可选,用于观察坐标轴方向

如果 Fixed Frame 设置成 base_link,有时仍能看到激光,但无法直观看到地图坐标系是否正确。建图阶段通常使用 map 作为 Fixed Frame 更容易判断全局地图是否在更新。

8. 第一次启动后如何确认节点真的工作

节点启动没有报错,不代表建图链路已经完整。建议按下面顺序检查。

8.1 检查节点列表

ros2 node list

至少应能看到底盘驱动、雷达驱动和 SLAM 节点。SLAM 节点名称可能是 /slam_toolbox,也可能因为命名空间而显示成其他形式。

8.2 检查地图话题

ros2 topic list | grep -E 'map|scan|odom|tf'

查看地图消息类型:

ros2 topic type /map

正常情况下应为:

nav_msgs/msg/OccupancyGrid

查看一帧地图消息:

ros2 topic echo /map --once

如果 /map 不存在,优先检查 SLAM 节点是否启动、参数文件是否加载成功,以及节点是否处于 active 状态。

8.3 检查地图是否随机器人运动更新

让 ROSLander 先原地小角度旋转,再沿直线缓慢移动。建议使用键盘控制节点,并适当降低机器人移动速度。观察 RViz2 中:

  • 激光点是否随机器人运动。
  • 地图边缘是否逐渐扩展。
  • 已知墙体是否保持清晰,而不是不断产生重影。
  • 机器人底盘模型或 TF 是否跟随运动。

刚启动时地图只有局部区域是正常的。真正需要关注的是机器人移动后地图是否继续更新,以及回到起点附近时是否能形成闭环。

8.4 检查 TF

ros2 run tf2_ros tf2_echo map odom
ros2 run tf2_ros tf2_echo odom base_footprint
ros2 run tf2_ros tf2_echo base_footprint lidar_frame

如果 robot_name 非空,按实际命名空间补全坐标系名称。map -> odom 只有在 SLAM 节点运行后才应出现;另外两段通常应在 SLAM 启动前就已经存在。

9. 正确的建图操作方法

9.1 建图前整理环境

建图的目标不是把当时看到的所有物体都画进地图,而是建立一张后续可以重复定位和导航的静态环境地图。开始前建议:

  1. 给 ROSLander 充足电量,避免建图中途电压下降导致底盘运动异常。
  2. 将容易移动的箱子、椅子和人员尽量移开。
  3. 避免让人员长时间站在雷达扫描范围内。
  4. 确认雷达安装牢固,不能在车体运动时晃动。
  5. 确认小车周围有足够空间,不要刚启动就贴着墙体或障碍物。

9.2 选择合适的起点

起点最好面对一面直墙或结构稳定的环境,让雷达在第一批扫描中获得足够多的几何特征。不要把雷达正对大面积玻璃、薄布帘或频繁移动的物体作为主要参考。

9.3 先闭环,再细扫

在较大的场地中,建议先沿主要通道移动一圈,让机器人回到起点附近,优先建立完整的整体轮廓。确认闭环后,再补充角落、房间入口和细节区域。

如果一开始就只在一个小区域反复转圈,局部地图可能很清晰,但全局坐标关系没有建立;如果只沿直线走很远而没有回到已知区域,累计误差可能越来越明显。

9.4 控制运动速度

建图阶段以低速、匀速为主。建议遵守下面几条原则:

  • 直线移动不要突然加速或急停。
  • 转弯时降低线速度,避免雷达扫描期间底盘姿态变化过快。
  • 不要连续做高速原地旋转。
  • 麦克纳姆底盘进行横移时,尽量观察地图是否出现侧向拉伸。
  • 如果地图明显跟不上运动,先降低速度和扫描负载,再调参数。

9.5 检查地图质量

建图完成后,至少检查以下内容:

检查项 合格表现
墙体边缘 轮廓清晰,重影较少
闭环 回到起点后整体地图没有明显错位
通行区域 后续需要行走的区域没有大块未知灰区
动态物体 没有把人员、移动箱子等长期障碍固化到地图
覆盖范围 机器人可能到达的位置已被雷达充分扫描
比例尺度 地图中的通道宽度与实际环境大致一致

如果地图边缘模糊、墙体重叠或整个场景发生弯曲,不建议直接保存作为导航地图。先找出数据、TF、速度或回环问题,再重新建图。

10. 保存地图

当地图已经闭环并且机器人停稳后,再执行保存。建议将地图统一保存在工作空间的地图目录中:

mkdir -p ~/ros2_ws/src/slam/maps
cd ~/ros2_ws/src/slam/maps

使用 Nav2 的地图保存工具:

ros2 run nav2_map_server map_saver_cli \
  -f $HOME/ros2_ws/src/slam/maps/map_01 \
  --ros-args -p map_subscribe_transient_local:=true

-f 后面填写的是文件前缀,不要手动添加 .yaml 或 .pgm。正常完成后,目录中应出现:

map_01.yaml
map_01.pgm

保存后立即检查:

ls -lh ~/ros2_ws/src/slam/maps/map_01.*

也可以查看 YAML 文件内容:

sed -n '1,40p' ~/ros2_ws/src/slam/maps/map_01.yaml

重点确认以下字段:

image: map_01.pgm
resolution: 0.05
origin: [-1.0, -1.0, 0.0]
negate: 0
occupied_thresh: 0.65
free_thresh: 0.25

实际 origin 和 resolution 以保存工具生成的内容为准,不要为了让地图“看起来居中”而随意改动。YAML 中的 image 路径必须能找到对应的图像文件;移动地图时,最好把 .yaml 和 .pgm 一起移动。

ROSLander 工程还提供了 map_save.py 节点。启用 slam.launch.py 的 enable_save:=true 后,可以检查保存服务:

ros2 service list | grep slam_toolbox/save_map
ros2 service type /slam_toolbox/save_map

该节点内部调用的保存命令仍然是 map_saver_cli -f map_01,所以最终文件名固定为 map_01.yaml 和 map_01.pgm。如果需要保存不同场地,先复制或重命名上一份地图,再按工程约定修改 map_save.py 中的文件前缀。

11. 一个完整的终端操作示例

下面给出 ROSLander 的一套完整命令顺序。每个终端都要加载 ROS2 和工作空间环境。

终端 1:停止 APP 自启并启动 SLAM 总入口

source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
sudo systemctl stop start_app_node.service
ros2 launch slam slam.launch.py \
  sim:=false \
  slam_method:=slam_toolbox \
  enable_save:=true

终端 2:确认输入数据

source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 topic type /scan
ros2 topic type /odom
ros2 topic hz /scan
ros2 run tf2_ros tf2_echo base_footprint lidar_frame

终端 3:启动 RViz2

source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 launch slam rviz_slam.launch.py

终端 4:启动键盘控制

source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 launch peripherals teleop_key_control.launch.py

使用键盘控制 ROSLander 低速移动,完成闭环并确认 RViz2 中地图质量合格后,再在终端 5 保存地图。

终端 5:保存地图

source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 run nav2_map_server map_saver_cli \
  -f $HOME/ros2_ws/src/slam/maps/map_01 \
  --ros-args -p map_subscribe_transient_local:=true

不要在建图还没有闭环时保存地图,也不要在机器人快速移动时执行保存。保存工具只负责把当前 /map 写入文件,无法替你判断地图质量。

12. ROSLander 的 launch 文件结构

ROSLander 已经提供了完整的 slam 功能包,不需要另外创建 mapping.launch.py。工程的启动关系可以简化为:

slam.launch.py
├── include/robot.launch.py
│   ├── controller.launch.py
│   ├── peripherals/lidar.launch.py
│   ├── 机器人模型与 TF
│   └── IMU、里程计和 EKF
└── 等待约 5 秒
    └── include/slam_base.launch.py
        ├── 读取 config/slam.yaml
        ├── sync_slam_toolbox_node
        └── enable_save 时启动 map_save.py

这种组织方式的关键点是:robot.launch.py 先把传感器输入和 TF 准备好,slam_base.launch.py 再启动算法节点。总入口已经处理了启动参数、命名空间和启动延迟,日常使用应优先调用:

ros2 launch slam slam.launch.py

12.1 如何查看 ROSLander 的 launch 参数

ros2 launch slam slam.launch.py --show-args

重点查看 sim、slam_method、enable_save、master_name 和 robot_name。如果修改了源码或参数文件,重新编译 slam 功能包并重新加载工作空间:

cd ~/ros2_ws
colcon build --packages-select slam --symlink-install
source install/setup.bash
ros2 pkg prefix slam

12.2 为什么调试时仍要分层检查

虽然日常可以使用一个总 launch,但排查问题时仍然应按数据链路分层检查:

  • 雷达没有 /scan,是雷达驱动或设备连接问题。
  • /scan 正常但 TF 缺失,是 URDF 或静态 TF 问题。
  • /scan、/odom 和 TF 都正常但 /map 不更新,是 SLAM 参数、时间戳或数据匹配问题。
  • 地图正常但保存失败,是地图保存工具或 QoS 配置问题。

ROSLander 的工程 launch 已经把这些层次组合起来。单独启动子 launch 只用于定位问题,确认后应回到 slam.launch.py 作为正式入口。

13. ROSLander 的算法入口边界

本文采用 ROSLander 工程中的 slam_toolbox 建图方案,默认通过 slam.launch.py 启动 sync_slam_toolbox_node。不要把通用 ROS2 教程中的异步 launch、GMapping launch 或其他小车工程的参数文件直接复制到 ROSLander 中。

如果使用的是另一份 ROSLander 源码或自行移植的工程,先检查以下三项,再决定是否更换算法入口:

ros2 pkg list | grep -E 'slam|gmapping|rtabmap'
ros2 launch slam slam.launch.py --show-args
ros2 topic list | grep -E 'scan|odom|map'

无论使用哪一种二维 SLAM 算法,都不能同时启动两个会发布 /map 或 map -> odom 的节点。ROSLander 默认链路已经能满足本篇建图任务,不需要额外启动 GMapping。

14. 常见问题与排查顺序

14.1 找不到 slam_toolbox 包

现象:

Package 'slam_toolbox' not found

排查:

echo $ROS_DISTRO
ros2 pkg list | grep slam
ros2 pkg prefix slam_toolbox

如果没有输出,安装对应发行版的软件包,并重新加载 ROS2 环境。不要只在一个终端安装或加载环境后,直接认为其他终端也已经生效。

14.2 节点启动了,但 /map 不存在

按下面顺序检查:

  1. ros2 node list 中是否有 /slam_toolbox。
  2. SLAM 终端是否有参数文件解析错误。
  3. /scan 是否存在且消息类型正确。
  4. scan_topic 是否与实际话题相同。
  5. base_frame、odom_frame 和 map_frame 是否拼写一致。
  6. 雷达坐标系到机器人底盘的 TF 是否可查询。
  7. use_sim_time 是否与真实运行环境匹配。

14.3 /scan 存在,但地图不更新

常见原因包括:

  • 机器人实际上没有移动。
  • /scan 的时间戳不更新或时间基准不一致。
  • 雷达数据全部无效或有效范围过小。
  • SLAM 节点无法查询 odom -> base_footprint 或 base_footprint -> lidar_frame。
  • scan_topic 填写成了错误的话题。
  • 机器人运动速度过快,扫描匹配跟不上。
  • 底盘里程计方向与实际运动方向相反。
  • 两个 SLAM 节点同时运行,输出相互覆盖。

建议先降低小车速度,手动查看一帧 /scan,再检查 TF,而不是直接提高地图更新频率。

14.4 地图出现重影

地图重影通常说明同一面墙在不同时间被投影到了不同位置。重点排查:

  1. 雷达支架是否松动。
  2. base_footprint -> lidar_frame 的静态变换是否填写正确。
  3. /odom 是否存在明显跳变。
  4. 底盘是否在原地打滑。
  5. 雷达扫描频率是否与运动速度匹配。
  6. 环境中是否有大量移动人员或移动物体。
  7. 是否在没有回环的情况下走了过长距离。

如果重影只在急转弯时出现,先降低角速度并重新建图;如果整个地图都沿固定方向偏移,优先检查雷达外参和坐标轴方向。

14.5 地图整体弯曲或回不到起点

这通常与累计里程计误差、扫描匹配失败或回环未建立有关。可以尝试:

  • 让机器人沿包含稳定墙体的路线移动。
  • 避免大面积空旷区域和重复纹理区域。
  • 先完成一圈闭环,再补充局部细节。
  • 减小运动速度和原地旋转速度。
  • 检查雷达量程和数据有效率。
  • 保证 do_loop_closing 没有被关闭。

不要把“地图范围变大”当作“建图成功”。地图范围扩大但结构逐渐弯曲,说明算法在继续接收数据,却没有得到足够稳定的几何约束。

14.6 RViz 中看不到激光

先检查 LaserScan 的 Topic 是否选择正确,再检查 Fixed Frame 是否可以通过 TF 连接到雷达坐标系。常见问题是:

  • RViz 订阅了 /scan,实际数据在 /scan_raw。
  • header.frame_id 写成了不存在的坐标系。
  • 雷达消息的 header.frame_id 不是 lidar_frame,或 TF 中没有对应的雷达坐标系。
  • Fixed Frame 设置成了 map,但 SLAM 尚未发布 map -> odom。

为了区分 RViz 问题和数据问题,可以先执行:

ros2 topic echo /scan --once

如果命令能读到有效数据,问题更可能在 RViz 话题选择或 TF 显示配置。

14.7 保存地图超时

先确认 /map 正在发布:

ros2 topic type /map
ros2 topic echo /map --once

然后确认使用的是 Nav2 的 map_saver_cli:

ros2 run nav2_map_server map_saver_cli -h

如果地图由瞬态本地 QoS 发布,保存工具需要以兼容的订阅方式接收最后一张地图,可以尝试:

ros2 run nav2_map_server map_saver_cli \
  -f $HOME/ros2_ws/src/slam/maps/map_01 \
  --ros-args -p map_subscribe_transient_local:=true

不同 ROS2 发行版的 map_saver_cli 参数可能略有差异,若本机帮助信息不包含该参数,应以 -h 输出和当前 Nav2 版本的配置为准。

14.8 地图保存成功,但重新加载失败

重点检查:

  • .yaml 和 .pgm 是否同时存在。
  • YAML 中 image 路径是否仍然有效。
  • 文件名大小写是否完全一致。
  • 地图目录是否有读取权限。
  • 是否在复制地图时只复制了 YAML,漏掉了 PGM。

可以从地图目录直接查看文件:

cd ~/ros2_ws/src/slam/maps
ls -lh map_01.yaml map_01.pgm

15. 一组快速体检命令

建图前可以把下面的命令依次执行一遍:

ros2 node list
ros2 topic list | grep -E 'scan|odom|map|tf'
ros2 topic type /scan
ros2 topic type /odom
ros2 topic hz /scan
ros2 topic hz /odom
ros2 topic echo /scan --once
ros2 topic echo /odom --once
ros2 run tf2_ros tf2_echo odom base_footprint
ros2 run tf2_ros tf2_echo base_footprint lidar_frame

如果启用了 robot_name 命名空间,替换最后两条命令中的参数。下面这份检查表也可以直接放在 ROSLander 实验记录中:

项目 检查结果
ROS2 环境已加载 通过 / 未通过
slam_toolbox 可找到 通过 / 未通过
/scan 类型正确 通过 / 未通过
/scan 频率稳定 通过 / 未通过
/odom 正在更新 通过 / 未通过
odom -> base_footprint 存在 通过 / 未通过
base_footprint -> lidar_frame 存在 通过 / 未通过
/map 正常发布 通过 / 未通过
RViz Fixed Frame 为 map 通过 / 未通过
地图闭环且无明显重影 通过 / 未通过
.yaml 与 .pgm 成对保存 通过 / 未通过

16. 参数调整建议

16.1 先调运行条件,再调算法参数

地图质量问题经常被误判为算法参数问题。推荐的调整顺序是:

  1. 确认雷达数据有效。
  2. 确认雷达安装不晃动。
  3. 确认 TF 外参正确。
  4. 确认里程计方向和尺度正确。
  5. 降低机器人运动速度。
  6. 再根据日志和地图表现调整 SLAM 参数。

如果输入数据和坐标变换没有问题,才有必要进一步调整扫描匹配距离、角度阈值、回环搜索范围或地图更新周期。

16.2 resolution

分辨率表示一个栅格像素对应的实际距离,常见值是 0.05 米。分辨率越小,地图细节越多,同时需要更多内存和计算资源。对于室内小车,不要为了追求“更清晰”就盲目设置极小分辨率。

16.3 minimum_travel_distance

该参数控制机器人移动到一定距离后才处理新的建图节点。设置过大,地图更新会显得不及时;设置过小,可能增加计算负载并把底盘微小抖动带入地图。

16.4 minimum_travel_heading

该参数控制机器人旋转到一定角度后才加入新的姿态节点。ROSLander 原地旋转时如果角度变化很快,应配合实际雷达频率和底盘稳定性调整,不要只依据单次实验结果修改。

16.5 do_loop_closing

大多数室内建图都需要回环检测。关闭回环后,机器人长距离运动时的累计误差更容易保留在最终地图中。只有在明确知道工程需要特殊模式时,才修改该参数。

17. 建图与导航不要同时误启动

建图和使用既有地图导航是两个不同模式。

建图阶段:

SLAM 节点发布 /map
SLAM 节点发布 map -> odom
机器人移动,地图持续更新

加载已有地图导航阶段:

map_server 发布静态 /map
AMCL 发布 map -> odom
导航栈根据地图规划路径

如果建图节点和导航定位节点同时运行,可能会有两个节点同时发布 /map 或 map -> odom。这会导致地图跳动、机器人位姿跳变或 RViz 中出现两套结果。

切换模式前,先确认旧节点已经退出:

ros2 node list

也可以在终端中使用 Ctrl+C 结束旧的 launch,再启动新的模式。不要只关闭 RViz,以为后端节点也已经退出。

18. 建图完成后的文件管理

建议为不同场地和不同版本使用可识别的名称:

~/ros2_ws/src/slam/maps/
├── map_01.yaml
├── map_01.pgm
├── roslander_lab_clean.yaml
├── roslander_lab_clean.pgm
├── roslander_office_01.yaml
└── roslander_office_01.pgm

不要只保留一个名为 new_map 或 final_map 的文件。地图通常会反复重建和优化,名称中包含场景和版本,后续排查时更容易确认导航使用的是哪一份地图。

如果地图需要复制到另一台机器,至少同时复制 YAML 和图像文件,并检查 YAML 中的 image 路径。更稳妥的做法是让 YAML 使用与地图文件同目录的相对图像路径。

19. 最短操作版

只保留核心操作时,可以按下面的顺序执行:

加载 ROS2 和工作空间环境
        -> 停止 start_app_node.service
        -> 启动 ROSLander 的 slam.launch.py
        -> 检查 /scan、/odom 与 TF
        -> 启动 rviz_slam.launch.py
        -> 启动 teleop_key_control.launch.py
        -> RViz2 将 Fixed Frame 设为 map
        -> 缓慢移动机器人并完成回环
        -> 检查地图质量
        -> map_saver_cli 保存 map_01.yaml + map_01.pgm

对应的最小命令组合如下:

source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash

ros2 topic type /scan
ros2 topic type /odom
sudo systemctl stop start_app_node.service
ros2 launch slam slam.launch.py \
  sim:=false \
  slam_method:=slam_toolbox \
  enable_save:=true

另开终端启动可视化和键盘控制:

source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 launch slam rviz_slam.launch.py
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 launch peripherals teleop_key_control.launch.py

建图结束后保存地图:

ros2 run nav2_map_server map_saver_cli \
  -f $HOME/ros2_ws/src/slam/maps/map_01 \
  --ros-args -p map_subscribe_transient_local:=true

20. 小结

SLAM 建图的核心不是“打开 RViz 后看到一张图”,而是让激光扫描、里程计和 TF 在同一个时间与坐标体系下稳定工作。SLAM 算法利用里程计估计运动,再用激光扫描进行匹配和校正,通过回环优化减少累计误差,最终输出可供导航使用的二维栅格地图。

启动建图节点时,最重要的顺序是:

  1. 先加载 ROS2 和工作空间环境。
  2. 启动底盘、雷达和机器人模型。
  3. 检查 /scan、/odom、odom -> base_footprint 和 base_footprint -> lidar_frame。
  4. 通过 ros2 launch slam slam.launch.py 启动 ROSLander 默认的同步 slam_toolbox 节点。
  5. 在 RViz2 中用 map 作为 Fixed Frame 检查地图更新。
  6. 低速移动并完成闭环。
  7. 确认地图质量后保存 .yaml 和 .pgm。

对于 ROSLander,建图前必须先关闭 start_app_node.service,再使用 slam.launch.py 启动工程已经组织好的底盘、IMU、里程计、雷达、TF 和 slam_toolbox 链路。确认 /scan、/odom、odom -> base_footprint 和 base_footprint -> lidar_frame 都正常后,再使用 rviz_slam.launch.py、teleop_key_control.launch.py 完成建图,最后保存 map_01.yaml 和 map_01.pgm。后续问题可以按照“输入话题 → TF → 时间戳 → SLAM 参数 → 地图保存”的顺序逐层定位。