ORB-SLAM3算法和代码学习——重定位Relocalization

news2024/11/17 9:40:49

0总述

重定位是ORB-SLAM系列保持跟踪稳定性的保障,从ORB-SLAM沿用至ORB-SLAM3。主要作用是在跟踪失败时,通过词袋向量搜索在关键帧数据库中寻找和当前帧相似的关键帧作为匹配帧,建立数据关联并计算当前帧的位姿,恢复相机的运动。

1重定位触发条件

当上一帧在跟踪运动模型环节TrackWithMotionModel()或者跟踪参考关键帧环节TrackReferenceKeyFrame跟踪失败时,会对当前帧的跟踪状态有一个判断。

  • 条件1:当前地图中关键帧数目较多(大于10)
  • 条件2(隐藏条件):当前帧距离上次重定位帧超过1s(说明还比较争气,值的救)
  • 条件3:不是IMU模式

同时满足条件1,2 || 1,3 则将状态标记为RECENTLY_LOST;如果是纯视觉模式,则在下一帧会进入重定位函数;如果是VI模式,则在下一帧会通过预积分更新位姿。

2重定位过程

  • Step 1:计算当前帧特征点的词袋向量
  • Step 2:找到与当前帧相似的候选关键帧
  • Step 3:通过BoW进行匹配
  • Step 4:通过EPnP算法估计姿态
  • Step 5:通过PoseOptimization对姿态进行优化求解
  • Step 6:如果内点较少,则通过投影的方式对之前未匹配的点进行匹配,再进行优化求解

2.1计算当前帧特征点的词袋向量

即将当前帧特征点对应的描述子向量转化为词袋向量,

mCurrentFrame.ComputeBoW();

主要使用DBOW库的函数,和跟踪参考关键帧中过程相同

mpORBvocabulary->transform(vCurrentDesc,// 当前的描述子vector
                                    mBowVec,// 输出,词袋向量,记录的是单词的id及其对应权重TF-IDF值
                                    mFeatVec,// 输出,记录node id及其对应的图像 feature对应的索引
                                    4);// 4表示从叶节点向前数的层数

2.2在当前子地图找到与当前帧相似的候选关键帧组

vector<KeyFrame*> vpCandidateKFs = mpKeyFrameDB->DetectRelocalizationCandidates(&mCurrentFrame, mpAtlas->GetCurrentMap());

步骤1:找出和当前帧具有公共单词的所有关键帧
遍历当前帧词袋向量中的每一个单词,对于每一个单词,会在关键帧数据库中寻找包含该单词的关键帧,并记录该关键帧与当前帧的公共单词数量pKFi->mnRelocWords。如果找不到候选关键帧,本次重定位就宣告失败了。

// 步骤1:找出和当前帧具有公共单词的所有关键帧
{
    unique_lock<mutex> lock(mMutex);
    // 遍历当前帧所有的单词
    for(DBoW2::BowVector::const_iterator vit=F->mBowVec.begin(), vend=F->mBowVec.end(); vit != vend; vit++)
    {
        // lKFs表示包含了第vit->first个单词的所有关键帧
        // 在mvInvertedFile中,每一个单词都对应一个list<KeyFrame*>
        list<KeyFrame*> &lKFs = mvInvertedFile[vit->first];
        // 遍历这些关键帧
        for(list<KeyFrame*>::iterator lit=lKFs.begin(), lend= lKFs.end(); lit!=lend; lit++)
        {
            KeyFrame* pKFi=*lit;
            // 如果pKFi还没有标记为重定位的候选帧,将该关键帧重定位相关的属性初始化一下
            // 因为两帧之间可能有多个公共单词,所以在遍历之前的单词时就将该关键帧进行了标记
            if(pKFi->mnRelocQuery!=F->mnId)
            {
                pKFi->mnRelocWords=0;// 共有的单词数
                pKFi->mnRelocQuery=F->mnId;// 具有相同单词的F的ID
                lKFsSharingWords.push_back(pKFi);// 放入候选
            }
            // 如果已经被标记过了,直接将公共单词数自增1
            pKFi->mnRelocWords++;
        }
    }
}

步骤2:只和具有共同单词较多的关键帧进行相似度计算
为了进一步保证重定位的鲁棒性,在候选关键帧比较多的的情况下会择优进行筛选,挑选和当前帧公共单词较多的关键帧进行后面相似度计算等相关操作。

