ガイド 第25

スピーカー切り抜き

pluggable backend と canvas フォールバック

@event/visuals

スピーカー写真の背景除去は「重い ONNX モデルを積むか / 何も載せないか」の二択になりがちだが、@event/visuals契約と実装を分離することでどちらも成立させる。「透過 PNG を返す」契約 (CutoutBackend) だけを固定し、抜き方は差し替え可能にする。同梱するのは依存ゼロで実際に動く canvas フォールバック 1 本、production 品質の MODNet / @imgly は差し込み口だけを用意し実体は未同梱とする。

CutoutBackend 契約

export type CutoutInput = ImageBitmap | HTMLCanvasElement | Blob;

export interface CutoutBackend {
  readonly id: string;
  removeBackground(input: CutoutInput, opts?: CutoutOpts): Promise<Blob>;   // 透過 PNG Blob
}

守るのは 1 点だけ ——「入力を受けて透過 PNG の Blob を返す」。これだけ守れば canvas でも ONNX でも WASM でも同じ穴に差せる。UI 側は契約にしか依存しないので、backend を差し替えても無改修で乗り換わる。

ディスパッチャは既定 backend を canvas fallback に固定する:

export function removeBackground(
  input: CutoutInput,
  backend: CutoutBackend = canvasLumaKeyBackend,   // 既定 = zero-dep
  opts?: CutoutOpts,
): Promise<Blob> {
  return backend.removeBackground(input, opts);
}

canvas フォールバックの原理(本物・動く)

canvasLumaKeyBackend はブラウザ内蔵の canvas 2D だけで動くカラーキー方式。新規 npm 依存はゼロ:

  1. 入力を canvas に描き、getImageData で RGBA 画素を取り出す
  2. 枠(端)の画素をリング状にサンプルして背景キー色を推定する(被写体は中央・背景は縁に多い、という自然写真の前提)。opts.keyColor で明示指定も可
  3. 各画素とキー色の RGB ユークリッド距離 d を出し、alpha を書き換える:

- d ≤ threshold → alpha 0(完全透過) - threshold < d < threshold + featheralpha × (d - threshold) / feather で線形羽化 - それ以上 → 元の alpha を保持(= 被写体)

  1. putImageDatacanvas.toBlob("image/png") で透過 PNG を返す
for (let i = 0; i < data.length; i += 4) {
  const dr = (data[i] ?? 0) - kr, dg = (data[i + 1] ?? 0) - kg, db = (data[i + 2] ?? 0) - kb;
  const dist = Math.sqrt(dr * dr + dg * dg + db * db);
  if (dist <= threshold) data[i + 3] = 0;
  else if (dist < threshold + feather) data[i + 3] = Math.round(((data[i + 3] ?? 255) * (dist - threshold)) / feather);
}

単色〜ほぼ単色の背景(スタジオ幕・白ホリ・単色パネル前)では十分機能する。完璧ではない——複雑な背景や髪の毛の半透明な毛先は抜けない。それは production backend の領分であり、canvas fallback は「依存ゼロで dev で今すぐ透過が出る」デモ用と割り切る。

opts(しきい値と羽化)

type CutoutOpts = {
  threshold?: number;   // 背景キーからの色距離しきい値 (0–441)。既定 72。大きいほど強く抜ける
  feather?: number;     // ソフト境界幅。既定 32。大きいほど輪郭が滑らか
  keyColor?: [number, number, number];   // 背景キー色を明示(未指定は枠からサンプル)
  maxSize?: number;     // 処理前の最大辺(px)。プレビュー再処理を軽く保つ(DL はフル解像度で撮り直す)
};

ブラウザ専用 = client で呼ぶ

document / createImageBitmap / canvas を使うので client でのみ動く(SSR で呼ぶと明示エラー)。呼び出し側コンポーネントに "use client" を付け、File を createImageBitmap でデコードして渡す:

"use client";
const bmp = await createImageBitmap(file);
const png = await removeBackground(bmp, canvasLumaKeyBackend, { threshold, feather, maxSize: 1600 });
bmp.close();
const url = URL.createObjectURL(png);   // <img src> でチェッカーボード背景の上にプレビュー

starter の /admin/cutoutfeatures.isEnabled("visuals") + admin 認証でガード)が参照実装。アップロード → しきい値/羽化スライダ → 透過プレビュー → 透過 PNG ダウンロードまで client で完結する。専用 feature flag は持たず visuals 配下の admin ツールとして扱う。

production backend を差す(MODNet / @imgly・未同梱)

髪境界まで抜く alpha matting は重い依存が要るため本パッケージには同梱しない。代わりに factory + 設定型の差し込み口だけを置き、依存を入れるまで明示的に throw する(vaporware を握らせず、動くのは canvas fallback だけと分かる形にする)。

// いずれも CutoutBackend を返す。実体は未同梱 → 依存導入まで throw。
export function createModnetBackend(config: ModnetBackendConfig): CutoutBackend;   // onnxruntime-web + MODNet .onnx
export function createImglyBackend(config?: ImglyBackendConfig): CutoutBackend;    // @imgly/background-removal

(A) MODNet (onnxruntime-web):

  1. yarn workspace <consumer> add onnxruntime-web
  2. MODNet の .onnx(例 modnet_photographic_portrait_matting.onnx)を public/models/
  3. createModnetBackendremoveBackground を実装 —— 512×512 に正規化した NCHW テンソル化 → ort.InferenceSession で推論 → 出力 matte(1ch alpha)を元寸法へ拡大し RGB × alpha で合成 → 透過 PNG

- onnxruntime-web の wasm/threads は COOP/COEP ヘッダと .wasm 配信設定が要る(Next config)

(B) @imgly/background-removal:

  1. yarn workspace <consumer> add @imgly/background-removal
  2. createImglyBackendremoveBackgroundimglyRemove(input, { publicPath, output: { format } }) を呼び透過 Blob を返す

- 初回にモデル資産(数十MB)を publicPath から取得する。CDN 直リンクは避け自前配信推奨

どちらも同じ CutoutBackend 契約なので、依存導入後は removeBackground(input, createModnetBackend({ modelUrl })) のように第2引数を差し替えるだけ。UI・章のワークフローは無改修で production 品質へ乗り換わる。

設計の要点

  • 契約が薄い(透過 PNG Blob を返すだけ)ので、抜き方の進化(canvas → ONNX → 将来の別モデル)を UI から隔離できる
  • 同梱は本物だけ——動く canvas fallback は実装込み、動かせない production backend は差し込み口のみ。「未同梱」を型とエラーで明示し、依存を勝手に増やさない
  • 重い依存(onnxruntime-web / @imgly)は消費側アプリが必要になった時点で add する。フレームワーク側(@event/visuals)は依存ゼロを保つ