ORB-SLAM2中关键知识点理解( 地图点、关键帧、图结构)

本文详细解析了ORBSLAM2的三大模块:Tracking、LocalMapping和LoopClosing,介绍了初始化过程、关键帧选择策略、跟踪方法、地图点筛选和回环闭合流程。此外,还涵盖了非线性优化、共视图和本质图在系统中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0. 目录

1. ORB SLAM2的总体框架是怎样的?

下图是论文里的原图,ORB SLAM2一共有三个线程,分别是Tracking,Local Mapping和Loop Closing,其中Tracking是负责提取关键点进行帧间匹配,并且初选关键帧,Loop Mapping是进行关键帧筛选和地图点剔除,同事进行一个局部优化,Loop Closing主要是进行回环检测。
在这里插入图片描述

2. ORB SLAM2是怎样完成初始化的?

在ORB SLAM2进行跟踪前,需要先进行初始化,初始化包括相机初始帧位姿,新建地图,新建关键帧等,ORB SLAM2中一共提供了三种相机接口,其中单目初始化过程较为复杂,双目和RGBD初始化与其相似但较为简单,以单目为例,单目初始化过程如下:
(1)寻找匹配点,对前两帧图像提取ORB特征之后进行特征点匹配,若匹配个数少于100则匹配失败;
(2)由匹配点恢复位姿,利用八点法同时计算单应矩阵和基础矩阵,并通过计算评分比选择合适的结果作为初始位姿;
(3)创建初始地图,根据前两帧的位姿,利用三角法恢复三维地图点,创建初始地图,将这两帧都设为关键帧,并完成关键帧雨地图点,关键帧雨关键帧之间的关联,对上述两个关键帧和地图点进行一次全局BA,并选择地图点深度的中位数作为单位尺寸1来进行地图的尺寸初始化。

值得注意的是,单目初始化深度是通过三角化获得的,因此需要具有一定的平移,纯旋转不满足对极几何的要求因此无法进行三角化。

双目和RGBD初始化过程和单目不同的是,初始化过程不需要三角恢复深度,而是选取有深度的点进行初始化,因此只需要一个图像帧,而且没有创建关联和全局BA这些操作。

3. ORB SLAM2是如何进行Tracking的?

Tracking的目的是兼顾计算速度和鲁棒性使得当前帧中提取的特征点跟踪匹配到上一帧,在ORB SLAM2中一共使用了三种模型,分别是运动模型(TrackWithMotionModel)、关键帧模型(TrackReferenceKeyFrame)和重定位(Relocalization),其中
(1)运动模型:假定相机运动是匀速的,通过上一帧的速度和位姿估计下当前帧的位姿,然后进行跟踪匹配,这里要知道的是,如果帧和帧之间都采用暴力匹配的话运行速度会受到很大限制,因此作者是采用跟踪匹配加速了匹配过程(就是将上一帧的特征点投影到下一帧中,然后在投影点的附近区域进行搜索);
(2)关键帧模型:就是和上一个关键帧进行匹配,这里是采用BoW进行匹配加速;
(3)重定位:和所有关键帧进行匹配,同样采用BoW进行匹配加速;
这几个模型的使用规则是,(1)不好使用用(2),(2)不好使用用(3),然后这里也可以看出,ORB SLAM2的作者为了提高程序运行速度无所不用其极。

在完成上述三个模型中的匹配后,程序还会将局部地图点投影到当前帧中实现一个匹配,目的是为了对局部地图和局部关键帧进行跟新(用作优化用,以提高精度)

4. ORB SLAM2是如何选取关键帧的?

关键帧的选取标准是:
(1)距离上一次重定位距离至少20帧
(2)局部地图线程空闲,或者距离上一次加入关键帧过去了20帧,如果需要关键帧插入(过了20帧)而Local Mapping线程忙,则发送信号给Local Mapping线程,停止局部地图优化,使得新的关键帧可以被及时处理
(3)当前帧跟踪至少50个点,确保了跟踪定位的精确度;
(4)当前帧跟踪到Local Map中参考帧的地图点数量少于90%,确保关键帧之间有明显的视觉变化,减少视觉冗余。

这里只是判断是否需要将当前帧创建为关键帧,并没有真的加入全局地图,因为Tracking线程的主要功能是局部定位,而处理地图中的关键帧、地图点,包括如何加入、如何删除的工作是在LocalMapping线程完成的

