Part IV · 第 15 章 状态估计与建图 · 15.3

地图表示

SLAM 告诉机器人自己在哪里,可地图里到底该存什么?没有一种地图能回答所有问题:定位要的是认得出的特征,规划要的是能不能过、离障碍多远,任务要的是“杯子在厨房”。本节从激光雷达建占据栅格讲起(反向传感器模型、最大量程、位姿漂移造成的双墙),再到导航栈真正使用的代价地图与膨胀、距离场、拓扑地图、路标与语义地图,最后处理会变化的世界:地图选什么,取决于读它的规划器和估计器。

约 40 分钟 前置:12.2 体素、网格与 SDF · 4.2 条件概率与贝叶斯定理 · 4.7 贝叶斯滤波 · 14.1 图搜索与 A* · 14.3 CHOMP 与 TrajOpt · 15.2 SLAM 与位姿图 6 个交互

    1为什么机器人需要它

    15.2 节的 SLAM 解决了“我在哪里”,并顺手留下一份地图。但“地图”这个词掩盖了一个设计决定:要存什么。同一栋楼,扫地机器人存一张 5 cm 分辨率的占据栅格,自动驾驶车存车道线和交通灯的位置,视觉 SLAM 存几十万个带描述子的三维点,而一个听得懂“去厨房拿杯子”的机器人还要知道哪片区域叫厨房、杯子在哪个柜子上。

    原因是地图有不同的读者,每个读者问不同的问题。定位问“从这里看出去,传感器会看到什么”,它需要独特、可重复识别的东西(角点、柱子、特征点)。运动规划问“机器人的身体能不能放在这里、离障碍多远”,它需要空闲空间与净空(14.1 节的 A* 在栅格上搜索,14.3 节的 CHOMP 要距离场的梯度)。任务规划问“东西在哪、房间怎么连”,它需要物体和地点。没有一种表示能把三个问题都答得又快又省,所以真实系统总是同时维护几种地图,各自为一个读者服务。

    12.2 节已经介绍了体素、OctoMap、TSDF,以及对数几率形式的占据栅格(那里的 FIG 2),本节不再重复,而是往前走:激光雷达的反向传感器模型怎样定、最大量程读数怎么办、位姿不准时地图会变成什么样;导航栈在占据栅格之上又搭了什么(代价地图、距离场、拓扑图);稀疏的路标地图与语义地图各自擅长什么;以及世界在变的时候,地图怎样“改口”和“遗忘”。

    2直觉:同一个世界,六张地图

    下面是一套 12 m × 8 m 的公寓:左边客厅(一张桌子、一根柱子),右下办公室(书架、角落里一张深色沙发),右上厨房(靠墙的橱柜),两扇 1 m 宽的门。同一个世界画成六种地图。拖动琥珀色探针,读数会同时列出六张地图对“这里”分别能回答什么,以及每张地图占多少内存。

    FIG 1一个世界,六张地图,各答各的问题
    分辨率 10 cm(120 × 80 格)。占据栅格:红 = 占据,青 = 空闲。代价地图:机器人半径 0.3 m、膨胀半径 1.0 m、衰减 \(k=3\);红 = 障碍本身(254),紫 = 机器人中心进去就必撞(253),琥珀 = 越靠近障碍越贵(越琥珀代价越高)。距离场:越青离最近障碍越远,探针处的箭头指向远离障碍最快的方向。拓扑图:青色细线是空闲空间的“骨架”(到两侧障碍等距的线),每个房间一个节点、每扇门一个节点。路标:琥珀色点是墙角、门框、家具角和柱子,绿线连向探针在 5 m 内能直接看到的路标。语义:物体框和房间名。
    试一试
    • 把探针放到一个占据栅格说“空闲”、代价地图却说“机器人中心不能在这里”(代价 ≥ 253)的格子上。两张地图哪张错了?
    核心洞见

    地图是给读者准备好的答案缓存。占据栅格答“是否被占”,代价地图答“对我这台机器人有多危险”,距离场答“离障碍多远、往哪躲”,拓扑图答“经过哪些房间和门”,路标答“从这里能认出什么”,语义地图答“这是什么”。上面那个任务里两张地图都没错:它们回答的是不同的问题。选地图,就是先问清楚谁来读它、问什么、要多快。

    3动手:用激光雷达建一张占据栅格

    12.2 节已经说明了占据栅格的核心:每格存“被占据”的对数几率 \(\ell\),每次测量往上加一个数(4.2 节的“证据相加”)。真正动手时,你得决定两件那里没细讲的事。第一是反向传感器模型(inverse sensor model):一束测得距离 \(z\) 的激光,对它经过的每个格子到底意味着什么?最常用的写法(Thrun 等《Probabilistic Robotics》第 9 章)只有一个参数:障碍的“厚度” \(\hl{alpha}{\alpha}\)。格心到传感器的距离 \(r\) 小于 \(z-\alpha/2\) 的格子加 \(\hl{lfree}{\ell_{\rm free}}\)(激光穿过了它,多半是空的);\(|r-z|\le\alpha/2\) 的格子加 \(\hl{locc}{\ell_{\rm occ}}\)(激光停在这里);更远的格子什么也不加(激光没到过那里)。第二是没有回波的射线:可能前方 5 m 内真的什么都没有,也可能打在了深色或透明的表面上没反射回来。你可以“忽略它”,也可以“把整条射线到最大量程都当作空闲”。

    还有一件事是地图之外的:画地图用的是估计的位姿。下面的机器人沿一条 57 m 的环线走过三个房间,每 0.2 s 扫描一次(360°、180 束、量程 5 m)。“里程计航向漂移”给估计的航向每走一米加一点偏差,地图就按这个有偏差的位姿去画,正如没有回环校正的里程计(15.1、15.2 节)。

    FIG 2激光雷达占据栅格建图:反向传感器模型、无回波与位姿漂移
    左:地图(越红越确定是占据,越青越确定是空闲,无色 = 未知),虚线是真实环境(机器人看不到它)。琥珀色圆盘是真实位姿,紫色空心圆是里程计估计的位姿,地图按紫色那个画;琥珀色细线是最近一次扫描,蓝线是最接近正前方、且有回波的那一束。右上:这一束的反向传感器模型,横轴是到传感器的距离 \(r\),线是 \(\Delta\ell(r)\),圆点是这束真正经过的格子(按格心距离取值)。\(\ell_{\rm occ}=+0.847\)(\(p=0.7\)),\(\ell_{\rm free}=-0.405\)(\(p=0.4\)),\(\ell\) 钳在 \([-2,3.5]\)。读数里:已探索 = 真实空闲格中被地图判为空闲(\(\ell\le-0.4\))的比例;墙面覆盖 = 被观测过的真实表面格中被判为占据(\(\ell\ge0.4\))的比例;误占据 = 离真实障碍超过一格却被判为占据的格子数;沙发误空 = 深色沙发里被判为空闲的格子数。
    试一试
    • 用默认设置(无漂移,\(\alpha\le0.2\) m)按“运行”:已探索超过 90%,同时误占据少于 30 格。
    • 把 \(\alpha\) 调到 0.35 m 以上再运行:误占据超过 300 格。墙朝哪个方向“长胖”了,为什么?
    • 把航向漂移设为 0.2 °/m 以上,跑完整圈(可按“跑完一圈”):墙面覆盖跌到 60% 以下,回到客厅时墙变成了两层。
    • 把“无回波”改成“当作空闲到最大量程”,让机器人经过办公室:深色沙发里超过 100 格被标成空闲。改回“忽略”再试。
    有意思的细节

    (1) 默认设置跑完一圈:已探索 91.9%,墙面覆盖 100%,误占据只有 5 格。已探索停在 92% 左右而不是 100%,主要是因为紧贴表面的那一圈空闲格落在占据带 \(\alpha/2\) 之内,每次都被加 \(\ell_{\rm occ}\);把 \(\alpha\) 降到 0.05 m,已探索能到 97.6%。 (2) 漂移小得惊人也会毁掉地图:每米 0.05°(一整圈不到 3°)时墙面覆盖降到 91.2%、误占据 200 格;每米 0.2° 时只剩 50.3%、误占据 689 格。 (3) “空闲到最大量程”只把已探索从 91.9% 提到 92.6%,却让沙发里 162 格变成了空闲:多出来的那一点覆盖不值这个风险。

    4给规划器的地图:代价地图、距离场与拓扑

    代价地图与膨胀

    占据栅格说的是“这一格里有没有东西”,可规划器关心的是“我的机器人放在这一格中心会不会撞”。对一个半径为 \(\rho\) 的圆形机器人,答案是:只要离最近障碍的距离小于 \(\rho\) 就会撞。这正是 7.2 节的构型空间障碍:把障碍向外“膨胀” \(\rho\),机器人就能当成一个点来规划。ROS 的 costmap_2d(以及 ROS 2 的 Nav2)把这件事做成了一个分层结构(Lu 等 2014 的 layered costmap):静态层来自 SLAM 建好的地图,障碍层来自实时传感器(这里是一辆可以拖动的推车),膨胀层按到最近障碍的距离 \(d\) 给每格一个 0 到 254 的代价,各层逐格取最大。代价的形状只有三个参数:内切半径 \(\hl{rins}{r_{\rm ins}}\)(以内一律 253,“必撞”)、膨胀半径 \(r_{\rm inf}\)(以外为 0)、以及中间指数衰减的速度 \(\hl{kscale}{k}\)。

    规划器(这里是 14.1 节的 A*,8 连通)把代价 ≥ 253 的格子当成墙,其他代价为 \(c\) 的格子步长乘以 \(1+3c/252\):离墙近的格子能走,但贵。所以 \(r_{\rm ins}\) 决定“能不能过”,\(r_{\rm inf}\) 和 \(k\) 决定“愿意离墙多远”。

    FIG 3分层代价地图:膨胀决定“能不能过”和“愿意离墙多远”
    左:合成后的代价地图(配色同 FIG 1:红 254、紫 253、越琥珀代价越高),绿色折线是 A* 从绿色起点到蓝色终点的路径,起点和终点处的圆是机器人的轮廓(半径 \(r_{\rm ins}\))。琥珀色方块是障碍层里的推车,可拖动。“显示”只改变画出哪几层,规划永远用全部层的合成。右:代价随距离的曲线 \(c(d)\)。读数里的“平均净空”是路径上各格到最近障碍距离的平均值。距离按格心之间计算,所以比到障碍表面的真实距离多出约半格(5 cm)。
    • 把机器人半径 \(r_{\rm ins}\) 慢慢加大,直到规划器报告“无路径”。门宽 1.0 m,这个半径和门宽的一半有什么关系?
    • 默认值是 costmap_2d 的出厂值(\(r_{\rm inf}=0.55\) m,\(k=10\)),路径的平均净空只有约 0.8 m,一路贴着墙走。把 \(r_{\rm inf}\) 调到 1.2 m 以上、\(k\) 调到 1 到 3 之间:平均净空超过 1.0 m,而路径只长了约一成。

    距离场:怎样从栅格算出来

    膨胀层的核心是“每格到最近障碍格的距离”,也就是 距离变换。12.2 节讲了 SDF 是什么、它的梯度为什么能把轨迹推离障碍(CHOMP、TrajOpt 正是这样用它的,见 14.3 节);这里关心的是从一张栅格怎样算出它。最直接的办法叫“野火”(brushfire):从所有障碍格同时出发做一次 Dijkstra(14.1 节),像火从障碍向外烧。问题在于步长:只走上下左右(4 连通),量出来的是曼哈顿距离;8 个方向都算 1 步,量出来的是切比雪夫距离;斜步算 \(\sqrt2\) 的“倒角”(chamfer)距离好得多,但仍不精确。精确的欧氏距离变换(Felzenszwalb 与 Huttenlocher)则把二维问题拆成“先每列、再每行”的一维问题,每个一维问题用抛物线下包络在线性时间里解完。

    下图左边是一个小栅格(每格看作 10 cm),点击格子可以添加或删除障碍。选一种算法,看等距带(交替的青色条纹)的形状:一个孤立障碍点周围,曼哈顿是菱形,切比雪夫是正方形,倒角是八边形,只有精确 EDT 是圆。

    FIG 4距离变换:野火的度量误差与精确 EDT
    40 × 26 格。红格是障碍(点击切换)。“距离”视图:青色条纹每 3 格换一次深浅,所以条纹的边界就是等距线。“误差”视图:所选算法相对精确 EDT 的相对误差,越琥珀高估越多,越紫低估越多。拖动琥珀色探针:实线圆的半径是所选算法给出的距离,虚线圆是精确距离。“出堆次数”是 Dijkstra 从优先队列取出的格子数;精确 EDT 做的是每行、每列各一遍线性扫描。
    • 选“4 连通”,把探针拖到一个距离被高估 30% 以上的地方(提示:障碍的斜对角方向)。再换成“倒角”:整张图的最大误差降到 8.3% 以下。

    拓扑地图:从骨架到“房间与门”

    距离场还藏着一个结构:它的“山脊”,即到两个(或更多)障碍等距的点,连成一张网,叫广义 Voronoi 图(GVD,12.2 节称之为中轴)。沿着它走的机器人离两侧障碍一样远,是最安全的路线;它把二维的空闲空间压缩成一维的骨架。Thrun(1998)进一步观察到:骨架上净空出现局部极小的地方,就是门、走廊口这样的“瓶颈”。把每个这样的关键点(\(\hl{crit}{\mathbf c}\))和它两侧最近的障碍点(“基点”)连起来,就是横穿瓶颈的关键线;沿关键线把空闲空间切开,剩下的每一块就是一个“地点”(房间)。地点做节点、关键线做连接它们的边,就得到一张拓扑地图:图很小(这里 3 个地点、2 扇门),规划在上面几乎不花时间,而且“经过办公室去厨房”这样的结果能直接说给人听。

    下图把这条流水线完整跑了一遍(按 Thrun 的思路做了简化,细节见第 5 节):距离场 → 骨架 → 净空小于 0.7 m 的窄段里的局部极小 → 关键线 → 区域 → 图。拖动客厅里的桌子、改变客厅门的宽度,看拓扑怎样变。

    FIG 5拓扑地图:骨架、关键点、区域与图
    青色细线是骨架(净空 ≥ 0.3 m 的部分,经过细化),其中净空小于 0.7 m 的窄段画成琥珀色。紫色菱形是关键点,紫色线段是关键线。每个区域淡淡地涂一种颜色(少于 40 格的碎片不算),节点上的标签是区域编号 P1、P2…(按从左到右排列)。拖动绿色起点和蓝色终点:读数给出图上的路线(地点和门的序列),并与栅格 A* 要展开的格子数比较。拖动桌子中心(琥珀色手柄)时,它离墙和柱子至少保持 0.7 m。
    • 把客厅门加宽到 1.4 m 以上:客厅和办公室合并成一个地点,图从 3 个地点变成 2 个。门并没有消失,是什么变了?
    • 把桌子拖进客厅的左上角,离两面墙都约 0.8 m:桌子背后的那条 L 形窄道被两条关键线封住,成了第 4 个地点。

    稀疏地图与语义地图

    到目前为止的地图都是稠密的:每一格都有值,不管那里有没有东西。另一类地图只存少数有用的东西。路标地图(landmark / feature map)只存可重复识别的点:EKF-SLAM 与 FastSLAM 估计的就是路标的坐标(15.2 节),视觉 SLAM(如 ORB-SLAM)存的是三维地图点,每个点带一个 32 字节的 ORB 描述子。它极其省内存、天生适合定位(用 11.4 节的 PnP 或 12.1 节的配准),但它不知道两个路标之间是不是空的,所以不能直接拿来规划。语义地图再往上一层,存的是“东西”和“地方”:物体的类别、位置和大小,房间的名字和连通关系。三维场景图(3D scene graph)把它们组织成层级:建筑 → 楼层 → 房间 → 地点 → 物体,可以回答“厨房里有没有杯子”这样的问题,也是语言驱动的机器人(Part VII)读取世界的接口。

    表示回答的问题内存随什么增长典型读者弱点
    占据栅格这里被占了吗?面积 / 分辨率²扫描匹配定位、建图没有距离;假设世界静止、格子独立
    代价地图对这台机器人有多危险?同上(常是滚动窗口)栅格规划器、局部控制器绑定一个机器人形状和一组参数
    距离场(ESDF)多远?往哪躲?同上,每格 4 字节轨迹优化(14.3 节)、MPC障碍一变就要重算(增量 EDT)
    拓扑图经过哪些地方?地点数全局路线、任务规划、人机对话没有几何细节;分割依赖阈值
    路标 / 特征地图从这里能认出什么?路标数定位、回环检测(15.2 节)不知道哪里是空的,不能直接规划
    语义 / 物体地图这是什么?东西在哪?物体和地点数任务规划、语言指令依赖识别器;类别会错,物体会被挪走
    一个想法,几种看法

    概率:占据栅格是每格一个二值状态的贝叶斯滤波(4.7 节),只有测量更新、没有预测步。几何:代价地图的内切区就是构型空间障碍(7.2 节),距离场是它的“连续版本”。图:栅格本身就是一张图(每格一个节点),拓扑地图是把它在瓶颈处切开、每块缩成一个节点后的“商图”。压缩:从栅格到路标,从路标到语义,每往上一层,存的东西更少、更抽象,能回答的几何问题也更少。

    5再看数学

    提示:把鼠标悬停在公式中带底色的符号上,图中对应的元素会被加粗或高亮(反向传感器模型的三段、内切半径、衰减曲线、探针的距离圆、关键点、被遗忘的墙)。

    反向传感器模型与对数几率

    12.2 节推导过:在各格独立、测量条件独立的假设下,格子 \(i\) 的对数几率按 \(\ell_{t,i}=\ell_{t-1,i}+\operatorname{logit}p(m_i\mid z_t,x_t)-\ell_0\) 更新(\(\ell_0=0\),即先验 \(p=0.5\))。中间那一项 \(p(m_i\mid z_t,x_t)\) 叫反向传感器模型,因为它把因果倒过来写:传感器模型本来是“世界 → 测量”的 \(p(z\mid m,x)\),这里要的却是“测量 → 这一格”。FIG 2 用的是最简单的分段形式(\(r_i\) 是格 \(i\) 的格心到传感器的距离,\(z\) 是这一束的读数;没有回波的射线在“忽略”策略下对所有格子都是 0):

    $$ \Delta\ell_i=\begin{cases}\hl{lfree}{\ell_{\rm free}}=\ln\dfrac{0.4}{0.6}=-0.405, & r_i<z-\hl{alpha}{\alpha}/2\\[6pt] \hl{locc}{\ell_{\rm occ}}=\ln\dfrac{0.7}{0.3}=+0.847, & |r_i-z|\le\hl{alpha}{\alpha}/2\\[6pt] 0, & r_i>z+\hl{alpha}{\alpha}/2\end{cases} $$

    \(\alpha\) 怎么选。占据带的宽度 \(\alpha\) 要盖住测距噪声(几个 \(\sigma\))和栅格的离散化(终点所在格的格心离真实终点最远可达半个对角线 \(h/\sqrt2\)),所以 \(\alpha\) 通常取一到两个格宽。取得太大,表面前方(朝传感器一侧)\(\alpha/2\) 内本来空闲的格子每次都被加 \(\ell_{\rm occ}\),墙就朝机器人“长胖”:这是 FIG 2 里 \(\alpha=0.4\) m 时约 400 个误占据格的来源。表面后方没有这个问题,因为射线停在表面上,不会再往后加。

    独立性假设的代价。每一束都被当成对每一格的独立证据,但同一次扫描里相邻的几束其实经过同一批格子、受同一个位姿误差影响;把它们当独立证据相加会让地图过度自信(4.2 节的“同一票数了两遍”)。Thrun(2003)用前向模型 \(p(z\mid m,x)\) 对整张地图做优化,能消除这种矛盾,但代价高得多;实际系统用钳位 \(\ell\in[\ell_{\min},\ell_{\max}]\) 来给自信设上限。

    位姿误差变成地图误差。一个航向误差 \(\delta\theta\) 会把距离 \(r\) 处的终点横向移动约 \(r\,\delta\theta\)。FIG 2 的漂移是每米 \(\kappa\),走完长 \(L\) 的环线后航向误差为 \(\kappa L\):\(\kappa=0.05\) °/m 时 \(57\times0.05\approx2.9^\circ\),即 0.050 弧度,5 m 外的墙就偏了 \(5\times0.050\approx0.25\) m,两格半。同一面墙被两个不同的位姿各画一遍,就是“双墙”。这就是建图离不开 15.2 节的回环与位姿图:先把位姿估对,再画地图(或在回环后用新位姿把地图重画一遍)。

    膨胀代价

    costmap_2d 的膨胀层用的就是下面这个函数(\(d\) 是格心到最近致命格的距离;它把结果截断成整数,FIG 3 四舍五入,最多差 1)。只要中心距离 \(d\le r_{\rm ins}\),圆形机器人就一定撞;\(r_{\rm ins}\) 外代价从 252 开始按 \(e^{-k(d-r_{\rm ins})}\) 衰减:\(k\) 大,代价只在墙边一窄条里有,路径就贴墙;\(k\) 小,代价铺得更开,路径就被推向房间中间。

    $$ c(d)=\begin{cases}254, & d=0\\ 253, & 0<d\le \hl{rins}{r_{\rm ins}}\\ \big\lfloor 252\,e^{-\hl{kscale}{k}\,(d-r_{\rm ins})}\big\rceil, & r_{\rm ins}<d\le r_{\rm inf}\\ 0, & d>r_{\rm inf}\end{cases} $$ $$ s=\ell_{\rm step}\Big(1+w\,\frac{c}{252}\Big),\qquad c<253 $$

    (254 是致命格,253 是内切区;\(s\) 是 A* 走进代价为 \(c\) 的格子的步长代价,\(\ell_{\rm step}\) 是 1 格或 \(\sqrt2\) 格,\(c\ge253\) 的格子禁止进入;\(\lfloor\cdot\rceil\) 是四舍五入(FIG 3 的做法),\(w=3\) 是 FIG 3 里代价的权重。)注意 \(r_{\rm ins}\) 是机器人轮廓的内切圆半径。非圆形机器人(比如长方形底盘)还有一个外接圆半径:中心距离在两者之间时,撞不撞取决于朝向,代价地图只能把它标成“可能撞”(这里的 252 及以下),真正的碰撞检查要交给知道轮廓的局部控制器。

    距离变换:误差界与线性时间的精确算法

    野火在无障碍的平面上走出的是格点路径,所以在方向 \((a,b)\)(\(a\ge b\ge0\),单位为格)上它量到的是:4 连通 \(a+b\),8 连通单位步长 \(a\),倒角 \(a+(\sqrt2-1)b\);真值是 \(\sqrt{a^2+b^2}\)。令 \(t=b/a\in[0,1]\),倒角与真值之比为 \(f(t)=\big(1+(\sqrt2-1)t\big)/\sqrt{1+t^2}\),在 \(t=\sqrt2-1\)(约 22.5°)处取最大值 \(\sqrt{4-2\sqrt2}\approx1.0824\)。于是 4 连通、8 连通(单位步长)和倒角三种野火的相对误差 \(e_4,e_8,e_{\rm ch}\) 的范围分别是:

    $$ e_4\in[0,\ +41.4\%],\qquad e_8\in[-29.3\%,\ 0],\qquad e_{\rm ch}\in[0,\ +8.24\%] $$

    精确算法(Felzenszwalb & Huttenlocher 2012)利用平方距离可分离:\(\hl{dt}{D}(x,y)^2=\min_{(x',y')\in\mathcal O}\big[(x-x')^2+(y-y')^2\big]\)。先对每一列算 \(g(x,y)=\min_{y'}(y-y')^2\)(只看这一列的障碍),再对每一行算

    $$ \hl{dt}{D}(x,y)^2=\min_{x'}\Big[(x-x')^2+g(x',y)\Big] $$

    每个 \(x'\) 贡献一条开口向上、顶点在 \((x',g(x',y))\) 的抛物线,答案是这些抛物线的下包络。抛物线两两只交一次,所以从左到右扫一遍、用一个栈维护“当前还在包络上的抛物线”,就能在 \(O(n)\) 内求出整行的下包络,整张图 \(O(N)\)。FIG 4 与本页所有其他距离场(FIG 1、3、5)都用这个算法,结果和逐点暴力搜索一致到浮点精度;算法同时记下每格最近的那个障碍格(“特征变换”),FIG 5 的骨架就是用它找出来的:相邻两格的最近障碍突然相隔很远,说明它们跨过了一道山脊。

    骨架、关键点与区域

    GVD 是自由空间中“最近障碍点不唯一”的点集。在 FIG 5 中,一个格子属于骨架的条件是:它的净空 \(D\ge0.3\) m,且某个 4 邻居的最近障碍格与它的相距超过两格;然后用 Zhang–Suen 细化把它削成一格宽。沿着骨架上一段净空小于 \(c_{\max}=0.7\) m 的窄段(它的两端都要接到宽阔处,否则只是伸进墙角的“毛刺”),净空序列的局部极小(要求两侧至少回升 4 cm)就是关键点 \(\hl{crit}{\mathbf c}\);过 \(\mathbf c\)、垂直于骨架方向一直延伸到两侧障碍的线段是关键线。这是对 Thrun(1998)的简化:他的关键点是 Voronoi 图上净空的局部极小,关键线是把关键点和它的两个基点连起来;在门口这样的直瓶颈里,两种画法几乎重合。\(c_{\max}\)、4 cm 的回升要求、“毛刺”规则和 40 格的区域下限都是本页为了让分割稳定而自己加的阈值。所以在这里,“是不是门”取决于 \(c_{\max}\):门宽超过 \(2c_{\max}=1.4\) m,它在算法眼里就只是房间里的一段宽敞地带,这就是 FIG 5 第一个任务的答案。

    改口与遗忘:钳位和衰减

    一个格子被观测为占据很多次之后,\(\ell\) 可能非常大;东西挪走以后,要清掉它需要 \(\ell/|\ell_{\rm free}|\) 次“穿过”。钳位把这个次数封顶:从 \(\ell_{\max}\) 降到判为占据的门槛 \(\ell_{\rm thr}\) 以下需要

    $$ n_{\rm clear}=\Big\lceil\frac{\ell_{\max}-\ell_{\rm thr}}{|\ell_{\rm free}|}\Big\rceil=\Big\lceil\frac{3.5-0.4}{0.405}\Big\rceil=8 $$

    钳位只对还能再看到的地方有效:要有射线重新穿过那一格。看不到的地方(传感器的盲区、被挡住的区域、量程以外)既不会被清掉,也不会被确认。衰减(每一步把 \(\ell\) 乘以 \(2^{-\Delta t/\tau}\),向“未知”回落)不依赖新的观测,所以能清掉看不到的鬼影,代价是同样会忘掉看不到的真墙:半衰期为 \(\tau\) 时,一个被钳在 \(\ell_{\max}\) 的格子在约 \(3.13\tau\) 后就不再被当作占据。FIG 6 把三种策略并排放在一起。

    $$ \ell(t)=\ell_0\,2^{-t/\hl{tau}{\tau}}\ \Rightarrow\ t_{\rm forget}=\hl{tau}{\tau}\log_2\frac{\ell_0}{\ell_{\rm thr}}=\hl{tau}{\tau}\log_2\frac{3.5}{0.4}\approx3.13\,\hl{tau}{\tau} $$

    6机器人中的它:会变的世界,与“谁来读地图”

    占据栅格假设世界是静止的,可真实世界里人会走过、椅子会被挪走。下图里机器人停在一个房间的左侧,开机时原地转一圈,把四周(包括身后的墙)都看了一遍,之后用 270° 激光雷达(身后 90° 是盲区)每 0.1 s 扫描一次。第 2 到 15 s 有人走进来、站了 5 s、再从右边的门出去;第 20 s 椅子被搬走。三张地图用同一串扫描并行更新:不钳位、钳位 \([-2,3.5]\)、钳位 + 衰减(半衰期 \(\tau\))。这里没有回波的射线按“空闲到最大量程”处理,否则从门口出去的射线永远清不掉门边的格子。

    FIG 6会变的世界:不钳位、钳位与衰减
    显示所选策略的地图(配色同 FIG 2)。琥珀色圆盘是机器人,两条虚线是视场边界,身后淡紫色扇形是盲区。虚线框是此刻真实存在的椅子和橱柜,琥珀色虚线圆是此刻的人。鬼影:地图判为占据(\(\ell\ge0.4\)),而此刻的真实环境里那一格及其 8 个邻居都是空的,用琥珀色方框标出。盲区墙:盲区里墙面格(共 46 格)仍被判为占据的比例。时间轴 60 s,“×4”快进。
    • 选“不钳位”,播放到 30 s 以后:20 s 时搬走的椅子仍在地图里(至少 5 个鬼影格)。此刻切到“钳位”:一个鬼影也没有。
    • 选“钳位 + 衰减”、\(\tau\le10\) s,播放到 40 s 以后:盲区墙只剩不到一半还被当作墙,而“钳位”那张图里它一直是 100%。
    真实系统里

    2D 激光建图:GMapping(Grisetti 等 2007,Rao-Blackwellized 粒子滤波)、Cartographer(Hess 等 2016,把扫描插入局部“子图”概率栅格,后端做回环)和 slam_toolbox(ROS 2 Nav2 的启动文件建图时调用的 SLAM,15.2 节)输出的都是占据栅格,存成一张图片加一个 YAML 文件(分辨率和原点),交给定位(AMCL 的粒子滤波,4.10 节)和导航使用。导航:ROS 的 costmap_2d 定义了 0(空闲)、253(内切)、254(致命)、255(未知)这几个特殊代价;膨胀层出厂值是 inflation_radius 0.55 m、cost_scaling_factor 10(正是 FIG 3 的默认值),实际部署常把衰减调小、半径调大,让机器人走在通道中间。全局用静态层 + 膨胀,局部用跟着机器人走的“滚动窗口”代价地图,障碍层按射线“标记”和“清除”;时空体素层(STVL,Nav2 常用的一个代价地图插件)则让标记过的体素在设定的衰减时间后过期,和 FIG 6 第三种策略里“看不到也会遗忘”是同一个思路(它按时间删除体素,而不是让对数几率衰减)。三维:OctoMap(八叉树占据),Voxblox 与 nvblox(TSDF → ESDF,给无人机和机械臂的轨迹优化用)。足式机器人:ANYmal 一类的系统用 2.5D 高程图(Fankhauser 等的 elevation_mapping 与 grid_map 库),每格存地面高度和它的方差,因为“能不能踩”比“有没有东西”更重要。定位地图:ORB-SLAM 的地图点与关键帧,自动驾驶的高精地图(车道线、路标)。语义:Kimera 与 Hydra 实时构建三维场景图,ConceptGraphs 等把视觉语言模型的特征挂到物体节点上,让语言指令能查询地图。

    地图跟着读者选

    把这一章串起来:估计器(15.1 的 VIO、15.2 的 SLAM 后端)要的是能反复认出来、能写进残差的东西,所以它维护路标或扫描子图;全局规划器(14.1 的 A*、14.2 的采样规划)要的是能不能过,所以读代价地图或拓扑图;轨迹优化与 MPC(14.3、13.4)要的是可微的距离,所以读 ESDF;任务规划(14.4、Part VII)要的是物体和地点,所以读语义地图。真实系统从同一份传感器数据和同一组位姿出发,同时维护几张地图,每张只为一个读者优化。

    7常见误区

    误区 1:把“未知”当成“空闲”

    没观测过的格子和观测过是空的格子,在一张只有 0/1 的地图里看起来一样,但意义完全不同。FIG 2 的深色沙发是最直接的例子:激光打上去没有回波,如果把无回波当成“空闲到最大量程”,沙发连同它后面的墙都会被标成空的,规划器会径直开过去。全局规划器一般有 allow_unknown 一类的开关:探索时允许穿过未知区,正常导航时应当保守。深色、透明(玻璃)、镜面和非常低矮的障碍都是 2D 激光的典型盲点,需要别的传感器(深度相机、超声、碰撞开关)补上。

    误区 2:用漂移的位姿建图,或回环后不重画

    FIG 2 里每米 0.05° 的航向漂移(走完一圈不到 3°)就足以让墙面覆盖掉到约 91%、多出约 200 个误占据格。占据栅格本身没有“修正位姿”的能力:已经加进去的证据不会因为后来位姿被优化而自动搬家。所以 Cartographer 一类的系统把扫描插入局部子图,回环后只移动子图的位姿;更简单的做法是在位姿图优化之后,用新位姿把所有扫描重新插一遍。

    误区 3:膨胀参数和机器人对不上

    \(r_{\rm ins}\) 小于真实轮廓,规划出来的路会擦墙;\(r_{\rm ins}\) 太大,门就“关上了”(FIG 3:半径达到门宽的一半 0.5 m,就无路可走)。膨胀半径小于外接半径时,转弯时车角可能扫到墙。\(k\) 太大,代价只是贴着墙的一条窄带,路径一路贴墙(默认值的平均净空约 0.8 m)。还要记得栅格距离按格心算,会比真实距离乐观约半格;分辨率越粗,这个乐观越大。

    误区 4:分辨率越细越好

    格子边长减半,内存和每次扫描要更新的格子数都翻几倍(2D 是 4 倍,3D 是 8 倍)。更隐蔽的问题是证据变稀:2° 一束的雷达在 9 m 外两束相隔约 31 cm,10 cm 的格子里有一大半永远没有射线经过,地图上出现条纹状的“未知”(FIG 6 房间右侧就能看到)。分辨率应当和传感器的角分辨率、量程以及机器人需要分辨的最小间隙一起选。

    误区 5:把会动的东西画进静态地图

    人、车、被推来推去的椅子不属于“地图”,属于“状态”。不钳位的对数几率会把站了 5 s 的人变成约 4 s 才清得掉的鬼影,把放了 20 s 的椅子变成到 60 s 时还清不掉的鬼影(FIG 6);钳位能让地图快速改口,但只对看得见的地方有效;衰减能清掉看不见的鬼影,但同样会忘掉看不见的墙。成熟的系统把动态物体检测出来单独跟踪(卡尔曼滤波,4.8 节),只把静态部分写进长期地图,局部代价地图则用短时的滚动窗口。

    8小练习

    1. 机器人航向有 1° 的误差,它把 6 m 外的一面墙画到地图上。墙大约偏了多少?
    横向偏移约为 \(r\,\delta\theta=6\times(\pi/180)\approx0.105\) m,也就是 10 cm 栅格的一整格。航向误差会被距离放大,所以远处的墙对航向最敏感,这正是 FIG 2 里漂移让墙“分层”的原因。
    2. 对数几率钳在 \([-2,3.5]\),\(\ell_{\rm free}=-0.405\),判为占据的门槛是 0.4。一个已被钳在 3.5 的格子,东西搬走后最少要几次“穿过”才不再被判为占据?
    需要 \(3.5-0.405n<0.4\),即 \(n>7.65\),所以 \(n=8\):8 次后 \(\ell=0.26\)。选项 D 是不钳位时的情况:那时清除时间与历史成正比,这正是钳位要解决的问题。
    3. 用 4 连通的野火(每步代价 1)计算距离变换。和真实的欧氏距离相比,它的最大相对误差是多少?
    4 连通量到的是曼哈顿距离 \(a+b\),在 45° 方向上是 \(2a\),而真值是 \(\sqrt2a\),比值 \(\sqrt2\),即高估 41.4%。Dijkstra 在图上是精确的,但这张图的度量不是欧氏的。选项 B 是 8 连通单位步长的结果,选项 C 是倒角 \((1,\sqrt2)\) 的结果。
    4. 一台只有相机的机器人要在一栋大楼里随时重新定位(被搬到任意位置后找回自己)。最合适的主地图是?
    重定位要回答“我看到的东西在地图里哪里”。特征点带描述子,可以把当前图像的特征和地图点匹配(11.5 节),再用 PnP 求位姿(11.4 节);ORB-SLAM 的重定位就是这样做的。代价地图和 ESDF 是为规划准备的,相机看不到“代价”;拓扑图太粗,只能告诉你在哪个房间。
    5. 一台圆形机器人半径 0.35 m,门宽 0.9 m。代价地图里 \(r_{\rm ins}=0.35\) m、\(r_{\rm inf}=1.0\) m。规划器会怎样?
    门里离两侧门框都超过 0.35 m 的位置是可行的:门中线两侧各 \(0.45-0.35=0.10\) m,共约 0.2 m 宽。这些位置离障碍不到 \(r_{\rm inf}\),所以有代价(膨胀区),但不是 253。膨胀半径只决定“贵不贵”,不决定“能不能过”,所以选项 A 错。

    9延伸阅读

    • H. Moravec & A. Elfes, High Resolution Maps from Wide Angle Sonar, ICRA 1985; A. Elfes, Using Occupancy Grids for Mobile Robot Perception and Navigation, IEEE Computer, 1989 — 占据栅格的起点
    • S. Thrun, W. Burgard & D. Fox, Probabilistic Robotics, MIT Press, 2005, Ch. 9 — 占据栅格建图、反向传感器模型与对数几率
    • S. Thrun, Learning Occupancy Grid Maps with Forward Sensor Models, Autonomous Robots, 2003 — 去掉格子独立假设的代价与收益
    • D. V. Lu, D. Hershberger & W. D. Smart, Layered Costmaps for Context-Sensitive Navigation, IROS 2014 — 分层代价地图,ROS 导航栈的设计
    • P. F. Felzenszwalb & D. P. Huttenlocher, Distance Transforms of Sampled Functions, Theory of Computing, 2012 — 线性时间的精确欧氏距离变换
    • G. Borgefors, Distance Transformations in Digital Images, Computer Vision, Graphics, and Image Processing, 1986 — 倒角距离变换及其误差
    • B. Lau, C. Sprunk & W. Burgard, Efficient Grid-Based Spatial Representations for Robot Navigation in Dynamic Environments, Robotics and Autonomous Systems, 2013 — 增量更新的距离场与 Voronoi 图
    • S. Thrun, Learning Metric-Topological Maps for Indoor Mobile Robot Navigation, Artificial Intelligence, 1998 — Voronoi 骨架上的关键点、关键线与区域分割(FIG 5 是它的简化版)
    • B. Kuipers, The Spatial Semantic Hierarchy, Artificial Intelligence, 2000 — 从控制、因果、拓扑到度量的分层空间知识
    • W. Hess, D. Kohler, H. Rapp & D. Andor, Real-Time Loop Closure in 2D LIDAR SLAM, ICRA 2016 — Cartographer:子图概率栅格与回环
    • A. Hornung, K. M. Wurm, M. Bennewitz, C. Stachniss & W. Burgard, OctoMap, Autonomous Robots, 2013; H. Oleynikova et al., Voxblox, IROS 2017 — 三维占据与 TSDF/ESDF(12.2 节已介绍)
    • A. Rosinol, A. Gupta, M. Abate, J. Shi & L. Carlone, 3D Dynamic Scene Graphs: Actionable Spatial Perception with Places, Objects, and Humans, RSS 2020 — 从度量到语义的分层地图