Watson Visual Recognitionを使った簡単顔認識アプリを作ってみよう

# はじめに
前回久々に投稿したらキャラが違うと言われた･･･。
気を取り直して、溜まっているネタを吐き出していこうと思う。

# Visual Recognitionの顔認識
Visual Recognition(VR)には現在以下の機能がある。
- Classify an image(画像分類)
- <font color="red">Detect Faces(顔検出)</font>
- Similarity Search(似ている画像探し)
で、この顔検出は、写真の中で顔がどの位置にあるか、年齢、性別を推測してくれる、というAPIだ。
ちなみに、[前回のフライドチキンの話](http://qiita.com/VegaSato/items/2cd40369ea7bd6ae26bf)は画像分類機能を使っている。
最後の似ている画像はベータ版だけど楽しそうなのでそのうち紹介したい。
(テキスト抽出もあったような気がするけど今リファレンスを見ると書いてないな･･･)

# 完成イメージ図
ブラウザーで稼働するこんなアプリケーションを作りたい。
PCのカメラの画像を入力にして、VRの顔検出を使って、年齢や性別を判断させてみる。(顔写真はハメコミ合成です)
![VR_APPL_02.png](https://qiita-image-store.s3.amazonaws.com/0/102519/ca301c3f-d522-8b17-9fa2-63e28a18ff40.png)

# 準備するもの
以下のものを準備しておく。
- ブラウザー(最新版にアップデートしておこう。少し古いFirefoxではカメラが表示されなかったことがあるので)
- HTMLファイル(画面表示とカメラコントロールを担当)
- Node-REDアプリ(VRの呼び出しを簡単にするためにNode-REDを使う)
- VRのサービス(のAPIキー)
最低限の見栄えは確保しつつ超簡単にやってみようと思う。

# HTML
HTMLファイルにJavaScriptのコードも含めて1つのファイルにまとめてみた。jquery、bootstrapを使い、bootswatchで見栄えをプラス。って言っても本当に最低限のみ。これ以上はコードが長くなって、私の手には負えなくなってしまう。。。

```html:index.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">
                <div id="human" style="display:none">
                  <h4>もしかして</h4>
                  <h3><div id="age">年齢：</div></h3>
                  <h3><div id="gender">性別：</div></h3>
                  <br>
                </div>
                <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";
        document.getElementById('human').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";
        document.getElementById('human').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/recognize");
        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 age = "";
        var gender = "";
        if ('age' in result) {                    // 結果表示
          if ('min' in result.age) {              // 年齢
            age = age + result.age.min;
          }
          age = age + "～";
          if ('max' in result.age) {
            age = age + result.age.max;
          }
          document.getElementById('age').innerHTML = "年齢：" + age + "歳";
          if (result.gender.gender == "MALE") {   // 性別
            gender = "男性";
          } else {
            gender = "女性";
          }
          document.getElementById('gender').innerHTML = "性別：" + gender;      
          document.getElementById('human').style="display:";
        } else {
          document.getElementById('age').innerHTML = "";
          document.getElementById('gender').innerHTML = "人間じゃない？";
          document.getElementById('human').style="display:";
        }
        // 表示切替
        document.getElementById('recording').style="display:none";
        document.getElementById('processing').style="display:none";
        document.getElementById('result').style="display:";  
      }
      startRecording();                           // メインロジック開始
   </script>
 </font>
 </body>
</html>
```

Node-REDを呼び出しているところは、各自のNode-REDアプリケーションのURLに変えて欲しい。

このアプリでは画像をBase64エンコードして送信している。色々試していたのだけど、Base64エンコードして送って、Node-RED側でデコードしてからVRに入れるとうまくいった。もっといい方法があるのかもしれないけど、とりあえず動くこと優先ということで。(いいやりかたあったら誰か教えて･･･)

# Node-RED
サーバー側はNode-REDを使う。Node-REDはサーバーアプリを作るのに非常に便利！と使いまくっている。別にNode-REDじゃなくても、というかHTMLファイルのJavaScriptから直接VRを呼び出すこともできるだろうけど、ここはあえてNode-REDを使うのである。

NodeｰREDのフローは以下のようになる。
<img src="https://qiita-image-store.s3.amazonaws.com/0/102519/0237e024-1509-5a73-77b6-06afd404de7c.png" width=600>

自分で手動で作るのが一番だが、面倒な人のためにインポート用のデータを以下に用意した。
これをメニューからインポートを選んでインポートして欲しい。インポート方法はこんな感じだ。
<img src="https://qiita-image-store.s3.amazonaws.com/0/102519/faf5bd74-349a-f239-0a7b-aea25465387c.png" width=600>

で、インポート用のソースがこちら。

```json:SampleFlow.json
[
    {
        "id": "9e10f181.d3a0c8",
        "type": "http in",
        "z": "5830e581.5f024c",
        "name": "",
        "url": "/vr/recognize",
        "method": "post",
        "swaggerDoc": "",
        "x": 121.11112976074219,
        "y": 118.8888931274414,
        "wires": [
            [
                "e951ed9f.6a36b"
            ]
        ]
    },
    {
        "id": "f3294ae9.4e336",
        "type": "visual-recognition-v3",
        "z": "5830e581.5f024c",
        "name": "顔検出",
        "apikey": "{}",
        "image-feature": "detectFaces",
        "lang": "ja",
        "x": 383.9945411682129,
        "y": 136.70076847076416,
        "wires": [
            [
                "97b63dc0.8adda8",
                "1c361dee.fa1f32"
            ]
        ]
    },
    {
        "id": "97b63dc0.8adda8",
        "type": "debug",
        "z": "5830e581.5f024c",
        "name": "",
        "active": true,
        "console": "false",
        "complete": "result",
        "x": 480.7988510131836,
        "y": 262.3025302886963,
        "wires": []
    },
    {
        "id": "1c361dee.fa1f32",
        "type": "function",
        "z": "5830e581.5f024c",
        "name": "出力整形",
        "func": "msg.payload = {};\nif (msg.result.images[0].faces.length > 0) {\n    msg.payload.age = {};\n    msg.payload.age.max = msg.result.images[0].faces[0].age.max;\n    msg.payload.age.min = msg.result.images[0].faces[0].age.min;\n    msg.payload.age.score = msg.result.images[0].faces[0].age.score;\n    msg.payload.gender = {};\n    msg.payload.gender.gender = msg.result.images[0].faces[0].gender.gender;\n    msg.payload.gender.score = msg.result.images[0].faces[0].gender.score;\n}\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 546.1560287475586,
        "y": 187.60018157958984,
        "wires": [
            [
                "9c79bd51.2a5db8"
            ]
        ]
    },
    {
        "id": "e6669034.bc05a",
        "type": "http response",
        "z": "5830e581.5f024c",
        "name": "",
        "x": 843.331729888916,
        "y": 133.1418914794922,
        "wires": []
    },
    {
        "id": "9c79bd51.2a5db8",
        "type": "function",
        "z": "5830e581.5f024c",
        "name": "CORS対応",
        "func": "msg.headers = {\"Access-Control-Allow-Origin\": \"*\"};\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 689.6529350280762,
        "y": 259.15968132019043,
        "wires": [
            [
                "cdc45085.511fb",
                "e6669034.bc05a"
            ]
        ]
    },
    {
        "id": "cdc45085.511fb",
        "type": "debug",
        "z": "5830e581.5f024c",
        "name": "",
        "active": true,
        "console": "false",
        "complete": "false",
        "x": 847.438777923584,
        "y": 329.02293968200684,
        "wires": []
    },
    {
        "id": "e951ed9f.6a36b",
        "type": "function",
        "z": "5830e581.5f024c",
        "name": "base64decode",
        "func": "msg.payload = new Buffer(msg.req.body, 'base64');\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 229.4027862548828,
        "y": 201.94244003295898,
        "wires": [
            [
                "dceb7b13.756ea",
                "f3294ae9.4e336"
            ]
        ]
    },
    {
        "id": "dceb7b13.756ea",
        "type": "debug",
        "z": "5830e581.5f024c",
        "name": "",
        "active": true,
        "console": "false",
        "complete": "payload",
        "x": 384.7867431640625,
        "y": 322.2430534362793,
        "wires": []
    }
]
```

インポートしたら、VRの顔検出を行うノードに自分のVRのサービスのAPIキーを追加する。
<img src="https://qiita-image-store.s3.amazonaws.com/0/102519/ef7a404f-b6a1-158a-5bce-031cc9a844d0.png" width=600>

できたらDeployして完了だ。

## CORS対応
ちなみにここで「CORSはどうなってる？」と思った人はするどい人だ。今回、HTMLに埋め込まれたJavaScriptからBluemixのNode-RED(アプリケーション)にリクエストを飛ばしているので、<font color="red">**クロスオリジンの問題**</font>が発生する。
これを回避するためにCORS対応を行っている。
といってもたいしたことではなく、レスポンスのヘッダーに以下を追加しているだけ。

```
msg.headers = {"Access-Control-Allow-Origine": "*"};
```

![Picture2.png](https://qiita-image-store.s3.amazonaws.com/0/102519/404809b0-58b7-cce1-7a26-e49569c8a73f.png)

これでCORSの問題を解消してブラウザーはレスポンスを受け取れるようになる。

# 実行！
さぁ、index.htmlをブラウザーで実行しよう。
カメラ使用許可が出てくるので許可して、あとはPCのカメラ(USBカメラでも大丈夫だと思う)に向かってニッコリ笑ってボタンをクリックするだけ！

ちなみに、人の顔が検出できなかった場合にはこんな感じのメッセージになる。
![VR_APPL_05.png](https://qiita-image-store.s3.amazonaws.com/0/102519/d30cf315-da3b-fa3a-f56e-f56d7ae0e28c.png)


# おわりに
ということで、簡単に顔検出アプリができあがった。カメラ画像取得＋VRという組み合わせの雛形になったわけだが、これを応用することでさまざまなアプリが作れると思う。
これを見てVRに興味を持った人は、ぜひ独自のアプリを開発いただきたい。

それでは今日はここまで。