LoginSignup
5
6

More than 5 years have passed since last update.

【AWS習作】面白サービスRekognitionとSlackを使って画像解析アプリを作る

Last updated at Posted at 2019-04-25

Rekognitionとは

画像内に存在する物体や表情、文字等をAPIを介して解析するサービス。
そんなRekognitionにも無料枠が存在するらしいので、何とか使ってみたい。

無料枠期間は1 か月あたり 5,000 枚の画像分析が可能との事。
多いんだか少ないんだか…
https://aws.amazon.com/jp/rekognition/pricing/

雑な構成図

image.png
お仕事でこのような構成図を出したら即鉄拳が飛んできそう。
この構成図に沿ってServerlessApplicationModel、略してSAMの定義を書いていきます。
Lambdaの言語はPythonを使います。

Slack側のイベントはfile sharedで発火させます。
Slackアプリのセットアップに関しては省略。

SAM

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Slack is app

Globals:
  Function:
    Runtime: python3.7
    Timeout: 15
    MemorySize: 256
    Environment:
      Variables:
        Bucket_Name:  <適切なS3バケットを指定してください>
        Dynamo_Table:  !Ref  SlackDynamo
        Oauth_Token:  <Slack側の認証トークンを入れてください>
        Slack_Api_Url :  https://slack.com/api/
        Webhook_url :  <Slack側のWebHookURLを入れてください>
Resources:
  Slackbobobo:
    Type:  AWS::Serverless::Function
    Properties:
      CodeUri:  'Slack_app/'
      Role:  <"適切なIAMロールを入れてください">
      Handler:  slack_bobobo.lambda_handler
      Events:
        Api:
          Type:  Api
          Properties:  
            Path:  /images
            Method:  post
  SlackDynamo:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: Imageid
        Type: String
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5

留意事項としては、まずIAMロールですがRekognition,S3,Lambda,DynamoDBに書き込んだり読み込んだりするポリシーをアタッチする必要があります。私はちまちまポリシー用のJSON作るのが面倒なので雑にFullAccess付けました。それと、Timeoutの値ですが、結構処理に時間掛かるのでデフォルトの3秒のままだと
image.png
こんなエラーが出ます。

それと、今回LambdaのAPIの通信に外部ライブラリのrequestsを利用しますので、依存モジュール共々pyファイルの同階層に入れておきます。
image.png

そして、下記のコマンドでスタックをデプロイします。

deploy.bash
aws cloudformation package \
 --template-file template.yaml  \
 --s3-bucket <"パッケージ格納用S3バケット"> \
 --output-template-file packaged-template.yaml

aws cloudformation deploy \
     --template-file packaged-template.yaml \
     --stack-name <"適当なスタック名を入れてください"> \
     --capabilities CAPABILITY_IAM

Lambda全ソース

slack_bobobo.py
#coding:utf-8
import boto3
import json
import os
import logging
import decimal
import requests
from botocore.exceptions import ClientError
from boto3.dynamodb.conditions import Key
from datetime import datetime, date, timedelta

#ロギング
logger = logging.getLogger()
logger.setLevel(logging.INFO)

dynamodb = boto3.resource('dynamodb',region_name = 'ap-northeast-1')
trgt_table = dynamodb.Table(os.getenv('Dynamo_Table'))

#SlackApiにイベントの種類に応じてGetを投げる
def get_slack_api(base_url,token,file_id):

    headers = {'Authorization': 'Bearer {}'.format(token)}

    response = requests.get(base_url+'files.info'+'?file='+file_id,headers=headers)

    return json.loads(response.text)

#画像ダウンロード
def get_image(image_url,token):
    headers = {'Authorization': 'Bearer {}'.format(token)}

    image = requests.get(image_url,headers=headers)

    return image.content

#S3にアップロード
def image_put_s3(image,key,mimetype,bucket_name):
    s3 = boto3.resource('s3',region_name = 'ap-northeast-1').Bucket(bucket_name)
    s3_obj = s3.Object(key)

    s3_obj.put(
    Body=image,
    StorageClass='STANDARD',
    ContentType=mimetype
    )

    return 

#Slackに投稿
def Slack_post(webhook_url,label,text,key):
    message ='S3に'+key+'をアップロードしました。\n'
    message +='解析結果\n'
    message +='\n物体検出\n'

    #物体検出
    for lb_n in label:
        message +=lb_n['Name']+':'+str(round(float(lb_n['Confidence']),1))+'%\n'

    message +='\nテキスト検出\n'

    #テキスト検出
    for tex_n in text:
        message +=tex_n['DetectedText']+':'+str(round(float(tex_n['Confidence']),1))+'%\n'

    item= { 'text':  message }

    headers = {'Content-type': 'application/json'}

    try:
      requests.post(webhook_url,json=item,headers=headers)

    except Exception as e:
      logging.info("type:%s", type(e))
      logging.error(e)

    return

