python opencvでモーション検知

##opencvでモーション検知

ざっくりとした流れ。
1. カメラと接続。
2. first frameをグレイスケールで保存する。比較基準のためのベース画像になる。
3. 以降のframeとfirstframeを随時比較する。
4. 比較によって差分フレームができる。(difference frame)変数のdelta frameに保存する
5. 差分フレームにおいて、特定の閾値において変化した場所を白、変化してない場所を黒にするthreshold frameを作成する。thresh_frameへ保存
6. thresh_frameにおいて変化した部分(白)の面積を調べる。100pixel*100pixelの10000pixel以上ならば変化したと見なす。
7. 変化した範囲のざっくりとして外形を取得し、画像上の四角形の位置情報を得る。具体的な外線もおまけにつける。
8. 元のBGRカラーの画像に四角形を描写する。四角形の位置と外線が出力されれば成功。緑枠の部分が変化した場所。
9. 2-8を繰り返す。

特に参考になったサイト：
opencvチュートリアル：
http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/index.html
(_,cnt,_)と外線について：
https://www.pyimagesearch.com/2015/08/10/checking-your-opencv-version-using-python/
背景比較：
https://www.blog.umentu.work/python3-opencv3%E3%81%A7%E8%83%8C%E6%99%AF%E5%B7%AE%E5%88%86%E3%82%92%E6%B1%82%E3%82%81%E3%82%8B/

```py

import cv2, pandas
from datetime import datetime

first_frame = None
video = cv2.VideoCapture(0)

#index0と1に予め0を入れておいてout of indexを避ける。
status_list=[0, 0]
times=[]

#変化時刻をcsvにするために使う
df = pandas.DataFrame(columns=["Start", "End"])

while True:
    check, frame=video.read()
    #モーション検知時1へ
    status=0
    gray=cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    #平滑化。精度が上がる
    gray = cv2.GaussianBlur(gray, (21,21),0)
    
    #firstframe保存
    if first_frame is None:
        first_frame = gray
        continue
        
    #差分フレーム。compare firstframe with currentframe
    delta_frame=cv2.absdiff(first_frame, gray)
    
    #threshhold　閾値
    #cv2.threshold() return tuple
    #http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_thresholding/py_thresholding.html
    #30以下を黒。30以上を白。255はgrayscaleの範囲
    thresh_frame = cv2.threshold(delta_frame, 30, 255, cv2.THRESH_BINARY)[1]
    
    #dilate:膨張する、させる
    thresh_frame = cv2.dilate(thresh_frame, None, iterations=2)
    
    #外線をfind and draw
    #white space=変化した部分の数だけcontourを得る
    #https://www.pyimagesearch.com/2015/08/10/checking-your-opencv-version-using-python/
    (_,cnts,_)=cv2.findContours(thresh_frame.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
    
    print(cnts)
    
    #外線それぞれの面積を調べる。100*100以上か判定
    for contour in cnts:
        if cv2.contourArea(contour) < 10000:
            continue
        status=1
        #外線座標を変数へ多値代入
        (x,y,w,h)=cv2.boundingRect(contour)
        
        #四角形を現在の表示frameに追加
        cv2.rectangle(frame, (x,y), (x+w, y+h), (0,255,0), 3)
        #外線そのまま表示。
        cv2.drawContours(frame, contour, -1, (0,255,0), 3)

    
    status_list.append(status)
    
    #motion検知　始まり
    if status_list[-1]==1 and status_list[-2]==0:
        times.append(datetime.now())
    #motion検知　終わり
    if status_list[-1]==0 and status_list[-2]==1:
        times.append(datetime.now())
        
    cv2.imshow("Gray Frame", gray)
    cv2.imshow("Delta Frame", delta_frame)
    cv2.imshow("Threshold Frame", thresh_frame)
    cv2.imshow("Color Frame", frame)
    
    key=cv2.waitKey(1)#wait 1millisecond
    if key==ord("q"):
        if status==1:
            times.append(datetime.now())
        break
        
print(status_list)
print(times)

for i in range(0, len(times),2):
    df=df.append({"Start":times[i],"End":times[i+1]}, ignore_index=True)

df.to_csv("Times.csv")
video.release()
cv2.destoryAllWindows


'''
#cntsの外線を(_,cnts,_)と書かないときのエラー
error: OpenCV(3.4.1) /Users/travis/build/skvark/opencv-python/opencv/modules/imgproc/src/shapedescr.cpp:272: error: (-215) npoints >= 0 && (depth == 5 || depth == 4) in function contourArea
'''
```

