(01)ORB-SLAM2源码无死角解析-(47) 跟踪线程→局部地图跟踪TrackLocalMap():局部地图更新

讲解关于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官方认证
 

一、前言

通过前面的博客,已经对 参考关键帧追踪TrackReferenceKeyFrame(),恒速模型跟踪当前普通帧TrackWithMotionModel() 以及 重定位跟踪 Relocalization()。他们分别适用于不同的情况,具体就不再这里提及了,因为在前面都进行了细致的分析,其中 恒速模型跟踪当前普通帧TrackWithMotionModel() 被使用得最多。

也就是说,对当前帧进行追踪,是从上诉的三种选择一种进行追踪,或者说位姿估算。这里可以很明显的看到,TrackReferenceKeyFrame() 与 TrackWithMotionModel() 追踪利用的都是一帧图像(最近关键帧或者上一帧),可想而知,其忽略了很多有价值的信息。虽然 Relocalization() 追踪使用了很多关键帧,利用的信息还是比较充足的,但是只有在追踪丢失的情况下才使用重定位追踪。

首先我们在src/Tracking.cc文件中,可以看到如下代码:

        // If we have an initial estimation of the camera pose and matching. Track the local map.
        // Step 3:在跟踪得到当前帧初始姿态后,现在对local map进行跟踪得到更多的匹配,并优化当前位姿
        // 前面只是跟踪一帧得到初始位姿,这里搜索局部关键帧、局部地图点,和当前帧进行投影匹配,得到更多匹配的MapPoints后进行Pose优化
        if(!mbOnlyTracking)
        {
            if(bOK)
                bOK = TrackLocalMap();
        }
        else
        {
            // mbVO true means that there are few matches to MapPoints in the map. We cannot retrieve
            // a local map and therefore we do not perform TrackLocalMap(). Once the system relocalizes
            // the camera we will use the local map again.

            // 重定位成功
            if(bOK && !mbVO)
                bOK = TrackLocalMap();
        }

可以看到,使用上诉三种跟踪方式中任意一种,只要跟踪成功,紧接着就会进行 局部地图追踪 TrackLocalMap()。局部地图追踪,可以理解为对上诉上种追踪获得位姿的优化。因为 局部地图跟踪 会采用更多的地图点进行追踪。

核心思想 : \color{red}{核心思想:} 核心思想: 在追踪的过程中,随着当前帧在一帧一帧的变化,可想而知,当前帧可以观测到的地图点也在变化,利用已经存在的地图点可以计算出相机位姿,同时当前帧会剩下一些未匹配上的关键点,那么这些关键点结合位姿则可以产生新的地图点,这些新的地图点可以提供给下一帧用于位姿估算。

大家这里注意一个点,当前帧生成新的地图点(核心代码为Tracking::UpdateLastFrame()),一般只会只会用于更新局部地图,只有当前帧为关键帧时,进行关键帧创建才会生成新的全局地图点。下面我们先来了解局部关键帧以及局部地图点的概念。

 

二、局部关键帧

从命名可以看到出来,所谓局部地图跟踪那么肯定是根据 局部地图点 进行跟踪,在了解 局部地图点 之前,先来看看局部关键帧。首先来查看下图:
在这里插入图片描述
一级共视关键帧 : \color{blue}{一级共视关键帧:} 一级共视关键帧: 假若当前帧为F,其能观测到地图中N个地图点,那么能够观测到这些地图点(至少一个)的关键帧,称为一级共视关键帧。如上图,当前帧F能观测到红色的三个地图点,同时这三个地图点又被 KF1与 KF2 观测到,那么那么称 KF1与 KF2 为 前帧F 的一级共视关键帧。

