正しい姿勢でテレワークを健康的に ! GluonCV × AWS Lambda で姿勢をチェックしてみよう

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

Author : 鳥山 菜海子 (監修 : 筑井 友啓)

みなさま、こんにちは ! プロトタイピングチームでインターンをしている鳥山です。

COVID-19 の影響により、多くの企業でテレワークが身近な働き方になっているのではないでしょうか。テレワークは通勤時間を短縮できる、育児と仕事の両立ができるというメリットがある一方、椅子に座っている時間が長く、肩や腰などの痛み、姿勢が悪化するというデメリットも存在します。

この記事を読んでいるあなたも、人の目がないからと、ついつい悪い姿勢やひどい椅子の座り方をしていませんか ?

本記事では、GluonCV が提供する学習済み姿勢推定モデルを用いて、撮影した写真の姿勢判定するサービスの作成に挑戦します。GluonCV とは、ディープラーニングのフレームワーク MXNet が提供するコンピュータビジョン用のインターフェースになります。そして、姿勢の推定結果は Slack に通知を送ります。また、作成した機械学習のモデルをコンテナ化して AWS Lambda にアップロードすることで、非常に低コストに機械学習モデルを利用することができます。


実際のサービスの実行例

サービスの実行例になります。(画像の一部をマスクして利用しております。)

姿勢が良い場合

姿勢が悪い場合

このように、姿勢が良い場合と悪い場合で Slack に通知されることが確認できます。

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

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


全体のアーキテクチャ構成

今回構築するアーキテクチャは以下のようになります。

まず、Amazon S3 に姿勢を判定したい画像をアップロードします。画像をアップロードしたタイミングで、AWS Lambda が S3 の PUT イベントを検知し、GluonCV を使って画像から姿勢判定をします。この Lambda 関数は、GluonCV など姿勢推定に必要なライブラリがインストールされたコンテナイメージを使って作成しています。本記事では、Amazon SageMaker ノートブックインスタンスでコードの記述とコンテナイメージのビルド、Amazon ECR へのプッシュを行いました。姿勢判定が終わった後は Amazon SNS で Lambda に渡し、Lambda で Slack に通知します。

このアーキテクチャの特徴として、GluonCV の呼び出しに AWS Lambda を利用しています。これにより、推論に必要なサーバーを常駐させず、推論が必要なときのみ Lambda 関数を起動することで、コストを最小限に抑えています。

また、Lambda 関数はデフォルトの zip 形式ではなく、コンテナ形式を採用しています。zip 形式の場合、最大で 250 MB までのデプロイサイズ制限がありますが、コンテナ形式の場合、最大で 10 GB までのイメージサイズをサポートしています。利用する学習モデルや、関数が依存するライブラリのファイルサイズが大きい今回のようなワークロードでは、コンテナ形式の Lambda が適しています。zip 形式、コンテナ形式の違いに関する詳細は、「コンテナ利用者に捧げる AWS Lambda の新しい開発方式 !」も併せてご確認ください。


2. 画像をアップロードするための Amazon S3 のセットアップ

それでは実際にサービスを作成していきます。

S3 のコンソール画面からバケットを作成 を選択し、バケット名を入力します。

バケット名はグローバルで一意になる必要があります。その他の設定はデフォルトのまま 作成 ボタンを押します。


3. Amazon SageMaker のセットアップ

次に、Amazon SageMaker を使って GluonCV を呼び出すコードを書いていきます。

ローカルでも実装は可能なのですが、AWS コマンドの設定、Python や Docker のインストールをせずにコンテナイメージのビルドと ECR へのプッシュができるため、今回はSageMaker を利用しています。

SageMaker コンソールから、ノートブック > ノートブックインスタンス > ノートブックインスタンスの作成 を選択します。

任意のインスタンス名を入力し、コンテナイメージのビルドにかかる時間を短縮するため、大きめのインスタンスのタイプを選択してください。今回は、ml.m5.xlarge を選択しました。こちらのスクリーンショットが設定例になります。

設定後、作成 ボタンを押してください。
ノートブックの作成には 5 分ほど時間がかかります。

完成したら、ノートブックの名前 をクリックし、アクセス許可と暗号化 の項目にある IAM ロール ARN をクリックします。

IAM の設定画面が開かれたら、AmazonEC2ContainerRegistryFullAccess のポリシーをアタッチします。ポリシーをアタッチすることで、SageMaker から ECR レポジトリの作成とコンテナイメージのブッシュが可能になります。


4. GluonCV で姿勢推定をする

4-1. 姿勢の判定方法

GluonCV を用いて姿勢の判定を行います。

今回は以下のようなロジックで姿勢を判定しました。

傾きの 0.1 という数字はいくつかの姿勢の画像を確認して決めました。

