女子プロゴルファーの顔診断AIを作ってみた①

#1. はじめに
今学んでるディープラーニングを使って制作物を作ろうと考えました。
画像を使ったAIアプリを作れたらいいなと思い、取り組みました。

題材を何にしようか悩んでいたら父親が女子プロゴルファーにハマっていたので女子プロの診断アプリを作成しようと思った。
渋野日向子プロ、小祝さくらプロ、原英莉花プロの画像診断アプリを作ることにした。

※本当はここにも顔写真載せたかったけど著作権の関係からだめそうなので諦めました。

#2. 進め方
#####①画像収集
#####②顔部分のみを取得
#####③不要データを削除(例えば違う顔だったり、サングラスかけてたり、なぜかおじさんの画像)
#####④テストデータとバリデーションデータとに分ける
#####⑤画像の水増し
#####⑥モデルの構築(今回はvgg16の転移学習)
#####⑦公開用のアプリケーション作成
#####⑧デプロイ
なかなかハードでした・・・試したこといろいろ書こうと思います。


#3. 画像収集
やり方はいろいろあると思いますがicrawlerを用いました。
###icrawlerとは？
Webクローラーのミニフレームワークです。画像や動画などのメディアデータをサポートしており、テキストやその他の種類のファイルにも適用可能です。Scrapyは重くて強力ですが、icrawlerは軽いみたいです。
[公式リファレンス](https://icrawler.readthedocs.io/en/latest/)
インストール方法も載っていますのでご参考ください。

```python:search.py
from icrawler.builtin import BingImageCrawler
import os
import shutil

# 検出する画像
golfer_lists = {'渋野日向子': 'shibuno', '小祝さくら': 'koiwai', '原英莉花': 'hara'}

# フォルダの作成
os.makedirs('./origin_image', exist_ok=True)

# keyに検索用の名前、valueはフォルダ名
for key, value in golfer_lists.items():
    # 取得した画像の保存先を指定
    crawler = BingImageCrawler(storage={'root_dir': value})
    # キーワードと取得枚数を指定
    crawler.crawl(keyword=key, max_num=1000)
    # フォルダを移動
    path = os.path.join('./', value)
    shutil.move(path, './origin_image/')
```

これでそれぞれ画像の取得ができました。1000枚としたけど実際は７００枚くらいでした。

#4. 顔部分の取得
顔部分の取得は[face_recognition](https://github.com/ageitgey/face_recognition)を使いました。

```python:face_recognition.py
import cv2
from PIL import  Image
import os, glob
import numpy as np
import random
from PIL import ImageFile
import face_recognition

# 元画像が入っている大元のフォルダ
in_dir = './origin_image/*'
# 顔部分のみ取り出した画像が入る
out_dir = './face'
# 各選手のフォルダ
in_file = glob.glob(in_dir)
# 各選手のフォルダ名を取得
fileName_lists = os.listdir('./origin_image')
# テストファイルの保存先
test_image_path = './test_image/'

# 各フォルダごとに処理を行う
for golfer, fileName in zip(in_file, fileName_lists):
    # 各選手の画像リストを取得
    in_jpg = glob.glob(golfer + '/*')
    # 各選手の画像名
    in_fileName=os.listdir(golfer)
    # 各選手のフォルダーパス
    folder_path = out_dir + '/' + fileName
    # 各選手の出力フォルダを作成
    os.makedirs(folder_path, exist_ok=True)

    # 各画像について処理を行う
    for i in range(len(in_jpg)):
        # 画像を読み込む
        # image(縦, 横, 3色)
        image = face_recognition.load_image_file(str(in_jpg[i]))
        faces = face_recognition.face_locations(image)
        
        # 画像が存在したら([(911, 2452, 1466, 1897)])のような出力がされる
        if len(faces) > 0:
            # 取得した顔画像の中で最大の物を選ぶ((top - bottom)*(right - left)を計算)
            face_max = [(abs(faces[i][0]-faces[i][2])) * (abs(faces[i][1]-faces[i][3])) for i in range(len(faces))]
            top, right, bottom, left = faces[face_max.index(max(face_max))]

            # 顔部分の画像を抜き出す
            faceImage = image[top:bottom, left:right]

            # Image.fromarray()にndarrayを渡すとPIL.Imageが得られ、そのsave()メソッドで画像ファイルとして保存できる。
            final = Image.fromarray(faceImage)

            final = np.asarray(final.resize((64, 64)))
            final = Image.fromarray(final)

            file_path = folder_path + '/' + str(i) + '.jpg'
            final.save(file_path)
        else:
            print('No Face')
```

ちょっと長いですがこのようになっています。

現在のフォルダ体系の確認
<img width="619" alt="フォルダ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621150/7367455b-6764-13a6-2ec6-ac052c0971e8.png">


これで顔部分を取得できました。
__あとはひたすら１枚ずつ確認する作業・・・・__
大変だけどこれが大事なんです。

#5. 訓練データとバリデーションデータに分ける
訓練データ80%、バリデーションデータ20%に分ける。

```python:split.py
# 訓練データとバリデーションデータとに分ける
import os, glob
import shutil

# 顔部分のみ取り出した画像が入る
in_dir = './face/*'
# 各選手のフォルダ ['./face/shibuno'　'./face/koiwai', './face/hara']
in_file = glob.glob(in_dir) 
# 各選手のフォルダ名を取得 ['shibuno', 'koiwai', 'hara']
fileName_lists = os.listdir('./face')
# テストファイルの保存先
test_image_path = './valid/'

# 各フォルダごとに処理を行う
for golfer, fileName in zip(in_file, fileName_lists):
    # 各選手の画像リストを取得
    in_jpg = glob.glob(golfer + '/*')
    # 各選手の画像名
    in_fileName=os.listdir(golfer)

    # validation用のデータを保存する
    test_path = test_image_path + fileName
    os.makedirs(test_path, exist_ok=True)

    # バリデーション用のフォルダへ移動する
    for i in range(len(in_jpg)//5):
        shutil.move(str(in_jpg[i]), test_path+'/')
```

<img width="614" alt="フォルダ２.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621150/c086db9a-3c04-2b24-4773-9d9cbf332afc.png">

こんな感じになっていればOK。
次に訓練データの画像の水増しを行う。

#6. データのかさ増し(水増し)
画像データを集めて、取捨選択することは非常に手間がかかり大変です。(ほんとに大変でした)
そこで画像データを反転したり、ずらしたりすることで新たなデータを作り出します。

```python:pic_add
import PIL
from keras.preprocessing.image import load_img, img_to_array, ImageDataGenerator, array_to_img
import numpy as np
import os, glob
import matplotlib.pyplot as plt
import cv2

# 元画像が入っている大元のフォルダ
in_dir = './face/*'
# 顔部分のみ取り出した画像が入る
out_dir = './face'
# 各選手のフォルダパス
in_files = glob.glob(in_dir)
# 各フォルダ名
folder_names = os.listdir('./face')

# 各選手のパスとフォルダ名
for file, name in zip(in_files, folder_names):
    # 各画像
    image_files = glob.glob(file + '/*')
    # 各ファイル名
    in_fileName = os.listdir(file)

    SAVE_DIR = './face/' + name
    # 保存先ディレクトリが存在しない場合、作成する。
    if not os.path.exists(SAVE_DIR):
        os.makedirs(SAVE_DIR)

    # 各画像それぞれに水増しを行う
    for num in range(len(image_files)):
        datagen = ImageDataGenerator(
        rotation_range=40,       # ランダムに回転する回転範囲（単位degree）
        width_shift_range=0.2,   # ランダムに水平方向に平行移動する、画像の横幅に対する割合
        height_shift_range=0.2,  # ランダムに垂直方向に平行移動する、画像の縦幅に対する割合
        shear_range=0.2,         # せん断の度合い。大きくするとより斜め方向に押しつぶされたり伸びたりしたような画像になる（単位degree）
        zoom_range=0.2,          # ランダムに画像を圧縮、拡大させる割合。最小で 1-zoomrange まで圧縮され、最大で 1+zoom_rangeまで拡大される。
        horizontal_flip=True,    # Trueを指定すると、ランダムに水平方向に反転します。
        fill_mode='nearest')

        img_array = cv2.imread(image_files[num])  # 画像読み込み
        img_array = img_array.reshape((1,) + img_array.shape)  # 4次元データに変換（flow()に渡すため）

        # flow()により、ランダム変換したイメージのバッチを作成。
        # 指定したディレクトリに生成画像を保存する。
        i = 0
        for batch in datagen.flow(img_array, batch_size=1,
                                save_to_dir=SAVE_DIR, save_prefix='add', save_format='jpg'):
            i += 1
            if i == 5:
                break  # 停止しないと無限ループ
```
これで訓練データの画像は１０００枚を超えた。

__かなり長くなったのでモデルの作成は次回に持ち越す。__


番外編としてカスケード分類器を用いた場合のコードも記載する。
####※カスケード分類器について
当初カスケード分類器を用いて顔部分を取得しようとしたが文字部分や顔を取得できずにデータ量が六分の１くらいになってしまった。
一応カスケード分類器についても載せておきます
下記位リンク先より「haarcascade_frontalface_default.xml」をダウンロードしておく。
>
(https://github.com/opencv/opencv/tree/master/data/haarcascades)

```python:cascade.py
import cv2
from PIL import  Image
import os, glob
import numpy as np
import random
from PIL import ImageFile

# 元画像が入っている大元のフォルダ
in_dir = './origin_image/*'
# 顔部分のみ取り出した画像が入る
out_dir = './face_image'
# 各選手のフォルダ
in_file = glob.glob(in_dir)
# 各選手のフォルダ名を取得
fileName_lists = os.listdir('./origin_image')
# テストファイルの保存先
test_image_path = './face_image/test_image/'

cascade_path = './haarcascade_frontalface_alt.xml'
face_cascade = cv2.CascadeClassifier(cascade_path)

# 各フォルダごとに処理を行う
for golfer, fileName in zip(in_file, fileName_lists):
    # 各選手の画像リストを取得
    in_jpg = glob.glob(golfer + '/*')
    # 各選手の画像名
    in_fileName=os.listdir(golfer)
    # 各選手のフォルダーパス
    folder_path = out_dir + '/' + fileName
    # 各選手の出力フォルダを作成
    os.makedirs(folder_path, exist_ok=True)

    # 各画像について処理を行う
    for i in range(len(in_jpg)):
        # 画像を読み込む
        image=cv2.imread(str(in_jpg[i]))
        # グレースケールにする
        image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        faces = face_cascade.detectMultiScale(image_gray, scaleFactor=1.1, minNeighbors=2, minSize=(64, 64))

        if len(faces) > 0:
            for j, face in enumerate(faces,1):
                x, y ,w, h =face
                save_img_path = folder_path + '/' + str(i) +'_' + str(j) + '.jpg'
                cv2.imwrite(save_img_path , image[y:y+h, x:x+w])
        else:
            print ('image' + str(i) + ':NoFace')
```

##次回
[女子プロゴルファーの顔診断AIを作ってみた②](https://qiita.com/ProgramWataru/items/58fe9370097107cc4150)