(01)ORB-SLAM2源码无死角解析-(50) 局部建图线程→流程以及处理新关键帧:ProcessNewKeyFrame()

讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解的(01)ORB-SLAM2源码无死角解析链接如下(本文内容来自计算机视觉life ORB-SLAM2 课程课件):
(01)ORB-SLAM2源码无死角解析-(00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/123092196
 
文末正下方中心提供了本人 联系方式, 点击本人照片即可显示 W X → 官方认证 {\color{blue}{文末正下方中心}提供了本人 \color{red} 联系方式,\color{blue}点击本人照片即可显示WX→官方认证} 文末正下方中心提供了本人联系方式,点击本人照片即可显示WX官方认证
 

一、前言

在前面博客:
(01)ORB-SLAM2源码无死角解析-(25) 关键帧KeyFrame→判断系统目前是否需要关键帧
(01)ORB-SLAM2源码无死角解析-(26) 关键帧KeyFrame→如何创建、插入关键帧
两篇博客中对关键帧的需求判断以及创建过程都进行了十分细致的讲解。
在这里插入图片描述
追踪线程中,会在合适的时候创建关键帧,这些关键帧除了追踪线程本身会用到,局部建图线程以及闭环线程都会用到,并且这里需要注意一个点:局部建图线程以及闭环线程不会对普通帧做处理,它们只关注关键帧。

从上图可以看出,局部建图线程主要包含了参入插入关键帧,剔除地图点,生成地图点,局部BA优化,剔除冗余关键帧。
 

二、LocalMapping::Run()

那么先来看看 局部建图线程 LocalMapping::Run() 的主体流程,位于 LocalMapping.cc 文件中,注释如下(大致看一下即可,后续有详细的介绍)。

( 1 ) : \color{blue}{(1):} (1): 首先告诉追踪线程,目前处于繁忙状态,之前你送过来的关键帧都没有处理完,所以现在不要不要发送关键帧过来,等我处理完了再说。
( 2 ) : \color{blue}{(2):} (2): 判断一下存储于 mlNewKeyFrames 变量中的关键帧是否都处理完成了,如果全部处理完成了,则会进一步判是否需要结束当前线程。
( 3 ) : \color{blue}{(3):} (3): 如果 mlNewKeyFrames 变量中有关键帧需要处理,则对关键帧进行如下处理: ①计算BoW、更新观测、描述子、共视图,插入到地图。②根据地图点的观测情况剔除质量不好的地图点。③当前关键帧与相邻关键帧通过三角化产生新的地图点,使得跟踪更稳。④ 检查并融合当前关键帧与相邻关键帧帧(两级相邻)中重复的地图点。⑤当局部地图中的关键帧大于2个的时候进行局部地图的BA。⑥检测并剔除当前帧相邻的关键帧中冗余的关键帧。⑦将当前帧加入到闭环检测队列中。
( 4 ) : \color{blue}{(4):} (4): 每处理完一帧关键帧,查看是否有复位线程的请求,并且取消繁忙状态,如果当前线程已经结束了就跳出主循环。
 

// 线程主函数
void LocalMapping::Run()
{

    // 标记状态,表示当前run函数正在运行,尚未结束
    mbFinished = false;
    // 主循环
    while(1)
    {
        // Tracking will see that Local Mapping is busy
        // Step 1 告诉Tracking,LocalMapping正处于繁忙状态,请不要给我发送关键帧打扰我
        // LocalMapping线程处理的关键帧都是Tracking线程发来的
        SetAcceptKeyFrames(false);

        // Check if there are keyframes in the queue
        // 等待处理的关键帧列表不为空
        if(CheckNewKeyFrames())
        {
            // BoW conversion and insertion in Map
            // Step 2 处理列表中的关键帧,包括计算BoW、更新观测、描述子、共视图,插入到地图等
            ProcessNewKeyFrame();

            // Check recent MapPoints
            // Step 3 根据地图点的观测情况剔除质量不好的地图点
            MapPointCulling();

            // Triangulate new MapPoints
            // Step 4 当前关键帧与相邻关键帧通过三角化产生新的地图点,使得跟踪更稳
            CreateNewMapPoints();

            // 已经处理完队列中的最后的一个关键帧
            if(!CheckNewKeyFrames())
            {
                // Find more matches in neighbor keyframes and fuse point duplications
                //  Step 5 检查并融合当前关键帧与相邻关键帧帧(两级相邻)中重复的地图点
                SearchInNeighbors();
            }

            // 终止BA的标志
            mbAbortBA = false;

            // 已经处理完队列中的最后的一个关键帧,并且闭环检测没有请求停止LocalMapping
            if(!CheckNewKeyFrames() && !stopRequested())
            {
                // Local BA
                // Step 6 当局部地图中的关键帧大于2个的时候进行局部地图的BA
                if(mpMap->KeyFramesInMap()>2)
                    // 注意这里的第二个参数是按地址传递的,当这里的 mbAbortBA 状态发生变化时,能够及时执行/停止BA
                    Optimizer::LocalBundleAdjustment(mpCurrentKeyFrame,&mbAbortBA, mpMap);

                // Check redundant local Keyframes
                // Step 7 检测并剔除当前帧相邻的关键帧中冗余的关键帧
                // 冗余的判定:该关键帧的90%的地图点可以被其它关键帧观测到
                KeyFrameCulling();
            }

            // Step 8 将当前帧加入到闭环检测队列中
            // 注意这里的关键帧被设置成为了bad的情况,这个需要注意
            mpLoopCloser->InsertKeyFrame(mpCurrentKeyFrame);
        }
        else if(Stop())     // 当要终止当前线程的时候
        {
            // Safe area to stop
            while(isStopped() && !CheckFinish())
            {
                // 如果还没有结束利索,那么等
                // usleep(3000);
                std::this_thread::sleep_for(std::chrono::milliseconds(3));
            }
            // 然后确定终止了就跳出这个线程的主循环
            if(CheckFinish())
                break;
        }

        // 查看是否有复位线程的请求
        ResetIfRequested();

        // Tracking will see that Local Mapping is not busy
        SetAcceptKeyFrames(true);

        // 如果当前线程已经结束了就跳出主循环
        if(CheckFinish())
            break;

        //usleep(3000);
        std::this_thread::sleep_for(std::chrono::milliseconds(3));

    }

    // 设置线程已经终止
    SetFinish();
}

 

三、ProcessNewKeyFrame();

前面对 局部建图线程LocalMapping::Run() 的主体流程进行了讲解,那么现在就来看看其中调用的第一个函数ProcessNewKeyFrame()。其本身的逻辑还是很简单的:

( 1 ) : \color{blue}{(1):} (1): 从 mlNewKeyFrames 中取出列表最前面的关键帧,随后马上把列表中的该关键帧删除,然后对该关键帧进行处理。

( 2 ) : \color{blue}{(2):} (2): 利用关键帧特征点的BRIEF描述子计算BoW,主要获得两个变量:
       ①mBowVec→单词word的id,以及该word对应的权重
       ②mFeatVec→第一个元素为节点id,第二个元素为该节点id下所有特征点在图像中的索引。

( 3 ) : \color{blue}{(3):} (3): 获得关键帧的地图点,然后循环对每个地图点进行处理。如果地图点不是坏点,则进一步处理。

( 4 ) : \color{blue}{(4):} (4):如果该地图点存在于关键帧中:
       ①如果地图点不是来自当前帧的观测(比如来自局部地图点),为当前地图点添加观测→pMP->AddObservation(mpCurrentKeyFrame, i);。
       ②跟新该点的平均观测方向和观测距离范围→pMP->UpdateNormalAndDepth();
       ③更新地图点的最佳描述子→pMP->ComputeDistinctiveDescriptors();

( 5 ) : \color{blue}{(5):} (5):如果该地图点不存在于关键帧中:如果当前帧中已经包含了这个地图点,但是这个地图点中却没有包含这个关键帧的信息,这些地图点可能来自双目或RGBD跟踪过程中新生成的地图点,或者是CreateNewMapPoints 中通过三角化产生

( 6 ) : \color{blue}{(6):} (6): 更新关键帧间的连接关系(共视图),将该关键帧插入到地图中。

代码位于 LocalMapping.cc 文件中,注释如下:

/**
 * @brief 处理列表中的关键帧,包括计算BoW、更新观测、描述子、共视图,插入到地图等
 * 
 */
void LocalMapping::ProcessNewKeyFrame()
{
    // Step 1:从缓冲队列中取出一帧关键帧
    // 该关键帧队列是Tracking线程向LocalMapping中插入的关键帧组成
    {
        unique_lock<mutex> lock(mMutexNewKFs);
        // 取出列表中最前面的关键帧,作为当前要处理的关键帧
        mpCurrentKeyFrame = mlNewKeyFrames.front();
        // 取出最前面的关键帧后,在原来的列表里删掉该关键帧
        mlNewKeyFrames.pop_front();
    }

    // Compute Bags of Words structures
    // Step 2:计算该关键帧特征点的词袋向量
    mpCurrentKeyFrame->ComputeBoW();

    // Associate MapPoints to the new keyframe and update normal and descriptor
    // Step 3:当前处理关键帧中有效的地图点,更新normal,描述子等信息
    // TrackLocalMap中和当前帧新匹配上的地图点和当前关键帧进行关联绑定
    const vector<MapPoint*> vpMapPointMatches = mpCurrentKeyFrame->GetMapPointMatches();
    // 对当前处理的这个关键帧中的所有的地图点展开遍历
    for(size_t i=0; i<vpMapPointMatches.size(); i++)
    {
        MapPoint* pMP = vpMapPointMatches[i];
        if(pMP)
        {
            if(!pMP->isBad())
            {
                if(!pMP->IsInKeyFrame(mpCurrentKeyFrame))
                {
                    // 如果地图点不是来自当前帧的观测(比如来自局部地图点),为当前地图点添加观测
                    pMP->AddObservation(mpCurrentKeyFrame, i);
                    // 获得该点的平均观测方向和观测距离范围
                    pMP->UpdateNormalAndDepth();
                    // 更新地图点的最佳描述子
                    pMP->ComputeDistinctiveDescriptors();
                }
                else // this can only happen for new stereo points inserted by the Tracking
                {
                    // 如果当前帧中已经包含了这个地图点,但是这个地图点中却没有包含这个关键帧的信息
                    // 这些地图点可能来自双目或RGBD跟踪过程中新生成的地图点,或者是CreateNewMapPoints 中通过三角化产生
                    // 将上述地图点放入mlpRecentAddedMapPoints,等待后续MapPointCulling函数的检验
                    mlpRecentAddedMapPoints.push_back(pMP); 
                }
            }
        }
    }    

    // Update links in the Covisibility Graph
    // Step 4:更新关键帧间的连接关系(共视图)
    mpCurrentKeyFrame->UpdateConnections();

    // Insert Keyframe in Map
    // Step 5:将该关键帧插入到地图中
    mpMap->AddKeyFrame(mpCurrentKeyFrame);
}

从上面可以看到几个重要的函数,分别为 pMP->UpdateNormalAndDepth();pMP->ComputeDistinctiveDescriptors();以及mpCurrentKeyFrame->UpdateConnections(),下面则分别对其进行详细讲解。
 

四、UpdateNormalAndDepth

在执行该函数之前,先执行了 pMP->AddObservation(mpCurrentKeyFrame, i),也就是说地图点的可观测者(能观测到该点的关键帧)已经发生了变化,那么该地图点的平均观测方向是需要更新的,代码位于src/MapPoint.cc 文件中:

( 1 ) : \color{blue}{(1):} (1): 获得观测到该地图点的所有关键帧,观测到该点的参考关键帧(第一次创建时的关键帧),以及地图点在世界坐标的位置。

( 2 ) : \color{blue}{(2):} (2): 计算地图点的的平均观测方向,首先计算地图点被单个关键帧观测到的方向,也就是用地图点的世界坐标减去该关键帧相机的世界坐标,即 mWorldPos - Owi。然后把所有关键帧观测该地图点的方向
去平均值即可。

( 3 ) : \color{blue}{(3):} (3): 根据参考帧相机到该地图点的距离,然后结合金字塔层级更新观测距离范围。

/**
 * @brief 更新地图点的平均观测方向、观测距离范围
 *
 */
void MapPoint::UpdateNormalAndDepth()
{
    // Step 1 获得观测到该地图点的所有关键帧、坐标等信息
    map<KeyFrame*,size_t> observations;
    KeyFrame* pRefKF;
    cv::Mat Pos;
    {
        unique_lock<mutex> lock1(mMutexFeatures);
        unique_lock<mutex> lock2(mMutexPos);
        if(mbBad)
            return;

        observations=mObservations; // 获得观测到该地图点的所有关键帧
        pRefKF=mpRefKF;             // 观测到该点的参考关键帧(第一次创建时的关键帧)
        Pos = mWorldPos.clone();    // 地图点在世界坐标系中的位置
    }

    if(observations.empty())
        return;

    // Step 2 计算该地图点的平均观测方向
    // 能观测到该地图点的所有关键帧,对该点的观测方向归一化为单位向量,然后进行求和得到该地图点的朝向
    // 初始值为0向量,累加为归一化向量,最后除以总数n
    cv::Mat normal = cv::Mat::zeros(3,1,CV_32F);
    int n=0;
    for(map<KeyFrame*,size_t>::iterator mit=observations.begin(), mend=observations.end(); mit!=mend; mit++)
    {
        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];          // 当前金字塔层对应的尺度因子,scale^n,scale=1.2,n为层数
    const int nLevels = pRefKF->mnScaleLevels;                              // 金字塔总层数,默认为8

    {
        unique_lock<mutex> lock3(mMutexPos);
        // 使用方法见PredictScale函数前的注释
        mfMaxDistance = dist*levelScaleFactor;                              // 观测到该点的距离上限
        mfMinDistance = mfMaxDistance/pRefKF->mvScaleFactors[nLevels-1];    // 观测到该点的距离下限
        mNormalVector = normal/n;                                           // 获得地图点平均的观测方向
    }
}

 

五、ComputeDistinctiveDescriptors

除了更新地图点的平均观测方向,还有可能需要更新最具有代表性的描述子。其主要思路:由于一个地图点会被许多相机观测到,因此在插入关键帧后,需要判断是否更新代表当前点的描述子先获得当前点的所有描述子,然后计算描述子之间的两两距离,最好的描述子与其他描述子应该具有最小的距离中值。代码位于src/MapPoint.cc 文件中,注释如下:

/**
 * @brief 计算地图点最具代表性的描述子
 *
 * 由于一个地图点会被许多相机观测到,因此在插入关键帧后,需要判断是否更新代表当前点的描述子 
 * 先获得当前点的所有描述子,然后计算描述子之间的两两距离,最好的描述子与其他描述子应该具有最小的距离中值
 */
void MapPoint::ComputeDistinctiveDescriptors()
{
    // Retrieve all observed descriptors
    vector<cv::Mat> vDescriptors;

    map<KeyFrame*,size_t> observations;

    // Step 1 获取该地图点所有有效的观测关键帧信息
    {
        unique_lock<mutex> lock1(mMutexFeatures);
        if(mbBad)
            return;
        observations=mObservations;
    }

    if(observations.empty())
        return;

    vDescriptors.reserve(observations.size());

    // Step 2 遍历观测到该地图点的所有关键帧,对应的orb描述子,放到向量vDescriptors中
    for(map<KeyFrame*,size_t>::iterator mit=observations.begin(), mend=observations.end(); mit!=mend; mit++)
    {
        // mit->first取观测到该地图点的关键帧
        // mit->second取该地图点在关键帧中的索引
        KeyFrame* pKF = mit->first;

        if(!pKF->isBad())        
            // 取对应的描述子向量                                               
            vDescriptors.push_back(pKF->mDescriptors.row(mit->second));     
    }

    if(vDescriptors.empty())
        return;

    // Compute distances between them
    // Step 3 计算这些描述子两两之间的距离
    // N表示为一共多少个描述子
    const size_t N = vDescriptors.size();
	
    // 将Distances表述成一个对称的矩阵
    // float Distances[N][N];
	std::vector<std::vector<float> > Distances;
	Distances.resize(N, vector<float>(N, 0));
	for (size_t i = 0; i<N; i++)
    {
        // 和自己的距离当然是0
        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;
        }
    }

    // Take the descriptor with least median distance to the rest
    // Step 4 选择最有代表性的描述子,它与其他描述子应该具有最小的距离中值
    int BestMedian = INT_MAX;   // 记录最小的中值
    int BestIdx = 0;            // 最小中值对应的索引
    for(size_t i=0;i<N;i++)
    {
        // 第i个描述子到其它所有描述子之间的距离
        // vector<int> vDists(Distances[i],Distances[i]+N);
		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;
        }
    }

    {
        unique_lock<mutex> lock(mMutexFeatures);
        mDescriptor = vDescriptors[BestIdx].clone();       
    }
}

 

