LoginSignup
2
2

More than 1 year has passed since last update.

"ワンピースのキャラクター"を判別するアプリを作ってみた

Posted at

はじめに

AidemyのWebアプリコースを受講し、その最終成果物の作成についてを記事にしました。
まず、どのようなアプリを作るか悩みましたが、単純に自分の好きなものとして、漫画が好きだったということで「ワンピース」のキャラクターを判別するアプリを作ることにしました。

目次

1. 開発環境
2. 画像収集
3. 学習データとテストデータの作成
4. 学習モデルの作成と学習
5. Webアプリ画面作成
6. アプリのデプロイと動作確認
7. 考察・感想
8. 参考文献

1. 開発環境

Windows10 Home
python 3.8.8
Google Colaboratory
Visual Studio Code (1.58.0)

2. 画像収集

まず、機械学習を行うにあたっての画像収集です。
今回のアプリでは、主要キャラクター「麦わらの一味」のメンバー9人(ルフィ・ゾロ・ナミ・ウソップ・サンジ・チョッパー・ロビン・フランキー・ブルック)の画像を集めました。

どの方法で画像収集しようかと考えましたが、手早く集めることができるgoogle-images-downloadを利用して、各200枚ほどの画像を集めました。
(参照)
Python|Google_Images_Downloadを使って特定キーワードで100枚以上の画像を一括入手する方法

画像のダウンロード
from google_images_download import google_images_download

response = google_images_download.googleimagesdownload()

arguments = {"keywords":"",#検索キーワードを入力
            "limit":300,#枚数
            "chromedriver":"C:\\Program Files (x86)\\chromedriver_win32\\chromedriver.exe",
            "format":"jpg"
            }

response.download(arguments)

まず、各キャラクター名で画像をダウンロードしました。1つのキーワードで300枚ほど画像を取得できたのですが、同じ画像が複数枚あったり、わかりにくい画像や関係ない画像などを除外すると100枚程度になってしまったため、いくつかキーワードで検索をかけて画像数を増やしました。

次に、画像の加工を行います。
下記のサイトを参考に顔検出でのトリミングを行いました。
OpenCVで顔認識・切り出し
OpenCVでアニメの顔検出

顔認識とトリミング
import os
import glob
import subprocess
import numpy as np
from PIL import Image
import cv2 as cv2

TITLE = '' #画像トリミングしたい画像の入ったフォルダ名
dir = './downloads/' + TITLE + '/' 
dir1 = './downloads/加工済み/' #保存先フォルダ名

files = glob.glob(dir + '*.jpg')

def pil2cv(image):
    ''' PIL型 -> OpenCV型 '''
    new_image = np.array(image, dtype=np.uint8)
    if new_image.ndim == 2:  # モノクロ
        pass
    elif new_image.shape[2] == 3:  # カラー
        new_image = cv2.cvtColor(new_image, cv2.COLOR_RGB2BGR)
    elif new_image.shape[2] == 4:  # 透過
        new_image = cv2.cvtColor(new_image, cv2.COLOR_RGBA2BGRA)
    return new_image
def cv2pil(image):
    ''' OpenCV型 -> PIL型 '''
    new_image = image.copy()
    if new_image.ndim == 2:  # モノクロ
        pass
    elif new_image.shape[2] == 3:  # カラー
        new_image = cv2.cvtColor(new_image, cv2.COLOR_BGR2RGB)
    elif new_image.shape[2] == 4:  # 透過
        new_image = cv2.cvtColor(new_image, cv2.COLOR_BGRA2RGBA)
    new_image = Image.fromarray(new_image)
    return new_image

#漫画用顔認識
cascade_file = "lbpcascade_animeface.xml"
cascade_face = cv2.CascadeClassifier(cascade_file)

for i,file in enumerate(files):
    try:
        print(file)
        image = Image.open(file)
        image = pil2cv(image)
        face_list = cascade_face.detectMultiScale(image,minSize=(20,20))

        for j, (x,y,w,h) in enumerate(face_list):
            trim = image[y: y+h, x:x+w]
            trim = cv2pil(trim)
            trim.save(dir1 + TITLE + str(i) + '_' + str(j+1) + '.jpg')
    except:
        pass

