この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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