上一節我們已經介紹了SIFT算法,SIFT算法對旋轉、尺度縮放、亮度變化等保持不變性,對視角變換、仿射變化、噪聲也保持一定程度的穩定性,是一種非常優秀的局部特征描述算法。但是其實時性相對不高。
SURF(Speeded Up Robust Features)算法改進了特征了提取和描述方式,用一種更為高效的方式完成特征點的提取和描述。
一 使用快速Hessian算法和SURF來提取和檢測特征
我們先用OpenCV庫函數演示一下快速Hessian算法和SURF來提取的效果,然后再來講述一下SURF算法的原理。
SURF特征檢測算法由Herbert Lowe於2006年發表,該算法比SIFT算法快好幾倍,它吸收了SIFT算法的思想。
SURF算法采用快速Hessian算法檢測關鍵點,而SURF算子會通過一個特征向量來描述關鍵點周圍區域的情況。這和SIFT算法很像,SIFT算法分別采用DoG和SIFT算子來檢測關鍵點和提取關鍵點的描述符。下面我們來演示一個例子:
# -*- coding: utf-8 -*- """ Created on Fri Aug 24 20:09:32 2018 @author: lenovo """ # -*- coding: utf-8 -*- """ Created on Wed Aug 22 16:53:16 2018 @author: lenovo """ ''' SURF算法 ''' import cv2 import numpy as np img = cv2.imread('./image/cali.bmp') img = cv2.resize(img,dsize=(600,400)) #轉換為灰度圖像 gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #創建一個SURF對象 surf = cv2.xfeatures2d.SURF_create(20000) #SIFT對象會使用Hessian算法檢測關鍵點,並且對每個關鍵點周圍的區域計算特征向量。該函數返回關鍵點的信息和描述符 keypoints,descriptor = surf.detectAndCompute(gray,None) print(type(keypoints),len(keypoints),keypoints[0]) print(descriptor.shape) #在圖像上繪制關鍵點 img = cv2.drawKeypoints(image=img,keypoints = keypoints,outImage=img,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) #顯示圖像 cv2.imshow('surf_keypoints',img) cv2.waitKey(0) cv2.destroyAllWindows()
我們把Hessian閾值設置為20000,閾值越高,能識別的特征就越少,因此可以采用試探法來得到最優檢測。
二 SURF算法原理
1、SURF特征檢測的步驟
- 尺度空間的極值檢測:搜索所有尺度空間上的圖像,通過Hessian來識別潛在的對尺度和選擇不變的興趣點。
- 特征點過濾並進行精確定位。
- 特征方向賦值:統計特征點圓形鄰域內的Harr小波特征。即在60度扇形內,每次將60度扇形區域旋轉0.2弧度進行統計,將值最大的那個扇形的方向作為該特征點的主方向。
- 特征點描述:沿着特征點主方向周圍的鄰域內,取$4×4$個矩形小區域,統計每個小區域的Haar特征,然后每個區域得到一個4維的特征向量。一個特征點共有64維的特征向量作為SURF特征的描述子。
2、構建Hessian(黑塞矩陣)
構建Hessian矩陣的目的是為了生成圖像穩定的邊緣點(突變點),跟Canny、拉普拉斯邊緣檢測的作用類似,為特征提取做准備。構建Hessian矩陣的過程對應着SIFT算法中的DoG過程。
黑塞矩陣(Hessian Matrix)是由一個多元函數的二階偏導數構成的方陣,描述了函數的局部曲率。由德國數學家Ludwin Otto Hessian於19世紀提出。
對於一個圖像$I(x,y)$,其Hessian矩陣如下:
$$H(I(x,y))=\begin{bmatrix} \frac{\partial^2I}{\partial{x^2}} & \frac{\partial^2I}{\partial{x}\partial{y}} \\ \frac{\partial^2I}{\partial{x}\partial{y}} & \frac{\partial^2I}{\partial{y^2}} \end{bmatrix}$$
H矩陣的判別式是:
$$Det(H)=\frac{\partial^2I}{\partial{x^2}}*\frac{\partial^2I}{\partial{y^2}}-\frac{\partial^2I}{\partial{x}\partial{y}} * \frac{\partial^2I}{\partial{x}\partial{y}}$$
在構建Hessian矩陣前需要對圖像進行高斯濾波,經過濾波后的Hessian矩陣表達式為:
$$H(x,y,\sigma)=\begin{bmatrix} L_{xx}(x,y,\sigma) & L_{xy}(x,y,\sigma) \\ L_{xy}(x,y,\sigma) & L_{yy}(x,y,\sigma) \end{bmatrix}$$
其中$(x,y)$為像素位置,$L(x,y,\sigma)=G(\sigma)*I(x,y)$,代表着圖像的高斯尺度空間,是由圖像和不同的高斯卷積得到。
我們知道在離散數學圖像中,一階導數是相鄰像素的灰度差:
$$L_x=L(x+1,y)-L(x,y)$$
二階導數是對一階導數的再次求導:
$$L_{xx}=[L(x+1,y)-L(x,y)]-[L(x,y)-L(x-1,y)]$$
$$=L(x+1,y)+L(x-1,y)-2L(x,y)$$
反過來看Hessian矩陣的判別式,其實就是當前點對水平方向二階偏導數乘以垂直方向二階偏導數再減去當前水平、垂直二階偏導的二次方:
$$Det(H)=L_{xx}*L_{yy}-L_{xy}*L_{xy}$$
通過這種方法可以為圖像中每個像素計算出其H行列式的決定值,並用這個值來判別圖像局部特征點。Hession矩陣判別式中的$L(x,y)$是原始圖像的高斯卷積,由於高斯核服從正太分布,從中心點往外,系數越來越小,為了提高運算速度,SURF算法使用了盒式濾波器來替代高斯濾波器$L$,所以在$L_{xy}$上乘了一個加權系數0.9,目的是為了平衡因使用盒式濾波器近似所帶來的誤差,則H矩陣判別式可表示為:
$$Det(H)=L_{xx}*L_{yy}-(0.9*L_{xy})^2$$
盒式濾波器和高斯濾波器的示意圖如下:
上面兩幅圖是$9×9$高斯濾波器模板分別在圖像垂直方向上二階導數$L_{yy}$和$L_{xy}$對應的值,下邊兩幅圖是使用盒式濾波器對其近似,灰色部分的像素值為0,黑色為-2,白色為1.
那么為什么盒式濾波器可以提高運算速度呢?這就涉及到積分圖的使用,盒式濾波器對圖像的濾波轉化成計算圖像上不同區域間像素的加減運算問題,這正是積分圖的強項,只需要簡單積分查找積分圖就可以完成。
3、構造尺度空間
同SIFT算法一樣,SURF算法的尺度空間由$O$組$S$層組成,不同的是,SIFT算法下一組圖像的長寬均是上一組的一半,同一組不同層圖像之間尺寸一樣,但是所使用的尺度空間因子(高斯模糊系數$\sigma$)逐漸增大;而在SURF算法中,不同組間圖像的尺寸都是一致的,不同的是不同組間使用的盒式濾波器的模板尺寸逐漸增大,同一組不同層圖像使用相同尺寸的濾波器,但是濾波器的尺度空間因子逐漸增大。如下圖所示:
4、特征點過濾並進行精確定位
SURF特征點的定位過程和SIFT算法一致,將經過Hessian矩陣處理的每個像素點(即獲得每個像素點Hessian矩陣的判別式值)與其圖像域(相同大小的圖像)和尺度域(相鄰的尺度空間)的所有相鄰點進行比較,當其大於(或者小於)所有相鄰點時,該點就是極值點。如圖所示,中間的檢測點要和其所在圖像的$3×3$鄰域8個像素點,以及其相鄰的上下兩層$3×3$鄰域18個像素點,共26個像素點進行比較。
初步定位出特征點后,再經過濾除能量比較弱的關鍵點以及錯誤定位的關鍵點,篩選出最終的穩定的特征點。
5、計算特征點主方向
SIFT算法特征點的主方向是采用在特征點鄰域內統計其梯度直方圖,橫軸是梯度方向的角度,縱軸是梯度方向對應梯度幅值的累加,取直方圖bin最大的以及超過最大80%的那些方向作為特征點的主方向。
而在SURF算法中,采用的是統計特征點圓形鄰域內的Harr小波特征,即在特征點的圓形鄰域內,統計60度扇形內所有點的水平、垂直Harr小波特征總和,然后扇形以0.2弧度大小的間隔進行旋轉並再次統計該區域內Harr小波特征值之后,最后將值最大的那個扇形的方向作為該特征點的主方向。該過程示意圖如下:
Harr特征的具體內容可以參考第九節、人臉檢測之Haar分類器。
6、生成特征描述
在SIFT算法中,為了保證特征矢量的旋轉不變性,先以特征點為中心,在附近鄰域內將坐標軸旋轉$\theta$(特征點的主方向)角度,然后提取特征點周圍$4×4$個區域塊,統計每小塊內8個梯度方向,這樣一個關鍵點就可以產生128維的SIFT特征向量。
SURF算法中,也是提取特征點周圍$4×4$個矩形區域塊,但是所取得矩形區域方向是沿着特征點的主方向,而不是像SIFT算法一樣,經過旋轉$\theta$角度。每個子區域統計25個像素點水平方向和垂直方向的Haar小波特征,這里的水平和垂直方向都是相對主方向而言的。該Harr小波特征為水平方向值之和、垂直方向值之和、水平方向值絕對值之和以及垂直方向絕對之和4個方向。該過程示意圖如下:
把這4個值作為每個子塊區域的特征向量,所以一共有$4×4×4$=64維向量作為SURF特征的描述子,比SIFT特征的描述子減少了一半。
三 特征點匹配
與SIFT特征點匹配類似,SURF也是通過計算兩個特征點間特征向量的歐氏距離來確定匹配度,歐式距離越短,代表兩個特征點的匹配度越好。不同的是SURF還加入了Hessian矩陣跡(矩陣特征值的和)的判斷,如果兩個特征點的矩陣跡正負號相同,代表着兩個特征點具有相同方向上的對比度變化,如果不同,說明這兩個特征點的對比度方向是相反的,即使歐氏距離為0,也直接剔除。
# -*- coding: utf-8 -*- """ Created on Fri Aug 24 20:09:32 2018 @author: lenovo """ # -*- coding: utf-8 -*- """ Created on Wed Aug 22 16:53:16 2018 @author: lenovo """ ''' SURF算法 ''' import cv2 import numpy as np '''1、加載圖片''' img1 = cv2.imread('./image/cali1.bmp',cv2.IMREAD_GRAYSCALE) img1 = cv2.resize(img1,dsize=(600,400)) img2 = cv2.imread('./image/cali2.bmp',cv2.IMREAD_GRAYSCALE) img2 = cv2.resize(img2,dsize=(600,400)) image1 = img1.copy() image2 = img2.copy() '''2、提取特征點''' #創建一個SURF對象 surf = cv2.xfeatures2d.SURF_create(25000) #SIFT對象會使用Hessian算法檢測關鍵點,並且對每個關鍵點周圍的區域計算特征向量。該函數返回關鍵點的信息和描述符 keypoints1,descriptor1 = surf.detectAndCompute(image1,None) keypoints2,descriptor2 = surf.detectAndCompute(image2,None) print('descriptor1:',descriptor1.shape,'descriptor2',descriptor2.shape) #在圖像上繪制關鍵點 image1 = cv2.drawKeypoints(image=image1,keypoints = keypoints1,outImage=image1,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) image2 = cv2.drawKeypoints(image=image2,keypoints = keypoints2,outImage=image2,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) #顯示圖像 cv2.imshow('surf_keypoints1',image1) cv2.imshow('surf_keypoints2',image2) cv2.waitKey(20) '''3、特征點匹配''' matcher = cv2.FlannBasedMatcher() matchePoints = matcher.match(descriptor1,descriptor2) print(type(matchePoints),len(matchePoints),matchePoints[0]) #提取強匹配特征點 minMatch = 1 maxMatch = 0 for i in range(len(matchePoints)): if minMatch > matchePoints[i].distance: minMatch = matchePoints[i].distance if maxMatch < matchePoints[i].distance: maxMatch = matchePoints[i].distance print('最佳匹配值是:',minMatch) print('最差匹配值是:',maxMatch) #獲取排雷在前邊的幾個最優匹配結果 goodMatchePoints = [] for i in range(len(matchePoints)): if matchePoints[i].distance < minMatch + (maxMatch-minMatch)/16: goodMatchePoints.append(matchePoints[i]) #繪制最優匹配點 outImg = None outImg = cv2.drawMatches(img1,keypoints1,img2,keypoints2,goodMatchePoints,outImg,matchColor=(0,255,0),flags=cv2.DRAW_MATCHES_FLAGS_DEFAULT) cv2.imshow('matche',outImg) cv2.waitKey(0) cv2.destroyAllWindows()
我們來看一看特征點匹配效果,可以看到好多點都匹配錯誤,這主要與我選擇的圖片有關,由於我選擇的圖片是用來做相機標點的,而當我們使用SURF算法提取特征點,圖片上大部分特征點都具有相同的性質,特征向量也近似相等,因此在匹配時會出現很大的誤差。
下面我們更換兩張圖片,再次進行特征點匹配:
我們可以看到這個匹配效果比剛才好了不少,而且我對Hessian閾值也進行了修改,這個值需要自己不斷的調整,以達到自己的期望。但是總體上來看,我們選擇的這兩幅圖片亮度和對比度差異都是很大的,而且拍攝所使用的相機也是不同的,左側是我自己用手機拍攝到的,右側是從網上下載的,匹配能有這樣的效果也已經不錯了。但是如果我們想達到更高的匹配度,我們應該盡量選擇兩張更為相似的圖片。下面是我稍微修改后的代碼:
# -*- coding: utf-8 -*- """ Created on Fri Aug 24 20:09:32 2018 @author: lenovo """ # -*- coding: utf-8 -*- """ Created on Wed Aug 22 16:53:16 2018 @author: lenovo """ ''' SURF算法 ''' import cv2 '''1、加載圖片''' img1 = cv2.imread('./image/match1.jpg') img1 = cv2.resize(img1,dsize=(600,400)) gray1 = cv2.cvtColor(img1,cv2.COLOR_BGR2GRAY) img2 = cv2.imread('./image/match2.jpg') img2 = cv2.resize(img2,dsize=(600,400)) gray2 = cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY) image1 = gray1.copy() image2 = gray2.copy() '''2、提取特征點''' #創建一個SURF對象 surf = cv2.xfeatures2d.SURF_create(10000) #SIFT對象會使用Hessian算法檢測關鍵點,並且對每個關鍵點周圍的區域計算特征向量。該函數返回關鍵點的信息和描述符 keypoints1,descriptor1 = surf.detectAndCompute(image1,None) keypoints2,descriptor2 = surf.detectAndCompute(image2,None) print('descriptor1:',descriptor1.shape,'descriptor2',descriptor2.shape) #在圖像上繪制關鍵點 image1 = cv2.drawKeypoints(image=image1,keypoints = keypoints1,outImage=image1,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) image2 = cv2.drawKeypoints(image=image2,keypoints = keypoints2,outImage=image2,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) #顯示圖像 cv2.imshow('surf_keypoints1',image1) cv2.imshow('surf_keypoints2',image2) cv2.waitKey(20) '''3、特征點匹配''' matcher = cv2.FlannBasedMatcher() matchePoints = matcher.match(descriptor1,descriptor2) print(type(matchePoints),len(matchePoints),matchePoints[0]) #提取強匹配特征點 minMatch = 1 maxMatch = 0 for i in range(len(matchePoints)): if minMatch > matchePoints[i].distance: minMatch = matchePoints[i].distance if maxMatch < matchePoints[i].distance: maxMatch = matchePoints[i].distance print('最佳匹配值是:',minMatch) print('最差匹配值是:',maxMatch) #獲取排雷在前邊的幾個最優匹配結果 goodMatchePoints = [] for i in range(len(matchePoints)): if matchePoints[i].distance < minMatch + (maxMatch-minMatch)/4: goodMatchePoints.append(matchePoints[i]) #繪制最優匹配點 outImg = None outImg = cv2.drawMatches(img1,keypoints1,img2,keypoints2,goodMatchePoints,outImg,matchColor=(0,255,0),flags=cv2.DRAW_MATCHES_FLAGS_DEFAULT) cv2.imshow('matche',outImg) cv2.waitKey(0) cv2.destroyAllWindows()
參考文章:
[2]SURF算法
[5]SURF原理及源碼解析(C++)