Algorithm & UI/UX
Canvasで実現する高速・高精度な
線画抽出アルゴリズムの裏側
写真から塗り絵用の「線画」を作るプロセス。そこには、モバイルSafariの描画制限を回避する工夫や、数百万画素を瞬時に処理するためのメモリ効率化の知恵が詰まっています。
1. モバイルSafariのバグを回避する「疑似ボックスブラー」
Canvas APIには便利な ctx.filter が存在しますが、iOS版Safariなど一部の環境では動作が不安定だったり、著しいパフォーマンス低下を招くことがあります。本ツールでは、線の太さを調整するために「画像を8方向に微細にずらして重ね描きする」という古典的ながら確実な手法を採用しました。
これにより、ブラウザのネイティブフィルタに依存せず、すべてのデバイスで一貫した「線の太さ(ぼかし)」を再現することが可能になりました。
// フィルタを使わずに「ぼかし」を再現する多重描画ハック
const offsets = [
[-offsetVal, -offsetVal], [0, -offsetVal], [offsetVal, -offsetVal],
[-offsetVal, 0], [offsetVal, 0],
[-offsetVal, offsetVal], [0, offsetVal], [offsetVal, offsetVal]
];
// 透明度を調整しながら重ね合わせる
ctx.globalAlpha = // 独自の透過係数;
for (let [ox, oy] of offsets) {
ctx.drawImage(originalImage, ox, oy, width, height);
}
ctx.globalAlpha = 1.0; 2. TypedArrayによるLuma(輝度)計算の高速化
エッジ検出を行う際、ピクセルごとにRGB値を参照して計算を行うとオーバーヘッドが大きくなります。本ツールでは、処理の初手で全ピクセルの輝度を Float32Array にキャッシュする「先行計算パス」を設けています。
計算結果を型付き配列に保持することで、メインの輪郭抽出ループ内での計算コストを最小限に抑え、高解像度な写真でも数ミリ秒で処理を完了させています。
// 全ピクセルの輝度を先に計算してキャッシュ
const lumaData = new Float32Array(width * height);
for (let i = 0; i < width * height; i++) {
const idx = i * 4;
// 人間の視覚特性に基づいた重み付けで輝度を算出
lumaData[i] = (r * RATIO_R) + (g * RATIO_G) + (b * RATIO_B);
}
// メインのエッジ検出ループでは、この配列を参照するだけで済む
for (let y = 0; y < height; y++) {
// ここに隣接ピクセルとの輝度差を比較する計算処理が入ります
} 3. iPhoneユーザーのためのHEIC動的変換
iPhoneの標準形式であるHEICは、そのままではブラウザの <img> タグで表示できません。本ツールでは heic2any ライブラリを導入していますが、初期ロードを重くしないよう、HEICファイルが選択された時のみ動的にモジュールをインポートする設計にしています。
// 必要な時だけライブラリをロードするオンデマンド・インポート
if (file.name.match(/\.(heic|heif)$/i)) {
const { default: heic2any } = await import('heic2any');
const blob = await heic2any({
blob: file,
toType: "image/jpeg",
quality: // 独自の変換品質設定
});
// 変換後のBlobを処理へ渡す
} Developer's Note
このツールの最大の特徴は、サーバーに1ミリもデータを送らずに「ブラウザ内」で完結することです。
フロントエンドエンジニアとしては、ブラウザのメインスレッドをロックさせないように MAX_SIZE の制限を設けつつ、いかに「見た目の劣化」を抑えてエッジを抽出するかのバランス調整に心血を注ぎました。
特に感度(しきい値)の計算式は、写真の明暗差が激しくても線が途切れないよう、何度も猫や風景の写真を使ってチューニングを行っています。ぜひ、お手元の写真で「プロっぽい線画」への変換を楽しんでいただければ幸いです。