一、概述
2004 年,不列颠哥伦比亚大学的D.Lowe在他的论文Distinctive Image Features from Scale-Invariant Keypoints 中提出了一种新算法 Scale Invariant Feature Transform (SIFT) ,该算法提取关键点并计算其描述符。
SIFT算法主要涉及四个步骤。
1、尺度空间极值检测
从上图可以看出,我们不能使用同一个窗口来检测不同尺度的关键点。小的角点也没有问题。但是要检测更大的角点,我们需要更大的窗口。为此,使用了尺度空间过滤。其中,高斯拉普拉斯算子对具有各种σ价值观。LoG 充当斑点检测器,可检测由于变化引起的各种大小的斑点σ. 简而言之,σ充当缩放参数。例如,在上图中,具有低的高斯核σ为小角提供高价值,而高斯核具有高σ非常适合较大的角落。所以,我们可以在尺度和空间上找到局部最大值,这给了我们一个列表( x , y, σ)值,这意味着在 (x,y) 处有一个潜在的关键点σ规模。
但是这个 LoG 有点昂贵,所以 SIFT 算法使用了高斯差分,它是 LoG 的近似值。高斯差值作为具有两个不同图像的图像的高斯模糊差值获得σ, 随它去σ和σ _. 这个过程是针对高斯金字塔中图像的不同倍频程完成的。如下图所示:
一旦找到这个DoG,就会在图像上搜索尺度和空间上的局部极值。例如,将图像中的一个像素与其 8 个相邻像素以及下一个比例的 9 个像素和前一个比例的 9 个像素进行比较。如果是局部极值,就是潜在的关键点。这基本上意味着关键点在该比例下得到最好的表示。如下图所示:
关于不同的参数,OpenCV官方给出了一些经验数据,主要为: octaves = 4, scale levels = 5, initial σ=1.6, .
2、关键点定位
一旦找到潜在的关键点位置,就必须对其进行细化以获得更准确的结果。他们使用尺度空间的泰勒级数扩展来获得更准确的极值位置,如果该极值处的强度小于阈值(根据论文为 0.03),则将其拒绝。这个阈值在 OpenCV中称为contrastThreshold
DoG 对边缘的响应更高,因此也需要去除边缘。为此,使用了类似于 Harris 角点检测器的概念。他们使用 2x2 Hessian 矩阵 (H) 来计算主曲率。我们从 Harris 角点检测器中得知,对于边缘,一个特征值大于另一个特征值。所以在这里他们使用了一个简单的函数,
如果该比率大于阈值(在 OpenCV 中称为edgeThreshold),则丢弃该关键点。它在纸上以 10 的形式给出。
所以它消除了任何低对比度的关键点和边缘关键点,剩下的是强烈的兴趣点。
3、方向分配
现在为每个关键点分配一个方向,以实现图像旋转的不变性。根据尺度在关键点位置周围取一个邻域,并在该区域计算梯度大小和方向。创建了一个具有 36 个 bin 覆盖 360 度的方向直方图(它由梯度幅度和高斯加权圆形窗口加权σ等于关键点比例的 1.5 倍)。取直方图中的最高峰,任何高于 80% 的峰也被认为是计算方向。它创建具有相同位置和比例但方向不同的关键点。它有助于匹配的稳定性。
4、关键点描述符
现在创建了关键点描述符。取关键点周围的 16x16 邻域。它分为 16 个 4x4 大小的子块。对于每个子块,创建 8 个 bin 方向直方图。因此共有 128 个 bin 值可用。它被表示为一个向量以形成关键点描述符。除此之外,还采取了一些措施来实现对光照变化、旋转等的鲁棒性。
5、关键点匹配
通过识别它们最近的邻居来匹配两个图像之间的关键点。但在某些情况下,第二个最接近的匹配可能非常接近第一个。这可能是由于噪音或其他一些原因而发生的。在这种情况下,采用最近距离与次近距离的比率。如果大于 0.8,则拒绝。根据论文,它消除了大约 90% 的错误匹配,而仅丢弃了 5% 的正确匹配。
二、类参考
1、函数原型
static Ptr<SIFT> cv::SIFT::create ( int nfeatures = 0,
int nOctaveLayers = 3,
double contrastThreshold = 0.04,
double edgeThreshold = 10,
double sigma = 1.6
)
2、参数详解
nfeatures | 要保留的最佳特征的数量。 特征按其分数排序(在 SIFT 算法中作为局部对比度测量) |
nOctaveLayers | 每个ctave中的层数。 3 是 D. Lowe 论文中使用的值。 ctave的数量是根据图像分辨率自动计算的。 |
contrastThreshold | 用于滤除半均匀(低对比度)区域中的弱特征的对比度阈值。 阈值越大,检测器产生的特征就越少。 |
三、OpenCV源码
1、源码路径
opencv\modules\features2d\src\sift.dispatch.cpp
2、源码代码
Ptr<SIFT> SIFT::create( int _nfeatures, int _nOctaveLayers,
double _contrastThreshold, double _edgeThreshold, double _sigma )
{
CV_TRACE_FUNCTION();
return makePtr<SIFT_Impl>(_nfeatures, _nOctaveLayers, _contrastThreshold, _edgeThreshold, _sigma, CV_32F);
}
Ptr<SIFT> SIFT::create( int _nfeatures, int _nOctaveLayers,
double _contrastThreshold, double _edgeThreshold, double _sigma, int _descriptorType )
{
CV_TRACE_FUNCTION();
// SIFT descriptor supports 32bit floating point and 8bit unsigned int.
CV_Assert(_descriptorType == CV_32F || _descriptorType == CV_8U);
return makePtr<SIFT_Impl>(_nfeatures, _nOctaveLayers, _contrastThreshold, _edgeThreshold, _sigma, _descriptorType);
}
void SIFT_Impl::detectAndCompute(InputArray _image, InputArray _mask,
std::vector<KeyPoint>& keypoints,
OutputArray _descriptors,
bool useProvidedKeypoints)
{
CV_TRACE_FUNCTION();
int firstOctave = -1, actualNOctaves = 0, actualNLayers = 0;
Mat image = _image.getMat(), mask = _mask.getMat();
if( image.empty() || image.depth() != CV_8U )
CV_Error( Error::StsBadArg, "image is empty or has incorrect depth (!=CV_8U)" );
if( !mask.empty() && mask.type() != CV_8UC1 )
CV_Error( Error::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;
}
Mat base = createInitialImage(image, firstOctave < 0, (float)sigma);
std::vector<Mat> gpyr;
int nOctaves = actualNOctaves > 0 ? actualNOctaves : cvRound(std::log( (double)std::min( base.cols, base.rows ) ) / std::log(2.) - 2) - firstOctave;
//double t, tf = getTickFrequency();
//t = (double)getTickCount();
buildGaussianPyramid(base, gpyr, nOctaves);
//t = (double)getTickCount() - t;
//printf("pyramid construction time: %g\n", t*1000./tf);
if( !useProvidedKeypoints )
{
std::vector<Mat> dogpyr;
buildDoGPyramid(gpyr, dogpyr);
//t = (double)getTickCount();
findScaleSpaceExtrema(gpyr, dogpyr, keypoints);
KeyPointsFilter::removeDuplicatedSorted( 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 );
}
if( _descriptors.needed() )
{
//t = (double)getTickCount();
int dsize = descriptorSize();
_descriptors.create((int)keypoints.size(), dsize, descriptor_type);
Mat descriptors = _descriptors.getMat();
calcDescriptors(gpyr, keypoints, descriptors, nOctaveLayers, firstOctave);
//t = (double)getTickCount() - t;
//printf("descriptor extraction time: %g\n", t*1000./tf);
}
}