猫の手ツール

Algorithm & UI/UX

全ピクセル走査をブラウザで回す
魚眼マッピングの最適化戦略

AIによる顔検知と、ピクセル単位の高度な座標変換をいかに両立させるか。Canvas 2Dでの高速レンダリングと正規化座標による汎用性の高い設計に注目します。

1. 魚眼歪曲のピクセル逆引きマッピング

本ツールでは、画像を直接変形させるのではなく、描画先の各ピクセルに対して「元画像のどの位置から色を拾ってくるか」を計算する、逆引きマッピングを採用しています。

この手法のメリットは、ピクセルの欠損を防ぎ、アンチエイリアス処理がなくても滑らかな歪みを実現できる点です。特定の半径(R)内において、中心からの距離(dist)に応じた冪乗関数(Math.pow)を用いることで、中心部ほど大きく膨らむ魚眼特有の視覚効果を生み出しています。

// ピクセル走査と座標変換のコア
function applyDistortion(ctx, cx, cy, boxSize) {
    const R = (boxSize / 2) * // 独自のチューニング値;
    const r2 = R * R;

    for (let y = minY; y <= maxY; y++) {
        for (let x = minX; x <= maxX; x++) {
            const dx = x - cx;
            const dy = y - cy;
            const dist2 = dx * dx + dy * dy;

            if (dist2 < r2) {
                // 魚眼・膨張の計算ロジック
                // ここに正規化距離とK値を用いた座標変換の計算処理が入ります
                const srcX = // 元画像の参照X座標算出
                const srcY = // 元画像の参照Y座標算出

                // 計算した座標からピクセル情報を転送
                const dstIdx = (y * w + x) * 4;
                const srcIdx = (Math.floor(srcY) * w + Math.floor(srcX)) * 4;
                // Uint8ClampedArray への高速コピー
            }
        }
    }
}

2. requestAnimationFrameによる描画の非同期制御

全ピクセルの走査は、数百万画素の画像では非常に重い処理となります。スライダーを動かすたびにこの計算を走らせると、ブラウザのメインスレッドを占有し、UIがフリーズしてしまいます。

そこで、`requestAnimationFrame`(RAF)を利用した描画のデバウンス処理を実装しています。現在のレンダリングが完了する前に次のリクエストが来ても、直前のリクエストをキャンセルすることで、最新の状態のみを効率的に描画し、入力遅延(Jank)を最小限に抑えています。

let renderRAF = null;

function requestRender() {
    // 進行中の描画予約があればキャンセル
    if (renderRAF) cancelAnimationFrame(renderRAF);
    
    // 次の描画タイミングに予約を入れ、負荷を平滑化
    renderRAF = requestAnimationFrame(renderCanvas);
}

// スライダー操作などのイベント
slider.addEventListener('input', () => {
    // 値の更新だけ行い、実際の重い計算はRAFに委ねる
    state.strength = // ...
    requestRender();
});

3. 正規化座標(0.0-1.0)による抽象化管理

検知された顔の座標を「ピクセル絶対値」ではなく、「画像全体に対する割合」で保持しています。

この設計により、内部処理用の高解像度キャンバス(最大2000px)と、プレビュー用の小サイズキャンバス、あるいは異なるアスペクト比のデバイスであっても、同一の座標データで破綻なく処理が可能です。また、スマホの縦横回転によるリサイズ時にも、再検知なしで即座にエフェクトを復元できる柔軟性を確保しています。

// 座標データの構造例
let currentBoxes = [
    {
        xCenter: 0.5, // 常に0.0〜1.0で管理
        yCenter: 0.5,
        width: 0.1,   // 画像幅に対する比率
        height: 0.1
    }
];

// レンダリング時の物理座標変換
function getPixelCoords(box, canvasWidth, canvasHeight) {
    return {
        px: box.xCenter * canvasWidth,
        py: box.yCenter * canvasHeight,
        radius: // ...比率に基づいた計算
    };
}

Developer's Note

このツールで最も苦労したのは、「いかにスマホでフリーズさせないか」という点です。 特にピクセル操作はJavaScriptのオーバーヘッドが大きいため、当初は複雑な補間処理を入れていましたが、あえて「最近傍補間」に絞り、その分を計算アルゴリズムの高速化に回すことで、ミドルレンジのスマホでも軽快に動く操作感を実現しました。

また、AIの自動検知を「絶対的な正解」とせず、あくまで初期値として提供し、人間がタップで補正できる「マニュアルの心地よさ」を両立させたのも実装上のこだわりです。