Python计算机视觉——SIFT特征


文章目录

  • Python计算机视觉——SIFT特征
  • 写在前面
  • 1 SIFT特征算法步骤
  • 1.1 尺度空间的极值检测
  • 1.2 特征点定位
  • 1.3 特征方向赋值
  • 1.4 特征点描述
  • 2 实验分析
  • 3 关键点匹配
  • 4 匹配地理标记图像


写在前面

Scale invariant feature transform(SIFT),中文含义就是尺度不变特征变换。由于在此之前的目标检测算法对图片的大小、旋转非常敏感,而SIFT算法是一种基于局部兴趣点的算法,因此不仅对图片大小和旋转不敏感,而且对光照、噪声等影响的抗击能力也非常优秀,因此,该算法在性能和适用范围方面较于之前的算法有着质的改变。SIFT算法具的特点:

  1. 图像的局部特征,对旋转、尺度缩放、亮度变化保持不变,对视角变化、仿射变换、噪声也保持一定程度的稳定性。
  2. 独特性好,信息量丰富,适用于海量特征库进行快速、准确的匹配。
  3. 多量性,即使是很少几个物体也可以产生大量的SIFT特征
  4. 高速性,经优化的SIFT匹配算法甚至可以达到实时性
  5. 扩招性,可以很方便的与其他的特征向量进行联合。

归结于上述优点,SIFT特征检测共分四个步骤实现:

  1. 尺度空间的极值检测 搜索所有尺度空间上的图像,通过高斯微分函数来识别潜在的对尺度和选择不变的兴趣点。
  2. 特征点定位 在每个候选的位置上,通过一个拟合精细模型来确定位置尺度,关键点的选取依据他们的稳定程度。
  3. 特征方向赋值 基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向,后续的所有操作都是对于关键点的方向、尺度和位置进行变换,从而提供这些特征的不变性。
  4. 特征点描述 在每个特征点周围的邻域内,在选定的尺度上测量图像的局部梯度,这些梯度被变换成一种表示,这种表示允许比较大的局部形状的变形和光照变换。

1 SIFT特征算法步骤

1.1 尺度空间的极值检测

搜索所有尺度空间上的图像,通过高斯差分金字塔(difference-of-Gaussian function)来识别潜在的对尺度和选择不变的兴趣点。这句话有三个概念要解释。尺度空间、高斯差分金字塔、兴趣点。

尺度空间理论最早于1962年提出,其主要思想是通过对原始图像进行尺度变换,获得图像多尺度下的空间表示。 从而实现边缘、角点检测和不同分辨率上的特征提取,以满足特征点的尺度不变性。尺度空间中各尺度图像的模糊程度逐渐变大,能够模拟人在距离目标由近到远时目标在视网膜上的形成过程。尺度越大图像越模糊

lift表现python sift python_计算机视觉

高斯差分金字塔可以理解为一种图像的滤波器,那为啥要使用高斯,因为高斯核是唯一可以产生多尺度空间的核。可以通过高斯差分图 像看出图像上的像素值变化情况(如果没有变化,也就没有特征。特征必须是变化尽可能多的点) 。DOG图像描绘的是目 标的轮廓。DOG金字塔的第1组第1层是由高斯金字塔的第1组第2层减第1组第1层得到的。以此类推,逐组逐层生成每一个差分图像,所有差分图像构成差分金字塔。DOG金字塔的构建可以用下图描述:每一组在层数上,DOG金字塔比高斯金字塔少一层。后续Sift特征点的提取都是在DOG金字塔上进行的。

lift表现python sift python_人工智能_02

哪些是这些感兴趣点?这些点是一些十分突出的点不会因光照、尺度、旋转等因素的改变而消失,比如角点、边缘点、暗区域的亮点以及亮区域的暗点。

1.2 特征点定位

以上方法检测到的极值点是离散空间的极值点,以下通过拟合三维二次函数来精确确定关键点的位置和尺度,同时去除低对比度的关键点和不稳定的边缘响应点(因为DoG算子会产生较强的边缘响应),以增强匹配稳定性、提高抗噪声能力。离散空间的极值点并不是真正的极值点,下图显示了二维函数离散空间得到的极值点与连续空间极值点的差别。利用已知的离散空间点插值得到的连续空间极值点的方法叫做子像素插值(Sub-pixel Interpolation)。

