[python] LINEで画像を送ると商品検索するbotを作ってみた

# :star: 概要
- LINEから画像が送信されるとMessagingAPIを使ってwebhookで画像を受け取る
- docomoの画像認識APIで画像に写っている商品を分析し、その結果をカルーセルで表示する
- docomoの画像認識APIで認識できる商品は、食品パッケージ/書籍/CD/DVD/ゲームソフト/PCソフトのみ

# :star: 実際に作ったもの

- 下のボタン、もしくは`@sga5626f`でID検索すると友達に追加できます

<a href="https://line.me/R/ti/p/%40sga5626f"><img height="36" border="0" alt="友だち追加" src="https://scdn.line-apps.com/n/line_add_friends/btn/ja.png"></a>

- 画像を送るとこんな感じで分析結果を返してくれます

<img width="375" alt="S__81625103.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/178077/f0fb3d7a-4fde-3b8a-4673-b067beb9d847.jpeg">

# :warning: 2018/7/12 追記
- docomoの画像認識APIのうち、オブジェクト認識APIは提供終了となりました
- よって、このbotはもう使えません（泣）


# :star: 事前に準備すること
- LINEのbotを用意する
- webhookを受け取るアプリを作る
- Herokuにデプロイする
- LINEとアプリを連携する

上記については前に書いた記事 [[python] LINEの文字起こし君を作ってみた](https://qiita.com/mikan3rd/items/f0b6ff57dbcedc79a22d) と同じですので割愛します

# :pencil: docomoAPIのAPIキーを取得する
- [# docomo Developer support](https://dev.smt.docomo.ne.jp/) に登録します
- APIの新規利用申請をします。必要事項を入力して、画像認識のAPIの利用申請をしましょう

![dev.smt.docomo.ne.jp__p=mypage.api.new.select&tx=5af15c5e6f2fa&rp=mypage.api.new.input(Laptop with MDPI screen).png](https://qiita-image-store.s3.amazonaws.com/0/178077/d719abc0-da40-9cee-a814-0a814e49b19e.png)

- 利用申請が完了すると、APIキーが発行されます

![dev.smt.docomo.ne.jp__p=mypage.api.index(Laptop with MDPI screen).png](https://qiita-image-store.s3.amazonaws.com/0/178077/66e2a849-7bf9-0109-f054-268ac360196a.png)

# :pencil: docomoAPIを使って画像から商品を検索する

- [# 画像認識API リファレンス](https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=image_recognition&p_name=api_1) に沿ってリクエストを送ります
- `candidates`がある場合は整形してlistの出力データを作ります
- `category`によって返ってくるデータが若干違う(例: bookの場合はmakerやbrandがない等)ので注意してください
- LINEのmessagingAPIで画像URLを送る場合、`https`しか受け付けないので`https`の場合のみ`imageUrl`を使うようにしています

```python:docomo.py

import requests

import settings

API_KEY = settings.API_KEY

endpoint = 'https://api.apigw.smt.docomo.ne.jp/imageRecognition/v1/recognize'


def search_product(image=None):
    if image is None:
        return '必要な情報が足りません'

    params = {
        'APIKEY': API_KEY,
        'recog': 'product-all',
        'numOfCandidates': 5,
    }

    headers = {"Content-Type": "application/octet-stream"}

    response = requests.post(
        endpoint,
        headers=headers,
        params=params,
        data=image,
    )

    status = response.status_code
    data = response.json()

    print(data)

    if status != 200 and status != 204:

        if status == 403:
            error = '今月に使用できる回数を超過しました'

        else:
            error = 'エラーが発生しました'

        print(status, data)
        return error

    candidates = data.get('candidates')

    if not candidates:
        return '商品が見つかりませんでした'

    results = []
    for content in candidates:
        content['detail']['maker'] = \
            content['detail'].get('maker') or  \
            content['detail'].get('publisher', '')

        content['detail']['brand'] = \
            content['detail'].get('brand') or  \
            content['detail'].get('author', '')

        imageUrl = 'https://i.vimeocdn.com/video/443809727.jpg?mw=700&mh=394'

        if content['imageUrl'].startswith('https'):
            imageUrl = content['imageUrl']

        result = {
            "thumbnail_image_url": imageUrl,
            "title": content['detail']['itemName'][:40],
            "text": "{maker}： {brand}\n発売日： {releaseDate}"
            .format(**content['detail']),
            "actions": {
                "label": "商品ページを見る",
                "uri": content['sites'][0]['url']
            }
        }
        results.append(result)

    print(results)

    return results
```

# :pencil: LINEでカルーセルテンプレートで返信する
- [# TemplateSendMessage - CarouselTemplate](https://github.com/line/line-bot-sdk-python#templatesendmessage---carouseltemplate) を参考に次のようにカルーセルテンプレートを作成します

```py

        result = search_product(image)

        if isinstance(result, str):
            messages = [
                TextSendMessage(text=result),
                TextSendMessage(text='食品パッケージ/書籍/CD/DVD/ゲームソフト/PCソフトを検索できるよ！')
            ]

        elif isinstance(result, list):

            columns = [
                CarouselColumn(
                    thumbnail_image_url=column['thumbnail_image_url'],
                    title=column['title'],
                    text=column['text'],
                    actions=[
                        URITemplateAction(
                            label=column['actions']['label'],
                            uri=column['actions']['uri'],
                        )
                    ]
                )
                for column in result
            ]

            messages = TemplateSendMessage(
                alt_text='template',
                template=CarouselTemplate(columns=columns),
            )

```

- これを`index.py`に組み込むと、こんな感じになります


```python:index.py

import os
from io import BytesIO

from flask import Flask, abort, request
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (CarouselColumn, CarouselTemplate, ImageMessage,
                            MessageEvent, TemplateSendMessage, TextMessage,
                            TextSendMessage, URITemplateAction)

import settings
from docomo import search_product

app = Flask(__name__)


line_bot_api = LineBotApi(settings.YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(settings.YOUR_CHANNEL_SECRET)


@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    # print("body:", body)

    # handle webhook body
    try:
        handler.handle(body, signature)

    except InvalidSignatureError as e:
        print("InvalidSignatureError:", e)
        abort(400)

    return 'OK'


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    # print("handle_message:", event)
    text = event.message.text

    messages = [
        TextSendMessage(text=text),
        TextSendMessage(text='画像を送ってみてね!'),
    ]

    reply_message(event, messages)


@handler.add(MessageEvent, message=ImageMessage)
def handle_image(event):
    # print("handle_image:", event)

    message_id = event.message.id
    message_content = line_bot_api.get_message_content(message_id)

    image = BytesIO(message_content.content)

    try:
        result = search_product(image)

        if isinstance(result, str):
            messages = [
                TextSendMessage(text=result),
                TextSendMessage(text='食品パッケージ/書籍/CD/DVD/ゲームソフト/PCソフトを検索できるよ！')
            ]

        elif isinstance(result, list):

            columns = [
                CarouselColumn(
                    thumbnail_image_url=column['thumbnail_image_url'],
                    title=column['title'],
                    text=column['text'],
                    actions=[
                        URITemplateAction(
                            label=column['actions']['label'],
                            uri=column['actions']['uri'],
                        )
                    ]
                )
                for column in result
            ]

            messages = TemplateSendMessage(
                alt_text='template',
                template=CarouselTemplate(columns=columns),
            )

        reply_message(event, messages)

    except Exception as e:
        print("error:", e)
        reply_message(event, TextSendMessage(text='エラーが発生しました'))


def reply_message(event, messages):
    line_bot_api.reply_message(
        event.reply_token,
        messages=messages,
    )


if __name__ == "__main__":
    port = os.environ.get('PORT', 3333)
    app.run(
        host='0.0.0.0',
        port=port,
    )

```


# :star: 完成!!
- 以上でLINEで画像を送ると商品検索するbotができました
- 正直そんなに検索に引っかかりません（涙）

# :star: 参考
- [今さらline botでdocomoAPIの画像認識を試してみたら意外に高性能だった](https://qiita.com/pentax0317/items/78c46a5ed69e0d73728c)
- [[python] LINEで画像を送ると年齢や感情を分析するbotを作った件](https://qiita.com/mikan3rd/items/7adc5987929acf748fec)