【試作】Raspberry Piを使った監視カメラ(OpenCVによる不審者検知)

## 目的
[M5StickCを使った玄関の鍵かけ忘れ防止システム(運用編)](https://qiita.com/marumen/items/5e435dc85e2b5e639d73) に監視カメラ要素を追加して、防犯システムを作る[^1]
(["まとめ"](#まとめ)でも言及するが、まだ課題が多く実運用には耐えられないため、今回は試作の位置づけ)

<INS><b>完成イメージ</b></INS>
![構成図_概要.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248487/d4e91268-f368-a79e-a3ef-db162db3ba36.png)

ドアの鍵状態の検知は、[M5StickCを使った玄関の鍵かけ忘れ防止システム(運用編)](https://qiita.com/marumen/items/5e435dc85e2b5e639d73) で実現済みなので、今回は監視カメラの機能を実現する

[^1]:実は以前似たことをやっているが、仕様を変更したので再度投稿した

## 環境
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248487/923430f7-221e-5bfd-474c-a91724641fb2.png)

<b>詳細</b>
* Raspberry Pi 3 ModelB
* カメラモジュール：Raspberry Pi Camera Module V2

https://www.amazon.co.jp/Raspberry-Pi-Camera-%E3%82%AB%E3%83%A1%E3%83%A9%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB-Daylight/dp/B01ER2SKFS

* 人感センサー(HC-SR501)

https://www.amazon.co.jp/Ren-He-HC-SR501%E4%BA%BA%E4%BD%93%E8%B5%A4%E5%A4%96%E7%B7%9A%E6%84%9F%E5%BF%9C%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB-%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E7%84%A6%E9%9B%BB%E5%9E%8B%E8%B5%A4%E5%A4%96%E7%B7%9A%E6%A4%9C%E5%87%BA%E5%99%A8PIR%E3%83%A2%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-%E3%83%9C%E3%83%87%E3%82%A3%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB/dp/B07DCKZS5S

## 作業メモ

### OpenCVによる顔認識プログラムの導入("環境"の③の下準備)
<b>参考サイト</b>
https://sozorablog.com/camera_shooting/
https://youtu.be/9P-Hq8Dh1R0

+ [参考サイト](https://youtu.be/9P-Hq8Dh1R0) で紹介されている顔認証プログラムを導入

```
$ git clone https://github.com/kotamorishi/installOpenCV.git
$ cd installOpenCV
$ ./installOpenCV.sh
・・
Cloning into 'facial_recognition'...
remote: Enumerating objects: 57, done.
remote: Counting objects: 100% (57/57), done.
remote: Compressing objects: 100% (30/30), done.
remote: Total 57 (delta 23), reused 52 (delta 22), pack-reused 0
Unpacking objects: 100% (57/57), done.
```

+ Opencvを導入
```
$ sudo pip3 install opencv-python
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting opencv-python
  Downloading https://www.piwheels.org/simple/opencv-python/opencv_python-4.7.0.68-cp37-cp37m-linux_armv7l.whl (11.8 MB)
     qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 11.8/11.8 MB 1.5 MB/s eta 0:00:00
Collecting numpy>=1.17.0
  Downloading numpy-1.21.6.zip (10.3 MB)
     qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 10.3/10.3 MB 1.7 MB/s eta 0:00:00
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
Building wheels for collected packages: numpy
  Building wheel for numpy (pyproject.toml) ... done
  Created wheel for numpy: filename=numpy-1.21.6-cp37-cp37m-linux_armv7l.whl size=12328135 sha256=6b8e66103117c4d84f9a40e5fac7bc91b8dfacff477fe57692beac60e901d5e4
  Stored in directory: /root/.cache/pip/wheels/f8/ee/2a/5ceed41a4d2d69e4f5133e5789b80cd7604fb4eeba6b9d9184
Successfully built numpy
Installing collected packages: numpy, opencv-python
  Attempting uninstall: numpy
    Found existing installation: numpy 1.16.2
    Uninstalling numpy-1.16.2:
      Successfully uninstalled numpy-1.16.2
Successfully installed numpy-1.21.6 opencv-python-4.7.0.68
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
```

+ 導入した環境の動作確認

自分の顔画像撮影
(自分を不審者として検知されないために撮影は必要)

```
$ cd ~/facial_recognition/
$ python3 headshots.py
```

撮影した画像の学習
```
$ python3 train_model.py
[INFO] start processing faces...
[INFO] processing image 1/12
[INFO] processing image 2/12
[INFO] processing image 3/12
[INFO] processing image 4/12
[INFO] processing image 5/12
[INFO] processing image 6/12
[INFO] processing image 7/12
[INFO] processing image 8/12
[INFO] processing image 9/12
[INFO] processing image 10/12
[INFO] processing image 11/12
[INFO] processing image 12/12
[INFO] serializing encodings...
```

学習により生成されたモデルの確認
```
$ ls -l
合計 968
-rw-r--r-- 1 pi pi    674  1月  2 17:20 README.md
drwxr-xr-x 4 pi pi   4096  1月  2 22:37 dataset
-rw-r--r-- 1 pi pi   7675  1月  2 22:41 encodings.pickle	# これ
・・・
```

顔認証を試してみる
<b>→ `maru` が学習時に登録した自分の名前で、自分の顔を撮影した際に検知できたことを確認</b>
```
$ python3 facial_req.py
[INFO] loading encodings + face detector...
[INFO] starting video stream...
# ここでカメラで自分の顔を撮影
maru
```

※ちなみにTeratermなどで`facial_req.py`を実行した場合、ディスプレイの画像表示ができずエラーになるケースがあるので、その場合はリモートデスクトップなどで確認してください
```
$ python3 facial_req.py
[INFO] loading encodings + face detector...
[INFO] starting video stream...
Unable to init server: Could not connect: 接続を拒否されました
Traceback (most recent call last):
  File "facial_req.py", line 105, in <module>
    cv2.imshow("Facial Recognition is Running", frame)
cv2.error: OpenCV(4.7.0) /tmp/pip-wheel-hb849x9b/opencv-python_1ad64f0dd2fc45758056f9a15186db35/opencv/modules/highgui/src/window_gtk.cpp:635: error: (-2:Unspecified error) Can't initialize GTK backend in function 'cvInitSystem'
```

### "環境"の機能を実装
#### 人が来たことを検知する機能("環境"の①)
<b>参考サイト</b>
https://qiita.com/atmaru/items/2282445d327b0af0e6c1
https://tomosoft.jp/design/?p=8685

+ 人感センサーの立ち上がりエッジを検出して、後述するカメラ撮影やLINE Notifyへの通知をするpython moduleを動作
+ `User Setting` を各自の環境に応じて設定が必要

```gpio_int.py
#!/usr/bin/python3

import RPi.GPIO as GPIO
import time
import sys
from importlib import import_module

##### User Setting ###############################
# moduleファイルのpath
modulePath = "/home/pi/facial_recognition"
# 人感センサーのGPIO接続先
DETECT_PIN = 23
##################################################

# moduleファイルのimport
sys.path.append(modulePath)
facial_req = import_module("facial_req_module")
send_message = import_module("send_message_module")

def main():
    GPIO.setwarnings(False)
    # layout設定
    GPIO.setmode(GPIO.BCM)
    # BCMの23番ピンを入力に設定
    GPIO.setup(DETECT_PIN, GPIO.IN)
    # callback登録（GPIO.RISING:立上がりエッジ検出、bouncetime:300ms）
    GPIO.add_event_detect(DETECT_PIN, GPIO.RISING, callback=callback, bouncetime=300)

    try:
        # 無限ループ
        while(True):
            time.sleep(1)

    # Keyboard入力があれば終わり
    except KeyboardInterrupt:
        print("break")
        GPIO.cleanup()

def callback(channel):
    print("button pushed %s"%channel)
    FlgUnknown = facial_req.facial_req()
    if FlgUnknown == 1:
       send_message.send_message()

if __name__ == "__main__":
    main()
```

#### カメラ撮影、OpenCVによる不審者特定する機能("環境"の②、③)
<b>参考サイト</b>
https://sozorablog.com/camera_shooting/

+ [前項](#opencvによる顔認識プログラムの導入環境のの下準備)で使った `facial_req.py`(顔認識するプログラム) をベースに`認識した顔が不審者かどうかを確認できる機能`を追加(プログラム中では `FlgUnknown` で情報管理)[^2]
+ `User Setting` を各自の環境に応じて設定が必要

[^2]:ちなみにベースにしたコードはMIT LICENSEなので、流用することに問題はなし

```facial_req_module.py
#! /usr/bin/python3

# import the necessary packages
from imutils.video import VideoStream
from imutils.video import FPS
import face_recognition
import imutils
import pickle
import time
import cv2

##### User Setting ###############################
# 顔認識プログラムのloop回数
MAX_LOOP_NUM = 30
# installした"facial_recognition"フォルダのpath
# unknown検知時の画像ファイル保存先path
datapath = "/home/pi/facial_recognition/"
##################################################

def facial_req():

        #Initialize 'currentname' to trigger only when a new person is identified.
        currentname = "unknown"
        #Determine faces from encodings.pickle file model created from train_model.py
        encodingsP = datapath + "encodings.pickle"
        #use this xml file
        cascade = datapath + "haarcascade_frontalface_default.xml"

        # load the known faces and embeddings along with OpenCV's Haar
        # cascade for face detection
        print("[INFO] loading encodings + face detector...")
        data = pickle.loads(open(encodingsP, "rb").read())
        detector = cv2.CascadeClassifier(cascade)

        # initialize the video stream and allow the camera sensor to warm up
        print("[INFO] starting video stream...")
        vs = VideoStream(src=0).start()
        #vs = VideoStream(usePiCamera=True).start()
        time.sleep(2.0)

        # loop over frames from the video file stream
        loop_count = 0
        for loop_count in range(MAX_LOOP_NUM):

                # "Unknown" flag
                FlgUnknown = 0

                # grab the frame from the threaded video stream and resize it
                # to 500px (to speedup processing)
                frame = vs.read()
                frame = imutils.resize(frame, width=500)

                # convert the input frame from (1) BGR to grayscale (for face
                # detection) and (2) from BGR to RGB (for face recognition)
                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

                # detect faces in the grayscale frame
                rects = detector.detectMultiScale(gray, scaleFactor=1.1,
                        minNeighbors=5, minSize=(30, 30),
                        flags=cv2.CASCADE_SCALE_IMAGE)

                if len(rects) > 0:

                        # OpenCV returns bounding box coordinates in (x, y, w, h) order
                        # but we need them in (top, right, bottom, left) order, so we
                        # need to do a bit of reordering
                        boxes = [(y, x + w, y + h, x) for (x, y, w, h) in rects]

                        # compute the facial embeddings for each face bounding box
                        encodings = face_recognition.face_encodings(rgb, boxes)
                        names = []

                        # loop over the facial embeddings
                        for encoding in encodings:
                                # attempt to match each face in the input image to our known
                                # encodings
                                matches = face_recognition.compare_faces(data["encodings"],
                                        encoding)
                                name = "Unknown" #if face is not recognized, then print Unknown

                                # check to see if we have found a match
                                if True in matches:
                                        # find the indexes of all matched faces then initialize a
                                        # dictionary to count the total number of times each face
                                        # was matched
                                        matchedIdxs = [i for (i, b) in enumerate(matches) if b]
                                        counts = {}

                                        # loop over the matched indexes and maintain a count for
                                        # each recognized face face
                                        for i in matchedIdxs:
                                                name = data["names"][i]
                                                counts[name] = counts.get(name, 0) + 1

                                        # determine the recognized face with the largest number
                                        # of votes (note: in the event of an unlikely tie Python
                                        # will select first entry in the dictionary)
                                        name = max(counts, key=counts.get)

                                        #If someone in your dataset is identified, print their name on the screen
                                        if currentname != name:
                                                currentname = name
                                                #print(currentname)

                                        # stop sequence
                                        facial_req_stop(vs)

                                        FlgUnknown = 0
                                        print("face detect " + currentname)
                                        return FlgUnknown

                        # update the list of names
                        names.append(name)

                        # not detect in list(=Unknown)
                        print("face detect Unknown")
                        cv2.imwrite(datapath + 'unknown.jpg', frame)
                        FlgUnknown = 1

                        # stop sequence
                        facial_req_stop(vs)
                        return FlgUnknown

                else:
                        print("not detect")
                        time.sleep(1)

        # stop sequence
        facial_req_stop(vs)

        # end of process
        return FlgUnknown

def facial_req_stop(vs):
        # do a bit of cleanup
        vs.stop()
```

#### 不審者を検知した場合にLINE Notify[^3]へ通知をする機能("環境"の④)
<b>参考サイト</b>
https://sozorablog.com/camera_shooting/ [^4]

+ [前項](#カメラ撮影opencvによる不審者特定する機能環境の)の`FlgUnknown`が`1(不審者あり)`の場合に実行される
+ 警告文とカメラ画像を送信する機能
+ `User Setting` を各自の環境に応じて設定が必要

[^3]:LINE Notifyの設定については、https://sozorablog.com/pythonline/ を参照ください
[^4]:何度も参考サイトとして出典しているが、本件では非常に参考にさせていただきました

```send_message_module.py
#!/usr/bin/python3

import requests

##### User Setting ###############################
# unknown検知時の画像ファイル保存先path
datapath = "/home/pi/facial_recognition/"
##################################################

def send_message():
        url = "https://notify-api.line.me/api/notify"
        token = "ZTmXhjE98KgvLLHQqAajJHYit9x8MUXkI8HO7zNFbOg"
        headers = {"Authorization" : "Bearer "+ token}
        files = {'imageFile': open(datapath + "unknown.jpg", "rb")}
        message =  "detect Unknown"
        payload = {"message" :  message}
        r = requests.post(url, headers = headers, params=payload, files=files)
```

### serviceとして登録
ここまでに実装した機能を毎回自動で起動するよう、システムのサービスとして登録する

以前、自分で作った記事を参考に登録

https://qiita.com/marumen/items/78f65ad946a69603382e#service%E3%81%A8%E3%81%97%E3%81%A6%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B

+ service用のファイル作成
```gpio_int.service
[Unit]
Description=GPIO INTTERUPT
After=multi-user.target
DefaultDependencies=no

[Service]
Type=simple
ExecStart=/usr/bin/gpio_int.py
Restart=no

[Install]
WantedBy=multi-user.target
```

+ 作成したファイルの配置
```
$ chmod u+x gpio_int.py
$ sudo cp gpio_int.py /usr/bin/
$ sudo cp gpio_int.service /etc/systemd/system/
```

+ サービスとして認識されているか確認
→ 認識はされているのでOK
```
$ sudo systemctl list-unit-files --type=service | grep gpio_int
gpio_int.service                       disabled
```

+ 動作確認
→ エラーなく起動し終了しているのでOK

```
$ sudo systemctl start gpio_int
$ sudo systemctl stop gpio_int
$ sudo systemctl status gpio_int
● gpio_int.service - GPIO INTTERUPT
   Loaded: loaded (/etc/systemd/system/gpio_int.service; disabled; vendor preset: enabled)
   Active: inactive (dead)

 3月 18 21:00:46 raspberrypi systemd[1]: Started GPIO INTTERUPT.
 3月 18 21:00:50 raspberrypi systemd[1]: Stopping GPIO INTTERUPT...
 3月 18 21:00:50 raspberrypi systemd[1]: gpio_int.service: Main process exited, code=killed, status=15/TERM
 3月 18 21:00:50 raspberrypi systemd[1]: gpio_int.service: Succeeded.
 3月 18 21:00:50 raspberrypi systemd[1]: Stopped GPIO INTTERUPT.
 3月 18 21:01:40 raspberrypi systemd[1]: Started GPIO INTTERUPT.
 3月 18 21:01:41 raspberrypi systemd[1]: Stopping GPIO INTTERUPT...
 3月 18 21:01:41 raspberrypi systemd[1]: gpio_int.service: Main process exited, code=killed, status=15/TERM
 3月 18 21:01:41 raspberrypi systemd[1]: gpio_int.service: Succeeded.
 3月 18 21:01:41 raspberrypi systemd[1]: Stopped GPIO INTTERUPT.
```

+ サービスの有効化
```
$ sudo systemctl enable gpio_int
Created symlink /etc/systemd/system/multi-user.target.wants/gpio_int.service → /etc/systemd/system/gpio_int.service.
```

+ 再起動して確認
→ 再起動後、サービスが起動していることを確認できたのでOK
```
$ sudo reboot
$ sudo systemctl status gpio_int
● gpio_int.service - GPIO INTTERUPT
   Loaded: loaded (/etc/systemd/system/gpio_int.service; enabled; vendor preset:
   Active: active (running) since Sat 2023-03-18 21:04:42 JST; 1min 33s ago
 Main PID: 846 (gpio_int.py)
    Tasks: 2 (limit: 1939)
   CGroup: /system.slice/gpio_int.service
           mq846 /usr/bin/python3 /usr/bin/gpio_int.py

 3月 18 21:04:42 raspberrypi systemd[1]: Started GPIO INTTERUPT.
```

### 動作結果(LINE Notify通知画面)
※画像は適当な雑誌の画像です

以下の通り、<b>不審者を検知した場合に、警告文とカメラ画像をLINE Notifyへ通知することができた</b>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248487/dcf9c27b-4b7e-0456-f490-924b5d3592e2.png)


## まとめ
目的の達成度は暫定達成くらいのレベル
>[M5StickCを使った玄関の鍵かけ忘れ防止システム(運用編)](https://qiita.com/marumen/items/5e435dc85e2b5e639d73) に監視カメラ要素を追加して、防犯システムを作る[^1]

理由としては実運用を考えると、残課題が多く残っているため
今後は以下を解決できる方法を検討していく必要あり

* 暗所での利用
  * 現在使っているカメラは暗所での撮影は非対応
  * 監視カメラという用途を考えると、暗所での撮影対応は必要
* モバイル対応
  * 簡単に電源供給ができない場所での利用を想定した場合に必要
  * モバイルバッテリーなどの利用を検討
  * その場合は消費電力を減らす努力も必要
* とにかく実現までのコスパが悪い
  * 今回はありものを使って実現したので、材料準備のコストはかかっていないが、一から揃える場合は結構なコストがかかる
  * 正直Switchbotでいいのでは？と思ってしまうところはある・・