从ORB-SLAM2的源码工程中学习C++编程——关键帧数据库

关键帧数据库

构建关键帧数据库,可以联系链表等常用数据结构的构建过程:创建、增加元素、删除元素、清理。

首先需要明确数据存储的数据类型:以关键帧作为数据库的元素。

这个地方需要理解两个概念:单词(词袋)和关键帧。
单词(词袋):预先构建好的,离线词典(ORBvoc.txt):它是DBoW2作者使用orb特征,使用大量图片训练的结果。

关键帧数据库-创建

根据单词数量确定关键帧的有多少个关键帧list:
单词数量同样表述的含义:每帧图像最多具备的特征数量,单词对应的特征,每个单词对应的list的关键帧,表述为此list下的关键帧都具有此特征。

std::vector<list<KeyFrame*> > mvInvertedFile; ///< 倒排索引,mvInvertedFile[i]表示包含了第i个word id的所有关键帧

是一个向量,里面存放的是一个个单词,每个单词对应一个list链表,每个链表里存放的是拥有该单词的关键帧。

KeyFrameDatabase::KeyFrameDatabase (const ORBVocabulary &voc):
    mpVoc(&voc)
{
    mvInvertedFile.resize(voc.size()); // number of words
}

此处需要理解:vector与list的区别:

STL提供三个基本的容器:vector、list、deque;
  • vector和built-in数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随即存取,即[]操作符,但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝,另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。
  • list就是数据结构中的双向链表(根据sgi stl源代码),因此它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随即存取变的非常没有效率,因此它没有提供[]操作符的重载。但由于链表的特点,它可以以很好的效率支持任意地方的删除和插入。
  • deque介于vector与list之间;

1、如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
2、如果你需要大量的插入和删除,而不关心随即存取,则应使用list
3、如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque。
vector适用:对象数量变化少,简单对象,随机访问元素频繁
list适用:对象数量变化大,对象复杂,插入和删除频繁

关键帧数据库-增加关键帧

根据关键帧具备的特征信息,将其添加到特征(单词)对应的列表中。

/**
 * @brief 根据关键帧的词包,更新数据库的倒排索引
 * @param pKF 关键帧
 */
void KeyFrameDatabase::add(KeyFrame *pKF)
{
    unique_lock<mutex> lock(mMutex);

    // 为每一个word添加该KeyFrame
    for(DBoW2::BowVector::const_iterator vit= pKF->mBowVec.begin(), vend=pKF->mBowVec.end(); vit!=vend; vit++)
        mvInvertedFile[vit->first].push_back(pKF);
}
DBoW2::BowVector::const_iterator表示一个map<DBoW2::WordID, DBoW2::WordValue>
vit->first  代表 关键帧对应的单词的ID号
mvInvertedFile[vit->first].push_back(pKF) 代表将关键帧插入到遍历到的单词对应的ID对应的list列表中。

由于系统是多线程运行的,故存在关键帧数据同时存取的可能,因此需要加锁,防止线程竞争。
unique_lock中的unique表示独占所有权。
unique_lock独占的是mutex对象,就是对mutex锁的独占。
用法:
(1)新建一个unique_lock 对象
(2)给对象传入一个std::mutex 对象作为参数;
std::mutex mymutex;
unique_lock< mutex > lock(mymutex);

因此加锁时新建一个对象lock
unique_lock< mutex > lock(mymutex);
而这个对象生命周期结束后自动解锁。

关键帧数据库-删除关键帧

需要删除每个单词对应的list列表下的关键帧;
因此需要选确定关键帧对应的单词,在进行单词向量的遍历,进而删除每个单词下的关键帧。

首先遍历关键帧对应的单词向量,找到对应的单词下的关键帧列表,然后遍历此关键帧列表,寻找与此关键帧相等的关键帧,然后擦除列表中此关键帧。

/**
 * @brief 关键帧被删除后,更新数据库的倒排索引
 * @param pKF 关键帧
 */
