[Astro] #144 ONNX Runtime Web と Real-ESRGAN で構築するブラウザ完結型 AI 画像アップスケーラー — 実装記録
はじめに
ブラウザ上でサーバーサイドの API を一切介さず、WebAssembly(WASM)ベースの ONNX Runtime と超解像 AI モデル(Real-ESRGAN_x4plus_anime_6B.onnx)を用いて、イラストを高精度に 2 倍・4 倍アップスケールする Web アプリケーション「ONNX Upscaler v1」を構築しました。
外部の重い Python バックエンドやクラウド GPU サーバーに頼ることなく、クライアントの CPU(WASM マルチスレッド)のみで高速なニューラルネットワーク推論を実現しています。画像の前処理からテンソル変換、WASM セッションのライフサイクル管理、2X / 4X のスケーリング描画、さらにリアルタイムのコンソールログやプログレスオーバーレイを備えた 3 カラムインターフェースを統合しています。
| モジュール No. | モジュール名 | 主要機能・処理内容 |
|---|---|---|
| 01 | INFERENCE ENGINE | ONNX Runtime Web (WASM), WASI パス設定, マルチスレッド(1〜8 Threads)制御 |
| 02 | TENSOR PREPROCESS | HTMLImageElement からの Canvas 読み込み, 最大サイズ制限, 3チャンネル(RGB)分離正規化 |
| 03 | NEURAL NETWORK | Real-ESRGAN x4plus Anime モデルによる超解像推論, テンソルライフサイクル管理 (dispose) |
| 04 | POST-PROCESSING | 出力 Float32Array バッファのスケール補正, ImageData 変換, 2X / 4X スケーリング描画 |
| 05 | EXPORT PIPELINE | PNG (Lossless) / JPEG (品質スライダー付き) フォーマットでのダイレクトファイルダウンロード |
| 06 | UI & CONSOLE | 3 カラムレイアウト (左:設定/右:ログ), プログレスオーバーレイ, リアルタイムステータス追跡 |
スクリーンショット
変換中(プログレス表示)
変換終了
動画(GIF)
1. 全体アーキテクチャと処理フロー
ブラウザにロードした ONNX Runtime Web (ort.all.min.js) を介して、ローカルの ONNX モデルファイルを WASM 環境で実行するパイプラインを構築しました。
データフロー & 処理パイプライン
[ Target Image (.png/.jpg) ] ──► [ Canvas Resizer & Normalizer ] ──► [ Float32Array Tensor [1, 3, H, W] ]
│
▼
[ Real-ESRGAN WASM Session ] ◄── (/models/RealESRGAN_x4plus_anime_6B.onnx)
│
▼
[ Raw Output Tensor Data ] ──► [ Post-Processing & Scale Correction ] ──► [ Canvas2D Output & Export ]
画像がドロップまたは選択されると、自動的にサイズが制限(最大512px等)内に調整され、RGB各チャンネルが 0.0〜1.0 の浮動小数点数テンソルへと変換されて WASM セッションに投入されます。
2. テンソル前処理と RGB チャネルの分離実装
Real-ESRGAN モデルの入力仕様([1, 3, H, W] の NCHW 形式、float32)に合わせ、HTML5 Canvas から取得したピクセルデータ(RGBA)をプレーンな Float32Array へ正確に分解・正規化します。
ピクセル配列から NCHW テンソルへの変換
const img = imgRef.current;
const MAX_SIZE = 512;
let width = img.naturalWidth;
let height = img.naturalHeight;
if (width > MAX_SIZE || height > MAX_SIZE) {
const ratio = Math.min(MAX_SIZE / width, MAX_SIZE / height);
width = Math.round(width * ratio);
height = Math.round(height * ratio);
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const { data } = imageData;
const floatData = new Float32Array(3 * width * height);
for (let i = 0; i < width * height; i++) {
floatData[i] = data[i * 4] / 255.0; // R チャンネル
floatData[width * height + i] = data[i * 4 + 1] / 255.0; // G チャンネル
floatData[2 * width * height + i] = data[i * 4 + 2] / 255.0; // B チャンネル (インデックスタイポ修正済み)
}
inputTensor = new ort.Tensor('float32', floatData, [1, 3, height, width]);
3. ONNX Runtime Web による WASM マルチスレッド推論
CDN 経由で読み込んだ ort グローバルオブジェクトを利用し、WASM の実行パスとスレッド数(1〜8スレッド)を動的に指定してセッションを初期化します。
推論セッションの生成と実行フロー
const ort = (window as any).ort;
ort.env.wasm.wasmPaths = '[https://cdn.jsdelivr.net/npm/[email protected]/dist/](https://cdn.jsdelivr.net/npm/[email protected]/dist/)';
ort.env.wasm.numThreads = numThreads; // ユーザー設定の並列スレッド数
let session = await ort.InferenceSession.create(
'/models/RealESRGAN_x4plus_anime_6B.onnx',
{ executionProviders: ['wasm'] }
);
const feeds: Record<string, any> = {};
feeds[session.inputNames[0]] = inputTensor;
const results = await session.run(feeds);
const outputTensor = results[session.outputNames[0]];
const outData = outputTensor.data as Float32Array;
4. ポストプロセッシングとマルチスケール描画
モデル自体はネイティブで 4 倍の解像度を出力しますが、UI 側でユーザーが 2 倍(2X)または 4 倍(4X)を選択できるよう、中間オフスクリーンキャンバスを経由したスケーリング処理を実装しています。
// 出力テンソルの値を 0-255 の範囲にマッピング
let maxVal = 0;
for (let i = 0; i < Math.min(1000, outData.length); i++) {
if (!Number.isNaN(outData[i]) && outData[i] > maxVal) maxVal = outData[i];
}
const scale = maxVal <= 1.5 ? 255.0 : 1.0;
const totalPixels = raw4xWidth * raw4xHeight;
for (let i = 0; i < totalPixels; i++) {
outImageData.data[i * 4] = Math.min(255, Math.max(0, Math.round((outData[i] || 0) * scale)));
outImageData.data[i * 4 + 1] = Math.min(255, Math.max(0, Math.round((outData[totalPixels + i] || 0) * scale)));
outImageData.data[i * 4 + 2] = Math.min(255, Math.max(0, Math.round((outData[2 * totalPixels + i] || 0) * scale)));
outImageData.data[i * 4 + 3] = 255;
}
tempCtx.putImageData(outImageData, 0, 0);
// ユーザーが選択した指定倍率(2x / 4x)に合わせて描画
const finalWidth = width * scaleFactor;
const finalHeight = height * scaleFactor;
const outCtx = outCanvas.getContext('2d')!;
outCtx.imageSmoothingEnabled = true;
outCtx.imageSmoothingQuality = 'high';
outCtx.drawImage(tempCanvas, 0, 0, raw4xWidth, raw4xHeight, 0, 0, finalWidth, finalHeight);
5. デバッグ記録: テンソルインデックスのタイポとレイアウトの競合
開発過程において、いくつかの重要な不具合とその解決を行いました。
- Bチャンネルのインデックス指定ミス(タイポ):
初期実装で
floatData[2 * width * height + i] = data[2 * width * height + i] / 255.0;と記述していたため、参照元バッファのオフセットがずれ、出力画像の色味が異常になる現象が発生しました。正しくはdata[i * 4 + 2]を参照するように修正し、正しい色空間を復元しました。 - CSS レイアウトにおける上下の隙間:
Astro の共通レイアウトとコンポーネント側の
.appスタイルでmargin-top/bottomとheight: calc(100vh - 140px)が二重に競合し、画面上部に不要な余白が生まれていました。min-height: calc(100vh - 150px)に一本化することで、ビューポートに美しく収まるレイアウトへ改善しました。
6. まとめ
- ブラウザ完結型の AI 超解像: サーバー負荷ゼロで、WASM によりクライアント端末の CPU パワーをフル活用したリアルタイム画像アップスケールを実現。
- 高速なマルチスレッド処理: ONNX Runtime Web のスレッド数を切り替えることで、環境に応じたパフォーマンスチューニングが可能。
- 堅牢なメモリ管理と UI 統合: テンソルの確実な
dispose処理や、プログレスバー付きオーバーレイ、コンソールログによる高いデバッグ性を備えた実用的なツールに仕上げました。
イラストやピクセルアートのブラウザ内高速高画質化パイプラインとして、今後さらにモデルバリエーションの拡充やタイル分割処理の導入を進めていきます。