二级共视关键帧 : \color{blue}{二级共视关键帧:} 二级共视关键帧: F的一级共视关键帧 KF1与 KF2 分别能够观测到上图的 KF1地图点(绿色),KF2地图点(绿色)。那么称能够观测到 KF1地图点 的关键帧为 KF1的共视关键帧,也叫 KF1的一级共视关键帧。此时 KF1的共视关键帧 对于 F来说,就是 二级共视关键帧。同理,KF2 对于 F 来说,也就是 二级共视关键帧。

当前帧 : \color{blue}{当前帧}: 当前帧: mCurrentFrame(当前帧是普通帧)
参考关键帧 : \color{blue}{参考关键帧:} 参考关键帧: 与当前帧共视程度最高的关键帧作为参考关键帧,mCurrentFrame.mpReferenceKF,在KeyFrame::UpdateConnections() 里确定关键帧的父子关系(当前帧必须是关键帧)
父关键帧 : \color{blue}{父关键帧:} 父关键帧: 和当前关键帧共视程度最高的关键帧
子关键帧 : \color{blue}{子关键帧:} 子关键帧: 是上述父关键帧的子关键帧
m v p L o c a l M a p P o i n t s : \color{blue}{mvpLocalMapPoints:} mvpLocalMapPoints: 示意图

局部关键帧 : \color{red}{局部关键帧:} 局部关键帧:
        ①当前帧的一级共视关键帧(包含所有)。
        ②当前帧的二级共视关键帧(只取了共视程度最高的一帧)。
        ③前面两种共视帧的父关键帧与子关键帧

局部关键帧主要包含了上述三种关键帧,可想而只,随着当前帧在变化,那么当前帧观测到的地图点也随着变化,从而局部关键帧也在变化。更新局部关键帧的代码位于 sec/Tracking.cc 文件中,注释如下:

/**
 * @brief 跟踪局部地图函数里,更新局部关键帧
 * 方法是遍历当前帧的地图点,将观测到这些地图点的关键帧和相邻的关键帧及其父子关键帧,作为mvpLocalKeyFrames
 * Step 1:遍历当前帧的地图点,记录所有能观测到当前帧地图点的关键帧 
 * Step 2:更新局部关键帧(mvpLocalKeyFrames),添加局部关键帧包括以下3种类型
 *      类型1:能观测到当前帧地图点的关键帧,也称一级共视关键帧
 *      类型2:一级共视关键帧的共视关键帧,称为二级共视关键帧
 *      类型3:一级共视关键帧的子关键帧、父关键帧
 * Step 3:更新当前帧的参考关键帧,与自己共视程度最高的关键帧作为参考关键帧
 */
