这篇文章是我从下面的三位大佬中对比来看,相互理解来学习,著作权归他们所有,至于我只是想归纳学习,仅此而已。
SIFT特征详解 - Brook_icv - 博客园
SIFT算法原理详解 - Alliswell_WP - 博客园
SIFT算法详解_zddhub的专栏-CSDN博客_sift
SIFT的全称是Scale Invariant Feature Transform,尺度不变特征变换,对旋转、尺度缩放、亮度等保持不变性,是一个非常稳定的局部特征
有4个主要步骤
2、尺度空间的极值检测 搜索所有尺度空间上的图像,通过高斯微分函数来识别潜在的对尺度和选择不变的兴趣点。
3、特征点定位 在每个候选的位置上,通过一个拟合精细模型来确定位置尺度,关键点的选取依据他们的稳定程度。
4、特征方向分配 基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向,后续的所有操作都是对于关键点的方向、尺度和位置进行变换,从而提供这些特征的不变性。
5、特征点描述 在每个特征点周围的邻域内,在选定的尺度上测量图像的局部梯度,这些梯度被变换成一种表示,这种表示允许比较大的局部形状的变形和光照变换。
构建尺度空间的目的;是为了检测出在不同的尺度下都存在的特征点
基本思想是:在图像信息处理模型中引入一个被视为尺度的参数,通过连续变化尺度参数获得多尺度下的尺度空间表示序列,对这些序列进行尺度空间主轮廓的提取,并以该主轮廓作为一种特征向量,实现边缘、角点检测和不同分辨率上的特征提取等
个图像的尺度空间,
定义为一个变化尺度的高斯函数与原图像的卷积。
σ称为尺度空间因子,它是高斯正态分布的标准差,反映了图像被模糊的程度,其值越大图像越模糊,对应的尺度也就越大,值越小表示图像被平滑的越少,相应的尺度也就越小。大尺度对应于图像的概貌特征,小尺度对应于图像的细节特征。
L(x,y,σ)L(x,y,σ)代表着图像的高斯尺度空间。
高斯金字塔的构建
2. 对图像做降采样(隔点采样)。
下面我们要在不用的空间尺度来检测特征点,而检测特征点的比较好的算子是
Δ2G(高斯拉普拉斯,LoG)的极大值和极小值同其它的特征提取函数,例如:梯度,Hessian或Harris角特征比较,能够产生最稳定的图像特征。
然而在实际的运算当中LoG的计算量过于大,所以提出了高斯差分(DoG)来近似计算。
σ—尺度空间坐标
O—组(octave)数
S— 组内层数
其中
σ0是基准层尺度,o为组octave的索引,s为组内层的索引。关键点的尺度坐标就是按关键点所在的组和组内的层。
下面的代码是从SIFT算法原理详解 - Alliswell_WP - 博客园 博客中拿过来的,主要是看原理实践。
/*
通过减去相邻来建立高斯尺度空间金字塔的差异
高斯金字塔的间隔
@param gauss_pyr高斯尺度空间金字塔
@param octvs比例空间的八度音阶数
@param intvls每个八度音程的间隔数
@return返回高斯尺度空间金字塔的差异
octvs x(intvls + 2)数组
*/
static IplImage*** build_dog_pyr(IplImage*** gauss_pyr, int octvs, int intvls)
{
IplImage*** dog_pyr;
int i, o;
dog_pyr = calloc(octvs, sizeof(IplImage**));
for(i = 0; i < octvs; i++)
dog_pyr[i] = calloc(intvls + 2, sizeof(IplImage*)); //高斯差分金字塔每组只需s+2幅图像,默认为5张
for(o = 0; o < octvs; o++)
for(i = 0; i < intvls + 2; i++)
{
dog_pyr[o][i] = cvCreateImage(cvGetSize(gauss_pyr[o][i]),
IPL_DEPTH_32F, 1);
cvSub(gauss_pyr[o][i+1], gauss_pyr[o][i], dog_pyr[o][i], NULL); //为高斯金字塔两层相减获得
}
return dog_pyr;
}
关键点是由DOG空间的局部极值点组成的,关键点的初步探查是通过同一组内各DoG相邻两层图像之间比较完成的。每个像素点要和其图像域(同一尺度空间)和尺度域(相邻的尺度空间)的所有相邻点进行比较,当其大于(或者小于)所有相邻点时,改点就是极值点。如图所示,中间的检测点要和其所在图像的3×3邻域8个像素点,以及其相邻的上下两层的3×3领域18个像素点,共(8+(2*3*3))26个像素点进行比较
/*
在DoG比例空间中检测极值处的特征。 坏功能被丢弃
基于对比度和主曲率的比率。
@param dog_pyr DoG规模空间金字塔
@param octvs由dog_pyr表示的缩放空间的八度音程
@param以每个八度音程为间隔
@param contr_thr特征对比度的低阈值
@param curv_thr主曲率特征比的高阈值
@param存储器存储器,用于存储检测到的功能
@return返回一系列检测到的特征,其尺度,方向,
和描述符尚未确定。
*/
static CvSeq* scale_space_extrema(IpImage*** dog_pyr, int octvs, int intvls,
double contr_thr, int curv_thr,
CvMemStorage* storage)
{
CvSeq* features;
double prelim_contr_thr = 0.5 * contr_thr / intvls; //分层多会导致直接差值得到的值变小,因此这里对阈值进行调整
struct feature* feat;
struct detection_data* ddata;
int o, i, r, c;
features = cvCreateSeq(0, sizeof(CvSeq), sizeof(struct feature), storage);
for(o = 0; o < octvs; o++)
for(i = 1; i <= intvls; i++)
for(r = SIFT_IMG_BORDER; r < dog_pyr[o][0]->height- SIFT_IMG_BORDER; r++) //忽略边框附近像素
for(c = SIFT_IMG_BORDER; c < dog_pyr[o][0]->width-SIFT_IMG_BORDER; c++)
/* perform preliminary check on contrast */
if(ABS(pixval32f(dog_pyr[o][i], r, c)) > prelim_contr_thr) //要求高对比度
if(is_extremum(dog_pyr, o, i, r, c))
{
feat = interp_extremum(dog_pyr, o, i, r, c, intvls, contr_thr);//亚像素精度,并排除低对比度点
if(feat)
{
ddata = feat_detection_data(feat);
if(!is_too_edge_like(dog_pyr[ddata->octv][ddata->intvl],
ddata->r, ddata->c, curv_thr)) //去除r太大的点
{
cvSeqPush(features, feat);
}
else
free(ddata);
free(feat);
}
}
return features;
}
以上方法检测到的极值点是离散空间的极值点,以下通过拟合三维二次函数来精确确定关键点的位置和尺度,同时去除低对比度的关键点和不稳定的边缘响应点(因为DoG算子会产生较强的边缘响应),以增强匹配稳定性、提高抗噪声能力。
离散空间的极值点并不是真正的极值点,二维函数离散空间得到的极值点与连续空间极值点的差别。利用已知的离散空间点插值得到的连续空间极值点的方法叫做子像素插值(Sub-pixel Interpolation)。
为了提高关键点的稳定性,需要对尺度空间DoG函数进行曲线拟合。利用DoG函数在尺度空间的Taylor展开式(拟合函数)为
注意:
关于向量求导,要看分母布局还是分子布局,第一项此处是分母布局(分子为行向量或者分母为列向量),所以求导后为(n*1)维的,此处应该没有转置符号
精确定位中的泰勒插值源码分析
/*
将尺度空间极值的位置和比例插值为子像素
精确度形成图像功能。 拒绝低对比度的功能。
基于Lowe论文的第4部分。
@param dog_pyr DoG规模空间金字塔
@param octv feature的缩放空间八度
@param intvl feature的八度音程间隔
@param r feature的图像行
@param c feature的图像列
@param intvls每个八度音程的总间隔
@param contr_thr特征对比阈值
@return返回由给定插值产生的特征
如果无法插入给定位置,则参数或NULL
如果插值的对比度太低,那么。 如果是一个功能
返回,其规模,方向和描述符尚未确定。
*/
static struct feature* interp_extremum( IplImage*** dog_pyr, int octv,
int intvl, int r, int c, int intvls,
double contr_thr )
{
struct feature* feat;
struct detection_data* ddata;
double xi, xr, xc, contr;
int i = 0;
while( i < SIFT_MAX_INTERP_STEPS )
{
interp_step( dog_pyr, octv, intvl, r, c, &xi, &xr, &xc ); //计算出off_X(x,y,sigma)
if( ABS( xi ) < 0.5 && ABS( xr ) < 0.5 && ABS( xc ) < 0.5 )
break; //任一维都小于0.5才能结束,否则需换点重算,重算次数有限制
c += cvRound( xc );
r += cvRound( xr );
intvl += cvRound( xi );
if( intvl < 1 ||
intvl > intvls ||
c < SIFT_IMG_BORDER ||
r < SIFT_IMG_BORDER ||
c >= dog_pyr[octv][0]->width - SIFT_IMG_BORDER ||
r >= dog_pyr[octv][0]->height - SIFT_IMG_BORDER )
{
return NULL;
}
i++;
}
/* ensure convergence of interpolation */
if( i >= SIFT_MAX_INTERP_STEPS )
return NULL;
contr = interp_contr( dog_pyr, octv, intvl, r, c, xi, xr, xc ); //计算新极值点的D(x)
if( ABS( contr ) < contr_thr / intvls )
return NULL;
//满足条件,存储这个特征点
feat = new_feature();
ddata = feat_detection_data( feat );
feat->img_pt.x = feat->x = ( c + xc ) * pow( 2.0, octv ); //乘以2^octv以得到对应于底层图像的位置
feat->img_pt.y = feat->y = ( r + xr ) * pow( 2.0, octv );
ddata->r = r; //行
ddata->c = c; //列
ddata->octv = octv;
ddata->intvl = intvl; //层
ddata->subintvl = xi; //off_sigma
return feat;
}
/*
执行极值插值的一步。 基于Eqn。 (3)在Lowe's论文中。
@param dog_pyr高斯尺度空间金字塔的差异
@param octv八度音阶空间
@param intvl间隔被插值
@param r行被插值
@param c列被插值
@param xi输出为插值子像素增量到间隔
@param xr输出为插值子像素增量到行
@param xc输出为插值子像素增量到col
*/
static void interp_step( IplImage*** dog_pyr, int octv, int intvl, int r, int c,
double* xi, double* xr, double* xc )
{
CvMat* dD, * H, * H_inv, X;
double x[3] = { 0 };
dD = deriv_3D( dog_pyr, octv, intvl, r, c ); //计算了dD/dx,dD/dy,dD/d(sigma),返回为列向量dD/dX
H = hessian_3D( dog_pyr, octv, intvl, r, c ); //用离散值近似计算出三维hessian矩阵,即公式中d2D/dX2
H_inv = cvCreateMat( 3, 3, CV_64FC1 );
cvInvert( H, H_inv, CV_SVD ); //矩阵求逆
cvInitMatHeader( &X, 3, 1, CV_64FC1, x, CV_AUTOSTEP );
cvGEMM( H_inv, dD, -1, NULL, 0, &X, 0 ); //计算结果为-(d2D/dX2)^(-1)*(dD/dX),即off_X
cvReleaseMat( &dD );
cvReleaseMat( &H );
cvReleaseMat( &H_inv );
*xi = x[2];
*xr = x[1];
*xc = x[0];
}
一个定义不好的高斯差分算子的极值在横跨边缘的地方有较大的主曲率,而在垂直边缘的方向有较小的主曲率。
DOG算子会产生较强的边缘响应,需要剔除不稳定的边缘响应点。获取特征点处的Hessian矩阵,主曲率通过一个2x2 的Hessian矩阵H求出:
其中,Dxx,Dxy,Dyy是候选点邻域对应位置的差分求得的。
H的特征值α和β代表x和y方向的梯度。
D的主曲率和H的特征值成正比,令为α最大特征值,β为最小的特征值,则公式的值在两个特征值相等时最小,随着的增大而增大。值越大,说明两个特征值的比值越大,即在某一个方向的梯度值越大,而在另一个方向的梯度值越小,而边缘恰恰就是这种情况。所以为了剔除边缘响应点,需要让该比值小于一定的阈值,因此,为了检测主曲率是否在某域值r下,只需检测
成立时将关键点保留,反之剔除。
高阶矩阵的求逆算法主要有归一法和消元法两种,现将三阶矩阵求逆公式总结如下:
描述子应具有较高的独立性,以保证匹配率。
特征描述符的生成大致有三个步骤:
为了保证特征矢量的旋转不变性,要以特征点为中心,在附近邻域内将坐标轴旋转θθ(特征点的主方向)角度,即将坐标轴旋转为特征点的主方向。旋转后邻域内像素的新坐标为:
为了使描述符具有旋转不变性,需要利用图像的局部特征为给每一个关键点分配一个基准方向。使用图像梯度的方法求取局部结构的稳定方向。对于在DOG金字塔中检测出的关键点点,采集其所在高斯金字塔图像3σ邻域窗口内像素的梯度和方向分布特征。梯度的模值和方向如下:
L为关键点所在的尺度空间值,按Lowe的建议,梯度的模值m(x,y)按σ=1.5σ_oct 的高斯分布加成,按尺度采样的3σ原则,邻域窗口半径为σ=3*1.5σ_oct 。
在完成关键点的梯度计算后,使用直方图统计邻域内像素的梯度和方向。梯度直方图将0~360度的方向范围分为36个柱(bins),其中每柱10度。如图5.1所示,直方图的峰值方向代表了关键点的主方向,(为简化,图中只画了八个方向的直方图)。
方向直方图的峰值则代表了该特征点处邻域梯度的方向,以直方图中最大值作为该关键点的主方向。为了增强匹配的鲁棒性,只保留峰值大于主方向峰值80%的方向作为该关键点的辅方向。因此,对于同一梯度值的多个峰值的关键点位置,在相同位置和尺度将会有多个关键点被创建但方向不同。仅有15%的关键点被赋予多个方向,但可以明显的提高关键点匹配的稳定性。实际编程实现中,就是把该关键点复制成多份关键点,并将方向值分别赋给这些复制后的关键点,并且,离散的梯度方向直方图要进行插值拟合处理,来求得更精确的方向角度值。
梯度直方图的抛物线插值
其中i∈[0,35],h 和H 分别表示平滑前和平滑后的直方图。由于角度是循环的,即00=3600,如果出现h(j),j超出了(0,…,35)的范围,那么可以通过圆周循环的方法找到它所对应的、在00=3600之间的值,如h(-1) = h(35)。
梯度直方图的抛物线插值
假设我们在第i个小柱子要找一个精确的方向,那么由上面分析知道:
设插值抛物线方程为h(t)=at2+bt+c,其中a、b、c为抛物线的系数,t为自变量,t∈[-1,1],此抛物线求导并令它等于0。
即h(t)´=0 得tmax=-b/(2a)
现在把这三个插值点带入方程可得:
/*
为直方图中的每个方向添加大于的数组指定的阈值。
@param功能将新功能添加到此数组的末尾
@param hist orientation histogram
@param n hist中的bin数
@param mag_thr为hist中大于此值的条目添加了新功能
@param feat新功能是具有不同方向的克隆
*/
static void add_good_ori_features( CvSeq* features, double* hist, int n,
double mag_thr, struct feature* feat )
{
struct feature* new_feat;
double bin, PI2 = CV_PI * 2.0;
int l, r, i;
for( i = 0; i < n; i++ )
{
l = ( i == 0 )? n - 1 : i-1;
r = ( i + 1 ) % n;
if( hist[i] > hist[l] && hist[i] > hist[r] && hist[i] >= mag_thr )
{
bin = i + interp_hist_peak( hist[l], hist[i], hist[r] ); //二次插值
bin = ( bin < 0 )? n + bin : ( bin >= n )? bin - n : bin;
new_feat = clone_feature( feat );
new_feat->ori = ( ( PI2 * bin ) / n ) - CV_PI; //恢复成对应弧度制角度
cvSeqPush( features, new_feat );
free( new_feat );
}
}
}
至此,将检测出的含有位置、尺度和方向的关键点即是该图像的SIFT特征点。
通过以上步骤,对于每一个关键点,拥有三个信息:位置、尺度以及方向。接下来就是为每个关键点建立一个描述符,用一组向量将这个关键点描述出来,使其不随各种变化而改变,比如光照变化、视角变化等等。这个描述子不但包括关键点,也包含关键点周围对其有贡献的像素点,并且描述符应该有较高的独特性,以便于提高特征点正确匹配的概率。
SIFT描述子是关键点邻域高斯图像梯度统计结果的一种表示。通过对关键点周围图像区域分块,计算块内梯度直方图,生成具有独特性的向量,这个向量是该区域图像信息的一种抽象,具有唯一性。
Lowe建议描述子使用在关键点尺度空间内4*4的窗口中计算的8个方向的梯度信息,共4*4*8=128维向量表征。表示步骤如下:
1. 确定计算描述子所需的图像区域
2. 将坐标轴旋转为关键点的方向,以确保旋转不变性,如6.2所示。
备注:关于Laplace拉普拉斯算子具有旋转不变性的数学公式证明和图像证明见我另一篇博客:为什么拉普拉斯算子具有旋转不变性——为什么拉普拉斯算子具有旋转不变性 - Alliswell_WP - 博客园
3. 将邻域内的采样点分配到对应的子区域内,将子区域内的梯度值分配到8个方向上,计算其权值。
旋转后的采样点坐标在半径为radius的圆内被分配到dXd的子区域,计算影响子区域的采样点的梯度和方向,分配到8个方向上。
4. 插值计算每个种子点八个方向的梯度。
梯度直方图的生成
将邻域内的采样点分配到对应的子区域内,将子区域内的梯度值分配到8个方向上,计算其权值。
旋转后的采样点 落在子区域的下标为
三线性插值
/*
将一个条目插入到形成的方向直方图数组中特征描述符。
@param hist方向直方图的二维数组
@param rbin子行的入口坐标
@param cbin子bin列的条目坐标
@param obin子入口的方向坐标
@param mag大小的条目
@param d方向直方图的2D数组的宽度
@param n每个方向直方图的数量
*/
static void interp_hist_entry( double*** hist, double rbin, double cbin,
double obin, double mag, int d, int n )
{
double d_r, d_c, d_o, v_r, v_c, v_o;
double** row, * h;
int r0, c0, o0, rb, cb, ob, r, c, o;
r0 = cvFloor( rbin ); //返回不大于参数的最大整数值
c0 = cvFloor( cbin );
o0 = cvFloor( obin );
d_r = rbin - r0; //得到小数部分
d_c = cbin - c0;
d_o = obin - o0;
/*
The entry is distributed into up to 8 bins. Each entry into a bin
is multiplied by a weight of 1 - d for each dimension, where d is the
distance from the center value of the bin measured in bin units.
*/
/*
该条目分配到最多8个箱。 每个进入垃圾箱
乘以每个维度的1 - d的权重,其中d是
以箱单位测量的箱子中心值的距离。
*/
//这里也可以看出前面rbin、cbin为何减0.5。这样原点上这点的d_r、d_c均为0.5,即原点上这点的方向将被平均分配叠加在它
//周围4个直方图上面
for( r = 0; r <= 1; r++ )
{
rb = r0 + r;
if( rb >= 0 && rb < d )
{
v_r = mag * ( ( r == 0 )? 1.0 - d_r : d_r ); //公式中的1-d_r
row = hist[rb]; //第rb行上的hist
for( c = 0; c <= 1; c++ )
{
cb = c0 + c;
if( cb >= 0 && cb < d )
{
v_c = v_r * ( ( c == 0 )? 1.0 - d_c : d_c );
h = row[cb]; //h表示第rb行cb列上的hist
for( o = 0; o <= 1; o++ )
{
ob = ( o0 + o ) % n;
v_o = v_c * ( ( o == 0 )? 1.0 - d_o : d_o ); //角度不是像ori_hist中那样直接四舍五入,而是分配到距它最近的两个方向上
h[ob] += v_o;
}
}
}
}
}
}
如图6.3所示,将由式(6-3)所得采样点在子区域中的下标(图中蓝色窗口内红色点)线性插值,计算其对每个种子点的贡献。如图中的红色点,落在第0行和第1行之间,对这两行都有贡献。对第0行第3列种子点的贡献因子为dr,对第1行第3列的贡献因子为1-dr,同理,对邻近两列的贡献因子为dc和1-dc,对邻近两个方向的贡献因子为do和1-do。则最终累加在每个方向上的梯度大小为:
5. 如上统计的4*4*8=128个梯度信息即为该关键点的特征向量。特征向量形成后,为了去除光照变化的影响,需要对它们进行归一化处理,对于图像灰度值整体漂移,图像各点的梯度是邻域像素相减得到,所以也能去除。得到的描述子向量为
,归一化后的特征向量为则
6. 描述子向量门限。非线性光照,相机饱和度变化对造成某些方向的梯度值过大,而对方向的影响微弱。因此设置门限值(向量归一化后,一般取0.2)截断较大的梯度值。然后,再进行一次归一化处理,提高特征的鉴别性。
7. 按特征点的尺度对特征描述向量进行排序。
至此,SIFT特征描述向量生成。
描述向量这块不好理解
drawKeypoints(
image,//原始图像,可以使三通道或单通道图像;
keypoints,//特征点集合list,向量内每一个元素是一个KeyPoint对象,包含了特征点的各种属性信息
outImage,//特征点绘制的画布图像,可以是原图像;
color,//颜色设置,绘制的特征点的颜色信息,默认绘制的是随机彩色;
flags//特征点的绘制模式,其实就是设置特征点的哪些信息需要绘制,哪些不需要绘制,有以下几种模式可选
cv2.DRAW_MATCHES_FLAGS_DEFAULT:创建输出图像矩阵,使用现存的输出图像绘制匹配对和特征点,对每一个关键点只绘制中间点;
cv2.DRAW_MATCHES_FLAGS_DRAW_OVER_OUTIMG:不创建输出图像矩阵,而是在输出图像上绘制匹配对;
cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS:对每一个特征点绘制带大小和方向的关键点图形;
cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS:单点的特征点不被绘制;
我们传入了DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS标志位,表明对图像每个关键点都绘制圆圈(大小)和方向。
)
#if 1 // sift 案例流程 int main(int argc, char** argv) { Mat src1, src2; src1 = imread("C:\\Users\\19473\\Desktop\\opencv_images\\616.png"); src2 = imread("C:\\Users\\19473\\Desktop\\opencv_images\\617.png");; namedWindow("src1", 0); namedWindow("src2", 0); imshow("src1", src1); imshow("src2", src2); vector
keyPoint1, keyPoint2; //创建SIFT变量,可以直接指定一些参数,也可以不指定,采用默认的参数 //Ptr sift = xfeatures2d::SIFT::create(); Ptr sift = SIFT::create(500, 3); //进行特征点检测 sift->detect(src1, keyPoint1); sift->detect(src2, keyPoint2); Mat result1, result2; //将检测侧的特征点绘制在图像上 drawKeypoints(src1, keyPoint1, result1, Scalar(0, 255, 0), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);//画出特征点 namedWindow("src1特征点", 0); imshow("src1特征点", result1); //将检测侧的特征点绘制在图像上 drawKeypoints(src2, keyPoint2, result2, Scalar(0, 255, 0), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);//画出特征点 namedWindow("src2特征点", 0); imshow("src2特征点", result2); Mat descriptors_1, descriptors_2; //计算图像上每个特征点的128维描述算子 sift->compute(src1, keyPoint1, descriptors_1); sift->compute(src2, keyPoint2, descriptors_2); BFMatcher matcher; vector matches; //两幅图像特征点匹配 matcher.match(descriptors_1, descriptors_2, matches); Mat img_matches; //绘制匹配结果 drawMatches(src1, keyPoint1, src2, keyPoint2, matches, img_matches, Scalar(0, 0, 255), Scalar(0, 0, 255)); namedWindow("match1", 0); imshow("match1", img_matches); //提取出最佳匹配结果 nth_element(matches.begin(), matches.begin() + 50, matches.end()); //剔除掉其余的匹配结果 matches.erase(matches.begin() + 50, matches.end()); Mat img_matches2; //绘制匹配结果 drawMatches(src1, keyPoint1, src2, keyPoint2, matches, img_matches2, Scalar(0, 0, 255), Scalar(0, 0, 255)); namedWindow("match2", 0); imshow("match2", img_matches2); waitKey(0); return 0; } #endif
SIFT特征以其对旋转、尺度缩放、亮度等保持不变性,是一种非常稳定的局部特征,在图像处理和计算机视觉领域有着很重要的作用,其本身也是非常复杂的,下面对其计算过程做一个粗略总结。