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

この記事は執筆時点(2015年)の情報に基づいて記載されております。記載のProject Oxfordは現在終了しており、Face APIはMicrosoft Azureの公式なAPIとして提供されております。 (このため、記事中のProject Oxford版サンプルは現在では動作いたしません。ご注意下さい。)

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="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Paul_Nurse_portrait.jpg/180px-Paul_Nurse_portrait.jpg" alt="ポール・ナース - Wikipedia" />
	  <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Mario_molina.jpg/220px-Mario_molina.jpg" alt="マリオ・モリーナ - Wikipedia" />
	  <img src="https://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を呼び出せ、顔検出や年齢推定を実現できることが伝われば幸いです。