FROM: http://www.cnblogs.com/ronny/p/4048213.html

上一篇文章 SURF算法与源码分析、上 中主要分析的是SURF特征点定位的算法原理与相关OpenCV中的源码分析,这篇文章接着上篇文章对已经定位到的SURF特征点进行特征描述。这一步至关重要,这是SURF特征点匹配的基础。总体来说算法思路和SIFT相似,只是每一步都做了不同程度的近似与简化,提高了效率。

1. SURF特征点方向分配

为了保证特征矢量具有旋转不变性,与SIFT特征一样,需要对每个特征点分配一个主方向。为些,我们需要以特征点为中心,以6s(s=1.2∗L/9为特征点的尺度)为半径的圆形区域,对图像进行Haar小波响应运算。这样做实际就是对图像进行梯度运算只不过是我们需要利用积分图像,提高计算图像梯度的效率。在SIFT特征描述子中我们在求取特征点主方向时,以是特征点为中心,在以4.5σ为半径的邻域内计算梯度方向直方图。事实上,两种方法在求取特征点主方向时,考虑到Haar小波的模板带宽,实际计算梯度的图像区域是相同的。用于计算梯度的Harr小波的尺度为4s。

与SIFT类似,使用σ=2s的高斯加权函数对Harr小波的响应值进行高斯加权。为了求取主方向值,需要设计一个以特征点为中心,张角为π/3的扇形滑动窗口。以步长为0.2弧度左右,旋转这个滑动窗口,并对滑动窗口内的图像Harr小波响应值dx、dy进行累加,得到一个矢量(mw,θw):

mw=∑wdx+∑wdy
θw=arctan(∑wdx/∑wdy)

主方向为最大Harr响应累加值所对应的方向,也就是最长矢量所对应的方向,即

θ=θw|max{mw}

可以依照SIFT求方方向时策略,当存在另一个相当于主峰值80%能量的峰值时,则将这个方向认为是该特征点的辅方向。一个特征点可能会被指定具有多个方向(一个主方向,一个以上辅方向),这可以增强匹配的鲁棒性。和SIFT的描述子类似,如果在mw中出现另一个大于主峰能量max{mw}80时的次峰,可以将该特征点复制成两个特征点。一个主的方向为最大响应能量所对应的方向,另一个主方向为次大响应能量所对应的方向。

图 1  求取主方向时扇形滑动窗口围绕特征点转动,统计Haar小波响应值,并计算方向角

2. 特征点特征矢量生成

生成特征点描述子与确定特征点方向有些类似,它需要计算图像的Haar小波响应。不过,与主方向的确定不同的是,这次我们不是使用一个圆形区域,而是在一个矩形区域来计算Haar小波响应。以特征点为中心,沿上一节讨论得到的主方向,沿主方向将s20s×20s的图像划分为4×4个子块,每个子块利用尺寸2s的Harr模板进行响应值进行响应值计算,然后对响应值进行统计∑dx、∑|dx|、∑dy、∑|dy|形成特征矢量。如下图2所示。图中,以特征点为中心,以20s为边长的矩形窗口为特征描述子计算使用的窗口,特征点到矩形边框的线段表示特征点的主方向。

图2 特征描述子表示

将20s的窗口划分成4×4子窗口,每个子窗口有5s×5s个像素。使用尺寸为2s的Harr小波对子窗口图像进行其响应值计算,共进行25次采样,分别得到沿主方向的dy和垂直于主方向的dx。然后,以特征点为中心,对dy和dx进行高斯加权计算,高斯核的参数为σ=3.3s(即20s/6)。最后,分别对每个子块的响应值进行统计,得到每个子块的矢量:

V子块=[∑dx,∑|dx|,∑dy,∑|dy|]

由于共有4×4个子块,因此,特征描述子共由4×4×4=64维特征矢量组成。SURF描述子不仅具有尺度和旋转不变性,而且对光照的变化也具有不变性。使小波响应本身就具有亮度不变性,而对比度的不变性则是通过将特征矢量进行归一化来实现。图3 给出了三种不同图像模式的子块得到的不同结果。对于实际图像的描述子,我们可以认为它们是由这三种不同模式图像的描述子组合而成的。

