拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Scan Context激光回环检测全解:原理、工程接入与调参实战

Scan Context激光回环检测全解:原理、工程接入与调参实战

做SLAM的人基本都有过这种经历:里程计一路推过去,误差一点点累积,地图从清晰慢慢变得模糊,直到某个瞬间机器人回到曾经经过的地方,回环检测模块把这条边接上,整个地图像被无形的手拽了一下,一下子恢复成全局一致的整体。这个“拽一下”的过程,就是回环检测在起作用。

Scan Context是我在实际工程里用得最多的激光回环方案。它简单、直接、效果好,被LIO-SAM等一批开源项目选作默认回环模块。它不是深度学习那套黑箱,而是把一帧三维点云编码成一张二维矩阵图,再把“判断是否来过这里”变成“两张图是否相似”的问题。整个过程可以复现、可以调参、可以拿到自己的点云数据上看到实际效果。

这篇文章我会把Scan Context从原理到使用完整讲透。适合已经跑通一套激光SLAM、想给系统加上回环能力的同学,也适合刚接触全局描述子、想知道这套方法为什么有效的新手。文章里会包含算法拆解、代码层面的接入思路,以及我在不同场景下调参、踩坑的真实经验。

1. 为什么需要Scan Context这样的全局描述子

1.1 回环检测到底要解决什么问题

先聊一个基础问题:回环检测在SLAM系统里到底扮演什么角色。

纯靠里程计(无论是轮式里程计、IMU还是激光里程计)递推位姿,误差会随着时间累积。这种累积误差最直接的表现就是:机器人转了一大圈回到起点,但地图上的起点和终点对不上,墙歪了、走廊重影了、拐角错位了。回环检测的价值,就是让系统识别出“当前位置曾经来过”,从而建立一个从当前关键帧到历史关键帧之间的相对约束。这个约束会被送入位姿图优化或因子图优化,把长期累积的漂移一次性拉回来。

所以回环检测的本质不是“找相似”,而是“找约束”。它输出的不是一个分数,而是一条能够帮助全局优化的边。这条边的准确性和可靠性,直接决定后端优化能纠正多少漂移。

理想中的回环检测模块需要满足几个条件:

  • 对视角变化鲁棒,机器人从不同方向经过同一地点时也要能识别。
  • 对场景局部变化鲁棒,比如临时停了一辆车、树长高了、货架位置微调。
  • 计算效率足够高,不能因为做回环检测拖慢整个SLAM系统。
  • 能够提供相对位姿信息,而不只是“相似/不相似”的布尔判断。

Scan Context在这几个维度上做到了不错的平衡。

1.2 从视觉方案到激光方案的路线迭代

早期很多SLAM系统的回环检测沿用视觉SLAM的路线,用词袋模型(Bag of Words)描述图像特征。视觉词袋在室内纹理丰富的环境里效果尚可,但到了激光SLAM的场景,问题就来了:很多环境点云特征丰富但图像纹理稀疏,比如夜间、逆光、大面积玻璃幕墙、白墙仓库,视觉特征提取本身都不稳定,更别提做闭环了。

也有方案用点云直方图、ESF(Ensemble of Shape Functions)这类全局统计特征。它们虽然鲁棒,但丢掉了空间拓扑信息,区分度不够。想象一下两个外形相似但内部结构不同的厂房,直方图可能给出很高的相似度,实际却是完全不同的地点。

Scan Context的切入点很有意思:它把点云编码成一张类似“鸟瞰图”的矩阵,保留了每个扇区内结构的分布信息,既比直方图有区分度,又避开了视觉特征的脆弱性。它本质上做的是一件事——把三维空间的结构“拍扁”成二维图像,然后用图像检索的思路去解决。这个思想后来影响了一大批圆形描述子方案,比如SC++、SCT、Intensity Context等,但Scan Context本身至今仍然是验证最充分、使用最广泛的基线方案。

1.3 Scan Context的核心思想:把点云“拍扁”成图像