トリミングはできたのですが、一部のキャラクターしか顔を認識しておらず、得られる画像数は多くありませんでした。
ですので、今回はほとんどの画像を自分でトリミングすることになりました。

3. 学習データとテストデータの作成

機械学習を行うにあたって、Google Colaboratoryの環境で学習を行うため、集めた画像をGoogle Driveから使用できるようにします。
まず、フォルダごとまとめてアップロードできるように、画像データを保存したフォルダをzipファイルに圧縮し、GoogleDriveにアップロードします。
次にGoogle Colaboratory上で画像をアップロードし、zipファイルを解凍します。

GoogleDriveからのアップロード
from google.colab import drive

drive.mount('/content/drive')

!cp /content/drive/My\ Drive/Charactor_Data.zip .
!unzip Charactor_Data.zip

続いて、画像ファイルをデータとして読み出し、学習用データとテストデータの作成とラベル付けを行います。
画像の処理:
 縦長・横長の画像があるため、サイズ変更の際の画像の伸びを防ぐため、画像に余白を追加して正方形の画像に加工する処理を追加
 画像サイズを200×200に指定

画像の読み込みと学習データ・ラベルデータの作成
#キャラクターのラベル
charactor = ['Luffy','Zoro','Nami','Usop','Sanji','Chopper','Robin','Franky','Brook']

#画像のPathを作成する関数
def make_path(name):
  path_name = os.listdir('./Charactor_Data/{}/'.format(name))
  return path_name

# 画像ファイルの一時保管用リスト
pre_list = [[] for i in range(9)]

#使用する画像のサイズ
SIZE = 200

#画像に余白を追加して、正方形の画像を返す関数
def make_square(img):
  height, width = img.shape[:2]
  #余白の色(黒)
  margin_color = [0,0,0]
  if width == height:
    return img
  elif width > height:
    #余白の幅(追加して画像が正方形になるように調整)
    margin = int((width - height) / 2)
    #余白をつける処理(画像,Top,Bottom,left,Right幅,余白のつけかた)
    img_square = cv2.copyMakeBorder(img,margin,margin,0,0,cv2.BORDER_CONSTANT)
    return img_square
  elif width < height:
    margin = int((height - width) / 2)
    img_square = cv2.copyMakeBorder(img,0,0,margin,margin,cv2.BORDER_CONSTANT)
    return img_square

#画像を読み込み、一時保管リストに追加する
for n,name in zip(range(9),charactor):
    path = make_path(name)
    for i in range(len(path)):
      #pathが".ipynb_checkpoints"の場合スキップ(画像読み取りのエラーになるため)
      if path[i] == ".ipynb_checkpoints":
          continue
      img = cv2.imread('./Charactor_Data/{}/'.format(name) + path[i])
      img = make_square(img)
      img = cv2.resize(img, (SIZE,SIZE))
      pre_list[n].append(img)

img_list = []
label_list = []
for i in range(9):
    img_list += pre_list[i]
    label_list += [i]*len(pre_list[i])

X = np.array(img_list)
y = np.array(label_list)

#データの配列をランダムに入れ替える
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

# データの分割
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)
y_test = to_categorical(y_test)

4. 学習モデルの作成と学習

Kerasを使って学習モデルを作成していきます。
各キャラクターの画像数が200ほどしか集められず、少ない量での学習になったため、VGG16にて転移学習を行いました。
(ResNetInception v3での転移学習も検討しましたが、VGG16が1番精度が良かったです。

CNNモデル作成
input_tensor = Input(shape=(SIZE,SIZE,3))
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(512,activation="relu"))
top_model.add(Dense(512,activation="relu"))
top_model.add(BatchNormalization())
top_model.add(Dense(9,activation="softmax"))

model = Model(inputs=vgg16.input,outputs=top_model(vgg16.output))

for layer in model.layers[:15]:
  layer.trainable = False

model.summary()

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

history = model.fit(X_train,y_train,batch_size=16,epochs=100,validation_data=(X_test, y_test))

#学習モデルの保存とダウンロード
model.save('model.h5')
files.download('./model.h5') 
結果の可視化
#学習指標の表示
loss=history.history['loss']
acc =history.history['accuracy']
val_loss=history.history['val_loss']
val_acc=history.history['val_accuracy']
epochs=len(loss)

