キャンバスを使用した画像フィルタ

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 ]
);

畳み込みフィルタのもう一つの例は、ボックスぼかしです。ボックスぼかしは、畳み込み行列内のピクセル値の平均を出力します。そのためには、それぞれの重みが 1 / (N x N) であるサイズ N x N の畳み込み行列を作成します。これにより、行列内の各ピクセルが出力画像に同等の量を貢献し、重みの合計が 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 フィルタは、画像の垂直方向と水平方向の勾配を計算し、計算された画像を組み合わせて画像のエッジを検出します。ここでは、まず画像をグレースケールに変換し、次に水平方向と垂直方向のグラデーションを取得し、最後にグラデーション画像を組み合わせて最終的な画像を作成します。

ここで言う「勾配」とは、画像の位置でのピクセル値の変化を意味します。ピクセルの左隣の値が 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
}

他にも素晴らしい畳み込みフィルタが 見つかりますたとえば、上記の畳み込みトイに ラプラス フィルタを実装して、その動作を確認してみてください。

まとめ

この短い記事が、HTML キャンバス タグを使用して JavaScript で画像フィルタを記述する基本コンセプトの紹介に役立つことを願っています。ぜひ、他にも画像フィルタをいくつか実装してみてください。とても楽しいですよ。

フィルタのパフォーマンスを向上させる必要がある場合は、通常、WebGL フラグメント シェーダーを使用して画像処理を行うようにフィルタを移植できます。シェーダーを使用すると、最もシンプルなフィルタをリアルタイムに実行でき、動画やアニメーションの後処理に使用できます。