AWS IoT Events に筋トレのトレーナーをやってもらおう (スクワット編)

2022-11-02
日常で楽しむクラウドテクノロジー

Author : 井上 昌幸

こんにちは。ソリューションアーキテクトの井上です。

聞くところによると、スクワットを一回した時の消費カロリーはだいたい 0.4 Kcal らしいです。ということは、ざっくり 350 回やれば、その後のビール一缶分 (140 Kcal) のカロリーをチャラにできるはずです ! ただ、人間は一人だと途中でサボったり、疲れてくるとなんかフォームがテキトーになって回数の割には効果がでなかったりしてくるものです。

今回はそんな状況をなんとかするため、AWS IoT Events にパーソナルトレーナーをやってもらおうという企画です。

動画のように、WebCam のライブ映像から骨格情報を検知して、AWS IoT Events に渡します。AWS IoT Events を使えばノーコードかつイベントドリブンに状態遷移を管理できます。

今回の場合だと骨格情報を元に、リアルタイムに筋トレの回数を数えたり、消費カロリーを計算したり、フォームの崩れを通知してもらったりします。ついでに筋トレの開始や回数のリセットの合図などもカメラ経由にジェスチャーで指示できるようにしてみます。

ご注意

本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。

*ハンズオン記事およびソースコードにおける免責事項 »

このクラウドレシピ (ハンズオン記事) を無料でお試しいただけます »

毎月提供されるクラウドレシピのアップデート情報とともに、クレジットコードを受け取ることができます。 


1. 全体構成

出来上がりは以下のようになる予定です。シンプルかつ IoT サービスに愛を感じる構成ですね。

  1. Jetson Nano と WebCam を使って筋トレの動画を撮り、Jetson の GPU に頑張ってもらって骨格検知まで行います。
  2. 各関節の XY 座標を抽出して AWS IoT Core を経由して AWS IoT Events へ。ここで事前に定義した状態遷移図に基づいて「スクワットのためにしゃがんでいる」とか「手を上げたので回数をリセット」のように座標から状態とイベントを決めます。
  3. MQTT over WebSocketとWebプッシュ通知を使って状態遷移を通知します。

エッジ側の GPU でライブ動画に対する推論 (骨格検知) という重めの処理をした上で、クラウド側では状態認識に必要なテキストの XY 座標のみを扱うアーキテクチャになっています。

図 1 : 全体のアーキテクチャ図

アプリケーションのサンプル画面と通知のイメージです。

図 2 : 今回作成するアプリケーションのサンプル画面と通知
(クリックすると拡大します)

お、いいんじゃない ? と思ったら、実際に作っていきましょう。


2. エッジデバイスの設定

2-1. ハードウェア

今回のエッジのハードウェア構成は以下の二つです。

エッジデバイス
  • Jetson Nano 4GB - Jetpack 4.6 [L4T 32.6.1]
USB カメラ
  • WebCam - Logitech C600

2-2. trt_pose のインストール

オープンソースの骨格検知だと OpenPose が有名ですが、今回は学習済みモデルがあり、OpenPose よりさらに簡単に設定できそうな trt_pose を使いました。インストール方法は基本的に GitHub の Readme に記載されている通りですが、NVIDIA のコンテナイメージを使うと ML 関連のライブラリが含まれているので楽です。コンテナでのインストール方法は以下を参照してください。

NVIDIAのサイトにある Machine Learning Container for Jetson and JetPack イメージ をベースに使います。JetPack のバージョンごとにコンテナが用意されているので、対応するものを選びます。今回は l4t-ml:r32.6.1-py3 を使用します。

sudo docker pull nvcr.io/nvidia/l4t-ml:r32.6.1-py3

コンテナを作成します。

sudo docker create \
    -it \
    --name pose \
    --gpus all \
    --network host \
    -e DISPLAY=$DISPLAY \
    -v /tmp/.X11-unix/:/tmp/.X11-unix \
    --device /dev/video1:/dev/video1:mwr \
    -v ~/work:/work \
    nvcr.io/nvidia/l4t-ml:r32.6.1-py3

コンテナを起動します。

sudo docker start -i pose
allow 10 sec for JupyterLab to start @ http://192.168.68.111:8888 (password nvidia)
JupterLab logging location:  /var/log/jupyter.log  (inside the container)

