🐈

AWS IoTとRaspberryPiを使ってタイムラプス監視カメラを作ろう!

2022/12/18に公開

この記事はLAPRAS Advent Calendar 2022の18日目の記事です。

https://qiita.com/advent-calendar/2022/lapras


こんにちは。今年7月からLAPRAS株式会社でSREをしているnappaです。

何を記事にするか色々悩みましたが、考えた末にRaspberryPiネタにすることにしました。

SRっぽさには欠けますが、ずーっと家で埃をかぶっていたRaspberryPiがあまりにも不憫だったもので・・・ご容赦下さい。

ちなみに他の候補としては「ファミコンのROMファイルの中身を見てみよう」「急に会社都合で退職しても慌てないためのトリセツ」なんてものも考えていたのですが、それらはまた別の機会にしようかと思います。

作るもの

今回作成するのはタイトルの通り監視カメラです。

お子さんやペットのいるご家庭であれば、ご自身が不在の際になにか問題がないか確認したいという欲求をお持ちですよね(多分)

それを叶える監視カメラ、いわゆる見守りカメラをお持ちの方は多いと思いますが、主機能としては下記の2機能が一般的でしょう。

  • 「今何しているか」を知るためリアルタイム映像を外部から確認することができる
  • 「何かが起こった」を知るための動体検知による通知が行われる

これをRaspberryPiで再現してもあまり面白みはないので、今回作成するのはタイムラプスを用いて「普段の様子」をざっくり、定期的にチェックできるようにするタイムラプス監視カメラです。

こちらがサンプル映像になります。

https://youtu.be/AnID7tb1I-I

わが家ではこたつの中に猫用のハンモックを設置し、出入りしやすいようにダンボールでトンネルを設置しています。この時はこたつ布団を片側だけめくり、1時間ほど撮影に協力してもらいました。

こんな感じのかわいい映像が1時間毎にSlack通知されるカメラ。どうでしょう?録画映像を全部チェックするのは大変ですし、サクッとざっくり見れるのは便利じゃないでしょうか。

ちなみにタイムラプス機能が搭載されている見守りカメラは市販されているので、そんなのやってられるか!って方はそちらをお買い求めください。

https://panasonic.jp/hns/products/hbc200.html

用意するもの

  • RaspberryPi 3 モデルB以降
  • なにか適当なS3バケット
    • 撮影した画像のアップロード先
  • Slack通知用のTokenと通知先のチャンネルID
  • Webカメラ or カメラモジュール
  • 被写体

今回の被写体は我が家の猫2匹に出演をお願いしました。

RaspberryPiの熱で暖をとっていますね。これだけでRaspberryPiも本望かもしれません。

構成

  1. AWS IoTで管理されたRaspberryPiが定期的にWebカメラで写真を撮影
  2. AWS IoTでメッセージをパブリッシュ
  3. AWS IoTのルールアクションで、パブリッシュされたメッセージをS3に保存
  4. EventBridgeSchedulerで定期的に実行されるLambdaがタイムラプス映像とSlack通知を実行

といったものになっています。

AWS IoTの設定

AWS IoTはIoTデバイスの認証認可等を含む管理、およびAWSの各種サービスと接続するマネージドサービスです。

今回はその中でも中核サービスであり、IoTデバイスとのセキュアな通信を行うためのAWS IoT Coreを使用してRaspberryPiからカメラの画像を受け取ります。IoTデバイスとの通信プロトコルは複数サポートされていますが、軽量なMQTTプロトコルを利用して構築したいと思います。

  • 論理エンティティにあたる「モノ」
  • AWS IoTに接続する際の認証に使用する「証明書」
  • 証明書に対する認可にあたる「ポリシー」
  • 証明書を持つデバイスからパブリッシュされた内容を処理する「ルール」

これらを順に作っていきたいと思います。

ポリシーの作成

証明書の制御を行うポリシーの作成を行います。聞き馴染みのあるIAMポリシーとは別物で、AWS IoTサービス下で設定、管理を行うものになります。

