视觉SLAM第11讲:回环检测

发布于:2025-09-16 ⋅ 阅读:(18) ⋅ 点赞:(0)

目录

11.1 概述

11.1.1 回环检测的意义

11.1.2 回环检测的方法

11.1.3 准确率和召回率

11.2 词袋模型

11.3 字典

11.3.1 字典结构 

11.3.2 实践:创建字典

1.feature_training.cpp

3.运行结果

11.4 相似度计算

11.4.1 理论部分

11.4.2 实践:相似度的计算

1.loop_closure.cpp

2.CMakeLists.txt

3.运行结果

11.5 实验分析与评述

11.5.1 增加字典规模

1.gen_vocab_large.cpp

2.CMakeLists.txt

3.运行结果

11.5.2 相似性评分的处理

11.5.3 关键帧的处理

11.5.4 检测后的验证

11.5.5 与机器学习的关系 

目标:

1.理解回环检测的必要性;
2.掌握基于词袋的外观式回环检测;

3.通过DBoW3的实验,学习词袋模型的实际用途。

        本讲中介绍SLAM 中另一个主要模块:回环检测。SLAM主体(前端、后端)主要的目的在于估计相机运动,而回环检测模块,无论是目标上还是方法上,都与前面讲的内容相差较大,所以通常被认为是一个独立的模块。将介绍主流视觉SLAM中检测回环的方式:词袋模型,并通过DBoW库上的程序实验直观理解。

11.1 概述

11.1.1 回环检测的意义

       前端提供特征点的提取和轨迹、地图的初值;而后端负责对所有这些数据进行优化。

       然而,如果像视觉里程计那样仅考虑相邻时间上的关键帧,那么,之前产生的误差将不可避免地累积到下一个时刻,使得整个SLAM出现累积误差,长期估计的结果将不可靠,或者说,我们无法构建全局一致的轨迹和地图。
       举个简单的例子:在自动驾驶的建图阶段,我们通常会指定采集车在某个给定区域绕若干圈以覆盖所有采集范围。

       假设我们在前端提取了特征,然后忽略特征点,在后端使用位姿图优化整个轨迹,如图。前端给出的只是局部的位姿间约束,例如,可能是x_{1}-x_{2},x_{2}-x_{3}等等。但是,由于x_{1}的估计存在误差,而x_{2}是根据x_{1}决定的,x_{3}又是由x_{2}决定的。依此类推,误差就会被累积起来,使得后端优化的结果如图所示,慢慢地趋向不准确。在这种应用场景下,我们应该保证,优化的轨迹和实际地点一致。当我们实际经过同一个地点时,估计轨迹也必定经过同一点。


       虽然后端能够估计最大后验误差,但所谓“好模型架不住烂数据”,只有相邻关键帧数据时,我们能做的事情并不多,也无从消除累积误差。

       但是,回环检测模块能够给出除了相邻帧的一些时隔更加久远的约束:例如之间的位姿变换。

       为什么它们之间会有约束呢?这是因为我们察觉到相机经过了同一个地方,采集到了相似的数据。而回环检测的关键,就是如何有效地检测出相机经过同一个地方这件事。如果我们能够成功地检测到这件事,就可以为后端的位姿图提供更多的有效数据,使之得到更好的估计,特别是得到一个全局一致的估计。由于位姿图可以看成一个质点——弹簧系统,所以回环检测相当于在图像中加入了额外的弹簧,提高了系统稳定性。也可直观地想象成回环边把带有累积误差的边“拉”到了正确的位置——如果回环本身正确的话。

       回环检测对于SLAM系统意义重大。一方面,它关系到我们估计的轨迹和地图在长时间下的正确性。另一方面,由于回环检测提供了当前数据与所有历史数据的关联,我们还可以利用回环检测进行重定位。重定位的用处就更多一些。

       例如,如果我们事先对某个场景录制了一条轨迹并建立了地图,那么之后在该场景中就可以一直跟随这条轨迹进行导航,而重定位可以帮助我们确定自身在这条轨迹上的位置。

       因此,回环检测对整个SLAM系统精度与稳健性的提升是非常明显的。甚至在某些时候,我们把仅有前端和局部后端的系统称为视觉里程计,而把带有回环检测和全局后端的系统称为SLAM。

