眠気検知と危険物検出を同時に回したかった。声と姿勢を混ぜた合成スコアまでまとめて面倒を見るハブ(監視の中枢)が欲しかった。だから midori304 で顔・音声・ジェスチャを一本化した監視エンジンを組んだ。意外だ。カメラの前で息を潜めても、予備値に戻すフォールバックの癖が表情に残る。
対象読者
- ライブ映像に対して複数モダリティをモニタリングしたいエンジニア
- face-api.js(顔認識ライブラリ)や MoveNet(姿勢推定モデル)を使ったリアルタイム推論をブラウザで検証している方
- 2200ms のヒステリシスやフォールバック設計を数値で抑えたいプロダクト開発者
記事に書いてあること
- midori225 を土台に、顔・音声・ジェスチャを MonitoringHub(監視用の統合クラス)がどう扱うか
- 居眠り監視・走行検知・危険物検知のしきい値と遅延対策の具体値
- VoiceAnalyzer / GestureAnalyzer のフォールバック(予備動作)とライブ切り替えの実装メモ
- IndexedDB(ブラウザ内蔵データベース)でアラート履歴と動画を扱うときの注意点
最初にまとめた監視ボード
midori304 の第一版では Visual Dashboard を開いた瞬間にモダリティ比率(顔・音声・動作の貢献度)を確認できるようにした。Face に 45%、Voice に 30%、Gesture に 25% の重みを与えたのは midori225 の検証で顔の信頼度が最も安定していたから。収入がない身なので検証時間を節約したかった。短文で言う。これだ。
モダリティの合成と重みの落とし所
測定ログでは Face スコアは 0.58 付近を安定して維持したが、Voice は fallback 0.35 からライブ時 0.58 まで波がある。Gesture は MoveNet(姿勢推定モデル)が attachVideo に失敗した瞬間に 0.40 に落ちた。live-dashboard.js の normalizeWeights() がゼロ除算にならないよう、安全策としてデフォルトの 45/30/25 に戻す処理を差し込んだ。失敗。デモでスライダーをいじり倒しても合計が 0 にはならない。
居眠り監視の具体的なしきい値
居眠り検出では EAR(Eye Aspect Ratio、目の開閉度合い)≤0.25、頭部前傾比≥0.48、モーション≤0.05 が 2200ms 続いたら raiseAlert() する。Grace 600ms を過ぎると candidateSince が破棄される。頭が前に倒れてから 2.2 秒我慢されると、MediaRecorder(メディアレコーダー)から pre/post 7000ms のクリップを切り出す。マズい。Grace の設定が低すぎると車内モニタでアルゴリズムが暴発した。
// libs/monitoring-hub.js(抜粋)
const drowsinessDefaults = {
earThreshold: 0.25,
headPitchThreshold: 0.48,
motionThreshold: 0.05,
holdDurationMs: 2200,
recoveryGraceMs: 600,
cooldownMs: 60000
};
改良後は EAR が 0.21 まで落ちても Grace 内で姿勢が戻れば candidateSince を維持し、false positive を 7→2 件に抑えた。
TinyFaceDetector から拾う指標の裏側
ランドマーク 68 点の中から両目 6 点ずつを使い、eyeAspectRatio() で瞬きの深さを出している。chinY - eyesCenterY を顔の高さで割った値が頭部前傾比だ。midori304 では averagePoint() を導入して肩と腰からトルソー長(胴体の長さ)を推定し、首が伸びたかどうかを見る。意外だ。肩が見えなくても鼻と顎だけで前傾を拾えた。
音声トーンの滑らかな更新
VoiceAnalyzer は RMS(二乗平均平方根)から dB を計算し、音量スコアは -60dB から -15dB の範囲を 0 から 1 に正規化します。ピッチは 180〜260Hz で最大 1、スコアは過去の値に80%、新しい値に20%の重みをかけて指数平滑します。マイク許可を拒否されたら fallback 0.35 に戻す。この処理を入れ忘れていた頃は Voice セクションが数値エラーを出し、Live Dashboard が散った。修理後は 120ms 間隔で音声解析の更新イベントを飛ばすようになった。
監視モジュールの有効化で起きた混線
setDetectorEnabled() をトグルと同期させるときに、unregisterDetector 直後に handleSafetyStateUpdate が呼ばれず UI が idle のまま硬直した。activeDetectorStates.delete(detectorId) と renderSafetyState() を順番に置いたら解決した。短文で言う。失敗。state が残っていた。
fallback とライブの境界
Voice と Gesture の fallback を切り替えるスイッチを実装したら、voiceState.ready=false の時だけ Live Dashboard が「停止中」と表示される仕様が見えてきた。Gesture は MoveNet が hasPose を返すまで fallback 0.40 を配信し続ける。居眠り検知だけを動かした夜、フォールバック値が高すぎて冷や汗をかいた。落ち着いた。
アラート履歴と IndexedDB の罠
アラート保存用のメソッド AlertHistoryDB.saveAlert()(IndexedDB(ブラウザ内蔵データベース)に1件ずつ書き込む関数)では、Blob(バイナリデータ)を ArrayBuffer(バイナリデータのバッファ)に変換して保存する。何度も失敗した。VideoRecorder が null を返しているのに、アラートの添付動画を表す alert.video.blob に無理やりアクセスしてクラッシュさせたのだ。修正後は storeVideo フラグで保存の可否を切り替え、最大 100 件までを発行時刻で降順取得している。
// libs/alert-history-db.js(抜粋)
const alertData = {
detectorId: alert.detectorId || 'unknown', // どの検知モジュールが発火したか
severity: alert.severity || 'medium', // 重要度
issuedAt: formatTimestamp(alert.issuedAt || alert.timestamp || new Date()),
videoBlob: alert.video && alert.video.blob
? await alert.video.blob.arrayBuffer() // 添付動画 (Blob) をバイト列に変換
: null,
videoMimeType: alert.video?.mimeType || 'video/webm'
};
MonitoringHub の流れを噛み砕く
MonitoringHub.loop() は face-api.js(顔認識ライブラリ) → MoveNet(姿勢推定モデル) → coco-ssd(物体検出モデル) → Detector 処理→ステート発火の順で流れる。ensureAuxModels() が CPU fallback を仕込んでいるので、古いノート PC(パーソナルコンピュータ)でも止まらない。時間をかけて観察した結果、240ms 以内にフレームを消化したら 8fps(FPS、フレームレート)でも滑らかだ。
使ってみて
デモを全画面で開く(Live Dashboard の重みを確認)
重み配分を変えながら、居眠り検知と走行検知のトグルを切り替えてみてほしい。Face を 40% 未満に下げると fallback 値が支配的になり、合成スコアが動きにくくなる。
- Face/Voice/Gesture の重みを変えても必ず正規化される(合計=1)
- Voice の fallback が連続するならマイク許可を再確認
- Gesture が Live に切り替わらないときは照明と画角を調整
同じような監視ハブを作っている方の指標決めの参考になれば嬉しい。
まとめ
- Face 45% / Voice 30% / Gesture 25% に固定することで fallback 時の揺れを見える化した
- EAR≤0.25・頭部前傾比≥0.48・モーション≤0.05 を 2200ms 継続したらアラートを出す
- VoiceAnalyzer は RMS を 120ms 間隔で平滑し、pitchScore と合わせて滑らかな Live 値を返す
- AlertHistoryDB は ArrayBuffer 変換を挟み、動画 Blob を安全に IndexedDB に格納する
- MonitoringHub.loop は face-api.js → MoveNet → coco-ssd → Detector の順番で副作用をまとめている
さらに深く学ぶには
最後まで読んでくださり、本当にありがとうございました。