Real-ESRGAN系の超解像モデルを onnxruntime-web で動かし、画像を2倍・4倍に拡大する機能を作りました。デモは数時間で動きましたが、実際のユーザー画像で安定させるまでに時間がかかったポイントをまとめます。
なぜタイル分割が必要か
4倍拡大モデルに2000×1500の画像をそのまま入れると、出力は8000×6000、Float32のRGBテンソルで約576MBです。中間層はさらに大きく、GPUメモリが足りずにクラッシュします。
そこで入力を例えば128×128のタイルに分割し、1枚ずつ推論して結果を貼り合わせます。
継ぎ目(シーム)対策
単純に分割すると、タイル境界に格子状の線が出ます。畳み込みの受容野がタイルの外側を見られないためです。
対策は**オーバーラップ(パディング)**です。
const TILE = 128, PAD = 16;
for (let y = 0; y < h; y += TILE) {
for (let x = 0; x < w; x += TILE) {
const sx = Math.max(0, x - PAD), sy = Math.max(0, y - PAD);
const ex = Math.min(w, x + TILE + PAD), ey = Math.min(h, y + TILE + PAD);
const out = await infer(crop(img, sx, sy, ex, ey));
// 出力からパディング分(×scale)を切り落として中央部分だけ貼る
paste(out, (x - sx) * S, (y - sy) * S, TILE * S, TILE * S, x * S, y * S);
}
}
PADは8pxでも目立たなくなりますが、空や肌のようななだらかな領域では16px以上が安全でした。
WebGPUとWASMの使い分け
- WebGPUが使える環境では、タイル1枚あたり数百ms程度。
- WASMフォールバックでは数秒かかることもあるため、タイルを小さくして進捗を細かく表示します。
-
requestAdapter()がnullを返す環境(ブロックリスト対象のGPUなど)は意外と多いので、必ずフォールバックを用意します。
メモリリーク対策
onnxruntime-web のテンソルは、使い終わったら明示的に dispose() しないとGPUメモリが解放されないことがあります。ループ内で毎回破棄しないと、50タイル目あたりで落ちる、という現象に悩まされました。
また、出力は巨大なCanvasに直接書かず、OffscreenCanvas をWorkerで扱うとメインスレッドが固まりません。ブラウザごとにCanvasの最大サイズ制限(面積や一辺の長さ)があるので、4倍拡大後のサイズが制限を超えないかも事前に確認します。
期待値の調整
超解像は「なかった情報を生成する」処理です。文字や顔は、それらしく見えても元と違う形になることがあります。証明写真や書類の文字を拡大する用途では、その旨をUIに書いておくべきだと考えています。
参考
挙動の比較には、同じくブラウザ内で処理するNibboの画像高画質化ツールを使いました。モデルの初回ダウンロード後は、画像をアップロードせずに2倍・4倍を試せます。
— Vitalii