11.1.2 回环检测的方法

      尽管随机检测在有些实现中确实有用,但我们至少希望有一个“哪处可能出现回环”的预计,才好不那么盲目地去检测。这样的方式大体有两种思路:基于里程计(Odometry based)的几何关系,或基于外观(Appearance based)的几何关系。

      基于里程计的几何关系是说,当我们发现当前相机运动到了之前的某个位置附近时,检测它们有没有回环关系——这自然是一种直观的想法,但是由于累积误差的存在,我们往往没法正确地发现“运动到了之前的某个位置附近”这件事实,回环检测也无从谈起。因此,这种做法在逻辑上存在一点问题,因为回环检测的目标在于发现“相机回到之前位置”的事实,从而消除累积误差。而基于里程计的几何关系的做法假设了“相机回到之前位置附近”,这样才能检测回环。这是有倒果为因的嫌疑的。因而也无法在累积误差较大时工作。

       另一种方法是基于外观的。它和前端、后端的估计都无关,仅根据两幅图像的相似性确定回环检测关系。这种做法摆脱了累积误差,使回环检测模块成为SLAM系统中一个相对独立的模块(当然前端可以为它提供特征点)。自提出以来,基于外观的回环检测方式能够有效地在不同场景下工作,成了视觉SLAM中主流的做法,并被应用于实际的系统中。

       除此之外,从工程角度我们也能提出一些解决回环检测的办法。例如,室外的无人车通常会配备GPS,可以提供全局的位置信息。利用GPS信息可以很轻松地判断汽车是否回到某个经过的点,但这类方法在室内就不怎么好用。

       在基于外观的回环检测算法中,核心问题是如何计算图像间的相似性。例如,对于图像A和图像B,我们要设计一种方法,计算它们之间的相似性评分:s(A,B)。当然,这个评分会在某个区间内取值,当它大于一定量后我们认为出现了一个回环。

       计算两幅图像之间的相似性很困难吗?例如直观上看,图像能够表示成矩阵,那么直接让两幅图像相减,然后取某种范数行不行呢?

为什么我们不这样做?
(1)前面也说过,像素灰度是一种不稳定的测量值,它严重地受环境光照和相机曝光的影响。假设相机未动,我们打开了一支电灯,那么图像会整体变亮。这样,即使对于同样的数据,我们也会得到一个很大的差异值。
(2)当相机视角发生少量变化时,即使每个物体的光度不变,它们的像素也会在图像中发生
位移,造成一个很大的差异值。

        由于这两种情况的存在,实际中,即使对于非常相似的图像,A-B也会经常得到一个(不符合实际的)很大的值。所以我们说,这个函数不能很好地反映图像间的相似关系。这里牵涉到一个“好”和“不好”的定义问题。我们要问,怎样的函数能够更好地反映相似关系,而怎样的函数不够好呢?从这里可以引出感知偏差(Perceptual Aliasing)和感知变异(Perceptual Variability)两个概念。

11.1.3 准确率和召回率

       从人类的角度看,我们能够以很高的精确度,感觉到“两幅图像是否相似”或“这两张照片是从同一个地方拍摄的”这一事实,但由于目前尚未掌握人脑的工作原理,我们无法清楚地描述自己是如何完成这个判断的。

       从程序角度看,我们希望程序算法能够得出和人类,或者和事实一致的判断。当我们觉得,或者事实上就是,两幅图像从同一个地方拍摄,那么回环检测算法也应该给出“这是回环”的结果。反之,如果我们觉得,或事实上,两幅图像是从不同地方拍摄的,那么程序也应该给出“这不是回环”的判断。当然,程序的判断并不总是与我们人类的想法一致,所以可能出现表中的4种情况。

                             
        假阳性(False Positive)又称为感知偏差,而假阴性(False Negative)称为感知变异。用缩写TP代表TruePositive(真阳性),用TN代表True Negative(真阴性)。由于我们希望算法和人类的判断一致,所以希望TP和TN尽量高,而FP和FN尽可能低。所以,对于某种特定算法,我们可以统计它在某个数据集上的TP、TN、FP、FN的出现次数,并计算两个统计量:准确率和召回率(Precision & Recall)。

              


       准确率描述的是算法提取的所有回环中确实是真实回环的概率。而召回率则是指,在所有真实回环中被正确检测出来的概率。

