【Rekognition, Jenkins】「この人、名前何だっけ？」に応えるAmazon Rekognition+Jenkins

#はじめに

これからのクリスマス、年末年始、大切な人と過ごす時間。
そんな雰囲気を破壊するゲリラ的思考「あれ、この人の名前が出てこない...！」
この窮地を救うサービス（ではないかもですが）Amazon Rekognitionを勉強のために使ってみました。

また、最近触れる機会のあったCICDツールであるJenkinsも絡ませました。

#Amazon Rekognitionでできること

初めに、Amazon Rekognitionとは何か、AWSのdocs(デベロッパーガイド)の内容をまとめます。

https://docs.aws.amazon.com/ja_jp/rekognition/latest/dg/what-is.html

簡単に抜粋すると、Amazon Rekognitionでは以下のことができます。
・学習済みのAIを使って画像や動画を分析できます。
・物体を認識し、ラベル付けしてくれます。
・顔を検出して表情や他の顔との類似度がわかります。
・有名人の顔を検出できるように、事前にトレーニングされた方法があります。
・不適切なコンテンツを検出できます。
・テキストを検出できます。

※AWSコンソール上ではAmazon Rekognitionのデモが、サンプルデータ付きで用意されており、すぐに動かすことができるようになっています。

他にもいろいろ書かれていますが、今回は画像内の情報を読み取り、あれこれします。

#Amazon Rekognitionで「名前を知る」方法を考える

さて、今回のやりたいことはAmazon Rekognitionで顔を検出し、その人の名前を出力することです。
まず、Amazon Rekognitionのdocsから参考になる記述をピックアップします。

・画像から知っている顔を検出するには...
Amazon Rekognitionでは顔の類似度を出してくれる方法が用意されています。
また、顔の情報はAWSのサーバーにコレクションとして保存でき、類似度を求めるときに利用できます。

・顔から名前を出すには...
顔の類似度を求めたら、顔の名前をどこかから持ってくる必要があります。
方法はいろいろと考えられると思いますが、今回はAmazon Rekognitionでクライアント側で指定できるIDであるExternalImageIdを使います。
このIDをAWS上のコレクションに保存する顔情報に紐づけます。

・Amazon RekognitionのAPIへ画像を入力するには...
Amazon Rekognitionを使うには、用意されているAPIを使うことになります。
APIへ画像を渡す方法ですが、S3に保存されているものを使う方法と、ローカルにある画像を送る方法があります。
docsにはS3の方法が載っていたのでこの記事ではローカルの方法でやります。

※Jenkinsについては、コードを実行するために使いますが、メインはAmazon Rekognitionのため、説明は省きます。

#作業するための環境を構築する

実際の作業についてまとめていきます。
初めにAmazon RekognitionのAPIを使うための環境を作ります。

##１．EC2の作成

