龙空技术网

卷积在前端图像处理上的应用

lovelyun690 616

前言:

而今咱们对“卷积的c语言程序”大概比较注意,小伙伴们都需要学习一些“卷积的c语言程序”的相关资讯。那么小编也在网络上汇集了一些关于“卷积的c语言程序””的相关内容,希望看官们能喜欢,大家一起来了解一下吧!

前言

前文我们了解了前端图像处理时对矩阵的应用,通过仿射矩阵对canvas做变换处理。

现在我们深入一下,通过矩阵,进行卷积运算,对canvas进行更高级的处理,比如边缘检测、锐化、模糊等等。

首先我们先了解一下二维卷积层的工作原理。

互相关运算

大学毕业多年,大部分同学对卷积的了解就剩“卷积”2个字了。

比如课本中对连续卷积的定义公式是:

对离散卷积的定义是:

都忘了对吧?没关系,我们接着往下看。

在图像处理中,我们用到的一般是互相关运算。下面我们看看《动手学深度学习》中的例子。

第一步:两个二维矩阵做某种特殊的乘法,输出第一个元素:0×0+1×1+3×2+4×3=19

第二步:向右移动输入矩阵的深色部分,得到第二个输出元素。同样,计算的过程也是输入矩阵的深色部分与核一一相乘。

输出中的各个元素是按照下面的方法算出:

0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 = 191 × 0 + 2 × 1 + 4 × 2 + 5 × 3 = 253 × 0 + 4 × 1 + 6 × 2 + 7 × 3 = 374 × 0 + 5 × 1 + 7 × 2 + 8 × 3 = 43

用动图演示,输入矩阵和核矩阵之间的卷积操作如下:

这种输入矩阵与核矩阵之间的相乘被称作为互相关(Cross-Correlation)运算。

下面我们看看互相关运算的程序实现。

// 卷积计算函数function convolutionMatrix(output, input, kernel) {  let w = input.width, h = input.height;  let iD = input.data, oD = output.data;  for (let y = 1; y < h - 1; y += 1) {    for (let x = 1; x < w - 1; x += 1) {      for (let c = 0; c < 3; c += 1) {        let i = (y * w + x) * 4 + c;        oD[i] = kernel[0] * iD[i - w * 4 - 4] +          kernel[1] * iD[i - w * 4] +          kernel[2] * iD[i - w * 4 + 4] +          kernel[3] * iD[i - 4] +          kernel[4] * iD[i] +          kernel[5] * iD[i + 4] +          kernel[6] * iD[i + w * 4 - 4] +          kernel[7] * iD[i + w * 4] +          kernel[8] * iD[i + w * 4 + 4];      }      oD[(y * w + x) * 4 + 3] = 255;    }  }  return output;}

这里的output和input都是图片的imageData数据,从左到右,从上到下,遍历图片,把像素点存在data数组里,每个像素点由r、g、b、a一共4个值组成,canvas像素操作这里就不赘述了,不清楚的可以去看之前写的《前端如何在像素级别操纵图片》。

kernel是3x3的核矩阵。

所以我们计算oD(输出数据)中某一点的值,由上面的动图演示可以直观的看到,还需要这个点周围的8个点的数据。而一个点又由r、g、b、a这4个参数组成,所以我们需要对不同的数据通道分别进行卷积运算,这里不需要处理透明度a的值,直接赋值为255。

所以上面的程序简单说就是,2个嵌套的for循环来遍历像素点(注意遍历时从1开始而不是0):

for (let y = 1; y < h - 1; y += 1) {  for (let x = 1; x < w - 1; x += 1) {  }}

遍历到某个点时,通过c的遍历分别对r、g、b通道进行卷积求值,c为0时,操作的是r通道,1时是g,2时是b。

这个点的r/g/b/a值在imageData.data数组中的下标是(y * w + x) * 4 + c

