はじめに
オープンソースのブラウザ動画エディタ Timeline Studio に、靴・商品・玩具などを対象にした「物体アウトライン」機能を実装しました。
画像や動画をサーバーへ送信せず、物体検出・セグメンテーション・フレーム追跡・アウトライン合成までをブラウザ内で実行します。
リポジトリはこちらです。
この記事では、実装の中心となる次の3点を紹介します。
- 物体検出とプロンプト型セグメンテーションを分離する理由
- 動画内で同じ物体を追跡し続ける方法
- 誤ったマスクを採用しないための品質ゲート
全体のパイプライン
単純なエッジ検出では、対象物だけでなく背景の模様や影まで抽出されます。そのため、処理を次の段階に分けました。
NanoDetで候補物体を検出
↓
編集対象となる物体を選択
↓
物体中心からプロンプト点を生成
↓
MagicTouchでピクセル単位のマスクを生成
↓
マスク品質を検証
↓
Alphaをキャッシュしてアウトラインを描画
検出モデルは「どこに物体があるか」を判断し、プロンプト型セグメンテーションは「どのピクセルがその物体か」を判断します。役割を分けることで、複数の物体がある画面でも対象を安定して選べます。
推論をWeb Workerへ分離する
モデル推論をメインスレッドで行うと、React UIやタイムライン操作が停止してしまいます。そこで、物体検出はWeb Workerで実行します。
worker.postMessage({
requestId,
type: "detect",
blob,
scoreThreshold: 0.24,
});
ONNX RuntimeのセッションはWebGPUを優先し、利用できない環境ではWASMへフォールバックします。
async function createSession(modelBytes) {
if (navigator.gpu) {
try {
return await ort.InferenceSession.create(modelBytes, {
executionProviders: ["webgpu"],
graphOptimizationLevel: "all",
});
} catch (error) {
console.warn("WebGPUの初期化に失敗。WASMへフォールバック");
}
}
return ort.InferenceSession.create(modelBytes, {
executionProviders: ["wasm"],
graphOptimizationLevel: "all",
});
}
この構成により、対応端末ではGPUを利用しつつ、一般的なブラウザでも機能を維持できます。
編集対象の物体を選ぶ
検出結果の信頼度だけで対象を決めると、動画の途中で別の物体へ切り替わることがあります。そこで、信頼度・面積・画面中央への近さ・前フレームとのIoU・クラス一致を組み合わせて順位付けします。
function calculateRank(item, previous) {
const area = getArea(item.box);
const centered = getCenterScore(item.box);
const previousIoU = boxIoU(item.box, previous?.box);
const identityBonus = previous?.label === item.label ? 0.5 : 0;
return (
item.score * 0.55 +
Math.min(area, 0.25) +
centered * 0.2 +
previousIoU * 1.2 +
identityBonus
);
}
動画では前フレームとのIoUを強く評価することで、画面内に新しい物体が入っても編集対象を簡単には変更しません。
プロンプト型セグメンテーション
対象が決まったら、検出ボックスの中心を正規化したプロンプト点としてセグメンテーションモデルへ渡します。
const result = segmenter.segment(image, {
keypoint: {
x: clamp(point.x, 0, 1),
y: clamp(point.y, 0, 1),
},
});
const probabilities = result.confidenceMasks.at(-1)
.getAsFloat32Array();
const alpha = new Uint8ClampedArray(probabilities.length);
for (let i = 0; i < probabilities.length; i += 1) {
const value = clamp(probabilities[i], 0, 1);
alpha[i] = value >= threshold ? Math.round(value * 255) : 0;
}
ただし、モデル出力をそのまま採用することはしません。面積、連結成分、検出ボックスからのはみ出し、前フレームからの変化量を検証します。背景や別の物体を含む場合、そのフレームの効果は表示しません。
動画は疎なアンカーとオプティカルフローで処理する
すべてのフレームで検出とセグメンテーションを実行すると計算量が大きくなります。そこで、一定間隔のアンカーフレームだけ完全な分析を行い、中間フレームはオプティカルフローでAlphaを伝播します。
const needsAnchor =
index === 0 ||
index - lastAnchorIndex >= anchorInterval ||
!trackedMaskBox;
if (needsAnchor) {
currentAlpha = await analyzeObjectAnchor(frame);
lastAnchorIndex = index;
} else {
currentAlpha = await propagateAlphaWithFlow(
previousFrame,
currentFrame,
currentAlpha,
);
}
伝播後のマスク面積が急変した場合は追跡を破棄し、次のフレームで再検出します。
const areaRatio = nextArea / previousArea;
if (!nextBox || areaRatio < 0.35 || areaRatio > 2.8) {
currentAlpha = null;
trackedMaskBox = null;
}
誤ったマスクを無理に補正して伝播するより、そのフレームではアウトラインを表示しない方が安全です。
AI分析と描画設定を分離する
物体分析は比較的重い処理ですが、色や幅、発光、材質の変更は即時に反映する必要があります。そのため、分析済みAlphaと描画設定を分離しました。
const objectOutline = {
targetKind: "object",
outline: {
color: "#f3efe4",
width: 12,
opacity: 1,
glow: 0.35,
},
material: {
id: "paper",
edgeDensity: 0.5,
shadowDepth: 0.32,
},
};
スタイル変更時はモデルを再実行せず、キャッシュ済みAlphaから画面を再合成します。プレビューと書き出しも同じAlphaと設定を利用するため、編集画面と出力結果の差を防げます。
まとめ
ブラウザ内の物体アウトラインは、1つのモデルだけで完成する機能ではありません。
- Workerによる推論の分離
- WebGPUとWASMのフォールバック
- 検出とプロンプト型セグメンテーションの組み合わせ
- 前フレームを利用した物体IDの維持
- 疎なアンカーとオプティカルフロー
- 誤検出を採用しない保守的な品質ゲート
- 分析結果と描画設定の分離
これらを組み合わせることで、サーバーへ素材を送信せずに、画像と動画の両方で編集可能なアウトライン効果を実現できました。
実装の詳細はGitHubで公開しています。