void KeyFrameDatabase::erase(KeyFrame* pKF)
{
    unique_lock<mutex> lock(mMutex);

    // Erase elements in the Inverse File for the entry
    // 每一个KeyFrame包含多个words,遍历mvInvertedFile中的这些words,然后在word中删除该KeyFrame
    for(DBoW2::BowVector::const_iterator vit=pKF->mBowVec.begin(), vend=pKF->mBowVec.end(); vit!=vend; vit++)
    {
        // List of keyframes that share the word
        list<KeyFrame*> &lKFs = mvInvertedFile[vit->first];

        for(list<KeyFrame*>::iterator lit=lKFs.begin(), lend= lKFs.end(); lit!=lend; lit++)
        {
            if(pKF==*lit)
            {
                lKFs.erase(lit);
                break;
            }
        }
    }
}

关键帧数据库-数据库清除

首先清理数据库中的所有关键帧,然后根据词袋大小重置数据库大小;

void KeyFrameDatabase::clear()
{
    mvInvertedFile.clear();// mvInvertedFile[i]表示包含了第i个word id的所有关键帧
    mvInvertedFile.resize(mpVoc->size());// mpVoc:预先训练好的词典
}

关键帧数据库-探测回环候选帧

vector封装数组,list封装了链表,map和set封装了二叉树
C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般平衡二叉树,所以被STL选择作为了关联容器的内部结构。
学习容器:set与multiset。
1 multiset
多重集合(multiset) 允许存在两个次序相等的元素的集合 < set >
2 set
集合(set) 由节点组成的红黑树,每个节点都包含着一个元素,节点之间以某种作用于元素对的谓词排列,没有两个不同的元素能够拥有相同的次序 < set >
注意
1、set中的元素都是排好序的
2、set集合中没有重复的元素

/**
 * @brief 在闭环检测中找到与该关键帧可能闭环的关键帧
 *
 * 1. 找出和当前帧具有公共单词的所有关键帧(不包括与当前帧相连的关键帧)
 * 2. 只和具有共同单词较多的关键帧进行相似度计算
 * 3. 将与关键帧相连(权值最高)的前十个关键帧归为一组,计算累计得分
 * 4. 只返回累计得分较高的组中分数最高的关键帧
 * @param pKF      需要闭环的关键帧
 * @param minScore 相似性分数最低要求
 * @return         可能闭环的关键帧
 * @see III-E Bags of Words Place Recognition
 */