理解Scan Context,最关键的一步是理解它的编码方式。

传统方法处理一帧点云,通常是提取关键点、计算特征描述子、做匹配。这个过程本身就有大量可调参数,而且特征提取的质量会直接影响回环性能。Scan Context换了一个思路:不去找特征,而是把整帧点云变换成一个固定尺寸的矩阵。

这个矩阵就是“Scan Context”。它每行代表一个径向环(ring),每列代表一个角向扇区(sector),矩阵值代表该区域内点云的高度信息。这样一来,一帧上万个点的点云,就压缩成了比如20行乘60列的数据,后续所有操作都基于这个紧凑的矩阵展开。

这种“先压缩、再匹配”的思路,决定了Scan Context的几个天然优势:

  • 不存在特征点提取环节,也就不存在特征缺失或特征分布不均的问题。
  • 矩阵表达固定维度,方便构建索引、做快速检索。
  • 计算相似度就是矩阵运算,可以用成熟的线性代数库加速。

代价是它丢失了部分精确几何信息。不过作为回环检测的顶层筛选方案,这恰恰是够用的——我们不需要它在稠密层面精确对齐,只需要它快速、准确地回答“哪些历史帧和当前帧来自同一地点”。

2. 算法拆解:点云如何变成一张“场景名片”

2.1 环和扇区:一张扇形网格

现在进入具体算法。第一步,理解“环”和“扇区”这两个概念。

首先设定一个最大扫描范围Lmax,比如80米。超出这个范围的点直接忽略,这样可以避免远处稀疏噪点干扰,也能让描述子聚焦在近处可靠的结构上。接下来,把以传感器为中心的平面空间,按照半径划分成nr个环(ring),每个环宽度相等,即Lmax/nr。再将360度方向划分成ns个扇区(sector),每个扇区角度相等,即360/ns度。

打个比方,这个过程就像把一张圆形披萨切成nr圈同心圆环,再沿半径方向切成ns个扇形小块。每个小格子就是一个“环形扇区单元”,对应矩阵中的一个元素。

对每个点,先用它的水平距离确定它落在哪个环里,再用它的水平角度确定它落在哪个扇区里,最后把z值塞进对应格子里。遍历完整帧点云后,就得到了一个nr行乘ns列的矩阵。横轴是角度方向,纵轴是径向距离。

这里有几个工程细节值得注意:

  • 环的划分用的是水平投影距离x² + y²的平方根,而不是三维欧氏距离。这是对的,因为雷达扫描本身就是按水平角度展开的,高度信息只作为格子内的值保存。
  • 实际实现时,角度计算推荐用atan2直接得到范围[-π,π]的角度,再映射到[0, 2π),避免角度边界问题。
  • 对于多线激光雷达,不同线的俯仰角不同,点云在垂直方向分布不均,但Scan Context不关心这些,它只需要每个格子里最高的那个点。

2.2 每格存什么:最大高度的选择逻辑

每个格子里的值,取的是该区域内所有点的最大高度,而不是平均高度,更不是点数密度。为什么是最大高度?

首先,最大高度能够反映场景的几何轮廓。比如一面墙、一个立柱、一块路牌,在对应格子里的高度值会明显高于地面,形成了可供匹配的结构特征。平均高度会把墙面和地面混在一起,导致区分度下降。

其次,最大高度对地面噪声不敏感。地面点的高度基本都在传感器附近,取最大值天然会选到那些“立起来”的结构,而不是地面上的测量噪声。同时它也天然忽略了一些低矮的动态干扰,比如地面上的纸箱、散落的碎石。

但这里也说一个它的弱点:如果场景里有一辆大型卡车临时停靠,它所在格子的最大高度会瞬间变大,导致描述子变化剧烈。这属于所有全局描述子的通病,Scan Context并没有完全解决,只是通过调整格子的空间分辨率把它控制在一定范围内。

