教程 · 中级 · 35 分钟

机器人激光雷达:一次扫描给你什么,又如何变成地图

旋转式激光雷达是如何拼出一帧扫描的,为什么转速越快角分辨率就越低,以及射线投射如何把一串距离读数变成占据栅格地图。

引言

一台测距传感器只告诉你沿一条线某个东西有多远。地图则告诉你一个房间的形状。导航里所有有趣的东西都住在这两者之间的空隙里,而这一页讲的就是那个空隙。

这两个端点在本站已经讲过:一侧是单点测距,另一侧是 SLAM和占据栅格上的 A*。但那些都假设栅格已经存在。没有任何地方说它是从哪儿来的,而答案——把一次扫描射线投射进格子——既简单又处处是坑。

二维激光雷达就是一台装在转台上的飞行时间传感器

这是最有用的一点认知,因为它意味着测距那一半你已经懂了。

业余级二维激光雷达里面是一颗光学飞行时间测距模块,和 VL53 系列转接板上的完全是同一类,横向指着然后被旋转起来。一个角度编码器报告每次测量时它朝哪儿看。电源和数据通过滑环或者光电/电感耦合跨过那道旋转边界。

所以一次“扫描”不是一张照片。它是一个 (角度, 距离) 对的列表,一次采一个,采满一整圈。下面每一条限制都从那句话里来。

这也告诉你为什么超声波传感器不被旋成激光雷达。超声波束有几十度宽,旋它几乎得不到任何好处——你只是在把一道本来就宽的波束抹着涂满整个房间。而正是窄光束让角分辨率一开始就有意义。用舵机扫一颗飞行时间传感器,就像扫描超声教程里做的那样,真的就是一台慢速低分辨率的激光雷达。

采样预算是固定的,所以分辨率和新鲜度要取舍

这是最让人意外的部分,而且值得在买任何东西之前就弄明白。

测距模块只有一个吞吐量数字:它每秒能采多少次测量。在 RPLIDAR A1 上大约是每秒 8000 个采样,而且你改转速它也不会变。转速是你自己选的,通常 1–10 Hz。

于是你每圈得到的点数,就只是预算除以转速:

points per revolution = samples per second ÷ spin rate
angular step          = 360° ÷ points per revolution
转速 每转点数 角步 扫描周期
5.5 Hz 1455 0.25° 182 ms
10 Hz 800 0.45° 100 ms

**你不可能同时拥有新鲜的扫描和精细的角分辨率。**转到 10 Hz 把你的延迟减半,角分辨率也几乎减半。这一个取舍对机器人行为的决定作用,超过宣传资料上那个量程数字;而且它是第一件该被有意识地设定、而不是丢给驱动默认值的东西。

在开阔走廊里快速移动的机器人:转快一点,你需要新鲜度,而墙又很大。在杂乱房间里慢慢建图的机器人:转慢一点,你需要细节,而且付得起那份延迟。

波束会发散,所以远处的障碍会掉在波束之间

角步是一个角度,不是一个距离。两条相邻的波束在传播过程中会分开:

gap between beams ≈ range × angular step (in radians)

在 0.45° 下,这意味着一米处有 7.9 mm 的间隙,四米处有 31 mm。中间的空间没有任何东西采样。

一张图:相邻激光波束之间的间隙对目标距离的变化,分别对应 5.5 赫兹(每转 1455 点、0.25 度)和 10 赫兹(每转 800 点、0.45 度)。两条线都从零线性上升,10 赫兹那条的斜率是两倍。一条水平虚线标出 20 毫米的椅腿,标记显示 10 赫兹的波束间距在 2.5 米以外超过这个宽度,5.5 赫兹则在 4.6 米以外。
采样间隙随距离线性增长,在 10 赫兹时比 5.5 赫兹快两倍。每条线与椅腿宽度相交的地方,就是那条腿不再能被检出的地方。 下载 SVG

拿一条 20 mm 的椅腿来说。以 10 Hz 旋转时,波束间隙在约 2.5 m 处超过 20 mm——再往外,那条腿完全可以整个坐在两条波束之间,什么都不返回。5.5 Hz 时你要到约 4.6 m 才会遇到同样的事。

