WebAssembly で高速画像処理を実現する - Wasm による画像変換とフィルタ適用
なぜ WebAssembly で画像処理なのか - JavaScript の限界と Wasm の優位性
ブラウザ内での画像処理は従来 JavaScript (Canvas API) で行われてきましたが、ピクセル単位の演算が大量に発生する画像処理では JavaScript の性能限界に直面します。WebAssembly (Wasm) は、C/C++/Rust などのシステム言語で書かれたコードをブラウザで実行可能なバイナリ形式にコンパイルし、ネイティブに近い速度で動作させる技術です。
JavaScript による画像処理の課題:
- 型付き配列の間接アクセス: Canvas の
getImageData()で取得した Uint8ClampedArray は、JIT 最適化が効きにくい間接的なメモリアクセスパターンを生みます - GC (ガベージコレクション) の停止: 大量の中間オブジェクトを生成する処理では、GC による予測不能な停止が発生し、フレームドロップの原因になります
- SIMD 命令の不在: JavaScript には SIMD (Single Instruction Multiple Data) 命令がなく、ピクセル並列処理を効率的に行えません
- 数値精度: JavaScript の Number 型は 64 ビット浮動小数点のみで、8 ビット整数演算に最適化されていません
WebAssembly の優位性:
- 予測可能な性能: AOT (Ahead-of-Time) コンパイルにより、実行時の JIT 最適化に依存しない安定した性能を提供
- 線形メモリ: 連続したメモリ空間に直接アクセスでき、画像データの走査が高速
- SIMD サポート: Wasm SIMD 拡張 (128 ビット) により、4 ピクセル同時処理が可能。Chrome 91+、Firefox 89+ で対応
- GC 不要: 手動メモリ管理 (Rust) または線形アロケータにより、GC 停止が発生しない
ベンチマーク比較 (1,920 × 1,080 px ガウシアンぼかし): JavaScript 約 180ms、Wasm (Rust) 約 35ms、Wasm + SIMD 約 12ms。Wasm は JavaScript の 5-15 倍高速です。
Rust から WebAssembly へのコンパイル環境構築
Rust は WebAssembly ターゲットへのコンパイルが最も成熟しており、wasm-bindgen エコシステムにより JavaScript との相互運用が容易です。画像処理ライブラリ (image crate) も Wasm 対応しており、本格的な画像処理パイプラインを構築できます。
環境構築手順:
- wasm-pack のインストール:
cargo install wasm-packで Rust → Wasm のビルドツールをインストール - プロジェクト作成:
wasm-pack new image-processorでテンプレートプロジェクトを生成 - Cargo.toml の設定:
[lib] crate-type = ["cdylib"]とwasm-bindgen依存を追加 - ビルド:
wasm-pack build --target webで .wasm ファイルと JS グルーコードを生成
基本的な画像処理関数の実装:
#[wasm_bindgen] pub fn grayscale(data: &mut [u8], width: u32, height: u32) { for i in (0..data.len()).step_by(4) { let gray = (0.299 * data[i] as f32 + 0.587 * data[i+1] as f32 + 0.114 * data[i+2] as f32) as u8; data[i] = gray; data[i+1] = gray; data[i+2] = gray; } }
JavaScript 側からの呼び出し:
import init, { grayscale } from './pkg/image_processor.js'; await init(); const ctx = canvas.getContext('2d'); const imageData = ctx.getImageData(0, 0, width, height); grayscale(imageData.data, width, height); ctx.putImageData(imageData, 0, 0);
メモリ管理の注意点:
- Wasm の線形メモリと JavaScript の ArrayBuffer 間でデータをコピーする際のオーバーヘッドに注意
wasm-bindgenの&mut [u8]パラメータは、JavaScript の Uint8Array を直接 Wasm メモリにマッピングし、コピーを回避します- 大きな画像 (4K 以上) では、Wasm メモリの初期サイズを
wasm-pack build時に指定する必要がある場合があります
Canvas API と WebAssembly の連携パターン
ブラウザでの画像処理では、Canvas API で画像データを取得し、Wasm で高速処理を行い、結果を Canvas に書き戻すパイプラインが基本パターンです。このデータフローを効率的に設計することが、全体のパフォーマンスを左右します。
基本的なデータフロー:
- 入力:
<img>や<video>→ Canvas に描画 →getImageData()で RGBA バイト配列を取得 - 処理: バイト配列を Wasm 関数に渡し、ピクセル演算を実行
- 出力: 処理済みバイト配列を
putImageData()で Canvas に書き戻し → 表示またはtoBlob()でエクスポート
効率的な連携のためのテクニック:
- SharedArrayBuffer の活用: Wasm メモリを SharedArrayBuffer として確保し、JavaScript と Wasm 間でゼロコピーのデータ共有を実現。ただし COOP/COEP ヘッダーの設定が必要
- ダブルバッファリング: 入力用と出力用の 2 つのバッファを Wasm メモリ内に確保し、処理中も前のフレームを表示し続けることでちらつきを防止
- OffscreenCanvas: Web Worker 内で OffscreenCanvas を使用し、メインスレッドをブロックせずに画像処理を実行。
canvas.transferControlToOffscreen()で Worker に転送 - チャンク処理: 大きな画像を行単位や矩形ブロック単位で分割処理し、途中経過をプログレッシブに表示
ImageBitmap を使った最適化:
createImageBitmap(blob)でデコード済みビットマップを取得し、Canvas への描画を高速化- ImageBitmap は GPU メモリに保持されるため、
drawImage()が高速 - ただし
getImageData()でピクセルデータを取得する際は CPU メモリへのコピーが発生するため、この段階がボトルネックになる場合があります
実用的な画像フィルタの Wasm 実装 - ぼかし、シャープネス、エッジ検出
実際の画像処理で頻繁に使用されるフィルタを WebAssembly で実装する方法を解説します。畳み込み (Convolution) ベースのフィルタは、カーネル行列をピクセルに適用する演算であり、Wasm の SIMD 命令で大幅に高速化できます。
畳み込みフィルタの基本構造 (Rust):
#[wasm_bindgen] pub fn convolve(src: &[u8], dst: &mut [u8], w: u32, h: u32, kernel: &[f32], ksize: u32) { let half = (ksize / 2) as i32; for y in 0..h as i32 { for x in 0..w as i32 { let mut r = 0.0f32; let mut g = 0.0f32; let mut b = 0.0f32; for ky in 0..ksize as i32 { for kx in 0..ksize as i32 { let px = (x + kx - half).clamp(0, w as i32 - 1) as u32; let py = (y + ky - half).clamp(0, h as i32 - 1) as u32; let idx = ((py * w + px) * 4) as usize; let k = kernel[(ky * ksize as i32 + kx) as usize]; r += src[idx] as f32 * k; g += src[idx+1] as f32 * k; b += src[idx+2] as f32 * k; } } let out = ((y as u32 * w + x as u32) * 4) as usize; dst[out] = r.clamp(0.0, 255.0) as u8; dst[out+1] = g.clamp(0.0, 255.0) as u8; dst[out+2] = b.clamp(0.0, 255.0) as u8; dst[out+3] = src[out+3]; } } }
代表的なカーネル行列:
- ガウシアンぼかし (3x3):
[1,2,1, 2,4,2, 1,2,1](各要素を 16 で割る) - シャープネス:
[0,-1,0, -1,5,-1, 0,-1,0] - エッジ検出 (Sobel X):
[-1,0,1, -2,0,2, -1,0,1] - エンボス:
[-2,-1,0, -1,1,1, 0,1,2]
SIMD による高速化: Wasm SIMD (128 ビット) を使用すると、4 つの f32 値を同時に演算できます。RGB 3 チャンネル + パディング 1 チャンネルを 1 つの v128 レジスタに格納し、カーネル乗算を並列実行することで、スカラー実装の 2-3 倍の速度向上が期待できます。
パフォーマンス最適化 - SIMD、並列処理、メモリレイアウト
WebAssembly による画像処理のパフォーマンスを最大限に引き出すための最適化テクニックを解説します。適切な最適化により、JavaScript 実装の 10-20 倍の速度を達成できます。
SIMD (Single Instruction Multiple Data) の活用:
- Wasm SIMD は 128 ビット幅のベクトル演算を提供し、4 つの 32 ビット値または 16 個の 8 ビット値を同時に処理可能
- Rust では
std::arch::wasm32モジュールの SIMD intrinsics を使用:v128_load、f32x4_mul、i8x16_addなど - コンパイル時に
RUSTFLAGS="-C target-feature=+simd128"を指定して SIMD 命令を有効化 - ブラウザ対応: Chrome 91+、Firefox 89+、Safari 16.4+ で利用可能
並列処理 (Web Workers + SharedArrayBuffer):
- 画像を水平方向に N 分割し、N 個の Web Worker で並列処理
- SharedArrayBuffer で入力画像を共有し、各 Worker が担当領域のみを処理
- 4 コア環境で理論上 4 倍の速度向上。実測では 2.5-3.5 倍程度 (Worker 起動オーバーヘッドのため)
- COOP/COEP ヘッダー:
Cross-Origin-Opener-Policy: same-originとCross-Origin-Embedder-Policy: require-corpが必須
メモリレイアウトの最適化:
- 画像データを RGBA インターリーブではなく、R/G/B/A プレーナー形式で格納すると、SIMD 処理効率が向上する場合があります
- キャッシュライン (64 バイト) に合わせたアライメントにより、メモリアクセスのレイテンシを削減
- Wasm メモリの初期サイズを十分に確保し、動的な grow を避ける (grow は全ページのゼロクリアが発生)
実践的なユースケースと既存ライブラリの活用
WebAssembly による画像処理は、特定のユースケースで特に効果を発揮します。また、ゼロから実装せずとも、既存の Wasm ベース画像処理ライブラリを活用することで、開発効率を大幅に向上できます。
効果的なユースケース:
- リアルタイムカメラフィルタ: getUserMedia で取得したビデオフレームに 60fps でフィルタを適用。JavaScript では 15-20fps が限界だが、Wasm なら 60fps を維持可能
- クライアントサイド画像圧縮: アップロード前にブラウザ内で WebP/AVIF に変換。サーバー負荷を削減し、アップロード時間を短縮
- 画像エディタ: Photopea のようなブラウザベース画像エディタ。レイヤー合成、フィルタ適用、色調補正をリアルタイムで実行
- 機械学習推論: ONNX Runtime Web (Wasm バックエンド) で画像分類や物体検出をブラウザ内で実行
既存の Wasm ベースライブラリ:
- Squoosh (libSquoosh): Google 製。MozJPEG、WebP、AVIF のエンコーダ/デコーダを Wasm で提供
- wasm-vips: libvips の Wasm ビルド。Sharp と同等の機能をブラウザ内で利用可能
- photon: Rust 製の画像処理ライブラリ。80 以上のフィルタと変換を Wasm で提供
- OpenCV.js: OpenCV の Wasm ビルド。コンピュータビジョン機能 (顔検出、特徴点抽出) をブラウザで利用
導入時の考慮事項:
- Wasm ファイルのサイズ: 画像処理ライブラリは 500KB-5MB になることがあり、初回ロード時間に影響。CDN キャッシュと遅延読み込みで対策
- ブラウザ互換性: Wasm 基本機能は全モダンブラウザ対応だが、SIMD や Threads は Safari の対応が遅れている場合がある
- デバッグ: Chrome DevTools の Wasm デバッガで DWARF 情報を使ったソースレベルデバッグが可能