轉自知乎
前言
提到傳統目標識別,就不得不提SIFT算法,Scale-invariant feature transform,中文含義就是尺度不變特征變換。此方法由David Lowe於1999年發表於ICCV(International Conference on Computer Vision),並經過5年的整理和晚上,在2004年發表於IJCV(International journal of computer vision)。由於在此之前的目標檢測算法對圖片的大小、旋轉非常敏感,而SIFT算法是一種基於局部興趣點的算法,因此不僅對圖片大小和旋轉不敏感,而且對光照、噪聲等影響的抗擊能力也非常優秀,因此,該算法在性能和適用范圍方面較於之前的算法有着質的改變。這使得該算法對比於之前的算法有着明顯的優勢,所以,一直以來它都在目標檢測和特征提取方向占據着重要的地位,截止2019年6月19日,這篇文章的引用量已經達到51330次(谷歌學術),受歡迎程度可見一斑,本文就詳細介紹一下這篇文章的原理,並一步一步編程實現本算法,讓各位對這個算法有更清晰的認識和理解。
SIFT
前面提到,SIFT是一個非常經典而且受歡迎的特征描述算法,因此關於這篇文章的學習資料、文章介紹自然非常多。但是很多文章都相當於把原文翻譯一遍,花大量篇幅在講高斯模糊、尺度空間理論、高斯金字塔等內容,容易讓人雲里霧里,不知道這種算法到底在講什么?重點又在哪里?
圖1 SIFT算法步驟
其實下載這篇文章之后打開看一下會發現,SIFT的思想並沒有想的那么復雜,它主要包含4個步驟:
- 尺度空間極值檢測:通過使用高斯差分函數來計算並搜索所有尺度上的圖像位置,用於識別對尺度和方向不變的潛在興趣點。
- 關鍵點定位:通過一個擬合精細的模型在每個候選位置上確定位置和尺度,關鍵點的選擇依賴於它們的穩定程度。
- 方向匹配:基於局部圖像的梯度方向,為每個關鍵點位置分配一個或多個方向,后續所有對圖像數據的操作都是相對於關鍵點的方向、尺度和位置進行變換,從而而這些變換提供了不變形。
- 關鍵點描述:這個和HOG算法有點類似之處,在每個關鍵點周圍的區域內以選定的比例計算局部圖像梯度,這些梯度被變換成一種表示,這種表示允許比較大的局部形狀的變形和光照變化。
由於它將圖像數據轉換為相對於局部特征的尺度不變坐標,因此這種方法被稱為尺度不變特征變換。
如果對這個算法思路進行簡化,它就包括2個部分:
- 特征提取
- 特征描述
特征提取
特征點檢測主要分為如下兩個部分,
- 候選關鍵點
- 關鍵點定位
候選關鍵點
Koenderink(1984)和Lindeberg(1994)已經證明,在各種合理的假設下,高斯函數是唯一可能的尺度空間核。因此,圖像的尺度空間被定義為函數,它是由一個可變尺度的高斯核和輸入圖像生成, 其中高斯核為, 為了有效檢測尺度空間中穩定的極點,Lowe於1999年提出在高斯差分函數(DOG)中使用尺度空間極值與圖像做卷積,這可以通過由常數乘法因子分隔的兩個相鄰尺度的差來計算。用公式表示就是, 由於平滑區域臨近像素之間變化不大,但是在邊、角、點這些特征較豐富的地方變化較大,因此通過DOG比較臨近像素可以檢測出候選關鍵點。
關鍵點定位
檢測出候選關鍵點之后,下一步就是通過擬合驚喜的模型來確定位置和尺度。 2002年Brown提出了一種用3D二次函數來你和局部樣本點,來確定最大值的插值位置,實驗表明,這使得匹配和穩定性得到了實質的改進。 他的具體方法是對函數進行泰勒展開, 上述的展開式,就是所要的擬合函數。 極值點的偏移量為, 如果偏移量在任何一個維度上大於0.5時,則認為插值中心已經偏移到它的鄰近點上,所以需要改變當前關鍵點的位置,同時在新的位置上重復采用插值直到收斂為止。如果超出預先設定的迭代次數或者超出圖像的邊界,則刪除這個點。
特征描述
前面講了一些有關特征點檢測的內容,但是SIFT實質的內容和價值並不在於特征點的檢測,而是特征描述思想,這是它的核心所在,特征點描述主要包括如下兩點:
- 方向分配
- 局部特征描述
方向分配
根據圖像的圖像,可以為每個關鍵定指定一個基准方向,可以相對於這個指定方向表示關鍵點的描述符,從而實現了圖像的旋轉不變性。 關鍵點的尺度用於選擇尺度最接近的高斯平滑圖像,使得計算是以尺度不變的方式執行,對每個圖像,分別計算它的梯度幅值和梯度方向, 然后,使用方向直方圖統計關鍵點鄰域內的梯度幅值和梯度方向。將0~360度划分成36個區間,每個區間為10度,統計得出的直方圖峰值代表關鍵點的主方向。
局部特征描述
通過前面的一系列操作,已經獲得每個關鍵點的位置、尺度、方向,接下來要做的就是用已知特征向量把它描述出來,這是圖像特征提取的核心部分。為了避免對光照、視角等因素的敏感性,需要特征描述子不僅僅包含關鍵點,還要包含它的鄰域信息。
SIFT使用的特征描述子和后面要講的HOG有很多相似之處。它一檢測得到的關鍵點為中心,選擇一個16*16的鄰域,然后再把這個鄰域再划分為4*4的子區域,然后對梯度方向進行划分成8個區間,這樣在每個子區域內疚會得到一個4*4*8=128維的特征向量,向量元素大小為每個梯度方向區間權值。提出得到特征向量后要對鄰域的特征向量進行歸一化,歸一化的方向是計算鄰域關鍵點的主方向,並將鄰域旋轉至根據主方向旋轉至特定方向,這樣就使得特征具有旋轉不變性。然后再根據鄰域內各像素的大小把鄰域縮放到指定尺度,進一步使得特征描述子具有尺度不變性。
以上就是SIFT算法的核心部分。
編程實踐
本文代碼已經放在github,感興趣的可以自行查看,
https://github.com/jakpopc/aiLearnNotes/blob/master/computer_vision/SIFT.py
本文實現SIFT特征檢測主要基於以下工具包:
- OpenCV
- numpy
其中OpenCV是一個非常知名且受歡迎的跨平台計算機視覺庫,它不僅包含常用的圖像讀取、顯示、顏色變換,還包含一些為人熟知的經典特征檢測算法,其中就包括SIFT,所以本文使用OpenCV進行讀取和SIFT特征檢測。 numpy是一個非常優秀的數值計算庫,也常用於圖像的處理,這里使用numpy主要用於圖像的拼接和顯示。
導入工具包
import numpy as np import cv2
圖像准備
首先寫一下讀取圖像的函數,
def load_image(path, gray=True): if gray: img = cv2.imread(path) return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) else: return cv2.imread(path)
然后,生成一副對原圖進行變換的圖像,用於后面特征匹配,本文選擇對圖像進行垂直鏡像變換,
def transform(origin): h, w = origin.shape generate_img = np.zeros(origin.shape) for i in range(h): for j in range(w): generate_img[i, w - 1 - j] = origin[i, j] return generate_img.astype(np.uint8)
顯示一下圖像變換的結果,
img1 = load_image('2007_002545.jpg') img2 = transform(img1) combine = np.hstack((img1, img2)) cv2.imshow("gray", combine) cv2.waitKey(0)
先用 xfeatures2d 模塊實例化一個sift算子,然后使用 detectAndCompute 計算關鍵點和描述子,隨后再用 drawKeypoints 繪出關鍵點,
# 實例化 sift = cv2.xfeatures2d.SIFT_create() # 計算關鍵點和描述子 # 其中kp為關鍵點keypoints # des為描述子descriptors kp1, des1 = sift.detectAndCompute(img1, None) kp2, des2 = sift.detectAndCompute(img2, None) # 繪出關鍵點 # 其中參數分別是源圖像、關鍵點、輸出圖像、顯示顏色 img3 = cv2.drawKeypoints(img1, kp1, img1, color=(0, 255, 255)) img4 = cv2.drawKeypoints(img2, kp2, img2, color=(0, 255, 255))
顯示出檢測的關鍵點為,
關鍵點已經檢測出來,最后一步要做的就是繪出匹配效果,本文用到的是利用 FlannBasedMatcher 來顯示匹配效果, 首先要對 FlannBasedMatcher 進行參數設計和實例化,然后用 *knn 對前面計算的出的特征描述子進行匹配,最后利用 drawMatchesKnn 顯示匹配效果,
# 參數設計和實例化 index_params = dict(algorithm=1, trees=6) search_params = dict(checks=50) flann = cv2.FlannBasedMatcher(index_params, search_params) # 利用knn計算兩個描述子的匹配 matche = flann.knnMatch(des1, des2, k=2) matchesMask = [[0, 0] for i in range(len(matche))] # 繪出匹配效果 result = [] for m, n in matche: if m.distance < 0.6 * n.distance: result.append([m]) img5 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, matche, None, flags=2) cv2.imshow("MatchResult", img5) cv2.waitKey(0)
檢測結果,
完整代碼如下,
import numpy as np import cv2 def load_image(path, gray=False): if gray: img = cv2.imread(path) return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) else: return cv2.imread(path) def transform(origin): h, w, _ = origin.shape generate_img = np.zeros(origin.shape) for i in range(h): for j in range(w): generate_img[i, w - 1 - j] = origin[i, j] return generate_img.astype(np.uint8) def main(): img1 = load_image('2007_002545.jpg') img2 = transform(img1) # 實例化 sift = cv2.xfeatures2d.SIFT_create() # 計算關鍵點和描述子 # 其中kp為關鍵點keypoints # des為描述子descriptors kp1, des1 = sift.detectAndCompute(img1, None) kp2, des2 = sift.detectAndCompute(img2, None) # 繪出關鍵點 # 其中參數分別是源圖像、關鍵點、輸出圖像、顯示顏色 img3 = cv2.drawKeypoints(img1, kp1, img1, color=(0, 255, 255)) img4 = cv2.drawKeypoints(img2, kp2, img2, color=(0, 255, 255)) # 參數設計和實例化 index_params = dict(algorithm=1, trees=6) search_params = dict(checks=50) flann = cv2.FlannBasedMatcher(index_params, search_params)