六、ComputeDistinctiveDescriptors

另外还有一个比较重要的操作,那就是更新关键帧之间的连接关系,或者说共视图。代码位于 src/KeyFrame.cc 文件中

( 1 ) : \color{blue}{(1):} (1): 首先这里回顾一下什么是共视程度,简单说,就是当前帧与其他关键帧有多少个公共地图点。核心变量为KFcounter,第一个参数表示某个关键帧,第2个参数表示该关键帧看到了多少当前帧的地图点,也就是共视程度。

( 2 ) : \color{blue}{(2):} (2): 获得当前帧与其他关键帧的共视程度之后,根据共视程度大于指定的阈值则建立共视(连接)关系,注意这里的,建立连接的过程就是对其余关键帧的 mConnectedKeyFrameWeights 进行设置,把共视程度设置为当前帧与其他的关键帧连接的权重,并且根据权重进行排序得到 mvpOrderedConnectedKeyFrames。

( 3 ) : \color{blue}{(3):} (3): 如果没有超过阈值的权重(共视程度),则对权重最大的关键帧建立连接。

( 4 ) : \color{blue}{(4):} (4): vPairs里存的都是相互共视程度比较高的关键帧和共视权重,对其由大到小进行排序

( 5 ) : \color{blue}{(5):} (5): 注意在步骤2中,建立的关系为: 其余关键帧→mConnectedKeyFrameWeights,另外我们还需要更新 单前帧→mConnectedKeyFrameWeights。这个比较简单,因为其等价于 KFcounter,同时还需要记录 mvpOrderedConnectedKeyFrames,也就是排序之后连接关系。

