原文出自:http://blog.csdn.net/xiaowei_cqu/article/details/8069548
SIFT简介
Scale Invariant Feature Transform,尺度不变特征变换匹配算法,是由David G. Lowe在1999年(《Object Recognition from Local Scale-Invariant Features》)提出的高效区域检测算法,在2004年(《Distinctive Image Features from Scale-Invariant Keypoints》)得以完善。
SIFT特征对旋转、尺度缩放、亮度变化等保持不变性,是非常稳定的局部特征,现在应用很广泛。而SIFT算法是将Blob检测,特征矢量生成,特征匹配搜索等步骤结合在一起优化。我会更新一系列文章,分析SIFT算法原理及OpenCV 2.4.2实现的SIFT源码:
- DoG尺度空间构造(Scale-space extrema detection)
- 关键点搜索与定位(Keypoint localization)
- 方向赋值(Orientation assignment)
- 关键点描述(Keypoint descriptor)
SIFT in OpenCV
构造函数:
重载操作符:
void SIFT::operator()(InputArray img, InputArray mask, vector<KeyPoint>& keypoints, OutputArray
descriptors, bool useProvidedKeypoints=false)
img:8bit灰度图像
mask:图像检测区域(可选)
keypoints:特征向量矩阵
descipotors:特征点描述的输出向量(如果不需要输出,需要传cv::noArray())。
useProvidedKeypoints:是否进行特征点检测。ture,则检测特征点;false,只计算图像特征描述。
函数源码
1.DoG尺度空间构造(Scale-space extrema detection)
尺度空间理论
尺度越大图像越模糊。
为什么要讨论尺度空间?
图像的尺度空间表达就是图像在所有尺度下的描述。
尺度空间表达与金字塔多分辨率表达
高斯模糊
高斯核是唯一可以产生多尺度空间的核(《Scale-space theory: A basic tool for analysing structures at different scales》)。一个图像的尺度空间L(x,y,σ) ,定义为原始图像I(x,y)与一个可变尺度的2维高斯函数G(x,y,σ)卷积运算。
二维空间高斯函数:
尺度空间:
尺度是自然客观存在的,不是主观创造的。高斯卷积只是表现尺度空间的一种形式。
二维空间高斯函数是等高线从中心成正太分布的同心圆:

分布不为零的点组成卷积阵与原始图像做变换,即每个像素值是周围相邻像素值的高斯平均。一个5*5的高斯模版如下所示:

高斯模版是圆对称的,且卷积的结果使原始像素值有最大的权重,距离中心越远的相邻像素值权重也越小。
在实际应用中,在计算高斯函数的离散近似时,在大概 3σ距离之外的像素都可以看作不起作用,这些像素的计算也就可以忽略。所以,通常程序只计算 (6σ+1)*(6σ+1)就可以保证相关像素影响。
高斯模糊另一个很厉害的性质就是线性可分:使用二维矩阵变换的高斯模糊可以通过在水平和竖直方向各进行一维高斯矩阵变换相加得到。
O(N^2*m*n)次乘法就缩减成了O(N*m*n)+O(N*m*n)次乘法。(N为高斯核大小,m,n为二维图像高和宽)
其实高斯这一部分只需要简单了解就可以了,在OpenCV也只需要一句代码:
GaussianBlur(dbl, dbl, Size(), sig_diff, sig_diff);
我这里详写了一下是因为这块儿对分析算法效率比较有用,而且高斯模糊的算法真的很漂亮~
金字塔多分辨率
金字塔是早期图像多尺度的表示形式。图像金字塔化一般包括两个步骤:使用低通滤波器平滑图像;对平滑图像进行降采样(通常是水平,竖直方向1/2),从而得到一系列尺寸缩小的图像。
上图中(a)是对原始信号进行低通滤波,(b)是降采样得到的信号。
而对于二维图像,一个传统的金字塔中,每一层图像由上一层分辨率的长、宽各一半,也就是四分之一的像素组成:
多尺度和多分辨率
尺度空间表达和金字塔多分辨率表达之间最大的不同是:
- 尺度空间表达是由不同高斯核平滑卷积得到,在所有尺度上有相同的分辨率;
- 而金字塔多分辨率表达每层分辨率减少固定比率。
DoG(Difference of Gaussian)
高斯拉普拉斯LoG金字塔

高斯差分DoG金字塔









// 构建nOctaves组(每组nOctaves+2层)高斯差分金字塔
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++ )
{
// 第o组第i副图像为高斯金字塔中第o组第i+1和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(), CV_16S);
}
}
}
这个比较简单,就是一个
subtract()函数。
至此,SIFT第一步就完成了。参见《SIFT原理与源码分析》
2.关键点搜索与定位(Keypoint localization)
由前一步《DoG尺度空间构造》,我们得到了DoG高斯差分金字塔:
如上图的金字塔,高斯尺度空间金字塔中每组有五层不同尺度图像,相邻两层相减得到四层DoG结果。关键点搜索就在这四层DoG图像上寻找局部极值点。
DoG局部极值点
寻找DoG极值点时,每一个像素点和它所有的相邻点比较,当其大于(或小于)它的图像域和尺度域的所有相邻点时,即为极值点。如下图所示,比较的范围是个3×3的立方体:中间的检测点和它同尺度的8个相邻点,以及和上下相邻尺度对应的9×2个点——共26个点比较,以确保在尺度空间和二维图像空间都检测到极值点。
在一组中,搜索从每组的第二层开始,以第二层为当前层,第一层和第三层分别作为立方体的的上下层;搜索完成后再以第三层为当前层做同样的搜索。所以每层的点搜索两次。通常我们将组Octaves索引以-1开始,则在比较时牺牲了-1组的第0层和第N组的最高层
高斯金字塔,DoG图像及极值计算的相互关系如上图所示。
关键点精确定位



