书接上回,介绍完了跟踪线程,已经得到了当前帧相机的位姿,并且当判断需要产生关键帧的时候,tracking
线程把新创建的关键帧插入到mpLocalMapper
这个线程的mlNewKeyFrames
容器中。所以这时候局部线程就根据这个新的关键帧来进行局部建图的操作。
局部建图的主要程序
// 主循环
while(1)
{
// Step 1 告诉Tracking,LocalMapping正处于繁忙状态,请不要给我发送关键帧打扰我
// LocalMapping线程处理的关键帧都是Tracking线程发来的
SetAcceptKeyFrames(false);
// 等待处理的关键帧列表不为空
if(CheckNewKeyFrames()){
// Step 2 处理列表中的关键帧,包括计算BoW、更新观测、描述子、共视图,插入到地图等
ProcessNewKeyFrame();
// Step 3 根据地图点的观测情况剔除质量不好的地图点
MapPointCulling();
// Step 4 当前关键帧与相邻关键帧通过三角化产生新的地图点,使得跟踪更稳
CreateNewMapPoints();
// 已经处理完队列中的最后的一个关键帧
if(!CheckNewKeyFrames()){
// Step 5 检查并融合当前关键帧与相邻关键帧帧(两级相邻)中重复的地图点
SearchInNeighbors();
}
// 终止BA的标志
mbAbortBA = false;
// 已经处理完队列中的最后的一个关键帧,并且闭环检测没有请求停止LocalMapping
if(!CheckNewKeyFrames() && !stopRequested())
{
// Step 6 当局部地图中的关键帧大于2个的时候进行局部地图的BA
if(mpMap->KeyFramesInMap()>2)
// 注意这里的第二个参数是按地址传递的,当这里的 mbAbortBA 状态发生变化时,能够及时执行/停止BA
Optimizer::LocalBundleAdjustment(mpCurrentKeyFrame,&mbAbortBA, mpMap);
// Step 7 检测并剔除当前帧相邻的关键帧中冗余的关键帧
// 冗余的判定:该关键帧的90%的地图点可以被其它关键帧观测到
KeyFrameCulling();
}
// Step 8 将当前帧加入到闭环检测队列中
// 注意这里的关键帧被设置成为了bad的情况,这个需要注意
mpLoopCloser->InsertKeyFrame(mpCurrentKeyFrame);
}
cvlife的注释版删掉一些冗余的解释后,主要就是这么一个流程,后续就逐个流程介绍。
step1,SetAcceptKeyFrames(false)
告诉Tracking,LocalMapping正处于繁忙状态,请不要给我发送关键帧打扰我。其实就是设置标志位让其他线程可以知道当前线程的信息(记得需要加互斥锁)。
step2,ProcessNewKeyFrame()
当mlNewKeyFrames
容器不为空(即有新的关键帧产生),就需要处理容器中的新关键帧。
- 计算新关键帧特征点的词袋向量。(老朋友了,在跟踪那一章对词袋解释的比较详细)
- 处理新关键帧中有效的地图点。①如果地图点不是来自当前帧的观测(比如来自局部地图点),为当前地图点添加观测(addobservation函数,主要可以通过这个函数知道地图点都被哪些关键帧观测到了,能够知道共视关系)。②更新地图点的平均观测方向(地图点与相机光心的夹角)和观测距离范围(观测距离的预测与金字塔层级有关,具体可以参考这篇博客:【ORB-SLAM2】MapPoint::PredictScale()小记)。③更新地图点的最佳描述子。
- 更新关键帧之间的连接关系。主要就是根据共视地图点数量对关键帧排序,更新新关键帧与其他关键帧的共视关系。
- 将新关键帧插入到全局地图中。
step3,MapPointCulling()
这个函数的主要作用就是提出质量不好的新增地图点。新增的地图点有两种:第一种是如果双目或者RGBD相机会在创建关键帧中新生成的地图点。第二种是后续三角化中产生的新地图点。
这个函数的过程如下:
- 根据相机类型设置不同的观测阈值。
cnThObs
- 遍历新增的地图点,并且检查好坏。检测条件:①如果已经是坏点就直接不要。②跟踪到该地图点的帧数比应该见到该地图点的帧数比例小于25%(就是在跟踪过程中(mnFound/mnVisible) < 25%这个比例)。③从该点建立开始,到现在已经过了不小于2个关键帧,但是观测到该点的相机数却不超过阈值
cnThObs
,所以删除。④从建立该点开始,已经过了3个关键帧而没有被剔除,则认为是质量高的点,所以不删除。
step4,CreateNewMapPoints()
这个函数主要是通过当前关键帧与相邻关键帧通过三角化产生新的地图点,这个是单目非常重要的函数,因为就是需要通过这样三角化不断产生统一尺度的地图3d点,才能在后续跟踪过程直接通过pnp求解出统一个尺度下的连续位姿(虽然这个尺度在slam长时间运行时会有尺度漂移,但是可以通过回环检测加sim3匹配来优化)。
这个函数的过程如下:
- 在当前关键帧的共视关键帧中找到共视程度最高的前10/20个相邻关键帧。(就是根据共视地图点数量的顺序来提取)
- 遍历相邻关键帧,首先判断两个关键帧之间的baseline。在双目的情况下,如果这个baseline小于双目相机自身的基线,则不进行三角化,因为基线短的时候恢复的深度误差大。在单目情况下则是检查这个baseline与当前关键帧地图点的深度中值的比值,如果太小同样也不进行三角化。
- 如果上述情况都满足,后续根据两个关键帧的位姿计算它们之间的基础矩阵(跟初始化的时候是反过来,初始化的时候是先通过匹配点对求解一个线性方程组,得到基础矩阵)。
- 通过词袋模型对两个关键帧未匹配的特征点快速匹配,用极线约束抑制离群点(极线约束就是依靠基础矩阵将特征点进行投影,然后根据点到直线距离与卡方分布联合抑制离群点),并且生成新的匹配点对
- 对每对匹配通过三角化生成3D点,和 Triangulate函数差不多。
- 如果三角化3d点成功,就构造
MapPoint
,并且添加地图点的各种属性(添加地图点的observation,计算最佳描述子,更新观测方向和深度等等),最终地图点加入mlpRecentAddedMapPoints
容器中,并且需要通过MapPointCulling()
来检测这个点的质量。
step5,SearchInNeighbors()
当已经处理完队列中的最后的一个关键帧,就会调用这个函数,检查并融合当前关键帧与相邻关键帧帧(两级相邻)中重复的地图点。注意一些命名规则:当前关键帧的邻接关键帧,称为一级相邻关键帧,也就是邻居,与一级相邻关键帧相邻的关键帧,称为二级相邻关键帧,也就是邻居的邻居。
这个函数的过程如下:
- 获得当前关键帧在共视图中权重排名前10/20的邻接关键帧(一级相邻关键帧)
- 获取二级相邻关键帧
- 将当前帧的地图点分别投影到两级相邻关键帧,寻找匹配点对应的地图点进行融合,称为正向投影融合。融合策略如下:①如果地图点能匹配关键帧的特征点,并且该点有对应的地图点,那么选择观测数目多的替换两个地图点。②如果地图点能匹配关键帧的特征点,并且该点没有对应的地图点,那么为该点添加该投影地图点
- 将两级相邻关键帧地图点分别投影到当前关键帧,寻找匹配点对应的地图点进行融合,称为反向投影融合。策略都是一样的,调用的同一个函数。
- 更新当前帧地图点的描述子、深度、平均观测方向等属性
- 更新当前帧与其它帧的共视连接关系
step6,Optimizer::LocalBundleAdjustment()
新的关键帧通过上面的函数都处理好了,后续就是进行局部BA,当全局地图的关键帧数量大于2就可以开始执行局部BA的优化。这里的优化就不是只优化当前关键帧的位姿了,而是将当前关键帧局部的关键帧及其地图点加到图中进行优化。
这个函数的过程如下:
- 将当前帧和当前帧的共视帧加入局部关键帧的容器中(其实就是当前帧与当前帧的一级相连关键帧)。
- 遍历局部关键帧,将这些关键帧的地图点加入到局部地图点的容器。
- 得到能够观测到局部地图点,但不属于局部关键帧的关键帧(二级相连),这些二级相连关键帧在局部BA优化时不优化(这些关键帧属于fixed keyframes)
- 通过g2o构造这个局部BA优化。注意的是局部关键帧的位姿和局部地图点的位置都是优化对象(不固定),而二级相连的关键帧负责提供约束(所以它们的位姿是固定的)。还需要注意的一点:这个局部BA会假设局部的尺度是一致的,所以优化的局部关键帧位姿都是SE3,与全局BA相反,因为全局BA优化的关键帧位姿都是Sim3(全局的范围更大,必须考虑尺度漂移)。
- 第一阶段优化后,找到误差不符合卡方分布的边,设置为外点,后续不进行优化。排除了外点后继续进行第二阶段的优化。在优化后重新计算误差,剔除连接误差比较大的关键帧和地图点(只是删除了它们之间的observation关系)。最终更新关键帧的位姿和地图点的位置及其属性(平均观测方向和深度)。
step7,KeyFrameCulling()
这个函数主要是提取当前关键帧在共视图中的关键帧,根据地图点在共视图中的冗余程度剔除该共视关键帧(冗余关键帧的判定:90%以上的地图点能被其他关键帧(至少3个)观测到)。函数的逻辑也比较简单,这里就不展开了。
step8
将当前关键帧加入到闭环检测的队列mlpLoopKeyFrameQueue
中,这个的逻辑一般都是新产生了一个关键帧都会加入到这个队列中,如果这个关键帧是冗余的话会被设置为bad,回环的时候会检测bad标志,所以不用担心。
总结
这个localmapping局部建图线程还是比较简单的,就是处理关键帧,处理完毕就执行一个局部BA的优化。最后给回环线程留了一个队列,用于检测是否有回环。