Anime AR Stock

Vol.12

杖の先端を推定する独自モデル — 59 枚から 397 枚、閾値 0.08 が本物を弾いていた話

  • custom model
  • keypoints
  • Web Worker
  • training data
Admin 注釈先端 / グリップ
ローカル学習Keras → TF.js
静的配信/models/wand
Web Worker端末で推論
図12. Hands に乗らない小道具は、注釈 → 学習 → Worker 上の TF.js という別パイプライン。

データ量と誤差の推移(学習ログより)

出力は presence(杖があるか)と、先端・グリップの正規化座標です。誤差は検証データでの正規化座標の L2 距離の平均で、0.1 は画像の幅の 1 割ずれていることを意味します。

  • 2026-07-20: 59 枚。MLP(64px)は平均誤差 0.652 でほぼ使えず、同じ 59 枚の CNN(224px)は 0.190〜0.210。
  • 2026-07-20: 123 枚で 0.151。
  • 2026-07-26: 250 枚で 0.085〜0.090。
  • 2026-09-13: presence 出力を追加。320 枚で 0.101、397 枚(現行)で 0.101。presence の正解率は 100%。

59 → 250 枚で誤差は半分以下になりましたが、250 → 397 枚ではむしろ少し悪化しています。同じタイミングで presence 出力を足し、「杖が映っていない画像」も学習に入れたことが影響していると考えていますが、切り分けはできていません。

もう一つ、どのバージョンでも先端の誤差がグリップより大きい(現行で先端 0.129 / グリップ 0.073)ことが分かります。グリップは手の中にあって位置が安定していますが、先端は細く、背景に溶けやすく、杖を振ると最も速く動く点です。光や呪文を出したいのはまさに先端なので、一番欲しい点が一番難しいという関係になっています。

不具合: モデルの誤差より厳しい閾値

推論結果は、先端とグリップの距離(棒の長さ)が一定以上でないと「杖ではない」として捨てます。この閾値が 0.08 だった頃、本番のユーザーが杖を画面に入れていても「杖が見つかりません」と表示される不具合がありました。

当時のモデルは 250 枚で学習していて、キーポイントの平均誤差が約 0.09 でした。つまり先端とグリップがそれぞれ 0.09 ずれうるモデルに対して、「2 点の距離が 0.08 未満なら捨てる」という、モデルの精度より細かい判定をしていたことになります。杖を奥に傾けたりカメラから離れたりして画面上の杖が短くなれば、正しい検出でも閾値を割りやすくなります。2026-08-09 に閾値を 0.04 に下げて解決しています。

lib/wandInferenceConfig.ts
1export const WAND_INFERENCE_INTERVAL_MS = 125;2 3/** Minimum tip–grip length (normalized) to treat as a wand hit. */4export const WAND_MIN_STICK_LENGTH = 0.04;   // 0.08 → 0.04(2026-08-09)5 6/**7 * Models trained before the presence output always report confidence=1,8 * so this only gates once a presence-aware model is deployed.9 */10export const WAND_MIN_CONFIDENCE = 0.5;

棒の長さのゲートは、もともと「杖が映っていない」ことを判定する手段としては間接的です。回帰モデルは杖が無くても何らかの座標を出すからです。その後、「杖が映っていないのに検出してしまう」誤検出の修正として presence 出力を足し、出力が 5 つのモデル(presence + 先端 xy + グリップ xy)なら presence を、4 つの古いモデルなら confidence = 1 として扱うよう、Worker 側で両方に対応させています。

推論パイプライン

入力は 224px。単純にリサイズすると縦横比が崩れ、細長い杖の先端の位置が歪むので、余白を付けて縮小する letterbox をかけ、出力座標を unletterboxPoint() で元の画像座標に戻します。推論は Worker で行い、メインスレッドは結果を受け取るだけです(Vol.11)。

vision/wandModel.worker.ts(要点抜粋)
1const letterboxed = letterboxResize(pixels, inputSize, box);   // 224px、余白付き2const values = await model.predict(preprocessed).data();3// 5-output models (presence + tip/grip) started shipping after the4// "no wand in frame" false-positive fix; 4-output models predate it.5if (values.length >= 5) {6  const confidence = clamp01(values[0]);7  const tip = unletterboxPoint(values[1], values[2], inputSize, box);8  const grip = unletterboxPoint(values[3], values[4], inputSize, box);9  post({ type: 'prediction', id: data.id, tip, grip, confidence });10}

このパイプラインは十二支の印と同じく、学習はローカル、配信は静的ファイル(uint8 量子化の graph-model、約 2.5MB)、推論はブラウザです。本番のサーバーは学習も推論もしません。

実装から得た知見

  • 後段の閾値は、モデルの誤差より細かくしない。キーポイントが 0.09 ずれるモデルに 0.08 の距離判定を掛けると、正しい検出を弾く。
  • 少ないデータでは構造の差が大きい。59 枚で MLP 0.65 に対して CNN 0.19。データを集める前にモデル構造を決める価値がある。
  • 回帰モデルは「無い」と言えない。対象が映っていないことを判定したいなら、presence のような専用の出力を最初から持たせる。
  • 細長い物体では、エフェクトを出したい先端ほど誤差が大きい。誤差は平均だけでなく点ごとに見る。