深度学习-推荐系统-CV-NLPOpenCv机器学习与计算机视觉

SIFT算法学习笔记(上)

2019-08-17  本文已影响12人  开飞机的乔巴

本博客内容来源于网络以及其他书籍,结合自己学习的心得进行重编辑,因为看了很多文章不便一一标注引用,如图片文字等侵权,请告知删除。

传统2D计算机视觉学习笔记目录------->传送门
传统3D计算机视觉学习笔记目录------->传送门

网上关于sift的教程已经很多很多了,很多人写的都很好,但是对于我个人一听到sift还是内心有恐惧(是不是第一次学习的时候比较迷糊导致的?),所以还是决定来自己梳理一遍sift,其实没有那么复杂。

SIFT简介

SIFT,即尺度不变特征变换(Scale-invariant feature transform,SIFT),是用于图像处理领域的一种描述方式。这种描述具有尺度不变性,可在图像中检测出关键点,是一种局部特征描述子。该方法于1999年由David Lowe首先发表于计算机视觉国际会议ICCV,2004年再次经David Lowe整理完善后发表于IJCV。截止目前,google 统计的引用数已达52190次。

SIFT 可以说是传统视觉算法特征提取的一个里程碑式的成就,不仅是其效果远超其他方法,其分析问题的思路,也启发了之后很多的算法。很多其他的特征点描述都在其之上进行修改优化。为什么要进行修改优化呢?第一、要进行满足自己的场景需求的独特优化,比如速度要求。第二、SIFT已经申请了专利,虽然可能大家很多人都在项目中偷偷使用。

SIFT特点

既然把sift吹的这么高,我们来看看其到底有什么特别之处。

可能目前由于深度学习的发展尤其是CNN,感觉不到上述特点有多么厉害。但对于当时,深度学习还没有被发展的这么好时,他解决了很多很难的问题,基本上也是其特点描述的,比如如下:

SIFT算法具体流程

这一节,我们先把sift算法流程描述清楚,然后再下一节在理解为什么这样是有效的(建议先读这一节,再度下一节,再度一下这一节,会有豁然开朗的感觉)。

1. 尺度空间极值检测

首先我们得先了解一下尺度空间什么?(这个在LOG斑点检测中提到)
尺度空间理论的基本思想是:在图像信息处理模型中引入一个被视为尺度的参数,通过连续变化尺度参数获得多个不同尺度下的尺度空间表示序列。正如我们在LOG中,使用不同的方差 σ 获得不同的高斯核,我们就可以认为σ就是一个被视为尺度的参数。

构造高斯金字塔

我们现在对尺度空间的定义有了解了,那我们具体怎么实现尺度空间?这里我们采用高斯金字塔的方式,下面我来详细解释。

先贴图

图像的金字塔模型是指,将原始图像不断降阶采样,得到一系列大小不一的图像,由大到小,从下到上构成的塔状模型。原图像为金子塔的第一层,每次降采样所得到的新图像为金字塔的一层,每个金字塔共n层。金字塔的层数根据图像的原始大小和塔顶图像的大小共同决定。

高斯金字塔在简单降采样的基础上加上了高斯滤波,将图像金字塔每层的一张图像使用不同参数做高斯模糊,使得金字塔的每层含有多张高斯模糊图像,将金字塔每层多张图像合称为一组(Octave)。简单来说就是例如上图中Octave1 中多张图像的大小是一样的,但是就是经过了不同尺度的高斯滤波。

  1. 对原始图像做不同尺度的高斯滤波
  2. 通过降采样得到下一组图像的初始图像,这个初始图像是由上一组的倒数第三张图片降采样得到的。
  3. 然后再对新得初始图像进行不同尺度的高斯滤波,循环2,3步骤,直至层数满足条件。

这个条件如下:
因为是隔点采样,所以金字塔的组数O满足:

其中M,N分别是原始图像的大小。

一般我们是使用DoG来代替LoG,而 DoG 金字塔由高斯金字塔相邻两层相减得到,则高斯金字塔每组需 S+3 层图像,实际计算时 S 通常在3到5之间,那么每组的高斯滤波的尺度满足如下条件:

设位于金字塔底的第1组的第1层图像,其高斯滤波的尺度为

o为组数,s为每一组的第s层图像。

则位于第1组的其它层图像的高斯滤波尺度分别为 同理,位于第2组的各层图像的高斯滤波尺度分别为 其中,k为相邻尺度的缩放因子

我们可以看出满足下一组初始图像是由本组的倒数第三张图片降采样得到的要求。

构造差分高斯金字塔

在斑点检测我们讨论过高斯算子(LOG)与差分高斯算子(DOG)的关系,这里不再详述。我们可以知道使用差分高斯金字塔代替高斯金字塔进行极值检测,会节省很多的计算时间。