11.2 词袋模型

       既然直接用两张图像相减的方式不够好,我们就需要一种更可靠的方式。结合前面几讲的内容,一种思路是:为何不像视觉里程计那样使用特征点来做回环检测呢?

       和视觉里程计一样,我们对两幅图像的特征点进行匹配,只要匹配数量大于一定值,就认为出现了回环。

       根据特征点匹配,我们还能计算出这两幅图像之间的运动关系。当然这种做法存在一些问题,例如,特征的匹配会比较费时、当光照变化时特征描述可能不稳定等,但离我们要介绍的词袋模型已经很相近了。下面我们先来介绍词袋的做法,再来讨论数据结构之类的实现细节。

       词袋,也就是Bag-of-Words(BoW),目的是用“图像上有哪几种特征”来描述一幅图像。例如,我们说某张照片中有一个人、一辆车;而另一张中有两个人、一只狗。根据这样的描述,就可以度量这两幅图像的相似性。再具体一些,我们要做以下三步:

(1)确定“人”“车”“狗”等概念——对应于BoW中的“单词”(Word),许多单词放在一起,组成了“字典”(Dictionary)。
(2)确定一幅图像中出现了哪些在字典中定义的概念——我们用单词出现的情况(或直方图)描述整幅图像。这就把一幅图像转换成了一个向量的描述。
(3)比较上一步中的描述的相似程度。
       以上面举的例子来说,首先我们通过某种方式得到了一本“字典”。字典上记录了许多单词,每个单词都有一定意义,例如“人”“车”“狗”都是记录在字典中的单词,我们不妨记为。然后,对于任意图像A,根据它们含有的单词,

记为

                                
       字典是固定的,所以只要用[1,1,0]^{T}这个向量就可以表达的意义。通过字典和单词,只需一个向量就可以描述整幅图像。该向量描述的是“图像是否含有某类特征”的信息,比单纯的灰度值更稳定。又因为描述向量说的是“是否出现”,而不管它们“在哪儿出现”,所以与物体的空间位置和排列顺序无关,因此在相机发生少量运动时,只要物体仍在视野中出现,我们就仍然保证描述向量不发生变化。

       基于这种特性,我们称它为Bag-of-Words,强调的是Words的有无,而无关其顺序。因此,可以说字典类似于单词的一个集合。

       回到上面的例子,同理,用[2,0,1]^{T}可以描述图像。如果只考虑“是否出现”而不考虑数量,也可以是[1,0,1]^{T},这时候这个向量就是二值的。于是,根据这两个向量,设计一定的计算方式,就能确定图像间的相似性。当然,对两个向量求差仍然有一些不同的做法,例如对于a,b\epsilon R^{W}

可以计算

                                     
        其中范数取L_{1}范数,即各元素绝对值之和。请注意在两个向量完全一样时,我们将得到1;完全相反时(a为0的地方b为1)得到0。这样就定义了两个描述向量的相似性,也就定义了图像之间的相似程度。

接下来的问题是什么呢?
(1)我们虽然清楚了字典的定义方式,但它到底是怎么来的呢?
(2)如果我们能够计算两幅图像间的相似程度评分,是否就足够判断回环了呢?
       所以接下来,我们首先介绍字典的生成方式,然后介绍如何利用字典实际地计算两幅图像间的相似性。

11.3 字典

11.3.1 字典结构 

       按照前面的介绍,字典由很多单词组成,而每一个单词代表了一个概念。一个单词与一个单独的特征点不同,它不是从单幅图像上提取出来的,而是某一类特征的组合。所以,字典生成问题类似于一个聚类(Clustering)问题。
       聚类问题在无监督机器学习(Unsupervised ML)中特别常见,用于让机器自行寻找数据中的规律。BoW的字典生成问题也属于其中之一。首先,假设我们对大量的图像提取了特征点,例如有N个。现在,我们想找一个有k个单词的字典,每个单词可以看作局部相邻特征点的集合,应该怎么做呢?这可以用经典的K-means(K均值)算法解决。
       K-means是一个非常简单有效的方法,因此在无监督学习中广为使用,下面对其原理稍做介绍。简单的说,当有N个数据,想要归成k个类,那么用K-means来做主要包括如下步骤:

(1)随机选取k个中心点:
(2)对每一个样本,计算它与每个中心点之间的距离,取最小的作为它的归类。

(3)重新计算每个类的中心点。
(4)如果每个中心点都变化很小,则算法收敛,退出;否则返回第2步。

       K-means 的做法是朴素且简单有效的,不过也存在一些问题,例如,需要指定聚类数量、随机选取中心点使得每次聚类结果都不相同,以及一些效率上的问题。随后,研究者们也开发出了层次聚类法、K-means++等算法以弥补它的不足,不过这都是后话,我们就不详细讨论了。总之,根据K-means,我们可以把已经提取的大量特征点聚类成一个含有k个单词的字典。现在的问题变成了如何根据图像中某个特征点查找字典中相应的单词。
       仍然有朴素的思想:只要和每个单词进行比对,取最相似的那个就可以了——这当然是简单有效的做法。然而,考虑到字典的通用性,我们通常会使用一个较大规模的字典,以保证当前使用环境中的图像特征都曾在字典里出现,或至少有相近的表达。如果你觉得对十个单词一比较不是什么麻烦事,那么对于一万个呢?十万个呢?

       也许读者学过数据结构,这种O(n)的查找算法显然不是我们想要的。如果字典排过序,
