百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

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

toqiye 2024-09-16 05:54 8 浏览 0 评论

前言

前文我们了解了前端图像处理时对矩阵的应用,通过仿射矩阵对canvas做变换处理。
现在我们深入一下,通过矩阵,进行卷积运算,对canvas进行更高级的处理,比如边缘检测、锐化、模糊等等。
首先我们先了解一下二维卷积层的工作原理。

互相关运算

大学毕业多年,大部分同学对卷积的了解就剩“卷积”2个字了。
比如课本中对连续卷积的定义公式是:

对离散卷积的定义是:

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

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

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

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

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

0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 = 19
1 × 0 + 2 × 1 + 4 × 2 + 5 × 3 = 25
3 × 0 + 4 × 1 + 6 × 2 + 7 × 3 = 37
4 × 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]; // 浮雕卷积核

相关推荐

暗网是什么?到底有多可怕?(暗网有多可怕)

https://mp.weixin.qq.com/s/O7l4rveLnXLt-XE2A0WZ3g

“暗网”是什么,到底有多可怕,互联网的“另外一个世界”

来源成戈科技说悄悄告诉你何为“暗网”,暗网的用途是什么?到底多可怕?暗网的英文是“deepnet或deepweb”,也就是深网的意思,我们都知道,南北极的冰川看起来非常雄伟,但是你物理稍微懂一点你就...

什么是暗网?暗网有什么用?进来我告诉你

网的英文是“deepnet或deepweb”,也就是深网的意思,我们都知道,南北极的冰川看起来非常雄伟,但是你物理稍微懂一点你就会知道,我们看到的只是冰山的10%,还有90%在水面以下,很容易看出来,...

网工跳槽必备,2022年最新大厂高频技术面试真题整理

2022年金三银四正在进行,很多粉丝问我要网络工程师面试方面的资料,有在学校准备实习的,有已经工作准备跳槽的。我翻看最近的笔记,正好有整理一份今年大厂的面试题分享给大家。可以先说的是,国内的互联网面试...

三天吃透操作系统面试八股文(三天吃透计算机网络八股文)

操作系统的四个特性?并发:同一段时间内多个程序执行(与并行区分,并行指的是同一时刻有多个事件,多处理器系统可以使程序并行执行)...

掌握前端面试八股文,提升个人能力,实战面试必备!

前言:前端面试是每个前端开发者职业发展中的重要环节。掌握一些常见的前端面试题目,不仅能够在面试中表现出色,还能够提升自身的技术能力和知识广度。本文将为你介绍一些实用的前端面试题目,帮助你在面试中脱颖而...

进大厂必备的Java八股文大全(2022最强精简易懂版)

2022年秋招即将来临,很多同学会问Java面试八股文有必要背吗?答案是,必须背,博主是个三本,今年凭借这篇八股文斩获了多个大厂暑期实习offer,相信秋招一定也可以发挥重要作用。你可以讨厌这种模式,...

2022最新软件测试八股文,能不能拿心仪Offer就看你背得怎样了

前言鉴于目前测试就业越来越严峻,内卷也成了测试领域的代名词了。我的一个HR朋友告诉我,由于门槛较低,现在普通测试岗(偏功能)的投递比已经将近100,也就是一个岗位差不多有百分简历投进来。所以现在还想从...

《面试八股文》之Dubbo17卷(面试 八股文)

作者:moon原文:https://mp.weixin.qq.com/s/-kVf5qWqcw-4AJF7LL3uWw前言...

前端面试八股文?不存在的!(2021前端面试问题)

最近刷到一位前端小姐姐海外工作分享的视频,分享了她的求职,面试以及工作生活的感受,了解到海外求职面试的一个过程,其中我们经常聊的...

凭借这份《2022测试八股文》候选者逆袭面试官,offer拿到手软

《2023测试面试八股文》800道软件测试面试真题,高清打印版打包带走,横扫软件测试面试高频问题,涵盖测试理论、Linux、MySQL、Web测试、接口测试、App测试、Python、Selen...

面试常考八股文及算法(一)(八股文的要求)

define和const的区别1.define是预处理指令,用于创建符号常量。`const`是C和C++的关键字,用于创建具有常量值的变量,本质是只读变量。2.`define`在预处理阶...

面试必备(背)--计算机网络八股文系列

1.OSI七层、TCP/IP四层的关系和区别?七层模型,亦称OSI(OpenSystemInterconnection),它是一个七层的、抽象的模型体,不仅包括一系列抽象的术语或概念,也包括...

Java面试一定要坚持背的八股文!错过会很可惜!

很多人对java面试题都嗤之以鼻,认为无法衡量出一个程序员的真实水平。还有一部分原因,也是因为太难背了。那我们到底还要不要背?背!当然要背!但也不是死记硬背。在我们背诵的过程中,就把java的核心知识...

面试问八股文的公司都是垃圾?(八股文负面影响)

做医生的需要有医师资格证,做财务的有CPA证书,做教师的有教师资格证等等,做程序员的从来没听说过面试的时候需要你提供什么证书,既然没有可以证明从业能力的证书,那面试的时候如何来判断候选人的基本能力呢?...

取消回复欢迎 发表评论: