ハエトリソウ捕食監視システム
Raspberry Pi Advent Calendar 2019 | 5日目
長男くんがハエトリソウがハエを食べるところが見てみたいというので、ラズパイカメラで監視して捕食の瞬間を動画におさめるシステムを製作しました。
また こういったIoTシステムで一番問題となる電源の確保をソーラ発電によるエコエネルギーで実施したく同時にシステムに導入しました。
目次
システム概要
システムはベランダに配置してラズパイカメラでハエトリソウを撮影しハエトリソウに虫を捕食するなどの動きがあるとその前後の動画を生成し、IFTTTを用いてケータイ端末に通知します。
電源はソーラパネルを使用して完全に自立したシステムとなります。
構成
ソーラパネルと鉛蓄電池をチャージコントローラに接続します。コントローラが出力する12Vをコンバータで5Vに変換してUSBコネクタで出力します。
負荷としてラズパイZero&ラズパイカメラをつなげます。
部品
- ソーラー充電コントローラ E32-SolarCharger
マイコンにESP32を使用。回路図は以下
http://indoor.lolipop.jp/IndoorCorgiElec/E32-SolarCharger/E32-SolarCharger-Schematic.pdf
- ソーラーパネル 12W
- 鉛蓄電池 12V12Ah
- インラインヒューズ コントローラと蓄電池の+側の配線に3Aヒューズ挿入
-
DC-DC コンバータモジュール 12Vー5V
- Raspberry Pi Zero W
-
Raspberry Pi カメラモジュール
太陽光発電システム
Indoor Corgi Elec.社製のESP32を搭載した太陽光発電充電コントローラE32-SolarChargerを使用します。マイクロSDカードスロットもありデータログを記録できます。
以下のサイトで製品概要やサンプルコードが提供されています。
Arduino IDEでカスタマイズも比較的簡単にできます。ESP32搭載ですのでWiFiで動作状況や設定変更が可能です。
E32-SolarCharger 概要
- バッテリの電圧と充電電流を監視してPWM制御します。
- 液晶ディスプレイにバッテリ電圧・電流、ソーラパネル発電電圧、動作モードを表示
- 測定値のログをSDカードに保存。
- 負荷とバッテリ間のスイッチでバッテリ電圧が低下した際に負荷の切断が可能。
- ソーラパネル発電電圧を観測して8Vを下回ると(夜間)コントローラをディープスリープさせて負荷のスイッチもオフして消費電力を抑える。
- ESP32のWiFi機能でブラウザで各種数値表示や動作モード変更が可能
ブラウザ表示 (設定)
ここではターゲットとするバッテリ充電電圧を13.8V、充電電流を1Aとしました。スリープモードは有効にしています。
充電動作
我が家のベランダは西向きで晴天時でも午前中の数時間しか満足に日照がなかったので、夜間はチャージコントローラをスリープし、さらに鉛蓄電池と負荷の間のスイッチをOFFして消費電力を低減します。夜は撮影しても見えませんので。
昼と夜間はソーラパネルによる発電電圧によって判断します (8V以下でスリープ、10V以上で解除)。
撮影システム
ラズパイZero Wにラズパイカメラを搭載しハエトリソウを撮影します。
撮影システムはNode-REDで構築しました。電源起動で自動に動作します。
Node-RED自動起動設定
以下のコマンドの実行でNode-REDがサービス化されラズパイ起動で自動的に起動します。
夜間からのスリープ明けの起床時にも自動でNode-RED起動します。
1 |
$ sudo systemctl enable nodered.service |
撮影Node-Redフロー
ラズパイカメラでハエトリソウを撮影し、捕食などの動きを検出して動画を生成しスマホに通知を送信します。これをNode-REDで構築しました。
①初期起動 injectノード
injectノードのペイロードを”Started!”とすることでNode-REDが起動した際にトリガ起動します。
参考
②監視プログラム起動 execノード
シェルコマンドで監視プログラムを起動します。
シェルでハエトリソウ監視Pythonプログラムを起動します。
1 2 |
#!/bin/sh python3 /home/pi/Camera/cameraMoveDet.py |
以下がハエトリソウ監視Pythonプログラムです。
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# -*- coding: utf-8 -*- import cv2 import numpy as np import os import datetime # カメラのキャプチャ cap = cv2.VideoCapture(0) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = cap.get(cv2.CAP_PROP_FPS) # VideoWriter を作成 fourcc = cv2.VideoWriter_fourcc('m','p','4','v') video1 = cv2.VideoWriter('/home/pi/Camera/video1.mp4', fourcc, fps, (width, height)) video2 = cv2.VideoWriter('/home/pi/Camera/video2.mp4', fourcc, fps, (width, height)) preFrame = 300 recFrame = 1200 moveTh = 6000 # フレーム差分の計算 def frame_sub(img1, img2, img3, th): # フレームの絶対差分 diff1 = cv2.absdiff(img1, img2) diff2 = cv2.absdiff(img2, img3) # 2つの差分画像の論理積 diff = cv2.bitwise_and(diff1, diff2) # 二値化処理 diff[diff < th] = 0 diff[diff >= th] = 255 # メディアンフィルタ処理(ゴマ塩ノイズ除去) mask = cv2.medianBlur(diff, 5) return diff # 動画撮影 def rec(): j = 0 while(j < recFrame): frame = cap.read()[1] video2.write(frame) # フレームを書き込む j += 1 def main(): i = 0 a = 0 p = 0 while(cap.isOpened()): frame = cap.read()[1] cv2.imwrite('/home/pi/Camera/buffer/{0:04d}.jpg'.format(i), frame) # 結果を表示 #cv2.imshow("Frame2", frame) i += 1 if i == preFrame: i = preFrame - 1 for j in range(1, preFrame): path1 = '/home/pi/Camera/buffer/{0:04d}.jpg'.format(j) path2 = '/home/pi/Camera/buffer/{0:04d}.jpg'.format(j-1) os.rename(path1, path2) # フレームを3枚取得してグレースケール変換 frame1 = cv2.cvtColor(cv2.imread('/home/pi/Camera/buffer/{0:04d}.jpg'.format(preFrame-4)), cv2.COLOR_RGB2GRAY) frame2 = cv2.cvtColor(cv2.imread('/home/pi/Camera/buffer/{0:04d}.jpg'.format(preFrame-3)), cv2.COLOR_RGB2GRAY) frame3 = cv2.cvtColor(cv2.imread('/home/pi/Camera/buffer/{0:04d}.jpg'.format(preFrame-2)), cv2.COLOR_RGB2GRAY) # フレーム間差分を計算 mask = frame_sub(frame1, frame2, frame3, th=10) # 白色領域のピクセル数を算出 moment = cv2.countNonZero(mask) print(moment) #動作ジャッジ if moment > moveTh: a += 1 else : a = 0 #ログ残す p += 1 if p > 800: p = 0 cv2.imwrite('/home/pi/Camera/log/{}.jpg'.format(datetime.datetime.today()), frame) # 動いたら動画作成 if a > 2: #連番から動画生成 for k in range(0, preFrame - 1): img = cv2.imread('/home/pi/Camera/buffer/{0:04d}.jpg'.format(k)) img = cv2.resize(img, (width, height)) video1.write(img) #動画撮影 rec() break #画像ファイル削除 for l in range(0, preFrame-1): os.remove('/home/pi/Camera/buffer/{0:04d}.jpg'.format(l)) video1.release() video2.release() cap.release() #cv2.destroyAllWindows() print("MOVE!!") if __name__ == '__main__': main() |
通常動画撮影
OpenCVでカメラ撮影実施します (L 7~11)。
動画はmp4形式で取り扱います (L 14)。
撮影は1フレームごとにjpgで常に直近の300枚を保存するようにします (L 52~71)。
800フレーム分の撮影実施ごとにログとして画像を保存ファイルに残します (L 92~96)。
動体検知
フレーム間差分法でハエトリソウの動きを検出します (L 23~39、L 73~84)。
フレーム間の差分が閾値(ここでは6000)を3回超えるとハエトリソウが動いたと判断 (L86~90、L100)。
どうしても #ハエトリソウ の捕食の瞬間を動画で残したい。
そんなシステムを構築したい。#ラズパイ #OpenCV #動体検出 pic.twitter.com/32uaDac7Pf— HomeMadeGarbage (@H0meMadeGarbage) July 13, 2019
ハエトリソウの動き検知で直近の300枚のjpg画像を動画video1.mp4にします (L 101~105)。
その後1200フレーム分の動画を撮ってvideo2.mp4にします (L 42~49、L 108)。
後処理
動画撮影後、動体検知前の直近の300枚のjpg画像を削除し文字列”MOVE!!”を次のノードに渡します (L 112~120)。
参考
③動画ファイル名生成 functionノード
ハエトリソウ動作検出後に動画保存のためのファイル名を生成します。日付と時刻で”YYYY-MMDD-hhmm”の文字列を次のノードに渡します。
1 2 3 4 5 6 |
if (msg.payload.match(/MOVE!!/)) { var dd = new Date(); msg.payload = dd.getFullYear() + "-" + (("0"+(dd.getMonth()+1)).slice(-2))+(("0"+dd.getDate()).slice(-2)) + "-" +(("0"+dd.getHours()).slice(-2))+(("0"+dd.getMinutes()).slice(-2)); return msg; } |
④動画合成 execノード
シェルコマンドで動画合成プログラムを起動します。
シェルでハエトリソウ動作の検出前の300フレーム分の動画 video1.mp4 と検出後の1200フレーム分の動画 video2.mp4 をffmpegで合成します。合成後のファイル名は③で生成した日時の文字列を使用します。
1 2 |
#!/bin/sh ffmpeg -f concat -safe 0 -i /home/pi/Camera/list.txt -y -c copy /home/pi/Camera/$1.mp4 |
list.txtは以下の通りです。
1 2 |
file '/home/pi/Camera/video1.mp4' file '/home/pi/Camera/video2.mp4' |
参考
⑤IFTTT http requestノード
あらかじめIFTTTで設定しておいたWebhooksトリガのURLをPOSTします。
IFTTTの設定
IFTTTとはif this then thatの略で各種Webサービス同士を連携して自分好みのWebサービス(アプレット)を生成することができます。ここではthis(トリガ)にwebにリクエストを受信できるWebhooksを使用しthat(アクション)にスマホに通知を送るNotificationsを使用しました。
Webhooksのイベント名は”Flytrap”としました。以下URLをリクエストすると”ハエトリソウに動きあり!”とスマホに通知が来ます。
https://maker.ifttt.com/trigger/Flytrap/with/key/”Webhook-key”
“Webhook-key”にはWebhookサービス登録時に得られる認証キーを入力します。
以上で動画が保存されるとIFTTTを介してスマホに通知が行きます。
⑥delayノード
IFTTT通知後、5秒待って監視プログラム②を再開します。
動作
2019/7/14から10/27まで運用しましたが、残念ながらハエトリソウの捕食動画は撮影できませんでした。悲しみ 🙁
フレーム間差分法で動作検出を実施しているため、急に太陽に雲がかかるなどで影が生成されても動作検出してしまう点が改善すべき点です。
ハエトリソウ捕食監視システム pic.twitter.com/04Gq16eG7U
— HomeMadeGarbage (@H0meMadeGarbage) December 1, 2019
雲の多い日はガンガン通知来ますが捕食はゼロでした。。。
来季は学習による画像認識などの検討も実施しリベンジしたいと考えております。
運用時は7分おきに静止画ログも残しておりましたので成長をタイムラプス動画にするなどで楽しむことはできました。
全然ハエ来ないハエ生きろ!!!
ただただ1週間のハエトリソウ成長記録です。#ラズパイ #食虫植物 #ベランダ太陽光発電 #ハエトリソウ pic.twitter.com/gFB1rNsRCw
— HomeMadeGarbage (@H0meMadeGarbage) July 25, 2019
9月のハエトリソウ
ハエはまだ来ません#ラズパイ #食虫植物 #ベランダ太陽光発電 #ハエトリソウ pic.twitter.com/DgvQaOC8Dd— HomeMadeGarbage (@H0meMadeGarbage) September 14, 2019
追記
2019/12/10 みんなのラズパイコンテスト
今年もこのネタでみんなのラズパイコンテストに参加いたしました!
今年は優良賞とスタートダッシュ賞のダブル受賞でした!わーい 😛 !
今年は優良賞とスタートダッシュ賞のダブル受賞でした!#ラズパイ #ラズパイコンテスト #みんなのラズパイコンテスト pic.twitter.com/cqZrdSYw4I
— HomeMadeGarbage (@H0meMadeGarbage) December 10, 2019
「ハエトリソウ捕食監視システム」への1件のフィードバック