SURF特征匹原理及源代码

SURF是对SIFT的改进,相对于SIFT,主要优点是速度更快,更适合做实时特征检查。

 

 

一、SURF原理:

  相对于SIFT,SUFT采用Hssian算法检测关键点,很大程度上提高了程序的运行速度,同时,在尺度空间的构建上,SIFT通过改变高斯卷积核的大小,构建不同的组,下文进行详细介绍。

 

二、SURF的实现:

  与SIFT步骤相同,可分为四步:尺度空间构建和极值检测、特征点精确定位和过滤、特征方向赋值和特征点描述。

  尺度空间构建和极值检测:

    SIFT通过高斯滤波后的同组图像中的每层相减得到DOG空间来提取特征点,SURF通过Harris矩阵(极大提升速度的原因),求取X和Y的导数,进行角点的大致判断。下面简要介绍实现步骤:

       Harris矩阵的表示:    

 

   上式中的L表示在经过高斯滤波后的图像的像素值的大小,Lxx、Lxy、Lyy分别表示在x方向的二次偏导导、在x和y方向上的二次偏导和在y方向的二次偏导。在进行Harris矩阵求取时,需满足尺度不变特性。即需先进行高斯滤波,得到多尺度的金字塔图像(具体构造会在下面介绍)。根据上式可得到Harris矩阵表示为行列式的值的大小:

          Det(H) = LxxLyy - LxyLxy

    根据图像像素点的离散性可知,图像像素值的求导可通过相邻像素点像素值的相减得到:

          Lx = L(x+1,y) - L(x,y)    Lxx = L(x+1,y) - L(x,y) - L(x,y)+L(x-1,y)  = L(x+1,y)+L(x-1,y)-2L(x,y)

   同理可求得Lxx、Lxy和Lyy的表达式,上式中的L均为经过高斯滤波后的像素值大小。由上面表达式可知,得到Harris响应值,需要先经过高斯平滑和二次求导两步操作,SURF算法作者采用箱式滤波模板近似代替上面两步,加快了求导的速度。下面暂时介绍SURF中箱式滤波原理(这个箱式滤波,在SURF中的使用方法,,,头疼、自闭、脱发!!!!!!!):

   SURF中箱式滤波:

      箱式滤波可简单理解为将像素点周围划分为多个区域,再将每个局部区域内的像素值相加再乘以一个权重值后,进行相加。

        

 

 

     上图中,第一幅表示为经过高斯滤波后在y方向的二阶求导,第二幅图像为经过高斯滤波后,在xy方向的求导,第三幅图像表示为箱式滤波在y方向的求导,第四幅图表示箱式滤波为在xy方向上的求导。在采用箱式滤波的过程中,针对每个局部区域像素值的计算,可通过积分图像进行快速计算,加快了计算的速度。(积分图像在另一篇博客中已有介绍)

    在采用箱式卷积核替换后,Harris响应值的计算可用下式表示:

            Det(H) = DxxDyy -(wDxy)2     //其中D表示盒子模板与图像的近似卷积  其中w为更好模拟而设的变量(原算法中建议取0.9)

     通过判断 Det(H)结果的符号,可简要判断是否为局部极值点,若为负值,则不是局部极值点,若为正值,则可能归类为局部极值点。

尺寸空间构建:

    相对于SIFT,SURF构建的图像金字塔不同组图像的尺寸大小相同,不同的是卷积核的大小(在此,,,查了很多博客,,,发现都写成高斯卷积核大小,,,很想骂人,,,在开始写为高斯卷积核,在计算Harris相应值时,又改为箱式滤波核,纠结了很久,看了不少博客,最终应该表示为箱式卷积核的大小)。下图为一个简要概括

        

    非极值抑制:

      通过上面得到极值点,将其响应值大小与上下两层响应值大小进行比较,进行判断是否为极值。(原理和SIFT相同)

 

主方向的计算:

      为确保得到的描述子具有旋转不变性,需对每一个特征点分配一个主方向。具体方法为:以特征点为中心,以6s(s为特征点的尺度)为半径的圆形内,对图像进行Haar小波响应运算(相当于对图像进行梯度运算,但采用积分图像,更有效率。Haar小波响应运算暂不介绍,如有兴趣,可网上百度查询)。后设置一个以方向为中心,以pi/3为张角的扇形滑动窗口,通过一定的弧度(大概0.2)为步长,旋转这个滑动窗口,对窗口内的图像Haar小波的响应值进行累加。主方向最大的Harr响应累加值对应的方向即为主方向。

 

