5.11. フレーム差分

フレーム差分は、新しい各フレームを保存済みの参照フレームと比較し、シーンの中で変化した部分を見つけます。これは何かが起きていないか監視するカメラアプリケーション(動き検出によるキャプチャ、侵入アラート、「何かが動いたら動画を保存する」といった用途)の主役であり、すべては先に説明したピクセル単位の演算、つまり絶対差分、しきい値処理、領域探索だけで構築され、それらを毎フレーム実行します。

5.11.1. 基本的なパイプライン

最初の段階は参照を取得することです。起動直後のどこかのタイミング(理想的には「変化なし」が意味する状態にシーンがあるとき)に、アプリケーションはフレームをキャプチャして保持します。このフレームが、以後のすべてのキャプチャと比較される基準となります。

reference = csi0.snapshot().copy()

.copy() が重要です。csi0.snapshot() 単体では、バッファがフレームバッファ上に存在する Image を返しますが、そこでは次の snapshot 呼び出しがそれを上書きしてしまいます。.copy() は参照用に別個のバッファを割り当て、このフレームのピクセルが次のキャプチャを越えて残るようにします。

第二段階は毎フレーム実行されます。新しい画像をキャプチャし、それと参照との絶対差分を計算します。それはまさに difference() が行うことです。

current = csi0.snapshot()
current.difference(reference)

この呼び出しの後、current には、参照を取得して以降シーンが変化したすべての位置を非ゼロのピクセルが示す画像が入ります。各ピクセルの大きさは、その位置でどれだけ変化したかに比例します。

第三段階は差分画像をしきい値処理します。生の差分には常にいくらかのノイズが含まれます。センサーのショットノイズによるわずかな輝度変動、照明のドリフトによる勾配変化、カメラのわずかな動きによるサブピクセルのジッターなどです。しきい値処理(ノイズフロアより上にしきい値を設定した binary())は、実際の動きとしてカウントできる十分に大きな変化だけを残し、それ以外を破棄して、非ゼロのピクセルが実際に変化した位置となる二値画像を生成します。

第四段階は、その二値マスクの連結領域、つまり連続したまとまりを形成する隣接した非ゼロピクセルのグループを抽出します。find_blobs() が一度の呼び出しでそれを行い、それぞれがバウンディングボックスとピクセル数を持つ動き領域のリストを返します。アプリケーションの残りの部分はそれに対して処理を行えます。

水平なパイプライン図。左端の2つのパネルは参照フレームと現在のフレームが並んでおり、その間にプラス記号があります。矢印がそのペアから3番目のパネルへと続き、そこには difference というラベルが付けられ、暗い背景に対していくつかのまとまりが明るく表示されています。そこから矢印が4番目のパネルへ続き、差分の二値しきい値処理版を示しており、同じまとまりが今度は塗りつぶされた白になっています。最後の矢印が5番目のパネルへ続き、各まとまりの周りに矩形のバウンディングボックスが描画された二値マスクを示しています。

フレーム差分パイプライン。参照フレームと現在のフレームが差分画像になり、しきい値処理によって差分が変化した位置の二値マスクになり、連結領域のステップによってマスクが動き領域のリストになります。

5.11.2. メモリ上およびディスク上の参照

基本的なパイプラインは参照フレームをRAMに保持します。これは、参照がスクリプトのこの実行でキャプチャされ、スクリプトが動作し続ける間だけ存続すればよい場合に適切な答えです。

長時間動作するアプリケーション(電源を入れ直した後に変化検出を再開すべきカメラ、以前のある時点からのあらゆる変化を検出する必要がある断続的なスクリプト)の場合、参照フレームは動作中のスクリプトより長く存続しなければなりません。そのパターンは、参照をディスクに保存することです。

csi0.snapshot().save("/sdcard/reference.bmp")

そして、各実行の開始時にそれを読み込み直します。

reference = image.Image("/sdcard/reference.bmp")

差分のロジックは変わりません。変わるのは参照がキャプチャの間どこに存在するかだけです。このディスク上の変種にはいくつかの改良を自然に加えられます。タイマーによる参照の自動再キャプチャ、ゆっくりとした照明ドリフトを追跡するためのオプションの移動平均などです。しかし中心となる置き換えは同じです。

5.11.3. 光源の分離

同じ減算のパターンは、少し異なる状況でも現れます。シーンの残りの部分に対して光源を分離する場合です。コツは「消灯時」の参照、つまり検出対象(IRビーコン、画面のピクセル、ステータスインジケータ)が点灯していないときに撮影したフレームをキャプチャし、その参照を以後の各フレームから減算することです。その結果、両方のキャプチャでシーンが同じだった場所はすべて輝度がゼロになり、実際に光源が点灯した場所だけが非ゼロの輝度になります。

5.11.4. difference と sub の選択

どの算術演算を選ぶかについての実践的な注意です。difference() は変化の絶対値(符号なし)を返すため、どちらの方向の変化(明るくなる、暗くなる)にも敏感ですが、その代わりに変化がどちらの方向に進んだかをアプリケーションに伝えません。純粋な動き検出ではそれが正しい答えです。動いたものはすべて興味の対象であり、輝度がどちらの方向に変化したかは関係ありません。

光源検出の場合、点灯したピクセルは常に消灯時の参照より明るいため、sub()(ゼロでクリッピングする)の方がより正直な選択です。現在のフレームが参照より暗い場所(点灯していない値の周辺のセンサーノイズに相当する場所)はどこでも、誤った「光が点いていた」という信号を報告するのではなく、ゼロにクリッピングされます。