SIFT源码解析(openMVG-2.1)
本文对openMVG 2.1中的sift相关源码进行了解析,算法原理部分请见上一篇文章万字长文详解SIFT特征提取,本文代码中涉及的一些细节问题可以当作上一篇文章的补充。调用例程
openMVG的项目源代码中有sift的调用例程,见:
openMVG-2.1\src\openMVG_Samples\features_siftPutativeMatches\siftmatch.cpp
其中核心调用sift的代码:
using namespace openMVG::features;
std::unique_ptr<Image_describer> image_describer(new SIFT_Anatomy_Image_describer);//核心类初始化,具体分析见下文
std::map<IndexT, std::unique_ptr<features::Regions>> regions_perImage; //用于储存特征
image_describer->Describe(imageL, regions_perImage); //主要执行函数
image_describer->Describe(imageR, regions_perImage);SIFT_Anatomy_Image_describer类
算法的核心类为 SIFT_Anatomy_Image_describer,位于SIFT_Anatomy_Image_Describer.hpp中
核心参数设置
其参数有一个struct传入,在初始化时会有一个默认的参数值初始化,如下:
Params(
int first_octave = 0, //如果设置为-1,则第一个octave中使用的是扩大一倍的图像,也就是upscale一次
int num_octaves = 6, //最大的octave数量
int num_scales = 3, // 每层octave输出的尺度数量
float edge_threshold = 10.0f, // 去除边缘时用到的阈值
float peak_threshold = 0.04f,// 去除对比度小的点时用到的阈值
bool root_sift = true// 是否使用改进的sift描述符first_octave,在openMVG中,默认的首层octave图像是源图像大小,可以通过这个参数设置为扩大一倍的大小。这里upscale主要是为了能够检测到更加细节的特征,此外,如果原图的分辨率本身较低,upscale一下也有助于检测到小尺度特征。
另,在opencv中,是直接默认首层upscale的。
num_octaves,设置的最大octave数量,此外,在后面的代码中还会根据图片尺寸算一个octave数量,来保证最后一层的分辨率不要太小(不小于32X32),在HierarchicalGaussianScaleSpace类中我们详细介绍。
num_scales每个octave的输出scale数量,这个数量加3,就是每个octave所需要的高斯层数量。
edge_threshold这里判定是否为边缘点的方法和之前原理篇讲的略有不同,但原理基本一致,都是得到局部Hessian矩阵后,判断特征值的大小情况。
在之前的判定中,我们计算出Hessian矩阵M后,采用这个公式计算其R响应:
\
其中α是一个经验值 0.04 到 0.06。
但在openMVG中,我们假设 $$λ_1 = γλ_2$$
其中γ > 1,该值越大,表示两者之间的比值越大,越有可能是边缘。
我们采用trace平方和determinant的比值来判定:
\[\frac{\mathrm{Tr}(\boldsymbol{M})^2}{\mathrm{Det}(\boldsymbol{M})}=\frac{(\lambda_1 + \lambda_2)^2}{\lambda_1\lambda_2}=\frac{(\gamma\lambda_2+\lambda_2)^2}{\gamma\lambda_2^2}=\frac{(\gamma + 1)^2}{\gamma}\]
这里如果得到的值越大,表示γ的值越大,也就表示越有可能是边缘点。edge_threshold就是这里设置的γ,SIFT原作者给出一个经验值10,在openMVG中也是用的这个经验值。
peak_threshold这个用来去掉对比度小的点,计算方法我们在SIFT_KeypointExtractor类中详述。
root_sift改进的sift描述符,详见Sift_DescriptorExtractor类。
算法流程核心 Describe_SIFT_Anatomy
包含整体的特征提取算法流程。核心流程函数!
std::unique_ptr<Regions_type> Describe_SIFT_Anatomy(
const image::Image<unsigned char>& image,
const image::Image<unsigned char>* mask = nullptr
)
//image:传入图像
//mask:掩码图像,用于过滤关键点。非零值表示感兴趣区域。通常传一个空即可
{
auto regions = std::unique_ptr<Regions_type>(new Regions_type);
if (image.size() == 0)
return regions;
// Convert to float in range
//将输入图像转换为浮点数类型,并将像素值归一化到 范围
const image::Image<float> If(image.GetMat().cast<float>()/255.0f);
// compute sift keypoints
// 计算 SIFT 关键点
{
const int supplementary_images = 3;
// 高斯层数和输出尺度之间的数量差3,详见sift原理文章。
// => in order to ensure each gaussian slice is used in the process 3 extra images are required:
// +1 for dog computation
// +2 for 3d discrete extrema definition
//生成高斯尺度空间,详见后文具体的类分析
HierarchicalGaussianScaleSpace octave_gen(
params_.num_octaves_,
params_.num_scales_,
(params_.first_octave_ == -1) // 这里判定首个octave是否要upscale,如果要upscale,相关尺度要进行调整
? GaussianScaleSpaceParams(1.6f/2.0f, 1.0f/2.0f, 0.5f, supplementary_images)
: GaussianScaleSpaceParams(1.6f, 1.0f, 0.5f, supplementary_images));
octave_gen.SetImage( If );//设置输入图像,详见下文相关类分析
std::vector<sift::Keypoint> keypoints; // 存储所有检测到的关键点
keypoints.reserve(5000);// 预分配空间,提高性能
Octave octave; // 存储当前octave的信息,以后每次迭代都是修改这个里面的信息
while ( octave_gen.NextOctave( octave ) )// 遍历octave
{
std::vector<sift::Keypoint> keys;
// Find Keypoints 创建一个 SIFT 关键点检测器
sift::SIFT_KeypointExtractor keypointDetector(
params_.peak_threshold_ / octave_gen.NbSlice(),
params_.edge_threshold_);
//参数1:这里就是原理篇中讲的对比度阈值计算方法,其中peak_threshold_为经验值
//参数2:边缘阈值,详见上文
keypointDetector(octave, keys); // 检测关键点,详细过程见下文
// 创建一个 SIFT 描述符提取器
sift::Sift_DescriptorExtractor descriptorExtractor;
descriptorExtractor(octave, keys); // 计算当前octave中关键点的方向和描述符
// Concatenate the found keypoints
std::move(keys.begin(), keys.end(), std::back_inserter(keypoints));
}
for (const auto & k : keypoints)
{
// Feature masking 特征掩码处理
if (mask)
{
const image::Image<unsigned char> & maskIma = *mask;
if (maskIma(k.y, k.x) == 0) // 如果关键点位于掩码的非感兴趣区域,则跳过该关键点
continue;
}
// Create the SIFT region 创建 SIFT 区域
{
// 将关键点的描述符转换为无符号字符类型,并添加到 Regions 对象中
regions->Descriptors().emplace_back(k.descr.cast<unsigned char>());
// 将关键点的坐标、尺度和方向添加到 Regions 对象中
regions->Features().emplace_back(k.x, k.y, k.sigma, k.theta);
}
}
}
return regions;
};在构建高斯金字塔时,往GaussianScaleSpaceParams(1.6f/2.0f, 1.0f/2.0f, 0.5f, supplementary_images)中传了一个0.5,我们默认输入图像已经进行了一次高斯卷积操作,其σ为这个0.5,那么第一层的图像就是从这个0.5卷积后的图像基础上,再进行一次卷积得到的。如果第一层所需的σ为1.6,则再次卷积所需的高斯σ为:
\[\sigma_{extra} = \sqrt{1.6^2-0.5^2}\]
而不是直接对图像做σ为1.6的卷积。
HierarchicalGaussianScaleSpace类
构建高斯金字塔的核心类
设置初始图象 SetImage
设置初始图像,主要是判断是否要upscale,以及计算octave的数量。
virtual void SetImage(const image::Image<float> & img)
{
const double sigma_extra =
sqrt(Square(m_params.sigma_min) - Square(m_params.sigma_in)) / m_params.delta_min; // 计算额外的高斯模糊标准差,用于在图像上应用额外的高斯滤波,见上文分析
if (m_params.delta_min == 1.0f) // 如果最小采样距离为 1.0f,直接对输入图像应用高斯滤波
{
image::ImageGaussianFilter(img, sigma_extra, m_cur_base_octave_image);
}
else// 如果最小采样距离为 0.5f,需要对图像进行上采样
{
if (m_params.delta_min == 0.5f)
{
image::Image<float> tmp;
ImageUpsample(img, tmp); //上采样
image::ImageGaussianFilter(tmp, sigma_extra, m_cur_base_octave_image);
}
else
{
OPENMVG_LOG_ERROR
<< "Upsampling or downsampling with delta equal to: "
<< m_params.delta_min << " is not yet implemented";
}
}
//-- Limit the size of the last octave to be at least 32x32 pixels
// 保证最后一个octave至少为 32x32 像素,这个要和传入的最大octave数量做个对比,取小的那个作为octave数量的确定值
const int nbOctaveMax = std::ceil(std::log2( std::min(m_cur_base_octave_image.Width(), m_cur_base_octave_image.Height())/32));
m_nb_octave = std::min(m_nb_octave, nbOctaveMax);
}const int index = (m_params.supplementary_levels == 0) ? 1 : m_params.supplementary_levels; 这里我们用第一个octave举例,如果supplementary_levels为3,则index = 3,那么下采样的层就是第6-3=3层,也就是sigma为\(2σ_{initial}\)的层,把这层作为第二个octave的基础图象。也就是说本来第二个octave的基础图象应该是\(2σ_{initial}\)对原图做卷积,这在第一个octave中已经计算过了,可以直接拿到第二个octave中去用。
SIFT_KeypointExtractor类
特征点提取类。在每一个octave迭代时,包含两个关键步骤,极值提取、极值修正。
默认参数
virtual bool NextOctave(Octave & octave)
{
if (m_cur_octave_id >= m_nb_octave)
{
return false;// 如果达到最大数量,返回 false 表示没有更多的octave可供计算
}
else
{
octave.octave_level = m_cur_octave_id;
if (m_cur_octave_id == 0) // 如果是第一个octave
{
octave.delta = m_params.delta_min; //设置采样率为最小采样率
}
else
{
octave.delta *= 2.0f; // 对于后续的octave,采样率翻倍(相当于图像缩小)
}
// init the "blur"/sigma scale spaces values
octave.slices.resize(m_nb_slice + m_params.supplementary_levels);
octave.sigmas.resize(m_nb_slice + m_params.supplementary_levels);
for (int s = 0; s < m_nb_slice+ m_params.supplementary_levels; ++s)
{
octave.sigmas =
octave.delta / m_params.delta_min * m_params.sigma_min * pow(2.0,(float)s/(float)m_nb_slice); //这里计算octave内每一层所需的sigma,其中k = 2^{1/s},详见原理篇文章
}
// Build the octave iteratively
octave.slices = m_cur_base_octave_image;
for (int s = 1; s < octave.sigmas.size(); ++s)
{
// Iterative blurring the previous image
const image::Image<float> & im_prev = octave.slices;
image::Image<float> & im_next = octave.slices;
const double sig_prev = octave.sigmas;
const double sig_next = octave.sigmas;
const double sigma_extra = sqrt(Square(sig_next) - Square(sig_prev)) / octave.delta;// 下一层的sigma是由当前层再进行一次卷积得到的,这个额外的卷积核计算方法遵循勾股定理
image::ImageGaussianFilter(im_prev, sigma_extra, im_next); //高斯模糊生成该层图像,每一层都是由上一层进行卷积的到,而非由原始图像卷积得到
}
// Prepare for next octave computation -> Decimation
++m_cur_octave_id;
if (m_cur_octave_id < m_nb_octave)
{
// Decimate => sigma * 2 for the next iteration
const int index = (m_params.supplementary_levels == 0) ? 1 : m_params.supplementary_levels;
// 对选定的切片进行下采样,作为下一个octave的基础图像
ImageDecimate(octave.slices, m_cur_base_octave_image);
}
return true;
}
}核心流程
float peak_threshold, // 对比度阈值,详见上文
float edge_threshold, // 边缘去除阈值,详见上文
int nb_refinement_step = 5 // 极值修正是一个迭代过程,这里限制了最大迭代次数极值提取 Find_3d_discrete_extrema
在 DoG 空间中查找 3D 离散极值点
// 重载函数调用运算符,计算给定高斯octave的差分高斯(DoG),并在 DoG 空间中查找离散极值点
//对这些关键点的位置进行细化,以得到更精确的关键点位置。
void operator()( const Octave & octave , std::vector<Keypoint> & keypoints )
{
if (!ComputeDogs(octave)) // 差分高斯(DoG),就是两个图像相减,不再赘述
return;
Find_3d_discrete_extrema(keypoints, 0.8f); //找到离散极值点,这里的0.8是对对比度阈值的一次缩放,可以初步筛出更多的极值点;默认值为1,对原阈值不做修改
Keypoints_refine_position(keypoints); //对极值点进行修正
}极值修正 Keypoints_refine_position
细化关键点的位置(空间位置和尺度),丢弃无法细化的关键点
//遍历输入的关键点列表,对每个关键点进行位置细化。//通过二次函数插值来优化关键点的位置,直到满足收敛条件或达到最大迭代次数。//对细化后的关键点进行峰值阈值、边缘响应和边界检查,只有通过所有检查的关键点才会被保留。void Keypoints_refine_position(std::vector & keypoints) const{std::vector kps;kps.reserve(keypoints.size());const float ofstMax = 0.6f; // 细化参数的最大偏移量// Ratio between two consecutive scales in the slice// 这个就是kconst float sigma_ratio = m_Dogs.sigmas / m_Dogs.sigmas;// 边缘响应阈值, 计算原理详见上文const float edge_thres = Square(m_edge_threshold + 1) / m_edge_threshold;const Octave & octave = m_Dogs;const int w = octave.slices.Width();const int h = octave.slices.Height();const float delta= octave.delta;for (const auto & key : keypoints) // 遍历keypoints{ float val = key.val; int ic = key.i; // current discrete value of x coordinate - at each interpolation int jc = key.j; // current discrete value of y coordinate - at each interpolation int sc = key.s; // current discrete value of s coordinate - at each interpolation float ofstX = 0.0f; float ofstY = 0.0f; float ofstS = 0.0f; bool isConv = false; // While position cannot be refined and the number of refinement is not too much // 迭代细化极值点 for ( int nIntrp = 0; !isConv && nIntrp < m_nb_refinement_step; ++nIntrp) { // Extrema interpolation via a quadratic function // only if the detection is not too close to the border (so the discrete 3D Hessian is well defined) if ( 0 < ic &&ic < (w-1) && 0 < jc && jc < (h-1) ) { // 通过taylor展开得到极值点附近的二次项近似,求取极值点偏移,求解过程实际是解一个导数为0的方程,详见算法原理篇 if (Inverse_3D_Taylor_second_order_expansion(octave, ic, jc, sc, &ofstX, &ofstY, &ofstS, &val, ofstMax)) isConv = true; } else { isConv = false; } if (!isConv) { // let's explore the neighborhood in // space... if (ofstX > +ofstMax && (ic+1) < (w-1) ) {++ic;} if (ofstX < -ofstMax && (ic-1) >0 ) {--ic;} if (ofstY > +ofstMax && (jc+1) < (h-1) ) {++jc;} if (ofstY < -ofstMax && (jc-1) >0 ) {--jc;} // ... and scale. /* if (ofstS > +ofstMax && (sc+1) < (ns-1)) {++sc;} if (ofstS < -ofstMax && (sc-1) > 0) {--sc;} */ } } if (isConv) { // Peak threshold check if ( std::abs(val) > m_peak_threshold )// 对比度高的点保留 { Keypoint kp = key; kp.x = (ic + ofstX) * delta; kp.y = (jc + ofstY) * delta; kp.i = ic; kp.j = jc; kp.s = sc; kp.sigma = octave.sigmas * pow(sigma_ratio, ofstS); // logarithmic scale kp.val = val; // Edge check 高斯差分对边缘的响应也很高,要把这些边缘点去除掉 if (Compute_edge_response(kp) >=0 && std::abs(kp.edgeResp)
页:
[1]