BACK_TO_BASE
Engineering Notebook // Build Log
/
22:33:49
/
NOTEBOOK_ENTRY

slam

基于 ROS2 的 SLAM 导航小车完全教程 本教程面向有 ROS2 基础但对 SLAM 和计算机视觉不太熟悉的开发者,以导航小车为载体,系统讲解 SLAM 原理与实践。 目录 1. 引言 1 引言 2. SLAM 核心原理 2 slam 核心原理 3. 激光 SLAM 实战 3 激光 slam 实战 4. 视觉 SLAM 入门 4 视觉 slam 入门 5. Nav2 导航栈 5 nav2 导航栈 6. 导航小车硬件搭建 6 导航小…

Notebook Time
10 min
Image Frames
0
View Tracks
296
FIELD_GUIDE

FIELD GUIDE

Use the guide rail to jump between sections.

基于 ROS2 的 SLAM 导航小车完全教程

本教程面向有 ROS2 基础但对 SLAM 和计算机视觉不太熟悉的开发者,以导航小车为载体,系统讲解 SLAM 原理与实践。


目录

  1. 引言
  2. SLAM 核心原理
  3. 激光 SLAM 实战
  4. 视觉 SLAM 入门
  5. Nav2 导航栈
  6. 导航小车硬件搭建
  7. Gazebo 仿真环境
  8. 进阶拓展
  9. 常见问题与调试技巧
  10. 参考资源

1. 引言

1.1 什么是导航小车

导航小车是一种能够在环境中自主移动的移动机器人。它的核心能力包括:

  • 感知:通过传感器(激光雷达、摄像头、IMU 等)获取周围环境信息
  • 定位:确定自身在环境中的位置和姿态
  • 建图:构建环境的地图表示
  • 路径规划:计算从当前位置到目标位置的最优路径
  • 运动控制:控制电机驱动小车沿规划路径行驶

典型应用场景包括:仓储物流机器人、服务机器人、巡检机器人、扫地机器人等。

1.2 教程目标

完成本教程后,你将能够:

  1. 理解 SLAM 的数学原理和工程实现
  2. 使用 slam_toolbox 进行激光 SLAM 建图
  3. 了解视觉 SLAM 的基本原理和 RTAB-Map 的使用
  4. 使用 Nav2 实现自主导航
  5. 在 Gazebo 仿真环境中完成完整的建图-导航流程

1.3 前置知识

  • ROS2 基础(节点、话题、服务、Action、Launch 文件)
  • Linux 基本操作
  • Python 或 C++ 基础
  • 基本的线性代数知识(矩阵运算、坐标变换)

1.4 环境准备

本教程基于以下环境:

# 推荐系统
Ubuntu 22.04 LTS + ROS2 Humble Hawksbill

# 安装核心依赖
sudo apt update
sudo apt install -y \
  ros-humble-slam-toolbox \
  ros-humble-navigation2 \
  ros-humble-nav2-bringup \
  ros-humble-turtlebot3* \
  ros-humble-rtabmap-ros \
  ros-humble-gazebo-ros-pkgs \
  ros-humble-robot-localization \
  ros-humble-joint-state-publisher-gui

# 设置 TurtleBot3 模型(用于仿真示例)
echo 'export TURTLEBOT3_MODEL=burger' >> ~/.bashrc
source ~/.bashrc

2. SLAM 核心原理

SLAM(Simultaneous Localization and Mapping,同时定位与建图)是移动机器人领域最核心的问题之一。简单来说:机器人在一个未知环境中移动,同时构建环境地图并确定自身在地图中的位置。

这是一个"鸡生蛋、蛋生鸡"的问题——要定位需要地图,要建图需要知道位置。SLAM 就是同时解决这两个相互依赖的问题。

2.1 SLAM 问题的数学描述

SLAM 问题可以用概率框架来描述。设:

  • x_t:t 时刻机器人的位姿(位置 + 朝向)
  • u_t:t 时刻的控制输入(如轮速)
  • z_t:t 时刻的传感器观测(如激光扫描)
  • m:环境地图

SLAM 要求解的是后验概率分布:

P(x_{1:t}, m | z_{1:t}, u_{1:t})

即:给定所有的观测和控制输入,求机器人轨迹和地图的联合概率分布。

2.2 SLAM 系统架构

一个完整的 SLAM 系统通常包含以下模块:

传感器数据
    │
    ▼
┌─────────┐
│  前 端   │ ← 处理传感器数据,估计短期运动
│ Frontend │   (特征提取、帧间匹配、里程计)
└────┬────┘
     │ 位姿估计 + 约束关系
     ▼
┌─────────┐
│  后 端   │ ← 全局优化,消除累积误差
│ Backend  │   (图优化 / 滤波器)
└────┬────┘
     │
     ▼
┌──────────┐
│ 回环检测  │ ← 识别"回到曾经去过的地方"
│Loop Close│   提供额外约束,修正漂移
└────┬─────┘
     │
     ▼
┌─────────┐
│ 地图构建  │ ← 生成最终地图
│ Mapping  │   (栅格图/点云/拓扑图)
└─────────┘

2.3 前端:传感器数据处理

前端的任务是从原始传感器数据中提取有用信息,估计机器人的相对运动。

2.3.1 激光雷达前端

激光雷达(LiDAR)发射激光束并测量反射时间,得到周围环境的距离信息。

扫描匹配(Scan Matching) 是激光 SLAM 前端的核心:

  • ICP(Iterative Closest Point,迭代最近点):将两帧点云对齐,找到最优的旋转和平移变换
  • NDT(Normal Distributions Transform):将点云转换为正态分布表示,通过优化分布的匹配度来对齐
  • 相关性扫描匹配(Correlative Scan Matching):在搜索空间中暴力搜索最佳匹配位置
· · · · · ·
· · · ·
· 墙壁 · ──ICP──▶ · 墙壁 ·
· · 求解 ΔT · ·
· · · · · ·

2.3.2 视觉前端

视觉前端从图像中提取特征点,通过特征匹配估计相机运动。

特征点法的基本流程:

  1. 特征检测:在图像中找到角点、斑点等显著特征(ORB、SIFT、SURF)
  2. 特征描述:为每个特征点生成描述子(一个向量,描述该点周围的图像信息)
  3. 特征匹配:在两帧图像之间匹配相似的特征点
  4. 运动估计:根据匹配的特征点对,计算相机的运动(本质矩阵 / PnP)
图像帧 t-1                    图像帧 t
┌──────────────┐            ┌──────────────┐
│  ★        ◆  │            │   ★       ◆  │
│     ●        │  特征匹配   │      ●       │
│  ▲      ■   │ ─────────▶ │   ▲     ■    │
│        ◆    │            │        ◆     │
└──────────────┘            └──────────────┘
   ★──★  ◆──◆  ●──●  ▲──▲  ■──■  ← 匹配对

通过匹配对求解相机运动 R, t

直接法不提取特征点,而是直接利用像素灰度信息:

  • 假设同一个空间点在不同视角下的灰度值不变(灰度不变假设)
  • 通过最小化光度误差来估计相机运动
  • 优点:能利用更多像素信息,在纹理缺乏的场景也能工作
  • 缺点:对光照变化敏感,计算量较大

2.4 后端:状态估计与优化

前端提供的是局部的、相对的运动估计,会不可避免地累积误差(漂移)。后端的任务是利用全局信息来优化整个轨迹和地图。

2.4.1 基于滤波的方法

扩展卡尔曼滤波(EKF-SLAM)

  • 将机器人位姿和所有路标点的位置组成一个大的状态向量
  • 通过预测(运动模型)和更新(观测模型)两步迭代
  • 优点:理论优雅,实时性好
  • 缺点:状态向量随路标数量增长,计算复杂度为 O(n²),不适合大规模环境
  • 假设高斯分布,对非线性系统的近似可能不准确
机器人位姿 路标1位置 路标2位置

协方差矩阵 P = ┌ ┐
│ P_xx P_xl1 P_xl2 ... │
│ P_l1x P_l1l1 P_l1l2 ... │
│ P_l2x P_l2l1 P_l2l2 ... │
│ ... │
└ ┘

粒子滤波(Particle Filter SLAM)

  • 用一组带权重的粒子来表示概率分布
  • 每个粒子代表一种可能的机器人轨迹
  • GMapping 就是基于 Rao-Blackwellized 粒子滤波的经典 SLAM 算法
  • 优点:能处理非高斯、非线性问题
  • 缺点:粒子数量多时计算量大,高维空间中粒子退化严重

2.4.2 基于图优化的方法(主流)

图优化(Graph-based SLAM) 是目前最主流的 SLAM 后端方法:

  • 节点(Vertex):代表机器人在各时刻的位姿
  • 边(Edge):代表两个位姿之间的约束关系(来自里程计、扫描匹配、回环检测等)
图优化示意:

x1 ──边(里程计)──▶ x2 ──边(里程计)──▶ x3
│ │
│ 边(回环检测) │
└──────────────────────────────────────┘

优化目标:调整所有节点的位姿,使得所有边的约束误差之和最小


其中:

常用的图优化库:

  • g2o(General Graph Optimization)
  • GTSAM(Georgia Tech Smoothing and Mapping)
  • Ceres Solver(Google 的非线性优化库)

2.5 回环检测(Loop Closure)

回环检测是 SLAM 中消除累积漂移的关键。当机器人回到之前去过的地方时,回环检测能识别出这一事实,并提供一个强约束来修正整个轨迹。

没有回环检测时(漂移累积):

  起点 ──▶ ──▶ ──▶ ──▶ ──▶ ──▶ ┐
                                 │
  实际回到起点,但估计位置偏了 ◀──┘
  (起点和终点之间有明显间隙)

有回环检测时:

  起点 ──▶ ──▶ ──▶ ──▶ ──▶ ──▶ ┐
    ▲                            │
    └── 检测到回环!添加约束 ◀────┘
  (整条轨迹被优化,间隙消除)

