4、环境感知与建图:占据栅格地图、概率栅格地图、OctoMap八叉树地图

做机器人导航这么多年,我越来越觉得——建图这件事,本质上就是回答一个问题:“这个地方到底能不能走?”

你想想看,机器人要导航,首先得知道自己周围长什么样。激光雷达扫一圈,回来一堆点云。但点云本身不是地图,它只是原始数据。我们需要把这些数据转化成机器人能理解的结构化表示。

这一章,我们就来聊聊三种最常用的地图表示方式:占据栅格地图概率栅格地图,以及OctoMap八叉树地图

4.1 占据栅格地图:最朴素的想法

先说最简单的。把空间划分成一个个小格子,每个格子要么是“有障碍物”,要么是“没有障碍物”。这就是占据栅格地图的核心思想。

我在项目中第一次接触这个,是在一个扫地机器人上。当时觉得,这不就是二维数组嘛,有啥难的?后来发现,事情没那么简单。

关键问题:传感器是有噪声的。你测到某个格子有障碍物,它就一定有吗?不一定。可能是噪声,可能是动态物体。

所以,纯粹的0/1二值化表示,在实际中几乎不可用。我们需要更优雅的方式——概率。

4.2 概率栅格地图:用概率说话

概率栅格地图,说白了就是给每个格子一个概率值。0表示绝对空闲,1表示绝对占据,0.5表示完全未知。

每次激光雷达打过来一束射线,我们就更新沿途格子的概率。射线穿过的格子,概率降低(更可能是空闲)。射线终点附近的格子,概率升高(更可能是障碍物)。

嗯,这里要注意:概率更新不是简单的加减法,而是用对数几率(log-odds)来做的。为什么?

因为概率乘法容易溢出,而且不方便累加。对数几率就没有这个问题。

# 一个简单的概率栅格更新示例
import numpy as np

# 初始化:所有格子概率0.5(未知)
grid = np.full((100, 100), 0.5)

# 对数几率转换
def log_odds(p):
    return np.log(p / (1 - p + 1e-10))

def inverse_log_odds(l):
    return 1.0 / (1.0 + np.exp(-l))

# 更新函数
def update_cell(grid, x, y, occupied, l_occ=0.9, l_free=0.4):
    # 当前对数几率
    l = log_odds(grid[y, x])
    # 更新
    if occupied:
        l += l_occ
    else:
        l -= l_free
    # 转回概率
    grid[y, x] = inverse_log_odds(l)

个人经验:我习惯把l_occ和l_free设成不对称的值。比如l_occ=0.9,l_free=0.4。这样即使偶尔有误检测,地图也不会被瞬间污染。说白了,就是让“占据”的证据权重更大一些。

4.3 二维 vs 三维:什么时候用哪种?

二维概率栅格地图,适合地面机器人。比如扫地机、AGV小车。计算量小,实时性好。

但如果你做的是无人机、机械臂,或者需要感知立体空间的机器人,二维就不够用了。这时候需要三维地图。

三维地图怎么做?最直接的想法:把二维栅格扩展到三维,变成体素(voxel)网格。但问题来了——三维空间太大了。

举个例子:一个10m×10m×3m的房间,用5cm分辨率,需要多少体素?

维度 数量
X方向 200
Y方向 200
Z方向 60
总计 2,400,000

240万个格子!而且大部分是空的。这就是三维栅格地图的致命伤——内存爆炸。

4.4 OctoMap八叉树地图:优雅的解决方案

OctoMap的出现,就是为了解决三维地图的内存问题。它的核心思想是:只在需要的地方细化

八叉树的结构很简单:

  • 根节点代表整个空间
  • 每个节点有8个子节点,代表8个卦限
  • 如果某个子区域内部是均匀的(全是空闲或全是占据),就不再细分
  • 如果区域内部有变化,就继续递归细分

这样做的好处很明显:大片的空旷区域,只需要一个节点表示。只有障碍物边界附近,才会产生大量细节点。

对比一下:同样是10m×10m×3m的空间,5cm分辨率。

  • 三维栅格:240万个格子,约2MB(如果每个格子1字节)
  • 八叉树地图:通常只有几万到几十万个节点,内存节省10~100倍

4.5 OctoMap的概率更新

OctoMap不是简单的占据/空闲二值树。每个节点也存储概率值,更新方式和概率栅格地图类似。

我曾在一次无人机避障项目中用过OctoMap。当时遇到一个坑:无人机飞得很快,激光雷达数据更新频率高,八叉树频繁更新,导致CPU占用飙升。

避坑指南:我曾经在OctoMap更新时,每来一帧激光数据就全量更新整棵树。结果CPU直接跑满。后来改成局部更新+定时全局合并的策略,才把性能降下来。

具体做法:只更新机器人周围5m范围内的节点,其他区域每隔10帧再统一更新一次。

4.6 三种地图的选型建议

说了这么多,到底该用哪种?我个人的经验是这样的:

  • 二维平面导航(扫地机、AGV):用概率栅格地图。简单、高效、成熟。
  • 三维空间感知(无人机、机械臂):用OctoMap。内存友好,支持多分辨率查询。
  • 纯障碍物检测(不建图,只避障):用占据栅格地图就够了。甚至可以用更简单的代价地图。

4.7 核心知识体系

下面这张图,是我自己总结的三种地图的核心逻辑对比。你看一眼就能明白它们之间的关系。

环境感知与建图:三种地图对比 占据栅格地图 • 0/1 二值化表示 • 每个格子:占据/空闲 • 不考虑不确定性 • 内存占用小 • 适合简单避障 缺点:无法处理噪声 无法表达未知区域 概率栅格地图 • 概率值 [0, 1] • 对数几率更新 • 支持不确定性 • 二维最常用 • 成熟稳定 缺点:三维内存爆炸 固定分辨率 OctoMap八叉树 • 八叉树结构 • 自适应分辨率 • 内存高效 • 支持三维 • 概率更新 缺点:更新开销大 实现复杂度高 从简单到复杂,从二维到三维,根据实际场景选择合适的地图表示

4.8 实践中的一点感悟

说实话,这三种地图没有绝对的好坏。我见过有人用二维概率栅格做无人机避障,也见过有人用OctoMap做扫地机导航。关键看你的场景和资源。

如果你刚开始做机器人导航,我建议从二维概率栅格地图入手。它足够简单,也足够强大。等遇到三维需求了,再切换到OctoMap也不迟。

嗯,这一章就到这里。地图建好了,下一步就是怎么用它来导航和避障了。

实用工具推荐:

  • ROS中的 map_server 包:加载和保存概率栅格地图
  • octomap_server 包:实时构建八叉树地图
  • 可视化工具:RViz + mapoctomap 显示插件

公众号:蓝海资料掘金营,微信deep3321