ORB-SLAM2算法13之跟踪线程Tracking

文章目录

  • 0 引言
  • 1 跟踪线程Tracking
    • 1.1 概述
    • 1.2 初始化
      • 1.2.1 单目初始化
      • 1.2.2 双目/RGBD初始化
    • 1.3 跟踪方法
      • 1.3.1 恒速模型跟踪
      • 1.3.2 参考关键帧跟踪
      • 1.3.3 重定位跟踪
    • 1.4 局部地图跟踪
      • 1.4.1 流程
      • 1.4.2 更新局部关键帧
      • 1.4.3 更新局部地图点
      • 1.4.4 进一步优化
    • 1.5 关键帧生成
      • 1.5.1 生成条件
      • 1.5.2 创建关键帧

0 引言

ORB-SLAM2算法7详细了解了System主类和多线程、ORB-SLAM2学习笔记8详细了解了图像特征点提取和描述子的生成、ORB-SLAM2算法9详细了解了图像帧、ORB-SLAM2算法10详细了解了图像关键帧、ORB-SLAM2算法11详细了解了地图点及ORB-SLAM2算法12详细了解了单目初始化,本文主要学习ORB-SLAM2中的三大线程中的跟踪线程Tracking

ORB-SLAM2算法13之跟踪线程Tracking_第1张图片

1 跟踪线程Tracking

1.1 概述

总的来说2种模式+3个目的+3种方法+5种状态。

Tracking跟踪线程有2种模式:SLAM模式(定位和建图)和Localization模式(仅定位),由变量mbOnlyTracking来控制两种模式:

  1. mbOnlyTracking默认为false,即SLAM模式,此时三大线程都在工作,定位的同时也在建图;
  2. mbOnlyTracking默认为true,即为Localization模式,此时只有Tracking线程工作,只定位,输出追踪的位姿,不会更新地图和关键帧。

Tracking跟踪线程有3种目的:

  1. 更新精确的位姿
  2. 更新地图点
  3. 更新关键帧

Tracking跟踪线程有3种方法:

  1. 恒速模型跟踪 TrackWithMotionModel()
  2. 参考关键帧跟踪 TrackReferenceKeyFrame()
  3. 重定位跟踪 Relocalization()

Tracking跟踪线程有5种状态:

  1. SYSTEM_NOT_READY: 系统没有准备好的状态;
  2. NO_IMAGES_YET :当前无图像;
  3. NOT_INITIALIZED: 有图像但是没有完成初始化;
  4. OK: 正常时候的工作状态;
  5. LOST :系统已经跟丢了的状态。

详细的流程图如下(参考3):

ORB-SLAM2算法13之跟踪线程Tracking_第2张图片

1.2 初始化

1.2.1 单目初始化

ORB-SLAM2算法12已经详细了解了单目初始化,这里就再简单列举主要的几个步骤:

  1. 得到用于初始化的第一帧,初始化需要两帧;
  2. 如果当前帧特征点数大于100,则得到用于单目初始化的第二帧;
  3. mInitialFramemCurrentFrame中找匹配的特征点对;
  4. 如果初始化的两帧之间的匹配点太少,重新初始化;
  5. 通过 H H H模型或 F F F模型进行单目初始化,得到两帧之间相对运动,初始化MapPoints
  6. 删除那些无法进行三角化的匹配点;
  7. 将三角化得到的3D点包装成MapPoints
# 单目初始化

void Tracking::MonocularInitialization()
{
    // Step 1 如果单目初始器还没有被创建,则创建。后面如果重新初始化时会清掉这个
    if(!mpInitializer)
    {
        // Set Reference Frame
        // 单目初始帧的特征点数必须大于100
        if(mCurrentFrame.mvKeys.size()>100)
        {
            // 初始化需要两帧,分别是mInitialFrame,mCurrentFrame
            mInitialFrame = Frame(mCurrentFrame);
            // 用当前帧更新上一帧
            mLastFrame = Frame(mCurrentFrame);
            // mvbPrevMatched  记录"上一帧"所有特征点
            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;

            // 由当前帧构造初始器 sigma:1.0 iterations:200
            mpInitializer =  new Initializer(mCurrentFrame,1.0,200);

            // 初始化为-1 表示没有任何匹配。这里面存储的是匹配的点的id
            fill(mvIniMatches.begin(),mvIniMatches.end(),-1);

            return;
        }
    }
    else    //如果单目初始化器已经被创建
    {
        // Try to initialize
        // Step 2 如果当前帧特征点数太少(不超过100),则重新构造初始器
        // NOTICE 只有连续两帧的特征点个数都大于100时,才能继续进行初始化过程
        if((int)mCurrentFrame.mvKeys.size()<=100)
        {
            delete mpInitializer;
            mpInitializer = static_cast<Initializer*>(NULL);
            fill(mvIniMatches.begin(),mvIniMatches.end(),-1);
            return;
        }

        // Find correspondences
        // Step 3 在mInitialFrame与mCurrentFrame中找匹配的特征点对
        ORBmatcher matcher(
            0.9,        //最佳的和次佳特征点评分的比值阈值,这里是比较宽松的,跟踪时一般是0.7
            true);      //检查特征点的方向

        // 对 mInitialFrame,mCurrentFrame 进行特征点匹配
        // mvbPrevMatched为参考帧的特征点坐标,初始化存储的是mInitialFrame中特征点坐标,匹配后存储的是匹配好的当前帧的特征点坐标
        // mvIniMatches 保存参考帧F1中特征点是否匹配上,index保存是F1对应特征点索引,值保存的是匹配好的F2特征点索引
        int nmatches = matcher.SearchForInitialization(
            mInitialFrame,mCurrentFrame,    //初始化时的参考帧和当前帧
            mvbPrevMatched,                 //在初始化参考帧中提取得到的特征点
            mvIniMatches,                   //保存匹配关系
            100);                           //搜索窗口大小

        // Check if there are enough correspondences
        // Step 4 验证匹配结果,如果初始化的两帧之间的匹配点太少,重新初始化
        if(nmatches<100)
        {
            delete mpInitializer;
            mpInitializer = static_cast<Initializer*>(NULL);
            return;
        }

        cv::Mat Rcw; // Current Camera Rotation
        cv::Mat tcw; // Current Camera Translation
        vector<bool> vbTriangulated; // Triangulated Correspondences (mvIniMatches)

        // Step 5 通过H模型或F模型进行单目初始化,得到两帧间相对运动、初始MapPoints
        if(mpInitializer->Initialize(
            mCurrentFrame,      //当前帧
            mvIniMatches,       //当前帧和参考帧的特征点的匹配关系
            Rcw, tcw,           //初始化得到的相机的位姿
            mvIniP3D,           //进行三角化得到的空间点集合
            vbTriangulated))    //以及对应于mvIniMatches来讲,其中哪些点被三角化了
        {
            // Step 6 初始化成功后,删除那些无法进行三角化的匹配点
            for(size_t i=0, iend=mvIniMatches.size(); i<iend;i++)
            {
                if(mvIniMatches[i]>=0 && !vbTriangulated[i])
                {
                    mvIniMatches[i]=-1;
                    nmatches--;
                }
            }

            // Set Frame Poses
            // Step 7 将初始化的第一帧作为世界坐标系,因此第一帧变换矩阵为单位矩阵
            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);

            // Step 8 创建初始化地图点MapPoints
            // Initialize函数会得到mvIniP3D,
            // mvIniP3D是cv::Point3f类型的一个容器,是个存放3D点的临时变量,
            // CreateInitialMapMonocular将3D点包装成MapPoint类型存入KeyFrame和Map中
            CreateInitialMapMonocular();
        }//当初始化成功的时候进行
    }//如果单目初始化器已经被创建
}

