OpenCVで手っ取り早く動体検知してみた

自分が書いた記事の中でだいぶ前に書いたOpenCVやRaspberryPiの記事がここ最近また皆さんに読まれるようになりました。最近のトレンドになっている「ディープラーニング」、「エッジコンピューティング」に関連したキーワードになっていることから参考にしていただけているのだと思います。その中でふとエッジコンピューティングを使う場面を考えた時、カメラで動体検知をする事例を割とよく見かけるようになった気がします。自動運転やロボット、そして定点カメラの機能として使っています。というわけで今回はOpenCVの関数を駆使して簡単な動体検知を実装してみようと思います。

# 動作環境
- OS : Windows10
- python3.6.5
- OpenCV4.0.0

OpenCVは```pip install opencv-python```を使ってインストールしてます。これだとOpenCVを動かすときに必須になるnumpyもインストール出来ます。

# 動画を再生するプログラム
まずは、動画を再生するプログラムを作成します。今回はOpenCVの公式パッケージで配布されている[こちら](https://github.com/opencv/opencv/blob/master/samples/data/vtest.avi)の動画を使用します。予めダウンロードしてソースコードと同じフォルダーに保存しておきます。

```python
import cv2

filepath = "vtest.avi"
cap = cv2.VideoCapture(filepath)
# Webカメラを使うときはこちら
# cap = cv2.VideoCapture(0)

while True:
    # 1フレームずつ取得する。
    ret, frame = cap.read()
    if not ret:
        break

    # 結果を出力
    cv2.imshow("Frame", frame)
    key = cv2.waitKey(30)
    if key == 27:
        break

cap.release()
cv2.destroyAllWindows()
```

ここで```cv2.waitKey(30)```は本来はOpenCVで出力しているウィンドウからのキー入力を待つ時間を指定するものですが、動画を再生する上ではコマ送りを遅くして動画が早く流れるのを防ぐ役割も果たしています。

# 動体検知をするための方法
さて、先程の動画を再生するコードに動体検知のコードを足していきます。順を追ってアルゴリズムを見ていきます。

## グレースケールに変換
彩度に依存することなくエッジ検出を行うためにグレースケールに変換して2値化します。具体的には、以下の関数を使用します。

```python
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
```

## 比較するためのフレームを切り出す
ソースコードのはじめに```avg = None```を定義して```avg```に比較用のフレームの配列を残します。そこで動画を出力するループの中に以下のコードを追加します。言うまでもなく、ここで残すフレームはグレースケールで変換したものです。

```python
if avg is None:
   avg = gray.copy().astype("float")
   continue
```

## 現在の画像と移動平均の差を求める
画像の累算器に加算して、そこから現在のフレームの差を求めます。具体的には以下のコードになります。

```python
cv2.accumulateWeighted(gray, avg, 0.6)
frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))
```

## 閾値を設定して2値化する
今度は閾値を設定し、フレームを2値化します。これによって前のフレームから変化の合った箇所の輪郭線をはっきりします。```findContours```関数を使用して以下のコードになります。

```python
contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
```

## 求めた閾値からオリジナルのフレームに描画する
ここまでで動体検知の処理が出来ました。ですが、結果を可視化する必要があります。```drawContours```関数を使用して結果の輪郭線をフレームに描画していきます。

```python
frame = cv2.drawContours(frame, contours, -1, (0, 255, 0), 3)
```

## 完成形
以上の内容を踏まえて、最終的な完成形が以下のソースコードになります。

```python
import cv2

filepath = "vtest.avi"
cap = cv2.VideoCapture(filepath)
# Webカメラを使うときはこちら
# cap = cv2.VideoCapture(0)

avg = None

while True:
    # 1フレームずつ取得する。
    ret, frame = cap.read()
    if not ret:
        break

    # グレースケールに変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # 比較用のフレームを取得する
    if avg is None:
        avg = gray.copy().astype("float")
        continue

    # 現在のフレームと移動平均との差を計算
    cv2.accumulateWeighted(gray, avg, 0.6)
    frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))

    # デルタ画像を閾値処理を行う
    thresh = cv2.threshold(frameDelta, 3, 255, cv2.THRESH_BINARY)[1]
    # 画像の閾値に輪郭線を入れる
    contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    frame = cv2.drawContours(frame, contours, -1, (0, 255, 0), 3)

    # 結果を出力
    cv2.imshow("Frame", frame)
    key = cv2.waitKey(30)
    if key == 27:
        break

cap.release()
cv2.destroyAllWindows()
```

# 結果
![result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170520/9629dd2b-7f9b-9dba-7e89-76e55415448e.gif)

画像で変化した箇所の輪郭をプロットする形になっているので、目で見てわかる人の動きの他に背景のちょっとした揺れも変化として検知しています。

#まとめ
今回はOpenCVの関数だけで動体検知の処理を実装しました。特に目新しいアルゴリズムを使用しているわけではありませんが、簡単に「動いたもの」を検知することができました。あくまで簡易的なシステムなゆえ、背景の変化などのいわゆるノイズも検知することになるので、今回の場合だと人や特定の物の動きだけを検知したいときには、更にアルゴリズムを改良したり、ディープラーニングを使用する必要があります。

# 参考
[輪郭: 初めの一歩 -OpenCV-Python Tutorials 1 documentation-](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_contours/py_contours_begin/py_contours_begin.html)
[OpenCVを利用して動画（カメラ）から動体検知をする方法について](https://developers.cyberagent.co.jp/blog/archives/12666/)
[OpenCVで動体検出をしてみた](https://dev.classmethod.jp/etc/yoshim-opencv-extract-diff/)