SURF特征点描述子计算:

          在SURF算法中,也是在特征点周围取一个正方形框,框的边长为20s(s是所检测到该特征点所在的尺度)。该框带方向,方向当然就是上面一步检测出来的主方向。然后把该框分为16个子区域,每个子区域统计25个像素的水平方向和垂直方向的haar小波特征,这里的水平方向和垂直方向是相对于主方向而言的。该haar小波特征为水平方向值之和、水平方向绝对值之和和垂直方向之和、垂直方向绝对值之和。这样每个小区域就有4个值,所以每个特征点就是16*4维的向量,相比SIFT而言,少了一半,这在特征匹配的过程中会大大的加速匹配的速度。

  在OpenSURF的实现源码中采用的方式,通过点旋转公式,把点旋转到主方向上并进行最近邻插值的对应点,公式如下:

 

 

 

 

 

OPENCV-contrib内接口:

    cv::Ptr<cv::xfeatures2d::SURF> surf = cv::xfeatures2d::SURF::create();

 

完整实现:

#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>

int main()
{
    cv::Mat imageL = cv::imread("T.png");
    cv::Mat imageR = cv::imread("-DT.png");


    //提取特征点方法


    cv::Ptr<cv::xfeatures2d::SURF> surf = cv::xfeatures2d::SURF::create();

    //特征点
    std::vector<cv::KeyPoint> keyPointL, keyPointR;
    //单独提取特征点
    surf->detect(imageL, keyPointL);
    surf->detect(imageR, keyPointR);

    //画特征点
    cv::Mat keyPointImageL;
    cv::Mat keyPointImageR;
    drawKeypoints(imageL, keyPointL, keyPointImageL, cv::Scalar::all(-1), cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
    drawKeypoints(imageR, keyPointR, keyPointImageR, cv::Scalar::all(-1), cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);

    //显示窗口
    cv::namedWindow("KeyPoints of imageL");
    cv::namedWindow("KeyPoints of imageR");

    //显示特征点
    cv::imshow("KeyPoints of imageL", keyPointImageL);
    cv::imshow("KeyPoints of imageR", keyPointImageR);

    //特征点匹配
    cv::Mat despL, despR;
    //提取特征点并计算特征描述子
    surf->detectAndCompute(imageL, cv::Mat(), keyPointL, despL);
    surf->detectAndCompute(imageR, cv::Mat(), keyPointR, despR);

 
    std::vector<cv::DMatch> matches;

    //如果采用flannBased方法 那么 desp通过orb的到的类型不同需要先转换类型
    if (despL.type() != CV_32F || despR.type() != CV_32F)
    {
        despL.convertTo(despL, CV_32F);
        despR.convertTo(despR, CV_32F);
    }

    cv::Ptr<cv::DescriptorMatcher> matcher = cv::DescriptorMatcher::create("FlannBased");
    matcher->match(despL, despR, matches);

    //计算特征点距离的最大值
    double maxDist = 0;
    for (int i = 0; i < despL.rows; i++)
    {
        double dist = matches[i].distance;
        if (dist > maxDist)
            maxDist = dist;
    }

    //挑选好的匹配点
    std::vector< cv::DMatch > good_matches;
    for (int i = 0; i < despL.rows; i++)
    {
        if (matches[i].distance < 0.03*maxDist)
        {
            good_matches.push_back(matches[i]);
        }
    }



    cv::Mat imageOutput;
    cv::drawMatches(imageL, keyPointL, imageR, keyPointR, good_matches, imageOutput);

    cv::namedWindow("picture of matching");
    cv::imshow("picture of matching", imageOutput);
    cv::waitKey(0);
    return 0;
}

 

源代码:

struct SURFInvoker
{
    enum{ORI_RADIUS = 6, ORI_WIN = 60, PATCH_SZ = 20};
    // Parameters
    const Mat* img;
    const Mat* sum;
    vector<KeyPoint>* keypoints;
    Mat* descriptors;
    bool extended;
    bool upright;

    // Pre-calculated values
    int nOriSamples;
    vector<Point> apt; // 特征点周围用于描述方向的邻域的点
    vector<float> aptw; // 描述 方向时的 高斯 权
    vector<float> DW;


    SURFInvoker(const Mat& _img, const Mat& _sum,
        vector<KeyPoint>& _keypoints, Mat& _descriptors,
        bool _extended, bool _upright)
    {
        keypoints = &_keypoints;
        descriptors = &_descriptors;
        img = &_img;
        sum = &_sum;
        extended = _extended;
        upright = _upright;

        // 用于描述特征点的 方向的 邻域大小: 12*sigma+1 (sigma =1.2) 因为高斯加权的核的参数为2sigma
        // nOriSampleBound为 矩形框内点的个数
        const int nOriSampleBound = (2 * ORI_RADIUS + 1)*(2 * ORI_RADIUS + 1); // 这里把s近似为1 ORI_DADIUS = 6s

        // 分配大小 
        apt.resize(nOriSampleBound);
        aptw.resize(nOriSampleBound);
        DW.resize(PATCH_SZ*PATCH_SZ); // PATHC_SZ为特征描述子的 区域大小 20s(s 这里初始为1了)

        /* 计算特征点方向用的 高斯分布 权值与坐标 */
        Mat G_ori = getGaussianKernel(2 * ORI_RADIUS + 1, SURF_ORI_SIGMA, CV_32F); // SURF_ORI_SIGMA = 1.2 *2 =2.5
        nOriSamples = 0;
        for (int i = -ORI_RADIUS; i <= ORI_RADIUS; i++)
        {
            for (int j = -ORI_RADIUS; j <= ORI_RADIUS; j++)
            {
                if (i*i + j*j <= ORI_RADIUS*ORI_RADIUS) // 限制在圆形区域内
                {
                    apt[nOriSamples] = cvPoint(i, j);
                    // 下面这里有个坐标转换,因为i,j都是从-ORI_RADIUS开始的。
                    aptw[nOriSamples++] = G_ori.at<float>(i + ORI_RADIUS, 0) * G_ori.at<float>(j + ORI_RADIUS, 0);
                }
            }
        }
        CV_Assert(nOriSamples <= nOriSampleBound); // nOriSamples为圆形区域内的点,nOriSampleBound是正方形区域的点

        /* 用于特征点描述子的高斯 权值 */
        Mat G_desc = getGaussianKernel(PATCH_SZ, SURF_DESC_SIGMA, CV_32F); // 用于生成特征描述子的 高斯加权 sigma = 3.3s (s初取1)
        for (int i = 0; i < PATCH_SZ; i++)
        {
            for (int j = 0; j < PATCH_SZ; j++)
                DW[i*PATCH_SZ + j] = G_desc.at<float>(i, 0) * G_desc.at<float>(j, 0);
        }

        /* x与y方向上的 Harr小波,参数为4s */
        const int NX = 2, NY = 2;
        const int dx_s[NX][5] = { { 0, 0, 2, 4, -1 }, { 2, 0, 4, 4, 1 } };
        const int dy_s[NY][5] = { { 0, 0, 4, 2, 1 }, { 0, 2, 4, 4, -1 } };

        float X[nOriSampleBound], Y[nOriSampleBound], angle[nOriSampleBound]; // 用于计算特生点主方向
        uchar PATCH[PATCH_SZ + 1][PATCH_SZ + 1];
        float DX[PATCH_SZ][PATCH_SZ], DY[PATCH_SZ][PATCH_SZ]; // 20s * 20s区域的 梯度值
        CvMat matX = cvMat(1, nOriSampleBound, CV_32F, X);
        CvMat matY = cvMat(1, nOriSampleBound, CV_32F, Y);
        CvMat _angle = cvMat(1, nOriSampleBound, CV_32F, angle);
        Mat _patch(PATCH_SZ + 1, PATCH_SZ + 1, CV_8U, PATCH);

        int dsize = extended ? 128 : 64;

        int k, k1 = 0, k2 = (int)(*keypoints).size();// k2为Harr小波的 模板尺寸
        float maxSize = 0;
        for (k = k1; k < k2; k++)
        {
            maxSize = std::max(maxSize, (*keypoints)[k].size);
        }
        // maxSize*1.2/9 表示最大的尺度 s
        int imaxSize = std::max(cvCeil((PATCH_SZ + 1)*maxSize*1.2f / 9.0f), 1);
        Ptr<CvMat> winbuf = cvCreateMat(1, imaxSize*imaxSize, CV_8U);
        for (k = k1; k < k2; k++)
        {
            int i, j, kk, nangle;
            float* vec;
            SurfHF dx_t[NX], dy_t[NY];
            KeyPoint& kp = (*keypoints)[k];
            float size = kp.size;
            Point2f center = kp.pt;
            /* s是当前层的尺度参数 1.2是第一层的参数,9是第一层的模板大小*/
            float s = size*1.2f / 9.0f;
            /* grad_wav_size是 harr梯度模板的大小 边长为 4s */
            int grad_wav_size = 2 * cvRound(2 * s);
            if (sum->rows < grad_wav_size || sum->cols < grad_wav_size)
            {
                /* when grad_wav_size is too big,
                * the sampling of gradient will be meaningless
                * mark keypoint for deletion. */
                kp.size = -1;
                continue;
            }

            float descriptor_dir = 360.f - 90.f;
            if (upright == 0)
            {
                // 这一步 是计算梯度值,先将harr模板放大,再根据积分图计算,与前面求D_x,D_y一致类似
                resizeHaarPattern(dx_s, dx_t, NX, 4, grad_wav_size, sum->cols);
                resizeHaarPattern(dy_s, dy_t, NY, 4, grad_wav_size, sum->cols);
                for (kk = 0, nangle = 0; kk < nOriSamples; kk++)
                {
                    int x = cvRound(center.x + apt[kk].x*s - (float)(grad_wav_size - 1) / 2);
                    int y = cvRound(center.y + apt[kk].y*s - (float)(grad_wav_size - 1) / 2);
                    if (y < 0 || y >= sum->rows - grad_wav_size ||
                        x < 0 || x >= sum->cols - grad_wav_size)
                        continue;
                    const int* ptr = &sum->at<int>(y, x);
                    float vx = calcHaarPattern(ptr, dx_t, 2);
                    float vy = calcHaarPattern(ptr, dy_t, 2);
                    X[nangle] = vx*aptw[kk];
                    Y[nangle] = vy*aptw[kk];
                    nangle++;
                }
                if (nangle == 0)
                {
                    // No gradient could be sampled because the keypoint is too
                    // near too one or more of the sides of the image. As we
                    // therefore cannot find a dominant direction, we skip this
                    // keypoint and mark it for later deletion from the sequence.
                    kp.size = -1;
                    continue;
                }
                matX.cols = matY.cols = _angle.cols = nangle;
                // 计算邻域内每个点的 梯度角度
                cvCartToPolar(&matX, &matY, 0, &_angle, 1);

                float bestx = 0, besty = 0, descriptor_mod = 0;
                for (i = 0; i < 360; i += SURF_ORI_SEARCH_INC) // SURF_ORI_SEARCH_INC 为扇形区域扫描的步长
                {
                    float sumx = 0, sumy = 0, temp_mod;
                    for (j = 0; j < nangle; j++)
                    {
                        // d是 分析到的那个点与 现在主方向的偏度
                        int d = std::abs(cvRound(angle[j]) - i);
                        if (d < ORI_WIN / 2 || d > 360 - ORI_WIN / 2)
                        {
                            sumx += X[j];
                            sumy += Y[j];
                        }
                    }
                    temp_mod = sumx*sumx + sumy*sumy;
                    // descriptor_mod 是最大峰值
                    if (temp_mod > descriptor_mod)
                    {
                        descriptor_mod = temp_mod;
                        bestx = sumx;
                        besty = sumy;
                    }
                }
                descriptor_dir = fastAtan2(-besty, bestx);
            }
            kp.angle = descriptor_dir;
            if (!descriptors || !descriptors->data)
                continue;

            /* 用特征点周围20*s为边长的邻域 计算特征描述子 */
            int win_size = (int)((PATCH_SZ + 1)*s);
            CV_Assert(winbuf->cols >= win_size*win_size);
            Mat win(win_size, win_size, CV_8U, winbuf->data.ptr);

            if (!upright)
            {
                descriptor_dir *= (float)(CV_PI / 180); // 特征点的主方向 弧度值
                float sin_dir = -std::sin(descriptor_dir); //  - sin dir
                float cos_dir = std::cos(descriptor_dir);

                float win_offset = -(float)(win_size - 1) / 2;
                float start_x = center.x + win_offset*cos_dir + win_offset*sin_dir;
                float start_y = center.y - win_offset*sin_dir + win_offset*cos_dir;
                uchar* WIN = win.data;

                int ncols1 = img->cols - 1, nrows1 = img->rows - 1;
                size_t imgstep = img->step;
                for (i = 0; i < win_size; i++, start_x += sin_dir, start_y += cos_dir)
                {
                    double pixel_x = start_x;
                    double pixel_y = start_y;
                    for (j = 0; j < win_size; j++, pixel_x += cos_dir, pixel_y -= sin_dir)
                    {
                        int ix = cvFloor(pixel_x), iy = cvFloor(pixel_y);
                        if ((unsigned)ix < (unsigned)ncols1 &&
                            (unsigned)iy < (unsigned)nrows1)
                        {
                            float a = (float)(pixel_x - ix), b = (float)(pixel_y - iy);
                            const uchar* imgptr = &img->at<uchar>(iy, ix);
                            WIN[i*win_size + j] = (uchar)
                                cvRound(imgptr[0] * (1.f - a)*(1.f - b) +
                                imgptr[1] * a*(1.f - b) +
                                imgptr[imgstep] * (1.f - a)*b +
                                imgptr[imgstep + 1] * a*b);
                        }
                        else
                        {
                            int x = std::min(std::max(cvRound(pixel_x), 0), ncols1);
                            int y = std::min(std::max(cvRound(pixel_y), 0), nrows1);
                            WIN[i*win_size + j] = img->at<uchar>(y, x);
                        }
                    }
                }
            }
            else
            {

                float win_offset = -(float)(win_size - 1) / 2;
                int start_x = cvRound(center.x + win_offset);
                int start_y = cvRound(center.y - win_offset);
                uchar* WIN = win.data;
                for (i = 0; i < win_size; i++, start_x++)
                {
                    int pixel_x = start_x;
                    int pixel_y = start_y;
                    for (j = 0; j < win_size; j++, pixel_y--)
                    {
                        int x = MAX(pixel_x, 0);
                        int y = MAX(pixel_y, 0);
                        x = MIN(x, img->cols - 1);
                        y = MIN(y, img->rows - 1);
                        WIN[i*win_size + j] = img->at<uchar>(y, x);
                    }
                }
            }
            // Scale the window to size PATCH_SZ so each pixel's size is s. This
            // makes calculating the gradients with wavelets of size 2s easy
            resize(win, _patch, _patch.size(), 0, 0, INTER_AREA);

            // Calculate gradients in x and y with wavelets of size 2s
            for (i = 0; i < PATCH_SZ; i++)
            for (j = 0; j < PATCH_SZ; j++)
            {
                float dw = DW[i*PATCH_SZ + j]; // 高斯加权系数
                float vx = (PATCH[i][j + 1] - PATCH[i][j] + PATCH[i + 1][j + 1] - PATCH[i + 1][j])*dw;
                float vy = (PATCH[i + 1][j] - PATCH[i][j] + PATCH[i + 1][j + 1] - PATCH[i][j + 1])*dw;
                DX[i][j] = vx;
                DY[i][j] = vy;
            }

            // Construct the descriptor
            vec = descriptors->ptr<float>(k);
            for (kk = 0; kk < dsize; kk++)
                vec[kk] = 0;
            double square_mag = 0;
            if (extended)
            {
                // 128维描述子,考虑dx与dy的正负号
                for (i = 0; i < 4; i++)
                for (j = 0; j < 4; j++)
                {
                    // 每个方块内是一个5s * 5s的区域,每个方法由8个特征描述
                    for (int y = i * 5; y < i * 5 + 5; y++)
                    {
                        for (int x = j * 5; x < j * 5 + 5; x++)
                        {
                            float tx = DX[y][x], ty = DY[y][x];
                            if (ty >= 0)
                            {
                                vec[0] += tx;
                                vec[1] += (float)fabs(tx);
                            }
                            else {
                                vec[2] += tx;
                                vec[3] += (float)fabs(tx);
                            }
                            if (tx >= 0)
                            {
                                vec[4] += ty;
                                vec[5] += (float)fabs(ty);
                            }
                            else {
                                vec[6] += ty;
                                vec[7] += (float)fabs(ty);
                            }
                        }
                    }
                    for (kk = 0; kk < 8; kk++)
                        square_mag += vec[kk] * vec[kk];
                    vec += 8;
                }
            }
            else
            {
                // 64位描述子
                for (i = 0; i < 4; i++)
                for (j = 0; j < 4; j++)
                {
                    for (int y = i * 5; y < i * 5 + 5; y++)
                    {
                        for (int x = j * 5; x < j * 5 + 5; x++)
                        {
                            float tx = DX[y][x], ty = DY[y][x];
                            vec[0] += tx; vec[1] += ty;
                            vec[2] += (float)fabs(tx); vec[3] += (float)fabs(ty);
                        }
                    }
                    for (kk = 0; kk < 4; kk++)
                        square_mag += vec[kk] * vec[kk];
                    vec += 4;
                }
            }
            // 归一化 描述子 以满足 光照不变性
            vec = descriptors->ptr<float>(k);
            float scale = (float)(1. / (sqrt(square_mag) + DBL_EPSILON));
            for (kk = 0; kk < dsize; kk++)
                vec[kk] *= scale;
        }
    }
};

 

 

 

参考:

 https://blog.csdn.net/songzitea/article/details/16986423

https://blog.csdn.net/msq19895070/article/details/24290599

https://wenku.baidu.com/view/d71c9d2e905f804d2b160b4e767f5acfa1c783b4.html?rec_flag=default

https://blog.csdn.net/qq_37764129/article/details/80969515

posted @ 2020-03-23 19:03  无趣的鱼  阅读(2638)  评论(0编辑  收藏  举报