育児×家事×IoT Raspberry Piで最強の防犯カメラを作ってみる(動画記録・配信、動体検知・Line通知、顔検知・顔認証、Alexa搭載)[3/6]

やりたいこと(再掲)

最終的にやりたいことは、以下の5つの機能を持つ「最強の防犯カメラ」を作ることです。機能①〜機能③は、市販されている多くの防犯カメラでも持っている機能ですが、機能④や機能⑤の顔認証機能を持つ監視カメラはまだ多くないと思います。

 機能①.動画を24時間撮影し、カメラ本体に動画で記録する
 機能②.動画をWebブラウザや他の機器から参照できるようにライブ配信する
 機能③.動体を検知したら、静止画をLineに通知する
 機能④.家族の顔を認証したら、静止画をLineに通知する
 機能⑤.家族の顔を認証したら、◯◯さんおかえり!と喋る

図1.png

家に帰ると、顔を見て「○○さん、おかえり!」と言ってくれる辺りが、スマートハウスに一歩づ近づいている気がします。

実現に向けた連載

最強の「防犯カメラ」を作成するために、以下のように少しずつに記事を書いていきます(予定)。
 1回目:カメラの設定と動画記録
 2回目:カメラ映像のライブ配信
 3回目:動体検知機能とLineへの通知   ←この記事
 4回目:顔認証機能とLineへの通知
 5回目:Raspberry PiへのAlexaの搭載
 6回目:顔認証後にAlexaで音声通知

3回目:動体検知機能とLineへの通知

前回はカメラの映像をPCやスマホのブラウザから見えるようにライブ配信することをやってみました。今回は、カメラ映像の中に動体を検知したらLineに通知することをやってみたいと思います。

1.Line Notifyのアクセストークンを取得

Lineにメッセージを通知するためには、Line Notifyのアクセストーンを取得必要があります。まずは、このアクセストークンを取得していきます。

1.Line Notifyのサイトにアクセス

PCのWebブラウザで、Line NotifyのWebサイト「https://notify-bot.line.me/ja/」にアクセスします。
図1.png

2.ログイン

画面右上の「ログイン」をクリックして、普段スマホで使っているLineのメールアドレスとパスワードでログインします。
図2.png

3.マイページの表示

ここが分かりにくいのですが、右上のアカウント名の右の「▼」印をクリックして「マイページ」を選択します。
図3.png

4.新規トークンの発行

画面に発行済みのアクセストークンの一覧が表示されますが、下にスクロールしていくと「トークンを発行する」ボタンが出てきますので、クリックします。
図4.png

5.トークン情報の入力

トークン情報の入力画面が表示されるので、トークン名に適当な名前を入力し、トークルームの選択で「1:1でLINE Notifyから通知を受け取る」を選択して、発行ボタンをクリックします。なお、家族など複数の宛先に通知を送りたい場合は、スマホで予めグループを作成しておくとグループを選択できます。
図5.png

6.トークンの取得

トークンが発行され、画面に表示されるので「コピー」ボタンを押して、メモ帳などに貼り付けておきます。
図6.png

以上で、アクセストークンの取得は完了です。

2.Lineへの通知機能の作成

ここからは、Lineへの通知機能をNode-red上に作成していきます。Pythonのプログラムから直接通知しても良いのですが、Node-redを経由させることで、他の機器との連携性が格段に高くなります。

この記事では、Raspberry PiにNode-redをインストールして進めますが、既にホームサーバやHomeAssistantを利用している場合には、そのNode-redを使っても大丈夫です。

1.Node-redのインストール

Raspberry PiへのNode-redのインストールは、既に多くの情報があるので詳しく書きませんが、以下のコマンドを実行して「y」で答えていくと、自動的にnode.jpをアップデートしてNode-redをインストールしてくれます。インストールには、20分ぐらいかかるので、コーヒータイムにしましょう。