コンテナ内にいるはずなので、そのまま作業ディレクトリ /work へ移動します。

JetCam をインストールします。

git clone https://github.com/NVIDIA-AI-IOT/jetcam
cd jetcam/
python3 setup.py install

torch2trt をインストールします。

git clone https://github.com/NVIDIA-AI-IOT/torch2trt
cd torch2trt
python3 setup.py install --plugins

関連するパッケージのインストールを行います。

pip3 install tqdm cython pycocotools

trt_pose をインストールします。

cd /work
git clone https://github.com/NVIDIA-AI-IOT/trt_pose
cd trt_pose
python3 setup.py install

Jupyter の拡張インストールを行います。

pip3 install ipywidgets
jupyter nbextension enable --py widgetsnbextension
jupyter labextension install @jupyter-widgets/jupyterlab-manager

ここは数分かかりました。インストールが終わったらコンテナを停止して、Jetson をリスタート、再度コンテナを起動します。ここで後で Jupyter notebook にログインするときに使うパスワードが表示されるのでメモっておきましょう。

ここまで終わったら、モデルを配置します。Readme の Step3 にあるモデル resnet18_baseline_att_224x224_A のファイル resnet18_baseline_att_224x224_A_epoch_249.pth/work/trt_pose/tasks/human_pose に置きます。

2-3. trt_pose の設定

インストールが終わったら骨格の検知結果をログ person.log に吐くように数行だけコードを変更します。変更対象のファイルはコンテナの中からだと /work/trt_pose/trt_pose/draw_objects.py です。変更点は多くないですが、元のファイルを以下のコードで置き換えるとよいでしょう。

import cv2
import json
import datetime
import time

table = _{_ 1: "nose", 2: "left_eye", 3: "right_eye", 4: "left_ear", 5: "right_ear", 6: "left_shoulder", 7: "right_should er", 8: "left_elbow", 9: "right_elbow", 10: "left_wrist", 11: "right_wrist", 12: "left_hip", 13: "right_hip", 14: "left _knee", 15: "right_knee", 16: "left_ankle", 17: "right_ankle", 18: "neck"_}_

class DrawObjects(object):
    
    def __init__(self, topology):
        self.topology = topology
        
    def __call__(self, image, object_counts, objects, normalized_peaks):
        topology = self.topology
        height = image.shape[0]
        width = image.shape[1]
        
        K = topology.shape[0]
        count = int(object_counts[0])
        K = topology.shape[0]
        for i in range(count):
            color = (0, 255, 0)
            obj = objects[0][i]
            C = obj.shape[0]

            dots = {}

            for j in range(C):
                k = int(obj[j])
                if k >= 0:
                    peak = normalized_peaks[0][j][k]
                    x = round(float(peak[1]) * width)
                    y = round(float(peak[0]) * height)
                    cv2.circle(image, (x, y), 3, color, 2)

                    dots[table[j+1]] = {"x": x, "y": y}

            with open("/work/log/person.log", "a+") as f:
                ts = datetime.datetime.now().isoformat()
                tis = int(time.time())
                shape = { "ts": ts, "tis": tis, "pid": i+1, "pos": dots}
                f.write(json.dumps(shape) + "\n")

            for k in range(K):
                c_a = topology[k][2]
                c_b = topology[k][3]
                if obj[c_a] >= 0 and obj[c_b] >= 0:
                    peak0 = normalized_peaks[0][c_a][obj[c_a]]
                    peak1 = normalized_peaks[0][c_b][obj[c_b]]
                    x0 = round(float(peak0[1]) * width)
                    y0 = round(float(peak0[0]) * height)
                    x1 = round(float(peak1[1]) * width)
                    y1 = round(float(peak1[0]) * height)
                    cv2.line(image, (x0, y0), (x1, y1), color, 2)

ログ用のディレクトリ /work/log を忘れずに作っておきます。

上記の変更内容を反映させるために再度インストールします。

cd /work/trt_pose/
python3 setup.py install

2-4. 骨格検知の実行

ここで実際に trt_pose を動かしてみましょう。Jetsonで Jupyter Notebook が起動しているはずです。先ほどメモった パスワード でログインできることを確認したら、trt_pose のライブデモ用ノートブック を持ってきて実行します。

