LoginSignup
26
35

More than 3 years have passed since last update.

ラズベリーパイZERO-WHで監視カメラ(動体検知と動画保存)を作成する

Last updated at Posted at 2020-06-02

はじめに

ラズベリーパイで監視カメラを作成しました。監視カメラといっても、入り口付近に設置し入場した人の確認を行うといったもので、カメラ設置場所に近づいた人(または物)に対し記録するものとしています。また24時間を想定しているので、夜でもある程度は動作してほしいと考えています(実際にはなかなか難しいようです)。
コストはかけないものとしているので、ラズパイZERO HWとカメラのみで動作するようにしました(ネットワークにつながるラズベリーパイなら大丈夫と思います)。
監視カメラとしては、カメラ画像より動体検知し、その後に動画を一定時間記録しこれをGoggleDriveに保存します。
また、できるだけ通信量が増えないように設定を変更できるようにしました(可能な範囲ですが)。
その代わりに、リアルタイム性は全く無視しています。
(ハードウエアへの電源供給やWifiなどによるネット接続は前提です)

具体的には、以下のように差分抽出画像(比較グレイスケール画像に変化部分を青色で表現しています)を検出したら、この画像を含め動画を数秒間撮影し、これを保存するといったものです。
cam99991231-121147_cl_mask-0.116-59.99.jpg

なお通信時には、前の記事
ラズベリーパイZERO-WHでカメラ画像をGoogleDriveにアップロードする(python+pydrive)を外部コマンドとして利用しています。

利用しているハードウエア

  • Raspberry Pi Zero WH
  • Raspberry Zero V2 Camera (今回のプログラムには不要)
  • microSD カード(32GB)
  • 外部設置用のケース
  • 電源アダプタ+電源ケーブル

利用しているソフトウエア

  • Linux raspberrypi 5.4.35+ #1314 Fri May 1 17:36:08 BST 2020 armv6l GNU/Linux
  • Python 3.7.3
  • OpenCv 4.1.1.26
  • Numpy 1.16.2

構築準備

ラズベリーパイZEOR HWのOSの構築、設定についてはここでは述べていません。
NumpyやOpneCVについても、参考先から構築を行ってください。

追記:環境構築についても記載しました。ラズベリーパイZERO-WHの監視カメラを動かすための環境構築作業

カメラの設置

今回は外に設置するため、防水ケースにしなければなりません。しかもラズベリーパイZEROでもそれなりに消費電力があるので、完全密閉というわけにはいきません。カメラの向きも調整できたほうが良いのですが、なかなか難しいです。
今回は、いくつかの設置に必要な要素はあきらめて安価なブレーカケース用いました。
カメラ用の穴をあけてケース内に固定しています。この中には、USBアダプタも一緒に入れています。ACケーブルを外にあるコンセントにさしています。
設置後のカメラの様子を以下に示します(なんて手作り感満載なんでしょう。とりあえずの設置です。中には、カメラとラズベリーパイZeroHW、USBケーブル(10cm)とUSBアダプタが入っています。USBアダプタを延長ACケールブル接続し、そのACケーブルが外部に出ています)。
カメラ設置1m.jpg
カメラ設置2m.jpg

※設置後に、赤外線検知ライトを後ろのほうに付けていますが、撮影画像はあまり変化しませんでした。

監視カメラプログラムについて

監視カメラプログラムは、プログラム内の初めに以下の必須設定を行います。

  1. 作業用のディレクトリの設定(TEMP_DIR) /tmp/temp(一例です。ほかの場所でも構いませんが、権限が必要です)
  2. 画像送信用のディレクトリの設定(SAVE_DIR) /tmp/video(一例です。ほかの場所でも構いませが、権限が必要です)
  3. プロセスIDを格納するファイル名の設定(PID_FILE) /tmp/_camOpne.pid (一例です。ほかの場所でも構いませんが、権限が必要です)
  4. 画像アップロード用のプログラムの設定(「ラズベリーパイZERO-WHでカメラ画像をGoogleDriveにアップロードする(python+pydrive)」) /home/pi/googleUploadDir.py(例として、ほかの場所でも構いませんが、プログラムが動作可能な状況であることが必要です)

上記の設定を行えば、カメラからの画像を判定し動画の撮影を行います。
しかし、カメラの環境や被写体などにより、想定通りの動作をするかはなかなか難しいと思います。
そのための確認方法や設定方法もプログラムの説明の中で解説します。

監視カメラプログラムの解説

