LINE Bot + Amazon Rekognition ~投稿画像の解析~

こんにちは。katoです。

 

前回に引き続きLINE BotAWSサービスを組み合わせた仕組みをご紹介したいと思います。

 

今回はAmazon Rekognitionと連携した画像解析Botの構築となります。

 

概要

 

前回の記事にてAPI GatewayとLambdaを組み合わせてLINE Botを開発しました。

 

このLINE BotAmazon Rekognitionを連携して画像解析Botとしての機能を追加していきたいと思います。

 

今回構築するシステムは下記のような構成となります。

 

 

 

画像が投稿されるとLambdaからAmazon Rekognitionのdetect_labels APIを実施します。

 

ラベル情報に基づき、人物が画像に含まれる場合には有名人検索、人物が含まれない場合にはラベル情報をAmazon Translateで翻訳して返すといった処理を実施します。

 

手順

 

前回のLINE Botに機能を追加するのでLambda関数の修正のみで作業は完了となります。

 

Amazon RekognitionもAmazon Translateもサービスのセットアップを行うことなく、APIを実行するだけで利用可能です。

 

import json
import boto3
import urllib
import re
import os
 
rekognition = boto3.client('rekognition')
translate = boto3.client('translate')
 
def lambda_handler(event, context):
    print (event)
    url = "https://api.line.me/v2/bot/message/reply"
    method = "POST"
    headers = {
        'Authorization': os.environ['CHANNEL_ACCESS_TOKEN'],
        'Content-Type': 'application/json'
    }
    if (event['events'][0]['type'] == "postback"):
        if (event['events'][0]['postback']['data'] == "blog"):
            message = [
                {
                    "type": "text",
                    "text": "ブログをお探しします!",
                    "quickReply": {
                        "items": [
                            {
                                "type": "action",
                                "action": {
                                    "type": "postback",
                                    "label": "おすすめ",
                                    "data": "Recommended",
                                    "displayText": "おすすめ"
                                }
                            },
                            {
                                "type": "action",
                                "action": {
                                    "type": "postback",
                                    "label": "キーワード検索",
                                    "data": "keyword",
                                    "displayText": "キーワード検索"
                                }
                            }
                        ]
                    }
                }
            ]
        elif (event['events'][0]['postback']['data'] == "Recommended"):
            message = [
                {
                    "type": "text",
                    "text": "おすすめはこちら!"
                },
                {
                    "type": "text",
                    "text": "https://xp-cloud.jp/blog/news/"
                }
            ]
        elif (event['events'][0]['postback']['data'] == "service"):
            message = [
                {
                    "type": "text",
                    "text": "どのようなサービスをお探しでしょうか?",
                    "quickReply": {
                        "items": [
                            {
                                "type": "action",
                                "action": {
                                    "type": "postback",
                                    "label": "AWS導入",
                                    "data": "AWS",
                                    "displayText": "AWS"
                                }
                            },
                            {
                                "type": "action",
                                "action": {
                                    "type": "postback",
                                    "label": "Backup",
                                    "data": "Backup",
                                    "displayText": "Backup"
                                }
                            },
                            {
                                "type": "action",
                                "action": {
                                    "type": "postback",
                                    "label": "AWS移行",
                                    "data": "Migration",
                                    "displayText": "Migration"
                                }
                            },
                            {
                                "type": "action",
                                "action": {
                                    "type": "postback",
                                    "label": "アプリ開発",
                                    "data": "app",
                                    "displayText": "app"
                                }
                            },
                            {
                                "type": "action",
                                "action": {
                                    "type": "postback",
                                    "label": "監視・運用",
                                    "data": "operation",
                                    "displayText": "operation"
                                }
                            },
                            {
                                "type": "action",
                                "action": {
                                    "type": "postback",
                                    "label": "VPN・専用線",
                                    "data": "VPN",
                                    "displayText": "VPN"
                                }
                            },
                            {
                                "type": "action",
                                "action": {
                                    "type": "postback",
                                    "label": "鳴子",
                                    "data": "NARUKO",
                                    "displayText": "NARUKO"
                                }
                            }
                        ]
                    }
                }
            ]
        elif (event['events'][0]['postback']['data'] == "AWS"):
            message = [
                {
                    "type": "text",
                    "text": "サービスサイトにご案内します!"
                },
                {
                    "type": "text",
                    "text": "https://xp-cloud.jp/service/#aws_consul"
                }
            ]
        elif (event['events'][0]['postback']['data'] == "Backup"):
            message = [
                {
                    "type": "text",
                    "text": "サービスサイトにご案内します!"
                },
                {
                    "type": "text",
                    "text": "https://xp-cloud.jp/service/#cloud"
                }
            ]
        elif (event['events'][0]['postback']['data'] == "Migration"):
            message = [
                {
                    "type": "text",
                    "text": "サービスサイトにご案内します!"
                },
                {
                    "type": "text",
                    "text": "https://xp-cloud.jp/service/#cloudmigration"
                }
            ]
        elif (event['events'][0]['postback']['data'] == "app"):
            message = [
                {
                    "type": "text",
                    "text": "サービスサイトにご案内します!"
                },
                {
                    "type": "text",
                    "text": "https://xp-cloud.jp/service/#application"
                }
            ]
        elif (event['events'][0]['postback']['data'] == "operation"):
            message = [
                {
                    "type": "text",
                    "text": "サービスサイトにご案内します!"
                },
                {
                    "type": "text",
                    "text": "https://xp-cloud.jp/service/#network"
                }
            ]
        elif (event['events'][0]['postback']['data'] == "VPN"):
            message = [
                {
                    "type": "text",
                    "text": "サービスサイトにご案内します!"
                },
                {
                    "type": "text",
                    "text": "https://xp-cloud.jp/service/#monitoring"
                }
            ]
        elif (event['events'][0]['postback']['data'] == "NARUKO"):
            message = [
                {
                    "type": "text",
                    "text": "サービスサイトにご案内します!"
                },
                {
                    "type": "text",
                    "text": "https://xp-cloud.jp/naruko/"
                }
            ]
        elif (event['events'][0]['postback']['data'] == "keyword"):
            message = [
                {
                    "type": "text",
                    "text": "「検索」の後にスペースを入れてキーワードを入力してください。\n\n例)検索 DynamoDB\n\nキーワードにスペースが含まれる場合にはAND検索となります。"
                }
            ]
    elif (event['events'][0]['type'] == "message"):
        if (event['events'][0]['message']['type'] == "image"):
            geturl = "https://api.line.me/v2/bot/message/" + str(event['events'][0]['message']['id']) + "/content"
            getmethod = "GET"
            request = urllib.request.Request(geturl, method=getmethod, headers=headers)
            with urllib.request.urlopen(request) as res:
                body = res.read()
                detect = rekognition.detect_labels(
                    Image={
                        "Bytes": body
                    }
                )
                labels = detect['Labels']
                print (labels)
                names = [n.get('Name') for n in labels]
                if ("Human" in names or "Person" in names):
                    response = rekognition.recognize_celebrities(
                        Image={
                            "Bytes": body
                        }
                    )
                    print (response)
                    if (len(response['CelebrityFaces']) == 0):
                        message = [
                            {
                                "type": "text",
                                "text": "人物を特定できませんでした。\n画像を変えてお試しください。"
                            }
                        ]
                    else:
                        if (len(response['CelebrityFaces'][0]['Urls']) != 0):
                            resurl = response['CelebrityFaces'][0]['Urls'][0]
                            resname = response['CelebrityFaces'][0]['Name']
                            message = [
                                {
                                    "type": "text",
                                    "text": resname
                                },
                                {
                                    "type": "text",
                                    "text": resurl
                                }
                            ]
                        else:
                            message = [
                                {
                                    "type": "text",
                                    "text": response['CelebrityFaces'][0]['Name']
                                }
                            ]
                else:
                    count = 0
                    message = []
                    for num in range(len(labels)):
                        if (count < 5):
                            response = translate.translate_text(
                                Text=labels[num]['Name'],
                                SourceLanguageCode='en',
                                TargetLanguageCode='ja'
                        )
                            messages = {
                                "type": "text", 
                                "text": response['TranslatedText']
                            }
                            message.append(messages)
                            count += 1
        else:
            if (event['events'][0]['message']['text'] == "案内"):
                message = [
                    {
                        "type": "text",
                        "text": "こんにちは!ご用件をお伺いできますでしょうか?",
                        "quickReply": {
                            "items": [
                                {
                                    "type": "action",
                                    "action": {
                                        "type": "postback",
                                        "label": "blog",
                                        "data": "blog",
                                        "displayText": "blog"
                                    }
                                },
                                {
                                    "type": "action",
                                    "action": {
                                        "type": "postback",
                                        "label": "service",
                                        "data": "service",
                                        "displayText": "service"
                                    }
                                }
                            ]
                        }
                    }
                ]
            elif (re.match("検索", event['events'][0]['message']['text'])):
                keywords = event['events'][0]['message']['text'].split()
                if (len(keywords) > 1):
                    keywords.remove("検索")
                    keyword = "+".join(keywords)
                    result = "https://xp-cloud.jp/blog/?s=" + str(keyword)
                    message = [
                        {
                            "type": "text",
                            "text": "検索結果をご案内します。"
                        },
                        {
                            "type": "text",
                            "text": result
                        }
                    ]
                else:
                    message = [
                        {
                            "type": "text",
                            "text": "入力を正しく処理できませんでした。\n「検索」の後にスペースを入れてキーワードを入力してください。\n\n例)検索 DynamoDB"
                        }
                    ]
            elif (event['events'][0]['message']['text'] == "こんにちは"):
                message = [
                    {
                        "type": "text",
                        "text": "こんにちは!"
                    }
                ]
            else:
                message = [
                    {
                        "type": "text",
                        "text": "ご利用の際には「案内」と入力してください。"
                    }
                ]
    else:
        message = [
            {
                "type": "text",
                "text": "申し訳ございません。ご入力を正しく受け取ることができませんでした。お手数ですが再度ご入力いただけますでしょうか。"
            }
        ]
    params = {
        "replyToken": event['events'][0]['replyToken'],
        "messages": message
    }
    request = urllib.request.Request(url, json.dumps(params).encode("utf-8"), method=method, headers=headers)
    with urllib.request.urlopen(request) as res:
        body = res.read()
    return 0

 