実行すると、Notebook 上にカメラの映像が表示されます。人の骨格が検知されたら、以下のようなログが出力されるはずです。

{"ts":"2022-09-19T02:36:13.929865","tis":1663554973,"pid":1,"pos":{"nose":{"x":77,"y":62},"left_eye":{"x":79,"y":59},"right_eye":{"x":75,"y":57},"left_ear":{"x":83,"y":54},"right_ear":{"x":72,"y":50},"left_shoulder":{"x":84,"y":67},"right_shoulder":{"x":58,"y":58},"left_elbow":{"x":86,"y":89},"right_elbow":{"x":48,"y":83},"left_wrist":{"x":85,"y":106},"right_wrist":{"x":53,"y":103},"left_hip":{"x":71,"y":106},"right_hip":{"x":55,"y":102},"left_knee":{"x":73,"y":139},"right_knee":{"x":57,"y":140},"left_ankle":{"x":71,"y":171},"right_ankle":{"x":57,"y":174},"neck":{"x":71,"y":63}}}

これでエッジ側の設定は完了です。あとはこのログを AWS IoT Core へ送信するのですが、その前にクラウド側の設定をしていきましょう。


3. AWS IoT Core の設定

3-1. Thing の作成

Jetson Nano を AWS IoT Core に接続するための Thing の設定です。AWS IoT Core のマネジメントコンソールから Thing を作って、MQTT で Publish するためのポリシーを追加した後、接続に必要なファイルを取得します。

  • AWS IoT Core のマネジメントコンソールから「Things」を開きます。
    • Create things」ボタンを選択します。
    • Create single thing」を選択します。
    • Thing properties の Thing name を jetson とし、残りはデフォルトのままで 「Next」ボタンを選択します。

  • Configure device certificate - optional 画面が表示されます。
    • Device certificate セクション にて「Auto-generate a new certificate (recommended)」を選択します。
    • Next」ボタンを選択します。

  • Attach policies to certificate - optional の画面が表示されます。
    • Policies で 「Create policy」を選択します。

  • ブラウザの新規タブが開き、Create policy の画面が表示されます
    • Policy name に jetson-policy と入力します。
    • Policy examples」タブを選択します。
    • Publish to any topic prefixed by the thing name」を選択します。
    • Add to policy」ボタンを選択します。
    • Create」ボタンを選択します。

  • Attach policies to certificate - optional の画面があるタブに戻ります。
    • さきほど作成した jetson-policy を選択します。
    • Create thing」ボタンを選択します。

  • Download certificates and keys のモーダル画面が表示されます。
    • 以下のファイルをダウンロードします。
      • Device certificate - <long-string>-certificate.pem.crt
      • Private key file - <long-string>-private.pem.key
      • Amazon trust services endpoint - AmazonRootCA1.pem
    • ダウンロードしたファイルは Jetson で使用しますので Jetson に置いておきます。分かりやすさのために <long-string>- 部分はファイル名から削除しておきましょう。

3-2. Rule の作成

AWS IoT Core から受け取ったメッセージをそのまま AWS IoT Events に渡すようにルールを設定します。AWS IoT Core デベロッパーガイドの「AWS IoT ルールの作成」を参考にルールを作成します。今回は以下のように全てのメッセージを AWS IoT Events に渡すように設定します。

  • SQL statement: SELECT * FROM 'pose'
  • Actions: Send a message to an IoT Events Input

4. AWS IoT Events の設定

AWS IoT Events では以下の二つを定義します。

  • Detector models - いわゆる状態遷移図。入ってくるデータの内容に応じた状態とその遷移条件を定義します。
  • Inputs - AWS IoT Eventsに入力されるデータのフォーマット。今回は先ほどの trt_poseログの JSON フォーマット が Input のフォーマットになります。

4-1. Inputs の作成

  • Create input」 ボタンを選択します。
  • Input name を pose_input と入力します。
  • Upload a JSON file にて JSON ファイルを選択します。ここで、JSONファイルは先ほどのサンプルログを保存した物を使います。ファイル名は pose_input.json とします。
  • Create」ボタンを押します。

クリックすると拡大します

4-1-1. Detector model の定義

事前に Detector model が使用するロールを作成します。

IAM にて、この Detectior model から AWS IoT Core の MQTT トピックに Publish できるように、以下のポリシーを持つロールを作成します。ここではロール名を role-0001 とします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "iot:Publish",
            "Resource": "arn:aws:iot:ap-northeast-1:<account_id>:topic/notification"
        }
    ]
}

Detector models で Action から「Import detector model」を選択し、以下のモデル Squat_challenge をインポートします。

{
    "detectorModelDefinition": {
        "states": [
            {
                "stateName": "Ready",
                "onInput": {
                    "events": [],
                    "transitionEvents": [
                        {
                            "eventName": "Down",
                            "condition": "$input.pose_input.pos.left_hip.y > $input.pose_input.pos.left_knee.y",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "cnt",
                                        "value": "$variable.cnt + 1"
                                    }
                                }
                            ],
                            "nextState": "SquatDown"
                        },
                        {
                            "eventName": "RightHandUp",
                            "condition": "$input.pose_input.pos.right_elbow.y < $input.pose_input.pos.nose.y ",
                            "actions": [],
                            "nextState": "CounterReset"
                        },
                        {
                            "eventName": "TimeUp",
                            "condition": "timeout(\"timer\")",
                            "actions": [],
                            "nextState": "Done"
                        }
                    ]
                },
                "onEnter": {
                    "events": [
                        {
                            "eventName": "Message",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "msg",
                                        "value": "\"👏 頑張りましょう!\""
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "TimeStamp",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "ts",
                                        "value": "$input.pose_input.ts"
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "Timer",
                            "condition": "true",
                            "actions": [
                                {
                                    "setTimer": {
                                        "timerName": "timer",
                                        "seconds": 60,
                                        "durationExpression": null
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "Notification",
                            "condition": "true",
                            "actions": [
                                {
                                    "iotTopicPublish": {
                                        "mqttTopic": "notification"
                                    }
                                }
                            ]
                        }
                    ]
                },
                "onExit": {
                    "events": []
                }
            },
            {
                "stateName": "SquatDown",
                "onInput": {
                    "events": [],
                    "transitionEvents": [
                        {
                            "eventName": "Up",
                            "condition": "$input.pose_input.pos.left_hip.y < $input.pose_input.pos.left_knee.y",
                            "actions": [],
                            "nextState": "Ready"
                        },
                        {
                            "eventName": "Bad",
                            "condition": "$input.pose_input.pos.left_wrist.y - $input.pose_input.pos.left_shoulder.y > 10 || $input.pose_input.pos.left_knee.y - $input.pose_input.pos.left_shoulder.y < 10",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "bct",
                                        "value": "$variable.bct + 1"
                                    }
                                }
                            ],
                            "nextState": "BadPose"
                        }
                    ]
                },
                "onEnter": {
                    "events": [
                        {
                            "eventName": "Message",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "msg",
                                        "value": "\"💪🏼 いいフォームです!\""
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "TimeStamp",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "ts",
                                        "value": "$input.pose_input.ts"
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "Notification",
                            "condition": "true",
                            "actions": [
                                {
                                    "iotTopicPublish": {
                                        "mqttTopic": "notification"
                                    }
                                }
                            ]
                        }
                    ]
                },
                "onExit": {
                    "events": []
                }
            },
            {
                "stateName": "BadPose",
                "onInput": {
                    "events": [],
                    "transitionEvents": [
                        {
                            "eventName": "Good",
                            "condition": "$input.pose_input.pos.left_wrist.y - $input.pose_input.pos.left_shoulder.y < 10 && $input.pose_input.pos.left_knee.y - $input.pose_input.pos.left_shoulder.y > 10",
                            "actions": [],
                            "nextState": "SquatDown"
                        }
                    ]
                },
                "onEnter": {
                    "events": [
                        {
                            "eventName": "Message",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "msg",
                                        "value": "\"👎 フォームが崩れています\""
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "TimeStamp",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "ts",
                                        "value": "$input.pose_input.ts"
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "Notification",
                            "condition": "true",
                            "actions": [
                                {
                                    "iotTopicPublish": {
                                        "mqttTopic": "notification"
                                    }
                                }
                            ]
                        }
                    ]
                },
                "onExit": {
                    "events": []
                }
            },
            {
                "stateName": "CounterReset",
                "onInput": {
                    "events": [],
                    "transitionEvents": [
                        {
                            "eventName": "RightHandDown",
                            "condition": "$input.pose_input.pos.right_elbow.y > $input.pose_input.pos.nose.y ",
                            "actions": [],
                            "nextState": "Ready"
                        }
                    ]
                },
                "onEnter": {
                    "events": [
                        {
                            "eventName": "TimeStamp",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "ts",
                                        "value": "$input.pose_input.ts"
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "Counter",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "cnt",
                                        "value": "0"
                                    }
                                },
                                {
                                    "setVariable": {
                                        "variableName": "bct",
                                        "value": "0"
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "Message",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "msg",
                                        "value": "\"👌 カウントをリセットします\""
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "Notification",
                            "condition": "true",
                            "actions": [
                                {
                                    "iotTopicPublish": {
                                        "mqttTopic": "notification"
                                    }
                                }
                            ]
                        }
                    ]
                },
                "onExit": {
                    "events": []
                }
            },
            {
                "stateName": "Done",
                "onInput": {
                    "events": [],
                    "transitionEvents": [
                        {
                            "eventName": "LeftHandUp",
                            "condition": "$input.pose_input.pos.left_elbow.y < $input.pose_input.pos.nose.y",
                            "actions": [],
                            "nextState": "Ready"
                        }
                    ]
                },
                "onEnter": {
                    "events": [
                        {
                            "eventName": "Message",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "msg",
                                        "value": "\"🙌 おつかれさまでした!\""
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "TimeStamp",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "ts",
                                        "value": "$input.pose_input.ts"
                                    }
                                }
                            ]
                        },
                        {
                            "eventName": "Notification",
                            "condition": "true",
                            "actions": [
                                {
                                    "iotTopicPublish": {
                                        "mqttTopic": "notification"
                                    }
                                }
                            ]
                        }
                    ]
                },
                "onExit": {
                    "events": []
                }
            },
            {
                "stateName": "CounterInit",
                "onInput": {
                    "events": [],
                    "transitionEvents": [
                        {
                            "eventName": "Start",
                            "condition": "true",
                            "actions": [],
                            "nextState": "Ready"
                        }
                    ]
                },
                "onEnter": {
                    "events": [
                        {
                            "eventName": "InitCounter",
                            "condition": "true",
                            "actions": [
                                {
                                    "setVariable": {
                                        "variableName": "cnt",
                                        "value": "0"
                                    }
                                },
                                {
                                    "setVariable": {
                                        "variableName": "bct",
                                        "value": "0"
                                    }
                                }
                            ]
                        }
                    ]
                },
                "onExit": {
                    "events": []
                }
            }
        ],
        "initialStateName": "CounterInit"
    },
    "detectorModelDescription": "Remove variable cal",
    "detectorModelName": "Squat_challenge",
    "evaluationMethod": "SERIAL",
    "key": null,
    "roleArn": "arn:aws:iam::<account_id>:role/service-role/role-0001"
}

