1.回环检测的必要性
因为累积误差,最后会使地图出现漂移。比如之前位姿图优化,只给后端提供相邻帧之间的约束,x1-x2,x2-x3,x1的误差就会传到x3.而回环检测能够给出时隔更久远的约束,比如x1-x100,它做的事就是检测相机经过了同一个地方,把带有累积误差的边拉回到了正确的位置。
回环检测提供了当前数据与历史数据之间的关联,一是可以保证轨迹和地图长时间准确,二是如果跟踪算法丢失,可以用它来做重定位。
前端和局部后端只是VO,VO带回环检测和全局后端才是slam.
2.实现方法
2.1简单的思路
2.1.1 任意匹配
对任意两幅图像做一遍特征匹配,根据正确匹配的数量确定哪两幅图像存在关联。这是假设了任意两幅图像都有可能存在回环。
有效,但检测数量太多,对于N个可能的回环,要检测Cn 2,是O(N*N)的复杂度。如果是图像的话,每秒就30帧呢,就算是关键帧,也不少。
2.1.2随机抽取
从N帧中随机抽取5帧与当前帧进行比较,之前有这么做来做特征匹配吧。运算量不高,但N帧数增长时,抽到回环的几率不高,检测的效率不高。
2.2基于哪处可能出现回环的预计
2.2.1基于里程计的几何关系
有点倒果为因了,在累积误差较大时不能工作。
2.2.2基于外观
这个是视觉slam主流的做法,它能够有效地在不同场景下工作,并已经应用在实际的系统中了。它跟前端和后端都没有关系,仅靠两幅图像之间的相似性来确定回环检测关系,这样就摆脱了累积误差,这样回环检测就是slam中一个相对独立的模块(当然前端会给它提供特征点)。
2.2.2.1计算图像之间的相似性
这是基于外观的核心问题。比如图像A,B。相似性评分记为s(A,B).但是
s(A,B)=||A-B||不行,因为
(1)像素灰度不稳定,受环境光照和相机曝光的影响,即使相机未动,光照变化了,或者打开一个电灯,那么图像整体变亮,对于同样的数据,A和B的差异值却会很大。
(2)相机视角微变,即使每个物体光度不变,它们的像素也会在图像中发生位移,差异值也会很大。
主要的问题就是,对于非常相似的图像,A和B的差异值也会很大。
评价s(A,B)的指标,准确率,召回率。
2.2.2.2准确率和召回率
对回环检测的结果进行分类
算法是回环,事实是,真阳性。
算法是,事实不是,假阳性,又称感知偏差(perceptual aliasing)
算法不是,事实是,假阴性,又称感知变异(perceptual variability)
算法不是,事实不是,真阴性。
想象一个2*2矩阵,算法是都是阳性。阳阴性是根据算法是与不是来判断的。
TP,TN,FP,FN.希望是TP,TN尽量高,FP,FN尽量低。
准确率TP/(TP+FP),召回率 TP/(TP+FN),都是TP来除,一个是算法检测出来的回环总数,一个是真实的回环总数。
假阳性:两幅图看起来很像,但不是同一个走廊,就是算法是,事实不是。
假阴性:两幅图像是,但是由于光照,图片不一样。就是算法不是,事实是。
准确率:算法提取的所有回环中确实是真实回环的概率
召回率:所有真实回环被检测出来的概率。
是一对矛盾来着。
一个算法有很多设置参数,调整参数,比如提高,算法严格,检出更少回环,准确率高了,但是召回率下降。如果宽松,召回率高了,准确率下降。
评价算法,就测试它在各种配置下的P和R值,画出Precision-Recall曲线,以召回率为横轴,准确率为纵轴,是一个向下的一个曲线。
SLAM对准确率更严格,对召回率则宽松一点。因为假阳性(检测是而实际不是)回环会在pose graph中添加错误的边,优化算法可能会给出完全错误的结果。想象一下,slam程序把所有的办公桌当成同一张,则走廊不直了,墙壁被交错在一起了,最后整个地图失效。召回率低一些,则顶多有部分的回环没有检测到,地图可能会受一些累积误差的影响,然而仅需一两次回环就可以完全消除它们了。
所以选择回环检测算法,倾向于把参数设置得更严格,或者在检测之后再加上回环验证。
A-B的准确率和召回率都不高。所以不用。
12.2词袋模型
词袋,就是bag of words,就是BoW.
词袋模型:首先由单词,单词组成字典,图像就可用字典描述,就有一个向量出现。字典是无序的,这个无序是指就管它出不出现,而不管它在哪出现。而且可以用单纯的0,1表示是否出现,也可以用数量表示出现了几次。这就可以称它为词袋模型。
计算相似度就可以用这个向量。
如果字典有W个单词,或者说a,b属于R W,它把相似度就表示为
s(a,b)=1-1/W||a-b||
这里范数用的是L1范数,所谓的L1范数是所有项的绝对值的和。
12.3单词怎么来
单词是聚类得到的,对图像特征进行聚类,现在聚类还是用的是orb特征,用orb特征的描述子聚的类。
首先用k-means聚类
(1)随机选取k个中心点,c1,c2,..,ck
(2)对剩下来的点,或者说对所有的点,计算它们与中心的点的距离,把它归到与它距离最小的那个类里面;
(3)重新计算每个类的中心点
(4)如果这一次相比上一次中心点的变化很小,说明算法收敛,否则继续执行(2)(3)(4)。
问题:K要指定,由于刚开始中心点是随机选取的,随机选取的结果不同,造成最后聚类的结果也不同。
这里用的k叉树,是k-means聚类的扩展
(1)根节点,用k-means得到第一层(实际中用k-means++,因为它聚类均匀,而k-means又可能不均匀),有没有可能会有一个类什么都没有呢?
(2)对第一层的每个节点,再分成k类,得到下一层,直到达到深度d
最后一层是叶节点,也就是单词了,单词总共有k的d次方个。
查找的时候,只要与每个节点的聚类中心比较即可,总共比较d次就可以了。奥,比如在第一层,确定它在第1类,然后从第一类再往下找,就不用管后面的9类了。
实践:生成字典相当容易,假设文件夹data里面放的是图像数据(彩色图),那么令
string path="../data/"+to_string(i+1)+".png",然后用imread读取之后把这些图像矩阵都放到images里。
提取的是ORB特征,提取之后生成关键点和描述子,描述子依次放入描述子矩阵descriptors.
定义字典DBoW3::Vocabulary vocab;这里是默认了k=10,d=5,可以不使用默认,自己定义。
使用DBoW3要先下载它,git clone https://github.com/rmsalinas/DBoW3,或者直接从书中的3rdparty文件中找到。编译它. mkdir build ,cd build,cmake ..,make,sudo make install
在CMakeLists.txt文件夹里要加
set( DBoW3_INCLUDE_DIRS "/usr/local/include" )
set( DBoW3_LIBS "/usr/local/lib/libDBoW3.a" )
可执行程序后面要添加它的库。
add_executable( feature_training feature_training.cpp )
target_link_libraries( feature_training ${OpenCV_LIBS} ${DBoW3_LIBS} )
程序中要include它的h文件,即#include "DBoW3/DBoW3.h"
只有定义了字典vocab,调用它的create函数,字典就创建好了。
vocab.create(descriptors);
然后把它保存成一个压缩文件,就可以在另外一个程序中直接使用这个字典了。
12.4相似度计算
TF_IDF
IDF就是建立字典的时候,因为是按所有的特征分的,所以单词wi下的特征数量是不相同的,毕竟wi是一个类,所以wi下的特征数量和所有特征数量相比,也算是wi的一个特征。所以IDF这里定义为IDFi=log(n/ni),因为认为特征数量越多,对图像的区分度越不高。建立字典的时候IDFi就可以求出来了。
TF就是对单幅图像A来说的,单词wi出现了ni次,而所有单词出现的次数为n,那么TF为
TFi=ni/n
而tf-idf里面所说的权重就是TFi*IDFi,记为gai.
现在词袋模型就变成了
vA=[ga1,ga2,..,gan],它是一个稀疏的向量。那么相似度这里是这么计算的
单项2*(|vAi|+|vBi|-|vAi-vBi|)
s(vA-vB)就为这些项的和。
这里是对vA,vB的每一项,哪个小就取哪个,把这些值再加起来,就是相似度?
实践:看回环
上面已经创建好字典文件了,是一个.yml.gz文件,直接DBoW3::Vocabulary vocab("这个文件")就可以直接用这个字典了。然后还是读图保存到images,提取每个图像的描述子保存到descriptors.
对于每个图像来说,定义词袋向量DBoW3::BowVector v1;然后字典可以直接把描述子的每一项转成为一个词袋向量。
vocab.transform(descriptors[i],v1);
得到v1,v2,用vocab的score函数就可以计算出它们的相似度了。
double score=vocab.score(v1,v2);
另一种就是先用字典和所有的描述子创建一个数据库,用数据库去比,而且会返回给跟每个图像最相似的几个图像。
创建数据库DBoW3::Database db(vocab,false,0);
数据库添加描述子的每一项就可以了
db.add(descriptors[i]);
然后比的时候直接db.query(descriptors[i],ret,4)就可以了,结果返回给ret,4是返回数量
ret格式如下:
DBoW3::QueryResults ret;
问题:认为最相似的图1和图10,也只有0.0525.太低了。
解决:创建大字典,然后用大字典来做。
结果:无关图像的相似性变小,使有关图像的相似性变得更显著。能够更好地区分了,但score是都下降了。
12.5.2相似性评分的处理
只用score不太好,因为环境不一样,比如办公室环境往往用同款式的桌椅,而另一些环境则每个地方都不一样。所以这里可以取一个先验相似度,某时刻关键帧与它上一时刻的关键帧的相似度
s(vt,vt-deltat),那vt的其他分值都参照这个来归一化,就是
s(vt,vtj)'=s(vt,vtj)/s(vt,vt-deltat)
如果s(vt,vtj)'>3,认为vt,vtj之间可能存在回环。
12.5.4处理关键帧
检测回环时,关键帧不能选择太近,否则检测出第n帧和n-1帧,n-2帧存在回环并没有什么意义。
检测出第1帧和第n帧,有可能第一帧和n-2帧也存在回环,这时候要把相近的回环聚成一类,怎么聚没说。
12.5.5验证回环
因为词袋不在意单词顺序,只在乎有没有出现,所以感知偏差是很容易出现的。所以要验证。
(1)回环缓存,单次检测到不认为它是,在一段时间内一直检测到认为它是。时间上的一致性。
(2)把回环检测出来的帧进行特征匹配,估计运动,估计完运动后放入位姿图,看跟之前的估计是否有很大出入。是有算呢,还是没有算呢?
12.5.6与机器学习的关系
回环检测类似于分类,而且回环中类别数量大,每类的样本数量很少,每当机器运动,图像发生变化,就会产生新的类,回环检测相当于两幅图像落入同一类。图像间相似性概念的一个学习。
改进方向
(1)对机器学习的图像特征进行聚类,而不是人为设计的ORB特征;
(2)用更好的方法进行聚类,而不是k叉树这种朴素的方式。
词袋模型会被机器学习给替代的。
1.回环检测的必要性
因为累积误差,最后会使地图出现漂移。比如之前位姿图优化,只给后端提供相邻帧之间的约束,x1-x2,x2-x3,x1的误差就会传到x3.而回环检测能够给出时隔更久远的约束,比如x1-x100,它做的事就是检测相机经过了同一个地方,把带有累积误差的边拉回到了正确的位置。
回环检测提供了当前数据与历史数据之间的关联,一是可以保证轨迹和地图长时间准确,二是如果跟踪算法丢失,可以用它来做重定位。
前端和局部后端只是VO,VO带回环检测和全局后端才是slam.
2.实现方法
2.1简单的思路
2.1.1 任意匹配
对任意两幅图像做一遍特征匹配,根据正确匹配的数量确定哪两幅图像存在关联。这是假设了任意两幅图像都有可能存在回环。
有效,但检测数量太多,对于N个可能的回环,要检测Cn 2,是O(N*N)的复杂度。如果是图像的话,每秒就30帧呢,就算是关键帧,也不少。
2.1.2随机抽取
从N帧中随机抽取5帧与当前帧进行比较,之前有这么做来做特征匹配吧。运算量不高,但N帧数增长时,抽到回环的几率不高,检测的效率不高。
2.2基于哪处可能出现回环的预计
2.2.1基于里程计的几何关系
有点倒果为因了,在累积误差较大时不能工作。
2.2.2基于外观
这个是视觉slam主流的做法,它能够有效地在不同场景下工作,并已经应用在实际的系统中了。它跟前端和后端都没有关系,仅靠两幅图像之间的相似性来确定回环检测关系,这样就摆脱了累积误差,这样回环检测就是slam中一个相对独立的模块(当然前端会给它提供特征点)。
2.2.2.1计算图像之间的相似性
这是基于外观的核心问题。比如图像A,B。相似性评分记为s(A,B).但是
s(A,B)=||A-B||不行,因为
(1)像素灰度不稳定,受环境光照和相机曝光的影响,即使相机未动,光照变化了,或者打开一个电灯,那么图像整体变亮,对于同样的数据,A和B的差异值却会很大。
(2)相机视角微变,即使每个物体光度不变,它们的像素也会在图像中发生位移,差异值也会很大。
主要的问题就是,对于非常相似的图像,A和B的差异值也会很大。
评价s(A,B)的指标,准确率,召回率。
2.2.2.2准确率和召回率
对回环检测的结果进行分类
算法是回环,事实是,真阳性。
算法是,事实不是,假阳性,又称感知偏差(perceptual aliasing)
算法不是,事实是,假阴性,又称感知变异(perceptual variability)
算法不是,事实不是,真阴性。
想象一个2*2矩阵,算法是都是阳性。阳阴性是根据算法是与不是来判断的。
TP,TN,FP,FN.希望是TP,TN尽量高,FP,FN尽量低。
准确率TP/(TP+FP),召回率 TP/(TP+FN),都是TP来除,一个是算法检测出来的回环总数,一个是真实的回环总数。
假阳性:两幅图看起来很像,但不是同一个走廊,就是算法是,事实不是。
假阴性:两幅图像是,但是由于光照,图片不一样。就是算法不是,事实是。
准确率:算法提取的所有回环中确实是真实回环的概率
召回率:所有真实回环被检测出来的概率。
是一对矛盾来着。
一个算法有很多设置参数,调整参数,比如提高,算法严格,检出更少回环,准确率高了,但是召回率下降。如果宽松,召回率高了,准确率下降。
评价算法,就测试它在各种配置下的P和R值,画出Precision-Recall曲线,以召回率为横轴,准确率为纵轴,是一个向下的一个曲线。
SLAM对准确率更严格,对召回率则宽松一点。因为假阳性(检测是而实际不是)回环会在pose graph中添加错误的边,优化算法可能会给出完全错误的结果。想象一下,slam程序把所有的办公桌当成同一张,则走廊不直了,墙壁被交错在一起了,最后整个地图失效。召回率低一些,则顶多有部分的回环没有检测到,地图可能会受一些累积误差的影响,然而仅需一两次回环就可以完全消除它们了。
所以选择回环检测算法,倾向于把参数设置得更严格,或者在检测之后再加上回环验证。
A-B的准确率和召回率都不高。所以不用。
12.2词袋模型
词袋,就是bag of words,就是BoW.
词袋模型:首先由单词,单词组成字典,图像就可用字典描述,就有一个向量出现。字典是无序的,这个无序是指就管它出不出现,而不管它在哪出现。而且可以用单纯的0,1表示是否出现,也可以用数量表示出现了几次。这就可以称它为词袋模型。
计算相似度就可以用这个向量。
如果字典有W个单词,或者说a,b属于R W,它把相似度就表示为
s(a,b)=1-1/W||a-b||
这里范数用的是L1范数,所谓的L1范数是所有项的绝对值的和。
12.3单词怎么来
单词是聚类得到的,对图像特征进行聚类,现在聚类还是用的是orb特征,用orb特征的描述子聚的类。
首先用k-means聚类
(1)随机选取k个中心点,c1,c2,..,ck
(2)对剩下来的点,或者说对所有的点,计算它们与中心的点的距离,把它归到与它距离最小的那个类里面;
(3)重新计算每个类的中心点
(4)如果这一次相比上一次中心点的变化很小,说明算法收敛,否则继续执行(2)(3)(4)。
问题:K要指定,由于刚开始中心点是随机选取的,随机选取的结果不同,造成最后聚类的结果也不同。
这里用的k叉树,是k-means聚类的扩展
(1)根节点,用k-means得到第一层(实际中用k-means++,因为它聚类均匀,而k-means又可能不均匀),有没有可能会有一个类什么都没有呢?
(2)对第一层的每个节点,再分成k类,得到下一层,直到达到深度d
最后一层是叶节点,也就是单词了,单词总共有k的d次方个。
查找的时候,只要与每个节点的聚类中心比较即可,总共比较d次就可以了。奥,比如在第一层,确定它在第1类,然后从第一类再往下找,就不用管后面的9类了。
实践:生成字典相当容易,假设文件夹data里面放的是图像数据(彩色图),那么令
string path="../data/"+to_string(i+1)+".png",然后用imread读取之后把这些图像矩阵都放到images里。
提取的是ORB特征,提取之后生成关键点和描述子,描述子依次放入描述子矩阵descriptors.
定义字典DBoW3::Vocabulary vocab;这里是默认了k=10,d=5,可以不使用默认,自己定义。
使用DBoW3要先下载它,git clone https://github.com/rmsalinas/DBoW3,或者直接从书中的3rdparty文件中找到。编译它. mkdir build ,cd build,cmake ..,make,sudo make install
在CMakeLists.txt文件夹里要加
set( DBoW3_INCLUDE_DIRS "/usr/local/include" )
set( DBoW3_LIBS "/usr/local/lib/libDBoW3.a" )
可执行程序后面要添加它的库。
add_executable( feature_training feature_training.cpp )
target_link_libraries( feature_training ${OpenCV_LIBS} ${DBoW3_LIBS} )
程序中要include它的h文件,即#include "DBoW3/DBoW3.h"
只有定义了字典vocab,调用它的create函数,字典就创建好了。
vocab.create(descriptors);
然后把它保存成一个压缩文件,就可以在另外一个程序中直接使用这个字典了。
12.4相似度计算
TF_IDF
IDF就是建立字典的时候,因为是按所有的特征分的,所以单词wi下的特征数量是不相同的,毕竟wi是一个类,所以wi下的特征数量和所有特征数量相比,也算是wi的一个特征。所以IDF这里定义为IDFi=log(n/ni),因为认为特征数量越多,对图像的区分度越不高。建立字典的时候IDFi就可以求出来了。
TF就是对单幅图像A来说的,单词wi出现了ni次,而所有单词出现的次数为n,那么TF为
TFi=ni/n
而tf-idf里面所说的权重就是TFi*IDFi,记为gai.
现在词袋模型就变成了
vA=[ga1,ga2,..,gan],它是一个稀疏的向量。那么相似度这里是这么计算的
单项2*(|vAi|+|vBi|-|vAi-vBi|)
s(vA-vB)就为这些项的和。
这里是对vA,vB的每一项,哪个小就取哪个,把这些值再加起来,就是相似度?
实践:看回环
上面已经创建好字典文件了,是一个.yml.gz文件,直接DBoW3::Vocabulary vocab("这个文件")就可以直接用这个字典了。然后还是读图保存到images,提取每个图像的描述子保存到descriptors.
对于每个图像来说,定义词袋向量DBoW3::BowVector v1;然后字典可以直接把描述子的每一项转成为一个词袋向量。
vocab.transform(descriptors[i],v1);
得到v1,v2,用vocab的score函数就可以计算出它们的相似度了。
double score=vocab.score(v1,v2);
另一种就是先用字典和所有的描述子创建一个数据库,用数据库去比,而且会返回给跟每个图像最相似的几个图像。
创建数据库DBoW3::Database db(vocab,false,0);
数据库添加描述子的每一项就可以了
db.add(descriptors[i]);
然后比的时候直接db.query(descriptors[i],ret,4)就可以了,结果返回给ret,4是返回数量
ret格式如下:
DBoW3::QueryResults ret;
问题:认为最相似的图1和图10,也只有0.0525.太低了。
解决:创建大字典,然后用大字典来做。
结果:无关图像的相似性变小,使有关图像的相似性变得更显著。能够更好地区分了,但score是都下降了。
12.5.2相似性评分的处理
只用score不太好,因为环境不一样,比如办公室环境往往用同款式的桌椅,而另一些环境则每个地方都不一样。所以这里可以取一个先验相似度,某时刻关键帧与它上一时刻的关键帧的相似度
s(vt,vt-deltat),那vt的其他分值都参照这个来归一化,就是
s(vt,vtj)'=s(vt,vtj)/s(vt,vt-deltat)
如果s(vt,vtj)'>3,认为vt,vtj之间可能存在回环。
12.5.4处理关键帧
检测回环时,关键帧不能选择太近,否则检测出第n帧和n-1帧,n-2帧存在回环并没有什么意义。
检测出第1帧和第n帧,有可能第一帧和n-2帧也存在回环,这时候要把相近的回环聚成一类,怎么聚没说。
12.5.5验证回环
因为词袋不在意单词顺序,只在乎有没有出现,所以感知偏差是很容易出现的。所以要验证。
(1)回环缓存,单次检测到不认为它是,在一段时间内一直检测到认为它是。时间上的一致性。
(2)把回环检测出来的帧进行特征匹配,估计运动,估计完运动后放入位姿图,看跟之前的估计是否有很大出入。是有算呢,还是没有算呢?
12.5.6与机器学习的关系
回环检测类似于分类,而且回环中类别数量大,每类的样本数量很少,每当机器运动,图像发生变化,就会产生新的类,回环检测相当于两幅图像落入同一类。图像间相似性概念的一个学习。
改进方向
(1)对机器学习的图像特征进行聚类,而不是人为设计的ORB特征;
(2)用更好的方法进行聚类,而不是k叉树这种朴素的方式。
词袋模型会被机器学习给替代的。