Help us understand the problem. What is going on with this article?

ディズニーのキャラクターを分類してみた

More than 1 year has passed since last update.

Aidemy Premium Planで画像認識を学び、
成果物として、ディズニーの4キャラクターを区別してみました。

目次

・1.FlickrAPIを用いて画像取得
・2.画像の選別
・3.水増し
・4.モデルを定義し、学習
・5.テスト
・6.感想

1.FlickrAPIを用いて画像取得

FrickrAPIの登録方法

こちらを参考にしました。
http://ykubot.com/2017/11/05/flickr-api/

FlickrAPIを扱うPython用のライブラリをインストール

GoogleCustomSerchAPIを使用し、画像を集めたのですが、課金なしでは1日あたり100件までしか集めれなかったため大量に画像を取得できるFlickrAPIを使用しました。

!pip install flickrapi

データ構造を出力

レスポンスに pages="〇〇" total="△△" のように総ページ数とトータル件数が返ってきますので、per_page の件数以上の結果がある場合は、ページ番号(page)を書き換えて、リクエストを繰り返して取得する必要があります。

import os, time
from flickrapi import FlickrAPI
from pprint import pprint

# FlickrのAPIキー
public_key = ""
secret_key = ""

search_word = "検索ワード"
# ダウンロード枚数
img_num = 500
# 保存するディレクトリ(存在しない場合は自動で作成される)
img_dir = "./disney/"

# flickerAPIにアクセスするオブジェクトを生成
flicker = FlickrAPI(public_key, secret_key, format = 'parsed-json')

# フォルダの作成
try:
    os.makedirs(img_dir)
except FileExistsError:
    pass

res = flicker.photos.search(
    # 検索時の検索ワードを指定
    text = search_word,
    # データの件数を指定(500枚以下しか取得できない)
    per_page = img_num,
    # ページ数を指定(1と指定すると検索結果の1~500, 2と指定すると501~1000枚目を取得)
    page = 4,
    # メディアを指定(画像)
    media = 'photos',
    # 並びを指定(関連順)
    sort = 'relevance',
    # 有害コンテンツの設定(除外)
    safe_search = 1,
    # 返却値に取得したいオプション値を指定(画像のurl、ライセンス情報)
    extras = 'url_q, lincence'
)

# pprint(pretty printer)でデータ構造を見やすい形で出力
pprint(res)

画像のURLのみを表示

'url_q'で画像のURLが返ってくるので出力するようにします。
もし、返ってこなければエラーが発生してしまうのでスルーします。

import time
# photos内のデータをimagesに格納
images = res['photos']
urls = []

# images(photos)の中にあるphpto内のデータをimageに順に格納
for image in images['photo']:
    # image(phpto)の中にあるurl_q(画像のURL)をurlに格納
    try:
        urls.append(image['url_q'])
        print(urls)
        time.sleep(0.01)
    # エラーが出た場合は、スルーしてfor文を続ける
    except KeyError:
        continue

ファイルをダウンロード

1秒間に10mb以上のファイルを表示することはできないそうです。
なのでtime.sleepを使用し、出力後に時間を空けます。

from urllib.request import urlretrieve

image_idx = 0

for url in urls:

    # ファイルパスを指定
    # 0001のように桁数を4桁にする
    filepath = 'disney/Mickey' + str("{0:04d}".format(image_idx)) + '.jpg'

    # ファイルパスが重複していたらスキップする
    if os.path.exists(filepath):
        continue

    # ダウンロード
    urlretrieve(url, filepath)
    print("Download:{}:{}".format(search_word, url))
    # 0.01秒待機
    time.sleep(0.01)
    image_idx += 1

print('end')

2.画像の選別

スクレイピングによって少ないキャラクターでは900枚、多いキャラクターでは3000枚以上の画像を取得しました。
opencvのカスケードを使用することによって画像から人の顔部分を取り出すことは可能なのですが、ディズニーのキャラクターの顔を取り出すことはできなかったので、自分の目で選び、学習に使う画像を選びました。
画像をGoogleColaboratory上でまとめて見ることができないので、一度マイドライブに保存して、画像を選びます。
また、GoogleColaboratoryのランタイムがリセットされるとGoogleColaboratoryのファイルデータが消えてしまうのでマイドライブに保存した方が良いと思います。

マイドライブとGoogleColaboratoryを接続する

GoogleColaboratoryでマイドライブのデータを扱えるようにします。

from google.colab import drive
drive.mount('/content/drive')

