SURF算法學習筆記

本博客內容來源于網(wǎng)絡以及其他書籍,結合自己學習的心得進行重編輯,因為看了很多文章不便一一標注引用,如圖片文字等侵權,請告知刪除。

傳統(tǒng)2D計算機視覺學習筆記目錄------->傳送門
傳統(tǒng)3D計算機視覺學習筆記目錄------->傳送門

前言

本篇的上一篇文章描寫的是SIFT算法SIFT算法學習筆記(上) ,本篇博主在SIFT算法詳細內容還很清晰的情況下,再描述一下SIFT的優(yōu)化算法-- SURF算法。因為SURF是SIFT的優(yōu)化算法,所以本文只描述與SIFT不同的地方,與SIFT相同的地方就不再重復描述,因而在讀本文之前,希望能夠看完SIFT算法的文章,或則對SIFT足夠理解。那么開始吧?。?!

SURF 簡介

SURF(Speeded-Up Robust Features)加速穩(wěn)健特征,是一種穩(wěn)健的局部特征點檢測和描述算法。最初由Herbert Bay發(fā)表在2006年的歐洲計算機視覺國際會議ECCV上,并在2008年正式發(fā)表在Computer Vision and Image Understanding期刊上。
Surf是可以說是對Sift算法的改進,該算子在保持 SIFT 算子優(yōu)良性能特點的基礎上,同時解決了 SIFT 計算復雜度高、耗時長的缺點,提升了算法的執(zhí)行效率,為算法在實時計算機視覺系統(tǒng)中應用提供了可能,sift在一般的計算機對于日常使用的圖片想做到實時基本上是不可能的。

下面我們來看一下SURF到底對SIFT做了什么改進,是怎么繼續(xù)保證SIFT的優(yōu)秀的效果的以及SURF和SIFT有什么效果上的區(qū)別。

SURF 流程

與Sift算法一樣,Surf算法的基本路程也可以分為四大部分:尺度空間建立、特征點定位、特征點方向確定,特征點描述。

基本的流程都是一樣的,那么SURF是怎么改進其執(zhí)行效率的呢?主要還是在兩個關鍵的優(yōu)化點:

  1. 積分圖在Hessian(黑塞矩陣)上的使用
  2. 降維的特征描述子的使用

在梳理SURF的流程之前,我們先了解幾個概念:積分圖,Hessian矩陣,harr小波特征。

  • 積分圖

我們知道圖像是由一系列的離散像素點組成, 所以圖像的積分其實就是所有的像素點求和. 圖像積分圖中每個點的值是原圖像中該點左上角的所有像素值之和。

通過上述公式,我們能比較好的看出,積分圖像可以采用增量的方式計算:

其中I(x,y)為像素點(x,y)的像素值。增量的計算方式,可以保證其計算速度。概念很簡單,理解沒有問題,具體怎么用我們之后在流程中再解釋。

  • Hessian矩陣

這個好像很熟悉?我們的確在之前的幾篇文章中,見過很多次這個矩陣了,包括在SIFT去除邊緣響應?,F(xiàn)在我們詳細解釋一下。

所謂 Hessian矩陣 就是一個多元函數(shù)的二階偏導數(shù)構成的方陣,描述了函數(shù)的局部曲率。此外還有Jacob矩陣也是描述函數(shù)的局部曲率的,是一個多元函數(shù)的一階偏導數(shù)構成的方陣。

Hessian矩陣如下:

Hessian 就是描述的一個點周圍像素梯度大小的變化率,其極值就是生成圖像穩(wěn)定的邊緣點(突變點),其兩個特征值,代表這其在兩個相互垂直方向是的梯度的變化率,當兩個特征值越大時,其圖像中的像素點的像素值波動越大。我們用兩個特征值相加即 Hessian的判別式(即行列式的值)來量化。

  • harr小波特征

這個還挺難解釋的,個人也還是不是理解的那么好,而且越看越迷糊,下次通過一個文章來寫吧。先立個flag

在我們了解了上述內容,我們來看接下來的步驟。

1. 構建尺度空間

同SIFT算法一樣,SURF算法的尺度空間由??組??層組成,不同的是,SIFT算法下一組圖像的長寬均是上一組的一半,同一組不同層圖像之間尺寸一樣,但是所使用的尺度空間因子(高斯模糊系數(shù)??)逐漸增大;而在SURF算法中,不同組間圖像的尺寸都是一致的,不同的是不同組間使用的盒式濾波器的模板尺寸逐漸增大,同一組不同層圖像使用相同尺寸的濾波器,但是濾波器的尺度空間因子(即高斯模糊系數(shù)??)逐漸增大。