以下に、監視カメラのメインプログラムを示します。
1. このプログラムは、24時間動作を前提に書いています。不具合により動作エラーになった場合に、プロセスの稼働状況や強制終了のためにプロセスIDを記録しています(241行目)。設定()によりますが、本プログラムは24時間後に終了するようにしています。
2. 監視カメラのクラス作成後(248行目)、カメラ画像をループ(252行目)により取得し続けます。
3. ループ内では、video_modeで状態を保持し、録画を行うか、取得した画像に変化(動体検知)の検出のいずれかを行います。
4. 検出基準を超えた場合は、そのフレームから保存対象として動画ファイルに格納します。
5. あと、動作確認用に、定期的にカメラ画像の保存と状態をログに記載します。

メインプログラムのみ
   239  #main
   240  try:
   241      pid = os.getpid()       # プロセスID(PID)の取得
   242      with open( PID_FILE, mode='w' ) as f:
   243          f.write( str( pid ) )
   244 
   245      if __name__ == "__main__":
   246          os.chdir( os.path.dirname( os.path.abspath(__file__) ) ) # カレントディレクトリをプログラムのあるディレクトリに移動する
   247          # 監視カメラ用クラス作成
   248          m_camera = MotionCameraClass()
   249          start_detect = dt.datetime.now() # 検知開始時刻
   250          count = 0
   251          video_mode = 0
   252          while True:
   253              count = count + 1
   254              if m_camera.is_opened() == False:
   255                  break
   256              status, frame = m_camera.read() 
   257              if status == True : # 正常な画像(frame)を処理する
   258                  if video_mode == True: # 録画中
   259                      video_mode = m_camera.video_write( frame )
   260                      if video_mode == False :
   261                          m_camera.check_upload()
   262                  else: # 確認中
   263                      if m_camera.check_motion( frame ):  # 動きの確認
   264                          video_mode = True
   265                          m_camera.video_write( frame )   # 録画開始
   266                  # 動作確認用
   267                  if (count % HEART_BEET_TIME) == 0 :
   268                      m_camera.log_print( str( count ) + "-{:.4f}".format( m_camera.moment ) )
   269                      if ( count % SAMPLE_OUTPUT ) == 0 :
   270                          m_camera.save_mask_imagefile( "check", frame ) # 撮影した画像を保存
   271              active_time = start_detect - dt.datetime.now()
   272              if( active_time.total_seconds() > MAX_REC_TIME ):
   273                  break
   274          # while end
   275          del m_camera
   276 
   277  except KeyboardInterrupt:
   278      pass
   279  else:
   280      print( 'MontionMain ExceptionError:', e )
   281      m_camera.print_cap_status()
   282      del m_camera

出力画像ファイルについて

本プログラムでは、カメラ動画のほかに、胴体検知結果の画像ファイルと、定期的にカメラに映った画像を出力するようにしています。
特に設置直後は、設定した条件により、動画モードにならない(または常に検出状態になる)場合がありますので、動作を見て設定を変更してください(特に後述のMOVE_THRESHHOLD)。

以下に、いくつかのファイル書式やログ内容を示すので、動作後のファイルを参照してパラメータの変更を行ってください。

ファイル種別 ファイル名書式 サンプル   
動体検知結果ファイル cam{YYYMMDD}-{HHMMSS}_cl_mask-{スコア}-{CPU温度}.jpg cam20200529-185505_cl_mask-0.123-53.00.jpg
動画ファイル   cam{YYYMMDD}-{HHMMSS}_video-{スコア}-{CPU温度}.mp4 cam20200529-185505_video-0.123-53.00.mp4
確認用カメラ画像    cam{YYYMMDD}-{HHMMSS}_check-{スコア}-{CPU温度}.jpg cam20200529-184621_check-0.105-52.46.jpg
ログファイル log{YYYMMDD}-{HHMMSS}.log  log20200528-174010.log 

動体検知結果ファイルは、検知結果の閾値(MOVE_THRESHHOLD)を超えた場合に出力します。直前の画像との差異がある部分について本プログラムでは青色となっている部分で表現しています(動体検知は縮小画像をグレースケールに変更後ノイズ除去(ぼかし)を行ったもので比較を行います。その差分結果を二値化して変化画素数比を指標にしています)。なお、動体検知画像は動画の最初のフレームを確認すると対象物がわかると思います。
ファイル名にあるスコア(ログファイル内にも同様のスコアを出力)で、その値を参考に閾値(MOVE_THRESHHOLD)を見直すことを想定しています。

 ※ 最初に記載していますが、カメラに近づいた場合、その被写体が画面に占める割合が大きくなるので、この手法にしています。遠方での人の動きなどは、変化の範囲も小さくノイズなどとの違いを分離検出するのは難しいと考えています。

