东方红B类题目中必然存在物体识别的部分,这部分往往是使用图像处理来解决,这篇文主要记载和分享个人在学习用opencv4处理数字图像时的卷积的应用。

卷积是什么

学习过《信号与系统》的友友对卷积都不陌生,在处理信号时的卷积是在滑动的时间窗口里两个信号相乘再求和,起到滤波的作用。在数字图像处理里卷积其实也是相乘再相加。

像下面图片演示的那样,3*3的卷积核从左到右,从上到下一个一个像素做卷积:卷积核和其覆盖的图像像素对应相乘后相加,最后再乘1/9,赋值给中间那个像素(锚点),这样就可以得到一张新的图像。但是当边缘的像素作为要处理的像素时,其周围不够9个像素,此时需要对其周围的像素进行填补在进行卷积,填补的方式也有多种,后面详细讲。卷积在数字图像中有四大应用,分别是:模糊、梯度、边缘、锐化。

卷积时边缘像素填补方式

  • BORDER_CONSTANT               cccc|123456|cccc

这种方式是在图像边缘填充固定的像素c,如上面演示的那样“|”里面的数字代表要处理的图像的像素“|”外的c就是填充的常数c了。

  • BORDER_REPLICATE               1111|123456|6666

这第二种方式,也很好理解,便是将边缘的像素复制过去。

  • BORDER_WRAP                     3256|123456|1234

第三种方式是将对边的边缘像素作为填充像素进行填充。

  • BORDER_REFLECT_101/BORDER_DEFAULT                5432|123456|2345

第四种也是常用的,以边缘的像素点作为对称点,进行对称填充。

知道了卷积是什么,接下来就是卷积的应用。其实,卷积的不同应用无非就是卷积核的不同罢了。

模糊

均值模糊

均值模糊,也称均值滤波。其实就是本文一开始举例的那种卷积核,内部元素都为1,再乘1/(宽*高)。

opencv4中提供了下面这个函数可以调用实现均值模糊,第一参数是要处理的图像,第二个是处理后输出的图像,第三个是卷积核的大小,第四个是锚点位置,第五个是边缘填充的方式。

其中锚点就是卷积后结果存放的位置,如果为负数就是默认的中心点,若卷积核为3*3的(0,0)是左上点(2, 2)是右下点。

void cv::blur	(InputArray src,OutputArray dst,Size ksize,Point anchor = Point(-1,-1),int borderType = BORDER_DEFAULT );

其卷积核3*3,要处理的图像是src,输出是dst,使用实例为

blur(src, dst, Size(3,3), Point(-1,-1), BORDER_DEFAULT );

高斯模糊

高斯模糊是将卷积核里的元素加以高斯分布的权重进行赋值。

这是一维中的高斯分布

这是二维中的高斯分布

二维高斯分布像素化

同样的高斯模糊opencv4也给提供的封装好的函数:

void cv::GaussianBlur(InputArray src,OutputArray dst,Size ksize,double sigmaX,double sigmaY = 0,int borderType = BORDER_DEFAULT )

其中sigmax和sigmay参数是卷积核分别在x和y方向上的标准差。其他参数在均值模糊有讲,这里不再赘述。

同样的以3*3的卷积核为例,因为高斯卷积核的元素取决于卷积核的大小和标准差,且卷积核大小和标准差可以换算,因此这两个参数只要输入一个即可,函数内部会相互计算。

而且当size和sigmax同时有输入时函数会以size为准,只有当size为(0,0)时,函数才会以sigmax为准。

GaussianBlur(src,dst,Size(3,3),0 );

盒子模糊

盒子模糊顾名思义,卷积核如同盒子一样可以变化为矩形,他也是均值模糊的一种但是和blur()函数不同之处在于其卷积核可以不是正方形。opencv4提供的接口:

void cv::boxFilter(InputArray src,OutputArray dst,int ddepth,Size ksize,Point anchor = Point(-1,-1),bool normalize = true,int borderType = BORDER_DEFAULT )

ddepth是图像深度,有CV_32F和CV_8U等可选,其意义为一个像素的数据类型为32位浮点型或8位无符号型等等,当输入为负数时,其深度与源图像一致。normalize是使能卷积核内部元素的归一化,归一化后可以使元素权重和为1,这样可以避免输出图像溢出或亮度过大等。

以一个矩形卷积核为例的使用案例:

boxFilter(src,dst,-1,Size(1,3));

当卷积核(1,3)时可以将图像作出上下晃动感;当卷积核(3,1)时可以将图像作出左右晃动感;

自定义模糊(自定义滤波)

与前面的模糊滤波最大的不同便在于可以自定义任意的卷积核。opencv4提供了一个函数接口可以快速实现:

void cv::filter2D(InputArray src,OutputArray dst,int ddepth,InputArray kernel,Point anchor = Point(-1,-1),double delta = 0,int borderType = BORDER_DEFAULT )	

