# 3. Costmap2D 代价地图
`Costmap2D` 是 Autonomy 的**二维代价栅格地图**实现,源自 Nav2 `nav2_costmap_2d`。它将静态地图、动态障碍与安全膨胀融合为一张 `uint8` 栅格,是**路径规划与碰撞检测的标准输入**。
> 模块级架构见 [§1.3](01_architecture.md#13-costmap2d-架构重点);公式见 [§3.5–§3.8](03_costmap2d.md#35-坐标变换)。
---
## 3.1 何时使用 Costmap2D
| 场景 | 是否适用 | 说明 |
|------|----------|------|
| 全局/局部路径规划 | ✅ | `PlannerServer` 已集成 |
| 激光动态避障 | ✅ | `obstacle_layer` + `feedLaserScan` |
| 静态 SLAM 地图融合 | ✅ | `static_layer` + `applyOccupancyGrid` |
| 地形高程/坡度 | ❌ | 用 [GridMap](04_grid_map.md) |
| 3D 体素占据 | 部分 | `voxel_layer` 投影到 2D |
---
## 3.2 架构:从 Wrapper 到栅格
### 3.2.1 对象层级
```
你的代码
│
▼
Costmap2DWrapper ← 持有、Start/Stop、feed 传感器
│
├── LayeredCostmap ← 插件调度、双缓冲
│ ├── plugins_[0] StaticLayer
│ ├── plugins_[1] ObstacleLayer
│ ├── plugins_[2] InflationLayer
│ └── combined_costmap_ (Costmap2D) ← getCostmap() 返回此对象
│
├── TF Buffer ← getRobotPose()
└── 后台线程 ← Start() 后周期性 updateMap()
```
**不要直接 new `Costmap2D`**:生产环境始终通过 `Costmap2DWrapper` 使用,由它负责插件加载、线程与 TF。
### 3.2.2 一张图看懂数据流
```mermaid
flowchart TB
subgraph 输入
M["MapServer / PGM
OccupancyGrid"]
L["/scan 激光"]
P["点云 PointCloud2"]
TF["TF: base_link→map"]
end
subgraph Costmap2DWrapper
direction TB
A["applyOccupancyGrid / loadMap"] --> SL["static_layer"]
B["feedLaserScan / feedPointCloud2"] --> OL["obstacle_layer
ObservationBuffer"]
C["getRobotPose"] --> UPD["updateMap()"]
UPD --> LC["LayeredCostmap"]
SL --> LC
OL --> LC
LC --> IL["inflation_layer"]
IL --> OUT["combined_costmap
uint8[Nx×Ny]"]
end
subgraph 输出
OUT --> PL["PlannerServer 复制 char map"]
OUT --> VIS["snapshotOccupancyGrid 可视化"]
end
M --> A
L --> B
P --> B
TF --> C
```
### 3.2.3 双缓冲机制
| 模式 | 行为 |
|------|------|
| 无 filter(常见) | 插件直接写入 `combined_costmap_`,`getCostmap()` 即最终结果 |
| 有 filter | 插件 → `primary_costmap_` → `copyWindow` → `combined_costmap_` → filter 修改 |
每次 `updateMap` 在更新窗口 $(x_0,y_0)…(x_n,y_n)$ 内先 `resetMap`(置 `default_value_`),再按插件顺序写入,**不是增量叠加到旧值上**。
---
## 3.3 详细使用指南
### 3.3.1 方式 A:通过 PlannerServer(推荐)
最常见路径——无需直接操作 costmap:
```lua
-- config/autonomy.lua
AUTONOMY = { planning = AUTONOMY_PLANNER }
-- config/planner/planner.lua 内已含 costmap 块
```
`PlannerServer` 构造时自动创建 `Costmap2DWrapper` 并 `Start()`。你只需:
1. 确保 `MapServer` 发布 `/map` 或调用 `applyOccupancyGrid`
2. 确保激光数据通过 ROS bridge 调用 `feedLaserScan`
3. TF 树中 `map` ↔ `base_link` 可用
### 3.3.2 方式 B:独立使用 Costmap2DWrapper
```cpp
#include "autonomy/map/map_options.hpp"
#include "autonomy/map/costmap_2d/costmap_2d_wrapper.hpp"
// 1. 加载配置
auto opts = autonomy::map::CreateCostmap2DOptions("config");
// 2. 构造(此时 init() 已执行:创建 LayeredCostmap、加载插件)
auto costmap = std::make_shared(
opts, "global_costmap");
// 3. 注入静态地图(二选一)
costmap->loadMap("config/map/warehouse.yaml"); // 从 YAML+PGM
// 或
costmap->applyOccupancyGrid(*map_server->GetStaticMapShared());
// 4. 启动后台更新线程
costmap->Start();
// 5. 传感器回调中喂数据
costmap->feedLaserScan(laser_scan_msg);
// 6. 读取(规划前加锁复制)
{
std::unique_lock lock(*costmap->getCostmap()->getMutex());
const auto* data = costmap->getCostmap()->getCharMap();
unsigned int mx, my;
costmap->getCostmap()->worldToMap(x, y, mx, my);
unsigned char c = data[my * size_x + mx];
}
// 7. 停止
costmap->Stop();
```
### 3.3.3 关键 API 速查
| API | 调用时机 | 作用 |
|-----|----------|------|
| `Start()` | 一切就绪后 | 启动后台线程:`updateMap` + `publishMap` |
| `Stop()` | 退出前 | 停止线程、deactivate 插件 |
| `applyOccupancyGrid(grid)` | 静态地图到达时 | 写入 `StaticLayer` |
| `feedLaserScan(scan)` | 每次激光回调 | 缓存到 `ObstacleLayer` |
| `getCostmap()` | 读取代价值 | 返回 `combined_costmap_` 指针 |
| `getMutex()` | 读取/规划前 | 线程安全锁 |
| `isReady()` | 规划前检查 | 是否至少更新过一次 |
| `isCurrent()` | 规划前检查 | 图层是否在超时内更新 |
| `snapshotOccupancyGrid(out)` | 可视化 | cost → OccupancyGrid 反向映射 |
### 3.3.4 全局图 vs 局部滚动图
| 配置 | `rolling_window` | 地图尺寸 | 适用 |
|------|------------------|----------|------|
| 全局代价地图 | `false` | 覆盖整个环境(或由 static_layer resize) | 室内导航、已知地图 |
| 局部代价地图 | `true` | 固定 $W \times H$(如 20m×20m),随机器人平移 | 大地图、动态环境 |
局部模式下 `updateMap` 每帧将 origin 设为 $(x_{\text{robot}} - W/2,\; y_{\text{robot}} - H/2)$,重叠区域数据保留,移出窗口的格重置。
### 3.3.5 完整配置示例
```lua
costmap = {
enabled = true,
name = "global_map",
frame_id = "map",
resolution = 0.05, -- 5 cm/cell
width = 20.0, -- 物理宽度 [m]
height = 20.0,
update_frequency = 5.0, -- 后台 updateMap 频率
rolling_window = false,
robot_radius = 0.22,
footprint = {
{x = 0.18, y = 0.14}, {x = 0.18, y = -0.14},
{x = -0.18, y = -0.14}, {x = -0.18, y = 0.14},
},
plugins = {"static_layer", "obstacle_layer", "inflation_layer"},
static_layer = {
enabled = true,
map_topic = "map",
subscribe_to_updates = false,
},
obstacle_layer = {
enabled = true,
footprint_clearing_enabled = true,
sensor_sources = {
scan = {
topic = "scan",
data_type = "LaserScan",
marking = true,
clearing = true,
obstacle_max_range = 3.0,
raytrace_max_range = 3.5,
},
},
},
inflation_layer = {
enabled = true,
inflation_radius = 0.35,
cost_scaling_factor = 3.0,
},
}
```
### 3.3.6 启动检查清单
| 步骤 | 检查 | 期望 |
|------|------|------|
| 1 | 日志 `Costmap2D update thread started` | 线程已启动 |
| 2 | `costmap->isReady()` | `true` |
| 3 | `costmap->isCurrent()` | `true`(TF 正常时) |
| 4 | 可视化快照 | 墙壁=深色,自由区=浅色,膨胀=渐变 |
| 5 | 机器人脚下格 | `FREE_SPACE` 或低代价(footprint clearing 生效) |
---
## 3.4 核心数据结构
路径:`autonomy/map/costmap_2d/costmap_2d.hpp`
| 成员 | 类型 | 含义 |
|------|------|------|
| `size_x_`, `size_y_` | `unsigned int` | 栅格尺寸(cell) |
| `resolution_` | `double` | 分辨率 [m/cell] |
| `origin_x_`, `origin_y_` | `double` | 地图左下角世界坐标 |
| `costmap_` | `unsigned char[]` | 一维代价值数组 |
| `default_value_` | `unsigned char` | 重置默认值 |
| `access_` | `recursive_mutex` | 线程锁 |
辅助结构:`MapLocation { unsigned int x, y }`
## 3.5 坐标变换
### 3.5.1 Map ↔ World
**世界 → 栅格**:
$$
m_x = \left\lfloor \frac{x - x_0}{\Delta} \right\rfloor, \quad
m_y = \left\lfloor \frac{y - y_0}{\Delta} \right\rfloor
$$
**栅格 → 世界**(cell 中心):
$$
x = x_0 + \left(m_x + \frac{1}{2}\right) \Delta, \quad
y = y_0 + \left(m_y + \frac{1}{2}\right) \Delta
$$
**线性索引**:
$$
\mathrm{idx} = m_y \cdot N_x + m_x
$$
### 3.5.2 边界与有效性
- `worldToMap`:坐标在地图外返回 `false`
- `worldToMapEnforceBounds`:裁剪到边界
- `worldToMapNoBounds`:不检查边界(内部算法用)
## 3.6 代价值体系
路径:`autonomy/map/costmap_2d/cost_values.hpp`
| 常量 | 值 | 规划行为 |
|------|-----|----------|
| `FREE_SPACE` | 0 | 自由通行 |
| `1 … 252` | 梯度 | 距障碍越近代价越高 |
| `INSCRIBED_INFLATED_OBSTACLE` | 253 | 内切区,机器人中心不可进入 |
| `LETHAL_OBSTACLE` | 254 | 阻塞 |
| `NO_INFORMATION` | 255 | 由 `allow_unknown` 决定 |
## 3.7 LayeredCostmap 多层架构
路径:`layered_costmap.hpp` / `layered_costmap.cpp`
### 3.7.1 双缓冲
| 缓冲区 | 写入者 | 读取者 |
|--------|--------|--------|
| `primary_costmap_` | 插件(static / obstacle / inflation) | filter 输入 |
| `combined_costmap_` | filter 输出 | `getCostmap()` 对外 |
### 3.7.2 updateMap 主循环
```
updateMap(robot_x, robot_y, robot_yaw)
│
├─ [rolling] updateOrigin(new_origin)
│
├─ foreach plugin: updateBounds(min_x, min_y, max_x, max_y)
│
├─ world bounds → cell bounds (x0, y0, xn, yn)
│
├─ resetMap(x0, y0, xn, yn)
│
├─ foreach plugin: updateCosts(master, x0, y0, xn, yn)
│
└─ [filters] copy primary → combined → filter updateCosts
```
## 3.8 InflationLayer 详解
路径:`layers/inflation_layer.hpp` / `.cpp`
### 3.8.1 代价函数
$$
c(d) =
\begin{cases}
254 & d = 0 \\
253 & d \cdot \Delta \leq r_i \\
\left\lfloor 252 \cdot e^{-\lambda (d \cdot \Delta - r_i)} \right\rfloor & \mathrm{otherwise}
\end{cases}
$$
默认参数:`inflation_radius_ = 0.55` m,`cost_scaling_factor_ = 10.0`,$r_i$ 由 footprint 自动计算。
### 3.8.2 传播算法
1. 扫描更新区域内所有格,将 `LETHAL_OBSTACLE`(及可选 `NO_INFORMATION`)作为种子
2. 按整数距离矩阵 $D_{ij} = \sqrt{i^2+j^2}$ 排序的等级 BFS
3. 4-邻域扩展,取 $\max(c_{\text{current}}, c_{\text{neighbor}})$ 写入
4. `updateBounds` 向外扩展 $R$ 保证边界正确
### 3.8.3 参数调优
| 参数 | 增大效果 | 减小效果 |
|------|----------|----------|
| `inflation_radius` | 更大安全裕度,窄通道难通过 | 路径更贴近障碍 |
| `cost_scaling_factor` | 代价衰减更快,远离障碍迅速变自由 | 更大范围保持高代价 |
## 3.9 图层详解
### 3.9.1 StaticLayer
| 模式 | 规则 |
|------|------|
| Trinary(默认) | $o \geq \tau_{\mathrm{lethal}} \Rightarrow \mathrm{LETHAL}$,否则 $\mathrm{FREE}$ |
| Scale | $c = (o / \tau_{\mathrm{th}}) \cdot 254$ |
非 rolling 模式下可 resize 整个 layered costmap 以匹配静态地图。
### 3.9.2 ObstacleLayer
**Marking**:障碍点 → `LETHAL_OBSTACLE`
**Clearing**:传感器原点 → 障碍点 Bresenham 射线 → `FREE_SPACE`
**ObservationBuffer**:多传感器观测缓存,按时间戳与范围过滤。
**Footprint clearing**:机器人 footprint 多边形区域清除障碍。
### 3.9.3 VoxelLayer
- `VoxelGrid`:每 XY cell 一个 `uint32`,最多 16 个 Z slice
- 3D 点云标记体素后投影到 2D costmap
- 配置:`z_voxels`, `z_resolution`, `origin_z`, `unknown_threshold`, `mark_threshold`
### 3.9.4 DenoiseLayer
- 连通域分析(4/8 连通)
- 过滤小于 `denoise_radius`(minimal_group_size)的孤立障碍簇
- 注意:过强去噪可能移除窄墙,导致穿墙规划
### 3.9.5 Filters
| Filter | 功能 |
|--------|------|
| `KeepoutFilter` | 禁行区(来自 filter mask) |
| `SpeedFilter` | 速度限制区 |
| `BinaryFilter` | 二值化过滤 |
## 3.10 Rolling Window
局部代价地图以机器人为中心滑动:
$$
x_0^{\text{new}} = x_{\text{robot}} - \frac{W}{2}, \quad
y_0^{\text{new}} = y_{\text{robot}} - \frac{H}{2}
$$
`updateOrigin` 算法:
1. 栅格对齐偏移 $\delta = \lfloor (x_0^{\text{new}} - x_0) / \Delta \rfloor$
2. 计算重叠区域,copy 到临时 buffer
3. 全图 reset → 更新 origin → 重叠数据写回
4. 非重叠区域 = `default_value_`
## 3.11 Footprint
路径:`footprint.hpp` / `footprint.cpp`
- 支持多边形 footprint 或 `robot_radius` 圆形近似
- 内切半径 $r_i$ 决定 `INSCRIBED_INFLATED_OBSTACLE` 边界
- 外接半径 $r_c$ 用于碰撞检测
配置示例(`planner.lua`):
```lua
footprint = {
{x = 0.18, y = 0.14},
{x = 0.18, y = -0.14},
{x = -0.18, y = -0.14},
{x = -0.18, y = 0.14},
},
robot_radius = 0.22,
```
## 3.12 Costmap2DWrapper 运行时
| 功能 | 说明 |
|------|------|
| `mapUpdateLoop` | 后台线程,按 `update_frequency` 调用 `updateMap` |
| `feedLaserScan` | 激光 → `ObservationBuffer` → ObstacleLayer |
| `applyOccupancyGrid` | 静态地图 → StaticLayer |
| `snapshotOccupancyGrid` | 导出可视化用 OccupancyGrid |
## 3.13 配置参考
完整示例见 `config/planner/planner.lua` 的 `costmap` 块。关键字段:
```lua
costmap = {
resolution = 0.05,
width = 20.0,
height = 20.0,
update_frequency = 5.0,
rolling_window = false, -- 全局图
plugins = {"static_layer", "obstacle_layer", "inflation_layer"},
-- 各图层参数块 ...
}
```
## 3.14 相关文档
- [§3.5–§3.8 坐标与膨胀](03_costmap2d.md#35-坐标变换)
- [模块架构设计](01_architecture.md)
- [§0 指南](00_guide.md)
- [Planning · Costmap 消费](../08_Planning/01_architecture.md#121-地图层-costmap2dwrapper)