卷积是本章所讨论的很多转换的基础。

先看看效果:opencv图像处理8-卷积

抽象的说,这个术语意味着我们对图像的每一个部分所做的操作。从这个意义上讲,我们在第五章所看到

的许多操作可以被理解成普通卷积的特殊情况。一个特殊的卷积所实现的功能是由所用的卷积核的形式决定的。这个核本质上是一个大小固定,

由数值参数构成的数组,数组的标定点通常位于数组的中心。数组的大小被称为核支撑。单就技术而言,核支撑实际上仅仅由核数组的非零部分

组成。
图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;  
}