lift表现python sift python_python_03

为了提高关键点的稳定性,需要对尺度空间DoG函数进行曲线拟合。利用DoG函数在尺度空间的泰勒展开式(拟合函数)为:
lift表现python sift python_python_04
其中lift表现python sift python_python_05求导并让方程等于零,可以得到极值点的偏移量为:
lift表现python sift python_计算机视觉_06
对应极值点,方程的值为:
lift表现python sift python_lift表现python_07
因此,为了寻找DoG函数的极值点, 每一个像素点要和它所有的相邻点比较,看其是否比它的图像域和尺度域 的相邻点大或者小。

且由于DoG函数在图像边缘有较强的边缘响应,因此需要排除边缘响应。 DoG函数的峰值点在边缘方向有较大的主曲率,而在垂直边缘的方向有 较小的主曲率。主曲率可以通过计算在该点位置尺度的2×2的Hessian矩 阵得到,导数由采样点相邻差来估计:
lift表现python sift python_人工智能_08
lift表现python sift python_lift表现python_09表示DOG金字塔中某一尺度的图像x方向求导两次。D的主曲率和H的特征值成正比。令 α,β为特征值,则:
lift表现python sift python_机器学习_10
该值在两特征值相等时达最小。即lift表现python sift python_lift表现python_11时保留关键点,反之剔除。

1.3 特征方向赋值

特征方向赋值基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向,后续的所有操作都是对于关键点的方向、尺度和位置进行变换,从而提供这些特征的不变性。确定关键点的方向采用梯度直方图统计法,统计以关键点为原 点,一定区域内的图像像素点对关键点方向生成所作的贡献。对于检测出的每一个关键点,梯度的模值和方向如下:
lift表现python sift python_python_12
在完成关键点的梯度计算后,使用直方图统计邻域内像素的梯度和方向。梯度直方图将0~360度的方向范围分为36个柱(bins),其中每柱10度。例如下图,直方图的峰值方向代表了关键点的主方向,(为简化,图中只画了八个方向的直方图)。

lift表现python sift python_人工智能_13

关键点的主方向:极值点周围区域梯度直方图的主峰值也是特征点方向。

关键点的辅方向:在梯度方向直方图中,当存在另一个相当于主峰值 80%能量的峰值时,则将这个方向认为是该关键点的辅方向。

1.4 特征点描述

通过以上步骤,对于每一个关键点,拥有三个信息:位置、尺度以及方向。接下来就是为每个关键点建立一个描述符,用一组向量将这个关键点描述出来,使其不随各种变化而改变,比如光照变化、视角变化等等。这个描述子不但包括关键点,也包含关键点周围对其有贡献的像素点,并且描述符应该有较高的独特性,以便于提高特征点正确匹配的概率。 SIFT描述子是关键点邻域高斯图像梯度统计结果的一种表示。通过对关键点周围图像区域分块,计算块内梯度直方图,生成具有独特性的向量,这个向量是该区域图像信息的一种抽象,具有唯一性。当然,经过Lowe实验表明,建议描述子使用在关键点尺度空间内4×4的窗口中计算的8个方向的梯度信息,共4×4×8=128维向量表征。

2 实验分析

SIFT选取的对象会使用DoG检测关键点,并且对每个关键点周围的区域计算特征向量,它主要包括两个操作:检测和计算,操作的返回值是关键点信息和描述符,最后在图像上绘制关键点,并用imshow函数显示这幅图像。

首先是处理图像得txt,txt中存储图片提取的重要信息,也就是这个函数的作用,你给我一张图,我就能给你一个txt。

def process_image(imagename,resultname,params="--edge-thresh 10 --peak-thresh 5"):
    """ process an image and save the results in a file"""
    path = os.path.abspath(os.path.join(os.path.dirname("__file__"),os.path.pardir))
    path = path+"\\ch02\\sift.exe "
    if imagename[-3:] != 'pgm':
        #create a pgm file
        im = Image.open(imagename).convert('L')
        im.save('tmp.pgm')
        imagename = 'tmp.pgm'
    cmmd = str(path+imagename+" --output="+resultname+
                " "+params)
    os.system(cmmd)
    print ('processed', imagename, 'to', resultname)