4-1-2. Detector model (状態遷移図) の説明

Web コンソールを見ると以下のような Detector model ができているはずです。

このモデルの状態と遷移条件は以下の表のようになっています。

各状態を表す丸いノードと矢印をクリックするとそれぞれのアクションと遷移の条件式が表示されるので、確認してみてください。

状態
説明
CounterInit スクワット成功/失敗のカウンターの初期化
Ready スクワットの準備状態
SquatDown ちゃんとスクワットしている。手が前に伸びていて、お尻が膝より下にある。スクワット成功としてカウント。
BadPose フォームが崩れている(手が伸びていない、または背中が丸まっている)。スクワット失敗としてカウント。
CounterReset 右手を上げている。これまでのカウントがリセットされ最初からスタートできる。手を下げると Ready へ。
Done Ready の状態で60秒たつとバテてるな?ということで強制終了。Done から左手を挙げたらReadyになり、再チャレンジが可能。

5. ログの送信

Jetson に戻って、先ほどのログをリアルタイムに AWS IoT Core に送る設定をします。だいぶ長くなってきた紙面の関係からここでは Mosquitto を使いますが、AWS IoT GreengrassAWS IoT Device SDK を使う方法もありますので、こちらもチャレンジしてみてください。

Mosquitto のインストール方法は Ubuntu の場合は以下です。mosquitto-clients だけインストールすれば OK です。

