【Python】機械学習を使って自分の顔に似合う髪型を出してくれるアプリ


# はじめに
皆さんこんにちは！僕は大学二年生のメディア関係の学科で学んでいるものです。今回は夏休み期間を使ってaidemy premium に入会して、PythonのAIアプリ開発について学習しました。その最後に、ここで学んだことを応用して、一つアプリを作ることになりました。
 
 
自分が作ったのは、**撮影した自分の顔に似合う髪型を紹介してくれるアプリ**です！　　　　　　　　大学生になると、垢抜けをして髪を染めたり髪型を大きく変えたりする人が増えていきます。僕もそれにあこがれて、雰囲気を少しでも変えてみたいと思うのですが、髪型が多すぎて迷ってしまいます…     
そこで、自動で自分に似合う髪型を提案してくれるアプリがあったらいいなと思って、今回の制作に至りました。




それでは、制作を始めていきます。

## ※実行環境
Google Colaborator
Visual Studio Code
Anaconda3
Python3


# 1．モデル制作

ここでは、アプリ開発におけるモデルを作っていきます。ここでいうモデルでは、男女別に顔写真をブラウザ内で検索して、それぞれフォルダに分ける役割を持っています。これがいわゆる機械学習です。
まず初めに、Google Colaboratory(Colab)を使ってGoogleドライブをマウント(連結)します。
    
    from google.colab import drive
    drive.mount('/content/drive')


次にicrawlerをインストールします。

    !pip install icrawler

Bing用クローラーのモジュールをインポートします。

    from icrawler.builtin import BingImageCrawler


ついに、モデルを作っていきます。この部分はかなり試行錯誤しましたが、カウンセリングも利用してなんとか完成しました。
※　途中で、以下のコードの中の一部で、完全な初心者の自分が一番苦戦した部分の変更前と変更後のコードを記載し、どのような変更を加えたのかを紹介します。
※　今回のアプリは、当初撮影した顔の形に合わせて、男女かを識別して、かつ、それぞれの性別に合わせた髪型の写真を提示するものにする予定でしたが、
時間の都合上見本となる写真の素材収集の手間を防ぐべく、男性の丸顔と男性の細顔の2パターンの顔の形で検索して素材を集めることにしました。



## ①必要なモジュールをインポート
必要なモジュールをインポートします。
openCV、numpy、matplotlib、tensorflowなどのライブラリーを使用しました。

    import os
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.preprocessing import OneHotEncoder   #one-hot encoder用に追加
    from tensorflow.keras.utils import to_categorical
    from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
    from tensorflow.keras.applications.vgg16 import VGG16
    from tensorflow.keras.models import Model, Sequential
    from tensorflow.keras import optimizers



## ②ファイルパスの指定
収集した細顔と丸顔それぞれのファイルのパスを設定します。

    # ------------------------6100
    path_male1 = os.listdir("/content/drive/MyDrive/課題/男性_正面顔_丸顔_全体_写真")
    path_male2 = os.listdir('/content/drive/MyDrive/課題/男性_正面顔_細顔_全体_写真')
    
    img_male1 = []
    img_male2 = []
    
    print(len(path_male1))
    print(len(path_male2))
    



## ③画像の前処理
画像の前処理として　RGBの変換、画像のリサイズ(300ｘ300→アプリは100x100)、画像データ格納用リストに読み込んだ画像を追加していく等の処理を行います。


    for i in range(len(path_male)):
        img = cv2.imread("/content/drive/MyDrive/課題/男性　正面顔　全体　写真/" + path_male[i])
        b,g,r = cv2.split(img)
        img = cv2.merge([r,g,b])
        img = cv2.resize(img, (300,300))
        img_male.append(img)

    for i in range(len(path_female)):
        img = cv2.imread('/content/drive/MyDrive/課題/女性　正面顔　全体　写真/' + path_female [i])
        b,g,r = cv2.split(img)
        img = cv2.merge([r,g,b])
        img = cv2.resize(img, (300,300))
        img_female.append(img)
    #--------- 6100
    for i in range(len(path_male1)):
      img = cv2.imread("/content/drive/MyDrive/課題/男性_正面顔_丸顔_全体_写真/" + path_male1 [i])
      
      if img is not None:
        b,g,r = cv2.split(img)
        img = cv2.merge([r,g,b])
        img = cv2.resize(img, (50,50))
        img = img.astype('float32') / 255　　　# sigmoid関数利用のため0～1に正規化：Normalize the image data
        img_male1.append(img)
      
      else:
        print(f"Skipping image {path_male1[i]}")

    for i in range(len(path_male2)):
      img = cv2.imread('/content/drive/MyDrive/課題/男性_正面顔_細顔_全体_写真/' + path_male2 [i])
      
      if img is not None:
        b,g,r = cv2.split(img)
        img = cv2.merge([r,g,b])
        img = cv2.resize(img, (50,50))
        img = img.astype('float32') / 255　　　# sigmoid関数利用のため0～1に正規化：Normalize the image data
        img_male2.append(img)
        
      else:
        print(f"Skipping image {path_male2[i]}")

    