CloudFormationでEC2を作って作業します。Jenkinsの要件からt2.microでもかろうじて動きます。
CloudFormationの書き方は[過去の記事](https://qiita.com/kokohei39/items/1766652a5a6372f275d3#１スタックファイル作成)を参考にしました。
EC2のインスタンスタイプはJenkinsが動くt2.microを選択しました。
Jenkinsの要件は以下のページで確認できます。

https://www.jenkins.io/doc/book/installing/linux/

##２．Jenkinsの構築

次にEC2でJenkinsを使えるようにします。
まず、Jenkinsで必要なJavaをインストールします。
以下の記事を参考にしました。詳しい手順は書いてあるので、気を付けた点を書いていきます。

https://qiita.com/yuuman/items/622347f86e444b8218b3

インストールの際は以下の点に注意しました。

・Jenkinsのデフォルトの設定ではJavaのパスが/usr/bin/javaであることが必要
・Jenkinsを実行するユーザーであるjenkinsがJavaを実行できることが必要

コマンドは以下の通りです。

```bash
mkdir -p $HOME/java-tmp/jdk
wget https://download.java.net/java/GA/jdk11/9/GPL/openjdk-11.0.2_linux-x64_bin.tar.gz -O $HOME/java-tmp/jdk-pkg
tar xzvf $HOME/java-tmp/jdk-pkg -C $HOME/java-tmp/jdk
sudo cp -r $HOME/java-tmp/jdk/`ls $HOME/java-tmp/jdk/` /usr/bin
sudo ln -s /usr/bin/`ls $HOME/java-tmp/jdk/`/bin/java /usr/bin/java
export JAVA_HOME=/usr/bin/java
export PATH=$PATH:$JAVA_HOME/bin
echo '
export JAVA_HOME=/usr/bin/java
export PATH=$PATH:$JAVA_HOME/bin
' >> $HOME/.bashrc
yes | rm -r $HOME/java-tmp
```

Javaがインストールできたら、他のパッケージとJenkinsをインストールして起動します。
Amazon Linuxのリポジトリの場合のインストールのコマンドを記載します。

```bash
sudo amazon-linux-extras install -y epel
sudo yum-config-manager --enable epel
sudo yum install -y fontconfig
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
sudo yum install -y jenkins

sudo systemctl start jenkins
sudo systemctl enable jenkins
```

コマンド実行後は、ブラウザからアクセスできるようになるので、アクセスして初期設定を行います。
今回は初期設定で勧められるプラグインをインストールしています。

##３．Amazon RekognitionのSDKのセットアップ

Amazon RekognitionのAPIが使えるようにします。この記事ではPythonを使います。
セットアップはコードを実行していくことになりますが、ユーザーのホームディレクトリを使うので、ユーザーjenkinsで作業します。
Jenkinsのブラウザからコードを実行すると、デフォルトではユーザーjenkinsが実行するので、ブラウザで作業します。

まず、Jenkinsでシェルを実行できるジョブを作成します。
ログイン後の画面の「新規ジョブ作成」からジョブ名を入力し、「フリースタイル・プロジェクトのビルド」を選択します。

<img width="150" alt="画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/a49cd43b-447a-aa2c-17cc-dfd81dc67e0d.png">

作成後はジョブの設定画面に移動して、「ビルド手順の追加」から「シェルの実行」を追加します。
追加後に出る入力欄にshellを書いていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/5c9d5ddd-11ae-3b5a-cc0a-51001aff2d6a.png)

shellの内容は以下の通りです。

まず、Jenkinsのデフォルトの設定になりますが、bashのオプションは以下のようになっています。

```
#!/bin/bash -xe
```

xeオプションで、実行するコマンドがログに出て、エラーで処理が中断するようになります。
入力欄の初めに上記のコードを書くことで、別のオプションを指定することが可能です。

https://heartbeats.jp/hbblog/2010/09/bash.html

AWSの`config`と`credential`の設定をします。
Jenkinsで実行すると、shell上でのユーザーの入力やttyを使った処理ができなくなるので注意します。

```bash
mkdir -p $HOME/.aws
echo "
[default]
output = アウトプット形式
region = リージョン
" > $HOME/.aws/config
echo "
[default]
aws_access_key_id = アクセスキーID
aws_secret_access_key = シークレットアクセスキー
" > $HOME/.aws/credentials
```

アクセスキーIDとシークレットアクセスキーの作成方法は以下のページで説明されています。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html

Amazon RekognitionのPython用のSDKをインストールします。

```bash
python3 -m pip install boto3
```

shellを書いたら、ジョブの設定画面から「ビルド実行」をクリックするとshellを実行できます。

※参考

https://pyteyon.hatenablog.com/entry/2019/05/24/003924

#顔から名前を出すコードを書く

##処理のフローチャート

顔から名前を出すコードを書きます。
Jenkinsで色々な処理を一連の処理として実行するジョブを作成します。

今回の流れは以下のフローチャートのようにします。
画像内の顔を検出して、コレクション内を検索し、登録されていなければ登録するといった感じです。