カメラ設置後は、デフォルト設定でも動画を全くサーバにアップしない状態になる可能性があります。この状況では、動作しているのかわからないので、確認用のカメラ静止画像を定期的に出力して転送するようにしています。一種のハートビートの様なものですね。通常は同じ画像になるので、頻度は少ないほうが良いと思われます。

ログファイルは、crontabなどで、毎日再起動の設定を行えば、再起動後の動画検出後になりますが、サーバで確認でまきす。

パラメータについて

必須設定に続いての設定項目です。説明を確認し環境に合わせて変更してください。パラメータによっては値には範囲があるものがあります。参考値も記載しているので、これらの範囲から選ぶことをお勧めします。

パラメータ設定部のみ
    13  SAVE_DIR         = "/tmp/video/"               # ファイル保存用ディレクトリ(full path)
    14  TEMP_DIR         = "/tmp/temp/"                # ファイル保存用ディレクトリ(full path)
    15  PID_FILE         = "/tmp/_camOpen.pid"         # pid file
    16  GOOGLE_UPLOAD    = "/home/pi/googleUploadDir.py"   # G-driveにファイルをアップロードするコマンド
    17  MAX_REC_TIME     = ( 3600 * 24 )     # 最長検出時間(秒数)
    18  START_INTERVAL   = 3                 # 監視間隔(秒)
    19  WAIT_SAVE_TIME   = 20                # 1ファイル当たりの録画時間
    20  WAIT_UPLORD_TIME = 200               # Uploadの最小間隔(秒)
    21 
    22  MOVE_THRESHHOLD  = 0.09              # 確認の変化ピクセル数の割合
    23  CHECK_IMG_HEIGHT = 240               # MOTION確認時の画像サイズ(高さ)
    24  CHECK_IMG_WIDTH  = 320               # MOTION確認時の画像サイズ(幅)
    25  VIDEO_HIGHT      =  720     # 1080   #  720  # 480
    26  VIDEO_WIDTH      = 1280     # 1920   # 1280  # 640
    27  VIDEO_CAMERA_FPS = 3        # 1-60
    28  GAMMA            = 2
    29  HEART_BEET_TIME  = ( VIDEO_CAMERA_FPS * 5 )     # 動作確認用のLog保存間隔
    30  SAMPLE_OUTPUT    = ( 900 * VIDEO_CAMERA_FPS )   # 動作確認用の画像保存間隔

説明を追加したものを表形式で示しました。

行番号 パラメータ デフォルト値 説明
13 SAVE_DIR "/tmp/video/" ファイル保存用ディレクトリ(full path)
14 TEMP_DIR "/tmp/temp/" ファイル保存用ディレクトリ(full path)
15 PID_FILE "/tmp/_camOpen.pid" 本プリグラムのプロセス番号を記録するファイル
16 GOOGLE_UPLOAD "/home/pi/googleUploadDir.py" G-driveにファイルをアップロードするコマンド。前の記事ラズベリーパイZERO-WHでカメラ画像をGoogleDriveにアップロードするpython+pydrive)を外部コマンドとして指定
17 MAX_REC_TIME ( 3600 * 24 ) 最長検出時間(秒数)これを超えると終了する
18 START_INTERVAL 3 カメラの起動時間(秒)。カメラ側でパラメータを自動で調整しているため?
19 WAIT_SAVE_TIME 20 1ファイル当たりの録画時間(秒)
20 WAIT_UPLORD_TIME 200 Uploadの最小間隔(秒) 。画像検出時に頻繁にファイルアップロードしないようにするためのもの。短いと、前のアップロードプログラムが終わる前に送信しようとしてしまうための抑制処置
22 MOVE_THRESHHOLD 0.09 確認の変化ピクセル数の割合。この値を超えた場合に録画を行う
23 CHECK_IMG_HEIGHT 240 動体検知を行うときの画像サイズ(高さ)
24 CHECK_IMG_WIDTH 320 動体検知を行うときの画像サイズ(幅)
25 VIDEO_HIGHT 720 カメラから取得する画素数(高さ)→(1080, 720, 480)
26 VIDEO_WIDTH 1280 カメラから取得する画素数(幅) →(1920, 1280, 640)
27 VIDEO_CAMERA_FPS 3 動画のフレーム数( 1-60 )ハードや画素数によるが、この設定では3以上は間に合わない
28 GAMMA 2 ガンマ補正用の値(昼から夜に変わるときでも差が出ないように補正する)
29 HEART_BEET_TIME ( VIDEO_CAMERA_FPS * 5 ) 動作確認用のLog保存間隔(秒)
30 SAMPLE_OUTPUT ( 900 * VIDEO_CAMERA_FPS ) 動作確認用の画像保存間隔(秒)

