滤镜主要是用来实现图像的各种特殊效果,比如灰色、颜色反转、黑白、马赛克、锐化等,我们在 Photoshop 中处理图片时经常能看到,这些看似很复杂的功能前端同学通过 Canvas 也能很容易实现。本文先通过几个简单的例子,解释如何实现简单的滤镜效果;之后再介绍卷积的基础知识,通过卷积运算来实现比较复杂的滤镜效果。
代码如下:
<canvas id="my_canvas"></canvas>
// 滤镜函数function filter (imageData, ctx) {
// todo... 处理 imageDatareturn imageData;
}
// 加载图片let img = new Image();
img.src = "img.jpg";
img.onload = function () {
// canvaslet myCanvas = document.querySelector("#my_canvas");
myCanvas.width = 400;
myCanvas.height = 300;
let myContext = myCanvas.getContext("2d");
// 将图片绘制到画布中
myContext.drawImage(img, 0, 0, myCanvas.width, myCanvas.height);
// 获取画布的像素数据let imageData = myContext.getImageData(0, 0, myCanvas.width, myCanvas.height);
// 处理像素数据
imageData = filter(imageData, myContext);
// 将处理过的像素数据放回画布
myContext.putImageData(imageData, 0, 0);
}
处理过程很简单,可是如何处理像素数据呢?
// 从 x=0,y=0 开始,取宽=2,高=2 的像素数据let imageData = ctx.getImageData(0, 0, 2, 2)
console.log(imageData);
getImageData 获取图片像素数据,方法返回 ImageData 对象,是拷贝了画布指定矩形的像素数据,如下图
imageData.data 中有四个(宽 x 高=2x2=4)像素的数据,每个像素数据,都存在着四方面的信息,即 RGBA 值:R - 红色 (0-255; 0 是黑色,255 是纯红色) G - 绿色 (0-255; 0 是黑色,255 是纯绿色) B - 蓝色 (0-255; 0 是黑色的,255 是纯蓝色) A – 透明度 (0-255; 0 是透明的,255 是完全可见不透明的)
既然我们知道了像素数据的含义,就可以在 filter 函数中对像素数据 imageData 进行相应的数学运算即可,现在我们对这三只小狗下手
顾名思义,就是只保留红色值不变,把绿色和蓝色去除掉(值设为 0)
// 滤镜函数 - 红色滤镜function filter (imageData, ctx) {
let imageData_length = imageData.data.length / 4; // 4 个为一个像素for (let i = 0; i < imageData_length; i++) {
// imageData.data[i * 4 + 0] = 0; // 红色值不变
imageData.data[i * 4 + 1] = 0; // 绿色值设置为 0
imageData.data[i * 4 + 2] = 0; // 蓝色值设置为 0
}
return imageData;
}
效果如下:
黑白照片效果,将颜色的 RGB 设置为相同的值即可使得图片为灰色,我们可以取三个色值的平均值。
// 滤镜函数 - 灰色滤镜function filter (imageData, ctx) {
let imageData_length = imageData.data.length / 4; // 4 个为一个像素for (let i = 0; i < imageData_length; i++) {
let newColor = (imageData.data[i * 4] + imageData.data[i * 4 + 1] + imageData.data[i * 4 + 2]) / 3;
imageData.data[i * 4 + 0] = newColor;
imageData.data[i * 4 + 1] = newColor;
imageData.data[i * 4 + 2] = newColor;
}
return imageData;
}
效果如下:
就是 RGB 三种颜色分别取 255 的差值
// 滤镜函数 - 反向滤镜function filter (imageData, ctx) {
let imageData_length = imageData.data.length / 4; // 4 个为一个像素for (let i = 0; i < imageData_length; i++) {
imageData.data[i * 4 + 0] = 255 - imageData.data[i * 4];
imageData.data[i * 4 + 1] = 255 - imageData.data[i * 4 + 1];
imageData.data[i * 4 + 2] = 255 - imageData.data[i * 4 + 2];
}
return imageData;
}
效果如下:
以上,通过控制每个像素 4 个数据的值,即可达到简单滤镜的效果。但是复杂的滤镜比如边缘检测,就需要用到卷积运算来实现。
卷积是一个常用的图像处理技术。在图像处理中,卷积操作是使用一个卷积核(kernel)对图像中的每一个像素进行一些列操作,可以改变像素强度,使用卷积技术,你可以获取一些流行的图像效果,比如边缘检测、锐化、模糊、浮雕等。
上图就是通过卷积运算后,输出的边缘检测图像效果,如果通过上面简单滤镜算法,很难想象我们能找到物体的边缘!现在来看一下怎么实现。
卷积运算是使用一个卷积核对输入图像中的每个像素进行一系列四则运算。卷积核(算子)是用来做图像处理时的矩阵,通常为 3x3 矩阵。使用卷积进行计算时,需要将卷积核的中心放置在要计算的像素上,一次计算核中每个元素和其覆盖的图像像素值的乘积并求和,得到的结构就是该位置的新像素值。
计算步骤如下:1、我们使用 3×3 的卷积核,将其覆盖在输入图像,对应的数字相乘,最后全部相加,即可得到第一个输出数据;2、把 3×3 的卷积核右移一格;3、重复 1 的计算过程,得到第二个数据;4、重复以上过程。
按照我们上面讲的图片卷积,如果原始图片尺寸为 6 x 6,卷积核尺寸为 3 x 3,则卷积后的图片尺寸为(6-3+1) x (6-3+1) = 4 x 4,卷积运算后,输出图片尺寸缩小了,这显然不是我们想要的结果!为了解决这个问题,可以使用 padding 方法,即把原始图片尺寸进行扩展,扩展区域补零,扩展尺寸为卷积核的半径(3x3 卷积核半径为 1,5x5 卷积核半径为 2)。
一个尺寸 6 x 6 的数据矩阵,经过 padding 后,尺寸变为 8 * 8,卷积运算后输出尺寸为 6 x 6,保证了图片尺寸不变化。
常用于检测物体边缘的卷积核是一个中间是 8,周围是-1 的 3x3 数据矩阵。
我们能感受到物体的边缘,是因为边缘有明显的色差。假设输入图像的部分色值为 10,部分色值为 50,那么 10 和 50 之间就存在色差,边缘就在这个地方。经过卷积计算之后,我们可以看到色值相同的部分都变成了 0 表现为黑色,只有边缘的色值计算结果大于 0(色值最小是 0,负数色值也是黑色),即色值为 120 的边缘就凸显出来了!代码如下:
// 卷积计算函数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;
}
// 滤镜函数function filter (imageData, ctx) {
let kernel = [-1, -1, -1,
-1, 8, -1,
-1, -1, -1]; // 边缘检测卷积核return convolutionMatrix(ctx.createImageData(imageData), imageData, kernel);
}
我们只要使用不同的卷积核就能得到不同的图像处理效果,比如使用下面这个卷积核,就能得到锐化效果
let kernel = [-1, -1, -1,
-1, 9, -1,
-1, -1, -1]; // 锐化卷积核
锐化也是一种针对边缘处理(增强)的效果,前面有提到“卷积核的总和加起来如果等于 1,计算结果不会改变图像的灰度强度”。所以只要把边缘检测卷积核中间的 8 改为 9,就能实现边缘增强,且图片亮度不变的锐化效果!
图像处理是一个很有意思的事情,大家还可以试试通过 navigator.mediaDevices 获取摄像头 video,然后通过 requestAnimationFrame 实时把当前 video 的图片数据通过滤镜处理后,再画到 canvas 中,这样我们就得到了滤镜处理过的视频(参考 Demo)!另外如果你看懂了本文的卷积部分,也许你就踏进了【神经网络与深度学习】的大门,因为卷积运算是神经网络与深度学习中最基本的组成部分,边缘检测只是一个入门样例,我们还可以用来做人脸识别等高级应用,想想都有一点小激动~
https://wqs.jd.com/demo/filter/index.html