2021SC@SDUSC
LoopClosing分析(4)
LoopClosing的分析已经过半了,这篇博客我们接着分析
- CorrectLoop也是重要的函数之一,对它进行重点分析。CorrectLoop是进行闭环矫正,主要过程可以概括为五步:
- 通过求解的Sim3以及相对姿态关系,调整与当前帧相连的关键帧位姿以及这些关键帧观测到的地图点位置(相连关键帧—当前帧)
- 将闭环帧以及闭环帧相连的关键帧的地图点和与当前帧相连的关键帧的点进行匹配(当前帧+相连关键帧—闭环帧+相连关键帧)
- 通过MapPoints的匹配关系更新这些帧之间的连接关系,即更新covisibility graph
- 对Essential Graph(Pose Graph)进行优化,MapPoints的位置则根据优化后的位姿做相对应的调整
- 创建线程进行全局Bundle Adjustment
void LoopClosing::CorrectLoop()
{
cout << "Loop detected!" << endl;
// Step 0:结束局部地图线程、全局BA,为闭环矫正做准备
// Step 1:根据共视关系更新当前帧与其它关键帧之间的连接
// Step 2:通过位姿传播,得到Sim3优化后,与当前帧相连的关键帧的位姿,以及它们的MapPoints
// Step 3:检查当前帧的MapPoints与闭环匹配帧的MapPoints是否存在冲突,对冲突的MapPoints进行替换或填补
// Step 4:通过将闭环时相连关键帧的mvpLoopMapPoints投影到这些关键帧中,进行MapPoints检查与替换
// Step 5:更新当前关键帧之间的共视相连关系,得到因闭环时MapPoints融合而新得到的连接关系
// Step 6:进行EssentialGraph优化,LoopConnections是形成闭环后新生成的连接关系,不包括步骤7中当前帧与闭环匹配帧之间的连接关系
// Step 7:添加当前帧与闭环匹配帧之间的边(这个连接关系不优化)
// Step 8:新建一个线程用于全局BA优化
// g2oSic: 当前关键帧 mpCurrentKF 到其共视关键帧 pKFi 的Sim3 相对变换
// mg2oScw: 世界坐标系到当前关键帧的 Sim3 变换
// g2oCorrectedSiw:世界坐标系到当前关键帧共视关键帧的Sim3 变换
// Send a stop signal to Local Mapping
// Avoid new keyframes are inserted while correcting the loop
// Step 0:结束局部地图线程、全局BA,为闭环矫正做准备
// 请求局部地图停止,防止在回环矫正时局部地图线程中InsertKeyFrame函数插入新的关键帧
mpLocalMapper->RequestStop();
if(isRunningGBA())
{
// 如果有全局BA在运行,终止掉,迎接新的全局BA
unique_lock<mutex> lock(mMutexGBA);
mbStopGBA = true;
// 记录全局BA次数
mnFullBAIdx++;
if(mpThreadGBA)
{
// 停止全局BA线程
mpThreadGBA->detach();
delete mpThreadGBA;
}
}
// Wait until Local Mapping has effectively stopped
// 一直等到局部地图线程结束再继续
while(!mpLocalMapper->isStopped())
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
// Ensure current keyframe is updated
// Step 1:根据共视关系更新当前关键帧与其它关键帧之间的连接关系
// 因为之前闭环检测、计算Sim3中改变了该关键帧的地图点,所以需要更新
mpCurrentKF->UpdateConnections();
// Retrive keyframes connected to the current keyframe and compute corrected Sim3 pose by propagation
// Step 2:通过位姿传播,得到Sim3优化后,与当前帧相连的关键帧的位姿,以及它们的地图点
// 当前帧与世界坐标系之间的Sim变换在ComputeSim3函数中已经确定并优化,
// 通过相对位姿关系,可以确定这些相连的关键帧与世界坐标系之间的Sim3变换
// 取出当前关键帧及其共视关键帧,称为“当前关键帧组”
mvpCurrentConnectedKFs = mpCurrentKF->GetVectorCovisibleKeyFrames();
mvpCurrentConnectedKFs.push_back(mpCurrentKF);
// CorrectedSim3:存放闭环g2o优化后当前关键帧的共视关键帧的世界坐标系下Sim3 变换
// NonCorrectedSim3:存放没有矫正的当前关键帧的共视关键帧的世界坐标系下Sim3 变换
KeyFrameAndPose CorrectedSim3, NonCorrectedSim3;
// 先将mpCurrentKF的Sim3变换存入,认为是准的,所以固定不动
CorrectedSim3[mpCurrentKF]=mg2oScw;
// 当前关键帧到世界坐标系下的变换矩阵
cv::Mat Twc = mpCurrentKF->GetPoseInverse();
// 对地图点操作
{
// Get Map Mutex
// 锁定地图点
unique_lock<mutex> lock(mpMap->mMutexMapUpdate);
// 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.0
g2o::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;
}
// 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;
// 取出经过位姿传播后的Sim3变换
g2o::Sim3 g2oCorrectedSiw = mit->second;
g2o::Sim3 g2oCorrectedSwi = g2oCorrectedSiw.inverse();
// 取出未经过位姿传播的Sim3变换
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) 内部做了相似变换 s*R*P +t
// 下面变换是:eigP3Dw: world →g2oSiw→ i →g2oCorrectedSwi→ world
Eigen::Matrix<double,3,1> eigCorrectedP3Dw = g2oCorrectedSwi.map(g2oSiw.map(eigP3Dw));
cv::Mat cvCorrectedP3Dw = Converter::toCvMat(eigCorrectedP3Dw);
pMPi->SetWorldPos(cvCorrectedP3Dw);
// 记录矫正该地图点的关键帧id,防止重复
pMPi->mnCorrectedByKF = mpCurrentKF->mnId;
// 记录该地图点所在的关键帧id
pMPi->mnCorrectedReference = pKFi->mnId;
// 因为地图点更新了,需要更新其平均观测方向以及观测距离范围
pMPi->UpdateNormalAndDepth();
}
// 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);
// 设置矫正后的新的pose
pKFi->SetPose(correctedTiw);
// Make sure connections are updated
// Step 2.4:根据共视关系更新当前帧与其它关键帧之间的连接
// 地图点的位置改变了,可能会引起共视关系\权值的改变
pKFi->UpdateConnections();
}
// Start Loop Fusion
// Update matched map points and replace if duplicated
// 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();
}
}
}
}
// Project MapPoints observed in the neighborhood of the loop keyframe
// into the current keyframe and neighbors using corrected poses.
// Fuse duplications.
// Step 4:将闭环相连关键帧组mvpLoopMapPoints 投影到当前关键帧组中,进行匹配,融合,新增或替换当前关键帧组中KF的地图点
// 因为 闭环相连关键帧组mvpLoopMapPoints 在地图中时间比较久经历了多次优化,认为是准确的
// 而当前关键帧组中的关键帧的地图点是最近新计算的,可能有累积误差
// CorrectedSim3:存放矫正后当前关键帧的共视关键帧,及其世界坐标系下Sim3 变换
SearchAndFuse(CorrectedSim3);
// After the MapPoint fusion, new links in the covisibility graph will appear attaching both sides of the loop
// Step 5:更新当前关键帧组之间的两级共视相连关系,得到因闭环时地图点融合而新得到的连接关系
// LoopConnections:存储因为闭环地图点调整而新生成的连接关系
map<KeyFrame*, set<KeyFrame*> > LoopConnections;
// Step 5.1:遍历当前帧相连关键帧组(一级相连)
for(vector<KeyFrame*>::iterator vit=mvpCurrentConnectedKFs.begin(), vend=mvpCurrentConnectedKFs.end(); vit!=vend; vit++)
{
KeyFrame* pKFi = *vit;
// Step 5.2:得到与当前帧相连关键帧的相连关键帧(二级相连)
vector<KeyFrame*> vpPreviousNeighbors = pKFi->GetVectorCovisibleKeyFrames();
// Update connections. Detect new links.
// Step 5.3:更新一级相连关键帧的连接关系(会把当前关键帧添加进去,因为地图点已经更新和替换了)
pKFi->UpdateConnections();
// Step 5.4:取出该帧更新后的连接关系
LoopConnections[pKFi]=pKFi->GetConnectedKeyFrames();
// Step 5.5:从连接关系中去除闭环之前的二级连接关系,剩下的连接就是由闭环得到的连接关系
for(vector<KeyFrame*>::iterator vit_prev=vpPreviousNeighbors.begin(), vend_prev=vpPreviousNeighbors.end(); vit_prev!=vend_prev; vit_prev++)
{
LoopConnections[pKFi].erase(*vit_prev);
}
// Step 5.6:从连接关系中去除闭环之前的一级连接关系,剩下的连接就是由闭环得到的连接关系
for(vector<KeyFrame*>::iterator vit2=mvpCurrentConnectedKFs.begin(), vend2=mvpCurrentConnectedKFs.end(); vit2!=vend2; vit2++)
{
LoopConnections[pKFi].erase(*vit2);
}
}
// Optimize graph
// Step 6:进行本质图优化,优化本质图中所有关键帧的位姿和地图点
// LoopConnections是形成闭环后新生成的连接关系,不包括步骤7中当前帧与闭环匹配帧之间的连接关系
Optimizer::OptimizeEssentialGraph(mpMap, mpMatchedKF, mpCurrentKF, NonCorrectedSim3, CorrectedSim3, LoopConnections, mbFixScale);
// Add loop edge
// Step 7:添加当前帧与闭环匹配帧之间的边(这个连接关系不优化)
// !这两句话应该放在OptimizeEssentialGraph之前,因为OptimizeEssentialGraph的步骤4.2中有优化
mpMatchedKF->AddLoopEdge(mpCurrentKF);
mpCurrentKF->AddLoopEdge(mpMatchedKF);
// Launch a new thread to perform Global Bundle Adjustment
// Step 8:新建一个线程用于全局BA优化
// OptimizeEssentialGraph只是优化了一些主要关键帧的位姿,这里进行全局BA可以全局优化所有位姿和MapPoints
mbRunningGBA = true;
mbFinishedGBA = false;
mbStopGBA = false;
mpThreadGBA = new thread(&LoopClosing::RunGlobalBundleAdjustment,this,mpCurrentKF->mnId);
// Loop closed. Release Local Mapping.
mpLocalMapper->Release();
cout << "Loop Closed!" << endl;
mLastLoopKFid = mpCurrentKF->mnId;//TODO old varible, it is not use in the new algorithm
}
- SearchAndFuse 函数是将闭环相连关键帧组mvpLoopMapPoints 投影到当前关键帧组中,进行匹配,新增或替换当前关键帧组中KF的地图点。因为闭环相连关键帧组mvpLoopMapPoints 在地图中时间比较久经历了多次优化,一般被认为是准确的;而当前关键帧组中的关键帧的地图点是最近新计算的,可能会出现累积误差
void LoopClosing::SearchAndFuse(const KeyFrameAndPose &CorrectedPosesMap)
{
// 定义ORB匹配器
ORBmatcher matcher(0.8);
// Step 1 遍历待矫正的当前KF的相连关键帧
for(KeyFrameAndPose::const_iterator mit=CorrectedPosesMap.begin(), mend=CorrectedPosesMap.end(); mit!=mend;mit++)
{
KeyFrame* pKF = mit->first;
// 矫正过的Sim 变换
g2o::Sim3 g2oScw = mit->second;
cv::Mat cvScw = Converter::toCvMat(g2oScw);
// Step 2 将mvpLoopMapPoints投影到pKF帧匹配,检查地图点冲突并融合
// mvpLoopMapPoints:与当前关键帧闭环匹配上的关键帧及其共视关键帧组成的地图点
vector<MapPoint*> vpReplacePoints(mvpLoopMapPoints.size(),static_cast<MapPoint*>(NULL));
// vpReplacePoints:存储mvpLoopMapPoints投影到pKF匹配后需要替换掉的新增地图点,索引和mvpLoopMapPoints一致,初始化为空
// 搜索区域系数为4
matcher.Fuse(pKF,cvScw,mvpLoopMapPoints,4,vpReplacePoints);
// Get Map Mutex
// 之所以不在上面 Fuse 函数中进行地图点融合更新的原因是需要对地图加锁
unique_lock<mutex> lock(mpMap->mMutexMapUpdate);
const int nLP = mvpLoopMapPoints.size();
// Step 3 遍历闭环帧组的所有的地图点,替换掉需要替换的地图点
for(int i=0; i<nLP;i++)
{
MapPoint* pRep = vpReplacePoints[i];
if(pRep)
{
// 如果记录了需要替换的地图点
// 用mvpLoopMapPoints替换掉vpReplacePoints里记录的要替换的地图点
pRep->Replace(mvpLoopMapPoints[i]);
}
}
}
}
这篇博客相对分析得更细致,我们下篇博客见