一、引入SIFT算法
上一篇文章我们重温学习了Harris角点检测算法的基本原理,但在实际生产使用Harris检测角点的时候,会发现一个问题,就是用于检测的输入图像的尺寸大小会直接影响到Harris的检测结果。这是为什么呢?主要是Harris角点检测是基于像素梯度去分析检测,输入图像的空间变小,像素间的内容被放大,进而影响到图像梯度,最终导致检测结果就会失败。
如上图所示, 原本一个角点的图像像素,对其进行局部放大后变成由多个像素组成。现在对其进行Harris角点检测,结果会变成了边缘。这就是Harris角点检测存在的一个缺陷,那如何处理呢?
由此引出当今广泛使用的特征检测算法: SIFT(Scale-Invariant Feature Transform)尺度不变特征变换。SIFT是一种广泛使用的计算机视觉算法,用于图像特征提取和描述。SIFT可以检测和描述局部特征,使其对旋转、缩放等操作具有较强的鲁棒性。该方法于1999年由加拿大教授David G.Lowe提出,2004年加以优化后并申请了专利,专利在2020年到期,现在在各大图像处理库中都可以免费使用了。
SIFT优缺点
SIFT算法不仅只有尺度不变性,当图像旋转,改变图像亮度,移动拍摄位置时,仍可得到较好的检测效果,它可以从原理上解决Harris角点检测受空间尺度影响这一缺陷。其优点总结如下:
-
SIFT特征是图像的局部特征,其对旋转、尺度缩放、亮度变化保持不变性,对视角变化、仿射变换、噪声也保持一定程度的稳定性;
-
独特性高,易区分,信息量丰富,适用于在海量特征数据库中进行快速、准确的匹配;
-
多量性,即使是单个物体或者少数几个物体也可以产生大量的SIFT特征向量;
-
可扩展性,可以很方便的与其他形式的特征向量进行联合;
优点很多,但缺点也是很明显的:
-
实时性不高,因为要对输入图像进行多个尺度的下采样和插值等操作;
-
对边缘光滑的目标无法准确提取特征(比如边缘平滑的图像,检测出的特征点过少,对圆更是无能为力)
二、尺度空间和SIFT金字塔
在介绍SIFT的算法原理之前,首先我们要先搞清什么是尺度空间和SIFT金字塔。
尺度空间和图像金字塔
尺度空间是图像在不同尺度(距离)下的连续表示。其中最常见的是使用高斯核对图像进行卷积。高斯滤波可以平滑图像,从而消除图像中的细微细节。随着高斯滤波核尺度的增加,图像变得越来越平滑,直到只剩下最基本的特征。
图像金字塔是尺度空间的一种具体实现方式。它是由一系列不同分辨率的图像组成,这些图像以金字塔的形状排列如下图所示。(有点似OpenGL的mipmap图层的概念)其中根据每层构成图像方式的不同,分成各种不同的图像金字塔。
其中我转载的这篇文章 【转载】OpenCV图像金字塔,能很好的说明什么是图像金字塔,特别是最的总结:
其实图像的尺度空间可以理解为 模拟人眼远近视觉效果的一种方式。这一理论主要基于人眼在观察不同距离的物体时,对物体细节和整体轮廓的感知能力会发生变化。具体来说,图像的尺度空间是指图像经过几个不同高斯核后形成的模糊图片的集合,这些图片模拟了人眼在不同距离观察物体时的视觉效果。
- 尺度:在图像处理中,尺度指的是图像内容的粗细程度,用于模拟观察者距离物体的远近程度。当观察者距离物体较远时,只能看到物体的大概轮廓;而当距离较近时,则能更清晰地看到物体的细节,如纹理、表面的粗糙度等。
- 尺度空间:因此,图像的尺度空间就是一幅图像在不同尺度(即不同模糊程度)下的表示集合。这些表示通过应用不同参数的高斯核进行高斯滤波得到,从而模拟了人眼在不同距离下对物体的感知。
反正记住一点,尺度不是指图像分辨率尺寸,是模拟人眼远近观察的空间距离。或者再用一个更容易理解的举例,就是模拟人类不同的近视眼度数!
在前面我们就解析到Harris角点检测存在的问题就是:在不同的尺度空间不能使用相同的窗口检测极值点。对小的角点要用小的窗口,对大的角点只能使用大的窗口。为了解决这个问题我们需要不同的尺度空间滤波器进行操作,其中SIFT 金字塔 就是接下来要重点描述的内容。
SIFT金字塔
SIFT金字塔,即尺度不变特征变换金字塔,是SIFT算法中的一个重要组成部分。它主要用于在不同尺度空间上查找图像中的关键点(特征点),并计算出这些关键点的方向。SIFT金字塔的构建过程主要涉及高斯金字塔(Gaussian Pyramid)的构建,以及在此基础上构建的差分金字塔(Difference of Gaussian,DoG金字塔)。
SIFT金字塔的构建其实就是高斯金字塔+DoG金字塔的融合,其构建过程如下:
- 初始化:将原始图像作为高斯金字塔的第一组(Octave)的第一层(Interval)。
- 高斯滤波迭代:对第一层的图像进行高斯滤波,滤波过程中,高斯核的σ值可以取固定值,如SIFT算法中常用的σ=1.6。得到第二层图像后,将σ乘以一个比例系数k(如k=√2),得到新的σ值,用于对上一层图像进行高斯滤波,得到下一层图像。重复此过程(通常执行4~5次操作),直到生成N层图像。在同一组内,每层图像的尺寸相同,但σ值逐渐增大,图像逐渐模糊。
- 降采样升组:将第一组的倒数第三层图像进行降采样(通常是行列各减半)得到第二组的第一层图像。然后开始对第二组进行步骤2的高斯迭代。第二组的图像尺寸是第一组的一半。
- 同组进行DoG:对于高斯金字塔的每一组,将相邻两层图像进行相减操作(下一层减上一层),得到差分图像。即每组(Octave)有N层高斯图像,有N-1层DoG图像。
为了让尺度体现其连续性,在高斯金字塔简单降采样的基础上加上了不同卷积核系数的高斯滤波,将图像金字塔每层的一张图像使用不同参数做高斯模糊,使得金字塔的每层含有多张高斯模糊图像,将金字塔每层多张图像合称为一组(Octave),金字塔的每一层相当于每一组图像,每组含有多层Interval高斯图像,Interval-1的DoG高斯差分图像。我们可以通过高斯差分图像看出图像上的像素值变化情况。(如果没有变化,也就没有特征。)DoG图像可以捕捉图像的局部特征,可以用于目标检测和边缘检测等任务。
到此,我们就构建好SIFT金字塔,后续Sift特征点的提取都是在SIFT金字塔上进行的。
三、SIFT算法原理
SIFT算法的实质主要涉及四个步骤,逐一把Sift搞清楚。
- 尺度空间的极值(初步)检测
- 关键点(极值点)的精确定位
- 关键点(极值点)的主方向匹配
- 关键点的特征描述
3.1 尺度空间的极值检测
在构建 Sift的DoG金字塔 之后,就可以在不同的尺度空间和 2D 平面中搜索局部最大值了。对于图像中的一个像素点而言,它需要与自己周围的 8 邻域,以及同级的尺度空间中上下两层相邻的 2x9 个领域点相比。如果是局部最大值,它就可能是一个关键点。基本上来说关键点是图像在相应尺度空间中的最好代表。如下图所示:
3.2 关键点的精确定位
以上方法检测到的极值点是离散空间的极值点,并不是精确的极值点位置,同时DoG对边界非常敏感(因为DoG算法会产生较强的边缘响应)所以我们必须要去除因为是边界而产生的极值点,以增强匹配稳定性、提高抗噪声能力。我们可以通过拟合三维二次函数来精确确定关键点的位置和尺度,去除低对比度的关键点和边界关键点都会被去除掉,剩下的就是我们感兴趣的关键点了。
- 关键点的精确定位
利用已知的离散空间点插值得到的连续空间点的方法叫做子像素插值/亚像素插值(Sub-pixel Interpolation)。是利用尺度空间的泰勒级数展开(对是的又是Taylor展开)来获得极值的准确位置。
其一般的数学表达公式为:
其中,。求导并让方程等于零,得出拟合的位置点:
把它再代入回公式1中,即可得到其拟合的极值点位置的(灰度)值表达式:
看到公式可能会有点奇怪,因为三个公式是可以俄罗斯套娃式的一直迭代下去。这时候就需要一个结束迭代的条件。当拟合的公式在任一维度上的偏移量大于0.5时(即x或y偏移大于下一个离散点的一半),意味着插值中心已经偏移到它的邻近点上,所以必须改变当前关键点的展开位置。
或者能在新的位置上反复插值直到收敛,或者还可以设定一个迭代次数,在超出图像边界的范围,此时这样的点应该删除,算法作者就定义了5次的迭代次数。
另外,D(x)拟合出来的的值过小,也是容易易受噪声的干扰而变得不稳定,所以将D(x)小于某个经验值(作者论文中使用0.03)的极值点删除。
最后,拟合计算技术之后将得到特征点的精确位置(原位置加上拟合的偏移量)以及尺度()。
- 去除边缘响应
DoG 算法对边界非常敏感,所以我们必须要把边界去除。前面讲Harris 算法除了可以用于角点检测之外其实还可以用于检测边界的。作者就是使用了同样的思路。作者使用 2x2 的 Hessian 矩阵计算主曲率。从 Harris 角点检测的算法中,我们知道当一个特征值远远大于另外一个特征值时检测到的是边界。
还记得Harris算法最终的检测表达式吗?𝜆1和𝜆2 就是H矩阵的特征值。
其中R的取值代表不同的检测结果。
- 如果两个值都大,且相差不大,则“E函数水平切片椭圆”趋近于圆,区域梯度在两个垂直的方向都有较强集中度,所以是角点区域。而对于其R值,则是要大于0且绝对值较大的时候,我们判断为角点区域。
- 如果其中一个值远大于另一个值,则,则“E函数水平切片椭圆”趋近于线,区域梯度只在一个方向上有较强集中度,所以是边缘区域。而对于其R值,则是要小于0且绝对值较大的时候,我们判断为边缘区域。
- 如果两个值都很小,则“E函数水平切片椭圆”趋近于点,区域梯度信息在两个方向都较弱,所以是像素值平坦区域。而对于R值,则是要接近于0的小数且绝对值较小的时候,我们判断为平坦区域。
Sift算法的作者Lowe沿用Harris这部分的理论,即一个特征值远远大于另外一个特征值时检测到的是边界。Sift算法论文中建议边界阈值为 R=10。R大于此阈值就会被剔除。
并且Lowe还确认 D的主曲率和H的特征值成正比。该值在两特征值相等时达最小。
Sift算法论文中建议阈值T为1.2,即时保留关键点,反之认为角点特征对比对低,要被剔除。
3.3 关键点的主方向匹配
说完关键点的精确定位与筛选之后,接下来就是第三步关键点的方向分配,这一步需要利用图像的局部特征为给每一个关键点分配一个基准方向,后面所有的对输入图像数据的操作,都相当于对关键点的方向、尺度和位置进行变换,使得特征的描述符具有旋转不变性。
至于如何找到关键点的方向,主要是分成如下三个小步骤:
1、确定极值点的领域范围。一般采用二维高斯模型,以关键点为中心,半径为3σ(σ是关键点的尺度)的圆形区域,其中σ乘以1.5是更常见的做法,以更好地覆盖关键点的局部结构。
2、计算该邻域内每个像素点的梯度幅值和梯度方向。梯度幅值和方向的计算公式如下,其中,L(x,y)是图像在点(x,y)处的灰度值,通过高斯核与图像卷积得到。
3、计算出梯度值和方向之后进行直方图统计。将梯度方向的范围(0°~360°)划分为多个方向区间(bin),常用的划分方式有每10°一个bin(共36个bin)或每45°一个bin(共8个bin)。但SIFT算法中常采用每10°一个bin的方式。对于邻域内的每个像素点,根据其梯度方向将其梯度幅值累加到对应的bin中。同时,为了提高稳定性,对每个像素点的梯度幅值进行高斯加权,加权系数随着像素点到关键点的距离增加而减小。
4、最后选择直方图峰值确认主方向。在构建的梯度直方图中,找到峰值对应的方向作为关键点的主方向。这个峰值代表了邻域内图像梯度的主方向,即该关键点的主方向。
辅方向检测(可选):如果梯度直方图中存在另一个峰值,其能量达到或超过主峰值的80%,则将该方向作为关键点的辅方向。这样可以增强匹配的鲁棒性。
至此,将检测出的含有位置、尺度和主方向的SIFT关键点。
3.4 关键点的特征描述
上述过程,只是找到关键点并确定了主方向,但SIFT算法核心用途在于图像的匹配,我们需要对关键点进行数学层面的特征描述,也就是算法核心的最后一个步骤,构建关键点的特征描述。
1、计算特征描述的区域范围:特征描述子与特征点所在的尺度有关,因此,特征描述符的求取应在特征点对应的高斯图像上进行。将关键点附近的邻域划分为d*d(Lowe建议d=4)个子区域,区域的大小与关键点主方向分配时相同,即每个区域都有3σ(σ是关键点的尺度)个子像素,考虑到实际计算时,需要采用双线性插值,所需图像窗口边长为3σ · (d+1)。
2、将坐标系旋转到关键点主方向:考虑到旋转因素,将坐标轴旋转对齐到关键点主方向,是为了确保旋转的不变性,如下图所示。所以实际计算所需的图像区域半径为:radius = 3σ · (d+1) · √2 / 2,计算结果向上取整。
3、插值计算每个子区域的八个方向的特征描述:如下图所示,将区域划分为4x4的子区域后,对每一区域内进行 关键点的主方向匹配 一样的操作,然后进行8个bin的直方图统计,获得每个梯度方向的幅值,此时共有4x4x8=128维的描述向量。
4、描述符的标准化:为了进一步提高描述符的鲁棒性,通常会对描述符进行归一化处理。这有助于减少光照变化对描述符的影响,并使得描述符在不同的图像之间更加具有可比性。
得到的描述子向量:
归一化后的特征向量:
则归一化后的特征描述向量为:
3.5 关键点特征的匹配
3.1~3.4 其实已经完整阐述了SIFT特征算法的原理,3.5这一步其实是SIFT的应用层面了,就是Sift特征算法如何对两幅图像做识别匹配?这里简单介绍一下其原理。
对模板图(参考图、Reference Image)和目标图(观测图, Observation Image)分别建立关键点描述子的集合。目标的识别就是通过两集合之间的关键点描述子的比对来完成。具有128维的关键点描述子的相似性度量采用的是二维的欧式距离。
模板图中关键点描述子:
实时图中关键点描述子:
任意两描述子相似性度量:
但一幅图可能有不少的关键点特征描述,可以采用穷举法来完成所有关键点的匹配,但是这样耗费的时间实在太多了,一般都采用kd树的数据结构来完成搜索。搜索的内容是以目标图像的关键点为基准,搜索与目标图像的特征点最邻近的原图像特征点和次邻近的原图像特征点。
四、OpenCV的SIFT
现在让我们来看看 OpenCV 中关于 SIFT 的函数吧。让我们从关键点检测和绘制开始吧。首先我们要创建对象。我们可以使用不同的参数,这并不是必须的,关于参数的解释可以查看文档。
int main()
{
Mat src = imread("F:\\other\\learncv\\sailuo1.jpg");
if (src.empty()) {
std::cout << "could not load image ... " << std::endl;
return -1;
}
cv::resize(src, src, Size(src.cols/10, src.rows/10));
//imshow("Reference", src);
// 初始化Sift检测器
int numFeature = 200;
Ptr<SIFT> detector = SIFT::create(numFeature);
// 对输入图像进行Sift关键点检测
std::vector<KeyPoint> keypoints;
Mat kpDescriptors;
detector->detect(src, keypoints, Mat());
detector->compute(src, keypoints, kpDescriptors);
// 检测关键点计算描述子。
detector->detectAndCompute(src, Mat(), keypoints, kpDescriptors);
std::cout << "keypoint Descriptors cols&rows : " << kpDescriptors.cols << kpDescriptors.rows << std::endl;
// 绘制关键点
Mat kpMat;
drawKeypoints(src, keypoints, kpMat);
imshow("Sift KeyPoint", kpMat);
cv::waitKey(0);
return 0;
}
函数 sift.detect() 可以在图像中找到关键点。如果你只想在图像中的一个区域搜索的话,也可以使用第三个参数创建一个掩模图像作为入参。返回的关键点是一个带有很多不同属性的特殊结构体,这些属性中包含它的坐标(x,y),有意义的邻域大小,确定其方向的角度、指定关键点强度的响应等。
OpenCV提供了绘制关键点的函数:drawKeyPoints(),绘制效果如下:
得到关键点之后,可以再用 sift.compute() 计算描述子。也可以直接使用 sift.detectAndCompute 一次性的把检测关键点和计算描述子的工作都完成。其中我们可以看看keypoint Descriptors 的输出是一个cv::Mat的格式,也就是一个二维矩阵。其大小是关键点数量*128维,如下图所示,所以此二维矩阵就包含塞罗奥特曼的关键点描述了。
所以现在我们得到了关键点,描述符等。那么赶紧看看如何匹配不同图像中的关键点。先别急,我们将在接下来的章节中学习SIFT进阶优化的版本——SURF。
Reference:
图像特征匹配方法——SIFT算法原理及实现-CSDN博客
主页 - OpenCV Python Tutorials (opencv-python-tutorials.readthedocs.io)