フレームレート(VIDEO_CAMERA_FPS )を上げれば、滑らかな画像になります。しかし、ファイルサイズも大きくなります。どうしてもフレームレートを上げたい場合は、ハードウエアの向上もしくは、画素数(VIDEO_HIGHT, VIDEO_WIDTH )を下げる、カメラ画像の読み込み(256行目のm_camera.read())の中の処理(画像回転、ガンマ補正)を省く(ガンマ補正を省くと、明るさが変化する場合に誤検出になる場合があります)方法があります。

Motionカメラのクラスの説明(class MotionCameraClass:)

クラス定義の最初に、インスタンス変数をまとめています。

  1. def print_cap_status( self ): # cv2.VideCameraの設定を表示
    プログラムの定義値やインスタンス変数の値を標準出力とログに出力します。

  2. def init( self ): # コンストラクタ
    OpenCvのcv2.VideoCaptureクラスを取得し、初期化設定を行います。カメラの画素数設定などを行いますが、それ以外の設定値についてはなかなか良い解決値がありません。私が確認した中では、cv2.CAP_PROP_BRIGHTNESSの値は重要です。デフォルトは10でしたが、夜間撮影などでは暗すぎたため、値を上げています。当然ですが、上げすぎると真っ白な画像になるので、確認しながら調整する必要があります。
    ガンマ補正用の返還テーブルの作成や出力先のディレクトリの確認・作成やファイルの移動を行います。

  3. def is_opened( self ): # カメラ準備の確認
    OpenCvで設定しているカメラが動作しているかを確認します(return値は正常時にTrue)。

  4. def read( self ): # カメラから画像を読み込み
    カメラ画像を読み込み、画像の回転とガンマ補正を行います。監視カメラの設置個所により、回転が不要な場合もあります(その場合は回転を行わない、もしくは別の角度に変更します)。
    カメラ画像を取得できない場合は、statusにFalseを返します。その場合は、再度カメラ画像の取得を試みることになります。

  5. def video_write( self, frame ) : # 画像を動画ファイルとして書き込み
    画像(fream)を動画として書き込みます。
    まだ動画ファイルを作成していない場合は、動画ファイルを作成してから画像を保存します。
    書き込み後に、動画の保存時間(WAIT_SAVE_TIME )を超えている場合は、動画のファイルを閉じます。
     また、必要に応じてファイルの転送外部ファイルを呼び出します。

  6. def check_motion( self, frame ): # 録画するかの判定
    引数の画像と、1つ前の画像を比較し、変化量が設定基準を超えているかの判定を行います。
    判定前に、画素数サイズを縮小し、ノイズ除去、比較、判定を行ない、1つ前の画像を保持しています。
    判定結果が閾値を超えている場合は、前の画像との比較結果をファイル出力します。
     比較前後の画像も出力はコメントアウトしているため行いません。
    検出結果の出力では、色付けなどで、OpenCvなどで扱う画像構造を少し理解しました。

  7. def save_mask_imagefile( self, name, frame ): ファイルへの出力
    出力画像ファイルの中で、静止画像はこれで出力する。「name」は、出力ファイルの書式にあった、「check」、「cl_mask」を指定して使用しています。
    出力ファイルは、動画も含め、すべて一度TEMP_DIRに作成しています。ファイル作成後にSACE_DIRに移動することで、作成中の転送を防いでいます。

詳しくは、プログラムを確認いただければと思います。

収集画像について

GoogleDriveに転送した画像は、WEBブラウザ(G-Drive)もしくは、PCのディレクトリに同期したフォルダで確認できます。以下に転送したデータを表示したGoogleDriveの例を示します。

  • WEBブラウザの表示例
    GoogleDriveWebImage1.jpg.png

  • PCのエクスプローラーの表示例
    GoogleDriveForderImage2..png

早朝の暗い時間(定期的な静止画像)から明るくなって車の往来で検出した物になります。

監視カメラのプログラム(全体)

camOpne.py
#!/usr/bin/env /usr/bin/python3
# -*- coding: utf-8 -*-
# Created on 2020年  5月  22日 土曜日 18:05:07 JST
# @author: ochiai_t

import os, sys, io, cv2
import subprocess
import datetime as dt
import numpy as np
from time import sleep
import logging

