AI Dev Lab
BGSnap

BGSnap ができるまで — ブラウザ内ONNXで画像の背景を切り抜く設計

@imgly/background-removal を使って画像の背景透過をクライアント完結で実現するWebサービスの設計記録。WASM推論パイプライン、UX工夫、メディア処理ラボの3本目の位置づけを解説します。

·decision改善·stage公開中

BGSnap ができるまで — ブラウザ内ONNXで画像の背景を切り抜く設計

BGSnap は、画像の背景を 一切サーバーに送らずに 透過するWebサービスです。 ONNX Runtime Web + WebAssembly でブラウザに被写体抽出モデルを呼び込み、 アップロードを完全に省きました。voice-scribe (音声) と clip-cast (動画) に続く、メディア処理ラボの3本目の柱です。

なぜこの形にしたか

「背景を透過するために、画像をクラウドにアップロードする」 のは技術的にはともかく、心理的にとても引っかかります。 プロフィール写真、商品写真、子供の写真、本人確認書類の切り抜き — これらを得体の知れないSaaSにアップしている人がどれだけ自覚的に同意しているか怪しいです。

WebAssembly + ONNX Runtime Web の組み合わせがブラウザでまともに動くようになって、 「ML推論をブラウザ内で完結させる」 構成が現実的になりました。 @imgly/background-removal はそれを「3行で呼べる」ところまで整えたOSSライブラリです。

このサービスは、その構成を そのまま製品の体感に翻訳した ことが本体です。何も特別なアルゴリズムは無く、ただ「アップロードしない」 を一発で伝える形に落とし込みました。

visual direction

フォトスタジオ / ライトボックス。

  • 深いプラム (#0c0716) を背景に、エレクトリックバイオレット (#a259ff) を主アクセント
  • ホットマゼンタ (#ff3dee) との 135° グラデーションを「切り抜き完了」の象徴に使う
  • ヒーローのタイトルは「一瞬で消す」だけグラデーション化、他はフラット
  • スポットライト風の上からの淡い発光を入れて、撮影スタジオ感を出す
  • voice-scribe (mint cyan / 縦バー / オシロ) と clip-cast (amber / 横タイムライン / 編集機) に対して、 こちらは紫 / フォトスタジオ という温度・形ともに別軸

実装の見どころ

1. ライブラリ依存を最小限に薄く包む

removeBackground(file) を呼ぶだけで結果の Blob が返るくらい、 @imgly のライブラリは綺麗に出来ています。 bgRunner.ts で薄くラップして、

  • 動的import (_libPromise で1回だけロード)
  • progress コールバックで kind: load / fetch / compute / done を UI に流す
  • 結果の ImageBitmap から width/height を測る
  • 入力/出力サイズと推論時間を一緒に返す

の最低限だけ持たせました。

const blob = await rb(input, {
 output: { format: "image/png", quality: 1 },
 progress: (key, current, total) => onProgress({ kind: "compute", ratio: current / total }),
})

これで API 側の都合が変わってもラップ層だけで吸収できます。

2. before/after compare slider

被写体抽出の結果を見せるのに、左右並べるよりも 「同じ位置で1枚の画像が透ける」 体験のほうがインパクトが強い。 Pointer Events で実装した可動式スライダーで、ハンドルをドラッグするとマスクされた境界が左右に動きます。

実装ポイント:

  • 後ろに before 画像を常時表示
  • 前面に after 画像を width: ${pct}% の overflow:hidden コンテナで重ね、 内側の img を width: 100vw; max-width: none にして 位置がぶれない ようにする
  • ハンドルは pointermove で世界座標→相対座標→pct に変換

3. backdrop chips で出力プレビューの色を切替

透過 PNG は、表示する背景色で印象が大きく変わります。 そこでステージの背景を チェッカー / 白 / 黒 / 紫 の4択でその場切替できるようにしました。 商品写真なら白、SNSアイコンなら紫、というふうにユーザーが「どこで使うか」を即座に確かめられます。

4. 動的 import + ssr:false

Next.js 16 の Server Component から @imgly/background-removal を import するとSSR時に死にます。 ContactForm / VoiceScribe / ClipCast と同じく、loader でラップして dynamic({ ssr: false }) を強制しています。

const BGSnapApp = dynamic(() => import("./BGSnapApp").then((m) => m.BGSnapApp), {
 ssr: false,
 loading: () => null,
})

苦労したところ

  • 初回モデルDLが ~80MB。 progress を見せないと「動かない」誤解を生むので、 推論前にローダーUIを出すフローを最初から組んだ。
  • 巨大画像 (>8000px) のメモリ占有。 短辺で5000pxを目安にFAQに明記。ライブラリ側にダウンサンプリングも入っているが、限界はある。
  • PNG出力の Blob 取り扱いBlobPart の型と ArrayBufferLike の所有権周りで一度ハマったが、 入出力とも Blob オブジェクトを直接受け渡す形に統一して回避。

今後の拡張

  • 複数オブジェクト分離: 1枚に複数被写体がある場合に切り分ける選択UI
  • エッジ精度の改善モード: 別モデルへの切替トグル (精度↔速度のトレードオフ)
  • 影 (drop shadow) 復元オプション: アイコンや商品写真の自然さを上げる
  • バッチ処理: 複数ファイルをドロップ→キュー実行
  • clip-cast / voice-scribe との連携: 動画フレームから切り抜き → サムネ生成、字幕音声から無人ナレ動画への合体など

このサービスから言えること

「メディア (音声・動画・画像) を扱う処理は全部する」 という主張を、3本目で実証しました。 これでサイトとしての立ち位置がはっきりした気がします。 「アップロードしないと使えない」現状のメディア系SaaSのほとんどは、 そろそろの代替が技術的に成立する時代に入ったということです。

[ ./next_action ]

読んだら、 BGSnap を実際に動かす。

この開発ログは BGSnap をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — 文章embeddingsで意味検索をブラウザ内に

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →