【Python】お手軽に顔認証を実装する②

## はじめに

Pythonのライブラリを使い、お手軽に顔認証を実装するテーマの第2回目になります。
前回の記事はこちら --> [【Python】お手軽に顔認証を実装する①](https://qiita.com/f-paico/items/e27e4eefc8c009ecdeab)

今回は、画像データをクライアントからサーバに送り、サーバで顔認証を行い、認証結果をクライアントに返却するプログラムを実装していきます。顔認証を実践で使うときは、このパターンが多いかと思いますので、ぜひ読んでみてください。



### 今回実装するシステムのイメージ図

![【Python】お手軽に顔認証を実装する.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674933/33be4b32-027b-527f-8109-e62d0949aa82.png)

## 動作環境

- M1 Mac
- Python 3.9.6

## ライブラリのインストール

前回に引き続き、顔検出・顔認証の機能に関しては、無料で使えるPythonのライブラリとして[face-recognition](https://pypi.org/project/face-recognition/)を使用します。クライアントとサーバの通信にはrequestsを使用します。画像データをエンコード・デコードする際はOpenCVを使用します。また、サーバで受信したデータからjpg画像を作成する際にはnumpyを使用します。

```bash
pip3 install --upgrade pip
pip3 install face-recognition
pip3 install requests
pip3 install numpy
pip3 install opencv-python
```

## 実装の準備

まず、Pythonでクライアント側プログラムを記述するclient.pyと、サーバ側プログラムを記述するapp.pyを作成します。そして、学習データとして用いる画像train.jpgとテストデータとして用いるtest.jpgを準備して同じ階層に置きます。私は、前回と同様、[face_recognitionライブラリのページ](https://github.com/ageitgey/face_recognition/tree/master/examples)からオバマさんの顔画像を2種類取得して、train.jpg、test.jpgとしています。

```bash
Sample
├── app.py
├── client.py
├── train.jpg
└── test.jpg
```

## 実装

### HTTP通信 - 画像データの送信（クライアント→サーバ）

まずは、顔検出・顔認証の機能は除いて実装していきます。まずはクライアントからサーバにHTTP通信で画像データを送るプログラムを作成していきます。

画像データを送るには、クライアント側でBase64という方式でエンコードして送信し、サーバ側でBase64の方式でデコードして、画像データを再生成する必要があります。Base64方式については、別の記事を参照してください。

以下のイメージ図の通りに実装していきます。

![【Python】お手軽に顔認証を実装する2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674933/6762a7ac-00f1-47bc-2101-04bf820bbcba.png)

では、client.pyを以下のとおり記述します。

```python:client.py
import base64
import requests
import json

# test.jpgを読み込み、base64方式でエンコードする
with open("test.jpg", "rb") as image_file:
    encode_bytes = base64.b64encode(image_file.read())
    encode_str = encode_bytes.decode("utf-8")

# サーバのエンドポイントを指定、今回はループバックIPを使用
endpoint = "http://127.0.0.1:8888/"

# HTTP通信のヘッダを設定
headers = {
    "Content-Type": "application/json"
}

# HTTP通信のデータを指定、今回はPOSTを使い画像データを送る
data = {
    "file_name": "test.jpg",
    "img_data": encode_str
}

# HTTPリクエストを実行
response = requests.post(url=endpoint, headers=headers, data=json.dumps(data))

# HTTPレスポンスが返ったら以下が実行される
status_code = response.status_code
json_data = response.json()
if status_code == 200:
    print(f"Status Code: {status_code}")
    print(f"Response Data: {json.dumps(json_data, indent=2)}")
else:
    print(f"Status Code: {status_code}")
```

次に、app.pyを以下のとおり記述します。

```python:app.py
from flask import Flask, request, jsonify
import numpy as np
import base64
import cv2

# FlaskでHTTPサーバ実装
app = Flask(__name__)

@app.route("/", methods=["POST"])
def index():
    # POSTリクエストが来た時に実行する
    if request.method == "POST":
        data = request.json
        # クライアントから送信されたデータを出力する
        print(data)
        # 画像データをbase64方式でデコードする
        img_binary = base64.b64decode(data["img_data"])
        # デコードした画像データをOpenCVで扱える形に変換する
        jpg = np.frombuffer(img_binary, dtype=np.uint8)
        img = cv2.imdecode(jpg, cv2.IMREAD_COLOR)
        # 画像データをjpgとして出力する
        cv2.imwrite("test_decode.jpg", img)
        # クライアントにステータスコード200を返却する
        return jsonify({"status": 200})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8888)
```

上記のプログラムを動作させるには、先にapp.pyを実行してHTTPサーバを立てた状態で、client.pyを実行します。

プログラムを実行すると、test_decode.jpgが出力（保存）されるはずなので、画像が正常に表示できるか確認しましょう。また、client.pyの出力結果として、サーバから返却されたステータスコード200が表示されるか確認しましょう。

ここまでで、画像データをクライアントからサーバに送り、サーバからクライアントに結果を返却するプログラムが実装できました。後は、[前回の記事](https://qiita.com/f-paico/items/e27e4eefc8c009ecdeab)で実装した顔認証のプログラムを組み込み、うまく動作させましょう。

### 顔認証プログラムの組み込み

上記のapp.pyのプログラムに、前回の顔認証のプログラムを埋め込んでいきます。
差分は、わかるようにハイライトしています。client.pyは変更箇所無しです。

```diff_python:app.py
from flask import Flask, request, jsonify
import numpy as np
import base64
import cv2
import face_recognition

# FlaskでHTTPサーバ実装
app = Flask(__name__)

@app.route("/", methods=["POST"])
def index():
    # POSTリクエストが来た時に実行する
    if request.method == "POST":
        data = request.json
        # クライアントから送信されたデータを出力する
        print(data)
        # 画像データをbase64方式でデコードする
        img_binary = base64.b64decode(data["img_data"])
        # デコードした画像データをOpenCVで扱える形に変換する
        jpg = np.frombuffer(img_binary, dtype=np.uint8)
        img = cv2.imdecode(jpg, cv2.IMREAD_COLOR)
        # 画像データをjpgとして出力する
        cv2.imwrite("test_decode.jpg", img)

+       # 学習させたい（登録したい）顔画像のファイル名をリストに格納
+       train_img_names = ["train.jpg", ]
+       # 学習させた画像に対して、認証できるかテストに使う顔画像のファイル名をリストに格納
+       test_img_name = "test_decode.jpg"
+
+       # 学習データの顔画像を読み込む
+       train_imgs = []
+       for name in train_img_names:
+           train_img = face_recognition.load_image_file(name)
+           train_imgs.append(train_img)
+
+       # テストデータ（認証する人の顔画像）を読み込む
+       test_img = face_recognition.load_image_file(test_img_name)
+
+       # 学習データの顔画像から顔の領域のみを検出する
+       train_img_locations = []
+       for img in train_imgs:
+           # modelはhogとcnnを指定でき、cnnは重いが精度良い、hogは軽量だが精度は普通
+           train_img_location = face_recognition.face_locations(img, model="hog")
+           # 顔検出に失敗するとtrain_img_locationの長さは1となる
+           # 顔検出に成功すると顔を検出し四角形で囲んだ四隅の座標を取得できる
+           assert len(train_img_location) == 1, "画像から顔の検出に失敗したか、2人以上の顔が検出されました"
+           train_img_locations.append(train_img_location)
+
+       # テストデータの顔画像から顔の領域のみを検出する
+       test_img_location = face_recognition.face_locations(test_img, model="hog")
+       assert len(test_img_location) == 1, "画像から顔の検出に失敗したか、2人以上の顔が検出されました"
+
+       # 学習データの特徴量を抽出する
+       train_img_encodings = []
+       for img, location in zip(train_imgs, train_img_locations):
+           (encoding,) = face_recognition.face_encodings(img, location)
+           train_img_encodings.append(encoding)
+
+       # テストデータの特徴量を抽出する
+       (test_img_encoding,) = face_recognition.face_encodings(test_img, test_img_location)
+
+       # 学習データとテストデータの特徴量を比較し、ユークリッド距離を取得する
+       # 距離を見ることで顔がどれだけ似ているかわかる
+       dists = face_recognition.face_distance(train_img_encodings, test_img_encoding)
+
+       # 学習データとテストデータの距離が0.40以下のとき、顔が一致と判定
+       answer = False
+       for dist in dists:
+           if dist < 0.40:
+               answer = True
+
+       # 顔認証の結果を出力する
+       print(answer)
+       if answer == True:
+           result = "Recognition OK"
+       else:
+           result = "Recognition NG"
+
+       return jsonify({"result": result, "status": 200})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8888)
```
以上のプログラムを実行する事で、顔認証が実現できます。実際に動作確認しておきましょう。

## ソースコード
コピペで使えるように、ソースコードを以下のとおり記載しておきます。

```python: client.py
import base64
import requests
import json


# test.jpgを読み込み、base64方式でエンコードする
with open("test.jpg", "rb") as image_file:
    encode_bytes = base64.b64encode(image_file.read())
    encode_str = encode_bytes.decode("utf-8")

# サーバのエンドポイントを指定、今回はループバックIPを使用
endpoint = "http://127.0.0.1:8888/"

# HTTP通信のヘッダを設定
headers = {
    "Content-Type": "application/json"
}

# HTTP通信のデータを指定、今回はPOSTを使い画像データを送る
data = {
    "file_name": "test.jpg",
    "img_data": encode_str
}

# HTTPリクエストを実行
response = requests.post(url=endpoint, headers=headers, data=json.dumps(data))

# HTTPレスポンスが返ったら以下が実行される
status_code = response.status_code
json_data = response.json()
if status_code == 200:
    print(f"Status Code: {status_code}")
    print(f"Response Data: {json.dumps(json_data, indent=2)}")
else:
    print(f"Status Code: {status_code}")
```

```python: app.py
from flask import Flask, request, jsonify
import numpy as np
import base64
import cv2
import face_recognition


# FlaskでHTTPサーバ実装
app = Flask(__name__)


@app.route("/", methods=["POST"])
def index():
    # POSTリクエストが来た時に実行する
    if request.method == "POST":
        data = request.json
        # クライアントから送信されたデータを出力する
        print(data)
        # 画像データをbase64方式でデコードする
        img_binary = base64.b64decode(data["img_data"])
        # デコードした画像データをOpenCVで扱える形に変換する
        jpg = np.frombuffer(img_binary, dtype=np.uint8)
        img = cv2.imdecode(jpg, cv2.IMREAD_COLOR)
        # 画像データをjpgとして出力する
        cv2.imwrite("test_decode.jpg", img)

        # 学習させたい（登録したい）顔画像のファイル名をリストに格納
        train_img_names = ["train.jpg", ]
        # 学習させた画像に対して、認証できるかテストに使う顔画像のファイル名をリストに格納
        test_img_name = "test_decode.jpg"

        # 学習データの顔画像を読み込む
        train_imgs = []
        for name in train_img_names:
            train_img = face_recognition.load_image_file(name)
            train_imgs.append(train_img)

        # テストデータ（認証する人の顔画像）を読み込む
        test_img = face_recognition.load_image_file(test_img_name)

        # 学習データの顔画像から顔の領域のみを検出する
        train_img_locations = []
        for img in train_imgs:
            # modelはhogとcnnを指定でき、cnnは重いが精度良い、hogは軽量だが精度は普通
            train_img_location = face_recognition.face_locations(img, model="hog")
            # 顔検出に失敗するとtrain_img_locationの長さは1となる
            # 顔検出に成功すると顔を検出し四角形で囲んだ四隅の座標を取得できる
            assert len(train_img_location) == 1, "画像から顔の検出に失敗したか、2人以上の顔が検出されました"
            train_img_locations.append(train_img_location)

        # テストデータの顔画像から顔の領域のみを検出する
        test_img_location = face_recognition.face_locations(test_img, model="hog")
        assert len(test_img_location) == 1, "画像から顔の検出に失敗したか、2人以上の顔が検出されました"

        # 学習データの特徴量を抽出する
        train_img_encodings = []
        for img, location in zip(train_imgs, train_img_locations):
            (encoding,) = face_recognition.face_encodings(img, location)
            train_img_encodings.append(encoding)

        # テストデータの特徴量を抽出する
        (test_img_encoding,) = face_recognition.face_encodings(test_img, test_img_location)

        # 学習データとテストデータの特徴量を比較し、ユークリッド距離を取得する
        # 距離を見ることで顔がどれだけ似ているかわかる
        dists = face_recognition.face_distance(train_img_encodings, test_img_encoding)

        # 学習データとテストデータの距離が0.40以下のとき、顔が一致と判定
        answer = False
        for dist in dists:
            if dist < 0.40:
                answer = True

        # 顔認証の結果を出力する
        print(answer)
        if answer == True:
            result = "Recognition OK"
        else:
            result = "Recognition NG"

        return jsonify({"result": result, "status": 200})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8888)
```

## おわりに

Pythonのライブラリとしてface-recognitionを使って、お手軽に顔認証を実装するテーマの第2回目はいかがでしたか。画像データをクライアントからサーバに送り、サーバで顔認証を実施して認証結果をクライアントに返却するという事を実現しました。様々な場面で活用できますので、その都度、ご参照いただければと思います。次回は、クライアントをpythonではなくAndroidのスマホアプリとして実装してみたいと思います。最後まで見ていただき、ありがとうございました。