适用于画布的图片滤镜

Ilmari Heikkinen

简介

HTML5 canvas 元素可用于编写图片滤镜。您需要做的是将图片绘制到画布上,读回画布像素,然后对其运行滤镜。然后,您可以将结果写入新的画布(或者,直接重复使用旧画布)。

听起来很简单?很好。我们开始干活儿了!

原始测试图片
原始测试图片

处理像素

首先,检索图片像素:

Filters = {};
Filters.getPixels = function(img) {
var c = this.getCanvas(img.width, img.height);
var ctx = c.getContext('2d');
ctx.drawImage(img);
return ctx.getImageData(0,0,c.width,c.height);
};

Filters.getCanvas = function(w,h) {
var c = document.createElement('canvas');
c.width = w;
c.height = h;
return c;
};

接下来,我们需要一种过滤图片的方法。如何使用 filterImage 方法接受滤镜和图片,并返回经过滤后的像素?

Filters.filterImage = function(filter, image, var_args) {
var args = [this.getPixels(image)];
for (var i=2; i<arguments.length; i++) {
args.push(arguments[i]);
}
return filter.apply(null, args);
};

运行简单过滤器

现在,我们已经组合了像素处理流水线,接下来可以编写一些简单的滤镜了。首先,我们将图片转换为灰度图片。

Filters.grayscale = function(pixels, args) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
// CIE luminance for the RGB
// The human eye is bad at seeing red and blue, so we de-emphasize them.
var v = 0.2126*r + 0.7152*g + 0.0722*b;
d[i] = d[i+1] = d[i+2] = v
}
return pixels;
};

您可以通过向像素添加固定值来调整亮度:

Filters.brightness = function(pixels, adjustment) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
d[i] += adjustment;
d[i+1] += adjustment;
d[i+2] += adjustment;
}
return pixels;
};

对图片进行阈值处理也非常简单。您只需将像素的灰度值与阈值进行比较,然后根据该值设置颜色即可:

Filters.threshold = function(pixels, threshold) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
var v = (0.2126*r + 0.7152*g + 0.0722*b >= threshold) ? 255 : 0;
d[i] = d[i+1] = d[i+2] = v
}
return pixels;
};

对图像进行卷积

卷积滤镜是图像处理中非常实用的通用滤镜。基本思想是,您从源图片中获取一个像素矩形的加权和,并将其用作输出值。卷积滤镜可用于模糊处理、锐化、浮雕、边缘检测以及许多其他用途。

Filters.tmpCanvas = document.createElement('canvas');
Filters.tmpCtx = Filters.tmpCanvas.getContext('2d');

Filters.createImageData = function(w,h) {
return this.tmpCtx.createImageData(w,h);
};

Filters.convolute = function(pixels, weights, opaque) {
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side/2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
// pad output by the convolution matrix
var w = sw;
var h = sh;
var output = Filters.createImageData(w, h);
var dst = output.data;
// go through the destination image pixels
var alphaFac = opaque ? 1 : 0;
for (var y=0; y<h; y++) {
for (var x=0; x<w; x++) {
  var sy = y;
  var sx = x;
  var dstOff = (y*w+x)*4;
  // calculate the weighed sum of the source image pixels that
  // fall under the convolution matrix
  var r=0, g=0, b=0, a=0;
  for (var cy=0; cy<side; cy++) {
    for (var cx=0; cx<side; cx++) {
      var scy = sy + cy - halfSide;
      var scx = sx + cx - halfSide;
      if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
        var srcOff = (scy*sw+scx)*4;
        var wt = weights[cy*side+cx];
        r += src[srcOff] * wt;
        g += src[srcOff+1] * wt;
        b += src[srcOff+2] * wt;
        a += src[srcOff+3] * wt;
      }
    }
  }
  dst[dstOff] = r;
  dst[dstOff+1] = g;
  dst[dstOff+2] = b;
  dst[dstOff+3] = a + alphaFac*(255-a);
}
}
return output;
};

下面是一个 3x3 锐化滤镜。查看它如何将权重集中在中心像素上。 为了保持图片的亮度,矩阵值的总和应为 1。

Filters.filterImage(Filters.convolute, image,
[  0, -1,  0,
-1,  5, -1,
  0, -1,  0 ]
);

下面是另一个卷积滤镜示例:方形模糊处理。方形模糊处理会输出卷积矩阵内像素值的平均值。具体方法是创建一个大小为 NxN 的卷积矩阵,其中每个权重为 1 / (NxN)。这样,矩阵中的每个像素都会对输出图片做出贡献,并且权重的总和为 1。

Filters.filterImage(Filters.convolute, image,
[ 1/9, 1/9, 1/9,
1/9, 1/9, 1/9,
1/9, 1/9, 1/9 ]
);

我们可以通过组合现有滤镜来制作更复杂的图片滤镜。例如,我们来编写一个 Sobel 滤镜。Sobel 滤镜会计算图片的垂直和水平梯度,并组合计算出的图片以查找图片中的边缘。我们在此处实现 Sobel 滤镜的方式是,先将图像灰度化,然后获取水平和垂直梯度,最后将梯度图像组合起来构成最终图像。

关于术语,“梯度”在这里是指图片位置的像素值变化。如果某个像素的左侧邻居的值为 20,右侧邻居的值为 50,则该像素的水平梯度为 30。垂直渐变采用了相同的思路,但使用的是上方和下方的邻近像素。

var grayscale = Filters.filterImage(Filter.grayscale, image);
// Note that ImageData values are clamped between 0 and 255, so we need
// to use a Float32Array for the gradient values because they
// range between -255 and 255.
var vertical = Filters.convoluteFloat32(grayscale,
[ -1, 0, 1,
-2, 0, 2,
-1, 0, 1 ]);
var horizontal = Filters.convoluteFloat32(grayscale,
[ -1, -2, -1,
  0,  0,  0,
  1,  2,  1 ]);
var final_image = Filters.createImageData(vertical.width, vertical.height);
for (var i=0; i<final_image.data.length; i+=4) {
// make the vertical gradient red
var v = Math.abs(vertical.data[i]);
final_image.data[i] = v;
// make the horizontal gradient green
var h = Math.abs(horizontal.data[i]);
final_image.data[i+1] = h;
// and mix in some blue for aesthetics
final_image.data[i+2] = (v+h)/4;
final_image.data[i+3] = 255; // opaque alpha
}

还有许多其他很酷的卷积滤镜等着您去发现。例如,尝试在上面的卷积玩具中实现 Laplace 滤波器,看看它有什么作用。

总结

希望这篇小文对您有所帮助,介绍了使用 HTML canvas 标记在 JavaScript 中编写图片滤镜的基本概念。建议您再实现一些图片滤镜,这很有趣!

如果您需要提高滤镜的性能,通常可以将其移植为使用 WebGL 片段着色器进行图片处理。借助着色器,您可以实时运行大多数简单的滤镜,从而将其用于视频和动画的后期处理。