vector<KeyFrame*> KeyFrameDatabase::DetectLoopCandidates(KeyFrame* pKF, float minScore)
{
    // 提出所有与该pKF相连的KeyFrame,这些相连Keyframe都是局部相连,在闭环检测的时候将被剔除
    set<KeyFrame*> spConnectedKeyFrames = pKF->GetConnectedKeyFrames();
    list<KeyFrame*> lKFsSharingWords;// 用于保存可能与pKF形成回环的候选帧(只要有相同的word,且不属于局部相连帧)

    // Search all keyframes that share a word with current keyframes
    // Discard keyframes connected to the query keyframe
    // 步骤1:找出和当前帧具有公共单词的所有关键帧(不包括与当前帧链接的关键帧)
    {
        unique_lock<mutex> lock(mMutex);

        // words是检测图像是否匹配的枢纽,遍历该pKF的每一个word
        for(DBoW2::BowVector::const_iterator vit=pKF->mBowVec.begin(), vend=pKF->mBowVec.end(); vit != vend; vit++)
        {
            // 提取所有包含该word的KeyFrame
            list<KeyFrame*> &lKFs =   mvInvertedFile[vit->first];

            for(list<KeyFrame*>::iterator lit=lKFs.begin(), lend= lKFs.end(); lit!=lend; lit++)
            {
                KeyFrame* pKFi=*lit;
                if(pKFi->mnLoopQuery!=pKF->mnId)// pKFi还没有标记为pKF的候选帧,如果相等说明回环关键帧候选序列中已经包含此关键帧。
                {
                    pKFi->mnLoopWords=0;
                    if(!spConnectedKeyFrames.count(pKFi))// 与pKF局部链接的关键帧不进入闭环候选帧,set集合下count函数表示是否存在关键帧pKFi。
                    {
                        pKFi->mnLoopQuery=pKF->mnId;// pKFi标记为pKF的候选帧,之后直接跳过判断
                        lKFsSharingWords.push_back(pKFi);
                    }
                }
                pKFi->mnLoopWords++;// 记录pKFi与pKF具有相同word的个数,如果pKFi->mnLoopQuery不等于pKF->mnId,mnLoopWords持续加1,表明pKFi关键帧与pKF关键帧的共视单词数量为mnLoopWords。哈希表的用法
            }
        }
    }

    if(lKFsSharingWords.empty())
        return vector<KeyFrame*>();

    list<pair<float,KeyFrame*> > lScoreAndMatch;
// pair<type, type> 形成一个两个元素的组合,此处为得分与关键帧的组合
    // Only compare against those keyframes that share enough words
    // 步骤2:统计所有闭环候选帧中与pKF具有共同单词最多的单词数
    int maxCommonWords=0;
    for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(), lend= lKFsSharingWords.end(); lit!=lend; lit++)
    {
        if((*lit)->mnLoopWords>maxCommonWords)
            maxCommonWords=(*lit)->mnLoopWords;
    }

    int minCommonWords = maxCommonWords*0.8f;

    int nscores=0;

    // Compute similarity score. Retain the matches whose score is higher than minScore
    // 步骤3:遍历所有闭环候选帧,挑选出共有单词数大于minCommonWords且单词匹配度大于minScore存入lScoreAndMatch
    for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(), lend= lKFsSharingWords.end(); lit!=lend; lit++)
    {
        KeyFrame* pKFi = *lit;

        // pKF只和具有共同单词较多的关键帧进行比较,需要大于minCommonWords
        if(pKFi->mnLoopWords>minCommonWords)
        {
            nscores++;// 这个变量后面没有用到

            float si = mpVoc->score(pKF->mBowVec,pKFi->mBowVec);

            pKFi->mLoopScore = si;
            if(si>=minScore)
                lScoreAndMatch.push_back(make_pair(si,pKFi));
                // make_pair 生成pair对象(健值/实值(key/value))
        }
    }

    if(lScoreAndMatch.empty())
        return vector<KeyFrame*>();

    list<pair<float,KeyFrame*> > lAccScoreAndMatch;
    float bestAccScore = minScore;

    // Lets now accumulate score by covisibility
    // 单单计算当前帧和某一关键帧的相似性是不够的,这里将与关键帧相连(权值最高,共视程度最高)的前十个关键帧归为一组,计算累计得分
    // 步骤4:具体而言:lScoreAndMatch中每一个KeyFrame都把与自己共视程度较高的帧归为一组,每一组会计算组得分并记录该组分数最高的KeyFrame,记录于lAccScoreAndMatch
    for(list<pair<float,KeyFrame*> >::iterator it=lScoreAndMatch.begin(), itend=lScoreAndMatch.end(); it!=itend; it++)
    {
        KeyFrame* pKFi = it->second;
        vector<KeyFrame*> vpNeighs = pKFi->GetBestCovisibilityKeyFrames(10);

        float bestScore = it->first; // 该组最高分数
        float accScore = it->first;  // 该组累计得分
        KeyFrame* pBestKF = pKFi;    // 该组最高分数对应的关键帧
        for(vector<KeyFrame*>::iterator vit=vpNeighs.begin(), vend=vpNeighs.end(); vit!=vend; vit++)
        {
            KeyFrame* pKF2 = *vit;
            //pKF2->mnLoopQuery==pKF->mnId表示pKF2作为pKF的候选帧,在步骤1中,已经将候选帧的mnLoopQuery标记为pKF->mnId了。
            if(pKF2->mnLoopQuery==pKF->mnId && pKF2->mnLoopWords>minCommonWords) // 此判断的含义为:仅仅统计pKF的候选帧,并且共视单词大于最小共视单词。
            {
                accScore+=pKF2->mLoopScore;// 因为pKF2->mnLoopQuery==pKF->mnId,所以只有pKF2也在闭环候选帧中,才能贡献分数
                if(pKF2->mLoopScore>bestScore)// 统计得到组里分数最高的KeyFrame
                {
                    pBestKF=pKF2;
                    bestScore = pKF2->mLoopScore;
                }
            }
        }

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

    // Return all those keyframes with a score higher than 0.75*bestScore
    float minScoreToRetain = 0.75f*bestAccScore;

    set<KeyFrame*> spAlreadyAddedKF;
    vector<KeyFrame*> vpLoopCandidates;
    vpLoopCandidates.reserve(lAccScoreAndMatch.size());// vector的reserver函数申请内存空间。

    // 步骤5:得到组得分大于minScoreToRetain的组,得到组中分数最高的关键帧 0.75*bestScore
    for(list<pair<float,KeyFrame*> >::iterator it=lAccScoreAndMatch.begin(), itend=lAccScoreAndMatch.end(); it!=itend; it++)
    {
        if(it->first>minScoreToRetain)
        {
            KeyFrame* pKFi = it->second;
            if(!spAlreadyAddedKF.count(pKFi))// 判断该pKFi是否已经在队列中了
            {
                vpLoopCandidates.push_back(pKFi);
                spAlreadyAddedKF.insert(pKFi);
            }
        }
    }

    return vpLoopCandidates;
}