( 5 ) : \color{blue}{(5):} (5): 如果这是当前帧第一次建立连接,初始化该关键帧的父关键帧为共视程度最高的那个关键帧,建立双向连接关系,将当前关键帧作为其子关键帧。

/*
 * 更新关键帧之间的连接图
 * 
 * 1. 首先获得该关键帧的所有MapPoint点,统计观测到这些3d点的每个关键帧与其它所有关键帧之间的共视程度
 *    对每一个找到的关键帧,建立一条边,边的权重是该关键帧与当前关键帧公共3d点的个数。
 * 2. 并且该权重必须大于一个阈值,如果没有超过该阈值的权重,那么就只保留权重最大的边(与其它关键帧的共视程度比较高)
 * 3. 对这些连接按照权重从大到小进行排序,以方便将来的处理
 *    更新完covisibility图之后,如果没有初始化过,则初始化为连接权重最大的边(与其它关键帧共视程度最高的那个关键帧),类似于最大生成树
 */
void KeyFrame::UpdateConnections()
{
    // 关键帧-权重,权重为其它关键帧与当前关键帧共视地图点的个数,也称为共视程度
    map<KeyFrame*,int> KFcounter; 
    vector<MapPoint*> vpMP;

    {
        // 获得该关键帧的所有地图点
        unique_lock<mutex> lockMPs(mMutexFeatures);
        vpMP = mvpMapPoints;
    }

    //For all map points in keyframe check in which other keyframes are they seen
    //Increase counter for those keyframes
    // Step 1 通过地图点被关键帧观测来间接统计关键帧之间的共视程度
    // 统计每一个地图点都有多少关键帧与当前关键帧存在共视关系,统计结果放在KFcounter
    for(vector<MapPoint*>::iterator vit=vpMP.begin(), vend=vpMP.end(); vit!=vend; vit++)
    {
        MapPoint* pMP = *vit;

        if(!pMP)
            continue;

        if(pMP->isBad())
            continue;

        // 对于每一个地图点,observations记录了可以观测到该地图点的所有关键帧
        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;
            // 这里的操作非常精彩!
            // map[key] = value,当要插入的键存在时,会覆盖键对应的原来的值。如果键不存在,则添加一组键值对
            // mit->first 是地图点看到的关键帧,同一个关键帧看到的地图点会累加到该关键帧计数
            // 所以最后KFcounter 第一个参数表示某个关键帧,第2个参数表示该关键帧看到了多少当前帧的地图点,也就是共视程度
            KFcounter[mit->first]++;
        }
    }

    // This should not happen
    // 没有共视关系,直接退出 
    if(KFcounter.empty())
        return;

    // If the counter is greater than threshold add connection
    // In case no keyframe counter is over threshold add the one with maximum counter
    int nmax=0; // 记录最高的共视程度
    KeyFrame* pKFmax=NULL;
    // 至少有15个共视地图点才会添加共视关系
    int th = 15;

    // vPairs记录与其它关键帧共视帧数大于th的关键帧
    // pair<int,KeyFrame*>将关键帧的权重写在前面,关键帧写在后面方便后面排序
    vector<pair<int,KeyFrame*> > vPairs;
    vPairs.reserve(KFcounter.size());
    // Step 2 找到对应权重最大的关键帧(共视程度最高的关键帧)
    for(map<KeyFrame*,int>::iterator mit=KFcounter.begin(), mend=KFcounter.end(); mit!=mend; mit++)
    {
        if(mit->second>nmax)
        {
            nmax=mit->second;
            pKFmax=mit->first;
        }

        // 建立共视关系至少需要大于等于th个共视地图点
        if(mit->second>=th)
        {
            // 对应权重需要大于阈值,对这些关键帧建立连接
            vPairs.push_back(make_pair(mit->second,mit->first));
            // 对方关键帧也要添加这个信息
            // 更新KFcounter中该关键帧的mConnectedKeyFrameWeights
            // 更新其它KeyFrame的mConnectedKeyFrameWeights,更新其它关键帧与当前帧的连接权重
            (mit->first)->AddConnection(this,mit->second);
        }
    }

    //  Step 3 如果没有超过阈值的权重,则对权重最大的关键帧建立连接
    if(vPairs.empty())
    {
	    // 如果每个关键帧与它共视的关键帧的个数都少于th,
        // 那就只更新与其它关键帧共视程度最高的关键帧的mConnectedKeyFrameWeights
        // 这是对之前th这个阈值可能过高的一个补丁
        vPairs.push_back(make_pair(nmax,pKFmax));
        pKFmax->AddConnection(this,nmax);
    }

    //  Step 4 对满足共视程度的关键帧对更新连接关系及权重(从大到小)
    // vPairs里存的都是相互共视程度比较高的关键帧和共视权重,接下来由大到小进行排序
    sort(vPairs.begin(),vPairs.end());         // sort函数默认升序排列
    // 将排序后的结果分别组织成为两种数据类型
    list<KeyFrame*> lKFs;
    list<int> lWs;
    for(size_t i=0; i<vPairs.size();i++)
    {
        // push_front 后变成了从大到小顺序
        lKFs.push_front(vPairs[i].second);
        lWs.push_front(vPairs[i].first);
    }

    {
        unique_lock<mutex> lockCon(mMutexConnections);

        // mspConnectedKeyFrames = spConnectedKeyFrames;
        // 更新当前帧与其它关键帧的连接权重
        mConnectedKeyFrameWeights = KFcounter;
        mvpOrderedConnectedKeyFrames = vector<KeyFrame*>(lKFs.begin(),lKFs.end());
        mvOrderedWeights = vector<int>(lWs.begin(), lWs.end());

        // Step 5 更新生成树的连接
        if(mbFirstConnection && mnId!=0)
        {
            // 初始化该关键帧的父关键帧为共视程度最高的那个关键帧
            mpParent = mvpOrderedConnectedKeyFrames.front();
            // 建立双向连接关系,将当前关键帧作为其子关键帧
            mpParent->AddChild(this);
            mbFirstConnection = false;
        }
    }
}

 

