Node.jsでAmazon Rekognitionを手っ取り早く試す(結果はnode-canvasで画像に描画)

# はじめに

AmazonのRekognitionで動作確認をしてみました。
公式サイトのドキュメントやGitHubのサンプルを見てもサーバーレスで格好良く(？)試すやり方が記載されていましたが、そこまでリソース(私やらAWSやらの)を使いたくなかったので極力シンプルにサクッと試せる方法を調べ実装＆動作確認をしてみました。

# やりたいこと

* Amazon Rekognitionの動作確認。
* S3とかLambdaを駆使しない。(簡単にサクッと)
* 手元にある画像ファイルをそのまま入力ファイルとして使いたい。
* 解析結果を入力データとして使った画像に描画して分かりやすく。

# 事前に必要なこと

### nodeの最新化
　諸事情でnodeは8.17.0を利用していましたが、canvasに新しいバージョンを要求されたので、14.15.3に切り替えました。

### 専用のIAMユーザーを作成し、AWS SDKやAWS CLIで利用できるような状態にしておく
　ソース内の"rekognition-dev"がその設定を行ったprofile名です。

AWS SDKのセットアップや、その他node-moduleのセットアップも当然必要ですが、その辺りは公式のドキュメントもありますので、ここでは省略したいと思います。

# 実行環境

|種別|種類(Ver)|
|:-:|:-:|
|OS|Linux(Ubuntu 18.04.6 LTS)|
|node|14.15.3|
|aws-sdk|2.997.0|
|canvas|2.8.0|

# ソース

```Javascript:rekognition_sample.js
// for AWS
const AWS = require('aws-sdk');

const MAX_LABELS = 50;                  // 検出数
const MIN_CONFIDENCE = 80;              // 精度
const REGION = "ap-northeast-1"

// for draw result
const imageSize = require('image-size');
const { createCanvas, loadImage } = require('canvas');

// for file control
const fs = require('fs');
const path = require('path');

const INPUT_DIR = "./01_input";
const OUTPUT_DIR = "./02_output";

AWS.config.credentials = new AWS.SharedIniFileCredentials({region: REGION, profile: 'rekognition-dev'});

fs.readdir(INPUT_DIR, (errReadDir, inputItems) => {
    if (errReadDir) {
        console.log("Failed read dir :", errReadDir);
        return;
    }

    inputItems.map((itemName) => {
        var  intputFilePath = path.join(INPUT_DIR, itemName);

        fs.readFile(intputFilePath, function(errorReadFile, inputData) {
            if (errorReadFile) {
                console.log("Failed read file :", errorReadFile);
                return;
            }
        
            var rekognition = new AWS.Rekognition({apiVersion: "2016-06-27", region: REGION, profile: 'rekognition-dev'});
        
            var labelParams = {
                Image: {
                    Bytes: inputData
                },
                MaxLabels: MAX_LABELS,
                MinConfidence: MIN_CONFIDENCE
            };

            console.time("detectLabels");
            rekognition.detectLabels(labelParams, function(errorDetectLabel, resultDetectLabels){
                console.timeEnd("detectLabels");
                if (errorDetectLabel) {
                    console.log("ERROR on detectLabels");
                    console.log(errorDetectLabel, errorDetectLabel.stack);
                } else {
                    outputImage(intputFilePath, itemName, resultDetectLabels);
                }
            });
        });
    });
});

function outputImage(intputFilePath, itemName, resultDetectLabels) {
    var width = imageSize(intputFilePath).width;
    var height = imageSize(intputFilePath).height;

    var canvas = new createCanvas(width, height);
    const context = canvas.getContext('2d');

    loadImage(intputFilePath).then((srcImage) => {
        context.drawImage(srcImage, 0, 0, width, height);

        context.font = '30px sans-serif';

        var personCount = 0;
        if (resultDetectLabels.Labels.length > 0) {
            for (var i = 0; i < resultDetectLabels.Labels.length; i++) {
                console.log("detectLabels : label", i, " =", resultDetectLabels.Labels[i].Name);

                context.strokeStyle = 'rgba(255, 0, 0, 1.0)';

                if (resultDetectLabels.Labels[i].Name === "Person") {
                    // 検出した人物の範囲
                    for (var j = 0; j < resultDetectLabels.Labels[i].Instances.length; j++) {
                        var personLeft = resultDetectLabels.Labels[i].Instances[j].BoundingBox.Left * width;
                        var personTop = resultDetectLabels.Labels[i].Instances[j].BoundingBox.Top * height;
                        var personWidth = resultDetectLabels.Labels[i].Instances[j].BoundingBox.Width * width;
                        var personHeight = resultDetectLabels.Labels[i].Instances[j].BoundingBox.Height * height;

                        context.strokeRect(personLeft, personTop, personWidth, personHeight);

                        personCount++;
                    }
                }
            }
        }

        context.font = '30px sans-serif';

        var strText1 = 'Person=' + personCount;
        var drawText1 = context.measureText(strText1);
        context.fillStyle = 'rgba(255, 0, 0, 1.0)';
        context.fillText(strText1, 10, drawText1.emHeightAscent);

        const out = fs.createWriteStream(path.join(OUTPUT_DIR, itemName));
        const stream = canvas.createJPEGStream();
        stream.pipe(out);
        out.on('finish', () =>  console.log('The JPEG file was created.'));
    });
}
```

# 実行

弊社代表のプロフィール写真を入力し実行した結果です。

![member01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1236186/c22355a5-c809-5312-014a-1e401ac24038.jpeg)