激光 SLAM 的回环检测

  • 将当前扫描与历史扫描进行匹配
  • 使用分支定界(Branch and Bound)加速搜索(如 Cartographer)

视觉 SLAM 的回环检测

  • 词袋模型(Bag of Words, BoW):将图像特征量化为"视觉单词",通过比较"单词"的分布来判断两幅图像是否相似
  • DBoW2 / DBoW3:常用的视觉词袋库

2.6 地图表示

不同的地图表示方式适用于不同的场景:

地图类型描述适用场景示例
栅格地图将环境划分为网格,每个格子标记为占据/空闲/未知2D 导航、路径规划Nav2 使用的 costmap
点云地图由大量 3D 点组成的地图3D 环境表示、精细建模PCL 点云
特征地图只保存特征点/路标的位置定位、稀疏表示ORB-SLAM 的地图
拓扑地图用节点和边表示环境的连通关系大规模环境、高层规划房间-走廊连接图
八叉树地图用八叉树结构表示 3D 占据信息3D 导航、无人机OctoMap

对于导航小车,最常用的是 2D 占据栅格地图(Occupancy Grid Map)

栅格地图示例(0=空闲, 1=占据, -1=未知):

  -1 -1 -1 -1 -1 -1 -1 -1
  -1  1  1  1  1  1  1 -1
  -1  1  0  0  0  0  1 -1
  -1  1  0  0  0  0  1 -1
  -1  1  0  0  R  0  1 -1    R = 机器人位置
  -1  1  0  0  0  0  1 -1
  -1  1  1  1  D  1  1 -1    D = 门
  -1 -1 -1 -1 -1 -1 -1 -1

2.7 主流 SLAM 算法对比

算法传感器方法特点ROS2 支持
slam_toolbox2D 激光图优化ROS2 原生支持,功能全面✅ 官方推荐
Cartographer2D/3D 激光子图+回环Google 出品,精度高✅ 有移植
GMapping2D 激光粒子滤波经典算法,简单易用⚠️ 仅 ROS1
Hector SLAM2D 激光扫描匹配不需要里程计⚠️ 有限支持
ORB-SLAM3单目/双目/RGB-D特征点法精度高,支持多传感器⚠️ 需自行适配
RTAB-Map视觉+激光图优化多传感器融合,功能强大✅ 官方支持
LIO-SAM3D 激光+IMU因子图紧耦合激光惯性里程计✅ 有移植
FAST-LIO23D 激光+IMU迭代卡尔曼极快速度,适合嵌入式✅ 有移植

对于导航小车入门,推荐路线

  1. 先用 slam_toolbox(激光 SLAM)快速上手
  2. 再学习 RTAB-Map(视觉 SLAM)拓展能力

3. 激光 SLAM 实战

激光 SLAM 是导航小车最成熟、最常用的建图方案。本章以 slam_toolbox 为主,辅以 Cartographer 的介绍。

3.1 激光雷达基础

3.1.1 工作原理

激光雷达通过发射激光脉冲并测量反射时间来计算距离:

距离 = (光速 × 飞行时间) / 2

2D 激光雷达通过旋转扫描,在一个平面内获取 360°(或指定角度范围)的距离数据,形成一帧"激光扫描"。

3.1.2 常见 2D 激光雷达选型

型号测距范围扫描频率角分辨率参考价格适用场景
RPLidar A112m5.5Hz~500 元入门学习
RPLidar A218m10Hz0.45°~1200 元室内导航
思岚 RPLidar S140m10Hz0.39°~2000 元室内外通用
YDLIDAR X410m7Hz0.5°~400 元低成本入门
北醒 TFmini Plus12m(单点)1000Hz-~200 元避障辅助

入门推荐:RPLidar A1 或 YDLIDAR X4,性价比高,ROS2 驱动成熟。

3.1.3 ROS2 中的激光数据

激光雷达在 ROS2 中发布 sensor_msgs/msg/LaserScan 消息:

# LaserScan 消息结构
header:
  stamp: {sec: 1234567890, nanosec: 0}
  frame_id: "laser_frame"
angle_min: -3.14159      # 起始角度(弧度)
angle_max: 3.14159       # 终止角度
angle_increment: 0.0175  # 角度增量(约1°)
time_increment: 0.0001   # 测量时间增量
scan_time: 0.1           # 完整扫描时间
range_min: 0.15          # 最小测距
range_max: 12.0          # 最大测距
ranges: [1.2, 1.3, ...]  # 距离数组
intensities: [47, 52, ...]  # 强度数组(可选)

3.2 slam_toolbox 详解

slam_toolbox 是 ROS2 官方推荐的 2D SLAM 解决方案,由 Steve Macenski(Nav2 的主要开发者)开发。

3.2.1 核心特性

  • 基于图优化的 SLAM
  • 支持在线(实时)和离线(回放)建图
  • 支持增量建图(Lifelong SLAM):可以在已有地图上继续建图
  • 支持序列化:保存和加载地图及位姿图
  • 支持多种模式:建图模式、纯定位模式、持续建图模式

3.2.2 安装与配置

# 安装(如果之前没有安装)
sudo apt install ros-humble-slam-toolbox

# 查看默认配置文件位置
ros2 pkg prefix slam_toolbox
# 配置文件在:/opt/ros/humble/share/slam_toolbox/config/

3.2.3 核心参数详解

创建自定义配置文件 my_slam_params.yaml

slam_toolbox:
  ros__parameters:
    # ===== 基本设置 =====
    solver_plugin: solver_plugins::CeresSolver  # 优化求解器
    ceres_linear_solver: SPARSE_NORMAL_CHOLESKY  # Ceres 线性求解器类型
    ceres_preconditioner: SCHUR_JACOBI            # 预条件器
    ceres_trust_strategy: LEVENBERG_MARQUARDT     # 信赖域策略
    ceres_dogleg_type: TRADITIONAL_DOGLEG
    ceres_loss_function: None                     # 损失函数(鲁棒核)

    # ===== 话题和坐标系 =====
    odom_frame: odom            # 里程计坐标系
    map_frame: map              # 地图坐标系
    base_frame: base_footprint  # 机器人基座坐标系
    scan_topic: /scan           # 激光扫描话题
    use_map_saver: true         # 是否使用地图保存服务

    # ===== 建图模式 =====
    # mapping: 建图模式(默认)
    # localization: 纯定位模式(使用已有地图)
    mode: mapping

    # ===== 扫描匹配参数 =====
    # 这些参数直接影响建图质量
    resolution: 0.05            # 地图分辨率(米/像素),0.05 = 5cm
    max_laser_range: 12.0       # 最大激光使用范围(超出的数据丢弃)
    minimum_travel_distance: 0.5  # 最小移动距离才添加新节点(米)
    minimum_travel_heading: 0.5   # 最小旋转角度才添加新节点(弧度)
    scan_buffer_size: 10          # 扫描缓冲区大小
    scan_buffer_maximum_scan_distance: 10.0  # 缓冲区中扫描的最大距离

    # ===== 回环检测参数 =====
    do_loop_closing: true                # 是否启用回环检测
    loop_match_minimum_chain_size: 10    # 回环匹配最小链长度
    loop_match_maximum_variance_coarse: 3.0  # 粗匹配最大方差
    loop_match_minimum_response_coarse: 0.35 # 粗匹配最小响应值
    loop_match_minimum_response_fine: 0.45   # 精匹配最小响应值

    # ===== 相关性扫描匹配参数 =====
    correlation_search_space_dimension: 0.5    # 搜索空间大小(米)
    correlation_search_space_resolution: 0.01  # 搜索空间分辨率
    correlation_search_space_smear_deviation: 0.1  # 高斯模糊偏差

    # ===== 更新间隔 =====
    transform_publish_period: 0.02  # TF 发布周期(秒)
    map_update_interval: 5.0        # 地图更新间隔(秒)

    # ===== 性能调优 =====
    enable_interactive_mode: true   # 启用 RViz 交互模式
    use_scan_matching: true         # 使用扫描匹配(强烈建议开启)
    use_scan_barycenter: true       # 使用扫描重心

    # ===== 调试 =====
    debug_logging: false
    throttle_scans: 1  # 每 N 帧处理一次(1=全部处理)

关键参数调优建议

参数调大效果调小效果建议
resolution地图粗糙,计算快地图精细,计算慢室内 0.05,室外 0.1
minimum_travel_distance节点少,地图稀疏节点多,地图密集0.3~0.5
max_laser_range使用更远的数据只用近处数据设为雷达实际量程的 80%
loop_match_minimum_response_coarse回环检测更严格回环检测更宽松0.3~0.5

3.2.4 Launch 文件

创建 slam_launch.py

import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    # 声明参数
    use_sim_time = LaunchConfiguration('use_sim_time', default='false')
    slam_params_file = LaunchConfiguration('slam_params_file', default='')

    return LaunchDescription([
        DeclareLaunchArgument(
            'use_sim_time',
            default_value='false',
            description='Use simulation (Gazebo) clock if true'
        ),
        DeclareLaunchArgument(
            'slam_params_file',
            default_value=os.path.join(
                get_package_share_directory('my_robot_slam'),
                'config',
                'my_slam_params.yaml'
            ),
            description='Full path to the SLAM Toolbox parameters file'
        ),

        # slam_toolbox 节点
        Node(
            package='slam_toolbox',
            executable='async_slam_toolbox_node',  # 异步模式(推荐)
            name='slam_toolbox',
            output='screen',
            parameters=[
                slam_params_file,
                {'use_sim_time': use_sim_time}
            ],
        ),

        # RViz2 可视化
        Node(
            package='rviz2',
            executable='rviz2',
            name='rviz2',
            arguments=['-d', os.path.join(
                get_package_share_directory('my_robot_slam'),
                'rviz',
                'slam.rviz'
            )],
            parameters=[{'use_sim_time': use_sim_time}],
        ),
    ])

