AWS Lambdaで顔認識アプリを作る

JAWS-UG名古屋での発表用に、顔認識をするようなアプリをLambdaで作りました。
内容としては、アップロードした画像を顔認識して、両目の座標を取得したのち目線を入れる、というものになります。

仕組み

仕組みとしては、以下のような構成になります。


この処理のポイントとしては、クライアントサイドでGUIDを生成して、そのGUID名を使ってデータをやり取りするところになります。クライアントはGUIDを生成して、AWS SDK for Javascriptを使ってデータをアップロードします。その後、クライアントサイドはGUID名/result.jsonをポーリングします。
通知を受けたLambdaはサーバサイドで顔認識を行い、結果をGUID/result.jsonの形で吐き出します。
クライアントサイドで作ったIDで、Lambdaと個別やりとりをする形になり、こうすることでEC2を使わずに、Lambdaと1 on 1で対話出来るようにしています。

デモサイト

以下のURLで、デモサイトを起動しています。
http://bit.ly/nagoya-lt

人が写っている画像ファイルを選択後、認識実行すると上記の仕組みが動き、画像に目線が入ります。

サーバサイド処理

実のところ、本当はサーバサイドで画像加工したかったのですが、phantomjsやopencvの設定が今一つうまく行かず、LT発表の時間が迫っていたため、今回は外部サービス(SkyBiometry)に頼ってしまいました(このサービスは、以前Ice Bucket Challengeのときに id:tottokug さんから教えてもらいました!)
ですのでサーバサイドは実に簡潔です。

var aws = require('aws-sdk');
var http = require('http');
exports.aws = aws;

exports.handler = function(event, context) {
  var bucket = event.Records[0].s3.bucket.name;
  var key = event.Records[0].s3.object.key;
  var region = event.Records[0].awsRegion;
  var op = event.Records[0].eventName;
  if(op !=="ObjectCreated:Put"){
    context.done(null, 'op');
    return;
  }
  console.log("key="+key);
  var skybio = "http://api.skybiometry.com/fc/faces/detect.json?api_key=XXXXXXX&api_secret=XXXXXXX&urls=";
  skybio += "https://S3バケット/" + key;
  console.log(skybio);

  http.get(skybio, function(res) {
    var body = "";
    res.on("data", function(chunk) {
      body += chunk;
    });
    res.on('end', function() {
      var uuid = key.split("/")[0];
      console.log("uuid=" + uuid);
      var resultKey = uuid + "/result.json";
      var s3 = new aws.S3({
        params : {
          Bucket : bucket,
          Key : resultKey
        }
      });
      s3.putObject({
        Body : body
      }, function(err) {
        if (err) {
          context.done("ERROR", err);
        } else {
          context.done(null, 'put result');
        }
      });
    });
  }).on('error', function(e) {
    console.log("Got error: " + e.message);
  });
};

クライアント処理

<!DOCTYPE html>
<html>
<head>
<title>S3 uploader</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.0.29.min.js"></script>
<script
	src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>          
	var s3BucketName = "バケット名";
	var s3RegionName = "us-west-2";  
	var guid;
	var fileName;
	var file;
	
	function uploadFile() {
		disableUploadButton();
		var params = {
				AccountId: "AWSアカウントID",
				RoleArn: "IAMロール",
				IdentityPoolId: "CogniteのIdentityPoolId"
		};
		AWS.config.region = 'us-east-1';
		AWS.config.credentials = new AWS.CognitoIdentityCredentials(params);
		AWS.config.credentials.get(function(err) {
			if (!err) {
				beginUpload();
			}else{
				alert(err);
			}
		});	 
	}
	function beginUpload(){
		$('#putButton').text('アップロード中....');
		guid = generateGUID();
		file = document.getElementById('fileToUpload').files[0];
		if (file) {
			fileName = file.name;
			var key = guid+"/"+fileName;
			var s3 = new AWS.S3({region: s3RegionName, maxRetries: 100});
			s3.putObject({Bucket: s3BucketName,Key: key, ContentType: file.type, Body: file},
					function(err, data) {
				if (data !== null) {
					waitForResult();
				}
				else {
					alert("エラーが発生しました。 "+err);
					enableUploaButton();
				}
			});
		}
	}
	function waitForResult(){
		$('#putButton').text('認識実行中....');
		var key = guid+"/result.json";
		var s3 = new AWS.S3({region: s3RegionName, maxRetries: 1});
		s3.getObject({Bucket: s3BucketName,Key: key},
				function(err, data) {
			if (data !== null) {
				showImage(data);
			}
			else {
				setTimeout(waitForResult,1000);
			}
		});
	}
	function showImage(data){
		var result = JSON.parse(data.Body);
		drawBlindfold(result);
	}
	function drawBlindfold(result){ 
		var canvas = document.getElementById('myCanvas');
		if ( ! canvas || ! canvas.getContext ) { 
			alert("canvas is not supported");
			return false; 
		}
		var photo = result.photos[0];
		canvas.width = photo.width;
		canvas.height = photo.height;
		var ctx = canvas.getContext('2d');
	
		var img = new Image();
		img.src = result.photos[0].url+"?" + new Date().getTime();
		img.onload = function() {
	
			ctx.drawImage(img, 0, 0);
			ctx.strokeStyle = 'rgb(0,0,0)';
			ctx.fillStyle = 'rgb(0,0,0)';
			//目線入れる
			var tags = photo.tags;
			for(var i = 0;i < tags.length;i++){
				var tag = tags[i];
				var eye_left = tag.eye_left;
				var eye_right = tag.eye_right;
				if(eye_left!=undefined && eye_right!=undefined){
					var w = photo.width;
					var h = photo.height;
					var rightX = w * eye_right.x / 100;
					var rightY = h * eye_right.y / 100;
					var leftX = w * eye_left.x / 100;
					var leftY = h * eye_left.y / 100;
					ctx.beginPath();
					var marginX = 30;var marginY=10;
					ctx.moveTo(rightX-marginX,rightY-marginY);
					ctx.lineTo(leftX+marginX,leftY-marginY);
					ctx.lineTo(leftX+marginX,leftY+marginY);
					ctx.lineTo(rightX-marginX,rightY+marginY);
					ctx.lineTo(rightX-marginX,rightY-marginY);
					ctx.closePath();
					ctx.stroke();
					ctx.fill();
				}
			}
			enableUploaButton();
		}
	}
	function disableUploadButton(){
		$("#putButton").attr('disabled', true);
		$('#putButton').text('認証情報取得中....');
	}
	function enableUploaButton(){
		$('#putButton').attr('disabled', false);
		$('#putButton').removeAttr('disabled');
		$('#putButton').text('認識完了');
	}
	function generateGUID() {
		var s4 = function() {
			return Math.floor((1 + Math.random()) * 0x10000)
			.toString(16)
			.substring(1);
		}
		return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
		s4() + '-' + s4() + s4() + s4();
	}
</script>
</head>
<body>
	<center>
		<h1>AWS Lambdaで顔認識</h1>
		<h2>画像ファイルを選択して、認識実行を押してください。</h2>
		<input type="file" id="fileToUpload" />
		<button id="putButton" onclick="uploadFile()">認識実行</button>
		<br> <br>
		<canvas id="myCanvas"></canvas>
</body>
</center>
</html>

いずれにせよ、工夫次第でS3+Lambdaだけで動的(っぽい)サイトが作れそうですね。