やりたいこと(再掲)
最終的にやりたいことは、以下の5つの機能を持つ「最強の防犯カメラ」を作ることです。機能①〜機能③は、市販されている多くの防犯カメラでも持っている機能ですが、機能④や機能⑤の顔認証機能を持つ監視カメラはまだ多くないと思います。
機能①.動画を24時間撮影し、カメラ本体に動画で記録する
機能②.動画をWebブラウザや他の機器から参照できるようにライブ配信する
機能③.動体を検知したら、静止画をLineに通知する
機能④.家族の顔を認証したら、静止画をLineに通知する
機能⑤.家族の顔を認証したら、◯◯さんおかえり!!と喋る
家に帰ると、顔を見て「○○さん、おかえり!!」と言ってくれる辺りが、スマートハウスに一歩づ近づいている気がします。
実現に向けた連載
最強の「防犯カメラ」を作成するために、以下のように少しずつに記事を書いていきます(予定)。
1回目:カメラの設定と動画記録
2回目:カメラ映像のライブ配信
3回目:動体検知機能とLineへの通知 ←この記事
4回目:顔認証機能とLineへの通知
5回目:Raspberry PiへのAlexaの搭載
6回目:顔認証後にAlexaで音声通知
3回目:動体検知機能とLineへの通知
前回はカメラの映像をPCやスマホのブラウザから見えるようにライブ配信することをやってみました。今回は、カメラ映像の中に動体を検知したらLineに通知することをやってみたいと思います。
Line Notifyのアクセストークンを取得
Lineにメッセージを通知するためには、Line Notifyのアクセストーンを取得必要があります。まずは、このアクセストークンを取得していきます。
①Line Notifyのサイトにアクセス
PCのWebブラウザで、Line NotifyのWebサイト「https://notify-bot.line.me/ja/」にアクセスします。
②ログイン
画面右上の「ログイン」をクリックして、普段スマホで使っているLineのメールアドレスとパスワードでログインします。
③マイページの表示
ここが分かりにくいのですが、右上のアカウント名の右の「▼」印をクリックして「マイページ」を選択します。
④新規トークンの発行
画面に発行済みのアクセストークンの一覧が表示されますが、下にスクロールしていくと「トークンを発行する」ボタンが出てきますので、クリックします。
⑤トークン情報の入力
トークン情報の入力画面が表示されるので、トークン名に適当な名前を入力し、トークルームの選択で「1:1でLINE Notifyから通知を受け取る」を選択して、発行ボタンをクリックします。なお、家族など複数の宛先に通知を送りたい場合は、スマホで予めグループを作成しておくとグループを選択できます。
⑥トークンの取得
トークンが発行され、画面に表示されるので「コピー」ボタンを押して、メモ帳などに貼り付けておきます。
以上で、アクセストークンの取得は完了です。
Lineへの通知機能の作成
ここからは、Lineへの通知機能をNode-red上に作成していきます。Pythonのプログラムから直接通知しても良いのですが、Node-redを経由させることで、他の機器との連携性が格段に高くなります。
この記事では、Raspberry PiにNode-redをインストールして進めますが、既にホームサーバやHomeAssistantを利用している場合には、そのNode-redを使っても大丈夫です。
①Node-redのインストール
Raspberry PiへのNode-redのインストールは、既に多くの情報があるので詳しく書きませんが、以下のコマンドを実行して「y」で答えていくと、自動的にnode.jpをアップデートしてNode-redをインストールしてくれます。インストールには、20分ぐらいかかるので、コーヒータイムにしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 |
②Node-redの自動起動設定
インストールが完了したら「/etc/systemd/system/node-red.service」を作成して、Node-redを自動起動するようにしておきます。
1 2 3 4 5 6 7 8 9 10 |
[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コマンドで自動起動を設定します。
1 2 3 |
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. |
③Node-red画面の表示
Node-redが起動したら、PCのブラウザで「http://<Raspberry PiのIPアドレス>:1880/」にアクセスして、Node-redの画面を表示させます。
④http inノードの追加
防犯カメラプログラムからの通知をWEB APIで受け付けるために、画面左側のパレットから「http in」ノードを探して、キャンパスにドラッグして追加します。「http in」の設定画面では、以下のように入力して「完了」ボタンをクリックします。
・メソッド:POST
・ファイルのアップロード:チェックを付ける
・URL:/CameraAction
・名前:CameraAction
⑤functionノードの追加1
次に「function」ノードを追加して、以下のように入力して「完了」ボタンをクリックします。
・名前:編集
・コード:図の下を参照
・出力数:2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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」でなかった場合も同様にエラーメッセージを返します。
⑥http responceノードの追加
防犯カメラのpythonプログラムに結果を返すために、「http responce」ノードを追加します。設定画面は特に何も変更する必要はありません。
⑦fileノードの追加1
Lineに画像を送信するために、一時的にファイルに保存します。「file」ノードを追加して「動作」の部分を設定して「完了」ボタンをクリックします。
・動作:ファイルを上書き
⑧functionノードの追加2
更にもう一つ「function」ノードを追加して、以下のように入力して「完了」ボタンをクリックします。
なお「Bearer」の後のxxxxxxの部分には、上で取得したLine Notifyのアクセストークンを記述してください。
・名前:編集
・コード:図の下を参照
・出力数:1
1 2 3 4 5 6 |
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行目で画像のファイルを指定します。
⑨execノードの追加
次に「exec」ノードを追加して、以下のように入力して「完了」ボタンをクリックします。上で設定した引数を用いて、curlコマンドを実行することでLineに通知を送ります。
・コマンド:curl
・引数:msg.payloadにチェックを付ける
⑩fileノードの追加2
送信後には画像ファイルは不要なので、もう一つ「file」ノードを追加して「動作」を設定して「完了」ボタンをクリックします。
・動作:ファイルを削除
11.ノードの接続
上記の7つのノードの追加ができたら、以下のようにノードを接続します。
12.デプロイ
最後に画面右上の「デプロイ」ボタンを押して、作成したフローをデプロイします。
13.写真格納用ディレクトリの作成
上のfunctionノードの設定で指定した、画像を一時的に格納するディレクトリを作成します。
1 |
root@raspberrypi:/home/pi# mkdir /root/.node-red/cameraPicts/ |
以上で、Line通知機能の設定は完了です。
動体検知プログラムの作成
ここからは、前回作成した「Camera.py」に動体検知機能を追加していきます。
記述量が多いので、段階的に記載していきます。なお、完全なソースコードはGitHubのnaka-kazz/RasPiCameraに置いたので、コピペが面倒な方はこちらからダウンロードしちゃって下さい。
①防犯カメラサービスの停止
前回、防犯カメラサービスを自動起動しているので、防犯カメラサービスが起動状態になっていると思います。機能追加のために一旦止めておきましょう。
1 |
root@raspberrypi:/home/pi/camera# systemctl stop flask.service |
②importと定数定義の追加
まずは、Camera.pyに以下のimportと定数を追加します。
・Webリクエストを可能にするための「import requests」の追加
・画像格納パス、サーバ通知URL、インターバル、動体検知の精度の4つの定数の追加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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が大変なことになってしまいます。また、動体検知の精度は、どの程度大きな変化(動体の面積)があった場合に通知を行うかを設定するものになります。プログラムが動くようになったら、動作させながら調整してみてください。
③グローバル変数の追加
Camera.pyの定数定義の下に、以下のグローバル変数を追加します。
1 2 3 4 5 6 |
################################################### ## グローバル変数 ################################################### #動体検知のための前情報保存用 befImg=None befTimes=[0,0,0,0,0,0] |
befImgは動体検知で、前画像と現画像を比較する場合に使う前画像の保存用の変数です。また、befTimesは前回サーバに通知を行った時間を格納しておくために利用します。動体検知ではbefTimes[0]しか利用しませんが、後の配列要素は次回以降の顔認証で使用します。
④moveDetect関数の追加
Cameraクラスのクラス定義の下に、以下のmoveDetect関数を追加します。この関数が動体検知のキーとなる関数になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
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のログに記載されます。
⑤moveDetect関数の呼出し部分の追加
上で定義したmoveDetect関数を呼び出す部分を、frames関数内の「#動画を記録」の下に追加します。
1 2 3 4 5 6 7 8 9 10 11 |
#動画を記録 out.write(stream.array) #動体検知メソッドを呼び出す Camera.moveDetect(stream.array) #<----------------------ここに追加 #ライブ配信用に画像を返す yield cv2.imencode('.jpg', stream.array)[1].tobytes() # 結果の画像を表示する #cv2.imshow('camera', stream.array) |
⑥画像格納ディレクトリの作成
上の定数定義で定義した画像の格納ディレクトリを作成しておきます。
1 |
root@raspberrypi:/home/pi/camera# mkdir /home/pi/camera/picts |
これで、プログラムの作成も完了です。
⑦テスト
それでは、作成したプログラムを動かしてみましょう。プログラムが起動したら、カメラの「前に手をかざす」など動かしてみましょう。以下のようにログが表示されて、Lineに通知が来れば完成です!!
1 2 3 4 5 6 7 8 9 10 |
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ノードの下にファイル名が表示されます。
Lineの画面は、プログラムの中で指定したメッセージと写真が送信されてきます。
また「/home/pi/camera/picts」フォルダには、動体検知した時の写真が格納されているはずです。Lineは「interval」で設定した間隔でしか通知されませんが、pictsフォルダには動体検知した全ての写真が格納されます。
おわりに
今回は防犯カメラに動体検知機能を追加し、Lineに通知することをやってみました。そろそろ、防犯カメラとして使えるものになってきたんじゃないかと思います。次回は、防犯カメラに顔認証機能を追加し、家族の顔を認証できるようにしてみたいと思います。
また、第1回目の記事の投稿からすごい反響があって驚いています。間違い・改善点や質問など、あったらコメント欄に書いていただければと思います。
連載記事
1回目:カメラの設定と動画記録
2回目:カメラ映像のライブ配信
3回目:動体検知機能とLineへの通知
4回目:顔認証機能とLineへの通知 ←次はこれ
5回目:Raspberry PiへのAlexaの搭載
6回目:顔認証後にAlexaで音声通知
コメント
楽しく拝見して毎日少しずつですがのめりこんでいます
さて、最強防犯カメラの②Node-redの自動起動設定を設定の後、systemctlコマンドで自動起動を設定しようとするのですが、
bash: sytemctrl:コマンドが見つかりませんと表示されNode-redを起動できません。
何が問題なのでしょうか?
一度真似してみて、家庭の玄関先におければ家族以外の入室者に声かけできそうですし、応用としてお年寄りの徘徊しそうな人に声かけ、出てしまったら管理者に連絡なんてことも出きるのではないかと思い、取り組み始めたところですが、何せ素人ですからすべてが外国語を勉強しているような錯覚(本当は感覚)を味わっています。
解決策をご教示戴ければ幸いです。 よろしくお願いいたします。
先ほどは失礼致しました
systemctlとすべきをsytemctrlとしておりました。
正しくうちこみましたが以下のようなエラーメッセージでした
root@raspberrypi:/etc/systemd/system# systemctl start node-red
root@raspberrypi:/etc/systemd/system# systemctl enable node-red
The unit files have no installation config (WantedBy=, RequiredBy=, Also=,
Alias= settings in the [Install] section, and DefaultInstance= for template
units). This means they are not meant to be enabled using systemctl.
Possible reasons for having this kind of units are:
• A unit may be statically enabled by being symlinked from another unit’s
.wants/ or .requires/ directory.
• A unit’s purpose may be to act as a helper for some other unit which has
a requirement dependency on it.
• A unit may be started when needed via activation (socket, path, timer,
D-Bus, udev, scripted systemctl call, …).
• In case of template units, the unit is meant to be enabled with some
instance name specified.
続けてCreated symlink /etc/systemd/system/multi-user.target.wants/node-red.service → /etc/systemd/system/node-red.service.をしてみたら
Created コマンドがみつかりません
度々申し訳ありませんがどうしたらよういのか教えてください。
記事をお読みいただきありがとうございます。Node-redがsystemctlから起動できないとのことですが、最近のRaspberry Pi用のNode-redはsystemctlのスクリプトが同封されていますので「# systemctl enable nodered.service」で起動できるかと思われます。お試しいただけますでしょうか?
下記ディレクトリの作成方法がわかりません
ラズパイのコード画面でしょうか?
>13.写真格納用ディレクトリの作成
>上のfunctionノードの設定で指定した、画像を一時的に格納するディレクトリを作成します。
また、
>完全なソースコードはGitHubのnaka-kazz/RasPiCameraに置いたので、コピペが面倒な方はこちらからダウンロードしちゃって下さい。
ソースコードのダウンロード方法もご教授いただけると幸いです。
ディレクトリの作成は、ラズパイのターミナル画面もしくは、SSH接続したターミナルから実行してください。また、GitHubからのダウンロード方法は、画面右の緑色の「Code」ボタンをクリックして、DownloadZIPを選択するとダウンロードできます。
node-redをダウンロードしたいのですが
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
と入力してもダウンロードできません。
ダウンロードするにはどうすればよいのか教えてください。
node-redをダウンロードしたいのですが
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
と入力してもダウンロードできません。
ダウンロードするにはどうすればよいのか教えてください。
すみません自力で解決できました。
申し訳分けございません。
ブログをお読みいただきありがとうございます。「bash」の後を「<」(半角のダイナリ)にしてみたら、うまくいきますでしょうか?
すみません。質問です。
⑦のテストを実行すると、動体検知は起動するのですが、LINEに通知が届かず、
「base_camera.py line 94, in _thread
for frame in frames_interator:」
「Camera.py line 136, in frame
Camera.moveDetect(stream.array)」
「Camera.py line 90, in moveDetect
response = requests.post(url, files=files, data=params
NameError: global name ‘requests’ is not defined」
上記3つのエラーが出てしまいます。
改善策がありましたら、申し訳ないですが教えてください。
ブログをお読みいただきありがとうございます。エラーからすると、プログラムの6行目の「import requests」が入っていないように見えます。ご確認いただけますでしょうか?
すみません「import requests」が抜けてました。ありがとうございます。
また質問になってしまい申し開けないのですが、プログラミングが起動しているのにLINEに通知が届きません。
現状としましては、⑦のテストで最初の画像までは自分のラズパイで出来ているのですが、2つ目の画像のnode-red画面での
「failノード」の下には「/root/.node-red/cameraPicts/move20201027_154301.jpg」と表示されるのですが、「deleteノード」の下には何も表示されていない状態になっています。
何度も質問してしまい申し訳ありませんが、改善策がありましたら教えてください。
編集ノードの設定誤りかと思われます。以下をご確認いただけますか?
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;