Canvas APIによる画像処理の基礎
現代のウェブアプリケーションにおいて、クライアントサイドでの画像編集機能はユーザー体験を向上させる重要な要素です。Canvas APIの2Dレンダリングコンテキスト(CanvasRenderingContext2D)を利用することで、サーバーを介さずにブラウザ上でピクセル単位の操作が可能になります。
/**
* Canvas要素の初期化と画像の描画
*/
function setupCanvas(imgElement) {
const cvs = document.createElement('canvas');
const renderCtx = cvs.getContext('2d');
cvs.width = imgElement.naturalWidth;
cvs.height = imgElement.naturalHeight;
renderCtx.drawImage(imgElement, 0, 0);
return { cvs, renderCtx };
}
ピクセルデータの取得と操作
画像処理の本質は、getImageData()によって取得されるRGBA形式のバイト配列を操作することにあります。この配列は[r1, g1, b1, a1, r2, g2, b2, a2, ...]という形式で、0から255の整数を保持しています。
/**
* 画像データのピクセルを反復処理する基本構造
*/
function processPixels(ctx, width, height) {
const imgData = ctx.getImageData(0, 0, width, height);
const pixels = imgData.data;
for (let offset = 0; offset < pixels.length; offset += 4) {
const red = pixels[offset];
const green = pixels[offset + 1];
const blue = pixels[offset + 2];
const alpha = pixels[offset + 3];
// ここに加工ロジックを記述
}
ctx.putImageData(imgData, 0, 0);
}
主要な画像フィルタの実装
1. グレースケール変換(輝度計算)
単に平均値を取るのではなく、人間の視覚特性に基づいた重み付け(Rec. 709など)を行うことで、より自然なグレースケール画像が得られます。
function convertToGrayscale(imgData) {
const d = imgData.data;
for (let i = 0; i < d.length; i += 4) {
// 人間の視覚感度に基づいた係数
const luminosity = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
d[i] = d[i + 1] = d[i + 2] = luminosity;
}
return imgData;
}
2. コンボリューション(畳み込み)によるぼかし
ガウスぼかしなどは、周辺ピクセルの値を特定のカーネル(行列)を用いて重み付け平均することで実現します。
function applyBoxBlur(imgData, width, height) {
const src = imgData.data;
const output = new Uint8ClampedArray(src.length);
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let r = 0, g = 0, b = 0;
// 3x3 近傍ピクセルの平均化
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const pos = ((y + ky) * width + (x + kx)) * 4;
r += src[pos];
g += src[pos + 1];
b += src[pos + 2];
}
}
const target = (y * width + x) * 4;
output[target] = r / 9;
output[target + 1] = g / 9;
output[target + 2] = b / 9;
output[target + 3] = src[target + 3];
}
}
return new ImageData(output, width, height);
}
高度な画像合成技術
ブレンドモードの自作ロジック
Canvas標準のglobalCompositeOperationも強力ですが、ピクセル演算を直接定義することで独自のブレンドロジックを実装できます。
const BlendModes = {
// 乗算
multiply: (src, dest) => (src * dest) / 255,
// スクリーン
screen: (src, dest) => 255 - (((255 - src) * (255 - dest)) / 255),
// オーバーレイ
overlay: (src, dest) => {
return dest < 128
? (2 * src * dest) / 255
: 255 - (2 * (255 - src) * (255 - dest)) / 255;
}
};
function composeLayers(topData, bottomData, mode = 'multiply') {
const top = topData.data;
const bottom = bottomData.data;
const result = new Uint8ClampedArray(top.length);
for (let i = 0; i < top.length; i += 4) {
result[i] = BlendModes[mode](top[i], bottom[i]);
result[i + 1] = BlendModes[mode](top[i+1], bottom[i+1]);
result[i + 2] = BlendModes[mode](top[i+2], bottom[i+2]);
result[i + 3] = 255;
}
return new ImageData(result, topData.width, topData.height);
}
パフォーマンス最適化:Web Workersの活用
画像処理は計算負荷が高いため、メインスレッドで行うとUIのフリーズを招きます。Web Workersに処理を委譲し、Transferable Objectsを使用してデータを効率的に転送します。
// main.js
const worker = new Worker('processor-worker.js');
function runAsyncFilter(imgData) {
// バッファを転送してゼロコピーを実現
const buffer = imgData.data.buffer;
worker.postMessage({
pixels: buffer,
width: imgData.width,
height: imgData.height
}, [buffer]);
worker.onmessage = (e) => {
const processedBuffer = e.data.pixels;
const newImgData = new ImageData(
new Uint8ClampedArray(processedBuffer),
e.data.width,
e.data.height
);
ctx.putImageData(newImgData, 0, 0);
};
}
// processor-worker.js
self.onmessage = (e) => {
const { pixels, width, height } = e.data;
const data = new Uint8ClampedArray(pixels);
// ここでフィルタ処理を実行
const result = someHeavyFilter(data);
self.postMessage({
pixels: result.buffer,
width,
height
}, [result.buffer]);
};
メモリ管理とリソースの再利用
高解像度画像を扱う場合、Canvas要素やImageDataの頻繁な生成はメモリ圧迫に繋がります。以下の点に注意して設計します。
- キャンバスプール: 同一サイズのCanvasが必要な場合、新しく作成せずに再利用する。
- TypedArrayの再利用: フィルタ処理ごとに配列を確保するのではなく、バッファを使い回す。
- OffscreenCanvas: 描画処理自体もWorkerで行うことで、よりスケーラブルな構成にする。