#rekogniton解析したやつをDynamoに投げる
def rekogniton_image(bucket_name,key):
    rekogniton = boto3.client('rekognition',region_name ='ap-northeast-1')

    #boto3の公式リファレンス
    #https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/rekognition.html
    reko_label = rekogniton.detect_labels(Image={'S3Object':{'Bucket':bucket_name,'Name':key}},MinConfidence=75)['Labels']
    reko_text = rekogniton.detect_text(Image={'S3Object':{'Bucket':bucket_name,'Name':key}})['TextDetections']

    #DynamoDBはFloat型をサポートしていない為、Decimal型に変換する必要がある
    item={
        'Imageid':key,
        'timestamp':int(datetime.utcnow().timestamp()),
        'label':json.loads(json.dumps(reko_label),parse_float=decimal.Decimal),
        'text':json.loads(json.dumps(reko_text),parse_float=decimal.Decimal)
        }

    res = trgt_table.put_item(Item=item)

    if(res):
        Slack_post(os.getenv('Webhook_url'),item['label'],item['text'],key)

    return     


def lambda_handler(event,context):
    #イベント設定用
    body = json.loads(event['body'])
    if "challenge" in body:
      response = {
      'statusCode':'200',
      'body':body,
      'headers':{'Content-Type':'application/json'}
      }
      return response
    else:
        try:
          #DynamoDB存在判定
          res_get_item = trgt_table.get_item(Key={'Imageid':body['event']['file_id']})

          if 'Item' not in res_get_item:
              file_res = get_slack_api(os.getenv('Slack_Api_Url'),os.getenv('Oauth_Token'),body['event']['file_id'])
          else:
            raise Exception("Already Exist Same Imageid!")

        except Exception as e:
          logging.info("type:%s", type(e))
          logging.error(e)

        else:
          try:
            image = get_image(file_res['file']['url_private'],os.getenv('Oauth_Token'))

          except Exception as e:
            logging.info("type:%s", type(e))
            logging.error(e)
          else:
            try:
              s3_key = file_res['file']['id'] + '.' + file_res['file']['mimetype'].split('/')[1]
              image_put_s3(image,s3_key,file_res['file']['mimetype'],os.getenv('Bucket_Name'))

            except ClientError as e:
              logging.info(e.response['Error']['Message'])
              logging.error(e)

            else:
              try:
                rekogniton_image(os.getenv('Bucket_Name'),s3_key)

              except ClientError as e:
                logging.info(e.response['Error']['Message'])
                logging.error(e)

              except Exception as e:
                logging.info("type:%s", type(e))
                logging.error(e)

              else:
                response = {
                'statusCode':'200',
                'body':'OK',
                'headers':{'Content-Type':'application/json'}
                }

                return response

ご覧の通りS3のオブジェクトキーは、Slackにアップロードした際のfile_idをそのまま流用しています。
Botのメッセージもおかげで珍妙な感じに。

実行

投稿します。
image.png

返ってきます。
image.png
単語の後ろにある謎の数字はConfidence値、つまり信頼度?です
しかし、HumanもPersonもどこにもいない筈なのに自信満々にこの結果をお出しされます。

テキストの検出は及第点といったところでしょうか。DEVILのIは読み込めてませんが。

シグルイ第一巻を解析してもらいます。
image.png

image.png
紛れもなく人間が描かれているのですが、デビルガンダムより控えめな結果が返ってきました。

テキストは…肝心の題名、作者名が解析されていません。日本語に対応していないという事なのでしょうか。

マスターガンダムを投稿します。
image.png

image.png
なんと、GundumではなくHaloが検出されます。

総じてかなり微妙な感じになりました。

それと、CloudWatchのAPIログを見てみると、
image.png

画像を投稿したと同時に何故か複数のAPIが発射されている事が判明します。
つまり…
image.png

これはひどい…
S3のPutアクションの単価はかなり割高で、折角の無料枠もたちまち擦り潰されてしまいます。
Dynamoの存在判定も同時にコンテナが起動している為、意味を成しません。

そこで、
Lambdaの同時実行数を弄る事で、多少は多重解析が緩和されます。
image.png

しかし、あくまで多少であって完全に無くなる事はありませんでした。
SlackイベントApiの仕様とかなんでしょうか。

総括

Rekognitionの力を引き出せませんでした。
ラズパイに音波センサーと監視カメラ取り付けて、閾値を超えたらS3に画像アップロードしてRekognitionで人間の存在判定する、みたいな使い方がいいのでしょうか。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6