root@raspberrypi:/home/pi# bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)

This can take 20-30 minutes on the slower Pi versions - please wait.

  Stop Node-RED                       ✔
  Remove old version of Node-RED      ✔
  Remove old version of Node.js       ✔
  Install Node.js LTS                 ✔   Node v12.14.1   Npm 6.13.7
  Clean npm cache                     ✔
  Install Node-RED core               ✔   1.0.3 
  Move global nodes to local          -
  Install extra Pi nodes              ✔
  Npm rebuild existing nodes          -
  Add shortcut commands               ✔
  Update systemd script               ✔

Any errors will be logged to   /var/log/nodered-install.log

2.Node-redの自動起動設定

インストールが完了したら「/etc/systemd/system/node-red.service」を作成して、Node-redを自動起動するようにしておきます。

/etc/systemd/system/node-red.service
[Unit]
Description = Node-red
[Service]
Restart = always
Environment="NODE_RED_OPTIONS=--userDir /home/root/.node-red/"
ExecStart = /usr/bin/node-red-pi $NODE_RED_OPTIONS
ExecReload = /bin/kill -s HUP ${MAINPID}
ExecStop=/bin/kill -s TERM ${MAINPID}
[Install]
WantedBy = multi-user.target

ファイルが作成できたら、systemctlコマンドで自動起動を設定します。

root@raspberrypi:/etc/systemd/system# systemctl start node-red
root@raspberrypi:/etc/systemd/system# systemctl enable node-red
Created symlink /etc/systemd/system/multi-user.target.wants/node-red.service → /etc/systemd/system/node-red.service.

3.Node-red画面の表示

Node-redが起動したら、PCのブラウザで「http://<Raspberry PiのIPアドレス>:1880/」にアクセスして、Node-redの画面を表示させます。
図7.png

4.http inノードの追加

防犯カメラプログラムからの通知をWEB APIで受け付けるために、画面左側のパレットから「http in」ノードを探して、キャンパスにドラッグして追加します。「http in」の設定画面では、以下のように入力して「完了」ボタンをクリックします。

 ・メソッド:POST
 ・ファイルのアップロード:チェックを付ける
 ・URL:/CameraAction
 ・名前:CameraAction
図8.png

5.functionノードの追加1

次に「function」ノードを追加して、以下のように入力して「完了」ボタンをクリックします。

 ・名前:編集
 ・コード:図の下を参照
 ・出力数:2
図9.png

let files = msg.req.files;
let action=msg.payload.action;
let text=msg.payload.text;

let msg1 = msg;
let msg2 = {};

//ファイル無し
if (files.length === 0) {
   msg1.payload = "サーバ通知ERR 添付ファイルなし";
   return [msg1, null];

//ファイル有り
}else{
    //動体検知
    if(action=='move'){
        msg1.payload = "サーバ通知OK";
        msg2.text = text;
        msg2.payload = files[0].buffer;
        msg2.filename = "/root/.node-red/cameraPicts/" + files[0].originalname;
        return [msg1, msg2];

    //その他
    }else{
        msg1.payload = "サーバ通知ERR アクション不正";
        return [msg1, null];
    }
}

このプログラムでは、まず添付ファイルがあるかを判定し、無かったらエラーメッセージを返すようにしています。添付ファイルがあった場合には「action」パラメータの値を見て「move」だったら、呼び出し元に返すメッセージを生成すると共に、ファイルを保存するためにmsg2に値を設定しています。また、「action」パラメータが「move」でなかった場合も同様にエラーメッセージを返します。

6.http responceノードの追加

防犯カメラのpythonプログラムに結果を返すために、「http responce」ノードを追加します。設定画面は特に何も変更する必要はありません。
図10.png

7.fileノードの追加1

Lineに画像を送信するために、一時的にファイルに保存します。「file」ノードを追加して「動作」の部分を設定して「完了」ボタンをクリックします。

 ・動作:ファイルを上書き
