クラウド側からのコマンドで動画が取得できる監視カメラを作ってみました

2020.03.16

この記事は公開されてから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