步骤1:找出和当前帧具有公共单词的所有关键帧(不包括与当前帧链接的关键帧)。具有共视关系的关键帧。
需要理解三个关键变量:mnLoopQuery(回环关键帧序列),mnLoopWords(记录pKFi与pKF具有相同word的个数),mnId(关键帧的id)。
步骤2:统计所有闭环候选帧中与pKF具有共同单词最多的单词数。通过最大公共单词数筛选关键帧。
步骤3:遍历所有闭环候选帧,挑选出共有单词数大于minCommonWords且单词匹配度大于minScore存入lScoreAndMatch。相似度计算,再次作为一个筛选关键帧的标准。
步骤4:具体而言:lScoreAndMatch中每一个KeyFrame都把与自己共视程度较高的帧归为一组,每一组会计算组得分并记录该组分数最高的KeyFrame,记录于lAccScoreAndMatch。滑窗的应用,考虑多个关键帧的组合影响,再次作为一个筛选标准
步骤5:得到组得分大于minScoreToRetain的组,得到组中分数最高的关键帧 0.75*bestScore。通过组合分数,筛选关键帧,返回候选关键帧的向量
回环检测的候选关键帧的筛选:是否具备共视关系,共视程度是否达到要求,相似度程度是否够高,紧密相连的关键帧组合的相似程度累积是否达到标准。逐层筛选关键帧,最后返回关键帧向量。

关键帧数据库-探测重定位候选帧

重定位候选帧与回环候选帧具有很大相似性。
重定位候选帧的选择:是否具有共视关系、共视程序较高、相似度计算选择相似程度高、关键帧组合的相似程度累计选择组合中类似度最高的关键帧。

区别:相对于关键帧的闭环检测DetectLoopCandidates,重定位检测中没法获得相连的关键帧。
除了第一步获取具有公共关系的关键帧过程中有区别,其余步骤是一致的。
还有第三步,计算每个候选帧的相似度是并没有进行筛选。“大于minScore存入lScoreAndMatch”这一步其实并不存在。

/**
 * @brief 在重定位中找到与该帧相似的关键帧
 *
 * 1. 找出和当前帧具有公共单词的所有关键帧
 * 2. 只和具有共同单词较多的关键帧进行相似度计算
 * 3. 将与关键帧相连(权值最高)的前十个关键帧归为一组,计算累计得分
 * 4. 只返回累计得分较高的组中分数最高的关键帧
 * @param F 需要重定位的帧
 * @return  相似的关键帧
 * @see III-E Bags of Words Place Recognition
 */