每个格子的值还做了一步特殊处理:如果某个格子里没有任何点,这个值就保持0。这样既表示“这个区域没有观测到结构”,也方便后续构造稀疏表示,减少存储和计算开销。

2.3 相似度度量:按列比较的妙处

有了矩阵,接下来就是如何判断两个矩阵相似。

直观的思路是把矩阵拉成一个长向量,直接算余弦距离或欧氏距离。但Scan Context并没有这么做,而是把矩阵按列拆开,每一列看成该扇区下从近到远的高度剖面,然后两两计算列之间的相似度,最后取平均。

为什么要按列比较?因为列对应的是“某个角度方向上的径向剖面”,这种剖面保留了场景结构的拓扑关系。而如果直接拉平整个矩阵,会丢失这种方向性的空间关系,反而让匹配精度下降。

论文中使用的列相似度函数是余弦距离:

s(v1, v2) = (v1·v2) / (||v1|| ||v2||)

对应的距离定义为:

d(I1, I2) = 1 - (1/ns) * Σ s(v1_j, v2_j)

值越小代表两帧越相似。这里使用余弦距离而不是欧氏距离,好处是它对描述子幅值的变化不那么敏感——两个格子的高度值都整体增大,比如传感器换了一款更高的型号,只要相对结构一致,余弦距离仍然能给出较好的相似度。

这一步是整个Scan Context匹配的核心。但注意,它假设两帧的朝向大致对齐。实际场景里这个假设几乎不成立,所以论文引入了旋转不变性的处理逻辑,我放到下一节专门讲。

2.4 快速检索:RingKey预筛的作用

直接拿当前帧的描述子和历史所有关键帧的描述子逐一算距离,在规模小的时候没问题,但关键帧多了以后计算量会线性上涨。Scan Context的解决办法是引入RingKey。

RingKey的定义很简洁:把Scan Context矩阵的每一行取平均,得到一个nr维的向量。由于行平均不依赖于列移位,这个向量天然对旋转不变。换句话说,不管机器人朝哪个方向站,同一位置计算出的RingKey基本一致。

检索时,先用所有历史帧的RingKey构建一个KD-Tree,对当前帧的RingKey做最近邻搜索,取距离最小的一批候选帧。然后再对这些候选帧计算完整的Scan Context距离(含列移位对齐)。这个“先粗筛、再精排”的设计,把全局暴力匹配变成了一个在几十个候选内做精确匹配的问题,效率提升非常明显。

我在实际使用中观察到的数据是:一帧Scan Context匹配1000个历史关键帧,RingKey预筛后候选集控制在20到50个左右,耗时能缩短一个数量级。而且预筛阶段的召回率足够高,很少出现正确候选被提前滤掉的情况。

3. 旋转不变性是怎么做到的

3.1 航向变化会让矩阵发生什么

这是Scan Context设计中最巧妙的一环。

假设机器人两次经过同一个位置,第一次朝北,第二次朝南。点云在空间中发生了180度旋转,鸟瞰图也随之旋转。反映到Scan Context矩阵上,就是所有列整体循环移位了ns/2列。如果此时直接计算两个矩阵的距离,结果是灾难性的——完全相同的地点,相似度却可能低得像完全不同的环境。

所以,要让Scan Context真正用于实际回环,必须解决旋转不变性。

3.2 列移位搜索:把旋转变成平移

论文给出的解法非常直接:既然旋转等价于列移位,那我枚举所有可能的列移位,找出最像的那一个。

具体来说,把候选矩阵的列循环移位k步,得到新矩阵,然后与当前矩阵计算距离。遍历k从0到ns-1,取距离最小的一次作为最终相似度。这个最小距离对应的k值,就是两次观测之间的角度差(以扇区宽度为单位)。这一过程等价于在角度维度上做了一个对齐,把两个任意朝向的观测调整到同一朝向再比较。

直接实现这个枚举的复杂度是O(nr × ns × ns),ns一般取60,也就是3600次列向量点积,计算量完全可接受。但如果历史帧数量很大,还是建议先通过RingKey粗筛缩小候选范围,再做这一步精确对齐,否则会拖慢整体回环检测频率。