SAVE_DIR         = "/tmp/video/"               # ファイル保存用ディレクトリ(full path)
TEMP_DIR         = "/tmp/temp/"                # ファイル保存用ディレクトリ(full path)
PID_FILE         = "/tmp/_camOpen.pid"         # pid file
GOOGLE_UPLOAD    = "/home/pi/googleUploadDir.py"   # G-driveにファイルをアップロードするコマンド
MAX_REC_TIME     = ( 3600 * 24 )     # 最長検出時間(秒数)
START_INTERVAL   = 3                 # 監視間隔(秒)
WAIT_SAVE_TIME   = 20                # 1ファイル当たりの録画時間
WAIT_UPLORD_TIME = 200               # Uploadの最小間隔(秒)

MOVE_THRESHHOLD  = 0.09              # 確認の変化ピクセル数の割合
CHECK_IMG_HEIGHT = 240               # MOTION確認時の画像サイズ(高さ)
CHECK_IMG_WIDTH  = 320               # MOTION確認時の画像サイズ(幅)
VIDEO_HIGHT      =  720     # 1080   #  720  # 480
VIDEO_WIDTH      = 1280     # 1920   # 1280  # 640
VIDEO_CAMERA_FPS = 3        # 1-60
GAMMA            = 2
HEART_BEET_TIME  = ( VIDEO_CAMERA_FPS * 5 )     # 動作確認用のLog保存間隔
SAMPLE_OUTPUT    = ( 900 * VIDEO_CAMERA_FPS )   # 動作確認用の画像保存間隔

# logfileの作成
log_filename = TEMP_DIR + "log{:%Y%m%d-%H%M%S}.log".format( dt.datetime.now() )
logging.basicConfig( level=logging.DEBUG, filename=log_filename, format='%(asctime)s:%(levelname)s::%(message)s')
logging.getLogger( log_filename )

