LINE MessagingAPIとIBM Visual Recognitionで画像認識Bot作ってみた！

AngelHackOsaka2018(https://angelhackosaka2018.peatix.com )というハッカソンイベントに運営スタッフとして参加しました．深夜にかけて時間の余裕があったので”ひとりハッカソン”と称して，協賛企業さんのAPIを使ってサービスを作ってみたのでご紹介します．

# 構想
- 画像処理のエンジニア → AIやりたい
- 香川県出身 → うどん
- モバイル → LINEをプラットホームに．

# 目標
- LINEでBotを作り，画像を投稿すると，AI処理した結果を返す （ミニマムサクセス）
- Webで適当に集めた画像でクラス分類問題を学習し，Webサービスとして提供（ミドルサクセス）
- うどん画像から店名と値段を推定 （フルサクセス）

# 結果
- ミニマムサクセスまで達成しました．
![Capture+_2018-06-17-12-38-15.png](https://qiita-image-store.s3.amazonaws.com/0/264957/246e7635-45f5-493e-8490-3ada82edccda.png)
- 時間切れでそれ以降の構想は開発断念．LineBotのタイトルが”うどん鑑定ごっこ”になっているのは断念の名残ですｗ

# 要素技術
- IBM Visual Recognition ・・・ 画像を送るとクラブ分類結果とその確信度を教えてくれる．３０日無料
- IBM Node-Red Starter ・・・ Webブラウザ上でブロックを配置するだけでWebアプリサービスを実装＆ローンチできるサービス．３０日無料
- LINE Messaging API ・・・ LINE上にBotを作成する．ライトユーザは基本無料．

# 手順
## 参考にすべき情報
先駆者の情報がいろいろネットに上がっているので，まずはそちらを参考に環境構築．

- LINE Messaging API → IBM Node-Red Starter ... https://simple-it-life.com/2017/08/20/line-bot/
- IBM Node-Red Starter → IBM Visual Recognition ... http://wp.tech-style.info/archives/1081

## おおまかな流れ
- とりあえず↑の２つをやる
- IBM Node-Red Starterにて，ブロック図を編集．
    - BlueMixのダッシュボードへ移動 https://console.bluemix.net/dashboard/apps/
    - Cloud Foundry アプリケーションの箇所にNode-Redが動いているレコードがあるのでクリック → ページ遷移．
    - Cloud Foundry アプリケーションの詳細，っていう画面が出るので「経路」ボタンwをクリックし，ドロップダウンメニューの一番上にあるURLを選択 → ページ遷移
    - Node-RED on IBM Cloud ，っていう画面になる．「Go to your Node-RED flow editor」をクリック．
    - Node-REDのエディター画面になる．画面右上のメニューから，読み込み→クリップボード
    - この記事末尾のコードをコピペ
    - Node-REDのエディター画面にて，それっぽいブロックのフローが表示されていればOK
- LINEのほうは変更不要
- スマホからBotに画像を投げつけると，クラス分類判定第一位の名前と確信度が返ってくるはず・・・！

# はまった点
- Node-RED
    - 画像を扱う例は多数あるものの，Web上に画像単体がUPされている例（http://test.com/test.jpg みたいな）とかブラウザからPOSTされた画像を扱う例が多数派で，LINEから画像をとってきてそれを処理する例が見当たらなかった．
    - LINE Messaging APIからPOSTされた画像の回収と，Visual RecognitionへのPOSTとを，シーケンシャルにやろうとすると，LINE Messaging APIへの応答文を作るのが難しい．２本のパラレルなルーチンを実行し，最後にmerge(＝力技で結合）するという形で実現．
- Visual Recognition
    - Web上に画像単体がUPされている例の画像を扱う，あるいは，サーバ内に画像がある場合を扱う(?)のが多数派
    - Node-Redのコード内で，画像データをgetしてmsg変数に格納し，っていう処理はあまりなかった
- LINE Messaging API
    - 画像を扱うドキュメントが少なく情報がわからん．．．Node-Redとなるとなおさら．


# 感想など
- モバイルプラットホーム⇔AIのapi，をあっさりと繋げることができて，さくっと面白いアプリを作れた印象
- 今回はデフォルトのVisualRecognitionを使ったので，一般物体が対象になった．自分で収集した画像でAIをスクラッチ(?)作成する機能も提供されているらしい．ぜひやりたい．
- Node-RedはWebサービスをさくっと作りたいときにとっても有用そうだ．
- 国内だとやっぱLINE最強．どんなユーザでも持っているので敷居が低くて◎


# Node-REDのエディター画面
![a.png](https://qiita-image-store.s3.amazonaws.com/0/264957/3a3e5d14-573e-d5f2-f217-28c0599a0e55.png)


# Node-REDのコード
```json
[{"id":"e2e8c7c9.3d94d8","type":"http in","z":"f47f9794.9e13","name":"post /image_recog","url":"/image_recog","method":"post","upload":true,"swaggerDoc":"","x":130,"y":93.99999237060547,"wires":[["794accf4.28039c"]]},{"id":"6819ac4f.32c0bc","type":"visual-recognition-v3","z":"f47f9794.9e13","name":"","apikey":"__PWRD__","vr-service-endpoint":"https://gateway.watsonplatform.net/visual-recognition/api","image-feature":"classifyImage","lang":"en","x":874.9999389648438,"y":92.99999237060547,"wires":[["eda37ad.22d5a08","3772cebc.660e02"]]},{"id":"eda37ad.22d5a08","type":"debug","z":"f47f9794.9e13","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"result","x":1037.9999389648438,"y":180,"wires":[]},{"id":"5ee73f81.59e6b8","type":"function","z":"f47f9794.9e13","name":"reserve headers","func":"msg.url = \"https://api.line.me/v2/bot/message/\" + msg.payload.events[0].message.id +'/content'\nmsg.headers  ={\"Content-Type\": \"application/json\",    \"Authorization\": \"Bearer YYm5YOlPsfTC2bzXFJwe/nhAkNuUeDaPhhxxp2+KThyOlkn4XbbftDzZ5URIf2VUIkP2umeUvEHq6UIxJDig3L/sqQSJCMmx5+NKeLfDpKlAwPaHISh6qdx4G4n+TxkuoduHvNT+2RBexJnhQ8VzpwdB04t89/1O/w1cDnyilFU=\"};\n\nreturn msg","outputs":1,"noerr":0,"x":487.86663818359375,"y":94.48332977294922,"wires":[["e648faa6.d4292"]]},{"id":"e648faa6.d4292","type":"http request","z":"f47f9794.9e13","name":"","method":"GET","ret":"bin","url":"","tls":"","x":665.8666381835938,"y":92.64995574951172,"wires":[["4d36507b.e9ce7","6819ac4f.32c0bc"]]},{"id":"4d36507b.e9ce7","type":"debug","z":"f47f9794.9e13","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":803.8666381835938,"y":181.433349609375,"wires":[]},{"id":"53d54378.3a798c","type":"function","z":"f47f9794.9e13","name":"generate header","func":"var event = msg.payload[\"events\"][0];\n//if(event[\"message\"][\"type\"] != \"text\"){\n//    return msg;\n//}\nvar message = event[\"message\"][\"text\"];\nvar replyToken = event[\"replyToken\"];\nvar replyMessage = {\"type\": \"text\", \"text\": message}\nmsg.payload = {\"messages\": [replyMessage], \"replyToken\": replyToken};\nmsg.headers  ={\"Content-Type\": \"application/json\",    \"Authorization\": \"Bearer YYm5YOlPsfTC2bzXFJwe/nhAkNuUeDaPhhxxp2+KThyOlkn4XbbftDzZ5URIf2VUIkP2umeUvEHq6UIxJDig3L/sqQSJCMmx5+NKeLfDpKlAwPaHISh6qdx4G4n+TxkuoduHvNT+2RBexJnhQ8VzpwdB04t89/1O/w1cDnyilFU=\"};\n\ncontext.global.payloadGenerated = msg.payload;\ncontext.global.headersGenerated = msg.headers;\ncontext.global.msg = msg;\n\nreturn msg","outputs":1,"noerr":0,"x":263.8666534423828,"y":281.3166809082031,"wires":[["da3949da.d249b8","33455b2a.45919c"]]},{"id":"da3949da.d249b8","type":"function","z":"f47f9794.9e13","name":"merge","func":"if(\n    (context.global.resultGenerated !== null) && (context.global.headersGenerated !== null) && (context.global.payloadGenerated !== null)\n){\n    //msg.url = null;\n    //msg.payload = context.global.payloadGenerated;\n    targetText = context.global.resultGenerated.images[0].classifiers[0].classes[0].class + \", score:\" + context.global.resultGenerated.images[0].classifiers[0].classes[0].score;\n    //msg.payload[\"messages\"] = [\"test\"];\n    //msg.headers = context.global.headersGenerated;\n    //var replyMessage = {\"type\": \"text\", \"text\": \"test test\"}\n    //msg.payload = {\"messages\": [replyMessage], \"replyToken\": replyToken};\n    //msg.payload = {\"messages\": [replyMessage], \"replyToken\": replyToken};\n    //msg.payload = context.global.payloadGenerated;\n    //msg.payload = {\"messages\": [replyMessage],  \"replyToken\": msg.payload[\"replyToken\"]};\n    //msg.payload = {\"messages\": [replyMessage],  \"replyToken\": msg.payload[\"replyToken\"]};\n    //msg.headers  ={\"Content-Type\": \"application/json\",    \"Authorization\": \"Bearer YYm5YOlPsfTC2bzXFJwe/nhAkNuUeDaPhhxxp2+KThyOlkn4XbbftDzZ5URIf2VUIkP2umeUvEHq6UIxJDig3L/sqQSJCMmx5+NKeLfDpKlAwPaHISh6qdx4G4n+TxkuoduHvNT+2RBexJnhQ8VzpwdB04t89/1O/w1cDnyilFU=\"};\n    msg = context.global.msg;\n    var replyMessage = {\"type\": \"text\", \"text\": targetText}\n    msg.payload = {\"messages\": [replyMessage],  \"replyToken\": msg.payload[\"replyToken\"]};\n\n    return msg;\n    \n}else{\n    //return msg;\n}\n\n\n\n\n","outputs":1,"noerr":0,"x":661.8666381835938,"y":445.3166809082031,"wires":[["ccb7c49.4b08a38","9ca59f7e.5be838"]]},{"id":"3772cebc.660e02","type":"function","z":"f47f9794.9e13","name":"reserve msg.result","func":"context.global.resultGenerated = msg.result;\nreturn msg;","outputs":1,"noerr":0,"x":1106.86669921875,"y":95.31664276123047,"wires":[["fee60f45.d04228","da3949da.d249b8"]]},{"id":"ccb7c49.4b08a38","type":"http request","z":"f47f9794.9e13","name":"LINE REPLY API実行","method":"POST","ret":"txt","url":"https://api.line.me/v2/bot/message/reply","tls":"","x":874.8666229248047,"y":445.4833679199219,"wires":[["ee18b3b6.a93838"]]},{"id":"ee18b3b6.a93838","type":"debug","z":"f47f9794.9e13","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1035.8666229248047,"y":515.1832885742188,"wires":[]},{"id":"9ca59f7e.5be838","type":"debug","z":"f47f9794.9e13","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":778.8666381835938,"y":514.1832885742188,"wires":[]},{"id":"fee60f45.d04228","type":"debug","z":"f47f9794.9e13","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":1144.86669921875,"y":249.183349609375,"wires":[]},{"id":"794accf4.28039c","type":"function","z":"f47f9794.9e13","name":"global","func":"context.global.resultGenerated = null; \ncontext.global.headersGenerated = null; \ncontext.global.payloadGenerated = null;\n\nreturn msg;","outputs":1,"noerr":0,"x":301.86663818359375,"y":93.73332977294922,"wires":[["5ee73f81.59e6b8","53d54378.3a798c"]]},{"id":"33455b2a.45919c","type":"debug","z":"f47f9794.9e13","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":397.86663818359375,"y":347.183349609375,"wires":[]}]
```