RaspberryPi 3 Model B+でIoT監視カメラをつくる(その3プログラミング) | そう備忘録

RaspberryPi 3 Model B+でIoT監視カメラをつくる(その3プログラミング)

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ディレクトリ

録画した動画ファイルを一時的に保存しておくディレクトリ

プログラム

2019/11/25追記

下記のプログラムに更に修正を加えている。

そのときに記事はこちら

プログラムの全体は以下の通り。

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行目で定数を定義している。

19行目
MAX_REC_TIME = 3600

最長録画時間は何らかの原因でカメラの前にずっと動く物があり続けて録画が長時間になってしまった時に、一旦中断する為の定数。

ローカルディスクのパンクやリソース不足に陥らないように一旦録画を中断してローカルのファイルをGoogle Driveにアップロードする為の処置。

23行目
DRIVE_LINK = "https://drive.google.com/open?id="

LINEにメッセージを送る際にメッセージのリンクをクリックすれば該当のGoogle Driveに飛ぶようにリンクを用意している。

24行目
INTERVAL = 0.5

モーションセンサーの値を検知する間隔。

とりあえず0.5を設定したが状況によって調整が必要かも。

25行目
AN_TEXT_SIZE = 24

録画の際に画面上部に注釈をつけることが出来るが、その際のテキストのサイズ。

尚、テキストは複数台の監視カメラを設置した事を想定して録画している場所を表示する事にした。

main部

SecurityCameraClassクラスの説明の前に73~103行目のメイン部を先に説明する。

81行目
sec_camera = SecurityCameraClass()

SecurityCameraClassのインスタンスを作成する。

この際に”__init__(self)”が呼び出される。

83行目
start_detect = dt.datetime.now()

先程のMAX_REC_TIMEで説明したが最大録画時間を制御する為に録画開始時刻をセットしている。

84~99行目
while True:
   ・
   ・
   ・
    sleep(INTERVAL)

INTERVAL間隔で処理を繰り返す

86行目
if GPIO.input(SC_CAMERA_PIN) == GPIO.HIGH:

GPIO(5番に接続)のinputでモーションセンサーを値をチェックしている。

  • Hight:検知
  • Low:未検知

尚、第1回の記事でも書いたがジャンパースイッチをリピートトリガーにしているので動きがある時はHighが検知され続ける。

87~89行目
if rec == False: # 録画 OFFなら
    start_detect = dt.datetime.now() # ビデオスタート時刻
    rec = sec_camera.StartRecording() # 録画開始

現在が録画をしていない状態なら”sec_camera.StartRecording()”で録画を開始する。

91~94行目
if  rec_time.total_seconds() >= MAX_REC_TIME: # 録画最大時間を超えた時
    rec = sec_camera.StopRecording() # 録画終了
    start_detect = dt.datetime.now() # ビデオスタート時刻
    rec = sec_camera.StartRecording() # 録画開始

録画時間が最大時間を超えた時には一旦録画を終了して再度録画を開始する。

96~98行目
if rec == True: # 録画 ON なら
    rec = sec_camera.StopRecording() # 録画終了
    start_detect = dt.datetime.now() # ビデオスタート時刻リセット

モーションセンサーの検知結果がLow(未検知)で現在の状態が録画中なら”sec_camera.StopRecording()”で録画を終了する。

102~103
GPIO.cleanup()
sec_camera.CloseCamera()
2019/09/29 追記

Ctrl+cでプログラムが強制終了した時もクリーンアップを実行する。

クラスの説明

SecurityCameraClassクラスの説明。

__init__(self)

SecurityCameraClassのインスタンスを最初に作成した時に呼び出されるコンストラクタ。

32~36行目
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

カメラの設置位置をテキストで指定する

前述の動画の上部に埋め込むカメラの設置位置を表すテキスト

37行目
self.camera = PiCamera()

Piカメラのインスタンスを作成している。

以降”camera.メソッド”でPiCameraのメソッドを呼び出せる。

38~40
self.camera.annotate_text = self.location
self.camera.annotate_text_size = AN_TEXT_SIZE
self.camera.rotation = 270
2019/09/29 追記

初期設定ファイルから読み込んだ録画場所の情報を注釈として挿入しているのとカメラの映像を270度回転している。

41~43行目
gauth = GoogleAuth() # GoogleDriveへの認証
gauth.LocalWebserverAuth()
self.drive = GoogleDrive(gauth)