删除边缘效应







至此,SIFT第二步就完成了。参见《SIFT原理与源码分析》
3.方向赋值(Orientation assignment)
梯度方向和幅值
在前文中,精确定位关键点后也找到改特征点的尺度值σ,根据这一尺度值,得到最接近这一尺度值的高斯图像:
使用有限差分,计算以关键点为中心,以3×1.5σ为半径的区域内图像梯度的幅角和幅值,公式如下:
梯度直方图
在完成关键点邻域内高斯图像梯度计算后,使用直方图统计邻域内像素对应的梯度方向和幅值。
有关直方图的基础知识可以参考《数字图像直方图》,可以看做是离散点的概率表示形式。此处方向直方图的核心是统计以关键点为原点,一定区域内的图像像素点对关键点方向生成所作的贡献。
梯度方向直方图的横轴是梯度方向角,纵轴是剃度方向角对应的梯度幅值累加值。梯度方向直方图将0°~360°的范围分为36个柱,每10°为一个柱。下图是从高斯图像上求取梯度,再由梯度得到梯度方向直方图的例图。
在计算直方图时,每个加入直方图的采样点都使用圆形高斯函数函数进行了加权处理,也就是进行高斯平滑。这主要是因为SIFT算法只考虑了尺度和旋转不变形,没有考虑仿射不变性。通过高斯平滑,可以使关键点附近的梯度幅值有较大权重,从而部分弥补没考虑仿射不变形产生的特征点不稳定。
通常离散的梯度直方图要进行插值拟合处理,以求取更精确的方向角度值。(这和《关键点搜索与定位》中插值的思路是一样的)。
关键点方向
直方图峰值代表该关键点处邻域内图像梯度的主方向,也就是该关键点的主方向。在梯度方向直方图中,当存在另一个相当于主峰值 80%能量的峰值时,则将这个方向认为是该关键点的辅方向。所以一个关键点可能检测得到多个方向,这可以增强匹配的鲁棒性。Lowe的论文指出大概有15%关键点具有多方向,但这些点对匹配的稳定性至为关键。
获得图像关键点主方向后,每个关键点有三个信息(x,y,σ,θ):位置、尺度、方向。由此我们可以确定一个SIFT特征区域。通常使用一个带箭头的圆或直接使用箭头表示SIFT区域的三个值:中心表示特征点位置,半径表示关键点尺度(r=2.5σ),箭头表示主方向。具有多个方向的关键点可以复制成多份,然后将方向值分别赋给复制后的关键点。如下图:
源码
// 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 )
{
//len:2r+1也就是以r为半径的圆(正方形)像素个数
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<short>(y, x+1) - img.at<short>(y, x-1));
float dy = (float)(img.at<short>(y-1, x) - img.at<short>(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); //幅角
// 计算直方图的每个bin
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;
}
这一步比较简单~参见《 SIFT原理与源码分析》。
SIFT描述子h(x,y,θ)是对关键点附近邻域内高斯图像梯度统计的结果,是一个三维矩阵,但通常用一个矢量来表示。矢量通过对三维矩阵按一定规律排列得到。
描述子采样区域


源码
- Point pt(cvRound(ptf.x), cvRound(ptf.y));
- //计算余弦,正弦,CV_PI/180:将角度值转化为幅度值
- 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); //d:SIFT_DESCR_WIDTH 4
- float hist_width = SIFT_DESCR_SCL_FCTR * scl; // SIFT_DESCR_SCL_FCTR: 3
- // scl: size*0.5f
- // 计算图像区域半径mσ(d+1)/2*sqrt(2)
- // 1.4142135623730951f 为根号2
- int radius = cvRound(hist_width * 1.4142135623730951f * (d + 1) * 0.5f);
- cos_t /= hist_width;
- sin_t /= hist_width;
区域坐标轴旋转


源码
- //计算采样区域点坐标旋转
- 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<short>(r, c+1) - img.at<short>(r, c-1));
- float dy = (float)(img.at<short>(r-1, c) - img.at<short>(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++;
- }
- }
计算采样区域梯度直方图

源码
- //计算梯度直方图
- 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;
- //n为SIFT_DESCR_HIST_BINS:8,即将360°分为8个区间
- 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;
- }
关键点描述源码
- // SIFT关键点特征描述
- // 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));
- //计算余弦,正弦,CV_PI/180:将角度值转化为幅度值
- 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); //d:SIFT_DESCR_WIDTH 4
- float hist_width = SIFT_DESCR_SCL_FCTR * scl; // SIFT_DESCR_SCL_FCTR: 3
- // scl: size*0.5f
- // 计算图像区域半径mσ(d+1)/2*sqrt(2)
- // 1.4142135623730951f 为根号2
- 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<short>(r, c+1) - img.at<short>(r, c-1));
- float dy = (float)(img.at<short>(r-1, c) - img.at<short>(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;
- //n为SIFT_DESCR_HIST_BINS:8,即将360°分为8个区间
- 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);
- for( k = 0; k < len; k++ )
- {
- dst[k] = saturate_cast<uchar>(dst[k]*nrm2);
- }
- }
至此SIFT描述子生成,SIFT算法也基本完成了~参见《 SIFT原理与源码分析》