txt信息分析:下面数据的每一行前 4 个数值依次表示兴趣点的坐标、尺度和方向角度,后面紧接着的是对应描述符的 128 维向量。也就是一个特征点就用128维的向量表示,可以理解为这个向量的身份证。可以发现前两行的坐标值相同,但是方向不同。当同一个兴趣点上出现不同的显著方向,这种情况就会出现的。

lift表现python sift python_人工智能_14

有了上述的特征信息,就能够读取特征属性值,然后将其以矩阵的形式返回:

def read_features_from_file(filename):
    """ read feature properties and return in matrix form"""
    f = loadtxt(filename)
    return f[:,:4],f[:,4:] # feature locations, descriptors

这里返回两个参数,前一个代表坐标、尺度和方向角度四个数,后一个表示返回128维向量。有了以上两个函数便可实现sift特征检测:整合代码如下:

from PIL import Image
from numpy import *
from pylab import *
import os

def process_image(imagename,resultname,params="--edge-thresh 10 --peak-thresh 5"):
    """ process an image and save the results in a file"""
    path = os.path.abspath(os.path.join(os.path.dirname("__file__"),os.path.pardir))
    path = path+"\\ch02\\sift.exe "
    if imagename[-3:] != 'pgm':
        #create a pgm file
        im = Image.open(imagename).convert('L')
        im.save('tmp.pgm')
        imagename = 'tmp.pgm'
    cmmd = str(path+imagename+" --output="+resultname+
                " "+params)
    os.system(cmmd)
    print ('processed', imagename, 'to', resultname)
    
def read_features_from_file(filename):
    """ read feature properties and return in matrix form"""
    f = loadtxt(filename)
    return f[:,:4],f[:,4:] # feature locations, descriptors

def plot_features(im,locs,circle=False):
    """ show image with features. input: im (image as array), 
        locs (row, col, scale, orientation of each feature) """

    def draw_circle(c,r):
        t = arange(0,1.01,.01)*2*pi
        x = r*cos(t) + c[0]
        y = r*sin(t) + c[1]
        plot(x,y,'b',linewidth=2)

    imshow(im)
    if circle:
        [draw_circle([p[0],p[1]],p[2]) for p in locs]
    else:
        plot(locs[:,0],locs[:,1],'ob')
    axis('off')
    
if __name__ == '__main__':
    imname=(r' ')
    im=Image.open(imname)
    process_image(imname,'luda.sift')
    l1,d1 = read_features_from_file('luda.sift')  #l1为兴趣点坐标、尺度和方位角度 l2是对应描述符的128 维向
    figure(dpi = 100)
    gray()
    plot_features(im,l1,circle = True)
    title('sift-features')
    show()

效果如下:sift提取的是图像的局部特征,对旋转、尺度缩放、亮度变化保持不变,对视角变化、仿射变换、噪声也保持一定程度的稳定性。同时也可以看出,可以看出即使是很少几个物体也可以产生大量的SIFT特征。

lift表现python sift python_人工智能_15

3 关键点匹配

在有了sift特征之后,便可以实现关键点的匹配。SIFT算法实现特征匹配主要有三个流程,1、提取关键点;2、对关键点附加 详细的信息(局部特征),即描述符;3、通过特征点(附带上特征向量的关 键点)的两两比较找出相互匹配的若干对特征点,建立景物间的对应关系。

lift表现python sift python_lift表现python_16

首先得准备两张size一样大的图片,他们要有相似的地方。分别调用sift检测出相应特征。

lift表现python sift python_机器学习_17

接着进行关键点匹配:下半部分是两张原图,上半部分是匹配后的图。

lift表现python sift python_python_18

当然也可以尝试两张一摸一样的图片进行特征匹配:能够看出两张完全一样的图片匹配结果一致

lift表现python sift python_计算机视觉_19


实验分析:可以看出,当拍摄角度和距离存在区别时,导致景观出现较大的改变(例如尚大楼只剩一半),此时两张图片相匹配的特征点小于阈值,因此就无法匹配出来。也就是当两张图片的差异越大,相匹配的特征点不足,则匹配结果越少。比如,同个场景,如果没有相同的特征目标,或是说相匹配的特征太少,那么可能不会匹配。相反,若是两张图一摸一样,那么特征点则完全能够匹配。

整体代码如下:

from PIL import Image
import os
from numpy import *
from pylab import *