同一条 20 毫米的椅腿在两个距离上与两条相邻波束对照画出。两米处波束相距 16 毫米,腿比间隙宽,于是有波束落在它上面并被检出。五米处波束相距 39 毫米,腿比间隙窄,于是它坐在波束之间,什么都不返回。
同一个物体、同一台传感器,画了两次。腿本身一个字都没变——变的只是有没有一条波束恰好指着它。 下载 SVG

这就解释了一种否则看上去像传感器坏了的行为:细的障碍随着机器人靠近才出现,在一两米外突然跳进地图。激光雷达并没有失灵。那条腿从来没有被采样过。

有两个值得围绕它做设计的后果:

  • **不要相信单次扫描去找细障碍。**像下面那样把多次扫描累积进栅格,才是真的能把桌腿找出来——每次扫描采的角都略有不同。
  • 椅子腿和桌腿是家用机器人的经典失败项,因为它们细、正好在激光雷达的高度上,而机器人很晚才知道它们。

扫描不是快照

一圈里的点不是同一瞬间测出来的。在 5.5 Hz 下扫一圈要 182 ms,而机器人不会礼貌地等着。

  • 以 0.5 m/s 行驶时,机器人在一圈里走了 91 mm。
  • 以 1 rad/s 转向时,这次扫描被抹开了 10.4°。

把那个点的列表当成在某一个位姿上拍的一张刚性快照,会把世界掰弯。直墙会变成弯曲或重影,而扫描匹配会与自己动作造成的修正打架。

这就是 sensor_msgs/LaserScan 为什么在测距旁边还带 scan_time 和 time_increment:它们让使用者能算出每个波束是何时采的。修正这一点——按该波束时间戳上机器人所处的位姿去变换每一个点——叫做去畸变,也叫运动补偿,而好的 SLAM 前端会替你做。

这在实际中意味着:如果你的地图静止时干净、一动就糊,那么在怀疑传感器之前,先长时间怀疑去畸变和时间戳。**建图时慢下来。**速度减半,畸变就减半,白拿的。

你只能拿到一个水平切片

二维激光雷达量的是一个平面。这个平面之上或之下的东西都不存在。

经典的三类:

  • **悬空物。**一张 700 mm 高的桌面,对一台 150 mm 高的激光雷达是隐形的。机器人自信地开到底下,然后卡住。
  • **低矮障碍。**门槛条、电缆、一只袜子。全都在平面之下。
  • **坠落。**一段向下的楼梯在激光雷达能看的每个方向上都是空的。它读起来像漂亮干净的地面。

再多的扫描处理也修不了这个,因为信息从来没有被采集到。真实的机器人会在激光雷达之外保留保险杠、悬崖传感器,通常还有一台朝下或前倾的测距仪——不是因为激光雷达不好,而是因为平面就是平面。当你读到某台吸尘器“有激光雷达却还有保险杠”时,原因就在这里。

从扫描到栅格:射线投射

现在到建图这一半。占据栅格把世界划分成方格,并为每一格保存“我有多确信它被占据”。

让它能工作的那个洞见是:**一次测距读数告诉你的是整条射线上的每一格,而不只是末端那一格。**如果一道波束走了 3 米才碰到东西,那么它穿过的所有空间必然都是空的——否则它早就停了。

于是每一道波束产出三个判定:

方格栅格,传感器在左,一道射线画向右上方的一格。射线穿过的格子阴影标为空闲,测距读数落下的那一格阴影标为占据,同方位上越过那一击继续延伸的格子阴影标为未知。图例说明:穿过的格子变为空闲、其对数几率下降,命中格变为占据、其对数几率上升,命中之后的格子保持未知、没有被标为空闲。
一道波束更新一整行格子。命中之后的格子处在阴影里——波束从没到过那里,所以它们保持未知,而未知和空闲是两件不同的事。 下载 SVG
  1. **射线穿过的格子 → 空闲的证据。**沿着从传感器到命中点的这条线走一遍,把每一格往空的方向轻推一点。
  2. **读数落下的那一格 → 占据的证据。**把它往满的方向推。
  3. **命中之后的格子 → 什么都没有。**波束停了。你对那面墙后面是什么一无所知,所以必须不去动那些格子。

走那条线就是线段光栅化——Bresenham 算法,和在屏幕上画一条直线用的是同一个。

达到最大量程的读数需要单独处理。很多驱动在什么都没返回时会报最大值或者无穷大。那不是“12 m 处有一面墙”,而是“什么都没找到”。把射线标为空闲,而末端不标任何占据格。