5. ORB SLAM2中有那些(非线性/后端)优化相关的操作?

ORB SLAM2中一共有五种优化方式,分别是局部BA优化全局BA优化闭环位姿优化全局位姿优化单帧BA优化(这个名字是我自己取的,方便理解)
(1)局部BA优化:运行在Local Mapping线程中,在剔除关键帧之前进行局部地图优化,当新的关键帧加入到convisibility graph时,作者在关键帧附近进行一次局部优化,优化的目标是最小重投影误差
在这里插入图片描述

(2)全局BA优化:用于单目初始化的CreateInitialMapMonocular函数以及闭环优化的RunGlobalBundleAdjustment函数(在闭环结束前新开一个线程,进行全局优化。
在这里插入图片描述

(3)闭环位姿优化:当检测到闭环时闭环连接的两个关键帧的位姿需要通过Sim3/SE3优化(以使得其尺度一致)。优化求解两帧之间的相似变换矩阵,使得二维对应点(feature)的投影误差最小
在这里插入图片描述

(4)全局位姿优化:这个优化就相当于《视觉SLAM十四讲》中的Pose Graph,值得注意的是如果是单目的话这里优化的是Sim3,如果是双目或者RGBD的话这里优化的SE3),ORB SLAM2中优化的对象是Essential Graph,Essential Graph指的是所有的关键帧顶点,但是优化边大大减少,包括spanning tree(生成树)和共视权重θ > 100 的边,以及闭环连接边闭环调整CorrectLoop过程中的优化。
在这里插入图片描述

(5)单帧BA优化:只优化当前帧pose,地图点固定,用于LocalTracking中运动模型跟踪、参考帧跟踪、地图跟踪TrackLocalMap、重定位,每进行过一次PnP投影操作将地图点投影到当前平面上之后,都会进行一次PoseOptimization位姿优化,通过BA优化重投影误差

6. ORB SLAM2中有维护了哪些图?

(1)Covisibility Graph:共视图,是一个无向有权图(Graph),这个概念最早来自2010的文章[Closing Loops Without Places]。该图中每个顶点就是关键帧,如果两个关键帧有相同的地图点(即它们有共视点),就把这两个顶点连接起来,连接边的权重就是两个关键帧共享的3D点的个数。局部BA优化依赖的就是一个局部的共视图,全局BA优化依赖的就是一个全局的共视图,总之共视图在ORB SLAM2中用得很多。
(2)Essential Graph:为了在优化阶段减小计算量,ORB-SLAM2作者提出了Essential Graph的概念,主要用它来进行全局位姿优化。它是共视图的子集,即Covisibity Graph的最小生成树(MST)。该图中顶点是关键帧,但只连接某个关键帧和与之拥有最多共享的地图点的关键帧,这样能够连接所有的顶点,但是边会减少很多。
(3)Spanning Graph:生成树,是代价最小的全联通图,Essential Graph就是基于Sanning Graph生成的。

7. ORB SLAM2中是如何对地图点进行筛选的?

在Local Mapping线程中在进行局部BA之前会先对地图点进行剔除,剔除规则如下:
(1)该地图点是坏点,直接从检查列表去掉;
(2)跟踪(匹配上)到该地图点的普通帧帧数(Increase Found)小于应该观测到该地图点的普通帧数量(25%*Increase Visible),即比值mnFound/mnVisible小于0.25,设置为坏点,并从检查列表去掉比值低说明这样的地图点该地图点虽在视野范围内但很少被普通帧检测到
(3)从添加该地图点的关键帧算起,当前关键帧至少是第三个添加该地图点的关键帧的条件下,看到该地图点的帧数<=2(双目和RGBD模式是帧数<=3),设置为坏点,并从检查列表去掉;因此在地图点刚建立的阶段,要求比较严格,很容易被剔除;而且单目的要求更严格,需要三帧都看到;
(4)若从添加该地图点的关键帧算起,一共有了大于三个关键帧,还存在列表中,则说明该地图点是高质量的,从检查列表中去掉

总而言之就是太少帧看到的地图点是坏点都要剔除掉,而看得多的则是高质量点

8. ORB SLAM2中Loop Closing的具体实现流程是怎样的?