def process_image(imagename,resultname,params="--edge-thresh 10 --peak-thresh 5"):
    """ process an image and save the results in a file"""
    path = os.path.abspath(os.path.join(os.path.dirname("__file__"),os.path.pardir))
    path = path+"\\ch02\\sift.exe "
    if imagename[-3:] != 'pgm':
        #create a pgm file
        im = Image.open(imagename).convert('L')
        im.save('tmp.pgm')
        imagename = 'tmp.pgm'
    cmmd = str(path+imagename+" --output="+resultname+
                " "+params)
    os.system(cmmd)
    print ('processed', imagename, 'to', resultname)


def read_features_from_file(filename):
	""" read feature properties and return in matrix form"""
	f = loadtxt(filename)
	return f[:,:4],f[:,4:] # feature locations, descriptors


def write_features_to_file(filename,locs,desc):
	""" save feature location and descriptor to file"""
	savetxt(filename,hstack((locs,desc)))
	

def plot_features(im,locs,circle=False):
	""" show image with features. input: im (image as array), 
		locs (row, col, scale, orientation of each feature) """

	def draw_circle(c,r):
		t = arange(0,1.01,.01)*2*pi
		x = r*cos(t) + c[0]
		y = r*sin(t) + c[1]
		plot(x,y,'b',linewidth=2)

	imshow(im)
	if circle:
		[draw_circle([p[0],p[1]],p[2]) for p in locs]
	else:
		plot(locs[:,0],locs[:,1],'ob')
	axis('off')


def match(desc1,desc2):
	""" for each descriptor in the first image, 
		select its match in the second image.
		input: desc1 (descriptors for the first image), 
		desc2 (same for second image). """
	
	desc1 = array([d/linalg.norm(d) for d in desc1])
	desc2 = array([d/linalg.norm(d) for d in desc2])
	
	dist_ratio = 0.6
	desc1_size = desc1.shape
	
	matchscores = zeros((desc1_size[0],1))
	desc2t = desc2.T #precompute matrix transpose
	for i in range(desc1_size[0]):
		dotprods = dot(desc1[i,:],desc2t) #vector of dot products
		dotprods = 0.9999*dotprods
		#inverse cosine and sort, return index for features in second image
		indx = argsort(arccos(dotprods))
		
		#check if nearest neighbor has angle less than dist_ratio times 2nd
		if arccos(dotprods)[indx[0]] < dist_ratio * arccos(dotprods)[indx[1]]:
			matchscores[i] = int(indx[0])
	
	return matchscores


def appendimages(im1,im2):
	""" return a new image that appends the two images side-by-side."""
	
	#select the image with the fewest rows and fill in enough empty rows
	rows1 = im1.shape[0]    
	rows2 = im2.shape[0]
	
	if rows1 < rows2:
		im1 = concatenate((im1,zeros((rows2-rows1,im1.shape[1]))), axis=0)
	elif rows1 > rows2:
		im2 = concatenate((im2,zeros((rows1-rows2,im2.shape[1]))), axis=0)
	#if none of these cases they are equal, no filling needed.
	
	return concatenate((im1,im2), axis=1)


def plot_matches(im1,im2,locs1,locs2,matchscores,show_below=True):
	""" show a figure with lines joining the accepted matches
		input: im1,im2 (images as arrays), locs1,locs2 (location of features), 
		matchscores (as output from 'match'), show_below (if images should be shown below). """
	
	im3 = appendimages(im1,im2)
	if show_below:
		im3 = vstack((im3,im3))
	
	# show image
	imshow(im3)
	
	# draw lines for matches
	cols1 = im1.shape[1]
	for i in range(len(matchscores)):
		if matchscores[i] > 0:
			plot([locs1[i,0], locs2[int(matchscores[i,0]),0]+cols1], [locs1[i,1], locs2[int(matchscores[i,0]),1]], 'c')
	axis('off')


def match_twosided(desc1,desc2):
	""" two-sided symmetric version of match(). """
	
	matches_12 = match(desc1,desc2)
	matches_21 = match(desc2,desc1)
	
	ndx_12 = matches_12.nonzero()[0]
	
	#remove matches that are not symmetric
	for n in ndx_12:
		if matches_21[int(matches_12[n])] != n:
			matches_12[n] = 0
	
	return matches_12