<img width=300 alt="画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/b84d5262-3944-8cac-6949-f1b36c44a355.png">

##１．画像内の顔をすべて検出するコード

Amazon RekognitionのAPIを使って画像内の顔を検出します。
docsでは以下のページにAPIのサンプルが載っています。

https://docs.aws.amazon.com/ja_jp/rekognition/latest/dg/API_DetectFaces.html

PythonのAPIについてより詳しい仕様は以下のページにあります。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/rekognition.html#Rekognition.Client.detect_faces

また、Jenkinsのジョブに記載するのはshellなので、shellからPythonを呼び出すコードが別途必要です。

APIへの画像の入力について、今回はローカルの画像を使います。
以下にローカルの画像を入力として、顔を検出するAPIを呼び出すコードを示します。

```python
import boto3
client=boto3.client("rekognition")
with open(画像ファイル, "rb") as image:
    response = client.detect_faces(
        Image={"Bytes": image.read()},
        Attributes=["ALL"]
    )
```

コード中のresponseには検出された複数の顔情報が入っています。
以下のように、本当に顔であるかの信頼度が低いものは、境界値を設けてresponseから省きました。

```python
for label in response["FaceDetails"]:
    if label["Confidence"] < 信頼度の境界値:
        response["FaceDetails"].remove(label)
```

responseの値は他のジョブから使えるようにJSON形式でファイルに保存しておきます。

```python
import json
with open(出力ファイル, "w") as f:
    f.write(json.dumps(response))
```

##２．画像内のすべての顔を切り取るコード

顔が検出出来たら、その顔がコレクションに登録されているのかを検索するのですが、その前にやることがあります。
コレクション内の顔と比較できる顔は1つまでです。検索にかける画像から複数の顔が検出されても、１度に比較できるのは１つまでとなります。
そのため、今回は画像内の顔を切り取り、１枚の画像につき顔が１つのみとなるようにします。切り取り後にできた画像それぞれについて、コレクション内の検索を行います。

まず、画像の切り取りはPythonパッケージであるPILを使うのでパッケージをインストールします。

```bash
python3 -m pip install pillow
```

切り取るためのコードは以下の通りです。
画像内の顔の場所は、顔の検出時にわかるBoundingBoxの値を使います。
BoundingBoxの説明は以下のページにあります。

https://docs.aws.amazon.com/ja_jp/rekognition/latest/dg/images-displaying-bounding-boxes.html

以下に画像から顔の部分を切り取るコードを示します。

```python
from PIL import Image

#画像の読み込み
img = Image.open(画像ファイル)

#BoundingBoxから切り取る領域の算出
img_width,img_height=img.size
cut_area = (
    int( BoundingBoxのLeft * img_width),
    int( BoundingBoxのTop * img_height),
    int(( BoundingBoxのLeft + BoundingBoxのWidth )*img_width),
    int(( BoundingBoxのTop + BoundingBoxのHeight )*img_height)
)

#画像の切り取り
out_img=img.crop(box=cut_area)

#切り取った画像の保存
out_img.save(出力先の画像ファイル)
```

これを検出された顔の数だけ行います。

##３．コレクションの画像から名前を出すコード

切り取った顔毎の画像を使ってコレクション内を検索します。
コレクション内を検索するには、コレクションIDが必要です。IDはコレクション作成時に指定できます。

以下、顔画像とコレクション内の顔情報を比較するを示します。

```python
import boto3
client=boto3.client("rekognition")
with open(比較したい顔画像, "rb") as image:
    response = client.search_faces_by_image(
        CollectionId=コレクションID,
        Image={"Bytes": image.read()},
        FaceMatchThreshold=顔の類似度の境界値,
        MaxFaces=1
    )
```

返ってくる顔情報の数は最大MaxFacesの値となり、その類似度はFaceMatchThresholdを超えたものとなります。

今回はExternalImageIdを顔の名前とするので、これが欲しい情報になります。

```python
face_name = response["FaceMatches"][0]["Face"]["ExternalImageId"]
```

