这里是Tracking部分的第二部分,详细讲述各分支的代码及其实现原理。
目标:从初始的两帧单目图像中,对SLAM系统进行初始化(得到初始两帧的匹配,相机初始位姿,初始MapPoints),以便之后进行跟踪。
方式:并行得计算基础矩阵E和单应矩阵H,恢复出最开始两帧相机位姿;三角化得到MapPoints的深度,获得点云地图。
单目的初始化有专门的初始化器,只有连续的两帧特征点均>100个才能够成功构建初始化器。
完成前两帧特征点的匹配
ORBmatcher matcher(0.9,true);
int nmatches = matcher.SearchForInitialization(mInitialFrame,mCurrentFrame,mvbPrevMatched,mvIniMatches,100);
mvIniMatches存储mInitialFrame,mCurrentFrame之间匹配的特征点。
这里的ORBmatcher类后面有机会再详细介绍。功能就是ORB特征点的匹配。
在得到超过100个匹配点对后,运用RANSAC 8点法同时计算单应矩阵H和基础矩阵E,并选择最合适的结果作为相机的初始位姿。
mpInitializer = new Initializer(mCurrentFrame,1.0,200);
mpInitializer->Initialize(mCurrentFrame, mvIniMatches, Rcw, tcw, mvIniP3D, vbTriangulated)
Initializer类是初始化类。下面要详细讲一下类成员函数 intilize(),能够根据当前帧和两帧匹配点计算出相机位姿R|t ,以及三角化得到 3D特征点。
(1)调用多线程分别用于计算fundamental matrix和homography
// 计算homograpy并打分
thread threadH(&Initializer::FindHomography,this,ref(vbMatchesInliersH), ref(SH), ref(H));
// 计算fundamental matrix并打分
thread threadF(&Initializer::FindFundamental,this,ref(vbMatchesInliersF), ref(SF), ref(F));
// Wait until both threads have finished
threadH.join();
threadF.join();
(2)计算得分比例,选取某个模型进行恢复R|t
float RH = SH/(SH+SF);
// 步骤5:从H矩阵或F矩阵中恢复R,t
if(RH>0.40)
return ReconstructH(vbMatchesInliersH,H,mK,R21,t21,vP3D,vbTriangulated,1.0,50);
else //if(pF_HF>0.6)
return ReconstructF(vbMatchesInliersF,F,mK,R21,t21,vP3D,vbTriangulated,1.0,50);
具体的:
(1) 计算单应矩阵cv::Mat Hn = ComputeH21(vPn1i,vPn2i);,并进行评分currentScore = CheckHomography(H21i, H12i, vbCurrentInliers, mSigma);,分别通过单应矩阵将第一帧的特征点投影到第二帧,以及将第二帧的特征点投影到第一帧,计算两次重投影误差,叠加作为评分SH。
(2) 计算基础矩阵cv::Mat Fn = ComputeF21(vPn1i,vPn2i);,并进行评分currentScore = CheckFundamental(F21i, vbCurrentInliers, mSigma);,两次重投影误差叠加作为评分SF。
如果恢复相机初始位姿成功,那么我们能得到前两帧图像的位姿,以及三角测量后的3维地图点。之后进行如下操作:
// Set Frame Poses
// 将初始化的第一帧作为世界坐标系,因此第一帧变换矩阵为单位矩阵
mInitialFrame.SetPose(cv::Mat::eye(4,4,CV_32F));
// 由Rcw和tcw构造Tcw,并赋值给mTcw,mTcw为世界坐标系到该帧的变换矩阵
cv::Mat Tcw = cv::Mat::eye(4,4,CV_32F);
Rcw.copyTo(Tcw.rowRange(0,3).colRange(0,3));
tcw.copyTo(Tcw.rowRange(0,3).col(3));
mCurrentFrame.SetPose(Tcw);
// 步骤6:将三角化得到的3D点包装成MapPoints
// Initialize函数会得到mvIniP3D,
// mvIniP3D是cv::Point3f类型的一个容器,是个存放3D点的临时变量,
// CreateInitialMapMonocular将3D点包装成MapPoint类型存入KeyFrame和Map中
CreateInitialMapMonocular();
先将当前帧位姿Tcw设置好,之后CreateInitialMapMonocular () 创建初始地图,并进行相应的3D点数据关联操作:
(1)创建关键帧
因为只有前两帧,所以将这两帧都设置为关键帧,并计算对应的BoW,并在地图中插入这两个关键帧。
(2)创建地图点+数据关联
将3D点包装成地图点,将地图点与关键帧,地图进行数据关联。
一个地图点可被多个关键帧观测到,将观测到这个地图点的关键帧与这个地图点进行关联;关键帧能够观测到很多地图点,将这些地图点与该关键帧关联。
//关键帧上加地图点
pKFini->AddMapPoint(pMP,i);
pKFcur->AddMapPoint(pMP,mvIniMatches[i]);
//地图点的属性:能够观测到的关键帧
pMP->AddObservation(pKFini,i);
pMP->AddObservation(pKFcur,mvIniMatches[i]);
地图点与关键帧上的特征点关联后,计算最合适的描述子来描述该地图点,用于之后跟踪的匹配。
// b.从众多观测到该MapPoint的特征点中挑选区分读最高的描述子
pMP->ComputeDistinctiveDescriptors();
// c.更新该MapPoint平均观测方向以及观测距离的范围
pMP->UpdateNormalAndDepth();
一个关键帧上特征点由多个地图点投影而成,将关键帧与地图点关联。
//Fill Current Frame structure
mCurrentFrame.mvpMapPoints[mvIniMatches[i]] = pMP;
mCurrentFrame.mvbOutlier[mvIniMatches[i]] = false;
关键帧之间会共视一个地图点,如果共视的地图点个数越多,说明这两个关键帧之间的联系越紧密。对于某个关键帧,统计其与其他关键帧共视的特征点个数,如果大于某个阈值,那么将这两个关键帧进行关联。
pKFini->UpdateConnections();
pKFcur->UpdateConnections();
(3)Global BA
对上述只有两个关键帧和地图点的地图进行全局BA。
// 步骤5:BA优化
Optimizer::GlobalBundleAdjustemnt(mpMap,20);
(4) 地图尺寸初始化(中值深度为1)
由于单目SLAM的的地图点坐标尺寸不是真实的,比如x=1可能是1m也可能是1cm。选择地图点深度的中位数作为单位尺寸1当作基准来进行地图的尺寸初始化
假设匀速运动模型,对上一帧的MapPoints进行跟踪,用上一帧位姿和速度估计当前帧位姿。
(1)上一帧地图点投影到当前帧,完成匹配。
int nmatches = matcher.SearchByProjection(mCurrentFrame,mLastFrame,th,mSensor==System::MONOCULAR);
这是ORBmatcher类中定义的类成员函数SearchByProjection()。
如果匹配点不够20个,扩大面积继续投影。
(2)优化相机位姿,BA算法,最小二乘法。
Optimizer::PoseOptimization(&mCurrentFrame);
(3)剔除优化后的outlier匹配点
在运动模型还未建立或者刚完成重定位时,通过BoW进行当前帧与参考关键帧特征点匹配,进而优化位姿
(1)将当前帧的描述子转化为BoW向量,与关键帧的BoW进行匹配。
mCurrentFrame.ComputeBoW();
int nmatches = matcher.SearchByBoW(mpReferenceKF,mCurrentFrame,vpMapPointMatches);
SearchByBoW()是ORBmatcher的类成员函数。
(2)将上一帧的位姿态作为当前帧位姿的初始值
mCurrentFrame.mvpMapPoints = vpMapPointMatches;
mCurrentFrame.SetPose(mLastFrame.mTcw); // 用上一次的Tcw设置初值,在PoseOptimization可以收敛快一些
(3)通过优化3D-2D的重投影误差来获得位姿
(4)剔除优化后的outlier匹配点(MapPoints)
当跟踪失败,将当前帧转成BoW向量向所有关键帧中查找与当前帧有充足匹配点的候选帧,Ransac迭代,PnP求解位姿,再通过最小化重投影误差对位姿进行优化。
(1)计算当前帧的BoW映射
(2)找到与当前帧相似的候选关键帧
vector vpCandidateKFs = mpKeyFrameDB->DetectRelocalizationCandidates(&mCurrentFrame);
(3)匹配当前帧与找到的候选关键帧,计算相机位姿Tcw(RANSAC+PNP算法)
int nmatches = matcher.SearchByBoW(pKF,mCurrentFrame,vvpMapPointMatches[i]);
通过BoW向量匹配上,之后初始化PnPSolver,使用RANSAC+PNP算法完成位姿估计。
PnPsolver* pSolver = new PnPsolver(mCurrentFrame,vvpMapPointMatches[i]);
pSolver->SetRansacParameters(0.99,10,300,4,0.5,5.991);
vpPnPsolvers[i] = pSolver;
nCandidates++;
PnPsolver* pSolver = vpPnPsolvers[i];
cv::Mat Tcw = pSolver->iterate(5,bNoMore,vbInliers,nInliers);
(4)优化相机位姿Tcw
TrackLocalMap()
。对相机位姿有个基本估计和初始特征匹配,将局部地图投影到当前帧寻找更多对应的匹配地图点MapPoints,优化相机位姿。
随着相机运动,我们向局部地图添加新的关键帧和3D地图点来维护局部地图,这样,即使跟踪过程中某帧出现问题,利用局部地图,我们仍然可以求出之后那些帧的正确位姿。以下是进行局部地图跟踪的流程:
(1)UpdateLocalMap()更新局部地图(关键帧UpdateLocalKeyFrames()+局部地图点UpdateLocalPoints());
更新局部关键帧mvpLocakKeyFrames
步骤如下:
更新局部地图点mvpLocalMapPoints
步骤如下:
mvpLocalMapPoints
;mvpLocalMapPoints.clear();mvpLocakKeyFrames
,将局部关键帧的地图点插入到mvpLocalMapPoints
。(2)SearchLocalPoints()匹配局部地图视野范围内的点和当前帧特征点;
步骤如下:
matcher.SearchByProjection(mCurrentFrame,mvpLocalMapPoints,th);,得到一组3D局部地图点-2D特征点匹配对。
(3)PoseOptimization()根据新的匹配点重新优化相机位姿。