RaspberryPiでWEBカメラを使ってOpenCVで動体検知な監視カメラをつくってみた

## やりたいこと
　居住しているマンションの玄関に夜な夜な不審者が出没しているようなので、玄関のドアにWEBカメラを取り付けRaspberryPiで解析・記録するシステムを作ってみました。仕様は次の通りです。

- 画面上に撮影した映像と時刻を表示する
- 玄関の前に誰かきたら静止画をjpeg形式で記録する
- 記録した静止画には撮影時刻を埋め込む
- 誤検知はある程度許容する

※ちなみに、カメラの設置は許可取得済みです。

## 参考にした記事

最初は[パソコン工房さん](https://www.pc-koubou.jp/magazine/19205)の記事を参考に顔検知でやろうと思いました、誤検知が多くて今回の用途には適しませんでした。
> https://www.pc-koubou.jp/magazine/19205

いろいろ考えた結果、動体検知（撮影している映像に変化があったことを検知）によって実現できそうだな、と思い試行錯誤の結果そこそこ上手く行ったので方法を紹介します。

なお、動体検知のやりかたは[こちらの記事](https://qiita.com/K_M95/items/4eed79a7da6b3dafa96d)を参考にしました。
> https://qiita.com/K_M95/items/4eed79a7da6b3dafa96d

## 必要なもの
- Raspberry Pi 3 Model B Plus Rev 1.3(USBが刺さるラズパイならなんでもいい？)
- USB接続のWEBカメラ(多分なんでも良い、結構古いものを使用)
- Python (今回はv2.7を使用)
- OpenCV　(今回はv3.2.0を使用）

OpenCVのインストール手順は、上記の[パソコン工房](https://www.pc-koubou.jp/magazine/19205)の記事を参考にしています。Pythonはもとから入っている物を使用。

## 構築手順
1. RaspberryPiのセットアップ
2. 必要に応じてVNC環境の設定
3. OpenCVのインストール
4. WEBカメラを接続
5. WEBカメラが認識されたか確認
6. プログラムを書いて実行

## WEBカメラの認識確認
USB接続のWEBカメラはドライバが最初から入っているみたいなので、接続すれば認識します。

```
$ lsusb
Bus 001 Device 006: ID 0c45:62e0 Microdia MSI Starcam Racer
Bus 001 Device 005: ID 0424:7800 Standard Microsystems Corp. 
Bus 001 Device 003: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 002: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
```

lsusbコマンドの結果にUSBカメラっぽい名前が表示されていればOKです。わからなければ、抜いたときと接続したときで項目が増えるか確認しましょう。

## ソースコード
書いたコードは下記の通りです。

```security_cam_motion.py
 # -*- coding: utf-8 -*-
import time
import datetime
import cv2 as cv

# WEBカメラを使って監視カメラを実現するプログラム
# 動体検知、そのときの日付時刻を埋め込んだjpgファイルを保存する


#画像を保存するディレクトリ
save_dir  = './image/'

#ファイル名は日付時刻を含む文字列とする
#日付時刻のあとに付加するファイル名を指定する
fn_suffix = 'motion_detect.jpg'

# VideoCaptureのインスタンスを作成する。
cap = cv.VideoCapture(0) 

#縦と横の解像度指定
cap.set(cv.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv.CAP_PROP_FRAME_HEIGHT, 480)

#2値化したときのピクセルの値
DELTA_MAX = 255

#各ドットの変化を検知するしきい値
DOT_TH = 20

#モーションファクター(どれくらいの点に変化があったか)が
#どの程度以上なら記録するか。
MOTHON_FACTOR_TH = 0.20

#比較用のデータを格納
avg = None

while True:
    
    ret, frame = cap.read()     # 1フレーム読み込む
    motion_detected = False     # 動きが検出されたかどうかを示すフラグ

    dt_now = datetime.datetime.now() #データを取得した時刻

    #ファイル名と、画像中に埋め込む日付時刻
    dt_format_string = dt_now.strftime('%Y-%m-%d %H:%M:%S') 
    f_name = dt_now.strftime('%Y%m%d%H%M%S%f') + "_" + fn_suffix


    # モノクロにする
    gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    
    #比較用のフレームを取得する
    if avg is None:
        avg = gray.copy().astype("float")
        continue
        
    
    # 現在のフレームと移動平均との差を計算
    cv.accumulateWeighted(gray, avg, 0.6)
    frameDelta = cv.absdiff(gray, cv.convertScaleAbs(avg))

    # デルタ画像を閾値処理を行う
    thresh = cv.threshold(frameDelta, DOT_TH, DELTA_MAX, cv.THRESH_BINARY)[1]

    #モーションファクターを計算する。全体としてどれくらいの割合が変化したか。
    motion_factor = thresh.sum() * 1.0 / thresh.size / DELTA_MAX 
    motion_factor_str = '{:.08f}'.format(motion_factor)
    
    #画像に日付時刻を書き込み
    cv.putText(frame,dt_format_string,(25,50),cv.FONT_HERSHEY_SIMPLEX, 1.5,(0,0,255), 2)
   #画像にmotion_factor値を書き込む
    cv.putText(frame,motion_factor_str,(25,470),cv.FONT_HERSHEY_SIMPLEX, 1.5,(0,0,255), 2)
    
    #モーションファクターがしきい値を超えていれば動きを検知したことにする
    if motion_factor > MOTHON_FACTOR_TH:
        motion_detected = True
    
    # 動き検出していれば画像を保存する
    if motion_detected  == True:
        #save
        cv.imwrite(save_dir + f_name, frame)
        print("DETECTED:" + f_name)
    
    
    # ここからは画面表示する画像の処理
    # 画像の閾値に輪郭線を入れる
    image, contours, hierarchy = cv.findContours(thresh.copy(), cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    frame = cv.drawContours(frame, contours, -1, (0, 255, 0), 3)
    
    
    # 結果の画像を表示する
    cv.imshow('camera', frame)


    # 何かキーが押されるまで待機する
    k = cv.waitKey(1000)  #引数は待ち時間(ms)
    if k == 27: #Esc入力時は終了
        break
    

print("Bye!\n")
# 表示したウィンドウを閉じる
cap.release()
cv.destroyAllWindows()

```

## 起動コマンド
```
$ sudo python security_cam_motion.py
```

## 実行結果
実行した結果このようになりました。

変化がなければ何も起きません。
下側にある数字はモーションファクターです。変化がないので0.0になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126400/5bdf7508-93bb-59ce-feb8-d850c5294d17.png)


　玄関前に人が現れると変化が認識され、jpegファイルが保存されます。モーションファクターが一定以上になってるので動きを検知したことがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126400/f6949835-9e21-943f-57f3-b1102cd47d78.png)

保存するjpegファイルには枠線を表示していません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126400/f2aa2a14-2a9b-40ae-29d2-f4b69ca80e30.png)

## モーションファクターについて
　モーションファクター(motion factor)は私が勝手につくた造語です。画面全体ピクセルのうちどれくらいの割合が変化したかを示す係数です。

　動き検知といっていますが、原理は前フレームとの差があるピクセルを探しているということになります。ちょっとした環境の変化で微妙に前のフレームとの差が検出されることがあます。その度に検知していると監視カメラの役に立ちません。
　一方、人がカメラの前に立ったならば、かなりの割合のピクセルが変化するはずです。ある程度の割合が変化したならば記録するということにしておけば、「玄関前に人が来たら記録する」という目的が達成できそうです。

##誤検知(偽陽性)について
　偽陽性の誤検知について考えます。つまり、「人が来てないのに検知してしまう」ことについてどう対処するか。監視カメラを設置した場所はただのマンションの一室なので人が来ることはまれです。なので、偽陽性でデータが埋め尽くされると困ります。誤検知(偽陽性)を防ぐ方法を考えます。
　どれくらいの変化があれば...ということについては、モーションファクターと`cv.threshold()` 関数の第2引数(各ドットの前画像との変化のしきい値)で調整できそうです。
`DOT_TH` 変数を第2引数としているのでこの変数で調整します。あまり小さい値にすると環境の変化やピクセルのゆれで誤検知してしまいます。
　それでも、実際の動きがなくてもいくつかのピクセルはたまに変化を誤検知するので、モーションファクターで誤検知を防ぎます。

```
   : 
#各ドットの変化を検知するしきい値
DOT_TH = 20
#モーションファクター(どれくらいの点に変化があったか)が
#どの程度以上なら記録するか。
MOTHON_FACTOR_TH = 0.20
   :
# デルタ画像を閾値処理を行う
thresh = cv.threshold(frameDelta, DOT_TH, DELTA_MAX, cv.THRESH_BINARY)[1]
#モーションファクターを計算する。全体としてどれくらいの割合が変化したか。
motion_factor = thresh.sum() * 1.0 / thresh.size / DELTA_MAX 
motion_factor_str = '{:.08f}'.format(motion_factor)
   :
```

　
##誤検知(偽陰性)について
　逆に、「人がいるのに記録されない」については監視カメラとしては役に立ちません。
　おそらくですがこのシステムでは人がいるのに記録されないケースとしては「壁と同じ配色の服を着ている」「画面の端に見切れている」「光学迷彩を着用している」とかになるとおもいます。この場合は何が写っているのわからなすぎて写っていても逆に役に立たない事が考えられるので許容することにしました。
　完璧を求めてもしょうがないですね。バランスが大事です。

---
以上