计算机视觉--SIFT特征提取与检索
一、SIFT(尺度不变特征变换)介绍
1.1 SIFT原理
SIFT包括兴趣点检测器和描述子。SIFT描述子具有非常强的稳健性,经常和许多不同的兴趣点检测器结合使用。SIFT特征对于尺度,旋转和亮度都具有不变性,因此,它可用于三维视角和噪声的可靠匹配。
SIFT 描述子h(x, y,θ)是对特征点附近邻域内高斯图像梯度统计结果的一种表示,它是一个三维的阵列,但通常将它表示成一个矢量。矢量是通过对三维阵列按一定规律进行排列得到的。特征描述子与特征点所在的尺度有关,因此,对梯度的求取应在特征点对应的高斯图像上进行。
1.2 SIFT算法的实现步骤
- 构建尺度空间,检测极值点,获得尺度不变性;
- 特征点过滤并进行精确定位,剔除不稳定的特征点;
- 在特征点处提取特征描述符,为特征点分配方向值;
- 生成特征描述子,利用特征描述符寻找匹配点;
- 计算变换参数。
1.3 SIFT算法的数学表达
- 确定描述子采样区域
SIFI 描述子h(x, y,θ)是对特征点附近邻域内高斯图像梯度统计结果的一种表示。将特征点附近邻域划分成BpX Bp个子区域,每个子区域的尺寸为mσ个像元,其中,m=3,Bp=4。σ为特征点的尺度值。考虑到实际计算时,需要采用双线性插值,计算的图像区域为mσ(Bp+ 1)。如果再考虑旋转的因素,那么,实际计算的图像区域应大mσ(Bp+ 1)√2。
- 生成描述子
2.1 旋转图像至主方向
为了保证特征矢量具有旋转不变性,需要以特征点为中心,将特征点附近邻域内 ( m σ ( B P + 1 ) 2 × m σ ( B P + 1 ) 2 ) (m_{\sigma }(B_{P}+1)\sqrt{2}×m_{\sigma }(B_{P}+1)\sqrt{2}) (mσ(BP+1)2×mσ(BP+1)2) 图像梯度的位置和方向旋转一个方向角θ,即将原图像x轴转到与主方向相同的方向。旋转公式如下: ( x y ) = ( c o s Θ − s i n Θ s i n Θ c o s Θ ) ( x y ) \begin{pmatrix} x\\ y \end{pmatrix} =\begin{pmatrix} cos\Theta &-sin\Theta \\ sin\Theta & cos\Theta \end{pmatrix} \begin{pmatrix} x\\ y \end{pmatrix} (xy)=(cosΘsinΘ−sinΘcosΘ)(xy) 在特征点附近邻域图像梯度的位置和方向旋转后,再以特征点为中心,在旋转后的图像中取一个mσBp x mσBp大小的图像区域。并将它等间隔划分成Bp X Bp个子区域,每个间隔为mσ像元。
2.2 生成特征向量
在每子区域内计算8个方向的梯度方向直方图,绘制每个梯度方向的累加值,形成一个种子点。与求特征点主方向时有所不同,此时,每个子区域的梯度方向直方图将0° -360°划分为8个方向范围,每个范围为45°,这样,每个种子点共有8个方向的梯度强度信息。由于存在4X4(Bp X Bp)个子区域,所以,共有4X4X8=128个数据,最终形成128维的SIFT特征矢量。
- 归一化特征向量
为了去除光照变化的影响,需对上述生成的特征向量进行归一化处理,在归一化处理后,对特征矢量大于α的要进行截断处理,即大于α的值只取α,然后重新进行一次归一化处理,其目的是为了提高鉴别性。
二、SIFT特征提取与检索实验
2.1 数据集准备
介绍:该数据集取自家乡的田野拍摄,照片之间有些有重合场景,但每张照片都互不相同,共16张。
2.2 SIFT特征提取并展示特征点
原理介绍:SIFT是一种检测局部特征的算法,该算法通过求一幅图中的特征点及其有关scale 和 orientation 的描述子得到特征并进行图像特征点匹配,获得了良好效果。
代码实现
# -*- 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 = '../data/empire.jpg'
im = array(Image.open(imname).convert('L'))
sift.process_image(imname, 'empire.sift')
l1, d1 = sift.read_features_from_file('empire.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()
结果展示
SIFT特征提取与Harris角点提取的比较:
对数据集中每张图片SIFT特征提取,展示特征点:
分析
- 对以上数据集进行SIFT特征提取,可以看出不同尺度、亮度、旋转变化的图片检测出的特征点大致相同,因此,得出结论:SIFT特征是图像的局部特征,其对旋转、尺度缩放、亮度变化保持不变性,对视角变化、仿射变换、噪声也保持一定程度的稳定性。
- 对比SIFT特征提取与Harris角点检测结果,可以看出Harris角点检测算法提取出的特征点明显低于SIFT算法。对于比较平坦的区域,Harris角点检测算法能检测到的特征点很少甚至没有。故而,由于无法检测到足够的特征点,Harris角点检测算法在特征点有效性、计算时效性以及特征点相似不变性三个方面均不如SIFT算法显的高效。
2.3 SIFT特征匹配
原理介绍:SIFT特征匹配算法是一种基于尺度空间的、对图像缩放、旋转甚至仿射变换保持不变性的特征匹配算法。SIFT特征匹配算法分两个阶段来实现:第1阶段是SIFT特征的生成,即从多幅待匹配图像中提取出对尺度缩放、旋转、亮度变化无关的特征向量; 第2阶段是SIFT特征向量的匹配。
代码实现
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/c3.jpg'
im2f = '../data/c4.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()
结果展示
1)有匹配特征点:
如下图,特征点匹配数量为75.
尺度与光线变化,如下图,特征点匹配数量为6.
2)无匹配特征点:
如下图,特征点匹配数量为0.
分析
- 在尺度变化不是很大的情况下,如结果1)中第一组图片,石头、树丛、丝瓜架等物体的各特征点被检测出匹配的数量更多。而如果增大尺度与光线的对比,相对来说,检测到的匹配特征点少很多,如结果1)的第二组图片,仅检测出6个匹配的特征点。
- SIFT特征是基于物体上的一些局部外观的兴趣点而与影像的大小和旋转无关。对于光线、噪声、些微视角改变的容忍度高。从实验结果 1)可以看出,两幅图片中绝大多数的匹配特征点都被检测出来,可知SIFT匹配的准确度高。
- 在实验过程中,也出现过两张图片有同样场景,但很少甚至没有检测出匹配特征点的情况,初步判断是误差难以避免,SIFT算法也会存在错配、误配现象。
2.4 数据集内部检索匹配最多的图片
需要实现:给定一张输入的图片(数据集中没有),在数据集内部进行检索,输出与其匹配最多的三张图片。
原理介绍:将输入图片与数据集内每张图片进行两两SIFT特征匹配,将匹配值最高的三张图片输出。
代码实现
# -*- 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 用于读取图片
def process_image(imagename, resultname, params="--edge-thresh 10 --peak-thresh 5"):
""" 处理一幅图像,然后将结果保存在文件中"""
if imagename[-3:] != 'pgm':
# 创建一个pgm文件
im = Image.open(imagename).convert('L')
im.save('tmp.pgm')
imagename = 'tmp.pgm'
cmmd = str("sift " + imagename + " --output=" + resultname + " " + params)
os.system(cmmd)
print 'processed', imagename, 'to', resultname
def read_features_from_file(filename):
"""读取特征属性值,然后将其以矩阵的形式返回"""
f = loadtxt(filename)
return f[:, :4], f[:, 4:] # 特征位置,描述子
def write_featrues_to_file(filename, locs, desc):
"""将特征位置和描述子保存到文件中"""
savetxt(filename, hstack((locs, desc)))
def plot_features(im, locs, circle=False):
"""显示带有特征的图像
输入:im(数组图像),locs(每个特征的行、列、尺度和朝向)"""
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:
for p in locs:
draw_circle(p[:2], p[2])
else:
plot(locs[:, 0], locs[:, 1], 'ob')
axis('off')
def match(desc1, desc2):
"""对于第一幅图像中的每个描述子,选取其在第二幅图像中的匹配
输入:desc1(第一幅图像中的描述子),desc2(第二幅图像中的描述子)"""
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), 'int')
desc2t = desc2.T # 预先计算矩阵转置
for i in range(desc1_size[0]):
dotprods = dot(desc1[i, :], desc2t) # 向量点乘
dotprods = 0.9999 * dotprods
# 反余弦和反排序,返回第二幅图像中特征的索引
indx = argsort(arccos(dotprods))
# 检查最近邻的角度是否小于dist_ratio乘以第二近邻的角度
if arccos(dotprods)[indx[0]] < dist_ratio * arccos(dotprods)[indx[1]]:
matchscores[i] = int(indx[0])
return matchscores
def match_twosided(desc1, desc2):
"""双向对称版本的match()"""
matches_12 = match(desc1, desc2)
matches_21 = match(desc2, desc1)
ndx_12 = matches_12.nonzero()[0]
# 去除不对称的匹配
for n in ndx_12:
if matches_21[int(matches_12[n])] != n:
matches_12[n] = 0
return matches_12
def appendimages(im1, im2):
"""返回将两幅图像并排拼接成的一幅新图像"""
# 选取具有最少行数的图像,然后填充足够的空行
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)
return concatenate((im1, im2), axis=1)
def plot_matches(im1, im2, locs1, locs2, matchscores, show_below=True):
""" 显示一幅带有连接匹配之间连线的图片
输入:im1, im2(数组图像), locs1,locs2(特征位置),matchscores(match()的输出),
show_below(如果图像应该显示在匹配的下方)
"""
im3 = appendimages(im1, im2)
if show_below:
im3 = vstack((im3, im3))
imshow(im3)
cols1 = im1.shape[1]
for i in range(len(matchscores)):
if matchscores[i] > 0:
plot([locs1[i, 0], locs2[matchscores[i, 0], 0] + cols1], [locs1[i, 1], locs2[matchscores[i, 0], 1]], 'c')
axis('off')
# 获取project2_data文件夹下的图片文件名(包括后缀名)
filelist = get_imlist('../data/field/')
# 输入的图片
im1f = 'c3.jpg'
im1 = array(Image.open(im1f))
process_image(im1f, 'out_sift_1.txt')
l1, d1 = read_features_from_file('out_sift_1.txt')
i = 0
num = [0] * 30 # 存放匹配值
for infile in filelist: # 对文件夹下的每张图片进行如下操作
im2 = array(Image.open(infile))
process_image(infile, 'out_sift_2.txt')
l2, d2 = read_features_from_file('out_sift_2.txt')
matches = 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()
结果展示
分析
- 将输入图片与数据集中图片两两进行特征匹配后的匹配数保存在num中,要找到与输入图片匹配特征点最多的图只要找num矩阵中数值最大的三个位置即可。
- 从结果可看出在数据集中进行检索匹配得到的三张图片与输入图片重合度较高,其中明显的相同物体是丝瓜架和龙眼树,数据集中含有龙眼树的照片不多,但在本次检索中成功匹配出来,可见匹配精确度高。
- SIFT特征的信息量大,适合在海量数据库中快速准确匹配,准确度也相当高。
2.5 匹配地理标记
原理介绍:用前面用到的SIFT描述子进行匹配,将两两进行特征匹配后的匹配数保存在matchscores中。
代码实现
# -*- coding: utf-8 -*-
from pylab import *
from PIL import Image
from PCV.localdescriptors import sift
from PCV.tools import imtools
import pydot
import os
os.environ['PATH'] = os.environ['PATH'] + (';d:\\Alike\\python_dw\\graphviz_dw\\bin\\')
""" 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 = 'D:/Alike/python_dw/Code_a/admin_code/data/field' # set this to the path where you downloaded the panoramio images
path = 'D:/Alike/python_dw/Code_a/admin_code/data/field/' # 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((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('pic3.png')
结果展示
1)数据集中图片两两进行特征匹配的匹配数:
2)
分析
- 如结果1),matchscores矩阵保存的是数据集中图片两两进行匹配的匹配数,从结果中可以清晰地得到特征点匹配的数量,数据集中田野的图片相互之间在matchscores矩阵对应位置的数据不为0,表示有匹配的特征点;而田野的图片与房子的图片匹配数在matchscores矩阵可以看到为0,表示不匹配。如果同时观察matchscores矩阵与地理位置匹配结果图,可发现即使匹配数仅为1,也能形成连线。
- 如以上结果2),可以看到两组图像(右上角两张是单独的一组)。其中一组有比较复杂连线的是田野的图像,因为拍摄田野的时候图像间会取到重叠部分,所以最终匹配时全部都有关联。第二组由右上角的两幅图像组成,这两幅是另外拍摄的房子图片,在地理位置匹配中明显没有与田野图片相关联,即与田野图片的匹配特征点数量为0;而在两幅图像间自己能检测到匹配特征点,从而形成关联。总体来说,在本组数据集下,匹配准确率较高。
- 为了进一步检测匹配准确度,我重新准备了一组数据集,该数据集包含四种场景,分别是:田野、电源插座、尚大楼、茶盘,共15张图片,如下图所示:
匹配地理标记的结果为:
观察以上结果,发现田野场景下的四张图片成功检测出匹配特征点,形成关联,但是其他三个场景都没有完整的检测出所有匹配的图片,可见仍然存在误差。
三、RANSAC算法剔除错配
3.1 RANSAC原理
RANSAC通过反复选择数据中的一组随机子集,来达成目标。在OpenCV中滤除误匹配对,采用RANSAC算法寻找一个最佳单应性矩阵H,矩阵大小为3×3。RANSAC目的是找到最优的参数矩阵使得满足该矩阵的数据点个数最多,通常令h3×3=1来归一化矩阵。由于单应性矩阵有8个未知参数,至少需要8个线性方程求解,对应到点位置信息上,一组点对可以列出两个方程,则至少包含4组匹配点对。
s
[
x
′
y
′
1
]
=
[
h
11
h
12
h
13
h
21
h
22
h
23
h
31
h
32
h
33
]
[
x
y
1
]
s\begin{bmatrix} x'\\ y'\\ 1 \end{bmatrix} =\begin{bmatrix} h_{11} & h_{12} & h_{13}\\ h_{21} & h_{22} & h_{23} \\ h_{31} & h_{32} & h_{33} \end{bmatrix} \begin{bmatrix} x\\ y\\ 1 \end{bmatrix}
s⎣⎡x′y′1⎦⎤=⎣⎡h11h21h31h12h22h32h13h23h33⎦⎤⎣⎡xy1⎦⎤ 其中(x,y)表示目标图像角点位置,(x’,y’)为场景图像角点位置,s为尺度参数。
RANSAC算法从匹配数据集中随机抽出4个样本并保证这4个样本之间不共线,计算出单应性矩阵,然后利用这个模型测试所有数据,并计算满足这个模型数据点的个数与投影误差(即代价函数),若此模型为最优模型,则对应的代价函数最小。
∑
i
=
1
n
(
x
i
′
h
11
x
1
+
h
12
y
1
+
h
13
h
31
x
1
+
h
32
y
1
+
h
33
)
2
+
(
y
i
′
h
21
x
1
+
h
22
y
1
+
h
23
h
31
x
1
+
h
32
y
1
+
h
33
)
2
\sum_{i=1}^{n} (x_{i}'\frac{h_{11}x_{1}+h_{12}y_{1}+h_{13}}{h_{31}x_{1}+h_{32}y_{1}+h_{33}})^{2} +(y_{i}'\frac{h_{21}x_{1}+h_{22}y_{1}+h_{23}}{h_{31}x_{1}+h_{32}y_{1}+h_{33}})^{2}
i=1∑n(xi′h31x1+h32y1+h33h11x1+h12y1+h13)2+(yi′h31x1+h32y1+h33h21x1+h22y1+h23)2
3.2 RANSAC算法步骤
1.随机选择四对匹配特征
2.根据DLT计算单应矩阵 H (唯一解)
3.对所有匹配点,计算映射误差ε=||pi’, Hpi||
4.根据误差阈值,确定inliers(例如3-5像素)
5.针对最大inliers集合,重新计算单应矩阵H
3.3 RANSAC算法代码实现
代码说明:
- 首先,从匹配的点对中选择8个点,使用归一化8点法估算出基础矩阵H。代码中,
compute_fundamental
函数用八点法实现, x1、x2为匹配点集合,8点法传入的x1,x2分别为对应的8个匹配点;再用compute_fundamental_normalized
函数将匹配点进行归一化处理。 - 函数
randSeed
用于利用这4对匹配点,计算基础矩阵。 - 函数
ransac
为ransac算法实现,计算其余的点对到其对应对极线的距离dp ,如果dp≤d则该点为内点,否则为外点。记下符合该条件的内点的个数为num迭代k次,或者某次得到内点的数目num占有的比例大于等于95%,则停止。选择num最大的基础矩阵作为最终的结果。保存最优的结果。
# -*- coding: utf-8 -*-
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 8 point algorithm. """
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 = 8):
'''
:param good: 初始的匹配点对
:param num: 选择随机选取的点对数量
:return: 8个点对list
'''
eight_point = random.sample(good, num)
return eight_point
def PointCoordinates(eight_points, keypoints1, keypoints2):
'''
:param eight_points: 随机八点
:param keypoints1: 点坐标
:param keypoints2: 点坐标
:return:8个点
'''
x1 = []
x2 = []
tuple_dim = (1.,)
for i in eight_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):
eight_points = randSeed(good)
x1,x2 = PointCoordinates(eight_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 = '../data/data4/1.jpg'
im2 = '../data/data4/2.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()#清除所有窗口
3.4 不同实验场景结果展示及分析
3.4.1 场景一:景深丰富
原图:
控制台输出:
分析:
观察景深丰富的场景,在整个建筑群中,匹配出较多的特征点,可以看出经过RANSAC算法后没有明显的错误点对剔除,SIFT特征匹配检测出315对匹配点,RANSAC算法剔除后有312对,变化不大。由于画面中下部分匹配特征点对较为密集,不便于区分,这里主要观察处于较高水平的几栋建筑物。
如下图红色椭圆圈出部分,图a中的紫色匹配线在图b中没有出现,我们可以看出a中的紫色线连接的两点明显不是匹配特征点,说明SIFI特征匹配出现了错配,而经过RANSAC算法剔除后,可以删除错配。
但是,同样发现原本图a中红色椭圆圈出部分检测出5对正确匹配,可图b中只剩下4对,所以RANSAC算法也可能使正确匹配特征同步被删除掉。
再观察蓝色椭圆内的匹配点连线,发现是一对错配,可经过RANSAC算法后也仍然存在,误差不可避免,但总体来说,已经达到了非常理想的效果。
3.4.2 场景二:景深单一
原图:
控制台输出:
分析: 观察景深单一的场景,对比使用RANSAC算法剔除错配前后的两幅结果图,可以发现前者匹配点连线杂乱,存在较多对错配,比如建筑物上的特征点错配到草坪上。经RANSAC算法后,发现这时匹配点连线条数相对于之前少了很多,原先检测出84对匹配特征,现在只剩26对,部分正确匹配特征被删除;但是可以发现原本的错配几乎全部删除,最终结果匹配准确率高,体现了RANSAC算法的鲁棒性。
同一物体在不同景深场景的匹配
原图:
控制台输出:
分析:这组实验观察同一物体在不同景深场景的匹配效果,主要是取尚大楼近景与远景的图像做对比。观察到由于景深不同,尚大楼的边缘轮廓检测出的正确匹配特征点数较多,但SIFT特征匹配在建筑物的中部出现了错配现象,中间部分的匹配点连线较为杂乱。经RANSAC算法筛选后,剔除掉193-86=107对匹配特征,虽然仍然存在错配,但原先的错配已经被删去较多,总体效果不错。
3.5 小结
- 对比两种不同场景的结果,发现景深单一场景经过RANSAC算法剔除错配后得到的匹配准确度高于景深丰富的场景,原因是景深丰富的场景中的各物体彼此之间干扰更大,有可能对RANSAC算法造成影响。
- RANSAC算法能够有效地弥补SIFT特征匹配的缺陷,删除错配,但是同样能够看到,SIFT特征匹配能够得到更多匹配特征点,经RANSAC算法后,匹配点对会减少,一些正确匹配点对也有可能被同步删除。 所以经过筛选后的匹配点虽然精确度高,但也会削减SIFT特征匹配的效果。
- RANSAC的优点是它能鲁棒的估计模型参数。例如,它能从包含大量局外点的数据集中估计出高精度的参数。
- RANSAC的缺点是它计算参数的迭代次数没有上限;如果设置迭代次数的上限,得到的结果可能不是最优的结果,甚至可能得到错误的结果。RANSAC只有一定的概率得到可信的模型,概率与迭代次数成正比。RANSAC的另一个缺点是它要求设置跟问题相关的阈值。RANSAC只能从特定的数据集中估计出一个模型,如果存在两个(或多个)模型,RANSAC不能找到别的模型。
四、总结
4.1 实验内容总结
- SIFT特征不只具有尺度不变性,即使改变旋转角度,图像亮度或拍摄视角,仍然能够得到好的检测效果。
- SIFT独特性好,信息量丰富,适合于在海量特征数据库中进行快速、准确的匹配。
- SIFT有多量性,即使少数的几个物体也可以产生大量的SIFT特征向量。
- 相比于Harris角点检测,SIFT特征提取在特征点有效性、计算时效性以及特征点相似不变性几个方面都较为高效。
4.2 实验过程总结
问题1:import sift出错
解决:安装vlfeat,具体步骤为——
1.下载0.9.20版本的vlfeat.
2.把vIfeat文件夹 下win64中的sift.exe和v.dIl这两个文件复制到项目的文件夹中.
3.修改Anaconda文件夹下的PCV文件夹里面的localdescriptors文件夹中的sift.py文件,使用记事本打开,修改其中的cmmd内的路径为:cmmd =str(r"D:\PythonWork\SIFT\sift.exe "+ imagename+”-- output=" + resultname+" "+params)
(路径是你项目文件夹中的sift.exe的路径)然后要记得在括号内加r! !不然会出错! !
然后就能够运行了。如果在运行过程中提示关于print的错误,记得根据错误提醒的文件夹,去修改相应的print语法,3.5的python的print用法是需要加括号。
问题2:报错"dot" not found in path
解决:首先注意安装pydot过程——
(找了很多种方法,很多网址上的描述都不一样,我自己是这么做的:)
- 安装graphviz软件,地址在: graphviz下载.选择下载后缀为.msi的文件,下载后双击安装。
- 把安装后的graphviz软件的bin目录设为环境变量(保险起见,用户变量和系统变量的Path都要新建,具体如下)。
- 安装pydot:pip install pydot
(按道理到这应该就安好了,但是我还是会报错,所以又尝试了其他方法,下面的操作可能需要也可能不需要,这边一起写了) - 在Anaconda Prompt中执行以下三个语句:①
pip install graphviz
②pip install pydot
③pip install pydot-ng
发现这时pydot包已经成功导入,但是运行可视化接连的图片代码会出现"dot" not found in path的错误。
解决:①修改pydot.py中的代码——
将Dot类中的"dot"改成"dot.exe"
②回到我们的代码,在代码开头处添加下载的graphviz的bin目录位置,如下
import os
os.environ['PATH'] = os.environ['PATH'] + (';d:\\Alike\\python_dw\\graphviz_dw\\bin\\')
③注意:pydot的Node节点添加图片时,图片的路径需要为绝对路径,且分隔符为/。
问题3:报错’module’ object has no attribute ‘xfeatures2d’
解决:经查阅资料,发现是opencv版本不对,需要opencv3.x的版本,默认安装的版本为4.x。
在命令行中执行,到Scripts目录下:
1.卸载已有opencv:①pip uninstall opencv-python
②pip uninstall opencv-contrib-python
(有多少卸多少)
2.安装3.x版本的opencv:pip install opencv-contrib-python==3.2.0.7 -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
安装完成后问题解决。