文章目录
一、函数的意义
本函数是第一层跟踪中三大跟踪方式其中之一的--重定位跟踪,这个跟踪方式是最复杂的,也是最耗时的跟踪方式,同时也是跟踪强度最高的跟踪方式,在其他两种方式都失效时,重定位跟踪用来兜底,一点重定位失败意味着,线程只能reset了,所以其重要程度不言而喻。
二、函数讲解
本函数的步骤如下所示,本函数是一个带有返回值的函数,重定位成功返回true,失败则false。首先我们要明白的是,什么是跟踪,其实跟踪就在一个新的帧来临的时候,我们要用各种方法找到这个帧和其他帧的匹配点以及这个新帧的位姿(重点),重定位跟踪,就是根据上一帧和参考关键帧已经没办法找到自身的位置了,这时候就需要重定位。所以我们首要任务是找到一帧可以让本帧找到自己的位置,因为关键帧是帧中的精品,所以我们考虑找到一个关键帧来帮助本帧找到自己的位姿,这里我们用了一个寻找候选关键帧的函数进行搜索候选关键帧,会在后面的函数调用中讲到。找到后遍历候选关键帧,进行第一特征匹配,这次匹配用的是词袋的方式快速匹配,目的是快速的筛查出哪些明显就不符合要求的候选关键帧,符合的要求的关键帧匹配结果来初始化位姿求解器pSolver。下一步就是进行第二次的特征匹配,这里使用的是重投影法(精匹配)对未匹配的点进行匹配,重投影法如下图所示,目的是增加匹配点的数量,至于为什么要增加匹配点的数量,是因为,在粗匹配后,尽心了一次位姿的优化,这个优化只优化可相机的位姿,没有对地图点位置进行优化,但是删除了一些匹配的地图点(不合格),删除之后地图点数量可能会不够,所以需要进行第二次的精匹配。这里有一个很有趣的点,就是代码中宁愿用两次相同的优化来排除外点,也不愿意多使用一点特征匹配(第二次特征匹配只有在内点数量小于50的时候才启动),这个其实是一个可以优化的点,事实上优化的时间(500ms左右)是远大于特征匹配的,所以完全可以先提取特征点,然后再进行优化。最后一点就是,只要找到一个关键帧就可以停止,宣布重定位成功!因为我们的目的是找到一个关键帧来确定当前帧的位姿,而不是找到匹配点最多的那个关键帧,所以找到一个能确定位姿的关键帧就够了。
- Step 1:计算当前帧特征点的词袋向量
- Step 2:找到与当前帧相似的候选关键帧
- Step 3:通过BoW进行匹配
- Step 4:通过EPnP算法估计姿态
- Step 5:通过PoseOptimization对姿态进行优化求解
- Step 6:如果内点较少,则通过投影的方式对之前未匹配的点进行匹配,再进行优化求解
三、函数代码
cpp
/**
* @details 重定位过程
*/
bool Tracking::Relocalization()
{
// Compute Bag of Words Vector
// Step 1:计算当前帧特征点的词袋向量
mCurrentFrame.ComputeBoW();
// Relocalization is performed when tracking is lost
// Track Lost: Query KeyFrame Database for keyframe candidates for relocalisation
// Step 2:用词袋找到与当前帧相似的候选关键帧
vector<KeyFrame*> vpCandidateKFs = mpKeyFrameDB->DetectRelocalizationCandidates(&mCurrentFrame);
// 如果候选关键帧数量为0,则返回false,表示重定位失败
if(vpCandidateKFs.empty())
return false;
// 获取候选帧的个数
const int nKFs = vpCandidateKFs.size();
// We perform first an ORB matching with each candidate
// If enough matches are found we setup a PnP solver
// 设置特征匹配器的参数
ORBmatcher matcher(0.75,true);
// 创建一个计算相机位姿求解器指针的容器
vector<PnPsolver*> vpPnPsolvers;
vpPnPsolvers.resize(nKFs);
// 创建一个装匹配的地图点的二维容器
vector<vector<MapPoint*> > vvpMapPointMatches;
vvpMapPointMatches.resize(nKFs);
// 创建放弃某个关键帧的标记
vector<bool> vbDiscarded;
vbDiscarded.resize(nKFs);
// 初始化有效的候选关键帧数目
int nCandidates=0;
// Step 3:遍历所有的候选关键帧,通过词袋进行快速匹配,用匹配结果初始化PnP Solver
for(int i=0; i<nKFs; i++)
{
// 获取第i个相似关键帧
KeyFrame* pKF = vpCandidateKFs[i];
// 如果这个关键帧被定义为坏,则给他赋予放弃标志(vbDiscarded[i] = true;)
if(pKF->isBad())
vbDiscarded[i] = true;
// 如果没有被定义为坏,通过词袋进行快速匹配
else
{
// 当前帧和候选关键帧用BoW进行快速匹配,匹配结果记录在vvpMapPointMatches,nmatches表示匹配的数目
int nmatches = matcher.SearchByBoW(pKF,mCurrentFrame,vvpMapPointMatches[i]);
// 如果匹配数目小于15,则则给他赋予放弃标志,并进入下一次循环
if(nmatches<15)
{
vbDiscarded[i] = true;
continue;
}
// 如果大于15,则创建一个位姿求解器,求解当前帧相机位姿
else
{
// 如果匹配数目够用,用匹配结果初始化EPnPsolver
PnPsolver* pSolver = new PnPsolver(mCurrentFrame,vvpMapPointMatches[i]);
// 设置RANSAC迭代的参数
pSolver->SetRansacParameters(0.99,10,300,4,0.5,5.991);
// 获取位姿提取器
vpPnPsolvers[i] = pSolver;
// 有效关键帧+1
nCandidates++;
}
}
}
// Alternatively perform some iterations of P4P RANSAC
// Until we found a camera pose supported by enough inliers
bool bMatch = false;
// 设置特征匹配器2的参数
ORBmatcher matcher2(0.9,true);
// Step 4: 通过一系列操作,直到找到能够匹配上的关键帧
// 为什么搞这么复杂?答:是担心误闭环
while(nCandidates>0 && !bMatch)
{
// 遍历每个关键帧
for(int i=0; i<nKFs; i++)
{
// 如果该关键帧被标记为放弃,则进入下一次循环
if(vbDiscarded[i])
continue;
// Perform 5 Ransac Iterations
// 内点标记
vector<bool> vbInliers;
// 内点数
int nInliers;
bool bNoMore;
// Step 4.1:通过EPnP算法估计姿态,迭代5次
PnPsolver* pSolver = vpPnPsolvers[i];
cv::Mat Tcw = pSolver->iterate(5,bNoMore,vbInliers,nInliers);
// If Ransac reachs max. iterations discard keyframe
// bNoMore 为true 表示已经超过了RANSAC最大迭代次数,就放弃当前关键帧
if(bNoMore)
{
vbDiscarded[i]=true;
nCandidates--;
}
// If a Camera Pose is computed, optimize
// Step 4.2:如果EPnP 计算出了位姿,对内点进行BA优化
if(!Tcw.empty())
{
// 拷贝当前帧位姿
Tcw.copyTo(mCurrentFrame.mTcw);
// EPnP 里RANSAC后的内点的集合
set<MapPoint*> sFound;
// 获取内点的数量
const int np = vbInliers.size();
// 遍历每个内点
for(int j=0; j<np; j++)
{
// 记录标记为true 的删除标记为false的
if(vbInliers[j])
{
mCurrentFrame.mvpMapPoints[j]=vvpMapPointMatches[i][j];
sFound.insert(vvpMapPointMatches[i][j]);
}
else
mCurrentFrame.mvpMapPoints[j]=NULL;
}
// 只优化位姿,不优化地图点的坐标,返回的是内点的数量
int nGood = Optimizer::PoseOptimization(&mCurrentFrame);
// 内殿数量不足,则认为重定位失败
if(nGood<10)
continue;
// 删除外点对应的地图点
for(int io =0; io<mCurrentFrame.N; io++)
if(mCurrentFrame.mvbOutlier[io])
mCurrentFrame.mvpMapPoints[io]=static_cast<MapPoint*>(NULL);
// If few inliers, search by projection in a coarse window and optimize again
// Step 4.3:如果内点较少,则通过投影的方式对之前未匹配的点进行匹配,再进行优化求解
if(nGood<50)
{
// 通过投影的方式将关键帧中未匹配的地图点投影到当前帧中, 生成新的匹配
int nadditional =matcher2.SearchByProjection(mCurrentFrame,vpCandidateKFs[i],sFound,10,100);
// 如果通过投影过程新增了比较多的匹配特征点对
if(nadditional+nGood>=50)
{
// 根据投影匹配的结果,再次采用3D-2D pnp BA优化位姿
nGood = Optimizer::PoseOptimization(&mCurrentFrame);
// If many inliers but still not enough, search by projection again in a narrower window
// the camera has been already optimized with many points
// Step 4.4:如果BA后内点数还是比较少(<50)但是还不至于太少(>30),可以挽救一下, 最后垂死挣扎
if(nGood>30 && nGood<50)
{
// 用更小窗口、更严格的描述子阈值,重新进行投影搜索匹配
// 清空内点
sFound.clear();
// 遍历特征点,记录本帧两次匹配的所有地图点
for(int ip =0; ip<mCurrentFrame.N; ip++)
if(mCurrentFrame.mvpMapPoints[ip])
sFound.insert(mCurrentFrame.mvpMapPoints[ip]);
// 用更小窗口、更严格的描述子阈值,重新进行投影搜索匹配
// ?很疑惑为什么
nadditional =matcher2.SearchByProjection(mCurrentFrame,vpCandidateKFs[i],sFound,3,64);
// Final optimization
// 如果成功挽救回来,匹配数目达到要求,最后BA优化一下
if(nGood+nadditional>=50)
{
nGood = Optimizer::PoseOptimization(&mCurrentFrame);
for(int io =0; io<mCurrentFrame.N; io++)
if(mCurrentFrame.mvbOutlier[io])
mCurrentFrame.mvpMapPoints[io]=NULL;
}
}
}
}
// If the pose is supported by enough inliers stop ransacs and continue
// 如果对于当前的候选关键帧已经有足够的内点(50个)了,那么就认为重定位成功
if(nGood>=50)
{
bMatch = true;
break;
}
}
}
}
// 折腾了这么久还是没有匹配上,重定位失败
if(!bMatch)
{
return false;
}
else
{
// 如果匹配上了,说明当前帧重定位成功了(当前帧已经有了自己的位姿)
// 记录成功重定位帧的id,防止短时间多次重定位
mnLastRelocFrameId = mCurrentFrame.mnId;
return true;
}
}
四、调用的函数
这里不再讲解通过词袋进行特征匹配和本函数中涉及到的优化函数,因为这些函数已经在参考关键帧跟踪和恒速跟踪模型中详细讲到,有问题的话可以点击查看。这里重点讲获取候选关键帧函数和重定位特征匹配。
1. KeyFrameDatabase::DetectRelocalizationCandidates()
1).函数讲解
这个函数分为如下四个步骤。首先冲关键帧列表中,找出与当前帧有相同词袋的关键帧当作初始候选关键帧,然后经过两轮筛选,找出最终的关键帧。第一轮是计算词袋的相似度,找出最高的相似度得分,以其0.8倍的值当作阈值,进行第一步筛选,小于阈值的关键帧排除;第二轮将上一轮的关键帧分别找到其共视关系最好的10个关键帧作为一组,计算累计得分,并提取每组的最高分的关键帧作为第二轮的候选关键帧,计算最高得分的0.75倍作为阈值,小于阈值的关键帧排除。剩下的关键帧为候选关键这,并将储存这些候选关键帧的容器当作返回值返回。
- Step 1. 找出和当前帧具有公共单词的所有关键帧
- Step 2. 只和具有共同单词较多的关键帧进行相似度计算
- Step 3. 将与关键帧相连(权值最高)的前十个关键帧归为一组,计算累计得分
- Step 4. 只返回累计得分较高的组中分数最高的关键帧
2).函数代码
cpp
/*
* @brief 在重定位中找到与该帧相似的候选关键帧组
* Step 1. 找出和当前帧具有公共单词的所有关键帧
* Step 2. 只和具有共同单词较多的关键帧进行相似度计算
* Step 3. 将与关键帧相连(权值最高)的前十个关键帧归为一组,计算累计得分
* Step 4. 只返回累计得分较高的组中分数最高的关键帧
* @param F 需要重定位的帧
* @return 相似的候选关键帧数组
*/
vector<KeyFrame*> KeyFrameDatabase::DetectRelocalizationCandidates(Frame *F)
{
// 创建共享词汇的列表
list<KeyFrame*> lKFsSharingWords;
// Search all keyframes that share a word with current frame
// Step 1:找出和当前帧具有公共单词(word)的所有关键帧
{
unique_lock<mutex> lock(mMutex);
// mBowVec 内部实际存储的是std::map<WordId, ordValueW>
// WordId 和 WordValue 表示Word在叶子中的id 和权重
// 遍历当前帧的所有单词
for(DBoW2::BowVector::const_iterator vit=F->mBowVec.begin(), vend=F->mBowVec.end(); vit != vend; vit++)
{
// 根据倒排索引,提取所有包含该wordid的所有KeyFrame
list<KeyFrame*> &lKFs = mvInvertedFile[vit->first];
// 遍历这些关键帧
for(list<KeyFrame*>::iterator lit=lKFs.begin(), lend= lKFs.end(); lit!=lend; lit++)
{
// 获取关键帧的指针
KeyFrame* pKFi=*lit;
// pKFi->mnRelocQuery起标记作用,是为了防止重复选取
if(pKFi->mnRelocQuery!=F->mnId)
{
// pKFi 的共享单词计数初始化为 0
pKFi->mnRelocWords=0;
// 将关键帧 pKFi 的重定位查询标记 mnRelocQuery 设置为当前帧 F 的 ID
pKFi->mnRelocQuery=F->mnId;
// 将关键帧 pKFi 添加到共享单词的候选关键帧列表 lKFsSharingWords
lKFsSharingWords.push_back(pKFi);
}
// 公共词汇数+1
pKFi->mnRelocWords++;
}
}
}
// 如果和当前帧具有公共单词的关键帧数目为0,无法进行重定位,返回空
if(lKFsSharingWords.empty())
return vector<KeyFrame*>();
// Only compare against those keyframes that share enough words
// Step 2:统计上述关键帧中与当前帧F具有共同单词最多的单词数maxCommonWords,用来设定阈值1
// 初始化最大的公共词汇数量
int maxCommonWords=0;
// 遍历共享词汇的帧
for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(), lend= lKFsSharingWords.end(); lit!=lend; lit++)
{
// 更新最大公共词汇数
if((*lit)->mnRelocWords>maxCommonWords)
maxCommonWords=(*lit)->mnRelocWords;
}
// 最小词汇数为最大词汇数的0.8倍(用于当筛选时的阈值)
int minCommonWords = maxCommonWords*0.8f;
// 创建一个列表,用于储存关键帧的相似度分数和对应的关键帧指针
list<pair<float,KeyFrame*> > lScoreAndMatch;
// 定义了一个计数器 nscores,用于记录参与相似度分数计算的关键帧数量
int nscores=0;
// Compute similarity score.
// Step 3:遍历上述关键帧,挑选出共有单词数大于阈值的关键帧
// 遍历这个列表
for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(), lend= lKFsSharingWords.end(); lit!=lend; lit++)
{
// 获取指针
KeyFrame* pKFi = *lit;
// 保留在最小阈值以上的关键帧,并存入得分匹配的列表中
if(pKFi->mnRelocWords>minCommonWords)
{
nscores++;
float si = mpVoc->score(F->mBowVec,pKFi->mBowVec);
pKFi->mRelocScore=si;
lScoreAndMatch.push_back(make_pair(si,pKFi));
}
}
// 如果没有这样的帧,重定位失败,返回空容器
if(lScoreAndMatch.empty())
return vector<KeyFrame*>();
// 创建一个累计积分的列表,第一个元素存储累积分数accScore,第二个元素储存对应关键帧的指针
list<pair<float,KeyFrame*> > lAccScoreAndMatch;
// 初始化最大累计积分
float bestAccScore = 0;
// Lets now accumulate score by covisibility
// Step 4:计算lScoreAndMatch中每个关键帧的共视关键帧组的总得分,得到最高组得分bestAccScore
// 遍历相似度得分列表容器
for(list<pair<float,KeyFrame*> >::iterator it=lScoreAndMatch.begin(), itend=lScoreAndMatch.end(); it!=itend; it++)
{
// 获取关键帧指针
KeyFrame* pKFi = it->second;
// 取出与关键帧pKFi共视程度最高的前10个关键帧
vector<KeyFrame*> vpNeighs = pKFi->GetBestCovisibilityKeyFrames(10);
// 初始化该组最高分数
float bestScore = it->first;
// 初始化该组累计得分
float accScore = bestScore;
// 初始化该组最高分数对应的关键帧
KeyFrame* pBestKF = pKFi;
// 遍历共视关键帧
for(vector<KeyFrame*>::iterator vit=vpNeighs.begin(), vend=vpNeighs.end(); vit!=vend; vit++)
{
// 获取指针
KeyFrame* pKF2 = *vit;
// 排除没有公共词汇的帧
if(pKF2->mnRelocQuery!=F->mnId)
continue;
// 只有pKF2也有公共词汇,才能贡献分数
accScore+=pKF2->mRelocScore;
// 统计得到组里分数最高的KeyFrame,并将最大得分关键帧跟新
if(pKF2->mRelocScore>bestScore)
{
pBestKF=pKF2;
bestScore = pKF2->mRelocScore;
}
}
// 将每组最高得分的关键帧加入到累计积分关键帧列表里面
lAccScoreAndMatch.push_back(make_pair(accScore,pBestKF));
// 更新所有组中的最高得分
if(accScore>bestAccScore)
bestAccScore=accScore;
}
// Return all those keyframes with a score higher than 0.75*bestScore
// Step 5:得到所有组中总得分大于阈值,组内得分最高的关键帧,作为候选关键帧组
// 最小得分为最大得分的0.75倍,用于筛选候选关键帧的阈值
float minScoreToRetain = 0.75f*bestAccScore;
set<KeyFrame*> spAlreadyAddedKF;
// 创建并初始化候选关键帧容器
vector<KeyFrame*> vpRelocCandidates;
vpRelocCandidates.reserve(lAccScoreAndMatch.size());
// 遍累计积分关键帧容器
for(list<pair<float,KeyFrame*> >::iterator it=lAccScoreAndMatch.begin(), itend=lAccScoreAndMatch.end(); it!=itend; it++)
{
// 引用得分
const float &si = it->first;
// 得分大于阈值才能计入候选关键帧
if(si>minScoreToRetain)
{
// 获取关键帧指针
KeyFrame* pKFi = it->second;
// // 判断该pKFi是否已经添加在队列中了,没有就添加进去
if(!spAlreadyAddedKF.count(pKFi))
{
vpRelocCandidates.push_back(pKFi);
spAlreadyAddedKF.insert(pKFi);
}
}
}
// 返回候选关键帧列表
return vpRelocCandidates;
}
2. ORBmatcher::SearchByProjection()
1).函数讲解
本函数分为以下五个步骤。本函数是一个具有返回值的函数,返回值为匹配的点的数量。这个函数和我们在恒速跟踪模型中看到的特征匹配很相似,不一样的点在于,那个匹配时特征点与特征点之间的匹配,这个函数的匹配是地图点与特征点的匹配,这里是将地图点投影到本帧当中来,然后以投影的(x,y)坐标为中心,以一个定值为半径来搜索匹配的点,这里同样在最后用直方图来筛选明显匹配错误的点。
- Step 1 遍历有效的局部地图点
- Step 2 设定搜索搜索窗口的大小。取决于视角, 若当前视角和平均视角夹角较小时, r取一个较小的值
- Step 3 通过投影点以及搜索窗口和预测的尺度进行搜索, 找出搜索半径内的候选匹配点索引
- Step 4 寻找候选匹配点中的最佳和次佳匹配点
- Step 5 筛选最佳匹配点
2).函数代码
cpp
/**
* @brief 通过投影地图点到当前帧,对Local MapPoint进行跟踪
* @param[in] F 当前帧
* @param[in] vpMapPoints 局部地图点,来自局部关键帧
* @param[in] th 搜索范围
* @return int 成功匹配的数目
*/
int ORBmatcher::SearchByProjection(Frame &F, const vector<MapPoint*> &vpMapPoints, const float th)
{
// 初始化匹配数量
int nmatches=0;
// 如果 th!=1 (RGBD 相机或者刚刚进行过重定位), 需要扩大范围搜索
const bool bFactor = th!=1.0;
// 遍历有效的局部地图点
for(size_t iMP=0; iMP<vpMapPoints.size(); iMP++)
{
// 获取地图点指针
MapPoint* pMP = vpMapPoints[iMP];
// 若该点未被观测,则返回
if(!pMP->mbTrackInView)
continue;
// 坏点返回
if(pMP->isBad())
continue;
// 获取地图点的预测尺度层级
const int &nPredictedLevel = pMP->mnTrackScaleLevel;
// The size of the window will depend on the viewing direction
// 根据观察方向计算搜索半径
float r = RadiusByViewingCos(pMP->mTrackViewCos);
// 如果需要扩大范围搜索,则乘以阈值th
if(bFactor)
r*=th;
// 通过投影点以及搜索窗口和预测的尺度进行搜索, 找出搜索半径内的候选匹配点索引
const vector<size_t> vIndices =
F.GetFeaturesInArea(pMP->mTrackProjX,pMP->mTrackProjY,r*F.mvScaleFactors[nPredictedLevel],nPredictedLevel-1,nPredictedLevel);
// 如果这个范围内搜索的特征点数量为0则返回
if(vIndices.empty())
continue;
// 获取该地图点的描述子
const cv::Mat MPdescriptor = pMP->GetDescriptor();
int bestDist=256;
int bestLevel= -1;
int bestDist2=256;
int bestLevel2 = -1;
int bestIdx =-1 ;
// Get best and second matches with near keypoints
// 寻找候选匹配点中的最佳和次佳匹配点
for(vector<size_t>::const_iterator vit=vIndices.begin(), vend=vIndices.end(); vit!=vend; vit++)
{
// 取出这个特征点的索引
const size_t idx = *vit;
// 如果本帧存在这个地图点,且观测数量大于0,则返回
if(F.mvpMapPoints[idx])
if(F.mvpMapPoints[idx]->Observations()>0)
continue;
// 如果右目也观测到了这个特征点
if(F.mvuRight[idx]>0)
{
// 计算在X轴上的投影误差
const float er = fabs(pMP->mTrackProjXR-F.mvuRight[idx]);
// 超过阈值,说明这个点不行,丢掉.
// 这里的阈值定义是以给定的搜索范围r为参考,然后考虑到越近的点(nPredictedLevel越大), 相机运动时对其产生的影响也就越大,
// 因此需要扩大其搜索空间.
// 当给定缩放倍率为1.2的时候, mvScaleFactors 中的数据是: 1 1.2 1.2^2 1.2^3 ...
if(er>r*F.mvScaleFactors[nPredictedLevel])
continue;
}
// 取出描述子
const cv::Mat &d = F.mDescriptors.row(idx);
// 计算地图点和候选投影点的描述子距离
const int dist = DescriptorDistance(MPdescriptor,d);
// 取出最匹配的点,和次匹配的点
if(dist<bestDist)
{
bestDist2=bestDist;
bestDist=dist;
bestLevel2 = bestLevel;
bestLevel = F.mvKeysUn[idx].octave;
bestIdx=idx;
}
else if(dist<bestDist2)
{
bestLevel2 = F.mvKeysUn[idx].octave;
bestDist2=dist;
}
}
// Apply ratio to second match (only if best and second are in the same scale level)
// 最佳距离小于阈值
if(bestDist<=TH_HIGH)
{
// 如果最好的匹配与次佳的距离太接近,且它们处于同一尺度层次,则放弃该匹配
if(bestLevel==bestLevel2 && bestDist>mfNNratio*bestDist2)
continue;
// 保存结果: 为Frame中的特征点增加对应的MapPoint
F.mvpMapPoints[bestIdx]=pMP;
// 匹配的特征点数+1
nmatches++;
}
}
// 然后匹配的特征点数
return nmatches;
}
五、总结
本函数是第一层追踪函数中兜底的那个函数,如果重定位追踪都失败了那就只能reset了,这个函数及其重要,其复杂度是三个追踪中最大的,所以不到万不得已是不会启动的。我们知道追踪的目的主要是找到新来帧的位姿,所以在丢掉位姿时,首要的事情就是找一个参考系,而这样的参考系最好是具有代表性的"关键帧",所有在重定位中,先找一个和本帧具有高度一致性的关键帧成了重定位的关键。本函数基本上就是找到合适的关键帧,然后根据该关键帧找到本帧的位姿。三大追踪在这里就全都结束了,如果有三大追踪之间逻辑关系还没有梳理清楚,欢迎交流,