はじめに
実家に据え付けたラズパイ家族見守りサービスは順調に動いている。私以外でこれを使うに値する唯一の人物・妹にも本LINEアプリを教えた。
もう一つ実家に置きたいIoTガジェットがある。ラズパイプログラミングの定番中の定番、監視カメラだ。
生活空間をのぞき見するわけではない。季節により彩りを変える庭を定点撮影し、ある程度たまったらタイムラプス動画を作りたいのだ。
メイン業務から離れてもOKのラズパイブームが職場に訪れた(半導体不足で入手困難なのに…)今、満を持してこいつに取り掛かることにした。
要件(←使い方あってる?)
- 当然、家庭内LAN内の外からアクセスできること。
- リアルタイムな画像取得は望まない。一定時間ごとに撮影するようにする。
- 「一定時間ごと」にこだわりたい。
15分ごとに撮影するとして、プログラムが走り始めたタイミングにより「x分y秒」「x+15分y秒」「x+30分y秒」…に撮影されるのはうれしくない。いつ開始しても毎時0分・15分・30分・45分に撮影するようにする。 - 「タイムラプス動画を作りたい」と書いたが、元画像があればタイムラプス動画なんていくらでも作れる。
それより重要なのは、リモートで、最新画像だけでなく過去画像を見ることができるようにすること。 - ただし家に余っているラズパイZero Wを使うため、あまりリッチな実装はできない。
仕組みを考える
監視カメラを家庭内LANでなく外から見れるようにするには、VPNを使う・サーバーを立てるといった難しいことをせねばならない。
桃鉄ガチャでHerokuが使えるようになりその過程でngrokに触ってみたりもしたが、これは短時間の使用に限られる。
remote.itという便利なサービスもあるようだが、今回はパス。
前述の通り、リアルタイム性を求めているわけではない。これは負け惜しみではない。タイムラプスが主たる目的だからだ。
あるときは1時間もしくはそれより短いインターバルで再生させ太陽の動きを見る。夏と冬で見比べることができればなおよい。またあるときは1日もしくはそれより長いインターバルで再生させ季節の移ろいを楽しむ。撮影場所によっては遠くのビルの完成具合を見ることができるかもしれない。
…と考えていたらいいアイデアを思いついた。画像のファイル名を日時にする。で、現在時刻を取得もしくは見たい画像の日時を入力してその画像を呼び出す。これならばHerokuでFlaskというサーバーサイドのアプリではなく、JavaScriptで画像のリンク先を変更するだけでよい。
となれば、画像を置くのは普通の無料レンタルサーバー、画像を送るのはFTPだな。増えすぎた画像をサーバーから削除するのは機能としては実装しない。自宅で手動で操作できるから。
あ、さらに思いついた。日時のファイル名で保存するだけでなく固定の名前で上書き保存して定期的にリロードすれば、ほぼほぼリアルタイムな映像所得ができるぞ。
つまりはこういう感じ。Webアプリと呼ぶのも烏龍がましい、ごくごく普通の写真アップロードシステムとJavaScriptだ。
技術トピック
Python
任意の単位の時間要素を操る
ここで前回の記事「Pythonのdatetimeで時間要素を動的に扱う」が登場する。
個別の関数として外に出すまでもないものだったので特筆すべきものはない。shiracamusさんにはお世話になりました。
cv2.imshow()の罠
ノートPCで開発し動作確認。そのコードをラズパイに移して動作確認。systemd
による自動起動や死活監視を織り込んで、さあヘッドレスで実行だ。
………。
……。
…。
はいダメー。
リモートデスクトップ接続でラズパイをPCから確認したところ、メインプログラムのcv2.imshow()
でエラーになっていることがわかった。もちろんこれまで問題なかったところだ。それどころか、リモートデスクトップ上ではちゃんと動く。ヘッドレスで自動起動したときだけエラーになるのだ。何だこりゃ。
ヘッドレスで起動してデスクトップが存在しないのにOpenCVのhighguiのウィンドウを表示させようとしていたのが間違いだった。
開発中に使っていたcv2.imshow()
をコメントアウトすることで正常に動いた。
画像保存されるまで待つ
ちかぢかラズパイZeroの新型が出るようだが、現在のZeroの遅さはちょっと我慢できないほど。
cv2.imwrite()
による画像保存が完了する前にFTP処理に移ってしまうとそんなファイルねーよというエラーになってしまう。
そこでファイルの存在が確認できるまで待つという処理を入れた。
if (略):
cv2.imwrite(filename, img)
while not os.path.isfile(filename):
time.sleep(0.1)
FTPでアップロードする処理
JavaScript
任意の単位の時間要素を操る
ここでJavaScriptで動的に関数を指定するが役に…立たない。Moment.jsを使った。
新しくプロジェクトを手掛ける際は以下のサイトを見てより適切なライブラリを使うとよいだろう。
置換と正規表現
ファイル名をYYYY/MM/DD HH:mm:ss.jpg
としたいところだが、ファイル名として不適切な文字が含まれている。そこでYYYY-MM-DD_HH+mm+ss.jpg
とした。時分秒を区切る:
を+
にしたのは-
との対比なだけで深い意味はない。
JavaScriptで置換はstring.replace(substr, newSubstr)
と書くが、この表記では最初のひとつしか置換できない。該当する複数の文字列をすべて置換するには正規表現を使う。
だがそれでもエラーになってしまった。+
は正規表現の制御に使われるメタ文字だったのだ。メタ文字を正規表現で使うには\
でエスケープする必要がある。
+
をメタ文字でない別の文字にしてもよかったのだが、新しい気づきを記念してこのままで行くことにした。
その後、この関数は使われないことになった。
function fileName2time(fileName) {
// ファイル名から時刻を取得する
// ファイル名は YYYY-MM-DD_HH+mm+ss.jpg とする
var strTime = fileName.substr(0, fileName.length-4); // 拡張子を取り除く
strTime = strTime.replace("_", " "); // _を に置換 1字しかないので正規表現しない
strTime = strTime.replace(/\+/g, ":"); // +を:に置換 複数あるので正規表現必須
return moment(strTime); // moment.jsで文字列から時刻を取得する
}
その他
ここで自分の言葉で書けるほど深く理解したわけではないが、
-
async
を使ったsleep関数 - アロー関数(コピペレベル)
-
img src
を書き換える際、同じファイル名だとキャッシュを参照して画像が変わらないことがあるのでパラメーターを渡して常にサーバーに見にいくようにする。パラメーターはユニークになるよう時刻(のシリアル値)とするとよい。 -
img src
で画像が見つからなかったとき代替画像を表示する
を今回新しく使った。
根本的にダメダメ
イベント発火するごとに関数が呼び出されるJavaScriptにおいて、変数の値を保持するにはどうしたらよいのだろう。
チュートリアルによくあるボタンを押すごとに数字が増えていくプログラムだとdocument.getElementById()
でその都度値を取得してたよな、と思いながら保持しておきたい値を全部html内に埋め込んだ。
完成させたあとイマドキのカウンターのプログラムを見てみたら…なんか私の知ってるJavaScriptとぜんぜん違う! これは一から勉強が必要だなあ。よく考えたらJavaScriptの勉強って一度もしたことないや。
コード
特にJavaScriptについてダメダメだが、とりあえず動くし、ここは恥をさらして将来への糧としよう。
ラズパイケースの自作
いま手元に残っているラズパイZero Wは過去のプロジェクトで失敗したものでユニバーサル基板をはんだ付けしてある。そのため公式ケースやフリスクの箱に収めることができない。
何か良い入れ物は無いものかと100円ショップを回ったが、結局は家に転がっていた空き箱を使った。
それは綿棒の容器。サイズが微妙に異なる二つの容器があったおかげで、単に格納するだけでなく五右衛門風呂の風呂底のように内部の底板に固定することができた。
容器の周囲にはホットメルトを点付けしてあり、蓋が簡単に外れないようにしてある。
ギャラリー
cssにこだわると際限がないので時間で打ち切った。
ここで見えているお宅はお向かいさん。プライバシー保護のためぼやけさせている…わけではなく、マニュアルフォーカスの安物カメラを買ったら近くも遠くも全然ピントが合わないでやんの。
###リアルタイムモード
1分に1回画像更新する。
画像は毎フレーム取得しているので動体検知や通知機能を織り込んでもよい。今回はラズパイZero Wで動かしているためそこまでしていない。
###タイムシフトモード
1日・1時間・1単位(私の場合は15分)で画像を切り替えることができる。
また、再生モードアイコンが出現する。
###再生モード
1日・1時間・1単位(私の場合は15分)でタイムラプス動画を再生させる。
input
タグで任意の時間スキップを設定できたり画像間の待ち時間を変更できたりする機能も考えたが、面倒なのでシンプル操作を追求してそれは無しとした。
###実家に取り付け
トライ用のカメラとは別のまともな(だが余り物の)カメラを取り付けたが、もっと高性能なカメラにすればよかった。
終わりに
プログラムの出来不出来は別として、プロジェクトを完成させることができたのは貴重な経験だった。
次は何を作ろうかな。