[GoogleAppsScript] 画像解析LINE-BOTを作成する

# はじめに
LINEとGoogle Cloud Vision API を使用して 画像解析を行うBOTを作成しました。
![cv_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/2fc50f99-c93d-d5b5-d62b-64caf902464f.png)

![cv_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/efcff4ed-19f8-8714-e4ea-17597ae128fb.png)

# Google Cloud Vision API
- Google Cloud Platform(GCP)が提供する画像認識サービスです。
- GCPで新しくプロジェクトして、`Cloud Vision API`を有効にしてトークンを発行します。

# 事前準備
[詳しくはこちら](https://qiita.com/yun_bow/items/33879bca8f82d1ac6cfa)

1. GoogleDriveにてGoogleAppsScriptを開いて、以下のスクリプトを実装します。

- `LINE_CHANNEL_TOKEN`にLINE Messaging APIで発行したトークンを指定します。
- `GOOGLE_API_KEY`にGoogle Cloud Vision APIで発行したトークンを指定します。
- `SSID`にスプレッドシートのIDと`SSN_USER`にシート名を指定します。

```JavaScript:main.gs
/**
 * 画像解析BOT
 */
const LINE_CHANNEL_TOKEN = '*****'; // LINE NOTIFYのアクセストークン
const GOOGLE_API_KEY = '*****';
const SSID = '*****';
const SSN_USER = 'user';
```

# フォロー登録
[詳しくはこちら](https://qiita.com/yun_bow/items/33879bca8f82d1ac6cfa)

# フォロー解除
[詳しくはこちら](https://qiita.com/yun_bow/items/33879bca8f82d1ac6cfa)

# リプライ
ユーザーが画像を入力すると`LINE Messaging API`からGASにPOSTが通知されます。GASでは、画像を取得して、GoogleCloudVisionに解析依頼を行います。その後、解析結果を`LINE Messaging API`にPOSTで返します。

```plantuml
@startuml
actor ユーザー
ユーザー -> LINEアプリ: 画像入力
activate LINEアプリ
LINEアプリ -> "LINE Messaging API"
deactivate LINEアプリ
activate "LINE Messaging API"
  "LINE Messaging API" -> GoogleAppsScript: ...POST (type: message)
deactivate "LINE Messaging API"
  activate GoogleAppsScript
    GoogleAppsScript -> "LINE Messaging API": GET /message/id/content
activate "LINE Messaging API"
GoogleAppsScript <-- "LINE Messaging API": ...画像バイナリファイル
deactivate "LINE Messaging API"
GoogleAppsScript -> GoogleCloudVision: GET /images:annotate
activate GoogleCloudVision
GoogleAppsScript <- GoogleCloudVision: 画像解析結果
deactivate GoogleCloudVision
    GoogleAppsScript -> "LINE Messaging API": POST /message/reply
  deactivate GoogleAppsScript
activate "LINE Messaging API"
"LINE Messaging API" -> LINEアプリ
deactivate "LINE Messaging API"
activate LINEアプリ
LINEアプリ -> LINEアプリ: 画像解析結果を表示
deactivate LINEアプリ
@enduml
```

```JavaScript:main.gs
/**
 * メッセージイベント処理
 * @param {Object} reqEvent 
 */
function executeMessage(reqEvent) {
    let msgList = [];
    let user = getUser(reqEvent.source.userId);
    if (user) {
        switch (reqEvent.message.type) {
            case 'image':
                let content = getLineContent(reqEvent.message.id);
                let result = getAnnotate(content.getBlob());
                console.log(result);

                for (let i in result.responses) {
                    let retObj = result.responses[i];
                    for (let key of Object.keys(retObj)) {
                        let msg = null;
                        let value = retObj[key];
                        switch (key) {
                            case 'webDetection':
                                msg = getMsgWebDetection(value);
                                break;
                            case 'labelAnnotations':
                                msg = getMsgLabelAnnt(value);
                                break;
                            case 'textAnnotations':
                                msg = getMsgTextAnnt(value);
                                break;
                            case 'landmarkAnnotations':
                                msg = getMsglandmarkAnnt(value);
                                break;
                            case 'logoAnnotations':
                                msg = getMsglogoAnnt(value);
                                break;
                        }
                        if (msg) {
                            msgList.push({
                                'type': 'text',
                                'text': msg,
                            });
                        }
                    }
                }
                if (0 < msgList.length) {
                    console.log(JSON.stringify(msgList).replace('\n', ''));
                    sendLineReply(reqEvent.replyToken, msgList);
                }
                break;
        }
    }
}

/**
 * WEB検出のメッセージを取得する
 * @param {Object} value 
 */
function getMsgWebDetection(value) {
    let msg = `【WEBの検出】\n\n`;
    for (let i in value.webEntities) {
        let item = value.webEntities[i];
        msg += `- ${LanguageApp.translate(item.description, 'en', 'ja')}\n`;
    }
    for (let key of Object.keys(value)) {
        switch (key) {
            case 'fullMatchingImages':
                msg += `\n<< 完全一致 >>\n`;
                for (let j in value[key]) {
                    let item = value[key][j];
                    msg += `[${parseInt(j) + 1}] ${item.url}\n`;
                }
                break;
            case 'partialMatchingImages':
                msg += `\n<< 部分一致 >>\n`;
                for (let j in value[key]) {
                    let item = value[key][j];
                    msg += `[${parseInt(j) + 1}] ${item.url}\n`;
                }
                break;
            case 'visuallySimilarImages':
                msg += `\n<< 類似 >>\n`;
                for (let j in value[key]) {
                    let item = value[key][j];
                    msg += `[${parseInt(j) + 1}] ${item.url}\n`;
                }
                break;
        }
    }
    return msg;
}

/**
 * ラベル検出のメッセージを取得する
 * @param {Array} value 
 */
function getMsgLabelAnnt(value) {
    let msg = '【ラベルの検出】\n\n';
    for (let i in value) {
        let item = value[i];
        msg += `- ${LanguageApp.translate(item.description, 'en', 'ja')}\n`;
    }
    return msg;
}

/**
 * 文字検出のメッセージを取得する
 * @param {Array} value 
 */
function getMsgTextAnnt(value) {
    return `【文字の検出】\n\n${value[0].description}\n`;
}

/**
 * 場所検出のメッセージを取得する
 * @param {Array} value 
 */
function getMsglandmarkAnnt(value) {
    let msg = '【場所の検出】\n\n';
    for (let i in value) {
        let item = value[i];
        msg += `- ${LanguageApp.translate(item.description, 'en', 'ja')}\n`;
        for (let j in item.locations) {
            let loc = item.locations[j];
            msg += `https://www.google.com/maps?q=${loc.latLng.latitude},${loc.latLng.longitude}\n`;
        }
        msg += `\n`;
    }
    return msg;
}