ではまず、AWSマネジメントコンソールからAWS IoTを開き、サイドメニューで「セキュリティ / ポリシー」から「ポリシーの作成」を選択しましょう。

適当なポリシー名を設定し、ポリシードキュメントで iot::Connect / iot:Publish の2点を追加してください。ポリシーリソースはそれぞれ下記の値になります。

ポリシーアクション ポリシーリソース
iot::Connect arn:aws:iot:{region}:{AWS-account-ID}:client/cat-camera
iot::Publish arn:aws:iot:{region}:{AWS-account-ID}:topic/capture

この設定により、 cat-camera というクライアントIDからのみ接続を許可し、capture というトピックに対するパブリッシュだけを許可する。という必要最低限なポリシーが出来上がります。

モノの作成

次は物理的なIoTデバイスをAWS IoTで管理するための論理エンティティである「モノ」を作成します。(Internet of ThingsのThingsにあたるものですが、日本語だとちょっとダサく感じるのは私だけでしょうか・・・)

サイドメニューの「管理 / すべてのデバイス / モノ」から「モノの作成」を選択しましょう。

単一のデバイスなので「1つのモノを作成」のまま「次へ」

「モノの名前」に cat-camera など好きな名前を入力し「次へ」
名前以外の項目はデフォルトでOKです。

デバイス証明書は「新しい証明書を自動生成」を選択します。
もし手持ちの証明書を使用する場合は「自分の証明書を使用」を選んでください。

先程作成したポリシーが表示されるはずなので、それを選択してモノを作成しましょう。

モノが作成されると証明書が自動作成されます。

  • デバイス証明書
  • パブリックキーファイル
  • プライベートキーファイル
  • ルートCA証明書

これらをそれぞれダウンロードしておきましょう。

これで必要な権限を持った証明書が紐付けられたモノ、が完成になります。

ルールの作成

AWS IoTは各デバイスからパブリッシュされた情報をSQLベースで取得し、各サービスに接続するルーティング機能が用意されています。接続先はAWSだけでなくHTTPSプロトコルのエンドポイントを指定することもできますが、今回はS3のバケットにデータを保存するシンプルなルールを作成します。

サイドメニューの「メッセージのルーティング / ルール」から「ルールの作成」を選択しましょう。

ルール名は適当でOKです。ハイフンは使えないのにご注意下さい。

SQLステートメントでは、先程ポリシーで指定した capture というトピックを取得する下記のSQLを入力しましょう。特にフィルタするわけではないのでWHERE句などは不要です。

SELECT * FROM 'capture'

最後に保存するバケットやキーを指定します。バケットは適当に用意したものを選択し、キーには captured/${timestamp()}.jpg を入力します。

またバケットに書き込む権限が必要になるので、「新しいロールを作成」から適当なロールを作成しましょう。自動的に選択したS3バケットに対する s3:PutObject 権限が許可されたポリシーがアタッチされたロールが作成されます。

入力後に次ページで内容の確認を行い、問題なければ「作成」して下さい。

これでAWS IoTに関する設定は終了になります。

RaspberryPiの設定

ここからはRaspberryPi上での設定作業になります。

OpenCVを使ってWebカメラから静止画を取得し、AWS IoTにパブリッシュするPythonスクリプトのサービスを作成するところまで行います。

OpenCV / AWS IoT SDKを使用する準備

まずは各パッケージを使用するために必要なものを整えていきます。

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install cmake libssl-dev git libatlas3-base
$ python3 -m pip install awsiotsdk opencv-python

これで OpenCV / AWS IoT SDKを使う準備が整いました。

Pythonスクリプトの配置

スクリプトはホームディレクトリ直下に cat-camera というディレクトリを切り、その中に配置します。
最終的なファイル構成は下記のとおりです。

.
├── cat-camera.py
└── certs
    ├── AmazonRootCA1.pem
    ├── certificate.pem.crt
    └── private.pem.key

まずはディレクトリを作成しましょう。

$ mkdir -p ~/cat-camera/certs