async vs sync 模式

  • async_slam_toolbox_node(异步):扫描处理不阻塞,适合实时建图
  • sync_slam_toolbox_node(同步):每帧都处理,适合离线建图或高精度需求

3.2.5 实际建图流程

# 终端 1:启动机器人底盘和激光雷达
ros2 launch my_robot_bringup robot.launch.py

# 终端 2:启动 SLAM
ros2 launch my_robot_slam slam_launch.py use_sim_time:=false

# 终端 3:用键盘遥控机器人建图
ros2 run teleop_twist_keyboard teleop_twist_keyboard

# 终端 4:建图完成后保存地图
ros2 run nav2_map_server map_saver_cli -f ~/maps/my_map

建图技巧

  1. 慢速移动:移动速度不要太快,给 SLAM 足够的处理时间
  2. 多次经过:重要区域多走几遍,增加约束
  3. 闭合回路:尽量走回起点,触发回环检测
  4. 避免长走廊:长走廊特征少,容易漂移,可以蛇形走位
  5. 注意动态障碍物:建图时尽量减少环境中的移动物体

保存的地图包含两个文件:

~/maps/my_map.pgm   # 地图图像(PGM 格式)
~/maps/my_map.yaml   # 地图元数据

地图 YAML 文件内容:

image: my_map.pgm
resolution: 0.050000          # 分辨率(米/像素)
origin: [-10.0, -10.0, 0.0]  # 地图原点 [x, y, yaw]
negate: 0
occupied_thresh: 0.65         # 占据阈值
free_thresh: 0.196            # 空闲阈值

3.3 Cartographer 简介

Google Cartographer 是另一个优秀的 SLAM 方案,采用子图(Submap)+ 回环检测的架构。

3.3.1 核心思想

Cartographer 架构:

  激光扫描 ──▶ Local SLAM ──▶ 子图 (Submap)
                  │                │
                  │                ▼
                  │         Global SLAM
                  │           (回环检测 + 优化)
                  │                │
                  ▼                ▼
              局部轨迹          全局一致的地图
  • Local SLAM:将连续的激光扫描插入到当前子图中,使用 Ceres 优化扫描匹配
  • Global SLAM:在后台运行,检测子图之间的回环,优化全局位姿图

3.3.2 ROS2 中使用 Cartographer

# 安装
sudo apt install ros-humble-cartographer ros-humble-cartographer-ros

# 配置文件示例 my_robot.lua
-- my_robot.lua
include "map_builder.lua"
include "trajectory_builder.lua"

options = {
  map_builder = MAP_BUILDER,
  trajectory_builder = TRAJECTORY_BUILDER,
  map_frame = "map",
  tracking_frame = "base_link",
  published_frame = "odom",
  odom_frame = "odom",
  provide_odom_frame = false,
  publish_frame_projected_to_2d = true,
  use_odometry = true,
  use_nav_sat = false,
  use_landmarks = false,
  num_laser_scans = 1,
  num_multi_echo_laser_scans = 0,
  num_subdivisions_per_laser_scan = 1,
  num_point_clouds = 0,
  lookup_transform_timeout_sec = 0.2,
  submap_publish_period_sec = 0.3,
  pose_publish_period_sec = 5e-3,
  trajectory_publish_period_sec = 30e-3,
  rangefinder_sampling_ratio = 1.,
  odometry_sampling_ratio = 1.,
  fixed_frame_pose_sampling_ratio = 1.,
  imu_sampling_ratio = 1.,
  landmarks_sampling_ratio = 1.,
}

-- 2D SLAM 配置
MAP_BUILDER.use_trajectory_builder_2d = true

TRAJECTORY_BUILDER_2D.min_range = 0.1
TRAJECTORY_BUILDER_2D.max_range = 12.0
TRAJECTORY_BUILDER_2D.missing_data_ray_length = 5.0
TRAJECTORY_BUILDER_2D.use_imu_data = false  -- 如果没有 IMU
TRAJECTORY_BUILDER_2D.use_online_correlative_scan_matching = true
TRAJECTORY_BUILDER_2D.motion_filter.max_distance_meters = 0.2
TRAJECTORY_BUILDER_2D.motion_filter.max_angle_radians = math.rad(1.0)

-- 子图参数
TRAJECTORY_BUILDER_2D.submaps.num_range_data = 90
TRAJECTORY_BUILDER_2D.submaps.grid_options_2d.resolution = 0.05

-- 回环检测
POSE_GRAPH.optimize_every_n_nodes = 90
POSE_GRAPH.constraint_builder.min_score = 0.65
POSE_GRAPH.constraint_builder.global_localization_min_score = 0.7

return options

3.4 slam_toolbox vs Cartographer 对比

特性slam_toolboxCartographer
ROS2 支持原生支持,官方推荐社区移植,维护较少
配置难度YAML,简单直观Lua,学习曲线较陡
增量建图✅ 支持 Lifelong SLAM❌ 不支持
纯定位模式✅ 内置✅ 支持
3D SLAM❌ 仅 2D✅ 支持 3D
建图质量优秀优秀(大环境略优)
计算资源较低较高
社区活跃度活跃Google 已停止维护

结论:对于 2D 导航小车,优先选择 slam_toolbox。


4. 视觉 SLAM 入门

视觉 SLAM 使用摄像头作为主要传感器,成本低、信息丰富,但算法复杂度更高。本章帮助你理解视觉 SLAM 的核心概念,并通过 RTAB-Map 在 ROS2 中实践。

4.1 相机模型与标定

4.1.1 针孔相机模型

相机将 3D 世界投影到 2D 图像平面,最基本的模型是针孔模型:

3D 世界点 P = [X, Y, Z]

投影到图像平面:
  u = fx * (X/Z) + cx
  v = fy * (Y/Z) + cy

相机内参矩阵 K:
  K = ┌ fx  0  cx ┐
      │  0  fy cy │
      └  0   0  1 ┘

其中:
  fx, fy = 焦距(像素单位)
  cx, cy = 主点(通常接近图像中心)

4.1.2 相机类型对比

类型优点缺点深度信息代表产品
单目相机成本低、体积小无法直接获取深度,存在尺度不确定性❌ 需要运动估计普通 USB 摄像头
双目相机可计算深度,成本适中计算量大,基线限制测距范围✅ 视差计算ZED 2, Intel T265
RGB-D 相机直接输出深度图,使用简单室外受阳光干扰,范围有限✅ 结构光/ToFIntel RealSense D435, Azure Kinect

导航小车推荐:Intel RealSense D435i(RGB-D + IMU),性价比高,ROS2 支持好。

4.1.3 相机标定

相机标定是视觉 SLAM 的前提,用于确定相机内参和畸变系数。

# 安装标定工具
sudo apt install ros-humble-camera-calibration

# 打印棋盘格标定板(8x6 内角点,方格边长 0.025m)
# 可以从网上下载打印

# 启动相机节点(以 RealSense 为例)
ros2 launch realsense2_camera rs_launch.py

# 启动标定工具
ros2 run camera_calibration cameracalibrator \
  --size 8x6 \
  --square 0.025 \
  --ros-args --remap image:=/camera/color/image_raw \
  --remap camera:=/camera/color

标定步骤:

  1. 在不同角度、距离、位置展示标定板
  2. 工具会自动检测角点并采集样本
  3. 当 X、Y、Size、Skew 四个进度条都变绿时,点击 CALIBRATE
  4. 标定完成后点击 SAVE,保存标定文件
  5. 点击 COMMIT 将标定参数写入相机驱动

4.2 视觉 SLAM 核心概念

4.2.1 特征点法 vs 直接法

特征点法(以 ORB-SLAM3 为代表):

图像 ──▶ 特征检测 ──▶ 特征描述 ──▶ 特征匹配 ──▶ 运动估计
         (ORB)       (二进制描述子)  (汉明距离)    (PnP/对极几何)

优点:
  - 对光照变化有一定鲁棒性
  - 匹配准确,定位精度高
  - 回环检测成熟(词袋模型)

缺点:
  - 特征提取耗时
  - 纹理缺乏的场景(白墙)特征点少
  - 只利用了图像中的少量信息

直接法(以 LSD-SLAM、DSO 为代表):

图像 ──▶ 选取像素 ──▶ 光度误差最小化 ──▶ 运动估计
         (梯度大的像素)  (灰度不变假设)

优点:
  - 不需要特征提取,速度可以更快
  - 能利用更多像素信息
  - 在弱纹理场景也能工作(半稠密/稠密方法)

缺点:
  - 对光照变化非常敏感
  - 需要好的初始值
  - 回环检测较困难

4.2.2 视觉里程计(Visual Odometry, VO)

视觉里程计是视觉 SLAM 的前端,估计相机的帧间运动:

单目 VO 流程:

帧 t-1          帧 t
  │               │
  ▼               ▼
特征提取        特征提取
  │               │
  └──── 特征匹配 ──┘
          │
          ▼
    本质矩阵 E = t^ · R
          │
          ▼
    分解得到 R, t(旋转和平移)
          │
          ▼
    三角化得到 3D 点
RGB-D VO 流程(更简单):

帧 t-1 (RGB + Depth)     帧 t (RGB + Depth)
  │                         │
  ▼                         ▼
特征提取 + 深度关联        特征提取 + 深度关联
  │                         │
  └──── 3D-3D 匹配 ────────┘
          │
          ▼
    ICP 或 PnP 求解 R, t

4.2.3 关键帧策略

不是每一帧都需要处理,视觉 SLAM 通常使用关键帧策略:

  • 当相机移动足够距离或旋转足够角度时,插入新关键帧
  • 当跟踪到的特征点数量下降到阈值以下时,插入新关键帧
  • 关键帧用于后端优化和地图维护
  • 非关键帧只用于实时跟踪

4.3 RTAB-Map 实战