マイドライブに保存

import numpy as np
nums = np.arange(2000)

for num in nums:
  saving_filename = 'Mickey' + str("{0:04d}".format(num)) + '.jpg'

  file_metadata = {
      'name': saving_filename,
      'mimeType': 'application/octet-stream'
  }
  media = googleapiclient.http.MediaFileUpload(saving_filename, 
                                               mimetype='application/octet-stream', 
                                               resumable=True)
  created = drive_service.files().create(body=file_metadata,
                                         media_body=media, 
                                         fields='id').execute()
print('end')

3.水増し

学習に使う画像を選別した結果1キャラクターあたり100枚まで減ってしまったので、画像を水増しします。
ディレクトリの値はmickey、donald、goofy、plutoと自分の手で変更しました。

from PIL import Image
import numpy as np
import cv2

nums = np.arange(2500)
for num in nums:
  try:
    im = np.array(Image.open('drive/My Drive/disney Mickey' + str("{0:04d}".format(num)) + '.jpg'))

    # 左右反転
    def horizontal_flip(image):
        image = image[:, ::-1, :]
        return image

    l = horizontal_flip(im)
    pil_img = Image.fromarray(l)
    pil_img.save('drive/My Drive/mickeyA' + str("{0:04d}".format(num)) + '.jpg')

    # 上下逆さま
    def vertical_flip(image):
      image = image[::-1, : , :]
      return image

    m = vertical_flip(im)
    pil_img = Image.fromarray(m)
    pil_img.save('drive/My Drive/mickeyB' + str("{0:04d}".format(num)) + '.jpg')

    # グレー
    def gray(image):
      image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
      return image

    n = gray(im)
    pil_img = Image.fromarray(n)
    pil_img.save('drive/My Drive/mickeyC' + str("{0:04d}".format(num)) + '.jpg')

    # 輝度値のベースを50上げる
    def right(image):
      image = im + 50 
      return image

    o = right(im)
    pil_img = Image.fromarray(o)
    pil_img.save('drive/My Drive/mickeyD' + str("{0:04d}".format(num)) + '.jpg')

    # ぼかし
    def blur(image):
      image = cv2.GaussianBlur(image, (5, 5), 0)
      return image

    p = blur(im)
    pil_img = Image.fromarray(p)
    pil_img.save('drive/My Drive/mickeyE' + str("{0:04d}".format(num)) + '.jpg')  

    # 元の画像と同じ画像
    def keep(image):
      image = image
      return image

    q = keep(im)
    pil_img = Image.fromarray(q)
    pil_img.save('drive/My Drive/mickeyF' + str("{0:04d}".format(num)) + '.jpg')

  except FileNotFoundError:
    continue

4.モデルを定義し、学習

ファイル名を変更する

ファイル名の数字が飛ばし飛ばしのため番号を揃えます。
次の工程で画像のファイルデータがNoneとなると処理ができないためです。

import os
import glob

chas = ['/mickey', '/donald', '/goofy', '/pluto']
path = 'drive/My Drive'

for cha in chas:
  pathes = path + cha
  files = glob.glob(pathes +'*.jpg')
  for i, f in enumerate(files):
    os.rename(f, os.path.join(pathes, '{}'.format(i) + ".jpg"))

モデルを定義し、学習

テストデータの精度は90%超えを出すことができました。

モデルは以下の通りです。

  • インスタンスの作成
  • 平坦層1
  • 全結合層1(relu関数)
  • 正規化
  • 全結合層2(relu関数)
  • ドロップアウト(入力ユニットをドロップする割合(0.5))
  • 正規化
  • 全結合層3(softmax関数 出力4)
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from keras.utils.np_utils import to_categorical
from keras.layers import Dense, Dropout, Flatten, Input
from keras.applications.vgg16 import VGG16
from keras.models import Model, Sequential
from keras import optimizers
from keras.preprocessing.image import ImageDataGenerator
import keras.callbacks

nums = np.arange(600)

img_Mickey = []
img_Donald = []
img_Goofy = []
img_Pluto = []

for num in nums:
  img = cv2.imread('drive/My Drive/mickey/' + str(num) + '.jpg')
  img = cv2.resize(img, (70,70))
  img_Mickey.append(img)

for num in nums:
  img = cv2.imread('drive/My Drive/donald/'+ str(num) + '.jpg')
  img = cv2.resize(img, (70,70))
  img_Donald.append(img)

