
StepFunctionsを使って画像内ラベル検出をしてみる
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
おはようございます、もきゅりんです。
ふとしたタイミングでStepFunctionsを触る機会があったので、巷でよくあるパターンと言われている構成をやってみました。
下図です。

例えば、こんな写真をアップロードしてみると、

DynamoDBにこんな感じの結果が挿入されるというシロモノです。

AWSってすげー。。何か自分、無能かつ無為に生きててゴメンって思わされました。
とりあえずStepFunctions(も)楽しいです。
やってみよう
前提条件
- AWS CLIがインストールされていること(手動で作成でも可)
 - StepFunctionsへの興味関心
 
S3バケットとDynamoDBテーブルをCFnで作成します。
※ 注 すべてのリソースは同じAWSリージョン内に作成する必要があります。
やること
- S3Bucket,DynamoDBを作成
 - Lambdaを準備
 - ステートマシン作成
 - S3イベント発生時にステートマシンを実行させる
 - テスト
 
1. S3Bucket, DynamoDBを作成
CFnで作成しちゃいます。
(コンソールから作成でも構いません。)
aws cloudformation deploy --template-file S3BucketDynamoDB.yml \ --stack-name demo-stepfunctions-resources
# S3BucketDynamoDB.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Create DynamoDB Table & S3 Bucket"
Resources:
  DynamoTable:
    Type: "AWS::DynamoDB::Table"
    Properties:
      AttributeDefinitions:
        - AttributeName: "id"
          AttributeType: "S"
      KeySchema:
        - AttributeName: "id"
          KeyType: "HASH"
      ProvisionedThroughput:
        ReadCapacityUnits: "5"
        WriteCapacityUnits: "5"
      TableName: "ImageInfo"
  S3Backet:
    Type: AWS::S3::Bucket
    Properties:
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
2. Lambdaを準備
やり方は他にも色々とあると思いますが、この稿では3つのLambdaを用意します。
- S3のメタデータを取得するLambda
 - 画像のラベル抽出をするLambda
 - DynamoDBにPutするLambda
 
(例えば、ステートマシンから直接DynamoDBテーブルにPutすることもできます)
本来はそれぞれに必要最小限の権限のロールを設定するのが定石なのですが、本番利用ではないという言い訳をしながら、下記のようなポリシーをlambda-basic-exec-roleに追加しています。
- AmazonS3ReadOnlyAccess
 - AmazonDynamoDBFullAccess
 - AmazonRekognitionReadOnlyAccess
 - TranslateReadOnly
 
Translateについては、画像のラベル検出結果が英語のため、ナンダコレ?となるのを避けるために日本語翻訳させるためです。
下記3つのLambdaをPython3.7で、一から作成します。
ロールは上記、無精なlambda-basic-exec-roleです。
S3からはイベント発生時間、画像のバイト数、クライアントIPアドレス、ファイル名を取得しています。
ラベル検出は主要な結果と信頼性のみ取得しています。
# GetS3Metadata
def lambda_handler(event, context):
    id = event['id']
    time = event['time']
    size = event['detail']['additionalEventData']['bytesTransferredIn']
    IP = event['detail']['sourceIPAddress']
    file_name = event['detail']['requestParameters']['key']
    s3_meta_data = {}
    s3_meta_data['id'] = id
    s3_meta_data['size'] = str(size)
    s3_meta_data['time'] = time
    s3_meta_data['IP'] = IP
    s3_meta_data['file_name'] = file_name
    return s3_meta_data
# GetImageLabels
import boto3
import copy
rekognition_client = boto3.client('rekognition')
translate_client = boto3.client(service_name='translate', use_ssl=True)
def lambda_handler(event, context):
    fileName = event['detail']['requestParameters']['key']
    bucket = event['detail']['requestParameters']['bucketName']
    # 画像のラベル検出
    response = rekognition_client.detect_labels(
        Image={'S3Object': {'Bucket': bucket, 'Name': fileName}})
    detect_dict = {}
    temp_d = {}
    for i, label in enumerate(response['Labels']):
        # ラベルを日本語に翻訳
        labelName = translate_client.translate_text(Text=label['Name'],
                                                    SourceLanguageCode="en", TargetLanguageCode="ja").get('TranslatedText')
        temp_d['Label'] = labelName
        temp_d['Confidence'] = str(label['Confidence'])
        detect_dict[i] = str(copy.deepcopy(temp_d))
    return detect_dict