首先是通过回环检测(Bow得分)和共视关系检查从所有关键帧中筛选出一组和当前帧有可能形成闭环的候选帧,然后利用相似求解器Sim3Solver求解出候选帧与当前帧之间的相似变换(注意这里是单目相似变换,而双目或者RGBD是刚体变换),利用相似变换找出更多的匹配地图点,然后进行闭环位姿优化(对应5中的回答),如果优化结果较好的话就不再判断其他候选帧。然后就是闭环矫正,通过就出来的相似变换对当前帧进行位姿调整并且传播到当前帧相连的关键帧,回环两侧的关键帧完成对齐,然后利用调整过的位姿更新这些相连关键帧对应的地图点同时在Covisibility Graph里面增加闭环边,然后进行Essential Graph的优化(即全局位姿优化)当前帧与闭环匹配帧之间的边不进行优化最后再来一个全局BA优化即完成了Loop Closing的全部流程

9. 什么是Sim3优化?

在这里插入图片描述
视觉SLAM总结——后端总结中总结的微分求导模型和扰动求导模型对其同样适用。Sim3优化其实就是Pose Graph优化,其优化目标残差是
在这里插入图片描述

1 、 地图点

1.1 地图点代表性描述子的计算

找最有代表性的描述子示意图
最有代表的描述子与其他描述子具有最小的距离中值

在这里插入图片描述

地图点法线朝向的计算
在这里插入图片描述

1.2 地图点和特征点的区别?

地图点是三维点,来自真实世界的三维物体,有唯一的id。不同帧里的特征点可能对应三维空间中同一个三维点,
特征点是二维点,是特征提取的点,大部分二维点在三维空间中没有对应地图点
生成地图点
关于生成地图点,主要有以下几个地方:

  1. 单目初始化时前两帧匹配生成地图点,双目左右目匹配生成地图点,RGB-D测量得到的地图点
  2. local mapping里共视关键帧之间用 LocalMapping::CreateNewMapPoints() 生成地图点
  3. Tracking::UpdateLastFrame() 和 Tracking::CreateNewKeyFrame()中为双目和RGB-D生成了新的临时地图点,单目不生成

2、关键帧

2. 1 什么是关键帧?

通俗来说,关键帧就是几帧普通帧里面具有代表性的一帧。

2.2 为什么需要关键帧

  1. 相近帧之间信息冗余度很高,关键帧是取局部相近帧中最有代表性的一帧,可以降低信息冗余度。举例来说,摄像头放在原处不动,普通帧还是要记录的,但关键帧不会增加。
  2. 关键帧选择时还会对图片质量、特征点质量等进行考察,在Bundle Fusion、RKD SLAM等RGB-D SLAM相关方案中常常用普通帧的深度投影到关键帧上进行深度图优化,一定程度上关键帧是普通帧滤波和优化的结果,防止无用的或错误的信息进入优化过程而破坏定位建图的准确性。
  3. 如果所有帧全部参与计算,不仅浪费了算力,对内存也是极大的考验,这一点在前端vo中表现不明显,但在后端优化里是一个大问题,所以关键帧主要作用是面向后端优化的算力与精度的折中,使得有限的计算资源能够用在刀刃上,保证系统的平稳运行。假如你放松 ORB_SLAM2关键帧选择条件,大量产生的关键帧不仅耗计算资源,还会导致local mapping 计算不过来,出现误差累积

2.3 如何选择关键帧?

2. 3.1 选择关键帧主要从关键帧自身和关键帧与其他关键帧的关系2方面来考虑。

  1. 关键帧自身质量要好,例如不能是非常模糊的图像、特征点数量要充足、特征点分布要尽量均匀等等;
  2. 关键帧与其他关键帧之间的关系,需要和局部地图中的其他关键帧有一定的共视关系但又不能重复度太高,以达到既存在约束,又尽量
    少的信息冗余的效果。

