0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】画像認識でパーソナルカラーを識別

Last updated at Posted at 2024-10-19

はじめに

私は、学生の頃から、自分に似合う服や色がわからず、買っても着なかったり、
着ても似合わなかったりと繰り返してきました。
社会人になり、パーソナルカラー診断により、ようやく自分の色やスタイルが
定まってきたと感じます。
今回は、誰でも簡単に、AIアプリですぐ診断ができるように
パーソナルカラー診断アプリを作成しました。

目次

1.実行環境
2.実際のアプリ
3.画像収集
4.画像の処理
5.モデルの作成
5-1 データが正しく読み込まれているか
5-2 モデルの学習の確認
5-3 モデルの評価
5-4 動作確認の結果から、過学習対策を検討
5-5 モデルを変更してみる(VGG16・ResNet)
6.HTML・SCRIPT.JSなどの作成
7.結果と考察
8.感想

1.実行環境

Visual Studio Code
Python 3.12.5
tensorflow 2.17.0

2.実際のアプリ

image.png

結果の表示
image.png

3.画像収集

必要な画像を用意します。
当初、インターネットで既に画像セットがあったものを利用させていただきました。
RoboflowRoboflow
personal color Classification Dataset by Capstonea
https://universe.roboflow.com/capstonea-9fv4r/personal-color

しかし、よく見ると、同じ写真が春にも秋にも入っていたりと、
画像の正確性に疑いが生じたため、自分自身でスクレイピングし、画像を収集することに切り替えました。

具体的には、芸能人のパーソナルカラーを調べ、
その芸能人の写真をスクレイピングで収集していきました。

パーソナルカラーを調べたサイト
https://personalcol0r.com/blube-winter/celebrity-3/

スクレイピングのコード
女優の名前を指定というところを、名前を変えて、ひたすら20枚ずつスクレイピングしていきました。実際には、男性もスクレイピング対象となっています。

import requests
from bs4 import BeautifulSoup
import os

# 女優の名前を指定
actress_name = "松坂桃李"
search_url = f"https://www.google.com/search?q={actress_name}&tbm=isch"

