前端也称为视觉里程计(VO),它根据相邻图像的信息估计出粗略的相机运动,给后端提供较好的初始值。
VO的实现方法,按照是否需要提取特征,分为特征点法的前端和不提特征的直接法前端。其中,基于特征点法的前端,长久以来(直到现在)被认为是视觉里程计的主流方法,它运行稳定,对光照、动态物体不敏感,是目前比较成熟的解决方案。
VO的主要问题是如何根据图像来估计相机运动。但是图像本身是一个由亮度和色彩组成的矩阵,如果直接通过矩阵来考虑运动估计,非常困难。所以,我们通常采用这种做法:首先在图像中选取比较有代表性的点,这些点在相机视角发生少量变化后会保持不变,所以我们可以在各个图像中找到相同的点,然后在这些点的基础上,讨论相机位姿估计问题,以及这些点的估计问题。
下面先来介绍一种最直接的提取特征的方式:在不同的图像间辨认角点,确定它们的对应关系。在这种方式中,角点就是所谓的特征。
但是,在大多数应用中,单纯的角点仍然是不能满足我们的很多需求的。比如说,当旋转相机时,角点的外观会发生变化,我们也就不容易辨认出那是同一个角点。
为此,计算机视觉领域的研究者们在长年的研究中设计了许多更加稳定的局部图像特征,如著名的:SIFT、SURF、ORB。这些人工设计的特征点具有如下特征:
(1)可重复性:相同的“区域”可以在不同的图像中找到。
(2)可区别性:不同的“区域”有不同的表达。
(3)高效率:同一图像中,特征点的数量应远小于像素的数量。
(4)本地性:特征仅与一小片图像区域相关。
特征点由 关键点 和 描述子 两部分组成。
比如,当谈论SIFT特征时,是指“提取SIFT关键点,并计算SIFT描述子”两件事情。关键点是指特征点在图像里的位置,有些特征点还具有朝向、大小等信息。描述子通常是一个向量,按照某种人为设计的方式,描述了该关键点周围像素的信息。描述子是按照“外观相似的特征应该具有相似的描述子”的原则来设计的。因此,只要两个特征点的描述子在向量空间上的距离相近,就可以认为它们是同样的特征点。
下面,简要的介绍几种著名的特征。
(1)研究者们曾经提出过许多图像特征,它们有些很精确,在相机的运动和光照变化下仍具有相似的表达,但相应地需要较大的计算量。其中,最经典的就是SIFT,它充分考虑了在图像变换中出现的光照、尺度、旋转等变化,但随之而来的是极大的计算量。到目前为止,普通PC的CPU还无法 实时 的计算SIFT特征,进行定位与建图。所以在SLAM中,我们很少使用这种“奢侈”的特征。
(2)还有一些特征,考虑适当的降低精度和健壮性,以提升计算的速度。比如:FAST关键点就属于计算特别快的一种特征点。(注意这里的“关键点”的表述,说明它没有描述子)。
(3)ORB特征是质量和性能之间较好的折中,它改进了FAST关键点不具有方向性的问题,并采用速度极快的二进制描述子BRIEF,使得整个图像特征提取的环节大大加速。
当然,大部分特征提取都具有较好的并行性,如果不考虑成本的话,可以通过GPU等设备来加速计算,经过GPU加速后的SIFT,就可以满足实时计算要求。
ORB特征是质量和性能之间较好的折中,因此下面通过介绍ORB特征来介绍提取特征的整个过程。
ORB特征也是由关键点和描述子两部分组成。(1)它的关键点称为"Oriented FAST",是一种改进的FAST角点。(2)它的描述子称为BRIEF(Binary Robust Independent Elementary Feature)。提取ORB特征分为两个步骤:
(1)FAST角点提取:找出图像中的“角点”。相较于原版的FAST,ORB中计算了特征点的主方向,为后续的BRIEF描述子增加了旋转不变特性。
(2)BRIEF描述子:对前一步提取出特征点的周围图像区域进行描述。
一、FAST关键点
FAST是一种角点,主要检测局部像素灰度变化明显的地方,以速度快著称。
它的思想是:如果一个像素与领域的像素差别较大(过亮或者过暗),那么它更可能是角点。相比于其他角点检测算法,FASE只需要比较像素亮度的大小,十分快捷。
FAST角点的检测过程如下:
(1)在图像中选取像素,假设它的亮度是
(2)设置一个阈值T(比如说设定为的20%)
(3)以像素为中心,选取半径为3的圆上的16个像素点
(4)若选取的圆上有连续的N个点的亮度大于或小于,那么像素p可以被认为是特征点
(N通常取12,即为FAST-12。其他常用的N取值为9和11,它们分别被称为FAST-9和FAST-11)。
(5)循环以上四步,对每一个像素执行相同的操作
对于FAST-12来说,为了更高效的检测,可以添加一项预测试操作,以快速的排除绝大多数不是角点的像素。具体的预测试方法如下:对于每个像素,直接检测领域圆上的第1、5、9、13个像素的亮度。只有当这4个像素中有3个同时大于或小于时,当前像素才有可能是一个角点(否则最多只有连续的10个像素满足此条件),否则该像素点就一定不是角点,可以直接排除。
此外,原始的FAST角点经常出现“扎堆”的现象。所以在第一遍检测之后,还需要用非极大值抑制,在一定区域内仅保留响应极大值的角点,以此来避免角点集中的问题。
FAST特征点的计算仅仅是比较像素间亮度的差异,速度非常快,但是也存在一些问题。
问题1:FAST特征点数量很大且不确定,而我们往往希望对图像提取固定数量的特征点。
ORB中的改进:我们可以指定最终要提取的角点数量N,对原始FAST角点分别计算Harris响应值,然后选取前N个具有最大响应值的角点作为最终提取的角点集合。
问题2:FAST角点不具有方向性和尺度的描述。
ORB中的改进:针对FAST角点不具有方向性和尺度的弱点,ORB添加了尺度和旋转的描述。
- 尺度不变性:构建图像金字塔,并在金字塔的每一层上检测角点来实现。
- 特征的旋转的描述由灰度质心法来实现。
- 质心:所谓的质心就是指以图像块灰度值作为权重的中心。
- 具体的操作步骤如下:
- 在一个小的图像块B中,定义图像块的矩为:,
- 通过矩可以找到图像块的质心:
- 连接图像块的几何中心O和质心C,得到一个方向方向,于是特征点的方向就可以定义为
通过上面的方法,FAST角点就有了尺度和方向性的描述,从而大大提升了其在不同图像间表述的健壮性。因此,在ORB中,把这种改进后的FAST也称为Oriented FAST。
二、BRIEF描述子
在提取了Oriented FAST关键点后,我们要对每个点计算描述子。ORB使用改进的BRIEF特征描述。
BRIEF是 一种二进制描述子,其描述向量由许多个0和1组成,这里的0和1编码了关键点附近两个像素(比如p和q)的大小关系:如果p比q大,则取1,反之就取0.如果我们取了128个这样的p、q,最后就得到一个128维的由0、1组成的向量。
关于p和q如何选取的问题,在作者原始的论文中给出了若干种挑选方法,大体上都是按照某种概率分布,随机的挑选p和q位置。
BRIEF使用了随机选点的比较,速度非常快,而且由于使用了二进制表达,存储起来也是非常方便的,适用于实时的图像匹配。
原始的BRIEF描述子不具有旋转不变性,因此在图像发生旋转时容易丢失。而ORB在FAST特征点提取阶段计算了关键点的方向,所以可以利用方向信息,计算旋转了之后的“Steer BRIEF”特征使得ORB的描述子具有较好的旋转不变性。
特征匹配部分
特征匹配是视觉SLAM中极为关键的一步,宽泛的说,特征匹配解决了SLAM中的数据关联问题,即确定当前看到的路标与之前看到的路标之间的对应关系。
然而,由于图像特征的局部特性,误匹配的情况广泛存在,而且长期都没有得到有效解决,这一问题目前已成为视觉SLAM中制约性能提升的一大瓶颈。出现这一问题的部分原因是场景中经常存在大量的重复文理,使得特征描述非常相似,在这种情况下,仅利用局部特征来解决误匹配是非常困难的。
下面,先来看一下如何进行匹配。
考虑两个时刻的图像。如果在图像中提取到特征点,在图像中提取到,如何来寻找这两个集合元素之间的对应关系呢?
最简单的特征匹配方法就是暴力匹配:即对每一个特征点与所有的测量描述子的距离,然后排序,取最近的一个作为匹配点。描述子距离表示了两个特征之间的相似程度。
不过在实际运用中还可以取不同的距离度量范数。什么意思呢?
- 对于浮点类型的描述子,使用欧式距离进行度量。
- 对于二进制类型的描述子(比如说BRIEF这样的),我们往往使用汉明距离进行度量---两个二进制串之间的汉明距离,指的是其不同位数的个数。
但是,当特征点数量很大时,暴力匹配法的运算量将会变的非常大,这不符合我们在SLAM中的实时性需求。此时,快速近似最相邻算法更加适合于匹配点数量极多的情况。这些匹配算法理论已经成熟,并且已经集成到了OpenCV中,所以这里就不描述它的实现细节了。
实践部分:特征提取与匹配
使用OpenCV进行图像特征提取和匹配需要用到feature2d模块,feature2d模块包含了很多图像特征提取、匹配的相关算法,是图像模式识别重要的辅助库。
上代码:
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/features2d/features2d.hpp>
using namespace std;
using namespace cv;
int main(int argc, char **argv)
{
if(argc != 3)
{
cout << "usage: feature_extraction img1 img2" << endl;
return 1;
}
//读取图像
Mat img_1 = imread(argv[1], CV_LOAD_IMAGE_COLOR);
Mat img_2 = imread(argv[2], CV_LOAD_IMAGE_COLOR); //CV_LOAD_IMAGE_COLOR表示将图像文件以彩色图的形式加载进来
//初始化
std::vector<KeyPoint> keypoints_1, keypoints_2;
Mat descriptors_1, descriptors_2;
Ptr<ORB> orb = ORB::create(500);
//第一步:检测Oriented FAST角点位置
orb->detect(img_1, keypoints_1);
orb->detect(img_2, keypoints_2);
//第二步:根据角点位置计算BRIEF描述子
orb->compute(img_1, keypoints_1, descriptors_1);
orb->compute(img_2, keypoints_2, descriptors_2);
Mat outimg1;
drawKeypoints(img_1, keypoints_1, outimg1, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
imshow("ORB特征点", outimg1);
//第三步:对两幅图像中的BRIEF描述子进行匹配,使用Hamming距离
vector<DMatch> matches; //DMatch中保存特征匹配的结果,详细见https://blog.csdn.net/Quincuntial/article/details/50114773
BFMatcher matcher(NORM_HAMMING);
//BFMatcher是一个蛮力匹配器,首先在第一幅图像中选取一个关键点后依次与第二幅图像的每个关键字进行描述子距离测试,最后返回距离最近的关键点
//对于BF匹配器,使用cv2::BFMatcher()来创建一个BFMatcher对象,它有两个可选参数,对于使用二进制描述子的ORBB BRIEF,使用汉明距离cv2.NORM_HAMMING
matcher.match(descriptors_1, descriptors_2, matches);
//第四步:进行匹配点对 筛选
double min_dist = 10000, max_dist = 0;
//找出所有匹配之间的最小距离和最大距离,即最相似的和最不相似的两组点之间的距离
for(int i = 0; i < descriptors_1.rows; i++)
{
double dist = matches[i].distance;
if(dist < min_dist)
{
min_dist = dist;
}
if(dist > max_dist)
{
max_dist = dist;
}
}
cout << "Max dist = " << max_dist << endl;
cout << "Min dist = " << min_dist << endl;
//当描述子之间的距离大于两倍的最小距离时,就认为匹配有误
//有的时候最小距离会非常小,设置一个经验值作为下限
std::vector<DMatch> good_matches; //good_matches保证正确的匹配结果
for(int i = 0; i < descriptors_1.rows; i++)
{
if(matches[i].distance <= max(2*min_dist, 30.0)) //如果最小距离太小了,就取30.0作为最小距离
{
good_matches.push_back(matches[i]);
}
}
//第5步:绘制匹配结果
Mat img_match;
Mat img_goodmatch;
drawMatches(img_1, keypoints_1, img_2, keypoints_2, matches, img_match);
drawMatches(img_1, keypoints_1, img_2, keypoints_2, good_matches, img_goodmatch);
imshow("所有匹配点对", img_match);
imshow("优化后匹配点对", img_goodmatch);
waitKey(0);
return 0;
}
注意:要将原图片放在跟可执行文件的同级目录下才能正确执行。不然的话会报下列错误:
运行结果:
特征的提取和匹配就先到这里了。