ORB-SLAM2 ---- 词袋模型BOW
文章目录
- 一、回环检测的重要性
- 二、回环检测的方法
- 三、词袋模型
- 四、词典
- 五、实例展示
- 1. 计算评分
- 2. 找出有相同单词的关键帧
- 3. 用词袋进行快速匹配
- 六、总结
一、回环检测的重要性
在前面的学习我们知道,噪声的影响是不可消除的,而上一帧的误差不可避免的会累积到下一帧,使得整个SLAM出现累计误差,长时间的累积会让后续的位姿和地图点位置产生很大的影响,即使我们有很好的后端模型来减少误差,但好模型架不住烂数据。在只有相邻数据时我们能做的事情并不多,但是如果能得到间隔更久远的约束,就可以将误差拉回来。这就是回环检测的意义,就可以将x1-x100之间的位姿进行联系,然后构建回环,将误差平摊到大量的关键帧之中。这个SLAM过程中,回环检测非常重要,以至于有些时候我们把仅有前端和后端的系统称为视觉里程计,而把带有回环检测和全局后端的系统称为SLAM 。
二、回环检测的方法
我们知道回环检测最重要的是检测到回环,我们怎么来检测这个回环?从原理上来说,就是要在之前的关键帧中找出与当前关键帧很相似的一幅图像。在前面提取特征点的时候用过灰度法,但是当时就说过灰度是不稳定的,灰度受到光线的影响严重,我们不能保证发生回环的几个关键帧的光线条件一致。用如下的公式来表示两个图像的差异,如果A图像的光线发生偏差,会导致结果很大,可能会将两个相似度很高的图像误认为是完全不匹配的图像。
这个时候就产生了一个问题,怎么样才能正确的判断两个图像是否相似?词袋模型就孕育而出了,词袋模型能很好的解决这个问题。
三、词袋模型
词袋,也就是Bag-of-Words(BoW),目的是用“图像上有哪几种特征”来描述一幅图像。例如,我们说某张照片中有一个人、一辆车;而另一张中有两个人、一只狗。根据这样的描述,就可以度量这两幅图像的相似性。再具体一些,我们要做以下三步:
- 确定“人”“车”“狗”等概念–对应于BoW中的“单词”(Word),许多单词放在一
起,组成了“字典”(Dictionary )。- 确定一幅图像中出现了哪些在字典中定义的概念-我们用单词出现的情况(或直方
图)描述整幅图像。这就把一幅图像转换成了一个向量的描述。- 比较上一步中的描述的相似程度。
以上面举的例子来说,首先我们通过某种方式得到了一本“字典”。字典上记录了许多单词,每个单词都有一定意义,例如“人”“车”“狗”都是记录在字典中的单词,我们不妨记为 W1,w2,w3。然后,对于任意图像A,根据它们含有的单词,可记为:
我们有两种方法描述一幅图片,第一种是用单词数量,第二种只用单词出没出现在该函数中,如[1,0,1],单词出现记为1,没出现记为0。无论是哪种方式都可以很好的描述一个图像的特征。我们可以用如下公式来表示两个图像的匹配度评分:
可以看出如果两个向量完全一致的时候,函数值为1(两幅图像相似度高),两个向量完全相反的时候函数值为0(两幅图像相似度低)。
四、词典
我们在局部建图和回环检测线程中,都会用到词袋,那词袋到底是什么?答案是一幅图像中包含的单词(Word)的集合。而词典是包含所有单词信息的一个库,再出现词袋的地方,都会提到快速匹配,但是一个一个单词进行比较匹配真的算得上快速匹配吗?如果单词是按照顺序一个一个排列得话当然不是,甚至在单词数量很大的时候,会出现匹配时间很长的情况。这里用到的是数据结构中的方法,能进行快速的索引。字典的生成类似于一个聚类的问题,我们将根节点分为k个树杈,每个树杈按照特点继续细分,这样每次进行匹配的时候,就只需要知道这个单词在那个叶节点(分到最后的一层,称之为叶)上,然后依次向上寻找匹配,这样效率将大大的提升,真正意义上的实现了快速匹配。
五、实例展示
1. 计算评分
下面这个代码片段是回环检测线程中检测回环函数中的片段,可以看出这里就是用了计算词袋相似度评分的方法,来判断候选帧和当前帧之间的相似情况。
// Step 3:遍历当前回环关键帧所有连接(>15个共视地图点)关键帧,计算当前关键帧与每个共视关键的bow相似度得分,并得到最低得分minScore
const vector<KeyFrame*> vpConnectedKeyFrames = mpCurrentKF->GetVectorCovisibleKeyFrames();
const DBoW2::BowVector &CurrentBowVec = mpCurrentKF->mBowVec;
float minScore = 1;
for(size_t i=0; i<vpConnectedKeyFrames.size(); i++)
{
KeyFrame* pKF = vpConnectedKeyFrames[i];
if(pKF->isBad())
continue;
const DBoW2::BowVector &BowVec = pKF->mBowVec;
// 计算两个关键帧的相似度得分;得分越低,相似度越低
float score = mpORBVocabulary->score(CurrentBowVec, BowVec);
// 更新最低得分
if(score<minScore)
minScore = score;
}
2. 找出有相同单词的关键帧
下面这个代码片段是DetectLoopCandidates()函数中的片段,可以看出它充分的使用了单词(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)
{
// 还没有标记为pKF的闭环候选帧
pKFi->mnLoopWords=0;
// 和当前关键帧共视的话不作为闭环候选帧
if(!spConnectedKeyFrames.count(pKFi))
{
// 没有共视就标记作为闭环候选关键帧,放到lKFsSharingWords里
pKFi->mnLoopQuery=pKF->mnId;
lKFsSharingWords.push_back(pKFi);
}
}
pKFi->mnLoopWords++;// 记录pKFi与pKF具有相同word的个数
}
3. 用词袋进行快速匹配
下面这个函数片段是跟踪线程中参考关键帧跟踪中的词袋快速匹配函数的片段,这里就利用词袋进行快速匹配,只有处于同一个note中的特征点才会进行匹配,这样就能大大的节省匹配时间。
const vector<cv::KeyPoint> &vKeysUn1 = pKF1->mvKeysUn;
const DBoW2::FeatureVector &vFeatVec1 = pKF1->mFeatVec;
const vector<MapPoint*> vpMapPoints1 = pKF1->GetMapPointMatches();
const cv::Mat &Descriptors1 = pKF1->mDescriptors;
const vector<cv::KeyPoint> &vKeysUn2 = pKF2->mvKeysUn;
const DBoW2::FeatureVector &vFeatVec2 = pKF2->mFeatVec;
const vector<MapPoint*> vpMapPoints2 = pKF2->GetMapPointMatches();
const cv::Mat &Descriptors2 = pKF2->mDescriptors;
// 保存匹配结果
vpMatches12 = vector<MapPoint*>(vpMapPoints1.size(),static_cast<MapPoint*>(NULL));
vector<bool> vbMatched2(vpMapPoints2.size(),false);
// Step 2 构建旋转直方图,HISTO_LENGTH = 30
vector<int> rotHist[HISTO_LENGTH];
for(int i=0;i<HISTO_LENGTH;i++)
rotHist[i].reserve(500);
//! 原作者代码是 const float factor = 1.0f/HISTO_LENGTH; 是错误的,更改为下面代码
const float factor = HISTO_LENGTH/360.0f;
int nmatches = 0;
DBoW2::FeatureVector::const_iterator f1it = vFeatVec1.begin();
DBoW2::FeatureVector::const_iterator f2it = vFeatVec2.begin();
DBoW2::FeatureVector::const_iterator f1end = vFeatVec1.end();
DBoW2::FeatureVector::const_iterator f2end = vFeatVec2.end();
while(f1it != f1end && f2it != f2end)
{
// Step 3 开始遍历,分别取出属于同一node的特征点(只有属于同一node,才有可能是匹配点)
if(f1it->first == f2it->first)
{
// 遍历KF中属于该node的特征点
for(size_t i1=0, iend1=f1it->second.size(); i1<iend1; i1++)
{
const size_t idx1 = f1it->second[i1];
MapPoint* pMP1 = vpMapPoints1[idx1];
if(!pMP1)
continue;
if(pMP1->isBad())
continue;
const cv::Mat &d1 = Descriptors1.row(idx1);
int bestDist1=256;
int bestIdx2 =-1 ;
int bestDist2=256;
// Step 4 遍历KF2中属于该node的特征点,找到了最优及次优匹配点
for(size_t i2=0, iend2=f2it->second.size(); i2<iend2; i2++)
{
const size_t idx2 = f2it->second[i2];
MapPoint* pMP2 = vpMapPoints2[idx2];
// 如果已经有匹配的点,或者遍历到的特征点对应的地图点无效
if(vbMatched2[idx2] || !pMP2)
continue;
if(pMP2->isBad())
continue;
const cv::Mat &d2 = Descriptors2.row(idx2);
int dist = DescriptorDistance(d1,d2);
if(dist<bestDist1)
{
bestDist2=bestDist1;
bestDist1=dist;
bestIdx2=idx2;
}
else if(dist<bestDist2)
{
bestDist2=dist;
}
}
// Step 5 对匹配结果进行检查,满足阈值、最优/次优比例,记录旋转直方图信息
if(bestDist1<TH_LOW)
{
if(static_cast<float>(bestDist1)<mfNNratio*static_cast<float>(bestDist2))
{
vpMatches12[idx1]=vpMapPoints2[bestIdx2];
vbMatched2[bestIdx2]=true;
if(mbCheckOrientation)
{
float rot = vKeysUn1[idx1].angle-vKeysUn2[bestIdx2].angle;
if(rot<0.0)
rot+=360.0f;
int bin = round(rot*factor);
if(bin==HISTO_LENGTH)
bin=0;
assert(bin>=0 && bin<HISTO_LENGTH);
rotHist[bin].push_back(idx1);
}
nmatches++;
}
}
}
六、总结
词袋模型是ORB-SLAM2中很重要的一个模型,他的作用也有很多,在上述一一列举过,最重要的作用是,在回环检测中匹配相似的图像,曾经该方法是最优的方法之一,现在各种机器学习的发展,例如深度学习,能更高效的完成相似图像的识别,这也是最近深度学习大量运用在SLAM中的原因。