1. Map 模块架构设计
本文描述 autonomy/map 的逻辑架构、核心组件关系与运行时数据流。
1.1 设计目标
Map 模块遵循以下设计原则:
与 Nav2 对齐:Costmap2D 接口语义、图层插件模式、代价值定义与 Navigation2
nav2_costmap_2d保持一致插件化图层:静态、障碍、膨胀等以 Layer 插件形式注册,支持动态库加载
双轨地图:2D 代价地图(规划/避障)与 2.5D GridMap(地形感知)并行,按需选用
配置驱动:Lua → Protobuf 的配置管线,便于版本管理与序列化
线程安全:
recursive_mutex保护 costmap 读写,规划端复制快照
1.2 模块总览:三条并行管线
autonomy/map 不是单一地图类,而是三条职责分明的管线,按需组合:
管线 |
入口类 |
输出 |
主要消费者 |
|---|---|---|---|
静态地图 |
|
|
|
2D 代价地图 |
|
|
|
2.5D 多层栅格 |
|
|
地形感知(需自行接入) |
┌──────────────┐
YAML+PGM ────────►│ MapServer │──── OccupancyGrid ────┐
└──────────────┘ │
▼
/scan / 点云 ───►┌──────────────────┐ ┌─────────────┐
│ Costmap2DWrapper │◄───────────│ static_layer│
│ (后台更新线程) │ └─────────────┘
└────────┬─────────┘
│ combined_costmap (uint8)
▼
┌──────────────────┐
│ PlannerServer │ ← 已集成
└──────────────────┘
点云 / 深度 ───►┌──────────────────┐
│ GridMapWrapper │──► elevation / slope / ...
│ (无自动传感器) │ 需应用层写入
└──────────────────┘
✗ 当前未桥接到 Costmap2D
1.3 Costmap2D 架构(重点)
1.3.1 解决什么问题
Costmap2D 将静态环境 + 动态障碍 + 安全膨胀融合为一张 \(N_x \times N_y\) 的 uint8 栅格,供规划器判断「哪里能走、离障碍多近」。它是 Autonomy 导航栈中唯一已与 Planning 打通的地图。
1.3.2 对象组合(从外到内)
Costmap2DWrapper ← 你直接持有的对象(MapInterface)
├── LayeredCostmap ← 管理插件链与双缓冲
│ ├── primary_costmap_ (Costmap2D) ← 有 filter 时插件写入这里
│ ├── combined_costmap_ (Costmap2D) ← 对外输出(getCostmap())
│ ├── plugins_[] ← static / obstacle / inflation ...
│ └── filters_[] ← keepout / speed / binary
├── TF Buffer ← getRobotPose()、坐标变换
├── footprint_ ← 内切/外接半径 → inflation
└── map_update_thread_ ← Start() 后周期性 updateMap + publishMap
关键认知:业务代码通常只接触 Costmap2DWrapper;Costmap2D 是底层栅格存储;Layer 是写入逻辑单元。
1.3.3 生命周期状态机
stateDiagram-v2
[*] --> Constructed: new Costmap2DWrapper(options)
Constructed --> Running: Start()
Running --> Paused: Pause()
Paused --> Running: Resume()
Running --> Stopped: Stop()
Paused --> Stopped: Stop()
Stopped --> Running: Start()
Stopped --> [*]: 析构
状态 |
行为 |
|---|---|
构造后 |
|
|
激活插件、启动后台线程:每 \(1/f\) 秒执行 |
|
线程仍运行,但插件可 deactivate(传感器数据仍可 |
|
停止线程、deactivate 插件 |
|
至少完成一次 |
|
各图层在超时内更新过 |
1.3.4 单次更新:数据如何流过各层
updateMap() 在后台线程中被周期性调用,核心逻辑在 LayeredCostmap::updateMap:
flowchart TD
A["① 取机器人位姿 getRobotPose()"] --> B{"rolling_window?"}
B -->|是| C["平移 origin 使机器人居中"]
B -->|否| D["② 各 plugin updateBounds()"]
C --> D
D --> E["③ 合并为世界坐标矩形 [min_x,max_x]×[min_y,max_y]"]
E --> F["④ 转为 cell 窗口 (x0,y0)…(xn,yn)"]
F --> G["⑤ resetMap:窗口内重置为 default_value"]
G --> H["⑥ 按 plugins 顺序 updateCosts"]
H --> I{"有 filters?"}
I -->|否| J["combined_costmap = 最终结果"]
I -->|是| K["插件→primary → copy → combined → filter"]
K --> J
各层在⑥中的职责:
顺序 |
图层 |
读入 |
写出 |
|---|---|---|---|
1 |
|
|
LETHAL(254) / FREE(0) |
2 |
|
|
marking→LETHAL;clearing→FREE |
3 |
|
下层 lethal 种子 |
1–253 指数衰减梯度 |
4+ |
|
下层结果 |
过滤孤立点 |
合并规则多为 updateWithMax:取 \(\max(c_{\text{master}}, c_{\text{layer}})\)。
1.3.5 外部数据如何进入
入口 API |
时机 |
目标 |
|---|---|---|
|
任意时刻(通常启动时) |
|
|
|
解析 YAML+PGM → StaticLayer |
|
传感器回调(ROS bridge) |
|
|
同上 |
|
|
每次更新 |
TF: |
传感器数据先缓存,在 updateCosts 阶段统一 marking/clearing,而非即时改栅格。
1.3.6 规划器如何读取
// PlannerServer 中的典型模式
std::unique_lock lock(costmap_wrapper->getMutex());
Costmap2D* cm = costmap_wrapper->getCostmap();
// 复制 char_map 到本地 buffer
memcpy(local_buf, cm->getCharMap(), size_x * size_y);
lock.unlock();
// 在本地 buffer 上搜索,不阻塞地图更新
1.4 GridMap 2.5D 架构(重点)
1.4.1 解决什么问题
GridMap 在 XY 平面维护多个命名浮点层(如 elevation、variance),用层值表达高度等地形信息,适合越野、台阶、坡度分析。它与 Costmap2D 并行存在,不负责路径规划的障碍表达(除非自行桥接)。
1.4.2 2.5D 心智模型
俯视图 (XY) 侧视概念 (XZ)
┌───┬───┬───┐ elevation 层存 z 值
│0.1│0.1│0.2│ ← 每个 cell ┌──────────────┐
├───┼───┼───┤ 有多个 float │ ╱╲ 台阶 │
│0.1│0.3│0.3│ 属性层 │ ╱ ╲ │
├───┼───┼───┤ └──────────────┘
│0.0│0.0│0.1│
└───┴───┴───┘
↑ position_ = 地图中心(世界坐标)
不是完整 3D 体素:第三维信息编码在层值中,查询 \((x,y)\) 时通过插值得到连续高程。
1.4.3 对象组合
GridMapWrapper ← Autonomy 封装(MapInterface)
└── grid_map::GridMap ← ETH 核心类
├── data_["elevation"] → Eigen::MatrixXf (rows × cols)
├── data_["variance"] → Eigen::MatrixXf
├── layers_ → ["elevation", "variance", ...]
├── basicLayers_ → 判定有效性的层(含 NaN 则无效)
├── position_ → 地图中心 [m]
├── length_ → 物理尺寸 [m]
├── resolution_ → [m/cell]
└── startIndex_ → 循环 buffer 起始(move 时用)
与 Costmap2D 的本质区别:GridMap 是 map<string, Matrix> 的多层浮点结构;Costmap2D 是单层 uint8[] + 插件流水线。
1.4.4 生命周期(当前实现)
stateDiagram-v2
[*] --> Constructed: new GridMapWrapper(options)
note right of Constructed: 构造时已 setGeometry / add layers
Constructed --> Started: Start()
Started --> Paused: Pause()
Paused --> Started: Resume()
Started --> Stopped: Stop()
Stopped --> Started: Start()
与 Costmap2D 的差异 |
说明 |
|---|---|
无后台更新线程 |
|
无内置 feed API |
点云/深度需应用层写入 |
publishMap 未实现 |
日志输出占位,需自行序列化发布 |
loadMap 未实现 |
文件加载为 TODO |
因此 GridMap 当前是数据容器 + 坐标/插值工具,而非开箱即用的感知流水线。
1.4.5 典型数据流(应用层驱动)
flowchart LR
A["传感器 / 算法"] --> B["应用层处理"]
B --> C["写入 GridMap 层<br/>atIndex / addDataFrom"]
C --> D["可选: move(position)<br/>滑动窗口"]
D --> E["atPosition 插值查询"]
E --> F["控制 / 可视化 / 自定义代价"]
1.4.6 循环 buffer 与 move()
move(new_position) 不改变世界系中的几何数据语义,只平移 buffer 窗口:
计算索引偏移 \(\Delta \mathbf{i} = \mathrm{round}((\mathbf{p}_{\text{new}} - \mathbf{p}_{\text{map}})/\Delta)\)
移出窗口的行/列置
NaN更新
startIndex_、position_
旧数据在 map frame 中保持 stationary,适合机器人移动时局部更新地形。
1.5 分层架构(总图)
┌─────────────────────────────────────────────────────────────────┐
│ 应用层 (Planning / Control / Navigator) │
│ getCostmap() / snapshotOccupancyGrid / GridMap 查询 │
└──────────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────────────┐
│ 封装层 (Wrapper / Server) │
│ Costmap2DWrapper MapServer GridMapWrapper │
│ · 后台更新线程 · YAML/PGM 加载 · 无自动更新 │
│ · 传感器 feed · OccupancyGrid 发布 · 应用层写层 │
│ · TF / footprint · 回调注入 · 插值查询 │
└──────────┬─────────────────────┬──────────────────────┬───────────┘
│ │ │
┌──────────▼──────────┐ ┌────────▼────────┐ ┌─────────▼──────────┐
│ LayeredCostmap │ │ OccupancyGrid │ │ grid_map::GridMap │
│ · primary buffer │ │ -1 / 0 / 100 │ │ float 多层矩阵 │
│ · combined buffer │ │ │ │ elevation / ... │
│ · plugin chain │ └─────────────────┘ └────────────────────┘
└──────────┬──────────┘
│
┌──────────▼──────────────────────────────────────────────────────┐
│ 图层插件 (Plugins) │
│ static_layer → obstacle_layer → inflation_layer → [filters] │
└─────────────────────────────────────────────────────────────────┘
1.5.1 服务层 — Costmap2DWrapper
Costmap2DWrapper 是 Costmap2D 对外的运行时入口,职责包括:
职责 |
实现要点 |
|---|---|
生命周期 |
实现 |
插件加载 |
从 |
传感器接入 |
|
位姿更新 |
|
静态地图 |
|
快照导出 |
|
1.5.2 静态地图层 — MapServer
MapServer 独立于 Costmap 运行,负责:
从 YAML+PGM 文件加载静态地图
运行时
SetStaticMap()注入通过
MapPublishCallback周期性或一次性发布OccupancyGrid消费者(
static_layer或Costmap2DWrapper::applyOccupancyGrid)订阅后写入 costmap
1.5.3 多层聚合 — LayeredCostmap
LayeredCostmap 管理插件链与双缓冲:
缓冲区 |
用途 |
|---|---|
|
插件写入目标 |
|
最终输出(filter 处理后) |
无 filter 时二者合一;有 filter 时:插件 → primary → copy → combined → filter 修改 combined。
1.5.4 GridMapWrapper 补充说明
GridMapWrapper 封装 ETH grid_map::GridMap:
多层浮点数据(
elevation、variance等)循环 buffer 实现高效滑动窗口
与 Costmap2D 无运行时自动同步(桥接模块
grid_map_costmap_2d当前未实现)
1.6 图层系统
1.6.1 类层次
Layer (abstract)
├── CostmapLayer : Layer + Costmap2D
│ ├── StaticLayer # 静态 SLAM 地图
│ ├── ObstacleLayer # 激光/点云障碍
│ └── VoxelLayer # 3D 体素 → 2D 投影
├── InflationLayer # 膨胀(无内部 costmap)
├── DenoiseLayer # 连通域去噪
├── RangeSensorLayer # 距离传感器
└── CostmapFilter : Layer
├── KeepoutFilter # 禁行区
├── SpeedFilter # 速度限制区
└── BinaryFilter # 二值过滤
1.6.2 默认插件链
plugins = {"static_layer", "obstacle_layer", "inflation_layer"}
更新顺序严格按 plugins 列表:updateBounds 累加更新矩形 → resetMap → 按序 updateCosts。
1.6.3 外部插件加载
planner.lua
costmap.plugins = { "static_layer", "obstacle_layer", ... }
costmap.obstacle_layer.plugin = "libautonomy_map_layers_obstacle_layer.so"
│
▼
PluginManager::LoadPlugin(.so)
│
▼
Layer::onInitialize() → updateBounds / updateCosts
1.7 端到端数据流
1.7.1 Costmap 更新时序
sequenceDiagram
participant Sensor as 激光 / 点云
participant CW as Costmap2DWrapper
participant LC as LayeredCostmap
participant SL as StaticLayer
participant OL as ObstacleLayer
participant IL as InflationLayer
participant PS as PlannerServer
Note over Sensor,PS: 传感器回调与更新线程异步
Sensor->>CW: feedLaserScan (任意时刻)
CW->>OL: 写入 ObservationBuffer
loop 每 1/f 秒
CW->>CW: getRobotPose() via TF
CW->>LC: updateMap(x, y, yaw)
LC->>SL: updateBounds → updateCosts
LC->>OL: updateBounds → updateCosts (消费 Buffer)
LC->>IL: updateBounds → updateCosts
end
PS->>CW: getMutex() + 复制 char map
PS->>PS: GlobalPlanner 搜索
1.7.2 单次 updateMap 步骤
Rolling window(若
rolling_window_ == true): $\(x_0^{\text{new}} = x_{\text{robot}} - W/2,\quad y_0^{\text{new}} = y_{\text{robot}} - H/2\)$ 调用Costmap2D::updateOrigin()平移窗口。各 plugin/filter 调用
updateBounds()扩展更新矩形 \([min_x, max_x] \times [min_y, max_y]\)。转为 cell 边界 \((x_0, y_0, x_n, y_n)\),
resetMap后按序updateCosts()。若有 filter:primary → copy → combined → filter
updateCosts()。
1.7.3 静态地图注入
YAML + PGM
│
▼
MapServer::LoadMap()
│
▼
OccupancyGrid (-1/0/100)
│
├── MapPublishCallback → /map 话题
│
└── Costmap2DWrapper::applyOccupancyGrid()
│
▼
StaticLayer::loadOccupancyGrid()
│
▼
Trinary / Scale 模式 → LETHAL / FREE
1.8 配置管线
config/planner/planner.lua
│
▼
LuaParameterDictionary
│
▼
map::CreateCostmap2DOptions() → proto::Costmap2DOptions
│
▼
Costmap2DWrapper(options)
│
├── options.plugins() → 图层插件链
├── options.static_layer() → StaticLayer 参数
├── options.obstacle_layer() → ObstacleLayer 参数
├── options.inflation_layer() → InflationLayer 参数
└── options.footprint() → 机器人 footprint
Costmap2DOptions proto 主要字段:
字段 |
类型 |
说明 |
|---|---|---|
|
|
栅格分辨率 [m/cell] |
|
|
地图物理尺寸 [m] |
|
|
更新频率 [Hz] |
|
|
是否滑动窗口 |
|
|
图层插件列表 |
|
|
机器人轮廓 |
|
|
圆形近似半径 |
1.9 Costmap2D 与 GridMap 对比
┌─────────────────┐ ┌─────────────────┐
│ Costmap2D │ │ GridMap │
│ uint8 0–255 │ │ float 多层 │
│ 左下角原点 │ │ 中心原点 │
│ 规划 / 避障 │ │ 地形 / 高程 │
└────────┬────────┘ └────────┬────────┘
│ │
│ grid_map_costmap_2d │
│ (当前未实现) │
└───────────┬───────────┘
▼
未来:互转 / 融合
维度 |
Costmap2D |
GridMap |
|---|---|---|
数值类型 |
|
|
原点 |
左下角 |
中心 |
规划集成 |
✅ PlannerServer |
❌ 需自行接入 |
桥接 |
|
同上 |
1.10 与系统其他模块的集成
config/autonomy.lua
planning = AUTONOMY_PLANNER
│
▼
system::Autonomy
planner_ = PlannerServer(options_.planner_options())
│
├── planner_.costmap_ = Costmap2DWrapper
├── Navigator(间接消费 costmap)
└── Visualization(订阅 costmap 快照)
独立 MapServer 通常在系统启动时单独构造,通过话题或回调与 Costmap 连接。
1.11 线程与并发
场景 |
策略 |
|---|---|
地图更新 vs 规划 |
规划时 |
多传感器写入 |
|
GridMap 读写 |
Eigen 矩阵无内置锁,外部需同步 |
后台更新 |
|
1.12 扩展自定义图层
实现步骤:
继承
CostmapLayer(有内部 costmap)或Layer(如 InflationLayer)实现
onInitialize()、updateBounds()、updateCosts()编译为
.so,在plugins和对应配置块中注册在
map_2d_option.proto中添加配置字段(如需)
最小接口示例:
class MyLayer : public CostmapLayer {
public:
void onInitialize() override { /* 读配置 */ }
void updateBounds(double rx, double ry, double ryaw,
double* min_x, double* min_y,
double* max_x, double* max_y) override { /* 扩展边界 */ }
void updateCosts(Costmap2D& master, int min_x, int min_y,
int max_x, int max_y) override { /* 写入 master */ }
};