void Tracking::UpdateLocalKeyFrames()
{
    // Each map point vote for the keyframes in which it has been observed
    // Step 1:遍历当前帧的地图点,记录所有能观测到当前帧地图点的关键帧
    map<KeyFrame*,int> keyframeCounter;
    for(int i=0; i<mCurrentFrame.N; i++)
    {
        if(mCurrentFrame.mvpMapPoints[i])
        {
            MapPoint* pMP = mCurrentFrame.mvpMapPoints[i];
            if(!pMP->isBad())
            {
                // 得到观测到该地图点的关键帧和该地图点在关键帧中的索引
                const map<KeyFrame*,size_t> observations = pMP->GetObservations();
                // 由于一个地图点可以被多个关键帧观测到,因此对于每一次观测,都对观测到这个地图点的关键帧进行累计投票
                for(map<KeyFrame*,size_t>::const_iterator it=observations.begin(), itend=observations.end(); it!=itend; it++)
                    // 这里的操作非常精彩!
                    // map[key] = value,当要插入的键存在时,会覆盖键对应的原来的值。如果键不存在,则添加一组键值对
                    // it->first 是地图点看到的关键帧,同一个关键帧看到的地图点会累加到该关键帧计数
                    // 所以最后keyframeCounter 第一个参数表示某个关键帧,第2个参数表示该关键帧看到了多少当前帧(mCurrentFrame)的地图点,也就是共视程度
                    keyframeCounter[it->first]++;      
            }
            else
            {
                mCurrentFrame.mvpMapPoints[i]=NULL;
            }
        }
    }

    // 没有当前帧没有共视关键帧,返回
    if(keyframeCounter.empty())
        return;

    // 存储具有最多观测次数(max)的关键帧
    int max=0;
    KeyFrame* pKFmax= static_cast<KeyFrame*>(NULL);

    // Step 2:更新局部关键帧(mvpLocalKeyFrames),添加局部关键帧有3种类型
    // 先清空局部关键帧
    mvpLocalKeyFrames.clear();
    // 先申请3倍内存,不够后面再加
    mvpLocalKeyFrames.reserve(3*keyframeCounter.size());

    // All keyframes that observe a map point are included in the local map. Also check which keyframe shares most points
    // Step 2.1 类型1:能观测到当前帧地图点的关键帧作为局部关键帧 (将邻居拉拢入伙)(一级共视关键帧) 
    for(map<KeyFrame*,int>::const_iterator it=keyframeCounter.begin(), itEnd=keyframeCounter.end(); it!=itEnd; it++)
    {
        KeyFrame* pKF = it->first;

        // 如果设定为要删除的,跳过
        if(pKF->isBad())
            continue;
        
        // 寻找具有最大观测数目的关键帧
        if(it->second>max)
        {
            max=it->second;
            pKFmax=pKF;
        }

        // 添加到局部关键帧的列表里
        mvpLocalKeyFrames.push_back(it->first);
        
        // 用该关键帧的成员变量mnTrackReferenceForFrame 记录当前帧的id
        // 表示它已经是当前帧的局部关键帧了,可以防止重复添加局部关键帧
        pKF->mnTrackReferenceForFrame = mCurrentFrame.mnId;
    }


    // Include also some not-already-included keyframes that are neighbors to already-included keyframes
    // Step 2.2 遍历一级共视关键帧,寻找更多的局部关键帧 
    for(vector<KeyFrame*>::const_iterator itKF=mvpLocalKeyFrames.begin(), itEndKF=mvpLocalKeyFrames.end(); itKF!=itEndKF; itKF++)
    {
        // Limit the number of keyframes
        // 处理的局部关键帧不超过80帧
        if(mvpLocalKeyFrames.size()>80)
            break;

        KeyFrame* pKF = *itKF;

        // 类型2:一级共视关键帧的共视(前10个)关键帧,称为二级共视关键帧(将邻居的邻居拉拢入伙)
        // 如果共视帧不足10帧,那么就返回所有具有共视关系的关键帧
        const vector<KeyFrame*> vNeighs = pKF->GetBestCovisibilityKeyFrames(10);
        // vNeighs 是按照共视程度从大到小排列
        for(vector<KeyFrame*>::const_iterator itNeighKF=vNeighs.begin(), itEndNeighKF=vNeighs.end(); itNeighKF!=itEndNeighKF; itNeighKF++)
        {
            KeyFrame* pNeighKF = *itNeighKF;
            if(!pNeighKF->isBad())
            {
                // mnTrackReferenceForFrame防止重复添加局部关键帧
                if(pNeighKF->mnTrackReferenceForFrame!=mCurrentFrame.mnId)
                {
                    mvpLocalKeyFrames.push_back(pNeighKF);
                    pNeighKF->mnTrackReferenceForFrame=mCurrentFrame.mnId;
                    //? 找到一个就直接跳出for循环?
                    break;
                }
            }
        }

        // 类型3:将一级共视关键帧的子关键帧作为局部关键帧(将邻居的孩子们拉拢入伙)
        const set<KeyFrame*> spChilds = pKF->GetChilds();
        for(set<KeyFrame*>::const_iterator sit=spChilds.begin(), send=spChilds.end(); sit!=send; sit++)
        {
            KeyFrame* pChildKF = *sit;
            if(!pChildKF->isBad())
            {
                if(pChildKF->mnTrackReferenceForFrame!=mCurrentFrame.mnId)
                {
                    mvpLocalKeyFrames.push_back(pChildKF);
                    pChildKF->mnTrackReferenceForFrame=mCurrentFrame.mnId;
                    //? 找到一个就直接跳出for循环?
                    break;
                }
            }
        }

        // 类型3:将一级共视关键帧的父关键帧(将邻居的父母们拉拢入伙)
        KeyFrame* pParent = pKF->GetParent();
        if(pParent)
        {
            // mnTrackReferenceForFrame防止重复添加局部关键帧
            if(pParent->mnTrackReferenceForFrame!=mCurrentFrame.mnId)
            {
                mvpLocalKeyFrames.push_back(pParent);
                pParent->mnTrackReferenceForFrame=mCurrentFrame.mnId;
                //! 感觉是个bug!如果找到父关键帧会直接跳出整个循环
                break;
            }
        }

    }

    // Step 3:更新当前帧的参考关键帧,与自己共视程度最高的关键帧作为参考关键帧
    if(pKFmax)
    {
        mpReferenceKF = pKFmax;
        mCurrentFrame.mpReferenceKF = mpReferenceKF;
    }
}

 