对于关键帧和普通的图像帧来说,该帧对应的描述子都可以被转化为一个对应的词袋向量,这个词袋向量包含了很多单词,向量非零部分的元素表示该图像包含某个单词,0表示不包含某个单词。相似度计算就是指计算两个图像帧之间词袋向量的差异,公共单词数越多,最终相似性得分就越高。在ORB-SLAM系列中采用L1范数计算两个词袋向量之间的差异。具体计算过程参考下图:

在这里插入图片描述

    // 遍历所有闭环候选帧,挑选出共有单词数大于阈值minCommonWords且单词匹配度大于minScore存入lScoreAndMatch
    // 遍历所有闭环候选帧,挑选出共有单词数大于阈值minCommonWords和相似性得分
    for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(), lend= lKFsSharingWords.end(); lit!=lend; lit++)
    {
        KeyFrame* pKFi = *lit;
        if(pKFi->mnRelocWords > minCommonWords)
        {
            //nscores++;
            float si = mpVoc->score(F->mBowVec,pKFi->mBowVec);// 计算相似性得分
            pKFi->mRelocScore=si;// 记录相似性得分
            lScoreAndMatch.push_back(make_pair(si,pKFi));// 先不进行筛选,全都放到容器中
        }
    }

步骤3:将与关键帧相连(权值最高)的前十个关键帧归为一组,计算累计得分
为了保证所选出的候选关键帧和当前帧具备较好的相似性,在进行上述操作初步筛选出与当前帧相似性较高的候选关键帧后,还要更进一步得看这些关键帧的共视关键帧与当前帧的相似性,具体操作如下:

首先,对于每个候选关键帧,选择与其共视程度最高,权重最高的前10个关键帧划分为一组,如果少于10个则选择全部的共视关键帧

要注意的是,对于候选关键帧的共视关键帧,只有和当前帧存在公共单词的共视关键帧才会参与到下一步的计算

vector<KeyFrame*> vpNeighs = pKFi->GetBestCovisibilityKeyFrames(10);

然后,遍历每一个组的关键帧,统计每个组得分最高的关键帧,以及总得分最高的组对应的最高分数bestAccScore

    // 单单计算当前帧和某一关键帧的相似性是不够的,这里将与关键帧相连(权值最高,共视程度最高)的前十个关键帧归为一组,计算累计得分
    // 具体而言:lScoreAndMatch中每一个KeyFrame都把与自己共视程度较高的帧归为一组,每一组会计算组得分并记录该组分数最高的KeyFrame,记录于lAccScoreAndMatch
    for(list<pair<float,KeyFrame*> >::iterator it=lScoreAndMatch.begin(), itend=lScoreAndMatch.end(); it!=itend; it++)
    {
        // 得到与该关键帧连接的前N个关键帧(已按权值排序),如果连接的关键帧少于N,则返回所有连接的关键帧
        KeyFrame* pKFi = it->second;
        vector<KeyFrame*> vpNeighs = pKFi->GetBestCovisibilityKeyFrames(10);

        float bestScore = it->first;// 该组最高分数
        float accScore = bestScore;// 该组累计得分
        KeyFrame* pBestKF = pKFi;// 该组最高分数对应的关键帧
        // 遍历该组的10个关键帧
        for(vector<KeyFrame*>::iterator vit=vpNeighs.begin(), vend=vpNeighs.end(); vit!=vend; vit++)
        {
            KeyFrame* pKF2 = *vit;
            if(pKF2->mnRelocQuery!=F->mnId)
                continue;

            accScore+=pKF2->mRelocScore;// 只有pKF2也在闭环候选帧中(重定位候选帧中),才能贡献分数
            if(pKF2->mRelocScore>bestScore)// 统计得到组里分数最高的KeyFrame
            {
                pBestKF=pKF2;
                bestScore = pKF2->mRelocScore;
            }

        }
        lAccScoreAndMatch.push_back(make_pair(accScore,pBestKF));// 记录所有组中组得分最高的关键帧
        if(accScore>bestAccScore)
            bestAccScore=accScore;// 得到所有组中最高的累计得分
    }

然后,根据最高分数bestAccScore再从每个组得分最高的关键帧里挑选大于阈值的关键帧,然后去掉不属于当前活跃子地图的关键帧,其余的作为最后的候选关键帧返回。

2.3遍历所有的候选关键帧,通过BoW进行快速匹配,用匹配结果初始化PnP Solver

  • 首先,通过BOW词袋向量搜索,快速查找候选关键帧与当前帧之间的匹配关系。这个过程与跟踪参考关键帧中的SearchByBoW相同,详细见之前的博客。
