SIFT特徵點演算法原始碼分析(opencv)
SIFT特徵點演算法原始碼分析
SIFT演算法在opencv中被實現為一個類: SIFT ,主要的操作都在這個類過載的"()"運算子中實現。下面介紹這個類,以及其中呼叫的一些關鍵的函式。
SIFT類的建構函式:初始化演算法引數
SIFT::SIFT(int nfeatures=0, int nOctaveLayers=3, double contrastThreshold=0.04, double edgeThreshold=
10, double sigma=1.6)
引數:
nfeatures:特徵點數目(演算法對檢測出的特徵點排名,返回最好的nfeatures個特徵點)。nOctaveLayers:金字塔中每組的層數(演算法中會自己計算這個值,後面會介紹)。
contrastThreshold:特徵點的判別閾值。這個引數的大小怎麼把握?
edgeThreshold:過濾掉邊緣效應的閾值。
sigma:金字塔第0層影象高斯濾波系
SIFT的“()”運算子:SIFT演算法的主要流程
void SIFT::operator()(InputArray _image, InputArray _mask, vector<KeyPoint>& keypoints, OutputArray _descriptors, bool useProvidedKeypoints) const { int firstOctave = -1, actualNOctaves = 0, actualNLayers = 0; firstOctave 代表最底層一組的組號。如果為0,則源影象所在的組就是最底層一組;如果為 -1,則需要將源影象放大 1 倍(先與sigma=0.5的高斯核卷積再雙線性插值放大),放大後的影象位於最底層一組; actualNOctaves 實際所需金字塔共多少組;actualNLayers 每組金字塔實際需要多少層。只有當特徵點keypoints被直接輸入時,這兩個引數才用得著。如果是從從影象中找keypoint,可以忽略這兩引數。 Mat image = _image.getMat(), mask = _mask.getMat(); if( image.empty() || image.depth() != CV_8U ) CV_Error( CV_StsBadArg, "image is empty or has incorrect depth (!=CV_8U)" ); if( !mask.empty() && mask.type() != CV_8UC1 ) CV_Error( CV_StsBadArg, "mask has incorrect type (!=CV_8UC1)" ); if( useProvidedKeypoints ) { firstOctave = 0; int maxOctave = INT_MIN; for( size_t i = 0; i < keypoints.size(); i++ ) { int octave, layer; float scale; unpackOctave(keypoints[i], octave, layer, scale); firstOctave = std::min(firstOctave, octave); maxOctave = std::max(maxOctave, octave); actualNLayers = std::max(actualNLayers, layer-2); } firstOctave = std::min(firstOctave, 0); CV_Assert( firstOctave >= -1 && actualNLayers <= nOctaveLayers ); actualNOctaves = maxOctave - firstOctave + 1; } 如果特徵點是直接輸入的,則依次讀取輸入的特徵點,看看這些特徵點需要多少層、多少組金字塔。 約束: 最底層的金字塔組號至少大於 -1 ,即源影象最多被放大一倍; 實際每組金字塔的層數不得大於nOctaveLayers,nOctaveLayers是在初始化SIFT時指定的; 實際的金字塔組數根據特徵點中的組號確定(按需分配,節省空間和提高效率)。 Mat base = createInitialImage(image, firstOctave < 0, (float)sigma); vector<Mat> gpyr, dogpyr; int nOctaves = actualNOctaves > 0 ? actualNOctaves : cvRound(log( (double)std::min( base.cols, base.rows ) ) / log(2.) - 2) - firstOctave; 對原始影象進行 方差=sigma^2 的高斯濾波,得到基礎影象base:金字塔最底層的影象。 計算金字塔的組數 nOctaves: 使得最高的一組金字塔的寬(或高)只有8個畫素(or 4個畫素?)。 //double t, tf = getTickFrequency(); //t = (double)getTickCount(); buildGaussianPyramid(base, gpyr, nOctaves); buildDoGPyramid(gpyr, dogpyr); 建立差分高斯金字塔DOG //t = (double)getTickCount() - t; //printf("pyramid construction time: %g\n", t*1000./tf); if( !useProvidedKeypoints ) { //t = (double)getTickCount(); findScaleSpaceExtrema(gpyr, dogpyr, keypoints); KeyPointsFilter::removeDuplicated( keypoints ); if( nfeatures > 0 ) KeyPointsFilter::retainBest(keypoints, nfeatures); //t = (double)getTickCount() - t; //printf("keypoint detection time: %g\n", t*1000./tf); if( firstOctave < 0 ) for( size_t i = 0; i < keypoints.size(); i++ ) { KeyPoint& kpt = keypoints[i]; float scale = 1.f/(float)(1 << -firstOctave); kpt.octave = (kpt.octave & ~255) | ((kpt.octave + firstOctave) & 255); kpt.pt *= scale; kpt.size *= scale; } if( !mask.empty() ) KeyPointsFilter::runByPixelsMask( keypoints, mask ); } else { // filter keypoints by mask //KeyPointsFilter::runByPixelsMask( keypoints, mask ); } 如果特徵點不是直接輸入的(false=useProvidedKeypoints),則需要先呼叫findScaleSpaceExtrema()找到特徵點,即尺度空間內的極值點(正的極大值和負的極小值),並呼叫removeDuplicated()去掉一些重複的特徵點(座標pt、鄰域大小size、方向angle都一致的特徵點被認為是重複)。如果特徵點個數超出上限,還要用retainBest() 來挑出最好的nfeatures個特徵點(響應response越高的特徵點被認為越"好")。 另外,由於findScaleSpaceExtrema()找特徵點時預設最底層的金字塔是第0組,因此如果實際的最底層是第 -1 組的話,需要將求出的關鍵點的組號減一,並將關鍵點的座標pt、鄰域尺寸size等乘以scale(=0.5,縮小一倍)。關鍵點的座標和鄰域尺寸都是以第0組的影象為基準的。 if( _descriptors.needed() ) { //t = (double)getTickCount(); int dsize = descriptorSize(); descriptorSize()被定義為 SIFT_DESCR_WIDTH*SIFT_DESCR_WIDTH*SIFT_DESCR_HIST_BINS , 即 4*4*8,共4*4個種子點,每個種子點需要8個bin(梯度直方圖)。 _descriptors.create((int)keypoints.size(), dsize, CV_32F); Mat descriptors = _descriptors.getMat(); calcDescriptors(gpyr, keypoints, descriptors, nOctaveLayers, firstOctave); //t = (double)getTickCount() - t; //printf("descriptor extraction time: %g\n", t*1000./tf); } } 呼叫calcDescriptors()為每個特徵點計算特徵向量,特徵向量是128維的。
子流程 createInitialImage() : 建立初始影象(第0層影象)
static Mat createInitialImage( const Mat& img, bool doubleImageSize, float sigma ) { Mat gray, gray_fpt; if( img.channels() == 3 || img.channels() == 4 ) cvtColor(img, gray, COLOR_BGR2GRAY); else img.copyTo(gray); gray.convertTo(gray_fpt, DataType<sift_wt>::type, SIFT_FIXPT_SCALE, 0); float sig_diff; if( doubleImageSize ) { sig_diff = sqrtf( std::max(sigma * sigma - SIFT_INIT_SIGMA * SIFT_INIT_SIGMA * 4, 0.01f) ); Mat dbl; resize(gray_fpt, dbl, Size(gray.cols*2, gray.rows*2), 0, 0, INTER_LINEAR); GaussianBlur(dbl, dbl, Size(), sig_diff, sig_diff); return dbl; } else { sig_diff = sqrtf( std::max(sigma * sigma - SIFT_INIT_SIGMA * SIFT_INIT_SIGMA, 0.01f) ); GaussianBlur(gray_fpt, gray_fpt, Size(), sig_diff, sig_diff); return gray_fpt; } }
如果需要將影象放大一倍(doubleImageSize為true),則現將源影象用雙線性插值放大一倍,再進行 方差 = ( sigma^2 - 4*0.5^2 ) 的高斯濾波,生成基礎影象;
如果不需要放大,則直接將源影象進行 方差 = ( sigma^2 - 0.5^2 ) 的高斯濾波,生成基礎影象;(0.5^2不乘4)
0.5是哪裡來的?輸入的源影象被認為是由某個假想的原始影象經過 方差 = 0.5^2 的高斯濾波後得來的。如果需要進行放大,那放大後的源影象就是由某個原始影象放大後經過 方差=(2*0.5)^2=4*0.5^2 高斯濾波得來。所有的 sigma 都是以假想的原始影象為基準的,所以做高斯濾波時,方差是 sigma^2 減去 4*0.5^2 或 0.5^2,而不是直接用sigma^2 。(兩個高斯核的疊加作用仍等效於一個新的高斯核,新高斯核的方差等於前兩個核的方差之和)
子流程 findScaleSpaceExtrema() :尋找尺度空間極值點(即特徵點)
void SIFT::findScaleSpaceExtrema( const vector<Mat>& gauss_pyr, const vector<Mat>& dog_pyr,
vector<KeyPoint>& keypoints ) const
{
int nOctaves = (int)gauss_pyr.size()/(nOctaveLayers + 3);
int threshold = cvFloor(0.5 * contrastThreshold / nOctaveLayers * 255 * SIFT_FIXPT_SCALE);
const int n = SIFT_ORI_HIST_BINS;
float hist[n];
KeyPoint kpt;
keypoints.clear();
for( int o = 0; o < nOctaves; o++ )
for( int i = 1; i <= nOctaveLayers; i++ )
{
int idx = o*(nOctaveLayers+2)+i;
const Mat& img = dog_pyr[idx];
const Mat& prev = dog_pyr[idx-1];
const Mat& next = dog_pyr[idx+1];
int step = (int)img.step1();
int rows = img.rows, cols = img.cols;
for( int r = SIFT_IMG_BORDER; r < rows-SIFT_IMG_BORDER; r++)
{
const sift_wt* currptr = img.ptr<sift_wt>(r);
const sift_wt* prevptr = prev.ptr<sift_wt>(r);
const sift_wt* nextptr = next.ptr<sift_wt>(r);
for( int c = SIFT_IMG_BORDER; c < cols-SIFT_IMG_BORDER; c++)
{
sift_wt val = currptr[c];
// find local extrema with pixel accuracy
if( std::abs(val) > threshold &&
((val > 0 && val >= currptr[c-1] && val >= currptr[c+1] &&
val >= currptr[c-step-1] && val >= currptr[c-step] && val >= currptr[c-step+1] &&
val >= currptr[c+step-1] && val >= currptr[c+step] && val >= currptr[c+step+1] &&
val >= nextptr[c] && val >= nextptr[c-1] && val >= nextptr[c+1] &&
val >= nextptr[c-step-1] && val >= nextptr[c-step] && val >= nextptr[c-step+1] &&
val >= nextptr[c+step-1] && val >= nextptr[c+step] && val >= nextptr[c+step+1] &&
val >= prevptr[c] && val >= prevptr[c-1] && val >= prevptr[c+1] &&
val >= prevptr[c-step-1] && val >= prevptr[c-step] && val >= prevptr[c-step+1] &&
val >= prevptr[c+step-1] && val >= prevptr[c+step] && val >= prevptr[c+step+1]) ||
(val < 0 && val <= currptr[c-1] && val <= currptr[c+1] &&
val <= currptr[c-step-1] && val <= currptr[c-step] && val <= currptr[c-step+1] &&
val <= currptr[c+step-1] && val <= currptr[c+step] && val <= currptr[c+step+1] &&
val <= nextptr[c] && val <= nextptr[c-1] && val <= nextptr[c+1] &&
val <= nextptr[c-step-1] && val <= nextptr[c-step] && val <= nextptr[c-step+1] &&
val <= nextptr[c+step-1] && val <= nextptr[c+step] && val <= nextptr[c+step+1] &&
val <= prevptr[c] && val <= prevptr[c-1] && val <= prevptr[c+1] &&
val <= prevptr[c-step-1] && val <= prevptr[c-step] && val <= prevptr[c-step+1] &&
val <= prevptr[c+step-1] && val <= prevptr[c+step] && val <= prevptr[c+step+1])))
這個這麼長的if語句尋找的是正的極大值和負的極小值
{
int r1 = r, c1 = c, layer = i;
if( !adjustLocalExtrema(dog_pyr, kpt, o, layer, r1, c1,
nOctaveLayers, (float)contrastThreshold,
(float)edgeThreshold, (float)sigma) )
continue;
adjustLocalExtrema() 用來精確求解角點位置,並過濾掉邊緣點。後面會解釋這個函式。
float scl_octv = kpt.size*0.5f/(1 << o);
float omax = calcOrientationHist(gauss_pyr[o*(nOctaveLayers+3) + layer],
Point(c1, r1),
cvRound(SIFT_ORI_RADIUS * scl_octv),
SIFT_ORI_SIG_FCTR * scl_octv,
hist, n);
求出關鍵點鄰域內的梯度方向直方圖並求出最大的bin包含的元素數量 omax。此直方圖在與關鍵點尺度相對應的金字塔層中求取。
scl_octv是關鍵點的組內尺度座標,SIFT_ORI_RADIUS被定義為3,因此鄰域的半徑為 3*關鍵點組內尺度;
求梯度直方圖時對鄰域內的點會依其距關鍵點的距離進行高斯加權,加權的Sigma=1.5*關鍵點組內尺度。(SIFT_ORI_SIG_FCTR被定義為1.5)。
到現在為止只是求出了直方圖,但還並未提取出關鍵點的主方向和輔方向。提取關鍵點方向由下面的幾行實現。
下面的SIFT_ORI_PEAK_RATIO被定義為0.8。元素數量多於 0.8 倍omax 的bin都被認為是關鍵點的主方向或輔方向。每個主/輔方向都用一個單獨的特徵點表示。
float mag_thr = (float)(omax * SIFT_ORI_PEAK_RATIO);
for( int j = 0; j < n; j++ )
{
int l = j > 0 ? j - 1 : n - 1;
int r2 = j < n-1 ? j + 1 : 0;
if( hist[j] > hist[l] && hist[j] > hist[r2] && hist[j] >= mag_thr )
{
float bin = j + 0.5f * (hist[l]-hist[r2]) / (hist[l] - 2*hist[j] + hist[r2]);
j是整數,但直方圖的極值點一般並不是準確地位於整數位置,因此這裡進行了極值點擬合。對直方圖一維二階泰勒展開,再取導數為0,即可擬合出精度更高的極值點,極值點的位置等於 (負的一階導數/二階導數)。
bin = bin < 0 ? n + bin : bin >= n ? bin - n : bin;
kpt.angle = 360.f - (float)((360.f/n) * bin);
if(std::abs(kpt.angle - 360.f) < FLT_EPSILON)
kpt.angle = 0.f;
keypoints.push_back(kpt);
}
}
}
}
}
}
}
上面出現了一個新函式:adjustLocalExtrema() 。它可以用來精確求解角點位置(二階泰勒級數擬合),並過濾掉邊緣點。下面闡述該函式的原理。
在 尺度空間(三維) 的 (r1,c1,layer) 點處,進行三維二階泰勒展開,可以求取極值的精確位置。
三維二階泰勒展開的原理如下:
△f = f ( x+△x, y+△y, z+△z ) - f ( x, y, z )
= f ( x+△x, y+△y, z+△z ) - f ( x+△x, y+△y, z ) +
f ( x+△x, y+△y, z ) - f ( x+△x, y, z ) +
f ( x+△x, y, z ) - f ( x, y, z )
// 進行泰勒展開近似
≈ Fz (x+△x, y+△y, z) * △z + 0.5 * Fzz(x, y, z) * △z^2 +
Fy (x+△x, y, z) * △y + 0.5 * Fyy(x, y, z) * △y^2 +
Fx (x, y, z) * △x + 0.5 * Fxx(x, y, z) * △x^2
≈ [ Fz (x, y, z) + Fzx * △x + Fyz * △y ] * △z + 0.5 * Fzz(x, y, z) * △z^2 +
[ Fy (x, y, z) + Fxy * △x ] * △y + 0.5 * Fyy(x, y, z) * △y^2 +
Fx (x, y, z) * △x + 0.5 * Fxx(x, y, z) * △x^2
上面推導的基本思路是 , f ( x, y, z ) 到 f ( x+△x, y+△y, z+△z ) 之間的增量,可以分解為:
" f ( x, y, z ) 到 f ( x+△x, y, z ) 的增量 " +
" f ( x+△x, y, z ) 到 f ( x+△x, y+△y, z ) 的增量 " +
" f ( x+△x, y+△y, z ) 到 f ( x+△x, y+△y, z+△z ) 的增量 "。
而這三個分量分別都可以用單變數(一維)的二階泰勒展開求解,由此組合起來就可以得到三維的二階泰勒展開式。
整理一下就是:
△f ≈ G0' * △ + 0.5* △'* H0 * △
其中G0代表點(x,y,z)處的梯度向量,單引號上角標代表轉置,△代表偏移( △x, △y, △z ),H0 代表點(x,y,z)處的Hessian矩陣。對兩邊求導並取導數為0的點,[△(△f)]/[△] = 0,得到的解就是極值點。 極值點的方程為: -G0 = H0 * △, 或 G0 = H0 * (-△)
這個公式以及上面的推理方法同樣適用於更高維的泰勒展開。
另外,上式的第二項 △'* H0 * △,構成了一個二次型:
2*(△f - G0' * △) = △'* H0 * △
H 的特徵向量反映了二階分量的變化方向,特徵值反映了對應的方向上二階分量變化的快慢和正負。在邊緣區域,垂直邊緣的方向上,一、二階分量變化都很劇烈,而在平行於邊緣的方向上,一、二階分量變化都很小。因此通過比較Hessian矩陣的兩個特徵值還可辨別出位於邊緣的點:大特徵值與小特徵值之比遠大於1的點是邊緣點(這些邊緣點要被幹掉)。設兩個特徵值分別是α和β,則 Tr(H)^2/det(H) = (α+β)^2/αβ = (1+γ)^2/γ 的值就可以反映出大、小特徵值之比。利用這個關係可以避免特徵值的求取。
這跟 Harris 角點檢測中去除邊緣點的原理類似,只不過Harris 角點檢測中用到的2X2特徵矩陣是由一階導數的二次式構成,而這裡的 Hession 矩陣是由二階導數構成。
adjustLocalExtrema() 的實現程式碼如下:
static bool adjustLocalExtrema( const vector<Mat>& dog_pyr, KeyPoint& kpt, int octv,
int& layer, int& r, int& c, int nOctaveLayers,
float contrastThreshold, float edgeThreshold, float sigma )
{
const float img_scale = 1.f/(255*SIFT_FIXPT_SCALE);
const float deriv_scale = img_scale*0.5f;
const float second_deriv_scale = img_scale;
const float cross_deriv_scale = img_scale*0.25f;
float xi=0, xr=0, xc=0, contr=0;
int i = 0;
for( ; i < SIFT_MAX_INTERP_STEPS; i++ )
{
int idx = octv*(nOctaveLayers+2) + layer;
const Mat& img = dog_pyr[idx];
const Mat& prev = dog_pyr[idx-1];
const Mat& next = dog_pyr[idx+1];
Vec3f dD((img.at<sift_wt>(r, c+1) - img.at<sift_wt>(r, c-1))*deriv_scale,
(img.at<sift_wt>(r+1, c) - img.at<sift_wt>(r-1, c))*deriv_scale,
(next.at<sift_wt>(r, c) - prev.at<sift_wt>(r, c))*deriv_scale);
float v2 = (float)img.at<sift_wt>(r, c)*2;
float dxx = (img.at<sift_wt>(r, c+1) + img.at<sift_wt>(r, c-1) - v2)*second_deriv_scale;
float dyy = (img.at<sift_wt>(r+1, c) + img.at<sift_wt>(r-1, c) - v2)*second_deriv_scale;
float dss = (next.at<sift_wt>(r, c) + prev.at<sift_wt>(r, c) - v2)*second_deriv_scale;
float dxy = (img.at<sift_wt>(r+1, c+1) - img.at<sift_wt>(r+1, c-1) -
img.at<sift_wt>(r-1, c+1) + img.at<sift_wt>(r-1, c-1))*cross_deriv_scale;
float dxs = (next.at<sift_wt>(r, c+1) - next.at<sift_wt>(r, c-1) -
prev.at<sift_wt>(r, c+1) + prev.at<sift_wt>(r, c-1))*cross_deriv_scale;
float dys = (next.at<sift_wt>(r+1, c) - next.at<sift_wt>(r-1, c) -
prev.at<sift_wt>(r+1, c) + prev.at<sift_wt>(r-1, c))*cross_deriv_scale;
Matx33f H(dxx, dxy, dxs,
dxy, dyy, dys,
dxs, dys, dss);
Vec3f X = H.solve(dD, DECOMP_LU);
H.solve(dD)用於求解線性方程組,該方程組是 dD = H * X 。
前面提到極值點的方程是 : G0 = H0 * (-△) ,這裡G0就是dD,H0就是H,(-△)就是X。因此為求得△,只需對方程解X求反。如下面的xi、xr和xc。
xi = -X[2];
xr = -X[1];
xc = -X[0];
if( std::abs(xi) < 0.5f && std::abs(xr) < 0.5f && std::abs(xc) < 0.5f )
break;
if( std::abs(xi) > (float)(INT_MAX/3) ||
std::abs(xr) > (float)(INT_MAX/3) ||
std::abs(xc) > (float)(INT_MAX/3) )
return false;
c += cvRound(xc);
r += cvRound(xr);
layer += cvRound(xi);
如果擬合的極值點與當前的整數點偏差大於0.5,則說明真正的極值點偏離當前整數點很多,需要換一個更好的整數點,然後重新擬合。知道偏差小於0.5或迭代超過一定次數。
if( layer < 1 || layer > nOctaveLayers ||
c < SIFT_IMG_BORDER || c >= img.cols - SIFT_IMG_BORDER ||
r < SIFT_IMG_BORDER || r >= img.rows - SIFT_IMG_BORDER )
return false;
}
// ensure convergence of interpolation
if( i >= SIFT_MAX_INTERP_STEPS )
return false;
{
int idx = octv*(nOctaveLayers+2) + layer;
const Mat& img = dog_pyr[idx];
const Mat& prev = dog_pyr[idx-1];
const Mat& next = dog_pyr[idx+1];
Matx31f dD((img.at<sift_wt>(r, c+1) - img.at<sift_wt>(r, c-1))*deriv_scale,
(img.at<sift_wt>(r+1, c) - img.at<sift_wt>(r-1, c))*deriv_scale,
(next.at<sift_wt>(r, c) - prev.at<sift_wt>(r, c))*deriv_scale);
float t = dD.dot(Matx31f(xc, xr, xi));
contr = img.at<sift_wt>(r, c)*img_scale + t * 0.5f;
if( std::abs( contr ) * nOctaveLayers < contrastThreshold )
return false;
// principal curvatures are computed using the trace and det of Hessian
float v2 = img.at<sift_wt>(r, c)*2.f;
float dxx = (img.at<sift_wt>(r, c+1) + img.at<sift_wt>(r, c-1) - v2)*second_deriv_scale;
float dyy = (img.at<sift_wt>(r+1, c) + img.at<sift_wt>(r-1, c) - v2)*second_deriv_scale;
float dxy = (img.at<sift_wt>(r+1, c+1) - img.at<sift_wt>(r+1, c-1) -
img.at<sift_wt>(r-1, c+1) + img.at<sift_wt>(r-1, c-1)) * cross_deriv_scale;
float tr = dxx + dyy;
float det = dxx * dyy - dxy * dxy;
求出了Hessian矩陣的軌跡和行列式,以便做邊緣判斷。
if( det <= 0 || tr*tr*edgeThreshold >= (edgeThreshold + 1)*(edgeThreshold + 1)*det )
return false;
}
kpt.pt.x = (c + xc) * (1 << octv);
kpt.pt.y = (r + xr) * (1 << octv);
kpt.octave = octv + (layer << 8) + (cvRound((xi + 0.5)*255) << 16);
kpt.size = sigma*powf(2.f, (layer + xi) / nOctaveLayers)*(1 << octv)*2;
kpt.response = std::abs(contr);
return true;
}
注意,關鍵點kpt的座標pt和鄰域size都是以第0組影象為基準的。size代表鄰域的直徑,它的大小為 2*Sigma, Sigma是關鍵點所在的尺度。鄰域的半徑正好是 Sigma 。
子流程 calcDescriptors() : 特徵點描述符的計算
static void calcDescriptors(const vector<Mat>& gpyr, const vector<KeyPoint>& keypoints,
Mat& descriptors, int nOctaveLayers, int firstOctave )
{
int d = SIFT_DESCR_WIDTH, n = SIFT_DESCR_HIST_BINS;
SIFT_DESCR_WIDTH 是4,SIFT_DESCR_HIST_BINS是8。
for( size_t i = 0; i < keypoints.size(); i++ )
{
KeyPoint kpt = keypoints[i];
int octave, layer;
float scale;
unpackOctave(kpt, octave, layer, scale);
CV_Assert(octave >= firstOctave && layer <= nOctaveLayers+2);
float size=kpt.size*scale;
Point2f ptf(kpt.pt.x*scale, kpt.pt.y*scale);
const Mat& img = gpyr[(octave - firstOctave)*(nOctaveLayers + 3) + layer];
解析出關鍵點金字塔組號、層號和比例。再根據組號和層號找到對應層的金字塔影象,根據scale計算出在該層金字塔影象上關鍵點的位置和鄰域大小。(關鍵點kpt內部儲存的座標pt和size都是以第0組第0層影象為基準的,因此要想在其他層中使用,需要先乘以scale變換一下)
float angle = 360.f - kpt.angle;
if(std::abs(angle - 360.f) < FLT_EPSILON)
angle = 0.f;
calcSIFTDescriptor(img, ptf, angle, size*0.5f, d, n, descriptors.ptr<float>((int)i));
計算關鍵點的描述符(一個128維的向量)
}
}
static void calcSIFTDescriptor( const Mat& img, Point2f ptf, float ori, float scl,
int d, int n, float* dst )
{
Point pt(cvRound(ptf.x), cvRound(ptf.y));
float cos_t = cosf(ori*(float)(CV_PI/180));
float sin_t = sinf(ori*(float)(CV_PI/180));
float bins_per_rad = n / 360.f;
float exp_scale = -1.f/(d * d * 0.5f);
exp_scale = -1.f / [2*(d/2)^2], 所以高斯加權的Sigma=d/2 ;
d=4,n=8。bins_per_rad表示每1°包含多少個bin。
float hist_width = SIFT_DESCR_SCL_FCTR * scl;
int radius = cvRound(hist_width * 1.4142135623730951f * (d + 1) * 0.5f);
scl等於 size*0.5,正好對應關鍵點的尺度。
SIFT_DESCR_SCL_FCTR = 3 ,hist_width是每個種子點(一個矩形區域)的寬度,共4*4個種子點。
radius 理論上應為 hist_width*d/2,但考慮到雙線性差值的需要,應將radius額外擴充套件一些,所以乘以 d+1;又考慮到旋轉時正方形的對角線,所以再乘以 根號2。
// Clip the radius to the diagonal of the image to avoid autobuffer too large exception
radius = std::min(radius, (int) sqrt((double) img.cols*img.cols + img.rows*img.rows));
cos_t /= hist_width;
sin_t /= hist_width;
這時 cos_t 和 sin_t 已經不單單是正餘弦值了,他們除了一個係數。
int i, j, k, len = (radius*2+1)*(radius*2+1), histlen = (d+2)*(d+2)*(n+2);
int rows = img.rows, cols = img.cols;
AutoBuffer<float> buf(len*6 + histlen);
float *X = buf, *Y = X + len, *Mag = Y, *Ori = Mag + len, *W = Ori + len;
float *RBin = W + len, *CBin = RBin + len, *hist = CBin + len;
for( i = 0; i < d+2; i++ )
{
for( j = 0; j < d+2; j++ )
for( k = 0; k < n+2; k++ )
hist[(i*(d+2) + j)*(n+2) + k] = 0.;
}
for( i = -radius, k = 0; i <= radius; i++ )
for( j = -radius; j <= radius; j++ )
{
// Calculate sample's histogram array coords rotated relative to ori.
// Subtract 0.5 so samples that fall e.g. in the center of row 1 (i.e.
// r_rot = 1.5) have full weight placed in row 1 after interpolation.
float c_rot = j * cos_t - i * sin_t;
float r_rot = j * sin_t + i * cos_t;
由於cos_t 和 sin_t除過了hist_width,所以這時旋轉後的座標的 c_rot 和 r_rot 的單位直接就是種子點了。下面的 rbin 和 cbin 計算的就是種子點的編號。種子點的編號從(-1,-1)到(d,d)。減去0.5是為了方便差值時計算權重。
float rbin = r_rot + d/2 - 0.5f;
float cbin = c_rot + d/2 - 0.5f;
int r = pt.y + i, c = pt.x + j;
if( rbin > -1 && rbin < d && cbin > -1 && cbin < d &&
r > 0 && r < rows - 1 && c > 0 && c < cols - 1 )
超出窗邊界或影象邊界的點不予考慮。注意這裡 rbin 和 cbin 的下線都是 -1,也就是說它們可能為負
{
float dx = (float)(img.at<sift_wt>(r, c+1) - img.at<sift_wt>(r, c-1));
float dy = (float)(img.at<sift_wt>(r-1, c) - img.at<sift_wt>(r+1, c));
X[k] = dx; Y[k] = dy; RBin[k] = rbin; CBin[k] = cbin;
W[k] = (c_rot * c_rot + r_rot * r_rot)*exp_scale;
k++;
}
}
len = k;
fastAtan2(Y, X, Ori, len, true);
magnitude(X, Y, Mag, len);
exp(W, W, len);
Ori 存放角度陣列,Mag存放梯度的能量陣列,W存放高斯權重
for( k = 0; k < len; k++ )
{
float rbin = RBin[k], cbin = CBin[k];
float obin = (Ori[k] - ori)*bins_per_rad;
float mag = Mag[k]*W[k];
mag等於高斯權重乘以梯度能量
int r0 = cvFloor( rbin );
int c0 = cvFloor( cbin );
int o0 = cvFloor( obin );
由於 rbin 可能為負,所以 r0 可能為 -1,這樣r0的取值就是 [-1,d-1],而後面雙線性插值時還會擴充套件到d,因此r0的取值是[-1,d],共d+2個取值,c0也一樣,所以hist陣列的r維和c維的size都是 d+2。
rbin -= r0;
cbin -= c0;
obin -= o0;
只保留rbin、cbin和obin的小數部分。每個樣點對與之相鄰的各個bin的貢獻比重與該樣點到每個bin的距離成反比。
if( o0 < 0 )
o0 += n;
if( o0 >= n )
o0 -= n;
角度的bin是迴圈的,所以處理一下 o0。這樣o0的範圍就是 [0,n-1] ,但後面雙線性差值時可能還會擴充套件到n,因此o0的範圍就是 [0,n],共n+1個值,所以 hist 陣列o維的長度應為 n+1 。( ?? 這裡的程式中o維的長度定義為了 n+2,是我分析錯了還是opencv確實保留得多了? )。
// histogram update using tri-linear interpolation
float v_r1 = mag*rbin, v_r0 = mag - v_r1;
float v_rc11 = v_r1*cbin, v_rc10 = v_r1 - v_rc11;
float v_rc01 = v_r0*cbin, v_rc00 = v_r0 - v_rc01;
float v_rco111 = v_rc11*obin, v_rco110 = v_rc11 - v_rco111;
float v_rco101 = v_rc10*obin, v_rco100 = v_rc10 - v_rco101;
float v_rco011 = v_rc01*obin, v_rco010 = v_rc01 - v_rco011;
float v_rco001 = v_rc00*obin, v_rco000 = v_rc00 - v_rco001;
對於一個三維的直方圖, 樣點對與之相鄰的位於(r,c,o)處的bin的貢獻是:
mag * (dis(r,rbin)) * (dis(c,cbin)) * (dis(o,obin))
其中 dis(r,rbin) 表示r到rbin的距離(差)。注意r,c,o需要滿足:dis(r,rbin)<1 && dis(c,cbin)<1 && dis(o,obin)<1 ,才算是與樣點相鄰。
int idx = ((r0+1)*(d+2) + c0+1)*(n+2) + o0;
hist[idx] += v_rco000;
hist[idx+1] += v_rco001;
hist[idx+(n+2)] += v_rco010;
hist[idx+(n+3)] += v_rco011;
hist[idx+(d+2)*(n+2)] += v_rco100;
hist[idx+(d+2)*(n+2)+1] += v_rco101;
hist[idx+(d+3)*(n+2)] += v_rco110;
hist[idx+(d+3)*(n+2)+1] += v_rco111;
}
// finalize histogram, since the orientation histograms are circular
for( i = 0; i < d; i++ )
for( j = 0; j < d; j++ )
{
int idx = ((i+1)*(d+2) + (j+1))*(n+2);
hist[idx] += hist[idx+n];
hist[idx+1] += hist[idx+n+1];
for( k = 0; k < n; k++ )
dst[(i*d + j)*n + k] = hist[idx+k];
}
將 hist 拷貝到 dst
// copy histogram to the descriptor,
// apply hysteresis thresholding
// and scale the result, so that it can be easily converted
// to byte array
float nrm2 = 0;
len = d*d*n;
for( k = 0; k < len; k++ )
nrm2 += dst[k]*dst[k];
float thr = std::sqrt(nrm2)*SIFT_DESCR_MAG_THR;
for( i = 0, nrm2 = 0; i < k; i++ )
{
float val = std::min(dst[i], thr);
dst[i] = val;
nrm2 += val*val;
}
nrm2 = SIFT_INT_DESCR_FCTR/std::max(std::sqrt(nrm2), FLT_EPSILON);
抑制暴走的特徵:每個特徵不能大於特徵向量總模長的 0.2 倍。
#if 1
for( k = 0; k < len; k++ )
{
dst[k] = saturate_cast<uchar>(dst[k]*nrm2);
}
歸一化並將每個特徵都轉化為8位深度。
#else
float nrm1 = 0;
for( k = 0; k < len; k++ )
{
dst[k] *= nrm2;
nrm1 += dst[k];
}
nrm1 = 1.f/std::max(nrm1, FLT_EPSILON);
for( k = 0; k < len; k++ )
{
dst[k] = std::sqrt(dst[k] * nrm1);//saturate_cast<uchar>(std::sqrt(dst[k] * nrm1)*SIFT_INT_DESCR_FCTR);
}
#endif
}