## ④データの分割    
機械学習モデル、特に二項分類 (0 または 1) 用のデータを準備します。これは、細顔と丸顔のデータを分類し、それぞれ0と1の数値として分類する作業です。


    #---- 6100
    X = np.array(img_male1 + img_male2)  #画像の準備
    y =  np.array([0]*len(img_male1) + [1]*len(img_male2))



    #--- 6100
    rand_index = np.random.permutation(np.arange(len(X)))   #--- 順番：randon
    X = X[rand_index]
    y = y[rand_index]

    X_train = X[int(len(X)*1.8):]
    y_train = y[int(len(y)*1.8):]
    X_test = X[int(len(X)*1.8):]
    y_test = y[int(len(X)*1.8):]


お待たせいたしました。それでは、どこを変更したのかを紹介します。
上記のコードの、データ分割、つまり、データをどのあたりでスライスをするかを設定する部分が変更点となります。



＊＊＊＊＊変更前＊＊＊＊＊

    X_train = X[int(len(X)*1.8):]
    y_train = y[int(len(y)*1.8):]
    X_test = X[int(len(X)*1.8):]
    y_test = y[int(len(X)*1.8):]


最初実行した際、
(0, 300, 300, 3)
と帰ってきました。
カッコ内の最初のy_testの部分が サイズ0のarray (つまり、空のarray) となっています。
そこで、スライスする長さを以下のように変更しました。


＊＊＊＊＊変更後＊＊＊＊＊

    #--- 6100
    X_train = X[:int(len(X)*0.8)]
    y_train = y[:int(len(y)*0.8)]
    X_test = X[:int(len(X)*0.8)]
    y_test = y[:int(len(y)*0.8)]


    y_train = to_categorical(y_train)[:1000]    #---2.1.4 OneHot
    y_test = to_categorical(y_test)[:1000]



続きです。
# ⑤vgg16のインスタンスの生成
今回のアプリ制作は、VGG16を利用した転移学習を取り入れることも目的としています。

VGG16とは、オックスフォード大学の研究者が作った画像認識用のニューラルネットワークのことで、真似るだけで高い認識精度を得ることができる、性能の良いモデルを作れます。しかも、100万枚を超える画像で事前学習させたモデルを、手軽に利用できるのです。

また、ここで重要になってくるのがsoftnmax関数です。

    #input_tensor = Input(shape=(300, 300, 3))
    input_tensor = Input(shape=(50, 50, 3))     #--- 6100
    vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

    top_model = Sequential()
    top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
    top_model.add(Dense(256, activation='relu'))
    #input_tensor = Input(shape=(300, 300, 3))
    input_tensor = Input(shape=(50, 50, 3))     #--- 6100
    vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

    top_model = Sequential()
    top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
    top_model.add(Dense(256, activation='relu'))

    #ドロップアウト
    #---------------------------
    top_model.add(Dropout(rate=0.5))
    #---------------------------

    #top_model.add(Dense(10, activation='softmax'))
    top_model.add(Dense(2, activation='softmax'))  #--- softnmax  

#### ※「top_model = Sequential()」とは？
モデルの一種です。
これを宣言した後に層を追加します。

#### ※「top_model.add(Flatten(input_shape=vgg16.output_shape[1:])) (Flatten層)」とは？
これは文字通り入力を「フラット」にする層で、3次元のデータを線に変換します。

#### ※「top_model.add(Dense(256, activation='relu')) (Dense層)」とは？
Dense層は全結合層と呼ばれ、前の層とすべてのニューロンが結合して、画像から特徴的なパターンを抽出してくれます。計算結果は、softmax(活性化関数)に渡されます。