现在我们构造出了差分高斯金字塔,下一步就是在DoG中寻找极值点,就是特征点的候选点。

空间极值点检测

这一步和在斑点检测学习笔记中LOG在尺度空间寻找极值点基本一样。极值点也是关键点的候选点,关键点是由DOG空间的局部极值点组成的,关键点的初步探查是通过同一组内各DoG相邻两层图像之间比较完成的。为了寻找DoG函数的极值点,每一个像素点要和它所有的相邻点比较,看其是否比它的图像域和尺度域的相邻点大或者小。

例如上图中,中间的检测点和它同尺度的8个相邻点和上下相邻尺度对应的9×2个点共26个点比较,以确保在尺度空间和二维图像空间都检测到极值点。

当然这样产生的极值点并不全都是稳定的特征点,因为某些极值点对比度较弱,而且DOG算子会产生较强的边缘响应。所以我们下一步还要对这些极值点进行更精确的过滤。

2. 关键点定位

在关键点定位,我们要通过上一步筛选出的极值点,通过拟合三维空间中的二次函数,来精确确定关键点的位置和尺度,此外我们还要删除对比度较弱的极值点,以及删除边缘响应点。我们一步一步来看

极值点精确定位

首先我们先描述清楚我们为什么要再进行极值点的精确定位?

如上图,我们的数字图像都是离散采样的,我们检测到的极值点也是经过离散采样,而真实空间都是连续的,所以我们通过你和一个函数,就可以过得更加精确的连续空间上的极值点。
具体的拟合过程,在此不尽兴详细的描述,过程就是通过对DOG函数的泰勒展开,过得真正的极值点与当前的极值点的偏移量,来求得真实的极值点。这个过程可以迭代多次进行,获得更精确的解。

删除低对比度极值点

所谓低对比度的点,是那些对噪声敏感的候选点 ,需要剔除。这些点就是那些检测到的极值点与真正的极值点相差较多的极值点。上一步我们获得了检测出的极值点与真正的极值点的差值,当差值大于某个阈值时,则删除这些极值点,因为这些极值点非常的不稳定。

消除边缘响应

由于DoG函数在图像边缘有较强的边缘响应,而边缘上的极值点抗噪性较差,因此我们还需要排除边缘响应。为什么边缘上的点不好呢?一方面图像边缘上的点是很难定位的,具有定位歧义性;另一方面这样的点很容易受到噪声的干扰而变得不稳定。
那么如何消除这些边缘响应呢?
我们知道曲面上每个点(非平点)都有两个主方向,并且沿这两个主方向的法曲率分别是曲面在该点法曲率的最大值和最小值。对于边缘上的点,沿垂直于边缘的方向上,法曲率最大,而沿边缘的方向上,法曲率最小。因此对于分布在边缘上附近的极值点,它们的法曲率最大值和最小值之比,一般情况下要比非边缘点的比值大。

根据这种思想,我们可以设一个比值的阈值,当比值大于这个阈值就认为极值点在边缘上。

而主曲率可以通过2×2的Hessian矩阵𝐻求出

这里Dxx表示DOG金字塔中某一尺度的图像x方向求导两次,微分可以通过计算邻近点的差值来近似计算。在我们之前的斑点检测到是不是用过这个?

又因为DOG的主曲率和H的特征值成正比,我们只需要计算 H 的较大特征值与较小特征值的比例即可。假设两个特征值分别为α(较大值),β(较小值),r = α/β 则有如下: 那么 在作者的论文中,r取10,也就是一个点的求得的H,满足下列式子

则这个极值点保存,否则,则删除。

3. 方向确定

通过上面两步我们就筛选出了sift的关键点(特征点),接下来,我们怎么就是怎么来描述这些关键点。这篇文章的内容是在是比较长了,而剩下的内容也有很长的篇幅,我将文章分成了两部分,大家也可暂停下来,看看sift的效果。
敬请跳转到SIFT算法学习笔记下

4. 关键点描述

敬请跳转到SIFT算法学习笔记下

SIFT是怎么保证效果的(理解sift的关键部分)

敬请跳转到SIFT算法学习笔记下

OpenCV SIFT特征效果展示[代码]