RTAB-Map(Real-Time Appearance-Based Mapping)是一个多传感器 SLAM 方案,支持激光雷达、RGB-D 相机、双目相机等,在 ROS2 中有良好的支持。

4.3.1 RTAB-Map 架构

传感器输入
├── RGB-D 相机 ──▶ 视觉里程计 (Visual Odometry)
├── 双目相机   ──▶ 视觉里程计
├── 激光雷达   ──▶ ICP 里程计
└── IMU        ──▶ 预积分

        │
        ▼
  短期记忆 (STM)  ← 最近的关键帧
        │
        ▼
  工作记忆 (WM)   ← 活跃的关键帧(用于回环检测)
        │
        ▼
  长期记忆 (LTM)  ← 不活跃的关键帧(存入数据库)

回环检测:基于视觉词袋模型,在 WM 中搜索相似帧
图优化:检测到回环后,优化全局位姿图

RTAB-Map 的独特之处在于其内存管理机制:通过 STM → WM → LTM 的转移,保证实时性,适合大规模环境。

4.3.2 安装与配置

# 安装
sudo apt install ros-humble-rtabmap-ros

# 安装 RealSense 驱动(如果使用 RealSense 相机)
sudo apt install ros-humble-realsense2-camera

4.3.3 使用 RGB-D 相机建图

创建 rtabmap_rgbd_launch.py

import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def generate_launch_description():
    use_sim_time = LaunchConfiguration('use_sim_time', default='false')

    return LaunchDescription([
        DeclareLaunchArgument('use_sim_time', default_value='false'),

        # RealSense 相机节点
        Node(
            package='realsense2_camera',
            executable='realsense2_camera_node',
            name='camera',
            parameters=[{
                'enable_color': True,
                'enable_depth': True,
                'enable_infra1': False,
                'enable_infra2': False,
                'enable_gyro': True,
                'enable_accel': True,
                'align_depth.enable': True,  # 对齐深度到彩色
                'rgb_camera.profile': '640x480x30',
                'depth_module.profile': '640x480x30',
            }],
        ),

        # RTAB-Map 视觉里程计
        Node(
            package='rtabmap_odom',
            executable='rgbd_odometry',
            name='rgbd_odometry',
            output='screen',
            parameters=[{
                'use_sim_time': use_sim_time,
                'frame_id': 'base_link',
                'odom_frame_id': 'odom',
                'publish_tf': True,
                'approx_sync': True,
                'Odom/Strategy': '0',       # 0=Frame-to-Map, 1=Frame-to-Frame
                'OdomF2M/MaxSize': '1000',   # Frame-to-Map 最大特征数
                'Vis/MaxFeatures': '600',    # 最大视觉特征数
                'Vis/MinInliers': '20',      # 最小内点数
            }],
            remappings=[
                ('rgb/image', '/camera/color/image_raw'),
                ('rgb/camera_info', '/camera/color/camera_info'),
                ('depth/image', '/camera/aligned_depth_to_color/image_raw'),
            ],
        ),

        # RTAB-Map SLAM 节点
        Node(
            package='rtabmap_slam',
            executable='rtabmap',
            name='rtabmap',
            output='screen',
            parameters=[{
                'use_sim_time': use_sim_time,
                'frame_id': 'base_link',
                'odom_frame_id': 'odom',
                'map_frame_id': 'map',
                'subscribe_depth': True,
                'subscribe_rgb': True,
                'approx_sync': True,
                'queue_size': 10,

                # 建图参数
                'Mem/IncrementalMemory': 'true',   # 增量建图模式
                'Mem/InitWMWithAllNodes': 'false',

                # 回环检测
                'Kp/MaxFeatures': '400',           # 关键点最大数量
                'Kp/DetectorStrategy': '6',        # 6=ORB
                'RGBD/NeighborLinkRefining': 'true',
                'RGBD/ProximityBySpace': 'true',
                'RGBD/OptimizeFromGraphEnd': 'false',

                # 栅格地图生成(用于导航)
                'Grid/FromDepth': 'true',          # 从深度图生成栅格
                'Grid/MaxGroundHeight': '0.05',    # 地面最大高度
                'Grid/MaxObstacleHeight': '1.5',   # 障碍物最大高度
                'Grid/CellSize': '0.05',           # 栅格分辨率
                'Grid/RangeMax': '5.0',            # 最大范围
            }],
            remappings=[
                ('rgb/image', '/camera/color/image_raw'),
                ('rgb/camera_info', '/camera/color/camera_info'),
                ('depth/image', '/camera/aligned_depth_to_color/image_raw'),
            ],
        ),

        # RViz2
        Node(
            package='rviz2',
            executable='rviz2',
            name='rviz2',
            parameters=[{'use_sim_time': use_sim_time}],
        ),
    ])

4.3.4 建图与定位切换

# 建图模式(默认)
ros2 launch my_robot_slam rtabmap_rgbd_launch.py

# 遥控机器人建图...

# 建图完成后,RTAB-Map 自动保存数据库到 ~/.ros/rtabmap.db

# 定位模式(使用已有地图)
ros2 launch my_robot_slam rtabmap_rgbd_launch.py \
  localization:=true

在定位模式下,需要将 RTAB-Map 参数修改为:

# 定位模式参数
'Mem/IncrementalMemory': 'false',   # 关闭增量建图
'Mem/InitWMWithAllNodes': 'true',   # 加载所有节点到工作记忆

4.3.5 RTAB-Map 可视化工具

RTAB-Map 提供了独立的可视化工具 rtabmap-databaseViewer,可以查看和分析建图结果:

# 安装可视化工具
sudo apt install rtabmap

# 打开数据库查看器
rtabmap-databaseViewer ~/.ros/rtabmap.db

在查看器中可以:

  • 查看所有关键帧和它们的特征点
  • 查看回环检测结果
  • 查看位姿图和优化结果
  • 导出 3D 点云和 2D 栅格地图

4.4 ORB-SLAM3 简介

ORB-SLAM3 是目前最完整的视觉 SLAM 系统之一,支持单目、双目、RGB-D 相机以及 IMU 融合。

4.4.1 系统架构

ORB-SLAM3 三线程架构:

线程 1:跟踪 (Tracking)
  - ORB 特征提取
  - 初始位姿估计(恒速模型 / 参考关键帧 / 重定位)
  - 局部地图跟踪
  - 关键帧决策

线程 2:局部建图 (Local Mapping)
  - 处理新关键帧
  - 局部 BA(Bundle Adjustment)
  - 地图点筛选
  - 新地图点创建

线程 3:回环检测与地图合并 (Loop Closing & Map Merging)
  - DBoW2 词袋检测回环
  - Sim(3) / SE(3) 位姿计算
  - 全局 BA 优化
  - 多地图合并(Atlas 系统)

4.4.2 在 ROS2 中使用

ORB-SLAM3 没有官方的 ROS2 包,需要使用社区移植版本:

# 安装依赖
sudo apt install libopencv-dev libeigen3-dev libpangolin-dev

# 克隆社区 ROS2 wrapper
cd ~/ros2_ws/src
git clone https://github.com/zang09/ORB_SLAM3_ROS2.git

# 编译
cd ~/ros2_ws
colcon build --packages-select orbslam3

# 运行(以 RGB-D 模式为例)
ros2 run orbslam3 rgbd \
  --ros-args \
  -p voc_file:=path/to/ORBvoc.txt \
  -p settings_file:=path/to/RealSense_D435i.yaml

注意:ORB-SLAM3 的 ROS2 适配可能需要根据具体版本调整,建议优先使用 RTAB-Map。

4.5 视觉 SLAM 常见问题

问题可能原因解决方案
跟踪丢失快速运动、特征点不足降低运动速度,增加特征点数量
尺度漂移(单目)单目相机无法确定绝对尺度使用 RGB-D 或双目相机,或融合 IMU
建图不一致回环检测失败确保回到已建图区域,调整回环检测阈值
深度图噪声大反光表面、透明物体调整深度滤波参数,避免对着玻璃/镜面
计算资源不足分辨率过高、特征点过多降低图像分辨率,减少最大特征点数

5. Nav2 导航栈

Nav2(Navigation2)是 ROS2 的官方导航框架,提供了从路径规划到运动控制的完整导航能力。SLAM 建好地图后,Nav2 负责让小车在地图中自主移动。

5.1 Nav2 架构概览

                    ┌─────────────────────────────────┐
                    │         BT Navigator             │
                    │   (行为树,协调整个导航流程)      │
                    └──────────┬──────────────────────┘
                               │
            ┌──────────────────┼──────────────────┐
            ▼                  ▼                  ▼
    ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
    │   Planner    │  │  Controller  │  │  Recovery    │
    │  Server      │  │  Server      │  │  Server      │
    │ (全局规划)  │  │ (局部控制)  │  │ (恢复行为)  │
    └──────┬───────┘  └──────┬───────┘  └──────┬───────┘
           │                 │                 │
           ▼                 ▼                 ▼
    ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
    │  NavFn /     │  │  DWB /       │  │  Spin /      │
    │  Smac /      │  │  MPPI /      │  │  BackUp /    │
    │  Theta*      │  │  RPP         │  │  Wait        │
    └──────────────┘  └──────────────┘  └──────────────┘
           │                 │
           ▼                 ▼
    ┌─────────────────────────────────┐
    │         Costmap 2D              │
    │  ┌───────────┐ ┌────────────┐  │
    │  │ Global    │ │ Local      │  │
    │  │ Costmap   │ │ Costmap    │  │
    │  └───────────┘ └────────────┘  │
    └─────────────────────────────────┘
           │                 │
           ▼                 ▼
    ┌─────────────────────────────────┐
    │    Map Server + AMCL            │
    │  (地图服务 + 自适应蒙特卡洛定位)│
    └─────────────────────────────────┘

5.2 核心组件详解

5.2.1 Map Server(地图服务器)

加载 SLAM 建好的地图,提供给导航系统使用:

