高斯拉普拉斯(Laplace of Gaussian)
Laplace算子作爲一種優秀的邊緣檢測算子,在邊緣檢測中得到了廣泛的應用。該方法通過對圖像求圖像的二階倒數的零交叉點來實現邊緣的檢測,公式表示如下:
由於Laplace算子是通過對圖像進行微分操作實現邊緣檢測的,所以對離散點和噪聲比較敏感。於是,首先對圖像進行高斯卷積濾波進行降噪處理,再採用Laplace算子進行邊緣檢測,就可以提高算子對噪聲和離散點的魯棒性,如此,拉普拉斯高斯算子Log(Laplace of Gaussian)就誕生了。
高斯卷積(Gaussian convolution ),高斯函數的表達式如下:
原圖像與高斯卷積的表達式如下:
因爲:
所以Log可以通過先對高斯函數進行偏導操作,然後進行卷積求解,公式表示如下:
2D高斯拉普拉斯算子可以通過任何一個方形核進行逼近,只要保證該核的所有元素的和或均值爲0,如下一個5×5的核進行逼近:
高斯拉普拉斯邊緣檢測算法的步驟:
1)對原圖像進行Log卷積。
2)檢測圖像中的過零點( Zero Crossings,也即從負到正或從正到負)。
3)對過零點進行閾值化。
尺度不變特徵變換匹配算法詳解
Scale Invariant Feature Transform(SIFT)
Just For Fun
zdd [email protected]
對於初學者,從David G.Lowe的論文到實現,有許多鴻溝,本文幫你跨越。
1、SIFT綜述
尺度不變特徵轉換(Scale-invariant feature transform或SIFT)是一種電腦視覺的算法用來偵測與描述影像中的局部性特徵,它在空間尺度中尋找極值點,並提取出其位置、尺度、旋轉不變量,此算法由 David Lowe在1999年所發表,2004年完善總結。
其應用範圍包含物體辨識、機器人地圖感知與導航、影像縫合、3D模型建立、手勢辨識、影像追蹤和動作比對。
此算法有其專利,專利擁有者爲英屬哥倫比亞大學。
局部影像特徵的描述與偵測可以幫助辨識物體,SIFT 特徵是基於物體上的一些局部外觀的興趣點而與影像的大小和旋轉無關。對於光線、噪聲、些微視角改變的容忍度也相當高。基於這些特性,它們是高度顯著而且相對容易擷取,在母數龐大的特徵數據庫中,很容易辨識物體而且鮮有誤認。使用 SIFT特徵描述對於部分物體遮蔽的偵測率也相當高,甚至只需要3個以上的SIFT物體特徵就足以計算出位置與方位。在現今的電腦硬件速度下和小型的特徵數據庫條件下,辨識速度可接近即時運算。SIFT特徵的信息量大,適合在海量數據庫中快速準確匹配。
SIFT算法的特點有:
1. SIFT特徵是圖像的局部特徵,其對旋轉、尺度縮放、亮度變化保持不變性,對視角變化、仿射變換、噪聲也保持一定程度的穩定性;
2. 獨特性(Distinctiveness)好,信息量豐富,適用於在海量特徵數據庫中進行快速、準確的匹配;
3. 多量性,即使少數的幾個物體也可以產生大量的SIFT特徵向量;
4. 高速性,經優化的SIFT匹配算法甚至可以達到實時的要求;
5. 可擴展性,可以很方便的與其他形式的特徵向量進行聯合。
SIFT算法可以解決的問題:
目標的自身狀態、場景所處的環境和成像器材的成像特性等因素影響圖像配準/目標識別跟蹤的性能。而SIFT算法在一定程度上可解決:
1. 目標的旋轉、縮放、平移(RST)
2. 圖像仿射/投影變換(視點viewpoint)
3. 光照影響(illumination)
4. 目標遮擋(occlusion)
5. 雜物場景(clutter)
6. 噪聲
SIFT算法的實質是在不同的尺度空間上查找關鍵點(特徵點),並計算出關鍵點的方向。SIFT所查找到的關鍵點是一些十分突出,不會因光照,仿射變換和噪音等因素而變化的點,如角點、邊緣點、暗區的亮點及亮區的暗點等。
Lowe將SIFT算法分解爲如下四步:
1. 尺度空間極值檢測:搜索所有尺度上的圖像位置。通過高斯微分函數來識別潛在的對於尺度和旋轉不變的興趣點。
2. 關鍵點定位:在每個候選的位置上,通過一個擬合精細的模型來確定位置和尺度。關鍵點的選擇依據於它們的穩定程度。
3. 方向確定:基於圖像局部的梯度方向,分配給每個關鍵點位置一個或多個方向。所有後面的對圖像數據的操作都相對於關鍵點的方向、尺度和位置進行變換,從而提供對於這些變換的不變性。
4. 關鍵點描述:在每個關鍵點周圍的鄰域內,在選定的尺度上測量圖像局部的梯度。這些梯度被變換成一種表示,這種表示允許比較大的局部形狀的變形和光照變化。
本文沿着Lowe的步驟,參考Rob Hess及Andrea Vedaldi源碼,詳解SIFT算法的實現過程。
2、高斯模糊
SIFT算法是在不同的尺度空間上查找關鍵點,而尺度空間的獲取需要使用高斯模糊來實現,Lindeberg等人已證明高斯卷積核是實現尺度變換的唯一變換核,並且是唯一的線性核。本節先介紹高斯模糊算法。
2.1二維高斯函數
高斯模糊是一種圖像濾波器,它使用正態分佈(高斯函數)計算模糊模板,並使用該模板與原圖像做卷積運算,達到模糊圖像的目的。
N維空間正態分佈方程爲:
(1-1)
其中,是正態分佈的標準差,值越大,圖像越模糊(平滑)。r爲模糊半徑,模糊半徑是指模板元素到模板中心的距離。如二維模板大小爲m*n,則模板上的元素(x,y)對應的高斯計算公式爲:
(1-2)
在二維空間中,這個公式生成的曲面的等高線是從中心開始呈正態分佈的同心圓,如圖2.1所示。分佈不爲零的像素組成的卷積矩陣與原始圖像做變換。每個像素的值都是周圍相鄰像素值的加權平均。原始像素的值有最大的高斯分佈值,所以有最大的權重,相鄰像素隨着距離原始像素越來越遠,其權重也越來越小。這樣進行模糊處理比其它的均衡模糊濾波器更高地保留了邊緣效果。
理論上來講,圖像中每點的分佈都不爲零,這也就是說每個像素的計算都需要包含整幅圖像。在實際應用中,在計算高斯函數的離散近似時,在大概3σ距離之外的像素都可以看作不起作用,這些像素的計算也就可以忽略。通常,圖像處理程序只需要計算的矩陣就可以保證相關像素影響。
2.2 圖像的二維高斯模糊
根據σ的值,計算出高斯模板矩陣的大小(),使用公式(1-2)計算高斯模板矩陣的值,與原圖像做卷積,即可獲得原圖像的平滑(高斯模糊)圖像。爲了確保模板矩陣中的元素在[0,1]之間,需將模板矩陣歸一化。5*5的高斯模板如表2.1所示。
下圖是5*5的高斯模板卷積計算示意圖。高斯模板是中心對稱的。
2.3分離高斯模糊
如圖2.3所示,使用二維的高斯模板達到了模糊圖像的目的,但是會因模板矩陣的關係而造成邊緣圖像缺失(2.3 b,c),越大,缺失像素越多,丟棄模板會造成黑邊(2.3 d)。更重要的是當變大時,高斯模板(高斯核)和卷積運算量將大幅度提高。根據高斯函數的可分離性,可對二維高斯模糊函數進行改進。
高斯函數的可分離性是指使用二維矩陣變換得到的效果也可以通過在水平方向進行一維高斯矩陣變換加上豎直方向的一維高斯矩陣變換得到。從計算的角度來看,這是一項有用的特性,因爲這樣只需要次計算,而二維不可分的矩陣則需要次計算,其中,m,n爲高斯矩陣的維數,M,N爲二維圖像的維數。
另外,兩次一維的高斯卷積將消除二維高斯矩陣所產生的邊緣。(關於消除邊緣的論述如下圖2.4所示, 對用模板矩陣超出邊界的部分——虛線框,將不做卷積計算。如圖2.4中x方向的第一個模板1*5,將退化成1*3的模板,只在圖像之內的部分做卷積。)
附錄1是用opencv2.2實現的二維高斯模糊和分離高斯模糊。表2.2爲上述兩種方法和opencv2.3開源庫實現的高斯模糊程序的比較。
3、尺度空間極值檢測
尺度空間使用高斯金字塔表示。Tony Lindeberg指出尺度規範化的LoG(Laplacion of Gaussian)算子具有真正的尺度不變性,Lowe使用高斯差分金字塔近似LoG算子,在尺度空間檢測穩定的關鍵點。
3.1 尺度空間理論
尺度空間(scale space)思想最早是由Iijima於1962年提出的,後經witkin和Koenderink等人的推廣逐漸得到關注,在計算機視覺鄰域使用廣泛。
尺度空間理論的基本思想是:在圖像信息處理模型中引入一個被視爲尺度的參數,通過連續變化尺度參數獲得多尺度下的尺度空間表示序列,對這些序列進行尺度空間主輪廓的提取,並以該主輪廓作爲一種特徵向量,實現邊緣、角點檢測和不同分辨率上的特徵提取等。
尺度空間方法將傳統的單尺度圖像信息處理技術納入尺度不斷變化的動態分析框架中,更容易獲取圖像的本質特徵。尺度空間中各尺度圖像的模糊程度逐漸變大,能夠模擬人在距離目標由近到遠時目標在視網膜上的形成過程。
尺度空間滿足視覺不變性。該不變性的視覺解釋如下:當我們用眼睛觀察物體時,一方面當物體所處背景的光照條件變化時,視網膜感知圖像的亮度水平和對比度是不同的,因此要求尺度空間算子對圖像的分析不受圖像的灰度水平和對比度變化的影響,即滿足灰度不變性和對比度不變性。另一方面,相對於某一固定座標系,當觀察者和物體之間的相對位置變化時,視網膜所感知的圖像的位置、大小、角度和形狀是不同的,因此要求尺度空間算子對圖像的分析和圖像的位置、大小、角度以及仿射變換無關,即滿足平移不變性、尺度不變性、歐幾里德不變性以及仿射不變性。
3.2 尺度空間的表示
一個圖像的尺度空間,定義爲一個變化尺度的高斯函數與原圖像的卷積。
(3-1)
其中,*表示卷積運算,
(3-2)
與公式(1-2)相同,m,n表示高斯模板的維度(由確定)。(x, y)代表圖像的像素位置。是尺度空間因子,值越小表示圖像被平滑的越少,相應的尺度也就越小。大尺度對應於圖像的概貌特徵,小尺度對應於圖像的細節特徵。
3.3 高斯金字塔的構建
尺度空間在實現時使用高斯金字塔表示,高斯金字塔的構建分爲兩部分:
1. 對圖像做不同尺度的高斯模糊;
2. 對圖像做降採樣(隔點採樣)。
圖像的金字塔模型是指,將原始圖像不斷降階採樣,得到一系列大小不一的圖像,由大到小,從下到上構成的塔狀模型。原圖像爲金子塔的第一層,每次降採樣所得到的新圖像爲金字塔的一層(每層一張圖像),每個金字塔共n層。金字塔的層數根據圖像的原始大小和塔頂圖像的大小共同決定,其計算公式如下:
(3-3)
其中M,N爲原圖像的大小,t爲塔頂圖像的最小維數的對數值。如,對於大小爲512*512的圖像,金字塔上各層圖像的大小如表3.1所示,當塔頂圖像爲4*4時,n=7,當塔頂圖像爲2*2時,n=8。
爲了讓尺度體現其連續性,高斯金字塔在簡單降採樣的基礎上加上了高斯濾波。如圖3.1所示,將圖像金字塔每層的一張圖像使用不同參數做高斯模糊,使得金字塔的每層含有多張高斯模糊圖像,將金字塔每層多張圖像合稱爲一組(Octave),金字塔每層只有一組圖像,組數和金字塔層數相等,使用公式(3-3)計算,每組含有多張(也叫層Interval)圖像。另外,降採樣時,高斯金字塔上一組圖像的初始圖像(底層圖像)是由前一組圖像的倒數第三張圖像隔點採樣得到的。
注:由於組內的多張圖像按層次疊放,因此組內的多張圖像也稱做多層,爲避免與金字塔層的概念混淆,本文以下內容中,若不特別說明是金字塔層數,層一般指組內各層圖像。
注:如3.4節所示,爲了在每組中檢測S個尺度的極值點,則DOG金字塔每組需S+2層圖像,而DOG金字塔由高斯金字塔相鄰兩層相減得到,則高斯金字塔每組需S+3層圖像,實際計算時S在3到5之間。取S=3時,假定高斯金字塔存儲索引如下:
第0組(即第-1組): 0 1 2 3 4 5
第1組: 6 7 8 9 10 11
第2組: ?
則第2組第一張圖片根據第一組中索引爲9的圖片降採樣得到,其它類似。
3.4 高斯差分金字塔
2002年Mikolajczyk在詳細的實驗比較中發現尺度歸一化的高斯拉普拉斯函數的極大值和極小值同其它的特徵提取函數,例如:梯度,Hessian或Harris角特徵比較,能夠產生最穩定的圖像特徵。
而Lindeberg早在1994年就發現高斯差分函數(Difference of Gaussian ,簡稱DOG算子)與尺度歸一化的高斯拉普拉斯函數非常近似。其中和的關係可以從如下公式推導得到:
利用差分近似代替微分,則有:
因此有
其中k-1是個常數,並不影響極值點位置的求取。
如圖3.2所示,紅色曲線表示的是高斯差分算子,而藍色曲線表示的是高斯拉普拉斯算子。Lowe使用更高效的高斯差分算子代替拉普拉斯算子進行極值檢測,如下:
(3-4)
在實際計算時,使用高斯金字塔每組中相鄰上下兩層圖像相減,得到高斯差分圖像,如圖3.3所示,進行極值檢測。
3.5 空間極值點檢測(關鍵點的初步探查)
關鍵點是由DOG空間的局部極值點組成的,關鍵點的初步探查是通過同一組內各DoG相鄰兩層圖像之間比較完成的。爲了尋找DoG函數的極值點,每一個像素點要和它所有的相鄰點比較,看其是否比它的圖像域和尺度域的相鄰點大或者小。如圖3.4所示,中間的檢測點和它同尺度的8個相鄰點和上下相鄰尺度對應的9×2個點共26個點比較,以確保在尺度空間和二維圖像空間都檢測到極值點。
由於要在相鄰尺度進行比較,如圖3.3右側每組含4層的高斯差分金子塔,只能在中間兩層中進行兩個尺度的極值點檢測,其它尺度則只能在不同組中進行。爲了在每組中檢測S個尺度的極值點,則DOG金字塔每組需S+2層圖像,而DOG金字塔由高斯金字塔相鄰兩層相減得到,則高斯金字塔每組需S+3層圖像,實際計算時S在3到5之間。
當然這樣產生的極值點並不全都是穩定的特徵點,因爲某些極值點響應較弱,而且DOG算子會產生較強的邊緣響應。
3.6 構建尺度空間需確定的參數
—尺度空間座標
O—組(octave)數
S— 組內層數
在上述尺度空間中,O和S,的關係如下:
(3-5)
其中是基準層尺度,o爲組octave的索引,s爲組內層的索引。關鍵點的尺度座標就是按關鍵點所在的組和組內的層,利用公式(3-5)計算而來。
在最開始建立高斯金字塔時,要預先模糊輸入圖像來作爲第0個組的第0層的圖像,這時相當於丟棄了最高的空域的採樣率。因此通常的做法是先將圖像的尺度擴大一倍來生成第-1組。我們假定初始的輸入圖像爲了抗擊混淆現象,已經對其進行的高斯模糊,如果輸入圖像的尺寸用雙線性插值擴大一倍,那麼相當於。
取式(3-4)中的k爲組內總層數的倒數,即
(3-6)
在構建高斯金字塔時,組內每層的尺度座標按如下公式計算:
(3-7)
其中初始尺度,lowe取,s爲組內的層索引,不同組相同層的組內尺度座標相同。組內下一層圖像是由前一層圖像按進行高斯模糊所得。式(3-7)用於一次生成組內不同尺度的高斯圖像,而在計算組內某一層圖像的尺度時,直接使用如下公式進行計算:
(3-8)
該組內尺度在方向分配和特徵描述時確定採樣窗口的大小。
由上,式(3-4)可記爲
(3-9)
圖3.5爲構建DOG金字塔的示意圖,原圖採用128*128的jobs圖像,擴大一倍後構建金字塔。
4、關鍵點定位
以上方法檢測到的極值點是離散空間的極值點,以下通過擬合三維二次函數來精確確定關鍵點的位置和尺度,同時去除低對比度的關鍵點和不穩定的邊緣響應點(因爲DoG算子會產生較強的邊緣響應),以增強匹配穩定性、提高抗噪聲能力。
4.1關鍵點的精確定位
離散空間的極值點並不是真正的極值點,圖4.1顯示了二維函數離散空間得到的極值點與連續空間極值點的差別。利用已知的離散空間點插值得到的連續空間極值點的方法叫做子像素插值(Sub-pixel Interpolation)。
爲了提高關鍵點的穩定性,需要對尺度空間DoG函數進行曲線擬合。利用DoG函數在尺度空間的Taylor展開式(擬合函數)爲:
(4-1)
其中,。求導並讓方程等於零,可以得到極值點的偏移量爲:
(4-2)
對應極值點,方程的值爲:
(4-3)
其中,代表相對插值中心的偏移量,當它在任一維度上的偏移量大於0.5時(即x或y或),意味着插值中心已經偏移到它的鄰近點上,所以必須改變當前關鍵點的位置。同時在新的位置上反覆插值直到收斂;也有可能超出所設定的迭代次數或者超出圖像邊界的範圍,此時這樣的點應該刪除,在Lowe中進行了5次迭代。另外,過小的點易受噪聲的干擾而變得不穩定,所以將小於某個經驗值(Lowe論文中使用0.03,Rob Hess等人實現時使用0.04/S)的極值點刪除。同時,在此過程中獲取特徵點的精確位置(原位置加上擬合的偏移量)以及尺度()。
4.2消除邊緣響應
一個定義不好的高斯差分算子的極值在橫跨邊緣的地方有較大的主曲率,而在垂直邊緣的方向有較小的主曲率。
DOG算子會產生較強的邊緣響應,需要剔除不穩定的邊緣響應點。獲取特徵點處的Hessian矩陣,主曲率通過一個2x2 的Hessian矩陣H求出:
(4-4)
H的特徵值α和β代表x和y方向的梯度,
(4-5)
表示矩陣H對角線元素之和,表示矩陣H的行列式。假設是α較大的特徵值,而是β較小的特徵值,令,則
(4-6)
導數由採樣點相鄰差估計得到,在下一節中說明。
D的主曲率和H的特徵值成正比,令爲α最大特徵值,β爲最小的特徵值,則公式的值在兩個特徵值相等時最小,隨着的增大而增大。值越大,說明兩個特徵值的比值越大,即在某一個方向的梯度值越大,而在另一個方向的梯度值越小,而邊緣恰恰就是這種情況。所以爲了剔除邊緣響應點,需要讓該比值小於一定的閾值,因此,爲了檢測主曲率是否在某域值r下,只需檢測
(4-7)
式(4-7)成立時將關鍵點保留,反之剔除。
在Lowe的文章中,取r=10。圖4.2右側爲消除邊緣響應後的關鍵點分佈圖。
4.3有限差分法求導
有限差分法以變量離散取值後對應的函數值來近似微分方程中獨立變量的連續取值。在有限差分方法中,我們放棄了微分方程中獨立變量可以取連續值的特徵,而關注獨立變量離散取值後對應的函數值。但是從原則上說,這種方法仍然可以達到任意滿意的計算精度。因爲方程的連續數值解可以通過減小獨立變量離散取值的間格,或者通過離散點上的函數值插值計算來近似得到。這種方法是隨着計算機的誕生和應用而發展起來的。其計算格式和程序的設計都比較直觀和簡單,因而,它在計算數學中使用廣泛。
有限差分法的具體操作分爲兩個部分:
1. 用差分代替微分方程中的微分,將連續變化的變量離散化,從而得到差分方程組的數學形式;
2. 求解差分方程組。
一個函數在x點上的一階和二階微商,可以近似地用它所臨近的兩點上的函數值的差分來表示。如對一個單變量函數f(x),x爲定義在區間[a,b]上的連續變量,以步長將區間[a,b]離散化,我們會得到一系列節點,
然後求出f(x)在這些點上的近似值。顯然步長h越小,近似解的精度就越好。與節點相鄰的節點有和,所以在節點處可構造如下形式的差值:
節點的一階向前差分
節點的一階向後差分
節點的一階中心差分
本文使用中心差分法利用泰勒展開式求解第四節所使用的導數,現做如下推導。
函數f(x)在處的泰勒展開式爲:
(4-8)
則,
(4-9)
(4-10)
忽略h平方之後的項,聯立式(4-9),(4-10)解方程組得:
(4-11)
(4-12)
二元函數的泰勒展開式如下:
將展開後忽略次要項聯立解方程得二維混合偏導如下:
(4-13)
綜上,推導了4.1,4.2遇到的所有導數計算。同理,利用多元泰勒展開式,可得任意偏導的近似差分表示。
在圖像處理中,取h=1,在圖4.2所示的圖像中,將像素0的基本中點導數公式整理如下:
4.4 三階矩陣求逆公式
高階矩陣的求逆算法主要有歸一法和消元法兩種,現將三階矩陣求逆公式總結如下:
若矩陣
可逆,即時,
(4-14)
5、關鍵點方向分配
爲了使描述符具有旋轉不變性,需要利用圖像的局部特徵爲給每一個關鍵點分配一個基準方向。使用圖像梯度的方法求取局部結構的穩定方向。對於在DOG金字塔中檢測出的關鍵點點,採集其所在高斯金字塔圖像3σ鄰域窗口內像素的梯度和方向分佈特徵。梯度的模值和方向如下:
(5-1)
L爲關鍵點所在的尺度空間值,按Lowe的建議,梯度的模值m(x,y)按的高斯分佈加成,按尺度採樣的3σ原則,鄰域窗口半徑爲。
在完成關鍵點的梯度計算後,使用直方圖統計鄰域內像素的梯度和方向。梯度直方圖將0~360度的方向範圍分爲36個柱(bins),其中每柱10度。如圖5.1所示,直方圖的峯值方向代表了關鍵點的主方向,(爲簡化,圖中只畫了八個方向的直方圖)。
方向直方圖的峯值則代表了該特徵點處鄰域梯度的方向,以直方圖中最大值作爲該關鍵點的主方向。爲了增強匹配的魯棒性,只保留峯值大於主方向峯值80%的方向作爲該關鍵點的輔方向。因此,對於同一梯度值的多個峯值的關鍵點位置,在相同位置和尺度將會有多個關鍵點被創建但方向不同。僅有15%的關鍵點被賦予多個方向,但可以明顯的提高關鍵點匹配的穩定性。實際編程實現中,就是把該關鍵點複製成多份關鍵點,並將方向值分別賦給這些複製後的關鍵點,並且,離散的梯度方向直方圖要進行插值擬合處理,來求得更精確的方向角度值,檢測結果如圖5.2所示。
至此,將檢測出的含有位置、尺度和方向的關鍵點即是該圖像的SIFT特徵點。
6、關鍵點特徵描述
通過以上步驟,對於每一個關鍵點,擁有三個信息:位置、尺度以及方向。接下來就是爲每個關鍵點建立一個描述符,用一組向量將這個關鍵點描述出來,使其不隨各種變化而改變,比如光照變化、視角變化等等。這個描述子不但包括關鍵點,也包含關鍵點周圍對其有貢獻的像素點,並且描述符應該有較高的獨特性,以便於提高特徵點正確匹配的概率。
SIFT描述子是關鍵點鄰域高斯圖像梯度統計結果的一種表示。通過對關鍵點周圍圖像區域分塊,計算塊內梯度直方圖,生成具有獨特性的向量,這個向量是該區域圖像信息的一種抽象,具有唯一性。
Lowe建議描述子使用在關鍵點尺度空間內4*4的窗口中計算的8個方向的梯度信息,共4*4*8=128維向量表徵。表示步驟如下:
1. 確定計算描述子所需的圖像區域
特徵描述子與特徵點所在的尺度有關,因此,對梯度的求取應在特徵點對應的高斯圖像上進行。將關鍵點附近的鄰域劃分爲d*d(Lowe建議d=4)個子區域,每個子區域做爲一個種子點,每個種子點有8個方向。每個子區域的大小與關鍵點方向分配時相同,即每個區域有個子像素,爲每個子區域分配邊長爲的矩形區域進行採樣(個子像素實際用邊長爲的矩形區域即可包含,但由式(3-8),不大,爲了簡化計算取其邊長爲,並且採樣點宜多不宜少)。考慮到實際計算時,需要採用雙線性插值,所需圖像窗口邊長爲。在考慮到旋轉因素(方便下一步將座標軸旋轉到關鍵點的方向),如下圖6.1所示,實際計算所需的圖像區域半徑爲:
(6-1)
計算結果四捨五入取整。
2. 將座標軸旋轉爲關鍵點的方向,以確保旋轉不變性,如6.2所示。
旋轉後鄰域內採樣點的新座標爲:
(6-2)
3. 將鄰域內的採樣點分配到對應的子區域內,將子區域內的梯度值分配到8個方向上,計算其權值。
旋轉後的採樣點座標在半徑爲radius的圓內被分配到的子區域,計算影響子區域的採樣點的梯度和方向,分配到8個方向上。
旋轉後的採樣點落在子區域的下標爲
(6-3)
Lowe建議子區域的像素的梯度大小按的高斯加權計算,即
(6-4)
其中a,b爲關鍵點在高斯金字塔圖像中的位置座標。
4. 插值計算每個種子點八個方向的梯度。
如圖6.3所示,將由式(6-3)所得采樣點在子區域中的下標(圖中藍色窗口內紅色點)線性插值,計算其對每個種子點的貢獻。如圖中的紅色點,落在第0行和第1行之間,對這兩行都有貢獻。對第0行第3列種子點的貢獻因子爲dr,對第1行第3列的貢獻因子爲1-dr,同理,對鄰近兩列的貢獻因子爲dc和1-dc,對鄰近兩個方向的貢獻因子爲do和1-do。則最終累加在每個方向上的梯度大小爲:
(6-5)
其中k,m,n爲0或爲1。
5. 如上統計的4*4*8=128個梯度信息即爲該關鍵點的特徵向量。特徵向量形成後,爲了去除光照變化的影響,需要對它們進行歸一化處理,對於圖像灰度值整體漂移,圖像各點的梯度是鄰域像素相減得到,所以也能去除。得到的描述子向量爲,歸一化後的特徵向量爲則
(6-7)
6. 描述子向量門限。非線性光照,相機飽和度變化對造成某些方向的梯度值過大,而對方向的影響微弱。因此設置門限值(向量歸一化後,一般取0.2)截斷較大的梯度值。然後,再進行一次歸一化處理,提高特徵的鑑別性。
7. 按特徵點的尺度對特徵描述向量進行排序。
至此,SIFT特徵描述向量生成。
描述向量這塊不好理解,我畫了個草圖,供參考:
7、SIFT的缺點
SIFT在圖像的不變特徵提取方面擁有無與倫比的優勢,但並不完美,仍然存在:
1. 實時性不高。
2. 有時特徵點較少。
3. 對邊緣光滑的目標無法準確提取特徵點。
等缺點,如下圖7.1所示,對模糊的圖像和邊緣平滑的圖像,檢測出的特徵點過少,對圓更是無能爲力。近來不斷有人改進,其中最著名的有SURF和CSIFT。
8、總結
本人研究SIFT算法一月有餘,鑑於相關知識的缺失,尺度空間技術和差分近似求導曾困我良久。Lowe在論文中對細節提之甚少,甚至隻字未提,給實現帶來了很大困難。經過多方查閱,實現,總結成此文。自認爲是到目前爲止,關於SIFT算法最爲詳盡的資料,現分享給你,望批評指正。
一同分享給你的還有同時實現的高斯模糊源碼,sift算法源碼,見附錄1,2。源碼使用vs2010+opencv2.2實現。
zdd
2012年4月28日 於北師大
2012年5月17日15:33:23第一次修正
修正內容:第3.3部分內容,圖3.1,圖3.5。
修正後代碼:http://download.csdn.net/detail/zddmail/4309418
參考資料
1、David G.Lowe Distinctive Image Features from Scale-Invariant Keypoints. January 5, 2004.
2、David G.Lowe Object Recognition from Local Scale-Invariant Features. 1999
3、Matthew Brown and David Lowe Invariant Features from Interest Point Groups. In British Machine Vision Conference, Cardiff, Wales, pp. 656-665.
4、PETER J. BURT, MEMBER, IEEE, AND EDWARD H. ADELSON, The Laplacian Pyramid as a Compact Image Code. IEEE TRANSACTIONS ON COMMUNICATIONS, VOL. COM-3l, NO. 4, APRIL 1983
5、宋丹 10905056 尺度不變特徵變換匹配算法Scale Invariant Feature Transform (SIFT)(PPT)
6、RaySaint 的博客SIFT算法研究http://underthehood.blog.51cto.com/2531780/658350
7、Jason Clemons SIFT: SCALE INVARIANT FEATURE TRANSFORM BY DAVID LOWE(ppt)
8、Tony Lindeberg Scale-space theory: A basic tool for analysing structures at different scales.1994
9、SIFT官網的Rob Hess <[email protected]> SIFT源碼
10、Opencv2.2 Andrea Vedaldi(UCLA VisionLab)實現的SIFT源碼 http://www.vlfeat.org/~vedaldi/code/siftpp.html, opencv2.3改用Rob Hess的源碼
11、科學計算中的偏微分方程有限差分法 楊樂主編
12、維基百科SIFT詞條:http://zh.wikipedia.org/zh-cn/Scale-invariant_feature_transform
13、百度百科SIFT詞條:http://baike.baidu.com/view/2832304.htm
14、其它互聯網資料
附錄1 高斯模糊源碼
http://blog.csdn.net/zddmail/article/details/7450033
http://download.csdn.net/detail/zddmail/4217704
附錄2 SIFT算法源碼
資助:如果此渣文對大家有幫助,敬請資助, 一毛兩毛的都行呀!不給媳婦籌錢的程序員不是好程序員!
1. 什麼是斑點
斑點通常是指與周圍有着顏色和灰度差別的區域。在實際地圖中,往往存在着大量這樣的斑點,如一顆樹是一個斑點,一塊草地是一個斑點,一棟房子也可以是一個斑點。由於斑點代表的是一個區域,相比單純的角點,它的穩定性要好,抗噪聲能力要強,所以它在圖像配準上扮演了很重要的角色。
同時有時圖像中的斑點也是我們關心的區域,比如在醫學與生物領域,我們需要從一些X光照片或細胞顯微照片中提取一些具有特殊意義的斑點的位置或數量。
比如下圖中天空的飛機、向日葵的花盤、X線斷層圖像中的兩個斑點。
在視覺領域,斑點檢測的主要思路都是檢測出圖像中比它周圍像素灰度值大或比周圍灰度值小的區域。一般有兩種方法來實現這一目標:
- 基於求導的微分方法,這類的方法稱爲微分檢測器;
- 基於局部極值的分水嶺算法。
這裏我們重點介紹第一種方法,主要檢測LOG斑點。而OpenCV中SimpleBlobDetector斑點檢測算子就實現了第二種方法,我們這裏也會介紹它的接口使用方法。
2. LOG斑點檢測
2.1 基本原理
利用高斯拉普通拉斯(Laplace of Gaussian,LOG)算子檢測圖像斑點是一種十分常用的方法,對於二維高斯函數:
它的拉普拉斯變換爲:
規範化的高斯拉普變換爲:
規範化算法子在二維圖像上顯示是一個圓對稱函數,如下圖所示。我們可以用這個算子來檢測圖像中的斑點,並且可以通過改變
2.2 LOG原理解釋
其實從更直觀的角度去解釋爲什麼LOG算子可以檢測圖像中的斑點是:
圖像與某一個二維函數進行卷積運算實際就是求取圖像與這一函數的相似性。同理,圖像與高斯拉普拉斯函數的卷積實際就是求取圖像與高斯拉普拉斯函數的相似性。當圖像中的斑點尺寸與高斯拉普拉斯函數的形狀趨近一致時,圖像的拉普拉斯響應達到最大。
從概率的角度解釋爲:假設原圖像是一個與位置有關的隨機變量X的密度函數,而LOG爲隨機變量Y的密度函數,則隨機變量X+Y的密度分佈函數即爲兩個函數的卷積形式(這一部分的理論,可以參見本博客概率與統計相關文章)。如果想讓X+Y能取到最大值,則X與Y能保持步調一致最好,即X上升時,Y也上升,X最大時,Y也最大。
那麼LOG算子是怎麼被構想出來的呢?
事實上我們知道Laplace可以用來檢測圖像中的局部極值點,但是對噪聲敏感,所以在我們對圖像進行Laplace卷積之前,我們用一個高斯低通濾波對圖像進行卷積,目標是去除圖像中的噪聲點。這一過程 可以描述爲:
先對圖像
然後對圖像的拉普拉斯圖像則爲:
而實際上有下面等式:
所以,我們可以先求高斯核的拉普拉斯算子,再對圖像進行卷積。也就是一開始描述的步驟。
2.3 LOG算子的實現
Mat Feat::getHOGKernel(Size& ksize, double sigma) { Mat kernel(ksize, CV_64F); Point centPoint = Point((ksize.width -1)/2, ((ksize.height -1)/2)); // first calculate Gaussian for (int i=0; i < kernel.rows; i++) { double* pData = kernel.ptr<double>(i); for (int j = 0; j < kernel.cols; j++) { double param = -((i - centPoint.y) * (i - centPoint.y) + (j - centPoint.x) * (j - centPoint.x)) / (2*sigma*sigma); pData[j] = exp(param); } } double maxValue; minMaxLoc(kernel, NULL, &maxValue); for (int i=0; i < kernel.rows; i++) { double* pData = kernel.ptr<double>(i); for (int j = 0; j < kernel.cols; j++) { if (pData[j] < EPS* maxValue) { pData[j] = 0; } } } double sumKernel = sum(kernel)[0]; if (sumKernel != 0) { kernel = kernel / sumKernel; } // now calculate Laplacian for (int i=0; i < kernel.rows; i++) { double* pData = kernel.ptr<double>(i); for (int j = 0; j < kernel.cols; j++) { double addition = ((i - centPoint.y) * (i - centPoint.y) + (j - centPoint.x) * (j - centPoint.x) - 2*sigma*sigma)/(sigma*sigma*sigma*sigma); pData[j] *= addition; } } // make the filter sum to zero sumKernel = sum(kernel)[0]; kernel -= (sumKernel/(ksize.width * ksize.height)); return kernel; }
2.4 多尺度檢測
我們注意到當
規範化的高斯拉普拉斯函數爲:
求
得到:
對於圖像中的斑點,在尺度
那麼在多尺度的情況下,同時在空間和尺度上達到最大值(或最小值)的點就是我們所期望的斑點。對於二維圖像
3 OpenCV進行斑點檢測
opencv中檢測Blobs的類爲SimpleBlobDetector,這個類在opencv中的定義如下:
class SimpleBlobDetector : public FeatureDetector { public: struct Params { Params(); float thresholdStep; float minThreshold; float maxThreshold; size_t minRepeatability; float minDistBetweenBlobs; bool filterByColor; uchar blobColor; bool filterByArea; float minArea, maxArea; bool filterByCircularity; float minCircularity, maxCircularity; bool filterByInertia; float minInertiaRatio, maxInertiaRatio; bool filterByConvexity; float minConvexity, maxConvexity; }; SimpleBlobDetector(const SimpleBlobDetector::Params ¶meters = SimpleBlobDetector::Params()); protected: ... };
算法的大致步驟如下:
- 對[minThreshold,maxThreshold)區間,以thresholdStep爲間隔,做多次二值化。
- 對每張二值圖片,使用findContours()提取連通域並計算每一個連通域的中心。
- 根據2得到的中心,全部放在一起。一些很接近的點[由theminDistBetweenBlobs控制多少纔算接近]被歸爲一個group,對應一個bolb特徵..
- 從3得到的那些點,估計最後的blob特徵和相應半徑,並以key points返回。
同時該支持提取特徵的方法,一共有5個選項,這裏就不多加描述了,默認是提取黑色圓形的Blob特徵。下面是一個示例
int main(int argc, char** argv) { Mat image = imread(argv[1]); vector<KeyPoint> keyPoints; SimpleBlobDetector::Params params; SimpleBlobDetector blobDetect(params); blobDetect.create("SimpleBlob"); blobDetect.detect(image, keyPoints); cout << keyPoints.size() << endl; drawKeypoints(image, keyPoints, image, Scalar(255,0,0)); namedWindow("blobs"); imshow("blobs", image); waitKey(); return 0; }
總體來說,OpenCV的斑點檢測效果還算不錯,但是在有些圖像的效果上明顯不如LOG算子檢測的檢測效果。
4. 擴展閱讀
一個與LOG濾波核近似的是高斯差分DOG濾波核,它的定義爲:
其中
DOG可以看作爲LOG的一個近似,但是它比LOG的效率更高。
前面介紹的微分算子在近圓的斑點檢測方面效果很好,但是這些檢測算子被限定於只能檢測圓形斑點,而且不能估計斑點的方向,因爲LOG算子等都是中心對稱的。如果我們定義一種二維高斯核的變形,記它在X方向與Y方向上具有不同的方差,則這種算子可以用來檢測帶有方向的斑點。
其中
5. 參考資料
1. 《現代數字圖像 -- 處理技術提高與應用案例詳解》
2. 《圖像局部不變性特徵與描述》
3. Lindeberg, T. Feature Detection with Automatic Scale Selection
4. Hui Kong. A Generalized Laplacian Of Gaussian Filter for Blob Detection and Its Applications.
如果說SIFT算法中使用DOG對LOG進行了簡化,提高了搜索特徵點的速度,那麼SURF算法則是對DoH的簡化與近似。雖然SIFT算法已經被認爲是最有效的,也是最常用的特徵點提取的算法,但如果不借助於硬件的加速和專用圖像處理器的配合,SIFT算法以現有的計算機仍然很難達到實時的程度。對於需要實時運算的場合,如基於特徵點匹配的實時目標跟蹤系統,每秒要處理8-24幀的圖像,需要在毫秒級內完成特徵點的搜索、特徵矢量生成、特徵矢量匹配、目標鎖定等工作,這樣SIFT算法就很難適應這種需求了。SURF借鑑了SIFT中簡化近似的思想,把DoH中的高斯二階微分模板進行了簡化,使得模板對圖像的濾波只需要進行幾個簡單的加減法運算,並且,這種運算與濾波器的尺度無關。實驗證明,SURF算法較SIFT在運算速度上要快3倍左右。
1. 積分圖像
SURF算法中要用到積分圖像的概念。藉助積分圖像,圖像與高斯二階微分模板的濾波轉化爲對積分圖像的加減運算。
積分圖像中任意一點
式中,
式中,
OpenCV中提供了用於計算積分圖像的接口
/* * src :輸入圖像,大小爲M*N * sum: 輸出的積分圖像,大小爲(M+1)*(N+1) * sdepth:用於指定sum的類型,-1表示與src類型一致 */ void integral(InputArray src, OutputArray sum, int sdepth = -1);
值得注意的是OpenCV裏的積分圖大小比原圖像多一行一列,那是因爲OpenCV中積分圖的計算公式爲:
一旦積分圖計算好了,計算圖像內任何矩形區域的像素值的和只需要三個加法,如上圖所示。
2. DoH近似
在斑點檢測這篇文章中已經提到過,我們可以利用Hessian矩陣行列式的極大值檢測斑點。下面我們給出Hessian矩陣的定義。
給定圖像
式中,
下面顯示的是上面三種高斯微分算子的圖形。
但是利用Hessian行列式進行圖像斑點檢測時,有一個缺點。由於二階高斯微分被離散化和裁剪的原因,導致了圖像在旋轉奇數倍的
爲了將模板與圖產像的卷積轉換爲盒子濾波運算,我們需要對高斯二階微分模板進行簡化,使得簡化後的模板只是由幾個矩形區域組成,矩形區域內填充同一值,如下圖所示,在簡化模板中白色區域的值爲正數,黑色區域的值爲負數,灰度區域的值爲0。
對於
濾波器響應的相關權重
其中
使用近似的Hessian矩陣行列式來表示圖像中某一點
3. 尺度空間表示
通常想要獲取不同尺度的斑點,必須建立圖像的尺度空間金字塔。一般的方法是通過不同
由於採用了盒子濾波和積分圖像,所以,我們並不需要像SIFT算法那樣去直接建立圖像金字塔,而是採用不斷增大盒子濾波模板的尺寸的間接方法。通過不同尺寸盒子濾波模板與積分圖像求取Hessian矩陣行列式的響應圖像。然後在響應圖像上採用3D非最大值抑制,求取各種不同尺度的斑點。
如前所述,我們使用
與SIFT算法類似,我們需要將尺度空間劃分爲若干組(Octaves)。一個組代表了逐步放大的濾波模板對同一輸入圖像進行濾波的一系列響應圖。每個組又由若干固定的層組成。由於積分圖像離散化的原因,兩個層之間的最小尺度變化量是由高斯二階微分濾波器在微分方向上對正負斑點響應長度
採用類似的方法來處理其他幾組的模板序列。其方法是將濾波器尺寸增加量翻倍(6,12,24,38)。這樣,可以得到第二組的濾波器尺寸,它們分別爲15,27,39,51。第三組的濾波器尺寸爲27,51,75,99。如果原始圖像的尺寸仍然大於對應的濾波器尺寸,尺度空間的分析還可以進行第四組,其對應的模板尺寸分別爲51,99,147和195。下圖顯示了第一組至第三組的濾波器尺寸變化。
在通常尺度分析情況下,隨着尺度的增大,被檢測到的斑點數量迅速衰減。所以一般進行3-4組就可以了,與此同時,爲了減少運算量,提高計算的速度,可以考慮在濾波時,將採樣間隔設爲2。
對於尺寸爲L的模板,當用它與積分圖運算來近似二維高斯核的濾波時,對應的二維高斯核的參數
4. 興趣點的定位
爲了在圖像及不同尺寸中定位興趣點,我們用了
下面顯示了我們用的快速Hessian檢測子檢測到的興趣點。
5. SURF源碼解析
這份源碼來自OpenCV nonfree模塊。
這裏先介紹SURF特徵點定位這一塊,關於特徵點的描述下一篇文章再介紹。
5.1 主幹函數 fastHessianDetector
特徵點定位的主幹函數爲fastHessianDetector,該函數接受一個積分圖像,以及尺寸相關的參數,組數與每組的層數,檢測到的特徵點保存在vector<KeyPoint>類型的結構中。
static void fastHessianDetector(const Mat& sum, const Mat& msum, vector<KeyPoint>& keypoints, int nOctaves, int nOctaveLayers, float hessianThreshold) { /*first Octave圖像採樣的步長,第二組的時候加倍,以此內推 增加這個值,將會加快特徵點檢測的速度,但是會讓特徵點的提取變得不穩定*/ const int SAMPLE_STEP0 = 1; int nTotalLayers = (nOctaveLayers + 2)*nOctaves; // 尺度空間的總圖像數 int nMiddleLayers = nOctaveLayers*nOctaves; // 用於檢測特徵點的層的 總數,也就是中間層的總數 vector<Mat> dets(nTotalLayers); // 每一層圖像 對應的 Hessian行列式的值 vector<Mat> traces(nTotalLayers); // 每一層圖像 對應的 Hessian矩陣的跡的值 vector<int> sizes(nTotalLayers); // 每一層用的 Harr模板的大小 vector<int> sampleSteps(nTotalLayers); // 每一層用的採樣步長 vector<int> middleIndices(nMiddleLayers); // 中間層的索引值 keypoints.clear(); // 爲上面的對象分配空間,並賦予合適的值 int index = 0, middleIndex = 0, step = SAMPLE_STEP0; for (int octave = 0; octave < nOctaves; octave++) { for (int layer = 0; layer < nOctaveLayers + 2; layer++) { /*這裏sum.rows - 1是因爲 sum是積分圖,它的大小是原圖像大小加1*/ dets[index].create((sum.rows - 1) / step, (sum.cols - 1) / step, CV_32F); // 這裏面有除以遍歷圖像用的步長 traces[index].create((sum.rows - 1) / step, (sum.cols - 1) / step, CV_32F); sizes[index] = (SURF_HAAR_SIZE0 + SURF_HAAR_SIZE_INC*layer) << octave; sampleSteps[index] = step; if (0 < layer && layer <= nOctaveLayers) middleIndices[middleIndex++] = index; index++; } step *= 2; } // Calculate hessian determinant and trace samples in each layer for (int i = 0; i < nTotalLayers; i++) { calcLayerDetAndTrace(sum, sizes[i], sampleSteps[i], dets[i], traces[i]); } // Find maxima in the determinant of the hessian for (int i = 0; i < nMiddleLayers; i++) { int layer = middleIndices[i]; int octave = i / nOctaveLayers; findMaximaInLayer(sum, msum, dets, traces, sizes, keypoints, octave, layer, hessianThreshold, sampleSteps[layer]); } std::sort(keypoints.begin(), keypoints.end(), KeypointGreater()); }
5.2 計算Hessian矩陣的行列式與跡calcLayerDetAndTrace
這個函數首先定義了尺寸爲9的第一層圖像的三個模板。模板分別爲一個
struct SurfHF { int p0, p1, p2, p3; float w; SurfHF() : p0(0), p1(0), p2(0), p3(0), w(0) {} };
resizeHaarPattern這個函數非常的巧妙,它把模板中的點座標。轉換到在積分圖中的相對(模板左上角點)座標。
static void resizeHaarPattern(const int src[][5], SurfHF* dst, int n, int oldSize, int newSize, int widthStep) { float ratio = (float)newSize / oldSize; for (int k = 0; k < n; k++) { int dx1 = cvRound(ratio*src[k][0]); int dy1 = cvRound(ratio*src[k][1]); int dx2 = cvRound(ratio*src[k][2]); int dy2 = cvRound(ratio*src[k][3]); /*巧妙的座標轉換*/ dst[k].p0 = dy1*widthStep + dx1; // 轉換爲一個相對距離,距離模板左上角點的 在積分圖中的距離 !!important!! dst[k].p1 = dy2*widthStep + dx1; dst[k].p2 = dy1*widthStep + dx2; dst[k].p3 = dy2*widthStep + dx2; dst[k].w = src[k][4] / ((float)(dx2 - dx1)*(dy2 - dy1));// 原來的+1,+2用 覆蓋的所有像素點平均。 } }
在用積分圖計算近似卷積時,用的是calcHaarPattern函數。這個函數比較簡單,只用知道左上與右下角座標即可。
inline float calcHaarPattern(const int* origin, const SurfHF* f, int n) { /*orgin即爲積分圖,n爲模板中 黑白 塊的個數 */ double d = 0; for (int k = 0; k < n; k++) d += (origin[f[k].p0] + origin[f[k].p3] - origin[f[k].p1] - origin[f[k].p2])*f[k].w; return (float)d; }
最終我們可以看到了整個calcLayerDetAndTrack的代碼
static void calcLayerDetAndTrace(const Mat& sum, int size, int sampleStep, Mat& det, Mat& trace) { const int NX = 3, NY = 3, NXY = 4; const int dx_s[NX][5] = { { 0, 2, 3, 7, 1 }, { 3, 2, 6, 7, -2 }, { 6, 2, 9, 7, 1 } }; const int dy_s[NY][5] = { { 2, 0, 7, 3, 1 }, { 2, 3, 7, 6, -2 }, { 2, 6, 7, 9, 1 } }; const int dxy_s[NXY][5] = { { 1, 1, 4, 4, 1 }, { 5, 1, 8, 4, -1 }, { 1, 5, 4, 8, -1 }, { 5, 5, 8, 8, 1 } }; SurfHF Dx[NX], Dy[NY], Dxy[NXY]; if (size > sum.rows - 1 || size > sum.cols - 1) return; resizeHaarPattern(dx_s, Dx, NX, 9, size, sum.cols); resizeHaarPattern(dy_s, Dy, NY, 9, size, sum.cols); resizeHaarPattern(dxy_s, Dxy, NXY, 9, size, sum.cols); /* The integral image 'sum' is one pixel bigger than the source image */ int samples_i = 1 + (sum.rows - 1 - size) / sampleStep; // 最大能遍歷到的 行座標,因爲要減掉一個模板的尺寸 int samples_j = 1 + (sum.cols - 1 - size) / sampleStep; // 最大能遍歷到的 列座標 /* Ignore pixels where some of the kernel is outside the image */ int margin = (size / 2) / sampleStep; for (int i = 0; i < samples_i; i++) { /*座標爲(i,j)的點是模板左上角的點,所以實際現在模板分析是的i+margin,j+margin點處的響應*/ const int* sum_ptr = sum.ptr<int>(i*sampleStep); float* det_ptr = &det.at<float>(i + margin, margin); // 左邊空隙爲 margin float* trace_ptr = &trace.at<float>(i + margin, margin); for (int j = 0; j < samples_j; j++) { float dx = calcHaarPattern(sum_ptr, Dx, 3); float dy = calcHaarPattern(sum_ptr, Dy, 3); float dxy = calcHaarPattern(sum_ptr, Dxy, 4); sum_ptr += sampleStep; det_ptr[j] = dx*dy - 0.81f*dxy*dxy; trace_ptr[j] = dx + dy; } } }
5.3 局部最大值搜索findMaximaInLayer
這裏算法思路很簡單,值得注意的是裏面的一些座標的轉換很巧妙,裏面比較重的函數就是interpolateKeypoint函數,通過插值計算最大值點。
/* * Maxima location interpolation as described in "Invariant Features from * Interest Point Groups" by Matthew Brown and David Lowe. This is performed by * fitting a 3D quadratic to a set of neighbouring samples. * * The gradient vector and Hessian matrix at the initial keypoint location are * approximated using central differences. The linear system Ax = b is then * solved, where A is the Hessian, b is the negative gradient, and x is the * offset of the interpolated maxima coordinates from the initial estimate. * This is equivalent to an iteration of Netwon's optimisation algorithm. * * N9 contains the samples in the 3x3x3 neighbourhood of the maxima * dx is the sampling step in x * dy is the sampling step in y * ds is the sampling step in size * point contains the keypoint coordinates and scale to be modified * * Return value is 1 if interpolation was successful, 0 on failure. */ static int interpolateKeypoint(float N9[3][9], int dx, int dy, int ds, KeyPoint& kpt) { Vec3f b(-(N9[1][5] - N9[1][3]) / 2, // Negative 1st deriv with respect to x -(N9[1][7] - N9[1][1]) / 2, // Negative 1st deriv with respect to y -(N9[2][4] - N9[0][4]) / 2); // Negative 1st deriv with respect to s Matx33f A( N9[1][3] - 2 * N9[1][4] + N9[1][5], // 2nd deriv x, x (N9[1][8] - N9[1][6] - N9[1][2] + N9[1][0]) / 4, // 2nd deriv x, y (N9[2][5] - N9[2][3] - N9[0][5] + N9[0][3]) / 4, // 2nd deriv x, s (N9[1][8] - N9[1][6] - N9[1][2] + N9[1][0]) / 4, // 2nd deriv x, y N9[1][1] - 2 * N9[1][4] + N9[1][7], // 2nd deriv y, y (N9[2][7] - N9[2][1] - N9[0][7] + N9[0][1]) / 4, // 2nd deriv y, s (N9[2][5] - N9[2][3] - N9[0][5] + N9[0][3]) / 4, // 2nd deriv x, s (N9[2][7] - N9[2][1] - N9[0][7] + N9[0][1]) / 4, // 2nd deriv y, s N9[0][4] - 2 * N9[1][4] + N9[2][4]); // 2nd deriv s, s Vec3f x = A.solve(b, DECOMP_LU); bool ok = (x[0] != 0 || x[1] != 0 || x[2] != 0) && std::abs(x[0]) <= 1 && std::abs(x[1]) <= 1 && std::abs(x[2]) <= 1; if (ok) { kpt.pt.x += x[0] * dx; kpt.pt.y += x[1] * dy; kpt.size = (float)cvRound(kpt.size + x[2] * ds); } return ok; } static void findMaximaInLayer(const Mat& sum, const Mat& mask_sum, const vector<Mat>& dets, const vector<Mat>& traces, const vector<int>& sizes, vector<KeyPoint>& keypoints, int octave, int layer, float hessianThreshold, int sampleStep) { // Wavelet Data const int NM = 1; const int dm[NM][5] = { { 0, 0, 9, 9, 1 } }; SurfHF Dm; int size = sizes[layer]; // 當前層圖像的大小 int layer_rows = (sum.rows - 1) / sampleStep; int layer_cols = (sum.cols - 1) / sampleStep; // 邊界區域大小,考慮的下一層的模板大小 int margin = (sizes[layer + 1] / 2) / sampleStep + 1; if (!mask_sum.empty()) resizeHaarPattern(dm, &Dm, NM, 9, size, mask_sum.cols); int step = (int)(dets[layer].step / dets[layer].elemSize()); for (int i = margin; i < layer_rows - margin; i++) { const float* det_ptr = dets[layer].ptr<float>(i); const float* trace_ptr = traces[layer].ptr<float>(i); for (int j = margin; j < layer_cols - margin; j++) { float val0 = det_ptr[j]; // 中心點的值 if (val0 > hessianThreshold) { // 模板左上角的座標 int sum_i = sampleStep*(i - (size / 2) / sampleStep); int sum_j = sampleStep*(j - (size / 2) / sampleStep); /* The 3x3x3 neighbouring samples around the maxima. The maxima is included at N9[1][4] */ const float *det1 = &dets[layer - 1].at<float>(i, j); const float *det2 = &dets[layer].at<float>(i, j); const float *det3 = &dets[layer + 1].at<float>(i, j); float N9[3][9] = { { det1[-step - 1], det1[-step], det1[-step + 1], det1[-1], det1[0], det1[1], det1[step - 1], det1[step], det1[step + 1] }, { det2[-step - 1], det2[-step], det2[-step + 1], det2[-1], det2[0], det2[1], det2[step - 1], det2[step], det2[step + 1] }, { det3[-step - 1], det3[-step], det3[-step + 1], det3[-1], det3[0], det3[1], det3[step - 1], det3[step], det3[step + 1] } }; /* Check the mask - why not just check the mask at the center of the wavelet? */ if (!mask_sum.empty()) { const int* mask_ptr = &mask_sum.at<int>(sum_i, sum_j); float mval = calcHaarPattern(mask_ptr, &Dm, 1); if (mval < 0.5) continue; } /* 檢測val0,是否在N9裏極大值,??爲什麼不檢測極小值呢*/ if (val0 > N9[0][0] && val0 > N9[0][1] && val0 > N9[0][2] && val0 > N9[0][3] && val0 > N9[0][4] && val0 > N9[0][5] && val0 > N9[0][6] && val0 > N9[0][7] && val0 > N9[0][8] && val0 > N9[1][0] && val0 > N9[1][1] && val0 > N9[1][2] && val0 > N9[1][3] && val0 > N9[1][5] && val0 > N9[1][6] && val0 > N9[1][7] && val0 > N9[1][8] && val0 > N9[2][0] && val0 > N9[2][1] && val0 > N9[2][2] && val0 > N9[2][3] && val0 > N9[2][4] && val0 > N9[2][5] && val0 > N9[2][6] && val0 > N9[2][7] && val0 > N9[2][8]) { /* Calculate the wavelet center coordinates for the maxima */ float center_i = sum_i + (size - 1)*0.5f; float center_j = sum_j + (size - 1)*0.5f; KeyPoint kpt(center_j, center_i, (float)sizes[layer], -1, val0, octave, CV_SIGN(trace_ptr[j])); /* 局部極大值插值,用Hessian,類似於SIFT裏的插值,裏面沒有迭代5次,只進行了一次查找,why? */ int ds = size - sizes[layer - 1]; int interp_ok = interpolateKeypoint(N9, sampleStep, sampleStep, ds, kpt); /* Sometimes the interpolation step gives a negative size etc. */ if (interp_ok) { /*printf( "KeyPoint %f %f %d\n", point.pt.x, point.pt.y, point.size );*/ keypoints.push_back(kpt); } } } } } }
6. 總結
總體來說,如果理解了SIFT算法,再來看SURF算法會發現思路非常簡單。尤其是局部最大值查找方面,基本一致。關鍵還是一個用積分圖來簡化卷積的思路,以及怎麼用不同的模板來近似原來尺度空間中的高斯濾波器。
這一篇主要討論分析的是SURF的定位問題,下面還有SURF特徵點的方向計算與描述子的生成,將在下一篇文章中詳細描述。
上一篇文章 SURF算法與源碼分析、上 中主要分析的是SURF特徵點定位的算法原理與相關OpenCV中的源碼分析,這篇文章接着上篇文章對已經定位到的SURF特徵點進行特徵描述。這一步至關重要,這是SURF特徵點匹配的基礎。總體來說算法思路和SIFT相似,只是每一步都做了不同程度的近似與簡化,提高了效率。
1. SURF特徵點方向分配
爲了保證特徵矢量具有旋轉不變性,與SIFT特徵一樣,需要對每個特徵點分配一個主方向。爲些,我們需要以特徵點爲中心,以
與SIFT類似,使用
主方向爲最大Harr響應累加值所對應的方向,也就是最長矢量所對應的方向,即
可以依照SIFT求方方向時策略,當存在另一個相當於主峯值80%能量的峯值時,則將這個方向認爲是該特徵點的輔方向。一個特徵點可能會被指定具有多個方向(一個主方向,一個以上輔方向),這可以增強匹配的魯棒性。和SIFT的描述子類似,如果在
圖 1 求取主方向時扇形滑動窗口圍繞特徵點轉動,統計Haar小波響應值,並計算方向角
2. 特徵點特徵矢量生成
生成特徵點描述子與確定特徵點方向有些類似,它需要計算圖像的Haar小波響應。不過,與主方向的確定不同的是,這次我們不是使用一個圓形區域,而是在一個矩形區域來計算Haar小波響應。以特徵點爲中心,沿上一節討論得到的主方向,沿主方向將
圖2 特徵描述子表示
將
由於共有
圖3 不同的圖像密度模式得到的不同的描述子結果
爲了充分利用積分圖像進行Haar小波的響應計算,我們並不直接旋轉Haar小波模板求得其響應值,而是在積圖像上先使用水平和垂直的Haar模板求得響應值dy和dx,然後根據主方向旋轉dx和dy與主方向操持一致,如下圖4所示。爲了求得旋轉後Haar小波響應值,首先要得到旋轉前圖像的位置。旋轉前後圖偈的位置關係,可以通過點的旋轉公式得到:
在得到點
圖4 利用積分圖像進行Haar小波響應計算示意圖,左邊爲旋轉後的圖像,右邊爲旋轉前的圖像
3. 特徵描述子的維數
一般而言,特徵矢量的長度越長,特徵矢量所承載的信息量就越大,特徵描述子的獨特性就越好,但匹配時所付出的時間代價就越大。對於SURF描述子,可以將它擴展到用128維矢量來表示。具體方法是在求
爲了實現快速匹配,SURF在特徵矢量中增加了一個新的變量,即特徵點的拉普拉斯響應正負號。在特徵點檢測時,將Hessian矩陣的跡的正負號記錄下來,作爲特徵矢量中的一個變量。這樣做並不增加運算量,因爲特徵點檢測進已經對Hessian矩陣的跡進行了計算。在特徵匹配時,這個變量可以有效地節省搜索的時間,因爲只有兩個具有相同正負號的特徵點纔有可能匹配,對於正負號不同的特徵點就不進行相似性計算。
簡單地說,我們可以根據特徵點的響應值符號,將特徵點分成兩組,一組是具有拉普拉斯正響應的特徵點,一組是具有拉普拉斯負響應的特徵點,匹配時,只有符號相同組中的特徵點才能進行相互匹配。顯然,這樣可以節省特徵點匹配的時間。如下圖5所示。
圖5 黑背景下的亮斑和白背景下的黑斑 因爲它們的拉普拉斯響應正負號不同,不會對它們進行匹配
4. 源碼解析
特徵點描述子的生成這一部分的代碼主要是通過SURFInvoker這個類來實現。在主流程中,通過一個parallel_for_()函數來併發計算。
struct SURFInvoker { enum{ORI_RADIUS = 6, ORI_WIN = 60, PATCH_SZ = 20}; // Parameters const Mat* img; const Mat* sum; vector<KeyPoint>* keypoints; Mat* descriptors; bool extended; bool upright; // Pre-calculated values int nOriSamples; vector<Point> apt; // 特徵點周圍用於描述方向的鄰域的點 vector<float> aptw; // 描述 方向時的 高斯 權 vector<float> DW; SURFInvoker(const Mat& _img, const Mat& _sum, vector<KeyPoint>& _keypoints, Mat& _descriptors, bool _extended, bool _upright) { keypoints = &_keypoints; descriptors = &_descriptors; img = &_img; sum = &_sum; extended = _extended; upright = _upright; // 用於描述特徵點的 方向的 鄰域大小: 12*sigma+1 (sigma =1.2) 因爲高斯加權的核的參數爲2sigma // nOriSampleBound爲 矩形框內點的個數 const int nOriSampleBound = (2 * ORI_RADIUS + 1)*(2 * ORI_RADIUS + 1); // 這裏把s近似爲1 ORI_DADIUS = 6s // 分配大小 apt.resize(nOriSampleBound); aptw.resize(nOriSampleBound); DW.resize(PATCH_SZ*PATCH_SZ); // PATHC_SZ爲特徵描述子的 區域大小 20s(s 這裏初始爲1了) /* 計算特徵點方向用的 高斯分佈 權值與座標 */ Mat G_ori = getGaussianKernel(2 * ORI_RADIUS + 1, SURF_ORI_SIGMA, CV_32F); // SURF_ORI_SIGMA = 1.2 *2 =2.5 nOriSamples = 0; for (int i = -ORI_RADIUS; i <= ORI_RADIUS; i++) { for (int j = -ORI_RADIUS; j <= ORI_RADIUS; j++) { if (i*i + j*j <= ORI_RADIUS*ORI_RADIUS) // 限制在圓形區域內 { apt[nOriSamples] = cvPoint(i, j); // 下面這裏有個座標轉換,因爲i,j都是從-ORI_RADIUS開始的。 aptw[nOriSamples++] = G_ori.at<float>(i + ORI_RADIUS, 0) * G_ori.at<float>(j + ORI_RADIUS, 0); } } } CV_Assert(nOriSamples <= nOriSampleBound); // nOriSamples爲圓形區域內的點,nOriSampleBound是正方形區域的點 /* 用於特徵點描述子的高斯 權值 */ Mat G_desc = getGaussianKernel(PATCH_SZ, SURF_DESC_SIGMA, CV_32F); // 用於生成特徵描述子的 高斯加權 sigma = 3.3s (s初取1) for (int i = 0; i < PATCH_SZ; i++) { for (int j = 0; j < PATCH_SZ; j++) DW[i*PATCH_SZ + j] = G_desc.at<float>(i, 0) * G_desc.at<float>(j, 0); } /* x與y方向上的 Harr小波,參數爲4s */ const int NX = 2, NY = 2; const int dx_s[NX][5] = { { 0, 0, 2, 4, -1 }, { 2, 0, 4, 4, 1 } }; const int dy_s[NY][5] = { { 0, 0, 4, 2, 1 }, { 0, 2, 4, 4, -1 } }; float X[nOriSampleBound], Y[nOriSampleBound], angle[nOriSampleBound]; // 用於計算特生點主方向 uchar PATCH[PATCH_SZ + 1][PATCH_SZ + 1]; float DX[PATCH_SZ][PATCH_SZ], DY[PATCH_SZ][PATCH_SZ]; // 20s * 20s區域的 梯度值 CvMat matX = cvMat(1, nOriSampleBound, CV_32F, X); CvMat matY = cvMat(1, nOriSampleBound, CV_32F, Y); CvMat _angle = cvMat(1, nOriSampleBound, CV_32F, angle); Mat _patch(PATCH_SZ + 1, PATCH_SZ + 1, CV_8U, PATCH); int dsize = extended ? 128 : 64; int k, k1 = 0, k2 = (int)(*keypoints).size();// k2爲Harr小波的 模板尺寸 float maxSize = 0; for (k = k1; k < k2; k++) { maxSize = std::max(maxSize, (*keypoints)[k].size); } // maxSize*1.2/9 表示最大的尺度 s int imaxSize = std::max(cvCeil((PATCH_SZ + 1)*maxSize*1.2f / 9.0f), 1); Ptr<CvMat> winbuf = cvCreateMat(1, imaxSize*imaxSize, CV_8U); for (k = k1; k < k2; k++) { int i, j, kk, nangle; float* vec; SurfHF dx_t[NX], dy_t[NY]; KeyPoint& kp = (*keypoints)[k]; float size = kp.size; Point2f center = kp.pt; /* s是當前層的尺度參數 1.2是第一層的參數,9是第一層的模板大小*/ float s = size*1.2f / 9.0f; /* grad_wav_size是 harr梯度模板的大小 邊長爲 4s */ int grad_wav_size = 2 * cvRound(2 * s); if (sum->rows < grad_wav_size || sum->cols < grad_wav_size) { /* when grad_wav_size is too big, * the sampling of gradient will be meaningless * mark keypoint for deletion. */ kp.size = -1; continue; } float descriptor_dir = 360.f - 90.f; if (upright == 0) { // 這一步 是計算梯度值,先將harr模板放大,再根據積分圖計算,與前面求D_x,D_y一致類似 resizeHaarPattern(dx_s, dx_t, NX, 4, grad_wav_size, sum->cols); resizeHaarPattern(dy_s, dy_t, NY, 4, grad_wav_size, sum->cols); for (kk = 0, nangle = 0; kk < nOriSamples; kk++) { int x = cvRound(center.x + apt[kk].x*s - (float)(grad_wav_size - 1) / 2); int y = cvRound(center.y + apt[kk].y*s - (float)(grad_wav_size - 1) / 2); if (y < 0 || y >= sum->rows - grad_wav_size || x < 0 || x >= sum->cols - grad_wav_size) continue; const int* ptr = &sum->at<int>(y, x); float vx = calcHaarPattern(ptr, dx_t, 2); float vy = calcHaarPattern(ptr, dy_t, 2); X[nangle] = vx*aptw[kk]; Y[nangle] = vy*aptw[kk]; nangle++; } if (nangle == 0) { // No gradient could be sampled because the keypoint is too // near too one or more of the sides of the image. As we // therefore cannot find a dominant direction, we skip this // keypoint and mark it for later deletion from the sequence. kp.size = -1; continue; } matX.cols = matY.cols = _angle.cols = nangle; // 計算鄰域內每個點的 梯度角度 cvCartToPolar(&matX, &matY, 0, &_angle, 1); float bestx = 0, besty = 0, descriptor_mod = 0; for (i = 0; i < 360; i += SURF_ORI_SEARCH_INC) // SURF_ORI_SEARCH_INC 爲扇形區域掃描的步長 { float sumx = 0, sumy = 0, temp_mod; for (j = 0; j < nangle; j++) { // d是 分析到的那個點與 現在主方向的偏度 int d = std::abs(cvRound(angle[j]) - i); if (d < ORI_WIN / 2 || d > 360 - ORI_WIN / 2) { sumx += X[j]; sumy += Y[j]; } } temp_mod = sumx*sumx + sumy*sumy; // descriptor_mod 是最大峯值 if (temp_mod > descriptor_mod) { descriptor_mod = temp_mod; bestx = sumx; besty = sumy; } } descriptor_dir = fastAtan2(-besty, bestx); } kp.angle = descriptor_dir; if (!descriptors || !descriptors->data) continue; /* 用特徵點周圍20*s爲邊長的鄰域 計算特徵描述子 */ int win_size = (int)((PATCH_SZ + 1)*s); CV_Assert(winbuf->cols >= win_size*win_size); Mat win(win_size, win_size, CV_8U, winbuf->data.ptr); if (!upright) { descriptor_dir *= (float)(CV_PI / 180); // 特徵點的主方向 弧度值 float sin_dir = -std::sin(descriptor_dir); // - sin dir float cos_dir = std::cos(descriptor_dir); float win_offset = -(float)(win_size - 1) / 2; float start_x = center.x + win_offset*cos_dir + win_offset*sin_dir; float start_y = center.y - win_offset*sin_dir + win_offset*cos_dir; uchar* WIN = win.data; int ncols1 = img->cols - 1, nrows1 = img->rows - 1; size_t imgstep = img->step; for (i = 0; i < win_size; i++, start_x += sin_dir, start_y += cos_dir) { double pixel_x = start_x; double pixel_y = start_y; for (j = 0; j < win_size; j++, pixel_x += cos_dir, pixel_y -= sin_dir) { int ix = cvFloor(pixel_x), iy = cvFloor(pixel_y); if ((unsigned)ix < (unsigned)ncols1 && (unsigned)iy < (unsigned)nrows1) { float a = (float)(pixel_x - ix), b = (float)(pixel_y - iy); const uchar* imgptr = &img->at<uchar>(iy, ix); WIN[i*win_size + j] = (uchar) cvRound(imgptr[0] * (1.f - a)*(1.f - b) + imgptr[1] * a*(1.f - b) + imgptr[imgstep] * (1.f - a)*b + imgptr[imgstep + 1] * a*b); } else { int x = std::min(std::max(cvRound(pixel_x), 0), ncols1); int y = std::min(std::max(cvRound(pixel_y), 0), nrows1); WIN[i*win_size + j] = img->at<uchar>(y, x); } } } } else { float win_offset = -(float)(win_size - 1) / 2; int start_x = cvRound(center.x + win_offset); int start_y = cvRound(center.y - win_offset); uchar* WIN = win.data; for (i = 0; i < win_size; i++, start_x++) { int pixel_x = start_x; int pixel_y = start_y; for (j = 0; j < win_size; j++, pixel_y--) { int x = MAX(pixel_x, 0); int y = MAX(pixel_y, 0); x = MIN(x, img->cols - 1); y = MIN(y, img->rows - 1); WIN[i*win_size + j] = img->at<uchar>(y, x); } } } // Scale the window to size PATCH_SZ so each pixel's size is s. This // makes calculating the gradients with wavelets of size 2s easy resize(win, _patch, _patch.size(), 0, 0, INTER_AREA); // Calculate gradients in x and y with wavelets of size 2s for (i = 0; i < PATCH_SZ; i++) for (j = 0; j < PATCH_SZ; j++) { float dw = DW[i*PATCH_SZ + j]; // 高斯加權係數 float vx = (PATCH[i][j + 1] - PATCH[i][j] + PATCH[i + 1][j + 1] - PATCH[i + 1][j])*dw; float vy = (PATCH[i + 1][j] - PATCH[i][j] + PATCH[i + 1][j + 1] - PATCH[i][j + 1])*dw; DX[i][j] = vx; DY[i][j] = vy; } // Construct the descriptor vec = descriptors->ptr<float>(k); for (kk = 0; kk < dsize; kk++) vec[kk] = 0; double square_mag = 0; if (extended) { // 128維描述子,考慮dx與dy的正負號 for (i = 0; i < 4; i++) for (j = 0; j < 4; j++) { // 每個方塊內是一個5s * 5s的區域,每個方法由8個特徵描述 for (int y = i * 5; y < i * 5 + 5; y++) { for (int x = j * 5; x < j * 5 + 5; x++) { float tx = DX[y][x], ty = DY[y][x]; if (ty >= 0) { vec[0] += tx; vec[1] += (float)fabs(tx); } else { vec[2] += tx; vec[3] += (float)fabs(tx); } if (tx >= 0) { vec[4] += ty; vec[5] += (float)fabs(ty); } else { vec[6] += ty; vec[7] += (float)fabs(ty); } } } for (kk = 0; kk < 8; kk++) square_mag += vec[kk] * vec[kk]; vec += 8; } } else { // 64位描述子 for (i = 0; i < 4; i++) for (j = 0; j < 4; j++) { for (int y = i * 5; y < i * 5 + 5; y++) { for (int x = j * 5; x < j * 5 + 5; x++) { float tx = DX[y][x], ty = DY[y][x]; vec[0] += tx; vec[1] += ty; vec[2] += (float)fabs(tx); vec[3] += (float)fabs(ty); } } for (kk = 0; kk < 4; kk++) square_mag += vec[kk] * vec[kk]; vec += 4; } } // 歸一化 描述子 以滿足 光照不變性 vec = descriptors->ptr<float>(k); float scale = (float)(1. / (sqrt(square_mag) + DBL_EPSILON)); for (kk = 0; kk < dsize; kk++) vec[kk] *= scale; } } };
5. 總結
實際上有文獻指出,SURF比SIFT工作更出色。他們認爲主要是因爲SURF在求取描述子特徵矢量時,是對一個子塊的梯度信息進行求和,而SIFT則是依靠單個像素梯度的方向