Technical Docs
Image Background Remover の設計
WASM/ONNXベースのAIモデルをブラウザ上で動作させ、画像データをサーバーに送信しない完全クライアントサイド型の背景除去ツールの設計・実装を解説します。
1. 設計思想 — なぜブラウザ完結なのか
背景除去ツールの実装方法として、サーバーサイドAPIを利用する方法(remove.bg、Clipdrop等)と、 ブラウザ内でAIを動作させる方法の2択があります。本ツールは後者を選択しました。
- プライバシー保護: ユーザーが選択した画像はネットワークに送出されません。すべての処理はユーザーのデバイス上で完結します。 個人写真や機密性の高い業務画像を扱う際も安心して使用できます。
- 無料・無制限: サーバーサイドAPIは従量課金が発生し、APIキーの管理コストもかかります。 WASMベースの処理はブラウザリソースのみで動作するため、枚数制限も課金も不要です。
- オフライン動作: 初回実行時にAIモデルをブラウザキャッシュ(Cache Storage)に保存します。 2回目以降はオフライン環境でも即座に起動できます。
トレードオフとして、初回のモデルダウンロード(数十MB)と、 サーバーGPUと比較して低速なCPU推論が生じます。 これらはプログレスバーによる可視化と、ブラウザキャッシュを活用した2回目以降の高速起動で緩和しています。
2. 技術アーキテクチャ
本ツールは2層で構成されています。Server Actions は使用しません(WASM処理がクライアントサイドに閉じているため不要)。
- 純粋関数層(
src/lib/tools/bg-remover.ts): ファイルのバリデーション(validateImageFile)とサイズ表示フォーマット(formatFileSize)を 副作用なしの純粋関数として実装します。 受け付けるMIMEタイプ(ACCEPTED_TYPES)と最大ファイルサイズ(MAX_FILE_SIZE_MB)を 定数としてエクスポートし、UIコンポーネント(DropZone)と純粋関数層の間で一元管理します。 - Client Component層: 3つのコンポーネントで構成されます。
BgRemoverClient.tsx— 状態管理の親コンポーネント。 処理フェーズ(idle → model-download → processing → done / error)をuseStateで管理し、各フェーズに応じた子コンポーネントを切り替えます。DropZone.tsx— ドラッグ&ドロップとファイル選択UIを担当します。ImageCompare.tsx— 処理前後の画像をスライダーで比較表示します。
設計上の注意点:@imgly/background-removal の呼び出しはLayer 1(純粋関数層)に含めていません。 WASM/ONNXランタイムへの依存があり副作用が生じるため、純粋関数の条件を満たせません。 また、Vitest(jsdom環境)でのテストも不可です。そのため、Layer 2(Client Component)内で 直接呼び出す設計としています。
3. WASM/ONNXパイプライン
背景除去の中核は @imgly/background-removal ライブラリが担います。 このライブラリは onnxruntime-web を使用してブラウザ内でONNXモデルを実行します。
- 動的インポートによるバンドル分割(
bundle-conditionalパターン):@imgly/background-removalはWASMバイナリを含む大容量のモジュールです。 ページ初回ロード時にバンドルすると起動が重くなるため、 ユーザーがファイルを選択した瞬間にawait import('@imgly/background-removal')で 動的インポートします。これにより、ツールを使わないユーザーへのコストが発生しません。 - プリロード最適化(
bundle-preloadパターン): DropZoneにマウスを乗せた瞬間(onMouseEnter)に同じ動的インポートを fire-and-forget で呼び出します。ユーザーがファイルを選択する前にモジュールのロードを 開始することで、処理開始までの体感時間を短縮します。 - モデル選択:
model: 'isnet'(フルFloat32精度)を指定しています。 ライブラリは3種類のモデルを提供しており、精度の高い順にisnet(フル精度)、isnet_fp16(半精度、デフォルト)、isnet_quint8(量子化)です。 背景と前景の境界判定精度を優先してフルモデルを採用しています。 - 進捗コールバックによるフェーズ判定:
progress(key, current, total)コールバックを利用して処理フェーズを判定します。keyがfetchまたはort:loadで始まる場合は 「モデルダウンロード」フェーズ、それ以外は「AI推論(背景除去)」フェーズとして それぞれ異なるUIを表示します。 - ObjectURLのライフサイクル管理:
URL.createObjectURL(file)で生成したURLは、使用後にURL.revokeObjectURL()で解放しないとメモリリークが発生します。useEffectのdeps(依存配列)に各URLを指定し、 URLが切り替わる際またはアンマウント時に自動解放するパターンを採用しています。
4. UIコンポーネント設計
本ツールは4つのフェーズそれぞれに最適化されたUIを提供します。
- DropZone(idleフェーズ): ドラッグ&ドロップとクリックによるファイル選択の両方に対応します。 子要素をまたぐ際に
DragLeaveが誤発火する問題をdragCounterRef(useRefでレンダーを発生させない)で解決しています。 counter をインクリメント/デクリメントし、ゼロになった時点でのみドラッグ状態を解除します。 - ローディングUI(model-download / processingフェーズ): 2フェーズで異なるUIを表示します。
model-downloadでは従来のプログレスバーとテキストを表示します。processing(AI推論中)では、オリジナル画像をblur-sm opacity-50でぼかしながら表示し、その上にスピナーと進捗テキストをオーバーレイします。 ユーザーは「自分の画像が処理されている」ことを視覚的に確認できます。 - ImageCompare スライダー(doneフェーズ): 処理前後の画像をドラッグ可能なスライダーで比較表示します。 After画像(チェッカーボード背景で透明度を可視化)を下層に配置し、 Before画像を上層で
clip-path: inset(0 {100-position}% 0 0)によりクリップします。 スライダー位置(position)はuseState(再レンダーに使うため必須)、 ドラッグ中フラグ(isDragging)はuseRef(再レンダー不要なため)で管理します。setPointerCaptureを使うことで、カーソルが要素外に出ても追跡を継続できます。 - 再処理ボタン: 結果に満足できない場合、同じ画像で再度AI処理を実行できます。 処理コアロジックを
runProcessing(file: File)に分離し、 ファイル受け取り(handleFile)と再処理(handleReprocess)の 両方から呼び出せる構造にしています。再処理時はoriginalUrlを変更しないため ObjectURLの無駄な生成と解放が発生しません。
5. バージョン整合性の落とし穴と解決策
実装中に発生した TypeError: r._OrtGetInputOutputMetadata is not a functionというエラーの原因と解決策を記録します。
- エラーの構造:
@imgly/background-removalは内部でonnxruntime-webを使用します。 AI推論セッションの生成時、JavaScriptバインディング(ローカルのnpmパッケージ)と WASMバイナリ(ライブラリのCDNから取得)のバージョンが一致しないと APIが存在しないためこのエラーが発生します。 - 根本原因:
@imgly/background-removal@1.7.0の peerDependency はonnxruntime-web@1.21.0(固定バージョン)と定められています。 しかしpackage.jsonに"onnxruntime-web": "^1.24.2"と記述したため、 より新しいv1.24.2がインストールされました。CDNが配信するWASMはv1.21.0向けにビルドされており、 v1.24.2のJSバインディングとAPIが不一致となりクラッシュしました。 - 解決策:
package.jsonのonnxruntime-webバージョン指定を"^1.24.2"(範囲指定)から"1.21.0"(固定)に変更します。^(キャレット)は「メジャーバージョンが同じなら更新を許可」する記法ですが、 WASM/Nativeバイナリを伴うライブラリでは互換性が厳密であるため、 peerDependencyの要求するバージョンに固定することが重要です。
6. 品質管理
本ツールは単体テストとE2Eテストの両方でカバーしています。
- 純粋関数テスト(Vitest)(
src/lib/tools/__tests__/bg-remover.test.ts):validateImageFileとformatFileSizeに対して21件のテストを実装しています。 JPEG・PNG・WebPの正常受け付け、GIF・PDF・空MIMEタイプの拒否、 上限ちょうど・上限超過のサイズ判定、エラーメッセージへのファイルサイズ埋め込みなどを網羅します。 WASM/AI処理はLayer 1に含めないことで、jsdom環境で高速・安定したテストが実現できています。 - E2Eテスト(Playwright)(
e2e/bg-remover.spec.ts): 5件のシナリオで主要なUIフローを検証します。 DropZone・対応形式ガイドの表示確認、 PDFアップロード時のバリデーションエラー表示、 エラー後の「やり直す」によるリセット、 21MBのサイズオーバーファイルのエラー表示をカバーします。 - 実際のAI処理はE2Eテスト対象外: 背景除去処理はWASMモデルのダウンロード(数十MB)と長時間のCPU推論を伴うため、 E2EテストのCIで実行することは現実的ではありません。 バリデーション・UI遷移・エラー表示といったAI処理に依存しないフローのみをE2Eでカバーし、 AI処理の品質は手動検証で担保しています。
role="alert"の衝突回避: Next.js のルートアナウンサー(id="__next-route-announcer__")はrole="alert"を持ち常にDOMに存在します。 E2Eテストでエラー要素を特定する際はpage.locator('[role="alert"]').filter({ hasText: "エラー" })のように.filter()で絞り込んでいます。