# Google画像検索の結果ページを取得
headers = {"User-Agent": "Mozilla/5.0"}
response = requests.get(search_url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')

# 画像のURLを取得
image_urls = []
for img in soup.find_all("img"):
    img_url = img.get("src")
    if img_url.startswith("http"):  # 画像URLだけを選別
        image_urls.append(img_url)

# 保存するフォルダを指定
save_folder = "actress_images"  
if not os.path.exists(save_folder):
    os.makedirs(save_folder)

# 保存する枚数を指定
max_images = 20  # 保存する画像の最大数

# 画像をダウンロードして保存
for idx, img_url in enumerate(image_urls):
    if idx >= max_images:
        break  # 20枚ダウンロードしたら終了
    
    img_data = requests.get(img_url).content
    with open(os.path.join(save_folder, f"{actress_name}_{idx}.jpg"), 'wb') as handler:
        handler.write(img_data)

print(f"{idx + 1} 枚の画像をダウンロードしました")

4.画像の処理

あきらかに顔がぶれているような写真や、モノクロの写真、2人以上で写っている写真などは削除しました。できるだけ、顔がアップのものを選びました。

5.モデルの作成

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout
from tensorflow.keras.utils import to_categorical  # ここで to_categorical をインポート
from sklearn.model_selection import train_test_split
import numpy as np
from tensorflow.keras.preprocessing.image import load_img, img_to_array
import os
from tensorflow.keras.optimizers import Adam

# 画像を読み込む関数
def load_images_from_folder(folder, target_size=(224, 224)):
    images = []
    labels = []
    for label, subfolder in enumerate(os.listdir(folder)):  # 各フォルダがラベルになる
        subfolder_path = os.path.join(folder, subfolder)
        if os.path.isdir(subfolder_path):  # ディレクトリかどうかをチェック
            for filename in os.listdir(subfolder_path):
                img_path = os.path.join(subfolder_path, filename)
                img = load_img(img_path, target_size=target_size)
                img_array = img_to_array(img)
                images.append(img_array)
                labels.append(label)
    return np.array(images), np.array(labels)

# トレーニングデータと検証データのパス
train_data_dir = r'C:\Users\korom\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Python 3.12\XXXXX\data\train'
valid_data_dir = r'C:\Users\korom\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Python 3.12\XXXXX\data\valid'
test_data_dir = r'C:\Users\korom\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Python 3.12\XXXXX\data\test'  # 追加

# トレーニングデータを読み込む
x_train, y_train = load_images_from_folder(train_data_dir)
x_valid, y_valid = load_images_from_folder(valid_data_dir)
print(x_train.shape) 
print(y_train.shape)

# データの正規化(0-1の範囲にする)
x_train = x_train / 255.0
x_valid = x_valid / 255.0

# ラベルをone-hotエンコーディング
y_train = to_categorical(y_train, 4)  # クラスが4つなので
y_valid = to_categorical(y_valid, 4)

# パーソナルカラー分類モデルの作成
def create_improved_model():
    model = Sequential([
        Conv2D(32, (3, 3), activation='relu', input_shape=(224, 224, 3)),
        MaxPooling2D(pool_size=(2, 2)),
        Conv2D(64, (3, 3), activation='relu'),
        MaxPooling2D(pool_size=(2, 2)),
        Conv2D(128, (3, 3), activation='relu'),  # 新しい畳み込み層を追加
        MaxPooling2D(pool_size=(2, 2)),
        Flatten(),
        Dense(64, activation='relu'),
        Dropout(0.5),  # ドロップアウト層
        Dense(32, activation='relu'),  # 新しい全結合層を追加
        Dropout(0.6),  # ドロップアウト層
        Dense(4, activation='softmax')  # 出力層
    ])

    model.compile(optimizer=Adam(learning_rate=0.0001), loss='categorical_crossentropy', metrics=['accuracy'])
    return model

# モデルの作成(新しいモデルを使う)
model = create_improved_model()

from tensorflow.keras.preprocessing.image import ImageDataGenerator

# データ拡張の設定
datagen = ImageDataGenerator(
    rotation_range=20,  # 20度まで回転
    width_shift_range=0.1,  # 横に10%まで動かす
    height_shift_range=0.1,  # 縦に10%まで動かす
    horizontal_flip=True  # 左右反転
)

# トレーニングデータに拡張を適用
datagen.fit(x_train)

# モデルの学習
model.fit(
    x_train, y_train,
    epochs=10, 
    validation_data=(x_valid, y_valid),
    batch_size=32
)

# 学習したモデルを保存
model.save('personal_color_model_with_xy.h5')

model.summary()

# 検証データで評価
loss, accuracy = model.evaluate(x_valid, y_valid)
print(f"Validation Loss: {loss}, Validation Accuracy: {accuracy}")

モデルを作成してみました。

このモデルがちゃんと動いているか確認します。

5-1.データが正しく読み込まれているか

 print(x_train.shape) と print(y_train.shape) で、読み込んだデータの形(サイズ)や枚数を確認しました。
image.png
 こちらは、208枚の画像が 224x224 ピクセル、3チャンネル(RGBカラー)で読み込まれていると出ました。
後ほど記載しますが、Trainにはもっと多くの枚数を用意していたため、間違ったファイルを読み込んでいることがわかりました。test → trainにすることで解決しました。やっぱり確認は必要です。

5-2. モデルの学習の確認

image.png
トレーニング精度はエポックが進むにつれて、最終的に99.31%に達していて、トレーニングデータに対してほぼ完璧に学習しているようです。
ただし、検証精度(val_accuracy)は約30%前後に留まっており、学習した内容が検証データにうまく適用できていない・・・。改善を検討します。

-過学習?
トレーニングデータでの損失(loss)がどんどん減少して精度が高くなる一方で、検証データでの損失は減少せず、逆にエポック5以降は増えています。例えば、エポック5では val_loss: 1.5484 だったのが、最終的には 2.7470 に悪化しています・・。検証精度も上がらず、最終的には val_accuracy: 0.2981 に。

5-3.モデルの評価

下記コードでモデルの評価をしました。

# 検証データで評価
loss, accuracy = model.evaluate(x_valid, y_valid)
print(f"Validation Loss: {loss}, Validation Accuracy: {accuracy}")

結果は下記です。めちゃめちゃ低いです・・・・ひえー。
確率をわずかに上回るくらいです。
学習はできていますが、精度が良くないという状況です。

Validation Loss: 2.110233783721924, 
Validation Accuracy: 0.2788461446762085

5-4動作確認の結果から、過学習対策を検討

下記を試してみます。

1.データ拡張:
画像データを回転させたり、反転させたりしてデータセットを増やすことでモデルがより一般化される可能性がありそうです。

# データ拡張の設定
datagen = ImageDataGenerator(
    rotation_range=20,  # 20度まで回転
    width_shift_range=0.1,  # 横に10%まで動かす
    height_shift_range=0.1,  # 縦に10%まで動かす
    horizontal_flip=True  # 左右反転
)

結果は、、全然改善されませんでした!

2.ドロップアウト率を増やす:
ドロップアウト率は0.5→0.6にしてみました。
あまり、改善はみられません・・・

3.データ数を増やしてみる

元の枚数
image.png

trainデータを350枚程度増やしてみました。
image.png

あまり変わりませんね・・・

4.バッチサイズを変えてみる 
32→128にしたことで少し改善しました。でも、まだまだ・・・

Validation Loss: 1.8274004459381104, 
Validation Accuracy: 0.33173078298568726

5.畳み込み層、プーリング層の数を増やす
あまり改善がありません・・

5-5 モデルを変更してみる(VGG16・ResNet)

せっかくWEB講座で勉強したので、VGG16にモデルを変更してみました。
また、Test Loss, Test Accuracyも出るようにしました。

import tensorflow as tf
from tensorflow.keras.applications import VGG16
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Flatten, Dense, Dropout
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.preprocessing.image import load_img, img_to_array
import numpy as np
import os
from sklearn.model_selection import train_test_split
from tensorflow.keras.optimizers import Adam

# ラベルのマッピング
label_map = {
    "autumn": 0,
    "summer": 1,
    "spring": 2,
    "winter": 3
}

# 画像を読み込む関数
def load_images_from_folder(folder, target_size=(50, 50)):
    images = []
    labels = []
    for label_name, label_value in label_map.items():  # ラベルのマッピングを使用
        subfolder_path = os.path.join(folder, label_name)
        if os.path.isdir(subfolder_path):
            for filename in os.listdir(subfolder_path):
                img_path = os.path.join(subfolder_path, filename)
                img = load_img(img_path, target_size=target_size)
                img_array = img_to_array(img)
                images.append(img_array)
                labels.append(label_value)
    return np.array(images), np.array(labels)

# トレーニングデータと検証データのパス
train_data_dir = r'C:\Users\korom\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Python 3.12\XXXXX\data\train'
valid_data_dir = r'C:\Users\korom\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Python 3.12\XXXXX\data\valid'
test_data_dir = r'C:\Users\korom\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Python 3.12\XXXXX\data\test'

# トレーニングデータを読み込む
x_train, y_train = load_images_from_folder(train_data_dir)
x_valid, y_valid = load_images_from_folder(valid_data_dir)

# データの正規化
x_train = x_train / 255.0
x_valid = x_valid / 255.0

# ラベルをone-hotエンコーディング
y_train = to_categorical(y_train, 4)
y_valid = to_categorical(y_valid, 4)

# パーソナルカラー分類モデルの作成(VGG16を使う)
def create_vgg16_model():
    base_model = VGG16(weights='imagenet', include_top=False, input_shape=(50, 50, 3))
    base_model.trainable = False  # 事前学習済みの層は固定

    model = Sequential([
        base_model,
        Dense(64, activation='relu'),
        Dropout(0.5),
        Dense(32, activation='relu'),
        Dropout(0.6),
        Dense(4, activation='softmax')  # 4つのクラスに対応
    ])

    model.compile(optimizer=Adam(learning_rate=0.0001), loss='categorical_crossentropy', metrics=['accuracy'])
    return model

# モデルの作成
model = create_vgg16_model()

from tensorflow.keras.preprocessing.image import ImageDataGenerator

# データ拡張の設定
datagen = ImageDataGenerator(
    rotation_range=20,
    width_shift_range=0.1,
    height_shift_range=0.1,
    horizontal_flip=True
)

# トレーニングデータに拡張を適用
datagen.fit(x_train)

# モデルの学習
model.fit(
    x_train, y_train,
    epochs=10,
    validation_data=(x_valid, y_valid),
    batch_size=32
)

# 学習したモデルを保存
model.save('personal_color_model_with_vgg16.h5')

model.summary()

# 検証データで評価
loss, accuracy = model.evaluate(x_valid, y_valid)
print(f"Validation Loss: {loss}, Validation Accuracy: {accuracy}")

# テストデータでの評価
x_test, y_test = load_images_from_folder(test_data_dir)
x_test = x_test / 255.0
y_test = to_categorical(y_test, 4)
test_loss, test_accuracy = model.evaluate(x_test, y_test)
print(f"Test Loss: {test_loss}, Test Accuracy: {test_accuracy}")

# テスト用の画像を予測
test_img = load_img(r"C:\Users\korom\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Python 3.12\XXXXX\data\igawatest.jpg", target_size=(50, 50))
test_img = img_to_array(test_img) / 255.0
prediction = model.predict(np.expand_dims(test_img, axis=0))
predicted_label = np.argmax(prediction)

# ラベルの逆マッピング
inverse_label_map = {v: k for k, v in label_map.items()}
predicted_class = inverse_label_map[predicted_label]

# 予測されたクラスを表示
print(f"Predicted class: {predicted_class}")

結果は下記です。

Test Loss: 1.3876512050628662, 
Test Accuracy: 0.2740384638309479

画像サイズを224に変えてみました。

Test Loss: 1.3827760219573975, 
Test Accuracy: 0.3173076808452606

少しだけ改善されました。
しかし、実行時間は20分程度かかりました。

VGG16→ResNet50へ変更
改善はなく、少しだけ値は下がってしまいました。

ResNetへの変更は、VGGモデルの以下を変えただけです。
2行目

from tensorflow.keras.applications import VGG16, ResNet50

55行目

base_model = ResNet50(weights='imagenet', include_top=False, input_shape=(224, 224, 3))

結果は、下記です。確率論を下回っています・・・。
(サイズは224,224)

Test Loss: 1.3866652250289917, 
Test Accuracy: 0.22705313563346863

6.HTML・SCRIPT.JSなどの作成

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <title>パーソナルカラー診断</title>
</head>
<body>
    
    <div class="container">
        <header>
            <h1>パーソナルカラー診断</h1>
            <p style="font-size: 18px; font-weight: bold;">できるだけ顔を近づけて撮影してください</p>
        </header>

        <div class="video-container">
            <video id="video" autoplay></video>
            <canvas id="canvas" style="display:none;"></canvas>
        </div>

        <div href="" class="btn btn--yellow btn--circle" id="capture">PUSH!</div>


        <div id="result">
            <!-- 結果がここに表示されます -->
        </div>

        <img id="styleImage" src="" alt="おすすめのスタイル" style="display:none;">
        <button onclick="window.location.href='/'" class="btn-gradient-radius">戻る</button>
    </div>

    <script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>



html {
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    font-size: 62.5%;
  }


body {
    font-family: Arial, sans-serif;
    background-color: #f0f0f0;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    
}

.container {
    background: #ffffff;
    border-radius: 20px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    padding: 20px;
    text-align: center;
    width: 500px;
    max-width: 800px;
}

header {
    margin-bottom: 20px;
}

h1 {
    padding: 1rem 2rem;
  color: #fff;
  background-image: -webkit-gradient(linear, left top, right top, from(#fa709a), to(#fee140));
  background-image: -webkit-linear-gradient(left, #fa709a 0%, #fee140 100%);
  background-image: linear-gradient(to right, #fa709a 0%, #fee140 100%);
}

.video-container {
    margin-bottom: 20px;
}

video {
    border: 2px solid #ddd;
    border-radius: 8px;
    width: 100%;
}

/* ボタンのスタイル設定 */
.btn,
a.btn,
button.btn {
  font-size: 1.6rem;
  font-weight: 700;
  line-height: 1.5;
  position: relative;
  display: inline-block;
  padding: 1rem 4rem;
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-transition: all 0.3s;
  transition: all 0.3s;
  text-align: center;
  vertical-align: middle;
  text-decoration: none;
  letter-spacing: 0.1em;
  color: #212529;
  border-radius: 0.5rem;
}

/* 円形ボタンのスタイル設定 */
a.btn--circle {
  border-radius: 50%;
  line-height: 100px;
  width: 100px;
  height: 100px;
  padding: 0;
  -webkit-box-shadow: 0 5px 0 #e6d900;
  box-shadow: 0 5px 0 #e6d900;
}

a.btn--circle:hover {
  -webkit-transform: translate(0, 3px);
  transform: translate(0, 3px);
  -webkit-box-shadow: 0 2px 0 #e6d900;
  box-shadow: 0 2px 0 #e6d900;
}

#result {
    margin-top: 20px;
    font-size: 18px;
    color: #333;
}

.btn-gradient-radius {
  display: inline-block;
  padding: 7px 20px;
  border-radius: 25px;
  text-decoration: none;
  color: #FFF;
  background-image: linear-gradient(45deg, #FFC107 0%, #ff8b5f 100%);
  transition: .4s;
}

.btn-gradient-radius:hover {
  background-image: linear-gradient(45deg, #FFC107 0%, #f76a35 100%);
}
document.addEventListener('DOMContentLoaded', () => {
  const video = document.getElementById('video');
  const canvas = document.getElementById('canvas');
  const result = document.getElementById('result');
  const captureButton = document.getElementById('capture');

  // Webカメラを起動
  navigator.mediaDevices.getUserMedia({ video: true })
    .then(stream => {
      video.srcObject = stream;
    })
    .catch(err => {
      console.error('カメラを起動できませんでした:', err);
    });

  // 画像を選択するための関数を追加
  function getStyleImage(type) {
    const images = {
      '春タイプ': '/static/images/spring_style.jpg',
      '夏タイプ': '/static/images/summer_style.jpg',
      '秋タイプ': '/static/images/autumn_style.jpg',
      '冬タイプ': '/static/images/winter_style.jpg'
    };
    return images[type];
  }

  // ボタンを押したらカメラ画像をキャプチャ
  captureButton.addEventListener('click', () => {
    const context = canvas.getContext('2d');
    context.drawImage(video, 0, 0, canvas.width, canvas.height);

    // 画像データをBase64形式に変換
    const imageData = canvas.toDataURL('image/png');

    // 画像をサーバーにPOSTリクエストで送信
    fetch('/capture', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ image: imageData })
    })
    .then(response => response.json())
    .then(data => {
      document.getElementById('result').innerHTML = `
        色の傾向: ${data.base}<br>
        おすすめのカラー: ${data.colors.join(', ')}<br>
        解説: ${data.description}
    
      `;

      // スタイル画像をセット
      const styleImage = document.getElementById('styleImage');
      styleImage.src = getStyleImage(data.base);
      styleImage.style.display = 'block';
    })
    .then(data => {
      console.log('サーバーからのレスポンス:', data); // デバッグ用ログ
  
      // エラーハンドリングを追加
      if (!data.colors || !Array.isArray(data.colors)) {
          throw new Error('カラー情報が無効です');
      }
  
      document.getElementById('result').innerHTML = `
        色の傾向: ${data.base}<br>
        おすすめのカラー: ${data.colors.join(', ')}<br>
        解説: ${data.description}
      `;
  
      // スタイル画像をセット
      const styleImage = document.getElementById('styleImage');
      styleImage.src = getStyleImage(data.base);
      styleImage.style.display = 'block';
    })
    .then(data => {
      console.log('サーバーからのレスポンス:', data); // デバッグ用ログ
  
      // エラーハンドリングを追加
      if (!data.colors || !Array.isArray(data.colors)) {
          throw new Error('カラー情報が無効です');
      }
  
      document.getElementById('result').innerHTML = `
        色の傾向: ${data.base}<br>
        おすすめのカラー: ${data.colors.join(', ')}<br>
        解説: ${data.description}
      `;
  
      // スタイル画像をセット
      const styleImage = document.getElementById('styleImage');
      styleImage.src = getStyleImage(data.base);
      styleImage.style.display = 'block';
  })
  
    .catch(err => {
      console.error('画像送信中にエラーが発生しました:', err);
    });
  });
});


7.結果と考察

いろいろ試してみましたが、これ以上精度を上げることは無理でした。
最終的な結果は下記です。

モデル➀ ※バッチサイズ128バージョン

Validation Loss: 1.8274004459381104, 
Validation Accuracy: 0.33173078298568726

モデル➁VGG16 ※画像サイズ224バージョン(20分ほど実行に時間がかかった)

Test Loss: 1.3827760219573975, 
Test Accuracy: 0.3173076808452606

モデル➂ResNet ※画像サイズ224バージョン(10分ほど実行に時間がかかった)

Test Loss: 1.3866652250289917, 
Test Accuracy: 0.22705313563346863

考えられる原因
 1.そもそものデータが正確ではない?
 (メイクや、ライト、撮影環境によって、顔色が変わっているかもしれない)
 2.顔色の判断という少しあいまいな領域のため、絶対にこれという値が出しにくい?
 3.データ数が少ない?
 (データ拡張は行ったが、今回少し顔の位置をずらしたり回転させただけだったので、
 もっと「色」にフォーカスをした拡張が必要だったかもしれない。例えば顔以外を黒にするなど。)
 4.顔部分以外の洋服の色が作用している?
 (顔だけ切り取るなどの加工が必要だったかもしれない)

8.感想

実際、受講してみてテキストベースで習っている間は学習がなかなか進みませんでしたが、
自分で成果物を作成するようになってから、興味が出て、また理解もほんの少しですが進んだような気がします。
成果物作成では、いろいろなモデルを試して、少しずつコードを変えながら行ったため、
最初のコードがなんだったっけ?とわからなくなることがありました。
その辺、履歴をきちんと追いながらコーディングしないといけないなと思いました。

最後に、とんちんかんな質問をする私に粘り強く、チューターの方が寄り添って教えていただけたことに感謝します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?