主要参考:https://zhuanlan.zhihu.com/p/349770246,在此基础上对1.0版本源码进行解读
Tracking.cc文件的主要内容有:
- 在构造函数中读取配置文件中的参数。
- 处理图像和imu数据,进行位姿跟踪,最主要的是track( )函数。
1、Tracking构造函数
在实例化system类时,调用Tracking构造函数。
mpTracker = new Tracking(this, mpVocabulary, mpFrameDrawer, mpMapDrawer,
mpAtlas, mpKeyFrameDatabase, strSettingsFile, mSensor, settings_, strSequence);
主要作用:读取相机参数、ORB特征点参数、惯导参数
Tracking::Tracking(System *pSys, ORBVocabulary* pVoc, FrameDrawer *pFrameDrawer, MapDrawer *pMapDrawer,
Atlas *pAtlas, KeyFrameDatabase* pKFDB, const string &strSettingPath, const int sensor, Settings* settings, const string &_nameSeq)
: mState(NO_IMAGES_YET), mSensor(sensor), mTrackedFr(0), mbStep(false),
mbOnlyTracking(false), mbMapUpdated(false), mbVO(false), mpORBVocabulary(pVoc), mpKeyFrameDB(pKFDB),
mbReadyToInitializate(false), mpSystem(pSys), mpViewer(NULL), bStepByStep(false),
mpFrameDrawer(pFrameDrawer), mpMapDrawer(pMapDrawer), mpAtlas(pAtlas), mnLastRelocFrameId(0), time_recently_lost(5.0),
mnInitialFrameId(0), mbCreatedMap(false), mnFirstFrameId(0), mpCamera2(nullptr), mpLastKeyFrame(static_cast<KeyFrame*>(NULL))
{
// Load camera parameters from settings file
// Step 1 从配置文件中加载相机参数
if(settings){
newParameterLoader(settings);
}
else{
cv::FileStorage fSettings(strSettingPath, cv::FileStorage::READ);
//读取相机参数
//如果是ROS,DepthMapFactor应该设为1,即深度不进行缩放
bool b_parse_cam = ParseCamParamFile(fSettings);
if(!b_parse_cam)
{
std::cout << "*Error with the camera parameters in the config file*" << std::endl;
}
// Load ORB parameters
//读取ORB特征提取的相关参数,该函数中还会
//根据参数构造ORB提取器mpORBextractorLeft(左目)、mpORBextractorRight(右目)、mpIniORBextractor(初始化用)
bool b_parse_orb = ParseORBParamFile(fSettings);
if(!b_parse_orb)
{
std::cout << "*Error with the ORB parameters in the config file*" << std::endl;
}
//读取imu参数,该函数中还会
//根据参数构建预积分处理器mpImuPreintegratedFromLastKF
bool b_parse_imu = true;
if(sensor==System::IMU_MONOCULAR || sensor==System::IMU_STEREO || sensor==System::IMU_RGBD)
{
b_parse_imu = ParseIMUParamFile(fSettings);
if(!b_parse_imu)
{
std::cout << "*Error with the IMU parameters in the config file*" << std::endl;
}
mnFramesToResetIMU = mMaxFrames;
}
if(!b_parse_cam || !b_parse_orb || !b_parse_imu)
{
std::cerr << "**ERROR in the config file, the format is not correct**" << std::endl;
try
{
throw -1;
}
catch(exception &e)
{
}
}
}
initID = 0; lastID = 0;
mbInitWith3KFs = false;
mnNumDataset = 0;
// 遍历下地图中的相机,然后打印出来了
vector<GeometricCamera*> vpCams = mpAtlas->GetAllCameras();
std::cout << "There are " << vpCams.size() << " cameras in the atlas" << std::endl;
for(GeometricCamera* pCam : vpCams)
{
std::cout << "Camera " << pCam->GetId();
if(pCam->GetType() == GeometricCamera::CAM_PINHOLE)
{
std::cout << " is pinhole" << std::endl;
}
else if(pCam->GetType() == GeometricCamera::CAM_FISHEYE)
{
std::cout << " is fisheye" << std::endl;
}
else
{
std::cout << " is unknown" << std::endl;
}
}
#ifdef REGISTER_TIMES
vdRectStereo_ms.clear();
vdResizeImage_ms.clear();
vdORBExtract_ms.clear();
vdStereoMatch_ms.clear();
vdIMUInteg_ms.clear();
vdPosePred_ms.clear();
vdLMTrack_ms.clear();
vdNewKF_ms.clear();
vdTrackTotal_ms.clear();
#endif
}
2、开始跟踪
以下是双目情况。单目和RGBD同理
//开启追踪,进行必要的设置和数据处理,随后在GrabImageStereo()内进行位置姿态计算。
mpSLAM->TrackStereo(imLeft,imRight,tImLeft,vImuMeas);
// 计算相机位姿
Sophus::SE3f Tcw = mpTracker->GrabImageStereo(imLeftToFeed,imRightToFeed,timestamp,filename);
GrabImageStereo(imLeftToFeed,imRightToFeed,timestamp,filename)函数
主要作用:
(1) 将彩色图转为灰度图
(2)构造Frame类
(3)跟踪:函数track()
/**
* @brief 输入左右目图像,可以为RGB、BGR、RGBA、GRAY
* 1、将图像转为mImGray和imGrayRight并初始化mCurrentFrame
* 2、进行tracking过程
* 输出世界坐标系到该帧相机坐标系的变换矩阵
* @param imRectLeft 左图
* @param imRectRight 右图
* @param timestamp 时间戳
* @param filename 文件名字,貌似调试用的
*/
Sophus::SE3f Tracking::GrabImageStereo(const cv::Mat &imRectLeft, const cv::Mat &imRectRight, const double ×tamp, string filename)
{
//cout << "GrabImageStereo" << endl;
mImGray = imRectLeft;
cv::Mat imGrayRight = imRectRight;
mImRight = imRectRight;
// step 1 :将RGB或RGBA图像转为单通道灰度图像
if(mImGray.channels()==3)
{
//cout << "Image with 3 channels" << endl;
if(mbRGB)
{
cvtColor(mImGray,mImGray,cv::COLOR_RGB2GRAY);
cvtColor(imGrayRight,imGrayRight,cv::COLOR_RGB2GRAY);
}
else
{
cvtColor(mImGray,mImGray,cv::COLOR_BGR2GRAY);
cvtColor(imGrayRight,imGrayRight,cv::COLOR_BGR2GRAY);
}
}
// 这里考虑得十分周全,甚至连四通道的图像都考虑到了
else if(mImGray.channels()==4)
{
//cout << "Image with 4 channels" << endl;
if(mbRGB)
{
cvtColor(mImGray,mImGray,cv::COLOR_RGBA2GRAY);
cvtColor(imGrayRight,imGrayRight,cv::COLOR_RGBA2GRAY);
}
else
{
cvtColor(mImGray,mImGray,cv::COLOR_BGRA2GRAY);
cvtColor(imGrayRight,imGrayRight,cv::COLOR_BGRA2GRAY);
}
}
//cout << "Incoming frame creation" << endl;
// Step 2 :构造Frame类
if (mSensor == System::STEREO && !mpCamera2)
mCurrentFrame = Frame(
mImGray, // 左目图像
imGrayRight, // 右目图像
timestamp, // 时间戳
mpORBextractorLeft, // 左目特征提取器
mpORBextractorRight, // 右目特征提取器
mpORBVocabulary, // 字典
mK, // 内参矩阵
mDistCoef, // 去畸变参数
mbf, // 基线长度
mThDepth, // 远点,近点的区分阈值
mpCamera); // 相机模型
else if(mSensor == System::STEREO && mpCamera2)
mCurrentFrame = Frame(mImGray,imGrayRight,timestamp,mpORBextractorLeft,mpORBextractorRight,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth,mpCamera,mpCamera2,mTlr);
else if(mSensor == System::IMU_STEREO && !mpCamera2)
mCurrentFrame = Frame(mImGray,imGrayRight,timestamp,mpORBextractorLeft,mpORBextractorRight,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth,mpCamera,&mLastFrame,*mpImuCalib);
else if(mSensor == System::IMU_STEREO && mpCamera2)
mCurrentFrame = Frame(mImGray,imGrayRight,timestamp,mpORBextractorLeft,mpORBextractorRight,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth,mpCamera,mpCamera2,mTlr,&mLastFrame,*mpImuCalib);
//cout << "Incoming frame ended" << endl;
mCurrentFrame.mNameFile = filename;
mCurrentFrame.mnDataset = mnNumDataset;
#ifdef REGISTER_TIMES
vdORBExtract_ms.push_back(mCurrentFrame.mTimeORB_Ext);
vdStereoMatch_ms.push_back(mCurrentFrame.mTimeStereoMatch);
#endif
//cout << "Tracking start" << endl;
// Step 3 :跟踪
Track();
//cout << "Tracking end" << endl;
// 返回位姿
return mCurrentFrame.GetPose();
}
3.Track( )函数
Track( )函数的流程图如下:
错误修正:
处理IMU时间戳异常情况:
- 当前图像时间 < 上一帧图像时间,说明出错了,清楚imu数据新建新的子地图
- 如果当前帧时间 >上一帧时间 +1 ,说明时间发生了跳变,没有imu直接重置地图,有imu处理方式如下:
(1)imu完成了第三次初始化,新建地图
(2)没有完成第三次初始化,重置地图。
重置地图和新建地图的区别,新建是把当前地图保存,在重新弄一个地图,重置是当前地图不要了,重新弄一个地图。
4、利用IMU计算位姿 PredictStateIMU()
有两种情况会用到此函数:
(a)视觉跟丢时用imu预测位姿。
(b)imu模式下,恒速模型跟踪时提供位姿初始值。
注意:
(1)此函数不会直接设置当前帧的位姿,而是记录当前帧的imu到世界坐标系的平移、旋转和速度。在后面TrackLocalMap( )中对位姿优化后才设置当前帧的位姿Tcw。
(2)地图更新(回环、融合、局部BA、IMU初始化时地图会调整)时,利用上一关键帧和距离上一关键帧的预积分,计算当前帧imu的位姿,因为此时关键帧经过了优化调整,认为更准。
(3)地图未更新时,利用前一帧和距离前一帧的预积分,计算当前帧imu的位姿,因为此时关键帧没有做优化调整,而前一帧距离更近,认为更准。
(4)用到的公式为forster预积分论文中的公式(26),需要做移项。
5、预积分 PreintegrateIMU()
算法流程:
(1) 如果上一帧不存在,则不进行预积分;如果没有imu数据,也不进行预积分,直接返回保存时间戳在两帧之间的imu数据至mvImuFromLastFrame
(2)构造预积分器pImuPreintegratedFromLastFrame(这个是上一帧到当前帧的预积分)对于n个imu数据,要进行n-1次计算得到两帧之间的预积分量。首先利用中值积分,得到每次计算预积分的加速度和角速度。对于头和尾的Imu数据,由于不能严格地和图像时间戳对齐,需要进行适当的补偿。
(3)开始计算预积分 IntegrateNewMeasurement( )(这个函数在我的另一篇文章中有说明:ORB-SLAM3源码阅读笔记(4)-ImuTypes),这里需要计算上一帧到当前帧的预积分pImuPreintegratedFromLastFrame,和上一关键帧到当前帧的预积分mpImuPreintegratedFromLastKF(在初始化帧和插入关键帧时会新建一个,地图更新时,PredictStateIMU需要相对于上一关键帧计算位姿)
(4)所有imu数据计算完成之后,记录两个预积分值,并设置当前帧为已预积分状态
6、恒速模型跟踪 TrackWithMotionModel()
算法流程:
(1) 构建ORB匹配器 ORBmatcher。
(2)更新上一帧的位姿和地图点(UpdateLastFrame()),这个函数主要是根据上一帧与它的参考关键帧的相对位姿,乘上它参考关键帧的位姿,来更新上一帧的位姿,即认为相对位姿是准的,而参考关键帧的位姿可能在优化时被调整过了。如果是双目或RGBD模式,还会产生临时地图点,增加匹配,跟踪结束后会删除。
(3)如果是imu模式,则调用PredictStateIMU( )来提供位姿估计的初始值。如果是纯视觉,则用上上帧到上一帧的相对位姿mVelocity,作为上一帧到当前帧的相对位姿,提供位姿估计初始值。
(4)通过将上一帧的地图点投影到当前帧,寻找匹配 matcher.SearchByProjection( )
(5)根据获得的匹配点进行位姿优化,Optimizer::PoseOptimization(&mCurrentFrame)
(6)去除外点,根据内点数判断是否跟踪成功,其中,imu模式总是返回true。
7、跟踪参考关键帧 TrackReferenceKeyFrame( )
有两种情况会用到此函数:
(a)刚刚进行了重定位,则跟踪参考关键帧。
(b)恒速模型为空或恒速跟踪失败。
bool Tracking::TrackReferenceKeyFrame()
{
// Compute Bag of Words vector
mCurrentFrame.ComputeBoW();//计算词袋向量mBowVec和特征向量mFeatVec
// We perform first an ORB matching with the reference keyframe
ORBmatcher matcher(0.7,true);
vector<MapPoint*> vpMapPointMatches;
//通过BoW加速匹配。用到了mFeatVec,两个关键帧中 只有节点相同的特征点才会被比较,相同节点中的特征点采用暴力搜索,并且需要检查方向性,
//并且最优的要明显好于次优的。
int nmatches = matcher.SearchByBoW(mpReferenceKF,mCurrentFrame,vpMapPointMatches);
if(nmatches<15)
{
cout << "TRACK_REF_KF: Less than 15 matches!!\n";
return false;
}
mCurrentFrame.mvpMapPoints = vpMapPointMatches;
mCurrentFrame.SetPose(mLastFrame.mTcw);//用上一帧的位姿作为初始值
//mCurrentFrame.PrintPointDistribution();
// cout << " TrackReferenceKeyFrame mLastFrame.mTcw: " << mLastFrame.mTcw << endl;
Optimizer::PoseOptimization(&mCurrentFrame);//位姿优化
// Discard outliers
int nmatchesMap = 0;
//删除外点,统计内点
for(int i =0; i<mCurrentFrame.N; i++)
{
//if(i >= mCurrentFrame.Nleft) break;
if(mCurrentFrame.mvpMapPoints[i])
{
if(mCurrentFrame.mvbOutlier[i])
{
MapPoint* pMP = mCurrentFrame.mvpMapPoints[i];
mCurrentFrame.mvpMapPoints[i]=static_cast<MapPoint*>(NULL);
mCurrentFrame.mvbOutlier[i]=false;
if(i < mCurrentFrame.Nleft){
pMP->mbTrackInView = false;
}
else{
pMP->mbTrackInViewR = false;
}
pMP->mbTrackInView = false;
pMP->mnLastFrameSeen = mCurrentFrame.mnId;
nmatches--;
}
else if(mCurrentFrame.mvpMapPoints[i]->Observations()>0)
nmatchesMap++;
}
}
if (mSensor == System::IMU_MONOCULAR || mSensor == System::IMU_STEREO)
return true;
else
return nmatchesMap>=10;
}
8、重定位 Relocalization( )
算法流程:
(1) 计算当前帧的BoW
( 2)利用反向索引在关键帧数据库中找到候选关键帧DetectRelocalizationCandidates()
(3)逐一与候选关键帧进行BoW加速匹配 SearchByBoW(),丢弃匹配点数小于15的候选帧,为剩下的候选帧构建MLPnPsolver
(4)对匹配数大于15的候选帧求解MLPnP,得到位姿之后进行位姿优化,如果优化内点数足够(大于50),直接退出循环,返回true
(5)如果优化内点数不够,则进行投影匹配,找到更多的匹配点,再进行位姿优化,如此抢救两次之后优化内点数还是不够的话,该候选帧求解失败,继续下一候选帧,直到所有候选帧求解都失败或有一个候选帧求解成功。
9、跟踪局部地图 TrackLocalMap( )
这个函数主要是利用局部窗口的关键帧和地图点,为当前帧找到更多的匹配地图点,再进行位姿优化,使得位姿更加准确。
算法流程:
(1)首先更新局部关键帧和局部地图点。找到与当前帧共视程度最高的关键帧pKFmax,放入mvpLocalKeyFrames中,将pKFmax的父子关键帧、共视程度最高的10帧关键帧也放入mvpLocalKeyFrames中,如果是ium模式还要将当前帧之前连续的20放入mvpLocalKeyFrames中。
(2)将mvpLocalKeyFrames中关键帧对应的地图点都放入mvpLocalMapPoints中。
(3)通过投影匹配找到与当前帧匹配的局部地图点 SearchByProjection()
(4)进行位姿优化。imu模式下地图更新时PoseInertialOptimizationLastKeyFrame()地图未更新时用PoseInertialOptimizationLastFrame()。
(5)删除无效地图点,根据内点数判断是否跟踪成功
10、关键帧选取策略 NeedNewKeyFrame( )
关键帧插入主要考虑以下几个因素:
(a)程序运行的模式(是否纯定位)、LocalMapping线程的状态等,限制了不能插入关键帧
(b)距离上一次插入关键帧的时间。imu模式有固定的最长插入间隔
(c)跟踪的好坏。跟踪得不好时需要快点插入关键帧。
(d)传感器的类型。单目插入关键帧最频繁。
bool Tracking::NeedNewKeyFrame()
{
//如果是imu模式,没初始化之前每隔0.25s就插入关键帧
if(((mSensor == System::IMU_MONOCULAR) || (mSensor == System::IMU_STEREO)) && !mpAtlas->GetCurrentMap()->isImuInitialized())
{
if (mSensor == System::IMU_MONOCULAR && (mCurrentFrame.mTimeStamp-mpLastKeyFrame->mTimeStamp)>=0.25)
return true;
else if (mSensor == System::IMU_STEREO && (mCurrentFrame.mTimeStamp-mpLastKeyFrame->mTimeStamp)>=0.25)
return true;
else
return false;
}
//纯定位模式不插入关键帧,因为局部建图线程不工作
if(mbOnlyTracking)
return false;
// If Local Mapping is freezed by a Loop Closure do not insert keyframes
//如果Local Mapping被Loop Closure请求停止了,则不插入关键帧
if(mpLocalMapper->isStopped() || mpLocalMapper->stopRequested())
{
return false;
}
// Return false if IMU is initialazing
if (mpLocalMapper->IsInitializing())
return false;
const int nKFs = mpAtlas->KeyFramesInMap();
// Do not insert keyframes if not enough frames have passed from last relocalisation
if(mCurrentFrame.mnId<mnLastRelocFrameId+mMaxFrames && nKFs>mMaxFrames)
{
return false;
}
// Tracked MapPoints in the reference keyframe
int nMinObs = 3;
if(nKFs<=2)
nMinObs=2;
//参考关键帧的地图点中,大于等于最小观测数目的地图点个数,即这些地图点被追踪到了
int nRefMatches = mpReferenceKF->TrackedMapPoints(nMinObs);
// Local Mapping accept keyframes?
bool bLocalMappingIdle = mpLocalMapper->AcceptKeyFrames();
// Check how many "close" points are being tracked and how many could be potentially created.
//统计近点中被跟踪到的个数和未跟踪到的个数(非单目和非单目imu)
int nNonTrackedClose = 0;
int nTrackedClose= 0;
if(mSensor!=System::MONOCULAR && mSensor!=System::IMU_MONOCULAR)
{
//特征点的个数N
int N = (mCurrentFrame.Nleft == -1) ? mCurrentFrame.N : mCurrentFrame.Nleft;
for(int i =0; i<N; i++)
{ //特征点的深度大于0小于远近点阈值
if(mCurrentFrame.mvDepth[i]>0 && mCurrentFrame.mvDepth[i]<mThDepth)
{
if(mCurrentFrame.mvpMapPoints[i] && !mCurrentFrame.mvbOutlier[i])
nTrackedClose++;//该点不是外点,则追踪到的近点加一
else
nNonTrackedClose++;//是外点,未追踪到的近点加一
}
}
}
bool bNeedToInsertClose;//跟踪到的近点不多,但未跟踪到的近点很多,说明跟踪得不好
bNeedToInsertClose = (nTrackedClose<100) && (nNonTrackedClose>70);
//当前帧和参考关键帧跟踪到点的比例。阈值越大,越容易达到插入关键帧的条件
float thRefRatio = 0.75f;
if(nKFs<2)
thRefRatio = 0.4f;
//单目插入最频繁
if(mSensor==System::MONOCULAR)
thRefRatio = 0.9f;
if(mpCamera2) thRefRatio = 0.75f;
if(mSensor==System::IMU_MONOCULAR)
{
if(mnMatchesInliers>350) // Points tracked from the local map
thRefRatio = 0.75f;
else
thRefRatio = 0.90f;
}
// 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 && mSensor!=System::IMU_MONOCULAR && mSensor!=System::IMU_STEREO && (mnMatchesInliers<nRefMatches*0.25 || bNeedToInsertClose) ;
// Condition 2: Few tracked points compared to reference keyframe. Lots of visual odometry compared to map matches.
const bool c2 = (((mnMatchesInliers<nRefMatches*thRefRatio || bNeedToInsertClose)) && mnMatchesInliers>15);
// Temporal condition for Inertial cases
bool c3 = false;
if(mpLastKeyFrame)
{ //imu模式下超过0.5s之后则c3为true
if (mSensor==System::IMU_MONOCULAR)
{
if ((mCurrentFrame.mTimeStamp-mpLastKeyFrame->mTimeStamp)>=0.5)
c3 = true;
}
else if (mSensor==System::IMU_STEREO)
{
if ((mCurrentFrame.mTimeStamp-mpLastKeyFrame->mTimeStamp)>=0.5)
c3 = true;
}
}
bool c4 = false;
if ((((mnMatchesInliers<75) && (mnMatchesInliers>15)) || mState==RECENTLY_LOST) && ((mSensor == System::IMU_MONOCULAR))) // MODIFICATION_2, originally ((((mnMatchesInliers<75) && (mnMatchesInliers>15)) || mState==RECENTLY_LOST) && ((mSensor == System::IMU_MONOCULAR)))
c4=true;
else
c4=false;
if(((c1a||c1b||c1c) && c2)||c3 ||c4)
{
// 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 && mSensor!=System::IMU_MONOCULAR)
{ //非单目和非单目imu模式,且关键帧队列中小于3,可以插入
if(mpLocalMapper->KeyframesInQueue()<3)
return true;
else
return false;
}
else //单目和单目imu模式直接不能插入,因为单目本来就插入比较密集,这里就不需要插入了
return false;
}
}
else
return false;
}