# PutDynamoDB
import boto3
import json
from datetime import datetime
dynamodb_resource = boto3.resource('dynamodb')
table = dynamodb_resource.Table('ImageInfo')
def lambda_handler(event, context):
    id = event[0]['id']
    size = event[0]['size']
    IP = event[0]['IP']
    time = event[0]['time']
    file_name = event[0]['file_name']
    image_labeling = event[1]
    time_stamp = datetime.now().strftime("%Y%m%d%H%M%S")
    # テーブル挿入
    table_put_item_response = table.put_item(
        Item={
            'id': id,
            'size': size,
            'IP': IP,
            'time': time,
            'file_name': file_name,
            'create_time_stamp': time_stamp,
            'detection': image_labeling
        }
    )
    return table_put_item_response
3. ステートマシン作成
ようやくStepFunctionsのお出ましです。
ステートマシンを作成します。
ロールはAWSLambdaFullAccessで進めています。
作成内容はこの図の通りです。

Resourceのarnについては、2で作成したLambdaをそれぞれ作成順に記入して下さい。
# DemoImageExtract.json
{
  "Comment": "Coordinate tasks for image analysis",
  "StartAt": "ImageAnalysis",
  "States": {
    "ImageAnalysis": {
      "Type": "Parallel",
      "Next": "PutDynamo",
      "Branches": [
        {
          "StartAt": "ImageExtractMetaData",
          "States": {
            "ImageExtractMetaData": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxx:function:GetS3Metadata",
              "OutputPath": "$",
              "End": true
            }
          }
        },
        {
          "StartAt": "ImageLabeling",
          "States": {
            "ImageLabeling": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-1:yyyyyyyyyyy:function:GetImageLabels",
              "OutputPath": "$",
              "End": true
            }
          }
        }
      ]
    },
      "PutDynamo": {
        "Type": "Task",
        "Resource": "arn:aws:lambda:ap-northeast-1:zzzzzzzzzzzzz:function:PutDynamo",
        "End": true
      }
    }
  }
4. S3イベント発生時にステートマシンを実行させる
Amazon CloudWatch Eventsを使用して、イベント発生時にStepFunctionsステートマシンを実行するために、Amazon S3 イベント発生時にステートマシンの実行を開始するに記載されている通り、進めていきます。
AWS CloudTrail で[証跡の表示]、[証跡の作成] の順に選択します。


[証跡名] に、(名称は何でもいいですが)「S3Event」と入力しておきます。

[バケット名] に、1で作成しておいたAmazonS3バケット名を入力し、[保存場所] で、[新しい S3 バケットを作成しますか] の横の [はい] を選択します。
[S3バケット] に、先に作成したAmazonS3バケットのアクションに関する情報を保存する新しいバケットの名前を(何でもよいので)適当に入力します。

CloudWatchイベントルールを作成していきます。
CloudWatchイベントコンソールに移動し、[イベント] を選択して [ルールの作成] を選択します。
記載された通りに入力していきます。1で作成したバケット名を入力します。

ターゲットを作成します。3で作成したステートマシンを選択します。
ロールについては、既存のものでも新規でも良いですが、既存のロールを選択した場合は、イベントを受けるステートマシン名を確認した方が良いと思います。
自動でロールを作成した場合、そのとき選んだステートマシンが宛先になっているためです。

ルールの名前(判れば何でも可)を入力し、[State] で [Enabled] を選択してから、[Create rule] を選択します。

テスト
S3バケットに何かアップロードしてみます。
かわいい蛇の写真。

アップロードします。

DynamoDBのテーブルでは、こんな感じに登録されます。

ちなみに、直接Rekognitionを使用した結果はこちら。

終わりに
アイディア次第でいろいろと面白い取り組みに利用できそうです。
今後も思いつきで取り組みたいと思います。
なお、自身のPython力の弱さのために、余分な時間を随分費やしたと痛感しました。。
不明点を懇切丁寧に教えてくれたMr.Kitano、感謝しかないです。ありがとうございました。
以上です、どなたかのお役に立てば幸いです。
参考
AWS SDK for Python (Boto) を使用したテキストの翻訳
Amazon S3 イベント発生時にステートマシンの実行を開始する
[やってみた]Step FunctionsのみでDynamo DBテーブルからアイテム取得/登録をしてみた #reinvent