class  MotionCameraClass: # Motionカメラのクラス
    before_image = None     # 
    moment = 0              # 明るさの尺度(感度調節用)
    video = None            # Video撮影クラス
    video_start     = dt.datetime.now()     # Video撮影開始時間
    upload_end_time = dt.datetime.now()     # Video撮影開始時間
    video_filename  = ""     # 撮影時の動画ファイル名
    save_video_filename = "" # 撮影後の移動先ファイル名
    g_tabel = None
    c_table = None

    def print_cap_status( self ):      # cv2.VideCameraの設定を表示
        msg = "\n" \
        + "cv2.CAP_PROP_FPS         ={}".format( self.cap.get( cv2.CAP_PROP_FPS ) ) + "\n" \
        + "cv2.CAP_PROP_FRAME_WIDTH ={}".format( self.cap.get( cv2.CAP_PROP_FRAME_WIDTH ) ) + "\n" \
        + "cv2.CAP_PROP_FRAME_HEIGHT={}".format( self.cap.get( cv2.CAP_PROP_FRAME_HEIGHT ) ) + "\n" \
        + "cv2.CAP_PROP_BRIGHTNESS  ={}".format( self.cap.get( cv2.CAP_PROP_BRIGHTNESS ) ) + "\n" \
        + "cv2.CAP_PROP_CONTRAST    ={}".format( self.cap.get( cv2.CAP_PROP_CONTRAST ) ) + "\n" \
        + "cv2.CAP_PROP_EXPOSURE    ={}".format( self.cap.get( cv2.CAP_PROP_EXPOSURE ) ) + "\n" \
        + "moment                   ={}".format( self.moment ) + "\n" \
        + "video_filename           ={}".format( self.video_filename ) + "\n" \
        + "video_start              ={0:%Y/%m/%d-%H:%M:%S}".format( self.video_start ) + "\n" \
        + "upload_end_time          ={0:%Y/%m/%d-%H:%M:%S}".format( self.upload_end_time ) + "\n" \
        + "save_video_filename      ={}".format( self.save_video_filename ) + "\n" \
        + "MAX_REC_TIME             ={}".format( MAX_REC_TIME     ) + "\n" \
        + "TEMP_DIR                 ={}".format( TEMP_DIR         ) + "\n" \
        + "SAVE_DIR                 ={}".format( SAVE_DIR         ) + "\n" \
        + "START_INTERVAL           ={}".format( START_INTERVAL   ) + "\n" \
        + "WAIT_SAVE_TIME           ={}".format( WAIT_SAVE_TIME   ) + "\n" \
        + "GOOGLE_UPLOAD            ={}".format( GOOGLE_UPLOAD    ) + "\n" \
        + "MOVE_THRESHHOLD          ={}".format( MOVE_THRESHHOLD  ) + "\n" \
        + "CHECK_IMG_HEIGHT         ={}".format( CHECK_IMG_HEIGHT ) + "\n" \
        + "CHECK_IMG_WIDTH          ={}".format( CHECK_IMG_WIDTH  ) + "\n" \
        + "VIDEO_HIGHT              ={}".format( VIDEO_HIGHT      ) + "\n" \
        + "VIDEO_WIDTH              ={}".format( VIDEO_WIDTH      ) + "\n" \
        + "VIDEO_CAMERA_FPS         ={}".format( VIDEO_CAMERA_FPS ) + "\n" \
        + "GAMMA                    ={}".format( GAMMA            ) + "\n" \
        + "HEART_BEET_TIME          ={}".format( HEART_BEET_TIME  ) + "\n" \
        + "SAMPLE_OUTPUT            ={}".format( SAMPLE_OUTPUT    ) + "\n" 
        self.log_print( msg )

    def __init__( self ):  # コンストラクタ
        self.cap = cv2.VideoCapture( 0 )
        sleep( START_INTERVAL )
        self.cap.set( cv2.CAP_PROP_FPS,          VIDEO_CAMERA_FPS )
        self.cap.set( cv2.CAP_PROP_FRAME_WIDTH,  VIDEO_WIDTH )
        self.cap.set( cv2.CAP_PROP_FRAME_HEIGHT, VIDEO_HIGHT )
        self.cap.set( cv2.CAP_PROP_BRIGHTNESS,   50 ) # 10 - 70, brightness
        self.cap.set( cv2.CAP_PROP_EXPOSURE,     1000.0)  # 1000.0
        self.print_cap_status()
        # ガンマ補正用のテーブルの作成
        gamma = GAMMA
        self.g_table = np.array( [((i / 255.0) ** (1 / gamma)) * 255 for i in np.arange(0, 256)]).astype("uint8" )
        # コントラスト補正用のテーブルの作成
        a = 10
        self.c_table = np.array( [255.0 / (1 + np.exp(-a * (i - 128) / 255)) for i in np.arange(0, 256)]).astype("uint8" )
        if os.path.isdir( TEMP_DIR ) == False :
            os.makedirs( TEMP_DIR ) 
        if os.path.isdir( SAVE_DIR ) == False :
            os.makedirs( SAVE_DIR )
        self.clean_files()

    def __del__( self ):      # カメラクローズ
        if self.video is None :
            self.video = None
        else :
            self.video.release()
            del self.video
        self.cap.release()
        del self.cap

    def create_filename( self, file_type, file_suffix ):    # ファイル名を作成する
        # video + .mp4
        # mask  + .png
        # pict  + .jpg
        time_stamp   =  "cam{0:%Y%m%d-%H%M%S}_".format( dt.datetime.now() ) # 日付時刻をセット
        filename = TEMP_DIR + time_stamp + file_type  \
                        + "-{:.3f}".format( self.moment ) \
                        + "-{:.2f}".format( self.get_cpu_temp() ) + file_suffix   # ディレクトリ、ファイル名をセット
        return( filename )

    def move_filename( self, filename ):    # ファイルをTEMP_DIRからSAVE_DIRに移動する
        move_filename = filename.replace( TEMP_DIR, SAVE_DIR )
        if os.path.exists( filename ):
            os.rename( filename, move_filename )
        return( move_filename )

    def create_video( self ) :              # 動画ファイルの作成
        filename = self.create_filename( "video", ".mp4" )    # video + .mp4
        # 動画保存時のfourcc設定(mp4用)
        fourcc = cv2.VideoWriter_fourcc( 'm', 'p', '4', 'v' )
        # 動画の仕様(ファイル名、fourcc, FPS, サイズ)
        self.video = cv2.VideoWriter( filename, fourcc, VIDEO_CAMERA_FPS, (VIDEO_WIDTH, VIDEO_HIGHT) )
        self.video_start = dt.datetime.now() 
        return( filename )

    def video_write( self, frame ) :    # 画像(frame)を動画として保存
        if self.video is None: # 動画ファイルを作成する
            self.video_filename = self.create_video() 
        # 動画書き込み
        self.video.write( frame )
        rec_time = dt.datetime.now() - self.video_start 
        if rec_time.total_seconds() > WAIT_SAVE_TIME :  #動画撮影時間の確認
            # 動画ファイルを閉じる
            self.video.release()
            # subprocess.Popen( [GOOGLE_UPLOAD, self.video_filename], close_fds=True )
            del self.video
            self.video = None
            self.before_image = None
            self.save_video_filename = self.move_filename( self.video_filename )
            return( False )
        return( True )

    def save_mask_imagefile( self, name,  frame ):
        filename = self.create_filename( name, ".jpg" )    # mask + .jpg
        cv2.imwrite( filename, frame )      # 画像ファイル作成
        return( self.move_filename( filename ) )

    def check_motion( self, frame ): # 録画するかの判定
        # 判定用にサイズを縮小
        mini = cv2.resize( frame, (CHECK_IMG_WIDTH, CHECK_IMG_HEIGHT) )
        gray = cv2.cvtColor( mini, cv2.COLOR_BGR2GRAY )       # カラーからグレイに
        gray2 = cv2.medianBlur( gray, 5 )                     # ノイズ除去
        tr = cv2.threshold( gray2, 3, 255, cv2.THRESH_BINARY )[1] 
        self.level = cv2.countNonZero( tr )                   # 白判定ピクセルを計測
        # 前画像の確認
        if self.before_image is None :
            self.before_image = gray2
            return( False )
        # 時間差の画像を取得し、差異が大きい場合は録画するための判定を返す
        fgbg = cv2.bgsegm.createBackgroundSubtractorMOG()
        fgmask = fgbg.apply( self.before_image )              # 前景領域のマスクを取得
        fgmask = fgbg.apply( gray2 )                          # 後景領域のマスクを取得
        thresh = cv2.threshold( fgmask, 3, 255, cv2.THRESH_BINARY )[1]
        self.moment = ( cv2.countNonZero( thresh )/thresh.size )  # 差分結果の変化画素数の計測
        if self.moment > MOVE_THRESHHOLD:
            self.log_print( "video_start " )
            # 画像変化が指定数を超えたので、マスク画像を保存し、動画撮影モードにする
            height, width = fgmask.shape[:2]
            color_mask = cv2.cvtColor( fgmask, cv2.COLOR_GRAY2BGR)
            color_mask[:, :, ( 1,2 )] = 0
            color_image = cv2.cvtColor( gray2, cv2.COLOR_GRAY2BGR)
            dst = cv2.addWeighted( color_image, 0.7, color_mask, 0.6, 0)
            # filename = self.save_mask_imagefile( "frame", frame )
            # filename = self.save_mask_imagefile( "before", self.before_image )
            # filename = self.save_mask_imagefile( "gray2", gray2 )
            # filename = self.save_mask_imagefile( "fgmask", fgmask )
            filename = self.save_mask_imagefile( "cl_mask", dst )
            return True
        self.before_image = gray2
        return False

    def check_upload( self ) :   # 動画作成後にアップロードを開始するか判定
        if os.path.exists( self.save_video_filename ):  # 動画撮影中ではなく
            rec_time = dt.datetime.now() - self.upload_end_time
            ti = rec_time.total_seconds() 
            if  ti >  WAIT_UPLORD_TIME :  # Upload経過時間(一定時間の間隔があいている場合に実施する)
                self.log_print( "upload file = " + self.save_video_filename ) ;
                subprocess.Popen( [GOOGLE_UPLOAD, self.save_video_filename], close_fds=True )
                self.video_filename = ""
                self.save_video_filename = ""
                self.upload_end_time = dt.datetime.now() # 次の実施時間用

    def is_opened( self ):            # cv2.cameraがopen状態かの確認
        return( self.cap.isOpened )

    def read( self ):               # cameraから画像データ(フレーム)を取得。画像回転やガンマ補正する
        status, frame = self.cap.read()
        if status == True: # 正常な画像(frame)を処理する
            # カメラ位置に合わせた回転
            frame = cv2.rotate( frame, cv2.cv2.ROTATE_180 )
            # ガンマ補正
            img_gamma = cv2.LUT( frame, self.g_table )
            # コントラスト補正 
            # img_gamma = cv2.LUT( img_gamma, self.c_table )
            return( status, img_gamma )
        return( status, frame )

    def get_cpu_temp( self ):   # CPU温度を取得(正常動作確認用)
        f = open( "/sys/class/thermal/thermal_zone0/temp","r" )
        tmp = 0
        for t in f:
            tmp = t[:2]+"."+t[2:5]
        f.close()
        return float(tmp)

    def log_print( self, msg ): # ログ保存用
        # time_stamp   =  "{0:%Y-%m-%d=%H:%M:%S}=".format( dt.datetime.now() ) # 日付時刻をセット
        # print( msg )
        logging.info( msg )
        return True 

    def clean_files( self ) :   # TEMP_DIRにあるファイルをSAVE_DIRに移動(主に前に動作した時のログファイル等をアップロードするため)
        for ff in os.listdir(TEMP_DIR):
             f_name = os.path.join( TEMP_DIR, ff )
             if f_name == log_filename :    # 現在使用中のlogfileは除外
                 continue
             if os.path.isfile( f_name ) == True :  # ファイルか確認
                 self.move_filename( f_name )       # ディレクトリを移動
                 self.log_print( "clean_file({})".format( f_name ) )
        return True 

