本文接着上一篇对于ORB-SLAM的系统介绍继续记录ORB-SLAM2的主线程tracking的相关内容。有很多细节部分还没有弄清楚,暂时先将整体思路捋顺。本文的内容最好参照着代码一起阅读,由于篇幅较长,并未在文中插入太多代码。在将整个ORB_SLAM2系统阅读完毕后,我在公开我注释的代码。
ORB-SLAM的tracking线程作为系统的主线程,也是SLAM前端视觉里程计的主要内容,实现的主要内容就是确定每一帧图像的位姿和确定关键帧。
由系统整体框架可知,tracking部分的主要内容有以下几个部分:
在阅读了代码之后,发现将tracking部分的主要内容划分为以下几个模块比较合适:
下文将对各个部分进行详述。
Tracking线程由两种模式:1.纯追踪模式 2.同步定位与建图模式(默认模式)
Tracking线程中涉及三种位姿计算方法:运动模型跟踪、参考关键帧跟踪、重定位
Tracking线程有四种状态:1.NO_IMAGES_YET 2.NOT_INITIALIZED 3.OK 4.LOST
Tracking 类中由很多成员变量。直接看代码会很难理解成员变量代表的意义,所以先简单介绍一下成员变量。
其中较为重要的变量有:
变量名 | 变量类型 | 说明 |
---|---|---|
mState | eTrackingState | 跟踪状态标志 |
mbOnlyTracking | bool | 跟踪模式标志 |
mCurrentFrame | Frame | 当前帧 |
mLastFrame | Frame | 上一帧 |
mpReferenceKF | KeyFrame* | 参考关键帧 |
mpLastKeyFrame | KeyFrame* | 上一关键帧 |
mvpLocalKeyFrames | std::vector |
局部地图关键帧 |
mvpLocalMapPoints | std::vector |
局部地图的地图点 |
mpMap | Map* | 指代整个地图 |
mlRelativeFramePoses | list < cv::Mat> | 图像帧与其参考关键帧之间的变换关系链表(用于绘制轨迹) |
mlpReferences | list |
每一帧的参考关键帧 (用于绘制轨迹) |
mlFrameTimes | list< double > | 每一帧的时间戳(用于绘制轨迹) |
其它变量在遇到的时候在做说明!
Tracking线程的入口是TrackStereo(),其中GrabImageStereo()返回位姿。GrabImageStereo()成员函数将输入图像转换为灰度图并构建当前帧,然后调用Track()函数。调用Track()函数表示进入了真正的跟踪流程。
进入跟踪流程后,下面开始介绍其中的各个功能模块。
这里的初始化指的是追踪过程的初始化环节,而不是Tracking类构造函数中的初始化内容。顺便说一下,构造函数中根据配置文件设置了相应的参数并声明了ORB特征点提取器。ORB特征提取的过程在构造当前帧的时候进行。
图像传输正常的情况下,追踪过程的第一步就是判断是否已经初始化。
判断部分相应代码为:
if(mState==NOT_INITIALIZED)// ———————————————未初始化
{
if(mSensor==System::STEREO || mSensor==System::RGBD)
StereoInitialization(); //双目和rgbd地图的初始化
else
MonocularInitialization(); // 单目初始化
mpFrameDrawer->Update(this); //更新帧的观测器
if(mState!=OK)
return;
}
else // ————————————————已初始化
{
。。。
}
下面介绍双目初始化过程。整个双目初始化的流程为:
初始化过程相对简单,逻辑并不复杂。我在代码中进行了注释,在此不在赘述。
/*******************************************************************************
* 函数属性:类Tracking的成员函数StereoInitialization()
* 函数参数介绍:NULL
* 备注:双目或者RGBDSLAM的第一帧处理函数,地图初始化
******************************************************************************/
void Tracking::StereoInitialization()
{
//当前帧关键点的数量大于500,才将此帧作为初始帧并认为其为关键帧
if(mCurrentFrame.N>500)
{
// 设置初始帧的位姿
mCurrentFrame.SetPose(cv::Mat::eye(4,4,CV_32F));
// 将当前帧(第一帧)作为初始关键帧(调用关键帧的构造函数)
KeyFrame* pKFini = new KeyFrame(mCurrentFrame,mpMap,mpKeyFrameDB);
// 将关键帧插入地图中. KeyFrame中包含了地图、反过来地图中也包含了KeyFrame,相互包含
mpMap->AddKeyFrame(pKFini);
// 创建地图点并将其与关键帧建立联系
for(int i=0; i0)
{
cv::Mat x3D = mCurrentFrame.UnprojectStereo(i); //将当前帧的第i个特征点反投影到3D世界坐标系下
MapPoint* pNewMP = new MapPoint(x3D,pKFini,mpMap); //用该特征点构造新的地图点
pNewMP->AddObservation(pKFini,i); //地图点添加关键帧 说明该地图点属于哪一关键帧
pKFini->AddMapPoint(pNewMP,i); //关键帧添加地图点 表明在该关键帧下可以看到该地图点
pNewMP->ComputeDistinctiveDescriptors(); //从众多观测到该MapPoint的特征点中挑选区分读最高的描述子
pNewMP->UpdateNormalAndDepth(); //更新该MapPoint平均观测方向以及观测距离的范围
mpMap->AddMapPoint(pNewMP); //将新的地图点加入到地图中
// 将地图点加入到当前针的mvpMapPoints中,为当前Frame的特征点与MapPoint之间建立联系
mCurrentFrame.mvpMapPoints[i]=pNewMP;
}
}
cout << "New map created with " << mpMap->MapPointsInMap() << " points" << endl;
//在局部地图中添加该初始关键帧
mpLocalMapper->InsertKeyFrame(pKFini);
//更新上一帧为当前帧
mLastFrame = Frame(mCurrentFrame);
mnLastKeyFrameId=mCurrentFrame.mnId;
// 更新上一关键帧为当前关键帧
mpLastKeyFrame = pKFini;
mvpLocalKeyFrames.push_back(pKFini); //将初始关键帧加入到局部地图的关键帧
mvpLocalMapPoints=mpMap->GetAllMapPoints(); //将全部地图点加入到当前局部地图点
mpReferenceKF = pKFini; //将当前关键帧作为参考关键帧
mCurrentFrame.mpReferenceKF = pKFini; //将该关键帧作为当前帧的参考关键帧
mpMap->SetReferenceMapPoints(mvpLocalMapPoints);//将当前局部地图点作为整个地图参考地图点,用于画图
mpMap->mvpKeyFrameOrigins.push_back(pKFini); //将关键帧加入地图的原始的关键帧
mpMapDrawer->SetCurrentCameraPose(mCurrentFrame.mTcw); //将当前帧加入到地图观测器
mState=OK; //更新跟踪状态
}
}
这部分内容是整个跟踪流程中非常重要的一部分,逻辑也较为复杂。
首先说明一下该模块的主要目的是:对当前帧的位姿进行初步估计并优化当前帧对应的地图点,这里之所以说是对位姿进行初步估计是因为在局部地图跟踪环节还要对位姿进行优化。
该部分内容的逻辑流程:
从流程图中可以看到相机位姿跟踪主要涉及三种跟踪方法:运动模型跟踪、参考关键帧跟踪、重定位(跟踪丢失时)。
值得一提的是,代码中有一个 mbVO 变量。这个变量只在纯定位模式下才被使用,mbVO为false表示此帧匹配了很多的MapPoints,跟踪很正常;mbVO为true表明此帧匹配了很少的MapPoints,少于10个,跟踪效果不好。
在纯跟踪模式下,当mbVO为true时,如果速度mVelocity不为空。则运动模型跟踪与重定位同时进行。定位和跟踪的结果分别用bOKMM和bOKReloc表示。 如果是跟踪成功且重定位失败,那么结果为跟踪的结果,但只要是重定位成功,那么整个跟踪过程就正常进行(定位与跟踪,更相信重定位)。 最后只要是跟踪和重定位只要一个成功,那么结果就正常。将最新的关键帧作为reference frame。
该部分代码为:
// 这时候既做跟踪又做重定位,定位和跟踪的结果分别用bOKMM和bOKReloc表示,只要mVelocity不为空就做基于恒速模型跟踪。
// 如果是跟踪成功重定位失败,那么结果借用跟踪的结果,但只要是重定位成功,那么整个跟踪过程就正常进行(定位与跟踪,更相信重定位),
// 最后只要是跟踪和重定位只要一个成功,那么结果就正常。将最新的关键帧作为reference frame
bool bOKMM = false;
bool bOKReloc = false;
vector vpMPsMM;
vector vbOutMM;
cv::Mat TcwMM;
if(!mVelocity.empty())
{
bOKMM = TrackWithMotionModel(); //根据运动模型进行追踪
vpMPsMM = mCurrentFrame.mvpMapPoints;
vbOutMM = mCurrentFrame.mvbOutlier;
TcwMM = mCurrentFrame.mTcw.clone();
}
bOKReloc = Relocalization(); //重定位获取相机位姿
if(bOKMM && !bOKReloc) // 跟踪成功,重定位失败
{
mCurrentFrame.SetPose(TcwMM);
mCurrentFrame.mvpMapPoints = vpMPsMM;
mCurrentFrame.mvbOutlier = vbOutMM;
if(mbVO) // 表明此帧匹配了很少的MapPoints,少于10个,跟踪效果不好
{
for(int i =0; iIncreaseFound(); //则将当前帧的检测到的地图点的查找次数增加
}
}
}
}
else if(bOKReloc) // 重定位成功 则整个跟踪进程正常进行
{
mbVO = false;
}
bOK = bOKReloc || bOKMM;
运动模型跟踪的主要思想是:
具体流程如下图所示:
需要注意的是:这里的速度(mVelocity)指的是上一帧与上上帧之间的位姿变换关系(即,当前帧的前一帧与前两帧之间的位姿变换关系)。
具体的匹配方法和优化方法在后面章节进行阐述。
当运动模型跟踪失败或mVelocity为空时,则需要进行参考关键帧跟踪。
参考关键帧跟踪的主要思想是:
具体流程如下图所示:
参考关键帧跟踪与运动模型跟踪的异同点:
重定位的过程相对与运动模型跟踪和参考关键帧跟踪要复杂一些。可以将重定位的过程总结为 三次匹配三次优化。
重定位过程的主要思路为:
具体流程如下图所示:
重定位的过程中进行了三次匹配三次优化。第一次匹配为词袋匹配,用于初步确定当前帧的地图点。后两次匹配均为投影匹配,目的是为了增加匹配点,为优化位姿做准备。而三次优化的过程是为了根据匹配点不断的优化当前帧的位姿,使其满足要求。
要理解局部地图跟踪的原理,首先要知道ORB_SLAM2是如何定义局部地图的。
局部地图包括两部分:局部地图关键帧,局部地图地图点。
那么如何确定哪些关键帧应该作为局部地图的关键帧呢?ORB_SLAM2采用的是这张策略:
局部地图的地图点即为局部关键帧的所有地图点的集合。
在了解了局部地图的定义之后,再来看局部地图跟踪的目的。
局部地图跟踪的目的就是:在得到当前帧的初始位姿之后,根据局部地图中的地图点和当前帧进行匹配,然后根据匹配结果进一步优化当前帧的位姿和当前帧的地图点。
局部地图跟踪的主要思路为:
局部地图跟踪的流程如下图所示:
相机位姿跟踪与局部地图跟踪之间有什么区别和联系?
如果相机位姿跟踪和局部地图跟踪都正常的话,接下来需要考虑的问题就是否需要将当前帧作为新的关键帧进行存储。
在ORB_SLAM论文中,插入新关键帧的条件是:
代码中插入关键帧的判定条件与论文稍微有点区别。
代码如下(ORB_SLAM代码):
// Condition 1a: More than "MaxFrames" have passed from last keyframe insertion
const bool c1a = mCurrentFrame.mnId>=mnLastKeyFrameId+mMaxFrames;
// Condition 1b: More than "MinFrames" have passed and Local Mapping is idle
const bool c1b = mCurrentFrame.mnId>=mnLastKeyFrameId+mMinFrames && bLocalMappingIdle;
// Condition 2: Less than 90% of points than reference keyframe and enough inliers
const bool c2 = mnMatchesInliers15;
if((c1a||c1b)&&c2)
{
// If the mapping accepts keyframes insert, otherwise send a signal to interrupt BA, but not insert yet
if(bLocalMappingIdle)
{
return true;
}
else
{
mpLocalMapper->InterruptBA();
return false;
}
}
else
return false;
从代码中可以看到,插入关键帧的判定条件有三条:
在做判定时,这三条判定条件的逻辑关系是 (c1a||c1b)&&c2 ,而并非单纯的" 与 "关系。此外,当满足判定条件时仍然需要在建图线程空闲时才能插入关键帧。
以上内容是ORB_SLAM中插入关键帧的判定依据,作者在ORB_SLAM2中对该判定依据又进行了拓展。
ORB_SLAM2论文进行了如下表述。
可以看出,ORB_SLAM2引入了近点和原点来拓展判断依据,从而使系统能够应当更有挑战性的场景。
ORNB_SLAM2中该部分内容的代码为:
//近点中被追踪数量小于100 并且近点未被追踪点数量大于70
bool bNeedToInsertClose = (nTrackedClose<100) && (nNonTrackedClose>70);
// Thresholds 设置阈值 参考比率(当前帧内点数量/参考关键帧中所有地图点被观察到的次数大于2或3次的地图点数量)
float thRefRatio = 0.75f;
if(nKFs<2)//如果整个地图中关键帧的数量小于2 则阈值设为0.4
thRefRatio = 0.4f;
if(mSensor==System::MONOCULAR)// 如果是单目相机 比例设为0.9
thRefRatio = 0.9f;
// Condition 1a: More than "MaxFrames" have passed from last keyframe insertion
const bool c1a = mCurrentFrame.mnId>=mnLastKeyFrameId + mMaxFrames;
// Condition 1b: More than "MinFrames" have passed and Local Mapping is idle
const bool c1b = (mCurrentFrame.mnId>=mnLastKeyFrameId+ mMinFrames && bLocalMappingIdle);
//Condition 1c: tracking is weak
const bool c1c = mSensor!=System::MONOCULAR && (mnMatchesInliers 15);
if((c1a||c1b||c1c)&&c2)
{
// If the mapping accepts keyframes, insert keyframe.
// Otherwise send a signal to interrupt BA
if(bLocalMappingIdle) //如果局部建图线程处于空闲状态则返回真
{
return true;
}
else //否则中断BA优化
{
mpLocalMapper->InterruptBA();
if(mSensor!=System::MONOCULAR)
{
if(mpLocalMapper->KeyframesInQueue()<3)// 如果关键帧队列中关键帧数量小于3 则添加关键帧
return true;
else
return false;
}
else
return false;
}
}
else
return false;
从代码中可以看到,插入关键帧的判定条件为:
在做判定时,这三条判定条件的逻辑关系是(c1a||c1b||c1c)&&c2,当满足判定条件时仍然考虑建图线程是否空闲。
在了解了插入关键帧的判定依据后,来看判定插入关键帧整个流程(该流程为ORB_SLAM2中的流程,而非ORB_SLAM)。
具体流程如下图所示。
从流程图中可以看出,是否需要插入关键帧不仅仅需要考虑3.3.1中提到的判定条件还有其他前提条件。
一旦当前帧满足插入关键帧的条件,接下来要做的就是创建关键帧并插入到建图线程中。
该过程主要由以下几步:
在经过相机位姿跟踪、局部地图跟踪和关键帧处理后,跟踪线程的大部分工作已经完成。其中还有一些其他的处理细节在这里没有写出,但是那部分代码比较好理解,属于边边角角的工作。
为了在程序结束后能够将相机的运动轨迹画出来,跟踪线程中提供了姿态保存的内容。
姿态保存部分分为两种情况:跟踪成功和跟踪丢失(通过当前帧的位姿是否为空来判定)。
在跟踪成功时,保存位姿的方式为:
Tcr
。Tcr
保存到mlRelativeFramePoses
链表中。mpReferenceKF
保存到mlpReferences
链表中。mlFrameTimes
和mlbLost
链表中。在跟踪丢失时,保存位姿的方式为:
mlRelativeFramePoses
链表中的最后一个元素再次存入该链表(相当于在链表末端复制了最后一个元素)。mlpReferences
链表和mlFrameTimes
链表的最后一个元素再次存入该链表。mlbLost
链表中。以这种方式保存姿态,在绘制运动轨迹时需要根据参考关键帧的位姿和Tcr
来计算每一帧的位姿。另外,跟踪丢失是运动轨迹将停止在最后一帧跟踪成功的图像帧的位置,直到重定位成功或系统重启。
至此,已经把ORB_SLAM2的Tracking线程的整体流程梳理了一遍。内容并不是很详细,后期如果有新的体会或感悟再来更新。
参考博客:
orb_slam代码解析(2)Tracking线程
一起学ORBSLAM2(4)tracking主线程