#### ※softmax(ソフトマックス)とは？
活性化関数とも呼ばれています。出力の合計値が1 (= 100%)になるように変換して出力する、ニューラルネットワークで頻繁に用いられる関数のことです。ここでは、細顔or丸顔の2択で選別して、確率で表示します。

# ⑥モデルの連結
個別のモデルデータを一つにして記録します。
    model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

    #vgg16の重みの固定
    for layer in model.layers[:19]:
        layer.trainable = False

    model.compile(loss='categorical_crossentropy',
                  optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
                  metrics=['accuracy'])

    model.summary()

    #学習過程の取得
    history = model.fit(X_train, y_train, batch_size=32, epochs=30, validation_data=(X_test, y_test))

# ⑦重みを保存
このモデルデータを、「model.h5」として保存、ダウンロードします。

    model.save(os.path.join(result_dir, 'model.h5'))

    files.download( '/content/results/model.h5' )

ここでいったん実行します。

## テスト
続いてのコードは、丸顔か細顔かを識別して、いずれのファイルから画像を取り出すためのテストコードです。
例えば、提出された顔画像が丸顔と細顔かで、細顔の確率が高い場合は細顔のファイルパスから画像を取り出します。

    X = np.array(img_male1 + img_male2)  #画像の準備
    y =  np.array([0]*len(img_male1) + [1]*len(img_male2))
より、提出された画像データと、フォルダ内にある画像を照らし合わせて、フォルダの中からこの細顔だと出力。

    def pred_face(img):

    # 画像の前処理
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img = cv2.resize(img, (50, 50))
    img = img / 255.0  # 正規化

    # モデルに画像を渡して性別を予測
    prediction = model.predict(np.expand_dims(img, axis=0))
    
    if prediction[0][0]<prediction[0][1]:
        return '細顔'
    else:
        return '丸顔'

ここでは、画像データが行列として格納されています。丸顔ファイルパス名male１で受け取った画像の一枚目、つまり、行列の一列目を出します。

    img = cv2.imread('/content/drive/MyDrive/課題/男性_正面顔_丸顔_全体_写真/' + path_male1[0])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    plt.imshow(img)
    plt.show()
    print(pred_face(img))

テストは無事成功です！
![2023-10-15_221347.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627656/7d6cb0ca-c4a4-4f9a-0b49-1dbdb6d93a06.png)


# 2.アプリ部分の制作
それでは、モデルは完成したので、ダウンロードしたモデルデータを利用して、アプリの機能を作っていきます。渡された画像に対して、丸顔か細顔かを識別して、それぞれに合った画像を何枚か表示できるようにしました。

## ①画像の収集
出力する用の細顔と丸顔の全体が写った画像をそれぞれ10枚ほど集めていきます。これは、手動での作業だったため、髪型はもちろん、全体的に顔が写っているものを選択しながら集めなければいけなかったため、大変でした。

集めた画像は、hosogao、marugaoの二つのファイルに入れました。

## ②Flaskサイド制作
ここで、Flaskを利用したコードを作っていきます。Flask側でコードを加えなければ実際にファイルをアップロードすることはできません。
こちらのコードを使うことで、アプリとしての機能を果たすことができます。

```python:my_app.py
    import os
    from flask import Flask, request, redirect, render_template, flash
    from werkzeug.utils import secure_filename
    from tensorflow.keras.models import Sequential, load_model
    from tensorflow.keras.preprocessing import image

    import numpy as np


    classes = ["丸顔","細顔"]
    image_size = 50

    UPLOAD_FOLDER = "uploads"
    ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

    app = Flask(__name__)

    def allowed_file(filename):
        return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

    model = load_model(r'./pymodel.h5')#学習済みモデルをロード


    @app.route('/', methods=['GET', 'POST'])
    def upload_file():
        if request.method == 'POST':
            if 'file' not in request.files:
                flash('ファイルがありません')
                return redirect(request.url)
            file = request.files['file']
            if file.filename == '':
                flash('ファイルがありません')
                return redirect(request.url)
            if file and allowed_file(file.filename):
                filename = secure_filename(file.filename)
                file.save(os.path.join(UPLOAD_FOLDER, filename))
                filepath = os.path.join(UPLOAD_FOLDER, filename)

                #受け取った画像を読み込み、np形式に変換
                img = image.load_img(filepath, target_size=(image_size,image_size))
                img = image.img_to_array(img)
                data = np.array([img])
            
            
           #変換したデータをモデルに渡して予測する
            if request.method == 'POST':
            # ... (existing code)

               result = model.predict(data)[0]
               predicted = result.argmax()
               pred_answer = "これは " + classes[predicted] + " です"

            # Save the image to the appropriate folder
            if pred_answer == "丸顔":
                save_folder = "marugao"
            else:
                save_folder = "hosogao"

            os.makedirs(os.path.join(UPLOAD_FOLDER, save_folder), exist_ok=True)
            file.save(os.path.join(UPLOAD_FOLDER, save_folder, filename))

            image_list = os.listdir(os.path.join(UPLOAD_FOLDER, save_folder))
            print(image_list)


            return render_template("index.html", answer=pred_answer, image_folder=save_folder , image_list=image_list)

        return render_template("index.html", answer="")


    if __name__ == "__main__":
        # port = int(os.environ.get('PORT', 8080))
        # app.run(host ='0.0.0.0',port = port)

    app.run()
```
画像を提出した際に、細顔と丸顔どちらかという表示のみで、見本画像が出てこないというトラブルがありましたが、image_folderにsave_folderの画像を入れて、結果次第でいずれかのフォルダから画像を出力できるように変数の変更を行うことで解決できました。

