OpenCVのカスケード分類器のパラメータをリアルタイムで変化させて顔認識結果を確認できるアプリを作りました。

# はじめに
画像データ扱ってディープラーニングやってみたいとなったら、顔認識は鉄板ですよね。

顔認識やりたいとなった時に、顔の部分だけを切り出した画像を用意しないといけないわけですが、OpenCVのカスケード分類器を使うことが多いと思います。

あれって公式Githubに顔のカスケード分類器だけで4種類もあって、どれ使えば..ってなるし

いざ顔認識させようと思ったらdetectMultiscaleのハイパーパラメータ調整に悩んでしまわないですか？

ということで4種類の学習済みカスケード分類器と、ハイパーパラメータの値をリアルタイムで変化させて、識別結果を確認できるアプリを`Streamlit`で作成しました。

また画像を回転させる機能も実装しました。

ハイパーパラメータ調整で悩んでいる方の助けになれば幸いです。

アプリ本体: [カスケード分類器シミュレーター](https://share.streamlit.io/rukaeto/opencv-practice/main/main.py)
ソースコード: [Github](https://github.com/rukaeto/opencv-practice)

アプリイメージ⇩
![スクリーンショット 2021-09-07 17.32.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890361/7a32c1de-66a7-033d-b020-e29e29beffca.png)


# 環境・ツール
Anacondaの仮想環境を使用しました。
VScodeでコーディング。

python: 3.9.5
opencv: 4.5.1 
streamlit: 0.84.1

# Streamlitに関して
個人的にめちゃ推しのライブラリです。

フロントエンドからバックエンドまでpythonファイルだけで完結するし、デプロイも楽という、僕みたいなよわよわエンジニアにはたまらんですね。

もちろんフロントエンドも凝ろうとすると難しいですが、プロトタイプのアプリや、とにかく形にしたいとなった時重宝するんではないかと思います。

後、意外とスペックは高くて驚きます。

[コチラ](https://qiita.com/okateru/items/6f9daf1094ef8c2d6d68)の記事で紹介している顔認識アプリも`Streamlit`で作ったのですが、学習済みモデルの容量が500MB近くでまぁ`heroku`になんてデプロイできないんですよ。

またGCPのApp Engineにもコチラの顔認識アプリを`Flask`で実装し直して、デプロイしてみたんですが、一番マックスのインスタンスタイプ(メモリ2GBくらいの）ですらオーバーしてデプロイできないくらい消費メモリもエグいアプリなんですよ。

それを`Streamlit`はまぁまぁの速度で動かしてくれるんで正直ビビってます。

`Streamlit`のアプリの[ギャラリー](https://streamlit.io/gallery)には他にも「こんなものまで作れるんか！」みたいなアプリもあるので是非見てみてください。

# コード紹介
画像の回転処理に関しては[コチラ](https://qiita.com/ryokomy/items/0d1a879cac59a0bfbdc5)の記事を参考にしております。

```python:main.py
import streamlit as st
import numpy as np
import cv2
from PIL import Image

# 学習済みカスケードファイル一覧
cascade_alt_tree_path = 'haarcascade_frontalface_alt_tree.xml'
cascade_alt_path = 'haarcascade_frontalface_alt.xml'
cascade_alt2_path = 'haarcascade_frontalface_alt2.xml'
cascade_default_path = 'haarcascade_frontalface_default.xml'

# カスケード分類器をインスタンス化
cascade_alt_tree = cv2.CascadeClassifier(cascade_alt_path)
cascade_alt = cv2.CascadeClassifier(cascade_alt_path)
cascade_alt2 = cv2.CascadeClassifier(cascade_alt2_path)
cascade_default = cv2.CascadeClassifier(cascade_default_path)

# 辞書化
cascade_dict = {
    'alt_tree' : cascade_alt_tree,
    'alt' : cascade_alt,
    'alt2' : cascade_alt2,
    'default' : cascade_default
}

# タイトル
st.title('カスケード分類器シミュレーター')

# サイドバー
selected_cascade = st.sidebar.selectbox('カスケード分類器を選択してください。',
    ['alt_tree', 'alt', 'alt2', 'default'])

# 識別に使うカスケード分類器
use_cascade = cascade_dict[selected_cascade]

selected_scale = st.sidebar.slider(label='scaleFactorの値を設定してください。',
                                    min_value=1.01,
                                    max_value= 2.0,
                                    value = 1.1)

selected_min_neighbors = st.sidebar.slider(label='minNeighborsの値を設定してください。',
                                min_value=1,
                                max_value=20,
                                value=2)

selected_min_size = st.sidebar.slider(label='minSizeの値を設定してください。',
                                        min_value=1,
                                        max_value=400,
                                        value = 50)

selected_angle = st.sidebar.slider(label='回転角の設定(左回転)',
                                    min_value= -180,
                                    max_value=180,
                                    value=0)

selected_color_name = st.sidebar.selectbox('描画する四角形の色を選択してください。',
                            ['赤', '白', '緑', '青', '黄色', '黒'])

color_dict = {
    '赤' : (255,0,0),
    '白' : (255,255,255),
    '緑' : (0,128,0),
    '青' : (0,0,255),
    '黒' : (0,0,0),
    '黄色' : (255,255,0)
}

selected_color = color_dict[selected_color_name]

# メインコンテンツ
col1, col2 = st.columns(2)
with col1:
    st.write('以下の設定で顔認識を行います。')
    st.text(f'カスケード分類器: {selected_cascade}')
    st.text(f'scaleFactor: {selected_scale}')
    st.text(f'minNeighbors: {selected_min_neighbors}')
    st.text(f'minSize: ({selected_min_size}, {selected_min_size})')
    if selected_angle == 0:
        st.text('画像を回転させない')
    elif selected_angle > 0:
        st.text(f'画像を左に{selected_angle}° 回転' )
    elif selected_angle < 0:
        st.text(f'画像を右に{-selected_angle}° 回転' )

with col2:
    uploaded_file = st.file_uploader('画像をアップロードすると識別を開始します。', type=['jpg', 'jpeg', 'png'])

# 画像がアップロードされた時
if uploaded_file is not None:
    img = Image.open(uploaded_file)
    np_img = np.array(img, dtype =np.uint8)
    img_gray = cv2.cvtColor(np_img, cv2.COLOR_BGR2GRAY)

    # 回転処理
    if selected_angle != 0:
        height, width = np_img.shape[:2]
        size = (height, width)
        angle_rad = selected_angle / 180.0*np.pi
        w_rot = int(np.round(height*np.absolute(np.sin(angle_rad))+width*np.absolute(np.cos(angle_rad))))
        h_rot = int(np.round(height*np.absolute(np.cos(angle_rad))+width*np.absolute(np.sin(angle_rad))))
        size_rot = (w_rot, h_rot)

        # 元画像の中心を軸にして画像を回転させる
        center = (width/2, height/2)
        scale = 1.0
        rotation_matrix = cv2.getRotationMatrix2D(center, selected_angle, scale)

        # 平行移動を加える
        affine_matrix = rotation_matrix.copy()
        affine_matrix[0][2] = affine_matrix[0][2] -width/2 + w_rot/2
        affine_matrix[1][2] = affine_matrix[1][2] -height/2 + h_rot/2

        np_img = cv2.warpAffine(np_img, affine_matrix, size_rot, flags=cv2.INTER_CUBIC)
        img_gray = cv2.cvtColor(np_img, cv2.COLOR_BGR2GRAY)

    face_list = use_cascade.detectMultiScale(img_gray, scaleFactor = selected_scale,
                                            minNeighbors = selected_min_neighbors,
                                            minSize = (selected_min_size, selected_min_size))
    face_number = len(face_list)
    if face_number == 0:
        st.error('顔が検出されませんでした。')
        st.image(np_img)
    if face_number > 0:
        for rect in face_list:
            cv2.rectangle(np_img, tuple(rect[0:2]), tuple(rect[0:2]+rect[2:4]), selected_color, thickness = 4)
        st.image(np_img)
```

素人なのでコーディングセンス壊滅のコードで見づらいですね...。 すいません。

`Streamlit`は他の言語で実装するってなると躊躇うような、スライダーやリスト、画像アップローダーなどをめちゃ簡単に実装できてしまうのがたまらんですね。

もっと流行ってほしい。

# 実際に動かしてみる

いつものごとく推しメンの田村保乃ちゃんの画像を使います。
正面のやつだと簡単に認識しちゃうので、斜めの自撮り画像でやってみましょう。

![スクリーンショット 2021-09-07 17.40.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890361/3dea8cb7-7e48-ce2e-9a86-be62756c7b76.png)

初期の設定のパラメータだとダメですね。
scaleFactorを1.09にしてみます。

![スクリーンショット 2021-09-07 17.42.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890361/b1748301-b6a4-1726-db2f-addc3e0d06c4.png)

ちゃんと認識されましたね。
このままカスケード分類器をalt2にしてみます。

![スクリーンショット 2021-09-07 17.44.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890361/2fbeef9d-5679-67b7-4559-d19aff8fb0f6.png)

検知してくれませんね。このまま画像回転も試して見ましょう。左に斜めになっているので右回転させます。
![スクリーンショット 2021-09-07 17.45.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890361/fd205c5c-9695-dfc2-892e-acd89b8b4336.png)

おおー認識してくれました。

とこんな感じでいちいちコード上の数値を変えたり、for文で繰り返したりすることなく、リアルタイムでパラメータ変えてシミュレーションできるので、（ニーズがあるかは別として）個人的には満足するものができたと思います。

# まとめ
実際に動かしてみて、ちょっとパラメータを変えるだけで検出結果がすぐ変わるので、顔認識のパラメータ調整の大変さを再認識することになりました。

これを画像ごとに自動的に最適なパラメータを設定して認識するようなアプリ、仕組みも作ってみたいですね。

顔認識のパラメータ調整に悩んでいる方の手助けや、`Streamlit`の魅力が少しでも伝われば幸いです。