🐈

【理論編】古典的な動体検知システムをOpenCVで実装してみる

に公開

はじめに

自動ドアや防犯カメラなど、身近なシステムで利用されている動体検知をPythonとOpenCVを使って実装してみようと思います。今回は、動体検知の基本的な理論を紹介していこうと思います。

このリポジトリが今回の実装内容です。

動体検知とは?

動体検知は、画像や映像の中から動いているものを検知する技術です。具体的には、防犯カメラ、自動ドア、交通量調査など、私たちの身の回りの多くのシステムで利用されています。

motion-detection

動体検知を実装するにはどうすればいいか?最もシンプルな問いは**前の瞬間と比べて、何が変わったか?**です。
この問いに答えるのが、今回の主役である背景差分法です。

背景差分法

背景差分法とは、その名の通り、現在の映像から背景だけの映像を引き算することで、動いている物体を抽出する手法です。

非常にシンプルですが、いくつかの課題が考えられます。
例えば、照明の微妙な変化や木の葉の揺れなど、本来は動きとして検知したくないものまで拾ってしまう可能性があります。
また、背景そのものが時間とともに変化することもあります。

これらの課題を克服するため、次のようなステップを踏んでいきます。

映像の入力と前処理

まず、OpenCVを使ってビデオソースからフレームを1枚ずつ読み込みます。しかし、カラー画像のままでは情報量が多すぎて処理が大変です。そこで、以下の手法をとります。

grayscale

  1. グレースケール化
    カラー画像はR,G,Bの3チャンネルで構成されていますが、グレースケール化することで1チャンネルにすることができます。これにより、処理の負荷を軽減することができます。
  2. ガウシアンブラーによる平滑化
    ガウシアンブラーは、画像のノイズを軽減するための手法です。具体的には、画像の各ピクセルの値を、その周辺のピクセルの値の平均値で置き換えることで、画像のノイズを軽減することができます。

背景モデルの構築

背景をどう定義するかは、この手法の肝です。単純に「最初のフレームを背景にする」という方法もありますが、それでは背景が少しでも変化したときに検知できなくなってしまいます。そこで、移動加重平均という考え方を用います。

数式で表すと以下のようになります。

B_t = (1 - \alpha) B_{t-1} + \alpha F_t

ここで、

  • B_t : t時点の背景モデル
  • B_{t-1} : t-1時点の背景モデル
  • F_t : t時点のフレーム
  • \alpha : 学習率

この式は、**「過去の背景を少しずつ忘れながら、新しい背景を学習していく」**ということを意味しています。これにより、背景が少しずつ変化しても、それに追従していくことができます。
例えば、学習率が0.01の場合、背景の変化にゆっくりと追従します。これは、背景の変化に敏感すぎず、誤検知を減らすことができます。逆に、学習率が0.99の場合、背景の変化に敏感に追従します。

この処理を繰り返すことで、平均的な背景を表現できるというわけです。

差分の計算

背景モデルが準備出来たら、現在のフレームと背景モデルの差分を計算します。具体的には、各ピクセルの輝度の差分を計算します。

background_diff

D_t = |F_t - B_t|

ここで、

  • D_t : t時点の差分画像
  • F_t : t時点のフレーム
  • B_t : t時点の背景モデル

これにより、変化があった部分だけが白く浮かび上がる差分画像が得られます。

閾値処理

ここで得られる差分画像は、まだ濃淡のあるグレースケール画像です。このままでは、**「どこからが動体なのか」**を判断することができません。これを「動きがあった部分」と「なかった部分」の白黒2値画像に変換する必要があります。

2値化するには、ある画素が閾値より多きければ白、小さければ黒、という処理を行えるのですが、閾値の値はどのように決めればいいのでしょうか?

例えば、閾値が小さすぎると、背景のわずかな変化でも動体として検知されてしまいます。逆に、閾値が大きすぎると、動体が検知されなくなってしまいます。

