ORB_SLAM2回环检测
词典是特征点的描述子的集合,属于同一类特征的特征点的描述子组成单词。
在局部建图线程中,处理完一个关键帧后,会将其放入回环检测线程
在使用关键帧数据库搜索候选关键帧组(DetectLoopCandidates)的时候,没有考虑关键帧的连续性。因此在DetectLoop()函数中检测三个连续的闭环候选关键帧,均与当前帧有较高的相似度。
考虑到单目相机的尺度漂移,计算当前帧与候选闭环帧的Sim3变换,而不是T。(1)使用词袋加速算法,找到与当前帧具有足够多的相同单词的候选关键帧集,并初步计算Sim3变换。(2)将候选闭环帧中的路标点投影到当前帧中,通过优化的方法进一步计算Sim3。(3)将闭环帧及其共视帧的路标点投影至当前帧中进行匹配。(4)判断所选闭环帧是否可靠((3)步中获得的匹配数目大于40)。
闭环校正。计算了当前帧和闭环帧的Sim3变换后,去更新当前帧及其共视帧的位姿,以及路标点的坐标。其中涉及了地图点的融合,共视图的更新,本质图的优化。随后建立了一个全局BA优化线程,去优化所有的关键帧位姿及路标点坐标。
回环检测是指:找到与当前帧4相似的关键帧1,这样的话就可以根据1直接估计4的位姿,而不是1–2--3–4。减少了误差传递。对于当前帧4的共视帧6,可以1—4---6来获得,而不是1–2--3–4--6(也就是回环校正,不过在回环校正的时候,还会去校正路标点坐标)。
void LocalMapping::Run()
............................................
mpLoopCloser->InsertKeyFrame(mpCurrentKeyFrame);
检测回环
获得满足连续条件的闭环关键帧,插入到容器mvpEnoughConsistentCandidates中。
虽然使用关键帧数据库,可以获得当前帧的候选闭环关键帧:
vector<KeyFrame*> KeyFrameDatabase::DetectLoopCandidates(KeyFrame* pKF, float minScore)
但这里检测出的关键帧不满足连续性,这里筛选出那些具有连续性的关键帧。产生闭环的关键帧应具有特点:连续的三个关键帧,且三个关键帧间的相似度评分很高。
bool LoopClosing::DetectLoop()
遍历当前帧的共视关键帧
遍历当前帧的共视关键帧(>15个路标点),使用DBow计算两帧间的词袋相似度。
const vector<KeyFrame*> vpConnectedKeyFrames = mpCurrentKF->GetVectorCovisibleKeyFrames();const DBoW2::BowVector &CurrentBowVec = mpCurrentKF->mBowVec;float minScore = 1;for(size_t i=0; i<vpConnectedKeyFrames.size(); i++){KeyFrame* pKF = vpConnectedKeyFrames[i];if(pKF->isBad())continue;const DBoW2::BowVector &BowVec = pKF->mBowVec;// 计算两个关键帧的相似度得分;得分越低,相似度越低float score = mpORBVocabulary->score(CurrentBowVec, BowVec);// 更新最低得分if(score<minScore)minScore = score;}
寻找候选的闭环关键帧
vector<KeyFrame*> vpCandidateKFs = mpKeyFrameDB->DetectLoopCandidates(mpCurrentKF, minScore);
筛选获得闭环帧
(1)组(group): 对于某个关键帧, 其和其具有共视关系的关键帧组成了一个"组";
(2)子候选组(CandidateGroup): 对于某个候选的回环关键帧, 其和其具有共视关系的关键帧组成的一个"组";
(3)连续(Consistent): 不同的组之间如果共同拥有一个及以上的关键帧,那么称这两个组之间具有连续关系;
(4)连续性(Consistency):称之为连续长度可能更合适,表示累计的连续的链的长度:A–B 为1, A–B--C–D 为3等;具体反映在数据类型 ConsistentGroup.second上;
(5)连续组(Consistent group): mvConsistentGroups存储了上次执行回环检测时, 新的被检测出来的具有连续性的多个组的集合.由于组之间的连续关系是个网状结构,因此可能存在 一个组因为和不同的连续组链都具有连续关系,而被添加两次的情况(当然连续性度量是不相同的);
(6)连续组链:自造的称呼,类似于菊花链A–B--C–D这样形成了一条连续组链.对于这个例子中,由于可能E,F都和D有连续关系,因此连续组链会产生分叉;为了简化计算,连续组中将只会保存最后形成连续关系的连续组们。(见下面的连续组的更新)
(7)子连续组: 上面的连续组中的一个组;
(8)连续组的初始值: 在遍历某个候选帧的过程中,如果该子候选组没有能够和任何一个上次的子连续组产生连续关系,那么就将添加自己组为连续组,并且连续性为0;
(9)连续组的更新: 当前次回环检测过程中,所有被检测到和之前的连续组链有连续的关系的组,都将在对应的连续组链后面+1,这些子候选组(可能有重复,见上)都将会成为新的连续组; 换而言之连续组mvConsistentGroups中只保存连续组链中末尾的组
(1)ConsistentGroup:连续组遍历,类型为pair<set<KeyFrame*>,int>,其中set<KeyFrame*>为连续组中的关键帧,int为连续组的长度。
(2)spCandidateGroup:每个闭环关键帧的共视关键帧集。
遍历每一个候选闭环关键帧
(1)找到当前候选闭环关键帧的共视关键帧(构成一个子候选组)
set<KeyFrame*> spCandidateGroup = pCandidateKF->GetConnectedKeyFrames();
(2)遍历上一次闭环检测到的子连续组
for(size_t iG=0, iendG=mvConsistentGroups.size(); iG<iendG; iG++)
(3) 遍历子候选组,判断子候选组中的关键帧是否存在于子连续组中,如果存在,结束循环
for(set<KeyFrame*>::iterator sit=spCandidateGroup.begin(), send=spCandidateGroup.end(); sit!=send;sit++){if(sPreviousGroup.count(*sit)){// 如果存在,该“子候选组”与该“子连续组”相连bConsistent=true;// 该“子候选组”至少与一个”子连续组“相连,跳出循环bConsistentForSomeGroup=true;break;}}
(4)将该子候选组插入到子连续组中,如果子连续组的长度满足要求,则将子候选关键帧插入候选闭环容器中
(5)如果子候选组中的关键帧在所有的子连续组中都找不到,则创建一个新的子连续组,插入到连续组中
if(!bConsistentForSomeGroup){ConsistentGroup cg = make_pair(spCandidateGroup,0);vCurrentConsistentGroups.push_back(cg);}
计算Sim3变换,获得闭环帧
(1)通过Bow加速描述子的匹配,筛选出与当前帧的匹配特征点数大于20的候选帧集合,利用RANSAC粗略地计算出当前帧与闭环帧的Sim3(当前帧—闭环帧)
(2)根据估计的Sim3,将每个候选帧中的路标点投影到当前帧中找到更多匹配,通过优化的方法计算更精确的Sim3(当前帧—闭环帧)。有一个帧成功了,就结束此次的循环。
(3)找到候选帧的共视关键帧,找到所有的路标点,投影到当前帧中进行匹配(当前帧–闭环帧+共视帧)(不进行优化)。
(4)判断候选帧是否可靠(如果(3)步匹配上的路标点的数量大于40,则闭环帧可靠)。
计算当前关键帧和上一步中的闭环候选关键帧的Sim3变换。Sim3变换多了一个尺度变换。
计算SIm3,而不是T的原因就是存在尺度漂移。
/*** @brief 计算当前关键帧和上一步闭环候选帧的Sim3变换* 1. 遍历闭环候选帧集,筛选出与当前帧的匹配特征点数大于20的候选帧集合,并为每一个候选帧构造一个Sim3Solver* 2. 对每一个候选帧进行 Sim3Solver 迭代匹配,直到有一个候选帧匹配成功,或者全部失败* 3. 取出闭环匹配上关键帧的相连关键帧,得到它们的MapPoints放入 mvpLoopMapPoints* 4. 将闭环匹配上关键帧以及相连关键帧的MapPoints投影到当前关键帧进行投影匹配* 5. 判断当前帧与检测出的所有闭环关键帧是否有足够多的MapPoints匹配* 6. 清空mvpEnoughConsistentCandidates* @return true 只要有一个候选关键帧通过Sim3的求解与优化,就返回true* @return false 所有候选关键帧与当前关键帧都没有有效Sim3变换*/
bool LoopClosing::ComputeSim3()
注意以上匹配的结果均都存在成员变量mvpCurrentMatchedPoints中,实际的更新步骤见CorrectLoop()步骤3
对于双目或者是RGBD输入的情况,计算得到的尺度=1
遍历上一步中的候选关键帧
如果候选关键帧在局部建图线程中被删除的话,就直接跳过。
// Step 1. 遍历闭环候选帧集,初步筛选出与当前关键帧的匹配特征点数大于20的候选帧集合,并为每一个候选帧构造一个Sim3Solverfor(int i=0; i<nInitialCandidates; i++){// Step 1.1 从筛选的闭环候选帧中取出一帧有效关键帧pKFKeyFrame* pKF = mvpEnoughConsistentCandidates[i];// 避免在LocalMapping中KeyFrameCulling函数将此关键帧作为冗余帧剔除pKF->SetNotErase();// 如果候选帧质量不高,直接PASS// 在局部建图线程中,如果一个关键帧被删除,则isBad为trueif(pKF->isBad()){vbDiscarded[i] = true;continue;}
通过词袋算法获得当前帧与候选帧间匹配上的特征点数量
// Step 1.2 将当前帧 mpCurrentKF 与闭环候选关键帧pKF匹配// 通过bow加速得到 mpCurrentKF 与 pKF 之间的匹配特征点// vvpMapPointMatches 是匹配特征点对应的地图点,本质上来自于候选闭环帧int nmatches = matcher.SearchByBoW(mpCurrentKF,pKF,vvpMapPointMatches[i]);// 粗筛:匹配的特征点数太少,该候选帧剔除if(nmatches<20){vbDiscarded[i] = true;continue;}else{// Step 1.3 为保留的候选帧构造Sim3求解器// 如果 mbFixScale(是否固定尺度) 为 true,则是6 自由度优化(双目 RGBD)// 如果是false,则是7 自由度优化(单目)Sim3Solver* pSolver = new Sim3Solver(mpCurrentKF,pKF,vvpMapPointMatches[i],mbFixScale);// Sim3Solver Ransac 过程置信度0.99,至少20个inliers 最多300次迭代pSolver->SetRansacParameters(0.99,20,300);vpSim3Solvers[i] = pSolver;}// 保留的候选帧数量nCandidates++;
对每个候选关键帧求解Sim3变换,成功的关键帧进行SearchBySim3
(1)遍历每个候选帧
for(int i=0; i<nInitialCandidates; i++)
(2)迭代计算每个候选帧的Sim3变换,如果达到迭代次数上限后,还没有合格的结果,就删除当前候选帧
KeyFrame* pKF = mvpEnoughConsistentCandidates[i];// 内点(Inliers)标志// 即标记经过RANSAC sim3 求解后,vvpMapPointMatches中的哪些作为内点vector<bool> vbInliers; // 内点(Inliers)数量int nInliers;// 是否到达了最优解bool bNoMore;// Step 2.1 取出从 Step 1.3 中为当前候选帧构建的 Sim3Solver 并开始迭代Sim3Solver* pSolver = vpSim3Solvers[i];// 最多迭代5次,返回的Scm是候选帧pKF到当前帧mpCurrentKF的Sim3变换(T12)cv::Mat Scm = pSolver->iterate(5,bNoMore,vbInliers,nInliers);// If Ransac reachs max. iterations discard keyframe// 总迭代次数达到最大限制还没有求出合格的Sim3变换,该候选帧剔除if(bNoMore){vbDiscarded[i]=true;nCandidates--;}
(3)如果计算出了Sim3变换,就将当前帧mpCurrentKF和候选关键帧pKF中的路标点相互投影匹配,以匹配上更多的路标点。随后根据匹配关系进行优化。只要有一个优化成功了,就直接结果while循环
if(!Scm.empty()){// 取出经过Sim3Solver 后匹配点中的内点集合vector<MapPoint*> vpMapPointMatches(vvpMapPointMatches[i].size(), static_cast<MapPoint*>(NULL));for(size_t j=0, jend=vbInliers.size(); j<jend; j++){// 保存内点if(vbInliers[j])vpMapPointMatches[j]=vvpMapPointMatches[i][j];}// Step 2.2 通过上面求取的Sim3变换引导关键帧匹配,弥补Step 1中的漏匹配// 候选帧pKF到当前帧mpCurrentKF的R(R12),t(t12),变换尺度s(s12)cv::Mat R = pSolver->GetEstimatedRotation();cv::Mat t = pSolver->GetEstimatedTranslation();const float s = pSolver->GetEstimatedScale();// 查找更多的匹配(成功的闭环匹配需要满足足够多的匹配特征点数,之前使用SearchByBoW进行特征点匹配时会有漏匹配)// 通过Sim3变换,投影搜索pKF1的特征点在pKF2中的匹配,同理,投影搜索pKF2的特征点在pKF1中的匹配// 只有互相都成功匹配的才认为是可靠的匹配matcher.SearchBySim3(mpCurrentKF,pKF,vpMapPointMatches,s,R,t,7.5);// Step 2.3 用新的匹配来优化 Sim3,只要有一个候选帧通过Sim3的求解与优化,就跳出停止对其它候选帧的判断// OpenCV的Mat矩阵转成Eigen的Matrix类型// gScm:候选关键帧到当前帧的Sim3变换g2o::Sim3 gScm(Converter::toMatrix3d(R),Converter::toVector3d(t),s);// 如果mbFixScale为true,则是6 自由度优化(双目 RGBD),如果是false,则是7 自由度优化(单目)// 优化mpCurrentKF与pKF对应的MapPoints间的Sim3,得到优化后的量gScmconst int nInliers = Optimizer::OptimizeSim3(mpCurrentKF, pKF, vpMapPointMatches, gScm, 10, mbFixScale);// 如果优化成功,则停止while循环遍历闭环候选if(nInliers>=20){// 为True时将不再进入 while循环bMatch = true;// mpMatchedKF就是最终闭环检测出来与当前帧形成闭环的关键帧mpMatchedKF = pKF;// gSmw:从世界坐标系 w 到该候选帧 m 的Sim3变换,都在一个坐标系下,所以尺度 Scale=1g2o::Sim3 gSmw(Converter::toMatrix3d(pKF->GetRotation()),Converter::toVector3d(pKF->GetTranslation()),1.0);// 得到g2o优化后从世界坐标系到当前帧的Sim3变换mg2oScw = gScm*gSmw;mScw = Converter::toCvMat(mg2oScw);mvpCurrentMatchedPoints = vpMapPointMatches;// 只要有一个候选帧通过Sim3的求解与优化,就跳出停止对其它候选帧的判断break;}}
如果所有候选关键帧经过SIM3变换均失败,则返回false
// 退出上面while循环的原因有两种,一种是求解到了bMatch置位后出的,另外一种是nCandidates耗尽为0if(!bMatch){// 如果没有一个闭环匹配候选帧通过Sim3的求解与优化// 清空mvpEnoughConsistentCandidates,这些候选关键帧以后都不会在再参加回环检测过程了for(int i=0; i<nInitialCandidates; i++)mvpEnoughConsistentCandidates[i]->SetErase();// 当前关键帧也将不会再参加回环检测了mpCurrentKF->SetErase();// Sim3 计算失败,退出了return false;}
取出与当前帧闭环匹配上的关键帧及其共视关键帧,以及这些共视关键帧的地图点
vector<KeyFrame*> vpLoopConnectedKFs = mpMatchedKF->GetVectorCovisibleKeyFrames();// 包含闭环匹配关键帧本身,形成一个“闭环关键帧小组“vpLoopConnectedKFs.push_back(mpMatchedKF);mvpLoopMapPoints.clear();// 遍历这个组中的每一个关键帧for(vector<KeyFrame*>::iterator vit=vpLoopConnectedKFs.begin(); vit!=vpLoopConnectedKFs.end(); vit++){KeyFrame* pKF = *vit;vector<MapPoint*> vpMapPoints = pKF->GetMapPointMatches();// 遍历其中一个关键帧的所有有效地图点for(size_t i=0, iend=vpMapPoints.size(); i<iend; i++){MapPoint* pMP = vpMapPoints[i];if(pMP){// mnLoopPointForKF 用于标记,避免重复添加if(!pMP->isBad() && pMP->mnLoopPointForKF!=mpCurrentKF->mnId){mvpLoopMapPoints.push_back(pMP);// 标记一下pMP->mnLoopPointForKF=mpCurrentKF->mnId;}}}}
将闭环关键帧及其共视关键帧的所有地图点投影到当前关键帧进行投影匹配
matcher.SearchByProjection(mpCurrentKF, mScw, mvpLoopMapPoints, mvpCurrentMatchedPoints,10);
判断闭环是否可靠
统计当前帧与候选闭环及其共视帧中匹配上的路标点的数量,小于40的话,认为不可靠。
// Step 4:将闭环关键帧及其共视关键帧的所有地图点投影到当前关键帧进行投影匹配// 根据投影查找更多的匹配(成功的闭环匹配需要满足足够多的匹配特征点数)// 根据Sim3变换,将每个mvpLoopMapPoints投影到mpCurrentKF上,搜索新的匹配对// mvpCurrentMatchedPoints是前面经过SearchBySim3得到的已经匹配的点对,这里就忽略不再匹配了// 搜索范围系数为10matcher.SearchByProjection(mpCurrentKF, mScw, mvpLoopMapPoints, mvpCurrentMatchedPoints,10);// If enough matches accept Loop// Step 5: 统计当前帧与检测出的所有闭环关键帧的匹配地图点数目,超过40个说明成功闭环,否则失败int nTotalMatches = 0;for(size_t i=0; i<mvpCurrentMatchedPoints.size(); i++){if(mvpCurrentMatchedPoints[i])nTotalMatches++;}if(nTotalMatches>=40){// 如果当前回环可靠,保留当前待闭环关键帧,其他闭环候选全部删掉以后不用了for(int i=0; i<nInitialCandidates; i++)if(mvpEnoughConsistentCandidates[i]!=mpMatchedKF)mvpEnoughConsistentCandidates[i]->SetErase();return true;}else {// 闭环不可靠,闭环候选及当前待闭环帧全部删除for(int i=0; i<nInitialCandidates; i++)mvpEnoughConsistentCandidates[i]->SetErase();mpCurrentKF->SetErase();return false;}
闭环校正
闭环关键帧的位姿会根据Sim3进行校正,同时与其相连的关键帧的位姿即路标点的位置也会被校正。
Sim3校正对旋转矩阵没有影响,只会影响平移矩阵。
(1)在上一帧计算当前帧和闭环帧的Sim3位姿变换时,建立了闭环帧及其共视帧的路标点与当前帧的联系,因此先更新共视图。
(2)根据计算的当前帧和闭环帧的Sim3变换,去更新当前帧及其共视帧的位姿,以及路标点的坐标。
(3)因为闭环帧已经经过了多次优化,认为是精确的,因此建立闭环帧及其共视帧的路标点与当前帧及其共视帧的联系,进行路标点的匹配、融合。
(4)优化本质图(只优化位姿)
(5)建立一个全局BA优化线程
结束局部地图线程、全局BA,为闭环校正做准备
mpLocalMapper->RequestStop();if(isRunningGBA()){// 如果有全局BA在运行,终止掉,迎接新的全局BAunique_lock<mutex> lock(mMutexGBA);mbStopGBA = true;// 记录全局BA次数mnFullBAIdx++;if(mpThreadGBA){// 停止全局BA线程mpThreadGBA->detach();delete mpThreadGBA;}}
更新共视图
在闭环检测、计算Sim3的过程中,建立了当前帧的特征点和其闭环帧、闭环帧的共视帧的路标点间的联系,因此这里需要更新一个共视图。
// Step 1:根据共视关系更新当前关键帧与其它关键帧之间的连接关系// 因为之前闭环检测、计算Sim3中改变了该关键帧的地图点,所以需要更新mpCurrentKF->UpdateConnections();
更新当前帧、与其相连的关键帧的位姿。
当前帧与世界坐标系之间的Sim变换在ComputeSim3函数中已经确定并优化,通过相对位姿关系,可以确定与当前帧相连的关键帧与世界坐标系之间的Sim3变换
(1)通过mg2oScw(认为是准的)来进行位姿传播,得到当前关键帧的共视关键帧的世界坐标系下Sim3 位姿.遍历"当前关键帧组"(当前帧+共视帧)
// Step 2.1:通过mg2oScw(认为是准的)来进行位姿传播,得到当前关键帧的共视关键帧的世界坐标系下Sim3 位姿(还没有修正)// 遍历"当前关键帧组""for(vector<KeyFrame*>::iterator vit=mvpCurrentConnectedKFs.begin(), vend=mvpCurrentConnectedKFs.end(); vit!=vend; vit++){KeyFrame* pKFi = *vit;cv::Mat Tiw = pKFi->GetPose();if(pKFi!=mpCurrentKF) //跳过当前关键帧,因为当前关键帧的位姿已经在前面优化过了,在这里是参考基准{// 得到当前关键帧 mpCurrentKF 到其共视关键帧 pKFi 的相对变换cv::Mat Tic = Tiw*Twc;cv::Mat Ric = Tic.rowRange(0,3).colRange(0,3);cv::Mat tic = Tic.rowRange(0,3).col(3);// g2oSic:当前关键帧 mpCurrentKF 到其共视关键帧 pKFi 的Sim3 相对变换// 这里是non-correct, 所以scale=1.0g2o::Sim3 g2oSic(Converter::toMatrix3d(Ric),Converter::toVector3d(tic),1.0);// 当前帧的位姿固定不动,其它的关键帧根据相对关系得到Sim3调整的位姿g2o::Sim3 g2oCorrectedSiw = g2oSic*mg2oScw;// Pose corrected with the Sim3 of the loop closure// 存放闭环g2o优化后当前关键帧的共视关键帧的Sim3 位姿CorrectedSim3[pKFi]=g2oCorrectedSiw;}cv::Mat Riw = Tiw.rowRange(0,3).colRange(0,3);cv::Mat tiw = Tiw.rowRange(0,3).col(3);g2o::Sim3 g2oSiw(Converter::toMatrix3d(Riw),Converter::toVector3d(tiw),1.0);// Pose without correction// 存放没有矫正的当前关键帧的共视关键帧的Sim3变换NonCorrectedSim3[pKFi]=g2oSiw;}
(2)校正当前帧的共视关键帧的路标点坐标
路标点世界坐标------(未校正的T)--------路标点相机坐标-------(校正的Sim3)--------路标点世界坐标
保持路标点和帧间的相对位置不变。
要记得更新地图点的平均观测方向和观测范围
// Correct all MapPoints obsrved by current keyframe and neighbors, so that they align with the other side of the loop// Step 2.2:得到矫正的当前关键帧的共视关键帧位姿后,修正这些关键帧的地图点// 遍历待矫正的共视关键帧for(KeyFrameAndPose::iterator mit=CorrectedSim3.begin(), mend=CorrectedSim3.end(); mit!=mend; mit++){KeyFrame* pKFi = mit->first;g2o::Sim3 g2oCorrectedSiw = mit->second;g2o::Sim3 g2oCorrectedSwi = g2oCorrectedSiw.inverse();g2o::Sim3 g2oSiw =NonCorrectedSim3[pKFi];vector<MapPoint*> vpMPsi = pKFi->GetMapPointMatches();// 遍历待矫正共视关键帧中的每一个地图点for(size_t iMP=0, endMPi = vpMPsi.size(); iMP<endMPi; iMP++){MapPoint* pMPi = vpMPsi[iMP];if(!pMPi)continue;if(pMPi->isBad())continue;if(pMPi->mnCorrectedByKF==mpCurrentKF->mnId) // 标记,防止重复矫正continue;// 矫正过程本质上也是基于当前关键帧的优化后的位姿展开的// Project with non-corrected pose and project back with corrected pose// 将该未校正的eigP3Dw先从世界坐标系映射到未校正的pKFi相机坐标系,然后再反映射到校正后的世界坐标系下cv::Mat P3Dw = pMPi->GetWorldPos();// 地图点世界坐标系下坐标Eigen::Matrix<double,3,1> eigP3Dw = Converter::toVector3d(P3Dw);// map(P) 内部做了变换 R*P +t // 下面变换是:eigP3Dw: world →g2oSiw→ i →g2oCorrectedSwi→ worldEigen::Matrix<double,3,1> eigCorrectedP3Dw = g2oCorrectedSwi.map(g2oSiw.map(eigP3Dw));cv::Mat cvCorrectedP3Dw = Converter::toCvMat(eigCorrectedP3Dw);pMPi->SetWorldPos(cvCorrectedP3Dw);// 记录矫正该地图点的关键帧id,防止重复pMPi->mnCorrectedByKF = mpCurrentKF->mnId;// 记录该地图点所在的关键帧idpMPi->mnCorrectedReference = pKFi->mnId;// 因为地图点更新了,需要更新其平均观测方向以及观测距离范围pMPi->UpdateNormalAndDepth();}
(3)将共视关键帧的Sim3转换为SE3,根据更新的Sim3,更新关键帧的位姿
// Update keyframe pose with corrected Sim3. First transform Sim3 to SE3 (scale translation)// Step 2.3:将共视关键帧的Sim3转换为SE3,根据更新的Sim3,更新关键帧的位姿// 其实是现在已经有了更新后的关键帧组中关键帧的位姿,但是在上面的操作时只是暂时存储到了 KeyFrameAndPose 类型的变量中,还没有写回到关键帧对象中// 调用toRotationMatrix 可以自动归一化旋转矩阵Eigen::Matrix3d eigR = g2oCorrectedSiw.rotation().toRotationMatrix(); Eigen::Vector3d eigt = g2oCorrectedSiw.translation(); double s = g2oCorrectedSiw.scale();// 平移向量中包含有尺度信息,还需要用尺度归一化eigt *=(1./s); cv::Mat correctedTiw = Converter::toCvSE3(eigR,eigt);// 设置矫正后的新的posepKFi->SetPose(correctedTiw);
更新当前帧路标点
更新当前帧中的路标点,应为在ComputeSim3()函数获取闭环帧的时候,将闭环帧及其共视帧的路标点和当前帧的特征点进行了匹配
// Step 3:检查当前帧的地图点与经过闭环匹配后该帧的地图点是否存在冲突,对冲突的进行替换或填补// mvpCurrentMatchedPoints 是当前关键帧和闭环关键帧组的所有地图点进行投影得到的匹配点for(size_t i=0; i<mvpCurrentMatchedPoints.size(); i++){if(mvpCurrentMatchedPoints[i]){//取出同一个索引对应的两种地图点,决定是否要替换// 匹配投影得到的地图点MapPoint* pLoopMP = mvpCurrentMatchedPoints[i];// 原来的地图点MapPoint* pCurMP = mpCurrentKF->GetMapPoint(i); if(pCurMP)// 如果有重复的MapPoint,则用匹配的地图点代替现有的// 因为匹配的地图点是经过一系列操作后比较精确的,现有的地图点很可能有累计误差pCurMP->Replace(pLoopMP);else{// 如果当前帧没有该MapPoint,则直接添加mpCurrentKF->AddMapPoint(pLoopMP,i);pLoopMP->AddObservation(mpCurrentKF,i);pLoopMP->ComputeDistinctiveDescriptors();}}} }
路标点融合
在局部建图的时候,已经获得了当前帧及其共视帧所联系的路标点。这里又已知闭环帧及其共视帧的路标点。闭环帧出现在当前帧之前,进行了多次优化,因此其路标点准确。所以将闭环帧及其共视帧的路标点投影到当前帧组中进行匹配,融合。
SearchAndFuse(CorrectedSim3);
更新当前帧的连接关系
前面进行了路标点的融合,这里要更改一下连接关系。
map<KeyFrame*, set<KeyFrame*> > LoopConnections:
KeyFrame:当前帧及其共视关键帧中的一帧
set<KeyFrame*>:KeyFrame的共视关键帧
优化共视图
Optimizer::OptimizeEssentialGraph(mpMap, mpMatchedKF, mpCurrentKF, NonCorrectedSim3, CorrectedSim3, LoopConnections, mbFixScale);
添加当前帧与闭环匹配帧之间的边
mpMatchedKF->AddLoopEdge(mpCurrentKF);mpCurrentKF->AddLoopEdge(mpMatchedKF);
建立一个线程进行全局BA优化
mpThreadGBA = new thread(&LoopClosing::RunGlobalBundleAdjustment,this,mpCurrentKF->mnId);
全局BA优化线程
优化所有的关键帧及路标点
void LoopClosing::RunGlobalBundleAdjustment(unsigned long nLoopKF)
相关文章:

nginx 启动 + uwsgi + django
https://www.cnblogs.com/chenice/p/6921727.html https://blog.csdn.net/Aaroun/article/details/78218131转载于:https://www.cnblogs.com/pythonClub/p/9746866.html

poj1741(树的点分治)
题目连接:POJ - 1741 看了好长时间才明白了点...... 网上讲解很多但感觉都不够详细。。。大概是太弱了吧-_-|| 学通了再回来写详解。。。 1 #include<iostream>2 #include<cstring>3 #include<cstdio>4 #include<algorithm>5 #define LL lo…

Android 串口通讯
概念 串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。串行接口(Serial Interface)是指数据一位一位地顺序传送。其特点是通信线路简单,只要一…

Linux07-OpenSSH
目录 一、使用SSH访问远程主机 1.1、什么是OpenSSH Secure Shell(SSH) 1.2、SSH主机密钥 二、配置基于SSH密钥的身份验证 2.1、基于SSH密钥的身份验证 2.2、自定义SSH服务配置 2.3、sftp传输文件 一、使用SSH访问远程主机 1.1、什么是OpenSSH Se…

ORB_SLAM2中的Sim3变换
对于双目、RGB-D相机,可获得深度,因此不存在尺度问题,因此Sim3中的尺度s1。 (1)通过词袋加速算法实现当前帧、闭环帧的特征点的匹配,建立闭环帧的路标点和当前帧的特征点间的联系。 (2ÿ…

Ubuntu16.04 下的网易云出现网络异常、无法播放,界面无响应问题的统一解决
能够在Linux系统下体验到原生界面的网易云音乐是件不错的事情,但是它总是经常性的出现网络异常,界面无响应的问题 为了听歌的体验,进行深入探究: 首先通过终端启用网易云音乐:sudo netease-cloud-music 会得到网易云音…

SpringBoot 概念和起步
一、概念和由来 1、什么是 Spring Boot Spring Boot 的设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用特定方式来进行配置,从而使开发人员不再需要定义样板化的配置。 Spring Boot 其实不是什么新的框架,它默认配置了很多框架的使用…

WKWebView Safari调试、JS互调、加载进度条、JS中alert、confirm、prompt
主要内容 Safari调试swift/OC与JS互调增加加载进度条支持JS中alert、confirm、prompt Safari调试 设置 —> safari --> 高级,开启JavaScript、网页检查器 打开Safari浏览器,选择调试的网页,同样在js里面可以断点调试: swift/OC与JS互调 这里…

CentOS7 打包RPM 升级OpenSSH8.3
目录 一、源码包 二、打包RPM 2.1、准备阶段 2.2、打包排错阶段 三、升级 漏扫设备发现OpenSSH有漏洞,需要升级到OpenSSH 8.1及以上版本,那么干脆就直接升级到发文时最新的版本,OpenSSH 8.3。做法是找到OpenSSH 8.3的源码包,…

步步为营-44-窗体之间传值--观察者模式
说明 :观察者模式又叫发布-订阅模式,其中又涉及到中介者模式 1 结构 2 创建Main窗体(中介者),ChildForm1(发布者),ChildForm2(订阅者),ChildForm3(订阅者), 2.1 ChildForm1中添加按钮,当按钮被点击是ChildForm2(订阅者),ChildForm3(订阅者),的文本框汇中获取信息 2.2 定义接口 …

java指令详解
Java是通过java虚拟机来装载和执行编译文件(class文件)的,java虚拟机通过命令java option 来启动,-option为虚拟机参数,通过这些参数可对虚拟机的运行状态进行调整. 一、如何查看参数列表: 虚拟机参数分为基本和扩展两…

wrs-arcface虹软人脸识别
前言 虹软人脸识别组件,支持活体识别、离线识别、图片人脸特征识别、图片是否同一人对比、相机人脸识别或对比,虹软免费版请使https://ext.dcloud.net.cn/plugin?id6084 功能 支持活体识别、离线识别图片人脸特征识别(年龄、性别、3DAngle)两张图片是否是同一人…

C++指针与引用的区别
(1)指针是一个变量,本身占有内存,内存中存储的是所指向对象的地址。引用是内存的别名。 (2)指针可以通过解引用的方式,取出所指向内存中的值。引用没有解引用。 (3)指针可…

Linux08-日志
目录 一、systemd的日志 1.1、sytemd-journald与systemd日志 1.2、systemd日志的持久化 二、系统常规日志 2.1、系统日志概述 2.2、查看系统日志文件 2.3、日志的轮转 2.4、分析系统日志 2.5、使用logger发送消息到日志 RHEL7的日志由2个服务负责记录,分别…

Java的小实验——各种测试以及说明
日期:2018.10.07 星期五 博客期:014 一、Java中的位运算 代码如下: 1 package Morts107;2 3 public class Test107 {4 public static void main(String[] args) {5 int z;6 z 13>>1;//00001101(13)---------------…

C++内存的分区
C内存分为四个区: (1)代码区:存放代码转译成的二进制代码。 (2)全局区:存放全局变量、静态变量(static)、常量(如字符串常量)。 全局区中还包含一…

SpringCloud的服务网关zuul
演示如何使用api网关屏蔽各服务来源 一、概念和定义 1、zuul最终还是使用Ribbon的,顺便测试一下Hystrix断路保护2、zuul也是一个EurekaClient,访问服务注册中心,获取元数据,使用本地的Ribbon负载均衡,Hystrix断路保护&…

wrs-tuya-cloud
前言 wrs-tuya-cloud是涂鸦官网针对云开发的插件,包含垂直品类硬件API(万能红外开放能力、设备连接服务、设备OTA固件升级、实时音视频、睡眠带开放能力、体脂秤开放能力、智能门锁开放能力、视频云存储 、邮件服务 、 语音消息服务、消息推送服务、短信服务 、内测…

Windows Server 2016 笔记
从业界普遍实践结果来看,Windows Server在服务器领域真是不太好用。但是,有些时候由于种种原因不得不用,所以还是有必要了解一下的。今天参加了一个Windows Server的培训,主要面对Windows Server 2016,写下这篇博客备忘…

(办公)网页发送到桌面快捷方式怎么做
转载自百度:https://jingyan.baidu.com/article/f79b7cb303d50a9145023e6e.html 有时候一个网页我们需要经常用到,每次找那个需要的网页很耗时间,那么我们怎么把我们需要的网页发送到桌面快捷方式呢? 这样下次我们直接点击桌面上的快捷方式就…

C++程序编译过程
程序编译的过程,是将源代码转换为计算机可执行的机械语言的过程。分为预处理、编译、汇编、链接四步。 (1)预处理:对程序进行预处理,比如将头文件的代码直接赋值到当前代码中等等. (2)编译&am…

Java的注释(详细版)
注释是对代码进行必要的说明,以便于后期的修改、维护和升级。Java的注释分为三种:第一种是**单行注释**:用双斜杠“//”来进行实例://单行注释第二种是**文档注释**:用斜杠“/”和星号“*”来进行实例:/***…

Hadoop的存储架构介绍
http://lxw1234.com/archives/2016/04/638.htm 该文章介绍了Hadoop的架构原理,简单易懂。 目前公司提供Hadoop的运算集群BMR,可以直接申请集群资源。转载于:https://www.cnblogs.com/blog-of-Fourier/p/6809811.html

编译OpenSSH8.4的RPM包及升级
目录 一、安装相关依赖包 二、创建rpmbuild目录并下载源码 三、打包及排错 四、升级到OpenSSH 8.4p1 以下是打包好的OpenSSH 8.4p1,包括7个rpm包,欢迎下载使用。 OpenSSH-8.4p1-Bundle 一、安装相关依赖包 根据以往经验,需要安装wget、…

centos 系统使用verdaccio搭建npm私库
.安装nodejs yum install -y nodejs 2.安装verdaccio npm install -g verdaccio --unsafe-perm 3.配置 a.修改配置文件 config.yaml,在其最后添加监听端口(使其可在外网访问) listen: 0.0.0.0:4873 b.对外开放4873端口 firewall-cmd --state …

视觉SLAM中PNP求解
PNP(Perspective-n-points)是SLAM中估计位姿的重要方法。已知条件为路标点在相机1中的相机坐标以及投影到相机2中的像素坐标,据此去估计相机1、相机2间的位姿。主要解法包括DLT、P3P、EPNP P3P 已知A、B、C在相机1坐标系下的坐标࿰…

Java程序的运行原理 用记事本编写Java代码
首先将Java代码写入源文件(.java)中→ 通过 javac 生成class文件(.class) → 再通过java命令执行程序:◆将class文件加载内存(相当于将东西输入大脑)◆检验class文件(大脑检查是否有语法等错误,若无误)◆将…

Linux下修改mysql的root密码后数据库消失怎么处理
Linux系统下如果没有通过password()函数修改mysql的root密码就会导致mysql数据库消失。有些人可能不知道而直接修改了mysql的root密码,于是产生了mysql数据库消失的问题,这个时候该怎么处理呢? 可以用下面的办法解决&a…

编译httpd-2.4.46的RPM包
目录 一、下载源码 二、编译&排错 2.1、第一次编译,解决依赖包问题。 2.2、第二次编译,解决anaconda导致的环境变量问题 2.3、第三次编译,解决apr版本过低问题 提供 apr-1.7.0、httpd-2.4.46 的RPM包下载。 apr-1.7.0-bundle.zip …

C/s模式B/S模式
C/s模式:是客户端/服务器(Client/Server)模式,主要指的是传统的桌面级的应用程序。比如我们经常用的信息管理系统。 C/S 客户端/服务器 例如QQ,网络游戏,需要下载客户端才能访问服务器的程序 B/S 浏览器/服务器 例如Intel…