int nmatches = matcher.SearchByBoW(pKF,mCurrentFrame,vvpMapPointMatches[i]);
  • 然后,将计算得到的匹配关系初始化MLPNP求解器pSolver,并重新设置RanSAC迭代求解器的相关参数
    for(int i=0; i<nKFs; i++)
    {
        KeyFrame* pKF = vpCandidateKFs[i];
        // 如果当前帧已经被判定为无效帧,跳过(什么时候会被判定为无效帧????)
        if(pKF->isBad())
            vbDiscarded[i] = true;
        else
        {
            // 当前帧和候选关键帧用BoW进行快速匹配,匹配结果记录在vvpMapPointMatches,nmatches表示匹配的数目
            // vvpMapPointMatches是一个2维vector数组,每一个元素为该关键帧中和当前帧成功匹配的所有特征点id
            int nmatches = matcher.SearchByBoW(pKF,mCurrentFrame,vvpMapPointMatches[i]);
            // 如果匹配数目小于15,认为该帧无效,跳过
            if(nmatches<15)
            {
                vbDiscarded[i] = true;
                continue;
            }
            else
            {
                // 如果匹配数目够用,用匹配结果初始化MLPnPsolver
                // ? 为什么用MLPnP? 因为考虑了鱼眼相机模型,解耦某些关系?
                // 参考论文《MLPNP-A REAL-TIME MAXIMUM LIKELIHOOD SOLUTION TO THE PERSPECTIVE-N-POINT PROBLEM》
                MLPnPsolver* pSolver = new MLPnPsolver(mCurrentFrame,vvpMapPointMatches[i]);
                // 初始化Ransac迭代器参数,构造函数调用了一遍,这里重新设置参数
                // 0.99,      // 模型最大概率值,默认0.9
                // 10,        // 内点的最小阈值,默认8
                // 300,       // 最大迭代次数,默认300
                // 6,         // 最小集,每次采样六个点,即最小集应该设置为6,论文里面写着I > 5
                // 0.5,       // 理论最少内点个数,这里是按照总数的比例计算,所以epsilon是比例,默认是0.4
                // 5.991;     // 卡方检验阈值 //This solver needs at least 6 points
                pSolver->SetRansacParameters(0.99,10,300,6,0.5,5.991);  //This solver needs at least 6 points
                // 对于每一对较好的匹配,都需要使用匹配结果初始化MLPNP求解器
                vpMLPnPsolvers[i] = pSolver;
                nCandidates++;
            }
        }
    }

2.4遍历所有符合条件的候选关键帧,直到找到能够匹配上的关键帧

1.基于RanSAC使用MLPNP求解当前帧的初始位姿

对于每一对匹配,在步骤3都构造了一个对应的求解器。求解器pSolver共迭代5次,对于每一次都使用RanSAC算法估计当前帧的位姿,RanSAC停止迭代的条件是达到最大的迭代次数或者提前找到的小于阈值的最优解。

如果是因为迭代次数达到最大值而停止迭代,说明该候选关键帧不好,将其设置为无效帧。

位姿求解器最终返回位姿是否计算成功的flag、每一对匹配点的匹配情况即是否为内点、内点的数量和初始位姿

// pnp求解位姿
bool bTcw = pSolver->iterate(5,bNoMore,vbInliers,nInliers, eigTcw);

2.如果MLPNP计算出了位姿,对内点进行优化

这里的位姿优化和跟踪运动模型或者跟踪参考关键帧使用的是同一个函数,以MLPNP的求解结果作为初始位姿,使用BA对当前的位姿进行优化,并进一步更新内点的情况,在优化函数的内部隐式地更新当前帧的位姿,返回内点的数量。

int nGood = Optimizer::PoseOptimization(&mCurrentFrame);

3.如果内点数量较少,则使用跟踪运动模型中的投影匹配

这一步又和跟踪运动模型相同了,主要是关键帧对应3D地图点到当前帧的投影匹配计算相机位姿,每经过一次投影匹配查找就会使用一次非线性优化,更新相机位姿,投影匹配详细的内容可以见另一篇跟踪参考关键帧的介绍文章。

首先将参考关键帧中对应的地图点根据相机内参投影到当前帧图像上(因为地图点都是在世界坐标系下,因此可以直接通过内参投影到当前帧)

根据特征点的网格信息和搜索半径,减小特征匹配的搜索范围加速特征匹配

然后就是进行描述子距离计算和旋转一致性检测,获得最终的匹配关系。

最终根据优化后内点的数量确定重定位是否成功,阈值为50,内点数量大于50则判定重定位成功,下一帧继续正常跟踪,反之则跟踪彻底凉凉,等待系统的重新初始化。

