Visual RecognitionのカスタムClassifierをNode-REDから使う

#はじめに
[前々々回](http://qiita.com/VegaSato/items/2cd40369ea7bd6ae26bf)と[前回](http://qiita.com/VegaSato/items/863b614cd5ab88cf2d44)は、フライドチキンと犬の写真から、それぞれを分類するカスタムClassifierを紹介した。
[前々回](http://qiita.com/VegaSato/items/14c13e011d9f205ac687)は、もともと入っている機能を使って、顔から年齢性別を認識させるアプリを紹介した。
今回はこれらを合体して、PCカメラでフライドチキンや犬の写真を撮ると、それをNode-REDアプリに送って、カスタムClassifierを呼び出して判別する、というアプリを作りたいと思う。

#Node-REDのフロー
Node-REDのフローは次のようになる。
<img src="https://qiita-image-store.s3.amazonaws.com/0/102519/97b44a63-9ccf-2fb3-54e3-23505b859f7e.png" width=600 />
見た目で言うと、違う点はカスタムClassifier設定のノードが追加されている点だ。

##カスタムClassifier対応
カスタムClassifierのClassifier IDと閾値を、msg.paramsのパラメーターとして指定する。例えば以下のような感じになる。

```js
msg.params = {};
msg.params.classifier_ids = "Chicken_or_Dog_1056118805";
msg.params.threshold = "0";
return msg;
```

ノードの中身のキャプチャーはこんな感じ。
<img src="https://qiita-image-store.s3.amazonaws.com/0/102519/af70fa85-5d86-a119-c048-7df2a9f3b6db.png" width=400 />

##VR呼び出し
VRの呼び出しは「Classify an image」を使う。
<img src="https://qiita-image-store.s3.amazonaws.com/0/102519/9a88448e-94c5-a0fd-5b1a-919770b9e3e2.png" width=400 />
API Keyは自分のVRサービスのAPI Keyを入れるのは前と同じ。Languageはクラス名なんだけど、学習時に英語で学習させていたのでEnglishを選択。

##出力整形
VRの実行結果は次のようなJSONで返される。

```json
{ "custom_classes": 2, 
  "images": [
    { "classifiers": [ 
      { "classes": [ 
        { "class": "FriedChiken", 
          "score": 0.144389 }, 
        { "class": "Labradoodle", 
          "score": 0.273302 } ], 
        "classifier_id": "Chicken_or_Dog_1056118805", 
        "name": "Chicken_or_Dog" } ], 
      "image": "1161028-58-swpyg9.jpg" } ], 
    "images_processed": 1 }
```

これをどう料理するかはアプリ次第。今回はスコアが高いほうのクラスと、スコアの割合を返すことにした。

<img src="https://qiita-image-store.s3.amazonaws.com/0/102519/0af5a617-34fd-e66e-37ae-fce8cf77186b.png" width=400 />

##HTMLの表示
HTML側では返されたスコアをもとに結果を表示する。例えばこんな感じ。

<img src="https://qiita-image-store.s3.amazonaws.com/0/102519/5e0609f7-2c53-c350-4291-2299522d9b2d.png" width=400 />

スコアとコメントの関係は以下のようにしてみた。このあたりはコントロール可能なところなので、自分で好きなように変えられるぞ。

|スコア|コメント|
|:---:|:---:|
|80以下|･･･何だ？|
|80～90|かな？|
|90～95|だよね|
|95以上|に違いない！|


サンプルソースは以下。かなりベタ～な書き方をしているけどサンプルなので・・・。

```html

<!doctype html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>Visual Recognition</title>
    <link rel="stylesheet" href="https://bootswatch.com/cyborg/bootstrap.min.css">
    <script src="http://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
  </head>
  
  <body>
    <!-- NavBar -->
    <nav class="navbar navbar-default">
      <div class="container-fluid">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="index.html">Watson API Demo</a>
        </div>
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
          <ul class="nav navbar-nav">
            <li class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">APIs <span class="caret"></span></a>
              <ul class="dropdown-menu" role="menu">
                <li><a href="index.html">Visual Recognition</a></li>
              </ul>
            </li>
          </ul>
        </div>
      </div>
    </nav>
    <!-- Container -->
    <div class="container">
      <div class="starter-template">
        <div class="row">
          <div class="col-md-6" id="video_area">
            <center>
              <p><video id="myVideo" width="400" height="300" autoplay="1"></video></p>
              <p><canvas id="myCanvas" width="400" height="300" style="display:none"></canvas></p>
            </center>
          </div>
          <div class="col-md-6" id="display_area">
            <center>
              <div id="recording" style="display:none">
                <!-- <p><img src="images/logo.png"></img></p> -->
                <br>
                <h3>送信ボタンを押してください</h3>
                <br>
                <p><a class="btn btn-primary" id="btn_submit" onClick="sendImage()">送信する</a></p>
              </div>
              <div id="processing" style="display:none">
                <!-- <p><img src="images/logo_animation.gif"></img></p> -->
                <br>
                <h3>処理中...</h3>
              </div>
              <div id="result" style="display:none">
                <h4>これは･･･</h4>
                <h3><div id="answer"></div></h3>
                <h3><div id="comment"></div></h3>
                <br>
                <p><a class="btn btn-primary" id="btn_retry" onClick="startRecording()">もう1回</a></p>
              </div>
            </center>
          </div>
        </div>
      </div>
    </div>

    <script type="text/javascript">
      //初期化
      navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
      window.URL = window.URL || window.webkitURL;    
      
      //カメラ入力
      var video = document.getElementById('myVideo');
      
      //中間出力
      var canvas = document.getElementById('myCanvas');
      var ctx = canvas.getContext('2d');
      
      //ストリーム作成とカメラ画像のストリーミング開始
      var localStream = null;
      navigator.getUserMedia({video: true, audio: false}, 
        function(stream) { // for success case
          console.log(stream);
          video.src = window.URL.createObjectURL(stream);
        },
        function(err) { // for error case
          console.log(err);
        }
      );
      
      // カメラ表示、Watson静止ロゴ＆説明表示
      var startRecording = function(){
        console.log("startRecording");
        
        // 表示切替
        document.getElementById('recording').style="display:";
        document.getElementById('processing').style="display:none";
        document.getElementById('result').style="display:none";
        
        // ビデオ開始
        video.play();
      }
      
      // カメラ停止＆カメラ画像をNode-REDに送る
      var sendImage = function() {
        console.log("sendImageToNodeRED");
        video.pause();                                                // ビデオ停止
        
        document.getElementById('recording').style="display:none";    // 表示切替
        document.getElementById('processing').style="display:";
        document.getElementById('result').style="display:none";
        
        var canvas_image = ctx.drawImage(video,0,0,400,300);          // カメラ→imgに変換
        console.log("canvas = "+ canvas);
        var dataURL = canvas.toDataURL("image/jpeg");                 // DataURLに変換
        var base64 = dataURL.replace(/^.*,/, '');                     // プレフィックスを削除してBase64部分だけ取り出し
        var xhr= new XMLHttpRequest();                                // Node-REDの呼び出し準備
        xhr.open("POST", "https://あなたのNode-REDアプリケーション.mybluemix.net/vr/chicken");
        xhr.onreadystatechange = function(e){                         // コールバック関数
          if(xhr.readyState == 4){
            if(xhr.status == 200){
              console.log(xhr.responseText);
              displayResult(JSON.parse(xhr.responseText));
            }else{
              console.log(e)
              console.log(xhr.responseText);
            }
          }
        };
        xhr.send(base64);                                             // Node-REDの呼び出し
      }
      
      // Node-REDの結果を分析＆表示
      var displayResult = function(result) {
        console.log("displayResult" + result);
        var answer = "";
        var score = "";
        switch (result.top_class) {
          case "FriedChiken":
            answer = "フライドチキン";
            break;
          case "Labradoodle":
            answer = "ラブラドゥードル";
            break;
          default:
            answer = "判定失敗";
            break;
        }
        
        if (Math.round(result.score * 100) > 95) {
          comment = "に違いない!";
        } else if (Math.round(result.score * 100) > 90) {
          comment = "だよね";
        } else if (Math.round(result.score * 100) > 80) {
          comment = "かな？";
        } else {
          comment = "･･･何だ？";
          answer = "";
        }                
        document.getElementById('answer').innerHTML = answer;
        document.getElementById('comment').innerHTML = comment;
        
        // 表示切替
        document.getElementById('recording').style="display:none";
        document.getElementById('processing').style="display:none";
        document.getElementById('result').style="display:";  
      }
      startRecording();      // メインロジック開始
   </script>
 </font>
 </body>
</html>
```


#おわりに
ということでサクっとカスタムClassifierを呼ぶことができました。キモはmsg.paramsでclassifier_idsを指定するところかな。ここができれば簡単に呼ぶことが可能。

ということで今日はここまで。

<font color="red">*フライドチキンはこのあとVegaSatoがおいしくいただきました。*</font>