vector<KeyFrame*> KeyFrameDatabase::DetectRelocalizationCandidates(Frame *F)
{
    // 相对于关键帧的闭环检测DetectLoopCandidates,重定位检测中没法获得相连的关键帧
    list<KeyFrame*> lKFsSharingWords;// 用于保存可能与F形成回环的候选帧(只要有相同的word,且不属于局部相连帧)

    // Search all keyframes that share a word with current frame
    // 步骤1:找出和当前帧具有公共单词的所有关键帧
    {
        unique_lock<mutex> lock(mMutex);

        // words是检测图像是否匹配的枢纽,遍历该pKF的每一个word
        for(DBoW2::BowVector::const_iterator vit=F->mBowVec.begin(), vend=F->mBowVec.end(); vit != vend; vit++)
        {
            // 提取所有包含该word的KeyFrame
            list<KeyFrame*> &lKFs = mvInvertedFile[vit->first];

            for(list<KeyFrame*>::iterator lit=lKFs.begin(), lend= lKFs.end(); lit!=lend; lit++)
            {
                KeyFrame* pKFi=*lit;
                if(pKFi->mnRelocQuery!=F->mnId)// pKFi还没有标记为pKF的候选帧
                {
                    pKFi->mnRelocWords=0;
                    pKFi->mnRelocQuery=F->mnId;
                    lKFsSharingWords.push_back(pKFi);
                }
                pKFi->mnRelocWords++;
            }
        }
    }
    if(lKFsSharingWords.empty())
        return vector<KeyFrame*>();

    // Only compare against those keyframes that share enough words
    // 步骤2:统计所有闭环候选帧中与当前帧F具有共同单词最多的单词数,并以此决定阈值
    int maxCommonWords=0;
    for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(), lend= lKFsSharingWords.end(); lit!=lend; lit++)
    {
        if((*lit)->mnRelocWords>maxCommonWords)
            maxCommonWords=(*lit)->mnRelocWords;
    }

    int minCommonWords = maxCommonWords*0.8f;

    list<pair<float,KeyFrame*> > lScoreAndMatch;

    int nscores=0;

    // Compute similarity score.
    // 步骤3:遍历所有闭环候选帧,挑选出共有单词数大于阈值minCommonWords且单词匹配度==大于minScore(这个步骤不存在)==存入lScoreAndMatch
    for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(), lend= lKFsSharingWords.end(); lit!=lend; lit++)
    {
        KeyFrame* pKFi = *lit;

        // 当前帧F只和具有共同单词较多的关键帧进行比较,需要大于minCommonWords
        if(pKFi->mnRelocWords>minCommonWords)
        {
            nscores++;// 这个变量后面没有用到
            float si = mpVoc->score(F->mBowVec,pKFi->mBowVec);
            pKFi->mRelocScore=si;
            lScoreAndMatch.push_back(make_pair(si,pKFi));
        }
    }

    if(lScoreAndMatch.empty())
        return vector<KeyFrame*>();

    list<pair<float,KeyFrame*> > lAccScoreAndMatch;
    float bestAccScore = 0;

    // Lets now accumulate score by covisibility
    // 步骤4:计算候选帧组得分,得到最高组得分bestAccScore,并以此决定阈值minScoreToRetain
    // 单单计算当前帧和某一关键帧的相似性是不够的,这里将与关键帧相连(权值最高,共视程度最高)的前十个关键帧归为一组,计算累计得分
    // 具体而言:lScoreAndMatch中每一个KeyFrame都把与自己共视程度较高的帧归为一组,每一组会计算组得分并记录该组分数最高的KeyFrame,记录于lAccScoreAndMatch
    for(list<pair<float,KeyFrame*> >::iterator it=lScoreAndMatch.begin(), itend=lScoreAndMatch.end(); it!=itend; it++)
    {
        KeyFrame* pKFi = it->second;
        vector<KeyFrame*> vpNeighs = pKFi->GetBestCovisibilityKeyFrames(10);

        float bestScore = it->first; // 该组最高分数
        float accScore = bestScore;  // 该组累计得分
        KeyFrame* pBestKF = pKFi;    // 该组最高分数对应的关键帧
        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; // 得到所有组中最高的累计得分
    }

    // Return all those keyframes with a score higher than 0.75*bestScore
    // 步骤5:得到组得分大于阈值的,组内得分最高的关键帧
    float minScoreToRetain = 0.75f*bestAccScore;
    set<KeyFrame*> spAlreadyAddedKF;
    vector<KeyFrame*> vpRelocCandidates;
    vpRelocCandidates.reserve(lAccScoreAndMatch.size());
    for(list<pair<float,KeyFrame*> >::iterator it=lAccScoreAndMatch.begin(), itend=lAccScoreAndMatch.end(); it!=itend; it++)
    {
        const float &si = it->first;
        // 只返回累计得分大于minScoreToRetain的组中分数最高的关键帧 0.75*bestScore
        if(si>minScoreToRetain)
        {
            KeyFrame* pKFi = it->second;
            if(!spAlreadyAddedKF.count(pKFi))// 判断该pKFi是否已经在队列中了
            {
                vpRelocCandidates.push_back(pKFi);
                spAlreadyAddedKF.insert(pKFi);
            }
        }
    }

    return vpRelocCandidates;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值