其中kernel即是自定义的卷积核了,其类型应该为mat型。delta参数是加在输出图像像素上的常数,可以增加亮度。其他参数前面已经有介绍过,这里不加赘述。

下面展示一个使用例子:

void arbFilter(InputArray src,OutputArray dst)
{
  //自定义卷积核
  Mat rob = (Mat_<int>(2,2)<<-1,0,0,1);
  filter2D(src,dst,CV_32F,rob,Point anchor = Point(-1,-1),double delta = 0);
  convertScaleAbs(dst,dst);
}
	

梯度

图像的梯度就像函数中的导数,可以求得像素的变化率,往往用来检测图像边缘。常用的以下三个算子:

 sobel算子       

scharr算子                 

 robert算子                               

分别求得x和y方向的梯度后,可经过两种不同的梯度计算方式L1和L2

L2:           L1:

以下分别有三个算子计算梯度的例子:

robert算子梯度计算

void robotGrad(InputArray src,OutputArray dst,double delta)
{
  Mat grad_x,grad_y;
  //自定义robot卷积核
  Mat robX = (Mat_<int>(2,2)<<1,0,0,-1);
  Mat robY = (Mat_<int>(2,2)<<0,1,-1,0);
  filter2D(src, grad_x, CV_32F, robX, Point(-1,-1), delta);
  filter2D(src, grad_y, CV_32F, robY, Point(-1,-1), delta);
  convertScaleAbs(grad_x,grad_x);
  convertScaleAbs(grad_y,grad_y);
  add(grad_x, grad_y, dst);
}
	

sobel算子梯度计算

sobel算子计算梯度opencv4提供了一个接口:

void cv::Sobel(InputArray src,OutputArray dst,int ddepth,int dx,int dy,int ksize = 3,double scale = 1,double delta = 0,int borderType = BORDER_DEFAULT)
复制

dx和dy参数是使能sobel算子的Gx和Gy的。例子中还用到addWeighted(),此函数用于将两张图像加权相加,避免过亮。

void sobelGrad(InputArray src,OutputArray dst,double delta)
{
  Mat grad_x,grad_y;
  //sobel算子分别计算x、y方向梯度
  Sobel(src, grad_x, CV_32F, 1,0);
  Sobel(src, grad_y, CV_32F, 0,1);
  convertScaleAbs(grad_x,grad_x);
  convertScaleAbs(grad_y,grad_y);
  addWeighted(grad_x, 0.5, grad_y, 0.5, 0, dst);
}
	

scharr算子梯度计算

scharr与sobel计算是很相似的,不同仅是scharr算子比sobel算子更能将图像细小的边缘显示出来。

void scharrGrad(InputArray src,OutputArray dst,double delta)
{
  Mat grad_x,grad_y;
  //sobel算子分别计算x、y方向梯度
  Scharr(src, grad_x, CV_32F, 1,0);
  Scharr(src, grad_y, CV_32F, 0,1);
  convertScaleAbs(grad_x,grad_x);
  convertScaleAbs(grad_y,grad_y);
  addWeighted(grad_x, 0.5, grad_y, 0.5, 0, dst);
}

边缘

卷积在图像边缘的应用,一般是使用特殊的卷积核去和图像进行卷积获取图像的边缘。这里用到的算子叫做拉普拉斯算子

拉普拉斯算子是二阶导数的思想得来,但我个人认为将其想象成是x和y方向的变化率得来的更加具体和易于理解。opencv有提供一下接口可以快速实现拉普拉斯算子滤波发现边缘。拉普拉斯也有很大的缺点:对噪声敏感。

void cv::Laplacian(InputArray src,OutputArray dst,int ddepth,int ksize = 1,double scale = 1,double delta = 0,int borderType = BORDER_DEFAULT)

参数ksize必须是奇数,其意义是拉普拉斯计算梯度过程取像素的范围,反映在图像上就是提取边缘的敏感度。

锐化

锐化可以使用一下算子对图像进行自定义滤波得到。观察这个算子可以发现是“原图-拉普拉斯变化图”的计算算子,就像在原图上加上经过边缘提取的拉普拉斯算子处理后的图像,如此达到锐化的效果。

依据上面的思想,既然拉普拉斯算子对噪声敏感,那么我们将原图模糊降噪后再和拉普拉斯算子边缘提取后的图像叠加不就可以克服拉普拉斯算子的缺点了。其代码例子如下:

void unsharpmask(InputArray src,OutputArray dst)
{
  Mat blur_im,lap_im;
  //将原图去噪
  boxFilter(src,blur_im,-1,Size(3,3));
  //获取边缘
  Laplacian(src,lap_im,-1,1);
  addWeighted(blur_im, 1, lap_im, -1, dst);
}