三、局部地图点

局部关键帧能够观测到的所有地图点,总和起来,称为局部地图点,如下图,其中红色的部分称为就是局部地图点:
在这里插入图片描述
其代码还是十分简单的,位于 sec/Tracking.cc 文件中,注释如下:

/*
 * @brief 更新局部关键点。先把局部地图清空,然后将局部关键帧的有效地图点添加到局部地图中
 */
void Tracking::UpdateLocalPoints()
{
    // Step 1:清空局部地图点
    mvpLocalMapPoints.clear();

    // Step 2:遍历局部关键帧 mvpLocalKeyFrames
    for(vector<KeyFrame*>::const_iterator itKF=mvpLocalKeyFrames.begin(), itEndKF=mvpLocalKeyFrames.end(); itKF!=itEndKF; itKF++)
    {
        KeyFrame* pKF = *itKF;
        const vector<MapPoint*> vpMPs = pKF->GetMapPointMatches();

        // step 2:将局部关键帧的地图点添加到mvpLocalMapPoints
        for(vector<MapPoint*>::const_iterator itMP=vpMPs.begin(), itEndMP=vpMPs.end(); itMP!=itEndMP; itMP++)
        {
            MapPoint* pMP = *itMP;
            if(!pMP)
                continue;
            // 用该地图点的成员变量mnTrackReferenceForFrame 记录当前帧的id
            // 表示它已经是当前帧的局部地图点了,可以防止重复添加局部地图点
            if(pMP->mnTrackReferenceForFrame==mCurrentFrame.mnId)
                continue;
            if(!pMP->isBad())
            {
                mvpLocalMapPoints.push_back(pMP);
                pMP->mnTrackReferenceForFrame=mCurrentFrame.mnId;
            }
        }
    }
}

 

四、结语

上述的的两个函数 UpdateLocalKeyFrames 与 UpdateLocalPoints 都是在 Tracking::UpdateLocalMap() 中被调用,如下所示:

/**
 * @brief 更新LocalMap
 *
 * 局部地图包括: 
 * 1、K1个关键帧、K2个临近关键帧和参考关键帧
 * 2、由这些关键帧观测到的MapPoints
 */
void Tracking::UpdateLocalMap()
{
    // This is for visualization
    // 设置参考地图点用于绘图显示局部地图点(红色)
    mpMap->SetReferenceMapPoints(mvpLocalMapPoints);

    // Update
    // 用共视图来更新局部关键帧和局部地图点
    UpdateLocalKeyFrames();
    UpdateLocalPoints();
}

可以明显的知道,更新了局部关键帧与局部地图点之后,相当于对局部地图进行了更新,那么其更新的目的时什么呢?当然是为了 局部地图跟踪TrackLocalMap() 服务,在下一篇博客中进行详细的讲解。

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

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值