对数几率:为什么用加法而不是乘法

你当然可以给每格存一个概率,把贝叶斯更新乘起来。实践中所有人都改存对数几率:

l = ln( p / (1 - p) )          p = 1 / (1 + e^(-l) )

有两个理由。把许多小概率乘起来会下溢,而把对数加起来不会。而且它让更新变成一次加法,便宜到可以对每一道波束的每一格、每一次扫描都做一遍。

取 p = 0.5(毫无头绪)对应 l = 0,再为命中和未命中各选一个值——比如 p_occ = 0.7(l = +0.847)和 p_free = 0.4(l = −0.405)——更新就是:

l_cell += l_occ     # this beam hit this cell
l_cell -= 0.405     # this beam passed through this cell

一格从 l = 0 出发,经过三次命中:

观测 对数几率 概率
起始(未知) 0.000 0.500
命中 +0.847 0.700
命中 +1.695 0.845
命中 +2.542 0.927
穿过 +2.136 0.894
穿过 +1.731 0.850

证据会累积,分歧会把它拉回来。整个算法就是这些。

**把总量钳住。**这件事比看上去重要。连续二十次命中不做钳位,会把一格推到 l = 16.9,也就是 p = 0.999999。现在有人把椅子挪走了。要撤销那份笃定,需要 42 次空闲观测。椅子就留下一个永久的幻影,而你的规划器会一直绕着那个不存在的东西转向。

把 l 钳到大约 ±3.5 就能解决:强信念依然是强的,但只要几次矛盾的扫描就能推翻它。如果你的地图上累积起已经消失的物体的鬼影,几乎总是这个原因。

未知是第三种状态,不是空闲的同义词

这是产出自信而危险的规划的根源。

刚初始化好的栅格完全是未知的。如果你的规划器把未知当作可通行,它会高高兴兴地直接穿过你从未看过的那些世界部分——而正因为它们没被探索过,那里恰恰就是意外住的地方。如果它把未知当作占据,机器人起步就会拒绝移动,因为一切都是未知。

两种都是错的;把三种状态分清楚,然后有意识地决定:

  • 占据——不许规划穿过。
  • 空闲——规划穿过。
  • 未知——只有付出代价时才规划穿过,或者只在有意探索时穿过。

前沿探索,也就是机器人决定下一步往哪看,完全建立在空闲/未知的边界上。把两者混掉,你就把它扔了。

选格子尺寸

常规答案是室内机器人 5 cm,而“能更细就细”并不是好理由,因为它有代价。

比典型距离上的波束间距还小的格子,是在凭空发明你并不拥有的精度——在 4 m 处波束相距 31 mm,所以 1 cm 的格子会在射线之间留下永远填不上的永久空洞。而比机器人间隙余量大得多的格子会让门口看起来过不去,因为一个占据格就能把整条缝吞掉。

实用规则:格子尺寸要比你必须钻过去的最窄缝隙略小一点,而且在你实际作业的距离上不要小于波束间距。然后在规划时按机器人半径膨胀障碍,而不是靠缩小格子来补偿。

出问题时

现象 通常原因
细障碍只在靠近时才出现 那个距离上波束间距超过了它的宽度——预期行为,不是故障
行驶时墙弯曲或重影 扫描没做去畸变;点被当成同一瞬间
静止时地图干净,一动就糊 同样原因;也试试更慢地建图
机器人钻到桌子底下卡住 二维平面看不见悬空物;需要第二台传感器
楼梯被读成空旷地面 坠落对水平平面不可见;需要悬崖传感器
已经搬走的障碍还留在地图上 对数几率没做钳位,于是笃定撤销不掉
整张地图边缘填满障碍 最大量程的“无回波”读数被标成了命中
规划器穿过未探索空间 未知和空闲混为一谈
细栅格里全是麻点状空洞 格子小于波束间距;射线之间什么都采不到
玻璃墙整个漏掉 光直接穿过去——这是超声波唯一赢的地方

最后一行是整页最诚实的补充。激光雷达在分辨率、速率和量程上都赢过声呐,然后在一扇玻璃门上输得一塌糊涂,因为声音会被反射而光不会。这正是不要只选一种传感器就宣称问题已解决的最清楚论据。

探索关联图谱

本教程所属的搭建

包含本教程的项目和学习路线。

延伸阅读

参考资料