for num in nums:
  img = cv2.imread('drive/My Drive/goofy/' + str(num) + '.jpg')
  img = cv2.resize(img, (70,70))
  img_Goofy.append(img)

for num in nums:
  img = cv2.imread('drive/My Drive/pluto/' + str(num) + '.jpg')
  img = cv2.resize(img, (70,70))
  img_Pluto.append(img)

X = np.array(img_Mickey + img_Donald + img_Goofy + img_Pluto)
y = np.array([0]*len(img_Mickey) + [1]*len(img_Donald) + [2]*len(img_Goofy) + [3]*len(img_Pluto))

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):]

# 正解ラベルをOne-hot形式に変えます
# 4クラスに分類する場合はone-hot表現にクラス数4を指定する必要があります
y_train = to_categorical(y_train, 4)
y_test = to_categorical(y_test, 4)

# vggをモデルに使用
# include_top は、もとのモデルの最後の全結合層の部分を用いるかどうか。元のモデルの畳み込み層による特徴抽出部分のみを用います
# weights は imagenet を指定すると、ImageNetで学習した重みを用い、None とするとランダムな重みを用います
input_tensor = Input(shape=(70, 70, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

model = Sequential()
model.add(Flatten(input_shape=vgg16.output_shape[1:]))
model.add(Dense(256, activation='relu'))# 活性化関数にreluを用います(抽出された特徴をより強調する働きがある)
model.add(BatchNormalization())# 正規化します
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))# 過学習を防ぐためにドロップアウト層を入れます
model.add(BatchNormalization())
model.add(Dense(4, activation='softmax'))# 4つに分類したいため、(Dense(4, ...)とします

# vggと、modelを連結
model = Model(input=vgg16.input, output=model(vgg16.output))

# vgg16による特徴抽出部分の重みは更新されると崩れてしまうので固定
for layer in model.layers[:15]:
    layer.trainable=False

# コンパイル
# 多クラス分類の場合は、損失関数に categorical_crossentropy を指定します
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=30, epochs=40, verbose=1, validation_data=(X_test, y_test))
# 汎化制度の評価・表示
score = model.evaluate(X_test, y_test, batch_size=30, verbose=0)
print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))
#accとval_accのプロット
plt.plot(history.history["acc"], label="acc", ls="-", marker="o")
plt.plot(history.history["val_acc"], label="val_acc", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()

loss とは、正解とどれくらい離れているかという数値であり、0に近づくほど正解に近いです。
acc とは、正解率のことで、10(100%)に近いほど正解に近いです。
val_loss と val_accはテストデータでの値です。
スクリーンショット 2018-12-21 21.35.24.png

5.テスト

画像を受け取り、判定する関数を定義

def character(img):
    #予測したい画像データ(img)をopenCVを用いて(70,70)にリサイズ
    img = cv2.resize(img, (70, 70))
    pred = np.argmax(model.predict(np.array([img]), verbose=0))
    if pred == 0:
      return 'ミッキー'
    elif pred == 1:
      return 'ドナルド'
    elif pred == 2:
      return 'グーフィー'
    else:
      return 'プルート'

予測の実行

path = "予測したい画像のパスを入力"
img = cv2.imread(path)
b,g,r = cv2.split(img) 
img = cv2.merge([r,g,b])
plt.imshow(img)
plt.show()
print(character(img))

オリジナルのデータで予測した際も20枚中17枚正しい結果を出せました。
スクリーンショット 2018-12-18 14.15.37.png

6.感想

精度90%超えを出せたのは、学習で使用する画像データを水増し前で1キャラクター100枚集められたことと、4キャラクターの特徴量が多いことがあげられると思います。

今回大変だったことは、1万枚近くの画像を集め自分で処理したことでした。

たくさん失敗したことにより、なぜこの関数を使っているかなど、自分で考え解決する力がついたと思います。

1ヶ月前のプログラミング初心者の自分では成果物を作りきることは難しかったと思います。
ですが、Aidemyのチューターさんのご指導のもと楽しく最後まで作ることができました。本当に1ヶ月間ありがとうございました!
今後は、この経験を生かしてもっとユーモアがあるものにも挑戦していきたいです。

参考にしたサイト

Flickr APIを使って画像ファイルをダウンロードする
ColaboratoryからGoogle Driveのファイルを読み書きする
機械学習で乃木坂46を顏分類してみた

kazama0119
21歳大学生です。 Twitterで自分が初学者の時に知りたかった事を中心に発信しているので、是非みていってください。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away