図11.png

8.functionノードの追加2

更にもう一つ「function」ノードを追加して、以下のように入力して「完了」ボタンをクリックします。
なお「Bearer」の後のxxxxxxの部分には、上で取得したLine Notifyのアクセストークンを記述してください。

 ・名前:編集
 ・コード:図の下を参照
 ・出力数:1
図12.png

msg.payload="-X POST https://notify-api.line.me/api/notify "+
    "-H 'Authorization: Bearer xxxxxxxxxxxxxxxxxx' "+
    "-F 'message="+msg.text+"' "+
    "-F 'imageFile=@"+msg.filename+"'";

return msg;

このプログラムでは、curlコマンドを用いてLine Notifyにアクセスする引数を生成しています。2行目でアクセストークンを設定し、3行目でLineに通知するメッセージ、4行目で画像のファイルを指定します。

9.execノードの追加

次に「exec」ノードを追加して、以下のように入力して「完了」ボタンをクリックします。上で設定した引数を用いて、curlコマンドを実行することでLineに通知を送ります。

 ・コマンド:curl
 ・引数:msg.payloadにチェックを付ける
図13.png

10.fileノードの追加2

送信後には画像ファイルは不要なので、もう一つ「file」ノードを追加して「動作」を設定して「完了」ボタンをクリックします。

 ・動作:ファイルを削除
図14.png

11.ノードの接続

上記の7つのノードの追加ができたら、以下のようにノードを接続します。
図15.png

12.デプロイ

最後に画面右上の「デプロイ」ボタンを押して、作成したフローをデプロイします。
図16.png

13.写真格納用ディレクトリの作成

上のfunctionノードの設定で指定した、画像を一時的に格納するディレクトリを作成します。

root@raspberrypi:/home/pi# mkdir /root/.node-red/cameraPicts/

以上で、Line通知機能の設定は完了です。

3.動体検知プログラムの作成

ここからは、前回作成した「Camera.py」に動体検知機能を追加していきます。
記述量が多いので、段階的に記載していきます。なお、完全なソースコードはGitHubのnaka-kazz/RasPiCameraに置いたので、コピペが面倒な方はこちらからダウンロードしちゃって下さい。

1.防犯カメラサービスの停止

前回、防犯カメラサービスを自動起動しているので、防犯カメラサービスが起動状態になっていると思います。
機能追加のために一旦止めておきましょう。

root@raspberrypi:/home/pi/camera# systemctl stop flask.service

2.importと定数定義の追加

まずは、Camera.pyに以下のimportと定数を追加します。

 ・Webリクエストを可能にするための「import requests」の追加
 ・画像格納パス、サーバ通知URL、インターバル、動体検知の精度の4つの定数の追加

/home/pi/camera/Camera.py
import picamera
import picamera.array
import cv2
import time
import datetime
import requests   #<--これを追加
from base_camera import BaseCamera

###################################################
## 定数定義
###################################################
#動画の格納パス
videopath='/home/pi/camera'
#<--------------------------ここから追加
#画像の格納パス
pictpath='/home/pi/camera/picts'
#サーバ通知のURL
url='http://localhost:1880/CameraAction'
#サーバ通知のインターバル(秒)
interval=30
#動体検知の精度
detectSize=1000
#<--------------------------ここまで追加

なお、動体通知のインターバルは、通知を行う間隔を秒単位で設定します。これを指定しないと、カメラの前に動くものがあるとひっきりなしに通知が来てLineが大変なことになってしまいます。また、動体検知の精度は、どの程度大きな変化(動体の面積)があった場合に通知を行うかを設定するものになります。プログラムが動くようになったら、動作させながら調整してみてください。

3.グローバル変数の追加

Camera.pyの定数定義の下に、以下のグローバル変数を追加します。

