透過畫布套用圖片濾鏡

Ilmari Heikkinen

簡介

HTML5 畫布元素可用於編寫圖片濾鏡。您需要做的是將圖片繪製到畫布上、讀取畫布像素,然後對其執行濾鏡。接著,您可以將結果寫入新的畫布 (或者,也可以直接重複使用舊的畫布)。

聽起來很簡單吧?那就好!開始動工!

原始測試圖片
原始測試圖片

處理像素

首先,請擷取圖片像素:

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 畫布標記,在 JavaScript 中編寫圖片濾鏡的基本概念。我很鼓勵您採用更多圖片濾鏡,真的很有趣!

如果您需要提升篩選器的效能,通常可以將篩選器移植至使用 WebGL 片段著色器的圖片處理作業。有了著色器,您就能即時執行大多數簡單的濾鏡,用於後製影片和動畫。