📲

Raspberry Pi+OpenCVで動体検出してLINE通知

2022/09/24に公開

カメラモジュールも無事使用できるようになったのでとりあえず目標であった動体検出してLINE通知をしてみました。
https://zenn.dev/technicarium/articles/449294af295d5c

OSバージョン

$ cat /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

カメラモジュールの認識状態

$ vcgencmd get_camera
supported=1 detected=1, libcamera interfaces=0

環境

Python

python --version
Python 3.10.6

OpenCV

$ pipenv graph
opencv-python==4.6.0.66
  - numpy [required: >=1.19.3, installed: 1.23.3]
  - numpy [required: >=1.19.3, installed: 1.23.3]
  - numpy [required: >=1.14.5, installed: 1.23.3]
  - numpy [required: >=1.17.3, installed: 1.23.3]
  - numpy [required: >=1.21.2, installed: 1.23.3]

コード

OpenCVはまったく使用したことがなかったのですが参考にさせていただいたサイトのサンプルソースをベースに書き足すことで実装できなたのでとても助かりました。

https://sasuwo.org/motion-detection/

ざっくり仕様

  • 起動時はLINE通知無効
  • スペースキーでLINE通知の有効/無効切り替え
  • 指定した時間内(INT_COUNT_INTERVAL)に検出された場所の輪郭サイズ(INT_DETECT_SIZE_W、INT_DETECT_SIZE_H)以上の場合カウントアップ
  • LINE通知条件
    • カウントアップした値が上限(INT_COUNT_MAX)を越えた場合
    • 前回通知時刻から指定秒(INT_INTERVAL)以上経過した場合
    • LINE通知有効状態
  • 終了はENTERキー
from wsgiref.simple_server import sys_version
import cv2
import datetime
import requests
import time
import sys

STR_NOTIFY_TOKEN = '[トークン]'
STR_NOTICE_MESSAGE = '[メッセージタイトル]'
STR_JPG_PATH='[画像パス]'
STR_JPG_FILE_NAME = 'test.jpg'
INT_DETECT_SIZE_W = 80 # 差を検知した箇所の内、カウント対象にする最小サイズ(横)
INT_DETECT_SIZE_H = 80 # 差を検知した箇所の内、カウント対象にする最小サイズ(縦)
INT_INTERVAL = 10 # LINE通知最小間隔(秒)
INT_COUNT_INTERVAL = 2 # カウントリセット間隔(秒)
INT_COUNT_MAX = 20 # LINE通知対象カウント数(INT_COUNT_INTERVAL秒内)

# LINE NOTIFY API発行
def fnc_line_notify():
    line_notify_api = 'https://notify-api.line.me/api/notify'
    headers = {'Authorization': f'Bearer {STR_NOTIFY_TOKEN}'}
    data = {'message': f'{STR_NOTICE_MESSAGE}'}
    files={'imageFile':open(STR_JPG_PATH + STR_JPG_FILE_NAME,'rb')}
    requests.post(line_notify_api, headers=headers, data=data,files=files)
    return

# メイン処理
# python3.10から使用可能なmatchを使用している為バージョン確認
if (sys.version_info.major == 3) and (sys.version_info.minor < 10):
    print('Python Version 3.10 以上で動作します')
    sys.exit()


print('動体検知を開始します。')
print('LINE通知は無効です')
print(str(datetime.datetime.now()))

# 初期化
cap = cv2.VideoCapture(0)
before = None

tm_line_notify_time = time.time()
bln_line = False
int_count = 0
tm_count_time = time.time()

# Enterキーが押されるまで無限ループ
while True:
    # 画像を取得
    ret, frame = cap.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の画像を2値化
    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 > INT_DETECT_SIZE_W and  h > INT_DETECT_SIZE_H:

            areaframe = cv2.rectangle(frame, (x, y), (x+w, y+h), (0,255,0), 2)

            # カウント開始時刻と現在時刻の差とカウント間隔を比較
            if time.time() - tm_count_time < INT_COUNT_INTERVAL :
                int_count += 1
            else:
                # カウント集計開始時刻更新
                tm_count_time = time.time()
                int_count = 0

            # LINE通知判定
            if (time.time() - tm_line_notify_time   > INT_INTERVAL and  
                bln_line and 
                int_count > INT_COUNT_MAX):
                # jpgファイル作成
                cv2.imwrite(STR_JPG_PATH + STR_JPG_FILE_NAME,areaframe)
                fnc_line_notify()
                tm_line_notify_time = time.time()

    # ウィンドウで表示
    cv2.imshow('target_frame', frame)

    # 入力キー判定
    match cv2.waitKey(1):
        # Enterキーが押されたらループを抜ける
        case 13:
            break
        # スペースキーでLINE通知無効化、有効化
        case 32:
            bln_line = not bln_line
            print('LINE通知は有効です') if bln_line else print('LINE通知は無効です')

print("動体検知を終了します。")
print(str(datetime.datetime.now()))

# ウィンドウの破棄
cv2.destroyAllWindows()

感想

過剰に検出して通知してしまうことを以下にほどよく抑えるかがちょっと苦労しました、検出サイズや通知間隔を設定したり検出回数を判定したりしました。

  • ご認識ケース
    • ガラスの反射による映り込みによる誤認識
    • 床の振動による誤認識
    • 雷雨による誤認識
    • カメラモジュールの揺れによる誤認識

Discussion