クラウド側からのコマンドで動画が取得できる監視カメラを作ってみました
1 はじめに
CX事業本部の平内(SIN)です。
Raspberry Piで監視カメラを作成し、クラウド側からコマンドを送ることで、指定した時間の動画が取得できる仕組みを試してみました。
この仕組みの利点としては、以下のようなものが上げられます。
- リクエストされたデータだけをmp4形式で送信するので、通信帯域に負荷が少ない
- 通信帯域に負荷が少ない為、比較的解像度の高い動画が保存可能
- 動画を取得する時以外は、通信環境が不要
- デバイスにディスクを追加することで、長期間の動画保存も可能
最初に、この監視カメラをドライブレコーダーのように使用してみた例です。
「動画を取得する時以外は、通信環境が不要」という特徴を生かして、雑ですが、車のフロントガラスの手前にシガラーターから電源を取って監視カメラとして動作させています。動画は、Wi-Fi環境が利用可能な所で、コマンドを送って取得しています。
2 構成
構成は、以下の通りです。
- Raspberry Piには、Webカメラが接続されており、OpenCVを使用して、撮影したフレームを全てディスクに保存しています。(今回は、5fpsとなっています)( ① )
- クラウド側からMQTTで取得した時間を指定してコマンドを送信します。図中では、Lambdaから送信しているようになっていますが、これは、何でも構いません。( ② )
- Raspberry Pi では、TopicをSubscribeしており、MQTTでコマンドを受信します。( ③ )
- コマンド(開始時間+撮影時間)に応じた動画を、保存されているフレーム画像から組み立て、S3に送信します。( ④ )
- Raspberry Piで動作するSDKへのパーミッション(AWS IoT及び、S3)は、CognitoのIdentity Poolsで付与されています。( ⑤ )
実は、当初、GStreamerやffmpegで数秒単位の動画を保存して、送信時にそれを連結する仕組みを考えたのですが、どうしても、連結部分の切れ目が目立つので諦めました。 これは、撮影開始時にカメラがピント合わせに少し時間を要するのと、明暗度の変化が原因だと思います。 また、動画を連結する場合、ファイル競合から、撮影中の時間帯の動画を取得できない問題もあり、今回のように、フレーム画像を保存する仕組みにしました。
3 環境
使用した環境は、以下のとおりです。
(1) Raspberry Pi
Raspberry Piは、Model 3B+ で、OSは、2月の最新版 (Raspbian Buster with desktop and recommended software) 2020-02-13-raspbian-buster-full.imgです。
$ cat /proc/cpuinfo | grep Revision Revision : a32082
$ lsb_release -a No LSB modules are available. Distributor ID: Raspbian Description: Raspbian GNU/Linux 10 (buster) Release: 10 Codename: buster
$ uname -a Linux raspberrypi 4.19.97-v7+ #1294 SMP Thu Jan 30 13:15:58 GMT 2020 armv7l GNU/Linux
タイムゾーンは、日本時間に変えました
$ sudo raspi-config
4 Localisation Options -> I2 Change Timezone -> Asia -> Tokyo
(2) Webカメラ
Logicool HD Pro Webcam C920
$ lsusb Bus 001 Device 004: ID 046d:0892 Logitech, Inc. OrbiCam
C920は、各種のフォーマットで出力可能ですが、今回は、1920×1080 5fpsで利用しました。
$ v4l2-ctl -d /dev/video0 --list-formats-ext (略) [0]: 'YUYV' (YUYV 4:2:2) Size: Discrete 1920x1080 Interval: Discrete 0.200s (5.000 fps)
4 OpenCV
「フレーム画像の保存」及び、「連結して動画作成」する作業は、OpenCVを使用しています。
上記の Raspbianイメージで、Python3からOpenCVを利用するために行った手順は以下の通りです。
モジュールのインストール
$ sudo apt-get update && sudo apt-get upgrade $ sudo apt-get install libhdf5-dev libhdf5-serial-dev libhdf5-103 $ sudo apt-get install libqtgui4 libqtwebkit4 libqt4-test python3-pyqt5 $ sudo apt-get install libatlas-base-dev libjasper-dev $ sudo pip3 install opencv-python $ sudo pip3 install opencv-contrib-python
環境変数
$ export LD_PRELOAD=/usr/lib/arm-linux-gnueabihf/libatomic.so.1
動作確認
$ python3 Python 3.7.3 (default, Dec 20 2019, 18:57:59) [GCC 8.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import cv2 >>>
5 フレーム画像保存
フレーム画像を保存しているコードです。
OpenCVでカメラからのフレーム毎の画像を、タイムスタンプのファイル名で保存しているだけです。結合する際に使用するための解像度等のフォーマットを記録(format.txt)しています。
#!/usr/bin/env python3 # coding: UTF-8 import cv2 import numpy as np import datetime import time import sys useMonitor = False # dataPath dataPath = './data' formatFile = "{}/format.txt".format(dataPath) print("formatFile:{}".format(formatFile)) cap = cv2.VideoCapture(0) ## 使用する解像度を指定する # 1フレーム 約380K 1秒で 約1.9M 1分で約114M 1時間で約6.8G 1日で約164G cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080) cap.set(cv2.CAP_PROP_FPS, 5) time.sleep(2) # フレームを表示する def show(frame, width, height, magnification): if(useMonitor==True): frame = cv2.resize(frame , (int(width * magnification), int(height * magnification))) cv2.imshow('frame', frame) cv2.waitKey(1) # 日時を付加 def setDateTime(dt, frame, width, height): # フォント font = cv2.FONT_HERSHEY_SIMPLEX; thickness = 2 fontSize = height * 0.002 dateStr = "{0:%Y-%m-%d %H:%M:%S}".format(dt) (w,h) = cv2.getTextSize(dateStr, font, fontSize, thickness)[0] cv2.putText(frame, dateStr, (int((width-w)/2),int(height-h-2)), font, fontSize,(255,255,255), thickness, lineType=cv2.LINE_8) return frame def main(): # 実際に利用できている解像度を取得する ret, frame = cap.read() fps = cap.get(cv2.CAP_PROP_FPS) height, width, channels = frame.shape[:3] print("width:{},height:{},fps:{}".format(width, height, fps)) # フォーマットを設定フィアルに記録する with open(formatFile, mode='w') as f: f.write("{},{},{}".format(width,height,fps)) # モニターの表示倍率 magnification = 0.5 while True: # フレーム取得 ret, frame = cap.read() # 日時取得 dt = datetime.datetime.now() timestamp = dt.timestamp() # 日時表示 frame = setDateTime(dt, frame, width, height) # ファイル名 filename = "{}/{}.jpg".format(dataPath, timestamp) date = dt_utc_aware = datetime.datetime.fromtimestamp(timestamp) cv2.imwrite(filename, frame) # モニター show(frame, width, height, magnification) cap.release() cv2.destroyAllWindows() main()
6 動画生成
動画を生成するコードです。
指定された開始時間(startTime)及び、終了時間(endTime)の間のフレーム画像を全部まとめてmp4を生成しています。
import glob import os import cv2 class Mp4(): def __init__(self, dataPath): self.dataPath = dataPath def create(self, startTime, endTime, fileName): print("start:{}".format(startTime)) print("end:{}".format(endTime)) s = startTime.timestamp() e = endTime.timestamp() formatFile = "{}/format.txt".format(self.dataPath) (width, height, fps) = self._getFormat(formatFile) print("width={} heighth={} fps={}".format(width,height,fps)) list = [] for t in self._getTimestampList(self.dataPath): if(startTime.timestamp() < t and t < endTime.timestamp()): list.append(t) # 動画生成 fourcc = cv2.VideoWriter_fourcc('m','p','4','v') video = cv2.VideoWriter(fileName, fourcc, fps, (width, height)) for t in list: file = '{}/{}.jpg'.format(self.dataPath, t) print(file) img = cv2.imread(file) video.write(img) video.release() def _getFormat(self, filaName): with open(filaName, mode='r') as f: w,h,f = f.read().split(',') width = int(w) height = int(h) fps = int(float(f)) return (width, height, fps) # タイムスタンプ一覧取得 def _getTimestampList(self, path): list = [] for path in glob.glob("{}/*.jpg".format(path)): fileName = os.path.split(path)[1] # ファイル名取得 t = fileName.replace('.jpg', '') # 拡張子削除 list.append(float(t)) list.sort() return list
7 サーバ
コマンドを受け付けるためのサーバ機能は、以下のようになっています。
MQTTでトピックをSubscribeし、到着したコマンドに基づいて、動画を生成してS3に送信しています。
$ pip3 install boto3 $ pip3 install AWSIoTPythonSDK $ pip3 install python-box
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import time import datetime as dt from datetime import timedelta from box import Box import json import datetime import Mqtt import Mp4 import S3 identityPoolId = 'ap-northeast-1:xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx' endPoint = "xxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com" clientId = "myClientId" topic = "monitorCameraTopic" # dataPath dataPath = './data' print("dataPath:{}".format(dataPath)) # root-CA rootCA = './root-CA.crt' print("rootCA={}".format(rootCA)) # Bucket bucketName = "monitoring-camera-data" def onSubscribe(message): command = Box(json.loads(str(message, 'utf-8'))) startTime = datetime.datetime.strptime(command.startTime, "%Y/%m/%d %H:%M:%S") seconds = command.seconds endTime = startTime + timedelta(seconds=seconds) fileName = "/tmp/output.mp4" mp4 = Mp4.Mp4(dataPath) mp4.create(startTime, endTime, fileName) print("{} created.".format(fileName)) s3 = S3.S3(identityPoolId) key = "{}.mp4".format(startTime) s3.putObject(bucketName, key, fileName) def main(): mqtt = Mqtt.Mqtt(identityPoolId, endPoint, clientId, topic, rootCA ,onSubscribe) mqtt.connect() while(True): time.sleep(0.5) main()
8 systemd
動画の生成には、少し時間が必要であり、その間のフレームの保存に影響が少ないようにと、2つのsystemdで起動させました。
- record.py (フレーム画像の保存)
- server.py(サーバ機能 コマンド受付+動画生成+動画送信)
(1) 環境設定ファイル
daemon.envというファイルを作成し、以下の通りとしました。 LD_PRELOADは、OpenCVのライブラリパス設定であり、PYTHONPATHは、Python3のライブラリ設定です。
/home/pi/MonitoringCamera/daemon.env
LD_PRELOAD="/usr/lib/arm-linux-gnueabihf/libatomic.so.1" PYTHONPATH=/usr/lib/python37.zip;/usr/lib/python3.7:/usr/lib/python3.7/lib-dynload:/home/pi/.local/lib/python3.7/site-packages:/usr/local/lib/python3.7/dist-packages:/usr/lib/python3/dist-packages
デーモン起動した場合に、Python3のライブラリへのパスが不十分になるようなので、下記の要領で、コマンドラインから実行した場合のパスを確認し、全部、:で区切って設定しました。
$ python3 >>> import sys >>> import pprint >>> pprint.pprint(sys.path) ['', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/home/pi/.local/lib/python3.7/site-packages', '/usr/local/lib/python3.7/dist-packages', '/usr/lib/python3/dist-packages'] >>>
(2) サービス設定
起動するプログラムに実行権限を追加します。
$ chmod 755 server.py $ chmod 755 record.py
.serviceの設定は、以下のとおりです。
$ sudo vi /etc/systemd/system/monitor-camera-server.service
[Unit] Description=MonotorCameraServer [Service] WorkingDirectory=/home/pi/MonitoringCamera EnvironmentFile=/home/pi/MonitoringCamera/daemon.env ExecStart=/home/pi/MonitoringCamera/server.py [Install] WantedBy=multi-user.target
$ sudo vi /etc/systemd/system/monitor-camera-record.service
[Unit] Description=MonotorCameraRecord [Service] WorkingDirectory=/home/pi/MonitoringCamera EnvironmentFile=/home/pi/MonitoringCamera/daemon.env ExecStart=/home/pi/MonitoringCamera/record.py [Install] WantedBy=multi-user.target
(3) 有効化
有効化します。
$ sudo systemctl enable monitor-camera-server.service $ sudo systemctl enable monitor-camera-record.service
(4) 動作確認
スタート
$ sudo systemctl start monitor-camera-server.service $ sudo systemctl start monitor-camera-record.service
確認
$ sudo systemctl status monitor-camera-server.service ● monitor-camera-server.service - MonotorCameraServer Loaded: loaded (/etc/systemd/system/monitor-camera-server.service; enabled; vendor preset: enabled) Active: active (running) since Sun 2020-03-15 13:39:55 JST; 5s ago Main PID: 1412 (python3) Tasks: 4 (limit: 2200) Memory: 25.7M CGroup: /system.slice/monitor-camera-server.service └─1412 python3 /home/pi/MonitoringCamera/server.py Mar 15 13:39:55 raspberrypi systemd[1]: Started MonotorCameraServer. $ sudo systemctl status monitor-camera-record.service ● monitor-camera-record.service - MonotorCameraRecord Loaded: loaded (/etc/systemd/system/monitor-camera-record.service; enabled; vendor preset: enabled) Active: active (running) since Sun 2020-03-15 13:41:40 JST; 1s ago Main PID: 1499 (python3) Tasks: 1 (limit: 2200) Memory: 13.9M CGroup: /system.slice/monitor-camera-record.service └─1499 python3 /home/pi/MonitoringCamera/record.py Mar 15 13:41:40 raspberrypi systemd[1]: Started MonotorCameraRecord.
ストップ
$ sudo systemctl stop monitor-camera-server.service $ sudo systemctl stop monitor-camera-record.service
9 Cognito
デバイス側で、MQTTのSubscribeと、S3へアップロードするパーミッションは、Cognitoのidentity poolで付与されています。
作成した identity poolに設定したロールは、以下の通りです。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "mobileanalytics:PutEvents", "cognito-sync:*" ], "Resource": "*" }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": [ "s3:PutObject" ], "Resource": [ "arn:aws:s3:::monitoring-camera-data" ] }, { "Sid": "VisualEditor2", "Effect": "Allow", "Action": [ "iot:Connect", "iot:Receive", "iot:Subscribe" ], "Resource": [ "arn:aws:iot:ap-northeast-1:439028474478:client/myClientId", "arn:aws:iot:ap-northeast-1:439028474478:topic/monitorCameraTopic", "arn:aws:iot:ap-northeast-1:439028474478:topicfilter/monitorCameraTopic" ] } ] }
10 動作確認
AWS IoTのテストから、コマンドをPublichして動作確認してみました。
上記のコマンドで、S3に動画が保存されます。
11 ディスク容量
この仕組みは、そのまま使うとデータが溜まり続けて、直ぐにディスクがいっぱいになってしまいます。
仮に保存するデータが1920×1080で5fpsだとすると、1フレーム 約380K、1秒で約1.9M、1分で約114M、1時間で約6.8G、1日で約164Gとなります。
下記は、crontabで1分毎に、1時間以上古いデータを削除しています。
長期間保存したい場合は、USBメモリとかでディスクを追加すればいいでしょう。
*/1 * * * * python3 /home/pi/MonitoringCamera/remove.py
remove.py
#!/usr/bin/env python3 # coding: UTF-8 import os import glob import datetime import time # dataPath dataPath = '/home/pi/MonitoringCamera/data' # 1時間以上前のデータを削除する hour = 1 # 指定時間より前のタイムスタンプを取得 dt = datetime.datetime.now() dt = dt - datetime.timedelta(hours=hour) timestamp = dt.timestamp() for path in glob.glob("{}/*.jpg".format(dataPath)): fileName = os.path.split(path)[1] # ファイル名取得 t = fileName.replace('.jpg', '') # 拡張子削除 # 指定時間より前のデータを削除する if(float(t) < timestamp): print("delete {}".format(path)) os.remove(path)
12 最後に
今回は、クラウド側からコマンドを送ることで、指定した時間の動画が取得できる仕組みを試してみました。
Amazon Rekognitionで動画を分析する場合、動画ファイルがS3に配置されている事が必要ですが、今回作成した仕組みであれば、そのまま利用可能だと思います。 また、機械学習等での分析では、ある程度の動画の解像度が必要になる場合がありますが、それにも適した形ではないかと考えています。
全てのコードは、下記に置きました。
https://github.com/furuya02/MonitoringCamera