《第12讲 回环检测》 读书笔记

本文是《视觉SLAM十四讲》第12讲的个人读书笔记,为防止后期记忆遗忘写的。


12.1 回环检测概述

12.1.1 回环检测的意义

VO 仅考虑相邻时间上的关联,使得整个 SLAM 会出现累积误差,无法构建全局一致的轨迹和地图。比方说前端给出的只是局部的位姿间约束,可能 是 x1 − x2, x2 − x3 等等。由于 x1 的估计存在误差,而 x2 是根据 x1 决定的,x3 又是由 x2 决定的。以此类推,误差就会被累积起来。使得后端优化的结果如图,慢慢地趋向不准确。

前端提供特征点的提取和轨迹、地图的初值,而后端负责对这所有的数据进行优化。虽然后端能够估计最大后验误差,但只有相邻关键帧数据时,也无从消除累积误差。

回环检测模块,能够给出除了相邻帧之外的,一些时隔更加久远的约束:例如 x1 − x100 之间的位姿变换。察觉到相机经过了同一个地方,采集到了相似的数据。而回环检测的关键,就是如何有效地检测出相机经过同一个地方这件事如果我们能 够成功地检测这件事,就可以为后端的 Pose Graph 提供更多的有效数据,使之得到更好 的估计,特别是得到一个全局一致(Global Consistent)的估计。可以想象成回环边把带有累计误差的边“拉”到了正确的位置

回环检测对于 SLAM 系统意义重大。一方面,它关系到我们估计的轨迹和地图在长时间下的正确性。另一方面,由于回环检测提供了当前数据与所有历史数据的关联,在跟踪算法丢 失之后,我们还可以利用回环检测进行重定位。因此,回环检测对整个 SLAM 系统精度与 鲁棒性的提升,是非常明显的。


12.1.2 方法

好了,前面我们知道:回环检测很重要。那么,现在问题来了,回环检测如何实现呢?

回环检测就是判断出相机来过这儿这件事,也就是比较两帧的图像之间的相似性。但是考虑到时间效率和命中率,我们不能全部两帧两帧进行比较,也不能无目的的随机抽取进行比较。至少希望有一个“哪处可能出现回环”的预计,才好不那么盲目地去检测。

有两种方法:基于里程计的几何关系 和 基于外观

基于里程计的几何关系:大概说,通过R和t的变换值,当我们发现当前相机运动到了之前的某个位置附近时,检测它们有没有回环关系。(比如向左旋转90度后,向右旋转90度回到原来位姿)。但是由于累积误差的存在,我们往往没法正确地发现“运动到了之前的某个位置附近”这件事实,回环检测也无从谈起。

基于外观:仅根据两张图像的相似性确定回环检测关系。这种做法摆脱了累计误差,使回环检测模块成为 SLAM 系统中一个相对独立的模块。

好,现在我们决定用基于外观的方法那么问题又来了:如何计算图像间的相似性。

在回环检测阶段的算法中,我们需要衡量一个算法的准确率召回率。但在理解这两个指标前,我们先需要理解“感知偏差”和“感知变异”这两个概念。


12.1.3 准确率和召回率

假阳性又称为感知偏差,而假阴性称为感知变异。在实际运用中,一个好的算法希望是 TP 和 TN 要尽量的高,而 FP 和 FN 要尽可能的低。而,我们需要用到准确率召回率这两个指标来衡量算法的好坏。准确率召回率公式如下:

准确率描述的是,算法提取的所有回环中,确实是真实回环的概率。而召回率则是说,在所有真实回环中,被正确检测出来的概率。在slam中,准确率和召回率是相关的,他们的关系往往如下:

矛盾如下:

算法中,当我们提高某个阈值时,算法可能变得更加“严格”——它检出更少的回环,使准确率得以提高。但同时,由于检出的数量变少了, 许多原本是回环的地方就可能被漏掉了,导致召回率的下降。反之,如果我们选择更加宽 松的配置,那么检出的回环数量将增加,得到更高的召回率,但其中可能混杂了一些不是 回环的情况,于是准确率下降了。

那么,slam中如何看待这一矛盾。

