【转载】SURF算法源码分析(草稿)
原文:http://blog.sina.com.cn/s/blog_916b71bb0100vnxl.html
SURF是继SIFT之后很有影响力的算法。从作者论文的实验上看,速度比SIFT提高了5~10倍,准确度有几个百分点的提升。SURF算法的速度提升极其明显,个人认为这个是最大的卖点。速度提升的关键,就是使用了积分图。积分图是个好东西,最成熟的Haar特征人脸检测算法也是使用积分图。
OpenCV 1.1提供了SURF算法的实现,性能固然不错,只是实现的不太容易看懂。在开发工作中有个原则,就是不要写太复杂的代码,否则调试起来很麻烦。在这里,我选择的是OpenSURF实现,它的结构清晰,也是基于OpenCV。所有代码在目录src里。
1. main.cpp是主程序文件。
1.1. main函数的第1行:
if (PROCEDURE == 1) return mainImage();
它是从静态图像上抽取特征,我们从这里入手。
1.2. mainImage函数
1.2.1 IpVec ipts; IpVec的定义在ipoint.h里,typedef std::vector<Ipoint> IpVec; 也就是说,IpVec是一个vector向量,每个成员是一个Ipoint。Ipoint是一个类,也在ipoint.h里,也许是这个类很简单,作者将它按照结构体使用,都是public。
1.3. surfDetDes函数,它是SURF描述子特征提取实现函数了。
1.4. drawIpoints函数,它将SUURF描述子绘制到图像上。
2. surfDetDes函数,在surflib.h里。
它就是SURF算法的步骤,每行语句调用一个函数,简洁:
2.1 计算积分图
2.2 计算hessian矩阵
2.3 提取兴趣点
2.4 计算SURF描述子
2.5 释放积分图
3. Integral函数,计算积分图,函数原型在integral.h里,实现在integral.cpp里。OpenCV里提供计算积分图的函数,作者没使用。
3.1 计算积分图的第1排,对应代码在第33行开始的循环。实际上,这个代码还可以这么写:
i_data[0] = data[0];
for(int j=1; j<width; j++)
{
i_data[j] = (i_data[j-1] + data[j]);
}
3.2 从第40行开始,是计算其他排的积分图,实际上,代码还可以这么写:
for(int i=1; i<height; ++i)
{
i_data[i*step] = data[i*step] + i_data[(i-1)*step];
for(int j=1; j<width; ++j)
{
i_data[i*step+j] = data[i*step+j] + i_data[(i-1)*step+j];
}
}
4. FastHessian,计算hessian矩阵的类,它的定义在fasthessian.h里,实现在fasthessian.cpp里。
4.1 FastHessian有两个构造函数,这里用的是第2个,用图像构造。这个构造函数很简单,两条语句分别调用saveParameters,这两个函数很简单不用多说。
5. FastHessian.getIpoints(),提取兴趣点。这个函数在FastHessian.cpp中。
5.1 首先定义一个二维数组,filter_map,5排4列的默认值,并且给了初值。然后清空ipts。
5.2 buildResponseMap函数。这个函数里使用的参数,即每个octave对应的inteval的矩形窗的大小,完全按照论文里给的设置,非常之好。
5.2.1 responseMap的是一个向量,每个元素是一个指向ResponseLayer的指针。ResponseLayer定义在responselayer.h文件里。它定义了每个octave的每个inteval的高度宽度步长等参数。INIT_SAMPLE的值是2。
5.2.2 下面的for循环,在octaves=5的情况下会都执行,这样就将所有的ResponseLayer加入到responseMap。这种写法有意思,一种技巧。由于这若干个octave有可能使用相同的滤波器窗口尺寸,所以不需要每个都加入。然后,多次执行buildResponseLayer。
5.3 buildResponseLayer函数。
举例说明上述步骤。
假如传入的图像,是800*600像素,那么,i_height= 600, i_width = 800,octaves = 5,intervals = 4,init_sample = 2. init_sample是初始抽样步长。
那么,w = 400, h = 300, s = 2。
加入的第1层ResponseLayer的参数是400, 300, 2, 9。那么,在该层里,width = 400, height = 300, step = 2, filter = 9, responses是400*300的float指针,laplacian是400*300的无符号字符指针。
在buildResponseLayer函数里,对该层的计算步骤如下:
b=5,也就是说计算边界是5,也就是说,计算box的值,最小最小,也要从图像的第[5, 5]像素开始,否则不符合box的尺寸需求。l = 3, w = 9, inverse_area = 1/81。
下面的两重循环,ar表示行,ac表示列,index表示数值的存贮位置。r, c,分别表示所取的抽样点,在输入图像的行号和列号。那么,下面计算Dxx,Dyy和Dxy,就是根据在原图的r,c来计算的。注意,为了减少计算量,计算Dxx是计算两个白区+一个黑区,再减去3个黑区的方式进行的。Dyy也是类似。
关于Dyy的计算,我们以r = 100, c = 200为例进行说明:
BoxIntegral(img, r - b, c - l + 1, w, 2*l - 1)的参数,就成了
BoxIntegral(img, 95, 198, 9, 5)。在BoxIntegral函数里,r1 = 94, c1 = 197, r2 = 103, c2 = 202。也就是说,整个dyy的计算区域是9*5,高度是9,宽度是5.
而BoxIntegral(img, r - l / 2, c - l + 1, l, 2*l - 1),就成了
BoxIntegral(img, 99, 198, 3, 5)。
这也就是说,做计算对时候,filter的宽度是5,而高度是9,ffiler不是正方形。这一点,跟论文里不同,论文里用的是正方形的filter。
计算完了Dxx, Dyy, Dxy之后,需要根据filter的尺寸进行平均。
responses[index] = (Dxx * Dyy - 0.81f * Dxy * Dxy);因为前面不是用正方形的filter,这里的系数是0.81,不是论文里推荐的0.91。注意,Dxy的计算正负符号跟论文里恰好相反,但平方之后是一样的,不要紧。
至此,buildResponseMap()函数到此结束了。
5.5 下面的二重循环计算兴趣点。对每个octave,只计算i = 0和i = 1两种情况。对于t的,每行每列,计算它是否是极值,对应函数isExtremum。判断极值的条件是,1)是否越界,2)hessian行列式的值大于阈值,3)在一个矩形邻域内,如果存在大于该点行列式值的点,那么就不是极值。
5.6 如果是兴趣点,就根据插值计算精确坐标。这一块的算法,跟SIFT是类似的,不描述了。
6. Surf.getDescriptors函数,计算描述子
这个函数考虑两种情况,第1种情况就是假设图像没有倾斜,待检测的图像和原图像方向几乎是一样的,第2种情况,就是考虑图像有倾斜。我们这里分析第2种情况,因为搞定第2种就能搞定第1种。
6.1 getOrientation函数,计算兴趣点的主方向。它有两个循环,都是从-6开始的。实际上,当i=-6的时候,不会有计算。i = -5的时候,j = -3, -2, ..., 3,计7次,i = -4时,j = -4, -3, ..., 1, ...,4,计9次,i = -3时,j = -5, -4, ..., 5, 计11次, i = -2时,j = -5, ..., 5,计11次,i = -1是,j = -5, ..., 5,计11次,i =0时,j = -5,..., 5,计11次。也就是说,一共是(7+9+11+11+11)*2+11 = 109。也就是前面有vector的109个元素的由来。计算resX和resY是haar特征,这个计算也是根据积分图来的。注意,函数getAngle可以用函数atan2可以替代它,我想可能是作者没有用过这个函数,又重新实现了一遍。再后面,将2pi分成2pi/0.15份,对所有的关键点,都积累计算其所属的bin,然后找出最大值,它就是主方向。
6.2 Surf::getDescriptor函数,计算描述子。这里,i = [-12, -7, -2, 3],j = [-12, -7, -2, 3]。对于每一对的[i,j],都按照如下计算:首先,要计算xs和ys,然后计算sample_x和sample_y,就是以x,y为中心,将图像旋转到主方向,这样得到的sample_x和sample_y,就是将当前图上的样本点,旋转到主方向,这样,得到的坐标就是在主方向上的坐标,实际上,也就是相当于在原始图的积分图上计算haar特征,而xs和ys就是计算的开始点。
这里的逻辑是这样的:
对原始图像来说,计算积分图,计算兴趣点,计算主方向,计算描述子。这样,描述子在主方向上。
对匹配图像来说,计算积分图,计算兴趣点。假设图像旋转了,那么,这个主方向就跟提取原始图像的主方向是不同的。而且,这个时候的积分图,跟原始图像的积分图也是不一样的。因此,在着一块要多次旋转。如果,我们要根据变化之后的积分图,根据变化之后的主方向,计算出不变的描述子,应该怎么做呢?兴趣点的坐标得到了,那么,在它周围取若干个点,对这些点,旋转到主方向上,这时候,就可以计算在主方向上兴趣点在改变的积分图上的haar特征值,这些haar特征,再旋转到主方向上,那就是在主方向上的haar特征。无论怎么旋转,在兴趣点的主方向上的积分图一定是相同的,基于这一点,就可以得到旋转不变的描述子。每个兴趣点的邻域,是分成4*4的子邻域,在每个子邻域,都计算dx, dy, abs(dx), abs(dy),这样描述子的维度就是4*4*4=64,当然,最后会做一次归一化。
7. 到这里SURF的主要步骤就ok了,剩下的是辅助性的细节,将来慢慢再补上。