GluonCV の姿勢推定を使って得られるキーポイントの座標は以下のような順番で格納されており、目の座標と肩の座標などを簡単に取得することができます。

from gluoncv import data
print(data.mscoco.keypoints.COCOKeyPoints.KEYPOINTS)

0: 'nose'
*1**:* ** *'left_eye'*
*2**:* ** *'right_eye'*
3: 'left_ear'
4: 'right_ear'
*5**:* ** *'left_shoulder'*
*6**:* ** *'right_shoulder'*
... (略)

4-2. 姿勢推定のモデルを利用するためのコードを書く

実際にコードを書きます。SageMaker ノートブックの Jupiter を開く を選択した後、New で新しい ipynb ファイルを作ります。カーネルは conda_mxnet_p36 を選択します。

その後、以下のコードを書きます。

import boto3
import sagemaker
from sagemaker import get_execution_role

# 基本的な情報の取得
role = get_execution_role()
region = boto3.session.Session().region_name
account_id = boto3.client('sts').get_caller_identity().get('Account')
session = sagemaker.Session()

# 任意の名前を設定
dirname = 'docker-gluon'
ecr_repository = 'gluon-ecr'

# ECRにアップロードするためのURIを作成
tag = ':latest'
uri_suffix = 'amazonaws.com'
processing_repository_uri = f'{account_id}.dkr.ecr.{region}.{uri_suffix}/{ecr_repository + tag}'

このコードではロールやリージョンの取得など基本的な設定をしています。

コードを実行した後、次のセルに以下を記述します。

!mkdir -p $dirname

$dirname (= ここでは docker-gluon) という名前のディレクトリを作成します。
前に「!」マークをつけることでコマンドの実行をすることができます。

次に Lambda に実行させるアプリケーションのコードを書いていきます。以下の内容をセルに書きます。

%%writefile $dirname/app.py

from gluoncv import model_zoo, data, utils
from gluoncv.data.transforms.pose import detector_to_alpha_pose, heatmap_to_coord_alpha_pose
import sagemaker
import os

# 指定したディレクトリの中身を消去する
def emptyDir(folder):    
    fileList = os.listdir(folder)  
    for f in fileList:        
        filePath = folder + '/'+f        
        if os.path.isfile(filePath):            
            os.remove(filePath)        
                        
def handler(event, context):
    print(f"Received event:\n{event}\nWith context:\n{context}")
    
    # データがあれば中身を削除する
    data_path = '/tmp/'
    emptyDir(data_path)    
    
    # S3にPUTされたオブジェクト情報を取得する
    bucket = event['Records'][0]['s3']['bucket']['name']
    object_name = event['Records'][0]['s3']['object']['key']
    print(f"Bucket name:\n{bucket}\nObject name:\n{object_name}")
    
    # 画像をS3からダウンロードする
    sess = sagemaker.Session()
    sagemaker.session.Session.download_data(sess, path=data_path, bucket=bucket, key_prefix=object_name)
    
    im_fname = data_path + object_name 
    x, img = data.transforms.presets.yolo.load_test(im_fname, short=512)

    # 物体検出するモデルを使う
    detector = model_zoo.get_model('yolo3_mobilenet1.0_coco', pretrained=True, root='/tmp')
    detector.reset_class(["person"], reuse_weights=['person'])
    class_IDs, scores, bounding_boxs = detector(x)
    pose_input, upscale_bbox = detector_to_alpha_pose(img, class_IDs, scores, bounding_boxs)

    # モデルを削除する
    emptyDir(data_path)  

    # 検知した人のポーズを推定モデルを使う
    pose_net = model_zoo.get_model('alpha_pose_resnet101_v1b_coco', pretrained=True, root='/tmp')
    predicted_heatmap = pose_net(pose_input)
    pred_coords, confidence = heatmap_to_coord_alpha_pose(predicted_heatmap, upscale_bbox)

   # 姿勢推定の結果から姿勢が正しいかどうかを求める
    np_coords=pred_coords.asnumpy()
    
    left_shoulder = np_coords[0, 5]
    right_shoulder = np_coords[0, 6]
    left_eye = np_coords[0, 1]
    right_eye = np_coords[0, 2]
    
    shoulder = abs((left_shoulder[1]-right_shoulder[1])/(left_shoulder[0]-right_shoulder[0]))
    eye = abs((left_eye[1]-right_eye[1])/(left_eye[0]-right_eye[0]))

    result = f'object={object_name}\n'
    if shoulder < 0.1 and eye < 0.1:
        result+="Good: 姿勢を保ちましょう"
    else:
        result+="Bad: 姿勢を見直しましょう"

    #モデルとダウンロードした画像を削除する
    emptyDir(data_path)
        
    return result