if __name__ == "__main__":
	imname1=(r'  ')
	process_image(imname1,'tmp.sift')
	l,d = read_features_from_file('tmp.sift')
	im = array(Image.open(imname1))
	# figure()
	# plot_features(im,l,True)
	

	imname2=(r'  ')
	process_image(imname2,'tmp2.sift')
	l2,d2 = read_features_from_file('tmp2.sift')
	im2 = array(Image.open(imname2))	
	# figure()
	# plot_features(im2,l2,True)

	m = match_twosided(d,d2)
	figure()
	plot_matches(im,im2,l,l2,m)

	show()

4 匹配地理标记图像

匹配地理标记图像指的是输入同一场景的序列图像,然后通过SIFT算法对地理标记图像进行两两匹配,构造连接矩阵,最后可视化图像连接关系。首先准备一序列的图片,对这些图像提取局部描述子。然后得到连接矩阵,最后利用pydot工具包可视化连接结果。为了创建显示可能图像组的图,如果匹配的数目高于一个阈值,我们使用边来连接相应的图像节点。同时,缩略图的最大边被定格在100 像素。代码如下:

# -*- coding: utf-8 -*-
from pylab import *
from PIL import Image
import sift
import imtools
import pydot

download_path = "E:\\master_workspace\\pcv-book-code-master\\ch02\\pano_imgs"  # set this to the path where you downloaded the panoramio images
path = "E:\\master_workspace\\pcv-book-code-master\\ch02\\pano_imgs\\results\\"  # path to save thumbnails (pydot needs the full system path)

# list of downloaded filenames
# imlist = imtools.get_imlist(download_path)
imlist = [os.path.join(download_path,f) for f in os.listdir(download_path) if f.endswith('.jpeg')]
nbr_images = len(imlist)

# extract features
featlist = [imname[:-4] + 'sift' for imname in imlist]
for i, imname in enumerate(imlist):
    sift.process_image(imname, featlist[i])
    
matchscores = zeros((nbr_images, nbr_images))

for i in range(nbr_images):
    for j in range(i, nbr_images):  # only compute upper triangle
#         print ('comparing ', imlist[i], imlist[j])
        l1, d1 = sift.read_features_from_file(featlist[i])
        l2, d2 = sift.read_features_from_file(featlist[j])
        matches = sift.match_twosided(d1, d2)
        nbr_matches = sum(matches > 0)
#         print ('number of matches = ', nbr_matches)
        matchscores[i, j] = nbr_matches
# print ("The match scores is: %d", matchscores)

# copy values
for i in range(nbr_images):
    for j in range(i + 1, nbr_images):  # no need to copy diagonal
        matchscores[j, i] = matchscores[i, j]
        
threshold = 1  # min number of matches needed to create link

g = pydot.Dot(graph_type='graph')  # don't want the default directed graph
for i in range(nbr_images):
    for j in range(i + 1, nbr_images):
        if matchscores[i, j] > threshold:
            print(i, j)
            # first image in pair
            im = Image.open(imlist[i])
            im.thumbnail((100, 100))
            filename = path + str(i) + '.png'
            im.save(filename)  # need temporary files of the right size
            g.add_node(pydot.Node(str(i), fontcolor='transparent', shape='rectangle', image=filename))

            # second image in pair
            im = Image.open(imlist[j])
            im.thumbnail((100, 100))
            filename = path + str(j) + '.png'
            im.save(filename)  # need temporary files of the right size
            g.add_node(pydot.Node(str(j), fontcolor='transparent', shape='rectangle', image=filename))

            g.add_edge(pydot.Edge(str(i), str(j)))
g.write_png('jmu.png')

实验分析,下图阈值设定为1。低阈值说明能够得到更多的匹配点,但同样置信度也会降低,因此会出现上图中错误匹配的情况。

lift表现python sift python_计算机视觉_20

当阈值设置为2时,更够看到匹配的图片变少,但是置信度即匹配的结果明显更加准确。从实验结果来看,SIFT可以解决一些角度,光照,杂物等问题实现地理场景匹配。但是也存在一些不足,比如,同个场景,如果没有相同的特征目标,或是说相匹配的特征太少,那么可能不会匹配。反过来,如上图所示,如果两个不同的场景外观上太过相似,那么可能就会被误判成同个场景。

lift表现python sift python_python_21