其中y是该点在图片中的行,行乘w(图片宽)得到该点所在行上方点的数量,再加x(该点在图片中的列),就能得到该点在所有点中的排位,由于每个点有4个值,所以还要乘4,那么从(y * w + x) * 4开始的4个值就是该点的rgba((y * w + x) * 4 + c中c分别取0,1,2,3)。

对于下标i的值,其左侧点对应的值是i - 4,右侧是i + 4

上方的点需要减一行,一行的点对应的值有w * 4个,所以正上方的点对应的值是i - w * 4,同理正下方是i + w * 4,对这两个点减4加4,就得到它们左右两点。位置如下图:

然后对他们做卷积互相关运算得到oD[i]。

oD[i] = kernel[0] * iD[i - w * 4 - 4] +        kernel[1] * iD[i - w * 4] +        kernel[2] * iD[i - w * 4 + 4] +        kernel[3] * iD[i - 4] +        kernel[4] * iD[i] +        kernel[5] * iD[i + 4] +        kernel[6] * iD[i + w * 4 - 4] +        kernel[7] * iD[i + w * 4] +        kernel[8] * iD[i + w * 4 + 4];

这样我们就对点通过核矩阵做了某个处理,从而处理了整张图片。

下面我们怎么调用这个函数。

在前面写过的《前端基础滤镜》一文中,曾经封装过一个CanvasImage类,这里我们可以增加一个convolution方法:

