這裏介紹了圖像特徵檢測算法-SIFT的Python實現,並且介紹瞭如何在一組圖像中利用SIFT算法連接相互匹配的圖像。
1 參考資料
(1)Python計算機視覺編程
(2)SIFT算法詳解
2 描述子實現代碼
這裏使用開源工具包VLFeat提供的二進制文件來計算圖像的SIFT特徵。用完整的Python實現SIFT特徵的所有步驟可能效率不是很高。VLFeat工具包可以從http://www.vlfeat.org/下載,二進制文件可以在所有主要的平臺上運行。VLFeat庫是用C語言來寫的,但是我們可以使用該庫提供的命令行接口。以在Windows 10 64bit平臺上爲例,下載的文件爲vlfeat-0.9.20-bin.tar.gz,解壓縮後,將vlfeat-0.9.20/bin/win64文件夾下的sift.exe和vl.dll拷貝到當前工作目錄下。
代碼如下所示:
# -*- coding: utf-8 -*-
from PIL import Image
from pylab import *
from numpy import *
import os
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')
imname = 'empire.jpg'
im1 = array(Image.open(imname).convert('L'))
process_image(imname, 'empire.sift')
l1,d1 = read_features_from_file('empire.sift')
figure()
gray()
plot_features(im1, l1, circle=True)
show()
效果如下所示:
3 匹配描述子實現代碼
對於將一幅圖像中的特徵匹配到另一幅圖像的特徵,一種穩健的準則是使用這兩個特徵距離和兩個最匹配特徵距離的比率。相比於圖像中的其他特徵,該準則保證能夠找到足夠相似的唯一特徵。使用該方法可以使錯誤的匹配數降低。
# -*- coding: utf-8 -*-
from PIL import Image
from pylab import *
from numpy import *
import os
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')
# 示例
im1f = './data/crans_1_small.jpg'
im2f = './data/crans_2_small.jpg'
im1 = array(Image.open(im1f))
im2 = array(Image.open(im2f))
process_image(im1f, 'out_sift_1.txt')
l1,d1 = read_features_from_file('out_sift_1.txt')
figure()
gray()
subplot(121)
plot_features(im1, l1, circle=False)
process_image(im2f, 'out_sift_2.txt')
l2,d2 = read_features_from_file('out_sift_2.txt')
subplot(122)
plot_features(im2, l2, circle=False)
matches = match_twosided(d1, d2)
print '{} matches'.format(len(matches.nonzero()[0]))
figure()
gray()
plot_matches(im1,im2,l1,l2,matches, show_below=True)
show()
4 可視化連接圖像
首先通過圖像間是否具有匹配的局部描述子來定義圖像間的連接,然後可視化這些連接情況。爲了完成可視化,可以在圖中顯示這些圖像,圖的邊代表連接。這裏使用pydot工具包,該工具包是功能強大的GraphViz圖形庫的Python接口。
安裝時,需要先安裝graphviz-2.38.msi,再運行命令pip install pydot,最後可在系統路徑PATH中添加graphviz的路徑:C:\Program Files (x86)\Graphviz2.38\bin。注意:pydot的Node節點添加圖片時,圖片的路徑需要爲絕對路徑,且分隔符爲/。
# -*- coding: utf-8 -*-
from PIL import Image
from pylab import *
from numpy import *
import os
import pydot
import sift
def get_imlist(path):
return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.jpg')]
# pydot需要絕對路徑,路徑分隔符爲/而非\
download_path = "D:/Program/PythonTest/PythonTest/data/panoimages"
path = "D:/Program/PythonTest/PythonTest/data/panoimages/"
# list of downloaded filenames
imlist = 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 craete 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:
#圖像對中的第一幅圖像
im = Image.open(imlist[i])
im.thumbnail((100,100))
filename = path + str(i) + '.png'
im.save(filename) #需要一定大小的臨時文件
g.add_node(pydot.Node(str(i), fontcolor='transparent',
shape='rectangle', image=filename))
#圖像對中的第二幅圖像
im = Image.open(imlist[j])
im.thumbnail((100,100))
filename = path + str(j) + '.png'
im.save(filename) #需要一定大小的臨時文件
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')