本文对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[0]); //主要执行函数
- image_describer->Describe(imageR, regions_perImage[1]);
复制代码 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响应:
\[R = \text{Det}(M) - \alpha \text{Tr}(M)^2=\lambda_{1}\lambda_{2}-\alpha(\lambda_{1}+\lambda_{2})^2\]
其中α是一个经验值 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 [0;1]
- //将输入图像转换为浮点数类型,并将像素值归一化到 [0, 1] 范围
- 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[s] =
- 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[0] = 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[s-1];
- image::Image<float> & im_next = octave.slices[s];
- const double sig_prev = octave.sigmas[s-1];
- const double sig_next = octave.sigmas[s];
- 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[octave.sigmas.size()-index], 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
细化关键点的位置(空间位置和尺度),丢弃无法细化的关键点
[code]//遍历输入的关键点列表,对每个关键点进行位置细化。//通过二次函数插值来优化关键点的位置,直到满足收敛条件或达到最大迭代次数。//对细化后的关键点进行峰值阈值、边缘响应和边界检查,只有通过所有检查的关键点才会被保留。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 // 这个就是k const float sigma_ratio = m_Dogs.sigmas[1] / m_Dogs.sigmas[0]; // 边缘响应阈值, 计算原理详见上文 const float edge_thres = Square(m_edge_threshold + 1) / m_edge_threshold; const Octave & octave = m_Dogs; const int w = octave.slices[0].Width(); const int h = octave.slices[0].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[sc] * pow(sigma_ratio, ofstS); // logarithmic scale kp.val = val; // Edge check 高斯差分对边缘的响应也很高,要把这些边缘点去除掉 if (Compute_edge_response(kp) >=0 && std::abs(kp.edgeResp) |