# map_server 参数
map_server:
  ros__parameters:
    yaml_filename: "/home/user/maps/my_map.yaml"
    topic_name: "map"
    frame_id: "map"

5.2.2 AMCL(自适应蒙特卡洛定位)

AMCL 使用粒子滤波在已知地图中定位机器人:

AMCL 工作原理:

1. 初始化:在地图上撒大量粒子(每个粒子代表一种可能的位姿)

   ·  ·  ·  ·  ·  ·  ·  ·  ← 粒子均匀分布
   ·  ·  ·  ·  ·  ·  ·  ·
   ·  ·  ·  ·  ·  ·  ·  ·

2. 预测:根据里程计移动所有粒子

3. 更新:用激光扫描数据评估每个粒子的权重
   - 粒子位姿与实际观测匹配度高 → 权重大
   - 粒子位姿与实际观测匹配度低 → 权重小

4. 重采样:丢弃低权重粒子,复制高权重粒子

   ·                       ← 粒子聚集到正确位置
   · · ·
   · · · · ·  ← 机器人真实位置附近
   · · ·
   ·

5. 重复 2-4,粒子逐渐收敛到真实位姿
# AMCL 关键参数
amcl:
  ros__parameters:
    # 粒子数量
    min_particles: 500
    max_particles: 2000

    # 里程计模型
    robot_model_type: "nav2_amcl::DifferentialMotionModel"
    alpha1: 0.2   # 旋转噪声(由旋转引起)
    alpha2: 0.2   # 旋转噪声(由平移引起)
    alpha3: 0.2   # 平移噪声(由平移引起)
    alpha4: 0.2   # 平移噪声(由旋转引起)
    alpha5: 0.2   # 平移噪声(原地旋转)

    # 激光模型
    laser_model_type: "likelihood_field"
    laser_max_range: 12.0
    max_beams: 60
    z_hit: 0.5
    z_rand: 0.5
    sigma_hit: 0.2

    # 更新阈值
    update_min_d: 0.25    # 最小移动距离才更新(米)
    update_min_a: 0.2     # 最小旋转角度才更新(弧度)

    # 坐标系
    base_frame_id: "base_footprint"
    odom_frame_id: "odom"
    global_frame_id: "map"

    # 初始位姿(可选,也可在 RViz 中设置)
    set_initial_pose: true
    initial_pose:
      x: 0.0
      y: 0.0
      z: 0.0
      yaw: 0.0

5.2.3 Costmap 2D(代价地图)

Costmap 是导航的核心数据结构,将环境信息转化为"代价值"供规划器使用:

代价值含义:
  0        = 空闲空间(Free)
  1-252    = 代价递增(离障碍物越近代价越高)
  253      = 内切障碍物(Inscribed)
  254      = 致命障碍物(Lethal)
  255      = 未知(No Information)

代价地图可视化(俯视图):

  ████████████████████████
  █                      █
  █   ░░░░░              █    █ = 致命障碍物 (254)
  █   ░░██░░             █    ░ = 膨胀区域 (1-253)
  █   ░░░░░░             █      = 空闲空间 (0)
  █          ░░░░░       █
  █          ░░██░░      █
  █          ░░░░░░      █
  █                      █
  ████████████████████████

Costmap 由多个**层(Layer)**叠加而成:

# Global Costmap 配置
global_costmap:
  global_costmap:
    ros__parameters:
      update_frequency: 1.0
      publish_frequency: 1.0
      global_frame: map
      robot_base_frame: base_link
      robot_radius: 0.15          # 机器人半径
      resolution: 0.05
      track_unknown_space: true
      plugins: ["static_layer", "obstacle_layer", "inflation_layer"]

      # 静态层:加载 SLAM 建好的地图
      static_layer:
        plugin: "nav2_costmap_2d::StaticLayer"
        map_subscribe_transient_local: true

      # 障碍物层:实时传感器数据
      obstacle_layer:
        plugin: "nav2_costmap_2d::ObstacleLayer"
        enabled: true
        observation_sources: scan
        scan:
          topic: /scan
          max_obstacle_height: 2.0
          clearing: true
          marking: true
          data_type: "LaserScan"
          raytrace_max_range: 5.0
          obstacle_max_range: 4.5

      # 膨胀层:在障碍物周围添加安全距离
      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        cost_scaling_factor: 3.0    # 代价衰减速度
        inflation_radius: 0.55      # 膨胀半径

# Local Costmap 配置
local_costmap:
  local_costmap:
    ros__parameters:
      update_frequency: 5.0
      publish_frequency: 2.0
      global_frame: odom
      robot_base_frame: base_link
      robot_radius: 0.15
      resolution: 0.05
      rolling_window: true        # 滚动窗口模式
      width: 3                    # 窗口宽度(米)
      height: 3                   # 窗口高度(米)
      plugins: ["voxel_layer", "inflation_layer"]

      voxel_layer:
        plugin: "nav2_costmap_2d::VoxelLayer"
        enabled: true
        observation_sources: scan
        scan:
          topic: /scan
          max_obstacle_height: 2.0
          clearing: true
          marking: true
          data_type: "LaserScan"

      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        cost_scaling_factor: 3.0
        inflation_radius: 0.55

5.2.4 Planner Server(全局路径规划)

全局规划器在 Global Costmap 上计算从起点到终点的路径:

规划器算法特点适用场景
NavFnDijkstra / A*经典算法,稳定可靠通用场景
Smac Planner 2DA* 变体支持多种启发式2D 导航
Smac Hybrid-A*Hybrid A*考虑运动学约束阿克曼转向车辆
Smac Lattice状态格子精确运动学模型复杂运动学
Theta*Theta*任意角度路径,更平滑全向移动机器人
planner_server:
  ros__parameters:
    expected_planner_frequency: 20.0
    planner_plugins: ["GridBased"]
    GridBased:
      plugin: "nav2_navfn_planner/NavfnPlanner"
      tolerance: 0.5            # 目标容差(米)
      use_astar: true           # 使用 A*(false 则用 Dijkstra)
      allow_unknown: true       # 允许规划经过未知区域

5.2.5 Controller Server(局部运动控制)

局部控制器根据全局路径生成实时速度指令:

控制器特点适用场景
DWB动态窗口法,经典可靠差速驱动小车
MPPI模型预测路径积分,效果好各种底盘类型
RPP纯追踪法,简单高效路径跟踪
Rotation Shim先原地旋转对准方向配合其他控制器使用
controller_server:
  ros__parameters:
    controller_frequency: 20.0
    min_x_velocity_threshold: 0.001
    min_y_velocity_threshold: 0.5
    min_theta_velocity_threshold: 0.001
    controller_plugins: ["FollowPath"]

    # DWB 控制器配置
    FollowPath:
      plugin: "dwb_core::DWBLocalPlanner"
      debug_trajectory_details: true
      min_vel_x: 0.0
      min_vel_y: 0.0
      max_vel_x: 0.26            # 最大前进速度
      max_vel_y: 0.0             # 差速驱动无侧向速度
      max_vel_theta: 1.0         # 最大旋转速度
      min_speed_xy: 0.0
      max_speed_xy: 0.26
      min_speed_theta: 0.0
      acc_lim_x: 2.5             # 前进加速度限制
      acc_lim_y: 0.0
      acc_lim_theta: 3.2         # 旋转加速度限制
      decel_lim_x: -2.5
      decel_lim_y: 0.0
      decel_lim_theta: -3.2
      vx_samples: 20             # 速度采样数
      vy_samples: 5
      vtheta_samples: 20
      sim_time: 1.7              # 模拟时间
      linear_granularity: 0.05
      angular_granularity: 0.025
      transform_tolerance: 0.2
      xy_goal_tolerance: 0.25    # 到达目标的位置容差
      trans_stopped_velocity: 0.25
      short_circuit_trajectory_evaluation: true
      critics: ["RotateToGoal", "Oscillation", "BaseObstacle",
                "GoalAlign", "PathAlign", "PathDist", "GoalDist"]

5.2.6 Recovery Server(恢复行为)

当导航遇到困难时(如被卡住),恢复行为帮助机器人脱困:

recoveries_server:
  ros__parameters:
    recovery_plugins: ["spin", "backup", "wait"]
    spin:
      plugin: "nav2_recoveries/Spin"
    backup:
      plugin: "nav2_recoveries/BackUp"
    wait:
      plugin: "nav2_recoveries/Wait"

5.3 完整导航 Launch 文件

import os
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription, DeclareLaunchArgument
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    nav2_bringup_dir = get_package_share_directory('nav2_bringup')
    my_robot_dir = get_package_share_directory('my_robot_navigation')

    use_sim_time = LaunchConfiguration('use_sim_time', default='false')
    map_file = LaunchConfiguration('map', default=os.path.join(
        my_robot_dir, 'maps', 'my_map.yaml'))
    params_file = LaunchConfiguration('params_file', default=os.path.join(
        my_robot_dir, 'config', 'nav2_params.yaml'))

    return LaunchDescription([
        DeclareLaunchArgument('use_sim_time', default_value='false'),
        DeclareLaunchArgument('map', default_value=map_file),
        DeclareLaunchArgument('params_file', default_value=params_file),

        # 启动 Nav2
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource(
                os.path.join(nav2_bringup_dir, 'launch', 'bringup_launch.py')
            ),
            launch_arguments={
                'use_sim_time': use_sim_time,
                'map': map_file,
                'params_file': params_file,
            }.items(),
        ),

        # RViz2
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource(
                os.path.join(nav2_bringup_dir, 'launch', 'rviz_launch.py')
            ),
            launch_arguments={
                'use_sim_time': use_sim_time,
            }.items(),
        ),
    ])

5.4 导航实操流程

# 步骤 1:启动机器人底盘和传感器
ros2 launch my_robot_bringup robot.launch.py

# 步骤 2:启动导航
ros2 launch my_robot_navigation navigation_launch.py \
  map:=/home/user/maps/my_map.yaml \
  use_sim_time:=false