Google Driveへアクセスする為の認証。

StartRecording(self)

録画開始メソッド。

46~48行目
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へファイルコピーする別メソッドを呼び出す。

52行目
self.camera.stop_recording() # 録画終了

録画終了。

53~54行目
executor = ThreadPoolExecutor(max_workers=3) # 同時実行は3つまでスレッド実行
executor.submit(self.OnTheread)

Google Driveへファイルコピーする別メソッドを呼び出す。

ファイルコピーは時間が掛かるので別スレッドとした。

OnTheread(self)

StopRecordingより呼び出される。

Google Driveへ録画ファイルをコピーした後にローカルディスクのファイルを削除する。

58~64行目
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”の値が更新されてしまうの可能性がある為。

66行目
sec_camera.LineMessage() # LINEにメッセージを送信

LINE Notifyよりメッセージを送信するメソッドを呼び出す。

LineMessage(self)

録画検知した事をLINE Notifyよりメッセージ送信する。

72行目
requests.post(LINE_URL, headers=headers, params=payload)

LINEメッセージのpost(送信)。

送信結果

LINE Notifyよりメッセージ

その他

以上でプログラムの説明は終了。

残りの課題としては、

  • RaspberryPiの電源を入れた時にPythonのプログラムを自動で起動させたい
  • モーションセンサーやカメラが基盤がむき出しなので外観をもう少し何とかしたい

があるが、次回以降の記事とする。

次回の記事はこちら

動画

この記事の内容を動画で説明しているので参考にして欲しい。

souichirou

やった事を忘れない為の備忘録 同じような事をやりたい人の参考になればと思ってブログにしてます。 主にレゴ、AWS(Amazon Web Services)、WordPress、Deep Learning、RaspberryPiに関するブログを書いています。 仕事では工場に協働ロボットの導入や中小企業へのAI/IoT導入のアドバイザーをやっています。 2019年7月にJDLA(一般社団法人 日本デイープラーニング協会)Deep Learning for GENERALに合格しました。 質問は記事一番下にあるコメントかメニュー上部の問い合わせからお願いします。

おすすめ

10件のフィードバック

  1. にしやす より:

    はじめまして。にしやすと申します。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

  2. みっちー より:

    とても参考になるホームページを作成いただきありがとうございます。
    初心者でとても初歩的なことをお聞きしているかもしれず大変申し訳ございません。
    opt
    │ │
    │ ├─security-camera
    このディレクトリ構造ですが、optの上のディレクトリは何になりますでしょうか。
    /optの下に作ろうとすると、以下のエラーが出てしまいます。
    Error creating directory /opt/security camera: 許可がありません

    お忙しいところ恐れ入りますが、アドバイスいただけると幸いです。

    • souichirou より:

      /optの配下に作成しています。
      エラーは権限が無いせいだと思います。
      sudo mkdir security-camera
      でいけませんか。

      sudoをつけると所有者やグループはrootになると思いますので必要に応じてchownコマンドで所有者、グループを変更してください。
      chown -R 権限を与えたい所有者名:権限を与えたいグループ名 security-camera/
      みたいな感じです。

  3. 匿名 より:

    初めまして。ysという者です。
    赤外線モーションセンサーを使用しているのですが、人やモノが動いていないのにずっと録画し続けて困っています。
    print(“GPIO”,GPIO.input(SC_CAMERA_PIN),”rec=”,rec,”録画秒数”,rec_time.total_seconds())を入力してデバックしたところ、ずっとGPIOが1でrec=Trueとなっています。
    この場合、モーションセンサーの不具合と考えていいのでしょうか?

    • souichirou より:

      ys さん こんにちは
      モーションセンサーが壊れている可能性はありますね。
      もしモーションセンサーが複数あるのなら、違うモーションセンサーで試してみたらどうでしょう。
      またはモーションセンサーにHC-SR501を使っているのであれば、ジャンパースイッチをシングルトリガーにして感度を最低(反時計回り)、遅延時間を最小(反時計回り)にして、状態に変化が無いか確認してみてください。
      もしかしたら感度が良すぎる状態なのかも知れません。
      ジャンパースイッチや感度、遅延時間については下記の記事を参照してください。
      https://www.souichi.club/raspberrypi/monitoring-camera-system01/

ys へ返信する コメントをキャンセル

名前、メール、サイト欄は任意です。
またメールアドレスは公開されません。