关键帧数据库
注意区分词典和关键帧数据库的区别。
词典:事先训练好的,用来计算关键帧的词袋信息。
关键帧数据库:根据关键帧的词袋生成,在回环检测、重定位的时候使用,寻找与当前帧最相似的候选关键帧。
在局部建图(LocalMapping)线程的ProcessNewKeyFrame函数中,计算了每个关键帧的词袋信息:
mpCurrentKeyFrame->ComputeBoW();
关键帧数据库中存放着所有关键帧的单词信息。形式为:
单词1:关键帧(1)(2)(3)
单词2:关键帧(3)(4)(5)
单词3:关键帧(5)(6)(7)
…
其作用为:在回环检测、重定位的时候,搜索与当前帧最相似的候选关键帧集合。
std::vector<list<KeyFrame*> > mvInvertedFile;
关键帧数据库。mvInvertedFile[i]表示含有第i个单词的关键帧的id组成的链表。
DBoW2::BowVector mBowVec;
mBowVec 内部实际存储的是std::map<WordId, WordValue>
WordId 和 WordValue 表示Word在叶子中的id 和权重
构造函数
KeyFrameDatabase::KeyFrameDatabase (const ORBVocabulary &voc):
mpVoc(&voc)
{
// 数据库的主要内容了
mvInvertedFile.resize(voc.size()); // number of words
}
其中mvInvertedFile的数据类型为:
std::vector<list<KeyFrame*> > mvInvertedFile;
mvInvertedFile[i]表示含有第i个单词的关键帧组成的链表。
关键帧插入关键帧数据库中
// 根据关键帧的BoW,更新数据库的倒排索引
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);
}
关键帧数据库中删除关键帧
通过mBowVec找到该关键帧包含哪些单词,然后在mvInvertedFile中将相应的关键帧删掉。
// 关键帧被删除后,更新数据库的倒排索引
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<KeyFrame*> KeyFrameDatabase::DetectLoopCandidates(KeyFrame* pKF, float minScore)
寻找可能与当前帧构成回环的关键帧。
(1)找出与当前帧具有相同单词的关键帧。
(2)设置最多的相同单词的880%为阈值,根据词典计算当前帧与候选帧的相似度。
(3)计算上述候选帧对应的共视关键帧组的总得分,只取最高组得分75%以上的组。
(4)得到上述组中分数最高的关键帧作为闭环候选关键帧。
找出与当前帧的共视点大于15的关键帧
set<KeyFrame*> spConnectedKeyFrames = pKF->GetConnectedKeyFrames();
寻找与当前帧有相同单词的关键帧
&emps;寻找与当前帧有相同单词的关键帧,并将其放入到链表list<KeyFrame*> lKFsSharingWords;中。其中变量mnLoopWords记录了当前帧与候选帧具有的相同的单词个数。
// 用于保存可能与当前关键帧形成闭环的候选帧(只要有相同的word,且不属于局部相连(共视)帧)
list<KeyFrame*> lKFsSharingWords;
// Search all keyframes that share a word with current keyframes
// Discard keyframes connected to the query keyframe
// Step 1:找出和当前帧具有公共单词的所有关键帧,不包括与当前帧连接(也就是共视)的关键帧
{
unique_lock<mutex> lock(mMutex);
// words是检测图像是否匹配的枢纽,遍历该pKF的每一个word
// mBowVec 内部实际存储的是std::map<WordId, WordValue>
// WordId 和 WordValue 表示Word在叶子中的id 和权重
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)
{
// 还没有标记为pKF的闭环候选帧
pKFi->mnLoopWords=0;
// 和当前关键帧共视的话不作为闭环候选帧
if(!spConnectedKeyFrames.count(pKFi))
{
// 没有共视就标记作为闭环候选关键帧,放到lKFsSharingWords里
pKFi->mnLoopQuery=pKF->mnId;
lKFsSharingWords.push_back(pKFi);
}
}
pKFi->mnLoopWords++;// 记录pKFi与pKF具有相同word的个数
}
}
}
设置最少单词阈值
// Only compare against those keyframes that share enough words
// Step 2:统计上述所有闭环候选帧中与当前帧具有共同单词最多的单词数,用来决定相对阈值
int maxCommonWords=0;
for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(), lend= lKFsSharingWords.end(); lit!=lend; lit++)
{
if((*lit)->mnLoopWords>maxCommonWords)
maxCommonWords=(*lit)->mnLoopWords;
}
// 确定最小公共单词数为最大公共单词数目的0.8倍
int minCommonWords = maxCommonWords*0.8f;
挑选候选关键帧
list<pair<float,KeyFrame*> > lScoreAndMatch;存放所有满足条件的候选关键帧。其中float变量是计算出来的两帧的词典相似度,KeyFrame*为候选关键帧。
// Compute similarity score. Retain the matches whose score is higher than minScore
// Step 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++;// 这个变量后面没有用到
// 用mBowVec来计算两者的相似度得分
float si = mpVoc->score(pKF->mBowVec,pKFi->mBowVec);
pKFi->mLoopScore = si;
if(si>=minScore)
lScoreAndMatch.push_back(make_pair(si,pKFi));
}
}
计算候选关键帧的共视关键帧与当前帧的得分
单单计算当前帧和某一候选关键帧的相似性是不够的,这里将与候选关键帧相连(权值最高,共视程度最高)的前十个关键帧归为一组,计算累计得分。在这十个关键帧中选择一个最好的插入到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也在闭环候选帧中,且公共单词数超过最小要求,才能贡献分数
if(pKF2->mnLoopQuery==pKF->mnId && pKF2->mnLoopWords>minCommonWords)
{
accScore+=pKF2->mLoopScore;
// 统计得到组里分数最高的关键帧
if(pKF2->mLoopScore>bestScore)
{
pBestKF=pKF2;
bestScore = pKF2->mLoopScore;
}
}
}
lAccScoreAndMatch.push_back(make_pair(accScore,pBestKF));
// 记录所有组中组得分最高的组,用于确定相对阈值
if(accScore>bestAccScore)
bestAccScore=accScore;
}
获得闭环候选关键帧
将lAccScoreAndMatch中的得分大于阈值的关键帧插入vpLoopCandidates中并返回。
for(list<pair<float,KeyFrame*> >::iterator it=lAccScoreAndMatch.begin(), itend=lAccScoreAndMatch.end(); it!=itend; it++)
{
if(it->first>minScoreToRetain)
{
KeyFrame* pKFi = it->second;
// spAlreadyAddedKF 是为了防止重复添加
if(!spAlreadyAddedKF.count(pKFi))
{
vpLoopCandidates.push_back(pKFi);
spAlreadyAddedKF.insert(pKFi);
}
}
}
获得重定位时的候选关键帧
过程与回环检测时的类似,但没有设置最小得分。
(1)找到与当前帧具有相同的单词的所有关键帧。
(2)根据阈值进行筛选,使用词典计算当前帧和重定位候选关键帧的相似度得分。
(3)将与重定位候选关键帧相连的十个关键帧看为一组,计算总得分,并选出其中得分最高的关键帧
(4)返回累计得分最高的几个组中的最好的关键帧
vector<KeyFrame*> KeyFrameDatabase::DetectRelocalizationCandidates(Frame *F)