那么二分查找显然可以提升查找效率,达到对数级别的复杂度。而实践中,我们可能会用更复杂的数据结构,例如 Fabmap中的Chou-Liu tree等。

        使用一种k叉树来表达字典。它的思路很简单,类似于层次聚类,是K-means的直接扩展。假定我们有N个特征点,希望构建一个深度为d、每次分叉为k的树,那么做法如下:
(1)在根节点,用 K-means把所有样本聚成k类(实际中为保证聚类均匀性会使用K-means++)。这样就得到了第一层。
(2)对第一层的每个节点,把属于该节点的样本再聚成k类,得到下一层。

(3)依此类推,最后得到叶子层。叶子层即为所谓的Words。

       实际上,最终我们仍在叶子层构建了单词,而树结构中的中间节点仅供快速查找时使用这样一个k分支、深度为d的树,可以容纳k^{d}个单词。另外,在查找某个给定特征对应的单词时,只需将它与每个中间节点的聚类中心比较(一共d次),即可找到最后的单词,保证了对数级别的查找效率。  

11.3.2 实践:创建字典
1.feature_training.cpp
#include "DBoW3/DBoW3.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <iostream>
#include <vector>
#include <string>

using namespace cv;
using namespace std;

/***************************************************
 * 本节演示了如何根据data/目录下的十张图训练字典
 * ************************************************/

int main( int argc, char** argv ) {
    // read the image 
    cout<<"reading images... "<<endl;
    vector<Mat> images;
    for ( int i=0; i<10; i++ )
    {
        string path = "../data/"+to_string(i+1)+".png";
        images.push_back( imread(path) );
    }
    // detect ORB features
    cout<<"detecting ORB features ... "<<endl;
    Ptr< Feature2D > detector = ORB::create();
    vector<Mat> descriptors;
    for ( Mat& image:images )
    {
        vector<KeyPoint> keypoints;
        Mat descriptor;
        detector->detectAndCompute( image, Mat(), keypoints, descriptor );
        descriptors.push_back( descriptor );
    }

    // create vocabulary 
    cout<<"creating vocabulary ... "<<endl;
    DBoW3::Vocabulary vocab;
    vocab.create( descriptors );
    cout<<"vocabulary info: "<<vocab<<endl;
    vocab.save( "vocabulary.yml.gz" );
    cout<<"done"<<endl;

    return 0;
}

2.CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(slam_10)

set(CMAKE_BUILD_TYPE "Release")
set( CMAKE_CXX_FLAGS "-std=c++11 -O3" )
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -march=native -g")

# opencv 
find_package( OpenCV 3.2 REQUIRED )
include_directories( ${OpenCV_INCLUDE_DIRS} )

# dbow3 
# dbow3 is a simple lib so I assume you installed it in default directory 
include_directories( "/usr/local/include" )
link_directories( "/usr/local/lib" )


# --- Executables ---
add_executable( feature_training feature_training.cpp )
target_link_libraries( feature_training ${OpenCV_LIBS} DBoW3)

#add_executable( loop_closure loop_closure.cpp )
#target_link_libraries( loop_closure ${OpenCV_LIBS} DBoW3 )

#add_executable( gen_vocab gen_vocab_large.cpp )
#target_link_libraries( gen_vocab ${OpenCV_LIBS} DBoW3 )
3.运行结果

11.4 相似度计算

11.4.1 理论部分

       下面我们来讨论相似度计算的问题。有了字典之后,给定任意特征f_{i},只要在字典树中逐层查找,最后都能找到与之对应的单词w_{j}——当字典足够大时,我们可以认为f_{i}w_{j}来自同一类物体(尽管没有理论上的保证,仅是在聚类意义下这样说)。那么,假设从一幅图像中提取了N个特征,找到这N个特征对应的单词之后,就相当于拥有了该图像在单词列表中的分布,或者直方图。理想情况下,相当于“这幅图里有一个人和一辆汽车”这样的意思。根据BoW的说法,不妨认为这是一个 Bag。

       注意,这种做法中我们对所有单词都是“一视同仁”的——有就是有,没有就是没有。这样做好不好呢?我们应考虑部分单词具有更强区分性这一因素。例如,“的”“是”这样的字可能在许许多多的句子中出现,我们无法根据它们判别句子的类型;但如果有“文档”“足球”这样的单词,对判别句子的作用就大一些,可以说它们提供了更多信息。所以概括起来,我们希望对单词的区分性或重要性加以评估,给它们不同的权值以起到更好的效果。

       TF-IDF(Term Frequency-Inverse Document Frequency),或译频率-逆文档频率
