SIFT算法的实质可以归为在不同尺度空间上查找特征点(关键点)的问题。
SIFT算法实现特征匹配主要有三个流程:1、提取关键点; 2、对关键点附加详细的信息(局部特征),即描述符;3、通过特征点(附带上特征向量的关键点)的两两比较找出相互匹配的若干对特征点,建立景物间的对应关系。
它可以模拟人在距离目标由近到远的过程中,目标在视网膜当中形成图像的过程,尺度越大越模糊,相当于我们观察远处的物体。如果需要识别出包含不同尺寸的同一物体的两幅图像,随着物体在图像中大小发生变化,属于该物体的局部区域的大小也会发生变化。SIFT算法采用高斯图像金字塔,对图像做高斯平滑和降采样,一幅图像可以产生几组图像,一组图像包括几层图像,从而实现尺度的连续性。高斯核是唯一可以产生 多尺度空间的核,一个 图像的尺度空间,L(x, y, σ) ,定义为原始图像 I(x, y)与一个可变尺度的2 维高斯函数G(x, y, σ) 卷积运算。
尺度归一化的高斯拉普拉斯算子能够得到最稳定的图像特征,但因为计算量太大,所以引入了高斯差分金字塔(DOG),DoG在计算上只需相邻高斯平滑后图像相减,即高斯差分金字塔的第1组第1层是由高斯金字塔的第1组第2层减第1组第1层得到的,以此类推,逐组逐层生成每一个差分图像,所有差分图像构成差分金字塔,每一组在层数上,高斯差分金字塔比高斯金字塔少一层,从而简化了计算。
高斯模糊是在Adobe Photoshop等图像处理软件中广泛使用的处理效果,通常用它来减小图像噪声以及降低细节层次。这种模糊技术生成的图像的视觉效果是好像经过一个半透明的屏幕观察图像。
根据σ的值,计算出高斯模板矩阵的大小(6σ+1)(6σ-1),使用公式计算高斯模板矩阵的值,与原图像做卷积,即可获得原图像的平滑(高斯模糊)图像。为了确保模板矩阵中的元素在[0,1]之间,需将模板矩阵归一化。55的高斯模板如下所示。
下图是55的高斯模板卷积计算示意图。高斯模板是中心对称的。
对某一灰度图像,首先进行升采样(即扩大两倍采样),然后对升采样之后的图片进行高斯模糊,从而生成一组采样图。(注:升采样不是必须的)
对原灰度图像进行降采样,然后高斯模糊,得到第二组采样图,每一组都有六层尺寸相同但模糊系数不同的采样图像得到。为了保持差分高斯金字塔的尺度空间(即模糊系数)的连续性,下一组(第i组)的第一层由上一组的第四层降采样之后得到,同时第i组的后面几层都是由该组第一层经过高斯模糊得到,不需要进行降采样。这样,几组采样图组合在一起,就构成了高斯金字塔。如下图所示:
在计算时,使用高斯金字塔每组中相邻上下两层图像相减,得到高斯差分图像
图像上的像素值如果没有变化,也就没有特征。特征必须是变化尽可能多的点。DOG图像描绘的是目标的轮廓。
特征点是由DOG空间的局部极值点组成的。为了寻找尺度空间的极值点,每个像素点要和该点的周围所有相邻点进行比较,当其与相邻点有明显差别的灰度值时,该点就是极值点。
此时找到的极值点标为候选特征点,因为存在噪声的干扰。如图所示,中间的检测点要和其所在图像的3×3邻域8个像素点,以及其相邻的上下两层的3×3领域18个像素点,共26个像素点进行比较,确保在尺度空间和二维图像空间都检测到极值点。
从上面的描述中可以知道,每组图像的第一层和最后一层是无法进行比较取得极值的。为了满足尺度变换的连续性,在每一组图像的顶层继续使用高斯模糊生成3幅图像,高斯金字塔每组有S+3层图像,DoG金字塔的每组有S+2组图像。这样产生的极值点并不全都是稳定的特征点,因为某些极值点响应较弱,而且DOG算子会产生较强的边缘响应。
通过比较检测得到的DoG的局部极值点是在离散的空间搜索得到的,由于离散空间是对连续空间采样得到的结果,物体的边缘轮廓在灰度图中,存在着灰度值的突变,这样的点在计算中就被“误以为”是特征值。因此在离散空间找到的极值点不一定是真正意义上的极值点,因此要将不满足条件的点剔除掉。
DoG函数的峰值点在边缘方向有较大的主曲率,而在垂直边缘的方向有较小的主曲率。主曲率可以通过计算在该点位置尺度的2×2的Hessian矩阵得到,导数由采样点相邻差来估计
Dxx 表示DOG金字塔中某一尺度的图像x方向求导两次
D的主曲率和H的特征值成正比。令 α ,β为特征值,则
该值在两特征值相等时达最小。Lowe论文中建议阈值T为1.2,即
时保留关键点,反之剔除。
主要删除两类:低对比度的极值点以及不稳定的边缘响应点。
为了实现图像旋转不变性,需要给特征点的方向进行赋值。利用特征点邻域像素的梯度分布特性来确定其方向参数,再利用图像的梯度直方图求取关键点局部结构的稳定方向。
像素点的梯度表示:
梯度幅值:
梯度方向:
利用以特征点为中心划出周边的16 x 16图像块,计算每个点的幅值和梯度方向,再离散为8(根据划分的角度设定)个bin方向,产生离散直方图
直方图:计算得到梯度方向后,使用直方图统计特征点邻域内像素对应的梯度方向和幅值。梯度方向的直方图的横轴是梯度方向的角度(这里以8个为例),纵轴是梯度方向对应梯度幅值的累加,在直方图的峰值就是特征点的主方向。
关键点主方向:极值点周围区域梯度直方图的主峰值也是特征点方向
关键点辅方向:在梯度方向直方图中,当存在另一个相当于主峰值80%能量的峰值时,则将这个方向认为是该关键点的辅方向
因此,对于同一梯度值的多个峰值的关键点位置,在相同位置和尺度将会有多个关键点被创建但方向不同。仅有15%的关键点被赋予多个方向,但可以明显的提高关键点匹配的稳定性。实际编程实现中,就是把该关键点复制成多份关键点,并将方向值分别赋给这些复制后的关键点,并且,离散的梯度方向直方图要进行插值拟合处理,来求得更精确的方向角度值。
以旋转之后的主方向为中心取8x8的窗口,左图中点为当前关键点的位置,每一个小格都代表了特征点邻域所在的尺度空间的一个像素,箭头方向代表了像素梯度方向,箭头长度代表该像素的幅值。然后在每个4x4的小块上绘制8个方向的梯度直方图,计算每个梯度方向的累加值,即可形成一个种子点,如右图所示,每个特征的由4个种子点组成,每个种子点有8个方向的向量信息,这种邻域方向性信息联合增强了算法的抗噪能力,同时对于含有定位误差的特征匹配也提供了比较理性的容错性。
旋转后的新坐标:
Lowe实验结果表明:描述子采用4×4×8=128维向量表征综合效果最优
分别对模板图和实时图建立关键点描述子集合。目标的识别是通过两点集内关键点描述子的比对来完成。具有128维的关键点描述子的相似性度量采用欧式距离
穷举匹配:
对比次数过多,算法复杂度大,因此不采用。一般都采用kd树的数据结构来完成搜索。搜索的内容是以目标图像的关键点为基准,搜索与目标图像的特征点最邻近的原图像特征点和次邻近的原图像特征点。
随机抽样一致算法(random sample consensus,RANSAC)采用迭代的方式从一组包含离群的被观测数据中估算出数学模型的参数。
RANSAC算法的基本假设是样本中包含正确数据(inliers,可以被模型描述的数据),也包含异常数据(outliers,偏离正常范围很远、无法适应数学模型的数据),即数据集中含有噪声。这些异常数据可能是由于错误的测量、错误的假设、错误的计算等产生的。同时RANSAC也假设,给定一组正确的数据,存在可以计算出符合这些数据的模型参数的方法。
如果场景是平面,或者近似平面,或者低视差时,我们能应用单应性矩阵(homography)
单应性变换是将一个平面内的点映射到另一个平面内的二维投影变换。在这里,平面是指图像或者三维中的平面表面。
单应性变换的目标是通过给定的几个点(通常是4对点)来得到单应性矩阵。
用一个3x3的矩阵来表示单应性,可以写成
考虑第一组对应点(X1, Y1)在第一张图像和(X2, Y2)第二张图像中。然后,Homography H以下列方式映射它们
坐标变换:
要得到两张图片的H矩阵,就必须至少知道4个相同对应位置的点,通常,这些点对应是通过匹配图像之间的SIFT或Harris等特征自动找到的。在本次实验中通过SIFT特征匹配找到这些对应点
# -*- coding: utf-8 -*-
from PIL import Image
from pylab import *
from PCV.localdescriptors import sift
from PCV.localdescriptors import harris
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)
imname = 'image/tom2.jpg'
im = array(Image.open(imname).convert('L'))
sift.process_image(imname, 'tom2.sift')
l1, d1 = sift.read_features_from_file('tom2.sift')
figure()
gray()
subplot(131)
sift.plot_features(im, l1, circle=False)
title(u'SIFT特征',fontproperties=font)
subplot(132)
sift.plot_features(im, l1, circle=True)
title(u'用圆圈表示SIFT特征尺度',fontproperties=font)
# 检测harris角点
harrisim = harris.compute_harris_response(im)
subplot(133)
filtered_coords = harris.get_harris_points(harrisim, 6, 0.1)
imshow(im)
plot([p[1] for p in filtered_coords], [p[0] for p in filtered_coords], '*')
axis('off')
title(u'Harris角点',fontproperties=font)
show()
from PIL import Image
from pylab import *
import sys
from PCV.localdescriptors import sift
if len(sys.argv) >= 3:
im1f, im2f = sys.argv[1], sys.argv[2]
else:
# im1f = '../data/sf_view1.jpg'
# im2f = '../data/sf_view2.jpg'
im1f = 'data/bui3.jpg'
im2f = 'data/bui4.jpg'
# im1f = '../data/climbing_1_small.jpg'
# im2f = '../data/climbing_2_small.jpg'
im1 = array(Image.open(im1f))
im2 = array(Image.open(im2f))
sift.process_image(im1f, 'out_sift_1.txt')
l1, d1 = sift.read_features_from_file('out_sift_1.txt')
figure()
gray()
subplot(121)
sift.plot_features(im1, l1, circle=False)
sift.process_image(im2f, 'out_sift_2.txt')
l2, d2 = sift.read_features_from_file('out_sift_2.txt')
subplot(122)
sift.plot_features(im2, l2, circle=False)
#matches = sift.match(d1, d2)
matches = sift.match_twosided(d1, d2)
print('{} matches'.format(len(matches.nonzero()[0])))
figure()
gray()
sift.plot_matches(im1, im2, l1, l2, matches, show_below=True)
show()
# -*- coding: utf-8 -*-
from PIL import Image
from pylab import *
from numpy import *
import os
from PCV.tools.imtools import get_imlist # 导入原书的PCV模块
import matplotlib.pyplot as plt # plt 用于显示图片
import matplotlib.image as mpimg # mpimg 用于读取图片
from PCV.localdescriptors import sift
filelist = get_imlist('data/')
# 输入的图片
im1f = 'fen1.jpg'
im1 = array(Image.open(im1f))
sift.process_image(im1f, 'out_sift_1.txt')
l1, d1 = sift.read_features_from_file('out_sift_1.txt')
i=0
num = [0]*30 #存放匹配值
for infile in filelist: # 对文件夹下的每张图片进行如下操作
im2 = array(Image.open(infile))
sift.process_image(infile, 'out_sift_2.txt')
l2, d2 = sift.read_features_from_file('out_sift_2.txt')
matches = sift.match_twosided(d1, d2)
num[i] = len(matches.nonzero()[0])
i=i+1
print ('{} matches'.format(num[i-1])) #输出匹配值
i=1
figure()
while i<4: #循环三次,输出匹配最多的三张图片
index=num.index(max(num))
print (index, filelist[index])
lena = mpimg.imread(filelist[index]) # 读取当前匹配最大值的图片
# 此时 lena 就已经是一个 np.array 了,可以对它进行任意处理
# lena.shape # (512, 512, 3)
subplot(1,3,i)
plt.imshow(lena) # 显示图片
plt.axis('off') # 不显示坐标轴
num[index] = 0 #将当前最大值清零
i=i+1
show()
# -*- coding: utf-8 -*-
from pylab import *
from PIL import Image
from PCV.localdescriptors import sift
from PCV.tools import imtools
import pydot
""" This is the example graph illustration of matching images from Figure 2-10.
To download the images, see ch2_download_panoramio.py."""
#download_path = "panoimages" # set this to the path where you downloaded the panoramio images
#path = "/FULLPATH/panoimages/" # path to save thumbnails (pydot needs the full system path)
download_path = "E:/ConputerVision_python/4self/ex2/data" # set this to the path where you downloaded the panoramio images
path = "E:/ConputerVision_python/4self/ex2/data/" # path to save thumbnails (pydot needs the full system path)
# list of downloaded filenames
imlist = imtools.get_imlist(download_path)
nbr_images = len(imlist)
# extract features
featlist = [imname[:-3] + '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: \n", 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 = 2 # 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:
# first image in pair
im = Image.open(imlist[i])
im.thumbnail((200, 200))
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((200, 200))
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('whitehouse.png')
import cv2
import numpy as np
import random
def compute_fundamental(x1, x2):
n = x1.shape[1]
if x2.shape[1] != n:
raise ValueError("Number of points don't match.")
# build matrix for equations
A = np.zeros((n, 9))
for i in range(n):
A[i] = [x1[0, i] * x2[0, i], x1[0, i] * x2[1, i], x1[0, i] * x2[2, i],
x1[1, i] * x2[0, i], x1[1, i] * x2[1, i], x1[1, i] * x2[2, i],
x1[2, i] * x2[0, i], x1[2, i] * x2[1, i], x1[2, i] * x2[2, i]]
# compute linear least square solution
U, S, V = np.linalg.svd(A)
F = V[-1].reshape(3, 3)
# constrain F
# make rank 2 by zeroing out last singular value
U, S, V = np.linalg.svd(F)
S[2] = 0
F = np.dot(U, np.dot(np.diag(S), V))
return F / F[2, 2]
def compute_fundamental_normalized(x1, x2):
""" Computes the fundamental matrix from corresponding points
(x1,x2 3*n arrays) using the normalized 4 point algorithm. """
# 从对应点计算基本矩阵(x1,x2 3*n数组)使用规范化的4点算法
n = x1.shape[1]
if x2.shape[1] != n:
raise ValueError("Number of points don't match.")
# normalize image coordinates
x1 = x1 / x1[2]
mean_1 = np.mean(x1[:2], axis=1)
S1 = np.sqrt(2) / np.std(x1[:2])
T1 = np.array([[S1, 0, -S1 * mean_1[0]], [0, S1, -S1 * mean_1[1]], [0, 0, 1]])
x1 = np.dot(T1, x1)
x2 = x2 / x2[2]
mean_2 = np.mean(x2[:2], axis=1)
S2 = np.sqrt(2) / np.std(x2[:2])
T2 = np.array([[S2, 0, -S2 * mean_2[0]], [0, S2, -S2 * mean_2[1]], [0, 0, 1]])
x2 = np.dot(T2, x2)
# compute F with the normalized coordinates
F = compute_fundamental(x1, x2)
# print (F)
# reverse normalization
F = np.dot(T1.T, np.dot(F, T2))
return F / F[2, 2]
def randSeed(good, num = 4):
'''
:param good: 初始的匹配点对
:param num: 选择随机选取的点对数量
:return: 4个点对list
'''
four_point = random.sample(good, num)
return four_point
def PointCoordinates(four_points, keypoints1, keypoints2):
'''
:param eight_points: 随机4点
:param keypoints1: 点坐标
:param keypoints2: 点坐标
:return:4个点
'''
x1 = []
x2 = []
tuple_dim = (1.,)
for i in four_points:
tuple_x1 = keypoints1[i[0].queryIdx].pt + tuple_dim
tuple_x2 = keypoints2[i[0].trainIdx].pt + tuple_dim
x1.append(tuple_x1)
x2.append(tuple_x2)
return np.array(x1, dtype=float), np.array(x2, dtype=float)
def ransac(good, keypoints1, keypoints2, confidence,iter_num):
Max_num = 0
good_F = np.zeros([3,3])
inlier_points = []
for i in range(iter_num):
four_points = randSeed(good)
x1,x2 = PointCoordinates(four_points, keypoints1, keypoints2)
F = compute_fundamental_normalized(x1.T, x2.T)
num, ransac_good = inlier(F, good, keypoints1, keypoints2, confidence)
if num > Max_num:
Max_num = num
good_F = F
inlier_points = ransac_good
print(Max_num, good_F)
return Max_num, good_F, inlier_points
def computeReprojError(x1, x2, F):
"""
计算投影误差
"""
ww = 1.0/(F[2,0]*x1[0]+F[2,1]*x1[1]+F[2,2])
dx = (F[0,0]*x1[0]+F[0,1]*x1[1]+F[0,2])*ww - x2[0]
dy = (F[1,0]*x1[0]+F[1,1]*x1[1]+F[1,2])*ww - x2[1]
return dx*dx + dy*dy
def inlier(F,good, keypoints1,keypoints2,confidence):
num = 0
ransac_good = []
x1, x2 = PointCoordinates(good, keypoints1, keypoints2)
for i in range(len(x2)):
line = F.dot(x1[i].T)
#在对极几何中极线表达式为[A B C],Ax+By+C=0, 方向向量可以表示为[-B,A]
line_v = np.array([-line[1], line[0]])
err = h = np.linalg.norm(np.cross(x2[i,:2], line_v)/np.linalg.norm(line_v))
# err = computeReprojError(x1[i], x2[i], F)
if abs(err) < confidence:
ransac_good.append(good[i])
num += 1
return num, ransac_good
if __name__ =='__main__':
im1 = 'milk1.jpg'
im2 = 'milk2.jpg'
print(cv2.__version__)
psd_img_1 = cv2.imread(im1, cv2.IMREAD_COLOR)
psd_img_2 = cv2.imread(im2, cv2.IMREAD_COLOR)
# 3) SIFT特征计算
sift = cv2.xfeatures2d.SIFT_create()
# find the keypoints and descriptors with SIFT
kp1, des1 = sift.detectAndCompute(psd_img_1, None)
kp2, des2 = sift.detectAndCompute(psd_img_2, None)
# FLANN 参数设计
match = cv2.BFMatcher()
matches = match.knnMatch(des1, des2, k=2)
# Apply ratio test
# 比值测试,首先获取与 A距离最近的点 B (最近)和 C (次近),
# 只有当 B/C 小于阀值时(0.75)才被认为是匹配,
# 因为假设匹配是一一对应的,真正的匹配的理想距离为0
good = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good.append([m])
print(good[0][0])
print("number of feature points:",len(kp1), len(kp2))
print(type(kp1[good[0][0].queryIdx].pt))
print("good match num:{} good match points:".format(len(good)))
for i in good:
print(i[0].queryIdx, i[0].trainIdx)
Max_num, good_F, inlier_points = ransac(good, kp1, kp2, confidence=30, iter_num=500)
# cv2.drawMatchesKnn expects list of lists as matches.
# img3 = np.ndarray([2, 2])
# img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, good[:10], img3, flags=2)
# cv2.drawMatchesKnn expects list of lists as matches.
img3 = cv2.drawMatchesKnn(psd_img_1,kp1,psd_img_2,kp2,good,None,flags=2)
img4 = cv2.drawMatchesKnn(psd_img_1,kp1,psd_img_2,kp2,inlier_points,None,flags=2)
cv2.namedWindow('image1', cv2.WINDOW_NORMAL)
cv2.namedWindow('image2', cv2.WINDOW_NORMAL)
cv2.imshow("image1",img3)
cv2.imshow("image2",img4)
cv2.waitKey(0)#等待按键按下
cv2.destroyAllWindows()#清除所有窗口
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
小结:图1、2、7、8、9、13、14需要提取的特征点应该主要在于水果上,但是由于背景网格篮子的干扰,以及水果本身纹理不是很明晰,导致提取到的特征点在背景上的比较多,水果上的特征点比较少比较稀疏。因此可以体现出sift算法的一个缺点是对边缘光滑的目标无法准确提取特征点。如果使用sift算法进行特征提取我们应该选取特征点明显的图像,且尽量避免背景干扰。但是对于改变图像尺度和方向(图1、2,13、14)对于特征提取的结果影响不明显,可以说sift算法提取特征具有尺度和旋转不变性。
对于其他图像,尤其是图10、11、12、15、16这些特征点明显的图像,sift算法就能较好的提取出其中的特征点。
相对于第三张Harris角点检测,sift算法能检测出更多的特征点,但是Harris角点检测同样对于纹理平坦的图像检测效果不佳。
1
2
3
小结:三组测试均为室外场景。如图1,两张图片的尺度和角度都有区别,光照程度也有所不同,这就导致了匹配的特征点有一些产生了错误,匹配出的结果也不是很理想,匹配度较低。但如图2,两张图片的尺度、光照一样,角度偏差不大,特征点明晰,因此匹配结果比较理想,连线对比较多而且大部分正确。图3匹配的特征点都比较正确,就是匹配的数量比较少,两张图片只是角度不同,应该会匹配出较多的特征点。
输入的图片:
输出的图片:
小结:计算输入图像与数据集中每张图像特征匹配值,并对求出的特征匹配值进行排序,选出值最高的三幅图像作为输出。其实只是在之前的代码上加了循环和判断,然后输出。
小结:对图像匹配的连接就是找到有较多匹配点的图像进行连接。这里设置的值为2,即如果两张图片有两个以上匹配点,他们之间就会有连线。在测试的过程中,例如第一列的匹配图像就是比较好的匹配结果,对比所有的图像匹配值,我发现好像类似建筑物这种纹理丰富,特征明显的图像找到的匹配值更多,但是就像倒数第二列的风车,在数据集中我也有放置不止两张图片,人眼观察很容易将其匹配,但是匹配出的值不是很乐观,没有在连线图中显示出来的几张图片都只有一个匹配值,我猜可能是因为风车太小了,而图像像素点比较少,导致一些易于判断的特征点并不能很好的被检测出来。
(1)sift更适用于图像特征检测,旋转不变性较强、可以解决一些光线和亮度问题,且比Harris角点检测灵活、匹配准确些。
(2)虽然sift算法能够解决旋转角度、光线亮度问题,但是也不是十分准确,对于旋转角度过大和光线亮度反差很大时,也会出现错误点匹配(甚至会出现找不到特征点),且sift算法对光线亮度方面没有旋转方面效果好。
(3)RANSAC的优点是它能鲁棒的估计模型参数。例如,它能从包含大量局外点的数据集中估计出高精度的参数。RANSAC的缺点是它计算参数的迭代次数没有上限;如果设置迭代次数的上限,得到的结果可能不是最优的结果,甚至可能得到错误的结果。