地图服务与代价地图:从配置到实战

各位同学,欢迎来到第二章。这一章我们聊聊地图和代价地图。说实话,很多新手在 Nav2 里栽跟头,十有八九是代价地图没配好。机器人要么撞墙,要么原地转圈,要么路径诡异得像喝醉了酒。

我个人习惯把代价地图比作机器人的「安全距离感」。你想想看,一个人闭着眼走路,靠的是触觉和听觉。机器人呢?它靠的就是这张代价地图。地图服务器告诉它「世界长什么样」,代价地图告诉它「哪里能走、哪里危险、哪里要绕开」。

好,我们一个一个拆开讲。

2.1 地图服务器 (map_server) 配置

地图服务器说白了就干一件事:把存好的地图文件加载进来,发布给导航系统。它本身不复杂,但坑不少。

先看一个典型的 map_server 启动配置:

# map_server.yaml
map_server:
  ros__parameters:
    yaml_filename: "my_map.yaml"
    use_sim_time: False

对应的地图 YAML 文件长这样:

# my_map.yaml
image: my_map.pgm
resolution: 0.05
origin: [-10.0, -10.0, 0.0]
occupied_thresh: 0.65
free_thresh: 0.196
negate: 0

这里有几个参数,我重点说一下:

参数 说明 我的建议
resolution 每个像素代表的实际距离(米) 室内常用 0.05,室外可以放宽到 0.1
origin 地图左下角在世界坐标系中的位置 [x, y, yaw] 这个最容易出错,我踩过坑
occupied_thresh 像素值大于此阈值视为障碍物 0.65 是安全值,别乱改
free_thresh 像素值小于此阈值视为自由空间 0.196 是经验值,够用
⚠ 避坑指南: 我曾经在 origin 参数上栽过跟头。有一次建图时地图原点没对齐,结果机器人一启动就认为自己在地图外面,直接报错。后来我养成了习惯:建图完成后,一定用 rviz2 看一眼地图原点是不是在合理位置。如果地图左下角坐标是 [-10, -10],那原点就在地图中心偏左下,别搞反了。

2.2 静态代价地图 (static_layer)

静态层,就是那张「不会变」的地图。它把 map_server 发布的地图数据转成代价地图。说白了,墙就是墙,门就是门,这些信息不会因为你机器人动一下就改变。

配置示例:

# static_layer.yaml
static_layer:
  plugin: "nav2_costmap_2d::StaticLayer"
  map_subscribe_transient_local: True
  enabled: True
  lethal_cost_threshold: 100
  first_map_only: False

关键参数:

  • lethal_cost_threshold:地图中像素值超过多少就视为「致命障碍」。默认 100,意思是地图里标记为障碍的像素,代价直接拉满。
  • first_map_only:设为 True 的话,只加载第一张地图,后面地图更新就不管了。我一般设 False,万一你后面要动态更新地图呢?
  • map_subscribe_transient_local:这个参数在 ROS2 里很重要。设为 True 可以保证订阅者晚到也能拿到地图数据。我建议一直开着。
💡 小技巧: 如果你发现机器人明明离墙还有一段距离,但代价地图上墙已经「膨胀」得不像话了,别急着调膨胀层。先看看 static_layer 的 lethal_cost_threshold 是不是设得太低了。我遇到过有人设成 50,结果地图上稍微有点灰的像素都被当成墙了。

2.3 障碍物代价地图 (obstacle_layer)

这才是动态世界的核心。激光雷达扫到的行人、突然出现的纸箱、临时摆放的锥桶——这些信息都靠 obstacle_layer 来更新。

配置示例:

# obstacle_layer.yaml
obstacle_layer:
  plugin: "nav2_costmap_2d::ObstacleLayer"
  enabled: True
  observation_sources: scan
  scan:
    topic: /scan
    sensor_frame: laser_frame
    data_type: LaserScan
    marking: True
    clearing: True
    min_obstacle_height: 0.0
    max_obstacle_height: 2.0
    obstacle_range: 2.5
    raytrace_range: 3.0