/**
 * ロゴ検出のメッセージを取得する
 * @param {Array} value 
 */
function getMsglogoAnnt(value) {
    let msg = '【ロゴの検出】\n\n';
    for (let i in value) {
        let item = value[i];
        msg += `- ${item.description}\n`;
    }
    return msg;
}

/**
 * Googleに画像を送信する
 * @param {Object} file ファイル
 */
function getAnnotate(file) {
    let url = `https://vision.googleapis.com/v1/images:annotate?key=${GOOGLE_API_KEY}`;
    let options = {
        'method': 'get',
        'headers': {
            'Content-Type': 'application/json; charset=UTF-8',
        },
        'payload': JSON.stringify({
            requests: [{
                image: {
                    content: Utilities.base64Encode(file.getBytes())
                },
                features: [{
                        type: 'WEB_DETECTION', // WEBの検出
                        maxResults: 5
                    },
                    {
                        type: 'LABEL_DETECTION', // ラベルの検出
                        maxResults: 5
                    },
                    {
                        type: 'TEXT_DETECTION', // 文字の検出
                        maxResults: 5
                    },
                    {
                        type: 'LANDMARK_DETECTION', // 場所の検出
                        maxResults: 5
                    },
                    {
                        type: 'LOGO_DETECTION', // ロゴ検出
                        maxResults: 5
                    },
                ],
            }]
        })
    };
    let response = UrlFetchApp.fetch(url, options);
    return JSON.parse(response.getContentText('UTF-8'));
}

/**
 * LINEからコンテンツを取得する
 * @param {String} messageId メッセージID
 */
function getLineContent(messageId) {
    let url = `https://api.line.me/v2/bot/message/${messageId}/content`;
    let options = {
        'method': 'get',
        'headers': {
            'Content-Type': 'application/json; charset=UTF-8',
            'Authorization': `Bearer ${LINE_CHANNEL_TOKEN}`
        }
    };
    return UrlFetchApp.fetch(url, options);
}
```
- `execute`関数では、LINE Messaging APIからの通知内容により、処理を振り分けしています。`message`の場合は、メッセージの内容を解析します。
- `getLineContent`関数でLINEに入力された画像を取得して、`getAnnotate`関数でGoogle Cloud Vision APIに画像を送信します。[検出できる内容はこちら](https://cloud.google.com/vision/docs/how-to)。
- 検出した内容ごとにメッセージを作成して`sendLineReply`関数でLINE Messaging APIにPOSTします。

# 参考リンク
- [【Rails】Cloud Vision APIを用いて画像解析し、過激な画像を検知する方法](https://qiita.com/matsubishi5/items/d2d8f9913bcec97904a1)
- [LINE BOTからAWS Lambda介してGoogle Cloud Vision APIを呼ぶPython](https://qiita.com/kurimoto/items/243a77b345b76848fdc4)
- [LINE APIが公開されたのでGoogle Cloud Vision使って画像解析しちゃうBOTをAWS Lambdaで作ってみた話](https://qiita.com/dkawashi/items/4329d8ec085678b346e4)
- [Python+Cloud Vision APIでOCR、Bounding Boxまで！](https://qiita.com/CJHJ/items/52c15ff636c52e93284d)
- [【初心者向け】【Ruby】Google画像認識で、APIの叩き方を学ぶ](https://qiita.com/moriwm77/items/6083f0a276ff1e920868)

# さいごに
ソースコードをGitHubに公開しています。

- [ソースファイルはこちら](https://github.com/yunbow/GoogleAppsScript-LINE-CloudVision)

以上です。