Google Cloud Vision APIを使って画像認識してくれるLINE Botを作る

GCPに用意されているVision APIは画像を投げると各種画像解析（ラベリングや文字の読み取り、顔検出等）を行ってくれます。
今回はLINE Botと組み合わせて簡単な画像認識Botを作りました。

##できたもの
LINEのトークルームで画像をアップロードすると、何が写っているのかを確からしさと一緒に返してくれます。
（今回は最大3つまでの候補を返します。）
![linebot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1264471/432c2c0a-ae72-f623-d855-c12b515c7b68.gif)

※某赤い彗星っぽい人は無視してください。

##作り方
動きを見ていただいたところで作り方を解説していきます。

###準備① LINE Bot
LINE DevelopersでBotアカウントを作ります。
以下の記事を参考にしました。
[1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)

###準備② Vision API
GCPでVision APIを使えるようにします。
こちらも参考記事があります。
[GCPのCloud Vision APIを使ってみた](https://qiita.com/popy1017/items/78cd72cd6337973c3b65)

###Botアプリケーションの作成
今回はNode.js環境でサーバー側のBotアプリケーションを作成しました。

使用した環境は以下の通りです。
node.js v15.12.0
axios v0.21.1
Line/bot-sdk v7.3.0
express v4.17.1

完成したコードは以下の通りです。（長いのでセクションにして畳んでいます。）
<details><summary>**コード全文**</summary><div>

```JavaScript
'use strict';

// ########################################
//               初期設定など
// ########################################

// パッケージを使用します
const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const fs = require('fs');

// ローカル（自分のPC）でサーバーを公開するときのポート番号です
const PORT = process.env.PORT || 3000;

// Messaging APIで利用するクレデンシャル（秘匿情報）です。
const config = {
  channelSecret: '[自分のチャネルシークレット]',
  channelAccessToken: '[自分のチャネルアクセストークン]'
};

//Google Cloud Vision API用のキー
const apiKey = "[自分のAPIキー]";

//画像保存用パス
const downloadPath = './image.jpg';

//Base64エンコード関数
function toBase64(imagePath) {
  const imageFile = fs.readFileSync(imagePath);
  return Buffer.from(imageFile).toString("base64");
}

// 返信用関数
const imagetovisionapi = async (event) => {
      // 「リプライ」を使って先に返事しておきます      
      await client.replyMessage(event.replyToken, {
          type: 'text',
          text: 'すまない。少し待ってもらえるかな'
      });
      
      let pushText = '';
      try {
          // 送られてきた画像を取得
        let getContent = await downloadContent(event.message.id, downloadPath);

        let visionApiUrl = `https://vision.googleapis.com/v1/images:annotate?key=${apiKey}`;//VisionAPIのURL
        let options = {
          requests: [
          {
            image: {
            content: toBase64(downloadPath),//画像はBase64にエンコードする
          },
           features: [
               {
                  type: "LABEL_DETECTION",
                  maxResults: 3//結果を3つまで返す
                },
              ],
            },
          ],
        };

        //VisionAPIを叩いて画像を解析
        let result = await axios.post(visionApiUrl, options);
        console.log(result.data.responses[0].labelAnnotations);

        //返信用にテキストを成型
        for(let item of result.data.responses[0].labelAnnotations) {
          pushText = pushText + String(item.score * 100).substring(0, 2) + "%の確率で" + item.description + "\n";
        }
        pushText = pushText + "だな。";
        
      } catch (error) {
          pushText = 'エラーが発生したようだ。';
          // APIからエラーが返ってきたらターミナルに表示する
          console.error(error);
      }

      // 「プッシュ」で後からユーザーに通知
      return client.pushMessage(event.source.userId, {
          type: 'text',
          text: pushText
      });
};