七、结语

该篇博首先讲解了 LocalMapping::Run() 的总体流程,其中重要的几个函数分别为:

ProcessNewKeyFrame();
MapPointCulling();
CreateNewMapPoints();
SearchInNeighbors();
Optimizer::LocalBundleAdjustment(mpCurrentKeyFrame,&mbAbortBA, mpMap);
KeyFrameCulling();
mpLoopCloser->InsertKeyFrame(mpCurrentKeyFrame);

目前我们只对 ProcessNewKeyFrame() 进行了详细的讲解,接下来的博客,对每一个函数进行细致的讨论。

 
 
本文内容来自计算机视觉life ORB-SLAM2 课程课件

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本文主要介绍ORB-SLAM2中的一些关键实现细节,包括词袋建立、关键帧选择策略、词袋检索和位姿估计。此外还详细介绍了视觉里程计、闭环检测、地图维护等模块的实现细节。 首先,ORB-SLAM2通过建立词袋的方式实现了特征点的高效匹配。ORB-SLAM2采用了二叉树的结构生成了一个层次化的词袋,该词袋可以快速地检索到最相似的词,并将该词作为当前帧所属的类别。在后续的帧匹配过程中,ORB-SLAM2只需要搜索与当前帧类别相同的关键帧中的点即可,大大加快了匹配的效率。 其次,ORB-SLAM2采用了一种称为“闭线性三角测量”的方式估计位姿。该方法将两个视角下的匹配点转化为视差向量,并通过求解一组线性方程组来估计相邻帧之间的相对位姿。同时,该方法还能有效地处理重复区域和遮挡等问题,具有较高的鲁棒性。 此外,在关键帧选择方面,ORB-SLAM2采用了一种基于路标点的策略,即当当前帧与地图中的路标点距离较远时,就将当前帧作为关键帧。这种策略可以确保全局地图的均匀性和关键帧的稠密性。 最后,ORB-SLAM2采用了基于基础矩阵的闭环检测方法,该方法可以在时间和空间复杂度上达到较好的平衡。同时,ORB-SLAM2还采用了一种优化地图点云的方式,通过通过图优化的方式优化地图中的点云位置,确保了地图的准确性和一致性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值