How-Old.netと同じ顔年齢表示をJavaScriptで実装する

2015年4月29日に米Microsoftが公開したHow-Old.netは、「写真から顔年齢を診断できる」と一躍話題になりました。同サイトはMicrosoftが進めるProject Oxfordの機械学習APIの一つ、Face APIを使って顔検出や性別・年齢の推定を行っています。Face APIはMicrossoft Azureのアカウントがあれば無料で使用できるため、実際にHow-Old.netと同じ機能を実装してみましょう。
※Face APIは執筆時点で無料(1分あたり20件かつ1か月あたり30,000件までの制限)ですが、有料に変更される場合もありますので適宜ご確認ください。

How-Old.net

Microsoft Azureのアカウント登録手順は『5分でAzureアカウントを作ろう!|始め方 アカウント作成 Windowsアカウント| | ナレコムazureレシピ』、Face APIを(0円)購入できるAzure Marketplaceのページはこちら、Subscription Keyを確認する手順は『空気を読んで年齢を答えてくれる例のあれの裏側にあるAPIがAzure Marketplaceに登場してきました。 - はつねの日記』などをご参考ください。以降は、Subscription Key(Project OxfordのDeveloperページで確認できるPrimary key)がある前提で記載しています。

画像をURLで指定する例

Face APIのリファレンスにはJavaScriptを含む各言語向けのサンプルも記載されており、通信テスト用の機能(Testing Console)も提供されているので動作確認は簡単です。実際にJavaScriptで実装してみましょう。

Subscription Keyを入力し、画像のクリックで処理結果が表示されます。(APIの回数制限を超えていると通信エラーになります。)
ポール・ナース - Wikipedia マリオ・モリーナ - Wikipedia アンゲラ・メルケル - Wikipedia
<label>Subscription Key(Primary key): <input type="text" id="sample1_key" /></label>
<div id="sample1_input">
  <img src="http://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Paul_Nurse_portrait.jpg/180px-Paul_Nurse_portrait.jpg" alt="ポール・ナース - Wikipedia" />
  <img src="http://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Mario_molina.jpg/220px-Mario_molina.jpg" alt="マリオ・モリーナ - Wikipedia" />
  <img src="http://upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Angela_Merkel_24092007.jpg/150px-Angela_Merkel_24092007.jpg" alt="アンゲラ・メルケル - Wikipedia" />
</div>
<div id="sample1_output"></div>
#sample1_input img{
  cursor:pointer;
}
#sample1_output{
  position:relative;
  overflow:hidden;
}
#sample1_output div{
  position:absolute;
}
#sample1_output .f{
  border:1px solid #fff;
}
#sample1_output .a{
  color:#D86718;
  background:#F1D100;
  font-size:15px;
  line-height:1em;
}
$(function(){
  var output = $('#sample1_output');
  //画像のクリックイベント
  $('#sample1_input').on('click', 'img', function(){
    var img = $(this);
    //前回の処理結果をクリアする。
    output.empty();
    //APIのURLパラメタを設定
    var params = {
      'Subscription-Key': $('#sample1_key').val()
      ,returnFaceAttributes: 'age,gender'
    };
    $.ajax({
      url: 'https://api.projectoxford.ai/face/v1.0/detect?' + $.param(params)
      ,type: 'POST'
      //contentTypeを明示する
      ,contentType:'application/json'
      //POST値はJSON形式の文字列で指定
      ,data:JSON.stringify({
        url: img.attr('src')
      })
    })
    .done(function(data) {
      //選択した画像を複製
      img.clone().appendTo(output);
      //検出した顔の分だけ繰り返す
      $.each(data, function(){
        var face = this.faceRectangle;
        var attr = this.faceAttributes;
        //顔を囲む矩形のdivを追加
        $('<div class="f" />').css({
          left:face.left+'px'
          ,top:face.top+'px'
          ,width:face.width+'px'
          ,height:face.height+'px'
        })
        .appendTo(output);
        //性別と年齢のdivを追加
        $('<div class="a" />').css({
          left:face.left+'px'
          ,top:(face.top + face.height)+'px'
        })
        .text(attr.gender +' '+ attr.age)
        .appendTo(output);
      });
    })
    .fail(function() {
      alert('error');
    });
  });
});