/home/pi/camera/Camera.py
###################################################
## グローバル変数
###################################################
#動体検知のための前情報保存用
befImg=None
befTimes=[0,0,0,0,0,0]

befImgは動体検知で、前画像と現画像を比較する場合に使う前画像の保存用の変数です。また、befTimesは前回サーバに通知を行った時間を格納しておくために利用します。動体検知ではbefTimes[0]しか利用しませんが、後の配列要素は次回以降の顔認証で使用します。

4.moveDetect関数の追加

Cameraクラスのクラス定義の下に、以下のmoveDetect関数を追加します。この関数が動体検知のキーとなる関数になります。

/home/pi/camera/Camera.py
class Camera(BaseCamera):
#<-----------------------------ここから追加
    ###################################################
    ## 動体検知のためのメソッド
    ###################################################
    @staticmethod
    def moveDetect(img):
        global befImg,befTimes

        #入力画像をグレースケールに変換
        grayImg=cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        #前画像がない場合、現画像を保存し終了
        if befImg is None:
            befImg = grayImg.copy().astype("float")
            return

        #前画像との差分を取得する
        cv2.accumulateWeighted(grayImg, befImg, 0.00001)
        delta = cv2.absdiff(grayImg, cv2.convertScaleAbs(befImg))
        thresh = cv2.threshold(delta, 50, 255, cv2.THRESH_BINARY)[1]
        image, contours, h = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  

        #画像内の最も大きな差分を求める
        max_area=0
        for cnt in contours:
            area = cv2.contourArea(cnt)
            if max_area < area:
                max_area = area;

        #次に備えて画像を保存
        befImg = grayImg.copy().astype("float")

        #動体が無かったら終了
        if max_area < detectSize:
            return

        #現在時間を取得
        nowstr=datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        nowTime=time.time()

        #画像をファイルに保存
        filename=pictpath+"/move_"+nowstr+".jpg"
        cv2.imwrite(filename, img)

        #ログ出力
        print(nowstr+' 動体検知 '+filename+' '+str(max_area))           

        #サーバに通知(一定時間間隔)
        if int(nowTime) - befTimes[0] > interval:
            #検知時間を保存
            befTimes[0]=int(nowTime)
            #サーバ通知
            file=open(filename, 'rb').read()
            files = {'file': ('move'+nowstr+'.jpg', file, 'image/jpeg')}
            params = {'action': 'move','text': '玄関で動体検知!'}
            response = requests.post(url, files=files, data=params)
            #サーバ通知結果をログ出力
            print(nowstr+' サーバ通知 動体 '+str(response.status_code)+':'+str(response.content))

プログラムの特徴的な部分だけ説明していきます。まず、動体検知に色情報は不要なので、処理を軽くするために、カメラの画像をグレースケール画像に変換しています。

次に「#前画像との差分を取得する」からの4行で、前画像と現画像の差分を求めて動体を検出しています。まず「cv2.accumulateWeighted()」で前画像との加重平均をとって累積しておきます、これをすることで画像の差分比較の精度が高まるようです。次に「cv2.absdiff()」で前画像と現画像の差分を求め、「cv2.threshold()」で画像の各ピクセルの値を2値(0か255)に振り分けます。これにより、動いている部分は白色、動いていない部分は黒色の画像が出来上がります。次に「cv2.findContours()」の部分で、白色の部分(動いた部分)を検索しcontours配列で返します。この処理はOpenCVを利用して動画(カメラ)から動体検知をする方法についてで分かりやすく解説して下さっている方がいるので、深く知りたい人は見てみると良いと思います。

「#画像内の最も大きな差分を求める」から5行では、2値に変換した画像内に複数ある動体部分について、最も面積が大きい部分の面積を求めています。そして、この最大面積を上の定数定義で定義したdetectSizeと比較して、detectSizeより大きかったら「動体有り!」と判断しています。

「#画像をファイルに保存」の部分では、動体を検知した時の画像を画像ファイルとして保存すると共に、ログ出力しています。