apt-get install software-properties-common
apt-add-repository ppa:mosquitto-dev/mosquitto-ppa
apt-get update
apt-get install mosquitto-clients

mosquitto_pub-l をつけると標準入力から受け取れるので、ログを tail -f してmosquitto_pub に渡せばログを 1 行ずつ AWS IoT Core に送ることができます。ここで書かれている--cafile --cert --key は先にダウンロードしたものです。

tail -f person.log | mosquitto_pub --cafile AmazonRootCA1.pem \
--cert certificate.pem.crt \
--key private.pem.key \
-h <account-specific-prefix>.iot.ap-northeast-1.amazonaws.com \
-p 8883 \
-t pose \
-i jetson \
-l \
-d \

6. 通知とWeb画面の設定

フロントエンドは AWS Amplify を使って爆速で開発 していきたいと思います。Amplify CLI を使えば、Amplify カテゴリ から必要な機能を簡単に自分のアプリケーションに追加できます。今回は、「図 2:今回作成するアプリケーションのサンプル画面と通知」にあるような UI を作ります。

Amplify カテゴリ から AuthenticationPubSub を追加することで、ユーザーが Amazon Cognito での認証・認可を経てログインしたら、AWS IoT Core からの MQTT メッセージを受け取れるようになります。また MQTT メッセージを受け取ったらブラウザの Web プッシュ機能でリアルタイムにデスクトップに通知を出すようなコードにしてみたいと思います。さらっと書きましたが PubSub に対応してるのは熱いですね、これはもう使うしかありません !

6-1. Vue プロジェクトの設定と Authentication の追加

フロントエンドの JS フレームワークは Vue を使います。Amplify Dev Center にある Vue のチュートリアル に沿って進めます。必要な手順は以下の 4 つです。今回はデータベースは使いませんので Connect API and database to the app はスキップできます。

  1. Prerequisites
  2. Set up fullstack project
  3. Add authentication
  4. Deploy and host app


ユーザー登録と認証が簡単に追加されましたね。作ったログイン画面で「Create Account」のタブからユーザーを作成してみてください。作成されたユーザーは Cognito に登録されます。

6-2. PubSub の追加

PubSub Library を追加し、さらに Cognitoで認証されたユーザーが AWS IoT Core に接続できるようにします。Amplify Dev Center にある PubSub の Getting started に沿って進めます。必要な手順は以下の  3 つです。

  1. Step 1: Create IAM policies for AWS IoT
  2. Step 2: Attach your policy to your Amazon Cognito Identity
  3. Step 3: Allow the Amazon Cognito Authenticated Role to access IoT Services


Step 2 に Cognito Identity Id をコードで取得する部分がありますが、これは Cognito のマネジメントコンソールの Federated Identities からも確認可能です。ap-northeast-1:d1f39b39-b546-4ebd-b3b8-d1f93d34ae73 のようなフォーマットです。

6-3. Web アプリのコードの変更

今回は、以下に Web アプリのサンプルコードを用意しました。Vue のプロジェクトディレクトリの src/App.vue を以下の内容で置き換えてください。

<script setup>
import { Authenticator } from '@aws-amplify/ui-vue';
import '@aws-amplify/ui-vue/styles.css';
import PubSub from './components/PubSub.vue'
</script>

<template>
  <authenticator>
    <template v-slot="{ user, signOut }">
      <link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
      <main class="container">
        <br><br>
        <h1 style="text-align: right">👤 {{ user.username }} さん</h1>
        <PubSub />
        <div class="grid">
          <a href="#" role="button" class="secondary outline" @click="signOut">Logout</a>
        </div>
      </main>
    </template>
  </authenticator>
</template>

また、src/components/PubSub.vue を以下の内容で作成します。

<script setup>
import Amplify from 'aws-amplify';
import { PubSub } from 'aws-amplify';
import { AWSIoTProvider } from '@aws-amplify/pubsub';
import { ref } from "vue";