ベースとなるスクリプトは同じものとなっておりますので、詳細は前回の記事をご参照ください。

xp-cloud.jp

 

追加したのは画像が投稿された部分の処理となります(218~282行目)。

 

LINEで画像が投稿されると以下のようなイベントでLambda関数がトリガーされます。

 

{
  'events': [
    {
      'type': 'message', 
      'replyToken': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 
      'source': {
        'userId': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 
        'type': 'user'
      }, 
      'timestamp': 1234567890123, 
      'message': {
        'type': 'image', 
        'id': '12345678901234', 
        'contentProvider': {
          'type': 'line'
        }
      }
    }
  ],
  'destination': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}

 

ここで重要になってくるのがmessageに含まれる「id」の値となります。

 

LINEに画像が投稿されると、画像はLINEサービス上に保存されます。

Lambdaで画像データを処理する場合には、LambdaからLINEサービス上の画像をダウンロードする必要があります。

 

画像は以下のようなURLに対して認証情報付きのGETリクエストを送ることで取得可能です。

 

https://api.line.me/v2/bot/message//content

 

ダウンロードした画像はバイナリ形式のデータとなっており、特に変換等することなくAWSサービスで利用可能です。

今回は画像をS3等に保存することなく、バイナリ形式のまま他のAWSサービスと連携させています。

 

