PythonとOpenCVで動体検知をしたい

#はじめに
AI、機械学習を勉強するにあたって、前にこちらの本を購入したことがあったのでその中にあったOpenCVでの動体検知をやってみたいと思います。
<img width="300"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654235/738b1026-0ded-65ab-31d6-4e5354de0fa0.jpeg">
[PythonによるAI・機械学習・深層学習アプリのつくり方](https://www.amazon.co.jp/%E3%81%99%E3%81%90%E3%81%AB%E4%BD%BF%E3%81%88%E3%82%8B-%E6%A5%AD%E5%8B%99%E3%81%A7%E5%AE%9F%E8%B7%B5%E3%81%A7%E3%81%8D%E3%82%8B-Python%E3%81%AB%E3%82%88%E3%82%8B-AI%E3%83%BB%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%BB%E6%B7%B1%E5%B1%A4%E5%AD%A6%E7%BF%92%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%82%8A%E6%96%B9-%E3%82%AF%E3%82%B8%E3%83%A9%E9%A3%9B%E8%A1%8C%E6%9C%BA/dp/4802611641)

※2020年10月にTensorFlow2に対応した新しいものが出ていました
→[2020年10月発売版](https://www.amazon.co.jp/%E3%81%99%E3%81%90%E3%81%AB%E4%BD%BF%E3%81%88%E3%82%8B-%E6%A5%AD%E5%8B%99%E3%81%A7%E5%AE%9F%E8%B7%B5%E3%81%A7%E3%81%8D%E3%82%8B-Python%E3%81%AB%E3%82%88%E3%82%8BAI%E3%83%BB%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%BB%E6%B7%B1%E5%B1%A4%E5%AD%A6%E7%BF%92%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%82%8A%E6%96%B9-TensorFlow2%E5%AF%BE%E5%BF%9C-%E3%82%AF%E3%82%B8%E3%83%A9%E9%A3%9B%E8%A1%8C%E6%9C%BA/dp/4802612796/ref=pd_lpo_14_t_0/355-9536945-6143916?_encoding=UTF8&pd_rd_i=4802612796&pd_rd_r=b43e60d2-58e2-4145-bbd9-6e17c6f92b8a&pd_rd_w=go1oN&pd_rd_wg=rNt2X&pf_rd_p=cb2cef9d-b0a3-4b58-a575-45abfc5e07e8&pf_rd_r=EY5AVDA82SP3WVB7C5ZB&psc=1&refRID=EY5AVDA82SP3WVB7C5ZB)

#環境
**Anacondaを使ってPython環境を用意します**

macOS BigSur 11.2.2
conda 4.9.2
Python 3.8.5
OpenCV 4.5.1.48
コードの実行はターミナルから

Anacondaのインストールは下記を参照
[MacOS版Anacondaのインストール](https://www.python.jp/install/anaconda/macos/install.html)

今回は「python_opencv」フォルダをつくってその中にファイルや「img」フォルダを置いています。

#OpenCVのテスト
まずはターミナルを開いて、conda環境を使えるように以下のコマンドを実行します

```
$ conda activate
```
次にOpenCVをインストールします。

**pipでインストールする場合**
以下のコマンドだけで完了します。

```
$ pip install opencv-python
```
**condaでインストールする場合**
Anacondaはライブラリの管理にcondaが使われています。
他のライブラリと管理方法を統一させるためにOpenCVもcondaでインストールしようと思ったのですが単純に`$ conda install opencv`だけではうまくいきませんでした。

[teratail python3.7の環境に、condaを使ってopencvをインストールしたいです](https://teratail.com/questions/253335)
上記にあるようにデフォルトの環境(base)とは別の名前の環境をつくってインストールする必要があるようです。

例として「use_cv」という名前の環境をつくってみます。
(ついでにmatplotlibもインストール)

コマンドの説明はこちら→[PythonJapan condaコマンド](https://www.python.jp/install/anaconda/conda.html)

```
$ conda create --name use_cv python=3.7
$ conda activate use_cv
$ conda install opencv matplotlib
```
これでOpenCVのインストールができたのでPCにある適当な画像を読み込んで表示してみましょう。

base(またはuse_cv)環境は有効にしたまま「python_opencv」フォルダに移動してその中のopencv_test.pyを実行します。
(test.jpgは自分のアイコンの画像)

```
$ cd python_opencv
$ python opencv_test.py
```

```python:opencv_test.py
import matplotlib.pyplot as plt
import cv2

img = cv2.imread("./img/test.jpg")
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.show()
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654235/474e9f26-3434-f912-1c3c-23329364a227.png">
ちゃんと画像が表示されました。

なお`cvtColor()`では色空間をBGRからRGBに変換しています。
OpenCVではカラーコードがBGRの順番でならんでおり、(R:255, G:100, B:0) の色を (B:255, G:100, R:0) として読み込んでしまいます。
そのため、OpenCVで読み込んでそのままmatplotlibで表示させようとすると色が正しく表示されません。

試しに`cvtColor()`を使わずに表示してみると赤と青が反転した画像になります。

```python
img = cv2.imread("./img/test.jpg")
plt.imshow(img)
plt.show()
```
![notcvt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654235/54d9e692-e2c8-9514-dd10-c9954ae948d5.png)

#動いている物体を検出する
pixabayから公園を撮影した動画をダウンロードしました。→[元の動画はこちら](https://pixabay.com/ja/videos/%E3%83%AA%E3%82%B9-%E5%85%AC%E5%9C%92-%E6%AD%A9%E8%A1%8C%E8%80%85-%E8%87%AA%E7%84%B6-10826/)
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654235/ce836b71-8f46-ad09-ba77-15dd222c3d20.gif)
歩いている人とリスも写っていますね。
これらの動いているものを赤い四角で囲んで動画内で表示してみます。

**全体のコード**

```python:move_capt.py
import cv2
import time

movie = cv2.VideoCapture('./movie/park.mp4')

red = (0, 0, 255) # 枠線の色
before = None # 前回の画像を保存する変数
fps = int(movie.get(cv2.CAP_PROP_FPS)) #動画のFPSを取得

while True:
    # 画像を取得
    ret, frame = movie.read()
    # 再生が終了したらループを抜ける
    if ret == False: break
    # 白黒画像に変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    if before is None:
        before = gray.astype("float")
        continue
    #現在のフレームと移動平均との差を計算
    cv2.accumulateWeighted(gray, before, 0.5)
    frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(before))
    #frameDeltaの画像を２値化
    thresh = cv2.threshold(frameDelta, 3, 255, cv2.THRESH_BINARY)[1]
    #輪郭のデータを得る
    contours = cv2.findContours(thresh,
                    cv2.RETR_EXTERNAL,
                    cv2.CHAIN_APPROX_SIMPLE)[0]

    # 差分があった点を画面に描く
    for target in contours:
        x, y, w, h = cv2.boundingRect(target)
        if w < 30: continue # 小さな変更点は無視
        cv2.rectangle(frame, (x, y), (x+w, y+h), red, 2)

    #ウィンドウでの再生速度を元動画と合わせる
    time.sleep(1/fps)
    # ウィンドウで表示
    cv2.imshow('target_frame', frame)
    # Enterキーが押されたらループを抜ける
    if cv2.waitKey(1) == 13: break

cv2.destroyAllWindows() # ウィンドウを破棄
```
**実行結果**
![ezgif.com-gif-maker (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654235/2e8a7887-86ac-7b44-c40f-a5770ec1b20e.gif)
小さいリスの動きもちゃんと検出されてます。

#各コードの説明
##VideoCapture
```python
movie = cv2.VideoCapture('./movie/park.mp4')
```
今回はカッコ内に動画のパスを入れていますが
`movie = cv2.VideoCapture(0)`
とすることでPCの内蔵カメラの映像を使えます。
動きの検出もリアルタイムで可能です。
内蔵カメラを使う場合は`cv2.destroyAllWindows()`の前に`movie.release()`を入れてカメラを解放するのを忘れずに。
##accumulateWeighted と absdiff
```python
cv2.accumulateWeighted(gray, before, 0.5)
frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(before))
```
`accumulateWeighted`でフレームの移動平均値を更新し、`absdiff`で現在のフレームと移動平均との差を計算しています。

beforeにはgrayの１つ前のフレームだけではなく、それ以前の全てのフレームに重み(第3引数)が加わった情報が蓄積されています。
重みは以前の画像データをどのくらいの早さで忘れるかを表しており

**重みを小さくする**
　→以前のフレームの情報を多く引き継ぐ
　→現在のフレームと以前のフレームとの小さな差も現れやすい
　→小さな動きも検知できる
**重みを大きくする**
　→以前のフレームの情報を少しだけ引き継ぐ
　→現在のフレームと以前のフレームとの小さな差が現れにくい
　→大きな動きがあった場合のみ検知する

という違いがでます。

**比較動画**
**weight = 0.05**
奥の道路を走る車の動きも検出
![weight_0.05.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654235/8aaa8362-e710-a065-9e57-c40583e34901.gif)
**weight = 0.8**
一番手前の男性とリスが走り出した時のみ検出
![weight_0.8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654235/afbd8744-2831-720b-f6de-b89a1b0fe597.gif)


##threshold
```python
thresh = cv2.threshold(frameDelta, 3, 255, cv2.THRESH_BINARY)[1]
```
ここではframeDeltaのフレーム情報を白と黒の**二値画像**に変換しています。
詳しくはこちら→[OpenCV-Python 画像のしきい値処理](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_thresholding/py_thresholding.html)

今回のコードではframeDeltaの中で画素値が3より大きい部分を255(白)に置き換えます。

試しにframeDeltaとそれを二値化したthreshの画像を比べてみましょう。

**比較動画**
**frameDelta**
うっすらとモヤのような感じです。
![framedelta.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654235/bd8c25e2-d986-695d-ce95-0a1072241d6d.gif)
**thresh**
モヤの部分がはっきりとした白になって表れました。
![threshold.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654235/5ea91c99-af50-fbbe-3874-365a67617f3e.gif)

これを行うことでどの部分が動いているのかという輪郭をきれいに捉えることができます。

##findContours
```python
#輪郭のデータを得る
contours = cv2.findContours(thresh,
                    cv2.RETR_EXTERNAL,
                    cv2.CHAIN_APPROX_SIMPLE)[0]

# 差分があった点を画面に描く
for target in contours:
    x, y, w, h = cv2.boundingRect(target)
    if w < 30: continue # 小さな変更点は無視
    cv2.rectangle(frame, (x, y), (x+w, y+h), red, 2)
```
threshの画像を元に輪郭のデータを取得しています。
`findContours`の説明はこちら→[OpenCV-Python 輪郭: 初めの一歩](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)

contoursは抽出された輪郭のリストで各輪郭はnumpy配列になっています。
その配列を`boundingRect`で輪郭の4点の位置として受け取り、`rectangle`で元のフレーム上に赤い四角形として描写します。
***
今回は動画内で動いているものを赤い四角で囲むところまでやってみました。
次は検出した結果を使って何かやってみたいですね。

ご意見や間違いなどがございましたらコメントよろしくお願いいたします。

#参考サイト、書籍
[PythonによるAI・機械学習・深層学習アプリのつくり方](https://www.amazon.co.jp/%E3%81%99%E3%81%90%E3%81%AB%E4%BD%BF%E3%81%88%E3%82%8B-%E6%A5%AD%E5%8B%99%E3%81%A7%E5%AE%9F%E8%B7%B5%E3%81%A7%E3%81%8D%E3%82%8B-Python%E3%81%AB%E3%82%88%E3%82%8B-AI%E3%83%BB%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%BB%E6%B7%B1%E5%B1%A4%E5%AD%A6%E7%BF%92%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%82%8A%E6%96%B9-%E3%82%AF%E3%82%B8%E3%83%A9%E9%A3%9B%E8%A1%8C%E6%9C%BA/dp/4802611641)
[Python+OpenCVとWebカメラを使って動体検知する話](https://ensekitt.hatenablog.com/entry/2018/06/11/200000)
[OpenCVで手っ取り早く動体検知してみた](https://qiita.com/KMiura95/items/4eed79a7da6b3dafa96d)
[OpenCVで動体検出をしてみた](https://dev.classmethod.jp/articles/yoshim-opencv-extract-diff/)
[OpenCV-Pythonチュートリアル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_tutorials.html)