SIFT是用于图像处理领域的一种描述。这种描述具有不变性,可在图像中检测出关键点,是一种局部特征描述子。本文参考David Lowe 老兄2004年发表的论文——Distinctive Image Features from Scale-Invariant Keypoints进行详细的叙述,阐述过程中使用python对关键步骤进行代码实现。
二、摘要
首先介绍David G. Lowe老兄论文的摘要部分,对整篇论文的核心思想进行概括了解,摘要是对论文内容的绝对浓缩,论文每个部分我都将对其内容做一个简单的思维导图,以此来方便大家对该部分内容整体框架的理解。
上面已经提到过2004年的这篇论文是非常经典的一篇论文,尤其是在图像识别领域,即使是在以深度学习为主流研究方向的现在,SIFT算法仍然具有其优势,在一些相关领域仍然被广泛使用,研究人员对SIFT算法的改进依旧如火如荼,截止目前为止,Distinctive Image Features from Scale-Invariant Keypoints已经被引用26,427次,如此多的引用率足以表明该篇论文的重要程度。
1.内容
论文提出一种能够从图片中提取不变性(invariant)特征的方法——SIFT,该方法能够对于不同视角下的物体或场景实现可靠匹配,算法提取到的特征对图像的尺度和旋转具有不变性,即我们平常所说的尺度不变性、旋转不变性。当对图像进行仿射畸变差(affine distortion)、改变三维视角(change in 3D viewpoint)、额外增加噪声(addition of noise)、改变光照强度(change in illumination)等变化,我们提取到的特征都表现出了很好的鲁棒性(robust)。
为了充分展示该算法的优越性,论文也讲述了一种利用提取到的特征进行物体识别的方法,通过识别的结果来说明该算法的优越性。对个体特征和通过已知目标特征组成的数据库对比,使用邻近算法(nearest-neior)进行特征匹配,然后根据霍夫变换(Hough Transform )识别属于单一目标的聚类,最后通过最小二乘法(least-squares)对参数进行确认。
2.结论
在实时识别的过程中,无论识别物体是杂乱还是被遮挡,该方法都能识别出来,鲁棒性很好。
This approach to recognition can robustly identify objects amongclutter and occlusion while achieving near real-time performance
三、 引言
1.研究背景
本文的算法主要解决的就是图像处理所面临的诸多问题,因此研究背景就不言而喻,图像匹配是计算机视觉领域诸多问题中的一个基础性方面,例如,物体或场景识(object or scence recognition)、解决多图片3D构建问题(solve for 3D structure from mutiple images)、立体匹配(stereo correspondence)、运动跟踪(motion tracking)等。
这一部分是概要是整篇文章的核心,论文后边都将根据这一部分陈述的内容进行详细的描述,并且整个代码的实现过程要是根据这部分的理论内容进行编写,引言部分主要是简单的概述一下,详细内容将会在后边展开。
①Scale-space extrema detection--尺度空间极值检测 :
应用高斯差分金字塔(difference-of-Gaussian function)识别出具有尺度和方向不变性的潜在感兴趣点。
use a difference-of-Gaussian function to indentify potential interest points that invariant to scale and orientation
②Keypoint localization--关键点定位:
在每个候选的位置上,通过一个拟合精细的模型来确定位置和尺度,关键点的选择依据于它们的稳定性
At each candidate location, a detailed model is fit to determine location and scale. Keypoints are selected based on measures of their stability
③Orientation assignment--方位定向:
基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向。所有后面的对图像数据的操作都相对于关键点的方向、尺度和位置进行变换,从而提供对于这些变换的不变性。
One or more orientations are assigned to each keypoint location based on local image gradient directions.All future operations are performed on image data that has been transformed relative to the assigned orientation, scale, and location for each feature, thereby providing invariance to these transformations.
④Keypoint descriptor--关键点描述符:
在每个关键点周围的邻域内,在选定的尺度上测量图像局部的梯度,这些梯度被变换成为一种表示,这种表示允许比较大的局部形状的变形和光照变化。
The local image gradients are measured at the selected scale in the region around each keypoint. These are transformed into a representation that allows for significant levels of local shape distortion and change in illumination
论文这一部分主要介绍的是一些相关领域的研究,主要是在图像匹配领域一些研究,主要集中在2004年之前,大部分是在2000年前后,可以看的出来,发明一个适应相关领域应用的优秀算法是需要经过大量的研究工作,这一部分笔者就不详细介绍了,思维导图中简单的写了一部分,1999年Lowe提出的方法就是目前的我们正在详细讲解论文的前身,也是在此基础上进行的整理和加深。Lowe在论文中写道:
This current paper provides a more in-depth development and analysis of this earlier work, while also presenting a number of improvements in stability and feature invariance.
如果你们想了解的话可以根据参考文献找到相应的论文进行阅读,因为本人水平也有限,就不对文中提到的相应研究做详细的叙述了。
其中, 是基准尺度
简单写一下推导过程:
当o=0,s=0,则
当o=0,s=1,则
.......
因此,第二个octave中的尺度序列为:
对于高斯金字塔的初始尺度做一下简单的说明,当图像通过相机拍摄时,相机的镜头已经对图像进行了一次初始的模糊,所以根据高斯模糊的性质有:
其中
第0层的尺度,
是被相机镜头模糊后的尺度。
这也就是为什么上图中的
=1.52的原因,就是考虑到相机已经对图像进行σ=0.5的模糊,当然Lowe老兄建议是
为1.6,至于为什么是1.6,其实是他通过实验得出的结论,下文也会对该参数进行介绍,因此最终我们实际得到的
是1.52。
为了尽可能多的保留原始图像信息,一般需要对原始图像进行扩大两倍采样,即升采样,从而生成一组采样图:octave_1,此组采样图的第一层的模糊参数为:
但是升采样不是必须的,完全可以直接用原图来构建高斯金字塔,正如上边图片所示的高斯金字塔那样进行操作。
我简单说一下什么是升采样:升采样其实是一种插值,就是在一幅图像中利用相关插值运算得到一幅大的图像,如果是比例因子为2的升采样,就是对每个相邻像素点插值出一个像素,于是n*n的图像变成2n X 2n 的图像。
刚才这一部分的说明,论文是在构建完高斯差分金字塔,完成局部极值点检测后阐述的,我在这复述一下原文的解释。
To make full use of the input, the image can be expanded to create more sample points than were present in the original.
We double the size of the imput image using liner interpolation prior to building the first level of the pyramid.
while the equivalent operation could effectively have been performed by using sets of subpixel-offset filters on the original imgae,the image doubling leads to a more efficient implementation.
We assume that the original image has a blur of at least σ=0.5, and that therefore the doubled image has σ=1.0
上边介绍了一下高斯金字塔内部的一些信息,而构建高斯差分金字塔所需要的公式如下:
上述公式中
函数表示的是一个图像的尺度空间(the scale space of an image),
表示一张输入的图片(an input image),
代表尺度变化的高斯函数(a variable-scale Gaussian),
表示高斯差分尺度空间。上边的这三个公式其实就是我们这一块构建高斯差分金字塔所使用的公式,很重要的,我会在下边进行一些阐述,并进行相关的推导来便于理解。
尺度空间使用高斯金字塔表示,Tony Lindeberg指出尺度规范化的LoG(Laplacion of Gaussian)算子具有真正的尺度不变性,Lowe使用的高斯差分金字塔近似LoG算子,用来在尺度空间检测稳定的关键点。
1994年Lindeberg研究发现高斯差分函数(difference-of-Gaussian function)和尺度归一化的拉普拉斯高斯函数(scale-normalized Laplacian of Gaussian)——
非常相似,2002年Mikolajczyk发现,相比于其他的函数,如梯度(the gradient)、黑塞矩阵(Hessian)、Harris corner function等,尺度归一化的拉普拉斯高斯函数
的极大值和极小值能够产生最稳定的图像特征(the most stable image features)。
对于为什么能用高斯差分金字塔来近似LoG算子,D以及
和之间的关系,论文中给出了简单的介绍。
对于为什么能够给出公式(4)的关系,我来给推一下,首先是左边,G是高斯函数,其公式形式我们已经知道,见公式(3),上边介绍高斯滤波的时候也介绍过。对高斯函数求偏导:
然后我们计算一下LoG算子,其中关于
算子,前面已经介绍了。
所以我们能够得到:
由导数定义:
因此得到结果,即利用差分来近似微分:
得到:
因此上述式子就解释了为什么能够用高斯差分函数来近似拉普拉斯高斯函数,当k=2时,就已经是拉普拉斯高斯函数了,公式中(k-1)是一个常量,因此并不影响极值点的位置。如下图所示:
红色曲线表示的是高斯差分算子,而蓝色曲线表示的是高斯拉普拉斯算子,Lowe使用更高效的高斯差分算子代替拉普拉斯算子进行极值检测,如公式3所示。
高斯差分金字塔的构建如下图所示(An efficient appraoch to construction of D(x,y,) is shown in Fig. 1.)
论文中所提到的内容及其详细的解释我们刚刚已经进行了解释,相信读者现在再来读原文应该会明白文中的意思的。至于高斯差分金字塔的构建就比较简单了,他是在高斯金字塔的基础上通过相邻两层相减得到的,这也就是为什么叫高斯差分金字塔的原因,以及我们上文提到高斯金字塔需要s+3幅图像。我把原文的内容在这里给大家放一下,供大家参考。
The initial image is incrementally convolved with Gaussians to produce images separated by a constant factor k in scale space,shown stacked in the left column
We choose to divide each octave of sacle space into an integer number,s,of intervals,so k= 2^(1/s). We must produce s+3 images in the stack of blurred images for each octave,so that final extrema detection covers a complete octave.
总结一下高斯金字塔的构建分为两部分:
1)对图像做不同尺度的高斯模糊
高斯模糊的过程上边我已经介绍了,使用高斯核对图像进行卷积操作,关于每层内部的取值及层间的的取值关系也已经介绍完成。下边我用代码给大家讲一下这块的过程,高斯核产生的函数上文已经定义过了,下边定义一下卷积:
defconvolve(kernel, img, padding, strides): ''' :param kernel: 输入的核函数 :param img: 输入的图片 :param padding: 需要填充的位置 :param strides: 高斯核移动的步长 :return: 返回卷积的结果 ''' result =None kernel_size = kernel.shape img_size = img.shape iflen(img_size) ==3: # 三通道图片就对每个通道分别卷积 channel = [] for i inrange(img_size[-1]): #对每个通道进行填充 pad_img = np.pad(img[:,:,i],((padding[0],padding[1]),(padding[2],padding[3])),'constant') #进行padding操作 temp = [] for j inrange (0,img_size[0],strides[1]): #进行卷积操作 高度 temp.append([]) for k inrange(0,img_size[1], strides[0]): #宽度 val = (kernel * pad_img[j * strides[1]:j * strides[1] + kernel_size[0], k * strides[0]:k * strides[0] + kernel_size[1]]).sum() #依次进行卷积操作 temp[-1].append(val) channel.append(np.array(temp)) channel =tuple(channel) #元组 result = np.dstack(channel) #将列表中的数组沿深度方向进行拼接 eliflen(img_size) ==2 : channel = [] pad_img = np.pad(img, ((padding[0], padding[1]), (padding[2], padding[3])), 'constant') # pad是填充函数 边界处卷积需要对边界外根据高斯核大小填0 for j inrange (0,img_size[0],strides[1]): channel.append([]) for k inrange(0,img_size[1],strides[0]): val = (kernel * pad_img[j * strides[1]:j * strides[1] + kernel_size[0], k * strides[0]:k * strides[0] + kernel_size[1]]).sum() # 卷积的定义 相当于用高斯核做加权和 channel[-1].append(val) result = np.array(channel) return result
上边的代码还进行了padding操作,为了保证对边缘效果,填充操作就是我们在卷积神经网络中使用的操作,包括步长等变量。
2)对图像做降采样(隔点采样)
上边的过程只是进行高斯处理的过程,因为我们需要得到金子塔形状,图像的金字塔模型是指将原始图像不断降阶采样,得到一系列大小不一的图像,由大到小,从下到上构成的塔状模型。原图像为金字塔的第一组(Octative),每次降采样所得到的新图像为金字塔的一组(每组一张图像),意思就是我们一组的图像是一样的,只是模糊处理不一样,但是每组使用的是同一张图片,金字塔组数的计算公式如下:
其中M,N为原图像的大小,t为塔顶图像的最小维数的对数值。
为了让尺度体现其连续性,高斯金字塔在简单降采样的基础上加上了高斯滤波,将图像金字塔每组含有多张高斯模糊图像(interval image),另外,在进行降采样时,高斯金字塔上一组图像的初始图像(底层图像)是由前一组图像的倒数第三张图像隔点采样得到。
Adjacent image scales are subtracted to produce the difference-of-Gaussian images shown on the right.
Once a complete octave has been processed, we resample the Gaussian image that has twice the initial value of sigma by taking every second pixel in each row and column.
其中第二段提到一个将采样的过程,对于一幅图像而言,降采样就是每隔几行、几列进行取一点,组成一个新的图像,而论文中提到的是隔点取点(take every second pixel in each row and column),最后会将一个
的图像变为
的图像。
下采样的过程用代码实现是很简单的:
def getDoG(img,n,sigma0,S= None,O=None): ''' :param img: 输入图像 :param n: 有几层用于提出特征 :param sigma0: 输入的sigma :param S: 金字塔每层有几张gauss滤波后的图像 :param O: 金字塔有几层 :return: 返回差分高斯金字塔和高斯金字塔 ''' if S == None: S = n + 3 # 至少有4张 (第一张和最后一张高斯金字塔无法提取特征,差分以后的第一张和最后一张也无法提取特征) if O == None: O = int(np.log2(min(img.shape[0], img.shape[1]))) - 3 # 计算最大可以计算多少层 O=log2(min(img长,img宽))-3 k = 2 ** (1.0/n) sigma = [[(k ** s) * sigma0 * (1 << o) for s in range(S)] for o in range(O)] # 每一层 sigma按照 k^1/s * sigama0 排列 下一层的sigma都要比上一层sigma大两倍 sample = [undersampling(img, 1 << o) for o in range(O)] # 降采样取图片作为该层的输入 Guass_Pyramid = [] for i in range(O): Guass_Pyramid.append([]) #声明二维空数组 for j in range(S): dim = int (6*sigma[i][j]+1) # 通常,图像处理只需要计算(6*sigma+1)*(6*sigma+1)的矩阵就可以保证相关像素影像 if dim % 2 == 0: #防止高斯核不是奇数 dim += 1 dim += 1 Guass_Pyramid[-1].append(convolve(GuassianKernel(sigma[i][j], dim), sample[i], [dim // 2, dim // 2, dim // 2, dim // 2],[1, 1])) # 在第i层添加第j张 经过高斯卷积的 该图片四周扩展 5//2=2 用于高斯卷积 DoG_Pyramid = [[Guass_Pyramid[o][s + 1] - Guass_Pyramid[o][s] for s in range(S - 1)] for o in range(O)] #每一层中 上一张减去下一张得到高斯核 return DoG_Pyramid,Guass_Pyramid,O #返回高斯金字塔和高斯差分金字塔
上边代码中使用的参数我们之前已经讲过,构建高斯差分金字塔的核心代码就是:
<DoG_Pyramid = [[Guass_Pyramid[o][s + 1] - Guass_Pyramid[o][s] for s in range(S - 1)]
至此为止,整个高斯差分金子塔的构建已经讲述完毕。
7.局部极值点检测
关键点是由DoG空间的局部极值点组成的,关键点的初步探查是通过同一组内各DoG相邻两层图像之间比较完成的。每一个像素点要和它所有的相邻点比较,看其是否比它的图像域和尺度域的相邻点大或者小。因此检测点所需要比较的点共26个点,分别为同尺度的8邻域内的点,和上下相邻尺度对应的9*2个点。高斯金字塔每组有(S+3)层,这个我们之前解释过,因此,高斯差分金字塔每组有(S+2)层。极值点的检测只在同一组中从第2层开始至倒数第2层为止的相邻层进行26个点DoG值的比较中寻找极大值或极小值。
In order to detect the local maxima and minima of D(x,y,sigma),each sample point is compared to its eight neiors in the current image and nine neiors in the scale above and below. It is selected only if it is larger than all of these neiors or smaller than all of them.
for i in range(img.shape[0]): for j in range(img.shape[1]): val = img[i][j] eight_neiborhood_prev = img_prev[max(0, i - 1):min(i + 2, img_prev.shape[0]), max(0, j - 1):min(j + 2, img_prev.shape[1])] eight_neiborhood = img_prev[max(0, i - 1):min(i + 2, img_prev.shape[0]),max(0, j - 1):min(j + 2, img_prev.shape[1])] eight_neiborhood_next = img_prev[max(0, i - 1):min(i + 2, img_prev.shape[0]),max(0, j - 1):min(j + 2, img_prev.shape[1])] # 阈值化,在高斯差分金字塔中找极值 # 如果某点大于阈值,并且,比周围8个点、上下18个点共26个点都大或者都小,则认为是关键点 if np.abs(val) > threshhold and \ ((val > 0 and (val >= eight_neiborhood_prev).all() and (val >= eight_neiborhood).all() and (val >= eight_neiborhood_next).all()) or (val < 0 and (val <= eight_neiborhood_prev).all() and ( val <= eight_neiborhood).all() and (val
我把这部分定义为参数的确定,在原文中,这一部分是通过几个实验进行分析,得出相关结果,最终确定S=3,=1.6。下边我们先来看一下Lowe老兄的实验结果并进行分析。
上边的线显示的是对经过转换后的图像在检测其匹配位置和尺度时所得到关键点的百分比。
下边的线表示正确匹配关键点的数量。
The top line in the first graph of Fig.3 shows the percent of keypoints that are detected at a matching location and scale in the transformed image.
The lower line on this graph shows the number of key points that are correctly matched to a database
通过测试每个组(octave)中不同数量的层(scales)的影响来展示模拟结果,分析上图,我们很容易能够观察到S=3的时候效果最好。
简单说明一下上图中两条线的意义,上图中其实已经有解释了,我将论文中对这两条线的解释说一下。左边图像横坐标为每组中样本层数的数量,纵坐标为重复性的概率,右边图纵坐标为关键点的数目。
Figure3 shows these simulation results used to examine the effect of varying the number of scales per octave at which the image function is smapled prior to extrema detection
As this graph shows, the highest repeatability is obtained when sampling 3 scales per octave
刚才的实验确定了参数S,我们还需要确定参数:
论文中是把的值确定为1.6的,虽然通过图像我们观察到,随着的增加,Repeatability还在继续增加,但是就效率而言,增大所带来的成本是不值的。
the results show that the repeatability comtinues to increase with sigma.
However, there is a cost to using a large sigma in terms of efficiency, so we have chosen to use sigma= 1.6,which provides close to optimal repeatability.
DoG, GuassianPyramid,octaves = getDoG(img, n, sigma0) # getDoG:得到高斯金字塔和高斯差分金字塔
六、关键点定位
通过前边的方法检测到极值点是离散空间的极值点,前边也简单阐述过所获得的极值点并不是十分准确,我们需要来精确确定关键点的位置和尺度,同时去除低对比度的关键点和不稳定的边缘相应点(因为DoG算子会产生较强的边缘响应),依次来增强匹配稳定性、提高抗噪声能力。具体的操作流程将会在下面进行阐述,我把原文在进行这一步所叙述文字写一下,供大家进行理解。
Once a keypoint candidate has been found by comparing a pixel to its neiors, the next step is to perform a detailed fit to the nearby data for location, scale, and ratio of principal curvatures. This information allows points to be rejected that have low contrast (and are therefore sensitive to noise) or are poorly localized along an edge.
离散空间的极值点并不是真正的极值点,下图显示了二维函数离散空间得到的极值点与连续空间极值点的差别。利用已知的离散空间点插值得到的连续空间极值点大的方法叫做子像素插值(sub-pixel interpolation)。
Lowe在1999年提出该方法时并没有进行关键点的精确定位,只是简单的定位出样本点所在区域位置和尺度的关键点。2002年的时候,Brown和Lowe提出了一种新的方法(Brown, M. and Lowe, D.G. 2002. Invariant features from interest point groups. In British Machine Vision Conference, Cardiff, Wales, pp. 656–665.),通过拟合三维二次函数来精确确定关键点的位置和尺度,为了提高关键点的稳定性,对对尺度空间DoG函数进行曲线拟合,用泰勒展开公式来进行:
其实在我们用代码的实现的过程中,不会直接用公式10来进行实现,而是对公式10进行下面一系列的推到,直接对结果进行代码构建,但是公式10是我们理解下面的原型模式。
任意一极值点在其处做三元二阶泰勒展开如下:
公式9的矩阵表示就是公式8,D就是f,D(X)就是f(X)
上边公式中的偏导我们的求法是采用有限差分求导法进行的,我简单介绍一下有限差分求导法:
The derivatives are estimated by taking differences of neioring sample points
有限差分法以变量离散取值后对应的函数值来近似微分方程中独立变量的连续值。在有限差分方法中,我们放弃了微分方程中独立变量可以取连续值的特征,而关注独立变量离散取值后对应的函数值。但是从原则上说,这种方法仍然可以达到满意的计算精度。因为方程的连续数值解可以通过减少独立变量离散取值的间隔,或者通过离散点上的函数值插值计算近似得到。
其中f所对应的下标就是图中的数字。
以上就是有限差分求导法的全部过程,读者可以对应相关图片并结合下面代码进行理解。
对求导,并让方程等于零,我们可以得到极值点的偏移量为
The loaction of the extremum, , is determined by taking the derivative of this function with respect to and setting it to zero, giving:
代表相对插值中心的偏移量,当它在任一维度上的偏移量(x,y,)大于0.5时,意味着插值中心已经偏移到它的邻近点上,所以必须改变当前关键点的位置,同时在新的位置上反复插值直到收敛。
H = [[dxx, dxy, dxs], [dxy, dyy, dys], [dxs, dys, dss]] X = np.matmul(np.linalg.pinv(np.array(H)), np.array(dD)) #求公式10的绝对值 xi = -X[2] xr = -X[1] xc = -X[0]
if np.abs(xi) < 0.5 and np.abs(xr) < 0.5 and np.abs(xc) < 0.5: break
If the offset is larger than 0.5 in any dimension, then it means that the extremum lies closer to a different sample point. the sample point is changed and the interpolation performed instead about that point. The final offset is added to the location of its sample point to get the interpolated estimate for the location of the extremum.
将公式10带入公式9,得到对应极值点:
The function value at the extrema, , is useful for rejecting unstable extrema with low contrast. This can be obtained by substitutinng Eqs(10) into (9),giving:
下边是对公式11的复现,其中t就是对公式11后半部分的解释。
t = (np.array(dD)).dot(np.array([xc, xr, xi])) contr = img[x, y] * img_scale + t * 0.5
文中提到,过小的点容易受噪声的干扰而变得不稳定,所以当值小于0.03时,本文认为应该舍弃。但是Rob Hess 在实现代码的是用的不是0.03,而是 T/S ,其中T=0.04
For the experiments in this paper, all extrema with a value of D(\hat x) less than 0.03 were discarded
# 舍去低对比度的点 :|fx| < T/S 为了和sigma的s区分,S用的是n,也就是我们上文提到的octavez中的scale if np.abs(contr) * n < contrastThreshold: return None, x, y, s
下边我给大家展示一下论文经过上述步骤所得到的效果,让大家对SIFT进行特征提取有个视觉上的理解。
a图是原图像,b图是我们进行关键点定位前得到的832个关键点,c图是舍弃小于0.03的的值后所得到的729关键点。d图是需要我们经过下面的介绍后进行操作得到的效果图。
(b) shows the 832 keypoints at all detected maxima adn minima of the difference-of-Gaussian function, while(c) shows the 729 keypoints that remian follwing removal of those with a value of D(\hat x) less than 0.03.
,通过对该问题的优化发现,结果与矩阵A的特征值相关,并且其特征向量及特征值分别为,特征值0.5所对应的特征向量为[-0.7071,0.7071],特征值1.5对应的特征向量为[0.7071,0.7071],我们来观察一下二次型函数以及对应的等高线:
等高线越密集的地方,说明函数值变化较快,而这个函数变化最快的方向是特征向量[0.7071,0.7071]对应的方向,等高线越稀疏的地方,说明函数值变化较慢,而变化较慢的方向既是特征向量[-0.7071,0.7071]所对应的方向,而且,较大特征值所对应的特征向量方向上的函数值变化最快,较小特征值所对应的特征向量方向上的函数值变化最慢。
我们介绍这一块的本意是为了应用hessian矩阵来消除边缘响应,那么边缘到底是什么特征呢?
如上图所示,一个二维平面上的一条直线,图像的特征具体可以描述为:沿着直线方向,亮度变化极小,垂直于直线方向,亮度由暗变亮,再由亮变暗,沿着这个方向,亮度变化很大。可以将边缘图像分布特征与二次型函数进行类比,如上边右边的图,我们可以找到两个方向,一个方向图像梯度变化最慢,另一个方向图像梯度变化最快。因此图像中的边缘特征与二次型函数的图像就能够对应在一起,而上边我们也说了,二阶导数反应的是图像梯度变化的快慢,而hessian矩阵的形式我们也见到了,因此,这也就是为什么能够使用hessian矩阵来对边缘进行检测以及进行边缘响应的消除。
Hessian matrix实际上就是多变量情形下的二阶导数,描述了各个方向上灰度梯度的变化,使用对应点的hessian矩阵得到的特征向量以及对应的特征值,较大特征值对应的特征向量是垂直于直线的,较小特征值对应的特征向量是沿着直线方向的。
我们本文中使用的hessian矩阵以及在实现过程中使用的是二阶矩阵:
The principal curvatures can be computed from a 2*2 Hessian matrix, H, computed at the location and scale of the keypoint:
H特征值和D的主曲率是成比例的(Harris, C. and Stephens, M.1988.Acombined corner and edge detector. In Fourth Alvey Vision Conference, Manchester, UK, pp. 147–151),因此在此基础之上,我们就不用准确来计算特征值,我们转而关心这个比例,假设较大特征值为,较小特征值为,因为已知两个特征值后,我们就可以计算H的迹和行列式了,公式我们以前在线性代数中都学过:
如果行列式为零,则应该将该极值点丢弃,即 Det(H)<0 ,舍去X
In the unlikely event that the determinat is negative, the curvatures have different signs so the point is discared as not being an extremum.
然后我们确定两个特征值之间的关系,假设,其中是一个比例来使较大特征值与较小特征值之间等式成立。然后我们会得到下边的关系:
因此该公式的值仅仅取决于的值,而不是特征值本身,这样对应计算来说是非常有效率的。而且我们通过公式(13)能够知道,该公式的值在两个特征值相等时最小,随着的增大而增大,说明两个特征值的比值越大,即在某一点方向的梯度值越大,而在另一个方向的梯度值越小,而边缘恰恰就是这种情况,我们在前边其实也讲过,所以为了剔除边缘响应点,只需要让该比值小于一定的阈值即可,因为值越大的话,两个方向的梯度变化就很大,肯定是边缘响应点了,所以我们需要检测下式是否成立就可以:
if det <= 0 or tr * tr * edgeThreshold >= (edgeThreshold + 1) * (edgeThreshold + 1) * det: return None,x,y,s #edgeThreshold 为r 值为10
如果成立,则保留该关键点,反之则剔除该关键点。在论文中,的取值设置为10。还记得我们前边介绍关键点提取的效果图吗?还有一幅(d)图片没说,(d)图片所展示的效果就是经过我们这一步实现后的效果。
The Eq depends only on the ratio of the eigenvalues rather than their individual values. The quantity Eq(13) is at minimum when the two eigenvalues are equal and it increases with r.
七、关键点方向分配
基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向。所有后面的对图像数据的操作都相对于关键点的方向、尺度和位置进行变换,从而提供对于这些变换的不变性。
因为这一步是关键点方向分配,所以需要对我们上一步得到的关键点进行处理,围绕该点选择一个窗口,窗口为圆形区域,以该特征点为中心,半径为3作为一个圆域,窗口内各采样点的梯度方向构成一个直方图,根据直方图的峰值确定特征点的主方向,直方图的峰值确定以后,任何大于峰值80%的方向创建一个具有该方向的特征点,这个方向认为是特征点的辅方向,因此,对于多峰值的情况,在同一位置和尺度就会产生多个具有不同方向的关键点,就是将该特征点复制成多份特征点,除了方向不同,都相同。
窗口内各采样点的梯度方向和梯度幅值计算公式如下:
For each image sample, L(x,y), at this scale, the gradient magnitude, m(x,y),and orientation,θ(x,y),is precomputed using pixel differences:
(a)图像梯度方向角θ和幅值m, (b)特征点为中心,
为半径的范围内计算高斯图像的方向角和梯度。
2004年的论文其实对这一块的讲解并不是很细致,其实整篇论文好多细节的东西Lowe老兄也没有讲,可能他觉得没讲的东西是我们都了解过的,但是对于我们来说就不是很友好了。我先来把这位老兄的叙述给大家看一下,中心思想就是这些,但是具体实施起来可能会不一样。
An orientation histogram is formed from the gradient orientations of sample points within a region around the keypoint. The orientation histogram has 36 bins covering the 360 degree range of orientations.
Each sample added to the histogram is weighted by its gradient magnitude and by a Gaussian-weighted circular window with a σ that is 1.5 times that of the scale of the keypoint.
为了构建上面所说的直方图,每个直方图分为36柱,每10°为一柱,我们需要完成特征点邻域内的高斯图像的梯度计算,然后才能使用直方图统计邻域内像素的梯度方向和幅值。计算过程为公式(15)(16)所陈述的。
上边代码所应用的公式是:
公式中的使用1.5σ,是因为论文中说的,刚才已经展示了:"by a Gaussian-weighted circular window with a σ that is 1.5 times "。上边公式的作用就是加权,在计算完梯度幅值后需要对其进行加权来构建直方图,下边也会介绍这一步骤。那么为什么我们需要对每个点梯度幅值要使用高斯权重呢?因为SIFT算法只考虑了尺度和旋转的不变性,并没有考虑仿射不变性。通过对各点梯度幅值进行高斯加权,是特征点附近的梯度幅值有较大的权重,这样可以部分弥补因没有仿射不变性而产生的特征点不稳定的问题。
因为我们已经通过公式(15)(16)完成特征邻域的高斯图像的梯度计算,需要使用直方图统计邻域内像素的梯度方向和幅值。
上边展示的就是梯度方向及其直方图,至于为什么正方形的长为
,是因为我们的尺度采样采用的是3σ原则,而刚才也介绍了需要用1.5σ进行加权,所以邻域窗口半径为
。绿色格点代表邻域范围,黑色箭头指向代表梯度方向,箭头长度代表梯度幅值。
介绍一下右边的直方图,横轴是梯度方向角,纵轴是梯度方向角对应的梯度幅值,但是经过高斯加权后的,也就是上边我单独介绍的代码。梯度方向直方图将0°~360°的范围,每10°分为一个柱状,共分36个柱状。直方图的峰值代表了该特征点处邻域内梯度的主方向,即该特征点的主方向。
下边来详细介绍一下梯度方向直方图的生成过程:
1)生成邻域内给像素的高斯权重。
使用公式(17)
for k in range(length): #BinNum为常值36 bin = int(np.round((BinNum / 360.0) * Ori[k])) if bin >= BinNum: bin -= BinNum if bin < 0: bin += BinNum temphist[bin] += W[k] * Mag[k] #加权
其中,H为由经过平滑后的直方图,角度θ的单位是度。由于角度是循环的,即0°=360°,如果出现H(j),j超出了(0,....15)的范围,那么可以通过圆周循环的方法找到它所对应与区间[0°,360°]之间的值,如
。
下边解释一下上边的拟合公式:
经过抛物线插值后我们用抛物线顶点横坐标作为主方向的角度,此时主方向是一个值,因为三个点就可以确定一条抛物线,因为我们选取了
,然后我们来把公式(19)推导一下:
设设插值抛物线方程为
,其中a,b,c,为抛物线的系数,t为自变量,
,利用我们中学的知识,我们知道
,把t的三个取值带入方程:
所以能够求出a,b,c
所以:
因为上述t的范围是[-1,1],因此相当于在此范围内i=0,所以在整个直方图中的索引号为:
至此为止,公式(19)推导完成,原文对这部分的介绍仅仅有一句话,刚开始看到这句话的时候都没怎么注意,Lowe老兄还真是不写废话。
Finally, a parabola is fit to the 3histogram values closest to each peak to interpolate the peak position for better accuracy
每个特征点除了必须分配一个主方向外,还可能有一个或更多个辅方向,增加辅方向的目的是为了增强图像匹配的鲁棒性。辅方向的定义是,当存在另一个柱体高度大于主方向柱体高度的80%时,则该柱体所代表的方向角度就是该特征点的辅方向。
The highest peak directions in the histogram is detected, and then any other local peak than is within 80% of the highest peak is used to also create a keypoint with the orientation
# SIFT_ORI_PEAK_RATIO =0.8,为了是否大于判断80% mag_thr = omax * SIFT_ORI_PEAK_RATIO for k in range(BinNum): if k > 0: l = k - 1 else: l = BinNum - 1 if k < BinNum - 1: r2 = k + 1 else: r2 = 0 if hist[k] > hist[l] and hist[k] > hist[r2] and hist[k] >= mag_thr: #进行抛物线插值 bin = k + 0.5 * (hist[l]-hist[r2]) /(hist[l] - 2 * hist[k] + hist[r2]) if bin < 0: bin = BinNum + bin else: if bin >= BinNum: bin = bin - BinNum temp = point[:] temp.append((360.0/BinNum) * bin) KeyPoints.append(temp)
通过我们上边步骤,每个特征点被分配了坐标位置、尺度和方向。在图像局部区域内,这些参数可以重复的用来描述局部二维坐标系统,因为这些参数具有不变性。使用这种方法的灵感来源于1997年Edelman提出的方法,下面就把原文写一下,就不展开介绍了,感兴趣的可以看一下原始论文(Edelman, S., Intrator, N., and Poggio, T. 1997. Complex cells and object recognition. Unpublished manuscript:
http://kybele.psych.cornell.edu/∼edelman/archive. html)
Their proposed representation was based upon a model of biological version, in particular of complex neurons in primary visual cortex. These complex neurons respond to a gradient at a particular orientation and spatial frequency, but the location of the gradient on the retina is allowed to shift over a small receptive filed rather than being precisely localized.
下面来计算局部图像区域的描述符,描述符即具有可区分性,又具有对某些变量的不变性,如光亮或三维视角。这些描述子不但包括关键点,也包含关键点周围对其有贡献的像素点,并且描述符应该有较高的独特性,以便于提高特征点正确匹配的概率。SIFT描述子是关键点邻域高斯图像梯度统计结果的一种表示,通过对关键点周围图像区域分块,计算块内梯度直方图,生成具有独特性的向量,这个向量是该区域图像信息的一种抽象,具有唯一性。
上图展示的是关键点描述符的计算,左边是图像梯度图像,右边是关键点描述符。描述符是与特征点所在的尺度有关的,所以描述特征点是需要在该特征点所在的高斯尺度图像上进行的,在高斯尺度图像上,以特征点为中心,将其附近邻域划分为dxd个子区域,论文中取d=4,上图下边的解释也说了,"the experiments in this paper use 4 x 4 descriptors computed from a 16 x 16 sample array".所以上图只是为了让我们理解所画,在真正的实验中,并没有使用2 x 2的描述符,所以计算的样本集合也不是左图中的8x8区域。每个子区域都是一个正方形,正方形的边长为3σ,也就是说正方形的边长有int(3σ)个像素,σ为相对于特征点所在的高斯金字塔的组的基准层图像的尺度,这样讲可能有点抽象,我们来看一张16邻域的图片。
上图中将特征点附近的邻域划分成
个子区域,每个子区域作为一个种子点,每个种子点有8个方向,因为此时的划分范围是45°,这也就是为什么在右图中我们只看到八个方向。理论上来说,每个子区域的矩形边长为
,即一个子区域中包含
个像素,16个子区域的像素个数为
。但是在实际应用中,由于如果邻域中像素的梯度方向为33°时,不能直接把它当作30°来进行处理,应该要把它按照相邻的梯度方向30°和40°的距离进行插值处理,这也就需要我们将描述特征点窗口边长变为
,d就是我们在上边提到过,此时d取的值为4。因此整个区域的像素变为。
为了保证特征点的具有旋转不变性,还需要以特征点为中心,将刚才确定的特征点邻域区域旋转θ(θ就是该特征点的方向)。由于是对正方形进行旋转,为了使旋转后的区域包括整个正方形,应该以从正方形的中心到它的边的最长距离为半径,也就是正方形对角线长度的一半,正方形边长的倍为对角线长度,所以半径为:
4)对所有落入该正方体内部的像素做上述处理,再进行求和运算ΣC,得到D
经过上边的三维直方图的计算,最终我们得到了该特征点的特征矢量。为了去除光照变化的影响,需要对特征矢量进行归一化处理:
则
为归一化后的特征矢量,尽管通过归一化处理可以消除对光照变化的影响,但是由于照相机饱和以及三维物体表面的不同数量不同角度的光照变化所引起的非线性光照变化仍然存在,它能够影响到一些梯度的相对幅值,但不太会影响梯度幅角。为了消除这部分影响,我们需要设定一个阈值t=0.2,保留Q中小于0.2的元素,而把Q中大于0.2的元素用0.2替代。最后再对Q进行一次归一化处理,以提高特征点的可区分性。
we reduce the influence of large gradient magnitudes by thresholding the values in thr unit feature vector to each be no larger than 0.2,and then renormalizing to unit length
This means that matching the magnitudes for large gradients is no longer as important, and that the distribution of orientations has greater emphasis.
可以看到,Lowe 老兄在论文中只告诉我们要这么做,但是具体怎么做老兄不说,所以读他的这篇论文真的是有点难度,很多东西都需要我们去了解过程。
nrm2 = 0 length = d * d * n for k in range(length): nrm2 += dst[k] * dst[k] thr = np.sqrt(nrm2) * SIFT_DESCR_MAG_THR #对光照阈值进行反归一化处理,SIFT_DESCR_MAG_THR=0.2 nrm2 = 0 for i in range(length): #把特征矢量中大于反归一化阈值的元素用thr 替代 val = min(dst[i], thr) dst[i] = val nrm2 += val * val nrm2 = SIFT_INT_DESCR_FCTR / max(np.sqrt(nrm2), FLT_EPSILON) for k in range(length): dst[k] = min(max(dst[k] * nrm2,0),255)