是文本检索中常用的一种加权方式,也用于BoW模型中。TF部分的思想是,某单词在一幅图像中经常出现,它的区分度就高。另外,IDF的思想是,某单词在字典中出现的频率越低,分类图像时区分度越高。

       我们可以在建立字典时计算IDF:统计某个叶子节点中的特征数量相对于所有特征数量的比例,作为IDF部分。假设所有特征数量为n,w_{i}数量为n_{i},那么该单词的IDF为

                                                 

       TF部分则是指某个特征在单幅图像中出现的频率。假设图像A中w_{i}单词出现了n_{i}次,
而一共出现的单词次数为,那么TF为

                                                    

      于是,w_{i}的权重等于TF乘 IDF之积

                                               

      考虑权重以后,对于某幅图像A,它的特征点可对应到许多个单词,组成它的BoW

                     

      由于相似的特征可能落到同一个类中,因此实际的v_{A}中会存在大量的零。无论如何,通过词袋我们用单个向量描述了一幅图像A。这个向量v_{A}是一个稀疏的向量,它的非零部分指示了图像A中含有哪些单词,而这些部分的值为TF-IDF的值。

      接下来的问题是:给定v_{A}v_{B},如何计算它们的差异呢?这个问题和范数定义的方式一样,存在若干种解决方式,例如范数形式

                   

       当然也有很多种别的方式等你探索,在这里我们仅举一例作为演示。至此,我们已说明了如何通过词袋模型计算任意图像间的相似度。下面通过程序实际演练。

11.4.2 实践:相似度的计算
1.loop_closure.cpp
#include "DBoW3/DBoW3.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <iostream>
#include <vector>
#include <string>

using namespace cv;
using namespace std;

/***************************************************
 * 本节演示了如何根据前面训练的字典计算相似性评分
 * ************************************************/
int main(int argc, char **argv) {
    // read the images and database  
    cout << "reading database" << endl;
    //DBoW3::Vocabulary vocab("vocabulary.yml.gz");
    DBoW3::Vocabulary vocab("vocab_larger.yml.gz");  // use large vocab if you want: 
    if (vocab.empty()) {
        cerr << "Vocabulary does not exist." << endl;
        return 1;
    }
    cout << "reading images... " << endl;
    vector<Mat> images;
    for (int i = 0; i < 10; i++) {
        string path = "../data/" + to_string(i + 1) + ".png";
        images.push_back(imread(path));
    }

    // NOTE: in this case we are comparing images with a vocabulary generated by themselves, this may lead to overfit.
    // detect ORB features
    cout << "detecting ORB features ... " << endl;
    Ptr<Feature2D> detector = ORB::create();
    vector<Mat> descriptors;
    for (Mat &image:images) {
        vector<KeyPoint> keypoints;
        Mat descriptor;
        detector->detectAndCompute(image, Mat(), keypoints, descriptor);
        descriptors.push_back(descriptor);
    }

    // we can compare the images directly or we can compare one image to a database 
    // images :
   cout << "comparing images with images " << endl;
    for (int i = 0; i < images.size(); i++) {
        DBoW3::BowVector v1;
        vocab.transform(descriptors[i], v1);
        for (int j = i; j < images.size(); j++) {
            DBoW3::BowVector v2;
            vocab.transform(descriptors[j], v2);
            double score = vocab.score(v1, v2);
            cout << "image " << i << " vs image " << j << " : " << score << endl;
        }
        cout << endl;
    }

    // or with database 
    cout << "comparing images with database " << endl;
    DBoW3::Database db(vocab, false, 0);
    for (int i = 0; i < descriptors.size(); i++)
        db.add(descriptors[i]);
    cout << "database info: " << db << endl;
    for (int i = 0; i < descriptors.size(); i++) {
        DBoW3::QueryResults ret;
        db.query(descriptors[i], ret, 4);      // max result=4
        cout << "searching for image " << i << " returns " << ret << endl << endl;
    }
    cout << "done." << endl;
}

2.CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(slam_10)