//ダウンロード関数
function downloadContent(messageId, downloadPath) {
    return client.getMessageContent(messageId)
      .then((stream) => new Promise((resolve, reject) => {
        const writable = fs.createWriteStream(downloadPath);
        stream.pipe(writable);
        stream.on('end', () => resolve(downloadPath));
        stream.on('error', reject);
      }));
  }


// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################

// LINE SDKを初期化
const client = new line.Client(config);

// LINEサーバーからのWebhookに反応
async function handleEvent(event) {
    // 受信したWebhookが「イメージ以外」であれば画像を送信するように示唆
    if (event.type !== 'message' || event.message.type !== 'image') {
      return client.pushMessage(event.source.userId, {
          type: 'text',
          text: '画像を送って欲しい'
      });
    }
    // 返信用の関数を実行
    return imagetovisionapi(event);
}



// ########################################
//          Expressによるサーバー部分
// ########################################

// expressを初期化します
const app = express();

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて処理
app.post('/webhook', line.middleware(config), (req, res) => {
  
  // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行
  if (req.body.events.length === 0) {
    res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答
    console.log('検証イベントを受信しました！'); // ターミナルに表示
    return; // これより下は実行されない
  } else {
    // 通常のメッセージなど … Webhookの中身を確認用にターミナルに表示
    console.log('受信しました:', req.body.events);
  }

  // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理し、
  // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返す
  Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開
// （環境によってはローカルネットワーク内にも公開される）
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);
```
</div></details>

##解説
ここからは、個人的に難しかった部分を解説していきます。

###トークルームで送信された画像の取得
LINE Botのトークルームでユーザーが送信した画像はLINEのサーバーに保管されます。
指定されたURLにメッセージIDをパラメーターに設定してGETリクエスト投げることで画像を取得できます。

[公式ドキュメント](https://developers.line.biz/ja/reference/messaging-api/#wh-image)

以下の記事を参考に、取得した画像を一旦サーバーローカルに保存しています。

[LINE BOTに送った画像をNode.jsで受信して保存するサンプル](https://qiita.com/n0bisuke/items/282ed2bd70447984c4d9)

ソース上だと以下の関数を用意しています。

```JavaScript
//ダウンロード関数
function downloadContent(messageId, downloadPath) {
    return client.getMessageContent(messageId)
      .then((stream) => new Promise((resolve, reject) => {
        const writable = fs.createWriteStream(downloadPath);
        stream.pipe(writable);
        stream.on('end', () => resolve(downloadPath));
        stream.on('error', reject);
      }));
}
```

この関数にメッセージIDと保存先のパスを渡すと送信された画像を保存してくれます。

###Vision APIの利用
Vison APIの実装には幾つかのパターンがあるのですが、今回は
**・REST APIを利用**
**・認証はAPIキー**
**・画像の渡し方はBase64にエンコードしてパラメーターに設定**
でやりました。

実際のソースコードはこんな感じ

```JavaScript
        let visionApiUrl = `https://vision.googleapis.com/v1/images:annotate?key=${apiKey}`;//VisionAPIのURL
        let options = {
          requests: [
          {
            image: {
            content: toBase64(downloadPath),//画像はBase64にエンコードする
          },
           features: [
               {
                  type: "LABEL_DETECTION",
                  maxResults: 3//結果を3つまで返す
                },
              ],
            },
          ],
        };

        //VisionAPIを叩いて画像を解析
        let result = await axios.post(visionApiUrl, options);
```

これで最大3つまで候補が返ってくるので、それを抽出して成型しています。

```JavaScript
        //返信用にテキストを成型
        for(let item of result.data.responses[0].labelAnnotations) {
          pushText = pushText + String(item.score * 100).substring(0, 2) + "%の確率で" + item.description + "\n";
        }
        pushText = pushText + "だな。";
```

##最後に
今回作ったBotですが、送る画像によって色んな答えが返ってくるのが楽しくて結構遊べてます。
**（因みに、Vison APIは画像*処理で1000回/月までは無料です。）**
次は返ってきたラベル候補を日本語化していきたいですねー。