Pythonの顔認識モデルをHerokuにデプロイしてFlutterから利用①

Pythonの強力な顔認識モデルをHerokuにデプロイしてモバイルアプリから呼び出せるようにしました。

第一回はPython側のコードとHerokuへのデプロイまで、第二回ではFlutterでそのデプロイした処理を呼び出す部分を紹介したいと思います。

第二回のFlutterでの呼び出し部は以下の記事を参照ください。
[Pythonの顔認識モデルをHerokuにデプロイしてFlutterから利用②](https://qiita.com/THERE2/items/21784491354ee117696c)

### この記事の投稿者について
Twitterで顔認識を活用したアプリ開発についてつぶやいています。
https://twitter.com/studiothere2

noteにアプリ開発の日記を連載しています。
https://note.com/there2


## 顔認識モデル

### 利用ライブラリ
Python face_recognitionを利用しています。
https://github.com/ageitgey/face_recognition

ドキュメントが非常に充実しており、とても簡単に利用できます。
ベンチマークで99.38%の精度ということで、人間と同等以上のパフォーマンスが期待できます。

### その他利用ライブラリ
あまり深く考えていませんが、Herokuでよく利用されていて軽量そうなものを選びました。

- Web開発フレームワーク：Flask
- HTTPサーバー：Gunicorn

### Pythonソースコード解説

必要最小限のシンプルなものですが、骨子がわかるかと思います。

```run.py
import face_recognition
from flask import Flask, jsonify, request, redirect
app = Flask(__name__)
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
```

まずは必要なライブラリのインポートと定数の宣言です。
appの変数名はGunicornの起動時にも利用します。

```run.py
@app.route('/')
def hello_world():
    return 'hello'
```
rootで呼ばれた時に、単純にhelloだけを表示します。
これはサーバーが生きているかどうかの確認に便利です。

```run.py
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
```
画像ファイルが送られてきたときに、そのファイルの拡張子がpng, jpg, jpeg, gifのいずれかであることを確認します。
これら以外の場合はface recognitionの処理対象外としています。

```run.py
@app.route('/embedding', methods=['GET', 'POST'])
def get_embedding():
    # Check if a valid image file was uploaded
    if request.method == 'POST':
        if 'file' not in request.files:
            print("file not in request.files")
            return redirect(request.url)

        file = request.files['file']

        if file.filename == '':
            print("file name is blank")
            return redirect(request.url)

```
メインの顔認識処理部です。
`/embedding`で呼び出します。
POSTでinput typeがfileでファイル名に上記のpng, jpg, jpeg, gifのいずれかの場合のみ処理します。
それ以外の場合はリダイレクトで呼び出し元に戻します。その場合、`response code`が`302`となります。

```run.py
        if file and allowed_file(file.filename):
            print("file found")

            # The image file seems valid! Detect faces and return the result.
            img = face_recognition.load_image_file(file)
            emb = face_recognition.face_encodings(img)[0]
            return jsonify(emb.tolist())
```
正しいファイル形式でPOSTされてきた場合、face_recognitionにファイルを読み込ませて、それを使って128次元のFloatのEmbeddingを取得します。
このEmbeddingを使うと、他の顔との類似度を算出する事ができます。
この部分は別途モバイルアプリ側で実装しますので、Python側ではEmbeddingした顔の特徴量情報のみを返します。
face_recognitionの戻り値はNumpyの配列なので、`tolist()`を呼び出してPythonの配列にする必要があります。
それをJson化したものを戻します。

```run.py
    # If no valid image file was uploaded, show the file upload form:
    return '''
    <!doctype html>
    <title>upload</title>
    <h1>Please select image and upload</h1>
    <form method="POST" enctype="multipart/form-data">
      <input type="file" name="file">
      <input type="submit" value="Upload">
    </form>
    '''
```

GETで呼ばれた場合は、ファイルアップロード用のHTMLを出力し、直接ファイルアップロードできるようにしています。

```run.py
if __name__ == "__main__":
    app.run(host='0.0.0.0')
```
メイン処理で、Flaskを起動します。hostを`0.0.0.0`としておくことで、Herokuのデプロイ先でのURLで利用できるようにします。

### 動作確認

```sh
pip install -r requirements.txt
```
必要なライブラリは`requirements.txt`にまとめていますので、上記のコマンドでまとめてインストールできます。
なおこの`requirements.txt`はHerokuにデプロイする際も必要となります。
バージョンによっては`cmake`, `dlib`, `face_recognition`の順にインストールしないとHerokuでうまくデプロイできないという情報があったので、以下のように`requirements.txt`に記載しておきました。

```requirements.txt
boost==0.1
cmake
boost-py
dlib
face_recognition_models
face-recognition
Flask==1.1.2
gunicorn==20.0.4
```

gunicornを使ってローカルで起動してみます。

```sh
gunicorn run:app --log-file -
```

うまく起動したら以下のように表示されると思います。

```sh
$ gunicorn run:app --log-file -
[2020-05-15 09:02:24 +0900] [11143] [INFO] Starting gunicorn 20.0.4
[2020-05-15 09:02:24 +0900] [11143] [INFO] Listening at: http://127.0.0.1:8000 (11143)
[2020-05-15 09:02:24 +0900] [11143] [INFO] Using worker: sync
[2020-05-15 09:02:24 +0900] [11146] [INFO] Booting worker with pid: 11146
```

ブラウザで`http://127.0.0.1:8000`を開いてみましょう。
`hello`と表示されたら成功です。

続いて、`http://127.0.0.1:8000/embedding`にアクセスすると以下のようにファイル選択のHTMLが表示されると思います。
人の顔画像のファイルをアップロードしてみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/ec31d429-0259-a9d0-4cc5-c7553d295b8e.png)