/tmp というディレクトリは Lambda 側で用意されている 512 MB のディレクトリになります。/tmp 以外のディレクトリにこのコードのまま保存しようとするとエラーが発生するため、基本的に /tmp でデータを扱うようにします。

もし、SageMaker 上でコードのデバッグを行いたい場合は、ノートブックで「! pip3 install gluoncv mxnet」を実行して必要なライブラリをインストールした後に、セル上にそのままコードの内容を貼り付けることでデバックすることが可能です。

以下のコードで、Lambda 関数が使用するコンテナイメージを作成するための Dockerfile を作成します。

%%writefile $dirname/Dockerfile

# ディレクトリの定義
ARG FUNCTION_DIR="/function"

FROM python:3.8-slim-buster as build-image

# aws-lambda-cppのビルド依存関係のインストール
RUN apt-get update && \
  apt-get install -y \
  g++ \
  make \
  cmake \
  unzip \
  libcurl4-openssl-dev

# グローバル引数を含める
ARG FUNCTION_DIR

# ディレクトリの作成
RUN mkdir -p ${FUNCTION_DIR}

# 実行コードのコピー
COPY app.py ${FUNCTION_DIR}

# awslambdaricのインストール
RUN pip install \
        --target ${FUNCTION_DIR} \
        awslambdaric

# マルチステージビルドをする
FROM python:3.8-slim-buster

# グローバル引数を含める
ARG FUNCTION_DIR

# 作業ディレクトリを関数のルートディレクトリに設定する
WORKDIR ${FUNCTION_DIR}


# ビルドしたイメージの依存関係をコピーする
COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR}


# 必要なライブラリのインストール
RUN apt -y update && apt install -y python-opencv && apt clean  

RUN pip3 install gluoncv \
             mxnet \
             boto3 \
             sagemaker

ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ]
CMD [ "app.handler" ]

最後にこのコードで、SageMaker 上でコンテナイメージと ECR のリポジトリを作成した後、作成したイメージを ECR にプッシュします。ECR にプッシュするまで 5 分ほど時間がかかります。

# 指定したディレクトリのDockerfileを元にイメージのビルドをする
!docker build -t $ecr_repository $dirname --no-cache

# ECRにログインする
!$(aws ecr get-login --region $region --registry-ids $account_id --no-include-email)

# ECRにリポジトリを作成する(2回目以降は不要)
!aws ecr create-repository --repository-name $ecr_repository

# 作成したイメージにタグをつける
!docker tag {ecr_repository + tag} $processing_repository_uri

# ECRにプッシュする
!docker push $processing_repository_uri 

作成したイメージは Amazon ECS のコンソールの左側にあるメニューの Amazon ECR のリポジトリから確認することができます。


5. コンテナベースでの AWS Lambda の設定

Lambda コンソールの 関数 > 関数の作成 を選択し、コンテナイメージ のオプションを選択します。

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

関数の作成後、こちらのような画面が表示されるため、トリガーを追加 を選択します。

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

選択後、トリガーに S3 を設定したものを二つ作成します。

バケットの選択 では先ほど作成したバケットを、イベントタイプ は「PUT」を選択してください。PUT イベントを選択することで、S3 に画像をアップロードしたことをきっかけに Lambda 関数が実行されるようになります。

サフィックスオプションには「.jpg 」と「.png」をそれぞれ入力します。サフィックスオプションを付与することで、jpg と png で終わる名前の PUT イベントのみに Lambda が起動するようになります。

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

S3 をトリガーにした後は、Lambdaの 設定 > アクセス権限 の実行ロールのセクションからロール名を選択し、IAM に AmazonS3FullAccess のポリシーをアタッチします。

補足として、今回は紹介記事ということで FullAccess 権限を与えていますが、実際のビジネスサービスを作る際は適切な権限を付与する必要があります。

その後、設定 > 基本設定の編集 をクリックし、「メモリ」と「タイムアウト」をこちらのように設定します。

一回に使われるメモリは 1500 MB ほど、初回の実行時間は 1 分弱かかるため、それよりも大きな値を設定します。

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

これで S3 に写真をアップロードしたときに姿勢が推定されるようになりました !

動作確認する場合は以下の条件で行います。

  • 画像の拡張子が「png」または「jpg」
  • 人が写っている写真を選択

png や jpg 以外は Lambda が起動しないようになっています。png や jpg 以外を扱いたい場合は Lambda の S3 トリガーイベントのサフィックスを修正します。

また、人の写っていない写真を利用すると、Lambda 関数の途中でうまく動かなくなる場合があります。Lambda がうまく動いているかどうかの確認は CloudWatch で確認することができます。


6. 通知サービスの設定

6-1. Amazon SNS の設定

