做激光SLAM的人,迟早会撞上Scan Context这个名字。最近在做回环检测方案调研,把这篇文章翻出来从理论到代码重新过了一遍,发现以前只记住了“极坐标鸟瞰图”几个字,很多细节其实没吃透。这篇论文学习的核心很简单:给定一帧三维激光点云,如何判断机器人之前是否来过这个地方。听起来像是一个二分问题,但真正落地时牵涉到降维、旋转不变性、快速检索、几何验证一整条链路。不管你是准备复现论文、想给自己的SLAM系统加回环模块,还是单纯想搞懂位置识别为什么难,这篇内容应该都能给你一些可落地的思路。
1. 为什么回环检测成了SLAM的“老大难”
1.1 位置识别的本质
回环检测,或者说位置识别,本质上是让机器人回答一个问题:“我现在看到的这堆点云,以前见过吗?”在人类视角里,认出一个路口靠的是路牌、店铺、树木轮廓这类显式线索,算法没有这种语义抽象,只能拿原始几何信息硬碰硬。
激光雷达每帧输出的点云动辄几万到几十万个点,如果直接拿这些点去和地图里每一帧做配准,计算量完全不可控。更麻烦的是,机器人到达同一个物理位置时,视角往往不是完全一致的,可能偏了几十厘米,转了几度角,甚至隔着一段时间,停着的车和路边堆的货物都不一样了。位置识别要容忍这些扰动,还要从庞大的历史数据里快速找到“最像”的那一帧。
所以整个问题就被拆成了两个层次:先压缩出紧凑的全局描述子,再用这个描述子做快速检索和相似度判断。Scan Context就是走这条路线的代表性方案,它把一帧点云压缩成一个二维矩阵,同时保留了足够多的空间结构信息。
1.2 点云配准为什么不够用
有人可能会想:我用ICP或者NDT,把当前帧和历史帧对齐,如果收敛且得分高,不就说明到过同一个地方吗?理论上是,实际不可行。假设机器人跑了五公里,每秒十帧,历史帧数就是几万帧。对每一帧都做一次配准,哪怕用体素滤波降采样,也要花掉秒级时间,哪有SLAM系统敢让前端堵这么久。
配准还有一个深层问题:初值。ICP这类点到点迭代方法对初始位姿非常敏感,两帧如果相隔几十米,初始变换猜错后基本收敛不到全局最优。位置识别的工作就是先把“大概在同一片区域”的候选找出来,给配准一个足够近的初值,然后再用精确配准去做验证。Scan Context解决的是前一半问题:快速圈定候选,顺带给出相似度分数。
另一个思路是直接训练一个神经网络提取点云全局特征,这在很多场景下表现也不错,但缺点是需要数据标注和训练成本,而且对环境变化和传感器型号比较敏感。Scan Context的优势在于它不学习任何参数,是个纯几何的确定性描述子,换个雷达、换个场地,拿来就能用。这一点在工程上极其宝贵。
2. 核心设计思路:一张极坐标鸟瞰图解决朝向问题
2.1 为什么要切成扇形和环形
Scan Context对点云的压缩方式非常直观:把三维点云拍扁到水平面上,以传感器为圆心,在径向方向和方位角方向分别做等间隔划分,于是整个平面被切成一个个扇环格。每个格子就是一个bin,bin的编号能由它所在的行和列唯一确定。整个点云最终变成一个 N_s 行 N_r 列的矩阵,这里 N_s 是方位角划分数,N_r 是径向划分数。
关键在于“列对应方位角,行对应径向距离”这个结构。想象你站在同一个路口,第一次面向北拍一张周围环境的照片,第二次面向东再拍一张。如果直接用笛卡尔坐标网格去描述这两个场景,两张“图”很难对得上,因为空间里的物体会随着视角变化跑到完全不同的网格里。但用极坐标切分成扇环后,旋转视角等价于矩阵的列发生循环平移。面向北时某个方向的建筑在第 10 列,面向东后同样的建筑跑到第 21 列,可整体扇形结构没有变。
这种几何上的对称性被直接用在了匹配阶段:查询帧和历史帧之间如果存在偏航角差异,只需要把描述子矩阵做若干次列循环移位,找到让相似度最高的那次位移,就能同时恢复出位姿初值和匹配分数。这个设计是Scan Context最聪明的地方,一句话概括就是:用极坐标把旋转问题变成平移问题。
2.2 每个bin填什么:高度比强度好用
设计完网格后,还需要决定每个bin里放什么数值。Scan Context给出的答案很朴实:取这个扇环格里面点云的最大高度。如果该bin里没有任何点,就记为0。这个选择有它的道理。
最大高度本质上刻画的是局部遮挡物的轮廓。比如一个bin里恰好有一栋建筑的墙面,那么最大高度就能反映墙的顶部有多高;如果是一片空地,就会得到0。把整个矩阵看成“伪图像”,高度值大的地方形成了明显的纹理,这些纹理反映的是环境中垂直结构的分布情况,比如路沿、墙面、树干、围栏。相比直接取平均高度,最大值的抗噪性更好,不受地面点稀释的影响;相比强度信息,高度不依赖激光反射率标定,换雷达后表现更稳定。
在实际处理中,使用最大高度前通常要做几件事。第一,过滤掉明显离群的噪点;第二,限制参与计算的距离范围,太远的点稀疏且噪声大,只会把矩阵后半部分弄花;第三,根据雷达安装高度,把z值限制在一个合理区间。这些细节论文里没有写死,但复现过的人都知道,bin里的数值对最终精度影响非常大。
2.3 和笛卡尔网格的区别
单从可视化角度,笛卡尔网格也能形成一张鸟瞰图,为什么偏偏用极坐标?直接原因是回环检测太怕旋转。机器人在相同位置的不同朝向,笛卡尔网格下的矩阵内容会彻底变化。比如一个边长一米的正方形网格,原地转90度,原本在某个网格里的墙可能被劈到好几个网格里,矩阵相似度急剧下降。
极坐标网格在旋转时会在列方向上发生整体平移,这就给后续处理留了操作空间。不过它也有代价:径向分辨率不均匀。靠雷达中心近的地方,扇环面积小,bin覆盖得更密集,点云细节丰富;远的地方扇环面积大,几个点可能落进同一个bin,丢失很多结构信息。设计时需要在近处细节和远处覆盖之间做一个平衡,这也是 N_r 的选择不能一味增大的原因。
另外,极坐标矩阵天然存在一个对称性:同一个环境,如果只是把雷达装高了或降低了,高度值整体会变,但由于bin存的是最大高度而不是绝对高度差,所以这类变化在很多时候还能容忍。当然,如果环境出现大面积重建,导致之前没有墙的地方突然立起高楼,任何全局描述子都会失效,这属于位置识别的天花板问题。
3. 匹配流程:从粗到精的两阶段检索
3.1 Ring Key:用一维向量快速圈定候选
拿到一帧点云并生成 Scan Context 矩阵后,下一步是在历史数据库里找到最相似的帧。如果直接拿 N_s × N_r 的矩阵做暴力搜索,每帧都是几千维向量,在几万帧数据里做最近邻搜索其实是可行的,但依然偏慢,尤其嵌入式平台上压力不小。
Scan Context 的聪明之处是先撸一个更短的特征出来,论文里叫Ring Key。做法非常简单:对矩阵的每一行求均值,于是 N_r 行就变成了一个 N_r 维向量。为什么这个向量对旋转不敏感?因为旋转在矩阵上表现为列循环平移,而行均值是在所有列上求平均,平移列不会改变每行的均值。相当于丢掉了方位信息,只保留“直径r处平均高度有多高”的径向分布描述。
Ring Key本身区分度不高,但用来做粗筛非常合适。可以把它建到 KD-tree 里,查询时先对 Ring Key 做近邻搜索,取回前 K 个候选,再用完整 Scan Context 在候选集里做精匹配。这个思路和图像领域先做BoW粗筛再做几何校验是一样的,用低维特征换速度,再用高维特征换精度。
3.2 Scan Context距离:按列比较的内在含义
精匹配阶段,需要计算当前描述子矩阵与历史帧矩阵之间的相似度。Scan Context 的距离公式并不是直接把两个矩阵展开成向量算欧式距离,而是按列做归一化点积之后再求平均。
具体来说,对每一列向量,计算两个向量之间的余弦相似度,然后把所有列的相似度取平均,再转换成距离值。列向量代表的是某个方位角上所有径向距离的高度纹理,所以按列比较相当于是把“某个方向周围有什么”作为基本比较单元。这比直接对整张矩阵做L2距离要稳定:矩阵里如果大部分bin是0,直接算L2距离会因为稀疏度而变得区分度很低,但余弦相似度能更好地抓到非零区域的形状差异。
还有一个很关键的细节:不同朝向。如果历史帧和当前帧的偏航角不一致,矩阵中的列会整体错位。Scan Context里的精匹配会尝试若干种列循环移位,把当前矩阵的历史对应列错开一格格进行比较,找出能让距离最小的移位量。这个移位量本身就能当两个关键帧之间的相对偏航角初值使用,对后续做ICP验证非常有益。
3.3 几何验证:别让相似度说了算
描述子匹配给出的只是一个“看起来像”的结果,并不保证几何上真的能对齐。真实场景里,相似度高的两帧可能来自两个布局相近但物理上不相通的空间,比如两条平行道路中间隔着一排楼,局部点云特征可能非常像。所以Scan Context在实际系统里很少作为最终判决,它通常负责给你一个候选帧和相对位姿初值。
拿到候选后,把两帧原始点云降采样,用ICP或NDT做一次精确配准。如果配准收敛且分数足够高,才判定这是真的回环;如果配准得分很差,哪怕描述子距离很小,也要果断丢掉这个候选。这个“描述子初筛 + 几何验证”的组合几乎是回环检测的标准流程,Scan Context在这条链路里扮演的是快速缩小搜索范围的角色,不是最终裁判。
我第一次在项目里集成时偷懒跳过几何验证,结果系统在一条环形走廊里疯狂报假回环,因为走廊各个位置的高度纹理太相似了。后来老老实实加了NDT验证,误检立刻降了下来。所以这条经验值得写进所有相关代码的注释里:相似度可以骗人,几何对齐骗不了人。
4. 实验结论:为什么它能打
4.1 数据集与评估指标怎么理解
论文里的实验基本遵循位置识别领域的通用做法:在多个真实场景数据集上,把历史帧建成库,用当前帧去检索,统计查准率和查全率。和普通分类任务不同,这里最看重的指标是“在100%精确率时能召回多少”。因为SLAM系统里,一个假回环会把整个地图拓扑搞坏,后续优化会严重变形,所以宁可漏检也不愿误检。Scan Context在很多场景下能在100%精确率附近保持较高召回,这是它能成为经典的核心原因。
另一个值得关注的是时间消耗。位置识别模块如果每帧查询要几十毫秒甚至上百毫秒,前端里程计早就漂到天上去了。论文里展示了粗检索加精匹配的两阶段方法能把单次查询压到毫秒级,同时在十万帧量级的库里也能保持线性可接受的搜索时间,这正是工程落地特别看重的指标。
4.2 与主流方案的对比结果
作者把Scan Context和几个当时主流的全局描述子做了对比,包括直接用点云集合做统计特征的方案,以及基于学习的特征提取方法。在大多数普通场景下,Scan Context在精度和召回上都有明显优势,尤其是在结构纹理丰富的城区环境里,它那套高度纹理矩阵能抓到很多区分性线索。
基于学习的方案在特定数据集上可能表现更强,但泛化到新场景时往往出现精度骤降。Scan Context则完全没有这个问题,因为它没有任何需要训练的参数,规则是预先定死的,环境怎么变它怎么描述。这种零训练成本的特性非常香,尤其对做产品原型或者学术调研的人来说,拿到算法当天就能出结果。
深度相机或者单线雷达也能用Scan Context吗?论文和后续工作里有相关讨论。单线雷达只有一圈线,生成的点云密度非常低,很多bin会变成0,描述子的区分度会下降,但依然能跑,只是检索效果不如多线雷达。使用多线雷达时,高度纹理的丰富度更足,近处细节也更多,bin里的最大高度能反映更强的空间特征。
4.3 参数敏感性:Ns和Nr如何取舍
Scan Context 有两个核心参数:方位角划分数 N_s 和径向划分数 N_r。N_s 直接决定角度分辨率,N_s=60意味着每秒多约6度一个列。如果 N_s 太小,旋转后的列移位粒度太粗,描述子区分度不够;如果太大,单个bin内的点更少,描述子对噪声更敏感,而且矩阵维度变大,存储和检索开销随之上升。
N_r 则决定了径向分辨率,同时影响可感知的距离范围。如果雷达有效距离是100米,N_r=20意味着每个环宽度5米,再远的细节全被平均掉。加大 N_r 能保留更多径向结构,但也会让每个bin更稀疏,近处可能还好,远处本来就是散点,反而容易引入噪声。论文里的默认设置通常是N_s=60、N_r=20附近的组合,但实际工程中要根据雷达量程、安装高度、环境尺度去标定。
我做过一个实验:把 N_r 从20加到40,在近距离场景里召回提升并不明显,但在空旷停车场反而下降了。原因是远处大片的0值bin参与相似度计算时,拉低了有效信号的比例。所以参数不是越大越好,关键要看有效测量范围能覆盖多少信息。
5. 复现与落地过程中的经验
5.1 点云预处理比想象中重要
很多人自己动手实现Scan Context,第一版跑在仿真环境里效果不错,换到真机上就开始抽风。主要问题通常出在输入点云质量上。
首先要把雷达坐标系下的点云根据位姿变换到世界坐标系或者以车体为中心的坐标系里。Scan Context的原始定义把点云投影到水平面,所以需要确保Z轴方向不与地面倾斜过大;如果雷达安装有俯仰角,必须做一次外参补偿,否则矩形分布会变形。
其次是滤波。离群点和下面这些“主力噪声”会给bin高度值造成很大干扰。比如行人、车辆、飘落的树叶,会在某个bin里堆出一个瞬时高度尖峰,导致两帧描述子在该列上出现无厘头的差异。比较稳妥的做法是先做一次半径离群点去除,然后限制距离范围,对z做截断。地面点的存在会让很多bin变成非零,因为这些点在同一个水平面上,直接让最大高度失效,所以地面滤波最好也做一下,效果会肉眼可见地变好。
5.2 阈值和候选数量怎么调
回环检测里有一个灵魂参数:相似度距离阈值。定得太松,误检飙升;定得太严,召回下降。论文里的思路是计算查询帧和历史帧的Scan Context距离后,用一个固定的距离阈值来判定是否回环。但真实世界没有万能阈值,需要做离线统计。
一个实用方法是把一批已知真值的数据跑一遍,统计正确匹配和错误匹配的距离分布,然后在两个分布重叠最少的地方定阈值。另外一个技巧是看匹配分数的相对关系,不只看绝对距离。比如某次查询得到的历史帧距离都是0.3附近,只有一帧是0.15,那这帧大概率就是真的回环;如果一堆候选都集中在0.2,说明当前环境平庸,置信度应该降一档。
候选数量同样需要调节。取太少可能把正确回环挡在粗筛外,取太多会让精匹配和几何验证变慢。建议先从top50开始观察,统计正确匹配在候选列表里的排名分布,如果正确匹配经常出现在第5名开外,就需要扩大候选数量;如果永远在前几名,可以适当缩小。
5.3 动态环境下的失效模式
Scan Context对静态结构敏感,但这些静态结构如果不更新,就会变成历史误导信息。停车场里下午停满了车,晚上空荡荡,两次经过同一个车位但点云差异巨大,描述子自然对不上。这是所有几何描述子共同的困境。
动态物体对Scan Context的破坏主要集中在近处的bin。车辆密集时,周围几米的bin里高度值会被车身高度主导,原本应该记录的路沿和建筑全被挡掉。处理方法有两种:一是做动态物体滤除,把来自高反射率车辆点云的bin剔掉;二是只保留远处结构,近处的bin权重降低。第二种方法在密集城区效果不算差,因为远处建筑轮廓更稳定。
另一个经典坑是植被。树冠点云非常不稳定,风一吹整个bin的高度都可能变。如果你做树的场景,建议对描述子做轻微平滑,或者在生成矩阵时忽略高度特别高的植被区域,否则每次风吹草动都会产生新描述子,匹配时全是噪声。
5.4 工程集成的小技巧
Scan Context 本身只负责出描述子和相似度,真正放进SLAM里还需要考虑几个工程问题。
第一个是存储。一帧N_s×N_r的矩阵即使以float保存,占的内存也不大,但长时间运行后历史帧数量可能爆炸。建议只对关键帧保存描述子,不要每帧都建库,不然检索速度会随时间退化。可以参考视觉SLAM里关键帧选取的方案:和上一关键帧距离、角度超过阈值时才插入新描述子。
第二个是增量更新。回环检测库里如果一直保留旧帧,环境变化后旧帧仍然参与匹配,容易把不同时期的地形搞混。一个可行的办法是定期清理与最新轨迹重叠度过高的旧帧,或者给每帧加一个时间权重,让太老的数据逐渐降低置信度。
第三个是位置识别结果如何接进后端优化。Scan Context匹配成功后会得到两帧之间的相对偏航初值,但完整位姿还是要靠ICP求得。把ICP得到的相对位姿作为回环边加入位姿图,再触发一次全局优化,地图的累计漂移才能被真正拽回来。
6. 常见问题排查实录
6.1 为什么相似度矩阵全是零
排查时先看原始点云的真实范围。如果雷达量程只有二三十米,但你的N_r覆盖到了100米,后半部分bin只能是空的,矩阵里大片0值,参数却以为在描述100米范围,匹配自然非常不稳。把N_r缩小到有效量程附近,或者把bin宽度加大,问题会立刻缓解。
另一个原因可能是地面滤波没有滤干净。地面点覆盖所有近处bin后,最大高度基本都趋近于雷达安装高度,所有方向的描述子都长得差不多,区分度被彻底吃掉。跑一个简单的平面分割,把地面点去掉,再看矩阵纹理,能明显感觉到结构变得清晰。
6.2 匹配结果频繁跳动
如果相同场景连续几帧匹配结果不一致,问题通常出在点云噪声上。某个bin里偶尔多了一个高层楼顶点,可能让描述子突变。这时候可以增加bin内点数量统计,只有点数超过阈值才更新最大高度,能大幅减少闪烁。
另外,如果近处几乎没有点,矩阵全0,匹配得分会虚高。可以引入一个有效bin比例统计,低于某个比例时直接拒绝匹配,避免在两个空旷区域之间产生假回环。
6.3 位姿初值偏差大
从Scan Context匹配得到的列移位量换算成偏航角时,单位是360/N_s度。如果N_s=60,最小角度步长就是6度,说明粗匹配给出的偏航角精度最多到6度。这个初值对NDT足够用,但如果后续ICP配准前又给了个离谱的平移初值,也可能收敛失败。
建议在使用Scan Context匹配结果做位姿初值时,不要直接用列移位对应的角度作为最终旋转,而是把它作为旋转初值,同时把平移设成0,交给ICP去优化。另外,给当前帧做一次体素滤波,控制点数在几千到上万个点,配准速度会明显提升,精度也不会差。
7. 关于后续扩展的思考
Scan Context还有不少可玩的空间。比如和IMU预积分结合,在候选帧粗筛时先利用里程计位置过滤掉物理上不可能到达的帧,能进一步降低误检;也可以把Scan Context的分辨率做成多尺度,先从低分辨率粗匹配找到区域,再在高分辨率矩阵里精算相对位姿,这样检索速度和精度都能兼顾。
近两年也有不少工作尝试把Scan Context和深度学习结合,用网络去学习相似度的距离函数,或者用它作为点云特征编码器的一部分。不过就工程可靠性而言,原始确定性方案仍然是最稳的基线。我自己的习惯是:新项目里需要快速可解释的回环检测模块,先上Scan Context,跑通全流程后再根据精度瓶颈决定要不要换更复杂的方案。
如果你正在复现Scan Context,建议动手前先画一张点云到矩阵的映射流程图,把坐标系、bin归属、高度取值这三步想清楚,再写代码会很顺。写完核心函数后,用两段人为平移旋转过的同一场景点云做单测,确认相似度接近1且列移位量正确,就说明实现基本没跑偏。这套验证方法比直接上数据集跑评测效率高得多,能节省大半天排查时间。
最后分享一个我自己踩过的坑:第一次做Scan Context时,为了多保留信息,把强度也叠进了bin值,结果在黄昏和夜晚两个时间段跑,同一地点的描述子差异巨大,回环全丢。后来老老实实只用高度,配合滤波,效果立刻回到正常水平。描述子设计的本质是做减法,把最鲁棒的空间结构信息留下来,其他看似有用的信息往往只是引入了不该有的变化。这个道理放之四海而皆准。