# 3.HTMLとCSSサイド制作
ここでは、アプリの見た目を作っていきます。今回は提出された画像に対して見本となる顔画像を結果として出力するアプリなので、どのような結果を返すのかをここで設定していきます。
## HTMLサイド制作
<!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Number Classifier</title>
        <link rel="stylesheet" href="./static/stylesheet.css">
    </head>
    <body>
        <header>   
            <img class="header_img" src="https://aidemyexstorage.blob.core.windows.net/aidemycontents/1621500180546399.png" alt="Aidemy">
            <a class="header-logo" href="#">Number Classifier</a>
    </header>

    <div class="main">    
        <h2> AIが送信された画像の数字を識別します</h2>
        <p>画像を送信してください</p>
        <form method="POST" enctype="multipart/form-data">
            <input class="file_choose" type="file" name="file">
            <input class="btn" value="submit!" type="submit">
        </form>
        <div class="answer">{{answer}}</div>
    </div>

    

    {% if answer %}
        <h2>こちらの髪型はいかがですか？</h2>
        {% for image_file in image_list %}
            <img src="{{ url_for('static', filename=image_folder + '/' + image_file) }}" alt="{{ answer }} Image">
    
            {% endfor %}
    {% endif %}
    
        </body>
    </html>

    <footer>
        <img class="footer_img" src="https://aidemyexstorage.blob.core.windows.net/aidemycontents/1621500180546399.png" alt="Aidemy">
        <small>&copy; 2019 Aidemy, inc.</small>   
    </footer>
    </body>
    </html>

## CSSサイド制作
    header {
        background-color: #76B55B;
        height: 60px;
        margin: -8px;
        display: flex;
        flex-direction: row-reverse;
        justify-content: space-between;
    }

    .header-logo {
        color: #fff;
        font-size: 25px;
        margin: 15px 25px;
    }

    .header_img {
        height: 25px;
        margin: 15px 25px;
    }

    .main {
        height: 370px;
    }

    h2 {
        color: #444444;
        margin: 90px 0px;
        text-align: center;
    }

    p {
        color: #444444;
        margin: 70px 0px 30px 0px;
        text-align: center;
    }

    .answer {
        color: #444444;
        margin: 70px 0px 30px 0px;
        text-align: center;
    }

    form {
        text-align: center;
    }

    footer {
        background-color: #F7F7F7;
        height: 110px;
        margin: -8px;
        position: relative;
    }

    .footer_img {
        height: 25px;
        margin: 15px 25px;
    }

    small {
        margin: 15px 25px;
        position: absolute;
        left: 0;
        bottom: 0;
    }

# 完成
そして完成したアプリがこちらになります！！
やはり画像のデータが少ないとアプリとしての精度が低くいですね。時間があればもう少し画像を集め、精度の高いアプリを作れると思います。

[こちらがリンクとなります](https://flask-my-ori-app.onrender.com)

# 終わりに
知識が何一つなく、挫折してしまいそうになり、完成までかなりの時間を要してしまいましたが、カウンセリングの力も借りてなんとか仕上げることができました！とても貴重な体験となり、aidemyの方々には本当に感謝しています。これからは、自分の力でアプリを手軽に作れるところまで行けたらいいなと思います。

最後までお読みいただきありがとうございました。