如何用OpenCV和Python实践卷积

前言

换一个角度来看的话,图像其实就是一个多维矩阵。不过跟你在学校里学到的传统矩阵不一样的是,图像还有深度,即图像中的通道数。一个标准的RGB图像有3个深度,分别为红、绿、蓝3个通道。

有了上述认识后,我们就不难理解模糊、锐化、边缘检测等这些基本的图像处理,其实就是图像这个大矩阵和一个小矩阵(内核)卷积的结果这一事实了。这个小矩阵或者说是内核从原始图像左上角开始,从左到右、从上到下依次滑过图像的每一个像素点,在每个像素点处都要进行卷积运算。

通常,我们会手动定义内核,这样我们可以获得各种自己想要的图像处理函数。你可能已经很熟悉模糊(平均平滑,高斯平滑,中值滤波等)、边缘检测(拉普拉斯,Sobel,,Scharr,Prewitt等)、以及锐化操作了,其实这些操作都是通过手动定义内核来实现的。

所以问题来了,有什么办法能自主学习这些各式各样的滤波器吗?能将这些滤波器用于图像分类和目标检测吗?

答案是肯定的。

但是在那之前,我们需要先了解一点内核和卷积的知识。

内核

让我们再次想象一下,原始图像是一个大矩阵,而内核是一个很小的矩阵(相对于原始图像):

convolutions_kernel_sliding

如上图所示,我们沿着原始图像,让内核从左到右、从上到下滑动。

在原始图像的每个(X,Y)坐标像素点处,内核中心点与该像素点重合,原始图像(X,Y)坐标周围与内核重合的像素点区域,与内核区域做卷积运算,得到一个输出值。这个输出值将被存储在输出图像中的(X,Y)坐标处。

如果你觉得这听起来有点乱,不用担心,稍后我将会举一个浅显易懂的例子,但在此之前,让我们先来看看什么是内核:

convolutions_kernel_example

上面我们定义了一个3×3的内核(猜猜这个内核可以用来做什么?)

内核可以是M×N的任意大小,但M和N必须是奇整数。

注意:很多常见的内核都是N×N大小的方形矩阵。

我们使用奇数尺寸的内核,是为了保证在图像的中心,(X,Y)坐标是有效的:

convolutions_kernel_sizes

上图中左边是一个3×3的矩阵。若矩阵的左上角为原点,且坐标是零索引的,那么,很显然,矩阵的中心位于(1,1)坐标处。而右边的2×2矩阵,其中心位于(0.5,0.5)处。但是,如我们所知,除非使用插值,否则不可能出现(0.5,0.5)这样的像素坐标,也就是说,我们的像素坐标必须是整数!这也就是为什么我们必须用奇数的内核尺寸的原因。

卷积

现在,我们已经基本了解了什么是内核,再来看看卷积吧。

在图像处理中,卷积需要三个元素:

  1. 一幅输入图像
  2. 一个内核矩阵(将与输入图像做卷积)
  3. 一幅输出图像(用于存储输入图像与内核卷积的结果)

卷积其实非常的简单。我们只需要:

  1. 从原始图像中选取一个(X,Y)坐标
  2. 将内核的中心点放到(X,Y)坐标处
  3. 输入图像中与内核重叠区域,与内核逐元素相乘,然后将这些乘法运算的值累加得到一个单一的值。这些乘积的总和称为内核输出
  4. 将步骤3获得的结果存储在输出图像中,存储位置与步骤1中的(X,Y)坐标相同

下面是一个卷积的例子,用之前定义的3 x 3大小内核对一幅图像的3 x 3区域进行模糊处理:

convolutions_example_01

因此:

convolutions_example_02

卷积之后,我们把输出结果 Oi,j = 126 存储到输出图像的(i,j)坐标处。

这一切就是这么简单!

卷积其实就是内核与输入图像中内核覆盖区域之间逐像素的乘积和。

用OpenCV和Python实践卷积