set(CMAKE_BUILD_TYPE "Release")
set( CMAKE_CXX_FLAGS "-std=c++11 -O3" )
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -march=native -g")

# opencv 
find_package( OpenCV 3.2 REQUIRED )
include_directories( ${OpenCV_INCLUDE_DIRS} )

# dbow3 
# dbow3 is a simple lib so I assume you installed it in default directory 
include_directories( "/usr/local/include" )
link_directories( "/usr/local/lib" )


# --- Executables ---
add_executable( feature_training feature_training.cpp )
target_link_libraries( feature_training ${OpenCV_LIBS} DBoW3)

add_executable( loop_closure loop_closure.cpp )
target_link_libraries( loop_closure ${OpenCV_LIBS} DBoW3 )

#add_executable( gen_vocab gen_vocab_large.cpp )
#target_link_libraries( gen_vocab ${OpenCV_LIBS} DBoW3 )
3.运行结果
caohao@ubuntu:~/桌面/slam-14lectures/slam_11/build$ ./loop_closure
reading database
reading images... 
detecting ORB features ... 
comparing images with images 
image 0 vs image 0 : 1
image 0 vs image 1 : 0.049439
image 0 vs image 2 : 0.0427394
image 0 vs image 3 : 0.0481251
image 0 vs image 4 : 0.0300521
image 0 vs image 5 : 0.042443
image 0 vs image 6 : 0.0376194
image 0 vs image 7 : 0.0539703
image 0 vs image 8 : 0.0382042
image 0 vs image 9 : 0.0441742

image 1 vs image 1 : 1
image 1 vs image 2 : 0.0653512
image 1 vs image 3 : 0.0452744
image 1 vs image 4 : 0.0353743
image 1 vs image 5 : 0.0295066
image 1 vs image 6 : 0.0217491
image 1 vs image 7 : 0.0539993
image 1 vs image 8 : 0.0604449
image 1 vs image 9 : 0.0607882

image 2 vs image 2 : 1
image 2 vs image 3 : 0.0562473
image 2 vs image 4 : 0.0341061
image 2 vs image 5 : 0.0523157
image 2 vs image 6 : 0.0457375
image 2 vs image 7 : 0.0623972
image 2 vs image 8 : 0.0440117
image 2 vs image 9 : 0.0408185

image 3 vs image 3 : 1
image 3 vs image 4 : 0.0439394
image 3 vs image 5 : 0.0517088
image 3 vs image 6 : 0.0390256
image 3 vs image 7 : 0.0259868
image 3 vs image 8 : 0.0383476
image 3 vs image 9 : 0.0549639

image 4 vs image 4 : 1
image 4 vs image 5 : 0.0483237
image 4 vs image 6 : 0.0563347
image 4 vs image 7 : 0.0477828
image 4 vs image 8 : 0.0489294
image 4 vs image 9 : 0.0374922

image 5 vs image 5 : 1
image 5 vs image 6 : 0.0617365
image 5 vs image 7 : 0.0488636
image 5 vs image 8 : 0.0401152
image 5 vs image 9 : 0.0478946

image 6 vs image 6 : 1
image 6 vs image 7 : 0.0456377
image 6 vs image 8 : 0.0481857
image 6 vs image 9 : 0.0466667

image 7 vs image 7 : 1
image 7 vs image 8 : 0.0339042
image 7 vs image 9 : 0.0554939

image 8 vs image 8 : 1
image 8 vs image 9 : 0.0292993

image 9 vs image 9 : 1

comparing images with database 
database info: Database: Entries = 10, Using direct index = no. Vocabulary: k = 10, L = 5, Weighting = tf-idf, Scoring = L1-norm, Number of words = 61536
searching for image 0 returns 4 results:
<EntryId: 0, Score: 1>
<EntryId: 7, Score: 0.0539703>
<EntryId: 1, Score: 0.049439>
<EntryId: 3, Score: 0.0481251>

searching for image 1 returns 4 results:
<EntryId: 1, Score: 1>
<EntryId: 2, Score: 0.0653512>
<EntryId: 9, Score: 0.0607882>
<EntryId: 8, Score: 0.0604449>

searching for image 2 returns 4 results:
<EntryId: 2, Score: 1>
<EntryId: 1, Score: 0.0653512>
<EntryId: 7, Score: 0.0623972>
<EntryId: 3, Score: 0.0562473>

searching for image 3 returns 4 results:
<EntryId: 3, Score: 1>
<EntryId: 2, Score: 0.0562473>
<EntryId: 9, Score: 0.0549639>
<EntryId: 5, Score: 0.0517088>