if(nGood<50)
{
    // 通过投影的方式将关键帧中未匹配的地图点投影到当前帧中, 生成新的匹配
    int nadditional =matcher2.SearchByProjection(mCurrentFrame,vpCandidateKFs[i],sFound,10,100);
    // 根据投影匹配的结果,再次采用3D-2D pnp BA优化位姿
    if(nadditional+nGood>=50)
    {
        // 记录再次优化后的内点数量
        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)
        {
            // 重新获取经过上述BOW匹配以及投影匹配后得到的Mappoint
            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,3,64);

            // Final optimization
            // 最终经过BOW匹配-优化-投影匹配-优化-缩小半径投影匹配之后,内点数量依然不到50,
            // 判定基于该候选关键帧进行的重定位失败
            // 如果成功了,就更新下当前帧的Mappoint
            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(nGood>30 && nGood<50)
    }//if(nadditional+nGood>=50)
}//if(nGood<50)

综上可以看到,重定位函数其实就是跟踪运动模型和跟踪参考关键帧的综合,首先通过BOW搜索寻找匹配关系然后MLPNP求解位姿和BA优化;如果结果不满足要求再进行投影匹配的BA优化,根据最终优化后内点的数量判断重定位是否成功。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/186813.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

正大国际期货:外盘短线交易九大生存准则:从亏损预期出发

一、生存是第一位 这并不是陈词滥调&#xff0c;投机是非常危险的活动。投机非并输赢那样简单&#xff0c;首要的任务是在顶峰和谷底之间的波动中生存&#xff0c;如果连生存都做不到&#xff0c;你根本就没有谈及赢的资格。 即使有了好的资金管理、正确的系统和行动的前提&a…

Ubuntu18.04下安装配置AndroidStudio软件图文教程

运行环境&#xff1a;操作系统为Ubuntu18.04&#xff0c;android-studio版本为2022.1.1.19-linux&#xff0c;Java版本为jdk8,安装路径/opt/android-studio/,当前用户为xqf222,sdk下载路径默认为/home/xqf222/Android/Sdk 详细步骤和指令如下&#xff1a; 1.安装JDK8&#xf…

VTK CT重建(一) MPR 多层面重建 四视图

除了MPR之外&#xff0c;在CT重建后处理中还有很多别的常用方法&#xff0c;包括 多层面重建&#xff08;MPR&#xff09;最大密度投影&#xff08;MIP&#xff09;最小密度投影&#xff08;MinIP&#xff09;表面阴影遮盖&#xff08;SSD&#xff09;容积漫游技术&#xff08…

go validator参数校验器自定义规则及提示(自定义异常返回提示语)

原文连接&#xff1a;https://segmentfault.com/a/1190000040445612 笔者针对参数为指针的情况做了一点小优化 这里我们用validator用来做参数校验&#xff0c;gin默认的github.com/go-playground/validator&#xff0c;可以直接使用 除此之外还有一些其他的工具也挺好用的&am…

Linux基础指令

本文已收录至《Linux知识与编程》专栏&#xff01;作者&#xff1a;ARMCSKGT演示环境&#xff1a;CentOS 7 目录 前言 正文 查看当前用户whoami 查看当前目录路径pwd 清理屏幕clear 查看目录下文件指令ls 进入目录指令cd 以树状结构显示目录文件tree 创建普通文件指令t…

Leetcode.1669 合并两个链表

题目链接 Leetcode.1669 合并两个链表 Rating : 1428 题目描述 给你两个链表 list1和 list2&#xff0c;它们包含的元素分别为 n个和 m个。 请你将 list1中下标从 a到 b的全部节点都删除&#xff0c;并将list2接在被删除节点的位置。 示例 1&#xff1a; 输入&#xff1a;li…

rtsp实时流通过rtmp推送到服务端

ffmpeg可以实现这个功能。ffmpeg支持rtsp协议&#xff0c;也支持rtmp。在这个案例中rtsp是输入&#xff0c; rtmp是输出&#xff0c;ffmpeg实现了转码的功能。下面可出一个整体思路流程图。 如图1所示&#xff1a;在获取都rtsp流以后&#xff0c;解复用&#xff08;demux&…

检测之VOC转YOLO

文章目录检测所用数据有几种文件格式&#xff0c;我们对于检测&#xff0c;将使用VOC格式做为基础&#xff0c;与其它格式的的互转实现部分如下&#xff1a;检测系列相关文章参考如下链接&#xff1a; VOC数据的结构介绍及自定义生成&#xff0c;用labelimg自已标注VOC标准数据…