1. pandasは後ほどdataframeのグラフを作るために使う。変化しはじめた開始時間、終了時間を保存する
2. grayscaleじゃないとうまく精度がでないっぽい。かつ閾値`threshold`して白黒画像を得るためにはgrayscaleである必要がある
3. `cv2.GaussianBlur`は画像を平らにできる、平滑化。ってなんのこちゃ。特定の四角い範囲ごとに数値の平均化を行う。(21,21)pixel範囲上において、画像内部のpixelの濃度差(高低差)を少なくする。平滑化すると、画像からノイズが減り比較しやすくなる。GaussianBlurについて: https://www.blog.umentu.work/python-opencv3%E3%81%A7gaussian%E3%82%AA%E3%83%9A%E3%83%AC%E3%83%BC%E3%82%BF%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%B9%B3%E6%BB%91%E5%8C%96/ 平滑化のメリット：http://imagingsolution.blog.fc2.com/blog-entry-88.html
4. `delta_frame`の作成。差分画像。`absdiff`を使う。grayscale画像は0白~255黒で表現される。2つのgray scaleの画像を渡す。その２つの画像の差を絶対値で表現した情報を得る。つまりfirstframeの画像と比較対象frameの差分が抽出された画像ができる。背景差分について：https://www.blog.umentu.work/python3-opencv3%E3%81%A7%E8%83%8C%E6%99%AF%E5%B7%AE%E5%88%86%E3%82%92%E6%B1%82%E3%82%81%E3%82%8B/
5. `thresh_frame`の作成。白黒画像。`cv2.threshold`を使う。グレースケール上において、閾値以下を黒。それ以上を白にする。つまり30は`delta_fram`において得た2つの画像間の差が30以上である場合に白へ。30以下ならば黒へ。この場合30はつまり2つの画像における変化量を意味できる。2つの画像において差がない場合は0。255はgrayscaleの範囲。0白~255黒。cv2.CHAIN_APPROX_SIMPLEは魔法の言葉。閾値以上を白へ。閾値以下を黒にするための記述。白と黒の位置を反転させたり、他にも種類がある。opencvの閾値のチュートリアル：　http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_thresholding/py_thresholding.html
6. `cv2.dilate`対象の白い領域を膨張させる。ノイズ除去に使われる。なんのこっちゃ。効用としては外線の取得精度が上がる。こいつがあると外線がよりはっきりと線になってくれる。ないと外線がなんかぶつ切りの点の集合みたいな感じになる。実際は引数のNoneのところに、具体的な処理を記述するっぽい。今回は単純に外線を線にするために使ってる。iteration=2は何回膨張処理を行うか。参考元公式チュートリアル:膨張処理：http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_morphological_ops/py_morphological_ops.html 膨張処理について：https://www.blog.umentu.work/python-opencv3%E3%81%A7%E7%94%BB%E7%B4%A0%E3%81%AE%E8%86%A8%E5%BC%B5%E5%87%A6%E7%90%86dilation%E3%81%A8%E5%8F%8E%E7%B8%AE%E5%87%A6%E7%90%86erosion-%E3%81%A1%E3%82%87%E3%81%A3%E3%81%A8%E8%A7%A3/
7. `(_,cnts,_)`はopencv3特有の書き方。`findContours`で取得した`thresh_frame`画像の白い部分の外線の情報を保存している。中身の参照には`cnts`で取得できる。普通の変数名とかただ名前を`cnts`とかにするとエラー出る。`findContours `の返り値を受けるためには、opencvのversionが3.00以上なら`(_,cnts,_)`じゃないとだめっぽい。参考元：https://www.pyimagesearch.com/2015/08/10/checking-your-opencv-version-using-python/
8. 上記の方法で受け取れば`cnts`として外線の情報を格納するリストが入ってる(`findContours`の返り値`)。contourは外線の意味。`cv2.contourArea`でpixel換算の面積が返ってくる。`cv2.boundingRect`で外接矩形(外線に外接する四角形)の1座標と縦横の長さをtupleで得られる。`cv2.drawContours`は外線の情報を受け取って、その外線通りに描写する。`cv2.rectangle`は四角形を描写する。前回やったやつ。ここでstatus=1の場合のみ、外線の描写を行う＝なにか元の画像と違うものが画面に入ってきた。
9. status_listでは画面内部が変わったどうかを判定して、時刻を`times`のリストに保存。0→1は何らかの変化発生。1→0は変化終了。この2点で時間を保存。開始時刻と終了時刻になる
10. `waitKey()`と`ord()`で`q`が入力されたときに画像のキャプチャを終わる。それぞれ入力された値のasciiかunicode?の数値を保存する。
11. pandasのdateframeにtimesの時刻を入れ直す。`ignore_index=True`はindexの流れをきれいな順序(0,1,0,1とかじゃなくて,0,1,2,3,4になる)にしてくれる。参考元：https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.append.html

##実際に使用すると

駄目な状況：
* 光源がカメラの内部にあると途端に精度がガバガバになる。あるいは動き続けるオブジェクトをfirstframeに入れちゃってもガバガバ。
* 逆にプログラム起動時にカメラを動かしちゃうとがバル。画面全部がでかい四角で囲まれる

良い状況；
* 部屋の電気を全部消したら結構うまくいく。一番うまくモーションを検知できたのは、部屋の電気真っ暗、光源がmacのスクリーンのみのとき。この場合は、手のひらをすばやくささっと出し入れしてもうまくキャッチしてくれる。きれいに細かい外形も描写してくれる。


##次
次はbokehライブラリーで取得したcsvをグラフにして、htmlで表示するように記述を加える。