はやい！やすい！うまい！Custom Vision と LINE bot でお寿司の判定をしてみた

#はじめに
MicrosoftのCustomVisionというサービスで、
はやい！（試行錯誤含めて3時間くらい）
やすい！（無料アカウント）
うまい！（お寿司はおいしい）
で、めちゃくちゃ気軽に機会学習ができるとのことなので、LINEbotを作ってみました。

# 事前準備
## CustomVisionの登録

[MicrosoftアカウントのOutlookメールアドレスを取得する方法](https://qiita.com/TomoyoH/items/fca3f49729d82d9884eb)
[Microsoft Learn | Microsoft Docs](https://docs.microsoft.com/ja-jp/learn/) にアクセスしてサインインします

## CustomVisionの準備
学習用の画像を準備します 
google-images-downloadを使うとそれ用のフォルダを勝手に作ってダウンロードしてくれるのでめっちゃ便利でした
<img width="1189" alt="スクリーンショット 2019-08-28 21.33.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/9defb3ad-59bd-5e59-4490-a16c18f305a8.png">

参考 -> [LINE動物図鑑の作り方](https://qiita.com/iizuka2019/items/e3ac6942a0f9138f561e) 

画像が準備できたらこちらのトレーニングを参考に画像判定用のモデルを作成します
[こちら](https://docs.microsoft.com/ja-jp/learn/modules/classify-images-with-custom-vision-service/index) （ただし、うまくAPIを発行できない場合があるので事前に[こちら](https://protoout.studio/posts/custom-vision-project-publish-error)を参考に、事前にAzure Portal側でCustom Vision用リソースグループやCustom Visionリソースを作っておくと上手くいきました（2019/08/29 現在））

今回は、「お寿司屋さんで写真を取ってLINEbotに送る」ことを想定してたので、そういう感じの画像に選別します
~~だんだん、「これは本当にマグロなのか？マグロとはなんなのか？」とゲシュタルト崩壊してきて興味深いです~~

タグを登録し、先ほどダウンロードした画像たちをアップロードします
![スクリーンショット 2019-08-28 23.03.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/d48c849e-a8ac-b015-705f-7bdff9ce61d1.png)

登録し終えたら、学習させます。
テストしてみると、イクラはかなりの精度で判定できましたが、
![スクリーンショット 2019-08-28 23.04.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/7c7f8b27-2d07-798d-1207-c4cf996af85f.png)
ハマチは少し微妙でした。
![スクリーンショット 2019-08-28 23.05.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/1246c4d5-b822-1cba-2a22-7e0af14fb105.png)

準備できたらpublishします
![スクリーンショット 2019-08-28 23.05.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/cd687ad4-4dd2-81bd-5c95-8b3837a7c921.png)

Prediction APIをクリックすると以下のようなポップアップが表示されます
今回使うのは赤枠の部分の**URL**と**Prediction-key**の二つなので控えておきます
![スクリーンショット 2019-08-28 23.06.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/1233c15f-b3fb-e3f4-6b38-35d48c6f6afc.png)


他参考
https://azure-recipe.kc-cloud.jp/2017/12/custom_vision_2017adcal/
https://azure.microsoft.com/ja-jp/services/cognitive-services/custom-vision-service/

## 実装
### LINEbotの準備
[1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)を参考にLINEのbotの作成を行います
APIは個人で使う想定（金額的な意味で）なのでBotはngrokを使って公開します

```javascript

'use strict';

//必要なものは都度 npm i してください
const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3002;
const cv = require('customvision-api');
const fs = require('fs');
const bodyParser = require('body-parser');
const Request = require('request');

const line_config = {
    channelSecret: 'botのチャンネルシークレット',
    channelAccessToken: 'botのアクセストークン'
};

const app = express();
const client = new line.Client(line_config);

app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
app.post('/webhook', line.middleware(line_config), (req, res) => {
    console.log(req.body.events);

    // botに送られる画像にアクセスするための設定
    const options = {
        url: `https://api.line.me/v2/bot/message/${req.body.events[0].message.id}/content`,
        method: 'get',
        headers: {
            'Authorization': 'Bearer ' + line_config.channelAccessToken,
        },
        encoding: null
    };
    Request(options, function(error, response, body) {
        if (!error && response.statusCode == 200) {
            //画像を保存します
            fs.writeFileSync(`/tmp/` + req.body.events[0].message.id + `.jpg`, new Buffer(body), 'binary');
            console.log('file saved');
            const filePath = `/tmp/` + req.body.events[0].message.id + `.jpg`;
            const cv_config = {
                "predictionEndpoint": "先ほどpublishしたPrediction APIのURL",
                "predictionKey": "先ほどpublishしたPrediction APIのpredictionKey"
            };

            cv.sendImage(
                filePath,
                cv_config,
                (data) => {
                    console.log(data); 
                    // => 出力結果
                    //   { id: 'xxxxxxxxxxxxxxxxxxxxxxxxx',
                    //   project: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
                    //   iteration: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
                    //   created: '2019-08-28T15:01:24.855Z',
                    //   predictions:
                    //    [ { probability: 0.833851159,
                    //        tagId: 'a5f078e9-490d-4040-afbf-6918d7744011',
                    //        tagName: 'hamachi' },
                    //      { probability: 0.09781464,
                    //        tagId: '35660c82-9df6-4511-9bd6-0e26db54ad4c',
                    //        tagName: 'maguro' },
                    //      { probability: 0.013582103,
                    //        tagId: '5d170ee1-9d02-40f6-ab8c-addeaec6cefc',
                    //        tagName: 'engawa' },
                    //      { probability: 0.011500218,
                    //        tagId: '37f42876-ce01-4cc3-8951-d7169e41b936',
                    //        tagName: 'tako' },
                    //      { probability: 0.0102164745,
                    //        tagId: 'c46ae2a3-d532-4b5f-8287-2ebd5537583d',
                    //        tagName: 'salmon' },
                    //      { probability: 0.00753958151,
                    //        tagId: 'b1a939d5-2a61-4518-90bf-dfd8542e646d',
                    //        tagName: 'hotate' },
                    //      { probability: 0.00548384944,
                    //        tagId: '0d944222-e4e5-4b95-9084-8c17c52c6ea2',
                    //        tagName: 'tobikko' },
                    //      { probability: 0.005075191,
                    //        tagId: '8f7b69ab-877e-464c-91d8-f5faf2d32633',
                    //        tagName: 'ikura' },
                    //      { probability: 0.004676578,
                    //        tagId: 'c1a43c10-6bcd-4893-b5b0-3016816e3c91',
                    //        tagName: 'ebi' },
                    //      { probability: 0.00426511373,
                    //        tagId: 'fbe51510-a6f3-4fba-a141-afa2ed6fcec0',
                    //        tagName: 'ika' },
                    //      { probability: 0.00379423774,
                    //        tagId: '730eb665-a967-444f-9a0f-7cd16527e249',
                    //        tagName: 'tamago' },
                    //      { probability: 0.00220071618,
                    //        tagId: '9e1789d3-dd45-48b9-b116-5aa8f711afec',
                    //        tagName: 'hokkigai' } ] }
                    let tagName = data.predictions[0].tagName;
                    let probability = data.predictions[0].probability;

                    client.replyMessage(req.body.events[0].replyToken, {
                        type: 'text',
                        text: tagName + 'の確率、' + probability 
                      }); 
                }
            );
        }
    })
    Promise
        .all(req.body.events.map(handleEvent))
        .then((result) => res.json(result));
})

app.listen(PORT);
console.log(`Server running at ${PORT}`);
```

めっちゃくちゃ簡単ですが、以下のように判定を返してくれました！！
<img width="375" alt="iOS の画像.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/9ae9439d-ebd8-29eb-cbcb-644aa5792e6d.png">


参考
[LINE BOTからNode.jsで画像を受け取って保存する](https://qiita.com/n0bisuke/items/17c795fea4c2b5571ce0)