RaspberryPi 3 Model B+でIoT監視カメラをつくる(その3プログラミング)
Contents
RaspberryPi(ラズパイ)でIoT監視カメラ
RaspberryPi 3 Model B+とカメラ(Raspberry Pi Camera Module V2)とモーションセンサー(HC-SR501)で動きがあった時だけ録画する監視カメラを作成したときの備忘録の3回目。
1回目はRaspberryPiとカメラ、モーションセンサーとの接続に関するこちらの記事を参照。
2回目はGoogleDriveにアクセスする為のGoogle Developersの設定とLINEにメッセージを送信するためのLINE Notifyでのアクセストークンの発行に関する記事。
今回はPythonのプログラミングに関する記事とする。
全体構成図
尚、1回目、2回目の記事でも載せているが今回も全体構成図を念の為、掲載する。
監視カメラシステムの全体構成図は以下の通り。
ハードウェア
使用したハードウェアは以下の通り。
Raspberry Pi 3B+
尚、この時はRaspberry Pi 3B+ で作成したが今なら Raspberry Pi 4B で作成するのが良いと思う。
後述するプログラムはラズパイ4でも問題なく動作した。
カメラモジュール
モーションセンサー
ディレクトリ構造
プログラム関連のディレクトリ構造は以下の通りとする。
├─opt
│ │
│ ├─security-camera
│ │ │ SecCamera.py
│ │ │ settings.yaml
│ │ │
│ │ ├──cert
│ │ │ initial.json
│ │ │ client_secret.json
│ │ │ credentials.json
│ │ │
│ │ └─video
│ │
SecCamera.py | 今回作成するプログラム |
settings.yaml | Google Driveにアクセスする為の設定ファイル 詳しくは前回の記事を参照 |
certディレクトリ | 初期設定ファイル、認証ファイルなどセキュアなファイルの格納場所 |
initial.json | 初期設定ファイル 詳しくは前回の記事を参照 |
client_secret.json | Google Driveにアクセスする為のクライアントID、クライアントシークレットが記載されたファイル 詳しくは前回の記事を参照 |
credentials.json | 初回にGoogle Driveにアクセスした時に認証情報などが保存されるファイル 詳しくは前回の記事を参照 |
videoディレクトリ | 録画した動画ファイルを一時的に保存しておくディレクトリ |
プログラム
下記のプログラムに更に修正を加えている。
そのときに記事はこちら
プログラムの全体は以下の通り。
Python歴が浅いのでしょぼい作りになっている可能性が大で恥ずかしいのだけれども誰かの参考になればと思い公開しておく。
# -*- coding: utf-8 -*-
"""
Created on Sun Jul 28 09:32:20 2019
@author: Souichirou Kikuchi
"""
from picamera import PiCamera
from time import sleep
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from concurrent.futures import ThreadPoolExecutor # スレッド処理
import RPi.GPIO as GPIO
import datetime as dt
import os
import json
import requests # LINEメッセージ
SC_CAMERA_PIN = 5 # ピンの名前を変数として定義
MAX_REC_TIME = 3600 # 最長録画時間(秒数)
SAVE_DIR = "./video/" # ファイル保存用ディレクトリ
INITIAL_FILE= "./cert/initial.json" # 初期設定ファイル
LINE_URL = "https://notify-api.line.me/api/notify" # LINEメッセージ用URL
DRIVE_LINK = "https://drive.google.com/open?id=" # LINEに表示するGoogleDriveへのリンク
INTERVAL = 0.5 # 監視間隔(秒)
AN_TEXT_SIZE = 24 # 録画画像上部に表示される注釈文字のサイズ
GPIO.setmode(GPIO.BCM) # ピンをGPIOの番号で指定
GPIO.setup(SC_CAMERA_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIOセットアップ
class SecurityCameraClass: # セキュリティカメラのクラス
def __init__(self):
with open(INITIAL_FILE) as f: # 初期設定ファイルの読み込み
jsn = json.load(f)
self.folder_id = jsn["folder_id"] # folder_idの読み込み
self.token = jsn["line_token"] # LINE用tokenの読み込み
self.location = jsn["location"] # 監視カメラ設置場所
self.camera = PiCamera()
self.camera.annotate_text = self.location
self.camera.annotate_text_size = AN_TEXT_SIZE
self.camera.rotation = 270
gauth = GoogleAuth() # GoogleDriveへの認証
gauth.LocalWebserverAuth()
self.drive = GoogleDrive(gauth)
def StartRecording(self): # 録画開始
time_stamp = "{0:%Y-%m-%d-%H-%M-%S}".format(dt.datetime.now()) # 日付時刻をセット
self.save_directory = SAVE_DIR + "video" + time_stamp + ".h264" # ディレクトリ、ファイル名をセット
self.camera.start_recording(self.save_directory) # 指定ディレクトリに録画
return True
def StopRecording(self): # 録画終了
self.camera.stop_recording() # 録画終了
executor = ThreadPoolExecutor(max_workers=3) # 同時実行は3つまでスレッド実行
executor.submit(self.OnTheread)
return False
def OnTheread(self): # ファイルのGoogleDriveへのアップロードは時間がかかるので別スレッドで行う
file_name = os.path.basename(self.save_directory)
del_file_name = self.save_directory # 対象ディレクトリとファイル名を保存しておかないと別スレッドなので更新されてしまう
f = self.drive.CreateFile({"title": file_name, # GoogleDrive
"mimeType": "video/H264",
"parents": [{"kind": "drive#fileLink", "id":self.folder_id}]})
f.SetContentFile(self.save_directory) # ファイル名指定
f.Upload() # アップロード
os.remove(del_file_name) # アップロード後にファイルは削除
sec_camera.LineMessage() # LINEにメッセージを送信
def LineMessage(self): # LINEに録画検知しましたメッセージを送信する
headers = {"Authorization" : "Bearer " + self.token}
message = "録画検知しました " + DRIVE_LINK + self.folder_id
payload = {"message" : message}
requests.post(LINE_URL, headers=headers, params=payload)
def CloseCamera(self): # カメラクローズ
self.camera.close()
#main
try:
if __name__ == "__main__":
os.chdir(os.path.dirname(os.path.abspath(__file__))) # カレントディレクトリをプログラムのあるディレクトリに移動する
sec_camera = SecurityCameraClass()
rec = False # 録画中フラグ OFF
start_detect = dt.datetime.now() # 検知開始時刻
while True:
rec_time = dt.timedelta(seconds=0)
if GPIO.input(SC_CAMERA_PIN) == GPIO.HIGH: # 検知
if rec == False: # 録画 OFFなら
start_detect = dt.datetime.now() # ビデオスタート時刻
rec = sec_camera.StartRecording() # 録画開始
rec_time = dt.datetime.now() - start_detect # 録画時間を計算
if rec_time.total_seconds() >= MAX_REC_TIME: # 録画最大時間を超えた時
rec = sec_camera.StopRecording() # 録画終了
start_detect = dt.datetime.now() # ビデオスタート時刻
rec = sec_camera.StartRecording() # 録画開始
else: # 未検知
if rec == True: # 録画 ON なら
rec = sec_camera.StopRecording() # 録画終了
start_detect = dt.datetime.now() # ビデオスタート時刻リセット
sleep(INTERVAL)
except KeyboardInterrupt:
pass
GPIO.cleanup()
sec_camera.CloseCamera()
モジュールの読み込み
7~16行目で必要なモジュールの読み込み。
ThreadPoolExecutorを読み込んでいるのはGoogle Driveにファイルコピーをする際にスレッドを使ってコピーをしたいから。
スレッドを使わないでコピーをすると時間のかかるコピー中はプログラムが停止してしまうのでモーションセンサーやカメラの処理が待ちになってしまう。
定数の定義
18~25行目で定数を定義している。
MAX_REC_TIME = 3600
最長録画時間は何らかの原因でカメラの前にずっと動く物があり続けて録画が長時間になってしまった時に、一旦中断する為の定数。
ローカルディスクのパンクやリソース不足に陥らないように一旦録画を中断してローカルのファイルをGoogle Driveにアップロードする為の処置。
DRIVE_LINK = "https://drive.google.com/open?id="
LINEにメッセージを送る際にメッセージのリンクをクリックすれば該当のGoogle Driveに飛ぶようにリンクを用意している。
INTERVAL = 0.5
モーションセンサーの値を検知する間隔。
とりあえず0.5を設定したが状況によって調整が必要かも。
AN_TEXT_SIZE = 24
録画の際に画面上部に注釈をつけることが出来るが、その際のテキストのサイズ。
尚、テキストは複数台の監視カメラを設置した事を想定して録画している場所を表示する事にした。
main部
SecurityCameraClassクラスの説明の前に73~103行目のメイン部を先に説明する。
sec_camera = SecurityCameraClass()
SecurityCameraClassのインスタンスを作成する。
この際に”__init__(self)”が呼び出される。
start_detect = dt.datetime.now()
先程のMAX_REC_TIMEで説明したが最大録画時間を制御する為に録画開始時刻をセットしている。
while True:
・
・
・
sleep(INTERVAL)
INTERVAL間隔で処理を繰り返す
if GPIO.input(SC_CAMERA_PIN) == GPIO.HIGH:
GPIO(5番に接続)のinputでモーションセンサーを値をチェックしている。
- Hight:検知
- Low:未検知
尚、第1回の記事でも書いたがジャンパースイッチをリピートトリガーにしているので動きがある時はHighが検知され続ける。
if rec == False: # 録画 OFFなら
start_detect = dt.datetime.now() # ビデオスタート時刻
rec = sec_camera.StartRecording() # 録画開始
現在が録画をしていない状態なら”sec_camera.StartRecording()”で録画を開始する。
if rec_time.total_seconds() >= MAX_REC_TIME: # 録画最大時間を超えた時
rec = sec_camera.StopRecording() # 録画終了
start_detect = dt.datetime.now() # ビデオスタート時刻
rec = sec_camera.StartRecording() # 録画開始
録画時間が最大時間を超えた時には一旦録画を終了して再度録画を開始する。
if rec == True: # 録画 ON なら
rec = sec_camera.StopRecording() # 録画終了
start_detect = dt.datetime.now() # ビデオスタート時刻リセット
モーションセンサーの検知結果がLow(未検知)で現在の状態が録画中なら”sec_camera.StopRecording()”で録画を終了する。
GPIO.cleanup()
sec_camera.CloseCamera()
Ctrl+cでプログラムが強制終了した時もクリーンアップを実行する。
クラスの説明
SecurityCameraClassクラスの説明。
__init__(self)
SecurityCameraClassのインスタンスを最初に作成した時に呼び出されるコンストラクタ。
with open(INITIAL_FILE) as f: # 初期設定ファイルの読み込み
jsn = json.load(f)
self.folder_id = jsn["folder_id"] # folder_idの読み込み
self.token = jsn["line_token"] # LINE用tokenの読み込み
self.location = jsn["location"] # 監視カメラ設置場所
JSON形式の初期設定ファイルをファイルを読み込む。
folder_id | 動画ファイルアップロード先のGoogle DriveのフォルダーID 詳しくは前回の記事を参照 |
line_token | LINEメッセージを送るためのアクセストークン 詳しくは前回の記事を参照 |
location | カメラの設置位置をテキストで指定する 前述の動画の上部に埋め込むカメラの設置位置を表すテキスト |
self.camera = PiCamera()
Piカメラのインスタンスを作成している。
以降”camera.メソッド”でPiCameraのメソッドを呼び出せる。
self.camera.annotate_text = self.location
self.camera.annotate_text_size = AN_TEXT_SIZE
self.camera.rotation = 270
初期設定ファイルから読み込んだ録画場所の情報を注釈として挿入しているのとカメラの映像を270度回転している。
gauth = GoogleAuth() # GoogleDriveへの認証
gauth.LocalWebserverAuth()
self.drive = GoogleDrive(gauth)
Google Driveへアクセスする為の認証。
StartRecording(self)
録画開始メソッド。
time_stamp = "{0:%Y-%m-%d-%H-%M-%S}".format(dt.datetime.now()) # 日付時刻をセット
self.save_directory = SAVE_DIR + "video" + time_stamp + ".h264" # ディレクトリ、ファイル名をセット
self.camera.start_recording(self.save_directory) # 指定ディレクトリに録画
日付時刻をファイル名に埋め込んで録画を開始する。
StopRecording(self)
録画終了メソッド。
録画の終了と共にGoogle Driveへファイルコピーする別メソッドを呼び出す。
self.camera.stop_recording() # 録画終了
録画終了。
executor = ThreadPoolExecutor(max_workers=3) # 同時実行は3つまでスレッド実行
executor.submit(self.OnTheread)
Google Driveへファイルコピーする別メソッドを呼び出す。
ファイルコピーは時間が掛かるので別スレッドとした。
OnTheread(self)
StopRecordingより呼び出される。
Google Driveへ録画ファイルをコピーした後にローカルディスクのファイルを削除する。
file_name = os.path.basename(self.save_directory)
del_file_name = self.save_directory # 対象ディレクトリとファイル名を保存しておかないと別スレッドなので更新されてしまう
f = self.drive.CreateFile({"title": file_name, # GoogleDrive
"mimeType": "video/H264",
"parents": [{"kind": "drive#fileLink", "id":self.folder_id}]})
f.SetContentFile(self.save_directory) # ファイル名指定
f.Upload() # アップロード
ファイル名指定でGoogle Driveへアップロードする。
アップロード終了後にローカルディスクのファイルを削除する為に”del_file_name”に削除対象(アップロード対象でもある)ファイル名をコピーしている。
ここでファイル名コピーしているのは別スレッドなのでアップロード中に”save_directory”の値が更新されてしまうの可能性がある為。
sec_camera.LineMessage() # LINEにメッセージを送信
LINE Notifyよりメッセージを送信するメソッドを呼び出す。
LineMessage(self)
録画検知した事をLINE Notifyよりメッセージ送信する。
requests.post(LINE_URL, headers=headers, params=payload)
LINEメッセージのpost(送信)。
その他
以上でプログラムの説明は終了。
残りの課題としては、
- RaspberryPiの電源を入れた時にPythonのプログラムを自動で起動させたい
- モーションセンサーやカメラが基盤がむき出しなので外観をもう少し何とかしたい
があるが、次回以降の記事とする。
次回の記事はこちら。
動画
この記事の内容を動画で説明しているので参考にして欲しい。
はじめまして。にしやすと申します。RaspberryPiを触るのは初めてなのですが、SOUICHIROさんの監視カメラの記事を読みながらちまちま勧めています。プログラムを実行させると下記のエラーがでて止まってしまいます。Googleなどで検索しても対策がヒットせずにちょっと途方にくれてしまいました。何かアドバイスをいただけないでしょうか?
・動体検知されたあとに録画を開始しようとしたところで発生しています。H.264のエンコード時にmacroblockのサイズを超えてしまっているようなのですが、、
motion detected
exception: Traceback (most recent call last):
File “/usr/share/mu-editor/mu/debugger/runner.py”, line 494, in run
debugger._runscript(filename)
File “/usr/share/mu-editor/mu/debugger/runner.py”, line 469, in _runscript
self.run(e)
File “/usr/lib/python3.7/bdb.py”, line 585, in run
exec(cmd, globals, locals)
File “”, line 1, in
File “/opt/security-camera/SecCamera.py”, line 101, in
rec = sec_camara.StratRecording() # 録画開始
File “/opt/security-camera/SecCamera.py”, line 54, in StratRecording
self.camera.start_recording(self.save_file_path) # 指定pathに録画
File “/usr/lib/python3/dist-packages/picamera/camera.py”, line 1046, in start_recording
camera_port, output_port, format, resize, **options)
File “/usr/lib/python3/dist-packages/picamera/camera.py”, line 723, in _get_video_encoder
self, camera_port, output_port, format, resize, **options)
File “/usr/lib/python3/dist-packages/picamera/encoders.py”, line 601, in __init__
parent, camera_port, input_port, format, resize, **options)
File “/usr/lib/python3/dist-packages/picamera/encoders.py”, line 187, in __init__
self._create_encoder(format, **options)
File “/usr/lib/python3/dist-packages/picamera/encoders.py”, line 727, in _create_encoder
(self.output_port.framesize, macroblocks_limit))
picamera.exc.PiCameraValueError: output resolution 1920×1200 exceeds macroblock limit (8192) for the selected H.264 profile and level
にしやす さん
こんにちは。
カメラはラズパイから扱えるカメラとの前提ですが、
camera.resolution = (1280, 720)
等としたらどうでしょうか。
詳しくはこちらを参照して下さい
https://picamera.readthedocs.io/en/release-1.13/api_camera.html?highlight=annotate_text_size#picamera
上記でNGの場合ですが、
raspivid -o video.h264 -t 10000
をLXTerminalから実行するとvideo.h264というファイルに保存されますか?
コマンドに関してはこちらを参照して下さい
https://www.souichi.club/raspberrypi/monitoring-camera-system01/
ありがとうございます。上手くいきました!!これから感度と検知距離の調整。楽しいです!!
上手くいきましたか。良かったです?
ぜひ楽しんで下さい。
とても参考になるホームページを作成いただきありがとうございます。
初心者でとても初歩的なことをお聞きしているかもしれず大変申し訳ございません。
opt
│ │
│ ├─security-camera
このディレクトリ構造ですが、optの上のディレクトリは何になりますでしょうか。
/optの下に作ろうとすると、以下のエラーが出てしまいます。
Error creating directory /opt/security camera: 許可がありません
お忙しいところ恐れ入りますが、アドバイスいただけると幸いです。
/optの配下に作成しています。
エラーは権限が無いせいだと思います。
sudo mkdir security-camera
でいけませんか。
sudoをつけると所有者やグループはrootになると思いますので必要に応じてchownコマンドで所有者、グループを変更してください。
chown -R 権限を与えたい所有者名:権限を与えたいグループ名 security-camera/
みたいな感じです。
初めまして。ysという者です。
赤外線モーションセンサーを使用しているのですが、人やモノが動いていないのにずっと録画し続けて困っています。
print(“GPIO”,GPIO.input(SC_CAMERA_PIN),”rec=”,rec,”録画秒数”,rec_time.total_seconds())を入力してデバックしたところ、ずっとGPIOが1でrec=Trueとなっています。
この場合、モーションセンサーの不具合と考えていいのでしょうか?
ys さん こんにちは
モーションセンサーが壊れている可能性はありますね。
もしモーションセンサーが複数あるのなら、違うモーションセンサーで試してみたらどうでしょう。
またはモーションセンサーにHC-SR501を使っているのであれば、ジャンパースイッチをシングルトリガーにして感度を最低(反時計回り)、遅延時間を最小(反時計回り)にして、状態に変化が無いか確認してみてください。
もしかしたら感度が良すぎる状態なのかも知れません。
ジャンパースイッチや感度、遅延時間については下記の記事を参照してください。
https://www.souichi.club/raspberrypi/monitoring-camera-system01/
返信ありがとうございます!
別のモーションセンサーを使用したら解決しました。
???