【顔分類その６】Djangoとkerasを利用した本田翼か佐倉綾音かを分類するWebアプリ

## 目的

Pythonとkerasと事前に生成した顔分類モデルを利用して、画像ファイルの人物が本田翼か佐倉綾音かを分類できましたので、Djangoフレームワークを利用してWebアプリを作ってみました。
これまでに生成した顔分類モデルと、OpenCVのカスケードファイルを利用しています。

## 参考

[【顔分類その１】PythonとGoogleカスタム検索APIを利用した画像ダウンロード](https://qiita.com/kerobot/items/f98b73cbd5f52c1fdac2)
[【顔分類その２】PythonとOpenCVを利用した画像ファイルからの顔の切り出し](https://qiita.com/kerobot/items/e3abe3f21808b4b584bd)
[【顔分類その３】PythonとOpenCVを利用した画像ファイルの水増し](https://qiita.com/kerobot/items/54bc1224424280150d1c)
[【顔分類その４】Pythonとkerasを利用した顔分類モデルの生成](https://qiita.com/kerobot/items/be4fd1a166073fbcff38)
[【顔分類その５】Pythonとkerasを利用して本田翼か佐倉綾音かを分類](https://qiita.com/kerobot/items/1ce75622d173d231d423)
[【顔分類その６】Djangoとkerasを利用した本田翼か佐倉綾音かを分類するWebアプリ](https://qiita.com/kerobot/items/65d4e1942a31c00317e2)

## 環境

* Windows 10 x64 1809
* Python 3.6.5 x64
* Power Shell 6 x64
* Visual Studio Code x64
* Git for Windows x64

## 環境構築

* Windows 10 x64 に Python 3.6.x x64 をインストールします。

* 環境変数の PATH に、インストールしたPythonのフォルダとPython\Scriptsフォルダを設定しておきます。

* 下記手順で Python の仮想環境を構築し、有効化してpipをアップデートします。

```powershell
> python -m venv venv
> .\venv\Scripts\activate.ps1
(venv)> python -m pip install --upgrade pip
```

* pip でいろいろインストールします。

```powershell
(venv)> pip install pylint
(venv)> pip install Django~=2.0.10
(venv)> pip install opencv-python
(venv)> pip install Pillow
(venv)> pip install tensorflow
(venv)> pip install keras
```

> 後ほど作成するプロジェクトのルートに、下記のような requirements.txt を作成しています。

```text
Django~=2.0.10
pylint
opencv-python
Pillow
tensorflow
keras
```

## プログラム作成

* Django のプロジェクトを作成します。

```powershell
(venv)> django-admin.exe startproject mysite .
```

> プロジェクト名は mysite としました。
> 基本的な手順は[Django Girls のチュートリアル](https://tutorial.djangogirls.org/ja/)を参考にしています。

* mysite/settings.py を修正します。

```python
TIME_ZONE = 'Asia/Tokyo' # 修正
LANGUAGE_CODE = 'ja'     # 修正
# 追加
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
```

* 念のためマイグレートしておきます。

```powershell
(venv)> python manage.py migrate
```

* ここまでの手順でWebサーバーを起動し、ブラウザから参照できることを確認します。

```powershell
(venv)> python manage.py runserver
```

> Ctrl+C でWebサーバーを停止します。

* Django のアプリケーションを作成します。

```powershell
(venv)> python manage.py startapp pred
```

> アプリケーション名は pred としました。

* mysite/settings.py を修正します。

```python
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pred', # 追加
]
```

* プロジェクトのルートにモデルファイルとカスケードファイルを配置します。

```text
img_pred_web
├─mysite
│  ├─settings.py
│  ├─urls.py
│  └─wsgi.py
├─pred
│  ├─migrations
│  ├─admin.py
│  ├─apps.py
│  ├─models.py
│  ├─tests.py
│  ├─urls.py
│  └─views.py
├─venv
├─db.sqlite3
├─haarcascade_frontalface_default.xml ← 配置
├─manage.py
├─model.h5 ← 配置
└─requirements.txt
```

* mysite/settings.py を修正します。

```python
# 追記
CASCADE_FILE_PATH = os.path.join(BASE_DIR, 'haarcascade_frontalface_default.xml')
MODEL_FILE_PATH = os.path.join(BASE_DIR, 'model.h5')
```

* mysite/urls.py を修正します。

```python
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('pred.urls')),
]
```

> pred アプリケーションの urls を設定しています。

* pred/urls.py を修正します。

```python
from django.urls import path
from .views import PredView

urlpatterns = [
    path('', PredView.as_view(), name='index'),
]
```

> 後ほど作成するビューへのURLパターンを設定しています。

* pred/forms.py を作成します。

```python
from django import forms

class ImageForm(forms.Form):
    image = forms.ImageField(label="判定する画像を選択してください",
                             error_messages={'missing' : '画像ファイルが選択されていません。',
                                             'invalid' : '分類する画像ファイルを選択してください。',
                                             'invalid_image' : '画像ファイルではないようです。'})
```

> 画像ファイルをアップロードするためのフォームを作成しています。

* pred/views.py を修正します。

```python
from django.shortcuts import render
from django.views.generic import TemplateView
from .forms import ImageForm
from .main import detect

class PredView(TemplateView):
    # コンストラクタ
    def __init__(self):
        self.params = {'result_list': [],
                       'result_name': "",
                       'result_img': "",
                       'form': ImageForm()}

    # GETリクエスト（index.htmlを初期表示）
    def get(self, req):
        return render(req, 'pred/index.html', self.params)

    # POSTリクエスト（index.htmlに結果を表示）
    def post(self, req):
        # POSTされたフォームデータを取得
        form = ImageForm(req.POST, req.FILES)
        # フォームデータのエラーチェック
        if not form.is_valid():
            raise ValueError('invalid form')
        # フォームデータから画像ファイルを取得
        image = form.cleaned_data['image']
        # 画像ファイルを指定して顔分類
        result = detect(image)
        # 顔分類の結果を格納
        self.params['result_list'], self.params['result_name'], self.params['result_img'] = result
        # ページの描画指示
        return render(req, 'pred/index.html', self.params)
```

> 後ほど作成する main.py を利用して顔分類を行います。

* pred/main.py を作成します。

```python
import base64
import io
import cv2
import keras
import numpy as np
from PIL import Image
from keras.backend import tensorflow_backend as backend
from django.conf import settings

def detect(upload_image):
    result_name = upload_image.name
    result_list = []
    result_img = ''

    # 設定からカスケードファイルのパスを取得
    cascade_file_path = settings.CASCADE_FILE_PATH
    # 設定からモデルファイルのパスを取得
    model_file_path = settings.MODEL_FILE_PATH
    # kerasでモデルを読み込む
    model = keras.models.load_model(model_file_path)
    # アップロードされた画像ファイルをメモリ上でOpenCVのimageに格納
    image = np.asarray(Image.open(upload_image))
    # 画像をOpenCVのBGRからRGB変換
    image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    # 画像をRGBからグレースケール変換
    image_gs = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2GRAY)
    # カスケードファイルの読み込み
    cascade = cv2.CascadeClassifier(cascade_file_path)
    # OpenCVを利用して顔認識
    face_list = cascade.detectMultiScale(image_gs, scaleFactor=1.1,
                                         minNeighbors=5, minSize=(64, 64))

    # 顔が１つ以上検出できた場合
    if len(face_list) > 0:
        count = 1
        for (xpos, ypos, width, height) in face_list:
            # 認識した顔の切り抜き
            face_image = image_rgb[ypos:ypos+height, xpos:xpos+width]
            # 切り抜いた顔が小さすぎたらスキップ
            if face_image.shape[0] < 64 or face_image.shape[1] < 64:
                continue
            # 認識した顔のサイズ縮小
            face_image = cv2.resize(face_image, (64, 64))
            # 認識した顔のまわりを赤枠で囲む
            cv2.rectangle(image_rgb, (xpos, ypos),
                          (xpos+width, ypos+height), (0, 0, 255), thickness=2)
            # 認識した顔を1枚の画像を含む配列に変換
            face_image = np.expand_dims(face_image, axis=0)
            # 認識した顔から名前を特定
            name, result = detect_who(model, face_image)
            # 認識した顔に名前を描画
            cv2.putText(image_rgb, f"{count}. {name}", (xpos, ypos+height+20),
                        cv2.FONT_HERSHEY_DUPLEX, 1, (0, 0, 255), 2)
            # 結果をリストに格納
            result_list.append(result)
            count = count + 1

    # 画像をPNGに変換
    is_success, img_buffer = cv2.imencode(".png", image_rgb)
    if is_success:
        # 画像をインメモリのバイナリストリームに流し込む
        io_buffer = io.BytesIO(img_buffer)
        # インメモリのバイナリストリームからBASE64エンコードに変換
        result_img = base64.b64encode(io_buffer.getvalue()).decode().replace("'", "")

    # tensorflowのバックエンドセッションをクリア
    backend.clear_session()
    # 結果を返却
    return (result_list, result_name, result_img)

def detect_who(model, face_image):
    # 予測
    predicted = model.predict(face_image)
    # 結果
    name = ""
    result = f"本田 翼 の可能性:{predicted[0][0]*100:.3f}% / 佐倉 綾音 の可能性:{predicted[0][1]*100:.3f}%"
    name_number_label = np.argmax(predicted)
    if name_number_label == 0:
        name = "Honda Tsubasa"
    elif name_number_label == 1:
        name = "Sakura Ayane"
    return (name, result)
```

> 画像をBASE64エンコードする処理は https://qiita.com/miler0528/items/59caf9bd1ed4212edd3a を参考にさせて頂きました。

* static/css/style.css を作成します。

> スタイルシートの設定は割愛します。

* templete/pred/base.html を作成します。

```html
{% load static %}
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>kerasで深層学習</title>
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
        <link rel="stylesheet" href="{% static 'css/style.css' %}">
    </head>
    <body>
        <div class="container">
            {% block content %}
            {% endblock %}
        </div>
    </body>
</html>
```

* templete/pred/index.html を作成します。

```html
{% extends 'pred/base.html' %}
{% block content %}
<div style="padding:1em;">
    <div class="page-header">
        <h1>kerasで深層学習　<small>本田翼か佐倉綾音のどちらかに分類してみる</small></h1>
    </div>
    <form method="POST" action="{% url 'index' %}" enctype="multipart/form-data" class="form-horizontal">
        {% csrf_token %}
        <div class='form-group'>
            <div class='col-sm-4'>{{form.as_p}}</div>
            <div class='col-sm-8'><button type="submit" class="btn btn-primary">分類する</button></div>
        </div>
    </form>
    <p class="message">判定画像</p>
    <ul>
        {% if result_name != "" %}
            <li>{{ result_name }}</li>
        {% else %}
            <li>画像なし</li>
        {% endif %}
    </ul>
    <p class="message">判定結果</p>
    <ol>
        {% if result_list %}
            {% for result in result_list %}
                <li>{{ result }}</li>
            {% endfor %}
        {% else %}
            <li>結果なし</li>
        {% endif %}
    </ol>
    <div class="image">
        {% if result_img != "" %}
            <img src="data:image/png;base64,{{result_img}}"/>
        {% endif %}
    </div>
</div>
{% endblock %}
```

* ここまででこんな感じになっているはずです。

```text
img_pred_web
├─mysite (プロジェクト)
│  ├─__init__.py
│  ├─settings.py ← 修正
│  ├─urls.py ← 修正
│  └─wsgi.py
├─pred (アプリケーション)
│  ├─migrations
│  ├─static
│  │  └─css
│  │     └─style.css ← 作成
│  ├─templates
│  │  └─pred
│  │     ├─base.html ← 作成
│  │     └─index.html ← 作成
│  ├─__init__.py
│  ├─admin.py
│  ├─apps.py
│  ├─forms.py ← 作成
│  ├─main.py ← 作成
│  ├─models.py
│  ├─tests.py
│  ├─urls.py ← 修正
│  └─views.py ← 修正
├─venv
├─db.sqlite3
├─haarcascade_frontalface_default.xml ← 配置
├─manage.py
├─model.h5 ← 配置
└─requirements.txt ← 作成
```

## プログラム実行

* Webサーバーを起動し、ブラウザから参照できることを確認します。

```powershell
(venv)> python manage.py runserver
```

* ブラウザを起動し、http://127.0.0.1:8000/ にアクセスします。

* このようなページになりました。
![WS000000.JPG](https://qiita-image-store.s3.amazonaws.com/0/283306/08df367d-8265-7402-b5a3-4bb72b83f1c4.jpeg)


* 画像ファイルを選択して分類してみます。

* 分類できたようです。うれしい。
![WS000001.JPG](https://qiita-image-store.s3.amazonaws.com/0/283306/f1ec83a9-dc4f-30cb-109b-90c21ff2eda1.jpeg)

## 最後に

深層学習で画像の分類を行いたいという勢いから、いろいろな情報を参考にやってみました。
keras(tensorflow)を利用したWebアプリ開発のお作法が正しいのか不安なので、時間があるときに見直してみます。
顔分類はいったん区切りとして、まったく別のことをやってみようかと妄想中です。