编辑:王抒伟
这个博客主要通过回答以下几个问题来实现卷积
- 什么是图像卷积?
- 卷积在做什么?
- 我们为什么要使用它们?
- 我们如何应用它们?
- 卷积在深度学习中的作用?
什么是图像卷积?
“卷积”一词这个词一听,就把人吓跑了,好像数学中的复杂术语,但实际上并非如此。 实际上,如果您以前曾经使用过计算机视觉,图像处理或OpenCV,都用到了卷积,只是你不知道。 例如PS 中图像模糊 或 图像平滑;或者用过美图软件的;或 ppt里面的图像工具;都用到了卷积。
实际上, 卷积只是两个矩阵后跟一个和的逐个元素相乘。 那么刚刚的卷积是什么:
- 取两个矩阵(它们都具有相同的尺寸)。
- 将它们逐个元素相乘(即, 不是点积,而是一个简单的乘法)。
- 将元素加在一起。
- 要了解有关卷积的更多信息,为什么使用卷积,如何应用卷积以及卷积在深度学习 图像分类中的 总体作用,请继续往下读。
这样想吧-图像只是 多维矩阵。我们的图像具有宽度(列数)和高度(行数),就像矩阵一样。 那么对于一个标准的RGB图像,我们的深度 3 就分别代表 红,绿,蓝通道的。
有了以上基础,我们可以将图像视为一个 大矩阵,将 内核或 卷积矩阵视为一个用于模糊、锐化、边缘检测和其他图像处理功能的 微小矩阵。
本质上,这个 微小的内核位于大图像的顶部, 并从左到右,从上到下滑动,在原始图像的每个(x,y)坐标上应用数学运算(即 卷积) 。
传统图像处理的方法,都是手动定义内核来实现基本的图像处理功能。 例如,你可能已经熟悉模糊(平均平滑,高斯平滑,中值平滑等),边缘检测(拉普拉斯,Sobel,Scharr,Prewitt等)和锐化- 所有这些操作都是手工形式专门设计 用于执行特定功能的内核。
那么人类的惰性引导我们, 是否有一种方法可以 自动学习这些类型的过滤器?甚至将这些过滤器用于 图像分类和 物体检测?
你简直就是个天才,继续往下看。
卷积在做什么?
我们需要更多地了解内核和卷积。
让我们将图像视为 大矩阵,将内核视为 微小矩阵(至少相对于原始的“大矩阵”图像而言):
内核是一个小的矩阵,它从左到右,从上到下滑动到较大的图像上。在输入图像的每个像素处,图像的邻域与内核进行卷积,并存储输出
如上图所示,我们沿着原始图像从左到右和从上到下滑动内核。
在原始图像的每个 (x,y)坐标处,我们停止并检查位于图像内核中心 的像素附近 。然后,我们采用该像素邻域,将 其与内核卷积,并获得单个输出值。然后,将该输出值以与内核中心相同的(x,y)坐标存储在输出图像中 。
如果这听起来有点疑惑,请放心,我们将在本博文后面的“了解图像卷积” 部分中回顾一个示例 。 但是在深入研究示例之前,让我们首先看一下卷积核的外观:
一个3 x 3内核,可以使用OpenCV和Python将其与图像进行卷积
上面我们定义了一个正方形的 3 x 3内核(对这个内核用于什么有任何猜测吗?) 内核可以是任意大小 的M×N个像素,前提是 二者 中号和 N是 奇整数。
注意:您通常会看到的大多数内核实际上都是N×N平方的矩阵。 我们使用 奇数的内核大小来确保在图像中心有一个有效的整数 (x,y)坐标
在 左侧,我们有一个 3 x 3的 矩阵。矩阵的中心显然位于 x = 1,y = 1,其中矩阵的左上角用作原点,并且我们的坐标为零索引。
但是在 右边,我们有一个 2 x 2的 矩阵。该矩阵的中心将位于 x = 0.5,y = 0.5处。但是我们知道,不应用插值,就没有像素位置(0.5,0.5)这样的东西 -我们的像素坐标必须是整数! 这正是我们使用奇数内核大小的原因-始终确保内核中心存在有效 (x,y)坐标。
既然我们已经讨论了内核的基础知识,那么让我们谈谈一个称为卷积的数学术语 。 在图像处理中,卷积需要三个组件:
- 输入图像。
- 我们将应用于输入图像的内核矩阵。
- 输出图像,用于存储与内核卷积的输入图像的输出。
卷积本身实际上非常容易。我们需要做的是:
- 从原始图像中选择一个 (x,y)坐标。
- 将内核的中心放置 在此 (x,y)坐标上。
- 对输入图像区域和内核进行逐元素乘法,然后将这些乘法运算的值求和为单个值。 这些乘法的总和称为 内核输出。
- 使用与步骤#1相同的 (x,y)坐标 ,但这一次,将内核输出存储在与输出图像相同的 (x,y)-位置。
- 在下面,您可以找到一个示例(使用数学符号表示为“ *”运算符)对具有3 x 3内核用于模糊的图像的 3 x 3区域 进行卷积 :
将3 x 3输入图像区域与3 x 3内核用于卷积
所以:
卷积运算的输出存储在输出图像中
应用此卷积后,我们将位于输出图像 O的坐标(i,j)的像素设置 为 O_i,j = 126。 卷积只是内核与输入图像的内核所覆盖的邻域之间元素级矩阵乘法的总和。
我们如何使用python和opencv实现卷积?
讨论卷积核和卷积很有趣,但是现在让我们继续看一些实际的代码,以确保您 了解如何实现卷积核和卷积。
打开一个新文件,命名 convolutions.py
,让我们开始工作:
# import the necessary packages
from skimage.exposure import rescale_intensity
import numpy as np
import argparse
import cv2
我们从2-5行开始 ,导入所需的Python包。您应该已经在系统上安装了NumPy和OpenCV,但是可能尚未安装scikit-image。要安装scikit-image,只需使用 :
代码语言:javascript复制pip install -U scikit-image
接下来,我们可以开始定义我们的自定义 卷积 方法:
代码语言:javascript复制def convolve(image, kernel):
# grab the spatial dimensions of the image, along with
# the spatial dimensions of the kernel
(iH, iW) = image.shape[:2]
(kH, kW) = kernel.shape[:2]
# allocate memory for the output image, taking care to
# "pad" the borders of the input image so the spatial
# size (i.e., width and height) are not reduced
pad = (kW - 1) // 2
image = cv2.copyMakeBorder(image, pad, pad, pad, pad,
cv2.BORDER_REPLICATE)
output = np.zeros((iH, iW), dtype="float32")
卷积 函数需要两个参数:(灰度) image 与 kernel 。
有了我们 image和kernel (我们假设是NumPy数组),然后确定每个空间的空间尺寸(即宽度和高度)(第10和11行)。
在继续之前,必须了解在图像上“滑动”卷积矩阵,应用卷积然后存储输出的过程实际上会 减小输出图像的空间尺寸。
为什么是这样?
回想一下,我们将计算“围绕”内核当前所在的输入图像的中心(x,y)坐标“居中” 。 这意味着对于沿着图像边界落下的像素,没有“中心”像素之类的东西。 空间尺寸的减小仅仅是将卷积应用于图像的副作用。有时,这种效果是理想的,而有时则不是。
然而,在大多数情况下,我们希望我们的 输出图像具有 相同的尺寸作为我们的 输入图像。为了确保这一点,我们使用padding技术,叫做“填充”(第16-19行)。在这里,我们只是沿图像边界复制像素,以使输出图像与输入图像的尺寸匹配。
还存在其他填充方法,包括 零填充(用零填充边界-在构建卷积神经网络时非常常见)和 环绕(其中边界像素是通过检查图像的另一端确定的)。在大多数情况下,您会看到重复填充或零填充。
现在,我们准备将实际的卷积应用于我们的图像:
代码语言:javascript复制 # loop over the input image, "sliding" the kernel across
# each (x, y)-coordinate from left-to-right and top to
# bottom
for y in np.arange(pad, iH pad):
for x in np.arange(pad, iW pad):
# extract the ROI of the image by extracting the
# *center* region of the current (x, y)-coordinates
# dimensions
roi = image[y - pad:y pad 1, x - pad:x pad 1]
# perform the actual convolution by taking the
# element-wise multiplicate between the ROI and
# the kernel, then summing the matrix
k = (roi * kernel).sum()
# store the convolved value in the output (x,y)-
# coordinate of the output image
output[y - pad, x - pad] = k
第24和25行遍历我们的图片 ,一次从左到右和从上到下1个像素“滑动”内核。
第29行从中提取感兴趣区域(ROI)图片 使用NumPy数组切片。
通过在第34行将ROI 和 kernel 进行卷积 运算,然后对矩阵中的条目求和。 输出值 ķ 然后存储在 输出 数组位于相同 (x,y)坐标(相对于输入图像)。
现在我们可以完成我们的 卷积 方法:
代码语言:javascript复制 # rescale the output image to be in the range [0, 255]
output = rescale_intensity(output, in_range=(0, 255))
output = (output * 255).astype("uint8")
# return the output image
return output
在处理图像时,我们通常会处理[0,255]范围内的像素值 。但是,在使用卷积时,我们经常会 超出此范围。
为了带来我们 输出 图片返回到[0,255]范围内 ,我们将使用rescale_intensity scikit-image的功能(第41行)。 我们还将第42行的图像转换回无符号的8位整数数据类型 (输出 image是浮点类型,以便处理[0,255]范围之外的像素值 。
最后, 输出 图像返回到第45行的调用函数 。
现在我们已经定义了 卷积 函数,让我们继续执行脚本的主干程序部分。 程序的这一部分将处理解析命令行参数,定义一系列我们将应用于图像的内核,然后显示输出结果:
代码语言:javascript复制# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,
help="path to the input image")
args = vars(ap.parse_args())
# construct average blurring kernels used to smooth an image
smallBlur = np.ones((7, 7), dtype="float") * (1.0 / (7 * 7))
largeBlur = np.ones((21, 21), dtype="float") * (1.0 / (21 * 21))
# construct a sharpening filter
sharpen = np.array((
[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]), dtype="int")
第48-51行处理解析我们的命令行参数。这里我们只需要一个参数,—image ,这是我们输入路径的路径。
然后,我们进入 第54和55行,它们定义了一个 7 x 7内核和一个 21 x 21内核,用于模糊/平滑图像。内核越大,图像越模糊。 检查该内核,您可以看到将内核应用于ROI的输出将只是输入区域的 平均值。
我们在 58-61行定义了一个 锐化内核,用于增强图像的线结构和其他细节。 对这些内核中的每一个进行详细解释超出了本教程的范围,可以参考这里,然后使用Setosa.io上出色的内核可视化工具。
我们再定义几个内核:
代码语言:javascript复制# construct the Laplacian kernel used to detect edge-like
# regions of an image
laplacian = np.array((
[0, 1, 0],
[1, -4, 1],
[0, 1, 0]), dtype="int")
# construct the Sobel x-axis kernel
sobelX = np.array((
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]), dtype="int")
# construct the Sobel y-axis kernel
sobelY = np.array((
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]), dtype="int")
第65-68行定义了 可以用作边缘检测形式的拉普拉斯算子。 注意:拉普拉斯算子对于检测图像中的模糊也非常有用。
最后,我们将 在 第71-80行定义两个Sobel滤波器。第一行(71-74行)用于检测 图像梯度的垂直变化。类似地, 第77-80行构造了一个用于检测梯度水平变化的滤波器 。
给定所有这些内核,我们将它们合并为一组称为“内核库”的元组:
代码语言:javascript复制# construct the kernel bank, a list of kernels we're going
# to apply using both our custom `convole` function and
# OpenCV's `filter2D` function
kernelBank = (
("small_blur", smallBlur),
("large_blur", largeBlur),
("sharpen", sharpen),
("laplacian", laplacian),
("sobel_x", sobelX),
("sobel_y", sobelY)
最后,我们准备使用我们的 kernelBank 对我们的 —image 图片进行一些处理:
代码语言:javascript复制# load the input image and convert it to grayscale
image = cv2.imread(args["image"])
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# loop over the kernels
for (kernelName, kernel) in kernelBank:
# apply the kernel to the grayscale image using both
# our custom `convole` function and OpenCV's `filter2D`
# function
print("[INFO] applying {} kernel".format(kernelName))
convoleOutput = convolve(gray, kernel)
opencvOutput = cv2.filter2D(gray, -1, kernel)
# show the output images
cv2.imshow("original", gray)
cv2.imshow("{} - convole".format(kernelName), convoleOutput)
cv2.imshow("{} - opencv".format(kernelName), opencvOutput)
cv2.waitKey(0)
cv2.destroyAllWindows()
第95和96行从磁盘加载我们的图像,并将其转换为灰度。卷积运算符当然可以应用于RGB(或其他多通道图像),但是为了简单起见,在本博文中,我们仅将滤镜应用于灰度图像。
我们开始循环遍历我们的一组内核 kernelBank 在 第99行 ,然后应用当前内核 应用到 灰色通过调用我们的自定义行104上的 image 卷积 。
最后, 第108-112行将输出图像显示到我们的屏幕上。
使用OpenCV和Python进行卷积的示例
在此图像中,您将看到一杯啤酒和三个3D打印的神奇宝贝:
图6:我们将要应用卷积的示例图像。
运行我们的脚本:
代码语言:javascript复制$ python convolutions.py --image 3d_pokemon.png
然后,您将看到应用我们的结果 smallBlur 内核到输入图像:
图7:使用我们的“卷积”函数应用小的模糊卷积,然后针对OpenCV的“ cv2.filter2D”函数的结果进行验证
在左侧,是原始图像。然后右边是卷积后的。最右边结果来自cv2.filter2D
。由于平滑内核的作用,我们的原始图像现在看起来“模糊”和“平滑”。
接下来,让我们应用更大的模糊效果:
图8:当我们使用更大的平滑核对图像进行卷积时,图像变得更加模糊
比较图7 和 图8,请注意,随着平均内核大小的 增加,输出图像中的模糊量也随之 增加。
我们还可以提高我们的形象:
图9:使用锐化内核会增强图像中类似边缘的结构和其他细节
让我们使用拉普拉斯算子计算边缘:
图10:通过与OpenCV和Python卷积应用Laplacian运算符
使用Sobel运算符查找垂直边缘:
图11:利用Sobel-x内核查找垂直图像
并使用Sobel查找水平边缘:
图12:使用Sobel-y运算符和卷积查找图像中的水平梯度
卷积在深度学习中的作用 在您浏览本博客文章时,我们必须 手动手动定义每个内核,以应用各种操作,例如平滑,锐化和边缘检测。 但是如果有一种方法可以 学习这些过滤器呢?是否可以定义一种可以查看图像并最终学习这些类型的运算符的机器学习算法?
实际上,这些算法是神经网络的一种子类型, 称为 卷积神经网络(CNN)。 通过应用卷积滤波器,非线性激活函数,池化和反向传播,CNN能够学习能够检测网络较低层中的边缘和类斑点结构的过滤器,然后将这些边缘和结构用作构建基块在网络的较深层中检测更高级别的对象(例如,面孔,猫,狗,杯子等)。