实现时还有一个细节:枚举列移位后的距离一般不会存在“多个相等的最小值”的情况,因为真实环境中的场景结构基本不是严格周期性的。但在非常对称的环境里(比如完全对称的厂房),可能出现多个接近的最小值,这时候建议取所有最小值对应的k值做平均,或者干脆拒绝对称性过强的帧做回环,避免误检。

3.3 从旋转偏移量反推相对航向

列移位k不仅能给出相似度,还能直接换算出两帧之间的粗相对航向角。

假设扇区数量为ns,那么最优列移位k对应的角度偏移为:

Δθ = (k / ns) × 360°

这个角度可以拿来做两件事。

第一,作为ICP精配准的初始值。很多回环验证流程中,拿到候选帧后会做一步ICP精配准,但ICP对初始位姿敏感,如果初值太差容易收敛到局部极小值。用Scan Context得到的粗航向角初始化,能显著提高ICP的成功率。

第二,作为回环边的观测值。在位姿图优化中,回环边的相对位姿如果不准,即使拓扑关系正确也可能导致优化后的地图变形。Scan Context给出的角度信息虽然不如ICP精配准精确,但作为一个先验约束,可以很好地约束优化方向。

我在自己的系统里同时用了这两点:先拿粗航向角给ICP初值,再用ICP精配准结果作为回环边的相对位姿观测。这样做的好处是回环边质量稳定,基本不需要额外的手动调优。

4. 从代码到系统:Scan Context的工程接入

4.1 在因子图SLAM中的位置

回环检测不是独立模块,它深深嵌在SLAM系统的全局优化框架里。以LIO-SAM这类因子图SLAM为例,整个系统分为前端和后端:

前端不断接收激光点云和IMU数据,通过帧间匹配输出里程计因子,并定期生成关键帧。后端维护一个因子图,把里程计因子、GPS因子(如果有)、回环因子不断加入图中,并定时执行图优化,更新所有节点位姿。

Scan Context在这里承担的是“回环因子生成器”的职责。它需要定期提取当前关键帧的描述子,与历史关键帧的描述子做检索匹配,找到可信的候选后,输出一条回环因子到因子图中。

这个定位决定了它在工程上需要满足几个约束:

  • 不能阻塞主流程,回环检测通常在单独线程中执行。
  • 候选帧必须足够少,否则后续几何验证的计算量会失控。
  • 必须有确认机制,防止误匹配污染因子图。

4.2 典型接入流程拆解

一个完整的Scan Context接入流程大概分五步。

第一步,关键帧描述子生成。每生成一个关键帧,就计算它的Scan Context描述子和RingKey,存入描述子数据库中。数据库可以简单保存为一组向量,也可以用向量检索库管理。

第二步,候选检索。当前关键帧的RingKey在KD-Tree中查找最近的若干个历史描述子。这一步要设置一个RingKey距离阈值,太远的不考虑,同时也要排除掉时间上过近的帧,避免把前后相邻帧当作回环。

第三步,精确匹配。对每个候选帧,计算当前帧描述子与它的Scan Context距离(含列移位对齐),筛掉距离大于阈值的候选。

第四步,几何验证。对通过描述子距离筛选的候选,用粗航向角做初值,执行ICP配准,检查内点比例和收敛后的位姿变化是否合理。这里的几何验证是防止回环误检的关键防线,因为描述子相似不必然代表空间一致,尤其在重复结构多的场景里。

第五步,加入因子图。通过验证的候选对生成回环因子,连同对应的相对位姿(通常用ICP精配准结果)一起作为新的边加入因子图中。

实际工程里,第四步的几何验证经常被简化甚至省略,因为Scan Context本身的误检率已经比较低。我个人建议不要省。一来ICP计算量不大,二来它可以剔除“描述子相似但空间不对应”的罕见误检,安全性提升明显。