##４．AWSに顔を登録するコード

AWSに顔を登録するコードを書きます。
まずAWS上にコレクションを作ります。手順は以下のページにまとまっています。

https://docs.aws.amazon.com/ja_jp/rekognition/latest/dg/create-collection-procedure.html

コレクションに顔情報を追加するコードは以下の通りです。

```python
import boto3
client=boto3.client("rekognition")
with open(登録したい顔の映った画像, "rb") as image:
    response=client.index_faces(
        CollectionId=コレクションを作成する際に使用したID,
        Image={"Bytes": image.read()},
        ExternalImageId=登録する顔の名前,
        MaxFaces=1
    )
```

##５．一連のコードを一つのジョブとして実行するpipelineの作成

今回やりたいのは、顔の画像をJenkinsにアップロードして、コレクション内を検索して名前を出すことです。
また、名前がわからないときは顔と名前をコレクションに追加します。
これをJenkinsのpipelineで一連のジョブとして実現します。

pipelineを作るには、ジョブ作成時のジョブを入力する画面で、「パイプライン」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/d616fc0e-0efc-4c31-216f-d72be241d61c.png)

pipelineのコードはジョブの設定画面のScriptの欄に記載します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/9cb8e588-a3de-3638-c20a-5fa8c052f9d1.png)

フローチャートを基にコードを書いていきます。
今まで書いたコードを、pipelineのstage単位にまとめます。stageの中でコードを書いたジョブを呼び出すようにします。
ここでは、stageの処理順を示します。以下のようになります。
言語はGroovyです。

```groovy
pipeline{
  agent any
  stages{
    stage('upload file') {
      //Jenkinsへのファイルのアップロード
    }
    stage("detect faces"){
      //画像内の顔の検出
    }
    stage("cut faces"){
      //顔の切り取り
    }
    stage("faces loop"){
      //複数の顔を処理するために繰り返します。
      steps{
        script{
          for(int i=0; i< 検出された顔の数 ;i++){
          //検出された顔の数だけ以降の処理を繰り返します。
            stage("search faces by image"){
              //コレクション内の顔の検索
              //切り取った画像のパスと対応する名前の出力
            }
            if (顔の名前が登録されているか){
              stage("index faces"){
                //顔の名前の入力
                //コレクションへ顔と名前の登録
              }
            }
          }
        }
      }
    }
  }
}
```

Groovyの書き方は以下のページを参考にしました。

https://qiita.com/miyasakura_/items/9d9a8873c333cb9e9f43

※顔が登録されているかの条件分岐にはifを使いました。pipelineにはwhenという機能がありますが、script内には書けないようです。

https://stackoverflow.com/questions/56148230/how-to-mark-jenkins-job-stage-as-skipped-for-scripted-pipeline

※Groovyを学び始めたばかりでかなり`{}`が多くなってしまいました。

stageでは「フリースタイル・プロジェクトのビルド」のジョブを呼び出すことができます。
以下、各stageの追加説明です。

###Jenkinsへのファイルのアップロード

Jenkinsではジョブを実行する際に、ファイルをアップロードしてパラメータとして使うことができます。
コードを書く必要はありません。GUI上で「ビルドのパラメータ化」を使うことで実現できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/15882d50-f514-7c33-0267-1c3b5ca5e5f4.png)

ジョブを実行する際の見た目は以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/16ad0538-5b4a-3019-56e9-a06a2499db25.png)

しかし、pipelineを使う場合は、「ビルドのパラメータ化」だけではアップロードできません。
ジョブの実行はできるのですが、どこを探してもアップロードしたはずのファイルは見つかりません。

https://issues.jenkins.io/browse/JENKINS-47333?page=com.atlassian.jira.plugin.system.issuetabpanels%3Achangehistory-tabpanel

これを解決するためにはJenkinsのプラグイン「File Parameter」を使います。
プラグインの検索とインストールは、「Jenkinsの管理」の「プラグインの管理」から行えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/f4aff69f-8f0b-cc4a-62ad-c6017dbd8922.png)