图3 不同的图像密度模式得到的不同的描述子结果

为了充分利用积分图像进行Haar小波的响应计算,我们并不直接旋转Haar小波模板求得其响应值,而是在积图像上先使用水平和垂直的Haar模板求得响应值dy和dx,然后根据主方向旋转dx和dy与主方向操持一致,如下图4所示。为了求得旋转后Haar小波响应值,首先要得到旋转前图像的位置。旋转前后图偈的位置关系,可以通过点的旋转公式得到:

x=x0–j×scale×sin(θ)+i×scale×cos(θ)
y=y0–j×scale×cos(θ)+i×scale×sin(θ)

在得到点(j,i)在旋转前对应积分图像的位置(x,y)后,利用积分图像与水平、垂直Harr小波,求得水平与垂直两个方向的响应值dx和dy。对dx和dy进行高斯加权处理,并根据主方向的角度,对dx和dy进行旋转变换,从而,得到旋转后的dx’和dy’。其计算公式如下:

dx′=w(−dx×sin(θ)+dy×cos(θ))
dy′=w(−dx×cos(θ)+dy×sin(θ))

图4 利用积分图像进行Haar小波响应计算示意图,左边为旋转后的图像,右边为旋转前的图像

3. 特征描述子的维数

一般而言,特征矢量的长度越长,特征矢量所承载的信息量就越大,特征描述子的独特性就越好,但匹配时所付出的时间代价就越大。对于SURF描述子,可以将它扩展到用128维矢量来表示。具体方法是在求∑dx、∑|dx|时区分dy<0和dy≥0情况。同时,在求取∑dy、∑|dy|时区分dx<0和dx≥0情况。这样,每个子块就产生了8个梯度统计值,从而使描述子特征矢量的长度增加到8×4×4=128维。

为了实现快速匹配,SURF在特征矢量中增加了一个新的变量,即特征点的拉普拉斯响应正负号。在特征点检测时,将Hessian矩阵的迹的正负号记录下来,作为特征矢量中的一个变量。这样做并不增加运算量,因为特征点检测进已经对Hessian矩阵的迹进行了计算。在特征匹配时,这个变量可以有效地节省搜索的时间,因为只有两个具有相同正负号的特征点才有可能匹配,对于正负号不同的特征点就不进行相似性计算。

简单地说,我们可以根据特征点的响应值符号,将特征点分成两组,一组是具有拉普拉斯正响应的特征点,一组是具有拉普拉斯负响应的特征点,匹配时,只有符号相同组中的特征点才能进行相互匹配。显然,这样可以节省特征点匹配的时间。如下图5所示。

图5 黑背景下的亮斑和白背景下的黑斑 因为它们的拉普拉斯响应正负号不同,不会对它们进行匹配

4. 源码解析

特征点描述子的生成这一部分的代码主要是通过SURFInvoker这个类来实现。在主流程中,通过一个parallel_for_()函数来并发计算。

struct SURFInvoker
{enum{ORI_RADIUS = 6, ORI_WIN = 60, PATCH_SZ = 20};// Parametersconst Mat* img;const Mat* sum;vector<KeyPoint>* keypoints;Mat* descriptors;bool extended;bool upright;// Pre-calculated valuesint 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.5nOriSamples = 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 表示最大的尺度 sint 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 dirfloat 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 easyresize(win, _patch, _patch.size(), 0, 0, INTER_AREA);// Calculate gradients in x and y with wavelets of size 2sfor (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 descriptorvec = 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;}}
};

5. 总结

实际上有文献指出,SURF比SIFT工作更出色。他们认为主要是因为SURF在求取描述子特征矢量时,是对一个子块的梯度信息进行求和,而SIFT则是依靠单个像素梯度的方向。