之前我们已经做了有关内核和卷积的有趣讨论。现在让我们去看一些代码,来了解一下内核和卷积是怎样被实现的。这些源代码也会对你了解卷积如何应用于图像处理有所帮助。

首先,打开一个新文件,将其命名为convolutions.py,写入以下内容:

1

第2-5行导入一些必要的Python包。你的系统上应该已经安装了NumPy和OpenCV,但你可能没有安装scikit-image。只需用pip就可完成安装:

1
$ pip install -U scikit-image

接下来,我们就可以定义卷积了

7

卷积函数convolve有两个输入参数:灰度图像image和内核kernel。

在第10和11行,分别获取了image和kernel的尺寸(宽和高)。

在继续之前,理解内核kernel滑动经过原始图像image的每一个像素点,逐点进行卷积运算,并将卷积结果存储到一幅输出图像相应位置的这一过程是很重要的。

为什么呢?

回想一下,我们之前是将内核矩阵的中心所在的位置设定为原始图像矩阵的中心,我们围绕这个中心对周围的像素进行卷积运算。这意味着图像边界处的像素没法进行卷积。对这些边缘点不进行卷积运算的结果就是输出图像会比输入图像小一圈,这对整幅图像来说当然有不好的影响,但有时侯这种影响恰恰是我们需要的,这只取决于你的具体应用场景罢了。

然而,在大多数情况下,我们希望输出图像与输入图像有一样的大小。为了保证这一点,我们采用填充处理(16-19行的padding)。这里,我们简单地复制输入图像边界的像素到输出图像,以使得输出图像匹配输入图像的尺寸。

其他的填充方法有零填充(边界全部用零填充,普遍用于构建卷积神经网络)和环绕(其中边界像素由图像取反后的结果决定)等。在大多数情况下,要么是重复要么就是零填充。

现在,我们就可以将卷积应用到我们的图像处理中去了。

21

24和25行遍历图像image,按着由左到右、从上到下的顺序,每次for循环只访问一个像素点。

第29行使用NumPy阵列提取图像中的感兴趣区域(ROI)。这个ROI的中心与图像中心(X,Y)坐标重合,从原图像image中提取和内核kernel一样大小区域,即为我们的ROI,接着执行下一步。

34行执行了卷积运算,在ROI和kernel之间逐像素相乘,接着累加。

38行将输出值k存储到输出矩阵output的(X,Y)坐标处(和输入图像相对应)。

到此为止,我们就已经完成我们的卷积运算了。

40

我们通常所处理的图像,其像素值均在[0,255]范围内。可一旦进行卷积运算后,像素值往往会超出范围。为了使我们的输出图像output像素值落回[0,255]范围内,我们可以借助scikit_image的rescale_intensity函数(41行)。同时,如42行所示,我们也将图像数据类型转变为8位无符号整型(之前为了方便对[0,255]范围之外的像素值进行处理,输出图像是浮点型的)。

最后在45行返回输出图像output给调用该函数的变量。

现在我们已经定义好了卷积函数,让我们再去看看脚本的驱动部分。我们先来解析一下命令行参数,然后定义几个将会应用到我们的图像处理中的内核,最后显示输出结果:

47

48-51行负责解析我们的命令行参数。这里,我们只需要一个参数,–image,这是输入路径。

然后我们继续看54和55行,54行定义了一个7×7的内核 smallBlur 用于对图像进行模糊平滑处理,55行定义了一个21×21内核 largeBlur 同样用于平滑图像。内核尺寸越大,经此处理后图像越模糊。仔细看这个大的内核,你会发现,应用该内核而得到的ROI几乎是输入区域的平均值。

58-61行,我们定义了一个用于增强线结构和图像其它细节的锐化内核 sharpen。关于这些内核的详细介绍不在本教程的范围之内。因此,如果你有兴趣了解更多关于内核建构的话,我建议从这里开始https://en.wikipedia.org/wiki/Kernel_(image_processing),然后接触一下http://setosa.io/ev/image-kernels/

