おことわり
本稿に掲載しているVisual Recognitionの解析結果は2017年3月現在のものです。
Watsonは日々進化していますので、本稿執筆時点と最新の結果は異なる可能性があります。
概要
Visual RecognitionのClassify an image API(イメージ分類=タグ付け機能)が日本語対応になりました。
Node-REDでアプリケーションを作成して日本語でのタグ付けを試してみました。
前置きはさておき、先に結果から
Watson Visual Recognitionとは
Visual Recognitionも他の機能と同様にWeb APIとして提供されています。
APIは大きく分けて4つ。
- Classify an image …イメージ分類
- Detect faces …顔検出および性別・年齢の推定
- Custom classifiers …カスタム・イメージ分類
- Collections …類似画像抽出(2017年3月現在、ベータ版)
上記1、2はトレーニング済みで提供されており、事前学習の必要はありません。
一方3、4はユーザーがトレーニング画像を準備してWatsonに学習させ、独自の分類を行わせることができます。
本稿で使ったAPIは上記1です。
その他についてはWatson Developer Cloudか、あるいは以下の記事を参照してください。
- [Swift] [iOS] Watson Visual Recognitionを使って顔解析アプリを作ってみた
- Watson Visual Recognitionがすごすぎて俺の中で話題になっている件
なお、Alchemy APIという名前でもイメージ分類や顔検出機能を提供していましたが、Visual Recognitionに統合され、Alchemy APIは非推奨となっています。
Visual Recognitionの利用開始手順については以下の記事が参考になります。
BluemixでWatson API のVisual Recognition を使う by curl
開発したアプリケーションについて
Node-REDとは
あらかじめ用意された「ノード」と呼ばれる部品をつなげることにより、最小限のコードでIBM Bluemix上にWebアプリを構築できます。
Node-REDの利用開始手順はこちらの記事などをご参照ください。
コードをほとんど書かずにNode-REDでWebアプリを作ろう
ノード定義イメージ
ノード定義はインポートすることができます。
今回は、こちらのサイトからインポートさせていただき、さらにカスタマイズを加えました。
画像の顔認識!Node-REDとVisual Recognitionでリア充判定をしてみた!
アプリに対してHTTPリクエストがあった際、画像のURLが渡された場合はVisual Recognitionにリクエストを行い、そうでない場合はHTMLのみを返しています。
Visual Recognition用のノードはBluemixにあらかじめ用意されており、API KeyとAPIの種類、言語を入力するだけで使えます。
ちなみに、Classify an image APIには、HTTP GETと、POSTのインターフェイスがあります。
前者はURLを、後者は画像ファイルを引数にとります。
詳細は公式ドキュメントを参照してください。
HTML
Node-REDではテンプレートエンジンMustacheを使うことができます。
scriptを埋め込まなくても条件分岐ができたり、配列を取り出してlistが作れたりと、便利ですね。
上図の"html"と命名したtemplateという種類のノードに、以下のHTMLソースを記述しています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Watson Visual Recognition Classifier</title>
<!-- Bootswatch(Free themes for Bootstrap) -->
<link rel="stylesheet" href="//bootswatch.com/cerulean/bootstrap.min.css">
<style>
body {
padding-bottom: 2rem;
}
#img-result {
width: 75%;
height: auto;
}
</style>
</head>
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="{{req._parsedUrl.pathname}}">Watson Visual Recognition Classifier</a>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-xs-1 bg-left"></div>
<div class="col-xs-10 bg-main">
<form action="{{req._parsedUrl.pathname}}" method="GET">
<div class="form-group row">
<p>画像のURLを入力して「送信」を押してください。</p>
<label for="imageUrl" class="col-form-label">URL</label>
<br>
<input type="url" class="form-control" name="imageUrl" required>
<br>
<button class="btn btn-primary">送信</button>
</div>
</form>
</div>
<div class="col-xs-1 bg-right"></div>
</div>
{{#payload.imageSrc}}
<!-- See: Mustache リクエストのimageSrcがNULLでない場合のみ表示される -->
<div class="row">
<div class="col-xs-1 bg-left"></div>
<div class="col-xs-10 bg-main">
<ul class="list-group">
<li class="list-group-item active">解析結果</li>
{{#payload.classes}}
<li class="list-group-item"><div class="text-primary">クラス:{{class}}</div> 確信度:{{score}}{{#type_hierarchy}} 分類構造:{{type_hierarchy}}{{/type_hierarchy}}</li>
{{/payload.classes}}
</ul>
<img src="{{payload.imageSrc}}" id="img-result" class="img-responsive">
</div>
<div class="col-xs-1 bg-right"></div>
</div>
{{/payload.imageSrc}}
</div>
<body>
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha384-3ceskX3iaEnIogmQchP8opvBy3Mi7Ce34nWjpBIwVTHfGYWQS9jwHDVRnpKKHJg7" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js" integrity="sha384-XTs3FgkjiBgo8qjEjBk0tGmf3wPrWtA6coPfQDfFEY8AnYJwjalXCiosYRBIBZX8" crossorigin="anonymous"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/js/bootstrap.min.js" integrity="sha384-BLiI7JTZm+JWlgKa0M0kGRpJbF2J8q+qreVrKBC47e3K6BW78kGLrCkeRX6I9RoK" crossorigin="anonymous"></script>
</html>
Classify an imageのレスポンスサンプル
{ "custom_classes": 0,
"images": [
{ "classifiers": [
{ "classes": [
{ "class": "ベン油", "score": 0.751, "type_hierarchy": "/自然/ベン油" },
{ "class": "自然", "score": 0.917 },
{ "class": "丘", "score": 0.865, "type_hierarchy": "/自然/丘" },
{ "class": "稜線", "score": 0.554, "type_hierarchy": "/自然/稜線" } ],
"classifier_id": "default",
"name": "default" } ],
"resolved_url": "https://example.com/aaaaa.jpg",
"source_url": "https://example.com/aaaaa.jpg" } ],
"images_processed": 1 }
JavaScript
上図の"class取り出し"と命名したfunctionという種類のノードに、以下のJavaScriptを記述しています。
上記レスポンスの"classes"配下の配列と、"resolved_url"の文字列を"html"ノードに渡してレンダリングします。
var classes = [];
msg.payload = {};
// VRのレスポンスからclassの配列を取り出す
classes = msg.result.images[0].classifiers[0].classes;
msg.payload.classes = classes;
msg.payload.imageSrc = msg.result.images[0].resolved_url;
return msg;
インポート用ノード定義
以下をコピーしてBluemix上のNode-REDにインポートすれば、同様のアプリを構築できます。
Visual RecognitionのAPIキーを設定することをお忘れなく。
[{"id":"b180e849.1c2848","type":"http in","z":"5d709634.480f18","name":"","url":"/vr-classify","method":"get","swaggerDoc":"","x":85,"y":30,"wires":[["80b1c068.51aa8","1592f1b.854650e"]]},{"id":"804eb7e9.388dd8","type":"visual-recognition-v3","z":"5d709634.480f18","name":"visual recognition","apikey":"","image-feature":"classifyImage","lang":"ja","x":311.8834228515625,"y":312.8118591308594,"wires":[["f54469b8.d8afe8","d0a7136.7578cf"]]},{"id":"f54469b8.d8afe8","type":"debug","z":"5d709634.480f18","name":"","active":true,"console":"false","complete":"result","x":496.687744140625,"y":281.41363525390625,"wires":[]},{"id":"b0b63e79.4ba05","type":"function","z":"5d709634.480f18","name":"class取り出し","func":"var classes = [];\nmsg.payload = {};\n// VRのレスポンスからclassの配列を取り出す\nclasses = msg.result.images[0].classifiers[0].classes;\nmsg.payload.classes = classes;\nmsg.payload.imageSrc = msg.result.images[0].resolved_url;\nreturn msg;","outputs":1,"noerr":0,"x":728,"y":474,"wires":[["c7c46c18.f1771","34891497.54e25c"]]},{"id":"dd318243.dc0ba","type":"http response","z":"5d709634.480f18","name":"","x":890.2205810546875,"y":193.25299072265625,"wires":[]},{"id":"80b1c068.51aa8","type":"debug","z":"5d709634.480f18","name":"","active":true,"console":"false","complete":"payload","x":322,"y":30,"wires":[]},{"id":"376b6ed7.9a8352","type":"change","z":"5d709634.480f18","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.imageUrl","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":232,"y":223,"wires":[["804eb7e9.388dd8","7f02a16d.b6f71"]]},{"id":"c7c46c18.f1771","type":"template","z":"5d709634.480f18","name":"html","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<html lang=\"ja\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\">\n <title>Watson Visual Recognition Classifier</title>\n <!-- Bootswatch(Free themes for Bootstrap) -->\n <link rel=\"stylesheet\" href=\"//bootswatch.com/cerulean/bootstrap.min.css\">\n <style>\n body {\n padding-bottom: 2rem;\n }\n #img-result {\n width: 75%;\n height: auto;\n }\n </style>\n </head>\n \n <body>\n <nav class=\"navbar navbar-default\">\n <div class=\"container-fluid\">\n <div class=\"navbar-header\">\n <a class=\"navbar-brand\" href=\"{{req._parsedUrl.pathname}}\">Watson Visual Recognition Classifier</a>\n </div>\n </div>\n </nav>\n <div class=\"container\">\n <div class=\"row\">\n <div class=\"col-xs-1 bg-left\"></div>\n <div class=\"col-xs-10 bg-main\">\n <form action=\"{{req._parsedUrl.pathname}}\" method=\"GET\">\n <div class=\"form-group row\">\n <p>画像のURLを入力して「送信」を押してください。</p>\n <label for=\"imageUrl\" class=\"col-form-label\">URL</label>\n <br>\n <input type=\"url\" class=\"form-control\" name=\"imageUrl\" required>\n <br>\n <button class=\"btn btn-primary\">送信</button>\n </div>\n </form>\n </div>\n <div class=\"col-xs-1 bg-right\"></div>\n </div>\n {{#payload.imageSrc}}\n <!-- See: Mustache リクエストのimageSrcがNULLでない場合のみ表示される -->\n <div class=\"row\">\n <div class=\"col-xs-1 bg-left\"></div>\n <div class=\"col-xs-10 bg-main\">\n <ul class=\"list-group\">\n \t<li class=\"list-group-item active\">解析結果</li>\n {{#payload.classes}}\n \t<li class=\"list-group-item\"><div class=\"text-primary\">クラス:{{class}}</div> 確信度:{{score}}{{#type_hierarchy}} 分類構造:{{type_hierarchy}}{{/type_hierarchy}}</li>\n {{/payload.classes}}\n </ul>\n <img src=\"{{payload.imageSrc}}\" id=\"img-result\" class=\"img-responsive\">\n </div>\n <div class=\"col-xs-1 bg-right\"></div>\n </div>\n {{/payload.imageSrc}}\n </div>\n <body>\n \n <script src=\"//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js\" integrity=\"sha384-3ceskX3iaEnIogmQchP8opvBy3Mi7Ce34nWjpBIwVTHfGYWQS9jwHDVRnpKKHJg7\" crossorigin=\"anonymous\"></script>\n <script src=\"//cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js\" integrity=\"sha384-XTs3FgkjiBgo8qjEjBk0tGmf3wPrWtA6coPfQDfFEY8AnYJwjalXCiosYRBIBZX8\" crossorigin=\"anonymous\"></script>\n <script src=\"//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/js/bootstrap.min.js\" integrity=\"sha384-BLiI7JTZm+JWlgKa0M0kGRpJbF2J8q+qreVrKBC47e3K6BW78kGLrCkeRX6I9RoK\" crossorigin=\"anonymous\"></script>\n</html>","x":762,"y":111,"wires":[["dd318243.dc0ba"]]},{"id":"1592f1b.854650e","type":"switch","z":"5d709634.480f18","name":"画像URLチェック","property":"payload.imageUrl","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","outputs":2,"x":175,"y":116,"wires":[["c7c46c18.f1771"],["376b6ed7.9a8352"]]},{"id":"7f02a16d.b6f71","type":"debug","z":"5d709634.480f18","name":"","active":true,"console":"false","complete":"payload","x":463,"y":223,"wires":[]},{"id":"34891497.54e25c","type":"debug","z":"5d709634.480f18","name":"","active":true,"console":"false","complete":"payload","x":799,"y":538,"wires":[]},{"id":"d0a7136.7578cf","type":"switch","z":"5d709634.480f18","name":"レスポンスチェック","property":"result.images[0].classifiers[0].classes.length","propertyType":"msg","rules":[{"t":"null"},{"t":"nnull"}],"checkall":"true","outputs":2,"x":432,"y":406,"wires":[["c7c46c18.f1771"],["b0b63e79.4ba05"]]},{"id":"ba454e74.81fd2","type":"comment","z":"5d709634.480f18","name":"解析NGの場合","info":"","x":629,"y":355,"wires":[]},{"id":"d1cad779.c8e9b8","type":"comment","z":"5d709634.480f18","name":"解析OKの場合","info":"","x":540,"y":457,"wires":[]},{"id":"54535598.a2cd2c","type":"comment","z":"5d709634.480f18","name":"初期表示の場合:画像URL=NULL","info":"","x":415,"y":89,"wires":[]},{"id":"fad119bd.6977f8","type":"comment","z":"5d709634.480f18","name":"解析実行の場合:画像URL≠NULL","info":"","x":313,"y":181,"wires":[]}]