在 SLAM 中,我们对准确率要求更高,而对召回率则相对宽容一些。 由于假阳性的(检测结果是而实际不是的)回环将在后端的 Pose Graph 中添加根本错误 的边,有些时候会导致优化算法给出完全错误的结果。召回率低一些,则顶多有部 分的回环没有被检测到,地图可能受一些累积误差的影响——然而仅需一两次回环就可以 完全消除它们了。所以说在选择回环检测算法时,我们更倾向于把参数设置地更严格一些, 或者在检测之后再加上回环验证的步骤。


12.2 词袋模型

好,现在我们通过准确率召回率,对算法好坏有了评判标准。那么算法中如何根据图片判断是否“故地重游”呢?

一种想法是通过特征点正确匹配的比例还确定是否有回环。但是,这种做法会存在一些问题,例如特征的匹配会比较费时、当光照变化时特征描述可能不稳定等。所以,我们需要构建词袋目的是用“图像上几处几集中的特征”来描述一个 图像。

比如两张图片,我们比较他的相似性过程:

  1. 确定“人、车、狗”等概念——对应于词袋中的“单词”,许多单词放在一起,组成了“字典”。
  2. 用单词出现的情况(或直方图)描述整张图像。这就把一个图像转换成了一个向量的描述。
  3. 比较上一步中的描述的相似程度。

例如“人”、“车”、“狗”都是记录在字典中的单词,对于任意图像 A,根据它们含有的单词,可记为:

所以只要用 [1, 1, 0]T 这个向量就可以表达 A 的意义。通过字典和单词,只需一个向量就可以描述整张图像了。该向量描述的是“图像是否含有某类特征”的信息,比单纯的灰度值更加稳定。

但是,描述向量说的是“是否出现”,而不管它们“在哪儿出现”,所以与物体的空间位置和排列顺序无关。因此在相机发生少量运动时,只要物体仍在视野中出现,我们就仍然保证描述向量不发生变化。基于这种特性,我们称它为 Bag-of-Words 而不是什么 List-of-Words,强调的是 Words 的有无,而无关其顺序。


12.3 字典

12.3.1 字典的结构

问题来了,怎么从图片中找出“猫”、“狗”这样的单词呢?也就是字典怎么产生的问题。

一个单词与 一个单独的特征点不同,它不是从单个图像上提取出来的,而是某一类特征的组合。所以,字典生成问题类似于一个聚类问题。聚类问题是无监督机器学习中一个特别常见的问题,用于让机器自行寻找数据中的规律的问题。每个单词可以看作局部相邻特征点的集合。

当我们有 N 个数据,想要归成 k 个类,这可以用经典的 K-means(K 均 值)算法解决。主 要有以下几个步骤:

不过也存在一些问题,例如需要指定聚类数量、 随机选取中心点使得每次聚类结果都不相同以及一些效率上的问题。总之,根据 K-means,我们可以把已经提取的大量特征点聚类成一个含有 k 个单词的字典了。

现在可以构建很多单词组合的字典了,现在的问题变为:如何根据图像中某个特征点,查找字典中相应的单词?

考虑到字典的通用性,我们通常会使用一个较大规模的字典,以保证当前使用环境中的图像特征都曾在字典里出现过。所以介绍 k 叉树,作为一种简单实用的结构来表达字典。它的思路很简单,类似于层次聚类,是 kmeans 的直接扩展。

假定我们有 N 个特征点,希望构建一个深度为 d,每次分叉为 k 的 树(构建单词数、字典),那么做法如下:

实际上,最终我们仍在叶子层构建了单词,而树结构中的中间节点仅供快速查找时使用。这样一个 k 分支,深度为 d 的树,可以容纳 k^d 个单词。

另一方面,在查找某个给定 特征对应的单词时,只需将它与每个中间结点的聚类中心比较(一共 d 次),即可找到最 后的单词,保证了对数级别的查找效率。


12.3.2 实践:创建字典

第 1 张图像与最后1张图像采自同一个地方,我们要看程序能否检测到这件事情。

首先,请安装本程序使用的 BoW 库,DBoW3 的使用方式非常容易,调用 DBoW3 的字典生成接口即可。在 DBoW3::Vocabulary 对象的构造函数中,我们能够指定树的分叉数量以及深度。不过这里使用了默认构造函数,也就是 k = 10, d = 5。这是一个小规模的字典,最大能容纳 10000 个单词。

