Amazon Rekognitionを使って顔の部分を切り取る

画像のサムネイル一覧などを表示させる時、サイズ固定で`object-fit:cover`とかすると画像の中心を基準に切り取られるので、特に人物画像だと顔が切れたりして残念な感じになることがあります。
そこで、Amazon Rekognitionを使って顔の部分が切り取られるようにしてみたいと思います。

## Amazon Rekognitionのアクセス権限を追加

AWSコンソールで[IAM](https://console.aws.amazon.com/iam/)→ユーザーで自分のアカウントの詳細ページを表示すると、『アクセス権限の追加』というボタンがあるので、『AmazonRekognitionReadOnlyAccess』を追加します。
グループで追加とか方法は色々あるけど、とりあえず『既存のポリシーを直接アタッチ』で良いかと。

## アクセスキー

同じく[IAM](https://console.aws.amazon.com/iam/)のユーザーページの『認証情報』タブから『アクセスキーの作成』ボタンで作成できます。

## AWS SDKを入れる
[言語は色々あります](https://aws.amazon.com/jp/tools/#sdk)が、PHPでやりたいので、AWS SDK for PHPをcomposerでインストールします。

```shell
cd ~/workspace/face
curl -sS https://getcomposer.org/installer | php
php composer.phar require aws/aws-sdk-php
```

`~/workspace/face/vendor/`以下に色々入りました。

## 顔認識

`RekognitionClient`を使って`DetectFaces`を呼びます。
画像はS3上にあるファイルを指定して処理することもできますが、とりあえずローカルのファイルを試してみます。
参考：[AWS SDK for PHP 3.x - Amazon Rekognition](https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-rekognition-2016-06-27.html#detectfaces)

```php
<?php
require 'vendor/autoload.php';
use Aws\Rekognition\RekognitionClient;

// 処理したい画像ファイル
$img_file = 'sample.jpg';

$options = [
    'region'      => 'us-west-2',
    'version'     => 'latest',
    'credentials' => [
        'key'    => '自分のアクセスキー',
        'secret' => '自分のシークレットキー'
    ]
];

try {
    $rekognition = new RekognitionClient($options);
    // Call DetectFaces
    $result = $rekognition->DetectFaces([
        'Image' => [
            'Bytes' => file_get_contents($img_file),
        ],
        'Attributes' => ['DEFAULT']
    ]);
} catch (Exception $e) {
    echo $e->getMessage() . PHP_EOL;
    exit('DetectFaces error');
}
var_dump($result['FaceDetails']);
?>
```

aws-cliを入れてスクリプトの実行ユーザーが使える状態になっていれば(`~/.aws/`に`config`と`credentials`というファイルがあるはず)、`$options['credentials']`は指定しなくても取れます。

>話がそれますが、aws-cliは
`aws configure --profile プロファイル名`
とかで複数のアクセスキーを設定できて、
`export AWS_PROFILE=プロファイル名`
で使用するプロファイルを切り替えられるようになっているので、状況によって使い分けられそうです。
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-multiple-profiles.html

`Attributes`は`ALL`と`DEFAULT`が指定でき、`ALL`は年齢、性別、笑ってるかとかメガネかけてるかとか髭があるかとか色々取れますが、処理に時間がかかるようです。

上記var_dumpの出力は以下のような感じになります。

```
array(2) {
  [0] =>
  array(5) {
    'BoundingBox' =>
    array(4) {
      'Width' =>
      double(0.22533021867275)
      'Height' =>
      double(0.21245421469212)
      'Left' =>
      double(0.54390054941177)
      'Top' =>
      double(0.14285714924335)
    }
    'Landmarks' =>
    array(5) {
      [0] =>
      array(3) {
        'Type' =>
        string(7) "eyeLeft"
        'X' =>
        double(0.60547262430191)
        'Y' =>
        double(0.22814635932446)
      }
      [1] =>
      array(3) {
        'Type' =>
        string(8) "eyeRight"
        'X' =>
        double(0.68980967998505)
        'Y' =>
        double(0.20752842724323)
      }
      [2] =>
      array(3) {
        'Type' =>
        string(4) "nose"
        'X' =>
        double(0.64649963378906)
        'Y' =>
        double(0.25950169563293)
      }
      [3] =>
      array(3) {
        'Type' =>
        string(9) "mouthLeft"
        'X' =>
        double(0.62794500589371)
        'Y' =>
        double(0.30737021565437)
      }
      [4] =>
      array(3) {
        'Type' =>
        string(10) "mouthRight"
        'X' =>
        double(0.70395141839981)
        'Y' =>
        double(0.28854155540466)
      }
    }
    'Pose' =>
    array(3) {
      'Roll' =>
      double(-14.593152999878)
      'Yaw' =>
      double(-9.3614797592163)
      'Pitch' =>
      double(5.2676825523376)
    }
    'Quality' =>
    array(2) {
      'Brightness' =>
      double(54.012958526611)
      'Sharpness' =>
      double(89.902519226074)
    }
    'Confidence' =>
    double(99.98902130127)
  }
  [1] =>
  array(5) {
    'BoundingBox' =>
    (以下略)
    ...
  }
}
```

顔が複数ある場合は、上記のように`$result['FaceDetails']`に複数入って返ってきます。
顔の表示面積が大きい順になってるぽい。

今回使いたいのは、`BoundingBox`で、これで画像のどこに顔があるかがわかります。
数値は比率で、画像の左上が(0, 0)、右下が(1, 1)なイメージ。

## 顔のところで切り取ってみる

`DetectFaces`で返ってきた`BoundingBox`を使って画像を切り取ってみます。
大きく写っている3人の顔が入る領域を計算し、その中心を基準に切り取ります。

```php:src/test.php
<?php
// 表示するサイズ
$disp_w = 200;
$disp_h = 150;
// 画像ファイル
$img_file = '../data/input/sample.jpg';

$size = getimagesize($img_file);
if (empty($size[0]) || empty($size[1])) {
    exit('no image size');
}
$width = $size[0];
$height = $size[1];

$disp_ratio = $disp_w / $disp_h;
$img_ratio = $width / $height;
if ($disp_ratio < $img_ratio) {
    // 横がはみ出る
    $crop_w = $disp_w * $height / $disp_h;
    $crop_h = $height;
} else {
    // 同じまたは縦がはみ出る
    $crop_w = $width;
    $crop_h = $disp_h * $width / $disp_w;
}
?>

<h3>original image</h3>
<img src="<?php echo $img_file; ?>">

<?php
require '../vendor/autoload.php';
use Aws\Rekognition\RekognitionClient;

$options = [
    'region'      => 'us-west-2',
    'version'     => 'latest',
    'credentials' => [
        'key'    => '自分のアクセスキー',
        'secret' => '自分のシークレットキー'
    ]
];

try {
    $rekognition = new RekognitionClient($options);
    // Call DetectFaces
    $result = $rekognition->DetectFaces([
        'Image' => [
            'Bytes' => file_get_contents($img_file),
        ],
        'Attributes' => ['DEFAULT']
    ]);
} catch (Exception $e) {
    echo $e->getMessage() . PHP_EOL;
    exit('DetectFaces error');
}

$min_x = $min_y = 1;
$max_x = $max_y = 0;
foreach ($result['FaceDetails'] as $key => $face) {
    $w = round($width * $face['BoundingBox']['Width']);
    $h = round($height * $face['BoundingBox']['Height']);
    $left = round($width * $face['BoundingBox']['Left']);
    $top = round($height * $face['BoundingBox']['Top']);
    // 顔部分を表示
    echo <<<EOD
<h3>img{$key}</h3>
<div style="width:{$w}px;height:{$h}px;background:url({$img_file}) no-repeat -{$left}px -{$top}px"></div>

EOD;
    echo '<pre>' . var_export($face['BoundingBox'], true) . '</pre>' . PHP_EOL;

    // 大きく写ってる上位3人が入る枠を調べる
    if ($key < 3) {
        $min_x = min($min_x, $face['BoundingBox']['Left']);
        $max_x = max($max_x, $face['BoundingBox']['Left'] + $face['BoundingBox']['Width']);
        $min_y = min($min_y, $face['BoundingBox']['Top']);
        $max_y = max($max_y, $face['BoundingBox']['Top'] + $face['BoundingBox']['Height']);
    }
}

// どう切ったらいい感じになるか計算
$center_x = $width * ($min_x + $max_x) / 2;
$center_y = $height * ($min_y + $max_y) / 2;
$half_w = $crop_w / 2;
$half_h = $crop_h / 2;

if ($disp_ratio < $img_ratio) {
    // 横がはみ出る
    $left = $center_x - $half_w;
    $right = $center_x + $half_w;
    if ($left < 0) {
        $position_x = 'left';
    } elseif ($width < $right) {
        $position_x = 'right';
    } else {
        $position_x = '-' . round($left * $disp_w / $crop_w) . 'px';
    }
    $position_y = 'center';
} else {
    // 縦がはみ出る
    $top = $center_y - $half_h;
    $bottom = $center_y + $half_h;
    if ($top < 0) {
        $position_y = 'top';
    } elseif ($height < $bottom) {
        $position_y = 'bottom';
    } else {
        $position_y = '-' . round($top * $disp_h / $crop_h) . 'px';
    }
    $position_x = 'center';
}
echo <<<EOD
<img src="{$img_file}" style="width:{$disp_w}px;height:{$disp_h}px;object-fit:cover;object-position:{$position_x} {$position_y}">
EOD;
?>
```

ブラウザで開くと元画像とそれぞれの顔の部分と、指定した表示サイズで切り取った画像が表示されます。

お試しということで、直接表示させてみましたが、`DetectFaces`に時間がかかる(数秒)ので、前もって取得して計算しておくとか、トリミングした画像を保存しておくとかすると良いかなーと思いました。
S3のファイルについても処理できるので、Lambdaとか使ってS3へアップと同時に切り取ったサムネイルも保存、とかするといいのでしょう、きっと。

ちなみに、CSS3の`object-position`って数値で指定したことなかったのですが、px指定だと、オフセット的な意味になるらしく、表示領域に対してどのくらいずらすか、という指定になります。

今回、顔が複数ある場合は大きく写っている3人までが入る領域を計算してそこを中心として切るようにしてみましたが、顔が横に並んでいて横を切る場合は、やっぱり残念な感じになってしまうかもしれないなー、と思ったり。難しい…。