// PubSub
Amplify.addPluggable(
  new AWSIoTProvider({
    aws_pubsub_region: 'ap-northeast-1',
    aws_pubsub_endpoint: 'wss://<account-specific-prefix>.iot.ap-northeast-1.amazonaws.com/mqtt',
    clientId: 'browser'
  })
);

let conn;

function sub(topic) { 
  conn = PubSub.subscribe(topic).subscribe({
    next: data => update(data),
    error: error => console.error(error),
    complete: () => console.log('Done'),
  });
}

function unsub() {
  conn.unsubscribe();
}

// Reactive and notification
let stn = ref('')
let msg = ref('')
let bct = ref(0)
let cnt = ref(0)
let cal = ref(0)

function update(data){
  const d = data.value.payload.state
  stn.value = d.stateName
  msg.value = d.variables.msg
  bct.value = d.variables.bct
  cnt.value = d.variables.cnt
  cal.value = (Number(d.variables.cnt)*0.4).toFixed(1)

  Notification.requestPermission().then(perm => {
    console.log(msg.value)
    if (perm === "granted") {
      new Notification(msg.value, { 
          body: `${d.variables.ts}\n💪 ${cnt.value} squats! 🔥 ${cal.value}kcal burned!`,
      })
    }
  })
}
</script>

<template>
  <h1>現在の状態:{{ stn }}</h1>
  <div class="grid">
    <div>
      <a href="#" role="button" class="secondary outline"><h3>スクワット成功</h3><h1>💪 {{ cnt }}回</h1></a>
    </div>
    <div>
      <a href="#" role="button" class="secondary outline"><h3>スクワット失敗</h3><h1>👎 {{ bct }}回</h1></a>
    </div>
    <div>
      <a href="#" role="button" class="secondary outline"><h3>カロリー消費量</h3><h1>🔥 {{ cal }}kcal</h1></a>
    </div>
  </div><br><br>
  <h1>メッセージ: {{ msg }}</h1>
  <div class="grid">
    <a href="#" role="button" class="outline" @click="sub('notification')">Subscribe</a>
    <a href="#" role="button" class="secondary outline" @click="unsub">Unsubscribe</a>
  </div><br>
</template>

Cognito 認証からの AWS IoT Core への PubSub がたったのこれだけです。

6-4. 確認とデプロイ

まずはローカルで動作を確認してみましょう。Vue のプロジェクトディレクトリで npm run serve を実行して、ブラウザで http://localhost:8080/ にアクセスしてみてください。ログイン画面から、先ほど作ったユーザーでログインしたら、図 2 の UI が出てくるはずです。「Subscribe」ボタンを押すと動作開始です。ローカルでも動作はしますが、必要に応じて amplify publish してクラウドにデプロイしましょう。

図 2 : 今回作成するアプリケーションのサンプル画面と通知
(クリックすると拡大します)

これで、IoT Eventsで状態が変わるとデスクトップに通知が出てくるはずです。あとはカメラに左を前にして横向きに立ち、力いっぱいスクワットをするだけです !


7. まとめ

今回は、カメラからの骨格情報をもとに AWS IoT Events に筋トレのトレーナーをやってもらう方法を紹介しました。周辺の設定はいろいろあるものの、AWS IoT Events によるイベントドリブンでノーコードな実装をリアルタイムなケースで使っているのがお分かりいただけたと思います。

今回は一人でのトレーニングを前提に書きましたが、trt_pose では一つのカメラで複数人を識別可能ですので、AWS IoT Events の Detector model (状態遷移図) の設定次第では、例えば「二人で一つの Detector model を設定し、協力して 100 回の筋トレをする」とか、「それぞれの人が個別のカメラと Detector model を設定して、消費カロリーで競争をする」といったように「AWS クラウド越しのソーシャル筋トレ」みたいなこともできるかもしれません。また、Alexaと音声で連携しても面白そうです。

是非、今回の構成に手を加えて遊んでみてください。


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます


筆者紹介

井上 昌幸
アマゾン ウェブ サービス ジャパン合同会社
IoT Specialist Solution Architect

Internet of Things と Robotics な角度から日々、面白い事を探しています。とっておきのアイデアをぜひ一緒にカタチにしましょう。

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する