#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 )
    {
//对目标图像提取ORB特征并存放至vector容器中
//使用默认参数,即每张图像 500 个特征点
        vector<KeyPoint> keypoints; 
        Mat descriptor;
        detector->detectAndCompute( image, Mat(), keypoints, descriptor );
        descriptors.push_back( descriptor );
    }
    
    // create vocabulary 
    cout<<"creating vocabulary ... "<<endl;
    DBoW3::Vocabulary vocab;
//使用了默认构造函数,也就是 k = 10, d = 5。
    vocab.create( descriptors );
    cout<<"vocabulary info: "<<vocab<<endl;
//最后我们把字典存储为一个压缩文件。
    vocab.save( "vocabulary.yml.gz" );
    cout<<"done"<<endl;
    
    return 0;
}

分支数量 k 为 10,深度 L 为 5 ,单词数量为 4983,没有充满最大容量。但是,剩下的 Weighting 和 Scoring 是什么呢?从字面上看,Weighting 是权重,Scoring 似乎指的是评分。但评分是如何计算的呢?


12.4 相似度计算

有了字典之后,给定任意特征 fi,只要在字典树中逐层查找,最后都能找到与之对应的单词 wj(狗,猫)。一张图像中提取了 N 个特征,找到这 N 个特征对应的单词之后,我们相当于拥有了该图像在单词列表中的分布,相当于是说“这张图里有 一个人和一辆汽车”这样的意思了。但是,我们对所有单词都是“一视同仁”的——有就是有,没有就是没有。这样无法通过向量形式直接判断两图片的相似性。我们需要对图片间的相似性有计算的方法。

我们希望对单词的区分性或重要性加以评估,给它们不同的权值以起到更好的效果。

在文本检索中,常用的一种做法称为 TF-IDF,或译频率-逆文档频率。TF 部分的思想是,某单词在一个图像中经常出现,它的区分度就高。另一方面,IDF 的思想是,某单词在字典中出现的频率越低, 则分类图像时区分度越高。

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

另一方面,TF 部分则是指某个特征在单个图像中出现的频率。假设图像 A 中,单词 wi 出现了 ni 次,而一共出现的单词次数为 n,那么 TF 为:

于是 wi 的权重等于 TF 乘 IDF 之积:

对于某个图像 A,它的特征点可对应到许多个单词,组成它的 Bag-ofWords:

我们用单个向量 vA 描述了一个图像 A。这个向量 vA 是一个稀疏的向量,它 的非零部分指示出图像 A 中含有哪些单词,而这些部分的值为 TF-IDF 的值。于是,我们有了一个更加好的描述图片的表达方式(带权重的向量)。

至于两向量怎么比较的问题,有很多种方法。一般BoW 库默认提供。


以下程序复制来自《视觉slam十四讲》相应章节。程序演示了两种比对方式:图像之间的直接比较以及图像与数据库之间的比较—— 尽管它们是大同小异的。

#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 leed to overfitting.  
    // 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;
}

存在的问题:演示实验中,我们看到相似图像 1 和 10 的评分明显高于其他图像对,然而就数值上看并没有我们想象的那么明显。


12.5 实验分析与评述

对上述问题的可能解释:

在机器学习领域,如果代码没出错而结果不满意时,我们首先怀疑“网络结构是否够大,层数是否足够深,数据样本是否够多”之类的问题,这依然是出于“好模型敌不过烂数 据”的大原则,但出现这种情况,我们首先会怀疑:是不是字典选的太小了?当我们扩展了字典的规模,可以看到,无关图像的相似性明显变小了。而相似的图像,虽然分值也略微下降,但相对于其他图像的评分,却变得更为显著了。

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

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

回环检测与机器学习有着千丝万缕的关联。回环检测本身非常像是一个分类问题。与传统模式识别的区别在于,回环中的类别数量很大,而每类的 样本很少——极端情况下,当机器人发生运动后,图像发生变化,就产生了新的类别,我们甚至可以把类别当成连续变量而非离散变量;而回环检测,相当于两个图像落入同一类, 则是很少出现的。从另一个角度,回环检测也相当于对“图像间相似性”概念的一个学习。 既然人类能够掌握图像是否相似的判断,让机器学习到这样的概念也是非常有可能的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值