$.ajaxでFaceAPIを呼び出しています。画像のURLを送信するためにオブジェクトをJSON形式の文字列に変換し、Content-Typeを指定する点以外は公式サンプルとほぼ同様です。検出された顔の位置に合うよう白い矩形を追加し、性別と年齢もdivで表示させています。(デザインや座標計算は簡略化しています。)

画像をバイナリで指定する例

先の例では画像のURLを指定してFaceAPIを呼び出しましたが、この場合インターネット上にある画像しか指定できません。<input type="file">で任意に選択させた画像を使いたい場合、通常はアップロードを受け付けるサーバー側の処理が必要となります。TumblrやTwitPic等のWebAPIと組み合わせる手もありますが、HTML5のFileAPIを使用すれば<input type="file">で選択ファイルをJavaScriptから参照できるため、クライアントサイド(ブラウザ)だけで選択画像をFaceAPIに送信できます。

Subscription Keyを入力し、画像の選択で処理結果が表示されます。(APIの回数制限を超えていると通信エラーになります。)
<label>Subscription Key(Primary key): <input type="text" id="sample2_key" /></label>
<div id="sample2_input">
  <label>顔写真: <input id="sample2_input" type="file" /></label>
</div>
<div id="sample2_output"></div>
#sample2_output{
  position:relative;
  overflow:hidden;
}
#sample2_output div{
  position:absolute;
}
#sample2_output .f{
  border:1px solid #fff;
}
#sample2_output .a{
  color:#D86718;
  background:#F1D100;
  font-size:15px;
  line-height:1em;
}
$(function(){
  var output = $('#sample2_output');
  //ファイル選択の変更イベント
  $('#sample2_input').on('change', 'input', function(event){
    //前回の処理結果をクリアする。
    output.empty();
    var reader = new FileReader();
    reader.onload = (function(theFile) {
      return function(e) {
        var dataurl = e.target.result;
        
        //DataURLをBLOBに変換
        var mime_base64 = dataurl.split(',', 2);
        var mime = mime_base64[0].split(';');
        mime = mime[0].split(':');
        mime = mime[1]? mime[1]: mime[0];
        var base64 = window.atob(mime_base64[1]);
        var len = base64.length;
        var bin = new Uint8Array(len);
        for (var i=0; i<len; i++)
        {
          bin[i] = base64.charCodeAt(i);
        }
        var blob = new Blob([bin], {type:mime});
        //APIのURLパラメタを設定
        var params = {
          'Subscription-Key': $('#sample2_key').val()
          ,returnFaceAttributes: 'age,gender'
        };
        $.ajax({
          url: 'https://api.projectoxford.ai/face/v1.0/detect?' + $.param(params)
          ,type: 'POST'
          //contentTypeを明示する
          ,contentType:'application/octet-stream'
          //processDataをfalseにして自動処理せずBLOBをPOSTする
          ,processData:false
          ,data:blob
        })
        .done(function(data) {
          //選択画像のDataURLで画像を生成
          $('<img />').attr('src', dataurl).appendTo(output);
          //検出した顔の分だけ繰り返す
          $.each(data, function(){
            var face = this.faceRectangle;
            var attr = this.faceAttributes;
            //顔を囲む矩形のdivを追加
            $('<div class="f" />').css({
              left:face.left+'px'
              ,top:face.top+'px'
              ,width:face.width+'px'
              ,height:face.height+'px'
            })
            .appendTo(output);
            //性別と年齢のdivを追加
            $('<div class="a" />').css({
              left:face.left+'px'
              ,top:(face.top + face.height)+'px'
            })
            .text(attr.gender +' '+ attr.age)
            .appendTo(output);
          });
        })
        .fail(function() {
          alert('error');
        });
      };
    })(event.target.files[0]);
    reader.readAsDataURL(event.target.files[0]);
  });
});

同じく$.ajaxでFaceAPIを呼び出しています。この例では選択したファイルをimgタグで表示するためDataURL形式で読み込み、バイナリでPOSTするためにBLOBへ変換しています。また、バイナリ送信時はprocessDataにfalseを指定し、Content-Typeも明示します。

実用においては、大きな画像でもはみ出さないよう補正したり、FileAPIへの対応可否チェックを入れる等さらに処理が必要ですが、ごく簡単な指定でFaceAPIを呼び出せ、顔検出や年齢推定を実現できることが伝われば幸いです。