「#サーバに通知(一定時間間隔)」の部分では、前回の通知時間を示す「befTime」と現在時間を比較して、定数定義で定義した「interval」よりも時間が経過していたらサーバに通知を行う処理をしています。「#サーバ通知」の部分では、上で保存した画像ファイルを開き、HTTPのパラメータとして「action=move」「text=玄関で動体を検知!」を指定しています。前者はNode-redでの判定条件、後者はLineに通知されるメッセージ内容です。そして「requests.post()」でNode-redのURLにHTTP POSTで画像ファイルとパラメータを送信しています。最後に、Node-redからの応答結果をログに出力します。ちなみに、ログ出力はprintを使っていますが、監視カメラプログラムをsystemdで起動しているので、ここでprintした内容はsystemdのログに記載されます。

5.moveDetect関数の呼出し部分の追加

上で定義したmoveDetect関数を呼び出す部分を、frames関数内の「#動画を記録」の下に追加します。

/home/pi/camera/Camera.py
#動画を記録
out.write(stream.array)

#動体検知メソッドを呼び出す
Camera.moveDetect(stream.array) #<----------------------ここに追加

#ライブ配信用に画像を返す
yield cv2.imencode('.jpg', stream.array)[1].tobytes()

# 結果の画像を表示する
#cv2.imshow('camera', stream.array)

6.画像格納ディレクトリの作成

上の定数定義で定義した画像の格納ディレクトリを作成しておきます。

root@raspberrypi:/home/pi/camera# mkdir /home/pi/camera/picts

これで、プログラムの作成も完了です。

7.テスト

それでは、作成したプログラムを動かしてみましょう。プログラムが起動したら、カメラの「前に手をかざす」など動かしてみましょう。以下のようにログが表示されて、Lineに通知が来れば完成です!!

root@raspberrypi:/home/pi/camera# ./cameraServer.sh 
Starting camera thread.
 * Serving Flask app "cameraServer" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit)
20200201_093113 動体検知 /home/pi/camera/picts/move_20200201_093113.jpg 90735.0
20200201_093113 サーバ通知 動体 200:サーバ通知OK

ちなみにNode-redの画面は、こんな感じでfileノードの下にファイル名が表示されます。
図17.png

Lineの画面は、プログラムの中で指定したメッセージと写真が送信されてきます。
図18.png

また「/home/pi/camera/picts」フォルダには、動体検知した時の写真が格納されているはずです。Lineは「interval」で設定した間隔でしか通知されませんが、pictsフォルダには動体検知した全ての写真が格納されます。

おわりに

今回は防犯カメラに動体検知機能を追加し、Lineに通知することをやってみました。そろそろ、防犯カメラとして使えるものになってきたんじゃないかと思います。次回は、防犯カメラに顔認証機能を追加し、家族の顔を認証できるようにしてみたいと思います。

また、第1回目の記事の投稿からすごい反響があって驚いています。間違い・改善点や質問など、あったらコメント欄に書いていただければと思います。

連載記事

 1回目:カメラの設定と動画記録
 2回目:カメラ映像のライブ配信
 3回目:動体検知機能とLineへの通知  <--この記事
 4回目:顔認証機能とLineへの通知
 5回目:Raspberry PiへのAlexaの搭載
 6回目:顔認証後にAlexaで音声通知

育児×IoTスマートホームの関連記事

Alexaをしゃべらせる(remote2 v3.3編)
自分のGoogleカレンダーにJWTを使って予定を追加してみる
Alexaに通知を送ってみる(Node-red編)
Node-redでスマートハウス-LG製テレビを操作してみる
Node-redでスマートハウス-Gravioで子供用プリキュアボタン
育児×IoT Gravioでミルク&オムツ交換記録をGoogleカレンダにつけてみる
育児×IoT HomeAssistantでスマートホームコントローラを作ってみる

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account