2.3.2 选取的指标主要有:

  1. 距离上一关键帧的帧数是否足够多(时间)——比如我每隔固定帧数选择一个关键帧,这样编程简单但效果不好。比如运动很慢的时候,就会选择大量相似的关键帧,冗余,运动快的时候又丢失了很多重要的帧。

  2. 距离最近关键帧的距离是否足够远(空间)/运动——比如相邻帧根据pose计算运动的相对大小,可以是位移也可以是旋转或者两个都考虑,运动足够大(超过一定阈值)就新建一个关键帧,这种方法比第一种好。但问题是如果对着同一个物体来回扫就会出现大量相似关键帧。

  3. 跟踪局部地图质量(共视特征点数目)——记录当前视角下跟踪的特征点数或者比例,当相机离开当前场景时(双目或比例明显降低)才会新建关键帧,避免了第2种方法的问题。缺点是数据结构和逻辑比较复杂。
    在这里插入图片描述

2.3.3 很重要的一点

在关键帧的运用上,我认为orbslam2做的非常好,跟踪线程选择关键帧标准较宽松,局部建图线程再跟据共视冗余度进行剔除,尤其是在回环检测中使用了以关键帧为代表的帧“簇”的概念,回环筛选中有一步将关键帧前后10帧为一组,计算组内总分,以最高分的组的0.75为阈值,滤除一些组,再在剩下的组内各自找最高分的一帧作为备选帧,这个方法非常好地诠释了“关键帧代表局部”的这个理念。

2.3.4 关键帧的类型及更新连接关系

父子关键帧

//KeyFrame.h 文件中
bool mbFirstConnection; // 是否是第一次生成树
KeyFrame* mpParent; // 当前关键帧的父关键帧 (共视程度最高的)
std::set<KeyFrame*> mspChildrens; // 存储当前关键帧的子关键帧

更新连接关系

//KeyFrame.cc
KeyFrame::UpdateConnections()
{
//省略...
// Step 5 更新生成树的连接
if(mbFirstConnection && mnId!=0)
{
// 初始化该关键帧的父关键帧为共视程度最高的那个关键帧
mpParent = mvpOrderedConnectedKeyFrames.front();
// 建立双向连接关系,将当前关键帧作为其子关键帧
mpParent->AddChild(this);
mbFirstConnection = false;
}
}
// 添加子关键帧(即和子关键帧具有最大共视关系的关键帧就是当前关键帧)
void KeyFrame::AddChild(KeyFrame *pKF)
{
unique_lock<mutex> lockCon(mMutexConnections);
mspChildrens.insert(pKF);
}
// 删除某个子关键帧
void KeyFrame::EraseChild(KeyFrame *pKF)
{
unique_lock<mutex> lockCon(mMutexConnections);
mspChildrens.erase(pKF);
}
// 改变当前关键帧的父关键帧
void KeyFrame::ChangeParent(KeyFrame *pKF)
{
unique_lock<mutex> lockCon(mMutexConnections);
// 添加双向连接关系
mpParent = pKF;
pKF->AddChild(this);
}
//获取当前关键帧的子关键帧
set<KeyFrame*> KeyFrame::GetChilds()
{
unique_lock<mutex> lockCon(mMutexConnections);
return mspChildrens;
}
//获取当前关键帧的父关键帧
KeyFrame* KeyFrame::GetParent()
{
unique_lock<mutex> lockCon(mMutexConnections);
return mpParent;
}
// 判断某个关键帧是否是当前关键帧的子关键帧
bool KeyFrame::hasChild(KeyFrame *pKF)
{
unique_lock<mutex> lockCon(mMutexConnections);
return mspChildrens.count(pKF);
}

更新局部关键帧

void Tracking::UpdateLocalKeyFrames()
{
//省略...
// 策略2.2:将自己的子关键帧作为局部关键帧(将邻居的子孙们拉拢入伙)
const set<KeyFrame*> spChilds = pKF->GetChilds();
for(set<KeyFrame*>::const_iterator sit=spChilds.begin(), send=spChilds.end(); sit!=send; sit++)
{
KeyFrame* pChildKF = *sit;
if(!pChildKF->isBad())
{
if(pChildKF->mnTrackReferenceForFrame!=mCurrentFrame.mnId)
{
mvpLocalKeyFrames.push_back(pChildKF);
pChildKF->mnTrackReferenceForFrame=mCurrentFrame.mnId;
//? 找到一个就直接跳出for循环?
break;
}
}
}
// 策略2.3:自己的父关键帧(将邻居的父母们拉拢入伙)
KeyFrame* pParent = pKF->GetParent();
if(pParent)
{
// mnTrackReferenceForFrame防止重复添加局部关键帧
if(pParent->mnTrackReferenceForFrame!=mCurrentFrame.mnId)
{
mvpLocalKeyFrames.push_back(pParent);
pParent->mnTrackReferenceForFrame=mCurrentFrame.mnId;
//! 感觉是个bug!如果找到父关键帧会直接跳出整个循环
break;
}
}
// 省略....
}