次にダウンロードしておいた証明書を手元のPCからRaspberryPi上に配置します。

# 手元のマシンから送信
$ scp xxxxxxxx-AmazonRootCA1.pem pi@{RaspberryPiのホスト}:~/cat-camera/certs/AmazonRootCA1.pem
$ scp xxxxxxxx-private.pem.key pi@{RaspberryPiのホスト}:~/cat-camera/certs/private.pem.key
$ scp xxxxxxxx-certificate.pem.crt pi@{RaspberryPiのホスト}:~/cat-camera/certs/certificate.pem.crt

最後にスクリプトを配置します。スクリプトの内容は下記のとおりです。

from awsiot import mqtt_connection_builder
from datetime import datetime
import awscrt
import cv2
import time

ENDPOINT = 'xxxxxxx'
CLIENT_ID = 'cat-camera'
CERT_FILEPATH = '/home/pi/cat-camera/certs/certificate.pem.crt'
PRI_KEY_FILEPATH = '/home/pi/cat-camera/certs/private.pem.key'
CA_FILEPATH = '/home/pi/cat-camera/certs/AmazonRootCA1.pem'
TOPIC_NAME = 'capture'

IMAGE_WIDTH_SIZE = 640
IMAGE_HEIGHT_SIZE = 480

CAPTURE_INTERVAL_SEC = 30


def main():
    # AWS IoTへの接続
    print('Begin Connect')
    mqtt_connection = mqtt_connection_builder.mtls_from_path(
        endpoint=ENDPOINT,
        port=8883,
        client_id=CLIENT_ID,
        cert_filepath=CERT_FILEPATH,
        pri_key_filepath=PRI_KEY_FILEPATH,
        ca_filepath=CA_FILEPATH,
        keep_alive_secs=30)

    connect_future = mqtt_connection.connect()
    connect_future.result()
    print('Connected!')

    # カメラのキャプチャ設定
    capture = cv2.VideoCapture(0)
    capture.set(cv2.CAP_PROP_FRAME_WIDTH, IMAGE_WIDTH_SIZE)
    capture.set(cv2.CAP_PROP_FRAME_HEIGHT, IMAGE_HEIGHT_SIZE)

    while True:
        # キャプチャ実行
        ret, frame = capture.read()

        if ret:
            # 画像をMQTTで送信
            image_bytes = cv2.imencode('.jpg', frame)[1].tobytes()

            mqtt_connection.publish(
              topic=TOPIC_NAME,
              payload=image_bytes,
              qos=awscrt.mqtt.QoS.AT_LEAST_ONCE)

            d = datetime.now().strftime('%Y/%m/%d %H:%M:%S')
            print(d, 'Published!')

        time.sleep(CAPTURE_INTERVAL_SEC)


if __name__ == '__main__':
    main()

カメラの画像をキャプチャし、 CAPTURE_INTERVAL_SEC 秒ごとにパブリッシュするだけのシンプルなものです。

7行目の ENDPOINT = 'xxxxxxx' の値はAWSマネジメントコンソールのAWS IoTサイドメニュー、設定にある「デバイスデータエンドポイント」に記載があるのでその値を使用して下さい。

エンドポイントの値を変更したらスクリプトをRaspberryPi上に作成しましょう。

$ vi ~/cat-camera/cat-camera.py

最後にスクリプトを実行しパブリッシュが成功しているかを確認して下さい。

$ python3 ~/cat-camera/cat-camera.py

正しく設定できていれば、しばらくするとS3バケット上にキャプチャされた画像が保存されているはずです。

Pythonスクリプトのサービス化

監視カメラというからには常時稼働してもらいたいものです。安易に nohup で起動しておくとちょっとした停電などで再起動すると終わりなので、ちゃんとサービス化してあげましょう。

こちらのユニットファイルを /etc/systemd/system/cat-camera.service として作成します。

$ sudo vi /etc/systemd/system/cat-camera.service
[Unit]
Description = cat camera daemon

[Service]
ExecStart = /usr/bin/python3 -u /home/pi/cat-camera/cat-camera.py
Restart = always
Type = simple
User=pi

