1. 为什么我要啃这篇ORB-SLAM3论文
ORB-SLAM3这篇论文,做SLAM的人基本都绕不过去。我第一次读它的时候,满篇的公式和术语砸过来,读了三遍才把核心逻辑理顺。后来我干脆花了两周时间,把全文逐段翻译了一遍,跳过相关研究和精度验证部分,只保留系统架构、算法原理和实现细节。这份翻译稿后来成了我们实验室新人的入门材料,反馈还不错。
这篇论文解决的核心问题是:如何用一个统一的视觉SLAM系统,同时支持单目、双目、RGB-D三种传感器模式,并且能在室内外各种场景下稳定运行。它最大的创新点在于多地图系统(Atlas)和基于IMU的紧耦合优化。适合谁看?如果你已经了解基本的SLAM概念,比如特征点、BA优化、回环检测,但想深入理解ORB-SLAM3到底怎么把这些东西串起来,那这份翻译整理就是为你准备的。
我翻译的时候有个原则:不追求逐字逐句的对应,而是把每个模块的设计动机、关键公式的物理含义、以及代码实现中容易踩坑的地方讲清楚。所以这份内容既有论文原文的严谨,也有我自己的理解注释。下面我按论文的章节顺序,把核心内容拆开来讲。
2. 系统整体架构与设计思路拆解
2.1 三个线程加一个地图集,各司其职
ORB-SLAM3的架构继承了ORB-SLAM2的三线程设计,但增加了一个多地图管理模块。三个线程分别是:跟踪线程(Tracking)、局部建图线程(Local Mapping)、闭环与地图合并线程(Loop and Map Merging)。这三个线程并行运行,通过共享的Atlas地图集进行数据交换。
跟踪线程负责实时处理每一帧图像,提取ORB特征,估计相机位姿。它的输出是当前帧的位姿和关键帧决策。局部建图线程负责把新的关键帧插入地图,进行局部BA优化,剔除冗余关键帧和地图点。闭环线程负责检测回环和地图合并,执行位姿图优化。
Atlas是一个多地图容器,里面可以同时存在多个子地图(sub-map)。每个子地图有自己的关键帧和地图点。当跟踪丢失时,系统不会像ORB-SLAM2那样直接重置,而是新建一个子地图,等后续重新定位成功后再把两个子地图合并。这个设计是ORB-SLAM3鲁棒性的关键。
注意:Atlas中的子地图不是随便建的。系统会维护一个“活跃地图”的概念,只有活跃地图参与跟踪和局部建图。非活跃地图只参与闭环检测和地图合并。
2.2 为什么用ORB特征而不是深度学习特征
论文里明确说了,选择ORB特征是因为它在CPU上能实时运行,而且对旋转和尺度有一定不变性。深度学习特征虽然匹配精度高,但计算量大,很难在嵌入式平台上跑到实时。ORB-SLAM3的目标是通用性,不是刷某个数据集的精度榜。
ORB特征的提取过程分两步:先构建图像金字塔,每层提取FAST角点;然后用BRIEF描述子对角点进行编码。金字塔的层数默认是8,尺度因子是1.2。这个参数在代码里可以调,但论文建议不要改,因为后续的所有阈值都是基于这个尺度因子设定的。
我实测下来,ORB特征在纹理丰富的场景下表现很好,但在白墙、玻璃幕墙这种低纹理区域,提取到的特征点会急剧减少。这时候系统会依赖IMU或者运动模型来维持跟踪。所以如果你的应用场景纹理很差,要么加辅助传感器,要么考虑换特征提取方法。
2.3 紧耦合的IMU处理策略
ORB-SLAM3的IMU处理是紧耦合的,意思是IMU的预积分结果直接参与视觉BA优化,而不是作为独立的姿态估计模块。论文里用了IMU预积分理论,把两帧之间的IMU测量值积分成相对运动约束,然后把这个约束加到优化目标函数里。
具体来说,IMU预积分包括旋转、速度、位置三个部分的增量。预积分的好处是,当优化过程中调整了关键帧的位姿,不需要重新积分IMU数据,只需要用预积分的雅可比矩阵进行线性化更新。这个技巧在VINS-Mono里也有,但ORB-SLAM3的实现更简洁。
IMU的初始化分两步:第一步是视觉-only的初始化,估计初始的相机位姿和地图点;第二步是视觉-惯性联合初始化,估计重力方向、尺度、IMU零偏和速度。论文里给了详细的初始化流程,我翻译的时候把公式都保留了,因为这部分对理解后续优化很重要。
提示:IMU初始化对激励运动有要求。如果设备静止或者匀速运动,初始化会失败。实际操作中,建议在初始化阶段让设备做充分的旋转和平移运动。
3. 核心模块的算法细节与实操要点
3.1 跟踪线程的三种模式
跟踪线程根据当前状态切换三种模式:正常跟踪、重定位、丢失。正常跟踪时,系统用恒速运动模型预测当前帧位姿,然后和上一帧进行特征匹配,优化位姿。如果匹配点太少,就进入重定位模式,用词袋模型在Atlas中搜索相似关键帧,尝试重新定位。如果重定位也失败,就标记为丢失,等待新建子地图。
恒速运动模型假设相机在相邻帧之间做匀速运动。这个假设在大多数情况下成立,但在急停、急转的时候会失效。论文里提到,当恒速模型预测的位姿和实际匹配结果差异太大时,系统会自动切换到参考关键帧模型。参考关键帧模型直接用上一关键帧的位姿作为初始值,更稳定但收敛慢。
我在调试的时候发现,跟踪线程的匹配阈值很关键。默认的ORB匹配阈值是汉明距离小于50,但这个值在光照变化剧烈的时候会偏大,导致误匹配。后来我把它调到40,误匹配少了,但匹配点数量也少了。所以这个参数需要根据场景调,没有万能值。
3.2 局部建图的关键帧策略
局部建图线程的核心任务是决定哪些帧应该成为关键帧,以及如何优化地图。关键帧的插入条件有三个:距离上一次关键帧超过20帧、距离上一次关键帧的平移超过基线、或者跟踪到的地图点少于50个。这三个条件满足任意一个就插入关键帧。
插入关键帧后,局部建图线程会做三件事:三角化新的地图点、剔除冗余地图点、执行局部BA。局部BA的窗口包括当前关键帧、它的共视关键帧、以及这些关键帧观测到的地图点。优化用g2o或者Ceres求解,论文里用的是g2o。
冗余地图点的剔除标准是:如果一个地图点被超过3个关键帧观测到,但其中至少2个关键帧的观测角度差异小于5度,就认为这个点是冗余的。这个策略能有效控制地图规模,避免地图点爆炸。
注意:局部BA的迭代次数默认是10次,但实际跑的时候,如果场景变化快,可以适当增加迭代次数。不过迭代次数越多,局部建图线程的耗时越长,可能影响跟踪线程的实时性。
3.3 闭环检测与地图合并的实现
闭环检测分两步:先用车载词袋模型(DBoW2)在Atlas中搜索候选关键帧,然后用几何验证确认闭环。候选关键帧的搜索范围包括当前活跃地图的所有关键帧,以及非活跃地图的关键帧。如果闭环候选在非活跃地图中,就触发地图合并。
地图合并的过程是:先把两个子地图的共视关键帧对齐,然后执行位姿图优化,把两个地图的坐标系统一。合并后,原来的非活跃地图变成活跃地图的一部分,所有关键帧和地图点都更新到新的坐标系下。
论文里特别强调了闭环检测的鲁棒性。为了避免误闭环,系统用了三个验证条件:候选关键帧和当前关键帧的共视关系、候选关键帧的位姿连续性、以及闭环后的重投影误差。只有三个条件都满足,才接受闭环。
我踩过的一个坑是:在重复纹理场景下,DBoW2会给出很多候选关键帧,但大部分是误匹配。后来我在几何验证阶段加了一个RANSAC迭代,把误闭环率降下来了。这个改动在论文里没有提,但实际工程中很有必要。
4. 实操过程与核心环节实现
4.1 环境搭建与依赖安装
ORB-SLAM3的编译依赖包括:OpenCV(建议3.4以上)、Eigen3、g2o、DBoW2、Sophus。这些库的版本兼容性很关键。我试过用OpenCV 4.x编译,结果DBoW2的接口不兼容,报了一堆错。后来换回OpenCV 3.4.10,一次编译通过。
编译步骤大致如下:
# 安装依赖 sudo apt-get install libopencv-dev libeigen3-dev libboost-all-dev # 编译g2o git clone https://github.com/RainerKuemmerle/g2o.git cd g2o && mkdir build && cd build cmake .. && make -j4 # 编译DBoW2 git clone https://github.com/dorian3d/DBoW2.git cd DBoW2 && mkdir build && cd build cmake .. && make -j4 # 编译ORB-SLAM3 git clone https://github.com/UZ-SLAMLab/ORB_SLAM3.git cd ORB_SLAM3 && chmod +x build.sh ./build.sh编译过程中最容易出问题的是g2o的版本。ORB-SLAM3用的是g2o的一个特定分支,如果直接用master分支,可能会有API不匹配。建议用论文作者提供的g2o版本,或者从ORB-SLAM3的第三方依赖里找。
提示:编译时如果遇到“undefined reference to symbol”之类的错误,大概率是链接顺序问题。把g2o和DBoW2的库放在ORB-SLAM3的库后面链接,通常能解决。
4.2 配置文件的关键参数解析
ORB-SLAM3的配置文件是YAML格式,里面包含了相机内参、IMU噪声参数、ORB特征参数等。我以EuRoC数据集的配置文件为例,把关键参数列出来:
| 参数名 | 含义 | 典型值 | 调整建议 |
|---|---|---|---|
| Camera.fx | 焦距x | 458.654 | 根据标定结果填 |
| Camera.k1 | 径向畸变 | -0.283 | 标定得到 |
| ORBextractor.nFeatures | 每层特征点数 | 1000 | 纹理差可增加到1500 |
| ORBextractor.scaleFactor | 金字塔尺度因子 | 1.2 | 不建议改 |
| IMU.NoiseGyro | 陀螺仪噪声 | 0.00016 | 查IMU手册 |
| IMU.NoiseAcc | 加速度计噪声 | 0.002 | 查IMU手册 |
IMU噪声参数对初始化影响很大。如果填得比实际噪声小,初始化会过于自信,导致后续优化发散;填得太大,初始化会过于保守,收敛慢。我的经验是,先查IMU的数据手册,找到噪声密度,然后乘以采样时间的平方根,得到离散噪声参数。
4.3 运行与调试的实操记录
跑EuRoC数据集的时候,我用的是单目+IMU模式。启动命令如下:
./Examples/Monocular-Inertial/mono_inertial_euroc \ ./Vocabulary/ORBvoc.txt \ ./Examples/Monocular-Inertial/EuRoC.yaml \ ./dataset/EuRoC/MH_01_easy \ ./Examples/Monocular-Inertial/EuRoC_TimeStamps/MH01.txt第一次跑的时候,系统在初始化阶段就卡住了。查日志发现是IMU初始化失败,原因是数据集开始阶段设备运动不够激励。后来我从数据集的第50帧开始跑,跳过前面的静止段,初始化就成功了。
跟踪阶段偶尔会出现跟丢的情况。我分析了一下,主要是快速旋转的时候,ORB特征匹配跟不上。后来我把ORBextractor.nFeatures从1000调到1500,跟丢次数明显减少。但代价是每帧的处理时间从15ms增加到22ms,在CPU上还能接受。
注意:如果跑的是双目模式,配置文件的Camera.bf参数(基线乘以焦距)必须准确。这个参数错了,深度估计会整体偏移,导致地图尺度不对。
5. 常见问题与排查技巧实录
5.1 初始化失败的几种典型情况
IMU初始化失败是最常见的问题。表现是系统一直停在“初始化中”状态,不输出位姿。原因通常有三个:一是设备运动不够激励,二是IMU噪声参数填错,三是视觉特征太少。
排查方法:先看日志里IMU预积分的残差,如果残差很大,说明噪声参数不对。再看视觉特征数量,如果每帧特征点少于100个,说明场景纹理太差。最后检查运动激励,如果设备在初始化阶段基本静止,那肯定失败。
我的经验是,初始化阶段让设备做“8字运动”最有效。具体来说,先绕Z轴旋转30度,再平移20厘米,重复三次。这样既有旋转激励又有平移激励,初始化成功率很高。
5.2 跟踪丢失的排查思路
跟踪丢失的表现是位姿输出中断,系统进入重定位模式。排查的时候,我一般按以下顺序检查:
- 检查图像质量:有没有过曝、欠曝、运动模糊。
- 检查特征点数量:如果每帧特征点少于50个,说明纹理太差。
- 检查运动速度:如果相邻帧的平移超过基线,恒速模型会失效。
- 检查IMU数据:如果IMU有跳变或者丢帧,预积分会出错。
有一次我遇到跟踪丢失,查了半天发现是IMU的时间戳和图像时间戳没对齐。ORB-SLAM3要求IMU和图像的时间戳严格同步,如果差超过5ms,预积分就会有问题。后来我在数据预处理阶段加了一个时间戳对齐,问题解决。
5.3 地图合并失败的常见原因
地图合并失败通常发生在闭环检测阶段。表现是系统检测到了闭环,但合并后地图错乱。原因可能是:两个子地图的尺度不一致、闭环候选关键帧的位姿估计错误、或者位姿图优化的初值太差。
排查方法:先看闭环候选关键帧的匹配点数量,如果少于30个,说明匹配不可靠。再看位姿图优化的残差,如果残差大于0.1,说明初值有问题。最后检查两个子地图的尺度,如果尺度差异超过10%,合并后地图会明显变形。
我踩过的一个坑是:在双目模式下,两个子地图的基线估计不一致,导致尺度差异。后来我在合并前加了一个尺度对齐步骤,用两个地图的共视地图点估计尺度因子,合并成功率大幅提升。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 初始化卡住 | IMU激励不足 | 查看IMU预积分残差 | 做8字运动 |
| 跟踪丢失 | 特征点太少 | 统计每帧特征点数 | 增加nFeatures |
| 闭环误检 | 重复纹理 | 检查几何验证残差 | 加RANSAC |
| 地图合并错乱 | 尺度不一致 | 比较共视地图点尺度 | 加尺度对齐 |
| 位姿漂移 | IMU零偏未估计 | 查看零偏收敛曲线 | 延长初始化时间 |
提示:ORB-SLAM3的日志系统很详细,遇到问题先看日志。日志里会输出每个模块的耗时、残差、匹配点数量等信息,比盲目调试高效得多。
6. 翻译过程中的理解难点与个人体会
翻译这篇论文的时候,最让我头疼的是IMU预积分部分的公式推导。论文里用了大量的李代数和李群符号,如果不熟悉流形上的优化,看起来会很吃力。我的建议是,先补一下《State Estimation for Robotics》这本书的前几章,把SO(3)和SE(3)的雅可比矩阵搞清楚,再回来看论文就顺畅多了。
另一个难点是地图合并的位姿图优化。论文里只给了优化目标函数,没有给具体的求解步骤。我后来对照g2o的源码,才把优化变量的更新方式搞清楚。简单来说,位姿图优化的变量是每个关键帧的位姿,约束是共视关系,求解用高斯牛顿或者列文伯格-马夸尔特。ORB-SLAM3用的是列文伯格-马夸尔特,因为它的阻尼因子能自适应调整,收敛更稳定。
翻译完整篇论文后,我最大的体会是:ORB-SLAM3的工程实现比论文写的要复杂得多。论文里很多细节都是一笔带过,比如特征匹配的阈值、关键帧的剔除策略、闭环的验证条件,这些在代码里都有更具体的实现。所以如果你想真正掌握ORB-SLAM3,光看论文不够,一定要结合代码看。
最后分享一个小技巧:读论文的时候,把每个模块的输入输出画成框图,然后对照代码找对应的函数。比如跟踪线程的输入是图像和IMU数据,输出是位姿和关键帧决策,对应的函数是Tracking::GrabImageMonocular和Tracking::Track。这样能把论文和代码对应起来,理解更深刻。
这个内容后续还可以这样扩展:一是把ORB-SLAM3的代码逐模块注释一遍,二是对比ORB-SLAM3和VINS-Mono、LVI-SAM等系统的异同,三是把ORB-SLAM3移植到嵌入式平台上的优化经验整理出来。这些方向我都还在做,有机会再分享。