#main
try:
    pid = os.getpid()       # プロセスID(PID)の取得
    with open( PID_FILE, mode='w' ) as f:
        f.write( str( pid ) )

    if __name__ == "__main__":
        os.chdir( os.path.dirname( os.path.abspath(__file__) ) ) # カレントディレクトリをプログラムのあるディレクトリに移動する
        # 監視カメラ用クラス作成
        m_camera = MotionCameraClass()
        start_detect = dt.datetime.now() # 検知開始時刻
        count = 0
        video_mode = 0
        while True:
            count = count + 1
            if m_camera.is_opened() == False:
                break
            status, frame = m_camera.read() 
            if status == True : # 正常な画像(frame)を処理する
                if video_mode == True: # 録画中
                    video_mode = m_camera.video_write( frame )
                    if video_mode == False :
                        m_camera.check_upload()
                else: # 確認中
                    if m_camera.check_motion( frame ):  # 動きの確認
                        video_mode = True
                        m_camera.video_write( frame )   # 録画開始
                # 動作確認用
                if (count % HEART_BEET_TIME) == 0 :
                    m_camera.log_print( str( count ) + "-{:.4f}".format( m_camera.moment ) )
                    if ( count % SAMPLE_OUTPUT ) == 0 :
                        m_camera.save_mask_imagefile( "check", frame ) # 撮影した画像を保存
            active_time = start_detect - dt.datetime.now()
            if( active_time.total_seconds() > MAX_REC_TIME ):
                break
        # while end
        del m_camera

