中級 graphics

画像処理

はじめに

画像処理はWasmの最も強力なユースケースの1つです。ピクセル単位の操作はCPU集約的であり、Rust/WasmはこれらのJavaScript処理より5〜10倍高速です。すべての処理はクライアントサイドで行われるため、画像がユーザーのブラウザから外に出ることはありません。

仕組み

┌───────────┐    getImageData()   ┌──────────┐   putImageData()   ┌───────────┐
│  <canvas>  │ ────────────────▶ │ Rust/Wasm │ ────────────────▶ │  <canvas>  │
│  (ソース) │    Uint8Array     │ (処理)   │    Uint8Array     │  (結果)   │
└───────────┘                    └──────────┘                    └───────────┘
  1. 画像をCanvasに読み込む
  2. ピクセルデータを Uint8Array(RGBA形式)として取得
  3. Rustに渡してインプレースで変更
  4. 変更されたピクセルをCanvasに戻す

JavaScriptとの統合

import init, { grayscale, brightness, sepia } from './pkg/image_wasm.js';

await init();

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// 画像を読み込む
const img = new Image();
img.onload = () => {
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);
};
img.src = 'photo.jpg';

// フィルターを適用
function applyFilter(filterFn) {
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    filterFn(imageData.data);  // インプレースで変更!
    ctx.putImageData(imageData, 0, 0);
}

// 使用例
applyFilter(grayscale);
applyFilter((pixels) => brightness(pixels, 30));
applyFilter(sepia);

ピクセル形式

Canvasの ImageData はRGBA形式のフラットな Uint8Array です:

Index:  0    1    2    3    4    5    6    7    ...
Data:  [R₀] [G₀] [B₀] [A₀] [R₁] [G₁] [B₁] [A₁] ...
        └── pixel 0 ──┘     └── pixel 1 ──┘

Rustでは chunks_exact_mut(4) を使って4バイトずつ処理します。

ボックスブラー

#[wasm_bindgen]
pub fn blur(pixels: &mut [u8], width: u32, height: u32, radius: u32) {
    let src = pixels.to_vec();
    let r = radius as i32;
    let w = width as i32;
    let h = height as i32;

    for y in 0..h {
        for x in 0..w {
            let mut sum_r = 0u32;
            let mut sum_g = 0u32;
            let mut sum_b = 0u32;
            let mut count = 0u32;

            for dy in -r..=r {
                for dx in -r..=r {
                    let nx = (x + dx).clamp(0, w - 1);
                    let ny = (y + dy).clamp(0, h - 1);
                    let idx = ((ny * w + nx) * 4) as usize;
                    sum_r += src[idx] as u32;
                    sum_g += src[idx + 1] as u32;
                    sum_b += src[idx + 2] as u32;
                    count += 1;
                }
            }

            let idx = ((y * w + x) * 4) as usize;
            pixels[idx] = (sum_r / count) as u8;
            pixels[idx + 1] = (sum_g / count) as u8;
            pixels[idx + 2] = (sum_b / count) as u8;
        }
    }
}

パフォーマンス比較

フィルター JavaScript Rust/Wasm 高速化
グレースケール(4K画像) 45ms 5ms 9倍
明るさ(4K) 40ms 4ms 10倍
ボックスブラー r=3(4K) 2,500ms 280ms 9倍
セピア(4K) 50ms 6ms 8倍

重要な最適化:インプレース変更

Rustは Uint8Arrayインプレースで変更します。データのコピーは不要です:

pub fn grayscale(pixels: &mut [u8]) {  // &mut = インプレースで変更
    // pixelsはCanvasのImageDataバッファそのもの
    // 変更はJS側に即座に反映される
}

これは最も高速なアプローチです。アロケーションもコピーもゼロです。

試してみよう

スターターコードには4つの画像フィルターが含まれています。実際のプロジェクトでは、Canvasの imageData.data をこれらの関数に直接渡して、結果を即座に確認できます。

試してみる

チャプタークイズ

すべての問題に正解してレッスンを完了しましょう