4. 占据栅格地图原理
栅格地图,说白了就是把环境切成一个个小方块,每个方块独立回答一个问题:「这里有障碍吗?」。我刚开始接触SLAM时,觉得这方法太朴素了——但后来才发现,正是这种「朴素」让它极其鲁棒。今天咱们就把它从数学到工程细节彻底聊透。
4.1 栅格地图的数学表示
我们把环境划分成均匀的网格,每个格子记作 mᵢ。整个地图就是一组随机变量:m = {m₁, m₂, …, mₙ}。每个 mᵢ 只有两种状态:占据(1)或空闲(0)。
你可能会问:为什么不直接用0和1?因为传感器有噪声,直接二值化太武断。所以我们用概率表示——p(mᵢ = 1) 表示格子被占的概率。我个人习惯用 对数概率(log-odds)来存,原因后面会讲。
4.2 贝叶斯更新公式
有了传感器新数据,怎么更新格子概率?贝叶斯公式就是我们的武器。对于单个栅格 mᵢ,给定观测 z₁ₜ 和位姿 x₁ₜ:
p(mᵢ | z₁ₜ, x₁ₜ) =
p(zₜ | mᵢ, z₁ₜ₋₁, x₁ₜ) · p(mᵢ | z₁ₜ₋₁, x₁ₜ₋₁) / p(zₜ | z₁ₜ₋₁, x₁ₜ)
看着复杂?实际用对数概率形式就清爽多了。定义 l(mᵢ) = log( p(mᵢ) / (1 - p(mᵢ)) ),更新公式变成:
lₜ(mᵢ) = lₜ₋₁(mᵢ) + lₒᵣₑᵥ(mᵢ) - l₀
其中 lₒᵣₑᵥ 是反演测量模型给出的对数概率,l₀ 是先验。说白了,就是加减法,没有乘除,数值稳定还快。我在项目里用这个公式跑了上千次,从未出现概率溢出。
4.3 反演测量模型
反演测量模型,听起来高大上,其实就是回答:「如果传感器读到这个值,格子应该更可能是占据还是空闲?」。以激光雷达为例,一束激光打到某个距离,那么:
- 击中点对应的格子 → 占据概率增加
- 激光路径上的格子(除击中点) → 空闲概率增加
- 未探测到的格子 → 保持原样
我见过不少新手直接把激光终点标记为占据,忽略了路径上的空闲信息。结果地图里全是「假墙」——明明激光穿过了空白区域,却什么都没学到。反演模型把这两部分都考虑进去,地图才干净。
4.4 地图分辨率与内存优化
分辨率直接决定地图精度。5cm 的格子比 10cm 的格子能表现更细的走廊边缘,但内存占用是平方倍增长。一张 100m×100m 的地图:
| 分辨率 | 栅格数量 | 内存(float32) | 典型场景 |
|---|---|---|---|
| 20 cm | 250,000 | ~1 MB | 室外大场景 |
| 10 cm | 1,000,000 | ~4 MB | 室内仓库 |
| 5 cm | 4,000,000 | ~16 MB | 高精度建图 |
| 2 cm | 25,000,000 | ~100 MB | 精细重建 |
你想想看,如果每个格子用 float64 存概率,内存直接翻倍。我一般用 int8 存对数概率,范围 -127~127,精度完全够用。另外,ROS2 的 nav_msgs/msg/OccupancyGrid 默认就用 int8,-1 表示未知,0~100 表示空闲到占据。
① 用对数概率 + int8 存储,比 float32 省 4 倍空间。
② 只更新传感器视野内的格子,懒加载未探索区域。
③ 对大型地图使用分块(tile)管理,只加载当前任务区域。
我在一个园区项目里,地图 500m×400m,分辨率 10cm,按传统方式要 80MB。用了分块 + int8 后,实际常驻内存只有 12MB。嗯,优化空间永远比你想的大。
occupancy_grid,记得把未知区域设为 -1,而不是 50。很多导航算法会把 50 当作「半信半疑」,导致规划路径畏畏缩缩。