Canvas API 応用テクニック - フィルター、合成、ピクセル操作の実践
Canvas API による画像処理の基礎アーキテクチャ
HTML5 の Canvas API は、ブラウザ上でピクセルレベルの画像処理を実現する強力なインターフェースです。サーバーサイドに画像を送信することなく、クライアントサイドで完結する画像編集機能を構築できます。
Canvas での画像処理の基本フロー:
- 画像を
Imageオブジェクトまたは<img>要素として読み込む drawImage()で Canvas に描画するgetImageData()でピクセルデータ (ImageData) を取得する- ピクセルデータを JavaScript で加工する
putImageData()で加工結果を Canvas に書き戻すtoDataURL()またはtoBlob()で結果を出力する
ImageData オブジェクトの構造:
ImageData.data は Uint8ClampedArray で、各ピクセルが R, G, B, A の 4 バイトで表現されます。1,920 × 1,080の画像なら 1920 * 1080 * 4 = 8,294,400 バイトの配列になります。
特定ピクセルへのアクセス:
const index = (y * width + x) * 4; で座標 (x, y) のピクセルの開始インデックスを計算します。data[index] が R、data[index+1] が G、data[index+2] が B、data[index+3] が A (透明度) です。
この基本構造を理解すれば、あらゆる画像フィルターやエフェクトを JavaScript で実装できます。
カスタムフィルターの実装 - グレースケール、セピア、反転
Canvas API を使えば、CSS フィルターでは実現できないカスタムフィルターを自由に実装できます。基本的なフィルターから始めて、原理を理解しましょう。
グレースケール変換:
カラー画像をグレースケールに変換するには、各ピクセルの RGB 値から輝度を計算します。人間の目の感度に合わせた加重平均が最も自然な結果を生みます:
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
この係数は ITU-R BT.601 規格に基づいており、人間の目が緑に最も敏感で、青に最も鈍感であることを反映しています。
セピア調変換:
セピアフィルターは、グレースケール化した後に暖色系の色味を加えます:
const newR = Math.min(255, gray * 1.2 + 40);const newG = Math.min(255, gray * 1.0 + 20);const newB = Math.min(255, gray * 0.8);
色反転 (ネガティブ):
各チャンネルの値を 255 から引くだけで実現できます: data[i] = 255 - data[i];
明るさ・コントラスト調整:
- 明るさ: 各チャンネルに定数を加算
data[i] = clamp(data[i] + brightness, 0, 255); - コントラスト: 128 を中心に値を拡大/縮小
data[i] = clamp((data[i] - 128) * contrast + 128, 0, 255);
パフォーマンスの注意点:
ピクセル単位のループは大量の計算を伴います。1,920 × 1,080の画像では約 800 万回のイテレーションが必要です。for ループ内での関数呼び出しを最小化し、ルックアップテーブル (LUT) を事前計算することで高速化できます。
畳み込みフィルター - ぼかし、シャープ、エッジ検出
畳み込み (Convolution) は、周囲のピクセル値を重み付けして合算する処理で、ぼかし、シャープネス、エッジ検出など多くの画像処理の基礎となる技術です。
畳み込みの仕組み:
カーネル (重み行列) を画像上でスライドさせ、各位置で周囲のピクセルとカーネルの積和を計算します。3x3 カーネルなら、対象ピクセルとその周囲 8 ピクセルの計 9 ピクセルを使用します。
代表的なカーネル:
ボックスブラー (均等ぼかし): [[1/9, 1/9, 1/9], [1/9, 1/9, 1/9], [1/9, 1/9, 1/9]]
ガウシアンブラー (3x3 近似): [[1/16, 2/16, 1/16], [2/16, 4/16, 2/16], [1/16, 2/16, 1/16]]
シャープネス: [[0, -1, 0], [-1, 5, -1], [0, -1, 0]]
エッジ検出 (Sobel 水平): [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]
実装上の注意点:
- 画像の端ではカーネルがはみ出すため、端の処理方法を決める必要がある (ゼロパディング、ミラーリング、クランプ)
- 入力と出力を別の配列にする (同じ配列を読み書きすると結果が壊れる)
- カーネルの重みの合計が 1 でない場合、結果を正規化する必要がある
- 大きなカーネル (5x5 以上) は計算量が急増するため、分離可能カーネルの活用を検討
ガウシアンブラーの最適化:
ガウシアンカーネルは分離可能 (separable) なため、NxN の 2D 畳み込みを N の 1D 畳み込み 2 回に分解できます。これにより計算量が O(N²) から O(2N) に削減されます。
合成モード (globalCompositeOperation) の活用
Canvas の globalCompositeOperation プロパティは、新しく描画する図形と既存のキャンバス内容をどのように合成するかを制御します。Photoshop のレイヤーブレンドモードに相当する機能です。
主要な合成モード:
source-over(デフォルト): 新しい描画が上に重なるmultiply: 乗算。色を掛け合わせて暗くなる。影や陰影の表現にscreen: スクリーン。色を反転して掛け合わせ、明るくなる。光の表現にoverlay: オーバーレイ。暗い部分は multiply、明るい部分は screendifference: 差の絶対値。画像の差分検出に使用可能destination-in: 既存の描画と新しい描画が重なる部分のみ残す。マスク処理にdestination-out: 既存の描画から新しい描画の形を切り抜く。消しゴム効果に
実用的な活用例:
画像のマスク処理:
- Canvas に画像を描画
globalCompositeOperation = 'destination-in'に設定- マスク形状 (円、多角形、テキストなど) を描画
- マスク形状と重なる部分のみが残る
カラーオーバーレイ:
- Canvas に画像を描画
globalCompositeOperation = 'multiply'に設定- 半透明の色付き矩形を描画
- 画像に色味が加わる (Instagram フィルター風)
注意点:
合成モードは putImageData() には適用されません。putImageData() は常にピクセルを直接上書きします。合成モードを活用するには drawImage() を使用してください。
OffscreenCanvas と Web Worker による高速化
大きな画像のピクセル処理はメインスレッドをブロックし、UI がフリーズする原因になります。OffscreenCanvas と Web Worker を組み合わせることで、画像処理をバックグラウンドスレッドに移し、UI の応答性を維持できます。
OffscreenCanvas の基本:
OffscreenCanvas は DOM に紐づかない Canvas で、Web Worker 内で使用できます:
const offscreen = new OffscreenCanvas(width, height);const ctx = offscreen.getContext('2d');
Web Worker での画像処理パターン:
- メインスレッドで画像を読み込み、
ImageBitmapに変換 ImageBitmapを Worker にtransferableとして転送 (コピーではなく所有権移転)- Worker 内で OffscreenCanvas に描画し、ピクセル処理を実行
- 処理結果を
ImageBitmapとして メインスレッドに返送 - メインスレッドで表示用 Canvas に描画
Transferable Objects の活用:
postMessage() で大きなデータを送る際、通常はデータがコピーされます。ArrayBuffer や ImageBitmap を transferable として指定すると、コピーではなく所有権の移転が行われ、転送コストがほぼゼロになります:
worker.postMessage({ imageData }, [imageData.data.buffer]);
パフォーマンス比較:
- メインスレッドでの処理: UI がフリーズ。1,920 × 1,080で約 50〜200 ms
- Web Worker での処理: UI は応答性を維持。処理時間は同等だがユーザー体験が向上
- WASM (WebAssembly) との組み合わせ: JavaScript の 2〜5 倍高速
実践プロジェクト - リアルタイム画像エディタの構築
ここまでの技術を組み合わせて、ブラウザ上で動作するリアルタイム画像エディタの設計パターンを紹介します。
アーキテクチャ設計:
- レイヤーシステム: 複数の Canvas を重ねて非破壊編集を実現。元画像レイヤー + フィルターレイヤー + 注釈レイヤー
- 履歴管理: Command パターンで Undo/Redo を実装。各操作を独立したオブジェクトとして記録
- リアルタイムプレビュー: スライダー操作に応じて即座にフィルターを適用。
requestAnimationFrameでスロットリング
パフォーマンス最適化のテクニック:
- プレビュー用の縮小画像: 編集中は 1/4 サイズの画像でプレビューし、確定時にフルサイズで処理
- 部分更新: 変更があった領域のみ再計算 (dirty rectangle 方式)
- LUT (ルックアップテーブル): 明るさ・コントラスト・ガンマなどの点変換は 256 要素の配列で事前計算
- Web Worker プール: 複数の Worker を事前に起動し、処理を分散
出力とエクスポート:
canvas.toBlob() で処理結果を Blob として取得し、ダウンロードリンクを生成します:
canvas.toBlob((blob) => { const url = URL.createObjectURL(blob); /* download link */ }, 'image/png');
JPEG で出力する場合は品質パラメータを指定: canvas.toBlob(callback, 'image/jpeg', 0.85);
制限事項と対策:
- CORS 制限: 外部ドメインの画像は
crossOrigin="anonymous"を設定しないとgetImageData()がセキュリティエラーになる - メモリ制限: 大きな画像 (4K 以上) は複数の Canvas に分割して処理
- モバイル対応: iOS Safari は Canvas サイズに制限あり (最大約 16 MP)