0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-18

目的

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

完成イメージ
構成図_概要.png

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

環境

image.png

詳細

  • Raspberry Pi 3 ModelB
  • カメラモジュール:Raspberry Pi Camera Module V2

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

作業メモ

OpenCVによる顔認識プログラムの導入("環境"の③の下準備)

参考サイト
https://sozorablog.com/camera_shooting/
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	# これ
・・・

顔認証を試してみる
maru が学習時に登録した自分の名前で、自分の顔を撮影した際に検知できたことを確認

$ 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'

"環境"の機能を実装

人が来たことを検知する機能("環境"の①)

参考サイト
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による不審者特定する機能("環境"の②、③)

参考サイト
https://sozorablog.com/camera_shooting/

  • 前項で使った facial_req.py(顔認識するプログラム) をベースに認識した顔が不審者かどうかを確認できる機能を追加(プログラム中では FlgUnknown で情報管理)2
  • User Setting を各自の環境に応じて設定が必要
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 Notify3へ通知をする機能("環境"の④)

参考サイト
https://sozorablog.com/camera_shooting/ 4

  • 前項FlgUnknown1(不審者あり)の場合に実行される
  • 警告文とカメラ画像を送信する機能
  • User Setting を各自の環境に応じて設定が必要
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として登録

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

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

  • 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通知画面)

※画像は適当な雑誌の画像です

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

image.png

まとめ

目的の達成度は暫定達成くらいのレベル

M5StickCを使った玄関の鍵かけ忘れ防止システム(運用編) に監視カメラ要素を追加して、防犯システムを作る1

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

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

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

  3. LINE Notifyの設定については、https://sozorablog.com/pythonline/ を参照ください

  4. 何度も参考サイトとして出典しているが、本件では非常に参考にさせていただきました

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?