Flaskで機械学習Webアプリを作ってみた


# 概要
OpenCVのHaar-like特徴分類器で顔領域を抽出し、OpenCVの顔推定器（LBPH）に学習させ、テストデータの顔画像とどれくらい類似しているのかを計算するモデルを作成します。
Flaskで、その作成したモデルLoadし、ローカルのブラウザで、顔画像を選択すると類似度が返ってくるアプリを作成します。
類似度が0に近くなれば似ているということになります。

![スクリーンショット 2018-11-28 22.05.57.png](https://qiita-image-store.s3.amazonaws.com/0/285122/bd8f33ac-4b93-5ecf-10fb-0bf518fd74be.png)

Webサービスと機械学習モデルのやりとりは以下の図のように行なっています。
<img width="1159" alt="スクリーンショット 2019-01-07 18.28.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/285122/853c8a00-452d-416c-c9c1-89be1ea94b32.png">


本記事のコードは<a href='https://github.com/ryo1923/images_classification_api' target='_blank'>こちら</a>

# ツリー構造
flask_api/
├── haarcascade_frontalface_alt.xml
├── sample_model.yml
├── save_model.ipynb
├── server.py
├── templates
｜　   ├── flask_api_index.html
｜　   ├── layout.html
｜　   └── result.html
└── train_images

# アルゴリズム
**LBPH**(Local Binary Patterns Histogram)とは
　顔を小さなセルに分割し、それぞれのエリアのヒストグラムを比較します。
　顔のサイズや形が異なっていても精度よく検出できるという特徴があります。

類似度検出には3つのアルゴリズムがあります。

- ヒストグラム比較
ヒストグラム比較とは、まず画像の色がどのように分布しているかのヒストグラムを作成します。
そして、二つの画像のヒストグラムを比較して、同じような分布であれば、二つの画像は「似ている」ということになります。
そのため、画像を取り込む時にはグレースケールで統一する必要があります。
今回はこのアルゴリズムを使用していきます。

- 特徴点マッチング
特徴点マッチングとは、画像同士の特徴点の距離を測り、近ければ類似度が高いとみなす手法です。
人で言えば、輪郭や目鼻立ち、眼の大きさなどです。
特徴点を抽出する方法・アルゴリズムは主に以下の二つです。
 - ORB(Oriented FAST and Rotated BRIEF)
 - AKAZE(Accelerated KAZE)

- テンプレートマッチング
画像の中から、類似度の高い部分だけをマッチングさせる手法です。
こちらは、何らかのオブジェクトなどのある程度形が決まったものを検出するのに最適です。

# 1. Flask install

[Flask docmentation](https://methane.github.io/flask-handson/start.html)
以下のコマンドを入力すればおkです！
あとは、機械学習に必要なモジュールをinstallしてください。

```terminal
$ pip install Flask
```

# 2. 機械学習モデル

トレーニング用の画像は、[PythonでGoogle Custom Search APIを使い画像収集してみた](https://qiita.com/kagami-r0927/items/f88d04cbb4a4f43a2c23)をみて、集めてみてください！

https://github.com/opencv/opencv/tree/master/data/haarcascades でHaar-like特徴分類器で使うファイルをダウンロードしてください。
トレーニングデータの顔画像の類似度を計算する機械学習モデルを作成し、保存します。

```save_model.py
# モデルの保存
import cv2
import os
import numpy as np
from PIL import Image
import re
import os.path

# フォルダ内の画像を習得
def get_images_and_labels():
    
    print('モデル保存中...')
    
    # トレーニング画像
    train_path = './train_images'

    # Haar-like特徴分類器
    cascadePath = './haarcascade_frontalface_alt.xml'
    faceCascade = cv2.CascadeClassifier(cascadePath)
    recognizer= cv2.face.LBPHFaceRecognizer_create()

    # 画像を格納する配列
    images = []
    # ラベルを格納する配列
    labels = []
    for f in os.listdir(train_path):
        # 画像のパス
        image_path = os.path.join(train_path, f)
        # 白黒で読み込み
        image_pil = Image.open(image_path).convert('L')
        # Numpyの配列に格納
        image = np.array(image_pil, 'uint8')
        # Haar-like特徴分類器で顔を検知
        faces = faceCascade.detectMultiScale(image)
        # 検出した画像の処理
        for(x, y, w, h) in faces:
            # 200×200にリサイズ
            roi = cv2.resize(image[y: y + h, x: x + w],
                             (200, 200), interpolation=cv2.INTER_LINEAR)
            # 画像を配列に格納
            images.append(roi)
            int_number = re.findall("\d+", f)
            for number in int_number:
                labels.append(int(number))
    # トレーニング実施
    recognizer.train(images, np.array(labels))

    # モデルの保存
    recognizer.write('sample_model.yml')
    
    print('モデルの保存が完了しました。')
```

スクレイピングで画像を集めてから、以下のコードを実行してください。

```
$ python save_model.py
```

`sample_model.yml`が作成されたらOKです！



# 3. HTMLファイルの作成
次にHTMLファイルを作成していきます。

```layput.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>API Sample</title>
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>
```

```result.html
{% extends "layout.html" %}
{% block content %}

<h3>類似度</h3>
{{ predict_Confidence }}

{% endblock %}
```

```flask_api_index.html
{% extends "layout.html" %}
{% block content %}

<form action="/result" method="post" enctype="multipart/form-data">
  <input type="file" name="image" accept="image/png, image/jpeg, image/jpg">
  <button type="submit">submit</button>
</form>

{% endblock %}
```

この3つのファイルをtemplatesディレクトリに入れてください。


# 4. サーバーの作成

サーバー側

```server.py
from flask import Flask, render_template, request, redirect, url_for, send_from_directory
import numpy as np
import cv2
from datetime import datetime
import os
import string
from PIL import Image


app = Flask(__name__)

def load_model():
    global recognizer
    print(" * Loading pre-trained model ...")
    cascadePath = './haarcascade_frontalface_alt.xml'
    faceCascade = cv2.CascadeClassifier(cascadePath)
    recognizer = cv2.face.LBPHFaceRecognizer_create()
    # recognizer = cv2.face.createLBPHFaceRecognizer()
    # recognizer = cv2.face.LBPHFaceRecognizer.create()
    recognizer.read('./sample_model.yml')

    print(' * Loading end')

@app.route('/')
def index():
    return render_template('./flask_api_index.html')

@app.route('/result', methods=['POST'])
def result():
    # submitした画像が存在したら処理する
    if request.files['image']:
        # 白黒画像として読み込み
        image_pil = Image.open(request.files['image']).convert('L')
        image = np.array(image_pil, 'uint8')
        # 類似度を出力
        label, predict_Confidence = recognizer.predict(image)
        predict_Confidence = str(predict_Confidence)
        # render_template('./result.html')
        return render_template('./result.html', title='類似度', predict_Confidence=predict_Confidence)

if __name__ == '__main__':
    load_model()
    app.debug = True
    app.run(host='localhost', port=5000)
```

このファイルを、

```
$ python server.py
```

で実行し、localhost:5000で接続してください。

![スクリーンショット 2018-11-28 22.07.48.png](https://qiita-image-store.s3.amazonaws.com/0/285122/9258679f-647a-9aa1-a6c7-aa97e8b34222.png)

このような画面になったら実際に実行してみましょう。

![スクリーンショット 2018-11-28 23.04.30.png](https://qiita-image-store.s3.amazonaws.com/0/285122/0774592c-76d2-e473-69b7-5ac8b3a18a82.png)

このように類似度が表示されればOKです!


# 参考

[OpenCVで乃木坂46秋元真夏と銀シャリ鰻和弘の類似度を調べてみた](https://aidemy.hatenablog.com/entry/2018/06/18/135601)
[Flaskで簡単につくる、画像処理した結果を見るだけのWebサービス](https://qiita.com/redshoga/items/60db7285a573a5e87eb6)