Notepad++作死,国产文本编辑器Notepad--发布

作死的Notepad Notepad 和 Notepad 都是基于 Windows 的文本编辑器&#xff0c;通常用于编写和编辑纯文本文件。 这两个应用程序都是简单的轻量级程序&#xff0c;提供基本的文本编辑功能。 Notepad是一口君经常使用的一款文本编辑软件&#xff0c;用了大概10年了。 然而Not…

配置并行(RH294)

当Ansible处理playbook的时候会顺序运行每个play确定play的主机列表之后Ansible将按顺序运行每个任务一般来说&#xff0c;所有主机必须在任何主机在play中启动下一个任务之前成功完成任务理论上&#xff0c;Ansible可以同时连接到play中的所有主机来执行每项任务Ansible所进行…

​力扣解法汇总1669. 合并两个链表

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣 描述&#xff1a; 给你两个链表 list1 和 list2 &#xff0c;它们包含的元素分别为 n 个和 m 个。…

解决Vue启动失败报错:Module not found: Error: Can‘t resolve ‘less-loader‘

问题描述 今天想在网上找一个好看的登录页面&#xff0c;把别人的代码引入进来之后&#xff0c;发现项目编译不了&#xff0c;并且报错了&#xff1a; Module not found: Error: Can’t resolve ‘less-loader’ 分析问题 从错误的日志就可以看出来&#xff0c;是缺少了less-…

Linux: 关于 SIGCHLD 的更多细节

僵尸进程 何为僵尸进程&#xff1f; 一个进程使用fork创建子进程&#xff0c;如果子进程退出&#xff0c;而父进程并没有调用 wait 或 waitpid获取子进程的状态信息&#xff0c;那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程成为僵尸进程的因素 子进程 先…

AOP的一点浅薄理解

AOP思想应该怎么去理解&#xff01; Aspect&#xff08;切面&#xff09;&#xff1a; Aspect 声明类似于 Java 中的类声明&#xff0c;在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。 Joint point&#xff08;连接点&#xff09;&#xff1a;表示在程序中明确定义的点…

C语言学习笔记-变量

我们知道每一个程序的运行都需要内存&#xff0c;那么C语言的变量的定义是什么含义呢&#xff1f; 假如我花了200元买了一块4G内存条&#xff0c;然后我定义了一个int a ;就意味着从这4G的内存上要拿走4个字节&#xff0c;又定义了一个int b&#xff1b;那么b同样也要从4G的内存…

【OpenGL学习】OpenGL实现 基于Phong模型的基础光照

基于Phong模型的基础光照 在本节中&#xff0c;我们将利用 Phong 光照模型来完成一个简单的光照场景的渲染。 一、Phong 光照模型 Phong光照模型是20世纪70年代被提出的一种渲染逼真图像的方法&#xff0c;模型的提出者是越南出生的计算机图形学研究员Bui Tuong Phong&#…

JavaScript中的String和自定义对象~

String对象&#xff1a; 它是 JavaScript 的一种基本的数据类型 String 对象的 length 属性声明了该字符串中的字符数&#xff0c;String 类定义了大量操作字符串的方法&#xff0c;例如从字符串中提取字符或子串&#xff0c;或者检索字符或子串 需要注意的是&#xff0c;Ja…

单行文本域,多行文本域隐藏问题

多行文本域隐藏问题 overflow: hidden; 首先是溢出隐藏&#xff0c;不可或缺 display: -webkit-box; 以弹性盒模型显示 -webkit-box-orient: vertical; 盒模型元素的排列方式 -webkit-line-clamp: 3; 显示行数 <style>.postnameStyle{font-size: 30rpx;font-weight: …

【科研】ET-BERT资料库梳理

作者原repo链接 https://github.com/linwhitehat/ET-BERT 0.资料总库 分为数据模型语料库 1.数据集 包含fine-tuning数据集&#xff08;cstnet-tls 1.3&#xff09;与公开数据集&#xff08;USTC-TFC、VPN-app、VPN-service的数据包级和流级&#xff09;目录链接 1.1 微调…

【博客602】net.ipv4.conf.eth0.route_localnet的作用

net.ipv4.conf.eth0.route_localnet的作用 背景&#xff1a;默认情况下不能将本机的请求跳转/转发到回环接口上 在某些场景下会用在一台主机内网络流量重定向&#xff0c;比如将在本机回环设备中的数据包强行转发到另一台主机上。结果发现原本在正常的NAT场景中生效的iptables…