SURF算法与源码分析、下相关推荐

  1. SURF算法与源码分析、上

    FROM:http://www.cnblogs.com/ronny/p/4045979.html 如果说SIFT算法中使用DOG对LOG进行了简化,提高了搜索特征点的速度,那么SURF算法则是对DoH ...

  2. Unidbg-Linker部分源码分析(下)

    Unidbg-Linker部分源码分析(下) Unidbg-Linker部分源码分析(下) 概要 align resolveLibrary VirtualModule initFunctions ca ...

  3. SpringCloudAlibaba注册中心与配置中心之利器Nacos实战与源码分析(下)

    源码资料 文档资料 <<Nacos架构与原理>>书籍于2021.12.21发布,并在Nacos官方网站非常Nice的提供其电子书的下载.我们学习Nacos源码更多是要吸取其优秀 ...

  4. 知识小罐头07(tomcat8请求源码分析 下)

    感觉最近想偷懒了,哎,强迫自己也要写点东西,偷懒可是会上瘾的,嘿嘿!一有写博客的想法要赶紧行动起来,养成良好的习惯. ok,继续上一篇所说的一些东西,上一篇说到Connector包装了那两个对象,最后 ...

  5. 7zip核心算法LZMA源码分析心得

    7zip核心算法LZMA分析心得 最近有空就研究了一下DEFLATE的LZ77压缩算法实现及7zip的LZMA压缩算法实现,现在记下相关心得如下: 一. DEFLATE中的LZ77算法实现比较简单,具 ...

  6. Linux驱动修炼之道-SPI驱动框架源码分析(上)

    Linux驱动修炼之道-SPI驱动框架源码分析(上)   SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设备可工作于m/s模式.主设备发起数据帧,允许多个从设 ...

  7. Spring AOP源码分析(七)ProxyFactoryBean介绍

    2019独角兽企业重金招聘Python工程师标准>>> 这篇文章里面就要说说Spring自己的AOP,搞清楚哪种方式是Spring自己实现的AOP,哪种方式是Spring引入aspe ...

  8. SPI驱动框架源码分析

     SPI驱动框架源码分析 2013-04-12 16:13:08 分类: LINUX SPI驱动框架源码分析 SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设 ...

  9. SRS源码分析-rtmp转rtc流程

    前言 SRS4.0支持将RTMP流转换成RTC流,本文将结合源码分析下这个过程. 配置 首先,需要在SRS4.0的启动配置文件里面开启RTC Server和RTC 能力,可以参考官方提供的配置文件./ ...

最新文章

  1. 多个class相同的input标签 获取当前值!方法!
  2. vue中如何使用mockjs摸拟接口的各种数据
  3. WinForm 捕获最小化事件
  4. Linux tar vi gcc 指令
  5. @Value和Hibernate问题
  6. python进阶15变量作用域LEGB
  7. kubenetes 1.4 修改kubelet启动参数修改方法
  8. Apache2.4项目配置PHP/TP项目方法
  9. struts2+spring+hibernte整合示例
  10. 一步一步教你如何安装Dart
  11. linux popen阻塞_linux popen()与system()的区别
  12. Linux的shell中echo改变输出显示样式
  13. web使用js调用摄像头扫码、拍照、录像
  14. [置顶]       cocos2d-x2.2.5走四棋儿源码“开源”
  15. 将安卓手机摄像头打造成电脑高清摄像头
  16. PHP从入门到精通学习路线图
  17. matlab函数power,Matlab中Powergui介绍.pdf
  18. 新手学Unity3d的一些网站及相应学习路线
  19. 一文教你用 Neo4j 快速构建明星关系图谱
  20. 相位展开(phase unwrapping)

热门文章

  1. sqlserver中判断表或临时表是否存在
  2. -jar参数运行应用时classpath的设置方法
  3. DPDK pci驱动探测(十八)
  4. linux shell写服务,Linux shell编写系统服务脚本
  5. xampp去运行php文件_从0开始构建一个属于你自己的PHP框架
  6. 浅谈JavaScript错误
  7. Lucene第一篇【介绍Lucene、快速入门】
  8. 20162329 2017-2018-1 《程序设计与数据结构》第九周学习总结
  9. 运行webpack-dev-srerver 端口占用错误及解决办法
  10. IDE神器intellij idea的基本使用