[Install]
WantedBy = multi-user.target

実行権限の付与、ユニットファイルの作成、サービスの有効化と起動を行います。

$ chmod 755 ~/cat-camera/cat-camera.py
$ sudo systemctl enable cat-camera
$ sudo systemctl start cat-camera

最後にステータスを確認してみて、問題なく動いていることが確認できれば完了です。

$ sudo systemctl status cat-camera -l
● cat-camera.service - cat camera daemon
     Loaded: loaded (/etc/systemd/system/cat-camera.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-12-16 08:46:06 JST; 8s ago
   Main PID: 3495 (python3)
      Tasks: 7 (limit: 1596)
        CPU: 1.928s
     CGroup: /system.slice/cat-camera.service
             └─3495 /usr/bin/python3 -u /home/pi/cat-camera/cat-camera.py

Dec 16 08:46:06 raspberrypi systemd[1]: Started cat camera daemon.
Dec 16 08:46:07 raspberrypi python3[3495]: Begin Connect
Dec 16 08:46:08 raspberrypi python3[3495]: Connected!

タイムラプス映像を作成、通知するLambdaの作成

面倒な手順が続きましたが、最後にメインとなるタイムラプス映像を作成するLambdaの作成を行います。

タイムラプス映像の作成には opencv-python-headless を使います。標準ライブラリ以外をLambdaで使用する場合、使用するライブラリをLambdaのコードと合わせてアップロードが必要になりますが、Serverless Frameworkを使えばそのあたりを簡単に対応してくれます。

Serverless Frameworkのプロジェクト作成

Serverless Frameworkをインストールし、プロジェクトの作成を行いましょう。

$ npm install --location=global serverless

$ serverless --version
Framework Core: 3.25.1
Plugin: 6.2.2
SDK: 4.3.2

$ serverless create --template aws-python3 --path cat-camera-serverless

次に requirements.txt をベースにライブラリを事前にまとめてくれる serverless-python-requirements をインストールします。

$ cd cat-camera-serverless
$ serverless plugin install -n serverless-python-requirements

使用するライブラリを requirements.txt に追加

OpenCVやSlackSDKなどを使用するため、requirements.txt に記載しておきます。

$ vi requirements.txt

opencv-python-headless==4.6.0.66
numpy==1.23.5
slack-sdk==3.19.5

Lambdaで実行するスクリプトの作成

AWS IoT経由でS3に保存されたキャプチャ画像をLambda内に取得し後タイムラプス画像に変換し、変換したファイルをS3上に保存 & Slackで通知する簡単なスクリプトです。

バケット名やSlack用のTokenは環境変数経由で使用するので、基本的にスクリプトはそのまま使用可能な状態になっています。

Serverless Frameworkプロジェクト内にある handler.py をこのスクリプトの内容で上書きしましょう。

$ vi handler.py
import boto3
import cv2
import os
import glob
import datetime
import shutil
from slack_sdk import WebClient

FRAME_SIZE = 2
FRAME_RATE = 10

WIDTH = 640
HEIGHT = 360


def notification_to_slack(video_path: str):
    slack_token = os.environ.get('SLACK_TOKEN')
    channel_id = os.environ.get('SLACK_CHANNEL_ID')

    client = WebClient(slack_token)
    client.files_upload_v2(
        channel=channel_id,
        file=video_path
    )


def timelapse(event, context):
    bucket_name = os.environ.get('BUCKET_NAME')
    bucket = boto3.resource('s3').Bucket(bucket_name)

    objects = list(bucket.objects.filter(Prefix='captured/'))

    if len(objects) == 0:
        print('No captured images')
        return

    print('Begin download captured images')
    event_key = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    work_dir = f'/tmp/{event_key}'

    print(f'Create work directory: {work_dir}')
    os.mkdir(work_dir)

    for obj in objects:
        print(f'Download: {obj.key}')
        file_name = os.path.basename(obj.key)
        output_path = f'{work_dir}/{file_name}'
        bucket.download_file(obj.key, output_path)
    print('Finished download captured images')

    print('Begin create timelapse video')
    image_paths = sorted(glob.glob(f'{work_dir}/*.jpg'))
    print(f'Total image count: {len(image_paths)}')

    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    video_file_path = f'{work_dir}/timelapse.mp4'
    video = cv2.VideoWriter(video_file_path, fourcc, FRAME_RATE, (WIDTH, HEIGHT))
    print('converting...')

    for path in image_paths:
        img = cv2.imread(path)
        img = cv2.resize(img, (WIDTH, HEIGHT))
        video.write(img)

    video.release()
    print('Finished create timelapse video')

    print('Upload timelapse video')
    bucket.put_object(Key=f'converted/{event_key}.mp4', Body=open(video_file_path, 'rb'))

    print('Timelapse video send to Slack')
    notification_to_slack(video_file_path)

    print('Begin delete captured images & work directory')

    for obj in objects:
        print(f'Delete: {obj.key}')
        obj.delete()

    print(f'Delete work directory: {work_dir}')
    shutil.rmtree(work_dir)

    print('Finished delete captured images & work directory')

Serverless Frameworkの設定

設定ファイルを変更します。

ランタイムがPythonであることは変わりませんが、LambdaへS3への操作を許可するIAMの設定、1時間毎に実行するスケジュール設定、プラグインの設定などを追加してあります。

Serverless Frameworkプロジェクト内にある serverless.yml をこの設定ファイルの内容で上書きします。

$ vi serverless.yml
service: cat-camera-serverless

frameworkVersion: '3'

provider:
  name: aws
  runtime: python3.9
  region: ap-northeast-1
  timeout: 180
  environment:
    BUCKET_NAME: ${env:BUCKET_NAME}
    SLACK_TOKEN: ${env:SLACK_TOKEN}
    SLACK_CHANNEL_ID: ${env:SLACK_CHANNEL_ID}

  iam:
    role:
      statements:
        - Effect: Allow
          Action:
              - s3:PutObject
              - s3:GetObject
              - s3:DeleteObject
              - s3:ListBucket
          Resource:
            - arn:aws:s3:::${self:provider.environment.BUCKET_NAME}
            - arn:aws:s3:::${self:provider.environment.BUCKET_NAME}/*
        - Effect: Allow
          Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
          Resource:
            - '*'

functions:
  hello:
    handler: handler.timelapse
    events:
      - schedule: cron(0 */1 * * ? *)

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    dockerizePip: non-linux
    noDeploy: []