class CanvasImage {  constructor(img, context) {    this.image = img;    this.context = context;  }  getData() {    return this.context.getImageData(0, 0, this.image.width, this.image.height);  }  setData(data) {    this.context.putImageData(data, 0, 0);  }  convolution() {    // TODO 后文再完善  }}

convolution方法中,我们调用卷积计算函数convolutionMatrix:

convolution(kernel) {  const imageData = this.getData()  const outData = convolutionMatrix(this.context.createImageData(imageData), imageData, kernel)  this.setData(outData)}

然后我们调用convolution,需要一个核矩阵,比如一个锐化卷积核:

const kernel = [-1, -1, -1,                -1, 9, -1,                -1, -1, -1]; // 锐化卷积核

接着我们创建一个CanvasImage的实例filter

filter = new CanvasImage(img, context)

然后我们就可以调用filter的convolution方法:

filter.convolution(kernel)

就可以看到图片被锐化处理了。(左侧是原图)

填充与步幅

现在我们知道,对图片数据(输入矩阵)进行卷积时,一般是使用一个卷积核矩阵进行互相关运算。

比如图一和图二中,我们使用高和宽为3的输入与高和宽为2的卷积核得到高和宽为2的输出。

一般来说,假设输入形状是nh x nw,卷积核形状是kh x kw,那么输出形状将是(nh - kh + 1)*(nw - kw + 1)。

所以输出形状由输入形状和卷积核形状决定。

接下来我们看看卷积层的两个超参数:填充(Padding)和步幅(Strides)。

填充 Padding

Padding是指在输入高和宽的两侧填充元素(通常是0)。

一般来说,在上下一共填充ph行,在左右共填充pw列,那么输出形状就是(nh - kh + 1 + ph) * (nw - kw + 1 + pw),即输出宽高分别增加ph和pw。

通常我们用的卷积核宽高都是奇数,比如1、3、5、7,为了使输入和输出的宽高相同,一般会设置ph = kh - 1pw = kw - 1,这样两端填充的个数就相等,分别是 ph / 2pw / 2

比如一个尺寸6 x 6的数据矩阵,经过padding后,尺寸变为8 * 8,卷积运算后输出尺寸为6 x 6,保证了图片尺寸不变化。

步幅 Stride

上面动图演示的卷积例子中,卷积核矩阵从输入矩阵的左上方开始,按从左往右、从上往下的顺序,依次在输入矩阵上滑动。我们将每次滑动的行数和列数称为步幅(Stride)。

目前为止,我们看到的例子,在高和宽两个方向上步幅均为1。

下图是在纵向上步幅为3、在横向上步幅为2的二维互相关运算:

可以看到,在输出第2个元素时,卷积窗口向右滑动了2列,计算出结果是0×0 + 0×1 + 1×2 + 2×3 = 8

在输出第3个元素时,卷积窗口向下滑动了3行,计算出结果是0×0 + 6×1 + 0×2 + 0×3 = 6

一般来说,当高上的步幅为sh,宽上的步幅为sw时,输出形状为[(nh - kh + ph + sh) / sh] * [(nw - kw + pw + sw) / sw]

比如,如果让sh和sw都为2,那么输出矩阵的宽高会只有输入矩阵的一半。

卷积核

上面介绍了卷积互相关运算及填充和步幅相关知识,下面我们来看看卷积核。

经过多年的研究,人们已经能够设计出不同的核矩阵,对图片进行转换,以达到不同的效果。不过,在深度学习出现之前,卷积核是人工设计的,需要消耗大量的时间和精力,然而深度学习出现之后,我们为卷积核初始化一些随机值,通过机器学习训练就可以得到卷积核。

卷积核特性

1、大小一般是奇数,这样它才有一个中心,例如3x3,5x5或者7x7。

2、卷积核上的每一位数称为权值,它们决定了这个像素的分量有多重。

3、它们的总和加起来如果等于1,计算结果不会改变图像的灰度强度。

4、如果大于1,会增加灰度强度,计算结果使得图像变亮。

5、如果小于1,会减少灰度强度,计算结果使得图像变暗。

6、如果和为0,计算结果图像不会变黑,但也会非常暗。

接下来我们看一些常见的卷积核。

边缘检测

比如常用的高斯-拉普拉斯算子:

// 可侦测水平和垂直边缘const kernel1 = [0, -1, 0,                -1, 5, -1,                0, -1, 0];// kernel1的基础上,还可侦测对角线的边缘,即斜的边缘const kernel2 = [-1, -1, -1,                -1, 8, -1,                -1, -1, -1];

图片的边缘是图像的最基本特征,所谓边缘是指其周围像素灰度有阶跃变化或屋顶变化的那些像素的集合。

边缘的种类可以分为两种:一种称为阶跃性边缘,它两边的像素的灰度值有着显著的不同;另一种称为屋顶状边缘,它位于灰度值从增加到减少到变化转折点。

我们能感受到物体的边缘,是因为边缘有明显的色差。比如输入图像的部分色值为10,部分色值为50,那么10和50之间就存在色差,边缘就在这个地方。

经过卷积计算之后,我们可以看到色值相同的部分都变成了0,表现为黑色,只有边缘的色值计算结果大于0(色值最小是0,负数色值也是黑色),即色值为120的边缘就凸显出来了。

除了高斯-拉普拉斯算子,还有Roberts、Sobel、Prewit、Kirsch等边缘算子。

但是高斯-拉普拉斯算子只需要一个算子,而其余的需要多个算子,然后取最大值,计算较为复杂,高斯-拉普拉斯算子对噪音敏感,可以先做模糊处理,即blur + Laplacian。

此外还有著名的Canny边缘检测算法,这里就不细说了。

锐化

锐化也是一种针对边缘处理(增强)的效果,

简单的锐化处理可以把边缘检测卷积核中间的8改为9。

const kernel = [-1, -1, -1,                -1, 9, -1,                -1, -1, -1]; // 锐化卷积核

或者,只让中心点与上下左右4个点过度的更加粗糙:

const kernel = [0, -1, 0,                -1, 5, -1,                0, -1, 0];

但是这些锐化效果都不是很好,会使噪点大量增多。

模糊

const kernel = [1 / 9, 1 / 9, 1 / 9,                1 / 9, 1 / 9, 1 / 9,                1 / 9, 1 / 9, 1 / 9]; // 模糊卷积核

值全为1/9的矩阵,意思是把周边元素和中心元素做了一个平均数,从而使点间过渡更加光滑,也就实现了模糊。这也称为高斯平滑滤波。

浮雕

const kernel = [-2, -1, 0,                -1, 1, 1,                0, 1, 2]; // 浮雕卷积核

标签: #卷积的c语言程序