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 话题。其他节点订阅这个话题,就能拿到地图数据。
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 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
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。你就能看到地图了。
4.7 避坑总结
最后,我把自己踩过的坑总结一下:
- 地图原点偏移:YAML里的origin一定要和建图时对齐。差一厘米,导航就偏一厘米。
- 分辨率搞错:0.05和0.1差一倍。地图加载后,用RViz量一下实际距离,确认分辨率对不对。
- 代价地图膨胀半径太大:机器人过不了窄门。我一般设成机器人半径的2~3倍,再根据实际效果微调。
- 未知区域处理不当:默认情况下,未知区域在代价地图里被视为可通行。如果你希望机器人避开未知区域,需要修改参数
track_unknown_space: true。