3、 共视图 本质图 拓展树

在这里插入图片描述在这里插入图片描述

3. 1.共视图 (Covisibility Graph)

共视图是无向加权图,每个节点是关键帧,如果两个关键帧之间满足一定的共视关系(至少15个共同观测地图点)他们就连成一条边,边的权重就是共视地图点数目
在这里插入图片描述
共视图的作用:

  1. 跟踪局部地图,扩大搜索范围
    Tracking:UpdateLocalKeyFrames()

  2. 局部建图里关键帧之间新建地图点
    LocalMapping::CreateNewMapPoints()
    LocalMapping:SearchlnNeighbors()

  3. 闭环检测、重定位检测
    LoopClosing::DetectLoop()、 LoopClosing:CorrectLoop()
    KeyFrameDatabase::DetectLoopCandidates
    KeyFrameDatabase::DetectRelocalizationCandidates

  4. 优化
    Optimizer::OptimizeEssentialGraph

3.2.本质图(Essential Graph)

本质图与共视图的区别,共视图相对与本质图比较稠密,本质图比共视图更稀疏,这是因为本质图的作用是用在闭环矫正时,用相似变换来矫正尺度漂移,把闭环误差均摊在本质图中。本质图中节点也是所有关键帧,但是连接边更少,只保留了联系紧密的边来使得结果更精确。 可以理解为本质图在共视图的基础上之保留了最强关联(最高共视关系的连接,我们认为这种情况下,信任度最高。相对与共视图的优化(直接全局BA)结果看来,最强的共视关系优化最可靠)

本质图中包含:

  1. 生成树(spanning tree)连接关系
  2. 形成闭环的连接关系,闭环后地图点变动后新增加的连接关系
  3. 共视关系非常好(至少100个共视地图点)的连接关系

本质图优化

//Optimizer.cc
Optimizer::OptimizeEssentialGraph()
{
// 省略....
// Spanning tree edge
// Step 4.1:添加生成树的边(有父关键帧)
// 父关键帧就是和当前帧共视程度最高的关键帧
if(pParentKF)
{
int nIDj = pParentKF->mnId;
g2o::Sim3 Sjw;
LoopClosing::KeyFrameAndPose::const_iterator itj = NonCorrectedSim3.find(pParentKF);
// 尽可能得到未经过Sim3传播调整的位姿
if(itj!=NonCorrectedSim3.end())
Sjw = itj->second;
else
Sjw = vScw[nIDj];
// 计算父子关键帧之间的相对位姿
g2o::Sim3 Sji = Sjw * Swi;
g2o::EdgeSim3* e = new g2o::EdgeSim3();
e->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(nIDj)));
e->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(nIDi)));
// 希望父子关键帧之间的位姿差最小
e->setMeasurement(Sji);
// 所有元素的贡献都一样;每个误差边对总误差的贡献也都相同
e->information() = matLambda;
optimizer.addEdge(e);
}
// 省略....
}

本质图优化和全局BA结果对比
从结果来看,

  1. 全局BA存在收敛问题。即使迭代100次,相对均方误差RMSE 也比较高
  2. essential graph 优化可以快速收敛并且结果更精确。θmin 表示被选为essential graph至少需要的共视地图点数目,从结果来看,θmin的大小对精度影响不大,但是较大的θmin值可以显著减少运行时间
  3. essential graph 优化 后增加全局 full BA 可以提升精度(但比较有限),但是会耗时较多

在这里插入图片描述

在这里插入图片描述

3.3 生成树(spanning tree)

子关键帧和父关键帧构成
在这里插入图片描述

参考资料:
ORB-SLAM论文翻译
https://blog.csdn.net/electech6/article/details/108425313

视频 录像
https://www.bilibili.com/video/BV1e54y1278y
ORB-SLAM源码讲解专题五:理解共视图、本质图、扩展树
视觉SLAM总结——ORB SLAM2中关键知识点总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大江东去浪淘尽千古风流人物

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值