[Watson] [Node-RED] 日本語に対応したVisual RecognitionのClassify an image（イメージ分類）を試してみた

# おことわり

本稿に掲載しているVisual Recognitionの解析結果は2017年3月現在のものです。
Watsonは日々進化していますので、本稿執筆時点と最新の結果は異なる可能性があります。

# 概要

Visual RecognitionのClassify an image API（イメージ分類＝タグ付け機能）が日本語対応になりました。
Node-REDでアプリケーションを作成して日本語でのタグ付けを試してみました。

# 前置きはさておき、先に結果から

![FireShot Capture 6 - Watson Visual Recognition_ - https___isol-watson-app.mybluemix.net_vr-classify_.png](https://qiita-image-store.s3.amazonaws.com/0/122917/452fcbd7-d6b3-ba2d-e512-57f7cdc4c72c.png)

---

![FireShot Capture 7 - Watson Visual Recognition_ - https___isol-watson-app.mybluemix.net_vr-classify_.png](https://qiita-image-store.s3.amazonaws.com/0/122917/0fe59caa-c62a-be13-d303-a35c8d7668ed.png)

---

![FireShot Capture 3 - Watson Visual Recognition_ - https___isol-watson-app.mybluemix.net_vr-classify_.png](https://qiita-image-store.s3.amazonaws.com/0/122917/0104a924-c17a-0f3d-c7fb-d1a181937e14.png)

---

![FireShot Capture 5 - Watson Visual Recognition_ - https___isol-watson-app.mybluemix.net_vr-classify_.png](https://qiita-image-store.s3.amazonaws.com/0/122917/f544e42f-10c3-644f-8a9f-3469a1bc68e2.png)

---

![FireShot Capture 4 - Watson Visual Recognition_ - https___isol-watson-app.mybluemix.net_vr-classify_.png](https://qiita-image-store.s3.amazonaws.com/0/122917/a5c471d3-9804-f7a4-d842-6a15027944ca.png)

# Watson Visual Recognitionとは

Visual Recognitionも他の機能と同様にWeb APIとして提供されています。
APIは大きく分けて4つ。

1. Classify an image　…イメージ分類
2. Detect faces　…顔検出および性別・年齢の推定
3. Custom classifiers　…カスタム・イメージ分類
4. Collections　…類似画像抽出（2017年3月現在、ベータ版）

上記1、2はトレーニング済みで提供されており、事前学習の必要はありません。
一方3、4はユーザーがトレーニング画像を準備してWatsonに学習させ、独自の分類を行わせることができます。

本稿で使ったAPIは上記1です。
その他については[Watson Developer Cloud](https://www.ibm.com/smarterplanet/jp/ja/ibmwatson/developercloud/visual-recognition.html)か、あるいは以下の記事を参照してください。

- [[Swift] [iOS] Watson Visual Recognitionを使って顔解析アプリを作ってみた](http://qiita.com/y-some/items/45e2c99b91485638e05b)
- [Watson Visual Recognitionがすごすぎて俺の中で話題になっている件](http://qiita.com/VegaSato/items/2cd40369ea7bd6ae26bf)

なお、Alchemy APIという名前でもイメージ分類や顔検出機能を提供していましたが、Visual Recognitionに統合され、Alchemy APIは非推奨となっています。

Visual Recognitionの利用開始手順については以下の記事が参考になります。

[BluemixでWatson API のVisual Recognition を使う by curl](http://qiita.com/MoriKen/items/6403cf5f6775bb8aa4c4)

# 開発したアプリケーションについて

## Node-REDとは

あらかじめ用意された「ノード」と呼ばれる部品をつなげることにより、最小限のコードでIBM Bluemix上にWebアプリを構築できます。

Node-REDの利用開始手順はこちらの記事などをご参照ください。

[コードをほとんど書かずにNode-REDでWebアプリを作ろう](http://ascii.jp/elem/000/001/139/1139255/)

## ノード定義イメージ

ノード定義はインポートすることができます。
今回は、こちらのサイトからインポートさせていただき、さらにカスタマイズを加えました。

[画像の顔認識！Node-REDとVisual Recognitionでリア充判定をしてみた！](http://wp.tech-style.info/archives/1081)

ノード定義の全容はこんな感じです。
![スクリーンショット 2017-03-26 21.47.32.png](https://qiita-image-store.s3.amazonaws.com/0/122917/23ee87cf-39e7-00f5-743f-10b039f4dc89.png)

アプリに対してHTTPリクエストがあった際、画像のURLが渡された場合はVisual Recognitionにリクエストを行い、そうでない場合はHTMLのみを返しています。

Visual Recognition用のノードはBluemixにあらかじめ用意されており、API KeyとAPIの種類、言語を入力するだけで使えます。
![スクリーンショット 2017-03-26 21.30.08.png](https://qiita-image-store.s3.amazonaws.com/0/122917/999bc52b-9fff-4bf0-c4f3-d23303337bb8.png)

ちなみに、Classify an image APIには、HTTP GETと、POSTのインターフェイスがあります。
前者はURLを、後者は画像ファイルを引数にとります。
詳細は[公式ドキュメント](https://www.ibm.com/watson/developercloud/visual-recognition/api/v3/#classify_an_image)を参照してください。

## HTML

Node-REDではテンプレートエンジン[Mustache](https://mustache.github.io/)を使うことができます。
scriptを埋め込まなくても条件分岐ができたり、配列を取り出してlistが作れたりと、便利ですね。

上図の"html"と命名したtemplateという種類のノードに、以下のHTMLソースを記述しています。

```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のレスポンスサンプル

```json

{ "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"ノードに渡してレンダリングします。

```js

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キーを設定することをお忘れなく。

```json

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

```