4.3 一个轻量接入示例

如果你已经有一套基于因子图的激光SLAM系统,接入Scan Context的伪代码大概长这样:

流程说明如下:

void loopDetection(const PointCloud& cloud, int keyframe_id) { // 生成描述子 MatrixXd sc = buildScanContext(cloud); VectorXd rk = buildRingKey(sc); // 检索候选 vector<int> candidates = searchByRingKey(rk, kdtree); // 精确匹配 for (int cand : candidates) { if (abs(cand - keyframe_id) < minKeyframes) continue; double dist = distanceWithShift(sc, sc_db[cand], keyframe_id, cand); if (dist > sc_threshold) continue; // 几何验证 Pose relPose = icpVerify(cloud, cloud_db[cand], dist); if (relPose.valid()) { addLoopFactor(keyframe_id, cand, relPose); } } }

这个流程并不复杂,但真正跑起来会有很多细节需要注意。比如描述子数据库的存储方式,建议使用类似faiss这样的向量检索库,而不是自己维护KD-Tree,尤其在关键帧数量达到数万规模后,性能差异会非常明显。

另外一点:Scan Context的计算频率不要和里程计频率一样高。激光雷达通常是10Hz,但每帧都做回环检测没有必要且浪费算力。实践中我一般设置为每隔1到2米或每1到2秒触发一次检测,同时保证每个关键帧都保存描述子,这样既不会漏掉回环,也不会对系统造成持续高负载。

5. 调参实战与经验教训

5.1 关键参数与推荐范围

Scan Context相关的参数不多,但每个参数的取值都对最终效果有直接且显著的影响。我把常用参数整理成一个表格,方便参考。

参数常见范围说明
nr(环数)16 ~ 32径向分辨率,越大越能区分近处结构
ns(扇区数)40 ~ 90角度分辨率,越大对航向变化越敏感
Lmax(最大范围)40 ~ 100 米描述子感知范围,根据传感器量程设定
RingKey检索数量20 ~ 50KD-Tree返回的候选帧数
Scan Context距离阈值0.15 ~ 0.3低于该值判为回环候选
最小关键帧间隔30 ~ 60 帧排除时间上过近的帧
最小回环距离10 ~ 20 米排除空间距离过近的匹配

这些参数不是拍脑袋定的,每个都有背后的考量。比如nr太小时,径向分辨率不足,两段距离相近但结构不同的区域会被合并,描述子的区分度大幅下降;nr太大时,远处区域因为点云稀疏,格子内容不稳定,反而导致噪声放大。20这个值在大多数场景下是合理的折中。

5.2 不同场景下的调整策略

参数最优值不是一个固定值,它和你的场景高度相关。我分别说几个典型场景的调整经验。

室内仓库场景,点云结构密集、墙和货架层次分明,但空间尺度不大。这种场景建议把Lmax缩到30米左右,nr保持20,ns可以适当提高到60或70。仓库里航向变化频繁,更高的角度分辨率有助于提升回环精度。同时由于环境本身重复度较高,距离阈值要调得严格一些,建议从0.15开始尝试。

室外园区或矿区场景,视野开阔、结构稀疏,点云数量和质量都很依赖雷达量程。这种场景建议把Lmax放到80到100米,nr取24左右,ns取60。距离阈值可以适当放宽到0.25左右,因为空旷环境本身区分度较低,过严会漏掉真实的回环。

停车场或地下车库场景,有大量重复立柱、车位标线,环境纹理重复性极强,是回环误检的高发区。这种场景除了把距离阈值调严到0.15以下,还要特别注意几何验证环节不能省。我遇到过两次“两张描述子非常像,但实际是不同区域的两个车位”的情况,全靠ICP验证拦下来。

森林或植被环境则比较麻烦。树叶和树冠在风吹时会产生大量高度变化,导致Scan Context矩阵中的格值跳动。这种情况下我会把Lmax缩小(因为远处植被不可靠),同时提高RingKey候选数量,因为单凭RingKey粗筛可能会漏掉一些结构相似但局部扰动的帧。

5.3 我踩过的几个坑

第一个坑是阈值拍脑袋定。早期我在一个园区测试时,把Scan Context距离阈值定成0.3,结果回环误检率居高不下。后来我统计了正负样本的距离分布,才发现正样本距离大多在0.1以下,负样本距离集中在0.2以上,0.3这个值根本没有区分度。建议拿到数据后,先离线统计一批真实回环对和随机对的距离分布,再根据分布曲线选择合适的阈值。

第二个坑是描述子生成的“脏数据”问题。如果前端雷达里程计在某一时刻位姿跳变(比如退化环境里匹配失败),生成的点云会带有明显畸变,这时候算出来的Scan Context也是脏的。如果脏描述子进了候选库,后面即使匹配成功,回环边的位姿也可能完全错误。我的处理办法是:在生成关键帧描述子时,检查该帧的里程计置信度,一旦发现退化或跳变,就不把描述子加入库中。

第三个坑是回环检测频率调太高。我曾经为了追求实时性,每帧激光数据都做一次回环检测,结果系统整体CPU占用飙升,主线程的帧间匹配帧率被明显拖慢。后来改成“每累计5米运动或每2秒触发一次”,回环检测延迟几乎察觉不到,CPU占用也降下来了。回环检测本来就是低频事件,没必要每帧都找。

6. 常见问题排查速查表

下面这部分内容适合遇到问题时直接查阅。我整理了工程里最常见的几类问题、可能原因和解决建议。

现象可能原因解决建议
真实回环没检出距离阈值设置过严统计正样本距离分布,按P-R曲线选阈值
真实回环没检出RingKey粗筛候选数不足增大检索候选数量,检查RingKey距离分布
真实回环没检出环境变化太大适当扩大Lmax,或提高nr以保留更多结构
回环误检环境对称性过高调严距离阈值,强制开启几何验证环节
回环误检RingKey KD-Tree距离度量不当检查RingKey是否做了归一化,必要时改用L1距离
回环检出了但优化后地图仍歪回环边相对位姿不准确认ICP精配准收敛,检查内点比例;考虑增加角度先验约束
匹配耗时过高历史帧数量过大降低检测频率,使用向量检索库,设置最小关键帧间隔
相邻帧被误判为回环未排除时间近邻设置最小关键帧间隔,最小空间距离阈值
相同地点不同高度检出失败高度差导致格值变化确认传感器安装高度固定;若无法避免,改用强度值作为格值

排查时有个总原则:先确认描述子本身对不对,再看匹配阈值是否合适。我遇到过不少“回环检不出”的问题,最后发现都是RingKey计算时行均值包含了空行0值,导致向量被大量0稀释,距离度量失真。这类数据层面的问题,往往比算法参数更隐蔽,也更容易被忽视。

7. 写在最后的一点体会

Scan Context这套方案我前后在不同数据集上跑了将近两年,整体感受是“上限高、下限也高”。它的原理足够简洁,让我很容易理解和调试;它又不至于过于简陋,在面对真实环境的各种噪声和干扰时依然能保持不错的鲁棒性。

有一点我的体会特别深:Scan Context对航向变化的容忍度,比我想象中强很多。我有一次在园区测试,车先顺时针绕了一圈,又逆时针绕了一圈,两次经过同一条路时航向接近相反,按照传统局部特征匹配的思路,这种情况基本不可能认出是同一地点。但Scan Context靠着列移位对齐,不仅正确检出了回环,还给出了相当接近真实值的相对航向。

如果你正准备给SLAM系统加回环功能,或者想深入理解全局描述子的工作原理,Scan Context是最好的起点。把它的原理吃透,再看后续的改进方案,思路会清晰很多。如果后续有机会,我也想整理一下基于强度值的Intensity Context、以及Scan Context和Neural Network结合的最新进展,到时候再和大家分享。

返回列表