4. 地图服务:地图数据类型nav_msgs/OccupancyGrid、map_server使用、静态地图与动态地图、代价地图概念

地图,是导航系统里最基础的东西。没有地图,机器人就是个瞎子。

我刚开始接触ROS导航时,以为地图就是一张图片。后来才发现,事情没那么简单。地图怎么存?怎么读?怎么更新?代价地图又是什么鬼?

这一章,咱们就把这些事彻底聊透。

4.1 地图数据类型:nav_msgs/OccupancyGrid

ROS2里,地图的数据类型叫 nav_msgs/OccupancyGrid。说白了,它就是一个二维网格,每个格子代表地图上的一小块区域。

这个数据结构的核心字段,我列在下面:

字段 类型 说明
header std_msgs/Header 时间戳、坐标系ID(通常是map)
info nav_msgs/MapMetaData 地图的元数据:分辨率、宽高、原点坐标
data int8[] 一维数组,存储每个格子的占用值

其中 info 里包含:

  • resolution:每个格子代表的实际距离,单位米/像素。常见值是0.05,也就是5厘米一格。
  • width / height:地图的像素宽高。
  • origin:地图左下角在真实世界中的坐标(Pose类型)。

data数组里的值,含义很明确:

  • 0:空闲(free),机器人可以走。
  • 100:占用(occupied),有障碍物。
  • -1:未知(unknown),还没探测到。

嗯,这里要注意:-1 不等于 0。未知区域和空闲区域,在导航算法里处理方式完全不同。我见过有人把未知区域当成空闲,结果机器人直接往墙里撞。

小技巧: 调试时可以用 ros2 topic echo /map 查看地图数据。但别直接看data数组,那会刷屏。看info就够了。

4.2 map_server:地图的加载与保存

map_server 是ROS2里负责地图读写的工具。它能把磁盘上的地图文件加载成 nav_msgs/OccupancyGrid 话题发布出来。

地图文件通常有两部分:

  • 图片文件:PNG、PGM、JPG等格式。白色是空闲,黑色是占用,灰色是未知。
  • YAML配置文件:描述图片的元数据。

一个典型的YAML文件长这样:

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

参数含义:

  • image:图片文件名,路径相对于YAML文件。
  • resolution:每个像素对应的米数。
  • origin:图片左下角在map坐标系下的位置 [x, y, yaw]。
  • occupied_thresh / free_thresh:像素灰度值转占用值的阈值。比如像素灰度值大于0.65算占用,小于0.196算空闲,中间算未知。
  • negate:是否反转黑白。通常设为0。

启动map_server的命令很简单:

ros2 run nav2_map_server map_server --ros-args -p yaml_filename:=/path/to/my_map.yaml

启动后,它会发布 /map 话题。其他节点订阅这个话题,就能拿到地图数据。

避坑指南: 我曾经因为YAML文件里origin写错,导致地图偏移了好几米。机器人定位一直不准,折腾了两天才发现是地图原点没对齐。检查地图时,先确认origin对不对。

4.3 静态地图 vs 动态地图

地图不是一成不变的。根据使用场景,我们把它分成两类:

类型 特点 适用场景
静态地图 预先建好的,固定不变。加载后一直发布同一份数据。 工厂车间、办公楼、仓库等环境固定的场景。
动态地图 实时更新,能反映环境变化。比如新增的障碍物、移动的人。 人流密集的商场、动态仓储、家庭环境。

静态地图用map_server加载一次就够了。但动态地图需要持续更新。怎么更新?

ROS2里,/map 话题可以随时被重新发布。比如SLAM节点在运行时,会不断发布最新的地图。或者你可以写一个节点,订阅激光雷达数据,实时修改地图的data数组。

我个人习惯的做法是:

  • 先用静态地图做全局路径规划。
  • 再用代价地图处理动态障碍物。

这样既保证了路径的稳定性,又能应对突发情况。

4.4 代价地图:导航的核心概念

代价地图(Costmap),是Nav2里最核心的概念之一。它本质上也是一张网格地图,但每个格子的值不是简单的占用/空闲,而是代价。

代价的含义:

  • 0:完全可通行。
  • 1~99:有风险,代价越高越不建议走。
  • 100:致命障碍,不能走。

代价地图是怎么生成的?

它从原始地图出发,对每个障碍物格子进行膨胀。膨胀半径内的格子,代价会逐渐升高。离障碍物越近,代价越大。

为什么要膨胀?

因为机器人有尺寸。你不能让机器人贴着墙走。膨胀相当于给障碍物加了一圈“安全距离”。

Nav2里,代价地图分为两层:

  • 全局代价地图(global_costmap):用于全局路径规划。通常基于静态地图,膨胀半径较大。
  • 局部代价地图(local_costmap):用于局部路径规划和避障。实时更新,膨胀半径较小,反应更快。

配置代价地图的参数,通常在 costmap_common_params.yaml 里:

robot_radius: 0.2
inflation_radius: 0.5
cost_scaling_factor: 10.0
  • robot_radius:机器人半径,单位米。
  • inflation_radius:膨胀半径。障碍物周围多少米内开始计算代价。
  • cost_scaling_factor:代价衰减速度。值越大,代价下降越快。
核心理解: 代价地图的本质,是把“能不能走”的问题,变成了“走这条路要付出多少代价”的问题。路径规划算法会选一条总代价最小的路径。

4.5 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

地图服务知识体系 地图服务 nav_msgs/OccupancyGrid map_server 静态 vs 动态地图 代价地图 (Costmap) 说明: • OccupancyGrid 是地图的数据载体,map_server 是加载工具。 • 静态地图用于全局规划,动态地图用于局部避障。 • 代价地图在原始地图基础上加入膨胀层,让路径更安全。

4.6 实战:加载并查看地图

光说不练假把式。咱们实际操作一下。

步骤1:准备地图文件

如果你没有现成的地图,可以用Nav2自带的测试地图:

ros2 run nav2_map_server map_server --ros-args -p yaml_filename:=/opt/ros/humble/share/nav2_bringup/maps/turtlebot3_world.yaml

步骤2:查看地图话题

ros2 topic list | grep map
ros2 topic echo /map --once

步骤3:用RViz可视化

启动RViz,添加Map显示,选择话题 /map。你就能看到地图了。

提示: 如果地图显示为全黑或全白,检查一下YAML里的occupied_thresh和free_thresh。这两个值设错了,地图就废了。

4.7 避坑总结

最后,我把自己踩过的坑总结一下:

  • 地图原点偏移:YAML里的origin一定要和建图时对齐。差一厘米,导航就偏一厘米。
  • 分辨率搞错:0.05和0.1差一倍。地图加载后,用RViz量一下实际距离,确认分辨率对不对。
  • 代价地图膨胀半径太大:机器人过不了窄门。我一般设成机器人半径的2~3倍,再根据实际效果微调。
  • 未知区域处理不当:默认情况下,未知区域在代价地图里被视为可通行。如果你希望机器人避开未知区域,需要修改参数 track_unknown_space: true。