# 步骤 3:在 RViz 中操作
# 3a. 点击 "2D Pose Estimate" 设置初始位姿
#     (在地图上点击并拖动,指示机器人当前位置和朝向)
# 3b. 观察 AMCL 粒子收敛
# 3c. 点击 "Nav2 Goal" 设置导航目标
#     (在地图上点击并拖动,指示目标位置和朝向)
# 3d. 机器人开始自主导航!

5.5 通过代码发送导航目标

#!/usr/bin/env python3
"""通过代码发送导航目标点"""

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseStamped
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult
from rclpy.duration import Duration

def main():
    rclpy.init()
    navigator = BasicNavigator()

    # 等待 Nav2 启动完成
    navigator.waitUntilNav2Active()

    # --- 设置初始位姿 ---
    initial_pose = PoseStamped()
    initial_pose.header.frame_id = 'map'
    initial_pose.header.stamp = navigator.get_clock().now().to_msg()
    initial_pose.pose.position.x = 0.0
    initial_pose.pose.position.y = 0.0
    initial_pose.pose.orientation.w = 1.0
    navigator.setInitialPose(initial_pose)

    # --- 单点导航 ---
    goal_pose = PoseStamped()
    goal_pose.header.frame_id = 'map'
    goal_pose.header.stamp = navigator.get_clock().now().to_msg()
    goal_pose.pose.position.x = 2.0
    goal_pose.pose.position.y = 1.0
    goal_pose.pose.orientation.w = 1.0

    navigator.goToPose(goal_pose)

    # 等待导航完成
    while not navigator.isTaskComplete():
        feedback = navigator.getFeedback()
        if feedback:
            # 打印剩余距离
            print(f'剩余距离: {feedback.distance_remaining:.2f}m')

        if Duration.from_msg(feedback.navigation_time) > Duration(seconds=120.0):
            navigator.cancelTask()
            print('导航超时,取消任务')
            break

    result = navigator.getResult()
    if result == TaskResult.SUCCEEDED:
        print('导航成功!')
    elif result == TaskResult.CANCELED:
        print('导航被取消')
    elif result == TaskResult.FAILED:
        print('导航失败')

    # --- 多点巡航(Waypoint Following)---
    waypoints = []
    for pt in [(1.0, 0.0), (2.0, 1.0), (1.0, 2.0), (0.0, 1.0)]:
        pose = PoseStamped()
        pose.header.frame_id = 'map'
        pose.header.stamp = navigator.get_clock().now().to_msg()
        pose.pose.position.x = pt[0]
        pose.pose.position.y = pt[1]
        pose.pose.orientation.w = 1.0
        waypoints.append(pose)

    navigator.followWaypoints(waypoints)

    while not navigator.isTaskComplete():
        feedback = navigator.getFeedback()
        if feedback:
            print(f'正在前往第 {feedback.current_waypoint + 1}/{len(waypoints)} 个点')

    print('巡航完成!')
    navigator.lifecycleShutdown()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

5.6 TF 坐标系关系

Nav2 依赖正确的 TF 树,这是导航能否正常工作的关键:

TF 树(导航小车典型结构):

  map
   │
   │  (由 AMCL 或 SLAM 发布)
   ▼
  odom
   │
   │  (由里程计/轮式编码器发布)
   ▼
  base_footprint
   │
   │  (静态 TF)
   ▼
  base_link
   ├──────────────┬──────────────┐
   │              │              │
   ▼              ▼              ▼
 laser_frame   camera_link    imu_link
               │
               ▼
            camera_optical_frame

关键变换:
  map → odom        : 全局定位修正(AMCL 发布)
  odom → base_link  : 里程计(连续但有漂移)
  base_link → 传感器 : 静态变换(URDF 定义)

确保 TF 正确的检查方法:

# 查看 TF 树
ros2 run tf2_tools view_frames

# 查看特定变换
ros2 run tf2_ros tf2_echo map base_link

# 检查 TF 是否有断裂
ros2 run tf2_tools view_frames  # 生成 frames.pdf

6. 导航小车硬件搭建

6.1 硬件清单

一个典型的 ROS2 导航小车包含以下硬件:

组件推荐选型参考价格说明
主控Jetson Nano / Raspberry Pi 4B500-800 元运行 ROS2 和 SLAM
底盘两轮差速底盘200-500 元带编码器电机
电机驱动L298N / TB6612 / STM32 驱动板30-100 元PWM 控制
激光雷达RPLidar A1 / YDLIDAR X4400-500 元2D SLAM 建图
RGB-D 相机Intel RealSense D435i(可选)1500-2000 元视觉 SLAM / 避障
IMUMPU6050 / BNO055(可选)20-150 元姿态辅助
电池12V 锂电池100-200 元供电
降压模块DC-DC 降压模块20 元5V 给主控供电

入门最小配置:主控 + 底盘 + 激光雷达,约 1200 元即可开始。

6.2 系统架构

┌─────────────────────────────────────────────┐
│              上位机 (Jetson / RPi)            │
│                                             │
│  ROS2 节点:                                 │
│  ├── slam_toolbox(SLAM 建图)               │
│  ├── nav2(导航)                            │
│  ├── rplidar_ros(激光雷达驱动)              │
│  ├── realsense2_camera(相机驱动,可选)      │
│  └── micro_ros_agent(与下位机通信)          │
│                                             │
│         USB/UART                            │
└──────────┬──────────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────────┐
│            下位机 (STM32 / Arduino)          │
│                                             │
│  功能:                                      │
│  ├── 电机 PWM 控制                           │
│  ├── 编码器读取 → 里程计计算                  │
│  ├── IMU 数据读取(可选)                     │
│  └── micro-ROS 通信                         │
│                                             │
│  发布话题:/odom, /imu/data                  │
│  订阅话题:/cmd_vel                          │
└─────────────────────────────────────────────┘

6.3 URDF 机器人模型

URDF(Unified Robot Description Format)描述机器人的物理结构:

<?xml version="1.0"?>
<robot name="my_nav_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">

  <!-- 基座 -->
  <link name="base_footprint"/>

  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.25 0.20 0.07"/>
      </geometry>
      <material name="blue">
        <color rgba="0.0 0.0 0.8 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.25 0.20 0.07"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="2.0"/>
      <inertia ixx="0.01" ixy="0" ixz="0"
               iyy="0.01" iyz="0" izz="0.01"/>
    </inertial>
  </link>

  <joint name="base_joint" type="fixed">
    <parent link="base_footprint"/>
    <child link="base_link"/>
    <origin xyz="0 0 0.05" rpy="0 0 0"/>
  </joint>

  <!-- 左轮 -->
  <link name="left_wheel">
    <visual>
      <geometry>
        <cylinder radius="0.033" length="0.026"/>
      </geometry>
      <material name="black">
        <color rgba="0.1 0.1 0.1 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.033" length="0.026"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.1"/>
      <inertia ixx="0.0001" ixy="0" ixz="0"
               iyy="0.0001" iyz="0" izz="0.0001"/>
    </inertial>
  </link>

  <joint name="left_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="left_wheel"/>
    <origin xyz="0 0.11 -0.017" rpy="-1.5708 0 0"/>
    <axis xyz="0 0 1"/>
  </joint>

  <!-- 右轮(与左轮对称) -->
  <link name="right_wheel">
    <visual>
      <geometry>
        <cylinder radius="0.033" length="0.026"/>
      </geometry>
      <material name="black">
        <color rgba="0.1 0.1 0.1 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.033" length="0.026"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.1"/>
      <inertia ixx="0.0001" ixy="0" ixz="0"
               iyy="0.0001" iyz="0" izz="0.0001"/>
    </inertial>
  </link>

  <joint name="right_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="right_wheel"/>
    <origin xyz="0 -0.11 -0.017" rpy="-1.5708 0 0"/>
    <axis xyz="0 0 1"/>
  </joint>

  <!-- 万向轮(前方支撑) -->
  <link name="caster_wheel">
    <visual>
      <geometry>
        <sphere radius="0.016"/>
      </geometry>
      <material name="grey">
        <color rgba="0.5 0.5 0.5 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <sphere radius="0.016"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.05"/>
      <inertia ixx="0.00001" ixy="0" ixz="0"
               iyy="0.00001" iyz="0" izz="0.00001"/>
    </inertial>
  </link>

  <joint name="caster_joint" type="fixed">
    <parent link="base_link"/>
    <child link="caster_wheel"/>
    <origin xyz="0.1 0 -0.034" rpy="0 0 0"/>
  </joint>

  <!-- 激光雷达 -->
  <link name="laser_frame">
    <visual>
      <geometry>
        <cylinder radius="0.035" length="0.04"/>
      </geometry>
      <material name="red">
        <color rgba="0.8 0.0 0.0 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.035" length="0.04"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.15"/>
      <inertia ixx="0.0001" ixy="0" ixz="0"
               iyy="0.0001" iyz="0" izz="0.0001"/>
    </inertial>
  </link>

  <joint name="laser_joint" type="fixed">
    <parent link="base_link"/>
    <child link="laser_frame"/>
    <origin xyz="0 0 0.06" rpy="0 0 0"/>
  </joint>

  <!-- RGB-D 相机(可选) -->
  <link name="camera_link">
    <visual>
      <geometry>
        <box size="0.025 0.09 0.025"/>
      </geometry>
      <material name="grey">
        <color rgba="0.5 0.5 0.5 1.0"/>
      </material>
    </visual>
  </link>

  <joint name="camera_joint" type="fixed">
    <parent link="base_link"/>
    <child link="camera_link"/>
    <origin xyz="0.12 0 0.04" rpy="0 0 0"/>
  </joint>

  <link name="camera_optical_frame"/>
  <joint name="camera_optical_joint" type="fixed">
    <parent link="camera_link"/>
    <child link="camera_optical_frame"/>
    <!-- 相机光学坐标系:Z 朝前,X 朝右,Y 朝下 -->
    <origin xyz="0 0 0" rpy="-1.5708 0 -1.5708"/>
  </joint>

