前端图像处理之滤镜

3,204 阅读8分钟

前言

滤镜主要是用来实现图像的各种特殊效果,比如灰色、颜色反转、黑白、马赛克、锐化等,我们在Photoshop中处理图片时经常能看到,这些看似很复杂的功能前端同学通过Canvas也能很容易实现。本文先通过几个简单的例子,解释如何实现简单的滤镜效果;之后再介绍卷积的基础知识,通过卷积运算来实现比较复杂的滤镜效果。

一、基础

1、图像处理流程中所用到的Canvas API主要有:

  • 清空给定矩形内的指定像素:clearRect(x, y, width, height)
  • 向画布上面绘制图片:drawImage(img, x, y, width, height)
  • 返回画布指定矩形的像素数据:getImageData(x, y, width, height)
  • 将图像数据放回画布上:putImageData(imgData, x, y)

2、处理过程

代码如下:

<canvas id="my_canvas"></canvas>
// 滤镜函数
function filter (imageData, ctx) {
    // todo... 处理imageData
    return imageData;
}
​
// 加载图片
let img = new Image();
img.src = "img.jpg";
img.onload = function () {
    // canvas
    let 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);
}

处理过程很简单,可是如何处理像素数据呢?

3、认识像素数据

// 从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进行相应的数学运算即可,现在我们对这三只小狗下手

1、单色滤镜(红色)

顾名思义,就是只保留红色值不变,把绿色和蓝色去除掉(值设为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;
}

效果如下:

2、灰色滤镜

黑白照片效果,将颜色的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;
}

效果如下:

3、反向滤镜

就是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)对图像中的每一个像素进行一些列操作,可以改变像素强度,使用卷积技术,你可以获取一些流行的图像效果,比如边缘检测、锐化、模糊、浮雕等。

上图就是通过卷积运算后,输出的边缘检测图像效果,如果通过上面简单滤镜算法,很难想象我们能找到物体的边缘!现在来看一下怎么实现。

1、卷积运算过程

卷积运算是使用一个卷积核对输入图像中的每个像素进行一系列四则运算。 卷积核(算子)是用来做图像处理时的矩阵,通常为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,保证了图片尺寸不变化。

2、卷积核特性

  • 大小应该是奇数,这样它才有一个中心,例如3x3,5x5或者7x7。
  • 卷积核上的每一位乘数被称为权值,它们决定了这个像素的分量有多重。
  • 它们的总和加起来如果等于1,计算结果不会改变图像的灰度强度。
  • 如果大于1,会增加灰度强度,计算结果使得图像变亮。
  • 如果小于1,会减少灰度强度,计算结果使得图像变暗。
  • 如果和为0,计算结果图像不会变黑,但也会非常暗。

3、边缘检测

常用于检测物体边缘的卷积核是一个中间是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)! 另外如果你看懂了本文的卷积部分,也许你就踏进了【神经网络与深度学习】的大门,因为卷积运算是神经网络与深度学习中最基本的组成部分,边缘检测只是一个入门样例,我们还可以用来做人脸识别等高级应用,想想都有一点小激动~

五、Demo

点击查看


如果你觉得这篇内容对你有价值,请点赞,并关注我们的官网和我们的微信公众号(WecTeam),每周都有优质文章推送:

WecTeam