Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

Rekognition リファレンス

API Actionsの一覧
今回使用する DetectFaces の説明

おおまかな流れ

スクリーンショット 2018-12-20 20.57.55.png

Amazon Lambda + S3 でのサムネイル生成は下記をそのまま使用させてもらいました。
AWS lambdaを使ってS3へのアップをトリガーにサムネイルを作る
(ありがとうございます!)

Amazon Rekognitionとの連携

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

const rekognition = new AWS.Rekognition();

「顔の中心」の定義

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

サムネイルの生成ロジック

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

            // 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

生成された画像サンプル (横長)

crop3.png

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

Retry

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

            // 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

まとめ

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

要検討事項

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

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

higehiki
エンジニアとはもう呼ばれないぐらい書いてません…。
https://www.paronym.jp/
paronym
インタラクティブ動画サービス「TIG」の開発・提供をしています
https://www.paronym.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away