</robot>

6.4 差速驱动里程计

差速驱动小车的运动学模型:

两轮差速模型:

┌─────────┐
v_L │ │ v_R
◄──── │ base │ ────►
左轮 │ │ 右轮
└─────────┘
L(轮距)


位姿更新(欧拉积分):

下位机里程计计算示例(伪代码):

// STM32 / Arduino 端里程计计算
void updateOdometry() {
    // 读取编码器脉冲
    int left_ticks = readEncoder(LEFT);
    int right_ticks = readEncoder(RIGHT);

    // 转换为距离(米)
    double left_dist = left_ticks * METERS_PER_TICK;
    double right_dist = right_ticks * METERS_PER_TICK;

    // 计算线速度和角速度
    double linear = (right_dist + left_dist) / 2.0;
    double angular = (right_dist - left_dist) / WHEEL_BASE;

    // 更新位姿
    theta += angular;
    x += linear * cos(theta);
    y += linear * sin(theta);

    // 发布 odom 话题(通过 micro-ROS)
    publishOdom(x, y, theta, linear / dt, angular / dt);
}

7. Gazebo 仿真环境

在没有实体硬件的情况下,Gazebo 仿真是学习 SLAM 和导航的最佳方式。

7.1 使用 TurtleBot3 快速开始

TurtleBot3 是 ROS2 官方推荐的教学机器人平台,提供了完整的仿真支持:

# 确保已安装
sudo apt install ros-humble-turtlebot3* ros-humble-gazebo-ros-pkgs

# 设置模型
export TURTLEBOT3_MODEL=burger

# 终端 1:启动 Gazebo 仿真世界
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

# 终端 2:启动 SLAM(slam_toolbox)
ros2 launch turtlebot3_cartographer cartographer.launch.py use_sim_time:=true
# 或者使用 slam_toolbox:
ros2 launch slam_toolbox online_async_launch.py use_sim_time:=true

# 终端 3:键盘遥控建图
ros2 run turtlebot3_teleop teleop_keyboard

# 终端 4:建图完成后保存
ros2 run nav2_map_server map_saver_cli -f ~/maps/turtlebot3_world

7.2 自定义 Gazebo 世界

创建一个简单的室内环境 my_world.sdf

<?xml version="1.0" ?>
<sdf version="1.8">
  <world name="my_world">
    <!-- 光照 -->
    <light type="directional" name="sun">
      <cast_shadows>true</cast_shadows>
      <pose>0 0 10 0 0 0</pose>
      <diffuse>0.8 0.8 0.8 1</diffuse>
      <specular>0.2 0.2 0.2 1</specular>
      <direction>-0.5 0.1 -0.9</direction>
    </light>

    <!-- 地面 -->
    <model name="ground_plane">
      <static>true</static>
      <link name="link">
        <collision name="collision">
          <geometry>
            <plane>
              <normal>0 0 1</normal>
              <size>20 20</size>
            </plane>
          </geometry>
        </collision>
        <visual name="visual">
          <geometry>
            <plane>
              <normal>0 0 1</normal>
              <size>20 20</size>
            </plane>
          </geometry>
          <material>
            <ambient>0.8 0.8 0.8 1</ambient>
          </material>
        </visual>
      </link>
    </model>

    <!-- 外墙 -->
    <model name="wall_north">
      <static>true</static>
      <pose>0 5 0.5 0 0 0</pose>
      <link name="link">
        <collision name="collision">
          <geometry><box><size>10 0.1 1</size></box></geometry>
        </collision>
        <visual name="visual">
          <geometry><box><size>10 0.1 1</size></box></geometry>
          <material><ambient>0.6 0.6 0.6 1</ambient></material>
        </visual>
      </link>
    </model>

    <model name="wall_south">
      <static>true</static>
      <pose>0 -5 0.5 0 0 0</pose>
      <link name="link">
        <collision name="collision">
          <geometry><box><size>10 0.1 1</size></box></geometry>
        </collision>
        <visual name="visual">
          <geometry><box><size>10 0.1 1</size></box></geometry>
          <material><ambient>0.6 0.6 0.6 1</ambient></material>
        </visual>
      </link>
    </model>

    <model name="wall_east">
      <static>true</static>
      <pose>5 0 0.5 0 0 1.5708</pose>
      <link name="link">
        <collision name="collision">
          <geometry><box><size>10 0.1 1</size></box></geometry>
        </collision>
        <visual name="visual">
          <geometry><box><size>10 0.1 1</size></box></geometry>
          <material><ambient>0.6 0.6 0.6 1</ambient></material>
        </visual>
      </link>
    </model>

    <model name="wall_west">
      <static>true</static>
      <pose>-5 0 0.5 0 0 1.5708</pose>
      <link name="link">
        <collision name="collision">
          <geometry><box><size>10 0.1 1</size></box></geometry>
        </collision>
        <visual name="visual">
          <geometry><box><size>10 0.1 1</size></box></geometry>
          <material><ambient>0.6 0.6 0.6 1</ambient></material>
        </visual>
      </link>
    </model>

    <!-- 室内隔墙 -->
    <model name="inner_wall_1">
      <static>true</static>
      <pose>-1 2 0.5 0 0 0</pose>
      <link name="link">
        <collision name="collision">
          <geometry><box><size>4 0.1 1</size></box></geometry>
        </collision>
        <visual name="visual">
          <geometry><box><size>4 0.1 1</size></box></geometry>
          <material><ambient>0.7 0.7 0.7 1</ambient></material>
        </visual>
      </link>
    </model>

    <!-- 障碍物(桌子) -->
    <model name="table">
      <static>true</static>
      <pose>2 -2 0.35 0 0 0</pose>
      <link name="link">
        <collision name="collision">
          <geometry><box><size>1.0 0.6 0.7</size></box></geometry>
        </collision>
        <visual name="visual">
          <geometry><box><size>1.0 0.6 0.7</size></box></geometry>
          <material><ambient>0.5 0.3 0.1 1</ambient></material>
        </visual>
      </link>
    </model>

  </world>
</sdf>

7.3 仿真中的完整 SLAM + 导航流程

# 步骤 1:启动仿真环境
ros2 launch my_robot_gazebo simulation.launch.py world:=my_world.sdf

# 步骤 2:启动 SLAM 建图
ros2 launch my_robot_slam slam_launch.py use_sim_time:=true

# 步骤 3:遥控建图
ros2 run teleop_twist_keyboard teleop_twist_keyboard

# 步骤 4:保存地图
ros2 run nav2_map_server map_saver_cli -f ~/maps/my_world_map

# 步骤 5:关闭 SLAM,启动导航
# Ctrl+C 关闭 SLAM 节点
ros2 launch my_robot_navigation navigation_launch.py \
  use_sim_time:=true \
  map:=$HOME/maps/my_world_map.yaml

# 步骤 6:在 RViz 中设置初始位姿(2D Pose Estimate)
# 步骤 7:在 RViz 中设置导航目标(2D Goal Pose)

7.4 Gazebo 仿真注意事项

注意事项说明
use_sim_time仿真中必须设为 true,使用 Gazebo 的仿真时钟
物理引擎默认使用 ODE,复杂场景可切换到 DART
传感器噪声Gazebo 可以为传感器添加高斯噪声,更接近真实
实时因子如果仿真卡顿,降低世界复杂度或减少传感器频率
GPU 加速相机传感器需要 GPU,确保安装了显卡驱动

8. 进阶拓展

8.1 多传感器融合

实际应用中,单一传感器往往不够可靠。多传感器融合可以显著提升系统的鲁棒性和精度。

8.1.1 激光 + IMU 融合

IMU(惯性测量单元)提供高频率的角速度和加速度数据,与激光雷达互补:

激光雷达:低频(10-20Hz)、高精度、无漂移
IMU:     高频(100-1000Hz)、短期精确、长期漂移

融合策略:
  - IMU 提供帧间的高频运动预测(预积分)
  - 激光扫描匹配修正 IMU 的累积漂移
  - 结果:高频率 + 高精度的位姿估计

在 ROS2 中使用 robot_localization 包进行传感器融合:

# ekf.yaml - 扩展卡尔曼滤波融合配置
ekf_filter_node:
  ros__parameters:
    frequency: 50.0
    sensor_timeout: 0.1
    two_d_mode: true          # 2D 导航小车设为 true
    publish_tf: true
    map_frame: map
    odom_frame: odom
    base_link_frame: base_link
    world_frame: odom

    # 里程计输入(来自轮式编码器)
    odom0: /wheel_odom
    odom0_config: [false, false, false,   # x, y, z
                   false, false, false,   # roll, pitch, yaw
                   true,  true,  false,   # vx, vy, vz
                   false, false, true,    # vroll, vpitch, vyaw
                   false, false, false]   # ax, ay, az

    # IMU 输入
    imu0: /imu/data
    imu0_config: [false, false, false,   # x, y, z
                  false, false, true,    # roll, pitch, yaw
                  false, false, false,   # vx, vy, vz
                  false, false, true,    # vroll, vpitch, vyaw
                  true,  false, false]   # ax, ay, az
    imu0_differential: true              # 使用差分模式
    imu0_remove_gravitational_acceleration: true
# 启动传感器融合
ros2 launch robot_localization ekf.launch.py

8.1.2 激光 + 视觉融合

RTAB-Map 天然支持激光和视觉的融合:

# RTAB-Map 激光+视觉融合配置
rtabmap:
  ros__parameters:
    subscribe_depth: true
    subscribe_scan: true        # 同时订阅激光数据

    # 视觉用于回环检测
    # 激光用于精确的扫描匹配和栅格地图生成

    Grid/FromDepth: false       # 从激光而非深度图生成栅格
    Grid/RangeMax: 12.0

    # ICP 参数(激光扫描匹配)
    Icp/MaxCorrespondenceDistance: 0.1
    Icp/MaxTranslation: 0.5
    Icp/Strategy: 1             # 1=libpointmatcher