searching for image 4 returns 4 results:
<EntryId: 4, Score: 1>
<EntryId: 6, Score: 0.0563347>
<EntryId: 8, Score: 0.0489294>
<EntryId: 5, Score: 0.0483237>

searching for image 5 returns 4 results:
<EntryId: 5, Score: 1>
<EntryId: 6, Score: 0.0617365>
<EntryId: 2, Score: 0.0523157>
<EntryId: 3, Score: 0.0517088>

searching for image 6 returns 4 results:
<EntryId: 6, Score: 1>
<EntryId: 5, Score: 0.0617365>
<EntryId: 4, Score: 0.0563347>
<EntryId: 8, Score: 0.0481857>

searching for image 7 returns 4 results:
<EntryId: 7, Score: 1>
<EntryId: 2, Score: 0.0623972>
<EntryId: 9, Score: 0.0554939>
<EntryId: 1, Score: 0.0539993>

searching for image 8 returns 4 results:
<EntryId: 8, Score: 1>
<EntryId: 1, Score: 0.0604449>
<EntryId: 4, Score: 0.0489294>
<EntryId: 6, Score: 0.0481857>

searching for image 9 returns 4 results:
<EntryId: 9, Score: 1>
<EntryId: 1, Score: 0.0607882>
<EntryId: 7, Score: 0.0554939>
<EntryId: 3, Score: 0.0549639>

done.

11.5 实验分析与评述

11.5.1 增加字典规模

       在机器学习领域,代码没有出错而结果却无法令人满意,我们首先怀疑“网络结构是否够大,层数是否足够深,数据样本是否够多”,等等。这依然是出于“好模型敌不过‘烂’数据”的大原则(也是因为缺乏更深层次的理论分析)。

1.gen_vocab_large.cpp
#include "DBoW3/DBoW3.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <iostream>
#include <vector>
#include <string>

using namespace cv;
using namespace std;


int main( int argc, char** argv )
{
    string dataset_dir = argv[1];
    ifstream fin ( dataset_dir+"/associate.txt" );
    if ( !fin )
    {
        cout<<"please generate the associate file called associate.txt!"<<endl;
        return 1;
    }

    vector<string> rgb_files, depth_files;
    vector<double> rgb_times, depth_times;
    while ( !fin.eof() )
    {
        string rgb_time, rgb_file, depth_time, depth_file;
        fin>>rgb_time>>rgb_file>>depth_time>>depth_file;
        rgb_times.push_back ( atof ( rgb_time.c_str() ) );
        depth_times.push_back ( atof ( depth_time.c_str() ) );
        rgb_files.push_back ( dataset_dir+"/"+rgb_file );
        depth_files.push_back ( dataset_dir+"/"+depth_file );

        if ( fin.good() == false )
            break;
    }
    fin.close();

    cout<<"generating features ... "<<endl;
    vector<Mat> descriptors;
    Ptr< Feature2D > detector = ORB::create();
    int index = 1;
    for ( string rgb_file:rgb_files )
    {
        Mat image = imread(rgb_file);
        vector<KeyPoint> keypoints;
        Mat descriptor;
        detector->detectAndCompute( image, Mat(), keypoints, descriptor );
        descriptors.push_back( descriptor );
        cout<<"extracting features from image " << index++ <<endl;
    }
    cout<<"extract total "<<descriptors.size()*500<<" features."<<endl;

    // create vocabulary 
    cout<<"creating vocabulary, please wait ... "<<endl;
    DBoW3::Vocabulary vocab;
    vocab.create( descriptors );
    cout<<"vocabulary info: "<<vocab<<endl;
    vocab.save( "vocab_larger.yml.gz" );
    cout<<"done"<<endl;

    return 0;
}
2.CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(slam_10)

set(CMAKE_BUILD_TYPE "Release")
set( CMAKE_CXX_FLAGS "-std=c++11 -O3" )
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -march=native -g")

# opencv 
find_package( OpenCV 3.2 REQUIRED )
include_directories( ${OpenCV_INCLUDE_DIRS} )

# dbow3 
# dbow3 is a simple lib so I assume you installed it in default directory 
include_directories( "/usr/local/include" )
link_directories( "/usr/local/lib" )


# --- Executables ---
add_executable( feature_training feature_training.cpp )
target_link_libraries( feature_training ${OpenCV_LIBS} DBoW3)

add_executable( loop_closure loop_closure.cpp )
target_link_libraries( loop_closure ${OpenCV_LIBS} DBoW3 )