这里我重点讲几个容易搞混的参数:

参数 作用 我的经验
marking 是否用该传感器标记障碍物 一般设为 True,除非你只用它来清除
clearing 是否用该传感器清除障碍物 必须 True,否则障碍物会「阴魂不散」
obstacle_range 多远距离内的点才被视为障碍物 2.5 米够用,太远容易误报
raytrace_range 多远距离内的射线用于清除障碍物 比 obstacle_range 大 0.5 米,这是标准做法
min/max_obstacle_height 只考虑这个高度范围内的障碍物 地面机器人一般设 0.0~2.0,别把天花板算进去

🔑 核心逻辑: marking 和 clearing 是一对兄弟。marking 负责「画」障碍物,clearing 负责「擦」障碍物。如果只 marking 不 clearing,机器人走过的地方会留下「鬼影」——明明障碍物已经没了,代价地图上还留着。我见过一个项目,机器人总是在同一个地方急刹车,排查了半天才发现是 clearing 没开。

2.4 膨胀层 (inflation_layer)

膨胀层,就是给障碍物「穿衣服」。机器人不是质点,它有尺寸。膨胀层的作用就是告诉规划器:「别靠太近,会撞到」。

配置示例:

# inflation_layer.yaml
inflation_layer:
  plugin: "nav2_costmap_2d::InflationLayer"
  enabled: True
  inflation_radius: 0.55
  cost_scaling_factor: 10.0

这两个参数是调参的重灾区:

  • inflation_radius:膨胀半径(米)。机器人离障碍物多远就开始「感到危险」。一般设为机器人半径的 1.2~1.5 倍。
  • cost_scaling_factor:代价衰减系数。值越大,代价衰减越快;值越小,代价影响范围越广。

我直接给个经验公式:

inflation_radius = 机器人半径 × 1.3
cost_scaling_factor = 10.0  # 起步值,后面微调
⚠ 避坑指南: 我曾经在一个仓库项目里,把 inflation_radius 设成了 1.5 米,结果机器人死活过不了 2 米宽的走廊。后来一算,机器人半径 0.4 米,膨胀 1.5 米,走廊两边各占 1.5 米,中间只剩 -1 米了——根本不可能通过。记住:膨胀半径不是越大越好,够用就行。

2.5 三层协作:一张图看懂

下面这张图展示了三个层如何协同工作:

代价地图三层协作流程 地图服务器 静态层 (static_layer) 激光雷达 / 传感器 障碍物层 (obstacle_layer) 主代价地图 (master_costmap) 膨胀层 (inflation_layer) 最终代价地图 → 规划器使用 三层叠加逻辑 静态层:固定障碍物 障碍物层:动态障碍物 膨胀层:安全距离 三者叠加 = 最终代价地图

流程很清楚:地图服务器提供底图 → 静态层加载固定障碍物 → 传感器实时更新障碍物层 → 两层合并成主代价地图 → 膨胀层加上安全距离 → 最终给规划器用。

2.6 调参实战建议

最后,我总结几条实战经验:

  1. 先调静态层,再调动态层。 静态层是基础,静态层没调好,后面都是白搭。
  2. 膨胀半径从机器人半径的 1.2 倍开始。 太大会堵死通道,太小会撞墙。
  3. obstacle_range 别设太大。 3 米以内足够,太远容易把远处的噪声当成障碍物。
  4. 用 rviz2 实时观察代价地图。 我调试时永远开着代价地图的可视化,一眼就能看出问题。
  5. 每次只改一个参数。 改完跑一遍,看效果。别一次改三个参数,出了问题你都不知道是哪个引起的。

💡 我的调试口诀: 静态层看墙对不对,障碍物层看人跟不跟,膨胀层看缝过不过。三关都过了,导航基本就稳了。

嗯,这一章的内容就到这。地图和代价地图是导航的「地基」,地基打牢了,后面的路径规划才能跑得稳。各位同学,动手试试吧。