[Astro] #146 ONNX Runtime Web + RMBG-1.4 による Background Remover 実装記録と断念の理由
はじめに
ONNX Runtime Web (WASM) と RMBG-1.4 モデルを使い、ブラウザ完結型の背景除去ツールを実装しようとした記録です。最終的には精度面で断念しましたが、技術的に得たものは多く、次回実装への参照として残します。
断念の結論を先に書くと:
- ライブラリ選定で大幅に時間を消費した
- RMBG-1.4 ONNX モデルの前処理を正しく実装できたが、オンラインサービスと同等の精度には届かなかった
- 精度差の原因は後処理(エッジ精緻化・アルファマット補正)の欠如と思われるが、ブラウザ WASM 環境での実装コストが高い
スクリーンショット
1. 当初の方針と挫折
1.1 最初の候補: @imgly/background-removal
最初に選んだのは @imgly/background-removal。npm パッケージで removeBackground() を呼ぶだけという手軽さが魅力だった。
import { removeBackground } from '@imgly/background-removal';
const blob = await removeBackground(imageSrc, {
publicPath: '...',
model: 'small',
});
しかし以下の問題が連鎖して詰んだ。
問題1: onnxruntime-web のバージョン競合
プロジェクトには [email protected](ONNX Upscaler が使用)が入っていたが、@imgly/[email protected] は [email protected] を要求する。npm install が ERESOLVE で失敗し、--legacy-peer-deps でも解決不可。
問題2: CDN の resources.json が空
publicPath を省略して CDN 自動取得にしようとしたが、@imgly/[email protected] の jsdelivr CDN 上の resources.json の中身が {} (2バイトの空JSON)。モデルファイルが CDN に正しく配置されていない。
問題3: ESM import の制約
index.mjs をブラウザで動的 import すると Failed to resolve module specifier "onnxruntime-web" エラー。ベアインポートはブラウザの ESM では解決できない。is:inline スクリプトの制約とも組み合わさり、回避策がない。
教訓: @imgly/background-removal は npm バンドラー環境(Vite/webpack)での使用を前提としており、Astro の is:inline 環境や CDN 直接利用には対応していない。バージョン 1.7.0 の CDN 配布構造が壊れている。
1.2 @huggingface/transformers への切り替え試み
次に @huggingface/transformers を試みたが、--legacy-peer-deps でインストールしたことで react-dom/client の createRoot エクスポートが壊れ、React コンポーネント全体がレンダリング不能になった。
SyntaxError: The requested module 'react-dom/client' does not provide
an export named 'createRoot'
npm install react react-dom でも同様の競合が起き、環境を元に戻すのに追加のコストがかかった。
2. 採用した構成: AiUpscaler と同一アーキテクチャ
依存関係の問題を回避するため、既存の ONNX Upscaler(AiUpscaler.tsx)と全く同じ構成に切り替えた。
background-remover.astro
└── <script is:inline src="onnxruntime-web CDN"> // グローバル ort オブジェクト
└── <BackgroundRemover client:only="react" />
BackgroundRemover.tsx
└── const ort = (window as any).ort; // グローバルから取得
└── ort.InferenceSession.create('/libs/rmbg-1.4.onnx')
onnxruntime-web の WASM バイナリは CDN から読み込み、モデルファイルは /public/libs/ に自前配置する方式。npm の依存関係を完全に回避できる。
モデルファイル
| モデル | サイズ | 取得元 |
|---|---|---|
rmbg-1.4.onnx | 176MB | HuggingFace briaai/RMBG-1.4 |
rmbg-1.4-quantized.onnx | 44MB | HuggingFace briaai/RMBG-1.4 |
176MB のフルモデルは精度優先、44MB の量子化モデルは速度優先。UI でユーザーが選択できるようにした。
curl -L "https://huggingface.co/briaai/RMBG-1.4/resolve/main/onnx/model.onnx" \
-o public/libs/rmbg-1.4.onnx
curl -L "https://huggingface.co/briaai/RMBG-1.4/resolve/main/onnx/model_quantized.onnx" \
-o public/libs/rmbg-1.4-quantized.onnx
3. 前処理の実装
RMBG-1.4 は ImageNet の mean/std で正規化した NCHW テンソルを入力として受け取る。
3.1 letterbox リサイズ
最初の実装では単純な引き伸ばし(アスペクト比無視)を使っていた。これが精度低下の大きな原因の一つ。正しくは letterbox 方式でアスペクト比を保ちながら正方形にパディングする。
const SIZE = inputSize; // 1024 or 512
const inCanvas = document.createElement('canvas');
inCanvas.width = SIZE;
inCanvas.height = SIZE;
const inCtx = inCanvas.getContext('2d')!;
// letterbox: 白でパディング
const scale = Math.min(SIZE / origW, SIZE / origH);
const scaledW = Math.round(origW * scale);
const scaledH = Math.round(origH * scale);
const offsetX = Math.round((SIZE - scaledW) / 2);
const offsetY = Math.round((SIZE - scaledH) / 2);
inCtx.fillStyle = '#ffffff';
inCtx.fillRect(0, 0, SIZE, SIZE);
inCtx.drawImage(img, offsetX, offsetY, scaledW, scaledH);
3.2 ImageNet 正規化
// mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
const MEAN = [0.485, 0.456, 0.406];
const STD = [0.229, 0.224, 0.225];
const floatData = new Float32Array(3 * SIZE * SIZE);
const nPix = SIZE * SIZE;
for (let i = 0; i < nPix; i++) {
floatData[i] = (data[i * 4] / 255.0 - MEAN[0]) / STD[0]; // R
floatData[nPix + i] = (data[i * 4 + 1] / 255.0 - MEAN[1]) / STD[1]; // G
floatData[2 * nPix + i] = (data[i * 4 + 2] / 255.0 - MEAN[2]) / STD[2]; // B
}
const inputTensor = new ort.Tensor('float32', floatData, [1, 3, SIZE, SIZE]);
NCHW(Batch=1, Channels=3, Height, Width)のプレーナー形式で並べる。
4. 後処理の実装
RMBG-1.4 の出力は [1, 1, H, W] の Float32Array。値はシグモイド済みの 0〜1 の確率マスク。
4.1 最初の実装(失敗): nearest-neighbor 直接サンプリング
// NG: 最近傍補間で直接ピクセルをサンプリング
for (let y = 0; y < origH; y++) {
for (let x = 0; x < origW; x++) {
const mx = Math.round(x * maskW / origW);
const my = Math.round(y * maskH / origH);
const alpha = maskData[my * maskW + mx];
outPix[(y * origW + x) * 4 + 3] = Math.round(alpha * 255);
}
}
エッジがジャギジャギになり実用に耐えない。
4.2 改善した実装: Canvas drawImage バイリニア補間
Canvas の drawImage が内部でバイリニア補間を行うことを利用する。マスクを一度 Canvas に描いてから drawImage でリサイズする。
// Step1: マスクをモデル出力サイズの Canvas に描く
const maskCanvas = document.createElement('canvas');
maskCanvas.width = maskW;
maskCanvas.height = maskH;
const maskCtx = maskCanvas.getContext('2d')!;
const maskImageData = maskCtx.createImageData(maskW, maskH);
for (let i = 0; i < maskData.length; i++) {
const alpha = Math.round(Math.min(1, Math.max(0, maskData[i])) * 255);
maskImageData.data[i * 4] = 255; // R
maskImageData.data[i * 4 + 1] = 255; // G
maskImageData.data[i * 4 + 2] = 255; // B
maskImageData.data[i * 4 + 3] = alpha; // A = マスク値
}
maskCtx.putImageData(maskImageData, 0, 0);
// Step2: drawImage でバイリニア補間リサイズ
const scaledMaskCanvas = document.createElement('canvas');
scaledMaskCanvas.width = origW;
scaledMaskCanvas.height = origH;
const scaledCtx = scaledMaskCanvas.getContext('2d')!;
scaledCtx.drawImage(maskCanvas, 0, 0, origW, origH);
const scaledMask = scaledCtx.getImageData(0, 0, origW, origH);
// Step3: 元画像のアルファチャンネルにマスクを適用
outCtx.drawImage(img, 0, 0, origW, origH);
const outImageData = outCtx.getImageData(0, 0, origW, origH);
for (let i = 0; i < outImageData.data.length; i += 4) {
outImageData.data[i + 3] = Math.round(
outImageData.data[i + 3] * (scaledMask.data[i + 3] / 255)
);
}
outCtx.putImageData(outImageData, 0, 0);
nearest-neighbor より明らかにエッジが滑らかになったが、オンラインサービスとの差は残った。
5. 結果と精度の比較
| 条件 | 結果 |
|---|---|
| 実写・単色背景(白・ベージュ) | ある程度切り抜ける |
| 実写・複雑背景 | 輪郭が荒い、足元等に残留あり |
| イラスト・白背景 | ある程度切り抜ける |
| Live2D / 3D CG スクリーンショット | 切り抜けない(CG特有の陰影・反射が誤認識) |
| オンラインサービス(同モデル)との比較 | 明確に劣る |
オンラインサービスが良好な結果を出せている理由は、後処理にエッジ精緻化・ガウシアンブラー・アルファマット補正を重ねていると推測される。これらをブラウザ WASM 環境で実装するコストは高く、今回は断念の判断をした。
6. 断念の理由と次回への知見
断念の理由
- RMBG-1.4 単体では、ブラウザ WASM 環境での精度がオンラインサービスに大幅に劣る
- 差を埋めるには後処理(モルフォロジー演算・アルファマット補正)が必要で、ブラウザ実装コストが高い
- 量子化モデル(44MB)は精度が低すぎ、フルモデル(176MB)はデプロイ制約(Cloudflare Pages 等のファイルサイズ上限)の問題がある
次回実装への知見
前処理で必ず守ること:
- letterbox パディングでアスペクト比を保つ(引き伸ばし禁止)
- ImageNet mean/std で正規化(
mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) - NCHW プレーナー形式でテンソルを作る
後処理で追加すべきこと:
- Canvas drawImage バイリニア補間でマスクをリサイズ
- アルファチャンネルへのガウシアンブラー(エッジ平滑化)
- しきい値処理(0.5 以下を完全透明に)
ライブラリ選定の教訓:
@imgly/background-removalは Vite/webpack バンドラー前提。Astrois:inline環境では使えない- CDN 配布構造を
curl -Iで事前確認してから採用を決める - 既存ツールと
onnxruntime-webのバージョンが競合する場合は ONNX Runtime をグローバルで読む方式に統一する
モデルについて:
- RMBG-1.4 の量子化モデル(44MB)は精度が低く実用的でない
- フルモデル(176MB)はデプロイコストが重い
- 代替として RMBG-2.0 や BiRefNet-lite の ONNX モデルを検討する価値がある
7. ファイル構成(参照用)
実装ファイルは削除せず保存してある。次回実装時の参照として使える。
src/pages/background-remover.astro
src/components/tools/BackgroundRemover.tsx
モデルファイルは容量の関係で削除済み。再取得は以下のコマンドで可能:
# フルモデル (176MB)
curl -L "https://huggingface.co/briaai/RMBG-1.4/resolve/main/onnx/model.onnx" \
-o public/libs/rmbg-1.4.onnx
# 量子化モデル (44MB)
curl -L "https://huggingface.co/briaai/RMBG-1.4/resolve/main/onnx/model_quantized.onnx" \
-o public/libs/rmbg-1.4-quantized.onnx