except KeyboardInterrupt:
    pass
else:
    print( 'MontionMain ExceptionError:', e )
    m_camera.print_cap_status()
    del m_camera

最後に

ラズベリーパイで監視カメラを作成するものはたくさんありました。はじめは、OpneCVの顔認証モデルを使用したもので作成したのですが、検出できないことが非常に多いです。学習モデルの変更などを行うためには画像を集める必要もあります(実はこの間にいろいろ試行錯誤して時間がかかっています。なかなか進みませんでした)。
今回は、簡単な方法で変化のある動画を収集することに切り替えてトライした結果です。これらの画像を見ていて、はやり顔を検出するのはかなり難しいと思いました。
動いているとどうしても画像もぶれますし、そもそも正面をとらえる機会は大変少ないです。この辺りは、被写体が正面を向かせるような仕組みを合わせて作らないと難しいと、改めて思いました。

あと、近くの被写体の動きは意外に早いということです。ふつうにカメラ前を横切ると、とらえられるのは、最後の背中だけということもよくあります。
それでも昔に比べカメラの性能はすごく上がっていると今回思いました静止画で見る分には、夜間でも予想よりきれいな画像です。たまたま街灯が逆光になる位置なのですが、それでも肉眼で見た感じに近い静止画を得ることができます。
とはいえ暗いため動いているものは画像が流れてしまいます。シャッター速度の変更ができればとは思いましたが、暗くなってしまうだろうということもあり、あまり調べていません。
ともあれ、何らかの画像が収集できるところまで来たので、Qiitaにて書いてみることにしました。収取画像は、サーバないし連携先のPCでさらなる解析を行えばよいかなと思っています。
今は試験的に家の近くに設置していますが、実際の場所はもっと動きがない所なので少し条件を変えることや、簡単にはアクセスできない場所になるので放置可能な設定を行う予定です。

わかっている問題

  1. 今回のハード構成と標準のLinux構成では、静止画や動画の保存時に数秒のタイムラグが発生します。その間の変化はとらえることができません。
  2. グレイスケールでの差分抽出のため、背景と変化した被写体のグレイスケール時の濃度が閾値を超えていない場合には差分として得ることができません。このためにも撮影環境の配色には注意が必要です。
  3. 夜間になった際に、カメラの設定を夜間モードに変更することも当初は考えていました。それなりの画像は取れていますが、カメラ画像から暗さを見て設定を変える(どのような設定が良いかもですが)方法が必要です。
26
35
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
35