之前結合不同人的資料理解了sift的原理,這裏通過opencv中的代碼來加深對sift的實現的理解。
使得能夠從原理性理解到源碼級的理解。不過該博文還是大量基於《趙春江, opencv2.4.9 源碼分析,SIFT http://blog.csdn.net/zhaocj》的。
在opencv3.0中,已經看不到sift.cpp源代碼了,在2.4.10中還是有的:opencv\sources\modules\nonfree\src下面。可以看出這是一個非免費代碼。使用需要付費的,畢竟sift是哥倫比亞大學的專利。
對於sift(1)原理的對照來解釋代碼。
ps:在2.3與2.4版本中該sift的代碼是兩個不同的版本。
ps:2.4.10相比較於2.4.4中該源碼的546處增加了兩行,修復了一個小bug。
#define CV_EXPORTS __declspec(dllexport)
#define CV_EXPORTS_W CV_EXPORTS
#define CV_WRAP
這兩個關鍵字在這裏有人回答了。
2、sift類的定義:
在文件“opencv\sources\modules\features2d\include\opencv2\features2d.hpp”中:
class CV_EXPORTS_W_SIMPLE KeyPoint{};
在文件“opencv\sources\modules\nonfree\include\opencv2\nonfree\features2d.hpp”中:
#ifndef __OPENCV_NONFREE_FEATURES_2D_HPP__
#define __OPENCV_NONFREE_FEATURES_2D_HPP__
#include "opencv2/features2d/features2d.hpp"
//該頭文件中包含了類Feature2D的定義。而看過裏面這個Feature2D又是基於兩個基類派生出來的,這裏就不接着往下挖了。
//class CV_EXPORTS_W Feature2D : public FeatureDetector,
// public DescriptorExtracto{}
#ifdef __cplusplus
namespace cv
{
class CV_EXPORTS_W SIFT : public Feature2D
{
public:
CV_WRAP explicit SIFT( int nfeatures=0, int nOctaveLayers=3,
double contrastThreshold=0.04, double edgeThreshold=10,
double sigma=1.6);
//!返回描述子的大小(128)
CV_WRAP int descriptorSize() const;
//! 返回描述子類型
CV_WRAP int descriptorType() const;
//! 使用SIFT算法找到關鍵點
void operator()(InputArray img, InputArray mask,
vector<KeyPoint>& keypoints) const;
//! 使用SIFT算法找到關鍵點然後計算描述子。
//! (可選的)使用該操作計算用戶提供的關鍵字的描述子
void operator()(InputArray img, InputArray mask,
vector<KeyPoint>& keypoints,
OutputArray descriptors,
bool useProvidedKeypoints=false) const;
AlgorithmInfo* info() const;
//!建立高斯金字塔
void buildGaussianPyramid( const Mat& base, vector<Mat>& pyr, int nOctaves ) const;
//!建立DoG金字塔
void buildDoGPyramid( const vector<Mat>& pyr, vector<Mat>& dogpyr ) const;
//!查找尺度空間的極值
void findScaleSpaceExtrema( const vector<Mat>& gauss_pyr, const vector<Mat>& dog_pyr,
vector<KeyPoint>& keypoints ) const;
protected:
void detectImpl( const Mat& image, vector<KeyPoint>& keypoints, const Mat& mask=Mat() ) const;
void computeImpl( const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors ) const;
CV_PROP_RW int nfeatures;//特徵
CV_PROP_RW int nOctaveLayers;//組數
CV_PROP_RW double contrastThreshold;
CV_PROP_RW double edgeThreshold;//邊緣閾值
CV_PROP_RW double sigma;//尺度
};
typedef SIFT SiftFeatureDetector;
typedef SIFT SiftDescriptorExtractor;
} /* namespace cv */
#endif /* __cplusplus */
#endif
/* End of file. */
上面就是SIFT類的定義,從中可以看出該類主要實現的是高斯金字塔的建立、DoG金字塔的建立、尺度空間中極值點的搜尋等等。
0、SIFT構造函數,查詢用戶是否提供關鍵點,然後調用1、2、3、4、5;
1、首先我們要做的就是建立初始圖像createInitialImage();
2、建立高斯金字塔 SIFT::buildGaussianPyramid();
3、建立DoG金字塔 SIFT::buildDoGPyramid();
4、在DoG中查找尺度空間極值點SIFT::findScaleSpaceExtrema();其中會調用4.1和4.2
4.1、對找到的極值點進行曲線插值擬合,並過濾 adjustLocalExtrema();
4.2、計算方向直方圖 calcOrientationHist();
5、計算描述子 calcDescriptors();
5.1、計算sift描述子calcSIFTDescriptor();
1、
//按照用戶指定( bool doubleImageSize)是否需要用線性插值的方法擴展圖像;然後進行高斯模糊。
//公式a
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);
//調整圖像的像素數據類型
//typedef float sift_wt;
//static const int SIFT_FIXPT_SCALE = 1
gray.convertTo(gray_fpt, DataType<sift_wt>::type, SIFT_FIXPT_SCALE, 0);
float sig_diff;
//如果需要擴大圖像的長寬尺寸
if( doubleImageSize )
{
// SIFT_INIT_SIGMA 爲0.5,即輸入圖像的尺度,SIFT_INIT_SIGMA×2=1.0,即圖像擴大2 倍以後的尺度,sig_diff 爲公式6 中的高斯函數所需要的方差
sig_diff = sqrtf( std::max(sigma * sigma - SIFT_INIT_SIGMA * SIFT_INIT_SIGMA * 4, 0.01f) );
Mat dbl;
//利用雙線性插值法把圖像的長寬都擴大2 倍
resize(gray_fpt, dbl, Size(gray.cols*2, gray.rows*2), 0, 0, INTER_LINEAR);
//利用上面提到的公式a對圖像進行高斯平滑處理
GaussianBlur(dbl, dbl, Size(), sig_diff, sig_diff);
//輸出圖像矩陣
return dbl;
}
else
{
//不需要擴大圖像的尺寸,sig_diff 爲公式6 中的高斯函數所需要的方差
sig_diff = sqrtf( std::max(sigma * sigma - SIFT_INIT_SIGMA * SIFT_INIT_SIGMA, 0.01f) );
//利用上面提到的公式a對圖像進行高斯平滑處理
GaussianBlur(gray_fpt, gray_fpt, Size(), sig_diff, sig_diff);
//輸出圖像矩陣
return gray_fpt;
}
}
2、構建高斯金字塔
//輸入一張初始圖像
涉及到公式b:
void SIFT::buildGaussianPyramid( const Mat& base, vector<Mat>& pyr, int nOctaves ) const
{
//向量數組sig 表示每組中計算各層圖像所需的方差,nOctaveLayers + 3 即爲S = s+3;
vector<double> sig(nOctaveLayers + 3);
//定義高斯金字塔的總層數,nOctaves*(nOctaveLayers + 3)即組數×層數
pyr.resize(nOctaves*(nOctaveLayers + 3));
//提前計算好各層圖像所需的方差:
// \sigma_{total}^2 = \sigma_{i}^2 + \sigma_{i-1}^2
//第一層圖像的尺度爲基準層尺度σ0
sig[0] = sigma;
//每一層的尺度倍數k
double k = pow( 2., 1. / nOctaveLayers );
//從第1層開始計算所有的尺度
for( int i = 1; i < nOctaveLayers + 3; i++ )
{
//由上面的公式計算前一層的尺度
double sig_prev = pow(k, (double)(i-1))*sigma;
//計算當前尺度
double sig_total = sig_prev*k;
//計算公式a 中高斯函數所需的方差,並存入sig 數組內
sig[i] = std::sqrt(sig_total*sig_total - sig_prev*sig_prev);
}
//遍歷高斯金字塔的所有層,構建高斯金字塔
for( int o = 0; o < nOctaves; o++ )
{
for( int i = 0; i < nOctaveLayers + 3; i++ )
{
//dst 爲當前層圖像矩陣
Mat& dst = pyr[o*(nOctaveLayers + 3) + i];
//如果當前層爲高斯金字塔的第0 組第0 層,則直接賦值
if( o == 0 && i == 0 )
//把由createInitialImage 函數得到的基層圖像矩陣賦予該層
dst = base;
// 新組的第0層需要從之前組中進行降採樣
//如果當前層是除了第0 組以外的其他組中的第0 層,則要進行降採樣處理
else if( i == 0 )
{
//提取出當前層所在組的前一組中的倒數第3 層圖像
const Mat& src = pyr[(o-1)*(nOctaveLayers + 3) + nOctaveLayers];
//隔點降採樣處理
resize(src, dst, Size(src.cols/2, src.rows/2),
0, 0, INTER_NEAREST);
}
//除了以上兩種情況以外的其他情況的處理,即正常的組內生成不同層
else
{
//提取出當前層的前一層圖像
const Mat& src = pyr[o*(nOctaveLayers + 3) + i-1];
//根據公式3,由前一層尺度圖像得到當前層的尺度圖像
GaussianBlur(src, dst, Size(), sig[i], sig[i]);
}
}
}
3、構建DoG 金字塔:
根據第2步得到的高斯金字塔,這裏得到DoG金字塔
void SIFT::buildDoGPyramid( const vector<Mat>& gpyr, vector<Mat>& dogpyr ) const
{
//計算金字塔的組的數量
int nOctaves = (int)gpyr.size()/(nOctaveLayers + 3);
//定義DoG 金字塔的總層數,DoG 金字塔比高斯金字塔每組少一層
dogpyr.resize( nOctaves*(nOctaveLayers + 2) );
//遍歷DoG 的所有層,構建DoG 金字塔
for( int o = 0; o < nOctaves; o++ )
{
for( int i = 0; i < nOctaveLayers + 2; i++ )
{
//提取出高斯金字塔的當前層圖像
const Mat& src1 = gpyr[o*(nOctaveLayers + 3) + i];
//提取出高斯金字塔的上層圖像
const Mat& src2 = gpyr[o*(nOctaveLayers + 3) + i + 1];
//提取出DoG 金字塔的當前層圖像
Mat& dst = dogpyr[o*(nOctaveLayers + 2) + i];
//DoG 金字塔的當前層圖像等於高斯金字塔的當前層圖像減去高斯金字塔的上層圖像
subtract(src2, src1, dst, noArray(), DataType<sift_wt>::type);
}
}
4、在DoG 尺度空間內找到極值點
在DoG尺度空間中找極值點,並按照主曲率的對比度刪除不好的極值點
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);
//SIFT_FIXPT_SCALE = 1,設定一個閾值用於判斷在DoG 尺度圖像中像素的大小
int threshold = cvFloor(0.5 * contrastThreshold / nOctaveLayers * 255 * SIFT_FIXPT_SCALE);
//SIFT_ORI_HIST_BINS = 36,定義梯度方向直方圖的柱的數量
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++ )
{
//DoG 金字塔的當前層索引
int idx = o*(nOctaveLayers+2)+i;
//DoG 金字塔當前層的尺度圖像
const Mat& img = dog_pyr[idx];
//DoG 金字塔下層的尺度圖像
const Mat& prev = dog_pyr[idx-1];
//DoG 金字塔上層的尺度圖像
const Mat& next = dog_pyr[idx+1];
int step = (int)img.step1();
//圖像的長和寬
int rows = img.rows, cols = img.cols;
//SIFT_IMG_BORDER = 5,該變量的作用是保留一部分圖像的四周邊界
for( int r = SIFT_IMG_BORDER; r < rows-SIFT_IMG_BORDER; r++)
{
//DoG 金字塔當前層圖像的當前行指針
const sift_wt* currptr = img.ptr<sift_wt>(r);
//DoG 金字塔下層圖像的當前行指針
const sift_wt* prevptr = prev.ptr<sift_wt>(r);
//DoG 金字塔上層圖像的當前行指針
const sift_wt* nextptr = next.ptr<sift_wt>(r);
for( int c = SIFT_IMG_BORDER; c < cols-SIFT_IMG_BORDER; c++)
{
//DoG 金字塔當前層尺度圖像的像素值
sift_wt val = currptr[c];
// find local extrema with pixel accuracy
//精確定位局部極值點
//如果滿足if 條件,則找到了極值點,即候選特徵點
if( std::abs(val) > threshold &&
//像素值要大於一定的閾值才穩定,即要具有較強的對比度
//下面的邏輯判斷被“與”分爲兩個部分,前一個部分要滿足像素值大於0,在3×3×3 的立方體內與周圍26 個鄰近像素比較找極大值,
//後一個部分要滿足像素值小於0,找極小值
((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])))
{
//三維座標,長、寬、層(層與尺度相對應)
int r1 = r, c1 = c, layer = i;
// adjustLocalExtrema 函數的作用是調整局部極值的位置,即找到亞像素級精度的特徵點,該函數在後面給出詳細的分析
//如果滿足if 條件,說明該極值點不是特徵點,繼續上面的for循環
if( !adjustLocalExtrema(dog_pyr, kpt, o, layer, r1, c1,
nOctaveLayers, (float)contrastThreshold,
(float)edgeThreshold, (float)sigma) )
continue;
//計算特徵點相對於它所在組的基準層的尺度,即公式5 所得尺度
float scl_octv = kpt.size*0.5f/(1 << o);
// calcOrientationHist 函數爲計算特徵點的方向角度,後面給出該
函數的詳細分析
//SIFT_ORI_SIG_FCTR = 1.5f , SIFT_ORI_RADIUS = 3 *
SIFT_ORI_SIG_FCTR
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);
//SIFT_ORI_PEAK_RATIO = 0.8f
//計算直方圖輔方向的閾值,即主方向的80%
float mag_thr = (float)(omax * SIFT_ORI_PEAK_RATIO);
//計算特徵點的方向
for( int j = 0; j < n; j++ )
{
//j 爲直方圖當前柱體索引,l 爲前一個柱體索引,r2 爲後一個柱體索引,如果l 和r2 超出了柱體範圍,則要進行圓周循環處理
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 )
{
//公式26
float bin = j + 0.5f * (hist[l]-hist[r2]) / (hist[l] - 2*hist[j] + hist[r2]);
//圓周循環處理
bin = bin < 0 ? n + bin : bin >= n ? bin - n : bin;
//公式27,得到特徵點的方向
kpt.angle = 360.f - (float)((360.f/n) * bin);
//如果方向角度十分接近於360 度,則就讓它等於0 度
if(std::abs(kpt.angle - 360.f) < FLT_EPSILON)
kpt.angle = 0.f;
//保存特徵點
keypoints.push_back(kpt);
}
}
}
}
}
}
爲了求公式15採用的有限差分的式子:
4.1、精確找到圖像的特徵點
//dog_pyr 爲DoG 金字塔,kpt 爲特徵點,octv 和layer 爲極值點所在的組和組內的層,r 和c 爲極值點的位置座標
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 )
{
// SIFT_FIXPT_SCALE = 1,img_scale 爲對圖像進行歸一化處理的係數
const float img_scale = 1.f/(255*SIFT_FIXPT_SCALE);
//上面圖片中27分母的倒數;
const float deriv_scale = img_scale*0.5f;
//上面圖片中28分母的倒數;
const float second_deriv_scale = img_scale;
//上面圖片中29分母的倒數;
const float cross_deriv_scale = img_scale*0.25f;
float xi=0, xr=0, xc=0, contr=0;
int i = 0;
// SIFT_MAX_INTERP_STEPS = 5,表示循環迭代5 次
for( ; i < SIFT_MAX_INTERP_STEPS; i++ )
{
//找到極值點所在的DoG 金字塔的層索引
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];//上層尺度圖像
//變量dD 就是公式12 中的∂f / ∂X,一階偏導的公式如上圖中27
Vec3f dD((img.at<sift_wt>(r, c+1) - img.at<sift_wt>(r, c-1))*deriv_scale,//f 對x 的一階偏導
(img.at<sift_wt>(r+1, c) - img.at<sift_wt>(r-1, c))*deriv_scale,//f 對y 的一階偏導
(next.at<sift_wt>(r, c) - prev.at<sift_wt>(r, c))*deriv_scale);//f 對尺度σ的一階偏導
//當前像素值的2 倍
float v2 = (float)img.at<sift_wt>(r, c)*2;
//下面是求二階純偏導,如上圖中28
//這裏的x,y,s 分別代表X = (x, y, σ)T 中的x,y,σ
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;
//下面是求二階混合偏導,如上圖中29
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;
//變量H 就是公式8 中的∂2f / ∂X2,它的具體展開形式可以在公式9 中看到
Matx33f H(dxx, dxy, dxs,
dxy, dyy, dys,
dxs, dys, dss);
//求方程A×X=B,即X=(A^-1)×B,這裏A 就是H,B 就是dD
Vec3f X = H.solve(dD, DECOMP_LU);
//可以看出上面求得的X 與公式12 求得的變量差了一個符號,因此下面的變量都加上了負號
xi = -X[2];//層座標的偏移量,這裏的層與圖像尺度相對應
xr = -X[1];//縱座標的偏移量
xc = -X[0];//橫座標的偏移量
//如果由泰勒級數插值得到的三個座標的偏移量都小於0.5,說明已經找到特徵點,則退出迭代
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);
//如果新的座標超出了金字塔的座標範圍,則說明該極值點不是特徵點,函數返回
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;//沒有找到特徵點,返回
{
//由上面得到的層座標計算它在DoG 金字塔中的層索引
int idx = octv*(nOctaveLayers+2) + layer;
//該層索引所對應的DoG 金字塔的當前層尺度圖像
const Mat& img = dog_pyr[idx];
//DoG 金字塔的下層尺度圖像
const Mat& prev = dog_pyr[idx-1];
//DoG 金字塔的上層尺度圖像
const Mat& next = dog_pyr[idx+1];
//再次計算公式12 中的∂f / ∂X
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);
//dD 點乘(xc, xr, xi),點乘類似於MATLAB 中的點乘
//變量t 就是公式13 中等號右邊的第2 項內容
float t = dD.dot(Matx31f(xc, xr, xi));
//計算公式13,求極值點處圖像的灰度值,即響應值
contr = img.at<sift_wt>(r, c)*img_scale + t * 0.5f;
//由公式14 判斷響應值是否穩定
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;//當前像素灰度值的2 倍
//計算矩陣H 的4 個元素
//二階純偏導,如上圖中的28
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;
//二階混合偏導,如上圖29
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;
//求矩陣的直跡,公式16
float tr = dxx + dyy;
//求矩陣的行列式,公式17
float det = dxx * dyy - dxy * dxy;
//邏輯“或”的前一項表示矩陣的行列式值不能小於0,後一項如公式19
if( det <= 0 || tr*tr*edgeThreshold >= (edgeThreshold + 1)*(edgeThreshold + 1)*det )
return false;//不是特徵點,返回
}
//保存特徵點信息
//特徵點對應於輸入圖像的橫座標位置
kpt.pt.x = (c + xc) * (1 << octv);
//特徵點對應於輸入圖像的縱座標位置
//需要注意的是,這裏的輸入圖像特指實際的輸入圖像擴大一倍以後的圖像,因爲這裏的octv 是包括了前面理論分析部分提到的金字塔
//的第‐1 組
kpt.pt.y = (r + xr) * (1 << octv);
//按一定格式保存特徵點所在的組、層以及插值後的層的偏移量
kpt.octave = octv + (layer << 8) + (cvRound((xi + 0.5)*255) << 16);
//特徵點相對於輸入圖像的尺度,即公式5
//同樣的,這裏的輸入圖像也是特指實際的輸入圖像擴大一倍以後的圖像,所以下面的語句其實是比公式5 多了一項——乘以2
kpt.size = sigma*powf(2.f, (layer + xi) / nOctaveLayers)*(1 << octv)*2;
//特徵點的響應值
kpt.response = std::abs(contr);
//是特徵點,返回
return true;
4.2、計算特徵點的方向角度
上圖是opencv中使用的方向直方圖的平滑公式,與lowe推薦的不同
// Computes a gradient orientation histogram at a specified pixel
//img 爲特徵點所在的高斯尺度圖像;
//pt 爲特徵點在該尺度圖像的座標點;
//radius 爲鄰域半徑,即公式30;
//sigma 爲高斯函數的方差,即公式33;
//hist 爲梯度方向直方圖;
//n 爲梯度方向直方圖柱體的數量,n=36
//該函數返回直方圖的主峯值
static float calcOrientationHist( const Mat& img, Point pt, int radius,
float sigma, float* hist, int n )
{
//len 爲計算特徵點方向時的特徵點領域像素的數量
int i, j, k, len = (radius*2+1)*(radius*2+1);
// expf_scale 爲高斯加權函數中e 指數中的常數部分
float expf_scale = -1.f/(2.f * sigma * sigma);
//分配一段內存空間
//X 表示x 軸方向的差分,Y 表示y 軸方向的差分,Mag 爲梯度幅值,Ori 爲梯度幅角,W 爲高斯加權值
//上述變量在buf 空間的分配是:X 和Mag 共享一段長度爲len 的空間,Y 和Ori 分別佔用一段長度爲len 的空間,W 佔用一段
//長度爲len+2 的空間,它們的空間順序爲X(Mag)在buf 的最下面,然後是Y,Ori,最後是W
float *X = buf, *Y = X + len, *Mag = X, *Ori = Y + len, *W = Ori + len;
//temphist 表示暫存的梯度方向直方圖,空間長度爲n+2,空間位置是在W 的上面
//之所以temphist 的長度是n+2,W 的長度是len+2,而不是n 和len,是因爲要進行圓周循環操作,必須給temphist 的前後各
//留出兩個空間位置
float* temphist = W + len + 2;
for( i = 0; i < n; i++ )
//直方圖變量清空
temphist[i] = 0.f;
//計算x 軸、y 軸方向的導數,以及高斯加權值
for( i = -radius, k = 0; i <= radius; i++ )
{
int y = pt.y + i;//鄰域像素的y 軸座標
//判斷y 軸座標是否超出圖像的範圍
if( y <= 0 || y >= img.rows - 1 )
continue;
for( j = -radius; j <= radius; j++ )
{
int x = pt.x + j;//鄰域像素的x 軸座標
//判斷x 軸座標是否超出圖像的範圍
if( x <= 0 || x >= img.cols - 1 )
continue;
//分別計算x 軸和y 軸方向的差分,即上圖中27 的分子部分,因爲只需要相對
值,所有分母部分可以不用計算
float dx = (float)(img.at<sift_wt>(y, x+1) - img.at<sift_wt>(y, x-1));
float dy = (float)(img.at<sift_wt>(y-1, x) - img.at<sift_wt>(y+1, x));
//保存變量,這裏的W 爲高斯函數的e 指數
X[k] = dx; Y[k] = dy; W[k] = (i*i + j*j)*expf_scale;
k++;//鄰域像素的計數值加1
}
}
//這裏的len 爲特徵點實際的鄰域像素的數量
len = k;
// compute gradient values, orientations and the weights over the pixel neighborhood
//計算鄰域中所有元素的高斯加權值W,梯度幅角Ori 和梯度幅值Mag
exp(W, W, len);
fastAtan2(Y, X, Ori, len, true);
magnitude(X, Y, Mag, len);
//計算梯度方向直方圖
for( k = 0; k < len; k++ )
{
//判斷鄰域像素的梯度幅角屬於36 個柱體的哪一個
int bin = cvRound((n/360.f)*Ori[k]);
//如果超出範圍,則利用圓周循環確定其真正屬於的那個柱體
if( bin >= n )
bin -= n;
if( bin < 0 )
bin += n;
//累積經高斯加權處理後的梯度幅值
temphist[bin] += W[k]*Mag[k];
}
// smooth the histogram
//平滑直方圖
//爲了圓周循環,提前填充好直方圖前後各兩個變量
temphist[-1] = temphist[n-1];
temphist[-2] = temphist[n-2];
temphist[n] = temphist[0];
temphist[n+1] = temphist[1];
for( i = 0; i < n; i++ )
{
//利用上圖中公式34,進行平滑直方圖操作,注意這裏與lowe建議的方法不一樣
hist[i] = (temphist[i-2] + temphist[i+2])*(1.f/16.f) +
(temphist[i-1] + temphist[i+1])*(4.f/16.f) +
temphist[i]*(6.f/16.f);
}
//計算直方圖的主峯值
float maxval = hist[0];
for( i = 1; i < n; i++ )
maxval = std::max(maxval, hist[i]);
return maxval;//返回直方圖的主峯值
}
5、計算特徵點描述符calcDescriptors 函數
相對於輸入圖像來說
static void calcDescriptors(const vector<Mat>& gpyr, const vector<KeyPoint>& keypoints,
Mat& descriptors, int nOctaveLayers, int firstOctave )
{
//SIFT_DESCR_WIDTH = 4,SIFT_DESCR_HIST_BINS = 8
int d = SIFT_DESCR_WIDTH, n = SIFT_DESCR_HIST_BINS;
//遍歷所有特徵點
for( size_t i = 0; i < keypoints.size(); i++ )
{
KeyPoint kpt = keypoints[i];//當前特徵點
int octave, layer;// octave 爲組索引,layer 爲層索引
float scale;//尺度
//從特徵點結構變量中分離出該特徵點所在的組、層以及它的尺度
//一般情況下,這裏的尺度scale = 2^(-o),o 表示特徵點所在的組,即octave 變量
unpackOctave(kpt, octave, layer, scale);
//確保組和層在合理的範圍內
CV_Assert(octave >= firstOctave && layer <= nOctaveLayers+2);
//得到當前特徵點相對於它所在高斯金字塔的組的基準層尺度圖像的尺度,即公式20
//特徵點變量保存的尺度是相對於輸入圖像的尺度,即上圖中12
//上圖中12 的表達式乘以2^(‐o) 就得到了公式20 的表達式
float size=kpt.size*scale;
//得到當前特徵點所在的高斯尺度圖像的位置座標,具體原理與上面得到的尺度相類似
Point2f ptf(kpt.pt.x*scale, kpt.pt.y*scale);
//得到當前特徵點所在的高斯尺度圖像矩陣
const Mat& img = gpyr[(octave - firstOctave)*(nOctaveLayers + 3) + layer];
//得到當前特徵點的方向角度
float angle = 360.f - kpt.angle;
//如果方向角度十分接近於360 度,則就讓它等於0 度
if(std::abs(angle - 360.f) < FLT_EPSILON)
angle = 0.f;
//計算特徵點的特徵矢量
// size*0.5f,這裏尺度又除以2,可能是爲了減小運算量(不是很確定)
calcSIFTDescriptor(img, ptf, angle, size*0.5f, d, n, descriptors.ptr<float>((int)i));
}
}
5.1、計算SIFT 算法中特徵點的特徵矢量
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));//特徵點的位置座標
//特徵點方向的餘弦和正弦,即cosθ 和sinθ
float cos_t = cosf(ori*(float)(CV_PI/180));
float sin_t = sinf(ori*(float)(CV_PI/180));
//n = 8,45 度的倒數
float bins_per_rad = n / 360.f;
//高斯加權函數中的e 指數的常數部分
float exp_scale = -1.f/(d * d * 0.5f);
//SIFT_DESCR_SCL_FCTR = 3.f,即3σ
float hist_width = SIFT_DESCR_SCL_FCTR * scl;
//特徵點鄰域區域的半徑,即公式28
int radius = cvRound(hist_width * 1.4142135623730951f * (d + 1) * 0.5f);
// 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;
//len 爲特徵點鄰域區域內像素的數量,histlen 爲直方圖的數量,即特徵矢量的長度,實際應爲d×d×n,之所以每個變量
//又加上了2,是因爲要爲圓周循環留出一定的內存空間
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);
//X 表示x 方向梯度,Y 表示y 方向梯度,Mag 表示梯度幅值,Ori 表示梯度幅角,W 爲高斯加權值,其中Y 和Mag
// 共享一段內存空間,長度都爲len,它們在buf 的順序爲X 在最下面,然後是Y(Mag),Ori,最後是W
float *X = buf, *Y = X + len, *Mag = Y, *Ori = Mag + len, *W = Ori + len;
//下面是三維直方圖的變量,RBin 和CBin 分別表示d×d 鄰域範圍的橫、縱座標,hist 表示直方圖的值。RBin
//和CBin 的長度都是len,hist 的長度爲histlen,順序爲RBin 在W 的上面,然後是CBin,最後是hist
float *RBin = W + len, *CBin = RBin + len, *hist = CBin + len;
//直方圖數組hist 清零
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.
//根據公式29 計算旋轉後的位置座標
float c_rot = j * cos_t - i * sin_t;
float r_rot = j * sin_t + i * cos_t;
//把鄰域區域的原點從中心位置移到該區域的左下角,以便後面的使用。因爲變量cos_t 和sin_t 都已
//進行了歸一化處理,所以原點位移時只需要加d/2 即可。而再減0.5f的目的是進行座標平移,從而在
//三線性插值計算中,計算的是正方體內的點對正方體8 個頂點的貢獻大小,而不是對正方體的中心點的
//貢獻大小。之所以沒有對角度obin 進行座標平移,是因爲角度是連續的量,無需平移
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;
//確定鄰域像素是否在d×d 的正方形內,以及是否超過了圖像邊界
if( rbin > -1 && rbin < d && cbin > -1 && cbin < d &&
r > 0 && r < rows - 1 && c > 0 && c < cols - 1 )
{
//根據上圖中27 計算x 和y 方向的一階導數,這裏省略了公式中的分母部分,因爲沒有分母部分不影響
//後面所進行的歸一化處理
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;
//高斯加權函數中的e 指數部分
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);//計算高斯加權函數
//遍歷所有鄰域像素
for( k = 0; k < len; k++ )
{
//得到d×d 鄰域區域的座標,即三維直方圖的底內的位置
float rbin = RBin[k], cbin = CBin[k];
//得到幅角所屬的8 等份中的某一個等份,即三維直方圖的高的位置
float obin = (Ori[k] - ori)*bins_per_rad;
float mag = Mag[k]*W[k];//得到高斯加權以後的梯度幅值
//向下取整
//r0,c0 和o0 爲三維座標的整數部分,它表示屬於的哪個正方體
int r0 = cvFloor( rbin );
int c0 = cvFloor( cbin );
int o0 = cvFloor( obin );
//小數部分
//rbin,cbin 和obin 爲三維座標的小數部分,即將中心點移動到正方體端點時C 點在正方體的座標
rbin -= r0;
cbin -= c0;
obin -= o0;
//如果角度o0 小於0 度或大於360 度,則根據圓周循環,把該角度調整到0~360度之間
if( o0 < 0 )
o0 += n;
if( o0 >= n )
o0 -= n;
// histogram update using tri-linear interpolation
//根據三線性插值法,計算該像素對正方體的8 個頂點的貢獻大小,即公式39 中得到的8 個立方體的體積,
//當然這裏還需要乘以高斯加權後的梯度值mag
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;
//得到該像素點在三維直方圖中的索引
int idx = ((r0+1)*(d+2) + c0+1)*(n+2) + o0;
//8 個頂點對應於座標平移前的8 個直方圖的正方體,對其進行累加求和
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
//由於圓周循環的特性,對計算以後幅角小於0 度或大於360 度的值重新進行調整,使其在0~360 度之間
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];
}
// 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;//特徵矢量的維數——128
for( k = 0; k < len; k++ )
nrm2 += dst[k]*dst[k];//平方和
//爲了避免計算中的累加誤差,對光照閾值進行反歸一化處理,即0.2 乘以公式31 中的分母部分,
//得到反歸一化閾值thr
float thr = std::sqrt(nrm2)*SIFT_DESCR_MAG_THR;
for( i = 0, nrm2 = 0; i < k; i++ )
{
//把特徵矢量中大於反歸一化閾值thr 的元素用thr 替代
float val = std::min(dst[i], thr);
dst[i] = val;
nrm2 += val*val;//平方和
}
//SIFT_INT_DESCR_FCTR = 512.f,浮點型轉換爲整型時所用到的係數
//歸一化處理,計算公式31 中的分母部分
nrm2 = SIFT_INT_DESCR_FCTR/std::max(std::sqrt(nrm2), FLT_EPSILON);
#if 1
for( k = 0; k < len; k++ )
{
//最終歸一化後的特徵矢量
dst[k] = saturate_cast<uchar>(dst[k]*nrm2);
}
#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
完整代碼:
#include "precomp.hpp"
#include <iostream>
#include <stdarg.h>
namespace cv
{
/*************** 定義會用到的參數和宏 ****************/
// 每一組中默認的採樣數,即小s的值。
static const int SIFT_INTVLS = 3;
//最初的高斯模糊的尺度默認值,即假設的第0組(某些地方叫做-1組)的尺度
static const float SIFT_SIGMA = 1.6f;
// 關鍵點對比 |D(x)|的默認值,公式14的閾值
static const float SIFT_CONTR_THR = 0.04f;
// 關鍵點的主曲率比的默認閾值
static const float SIFT_CURV_THR = 10.f;
//是否在構建高斯金字塔之前擴展圖像的寬和高位原來的兩倍(即是否建立-1組)
static const bool SIFT_IMG_DBL = true;
// 描述子直方圖數組的默認寬度,即描述子建立中的4*4的周邊區域
static const int SIFT_DESCR_WIDTH = 4;
// 每個描述子數組中的默認柱的個數( 4*4*8=128)
static const int SIFT_DESCR_HIST_BINS = 8;
// 假設輸入圖像的高斯模糊的尺度
static const float SIFT_INIT_SIGMA = 0.5f;
// width of border in which to ignore keypoints
static const int SIFT_IMG_BORDER = 5;
//公式12的爲了尋找關鍵點插值中心的最大迭代次數
static const int SIFT_MAX_INTERP_STEPS = 5;
// 方向梯度直方圖中的柱的個數
static const int SIFT_ORI_HIST_BINS = 36;
// determines gaussian sigma for orientation assignment
static const float SIFT_ORI_SIG_FCTR = 1.5f;
// determines the radius of the region used in orientation assignment
static const float SIFT_ORI_RADIUS = 3 * SIFT_ORI_SIG_FCTR;
// orientation magnitude relative to max that results in new feature
static const float SIFT_ORI_PEAK_RATIO = 0.8f;
// determines the size of a single descriptor orientation histogram
static const float SIFT_DESCR_SCL_FCTR = 3.f;
// threshold on magnitude of elements of descriptor vector
static const float SIFT_DESCR_MAG_THR = 0.2f;
// factor used to convert floating-point descriptor to unsigned char
static const float SIFT_INT_DESCR_FCTR = 512.f;
#if 0
// intermediate type used for DoG pyramids
typedef short sift_wt;
static const int SIFT_FIXPT_SCALE = 48;
#else
// intermediate type used for DoG pyramids
typedef float sift_wt;
static const int SIFT_FIXPT_SCALE = 1;
#endif
static inline void
unpackOctave(const KeyPoint& kpt, int& octave, int& layer, float& scale)
{
octave = kpt.octave & 255;
layer = (kpt.octave >> 8) & 255;
octave = octave < 128 ? octave : (-128 | octave);
scale = octave >= 0 ? 1.f/(1 << octave) : (float)(1 << -octave);
}
//1、創建基圖像矩陣createInitialImage 函數:
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);
//typedef float sift_wt;
//static const int SIFT_FIXPT_SCALE = 1
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;
}
}
2、建立高斯金字塔
void SIFT::buildGaussianPyramid( const Mat& base, vector<Mat>& pyr, int nOctaves ) const
{
vector<double> sig(nOctaveLayers + 3);
pyr.resize(nOctaves*(nOctaveLayers + 3));
// precompute Gaussian sigmas using the following formula:
// \sigma_{total}^2 = \sigma_{i}^2 + \sigma_{i-1}^2
sig[0] = sigma;
double k = pow( 2., 1. / nOctaveLayers );
for( int i = 1; i < nOctaveLayers + 3; i++ )
{
double sig_prev = pow(k, (double)(i-1))*sigma;
double sig_total = sig_prev*k;
sig[i] = std::sqrt(sig_total*sig_total - sig_prev*sig_prev);
}
for( int o = 0; o < nOctaves; o++ )
{
for( int i = 0; i < nOctaveLayers + 3; i++ )
{
Mat& dst = pyr[o*(nOctaveLayers + 3) + i];
if( o == 0 && i == 0 )
dst = base;
// base of new octave is halved image from end of previous octave
else if( i == 0 )
{
const Mat& src = pyr[(o-1)*(nOctaveLayers + 3) + nOctaveLayers];
resize(src, dst, Size(src.cols/2, src.rows/2),
0, 0, INTER_NEAREST);
}
else
{
const Mat& src = pyr[o*(nOctaveLayers + 3) + i-1];
GaussianBlur(src, dst, Size(), sig[i], sig[i]);
}
}
}
}
3、建立DoG金字塔
void SIFT::buildDoGPyramid( const vector<Mat>& gpyr, vector<Mat>& dogpyr ) const
{
int nOctaves = (int)gpyr.size()/(nOctaveLayers + 3);
dogpyr.resize( nOctaves*(nOctaveLayers + 2) );
for( int o = 0; o < nOctaves; o++ )
{
for( int i = 0; i < nOctaveLayers + 2; i++ )
{
const Mat& src1 = gpyr[o*(nOctaveLayers + 3) + i];
const Mat& src2 = gpyr[o*(nOctaveLayers + 3) + i + 1];
Mat& dst = dogpyr[o*(nOctaveLayers + 2) + i];
subtract(src2, src1, dst, noArray(), DataType<sift_wt>::type);
}
}
}
4、2 建立方向直方圖
// Computes a gradient orientation histogram at a specified pixel
static float calcOrientationHist( const Mat& img, Point pt, int radius,
float sigma, float* hist, int n )
{
int i, j, k, len = (radius*2+1)*(radius*2+1);
float expf_scale = -1.f/(2.f * sigma * sigma);
AutoBuffer<float> buf(len*4 + n+4);
float *X = buf, *Y = X + len, *Mag = X, *Ori = Y + len, *W = Ori + len;
float* temphist = W + len + 2;
for( i = 0; i < n; i++ )
temphist[i] = 0.f;
for( i = -radius, k = 0; i <= radius; i++ )
{
int y = pt.y + i;
if( y <= 0 || y >= img.rows - 1 )
continue;
for( j = -radius; j <= radius; j++ )
{
int x = pt.x + j;
if( x <= 0 || x >= img.cols - 1 )
continue;
float dx = (float)(img.at<sift_wt>(y, x+1) - img.at<sift_wt>(y, x-1));
float dy = (float)(img.at<sift_wt>(y-1, x) - img.at<sift_wt>(y+1, x));
X[k] = dx; Y[k] = dy; W[k] = (i*i + j*j)*expf_scale;
k++;
}
}
len = k;
// compute gradient values, orientations and the weights over the pixel neighborhood
exp(W, W, len);
fastAtan2(Y, X, Ori, len, true);
magnitude(X, Y, Mag, len);
for( k = 0; k < len; k++ )
{
int bin = cvRound((n/360.f)*Ori[k]);
if( bin >= n )
bin -= n;
if( bin < 0 )
bin += n;
temphist[bin] += W[k]*Mag[k];
}
// smooth the histogram
temphist[-1] = temphist[n-1];
temphist[-2] = temphist[n-2];
temphist[n] = temphist[0];
temphist[n+1] = temphist[1];
for( i = 0; i < n; i++ )
{
hist[i] = (temphist[i-2] + temphist[i+2])*(1.f/16.f) +
(temphist[i-1] + temphist[i+1])*(4.f/16.f) +
temphist[i]*(6.f/16.f);
}
float maxval = hist[0];
for( i = 1; i < n; i++ )
maxval = std::max(maxval, hist[i]);
return maxval;
}
4.1、尺度空間中關鍵點插值過濾
//
// Interpolates a scale-space extremum's location and scale to subpixel
// accuracy to form an image feature. Rejects features with low contrast.
// Based on Section 4 of Lowe's paper.
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);
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);
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;
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;
}
4、尋找尺度空間中的極值
//
// Detects features at extrema in DoG scale space. Bad features are discarded
// based on contrast and ratio of principal curvatures.
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])))
{
int r1 = r, c1 = c, layer = i;
if( !adjustLocalExtrema(dog_pyr, kpt, o, layer, r1, c1,
nOctaveLayers, (float)contrastThreshold,
(float)edgeThreshold, (float)sigma) )
continue;
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);
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]);
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);
}
}
}
}
}
}
}
5.1、計算sift的描述子
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);
float hist_width = SIFT_DESCR_SCL_FCTR * scl;
int radius = cvRound(hist_width * 1.4142135623730951f * (d + 1) * 0.5f);
cos_t /= hist_width;
sin_t /= hist_width;
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;
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 )
{
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);
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];
int r0 = cvFloor( rbin );
int c0 = cvFloor( cbin );
int o0 = cvFloor( obin );
rbin -= r0;
cbin -= c0;
obin -= o0;
if( o0 < 0 )
o0 += n;
if( o0 >= n )
o0 -= n;
// 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;
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];
}
// 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);
#if 1
for( k = 0; k < len; k++ )
{
dst[k] = saturate_cast<uchar>(dst[k]*nrm2);
}
#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
}
5、計算描述子
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;
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];
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));
}
}
//////////////////////////////////////////////////////////////////////////////////////////
//nfeatures 表示需要輸出的特徵點的數量,程序能夠通過排序輸出最好的前nfeatures 個特徵點,如果nfeatures=0,
//則表示輸出所有的特徵點;
//nOctaveLayers S=s+3中的參數小s;
//contrastThreshold 爲公式14 中的參數T;
//edgeThreshold 爲公式19 中的γ;
//sigma 表示基準層尺度σ0。
SIFT::SIFT( int _nfeatures, int _nOctaveLayers,
double _contrastThreshold, double _edgeThreshold, double _sigma )
: nfeatures(_nfeatures), nOctaveLayers(_nOctaveLayers),
contrastThreshold(_contrastThreshold), edgeThreshold(_edgeThreshold), sigma(_sigma)
{
}
int SIFT::descriptorSize() const
{
return SIFT_DESCR_WIDTH*SIFT_DESCR_WIDTH*SIFT_DESCR_HIST_BINS;
}
int SIFT::descriptorType() const
{
return CV_32F;
}
//SIFT 類中的重載運算符( ):
void SIFT::operator()(InputArray _image, InputArray _mask,
vector<KeyPoint>& keypoints) const
{
(*this)(_image, _mask, keypoints, noArray());
}
//_imgage 爲輸入的8 位灰度圖像;
//_mask 表示可選的輸入掩碼矩陣,用來標註需要檢測的特徵點的區域;
//keypoints 爲特徵點矢量;
//descriptors 爲輸出的特徵點描述符矩陣,如果不想得到該值,則需要賦予該值爲cv::noArray();
//useProvidedKeypoints 爲二值標識符,默認爲false 時,表示需要計算輸入圖像的特徵點;
//爲true 時,表示無需特徵點檢測,而是利用輸入的特徵點keypoints 計算它們的描述符。
void SIFT::operator()(InputArray _image, InputArray _mask,
vector<KeyPoint>& keypoints,
OutputArray _descriptors,
bool useProvidedKeypoints) const
{
//定義並初始化一些變量
//firstOctave 表示金字塔的組索引是從0 開始還是從‐1 開始,‐1 表示需要對輸入圖像的長寬擴大一倍,
//actualNOctaves 和actualNLayers 分別表示實際的金字塔的組數和層數
int firstOctave = -1, actualNOctaves = 0, actualNLayers = 0;
//得到輸入圖像和掩碼的矩陣
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 語句表示不需要計算圖像的特徵點,只需要根據輸入的特徵點keypoints 參數計算它們的描述符
if( useProvidedKeypoints )
{
//因爲不需要擴大輸入圖像的長寬,所以重新賦值firstOctave 爲0
firstOctave = 0;
//給maxOctave 賦予一個極小的值
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);
}
//確保最小組索引號不大於0
firstOctave = std::min(firstOctave, 0);
//確保最小組索引號大於等於‐1,實際層數小於等於輸入參數nOctaveLayers
CV_Assert( firstOctave >= -1 && actualNLayers <= nOctaveLayers );
//計算實際的組數
actualNOctaves = maxOctave - firstOctave + 1;
}
//創建基層圖像矩陣base.
//createInitialImage 函數的第二個參數表示是否進行擴大輸入圖像長寬尺寸操作,true
//表示進行該操作,第三個參數爲基準層尺度σ0
Mat base = createInitialImage(image, firstOctave < 0, (float)sigma);
//gpyr 爲高斯金字塔矩陣向量,dogpyr 爲DoG 金字塔矩陣向量
vector<Mat> gpyr, dogpyr;
// 計算金字塔的組的數量, 當actualNOctaves > 0 時, 表示進入了上面的if( useProvidedKeypoints )語句,
//所以組數直接等於if( useProvidedKeypoints )內計算得到的值
//如果actualNOctaves 不大於0,則計算組數
//這裏面還考慮了組的初始索引等於‐1 的情況,所以最後加上了 – firstOctave 這項
int nOctaves = actualNOctaves > 0 ? actualNOctaves :
cvRound(log( (double)std::min( base.cols, base.rows ) ) / log(2.) - 2) - firstOctave;
//double t, tf = getTickFrequency();
//t = (double)getTickCount();
//buildGaussianPyramid 和buildDoGPyramid 分別爲構建高斯金字塔和DoG 金字塔函數
buildGaussianPyramid(base, gpyr, nOctaves);
buildDoGPyramid(gpyr, dogpyr);
//t = (double)getTickCount() - t;
//printf("pyramid construction time: %g\n", t*1000./tf);
// useProvidedKeypoints 爲false,表示需要計算圖像的特徵點
if( !useProvidedKeypoints )
{
//t = (double)getTickCount();
//在尺度空間內找到極值點
findScaleSpaceExtrema(gpyr, dogpyr, keypoints);
//在特徵點檢測的過程中(尤其是在泰勒級數擬合的過程中)會出現特徵點被重複檢測到的現象,
//因此要剔除掉那些重複的特徵點
//KeyPointsFilter 類是在modules/features2d/src/keypoint.cpp 定義的
KeyPointsFilter::removeDuplicated( keypoints );
//根據掩碼矩陣,只保留掩碼矩陣所涵蓋的特徵點
if( !mask.empty() )
KeyPointsFilter::runByPixelsMask( keypoints, mask );//??
//保留那些最好的前nfeatures 個特徵點
if( nfeatures > 0 )
KeyPointsFilter::retainBest(keypoints, nfeatures);
//t = (double)getTickCount() - t;
//printf("keypoint detection time: %g\n", t*1000./tf);
//如果firstOctave < 0,則表示對輸入圖像進行了擴大處理,所以要對特徵點的一些變量進行適當調整。
//這是因爲firstOctave < 0,金字塔增加了一個第‐1 組,而在檢測特徵點的時候,所有變量都是基於
//這個第‐1 組的基準層尺度圖像的
if( firstOctave < 0 )
//遍歷所有特徵點
for( size_t i = 0; i < keypoints.size(); i++ )
{
KeyPoint& kpt = keypoints[i];//提取出特徵點
//其實這裏的firstOctave = ‐1,所以scale = 0.5
float scale = 1.f/(float)(1 << -firstOctave);
//重新調整特徵點所在的組
kpt.octave = (kpt.octave & ~255) | ((kpt.octave + firstOctave) & 255);
//特徵點的位置座標調整到真正的輸入圖像內,即得到的座標除以2
kpt.pt *= scale;
//特徵點的尺度調整到相對於輸入圖像的尺度,即得到的尺度除以2
kpt.size *= scale;
}
}
else
{
// filter keypoints by mask
//KeyPointsFilter::runByPixelsMask( keypoints, mask );
}
//如果需要得到特徵點描述符,則進入下面的if 內,生成特徵點描述符
if( _descriptors.needed() )
{
//t = (double)getTickCount();
//dsize 爲特徵點描述符的大小
//即SIFT_DESCR_WIDTH*SIFT_DESCR_WIDTH*SIFT_DESCR_HIST_BINS=4×4×8=128
int dsize = descriptorSize();
//創建特徵點描述符,爲其開闢一段內存空間
_descriptors.create((int)keypoints.size(), dsize, CV_32F);
//描述符的矩陣形式
Mat descriptors = _descriptors.getMat();
//計算特徵點描述符,calcDescriptors 函數在後面將給出詳細的分析
calcDescriptors(gpyr, keypoints, descriptors, nOctaveLayers, firstOctave);
//t = (double)getTickCount() - t;
//printf("descriptor extraction time: %g\n", t*1000./tf);
}
}
void SIFT::detectImpl( const Mat& image, vector<KeyPoint>& keypoints, const Mat& mask) const
{
(*this)(image, mask, keypoints, noArray());
}
void SIFT::computeImpl( const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors) const
{
(*this)(image, Mat(), keypoints, descriptors, true);
}
}//namespace cv;