デプロイ

これで準備は整いました。

環境変数を設定し、デプロイを行いましょう。

# 画像のアップロードに使用するバケット名
$ export BUCKET_NAME=nxxxxxx

# SlackAppのBot User用Token
# ※chat:write/ files:read / files:writeの権限を付与したトークンを使用して下さい
$ export SLACK_TOKEN=xxxxxxxxx

# 通知先チャンネルのID
$ export SLACK_CHANNEL_ID=xxxxxxx

$ serverless deploy

動作確認

少し駆け足気味になってしまいましたが、すべて正しく設定されていれば下記のように毎時0分にタイムラプス映像がSlackに通知されるはずです。

可愛い姿が見れましたね。お疲れさまです!

おわりに

手順は少々面倒ですが、アクセスキーを使わず必要最低限の権限でタイムラプス監視カメラを作ることができました。

仮にデバイスが盗難されても被害は最小限にできますし、AWS IoTのセキュリティ機能でアラームを作成したりと色々な事ができるようになります。セキュリティ面以外にも、複数の監視カメラの映像をLambda上でまとめてタイムラプス化してみたり、画像のキャプチャではなく映像自体を Kinesis Data Streams と連携して手を加えたりと、AWSの豊富なサービスを使えるので夢が広がりますね。

明日は弊社CTOの @rocky が痩せた記録に関してなにかを書くようですね。自分もダイエットしなければいけない体型をしているので興味津々です。

https://qiita.com/advent-calendar/2022/lapras

Discussion