add_executable( gen_vocab gen_vocab_large.cpp )
target_link_libraries( gen_vocab ${OpenCV_LIBS} DBoW3 )
3.运行结果

11.5.2 相似性评分的处理

       对任意两幅图像,我们都能给出一个相似性评分,但是只利用这个分值的绝对大小对我们并不一定有很好的帮助。例如,有些环境的外观本来就很相似,像办公室往往有很多同款式的桌椅一样;另一些环境则各个地方都有很大的不同。考虑到这种情况,我们会取一个先验相似度s(v_{t},v_{t-\Delta t}),它表示某时刻关键帧图像与上一时刻的关键帧的相似性。然后,其他的分值都参照这个值进行归一化

                        

       站在这个角度上,我们说:如果当前帧与之前某关键帧的相似度超过当前帧与上一个关键帧相似度的3倍,就认为可能存在回环。这个步骤避免了引人绝对的相似性阈值,使得算法能够适应更多的环境。     

11.5.3 关键帧的处理

       在检测回环时,我们必须考虑到关键帧的选取。如果关键帧选得太近,那么将导致两个关键帧之间的相似性过高,相比之下不容易检测出历史数据中的回环。例如,检测结果经常是第n帧和第n-2帧、第n-3帧最为相似,这种结果似乎太平凡了,意义不大。所以从实践上说,用于回环检测的帧最好稀疏一些,彼此之间不太相同,又能涵盖整个环境。
       另外,如果成功检测到了回环,例如,回环出现在第1帧和第n帧。那么很可能第n+1帧、第n+2帧都会和第1帧构成回环。确认第1帧和第n帧之间存在回环对轨迹优化是有帮助的,而接下去的第n+1帧、第n+2帧都会和第1帧构成回环产生的帮助就没那么大了,因为我们已经用之前的信息消除了累积误差,更多的回环并不会带来更多的信息。所以,我们会把“相近”的回环聚成一类,使算法不要反复地检测同一类的回环。

11.5.4 检测后的验证

       词袋的回环检测算法完全依赖于外观而没有利用任何的几何信息,这导致外观相似的图像容易被当成回环。并且,由于词袋不在乎单词顺序,只在意单词有无的表达方式,更容易引发感知偏差。所以,在回环检测之后,我们通常还会有一个验证步骤。
       验证的方法有很多。一个方法是设立回环的缓存机制,认为单次检测到的回环并不足以构成良好的约束,而在一段时间中一直检测到的回环,才是正确的回环。这可以看成时间上的一致性检测。另一个方法是空间上的一致性检测,即对回环检测到的两个帧进行特征匹配,估计相机的运动。然后,把运动放到之前的位姿图中,检查与之前的估计是否有很大的出入。总之,验证部分通常是必需的,但如何实现却是见仁见智的问题。

11.5.5 与机器学习的关系 

       从前面的论述中可以看出,回环检测与机器学习有着千丝万缕的关联。回环检测本身非常像是一个分类问题。与传统模式识别的区别在于,回环中的类别数量很大,而每类的样本很少——极端情况下,当机器人发生运动后,图像发生变化,就产生了新的类别,我们甚至可以把类别当成连续变量而非离散变量;而回环检测,相当于两幅图像落入同一类,是很少出现的。从另一个角度看,回环检测也相当于对“图像间相似性”概念的一个学习。既然人类能够掌握图像是否相似的判断,让机器学习到这样的概念也是非常有可能的。
        词袋模型本身是一个非监督的机器学习过程——构建词典相当于对特征描述子进行聚类,而树只是对所聚的类的一个快速查找的数据结构。既然是聚类,结合机器学习里的知识,我们至少可以问:
(1)是否能对机器学习的图像特征进行聚类,而不是对SURF、ORB这样的人工设计特征进
行聚类?
(2)是否有更好的方式进行聚类,而不是用树结构加上K-means这些较朴素的方式?
       结合目前机器学习的发展,二进制描述子的学习和无监督的聚类,都是非常有望在深度学习框架中得以解决的问题。我们也陆续看到了利用机器学习进行回环检测的工作。尽管目前词袋方法仍是主流,但未来深度学习方法很有希望打败这些人工设计特征的、“传统”的机器学习方法。毕竟词袋方法在物体识别问题上已经明显不如神经网络了,而回环检测又是非常相似的一个问题。例如,BoW模型的改进形式VLAD就有基于CNN的实现,同时也有一些网格在训练之后,可以从图像直接计算采集时刻相机的位姿,这些都可能成为新的回环检测算法。