んー、お試し画像としては面白みがないですねｗ。

# ソースの説明

それではソースの説明を個々に記載します。

## new AWS.SharedIniFileCredentials

```Javascript:
AWS.config.credentials = new AWS.SharedIniFileCredentials({region: REGION, profile: 'rekognition-dev'});
```

私が試す実行環境ではデフォルトのプロファイル(アクセスキーとシークレットキー)ではなく、今回追加したrekognition-devのプロファイルでしたので上記のコードが必要でした。デフォルトプロファイルが利用したいEC2のもの、または1個しかない場合は不要だと思います。

## fs.readFile

```Javascript:
        fs.readFile(intputFilePath, function(errorReadFile, inputData) {
```

公式のドキュメントではdetectLabels()の画像データ(Image:Bytes)にはbase64化した文字列を設定するように書かれていましたが、JavaScript SDKではBuffer形式のままでいいようです。以下のように'base64'を指定する必要はありません。

```Javascript:
        fs.readFile(intputFilePath, 'base64', function(errorReadFile, inputData) {
```

## new AWS.Rekognition

```Javascript:
            var rekognition = new AWS.Rekognition({apiVersion: "2016-06-27", region: REGION, profile: 'rekognition-dev'});
```

JavaScript SDKを利用するための準備です。こちらもデフォルトプロファイルが利用したいEC2のもの、または1個しかない場合はprofileの指定は不要だと思います。apiVersionは公式のサンプルで指定されているものをそのまま指定しました。

## rekognition.detectLabels

```Javascript:
            var labelParams = {
                Image: {
                    Bytes: inputData
                },
                MaxLabels: MAX_LABELS,
                MinConfidence: MIN_CONFIDENCE
            };

            rekognition.detectLabels(labelParams, function(errorDetectLabel, resultDetectLabels){
```

あとはdetectLabels()に必要なパラメータを構築して呼び出すだけですね。これだけです。

今回は手元にある画像ファイルのデータに対し解析する方式を採用していますが、パラメータの指定の仕方によってS3にアップロードしている画像ファイルを入力とすることもできます。Rekognitionのサンプルでよく見るのはS3を利用する方法でした。詳細についてはdetectLabels()のドキュメントをご確認ください。

## 結果の描画

```Javascript:
        if (resultDetectLabels.Labels.length > 0) {
            for (var i = 0; i < resultDetectLabels.Labels.length; i++) {
                console.log("detectLabels : label", i, " =", resultDetectLabels.Labels[i].Name);

                context.strokeStyle = 'rgba(255, 0, 0, 1.0)';

                if (resultDetectLabels.Labels[i].Name === "Person") {
                    // 検出した人物の範囲
                    for (var j = 0; j < resultDetectLabels.Labels[i].Instances.length; j++) {
                        var personLeft = resultDetectLabels.Labels[i].Instances[j].BoundingBox.Left * width;
                        var personTop = resultDetectLabels.Labels[i].Instances[j].BoundingBox.Top * height;
                        var personWidth = resultDetectLabels.Labels[i].Instances[j].BoundingBox.Width * width;
                        var personHeight = resultDetectLabels.Labels[i].Instances[j].BoundingBox.Height * height;

                        context.strokeRect(personLeft, personTop, personWidth, personHeight);

                        personCount++;
                    }
                }
            }
        }
```

rekognition.detectLabels()で指定したresultDetectLabelsに結果が返ってきます。

今回は人物だけを検出したかったので、if (resultDetectLabels.Labels[i].Name === "Person") { としています。
ここを変更すれば

* 車
* 自転車

など色々な物体が検出できるようになります。

また検出した人物の位置(Left、Top)、大きさ(Width、Height)は元画像の解像度に対する割合で返却されてきますので、Left * width といった形で元画像を基準とした位置と大きさに変換しています。あとは context.strokeRect() で描画するだけです。

# あとがき

## 解析速度

```Text
            console.time("detectLabels");
            rekognition.detectLabels(labelParams, function(errorDetectLabel, resultDetectLabels){
                console.timeEnd("detectLabels");
```

上記の console.time("detectLabels") ～ console.timeEnd("detectLabels") の結果が以下になります。

```Text
detectLabels: 552.95ms
```

前述したように手元の画像ファイル(80KB)を入力としたため、S3を利用する方式ですと気持ち短縮されるかもしれません。

## 顔検出について

rekognition.detectFaces()を利用すれば顔検出もでき、性別判定／年齢推定／表情判定などが行えます。
やり方は上記のrekognition.detectLabels()とほぼ一緒なのですごく簡単に試せます。

## 苦労したこと

今回の動作確認で一番苦労したのはRekognitionの利用方法より、Node.jsでのCanvas(node-canvas)の使い方(検出結果を入力画像へ描画し保存する)でした。
ソースを貼り付けているので不要かとは思いますが、以下ポイントだけ。

```JavaScript
const { createCanvas, loadImage } = require('canvas');
````

canvasでやりたいことが具体的にどういう定義をして、その関数をどのように使えばよいか、に辿り着くまでに時間がかかりました。
結果上記の定義でOKでした。createCanvas, loadImageは次のコンストラクタ名、関数名になります。

```JavaScript
    var canvas = new createCanvas(width, height);
    const context = canvas.getContext('2d');

    loadImage(intputFilePath).then((srcImage) => {
        context.drawImage(srcImage, 0, 0, width, height);
````

これでようやく検出結果を描画する下地(画像)ができました。
あとは検出した位置に四角を書いたり結果(検出数)を描画したりするだけです。