前言
其实前几篇文章都是在继续为Tracking.cc的框架填坑,当然今天的也不例外,今天继续为GrabImageMonocular() 中的跟踪(Track() 函数)填坑,GrabImageMonocular() 首次出现于System.cc中的cv::Mat System::TrackMonocular(const cv::Mat &im, const double ×tamp)。
关于计算单应矩阵H和基础矩阵F、BA什么的后续再开一篇文章写吧,不然这篇文章自己会看不下去。
一入SLAM坑,终生SLAM人。
来,开始GrabImageMonocular() 中的第三步Track() 函数中的第一步单目初始化MonocularInitialization() 。
一、Track()函数
位于Tracking.cc文件中,首次出现于GrabImageMonocular() 函数。
void Tracking::Track()
该函数主要包括三部分: step1:初始化 step2:跟踪 step3:记录位姿信息,用于轨迹复现
继续着重关注单目系统。
二、单目初始化MonocularInitialization()
作用:并行计算基础矩阵F和单应性矩阵H,选取其中一个模型,恢复出最开始两帧之间的相对姿态以及点云得到初始两帧的匹配、相对运动、初始地图点。
流程:
- (未创建单目初始化器)得到用于初始化的第一帧,但初始化需要两帧;
- (已创建单目初始化器)如果当前帧特征点数大于100,则得到用于单目初始化的第二帧;
- 在初始化帧和当前帧中找到匹配的特征点对;
- 如果初始化的两帧之间的匹配点太少,重新初始化;
- 通过H模型或F模型进行单目初始化,得到两帧间相对运动、初始地图点;
- 删除那些无法进行三角化的匹配点;
- 将三角化得到的3D点包装成地图点。
1. 判断单目初始化器是否创建,若没有就创建。
如果后续重新初始化,这个初始化器会被删掉。
if(!mpInitializer)
{
if(mCurrentFrame.mvKeys.size()>100)
{
mInitialFrame = Frame(mCurrentFrame);
mLastFrame = Frame(mCurrentFrame);
mvbPrevMatched.resize(mCurrentFrame.mvKeysUn.size());
for(size_t i=0; i<mCurrentFrame.mvKeysUn.size(); i++)
mvbPrevMatched[i]=mCurrentFrame.mvKeysUn[i].pt;
if(mpInitializer)
delete mpInitializer;
mpInitializer = new Initializer(mCurrentFrame,1.0,200);
fill(mvIniMatches.begin(),mvIniMatches.end(),-1);
return;
}
}
2. 已创建初始化器,判断特征点数目
如果当前帧特征点数太少(小于100),则重新创建新的初始化器; 只有连续两帧的特征点个数都大于100时,才能继续进行初始化过程。
3. 在初始化帧与当前帧中找匹配的特征点对(关键)
此步骤中,创建了一个特征点匹配器,匹配初始化帧和当前帧的特征点对。
ORBmatcher matcher(0.9,true);
int nmatches = matcher.SearchForInitialization(mInitialFrame,mCurrentFrame,mvbPrevMatched,mvIniMatches,100);
初始化参考帧和当前帧的特征点匹配SearchForInitialization()
旋转直方图(方向梯度直方图HOG): 核心思想:用梯度或边缘的方向密度分布,表述出图像中局部目标的表象和形状。 本质:统计梯度信息,而梯度变化通常发生于图像边缘。 实现方法:首先将图像分成若干个相连通的细胞单元(cell),随后采集细胞单元中各像素的梯度或者边缘方向分布,最后组合成 为构成特征的描述器。 相关文献:Histogram of Oriented Gradients
int ORBmatcher::SearchForInitialization(Frame &F1, Frame &F2, vector<cv::Point2f> &vbPrevMatched, vector<int> &vnMatches12, int windowSize)
步骤:
- 构建旋转直方图
- 在圆形窗口内搜索当前帧F2中所有的候选匹配特征点
- 遍历搜索搜索窗口中的所有潜在的匹配候选点,找到最优的和次优的
- 对最优、次优结果进行检查,直至满足阈值:最优/次优比例,删除重复匹配
- 计算匹配点旋转角度差所在的直方图
- 筛除旋转直方图中“非主流”部分
- 保存通过筛选、匹配好的特征点
预备工作:
vnMatches12 = vector<int>(F1.mvKeysUn.size(),-1);
step1:构建旋转直方图,HISTO_LENGTH = 30 (30是指30个bin,就是30个向量容器,360度划分成了30份)
vector<int> rotHist[HISTO_LENGTH];
for(int i=0;i<HISTO_LENGTH;i++)
rotHist[i].reserve(500);
const float factor = 1.0f/HISTO_LENGTH;
vector<int> vMatchedDistance(F2.mvKeysUn.size(),INT_MAX);
vector<int> vnMatches21(F2.mvKeysUn.size(),-1);
for(size_t i1=0, iend1=F1.mvKeysUn.size(); i1<iend1; i1++)
{
cv::KeyPoint kp1 = F1.mvKeysUn[i1];
int level1 = kp1.octave;
if(level1>0)
continue;
step2:在窗口内搜索当前帧F2中所有的候选匹配特征点(GetFeaturesInArea()函数)
vector<size_t> vIndices2 = F2.GetFeaturesInArea(vbPrevMatched[i1].x,vbPrevMatched[i1].y, windowSize,level1,level1);
if(vIndices2.empty())
continue;
cv::Mat d1 = F1.mDescriptors.row(i1);
int bestDist = INT_MAX;
int bestDist2 = INT_MAX;
int bestIdx2 = -1;
step3:遍历搜索搜索窗口中的所有潜在的匹配候选点,找到最优的和次优的
step4:对最优次优结果进行检查,满足阈值、最优/次优比例,删除重复匹配 即使得到了最佳描述子的匹配距离,也不能保证配对成功,要小于设定的阈值。
step5:计算匹配点旋转角度差所在的直方图
step6:筛除旋转直方图中“非主流”部分,为了剔除误匹配的点对(ComputeThreeMaxima()函数)
step7:将通过筛选的匹配好的特征点保存到vbPrevMatched
返回成功匹配的特征点数目。
搜索当前帧F2中的候选匹配特征点GetFeaturesInArea()
找到在以(x,y)为中心,半径为r的圆形内,金字塔层级在[minLevel, maxLevel] 的特征点.
基本流程:计算圆的上下左右边界(网格的索引),然后开始查找遍历网格里是否有符合要求的匹配点,有的话再检查一次是否在圆内(有可能在外接正方形内)。
优点:相比于遍历,速度较快。
vector<size_t> Frame::GetFeaturesInArea(const float &x, const float &y, const float &r, const int minLevel, const int maxLevel) const
图源:@计算机视觉life
step1:计算半径为r的圆左右上下边界所在的网格列和行的id
const int nMinCellX = max(0,(int)floor((x-mnMinX-r)*mfGridElementWidthInv));
if(nMinCellX>=FRAME_GRID_COLS)
return vIndices;
const int nMaxCellX = min((int)FRAME_GRID_COLS-1, (int)ceil((x-mnMinX+r)*mfGridElementWidthInv));
if(nMaxCellX<0)
return vIndices;
...
Step 2 遍历圆形区域内的所有网格,寻找满足条件的候选特征点,并将其index放到输出里
for(int ix = nMinCellX; ix<=nMaxCellX; ix++)
{
for(int iy = nMinCellY; iy<=nMaxCellY; iy++)
{
const vector<size_t> vCell = mGrid[ix][iy];
if(vCell.empty())
continue;
for(size_t j=0, jend=vCell.size(); j<jend; j++)
{
const cv::KeyPoint &kpUn = mvKeysUn[vCell[j]];
if(bCheckLevels)
{
if(kpUn.octave<minLevel)
continue;
if(maxLevel>=0)
if(kpUn.octave>maxLevel)
continue;
}
const float distx = kpUn.pt.x-x;
const float disty = kpUn.pt.y-y;
if(fabs(distx)<r && fabs(disty)<r)
vIndices.push_back(vCell[j]);
}
}
}
return vIndices;
}
筛选旋转角度差落在直方图区间内数量最多的前三个bin的索引ComputeThreeMaxima()
void ORBmatcher::ComputeThreeMaxima(vector<int>* histo, const int L, int &ind1, int &ind2, int &ind3)
函数源码没什么特别难理解的东西,可自行查看。
图(b)是要舍弃的那部分“非主流”方向,即误匹配点。
4. 验证匹配结果,如果初始化的两帧之间的匹配点太少,重新初始化
5. 通过H模型或F模型进行单目初始化,得到两帧间相对运动、初始地图点(最关键)
if(mpInitializer->Initialize(
mCurrentFrame,
mvIniMatches,
Rcw, tcw,
mvIniP3D,
vbTriangulated))
bool Initializer::Initialize(
const Frame &CurrentFrame,
const vector<int> &vMatches12,
cv::Mat &R21, cv::Mat &t21,
vector<cv::Point3f> &vP3D,
vector<bool> &vbTriangulated)
计算基础矩阵和单应性矩阵,选取最佳的来恢复出最开始两帧之间的相对姿态,并进行三角化得到初始地图点
步骤:
- 重新记录特征点对的匹配关系(其实只是放在另一个容器里而已)
- 在所有匹配特征点对中随机选择8对匹配特征点为一组,用于估计H矩阵和F矩阵
- 计算fundamental 矩阵 和homography 矩阵,为了加速分别开了线程计算
- 计算得分比例来判断选取哪个模型来求位姿R,t
返回是否成功初始化,是则为true,否为false。
step1:重新记录特征点对的匹配关系存储在mvMatches12 ,是否有匹配存储在mvbMatched1
for(size_t i=0, iend=vMatches12.size();i<iend; i++)
{
if(vMatches12[i]>=0)
{
mvMatches12.push_back(make_pair(i,vMatches12[i]));
mvbMatched1[i]=true;
}
else
mvbMatched1[i]=false;
}
const int N = mvMatches12.size();
vector<size_t> vAllIndices;
vAllIndices.reserve(N);
vector<size_t> vAvailableIndices;
for(int i=0; i<N; i++)
{
vAllIndices.push_back(i);
}
step2:在所有匹配特征点对中随机选择8对匹配特征点为一组,用于估计H矩阵和F矩阵,此处默认最大迭代次数是200。
step3:计算fundamental矩阵和homography 矩阵,为了加速分别开了线程计算
vector<bool> vbMatchesInliersH, vbMatchesInliersF;
float SH, SF;
cv::Mat H, F;
thread threadH(&Initializer::FindHomography,
this,
ref(vbMatchesInliersH),
ref(SH),
ref(H));
thread threadF(&Initializer::FindFundamental,this,ref(vbMatchesInliersF), ref(SF), ref(F));
threadH.join();
threadF.join();
step4:计算得分比例来判断选取哪个模型来求位姿R,t
float RH = SH/(SH+SF);
if(RH>0.40)
return ReconstructH(vbMatchesInliersH,
H,
mK,
R21,t21,
vP3D,
vbTriangulated,
1.0,
50);
else
return ReconstructF(vbMatchesInliersF,F,mK,R21,t21,vP3D,vbTriangulated,1.0,50);
return false;
6. 初始化后,删除无法进行三角化的匹配点
7. 将初始化的第一帧作为世界坐标系
因为第一帧是世界坐标系,所以第一帧的变换矩阵T为单位矩阵。
8. 创建初始化地图点CreateInitialMapMonocular()
单目相机成功初始化后用三角化得到的点生成地图点。
步骤:
step1:将单目初始化的第一、二帧创建为关键帧
KeyFrame* pKFini = new KeyFrame(mInitialFrame,mpMap,mpKeyFrameDB);
KeyFrame* pKFcur = new KeyFrame(mCurrentFrame,mpMap,mpKeyFrameDB);
KeyFrame::KeyFrame(Frame &F, Map *pMap, KeyFrameDatabase *pKFDB):
{
mnId=nNextId++;
mGrid.resize(mnGridCols);
for(int i=0; i<mnGridCols;i++)
{
mGrid[i].resize(mnGridRows);
for(int j=0; j<mnGridRows; j++)
mGrid[i][j] = F.mGrid[i][j];
}
SetPose(F.mTcw);
}
step2:将初始关键帧、当前关键帧的描述子转为BoW(bag of words),就将初始关键帧和当前关键帧转化为词袋中要用的变量
pKFini->ComputeBoW();
pKFcur->ComputeBoW();
void KeyFrame::ComputeBoW()
{
if(mBowVec.empty() || mFeatVec.empty())
{
vector<cv::Mat> vCurrentDesc = Converter::toDescriptorVector(mDescriptors);
mpORBvocabulary->transform(vCurrentDesc,
mBowVec,
mFeatVec,
4);
}
}
step3:将关键帧插入到地图
mpMap->AddKeyFrame(pKFini);
mpMap->AddKeyFrame(pKFcur);
void Map::AddKeyFrame(KeyFrame *pKF)
{
unique_lock<mutex> lock(mMutexMap);
mspKeyFrames.insert(pKF);
if(pKF->mnId>mnMaxKFid)
mnMaxKFid=pKF->mnId;
}
step4:用初始化得到的3D点来生成地图点 跟前面的一样,mvIniMatches[i] 表示初始化两帧特征点匹配关系,i表示帧1中关键点的索引值,vMatches12[i] 的值为帧2的关键点索引值;没有匹配关系的话,vMatches12[i] 值为 -1。
step4.1:预备工作
for(size_t i=0; i<mvIniMatches.size();i++)
{
cv::Mat worldPos(mvIniP3D[i]);
MapPoint* pMP = new MapPoint(worldPos,pKFcur,mpMap);
step4.2: 为该地图点添加属性:
- 观测到该MapPoint的关键帧
- 该MapPoint的描述子
- 该MapPoint的平均观测方向和深度范围
pKFini->AddMapPoint(pMP,i);
pKFcur->AddMapPoint(pMP,mvIniMatches[i]);
pMP->AddObservation(pKFini,i);
pMP->AddObservation(pKFcur,mvIniMatches[i]);
pMP->ComputeDistinctiveDescriptors();
pMP->UpdateNormalAndDepth();
mCurrentFrame.mvpMapPoints[mvIniMatches[i]] = pMP;
mCurrentFrame.mvbOutlier[mvIniMatches[i]] = false;
mpMap->AddMapPoint(pMP);
计算观测到地图点的特征点中最具有代表性的描述子ComputeDistinctiveDescriptors()
由于一个MapPoint会被许多相机观测到,因此在插入关键帧后,需要判断:是否更新当前地图点最适合的描述子。 先获得当前地图点的所有描述子,然后计算描述子之间的两两距离,最好的描述子与其他描述子应该具有最小的距离中值。
void MapPoint::ComputeDistinctiveDescriptors()
step1:跳过坏点,获取所有观测(observations) 观测:记录能观测到该地图点的关键帧特征点和该特征点关键帧中的索引
step2:遍历观测到3d点的所有关键帧,获得ORB特征点的描述子,并插入到vDescriptors中
for(map<KeyFrame*,size_t>::iterator mit=observations.begin(), mend=observations.end(); mit!=mend; mit++)
{
KeyFrame* pKF = mit->first;
if(!pKF->isBad())
vDescriptors.push_back(pKF->mDescriptors.row(mit->second));
}
step3:获得这些描述子两两之间的距离
const size_t N = vDescriptors.size();
std::vector<std::vector<float> > Distances;
Distances.resize(N, vector<float>(N, 0));
for (size_t i = 0; i<N; i++)
{
Distances[i][i]=0;
for(size_t j=i+1;j<N;j++)
{
int distij = ORBmatcher::DescriptorDistance(vDescriptors[i],vDescriptors[j]);
Distances[i][j]=distij;
Distances[j][i]=distij;
}
}
step4:选择最有代表性的描述子,它与其他描述子应该具有最小的距离中值
for(size_t i=0;i<N;i++)
{
vector<int> vDists(Distances[i].begin(), Distances[i].end());
sort(vDists.begin(), vDists.end());
int median = vDists[0.5*(N-1)];
if(median<BestMedian)
{
BestMedian = median;
BestIdx = i;
}
}
更新该MapPoint平均观测方向以及观测距离的范围UpdateNormalAndDepth()
由于一个MapPoint会被许多相机观测到,因此在插入关键帧后,需要更新相应变量,创建新的关键帧的时候会调用。
void MapPoint::UpdateNormalAndDepth()
step1:获得该地图点的相关信息
observations=mObservations;
pRefKF=mpRefKF;
Pos = mWorldPos.clone();
step2:计算该地图点的法线方向,也就是朝向等信息
能观测到该地图点的所有关键帧,对该点的观测方向归一化为单位向量,然后进行求和得到该地图点的朝向。 初始值为0向量,累加为归一化向量,最后除以总数n。
KeyFrame* pKF = mit->first;
cv::Mat Owi = pKF->GetCameraCenter();
cv::Mat normali = mWorldPos - Owi;
normal = normal + normali/cv::norm(normali);
n++;
}
cv::Mat PC = Pos - pRefKF->GetCameraCenter();
const float dist = cv::norm(PC);
const int level = pRefKF->mvKeysUn[observations[pRefKF]].octave;
const float levelScaleFactor = pRefKF->mvScaleFactors[level];
const int nLevels = pRefKF->mnScaleLevels;
{
unique_lock<mutex> lock3(mMutexPos);
mfMaxDistance = dist*levelScaleFactor;
mfMinDistance = mfMaxDistance/pRefKF->mvScaleFactors[nLevels-1];
mNormalVector = normal/n;
}
step4.3:更新关键帧间的连接关系
pKFini->UpdateConnections();
pKFcur->UpdateConnections();
更新图上的连接关系UpdateConnections(),关键
在没有执行这个函数前,关键帧只有与地图点之间有连接关系,该函数可以更新关键帧之间的连接关系。
流程:
- 首先获得该关键帧的所有地图点,统计观测到这些三维地图点的每个关键帧与其它所有关键帧之间的共视程度。
即对每一个找到的共视关键帧建立一条边,边的权重是该关键帧与当前关键帧公共三维地图点的个数。 - 并且该权重必须大于一个阈值,如果没有超过该阈值的权重,那么就只保留权重最大的边(与其它关键帧的共视程度比较高)
- 对这些连接按照权重从大到小进行排序,以方便将来的处理
更新完共视图之后,如果没有初始化过,则初始化为连接权重最大的边(与其它关键帧共视程度最高的那个关键帧),类似于最大生成树。
源码步骤:
void KeyFrame::UpdateConnections()
{
map<KeyFrame*,int> KFcounter;
vector<MapPoint*> vpMP;
{
unique_lock<mutex> lockMPs(mMutexFeatures);
vpMP = mvpMapPoints;
}
step1:统计每一个地图点都有多少关键帧与当前关键帧存在共视关系,统计结果放在KFcounter
代码有点绕,梳理一下:
最终目标:当前关键帧与共视关键帧的共视程度,即观测到公共三维地图点的个数。
① 获得当前关键帧有可以观测到许多地图点
② 对这些地图点分别进行以下操作:获取所有能观测到这个地图点的关键帧
③ 判断这些关键帧是否为当前关键帧本身
④ 如果不是,那就KFcounter.second值加一,即这个共视关键帧与当前帧的共视程度+1
for(vector<MapPoint*>::iterator vit=vpMP.begin(), vend=vpMP.end(); vit!=vend; vit++)
{
MapPoint* pMP = *vit;
if(!pMP)
continue;
if(pMP->isBad())
continue;
map<KeyFrame*,size_t> observations = pMP->GetObservations();
for(map<KeyFrame*,size_t>::iterator mit=observations.begin(), mend=observations.end(); mit!=mend; mit++)
{
if(mit->first->mnId==mnId)
continue;
KFcounter[mit->first]++;
}
}
step2:找到对应权重最大的关键帧(共视程度最高的共视关键帧)
int nmax=0;
KeyFrame* pKFmax=NULL;
int th = 15;
vector<pair<int,KeyFrame*> > vPairs;
vPairs.reserve(KFcounter.size());
for(map<KeyFrame*,int>::iterator mit=KFcounter.begin(), mend=KFcounter.end(); mit!=mend; mit++)
{
if(mit->second>nmax)
{
nmax=mit->second;
pKFmax=mit->first;
}
if(mit->second>=th)
{
vPairs.push_back(make_pair(mit->second,mit->first));
(mit->first)->AddConnection(this,mit->second);
}
}
为关键帧之间添加或更新连接AddConnection()
void KeyFrame::AddConnection(KeyFrame *pKF, const int &weight)
{
{
unique_lock<mutex> lock(mMutexConnections);
if(!mConnectedKeyFrameWeights.count(pKF))
mConnectedKeyFrameWeights[pKF]=weight;
else if(mConnectedKeyFrameWeights[pKF]!=weight)
mConnectedKeyFrameWeights[pKF]=weight;
else
return;
}
UpdateBestCovisibles();
}
更新连接关系后进行重新排序UpdateBestCovisibles()
其实这个函数就是更新共视图。
void KeyFrame::UpdateBestCovisibles()
{
unique_lock<mutex> lock(mMutexConnections);
vector<pair<int,KeyFrame*> > vPairs;
vPairs.reserve(mConnectedKeyFrameWeights.size());
for(map<KeyFrame*,int>::iterator mit=mConnectedKeyFrameWeights.begin(), mend=mConnectedKeyFrameWeights.end(); mit!=mend; mit++)
vPairs.push_back(make_pair(mit->second,mit->first));
sort(vPairs.begin(),vPairs.end());
list<KeyFrame*> lKFs;
list<int> lWs;
for(size_t i=0, iend=vPairs.size(); i<iend;i++)
{
lKFs.push_front(vPairs[i].second);
lWs.push_front(vPairs[i].first);
}
mvpOrderedConnectedKeyFrames = vector<KeyFrame*>(lKFs.begin(),lKFs.end());
mvOrderedWeights = vector<int>(lWs.begin(), lWs.end());
}
ok,回到CreateInitialMapMonocular()
step5:全局BA(先不展开了)
Optimizer::GlobalBundleAdjustemnt(mpMap,20);
step6:取场景的中值深度,用于尺度归一化
float medianDepth = pKFini->ComputeSceneMedianDepth(2);
float invMedianDepth = 1.0f/medianDepth;
if(medianDepth<0 || pKFcur->TrackedMapPoints(1)<100)
{
cout << "Wrong initialization, reseting..." << endl;
Reset();
return;
}
取场景中值深度ComputeSceneMedianDepth()
评估当前关键帧场景深度,q=2表示中值,只有在单目情况下才会使用。 过程:对当前关键帧下所有地图点的深度进行从小到大排序, 返回距离头部1/q处的深度值作为当前场景的平均深度。
源码没什么,可自行阅读
step7:将两帧之间的变换T归一化到平均深度1的尺度下
step8:把地图点的尺度也归一化到1
step9:将关键帧插入局部地图,更新归一化后的位姿、局部地图点
创建初始地图点CreateInitialMapMonocular() 结束
返回MonocularInitialization() 函数,单目初始化结束。
总结
啊、梳理and整理好长啊。
|