plt.plot(range(epochs), loss, marker = '.', label = 'loss')
plt.plot(range(epochs), val_loss, marker = '.', label = 'val_loss')
plt.legend(loc = 'best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()

plt.plot(range(epochs), acc, marker = '.', label = 'accracy')
plt.plot(range(epochs), val_acc, marker = '.', label = 'val_accracy')
plt.legend(loc = 'best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('accracy')
plt.show()

#混同行列の計算を行う関数
from sklearn.metrics import classification_report,confusion_matrix
import seaborn as sns

def Confusion(imgs,y_true):
  def pred(img):
    img = cv2.resize(img,(SIZE,SIZE))
    pred = np.argmax(model.predict(img.reshape(-1,SIZE,SIZE,3)))
    return pred

  y_pred = []
  for x in imgs:
    y_pred.append(pred(x))

  y_true = np.argmax(y_true, axis=1)
  conf = classification_report(y_true, y_pred,target_names=charactor,output_dict=True)
  cm   = confusion_matrix(y_pred,y_true)
  return conf,cm

conf_dic,cm = Confusion(X_test,y_test)

#F値
F_vals = []
for name in charactor:
  F = conf_dic[name]['f1-score']
  F_vals.append(F)

F_dict = dict(zip(charactor,F_vals))
F_series = pd.Series(F_dict)
#可視化する
F_series.plot.barh()
plt.xlim([0.2,1.0])
plt.xlabel('F1-Score')
plt.show()

#ヒートマップで混同行列を可視化する
cm = pd.DataFrame(data=cm,index=charactor,columns=charactor)
sns.heatmap(cm,annot=True,square=True,cmap="Blues")
plt.show()

loss.pngaccracy.png
F1_score.png
・混同行列
混同行列2.png

精度としては、85%ほどでした。
できれば90%を超えられたら嬉しかったのですが、画像の数も少なかったのでこの精度でもよく伸びてくれたほうかなと思います。
F値については多少キャラによっては低いものもありますが、それでも0.8近くは出ていました。
混同行列を見ても、完全に判別できているわけではないですが、しっかりと見分けることができているように見えます。
まだまだ、精度を伸ばすために検討の余地はあるとは思いますが、今回はこの状態で進めたいと思います。

5. Webアプリ画面作成

学習モデルの作成が終わったので、次はWebアプリの作成に進みます。
まずは、HTML,CSSでアプリの画面を作っていきます。
Aidemyの学習講座で練習課題として作成した数字認識アプリをベースとして、少しだけ書き換えました。もう少しアレンジ出来たらよかったですが、今回は時間も限られていましたので、シンプルに少しだけの書き換えにしました。HTMLCSSについては、後々学んでいきたいと思います。

HTMLコード
<!DOCTYPE html>
<html lang='ja'>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content="device-width, initial-scale=1.0">
    <meta http-equiv='X-UA-Compatible' content="ie=edge">
    <title>ONEPICEキャラ判別</title>
    <link rel='stylesheet' href="./static/stylesheet.css">
</head>
<body>
    <header>
        <a class='header-logo' href="#">ONEPICE キャラ判別</a>
    </header>

    <div class='main'>    
        <h2> 麦わらの一味を識別します</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'>
            <p class='answer_img'><img src="{{ path }}" style="margin-top: 15px; vertical-align: bottom; width: 200px;"></p>
            <p class='answer_text'>{{answer}}</p>
        </div>
    </div>

    <footer>
    </footer>
</body>
</html>
CSSコード
header {
    background-image:url(./img/Old-map.jpg);
    background-size: cover;
    width: 100%;
    height: 120px;
    margin: -8px;
    display: flex;
    position: fixed; 
    text-align: center;
}

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

.main {
    padding: 120px;
}

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

p {
    color: #444444;
    margin: 50px 0px 30px 0px;
    text-align: center;
}
.answer {
    flex-direction: column;
    text-align: center;
    margin: 20px 0px;
}
.answer_text {
    color: #444444;
    font-size: 40px;
    font: bold;
    margin: 20px 0px 30px 0px;
}

form {
    text-align: center;
}

footer {
    background-image:url(./img/Old-map.jpg);
    position: absolute;
    background-size: cover;
    height: 80px;
    bottom: 0px;
    margin:-8px;
    width: 100%;
    position: fixed; 
    text-align: center;
}

下のようなページを表示することができました。
topPage.png

画面ができたので、次はFraskコード作成をしていきます。これもHTML,CSS同様Aidemyの学習課題をベースにして作り替えてました。
今回のアプリでは、判定したキャラクターの画像(各キャラクターごとに決まった画像)とキャラクターの名前を返すようにしました。

Frask
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

charactor = ['MONKY・D・LUFFY','RORONOA ZORO','NAMI','USOP','SANJI','CHOPPER','NICO ROBIN','FRANKY','BROOK']
char = ['Luffy','Zoro','Nami','Usop','Sanji','Chopper','Robin','Franky','Brook']
SIZE = 200

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('./model.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)

            img = image.load_img(filepath,target_size=(SIZE,SIZE))
            img = image.img_to_array(img)
            data = np.array([img])
            result = model.predict(data)[0]
            predicted = result.argmax()
            pred_answer = charactor[predicted]
            path = "static/img/" + char[predicted] + ".jpg"

            return render_template('index.html',path=path,answer=pred_answer)
    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)

6. アプリのデプロイと動作確認

アプリのデプロイを行います。
ここで少し詰まった内容ですが、デプロイの際に下記のようなエラーが出てしまいました。

Compiled slug size: 725.5M is too large (max is 500M)

これについては、下のサイトを参考に解決しました。
Herokuでデプロイする際に出たSlug Sizeエラーの解決方法
原因はSlugsizeが最大の500Mに収まっていないというエラーでした。
tensorFlowのサイズが大きすぎるようで、バージョンを下げることで500M未満に抑えることができ、無事デプロイ完了することができました。

最後にいくつかの画像を使って、動作を確認したいと思います。
F値の低かったルフィとウソップの2つで実行してみたいと思います。
まずはルフィの画像で実行します。
使用した画像はこちらです。

実行結果はこうなりました ↓
結果①.jpg
正しい結果が表示され、それに応じた画像もちゃんと表示されています。

次はウソップの画像で実行します。
使用した画像はこちらです。

実行結果はこうなりました ↓
結果②.jpg
こちらもうまく判定してくれました。

今回結果として載せたのは2例でしたが、ほかのキャラクターでも試し、一部誤判定のものもありましたが、たいていの画像は正しく判定してくれていました。

作成したアプリ ↓ 
ONEPICE キャラ判別
興味があれば、見てみてください!

7. 考察・感想

画像としてはカラー(アニメ画像を含む)とモノクロ画像を混合した学習データだったため、キャラクターごとのわかりやすい髪の色も精度がよくなった要因かと思ったのですが、漫画のモノクロ画像でもしっかり判別してくれていたため、うまくキャラクターの特徴をとらえてくれていたのだと思います。

精度85%ということで、少ない画像の割にはいい精度だったように思います。
精度を上げようと水増し処理を試してはみたものの、漫画のキャラクターであまり複雑ではないために、似た画像が多くなって過学習になったのか精度が大幅に落ちてしまいました。(単純に画像のが悪かったのが影響したのかもしれませんが…)
ということで、今回は水増しなしにしました。
精度を上げようと思った場合、画像の質を上げて数を増やす必要がありそうです。

今回アプリづくりで一番苦労したことはやっぱり画像集めでした。同じ画像が何枚もかぶって取得されて思った以上に画像が集まらなかったことや、学習用に画像をトリミングするのに思った以上に時間がかかり大変でした。また、初めてのアプリを作成は、エラーが出ては調べてを繰り返しで思うように作れなかったりと大変ではありましたが、自分で作るというのはとても勉強になりました。そして、やっぱり作るのは楽しいです。
これからも、精度の上げ方の検討や今回使った以外の画像取得方法など、もっといろいろなことを勉強していきたいと思います。

最後まで読んでいただきありがとうございました。
コードの書き方や精度の上げ方など、アドバイスがあればいただければ幸いです。

8. 参考文献

今回参考にさせていただいたサイト(本文未掲載分)を載せさせていただきます。
ありがとうございました。
"春に咲く花の画像" を判別するAIアプリを作ってみた
OpenCVで画像に余白を追加
Python】Pillow ↔ OpenCV 変換
Python, pandas, seabornでヒートマップを作成

2
2
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
2
2