#include <opencv2/opencv.hpp>
#include <iostream>
#include <opencv2/xfeatures2d.hpp>
void extracte_sift(cv::Mat input,std::vector<cv::KeyPoint> &keypoint,cv::Mat &descriptor){
    cv::Ptr<cv::Feature2D> f2d = cv::xfeatures2d::SIFT::create();
    f2d->detect(input,keypoint);
    cv::Mat image_with_kp;
    f2d->compute(input,keypoint,descriptor);
    cv::drawKeypoints(input, keypoint, image_with_kp, cv::Scalar::all(-1),cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
    cv::imwrite("sift"+std::to_string(keypoint.size())+".png",image_with_kp);
}

void match_two_image(cv::Mat image1,cv::Mat image2, std::vector<cv::KeyPoint> keypoint1,std::vector<cv::KeyPoint> keypoint2,cv::Mat descriptor1,cv::Mat descriptor2){
    cv::FlannBasedMatcher matcher;
    std::vector<cv::DMatch> matches, goodmatches;
    matcher.match(descriptor1,descriptor2, matches);
    cv::Mat  good_matches_image;
    double max_dist = 0; double min_dist = 1000;
    for (int i = 0; i < descriptor1.rows; i++) {
        if (matches[i].distance > max_dist) {
            max_dist = matches[i].distance;
        }
        if (matches[i].distance < min_dist) {
            min_dist = matches[i].distance;
        }
    }
    for (int i = 0; i < descriptor1.rows; i++) {
        if (matches[i].distance < 4 * min_dist) {
            goodmatches.push_back(matches[i]);
        }
    }
    cv::drawMatches(image1, keypoint1, image2, keypoint2,
                    goodmatches, good_matches_image, cv::Scalar::all(-1), cv::Scalar::all(-1),
                    std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
    cv::imwrite("good_matches_image.png",good_matches_image);
    {
        std::vector <cv::KeyPoint> RAN_KP1, RAN_KP2;
        std::vector<cv::Point2f> keypoints1, keypoints2;
        for (int i = 0; i < goodmatches.size(); i++) {
            keypoints1.push_back(keypoint1[goodmatches[i].queryIdx].pt);
            keypoints2.push_back(keypoint2[goodmatches[i].trainIdx].pt);
            RAN_KP1.push_back(keypoint1[goodmatches[i].queryIdx]);
            RAN_KP2.push_back(keypoint2[goodmatches[i].trainIdx]);
        }

        std::vector<uchar> RansacStatus;
        cv::findFundamentalMat(keypoints1, keypoints2, RansacStatus, cv::FM_RANSAC);

        std::vector <cv::KeyPoint> ransac_keypoints1, ransac_keypoints2;
        std::vector <cv::DMatch> ransac_matches;
        int index = 0;
        for (size_t i = 0; i < goodmatches.size(); i++)
        {
            if (RansacStatus[i] != 0)
            {
                ransac_keypoints1.push_back(RAN_KP1[i]);
                ransac_keypoints2.push_back(RAN_KP2[i]);
                goodmatches[i].queryIdx = index;
                goodmatches[i].trainIdx = index;
                ransac_matches.push_back(goodmatches[i]);
                index++;
            }
        }
        cv::Mat after_ransac_sift_match;
        cv::drawMatches(image1, ransac_keypoints1, image2, ransac_keypoints2,
                        ransac_matches, after_ransac_sift_match, cv::Scalar::all(-1), cv::Scalar::all(-1),
                        std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
        cv::imwrite("after_ransac_sift_match.png",after_ransac_sift_match);
    }
}

int main(int argc, char *argv[])
{
    cv::Mat image1 = cv::imread(argv[1]);
    cv::Mat image2 = cv::imread(argv[2]);
    std::vector<cv::KeyPoint> keypoint1,keypoint2;
    cv::Mat descriptor1, descriptor2;
    extracte_sift(image1,keypoint1,descriptor1);
    extracte_sift(image2,keypoint2,descriptor2);
    match_two_image(image1,image2,keypoint1,keypoint2,descriptor1,descriptor2);
    return 0;
}

标识出sift特征点的原图

原图1 原图2
初次匹配较好的结果 经过ransac后的结果

总结

总体来说实验的结果还是比较好的验证了我们提出的sift特征点的特点。在这两张图片上都找出了大量的特征点。通过ransac可以看出大部分处于同一位置,即使有这较大光照差异,旋转,放缩,以及视角变换,其特征点的描述依旧是比较接近的。当然有一些匹配错了,但是其局部相似度还是很高的,不妨碍sift是一个优秀的特征提取描述算法。


重要的事情说三遍:

如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

传统2D计算机视觉学习笔记目录------->传送门
传统3D计算机视觉学习笔记目录------->传送门

任何人或团体、机构全部转载或者部分转载、摘录,请保留本博客链接或标注来源。博客地址:开飞机的乔巴

作者简介:开飞机的乔巴(WeChat:zhangzheng-thu),现主要从事机器人抓取视觉系统以及三维重建等3D视觉相关方面,另外对slam以及深度学习技术也颇感兴趣,欢迎加我微信或留言交流相关工作。

上一篇下一篇

猜你喜欢

热点阅读