1.2.2 双目/RGBD初始化

双目/RGBD初始化比单目初始化简单点,其中双目可通过视差获取深度,RGBD相机直接获取深度,然后得到3D点,创建地图点并完成初始化。

  1. 首先要求当前帧的特征点数大于500
  2. 然后设置当前帧的初始位姿为单位矩阵;
  3. 将当前初始化帧构造成初始关键帧,包括关键帧ID,位姿等;
  4. 将初始关键帧添加到地图中;
  5. 为具有真正深度的特征点构造MapPoint(深度为正的点通过反投影得到该特征点在世界坐标系下的3D坐标,构造3D点为地图点,给地图点添加属性,添加地图点到地图中,记录特征点与地图点的索引);
  6. 将该初始关键帧添加到局部地图中;
  7. 更新状态量。
/*
 * @brief 双目和rgbd的地图初始化,比单目简单很多
 *
 * 由于具有深度信息,直接生成MapPoints
 */
void Tracking::StereoInitialization()
{
    // 初始化要求当前帧的特征点超过500
    if(mCurrentFrame.N>500)
    {
        // Set Frame pose to the origin
        // 设定初始位姿为单位旋转,0平移
        mCurrentFrame.SetPose(cv::Mat::eye(4,4,CV_32F));

        // Create KeyFrame
        // 将当前帧构造为初始关键帧
        // mCurrentFrame的数据类型为Frame
        // KeyFrame包含Frame、地图3D点、以及BoW
        // KeyFrame里有一个mpMap,Tracking里有一个mpMap,而KeyFrame里的mpMap都指向Tracking里的这个mpMap
        // KeyFrame里有一个mpKeyFrameDB,Tracking里有一个mpKeyFrameDB,而KeyFrame里的mpMap都指向Tracking里的这个mpKeyFrameDB
        KeyFrame* pKFini = new KeyFrame(mCurrentFrame,mpMap,mpKeyFrameDB);

        // Insert KeyFrame in the map
        // KeyFrame中包含了地图、反过来地图中也包含了KeyFrame,相互包含
        // 在地图中添加该初始关键帧
        mpMap->AddKeyFrame(pKFini);

        // Create MapPoints and asscoiate to KeyFrame
        // 为每个特征点构造MapPoint
        for(int i=0; i<mCurrentFrame.N;i++)
        {
            //只有具有正深度的点才会被构造地图点
            float z = mCurrentFrame.mvDepth[i];
            if(z>0)
            {
                // 通过反投影得到该特征点的世界坐标系下3D坐标
                cv::Mat x3D = mCurrentFrame.UnprojectStereo(i);
                // 将3D点构造为MapPoint
                MapPoint* pNewMP = new MapPoint(x3D,pKFini,mpMap);

                // 为该MapPoint添加属性:
                // a.观测到该MapPoint的关键帧
                // b.该MapPoint的描述子
                // c.该MapPoint的平均观测方向和深度范围

                // a.表示该MapPoint可以被哪个KeyFrame的哪个特征点观测到
                pNewMP->AddObservation(pKFini,i);
                // b.从众多观测到该MapPoint的特征点中挑选区分度最高的描述子             
                pNewMP->ComputeDistinctiveDescriptors();
                // c.更新该MapPoint平均观测方向以及观测距离的范围
                pNewMP->UpdateNormalAndDepth();

                // 在地图中添加该MapPoint
                mpMap->AddMapPoint(pNewMP);
                // 表示该KeyFrame的哪个特征点可以观测到哪个3D点
                pKFini->AddMapPoint(pNewMP,i);

                // 将该MapPoint添加到当前帧的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;

        // 把当前(最新的)局部MapPoints作为ReferenceMapPoints
        // ReferenceMapPoints是DrawMapPoints函数画图的时候用的
        mpMap->SetReferenceMapPoints(mvpLocalMapPoints);
        mpMap->mvpKeyFrameOrigins.push_back(pKFini);
        mpMapDrawer->SetCurrentCameraPose(mCurrentFrame.mTcw);

        //追踪成功
        mState=OK;
    }
}

1.3 跟踪方法

1.3.1 恒速模型跟踪

该跟踪方法是ORB-SLAM2中的常用方法,如果不是没有运动速度刚完成重定位,就会优先选择恒速模型跟踪。基本思想是假设物体处于匀速运动,用上一帧的位姿和速度来估计当前帧的位姿,而匹配是通过投影来与上一帧看到的地图点进行匹配。详细的实现步骤如下:

  1. 创建ORB特征提取器,并设定最小距离 < 0.9 ∗ * 次小距离才算匹配成功;
  2. 更新上一帧的位姿,上一帧位姿等于上一帧到其参考帧位姿  ∗ *  其参考帧到世界坐标系(系统第一帧)位姿;
  3. 根据之前估计的速度,初始化当前帧的位姿,等于运动速度 ∗ * 上一帧的位姿;
  4. 用上一帧地图点来进行投影匹配,如果匹配点不够,则扩大搜索半径再来一次,如果还是不能获得足够的匹配点,则认为跟踪失败;
  5. 利用3D-2D投影关系,优化当前帧位姿;
  6. 剔除地图点中外点,并累加成功匹配到的地图点数目;
  7. 如果累加成功匹配的地图点数目超过10个点就认为跟踪成功。
// 根据恒速模型用上一帧地图点来对当前帧进行跟踪

bool Tracking::TrackWithMotionModel()
{
    // 最小距离 < 0.9*次小距离 匹配成功,检查旋转
    ORBmatcher matcher(0.9,true);

    // Update last frame pose according to its reference keyframe
    // Create "visual odometry" points
    // Step 1:更新上一帧的位姿;对于双目或RGB-D相机,还会根据深度值生成临时地图点
    UpdateLastFrame();

    // Step 2:根据之前估计的速度,用恒速模型得到当前帧的初始位姿。
    mCurrentFrame.SetPose(mVelocity*mLastFrame.mTcw);
    
    // 清空当前帧的地图点
    fill(mCurrentFrame.mvpMapPoints.begin(),mCurrentFrame.mvpMapPoints.end(),static_cast<MapPoint*>(NULL));

    // Project points seen in previous frame
    // 设置特征匹配过程中的搜索半径
    int th;
    if(mSensor!=System::STEREO)
        th=15;//单目
    else
        th=7;//双目

    // Step 3:用上一帧地图点进行投影匹配,如果匹配点不够,则扩大搜索半径再来一次
    int nmatches = matcher.SearchByProjection(mCurrentFrame,mLastFrame,th,mSensor==System::MONOCULAR);

    // If few matches, uses a wider window search
    // 如果匹配点太少,则扩大搜索半径再来一次
    if(nmatches<20)
    {
        fill(mCurrentFrame.mvpMapPoints.begin(),mCurrentFrame.mvpMapPoints.end(),static_cast<MapPoint*>(NULL));
        nmatches = matcher.SearchByProjection(mCurrentFrame,mLastFrame,2*th,mSensor==System::MONOCULAR); // 2*th
    }

    // 如果还是不能够获得足够的匹配点,那么就认为跟踪失败
    if(nmatches<20)
        return false;

    // Optimize frame pose with all matches
    // Step 4:利用3D-2D投影关系,优化当前帧位姿
    Optimizer::PoseOptimization(&mCurrentFrame);

    // Discard outliers
    // Step 5:剔除地图点中外点
    int nmatchesMap = 0;
    for(int i =0; i<mCurrentFrame.N; i++)
    {
        if(mCurrentFrame.mvpMapPoints[i])
        {
            if(mCurrentFrame.mvbOutlier[i])
            {
                // 如果优化后判断某个地图点是外点,清除它的所有关系
                MapPoint* pMP = mCurrentFrame.mvpMapPoints[i];

                mCurrentFrame.mvpMapPoints[i]=static_cast<MapPoint*>(NULL);
                mCurrentFrame.mvbOutlier[i]=false;
                pMP->mbTrackInView = false;
                pMP->mnLastFrameSeen = mCurrentFrame.mnId;
                nmatches--;
            }
            else if(mCurrentFrame.mvpMapPoints[i]->Observations()>0)
                // 累加成功匹配到的地图点数目
                nmatchesMap++;
        }
    }    

    if(mbOnlyTracking)
    {
        // 纯定位模式下:如果成功追踪的地图点非常少,那么这里的mbVO标志就会置位
        mbVO = nmatchesMap<10;
        return nmatches>20;
    }

    // Step 6:匹配超过10个点就认为跟踪成功
    return nmatchesMap>=10;
}

1.3.2 参考关键帧跟踪

该跟踪方法是相机没有运动刚完成重定位恒速运动模型下匹配较少特征点的情况下使用。基本思想是在以上一种条件下尝试和最近一个关键帧mpReferenceKF去做特征匹配,匹配过程中使用BOW向量的正向索引进行加速。详细的实现步骤如下:

  1. 计算当前帧特征描述子,并转化为BOW向量;
  2. 通过词袋BOW加速当前帧与参考帧之间的特征点匹配,如果匹配数小于15,则跟踪失败;
  3. 将上一帧的位姿作为当前帧位姿的初始值;
  4. 通过优化3D-2D的重投影误差来获得位姿;
  5. 剔除优化后的匹配点中的外点,并累加成功匹配到的地图点数目;
  6. 如果累加成功匹配的地图点数目超过10个点就认为跟踪成功。
// 用参考关键帧的地图点来对当前普通帧进行跟踪

bool Tracking::TrackReferenceKeyFrame()
{
    // Compute Bag of Words vector
    // Step 1:将当前帧的描述子转化为BoW向量
    mCurrentFrame.ComputeBoW();

    // We perform first an ORB matching with the reference keyframe
    // If enough matches are found we setup a PnP solver
    ORBmatcher matcher(0.7,true);
    vector<MapPoint*> vpMapPointMatches;

    // Step 2:通过词袋BoW加速当前帧与参考帧之间的特征点匹配
    int nmatches = matcher.SearchByBoW(
        mpReferenceKF,          //参考关键帧
        mCurrentFrame,          //当前帧
        vpMapPointMatches);     //存储匹配关系

    // 匹配数目小于15,认为跟踪失败
    if(nmatches<15)
        return false;

    // Step 3:将上一帧的位姿态作为当前帧位姿的初始值
    mCurrentFrame.mvpMapPoints = vpMapPointMatches;
    mCurrentFrame.SetPose(mLastFrame.mTcw); // 用上一次的Tcw设置初值,在PoseOptimization可以收敛快一些

    // Step 4:通过优化3D-2D的重投影误差来获得位姿
    Optimizer::PoseOptimization(&mCurrentFrame);

    // Discard outliers
    // Step 5:剔除优化后的匹配点中的外点
    //之所以在优化之后才剔除外点,是因为在优化的过程中就有了对这些外点的标记
    int nmatchesMap = 0;
    for(int i =0; i<mCurrentFrame.N; i++)
    {
        if(mCurrentFrame.mvpMapPoints[i])
        {
            //如果对应到的某个特征点是外点
            if(mCurrentFrame.mvbOutlier[i])
            {
                //清除它在当前帧中存在过的痕迹
                MapPoint* pMP = mCurrentFrame.mvpMapPoints[i];

                mCurrentFrame.mvpMapPoints[i]=static_cast<MapPoint*>(NULL);
                mCurrentFrame.mvbOutlier[i]=false;
                pMP->mbTrackInView = false;
                pMP->mnLastFrameSeen = mCurrentFrame.mnId;
                nmatches--;
            }
            else if(mCurrentFrame.mvpMapPoints[i]->Observations()>0)
                //匹配的内点计数++
                nmatchesMap++;
        }
    }
    // 跟踪成功的数目超过10才认为跟踪成功,否则跟踪失败
    return nmatchesMap>=10;
}

1.3.3 重定位跟踪

假如恒速模型跟踪失败,并且参考关键帧跟踪也失败,则需要进行重定位。基本思想是位置丢失后,使用当前帧的BOW特征向量在关键帧数据库中匹配最相近的关键帧,从而求出当前帧位姿,但由于此刻没有好的初始位姿信息,需要使用传统的3D-2D匹配点的EPnP算法来求解出一个初始位姿,然后再使用最小化重投影误差来优化更新位姿。详细的实现步骤如下:

  1. 计算当前帧特征描述子,并转化为BOW向量;
  2. 初次筛选找到与当前帧相似的候选关键帧;
  3. 通过BOW二次筛选候选帧;
  4. 通过EPnP算法估计姿态;
  5. 通过PoseOptimization对姿态进行优化求解;
  6. 如果内点较少,则通过投影的方式对之前未匹配的点进行匹配,再进行优化求解。
// 重定位跟踪

bool Tracking::Relocalization()
{
    // Compute Bag of Words Vector
    // Step 1:计算当前帧特征点的词袋向量
    mCurrentFrame.ComputeBoW();

    // Relocalization is performed when tracking is lost
    // Track Lost: Query KeyFrame Database for keyframe candidates for relocalisation
    // Step 2:用词袋找到与当前帧相似的候选关键帧
    vector<KeyFrame*> vpCandidateKFs = mpKeyFrameDB->DetectRelocalizationCandidates(&mCurrentFrame);
    
    // 如果没有候选关键帧,则退出
    if(vpCandidateKFs.empty())
        return false;

    const int nKFs = vpCandidateKFs.size();

    // We perform first an ORB matching with each candidate
    // If enough matches are found we setup a PnP solver
    ORBmatcher matcher(0.75,true);
    //每个关键帧的解算器
    vector<PnPsolver*> vpPnPsolvers;
    vpPnPsolvers.resize(nKFs);

    //每个关键帧和当前帧中特征点的匹配关系
    vector<vector<MapPoint*> > vvpMapPointMatches;
    vvpMapPointMatches.resize(nKFs);
    
    //放弃某个关键帧的标记
    vector<bool> vbDiscarded;
    vbDiscarded.resize(nKFs);

    //有效的候选关键帧数目
    int nCandidates=0;

    // Step 3:遍历所有的候选关键帧,通过词袋进行快速匹配,用匹配结果初始化PnP Solver
    for(int i=0; i<nKFs; i++)
    {
        KeyFrame* pKF = vpCandidateKFs[i];
        if(pKF->isBad())
            vbDiscarded[i] = true;
        else
        {
            // 当前帧和候选关键帧用BoW进行快速匹配,匹配结果记录在vvpMapPointMatches,nmatches表示匹配的数目
            int nmatches = matcher.SearchByBoW(pKF,mCurrentFrame,vvpMapPointMatches[i]);
            // 如果和当前帧的匹配数小于15,那么只能放弃这个关键帧
            if(nmatches<15)
            {
                vbDiscarded[i] = true;
                continue;
            }
            else
            {
                // 如果匹配数目够用,用匹配结果初始化EPnPsolver
                // 为什么用EPnP? 因为计算复杂度低,精度高
                PnPsolver* pSolver = new PnPsolver(mCurrentFrame,vvpMapPointMatches[i]);
                pSolver->SetRansacParameters(
                    0.99,   //用于计算RANSAC迭代次数理论值的概率
                    10,     //最小内点数, 但是要注意在程序中实际上是min(给定最小内点数,最小集,内点数理论值),不一定使用这个
                    300,    //最大迭代次数
                    4,      //最小集(求解这个问题在一次采样中所需要采样的最少的点的个数,对于Sim3是3,EPnP是4),参与到最小内点数的确定过程中
                    0.5,    //这个是表示(最小内点数/样本总数);实际上的RANSAC正常退出的时候所需要的最小内点数其实是根据这个量来计算得到的
                    5.991); // 自由度为2的卡方检验的阈值,程序中还会根据特征点所在的图层对这个阈值进行缩放
                vpPnPsolvers[i] = pSolver;
                nCandidates++;
            }
        }
    }

    // Alternatively perform some iterations of P4P RANSAC
    // Until we found a camera pose supported by enough inliers
    // 这里的 P4P RANSAC是Epnp,每次迭代需要4个点
    // 是否已经找到相匹配的关键帧的标志
    bool bMatch = false;
    ORBmatcher matcher2(0.9,true);

    // Step 4: 通过一系列操作,直到找到能够匹配上的关键帧
    // 为什么搞这么复杂?答:是担心误闭环
    while(nCandidates>0 && !bMatch)
    {
        //遍历当前所有的候选关键帧
        for(int i=0; i<nKFs; i++)
        {
            // 忽略放弃的
            if(vbDiscarded[i])
                continue;
    
            //内点标记
            vector<bool> vbInliers;     
            
            //内点数
            int nInliers;
            
            // 表示RANSAC已经没有更多的迭代次数可用 -- 也就是说数据不够好,RANSAC也已经尽力了。。。
            bool bNoMore;

            // Step 4.1:通过EPnP算法估计姿态,迭代5次
            PnPsolver* pSolver = vpPnPsolvers[i];
            cv::Mat Tcw = pSolver->iterate(5,bNoMore,vbInliers,nInliers);

            // If Ransac reachs max. iterations discard keyframe
            // bNoMore 为true 表示已经超过了RANSAC最大迭代次数,就放弃当前关键帧
            if(bNoMore)
            {
                vbDiscarded[i]=true;
                nCandidates--;
            }

            // If a Camera Pose is computed, optimize
            if(!Tcw.empty())
            {
                //  Step 4.2:如果EPnP 计算出了位姿,对内点进行BA优化
                Tcw.copyTo(mCurrentFrame.mTcw);
                
                // EPnP 里RANSAC后的内点的集合
                set<MapPoint*> sFound;

                const int np = vbInliers.size();
                //遍历所有内点
                for(int j=0; j<np; j++)
                {
                    if(vbInliers[j])
                    {
                        mCurrentFrame.mvpMapPoints[j]=vvpMapPointMatches[i][j];
                        sFound.insert(vvpMapPointMatches[i][j]);
                    }
                    else
                        mCurrentFrame.mvpMapPoints[j]=NULL;
                }

                // 只优化位姿,不优化地图点的坐标,返回的是内点的数量
                int nGood = Optimizer::PoseOptimization(&mCurrentFrame);

                // 如果优化之后的内点数目不多,跳过了当前候选关键帧,但是却没有放弃当前帧的重定位
                if(nGood<10)
                    continue;

                // 删除外点对应的地图点
                for(int io =0; io<mCurrentFrame.N; io++)
                    if(mCurrentFrame.mvbOutlier[io])
                        mCurrentFrame.mvpMapPoints[io]=static_cast<MapPoint*>(NULL);

                // If few inliers, search by projection in a coarse window and optimize again
                // Step 4.3:如果内点较少,则通过投影的方式对之前未匹配的点进行匹配,再进行优化求解
                // 前面的匹配关系是用词袋匹配过程得到的
                if(nGood<50)
                {
                    // 通过投影的方式将关键帧中未匹配的地图点投影到当前帧中, 生成新的匹配
                    int nadditional = matcher2.SearchByProjection(
                        mCurrentFrame,          //当前帧
                        vpCandidateKFs[i],      //关键帧
                        sFound,                 //已经找到的地图点集合,不会用于PNP
                        10,                     //窗口阈值,会乘以金字塔尺度
                        100);                   //匹配的ORB描述子距离应该小于这个阈值

                    // 如果通过投影过程新增了比较多的匹配特征点对
                    if(nadditional+nGood>=50)
                    {
                        // 根据投影匹配的结果,再次采用3D-2D pnp BA优化位姿
                        nGood = Optimizer::PoseOptimization(&mCurrentFrame);

                        // If many inliers but still not enough, search by projection again in a narrower window
                        // the camera has been already optimized with many points
                        // Step 4.4:如果BA后内点数还是比较少(<50)但是还不至于太少(>30),可以挽救一下, 最后垂死挣扎 
                        // 重新执行上一步 4.3的过程,只不过使用更小的搜索窗口
                        // 这里的位姿已经使用了更多的点进行了优化,应该更准,所以使用更小的窗口搜索
                        if(nGood>30 && nGood<50)
                        {
                            // 用更小窗口、更严格的描述子阈值,重新进行投影搜索匹配
                            sFound.clear();
                            for(int ip =0; ip<mCurrentFrame.N; ip++)
                                if(mCurrentFrame.mvpMapPoints[ip])
                                    sFound.insert(mCurrentFrame.mvpMapPoints[ip]);
                            nadditional =matcher2.SearchByProjection(
                                mCurrentFrame,          //当前帧
                                vpCandidateKFs[i],      //候选的关键帧
                                sFound,                 //已经找到的地图点,不会用于PNP
                                3,                      //新的窗口阈值,会乘以金字塔尺度
                                64);                    //匹配的ORB描述子距离应该小于这个阈值

                            // Final optimization
                            // 如果成功挽救回来,匹配数目达到要求,最后BA优化一下
                            if(nGood+nadditional>=50)
                            {
                                nGood = Optimizer::PoseOptimization(&mCurrentFrame);
                                //更新地图点
                                for(int io =0; io<mCurrentFrame.N; io++)
                                    if(mCurrentFrame.mvbOutlier[io])
                                        mCurrentFrame.mvpMapPoints[io]=NULL;
                            }
                            //如果还是不能够满足就放弃了
                        }
                    }
                }

                // If the pose is supported by enough inliers stop ransacs and continue
                // 如果对于当前的候选关键帧已经有足够的内点(50个)了,那么就认为重定位成功
                if(nGood>=50)
                {
                    bMatch = true;
                    // 只要有一个候选关键帧重定位成功,就退出循环,不考虑其他候选关键帧了
                    break;
                }
            }
        }//一直运行,知道已经没有足够的关键帧,或者是已经有成功匹配上的关键帧
    }

    // 折腾了这么久还是没有匹配上,重定位失败
    if(!bMatch)
    {
        return false;
    }
    else
    {
        // 如果匹配上了,说明当前帧重定位成功了(当前帧已经有了自己的位姿)
        // 记录成功重定位帧的id,防止短时间多次重定位
        mnLastRelocFrameId = mCurrentFrame.mnId;
        return true;
    }
}

1.4 局部地图跟踪

1.4.1 流程

1.3章节的三个跟踪模型得到的位姿和地图点都是粗略的,局部地图匹配就是为了进一步优化地图和位姿。

  1. 更新局部关键帧mvpLocalKeyFrames和局部地图点mvpLocalMapPoints
  2. 在局部地图中查找与当前帧匹配的地图点, 其实也就是对局部地图点进行跟踪;
  3. 更新局部所有地图点后对位姿再次优化;
  4. 更新当前帧的地图点被观测程度,并统计跟踪局部地图的效果;
  5. 决定是否跟踪成功。

1.4.2 更新局部关键帧

作者在论文中对局部关键帧有如下定义:局部关键帧包含了两个集合(set),第一,与当前帧有共视关系(share map points)的关键帧,记作集合K1;第二,与集合K1在共视图中(covisibility graph)有良好共视关系的帧(具体见下),记作K2

凡是和当前帧有共同的地图点的关键帧都归为K1K2集合需要满足以下三种条件:

  • K1有良好共视关系的子关键帧(作者选取了最佳共视的10帧)
  • K1中元素的子关键帧
  • K1中元素的父关键帧

满足其中一种条件的关键帧都可以被归为K2集合。

在处理完局部关键帧后,还需要添加参考关键帧与当前帧共视程度最高(有最多share map points)的关键帧作为参考关键帧。

1.4.3 更新局部地图点

上一步得到了所有的局部关键帧,然后更新局部地图点,即所有局部关键帧包含的地图点构成局部地图点。主要有以下三步:

  1. 清空局部地图点;
  2. 将局部关键帧的地图点添加到mvpLocalMapPoints
  3. 同时设置地图点更新标志,来避免重复添加出现在多帧上的地图点。

1.4.4 进一步优化

虽然上一步得到了很多局部地图点,但有很多是不可用的,所以还需按照以下步骤进一步筛选:

  1. 遍历当前帧的特征点,如果已经有相应的 3D 地图点,则进行标记,不需要进行重投影匹配,并且标记已经被遍历过;
  2. 遍历局部地图的所有地图点,如果没有被遍历过,把地图点反投影到当前帧下,保留在当前帧视野内的地图点;
  3. 根据反投影后的2D位置,设置一个半径,并在此范围进行搜索匹配点
  4. 更新局部所有 MapPoints 后对位姿再次优化,采用的是g2o优化器;
  5. 更新地图点状态;
  6. 正常情况下,找到的内点匹配点对数大于 30 算成功,但如果刚进行过重定位则需要内点匹配点对数大于 50 才认为跟踪成功。
// 用局部地图进行跟踪,进一步优化位姿

bool Tracking::TrackLocalMap()
{
    // Update Local KeyFrames and Local Points
    // Step 1:更新局部关键帧 mvpLocalKeyFrames 和局部地图点 mvpLocalMapPoints
    UpdateLocalMap();

    // Step 2:筛选局部地图中新增的在视野范围内的地图点,投影到当前帧搜索匹配,得到更多的匹配关系
    SearchLocalPoints();

    // Optimize Pose
    // 在这个函数之前,在 Relocalization、TrackReferenceKeyFrame、TrackWithMotionModel 中都有位姿优化,
    // Step 3:前面新增了更多的匹配关系,BA优化得到更准确的位姿
    Optimizer::PoseOptimization(&mCurrentFrame);
    mnMatchesInliers = 0;

    // Update MapPoints Statistics
    // Step 4:更新当前帧的地图点被观测程度,并统计跟踪局部地图后匹配数目
    for(int i=0; i<mCurrentFrame.N; i++)
    {
        if(mCurrentFrame.mvpMapPoints[i])
        {
            // 由于当前帧的地图点可以被当前帧观测到,其被观测统计量加1
            if(!mCurrentFrame.mvbOutlier[i])
            {
                // 找到该点的帧数mnFound 加 1
                mCurrentFrame.mvpMapPoints[i]->IncreaseFound();
                //查看当前是否是在纯定位过程
                if(!mbOnlyTracking)
                {
                    // 如果该地图点被相机观测数目nObs大于0,匹配内点计数+1
                    // nObs: 被观测到的相机数目,单目+1,双目或RGB-D则+2
                    if(mCurrentFrame.mvpMapPoints[i]->Observations()>0)
                        mnMatchesInliers++;
                }
                else
                    // 记录当前帧跟踪到的地图点数目,用于统计跟踪效果
                    mnMatchesInliers++;
            }
            // 如果这个地图点是外点,并且当前相机输入还是双目的时候,就删除这个点
            // ?单目就不管吗
            else if(mSensor==System::STEREO)  
                mCurrentFrame.mvpMapPoints[i] = static_cast<MapPoint*>(NULL);

        }
    }

    // Decide if the tracking was succesful
    // More restrictive if there was a relocalization recently
    // Step 5:根据跟踪匹配数目及重定位情况决定是否跟踪成功
    // 如果最近刚刚发生了重定位,那么至少成功匹配50个点才认为是成功跟踪
    if(mCurrentFrame.mnId<mnLastRelocFrameId+mMaxFrames && mnMatchesInliers<50)
        return false;

    //如果是正常的状态话只要跟踪的地图点大于30个就认为成功了
    if(mnMatchesInliers<30)
        return false;
    else
        return true;
}

1.5 关键帧生成

1.5.1 生成条件

跟踪成功后,还需要判断是否需要生成关键帧,为了确保定位精度,并且不会对大场景造成过多的计算负担,所以是否生成关键帧,需同时满足以下条件:

  1. 系统模式判断,如果仅仅需要跟踪定位,不需要建图,那么不需要生成关键帧
  2. 根据地图中关键帧的数量设置一些参数(系统一开始关键帧少的时候,可以放宽一些条件,多创建一些关键帧);
  3. 距离上一次重定位距离至少1S
  4. 当前帧跟踪至少50个点,保证精度;
  5. 当前帧跟踪到LocalMap中参考帧的地图点数量少于90%,确保关键帧之间有明显的视觉变化;
  6. 局部地图线程空闲或距离上一次加入关键帧过去了20帧(如果需要关键帧插入过了20帧。而LocalMapping线程忙,则发送信号给线程,停止局部地图优化,使得新的关键帧可以被及时处理)。
// 判断当前帧是否需要插入关键帧

bool Tracking::NeedNewKeyFrame()
{
    // Step 1:纯VO模式下不插入关键帧
    if(mbOnlyTracking)
        return false;

    // If Local Mapping is freezed by a Loop Closure do not insert keyframes
    // Step 2:如果局部地图线程被闭环检测使用,则不插入关键帧
    if(mpLocalMapper->isStopped() || mpLocalMapper->stopRequested())
        return false;
    // 获取当前地图中的关键帧数目
    const int nKFs = mpMap->KeyFramesInMap();

    // Do not insert keyframes if not enough frames have passed from last relocalisation
    // mCurrentFrame.mnId是当前帧的ID
    // mnLastRelocFrameId是最近一次重定位帧的ID
    // mMaxFrames等于图像输入的帧率
    //  Step 3:如果距离上一次重定位比较近,并且关键帧数目超出最大限制,不插入关键帧
    if( mCurrentFrame.mnId < mnLastRelocFrameId + mMaxFrames && nKFs>mMaxFrames)                                     
        return false;

    // Tracked MapPoints in the reference keyframe
    // Step 4:得到参考关键帧跟踪到的地图点数量
    // UpdateLocalKeyFrames 函数中会将与当前关键帧共视程度最高的关键帧设定为当前帧的参考关键帧 

    // 地图点的最小观测次数
    int nMinObs = 3;
    if(nKFs<=2)
        nMinObs=2;
    // 参考关键帧地图点中观测的数目>= nMinObs的地图点数目
    int nRefMatches = mpReferenceKF->TrackedMapPoints(nMinObs);

    // Local Mapping accept keyframes?
    // Step 5:查询局部地图线程是否繁忙,当前能否接受新的关键帧
    bool bLocalMappingIdle = mpLocalMapper->AcceptKeyFrames();

    // Check how many "close" points are being tracked and how many could be potentially created.
    // Step 6:对于双目或RGBD摄像头,统计成功跟踪的近点的数量,如果跟踪到的近点太少,没有跟踪到的近点较多,可以插入关键帧
     int nNonTrackedClose = 0;  //双目或RGB-D中没有跟踪到的近点
    int nTrackedClose= 0;       //双目或RGB-D中成功跟踪的近点(三维点)
    if(mSensor!=System::MONOCULAR)
    {
        for(int i =0; i<mCurrentFrame.N; i++)
        {
            // 深度值在有效范围内
            if(mCurrentFrame.mvDepth[i]>0 && mCurrentFrame.mvDepth[i]<mThDepth)
            {
                if(mCurrentFrame.mvpMapPoints[i] && !mCurrentFrame.mvbOutlier[i])
                    nTrackedClose++;
                else
                    nNonTrackedClose++;
            }
        }
    }

    // 双目或RGBD情况下:跟踪到的地图点中近点太少 同时 没有跟踪到的三维点太多,可以插入关键帧了
    // 单目时,为false
    bool bNeedToInsertClose = (nTrackedClose<100) && (nNonTrackedClose>70);

    // Step 7:决策是否需要插入关键帧
    // Thresholds
    // Step 7.1:设定比例阈值,当前帧和参考关键帧跟踪到点的比例,比例越大,越倾向于增加关键帧
    float thRefRatio = 0.75f;

    // 关键帧只有一帧,那么插入关键帧的阈值设置的低一点,插入频率较低
    if(nKFs<2)
        thRefRatio = 0.4f;

    //单目情况下插入关键帧的频率很高    
    if(mSensor==System::MONOCULAR)
        thRefRatio = 0.9f;

    // Condition 1a: More than "MaxFrames" have passed from last keyframe insertion
    // Step 7.2:很长时间没有插入关键帧,可以插入
    const bool c1a = mCurrentFrame.mnId>=mnLastKeyFrameId+mMaxFrames;

    // Condition 1b: More than "MinFrames" have passed and Local Mapping is idle
    // Step 7.3:满足插入关键帧的最小间隔并且localMapper处于空闲状态,可以插入
    const bool c1b = (mCurrentFrame.mnId>=mnLastKeyFrameId+mMinFrames && bLocalMappingIdle);

    // Condition 1c: tracking is weak
    // Step 7.4:在双目,RGB-D的情况下当前帧跟踪到的点比参考关键帧的0.25倍还少,或者满足bNeedToInsertClose
    const bool c1c =  mSensor!=System::MONOCULAR &&             //只考虑在双目,RGB-D的情况
                    (mnMatchesInliers<nRefMatches*0.25 ||       //当前帧和地图点匹配的数目非常少
                      bNeedToInsertClose) ;                     //需要插入

    // Condition 2: Few tracked points compared to reference keyframe. Lots of visual odometry compared to map matches.
    // Step 7.5:和参考帧相比当前跟踪到的点太少 或者满足bNeedToInsertClose;同时跟踪到的内点还不能太少
    const bool c2 = ((mnMatchesInliers<nRefMatches*thRefRatio|| bNeedToInsertClose) && mnMatchesInliers>15);

    if((c1a||c1b||c1c)&&c2)
    {
        // If the mapping accepts keyframes, insert keyframe.
        // Otherwise send a signal to interrupt BA
        // Step 7.6:local mapping空闲时可以直接插入,不空闲的时候要根据情况插入
        if(bLocalMappingIdle)
        {
            //可以插入关键帧
            return true;
        }
        else
        {
            mpLocalMapper->InterruptBA();
            if(mSensor!=System::MONOCULAR)
            {
                // 队列里不能阻塞太多关键帧
                // tracking插入关键帧不是直接插入,而且先插入到mlNewKeyFrames中,
                // 然后localmapper再逐个pop出来插入到mspKeyFrames
                if(mpLocalMapper->KeyframesInQueue()<3)
                    //队列中的关键帧数目不是很多,可以插入
                    return true;
                else
                    //队列中缓冲的关键帧数目太多,暂时不能插入
                    return false;
            }
            else
                //对于单目情况,就直接无法插入关键帧了
                //可能是单目关键帧相对比较密集
                return false;
        }
    }
    else
        //不满足上面的条件,自然不能插入关键帧
        return false;
}

1.5.2 创建关键帧

判断是否生成关键帧之后,利用CreateNewKeyFrame()创建关键帧,详细的步骤如下:

  1. 构造关键帧;
  2. 当前关键帧设置为当前帧的参考关键帧;
  3. 根据Tcw计算额外矩阵,关键帧相对于普通帧会额外计算一些矩阵( mRcw 旋转矩阵、mRwc 旋转矩阵的逆、mtcw 平移向量和 mOw 光心在世界坐标系下的坐标);
  4. 获取正深度特征点,用于重新构建地图点;
  5. 按照深度从小到大排序;
  6. 将距离比较近的点包装成地图点;
  7. 将关键帧加入到待处理队列 mlNewKeyFrames 中供局部建图线程处理。
// 创建关键帧

void Tracking::CreateNewKeyFrame()
{
    // 如果局部建图线程关闭了,就无法插入关键帧
    if(!mpLocalMapper->SetNotStop(true))
        return;

    // Step 1:将当前帧构造成关键帧
    KeyFrame* pKF = new KeyFrame(mCurrentFrame,mpMap,mpKeyFrameDB);
    // Step 2:将当前关键帧设置为当前帧的参考关键帧
    // 在UpdateLocalKeyFrames函数中会将与当前关键帧共视程度最高的关键帧设定为当前帧的参考关键帧
    mpReferenceKF = pKF;
    mCurrentFrame.mpReferenceKF = pKF;

    // 这段代码和 Tracking::UpdateLastFrame 中的那一部分代码功能相同
    // Step 3:对于双目或rgbd摄像头,为当前帧生成新的地图点;单目无操作
    if(mSensor!=System::MONOCULAR)
    {
        // 根据Tcw计算mRcw、mtcw和mRwc、mOw
        mCurrentFrame.UpdatePoseMatrices();

        // We sort points by the measured depth by the stereo/RGBD sensor.
        // We create all those MapPoints whose depth < mThDepth.
        // If there are less than 100 close points we create the 100 closest.
        // Step 3.1:得到当前帧有深度值的特征点(不一定是地图点)
        vector<pair<float,int> > vDepthIdx;
        vDepthIdx.reserve(mCurrentFrame.N);
        for(int i=0; i<mCurrentFrame.N; i++)
        {
            float z = mCurrentFrame.mvDepth[i];
            if(z>0)
            {
                // 第一个元素是深度,第二个元素是对应的特征点的id
                vDepthIdx.push_back(make_pair(z,i));
            }
        }

        if(!vDepthIdx.empty())
        {
            // Step 3.2:按照深度从小到大排序
            sort(vDepthIdx.begin(),vDepthIdx.end());

            // Step 3.3:从中找出不是地图点的包装为地图点 
            // 处理的近点的个数
            int nPoints = 0;
            for(size_t j=0; j<vDepthIdx.size();j++)
            {
                int i = vDepthIdx[j].second;

                bool bCreateNew = false;

                // 如果这个点对应在上一帧中的地图点没有,或者创建后就没有被观测到,那么就包装为地图点
                MapPoint* pMP = mCurrentFrame.mvpMapPoints[i];
                if(!pMP)
                    bCreateNew = true;
                else if(pMP->Observations()<1)
                {
                    bCreateNew = true;
                    mCurrentFrame.mvpMapPoints[i] = static_cast<MapPoint*>(NULL);
                }

                // 如果需要就新建地图点,这里的地图点不是临时的,是全局地图中新建地图点,用于跟踪
                if(bCreateNew)
                {
                    cv::Mat x3D = mCurrentFrame.UnprojectStereo(i);
                    MapPoint* pNewMP = new MapPoint(x3D,pKF,mpMap);
                    // 这些添加属性的操作是每次创建MapPoint后都要做的
                    pNewMP->AddObservation(pKF,i);
                    pKF->AddMapPoint(pNewMP,i);
                    pNewMP->ComputeDistinctiveDescriptors();
                    pNewMP->UpdateNormalAndDepth();
                    mpMap->AddMapPoint(pNewMP);

                    mCurrentFrame.mvpMapPoints[i]=pNewMP;
                    nPoints++;
                }
                else
                {
                    // 因为从近到远排序,记录其中不需要创建地图点的个数
                    nPoints++;
                }

                // Step 3.4:停止新建地图点必须同时满足以下条件:
                // 1、当前的点的深度已经超过了设定的深度阈值(35倍基线)
                // 2、nPoints已经超过100个点,说明距离比较远了,可能不准确,停掉退出
                if(vDepthIdx[j].first>mThDepth && nPoints>100)
                    break;
            }
        }
    }

    // Step 4:插入关键帧
    // 关键帧插入到列表 mlNewKeyFrames中,等待local mapping线程临幸
    mpLocalMapper->InsertKeyFrame(pKF);

    // 插入好了,允许局部建图停止
    mpLocalMapper->SetNotStop(false);

    // 当前帧成为新的关键帧,更新
    mnLastKeyFrameId = mCurrentFrame.mnId;
    mpLastKeyFrame = pKF;
}

Reference:

  1. https://github.com/raulmur/ORB_SLAM2
  2. https://github.com/electech6/ORB_SLAM2_detailed_comments/tree/master
  3. https://blog.csdn.net/qq_41694024/article/details/128321440



须知少时凌云志,曾许人间第一流。



⭐️

你可能感兴趣的:(SLAM,#,V-SLAM,slam,vslam,ORB-SLAM2,Tracking线程,跟踪方法,局部地图跟踪,初始化)