インストールが終わったら、以下のプラグインの説明を参考にコードを書きます。

https://plugins.jenkins.io/file-parameters/

以下、上記で示したpipelineのstageに対応するコードを示します。

```groovy
stage('upload file') {
  steps {
    unstash 「パラメータのビルド化」にて設定した名前
  }
}
```

###切り取った画像のパスと対応する名前の出力

コレクション内の検索後に、検索に使った画像と顔の名前を表示します。
今回は簡単にログとして表示します。
また、画像はJenkinsのuserContentフォルダーに置いておくとします。これにより、ブラウザから直接アクセスできるようになります。

以下のコードでログを出します。
顔の名前は、コレクションの画像から検索して求めた名前となります。

```groovy
photo_url = sh( script: "echo http://`curl inet-ip.info`:8080/userContent/画像のパス", returnStdout: true)
println "url : ${photo_url}"
println "name : ${顔の名前}"
```

###顔の名前の入力

顔情報の登録を行うために、顔の名前の入力を要求するコードを書きます。
コードは以下のようになります。

```groovy
def inputFaceName = input(
  id : "inputFaceName",
  message : "${photo_url}\nこの人の名前は？",
  parameters : [ string(name: 'faceName', defaultValue: 初期値) ],
  ok : "です！"
)
println "name : ${inputFaceName}"
```

また、入力された名前を使って、顔と名前の登録をするジョブを呼び出すコードは以下のようになります。

```groovy
build job : 顔の登録をするジョブの名前, parameters: [
  [$class: "StringParameterValue", name: "PHOTO_FILE", value: 登録したい顔が映った画像],
  [$class: "StringParameterValue", name: "FACE_NAME", value: inputFaceName]
]
```

※参考

https://stackoverflow.com/questions/56507729/no-such-field-found-field-java-lang-string-sinput-error-while-running-my-jenkin

#顔認識のデモ

作ったジョブを動かしてみます。
ちゃんと登録した顔を認識してくれるかを確認します。

まず、コレクションに顔を登録していない状態でやってみます。
使う画像は以下の通りです。

<img width=300 alt="画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/d54bba1b-5ec7-be03-5410-ef13879fa5e7.jpeg">

実行すると、顔が検出され、以下の画像ができました。

<img width=150 alt="画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/d0f5cc9e-469b-3ad7-dfb1-d5b333a0ef73.png">

また、コレクションに登録されていないので、名前の入力が求められます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/5ca1f86c-13da-d464-15f4-0acfc9847ed9.png)

入力をしてジョブを再開させると無事ジョブが終了します。

次に以下の画像を使って、再度ジョブを実行しました。

<img width=300 alt="画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/cc97f873-bce9-99e1-ec44-0eb4159c3ac1.jpeg">

ジョブを実行すると２つの顔が検出されました。

画像ファイル名（１枚目）：0.JPEG

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/2af70113-b49c-5601-53cc-b458fafae73b.png)

画像ファイル名（２枚目）：1.JPEG

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/43d77018-e50e-250c-562e-894ef39a05b4.png)

ジョブの経過を見てみると１枚目の画像の顔は認識して、２枚目の登録されていない顔に対しては名前の入力を要求してくれました。

・1枚目の顔検索結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/57a3af5d-d1e9-8485-2424-8ac7f485966f.png)

・2枚目の名前の入力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/fbafff57-0161-ee59-1873-9ac3ccc286d8.png)

以上がデモになります。

#終わりに

今回は機械学習の勉強のためにAmazon Rekognitionに触れました。
AWS SAA合格後の目標としてMLS合格を目指しています。
まだ機械学習の表面的な部分しか触れていないので、これからも勉強していきます。

ただ今回はAWSのdocsを理解することよりも、Groovyのコーディングすることの方が疲れました。。。
これで楽しい年末になることを願っています。