8.1.3 融合架构对比

融合方式说明代表算法
松耦合各传感器独立处理,结果在后端融合robot_localization (EKF)
紧耦合传感器原始数据在同一优化框架中联合处理LIO-SAM, VINS-Fusion

松耦合实现简单,紧耦合精度更高但复杂度也更高。导航小车入门建议从松耦合开始。

8.2 3D SLAM 简介

当导航场景涉及多层建筑、复杂地形或无人机时,需要 3D SLAM。

8.2.1 3D 激光 SLAM

常用 3D 激光雷达:Velodyne VLP-16、Livox Mid-360、Ouster OS1

代表算法:

算法特点适用场景
LOAM经典的激光里程计和建图通用 3D 建图
LeGO-LOAM轻量级 LOAM,利用地面分割地面车辆
LIO-SAM激光+IMU 紧耦合,因子图优化高精度建图
FAST-LIO2迭代卡尔曼滤波,极快速度嵌入式平台
Point-LIO逐点处理,超高频率高动态场景

8.2.2 3D 视觉 SLAM

  • ORB-SLAM3:支持单目/双目/RGB-D 的 3D 视觉 SLAM
  • RTAB-Map:支持 3D 点云地图和 OctoMap 生成
  • VINS-Fusion:视觉惯性 SLAM,支持 GPS 融合

8.3 语义 SLAM

传统 SLAM 只关注几何信息(哪里有障碍物),语义 SLAM 还理解"障碍物是什么"。

传统 SLAM 地图:
  ████████████
  █   ██     █
  █          █    ← 只知道哪里有障碍物
  █     ██   █
  ████████████

语义 SLAM 地图:
  ████████████
  █   桌子    █
  █          █    ← 知道障碍物的类别
  █     椅子  █
  ████████████

实现方式:

  1. 使用深度学习目标检测(YOLO、Mask R-CNN)识别物体
  2. 将语义标签与 SLAM 地图中的几何信息关联
  3. 构建语义地图,支持更智能的导航决策
# 简单的语义 SLAM 概念示例(伪代码)
# 结合 YOLO 检测和 RTAB-Map

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from vision_msgs.msg import Detection2DArray

class SemanticMapper(Node):
    def __init__(self):
        super().__init__('semantic_mapper')

        # 订阅 YOLO 检测结果
        self.det_sub = self.create_subscription(
            Detection2DArray, '/yolo/detections', self.detection_cb, 10)

        # 订阅深度图
        self.depth_sub = self.create_subscription(
            Image, '/camera/depth/image_raw', self.depth_cb, 10)

        self.semantic_objects = []  # 语义物体列表

    def detection_cb(self, msg):
        for det in msg.detections:
            label = det.results[0].hypothesis.class_id
            # 结合深度信息计算 3D 位置
            # 将语义标签和位置存入语义地图
            self.get_logger().info(f'检测到: {label}')

    def depth_cb(self, msg):
        # 处理深度图
        pass

8.4 深度学习在 SLAM 中的应用

深度学习正在改变 SLAM 的多个环节:

应用方向说明代表工作
深度估计从单目图像预测深度图MiDaS, Depth Anything
特征提取与匹配学习更鲁棒的特征描述子SuperPoint + SuperGlue
位姿估计端到端学习相机位姿PoseNet, MapNet
回环检测基于深度学习的场景识别NetVLAD, Patch-NetVLAD
动态物体去除识别并去除动态物体的影响DynaSLAM
NeRF/3DGS神经辐射场/3D 高斯泼溅用于场景重建iMAP, NICE-SLAM, SplaTAM

趋势:传统几何方法 + 深度学习的混合方案是当前主流方向,纯端到端的深度学习 SLAM 仍在研究阶段。


9. 常见问题与调试技巧

9.1 SLAM 建图问题

问题可能原因解决方案
地图扭曲/重影里程计漂移严重,回环检测失败检查里程计精度,降低移动速度,确保走回路触发回环
地图有大量噪点激光雷达数据质量差检查雷达安装是否稳固,调整 max_laser_range 过滤远处噪声
建图时地图不更新TF 变换缺失或话题名不匹配ros2 topic listros2 run tf2_tools view_frames 检查
长走廊建图漂移特征不足,扫描匹配退化蛇形走位增加特征,或融合 IMU
建图速度慢分辨率过高或计算资源不足降低分辨率,增大 minimum_travel_distance

9.2 导航问题

问题可能原因解决方案
机器人不动AMCL 未定位成功,costmap 未就绪在 RViz 中设置正确的初始位姿
路径规划失败起点或终点在障碍物内检查 costmap 膨胀参数,确保机器人位置正确
机器人撞墙膨胀半径太小,控制器跟踪不准增大 inflation_radius,调整控制器参数
机器人原地打转到达目标附近但角度不满足调大 xy_goal_toleranceyaw_goal_tolerance
机器人走奇怪的路径costmap 代价值不合理检查 costmap 层配置,调整 cost_scaling_factor
恢复行为频繁触发空间狭窄或参数不合适调整恢复行为参数,或在狭窄区域降低速度

9.3 TF 问题排查

TF 问题是 ROS2 导航中最常见的错误来源:

# 1. 查看完整 TF 树
ros2 run tf2_tools view_frames
# 生成 frames.pdf,检查是否有断裂

# 2. 检查特定变换是否存在
ros2 run tf2_ros tf2_echo map base_link
# 如果报错说找不到变换,说明 TF 链断裂

# 3. 查看 TF 发布频率
ros2 run tf2_ros tf2_monitor

# 4. 常见 TF 错误及解决
# "Could not find a connection between 'map' and 'base_link'"
#   → 检查 AMCL 或 SLAM 是否在运行(它们发布 map→odom)
#   → 检查里程计是否在发布 odom→base_link

# "Lookup would require extrapolation into the future"
#   → 时间同步问题,检查 use_sim_time 设置
#   → 仿真中必须所有节点都设置 use_sim_time:=true

9.4 性能优化建议

场景:Jetson Nano 上运行 SLAM + Nav2

优化措施:
1. 降低激光扫描频率:throttle_scans: 2(每2帧处理1帧)
2. 降低地图分辨率:resolution: 0.1(10cm)
3. 减少 AMCL 粒子数:max_particles: 500
4. 降低 costmap 更新频率:update_frequency: 2.0
5. 使用异步 SLAM 模式:async_slam_toolbox_node
6. 关闭不必要的可视化:不运行 RViz(或在远程电脑运行)
7. 降低相机分辨率:320x240 足够用于避障

9.5 实用调试命令速查

# === 话题调试 ===
ros2 topic list                          # 列出所有话题
ros2 topic echo /scan --once             # 查看一帧激光数据
ros2 topic hz /scan                      # 查看话题频率
ros2 topic info /scan                    # 查看话题类型和发布/订阅者

# === 节点调试 ===
ros2 node list                           # 列出所有节点
ros2 node info /slam_toolbox             # 查看节点详情
ros2 param list /slam_toolbox            # 列出节点参数
ros2 param get /slam_toolbox resolution  # 获取参数值

# === 服务调试 ===
ros2 service list                        # 列出所有服务
ros2 service call /map_server/load_map nav2_msgs/srv/LoadMap "{map_url: '/path/to/map.yaml'}"

# === TF 调试 ===
ros2 run tf2_tools view_frames           # 生成 TF 树图
ros2 run tf2_ros tf2_echo map base_link  # 实时查看变换

# === 录包与回放 ===
ros2 bag record -a                       # 录制所有话题
ros2 bag record /scan /odom /tf /tf_static  # 录制指定话题
ros2 bag play my_bag --clock             # 回放(发布仿真时钟)

# === 可视化 ===
ros2 run rviz2 rviz2                     # 启动 RViz2
ros2 run rqt_graph rqt_graph             # 查看节点关系图
ros2 run rqt_tf_tree rqt_tf_tree         # 查看 TF 树

10. 参考资源

10.1 书籍

书名作者说明
《视觉 SLAM 十四讲》高翔视觉 SLAM 入门必读,理论+实践
《概率机器人》Thrun 等SLAM 理论经典教材
《机器人学中的状态估计》Barfoot深入理解状态估计和优化
《ROS 机器人开发实践》胡春旭ROS/ROS2 实践指南

10.2 在线课程与教程

  • 古月居 ROS2 教程:中文 ROS2 入门系列
  • 鱼香 ROS:ROS2 中文社区,教程丰富
  • Articulated Robotics:YouTube 上优质的 ROS2 导航小车教程
  • Nav2 官方文档:navigation.ros.org
  • slam_toolbox Wiki:GitHub 上的详细文档

10.3 开源项目参考

项目说明
TurtleBot3ROS2 官方教学机器人,完整的 SLAM + 导航示例
Linorobot2开源 ROS2 导航小车项目,硬件+软件完整方案
Nav2 示例nav2_bringup 包中的完整导航配置示例
RTAB-Map 示例rtabmap_demos 包中的多种传感器配置示例

10.4 常用 ROS2 包索引

包名功能安装命令
slam_toolbox2D 激光 SLAMsudo apt install ros-humble-slam-toolbox
navigation2导航栈sudo apt install ros-humble-navigation2
rtabmap_ros多传感器 SLAMsudo apt install ros-humble-rtabmap-ros
robot_localization传感器融合sudo apt install ros-humble-robot-localization
cartographer_rosGoogle SLAMsudo apt install ros-humble-cartographer-ros
rplidar_rosRPLidar 驱动sudo apt install ros-humble-rplidar-ros
realsense2_cameraRealSense 驱动sudo apt install ros-humble-realsense2-camera
teleop_twist_keyboard键盘遥控sudo apt install ros-humble-teleop-twist-keyboard