sift的java實現解述
程式碼已經開源到github上,https://github.com/alibaba/simpleimage專案,其中的 analyze模組中。
原始圖片為:
主要呼叫方法:
BufferedImage img = ImageIO.read(logoFile); RenderImage ri = new RenderImage(img); SIFT sift = new SIFT(); sift.detectFeatures(ri.toPixelFloatArray(null)); List<KDFeaturePoint> al = sift.getGlobalKDFeaturePoints();
同樣可以再讀另一個張圖得到另一個 List<KDFeaturePoint> al1,然後兩個List進行match
List<Match> ms = MatchKeys.findMatchesBBF(al, al1);
ms = MatchKeys.filterMore(ms);
先從上面的呼叫入口,詳細講解圖sift的特徵點生,至於match,有空再說。其中最難理解的是極值點的查詢,主要對這部分講解。
一.構建尺度空間,檢測極值點,獲得尺度不變性
ImagePixelArray就是儲存一張圖片的象素點進行灰度化後的陣列。在RenderImage的toPixelFloatArray方法中預設灰度處理只是簡單將rgb的值換成r,g,b的平均值並做歸一化處理(除以255);
從呼叫入口我們進入主類SIFT的使用者介面detectFeatures方法,它實際呼叫了detectFeaturesDownscaled(ImageMap img, int bothDimHi, double startScale) ,我們看隨了開始的幾行init工作,最後對圖片使用preprocSigma(1.5)的引數進行高斯模數預處理。
進行高斯模糊的目的是為了使大片的灰度相近的點連成一片,而使一些比較突出的點更加區別於其它點,就象我們把一張灰度圖片使用“版畫”效果會把大量的連片點去掉只留下輪廓。if (preprocSigma > 0.0) { GaussianArray gaussianPre = new GaussianArray(preprocSigma); img = gaussianPre.convolve(img); }
經過上面的預處理效果為:
現在我們還看不出強烈的效果,僅是pre的處理了一下。再接著使用Pyramid的buildOctaves方法構建8度金字塔,我們跟蹤到方法內。
public int buildOctaves(ImagePixelArray source, float scale, int levelsPerOctave, float octaveSigm, int minSize) {
this.octaves = new ArrayList<OctaveSpace>();
OctaveSpace downSpace = null;
ImagePixelArray prev = source;
while (prev != null && prev.width >= minSize && prev.height >= minSize) {
OctaveSpace osp = new OctaveSpace();
// Create both the gaussian filtered images and the DOG maps
osp.makeGaussianImgs(prev, scale, levelsPerOctave, octaveSigm); //構建當前8度空間的高斯模糊影象
osp.makeGaussianDiffImgs();
octaves.add(osp);
prev = osp.getLastGaussianImg().halved(); //下一個8度空間的原始圖象
if (downSpace != null) downSpace.up = osp;
osp.down = downSpace;
downSpace = osp;
scale *= 2.0;
}
return (octaves.size());
}
先不看
整個方法就是以原圖為基礎不斷地構造圖層,這裡也是高斯金字塔最微妙的地方。尺度空間概念不是簡單的不同大小尺寸組成的尖塔,也不是相同大小不同模糊因子處理過的直方塔,其實說是金字塔不太準確,更象中國傳統的寶塔。
它首先由原始圖象根據不同的模糊因子進行模糊(說成是平滑更確切,就是把比較相近的顏色值讓他們更相近以便突出反差很強的點),這是在同一尺寸上做的。這些相同尺寸不同高斯模糊因子處理過的影象集合叫一個8度空間。相當於寶塔中的一層,然後再向下采樣,即以其中一幅進行1/2縮小作為原圖再進行另一個8度空間的高斯模糊處理,直到圖層的width或hight小於minSize(32),這樣不斷模糊並向下取樣的構成的所有8度空間的集合才叫高斯金字塔(高斯寶塔?)。這是為了能檢測到原圖的某一點在不同尺度上都有穩定的特徵。
然後我們回頭來看osp.makeGaussianImgs方法。對1個8度空間原始圖象,以不同的 sigma 引數構建多張高斯模糊圖。因為最底層的原圖最大,我選了塔中scale為2的那一個8度間空的smoothedImgs.
可以看出隨著模糊因子變化模糊程度在加大。在塔中的每一個8度空間得到了一個smoothedImgs 陣列。這裡一共生成6幅影象,原因在方法內部有註釋,我們最終要在3層的差分尺度中獲取極值點,而每個尺度獲取極值點都要在立體空間(這也是sift最區別於其它特徵的革命性突破)上比較,即要比較它上一幅和下一幅的對應點,那麼3層的尺度至少要5幅差分影象,而5幅差分影象至少要6幅高斯模糊圖象才能生成。
然後對smoothedImgs中的圖象通過osp.makeGaussianDiffImgs();依次求差放入diffImags陣列中。
對於使用不同引數進行模糊的兩張圖片,象素相近的連片部分差值極小,只有邊緣,轉角等特徵的點表現出較大的差值:
先不要在意連片的黑色,因為求差後的值很小並已經做了歸一化,我把它還原到圖片上時進行絕對值(可能為負)乘10然後模 255,以便清楚地顯示出來。可以看出這些圖片中特徵強烈的點都是邊緣,轉角等地方。
二.特徵點過濾並進行精確定位
現在回到detectFeaturesDownscaled,經過上面的處理,金字塔中每個8度空間上都有一個OctaveSpace物件儲存著一個差分圖象陣列。下面的findPeaks其實就是從第2個圖象開始到倒數第二個圖象迴圈,當前圖象上每一個點和周圍的點比較,如果是最大值或是最小值就視為極值點。(這裡的周圍是立體的周圍,不僅和當前影象上點周圍的8個點比較,還要和他上一幅和下一幅對應的9個點比較)
checkMinMax(current, c, x, y, ref, true);
checkMinMax(below, c, x, y, ref, false);
checkMinMax(above, c, x, y, ref, false);
if (ref.isMin == false && ref.isMax == false) continue;
peaks.add(new ScalePoint(x, y, curLev));
回到detectFeaturesDownscaled,下面的filterAndLocalizePeaks方法主要是根據原始論文的page12/13的方法進行過慮和精確定位。
isTooEdgelike是對太象邊緣的點進行過慮,這個意思就是連續的線不是好的極值點,只有角點這樣的孤立的點才是極值點,為什麼“太象邊緣”的連續的線不好呢?邊緣點的特點是沿邊緣方向的梯度很小,簡單說一條線的點差別不大,而和它相切的方向梯度很大。由於梯度大它們很容易成為極值點,但因為沿線的兩個點之間梯度又幾乎沒有區別,極值點本身是區域性特徵,所以邊緣線上兩個點對於某極值進行投射時根本無法區別是點1還是點2.所以要把這些“線性連續點去掉”。
在過慮掉“太象邊緣”的點後,下面的localizeIsWeak就精確化每個尺度空間上的極值點,因為極值點是在連續的尺度空間中計算出來的非常精確化的座標比如(0.12345678,0.23456789),而原始圖象的點是以整數為座標的,相對尺度空間的座標而言是雜湊的。而極值點最終要對映到原始影象的整數座標上,所以要有一個調整過程。
根據極值點的座標和sigma引數,主要是三元一/二價導數和亞象素計算。這樣精確匹配到原始點後會得到一個原始點的座標,sigma引數和調整值 local.dValue,下面的過慮條件就簡單了:
if (Math.abs(peak.local.scaleAdjust) > scaleAdjustThresh) continue;if (Math.abs(peak.local.dValue) <= dValueLoThresh) continue;
簡單說當匹配到原始點在某一範圍之外的都過慮掉,注意原始論文上建議dValueLoThresh為0.03,這裡實際是0.008。
三.為每個關鍵點指定方向引數
查詢極值點的工作都完成了,下面就是對這些點和周圍的點比較生成一些向量:
在生成關鍵的方向和梯度時,我們用一個pretreatMagnitudeAndDirectionImgs方法把差分圖上所有點的梯度方向和梯度值先計算出來,因為特徵點的方向最終是它周圍的64個點的梯度方向梯值加權計算出來的,這樣每一點可能被多個特徵點作為“周圍點”,如果當作為周圍點才計算某點的梯度方向梯值加,很多點會被多次計算,這樣計算的總次數會大於所有點計算一次。所以我們會把每個點先計算出來存在一個數組中。
詳見 makeFeaturePoint的註釋四.生成關鍵點的描述子