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

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

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

#雑な構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/4875e425-2d14-5802-b6a0-f1b672ff70de.png)
お仕事でこのような構成図を出したら即鉄拳が飛んできそう。
この構成図に沿ってServerlessApplicationModel、略してSAMの定義を書いていきます。
Lambdaの言語はPythonを使います。

Slack側のイベントはfile sharedで発火させます。
Slackアプリのセットアップに関しては省略。
#SAM
```yaml: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/9f9511fd-3da6-b8c2-651c-2084cebd1908.png)
こんなエラーが出ます。

それと、今回LambdaのAPIの通信に外部ライブラリのrequestsを利用しますので、依存モジュール共々pyファイルの同階層に入れておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/712e06a5-c396-0985-fdf8-c4bbfa75ca52.png)

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

```bash: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全ソース
```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/730b0ab7-495f-fa67-1704-810df1f771c8.png)

返ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/6f604fa5-2504-8857-f6ca-40a60dd9444c.png)
単語の後ろにある謎の数字はConfidence値、つまり信頼度?です
しかし、HumanもPersonもどこにもいない筈なのに自信満々にこの結果をお出しされます。

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

シグルイ第一巻を解析してもらいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/b2648e37-fe01-a7df-0668-d1e99c6cd70b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/40c7029a-052d-d05c-2a8a-7463d9024807.png)
紛れもなく人間が描かれているのですが、デビルガンダムより控えめな結果が返ってきました。

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

マスターガンダムを投稿します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/4a4b308a-ea68-8e24-cb2d-a8b39158f92e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/a2496aea-8a83-2292-1723-a5c696e51f21.png)
なんと、GundumではなくHaloが検出されます。

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


それと、CloudWatchのAPIログを見てみると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/a88ce9b5-ffb6-67ae-acca-455387ef6091.png)

画像を投稿したと同時に何故か複数のAPIが発射されている事が判明します。
つまり…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/98c0d1b6-8f76-626e-ec43-4c459dc7646c.png)

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

そこで、
Lambdaの同時実行数を弄る事で、多少は多重解析が緩和されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/7835e373-cd92-0da6-a197-876e5375c811.png)

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

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