次のように128個のFloatの配列が表示されたら成功です。
これがアップロードした画像の特徴量となるEmbeddingの中身です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/2a8c46d6-e6d1-9b28-d9df-1451e867c8d0.png)

## クラウド環境
### Herokuの利用

https://jp.heroku.com/

WEBサービスのデプロイする先としてHerokuを選択しました。
Pythonで利用できること、デプロイが簡単でスケールしやすい事、利用料がリーズナブルだと感じた事からHerokuを選択しました。
試しの利用なら無料で利用できます。

対抗としてFirebaseのCloud Functionも考えましたが、今回のモデルは読み込みにある程度負担がかかりますので都度起動されるCloud Functionより常時起動しているVM型の方が良いと思いました。

Herokuも無料版だと30分利用がないとインスタンスが落ちてしまって呼び出し時に起動が走るようですが、有料版にすればインスタンスが落ちなくなります。
必要に応じて有料版に移行すればよいでしょう。

### Herokuへのデプロイ

デプロイ作業は難しくありません。
Herokuへユーザ登録して以下のセットアップ手順に沿っていけばよいでしょう。
https://devcenter.heroku.com/articles/getting-started-with-python

Herokuのコマンドラインツールをインストールします。

```sh
sudo snap install heroku --classic
```

Herokuへログインします。
ブラウザが立ち上がって、ブラウザからログインできます。

```sh
heroku login
```

Herokuにアプリを登録します。
適当な名前でアプリが登録され、Herokuのダッシュボードで確認できるようになります。

```sh
heroku create
```

Procfileを作成します。
このファイルは、Herokuにどのようにサーバを起動すれば良いかを教えるファイルです。
以下のようにWEBとしてgunicornを起動するように指示を書いておきます。

```Procfile
web: gunicorn run:app --log-file -
```

以上で準備完了です。あとはgitコマンドでherokuのリポジトリにプッシュすればデプロイされます。
※事前にgitコミットを済ませておく必要があります。

```
git push heroku master
```

これでHerokuにデプロイされて、URLが表示されますので、そのURLにアクセスして動作する事を確認してみてください。

なお、初回のデプロイにはかなり時間がかかります。特にdlibがC++で書かれたライブラリでコンパイルが必要なため、インストール完了までかなり待たされました。
30分ぐらいかかったかもしれません。
エラー終了するのでなければ気軽に待ちましょう。

次回はFlutterからこのWEBサービスを呼び出す部分を紹介したいと思います。