[Astro] #146 ONNX Runtime Web + RMBG-1.4 による Background Remover 実装記録と断念の理由

[Astro] #146 ONNX Runtime Web + RMBG-1.4 による Background Remover 実装記録と断念の理由

はじめに

ONNX Runtime Web (WASM) と RMBG-1.4 モデルを使い、ブラウザ完結型の背景除去ツールを実装しようとした記録です。最終的には精度面で断念しましたが、技術的に得たものは多く、次回実装への参照として残します。

断念の結論を先に書くと:

  • ライブラリ選定で大幅に時間を消費した
  • RMBG-1.4 ONNX モデルの前処理を正しく実装できたが、オンラインサービスと同等の精度には届かなかった
  • 精度差の原因は後処理(エッジ精緻化・アルファマット補正)の欠如と思われるが、ブラウザ WASM 環境での実装コストが高い

スクリーンショット

[Astro] #146 ONNX Runtime Web + RMBG-1.4 による Background Remover 実装記録と断念の理由 [Astro] #146 ONNX Runtime Web + RMBG-1.4 による Background Remover 実装記録と断念の理由

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/clientcreateRoot エクスポートが壊れ、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.onnx176MBHuggingFace briaai/RMBG-1.4
rmbg-1.4-quantized.onnx44MBHuggingFace 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 等のファイルサイズ上限)の問題がある

次回実装への知見

前処理で必ず守ること:

  1. letterbox パディングでアスペクト比を保つ(引き伸ばし禁止)
  2. ImageNet mean/std で正規化(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
  3. NCHW プレーナー形式でテンソルを作る

後処理で追加すべきこと:

  1. Canvas drawImage バイリニア補間でマスクをリサイズ
  2. アルファチャンネルへのガウシアンブラー(エッジ平滑化)
  3. しきい値処理(0.5 以下を完全透明に)

ライブラリ選定の教訓:

  • @imgly/background-removal は Vite/webpack バンドラー前提。Astro is: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