1. 实验6 图像特征提取与描述
1.1. 学习目标
- 理解
SIFT
算法的原理 - 能够使用
SIFT
进行关键点的检测
1.2. SIFT/SURF算法
- 尺度不变特征转换即SIFT (Scale-invariant feature transform)。
- 它用来侦测与描述影像中的局部性特征,它在空间尺度中寻找极值点,并提取出其位置、尺度、旋转不变量。
- 此算法由 David Lowe在1999年所发表,2004年完善总结。应用范围包含物体辨识、机器人地图感知与导航、影像缝合、3D模型建立、手势辨识、影像追踪和动作比对等领域。
1.2.1. 基本流程
Lowe将SIFT算法分解为如下四步:
- 尺度空间极值检测:搜索所有尺度上的图像位置。通过高斯差分函数来识别潜在的对于尺度和旋转不变的关键点。
- 关键点定位:在每个候选的位置上,通过一个拟合精细的模型来确定位置和尺度。关键点的选择依据于它们的稳定程度。
- 关键点方向确定:基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向。所有后面的对图像数据的操作都相对于关键点的方向、尺度和位置进行变换,从而保证了对于这些变换的不变性。
- 关键点描述:在每个关键点周围的邻域内,在选定的尺度上测量图像局部的梯度。这些梯度作为关键点的描述符,它允许比较大的局部形状的变形或光照变化。
尺度空间极值检测
- 在不同的尺度空间是不能使用相同的窗口检测极值点,对小的关键点使用小的窗口,对大的关键点使用大的窗口,为了达到上述目的,我们使用尺度空间滤波器。
- 高斯核是唯一可以产生多尺度空间的核函数。-
《Scale-space theory: A basic tool for analysing structures at different scales》
- 高斯核是唯一可以产生多尺度空间的核函数。-
- 一个图像的尺度空间L(x,y,σ),定义为原始图像I(x,y)与一个可变尺度的2维高斯函数G(x,y,σ)卷积运算 ,即:
其中:
- 在不同的尺度空间是不能使用相同的窗口检测极值点,对小的关键点使用小的窗口,对大的关键点使用大的窗口,为了达到上述目的,我们使用尺度空间滤波器。
- σ是尺度空间因子,它决定了图像的模糊的程度。在大尺度下(σ值大)表现的是图像的概貌信息,在小尺度下(值小)表现的是图像的细节信息。 在计算高斯函数的离散近似时,在大概3σ距离之外的像素都可以看作不起作用,这些像素的计算也就可以忽略。所以,在实际应用中,只计算的高斯卷积核就可以保证相关像素影响。
下面我们构建图像的高斯金字塔,它采用高斯函数对图像进行模糊以及降采样处理得到的,高斯金字塔构建过程中,首先将图像扩大一倍,在扩大的图像的基础之上构建高斯金字塔,然后对该尺寸下图像进行高斯模糊,几幅模糊之后的图像集合构成了一个Octave,然后对该Octave下选择一幅图像进行下采样,长和宽分别缩短一倍,图像面积变为原来四分之一。这幅图像就是下一个Octave的初始图像,在初始图像的基础上完成属于这个Octave的高斯模糊处理,以此类推完成整个算法所需要的所有八度构建,这样这个高斯金字塔就构建出来了,整个流程如下图所示:
利用LoG(高斯拉普拉斯方法),即图像的二阶导数,可以在不同的尺度下检测图像的关键点信息,从而确定图像的特征点。但LoG的计算量大,效率低。所以我们通过两个相邻高斯尺度空间的图像的相减,得到DoG(高斯差分)来近似LoG。
- 为了计算DoG我们构建高斯差分金字塔,该金字塔是在上述的高斯金字塔的基础上构建而成的,建立过程是:在高斯金字塔中每个Octave中相邻两层相减就构成了高斯差分金字塔。如下图所示:
- 高斯差分金字塔的第1组第1层是由高斯金字塔的第1组第2层减第1组第1层得到的。以此类推,逐组逐层生成每一个差分图像,所有差分图像构成差分金字塔。概括为DOG金字塔的第o组第l层图像是有高斯金字塔的第o组第l+1层减第o组第l层得到的。后续Sift特征点的提取都是在DOG金字塔上进行的
- 在 DoG 搞定之后,就可以在不同的尺度空间中搜索局部最大值了。对于图像中的一个像素点而言,它需要与自己周围的 8 邻域,以及尺度空间中上下两层中的相邻的 18(2x9)个点相比。如果是局部最大值,它就可能是一个关键点。基本上来说关键点是图像在相应尺度空间中的最好代表。如下图所示:
- 搜索过程从每组的第二层开始,以第二层为当前层,对第二层的DoG图像中的每个点取一个3×3的立方体,立方体上下层为第一层与第三层。这样,搜索得到的极值点既有位置坐标(DoG的图像坐标),又有空间尺度坐标(层坐标)。当第二层搜索完成后,再以第三层作为当前层,其过程与第二层的搜索类似。当S=3时,每组里面要搜索3层,所以在DOG中就有S+2层,在初使构建的金字塔中每组有S+3层。
1.2.2. 关键点定位
- 由于DoG对噪声和边缘比较敏感,因此在上面高斯差分金字塔中检测到的局部极值点需经过进一步的检验才能精确定位为特征点。
- 使用尺度空间的泰勒级数展开来获得极值的准确位置, 如果极值点的 灰度值小于阈值(一般为0.03或0.04)就会被忽略掉。 在 OpenCV 中这种阈值被称为 contrastThreshold。
- DoG 算法对边界非常敏感, 所以我们必须要把边界去除。 Harris 算法除了可以用于角点检测之外还可以用于检测边界。从 Harris 角点检测的算法中,当一个特征值远远大于另外一个特征值时检测到的是边界。那在DoG算法中欠佳的关键点在平行边缘的方向有较大的主曲率,而在垂直于边缘的方向有较小的曲率,两者的比值如果高于某个阈值(在OpenCV中叫做边界阈值),就认为该关键点为边界,将被忽略,一般将该阈值设置为10。
- 将低对比度和边界的关键点去除,得到的就是我们感兴趣的关键点
1.2.3. 关键点方向确定
- 经过上述两个步骤,图像的关键点就完全找到了,这些关键点具有尺度不变性。为了实现旋转不变性,还需要为每个关键点分配一个方向角度,也就是根据检测到的关键点所在高斯尺度图像的邻域结构中求得一个方向基准。
- 对于任一关键点,我们采集其所在高斯金字塔图像以r为半径的区域内所有像素的梯度特征(幅值和幅角),半径r为:
- 其中σ是关键点所在octave的图像的尺度,可以得到对应的尺度图像。
梯度的幅值和方向的计算公式为:
邻域像素梯度的计算结果如下图所示:
完成关键点梯度计算后,使用直方图统计关键点邻域内像素的梯度幅值和方向。具体做法是,将360°分为36柱,每10°为一柱,然后在以r为半径的区域内,将梯度方向在某一个柱内的像素找出来,然后将他们的幅值相加在一起作为柱的高度。因为在r为半径的区域内像素的梯度幅值对中心像素的贡献是不同的,因此还需要对幅值进行加权处理,采用高斯加权,方差为1.5σ。如下图所示,为简化图中只画了8个方向的直方图。
- 每个特征点必须分配一个主方向,还需要一个或多个辅方向,增加辅方向的目的是为了增强图像匹配的鲁棒性。辅方向的定义是,当一个柱体的高度大于主方向柱体高度的80%时,则该柱体所代表的的方向就是给特征点的辅方向。
直方图的峰值,即最高的柱代表的方向是特征点邻域范围内图像梯度的主方向,但该柱体代表的角度是一个范围,所以我们还要对离散的直方图进行插值拟合,以得到更精确的方向角度值。利用抛物线对离散的直方图进行拟合,如下图所示:
- 获得图像关键点主方向后,每个关键点有三个信息(x,y,σ,θ):位置、尺度、方向。由此我们可以确定一个SIFT特征区域。通常使用一个带箭头的圆或直接使用箭头表示SIFT区域的三个值:中心表示特征点位置,半径表示关键点尺度,箭头表示方向。如下图所示:
1.2.4. 关键点描述
通过以上步骤,每个关键点就被分配了位置,尺度和方向信息。接下来我们为每个关键点建立一个描述符,该描述符既具有可区分性,又具有对某些变量的不变性,如光照,视角等。而且描述符不仅仅包含关键点,也包括关键点周围对其有贡献的的像素点。主要思路就是通过将关键点周围图像区域分块,计算块内的梯度直方图,生成具有特征向量,对图像信息进行抽象。
描述符与特征点所在的尺度有关,所以我们在关键点所在的高斯尺度图像上生成对应的描述符。以特征点为中心,将其附近邻域划分为个子区域(一般取d=4),每个子区域都是一个正方形,边长为3σ,考虑到实际计算时,需进行三次线性插值,所以特征点邻域的为3σ(d+1)* 3σ(d+1)的范围,如下图所示:
- 为了保证特征点的旋转不变性,以特征点为中心,将坐标轴旋转为关键点的主方向,如下图所示:
- 计算子区域内的像素的梯度,并按照σ=0.5d进行高斯加权,然后插值计算得到每个种子点的八个方向的梯度,插值方法如下图所示:
每个种子点的梯度都是由覆盖其的4个子区域插值而得的。如图中的红色点,落在第0行和第1行之间,对这两行都有贡献。对第0行第3列种子点的贡献因子为dr,对第1行第3列的贡献因子为1-dr,同理,对邻近两列的贡献因子为dc和1-dc,对邻近两个方向的贡献因子为do和1-do。则最终累加在每个方向上的梯度大小为:
SIFT在图像的不变特征提取方面拥有无与伦比的优势,但并不完美,仍然存在实时性不高,有时特征点较少,对边缘光滑的目标无法准确提取特征点等缺陷,自SIFT算法问世以来,人们就一直对其进行优化和改进,其中最著名的就是SURF算法。
1.2.5. OpenCV实现
在OpenCV中利用SIFT检测关键点的流程如下所示:
- 实例化sift
sift = cv.SIFT.create()
利用sift.detectAndCompute()检测关键点并计算
kp,des = sift.detectAndCompute(gray,None)
- 参数:
- gray: 进行关键点检测的图像,注意是灰度图像
- 返回:
- kp: 关键点信息,包括位置,尺度,方向信息
- des: 关键点描述符,每个关键点对应128个梯度信息的特征向量
- 参数:
- 将关键点检测结果绘制在图像上
cv.drawKeypoints(image, keypoints, outputimage, color, flags)
- 参数:
- image: 原始图像
- keypoints:关键点信息,将其绘制在图像上
- outputimage:输出图片,可以是原始图像
- color:颜色设置,通过修改(b,g,r)的值,更改画笔的颜色,b=蓝色,g=绿色,r=红色。
- flags:绘图功能的标识设置
- cv2.DRAW_MATCHES_FLAGS_DEFAULT:创建输出图像矩阵,使用现存的输出图像绘制匹配对和特征点,对每一个关键点只绘制中间点
- cv2.DRAW_MATCHES_FLAGS_DRAW_OVER_OUTIMG:不创建输出图像矩阵,而是在输出图像上绘制匹配对
- cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS:对每一个特征点绘制带大小和方向的关键点图形
- cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS:单点的特征点不被绘制
- 参数:
- 实例化sift
例子
利用SIFT算法在中央电视台的图片上检测关键点,并将其绘制出来:
import cv2 as cv import numpy as np import matplotlib.pyplot as plt # 1 读取图像 img = cv.imread('./image/tv.jpg') gray= cv.cvtColor(img,cv.COLOR_BGR2GRAY) # 2 sift关键点检测 # 2.1 实例化sift对象 sift = cv.SIFT.create() # 2.2 关键点检测:kp关键点信息包括方向,尺度,位置信息,des是关键点的描述符 kp,des=sift.detectAndCompute(gray,None) # 2.3 在图像上绘制关键点的检测结果 cv.drawKeypoints(img,kp,img,flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) # 3 图像显示 plt.figure(figsize=(8,6),dpi=100) plt.imshow(img[:,:,::-1]),plt.title('sift检测') plt.xticks([]), plt.yticks([]) plt.show()
1.2.6. 特征匹配
蛮力法(Brute-Force Matcher)
- 蛮力法很简单。它采用第一个集合中一个特征的描述符,并通过一些距离计算与第二个集合中的所有其他特征匹配。返回最接近的一个。
matcher= cv.BFMatcher(int normType=NORM_L2, bool crossCheck=false)
img1 = cv.imread('./image/exp07/1.jpg',cv.IMREAD_GRAYSCALE) img2 = cv.imread('./image/exp07/2.jpg',cv.IMREAD_GRAYSCALE) sift = cv.SIFT.create() kp1,des1= sift.detectAndCompute(img1,None) kp2,des2= sift.detectAndCompute(img2,None) bf = cv.BFMatcher() # Match descriptors. matches = bf.knnMatch(des1,des2,k=2) good =[] for m,n in matches: if m.distance < 0.75*n.distance: good.append([m]) img3 = cv.drawMatchesKnn(img1,kp1,img2,kp2,good,None,flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS) plt.imshow(img3),plt.show()
- 蛮力法很简单。它采用第一个集合中一个特征的描述符,并通过一些距离计算与第二个集合中的所有其他特征匹配。返回最接近的一个。
快速近似最近邻法FLANN
- 它包含一系列算法,这些算法针对大型数据集中的快速最近邻搜索和高维特征进行了优化。对于大型数据集,它比BFMatcher工作得更快。我们将看到基于FLANN的matcher的示例
img1= cv.imread('./image/exp07/4.jpg',cv.IMREAD_GRAYSCALE) img2= cv.imread('./image/exp07/5.jpg',cv.IMREAD_GRAYSCALE) sift = cv.SIFT.create() kp1,des1= sift.detectAndCompute(img1,None) kp2,des2= sift.detectAndCompute(img2,None) FLANN_INDEX_KDTREE=1 index_params= dict(algorithm=FLANN_INDEX_KDTREE,trees=5) search_params=dict(checks=50) flann = cv.FlannBasedMatcher(index_params,search_params) matches = flann.knnMatch(des1,des2,k=2) matchMask = [[0,0]for i in range(len(matches))] for i,(m,n) in enumerate(matches): if m.distance <0.75*n.distance: matchMask[i] = [1,0] draw_params = dict(matchColor=(0,255,0),singlePointColor=(255,0,0),matchesMask=matchMask,flags=cv.DrawMatchesFlags_DEFAULT) img3= cv.drawMatchesKnn(img1,kp1,img2,kp2,matches,None,**draw_params) plt.imshow(img3,) plt.show()
1.3. 实验内容
- 按照以上实验内容,对
images文件夹
的图像进行以下处理,并截图和记录实验结果- SIFT特征提取,对图像中的人进行SIFT特征绘制。
- BF匹配,将文件夹中
6.jpg
中所有其他图像进行FLANN匹配,绘制出最终匹配结果 - FLANN匹配,将文件夹中
6.jpg
中所有其他图像进行FLANN匹配,绘制出最终匹配结果 - 简单的检索,编写程序确定与图
6.jpg
最接近的图像是哪一个? - 要求:
- 截图,并在截图中写上自己的学号和姓名
- 提交实验代码和报告,其中报告中的截图请嵌入姓名和学号,打包以上提交到学习通平台。