顔を中心にしたサムネイルの自動生成

デジタルクエスト AdventCalendarクリスマスイブ担当の綿引です。
お仕事でご要望いただいた要件で、「画像ファイルのサムネイルは顔を中心にしてほしい！」といただきまして、そちらの検証をしてみました。

サムネイル生成といえば、「Amazon Lambda + S3」
今回はここに 「+ Amazon Rekognition」 の組み合わせで、ご所望の「顔を中心に」にトライしました。

## Rekognition リファレンス
[API Actionsの一覧](https://docs.aws.amazon.com/ja_jp/rekognition/latest/dg/API_Operations.html)
[今回使用する DetectFaces の説明](https://docs.aws.amazon.com/ja_jp/rekognition/latest/dg/faces-detect-images.html)

## おおまかな流れ
![スクリーンショット 2018-12-20 20.57.55.png](https://qiita-image-store.s3.amazonaws.com/0/18074/ca7ed8d8-7cfe-70db-0ecb-daac7275d44c.png)

Amazon Lambda + S3 でのサムネイル生成は下記をそのまま使用させてもらいました。
[AWS lambdaを使ってS3へのアップをトリガーにサムネイルを作る](https://qiita.com/DaichiSaito/items/8926a4259fcfa1cde7fe)
（ありがとうございます！）

## Amazon Rekognitionとの連携
Lambda上からは特に意識することなく、newで呼ぶだけでした。

```javascript
const rekognition = new AWS.Rekognition();
```

## 「顔の中心」の定義
検証なので、やや強引ではありますが、顔の真ん中あたりにある「鼻」の座標を「顔の中心」と定義します。
※顔の正確な座標は、BoundingBoxパラメータを使用した方がよいです。

## サムネイルの生成ロジック
outputのimage sizeは、縦横ともに200pxと指定されていましたので、鼻が中央にくるように、
かつ、切り抜きサイズが画像サイズ外とならないように、条件分岐を適度につけます。
(以下抜粋)

```javascript
			// rekognition分析
			rekognition.detectFaces(params, (err, rdata) => {
				if (err) {
					context.done('error can not detectLabels', err);
				}
				var nosePosition = new Object();
				nosePosition.X = rdata.FaceDetails[0].Landmarks[4].X * value.width;
				nosePosition.Y = rdata.FaceDetails[0].Landmarks[4].Y * value.height;
				
				//切り抜きサイズ計算
				var cropParam = new Object();
				if(value.width > value.height){ // 横長画像
					cropParam.L = value.height;
					cropParam.Y = 0;
					if(nosePosition.X - (value.height / 2) < 0){
						cropParam.X = 0;
					}else if(value.width < cropParam.L + nosePosition.X - (value.height / 2)){
						cropParam.X = cropParam.L + cropParam.X - value.width;
					}else{
						cropParam.X = nosePosition.X - (value.height / 2);
					}					
				}else{ // 縦長画像
					cropParam.L = value.width;
					cropParam.X = 0;
					if(nosePosition.Y - (value.width / 2) < 0){
						cropParam.Y = 0;
					}else if(value.height < cropParam.L + nosePosition.Y - (value.width / 2)){
						cropParam.Y = cropParam.L + cropParam.Y - value.height;
					}else{
						cropParam.Y = nosePosition.Y - (value.width / 2);
					}
				}
				console.log("cropParam: ");
				console.log(cropParam);
				gm(data.Body)
						.crop(cropParam.L, cropParam.L, cropParam.X, cropParam.Y)
						.resize(null, '200')
```

### 生成された画像サンプル (縦長)
![crop1.png](https://qiita-image-store.s3.amazonaws.com/0/18074/cbf6d92e-e79d-8d02-0f1c-1fa4a2a983dc.png)

### 生成された画像サンプル (横長)
![crop3.png](https://qiita-image-store.s3.amazonaws.com/0/18074/8f2c8fc4-27f1-62f5-aff3-ec9a40bbfeaf.png)


複数名が写っているときは、最初に顔認識された方(0番目)に寄りました。
なぜ女の子が0番目なのかはわかりません。
うちのムスメかわいいって、Rekognitionされたんだと思います。

## Retry
とか言ってお茶を濁しているわけにもいかないので、
きちんと複数人分のデータを取得して平均値を出すようにロジックを修正します。
(以下抜粋)

```javascript
			// rekognition分析
			rekognition.detectFaces(params, (err, rdata) => {
				if (err) {
					context.done('error can not detectLabels', err);
				}
				
				var nosePosition = new Array();
				var fixPosition = new Object();
				var i = 0;
				var sumX = 0;
				var sumY = 0;
				
				rdata.FaceDetails.forEach(function( faceParam ) {
					var X = rdata.FaceDetails[i].Landmarks[4].X * value.width;
					var Y = rdata.FaceDetails[i].Landmarks[4].Y * value.height;
					nosePosition[i] = {"X":X, "Y":Y};
					sumX += X;
					sumY += Y;
					i++;
				});
				
				fixPosition.X = sumX/i;
				fixPosition.Y = sumY/i;
				
				//切り抜きサイズ計算
				var cropParam = new Object();
				if(value.width > value.height){ // 横長画像
					cropParam.L = value.height;
					cropParam.Y = 0;
					if(fixPosition.X - (value.height / 2) < 0){
						cropParam.X = 0;
					}else if(value.width < cropParam.L + fixPosition.X - (value.height / 2)){
						cropParam.X = cropParam.L + cropParam.X - value.width;
					}else{
						cropParam.X = fixPosition.X - (value.height / 2);
					}					
				}else{ // 縦長画像
					cropParam.L = value.width;
					cropParam.X = 0;
					if(fixPosition.Y - (value.width / 2) < 0){
						cropParam.Y = 0;
					}else if(value.height < cropParam.L + fixPosition.Y - (value.width / 2)){
						cropParam.Y = cropParam.L + cropParam.Y - value.height;
					}else{
						cropParam.Y = fixPosition.Y - (value.width / 2);
					}
				}
				console.log("cropParam: ");
				console.log(cropParam);
				gm(data.Body)
						.crop(cropParam.L, cropParam.L, cropParam.X, cropParam.Y)
						.resize(null, '200')
```
### 生成された画像サンプル (横長)
![crop4.png](https://qiita-image-store.s3.amazonaws.com/0/18074/02b47deb-a69d-9703-6f45-7b0d3515f2b1.png)

## まとめ
Rekognition 初めて触ったんですが、ほんとうにイージーでとても助かりました。
顔同士の比較、画像へのタグ付けなどにも適用していけるので、サービスにも活用できそうです。
今回の検証から更に、BoudingBoxパラメータを見て、顔のzoomをある程度揃えたサムネイルが生成できるような調整もしてみたいです。

## 要検討事項
- 異常系のキャッチとリカバリ
- Lambda, Rekognition の制限とパフォーマンス
- crop座標算出ロジックの改善 (zoomをできるだけ均等にする努力)

ご一読いただきありがとうございました。