AI
Watson
node-red
linebot

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
  • 時間切れでそれ以降の構想は開発断念.LineBotのタイトルが”うどん鑑定ごっこ”になっているのは断念の名残ですw

要素技術

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

手順

参考にすべき情報

先駆者の情報がいろいろネットに上がっているので,まずはそちらを参考に環境構築.

おおまかな流れ

  • とりあえず↑の2つをやる
  • 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への応答文を作るのが難しい.2本のパラレルなルーチンを実行し,最後に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

Node-REDのコード

[{"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":[]}]