SNSコンソ ールからトピックの作成を行います。トピック > トピックの作成 をクリックし、こちらのように赤枠の中の設定を行います。

タイプはスタンダードで任意の名前を設定したら、他はそのままで トピックの作成 ボタンをクリックします。

6-2. 通知先 Slack の設定

次に、Slack API のサイトから、Create New App > From scratch を選択後、Bot の名前と導入するワークスペースを決めます。

その後、Features のサイドバーの OAuth & Permissions のスクロールしたところにある Scopes で chat:write と chat:write.public を設定してください。

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

その後、上にある install to workspace のボタンを押すと、Bot の Token が出てきます。

この Token は後で利用するため、どこかにメモしておきましょう。

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

6-3. 通知を送るための AWS Lambda の設定

Lambda コンソールの 関数 > 関数の作成 から次の画面を開き、こちらのように情報を入力します。

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

次に lambda_function.py を全て以下のコードに置き換えます。

lambda_function.py

import json
import urllib
import urllib.request
import os

# 定数の設定
SLACK_URL = "https://slack.com/api/chat.postMessage"
TOKEN = os.environ['token']
CHANNNEL_ID= os.environ['channel_id']

def lambda_handler(event, context):
    # SNSから送られてきたテキストをjsonにパースする
    json_dict = json.loads(event['Records'][0]['Sns']['Message'])
    text = json_dict['responsePayload']

    # 送信するデータをまとめる
    data = urllib.parse.urlencode(
        (("token", TOKEN), ("channel", CHANNNEL_ID), ("text", text)))
    data = data.encode("ascii")

    # 送信するリクエストの設定
    request = urllib.request.Request(SLACK_URL, data=data, method="POST")
    request.add_header("Content-Type", "application/x-www-form-urlencoded")

    # リクエストの送信
    return urllib.request.urlopen(request).read())

置き換えたら、「Deploy」 ボタンを押しましょう。

次に、設定タブの環境変数で Slack のチャンネル ID と Token を用意します。

channel_id」は送信したい Slack チャンネルの ID を、「token」は先ほど用意した Bot の Token を使ってください。

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

Slack のチャンネル ID はチャンネル詳細から取得することができます。

6-4. Amazon SNS との接続

Lambda コンソールのメニューにある トリガーを追加 を選択します。

SNS を設定し、先ほど作成した SNS トピックを紐付けます。

こちらで SNS との連携が完了しました。

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

最後にコンテナイメージの Lambda と SNS の連携をします。

コンテナで作成した Lambda の画面に戻り、先ほどと同じように トリガーの追加 をクリックします。

その後、SNS を選択し、こちらのように情報を入力します。

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

最後に、設定 > アクセス権限 の実行ロールのセクションからロール名を選択し、IAM の画面で AmazonSNSFullAccess をアタッチします。

以上で完成です !

S3 に画像をアップロードすると、記事の最初に紹介した実行例のように Slack に通知されます。


7. リソースの削除

最後に使ったサービスの削除を行います。

  • Slack Bot は Slack API のサイトの「Basic information」をクリックして、スクロールした先にある「Delete App」を選択して Bot を削除します。
  • Lambda は 関数 のタブから作成した関数をチェックしたのち、アクション から削除します。
  • ECR はリポジトリ名を選択して削除します。
  • SageMaker ノートブックインスタンスは アクション で停止を選択したのちに、削除します。
  • SNS の トピック の画面から削除します。
  • S3 はバケットの中身を空にした後に、削除します。

まとめ

本記事では、GluonCV と AWS Lambda をメインに姿勢を判定するサービスを作成しました。

発展形としましては、フロントエンドを構築して Web カメラから画像を取得できるようにすると、定期的に姿勢の確認をすることもできます。また、コンテナを使って、低コストで機械学習サービスの構築することにも役に立ちます。

ぜひ皆さんもこのサービスを作って、正しい姿勢を手に入れましょう !


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

筆者プロフィール

鳥山 菜海子
アマゾン ウェブ サービス ジャパン株式会社
プロトタイピングエンジニア インターン

気象学の研究室で台風の眼の研究をしている大学院生。変わった台風進路や気圧が低い台風、眼が綺麗な台風を見るとテンションが上がる。研究以外に大学内での開発コミュニティの運営をしている。好きなサービスは AWS Lambda で趣味でも Lambda を使ったサービスを構築している。

監修者プロフィール

筑井 友啓
アマゾン ウェブ サービス ジャパン合同会社
プロトタイピングエンジニア

2020 年からアマゾンウェブサービスジャパンでプロトタイピングエンジニアとして活動しています。AWS 入社前はスタートアップに所属し、主にフロントエンド開発をリードしておりました。妻と 2 匹の柴犬 (しじみとアサリ) で騒がしくも楽しい生活を送っています。

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

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