最適な閾値の決め方としては、大津の2値化という手法があります。これは、ヒストグラムのピークを2つ見つけ、その間の値を閾値とする手法です。詳しいことは、今度記事にしようと思います。OpenCVでは、cv2.threshold関数で大津の2値化をオプションとして選択することが出来ます。これにより、照明環境がある程度変化しても安定して2値化を行うことが出来ます。

形態学的処理

ここまでで、2値化された画像が得られました。しかし、このままでは、ノイズが残っていたり、動体が分断されてしまっていることがあります。そこで、**形態学的処理(モルフォロジー変換)**という手法を用いて、これらの課題を解決します。

形態学的処理には、以下の2つの基本的な操作があります。

  • 膨張 (Dilation): 白い領域(検知された動体)を広げる処理です。これにより、動体内部にある小さな穴を埋めたり、途切れてしまった動体の一部を繋げ合わせたりする効果があります。cv2.dilate関数を使用します。
  • 収縮 (Erosion): 白い領域を削る処理です。これにより、背景に残ってしまった孤立した小さな白い点(ノイズ)を消滅させることができます。cv2.erode関数を使用します。

さらに、これらを組み合わせることでより効果的なノイズ除去が可能です。

opening

  • オープニング (Opening): 収縮した後に膨張を行います。小さなノイズを除去するのに有効です。
  • クロージング (Closing): 膨張した後に収縮を行います。物体内部の穴を埋めるのに有効です。

動体検知では、これらの処理を適切に組み合わせることで、ノイズの少ない綺麗なマスク画像を作成します。

輪郭抽出

ノイズを除去して綺麗になった2値画像(マスク画像)から、動体の領域を特定するために輪郭抽出を行います。これにより、動体の位置や大きさをデータとして扱うことができるようになります。

OpenCVでは、cv2.findContours関数で輪郭を抽出することが出来ます。この関数は、白い領域の境界線を座標のリストとして返します。
よく使われる重要なパラメータとして、以下の2つがあります。

  • 抽出モード (cv2.RETR_EXTERNAL など):
    階層構造(親子関係)を持つ輪郭のうち、どれを抽出するかを指定します。動体検知では、物体の「外形」が分かれば十分な場合が多いため、最も外側の輪郭のみを抽出する cv2.RETR_EXTERNAL が一般的です。
  • 近似手法 (cv2.CHAIN_APPROX_SIMPLE など):
    輪郭の座標をすべて保存するか、圧縮するかを指定します。 cv2.CHAIN_APPROX_SIMPLE を指定すると、例えば直線の輪郭は「始点」と「終点」だけを保存します。これにより、データ量を大幅に削減できます。

こうして得られた輪郭(Contours)を使って、動体の面積を計算したり、その周りに四角形を描画したりします。

フィルタリングとイベント発生

検出されたすべての輪郭が、意味のある動きとは限りません。非常に小さな輪郭は、ノイズである可能性が高いです。そこで、輪郭の面積を計算し、ある程度の大きさがあるものだけを抽出します。これにより、誤検知を減らすことができます。

さらに、輪郭の重心を計算し、その位置を記録します。一定時間内に同じ場所で繰り返し動きが検出された場合、それはイベントとして扱われます。例えば、猫が同じ場所で10秒間動き続けた場合、それは「猫が10秒間そこにいた」というイベントとして記録されます。

検出結果の可視化

検出された動体を可視化するために、元の画像に輪郭を描画します。OpenCVでは、cv2.rectangle関数で矩形を描画することが出来ます。これにより、動体の位置や大きさを視覚的に確認することができます。

まとめ

今回は、背景差分法を用いた動体検出の基本的な実装方法について解説しました。もちろん、この実装方法はかなり古典的で、これだけでは実用的なシステムとしては不十分です。実際には、より高度な機械学習を用いた動体検出アルゴリズムが主流となっています。しかし、この実装方法は、より高度な動体検出アルゴリズムを理解する上で、基本的な考え方として理解しておくと良いと思います。

次の記事では、これをOpenCVPythonで実際に実装してみようと思います。

GitHubで編集を提案

Discussion