让我们再来多定义几个内核:

63

65-68行定义了一个可用作边缘检测的拉普拉斯算子laplacian。

注意:拉普拉斯在检测图像中的模糊操作方面也是非常有用的。

71-80行,我们最后定义了两个Sobel滤波器。第一个(71-74行)用于检测图像垂直梯度的变化,第二个(77-80行)用于检测图像水平梯度的变化。

上述这些内核,我把他们放到一个组里,组成一个“kernel bank”:

82

最后,我们打算将我们的 kernelBank 应用到我们的图像处理中去了:

94

95和96行从磁盘加载我们的待处理图像,并将其转换为灰度图像。卷积运算当然可以用于RGB图像(或其它多通道图像),但这里为简单起见,我们将只介绍用于灰度图像的情况。

99行开始遍历我们在 kernelBank 集合中的所有内核。

104行通过调用我们之前定义的卷积函数convolve,将当前的内核kernel应用到灰度图像gray上。

105行,为了让整个过程看起来更完整,我们调用了cv2.filter2D这个函数,它同样可以完成一个卷积运算。但cv2.filter2D函数是我们之前定义的卷积函数 convolve 的优化版本。在这篇文章中,我详细说明卷积的执行过程是为了让你更好地理解卷积是如何工作的。

最后,108-112行让输出图像在我们的屏幕上显示。

用OpenCV和Python实践卷积的例子

下面这张图片里,你会看到几个玻璃杯,其中一个装有啤酒,以及三个3D打印的神奇宝贝:

3d_pokemon-768x576

想要运行我们刚刚写好的脚本,只需发出如下命令:

1
$ python convolutions.py --image 3d_pokemon.png

然后你就能看到应用我们的smallBlur内核处理输入图像后的结果:

convolutions_opencv_smallblur-768x204

在左边,是我们的原始图像,中间是应用我们自己编写的卷积函数convolve的结果,右边是应用cv2.filter2D函数的结果。可以看到,我们自定义convolve函数的输出结果和cv2.filter2D函数是匹配的,这说明我们的卷积函数可以正常工作。此外,我们的原始图像现在看起来有点“模糊”,这得益于平滑内核。

 

接下来,我们可以让图像变得更模糊一些:

convolutions_opencv_largeblur-768x286

上图中我们采用了 largeBlur 内核,与smallBlur内核相比,可以看到随着平滑内核尺寸的增加,输出图像模糊度也在增加。

 

我们也可以对我们的图像进行增强处理:

convolutions_opencv_sharpen-768x288

接下来,我们可以通过拉普拉斯算子对图像进行边缘检测:

convolutions_opencv_laplacian-768x293

下面这一幅图像里,我们通过Sobel算子找到图像的垂直边缘:

convolutions_opencv_sobelx-768x295

接着找到图像的水平边缘:

convolutions_opencv_sobely

卷积在深度学习中扮演的角色

正如你所了解的,我们必须手动定义各种用于实现不同功能的内核,如平滑,锐化和边缘检测等。

这当然没有问题,但是否有一种方法,可以自主学习这些类型的滤波器,完成滤波器的自主设计呢?是否可能设计一个机器学习算法,通过对图像进行学习,最终学会这些操作呢?

答案是肯定的。

这些算法被称为卷积神经网络(CNNs),它是神经网络的子类型。通过应用卷积滤波器,非线性激活函数,乘积和和反向传播算法,CNNs可以对检测浅层神经网络的简单结构如边缘和斑点的滤波器进行学习,然后使用这些边缘和结构作为构建块,最终完成检测深层神经网络的较高级别的对象(面部,猫,狗,杯子等)的工作。

究竟CNNs是怎样做到这一点的呢?

我会在稍后的几篇文章里介绍,但在此之前,你必须掌握足够的基础知识。

 

原文链接:http://www.pyimagesearch.com/2016/07/25/convolutions-with-opencv-and-python/

Leave a Reply