左圖表示的是傳統(tǒng)方式建立一個如圖所看到的的金字塔結構。圖像的大小是變化的。而且運 算會重復使用高斯函數(shù)對子層進行平滑處理。右圖說明Surf算法使原始圖像保持不變而僅僅改變?yōu)V波器大小。Surf採用這樣的方法節(jié)省了降採樣過程,其處理速度自然也就提上去了。

  • SURF中盒式濾波器解釋

    在構建尺度空間的時候,我們使用剛才解釋的Hessian矩陣,但是在構造Hessian矩陣前需要對圖像進行高斯濾波,經(jīng)過濾波后的Hessian矩陣表述為:

    圖中,Lxx(x,??) 是對高斯函數(shù)進行x方向的二次導數(shù),其他的類似。
    其實,當Hessian矩陣的判別式取得局部極大值時,判定當前點是比周圍鄰域內其他點更亮或更暗的點,由此來定位關鍵點的位置。

    Hessian矩陣判別式中的L(x,y)是原始圖像的高斯卷積,由于高斯核實服從正態(tài)分布的,從中心點往外,系數(shù)越來越低,為了提高運算速度,Surf使用了盒式濾波器來近似替代高斯濾波器,所以在Dxy上乘了一個加權系數(shù)0.9,目的是為了平衡因使用盒式濾波器近似所帶來的誤差:

    那怎么用用合適濾波器來近似高斯濾波器呢?有為什么能提高速度呢?

    上邊兩幅圖是9*9高斯濾波器模板分別在圖像上垂直方向上二階導數(shù)Dyy和Dxy對應的值,下邊兩幅圖是使用盒式濾波器對其近似,灰色部分的像素值為0,黑色為-2,白色為1。

    盒式濾波器對圖像的濾波轉化成計算圖像上不同區(qū)域間像素和的加減運算問題,這正是積分圖的強項,只需要簡單幾次查找積分圖就可以完成。這樣就大大的提高了構造尺度空間的速度。

支持我們已經(jīng)解釋了surf是怎么構造尺度空間的并能達到和sift類似的效果,為什么要按這種方式構造尺度的,有怎么提高構造尺度空間的速度的。

2. 特征點定位

這一部分和SIFT查找方式一樣,找到尺度空間的局域極大值,然后刪除響應比較弱的關鍵點以及錯誤定位的關鍵點,以及進行亞像素分析。

3. 特征點方向確認

Sift特征點方向分配是采用在特征點鄰域內統(tǒng)計其梯度直方圖,取直方圖最大值的以及超過最大值80%的那些方向作為特征點的主方向。

而在Surf中,采用的是統(tǒng)計特征點圓形鄰域內的harr小波特征。即在特征點的圓形鄰域內,統(tǒng)計60度扇形內所有點的水平、垂直harr小波特征總和,然后扇形以0.2弧度大小的間隔進行旋轉并再次統(tǒng)計該區(qū)域內harr小波特征值之后,最后將值最大的那個扇形的方向作為該特征點的主方向。該過程示意圖如下:

這樣做的有點就是統(tǒng)計的方向更加精確,但是計算時間會略有上升,但是畢竟計算的只有關鍵點,可忽略不計。

4. 生成特征點描述向量

在SIFT中,是提取特征點周圍44個區(qū)域塊,統(tǒng)計每小塊內8個梯度方向,用著44*8=128維向量作為Sift特征的描述子。

SURF算法中,也是在特征點周圍取一個4*4的矩形區(qū)域塊,但是所取得矩形區(qū)域方向是沿著特征點的主方向。每個子區(qū)域統(tǒng)計25個像素的水平方向和垂直方向的haar小波特征,這里的水平和垂直方向都是相對主方向而言的。該haar小波特征為水平方向值之后、垂直方向值之后、水平方向絕對值之后以及垂直方向絕對值之和4個方向。該過程示意圖如下:

這樣SURF提取的就是一個444 的64維的特征向量。

OpenCV SURF特征效果展示[代碼]

#include <opencv2/opencv.hpp>
#include <iostream>
#include <opencv2/xfeatures2d.hpp>
void extracte_surf(cv::Mat input,std::vector<cv::KeyPoint> &keypoint,cv::Mat &descriptor){
    cv::Ptr<cv::Feature2D> f2d = cv::xfeatures2d::SURF::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("surf"+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 < 6 * 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_surf_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_surf(image1,keypoint1,descriptor1);
    extracte_surf(image2,keypoint2,descriptor2);
    match_two_image(image1,image2,keypoint1,keypoint2,descriptor1,descriptor2);
    return 0;
}

圖片還是SIFT文章中使用的圖片

原圖1 原圖2
原圖1提取SURF 原圖2提取SURF
經(jīng)過ransac后的特征點匹配圖

總結

surf如何保證那些sift類似的那些特點的,按照sift的想法還是很容易分析的,這里就不一一分析。

從實驗結果上看,surf的特征點的提取效果還是不錯的,特征點的數(shù)目也很多,而且時間上有大大的優(yōu)化。論文《A comparison of SIFT, PCA-SIFT and SURF 》對三種方法給出了性能上的比較,結論是:SIFT在尺度和旋轉變換的情況下效果最好,SURF在亮度變化下匹配效果最好,在模糊方面優(yōu)于SIFT,而尺度和旋轉的變化不及SIFT,旋轉不變上比SIFT差很多。速度上看,SURF是SIFT速度的3倍。


重要的事情說三遍:

如果我的文章對您有所幫助,那就點贊加個關注唄 ( * ^ __ ^ * )

如果我的文章對您有所幫助,那就點贊加個關注唄 ( * ^ __ ^ * )

如果我的文章對您有所幫助,那就點贊加個關注唄 ( * ^ __ ^ * )

傳統(tǒng)2D計算機視覺學習筆記目錄------->傳送門
傳統(tǒng)3D計算機視覺學習筆記目錄------->傳送門

任何人或團體、機構全部轉載或者部分轉載、摘錄,請保留本博客鏈接或標注來源。博客地址:開飛機的喬巴

作者簡介:開飛機的喬巴(WeChat:zhangzheng-thu),現(xiàn)主要從事機器人抓取視覺系統(tǒng)以及三維重建等3D視覺相關方面,另外對slam以及深度學習技術也頗感興趣,歡迎加我微信或留言交流相關工作。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容