卷积是本章所讨论的很多转换的基础。
先看看效果:
抽象的说,这个术语意味着我们对图像的每一个部分所做的操作。从这个意义上讲,我们在第五章所看到
的许多操作可以被理解成普通卷积的特殊情况。一个特殊的卷积所实现的功能是由所用的卷积核的形式决定的。这个核本质上是一个大小固定,
由数值参数构成的数组,数组的标定点通常位于数组的中心。数组的大小被称为核支撑。单就技术而言,核支撑实际上仅仅由核数组的非零部分
组成。
图6-1描述了以数组中心为定标点的3×3卷积核。若要计算一个特定点的卷积值,首先将核的标定点定位到图像的第一个像素点,核的其余元素覆
盖图像中其相对应的局部像素点。对于每一个核点,我们可以得到这个点的核的值以及图像中相应图像点的值。将这些值相乘并求和,并将这个
结果放置在与输入图像标定点所相对应的位置。通过在整个图像上扫描卷积核,对图像的每个点重复此操作。
当然我们可以用方程来表示这个过程,如果我们定义图像为I(x,y),核为G(i,j) (其中 0 < i < Mi –1 和 0 < j < Mj –1),标定点位于相
应核的(ai,aj)坐标上,则卷积H(x,y)定义为:
注意到运算次数,至少第一眼看似乎等于图像的像素数乘以核的像素数[63]。这需要很大的计算量并且也不是仅仅用其中的一些for循环以及许多
指针再分配就能做的事情。类似这种情况,你最好让OpenCV来做这个工作以利用OpenCV已编程实现的最优方法。其函数为cvFilter2D ();
[63]这里我们说“第一眼看”的意思是在频域中也可能进行卷积操作。在这种情况下,对于一个N×N的图像和一个M×M的核(N>M),计算复杂度
将会按照N2 log(N)成比例增加,而不是在空间域内预计的N2M2。这是因为频域的计算量同核的大小是相对独立的,对于大核更加有效。OpenCV会
根据核的大小自动决定是否做频域内的卷积。
void cvFilter2D(
const CvArr* src,
CvArr* dst,
const CvMat* kernel,
CvPoint anchor = cvPoint(-1,-1)
);
这里我们创建一个适当大小的矩阵,将系数连同原图像和目标图像一起传递给cvFilter2D()。我们还可以有选择地输入一个CvPoint指出核的中心
位置,但默认值(cvPoint(-1,-1))就会被认为是核的中心。如果定义了标定点,核的大小可以是任意偶数尺寸,否则大小就是奇数。
原图像src和目标图像dst大小应该是相同的,有些人可能认为考虑到卷积核的额外的长和宽,原图像src应该大于目标图像dst。但是在OpenCV里
原图像src和目标图像dst的大小是可以一样的,因为在默认情况下,在卷积之前,OpenCV通过复制原图像src的边界创建了虚拟像素,这样以便于
目标图像dst边界的像素可以被填充。复制是通过input(–dx, y) = input(0, y), input(w + dx, y) = input(w – 1, y)等实现的,还有一
些可以替换此默认行为的方法,我们将在下一节讨论。
提示一下,这里我们所讨论的卷积核的系数应该是浮点类型的,这就意味着我们必须用CV_32F来初始化矩阵。
做卷积时自然出现的一个问题是如何处理卷积边界。例如,在使用刚才所讨论的卷积核时,当卷积点在图像边缘时会发生什么?许多使用
cvFilter2D()的OpenCV内置函数必须用各种方式来解决这个问题。同样在你做卷积时,有必要知道如何有效解决这个问题。这个解决方法就是使
用cvCopyMakeBorder()函数,它可以将特定的图像轻微变大,然后以各种方式自动填充图像边界。
void cvCopyMakeBorder(
const CvArr* src,
CvArr* dst,
CvPoint offset,
int bordertype,
CvScalar value = cvScalarAll(0)
);
Offset变量告诉cvCopyMakeBorder()将原图像的副本放到目标图像中什么位置。典型情况是,如果核为N×N(N为奇数)时,那么边界在每一侧的
宽度都应是 (N – 1)/2,即这幅图像比原图像宽或高N – 1。在这种情况下,可以把Offset设置为cvPoint((N-1)/2,(N-1)/2),使得边界在每
一侧都是偶数。[64]
[64]当然,标定点在中心、N×N并且N是奇数时的情形是最简单的。在一般情况下,如果核是N×M并且标定点在(ax,ay),那么目标图像将比原图
像宽N-1,高M-1个像素。Offset的值仅仅是(ax,ay)。
Bordertype既可以是IPL_BORDER_CONSTANT,也可以是IPL_BORDER_REPLICATE(见图6-2)。在第一种情况下,value变量被认为是所有在边界的像
素应该设置的值。在第二种情况下,原始图像边缘的行和列被复制到大图像的边缘。注意到测试的模板图像边缘是比较精细的(注意图6-2右上角
的图像)。在测试的模板图像中,除了在圆图案边缘附近的像素变白外,有一个像素宽的黑色边界。这里定义了另外两种边界类型,
IPL_BORDER_REFLECT 和IPL_BORDER_WRAP,目前还没有被OpenCV所实现,但以后可能会在OpenCV中实现。
图6-2 扩大的图像边界,左边一列显示的是IPL_BORDER_CONSTANT,边界是用零值填充的,右面一列是IPL_BORDER_REPLICATE,在水平和垂直两
个方向复制边界像素。
我们在前面已经提到,当调用OpenCv库函数中的卷积功能时,cvCopyMakeBorder()函数就会被调用。在大多数情况下,边界类型为
IPL_BORDER_REPLICATE,但有时并不希望用它。所以在另一种场合,可能用到cvCopyMakeBorder()。你可以创造一幅具有比想要得到的边界稍微
大一些的图像,无论调用任何常规操作,接下来就可以剪切到对原图像所感兴趣的部分。这样一来, OpenCV的自动加边就不会影响所关心的像素。
注:高斯模板:
3*3的是
1 2 1
2 4 2
1 2 1
5*5的是
1 2 3 2 1
2 5 6 5 2
3 6 8 6 3
2 5 6 5 2
1 2 3 2 1
#include "cv.h" #include "highgui.h" #include <stdio.h> int main() { IplImage *src=0; IplImage*dst =0; IplImage*dst2=0; float k[9]={ 1,2,1, 2,4,2, 1,2,1}; //高斯卷积3*3的核 for(int i = 0 ; i< 9;i++){ k[i] = float(k[i]/16); } CvMat Km; Km = cvMat(3,3,CV_32F,k); float k2[25] = { 1 ,2, 3, 2, 1, 2 ,5 ,6 ,5, 2, 3 ,6 ,8 ,6, 3, 2 ,5 ,6 ,5, 2, 1 ,2 ,3 ,2, 1};//高斯卷积5*5的核 for(int j = 0 ; j<25; j++){ k2[j] = float(k2[j]/85); } CvMat Km2; Km2 = cvMat(5,5,CV_32F,k2); src=cvLoadImage("1.bmp",0);//Force to gray image dst=cvCloneImage(src); //使用cvCloneImage时,dst无需初始化,直接复制 dst2 = cvCloneImage(src); cvNamedWindow("src",CV_WINDOW_AUTOSIZE); cvNamedWindow("filter3*3",CV_WINDOW_AUTOSIZE); cvNamedWindow("filter5*5",CV_WINDOW_AUTOSIZE); cvShowImage("src",src); cvFilter2D(src,dst,&Km,cvPoint(-1,-1)); cvFilter2D(src, dst2, &Km2,cvPoint(-1,-1)); //卷积本身公式比较复杂,但是经过舍去高阶小量,简化后成为模板操作 cvShowImage("filter3*3",dst); cvShowImage("filter5*5", dst2); cvWaitKey(0); cvReleaseImage(&src); cvReleaseImage(&dst); cvReleaseImage(&dst2); cvDestroyAllWindows(); return 0; }