ダウンロードデータに対してdetect_labels APIを実行し、「Human」または「Person」のラベルが含まれるものを有名人検索に回しています。

 

最初はdetect_labelsではなく、detect_faces APIを利用して画像に人が含まれるかを確認しようかと思いましたが、実際に試してみると、動物の画像などでも顔として認識するケースがあった為、ラベルでの判定を採用しました。

 

画像に人物が含まれる場合には、recognize_celebrities APIを実行し、有名人が見つかればその情報をレスポンスとして返しています。

 

画像に人物が含まれていない場合には、スコアの高い5つのラベル情報をAmazon Translateに渡して、翻訳したラベルをレスポンスとして返しています。

 

動作確認

 

まず初めに有名人の画像を投稿してみます。

 

※実際の投稿画像にはモザイクはかかっていません。

 

有名人が見つかるとその結果を返しています。

 

レスポンスのパターンとしては2種類あり、名前のみ登録されているパターンと有名人の情報が書かれたサイトURLが含まれているパターンがあります。

 

今回のレスポンスは名前+URLとなっております。

 

この有名人検索ですが、結構な有名人でないと判定できず、また、類似人物の検索(そっくりさん)も探すことはできませんでした。

Amazon Rekognitionの精度の高さの現れですが、似ている芸能人とかを探してみたいと思っていたので少し残念でした。

 

なお、有名人として登録されていない場合には以下のようなレスポンスが返ってきます。

 

 

Amazno Rekognitionの有名人として登録されていない個人などを特定したい場合には、コレクションとしてAmazon Rekognitionに情報を登録する必要があるようです。

 

 

次に人物以外の画像を投稿してみます。

 

 

ラベルの単語情報をAmazon Translateで翻訳しているので少し変な日本語となる場合もありますが、正しくラベル情報を取得することができています。

 

画像内の重要な情報を表示(今回でいえば猫とか)するわけではなく、単純にスコアの高いラベル情報を表示しているのみなので、画像を変えても類似したレスポンスとなる場合があります。

 

まとめ

 

LINE BotAmazon Rekognitionを連携した画像解析Botとしての機能をご紹介しました。

 

今回はAmazon Rekognitionの標準的な機能のみを利用して簡易的なBotを構築しましたが、同じような仕組みで、商品案内などを行うカスタマー向けのBotも開発することが可能かと思われます。

 

例えば、カスタマーの投稿に基づいて類似商品を紹介したり、有名人に関連するノベルティやイベント情報を案内するなど、様々な用途に活用できるかと思われます。

 

Amazon Rekognitionを利用すれば簡単に画像解析の仕組みをLINE Botに組み込むことが可能なので、興味のある方はぜひ試してみてください。

 

  このブログの著者