スマホで撮った写真をBluemixのNode-REDにアップロードし、Node-REDフローでWatson Visual Recognitionの顔識別機能を呼び出して、年齢・性別を判定するアプリケーションを作成します。
写真を撮影してアップロードするところまでの手順は別記事「スマホで撮った写真をNode-REDにアップロードする」で紹介します。本記事では以下の手順を紹介します。
- 写真データを圧縮する
- Watson Visual Recognitionの顔識別機能を呼び出す
- Watson Visual Recognitionの応答データをmustacheで整形してブラウザに返す
手順の説明に入る前に、写真のアップロードも含めたNode-REDフローの全体像を以下に示しておきます。
またNode-REDフローのエクスポート・データも添付します。
[{"id":"cf327624.6a5458","type":"visual-recognition-v3","z":"be2b435.a0f1b4","name":"Visual Recognition","apikey":"","image-feature":"detectFaces","x":311.3666687011719,"y":384,"wires":[["d7a95cc8.af6a98","b0c22b36.a7a388"]]},{"id":"6917833b.7470d4","type":"template","z":"be2b435.a0f1b4","name":"写真アップロード","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"<html>\n<head><title>Watson Visual Recognition on Node-RED</title></head>\n<body>\n<h1>Watson Visual Recognition on Node-RED</h1>\n<h2>Select an image file</h2>\n<form action=\"/vrimage3\" method=\"post\" enctype=\"multipart/form-data\">\n <input type=\"file\" name=\"imagedata\" accept=\"image/*\" />\n <input type=\"submit\" value=\"Analyze\"/>\n</form>\n</body>\n</html>","x":333.3666687011719,"y":72,"wires":[["e4bf8aec.e044d"]]},{"id":"ab43f30d.9ec7f","type":"http in","z":"be2b435.a0f1b4","name":"[GET] /vrimage3","url":"/vrimage3","method":"get","swaggerDoc":"","x":132.86666870117188,"y":38,"wires":[["6917833b.7470d4"]]},{"id":"e4bf8aec.e044d","type":"http response","z":"be2b435.a0f1b4","name":"http response","x":542.8666687011719,"y":72,"wires":[]},{"id":"b00efb52.29892","type":"http in","z":"be2b435.a0f1b4","name":"[POST] /vrimage3","url":"/vrimage3","method":"post","swaggerDoc":"","x":128.36666870117188,"y":147,"wires":[["441fe1c3.ee57a8"]]},{"id":"fafc7840.148d48","type":"http response","z":"be2b435.a0f1b4","name":"http response","x":322.8666687011719,"y":577.9999694824219,"wires":[]},{"id":"b0c22b36.a7a388","type":"function","z":"be2b435.a0f1b4","name":"add HTTP header","func":"msg.headers = {\"content-type\" : \"text/html\" };\nreturn msg;","outputs":1,"noerr":0,"x":320.8666687011719,"y":474.9999694824219,"wires":[["7b2dd0.8167923"]]},{"id":"d7a95cc8.af6a98","type":"debug","z":"be2b435.a0f1b4","name":"","active":true,"console":"false","complete":"result","x":615.8666687011719,"y":382,"wires":[]},{"id":"8d9d5d07.9137d8","type":"function","z":"be2b435.a0f1b4","name":"Calling TinyPNG (POST) ","func":"var auth = new Buffer(\"api:XXX-XXX-XXXXXXXXXXXXXXXXXXXXXXXX\") ;\nauth = \"Basic \" + auth.toString(\"base64\");\nmsg.url = \"https://api.tinify.com/shrink\";\n\nmsg.method= \"POST\";\nmsg.headers = { \"Authorization\" : auth , \"Content-Type\" : \"image/jpeg\"};\nmsg.payload = msg.file;\n\nreturn msg;\n","outputs":1,"noerr":0,"x":313.3666687011719,"y":229,"wires":[["593685e0.be74cc","57a6a770.706c4"]]},{"id":"593685e0.be74cc","type":"debug","z":"be2b435.a0f1b4","name":"","active":false,"console":"false","complete":"payload","x":613.3666687011719,"y":229,"wires":[]},{"id":"57a6a770.706c4","type":"http request","z":"be2b435.a0f1b4","name":"TinyPNG","method":"use","ret":"txt","url":"","x":317.8666687011719,"y":271,"wires":[["f1085d67.43453","e28c4546.58987"]]},{"id":"f1085d67.43453","type":"debug","z":"be2b435.a0f1b4","name":"","active":true,"console":"false","complete":"payload","x":611.3666687011719,"y":273,"wires":[]},{"id":"e28c4546.58987","type":"function","z":"be2b435.a0f1b4","name":"parse","func":"msg.payload = JSON.parse(msg.payload).output.url;\nreturn msg;","outputs":1,"noerr":0,"x":320.3666687011719,"y":343,"wires":[["cf327624.6a5458","bc2227ff.c28c1"]]},{"id":"bc2227ff.c28c1","type":"debug","z":"be2b435.a0f1b4","name":"","active":false,"console":"false","complete":"payload","x":615.3666687011719,"y":341,"wires":[]},{"id":"7b2dd0.8167923","type":"template","z":"be2b435.a0f1b4","name":"Display Face Detection Result","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<h1>Visual Recognition on Bluemix Node-RED</h1>\n{{#result}}\n {{#images}}\n <div><img src=\"{{source_url}}\" height='300'/></div><br>\n {{#faces}}\n <br><b>年齢</b>\n <table border='1'>\n <thead>\n <tr><th>Max</th><th>Min</th><th>Score</th></tr>\n </thead>\n <tbody>\n {{#age}}\n <tr><td>{{max}}</td><td>{{min}}</td><td>{{score}}</td></tr>\n {{/age}} \n </tbody>\n </table>\n <br><b>性別</b>\n <table border='1'>\n <thead>\n <tr><th>Gender</th><th>Score</th></tr>\n </thead>\n <tbody>\n {{#gender}}\n <tr><td>{{gender}}</td><td>{{score}}</td></tr>\n {{/gender}} \n </tbody>\n </table>\n <br><b>該当者</b>\n <table border='1'>\n <thead>\n <tr><th>Name</th><th>Score</th></tr>\n </thead>\n <tbody>\n {{#identity}}\n <tr><td>{{name}}</td><td>{{score}}</td></tr>\n {{/identity}} \n </tbody>\n </table>\n {{/faces}}\n {{/images}} \n{{/result}}","x":315.3666687011719,"y":527.9999694824219,"wires":[["fafc7840.148d48"]]},{"id":"441fe1c3.ee57a8","type":"function","z":"be2b435.a0f1b4","name":"jpeg抽出","func":"var buf = msg.req.body;\nvar SOI = new Buffer(\"FFD8\",\"hex\");\nvar EOI = new Buffer(\"FFD9\",\"hex\");\nvar iSOI = 0;\nvar file = \"\";\n\nfor (var i=0 ; i<=buf.length ; i++) {\n if(SOI.equals(buf.slice(i,i+2))) {\n iSOI = i;\n }\n if(EOI.equals(buf.slice(i,i+2))) {\n file = buf.slice(iSOI,i+2);\n break;\n }\n }\n\nmsg.file = file;\nreturn msg;","outputs":1,"noerr":0,"x":313.3666687011719,"y":179,"wires":[["abdac4cd.ac2ff","8d9d5d07.9137d8"]]},{"id":"abdac4cd.ac2ff","type":"debug","z":"be2b435.a0f1b4","name":"","active":false,"console":"false","complete":"file","x":598.8666687011719,"y":179,"wires":[]}]
1. 写真データを圧縮する
Node-REDのWatson Visual Recognitonノードの説明(ノードをクリックして、Node-RED画面右側のinfoタブに表示される情報)をよく見ると、"Maximum image size is 2MB"という記述があります。
昨今のスマホのカメラは高解像度のため2MBにおさまらないことが多いので、Watson Visual Recognitionに写真データを入力する前にサイズを圧縮しておきます。
圧縮機能は手っ取り早く外部のサービスに頼ることにして、今回はREST APIで呼び出せて無料枠(API呼び出し500件/月まで無料)のあるTinyPNGというサービスを利用しました。
1-1. TinyPNGのAPIキーを取得する
まずTinyPNG Developer APIのページ(https://tinypng.com/developers )を開きます。
ここに名前とメールアドレスを入力して"Get your API key"をクリックすると、まもなく通知のメールが届きます。
メールの中にDashboardへのリンクがあるので、これをクリックしてDashboardを開きます。そこにAPIキーが書かれているので、後ほどNode-REDにコピペして使用します。
1-2. TinyPNGのAPIをコールする
Node-REDフローの以下の部分でTinyPNGのAPIをコールしています。
functionノードの中味は以下のとおりです。TinyPNGのDashboardで入手したAPIキーを1行目に記入します。7行目のmsg.fileは、直前のfunctionノード(jpeg抽出)でjpegファイルをmsg.fileに格納してreturnしているので、これに合わせてあります。jpegファイルを後続のhttp requestノードに渡すためにmsg.payloadに格納してreturnしています。
var auth = new Buffer("api:XXX-XXX-XXXXXXXXXXXXXXXXXXXXXXXX") ;
auth = "Basic " + auth.toString("base64");
msg.url = "https://api.tinify.com/shrink";
msg.method= "POST";
msg.headers = { "Authorization" : auth , "Content-Type" : "image/jpeg"};
msg.payload = msg.file;
return msg;
http requestノードの中味は以下のとおりです。名前を指定しただけで他はデフォルトのままです。
2. Watson Visual Recognitionの顔識別機能を呼び出す
TinyPNGをコールしたhttp requestノードは、TinyPNGからの応答をJSON形式の文字列として、以下の例のようにmsg.payloadにセットして返します。
{"input":{"size":5856,"type":"image/jpeg"},"output":{"size":5722,"type":"image/jpeg","width":115,"height":115,"ratio":0.9771,"url":"https://api.tinify.com/output/ibuikute0r6t7p27.jpg"}}
圧縮した画像そのものは応答には含まれていませんが、画像のURLが応答の一部として渡ってきます。これを抜き出してWatson Visual Recognitionに渡すのですが、ここではJSON.parseでJSON形式の文字列をJSONオブジェクトに変換し、オブジェクトのプロパティとしてurlの値を取り出します。
以下にfunctionノードの中味を示します。
msg.payload = JSON.parse(msg.payload).output.url;
return msg;
次にWatson Visual Recognitionノードの中味を以下に示します。
API Keyのフィールドは、表示される場合とされない場合があります。Node-REDのランタイムにWatson Visual Recognitionインスタンスが接続済の場合には、このフィールドは表示されません。API Keyは内部で自動的に連携されます。
Watson Visual Recognitionインスタンスが接続されていない場合にはこのフィールドが表示されるので、Watson Visual Recognitionインスタンスのcredentialsのapi_keyの値をここにコピペします。
"Detect"では"Detect Faces"(顔識別)を選択しておきます。
3. Watson Visual Recognitionの応答データをmustacheで整形してブラウザに返す
Watson Visual Recognitionからの応答直後は、msg.headersにセットされているcontent-typeの値が"application/json"になっているので、ブラウザにHTMLを返すために"text/html"に置き換えておきます。functionノードの中味を以下に示します。
Watson Visual Recognitionは顔識別の結果をJSONオブジェクトとして、以下の例のようにmsg.resultにセットして返します。
このJSONオブジェクトとmustacheというテンプレートを使って、ブラウザに出力するHTMLを組み立てます。mustacheは{{}}でJSON要素の名前(key)をくくってやると、対応するJSON要素の値(value)に置き換えてくれます。
興味のある方は解説記事や書籍を探してみて下さい。今回作成したtemplateノードの中味を以下に示します。
テンプレートの全文をあらためて以下に掲載します。
<h1>Visual Recognition on Bluemix Node-RED</h1>
{{#result}}
{{#images}}
<div><img src="{{source_url}}" height='300'/></div><br>
{{#faces}}
<br><b>年齢</b>
<table border='1'>
<thead>
<tr><th>Max</th><th>Min</th><th>Score</th></tr>
</thead>
<tbody>
{{#age}}
<tr><td>{{max}}</td><td>{{min}}</td><td>{{score}}</td></tr>
{{/age}}
</tbody>
</table>
<br><b>性別</b>
<table border='1'>
<thead>
<tr><th>Gender</th><th>Score</th></tr>
</thead>
<tbody>
{{#gender}}
<tr><td>{{gender}}</td><td>{{score}}</td></tr>
{{/gender}}
</tbody>
</table>
<br><b>該当者</b>
<table border='1'>
<thead>
<tr><th>Name</th><th>Score</th></tr>
</thead>
<tbody>
{{#identity}}
<tr><td>{{name}}</td><td>{{score}}</td></tr>
{{/identity}}
</tbody>
</table>
{{/faces}}
{{/images}}
{{/result}}
最後にブラウザの応答画面例を示します。mustacheテンプレートのレイアウトに従って、スマホで撮影した写真とWatson Visual Recognitionの判定結果が表示されています。
(※イラスト部分は合成です。ちなみにこのイラストはWatson Visual Recognitionには顔と認識されませんでした。)
以上です。