サンリオキャラクターを画像認識するAIを作りました。
ImageDownloader
Chromeの拡張機能でWebサイトの画像をダウンロードできる
(公式)からchromeに追加をクリックして入手
分類するキャラクター
無料の画像サイトから拝借しました。
バツ丸
シナモロール
ケロッピー
ハローキティ
リトルツインスターズ
マイメロディ
ポムポムプリン
タキシードサム
画像水増し
ダウンロードした画像だけでは少ないので、水増しする
kerasを使っても、cv2を使ってもどちらでも可
import keras
import numpy as np
import os
from keras.utils import np_utils
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
path = "フォルダパス"
files = os.listdir(path)
count = 0
file_count = len(files) + 1
print(len(files))
for file in files:
img = image.load_img(path + "/" + file)
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
datagen = ImageDataGenerator(
rotation_range = 180,
width_shift_range= 0.2,
height_shift_range = 0.3,
zoom_range = [0.8, 1.2],
channel_shift_range = 30
)
for d in datagen.flow(x, batch_size=1):
new_img = image.array_to_img(d[0], scale=True)
image.save_img(path + "/" + str(file_count) + ".jpg", new_img)
file_count += 1
count += 1
if count == 8:
count = 0
break
print("finish")
import os
import numpy as np
import cv2
file_path = "フォルダパス"
def save_img(img, n, path=file_path):
path = os.getcwd()
cv2.imwrite(path + "/" + str(n) + ".jpg", img)
def scratch_image(img, warp=True, flip_y=True, flip_x=True, gray=True, thr=True, filt=True, resize=True, erode=True):
methods = [warp, flip_y, flip_x, gray, thr, filt, resize, erode]
img_size = img.shape
filter1 = np.ones(3,3)
mat = cv2.getRotationMatrix2D(tuple(np.array([img.shape[1], img.shape[0]] / 2), 90, 1.0))
scratch = np.array([
lambda x: cv2.warpAffine(x, mat, img.shpae[::-1][1:3]),
lambda x: cv2.flip(x, 1),
lambda x: cv2.flip(x, 0),
lambda x: cv2.cvtColor(x, cv2.COLOR_BGR2GRAY),
lambda x: cv2.threshold(x, 120, 255, cv2.THRESH_BINARY)[1],
lambda x: cv2.GaussianBlur(x, (75, 75), 0),
lambda x: cv2.resize(cv2.resize(x, (img_size[1]//5, img_size[0]//5))),
lambda x: cv2.erode(x, filter1)
])
doubling_images = lambda f, imag:(imag + [f(i) for i in imag])
for func in scratch[methods]:
images = doubling_images(func, images)
return images
def scratch_image2(img): # OpenCVによるデータの水増し(lambda)
filter1 = np.array([[0, 1, 0],
[1, 0, 1],
[0, 1, 0]], np.uint8)
filter2 = np.ones((3, 3))
list_resize = [2, 3, 5, 7]
list_mosaic = [3, 5, 7, 10]
list_rotation = [45, 135, 225, 315]
list_flip = [0, 1, -1]
list_cvt1 = [0]
list_cvt2 = [0]
list_THRESH_BINARY = [50, 100, 150, 200]
list_THRESH_BINARY_INV = [50, 100, 150, 200]
list_THRESH_TRUNC = [50, 100, 150, 200]
list_THRESH_TOZERO = [50, 100, 150, 200]
list_THRESH_TOZERO_INV = [50, 100, 150, 200]
list_gauss = [11, 31, 51, 71]
list_gray = [0]
list_nois_gray = [0]
list_nois_color = [0]
list_dilate = [filter1, filter2]
list_erode = [filter1, filter2]
parameters = [list_resize, list_mosaic, list_rotation, list_flip, list_cvt1, list_cvt2, list_THRESH_BINARY, \
list_THRESH_BINARY_INV, list_THRESH_TRUNC, list_THRESH_TOZERO, list_THRESH_TOZERO_INV, list_gauss, \
list_gray, list_nois_gray, list_nois_color, list_dilate, list_erode]
methods = np.array([
lambda i: cv2.resize(img, (img.shape[1] // i, img.shape[0] // i)),
lambda i: cv2.resize(cv2.resize(img, (img.shape[1] // i, img.shape[0] // i)), (img.shape[1],img.shape[0])),
lambda i: cv2.warpAffine(img, cv2.getRotationMatrix2D(tuple(np.array([img.shape[1] / 2, img.shape[0] /2])), i, 1), (img.shape[1], img.shape[0])),
lambda i: cv2.flip(img, i),
lambda i: cv2.cvtColor(img, cv2.COLOR_BGR2LAB),
lambda i: cv2.bitwise_not(img),
lambda i: cv2.threshold(img, i, 255, cv2.THRESH_BINARY)[1],
lambda i: cv2.threshold(img, i, 255, cv2.THRESH_BINARY_INV)[1],
lambda i: cv2.threshold(img, i, 255, cv2.THRESH_TRUNC)[1],
lambda i: cv2.threshold(img, i, 255, cv2.THRESH_TOZERO)[1],
lambda i: cv2.threshold(img, i, 255, cv2.THRESH_TOZERO_INV)[1],
lambda i: cv2.GaussianBlur(img, (i, i), 0),
lambda i: cv2.imread(img, i),
lambda i: cv2.fastNlMeansDenoising(cv2.imread(img, i)),
lambda i: cv2.fastNlMeansDenoisingColored(img),
lambda i: cv2.dilate(img, i),
lambda i: cv2.erode(img, i)
])
num = 0
for ind, method in enumerate(methods):
for parameter in parameters[ind]:
num += 1
cnv_img = method(parameter)
save_img(cnv_img, num)
def resize_image(image, n, image_size=(256,256)):
path = os.getcwd()
img = cv2.imread(image)
img = cv2.resize(img, image_size)
cv2.imwrite(path + "/" + str(n) + ".jpg" )
files = os.listdir(file_path)
count = len(files) + 1
for file in files:
image = cv2.imread(file_path +"/" + file)
scratch_images = scratch_image(img)
for im in scratch_images:
cv2.imwrite(file_path + "/" + str(count) + "/" + ",jpg", im)
画像サイズ調整
画像を読み込むときにもサイズ調整するので、ここではしてもしなくてもよい。
from PIL import Image
import sys
import os
import re
import cv2
path = "サイズ変更前フォルダパス"
resize_path = "サイズ変更後フォルダパス"
files = os.listdir(path)
for file in files:
img = cv2.imread(path + "/" + file)
img = cv2.resize(img, (150, 150))
cv2.imwrite(resize_path + "/" + file, img)
print("finish")
kerasで学習させる
# 必要なライブラリのインポート
import os
import numpy as np
import matplotlib.pyplot as plt
import cv2
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
画像ファイルの読み込み
# google colabを利用
# ディレクトリパスを変数に入れる
badtzmaru = "/content/drive/MyDrive/Colab Notebooks/バツ丸画像の保存ディレクトリ/"
# os.listdirでディレクトリ内のファイルを一覧で取得(ループ処理するため)
path_badtzmaru = os.listdir(badtzmaru)
# 以下同
cinnamon = "/content/drive/MyDrive/Colab Notebooks/シナモロール画像ディレクトリ/"
path_cinnamon = os.listdir(cinnamon)
keroppi = "/content/drive/MyDrive/Colab Notebooks/けろっぴ画像ディレクトリ/"
path_keroppi = os.listdir(keroppi)
kitty = "/content/drive/MyDrive/Colab Notebooks/キティ画像ディレクトリ/"
path_kitty = os.listdir(kitty)
LTS = "/content/drive/MyDrive/Colab Notebooks/キキララ画像ディレクトリ/"
path_LTS = os.listdir(LTS)
mymelody = "/content/drive/MyDrive/Colab Notebooks/マイメロ画像ディレクトリ/"
path_mymelody = os.listdir(mymelody)
pompom = "/content/drive/MyDrive/Colab Notebooks/ポムポムプリン画像ディレクトリ/"
path_pompom = os.listdir(pompom)
sam = "/content/drive/MyDrive/Colab Notebooks/タキシードサム画像ディレクトリ/"
path_sam = os.listdir(sam)
# 各画像データを格納するリスト
img_badtzmaru = []
img_cinnamon = []
img_keroppi = []
img_kitty = []
img_LTS = []
img_mymelody = []
img_pompom = []
img_sam = []
# ループ処理で画像データをリストに格納していく
for i in range(len(path_badtzmaru)):
# 画像をパスで読み込み
img = cv2.imread(badtzmaru + path_badtzmaru[i])
# 画像サイズを変更
img = cv2.resize(img, (150,150))
# リストに追加
img_badtzmaru.append(img)
for i in range(len(path_cinnamon)):
img = cv2.imread(cinnamon + path_cinnamon[i])
img = cv2.resize(img, (150,150))
img_cinnamon.append(img)
for i in range(len(path_keroppi)):
img = cv2.imread(keroppi + path_keroppi[i])
img = cv2.resize(img, (150,150))
img_keroppi.append(img)
for i in range(len(path_kitty)):
img = cv2.imread(kitty + path_kitty[i])
img = cv2.resize(img, (150,150))
img_kitty.append(img)
for i in range(len(path_LTS)):
img = cv2.imread(LTS + path_LTS[i])
img = cv2.resize(img, (150,150))
img_LTS.append(img)
for i in range(len(path_mymelody)):
img = cv2.imread(mymelody + path_mymelody[i])
img = cv2.resize(img, (150,150))
img_mymelody.append(img)
for i in range(len(path_pompom)):
img = cv2.imread(pompom + path_pompom[i])
img = cv2.resize(img, (150,150))
img_pompom.append(img)
for i in range(len(path_sam)):
img = cv2.imread(sam + path_sam[i])
img = cv2.resize(img, (150,150))
img_sam.append(img)
訓練データとテストデータを作成
# 各画像をまとめて配列にする
X = np.array(img_badtzmaru + img_cinnamon + img_keroppi + img_kitty + img_LTS + img_mymelody + img_pompom + img_sam)
# 各画像にラベルを付けて配列にする
# ラベル:0 => バツ丸、 ラベル:1 => シナモロール・・・
y = np.array([0]*len(img_badtzmaru) + [1]*len(img_cinnamon) + [2]*len(img_keroppi) + [3]*len(img_kitty) + [4]*len(img_LTS) + [5]*len(img_mymelody) + [6]*len(img_pompom) + [7]*len(img_sam))
# np.arangeでXと同じ大きさの連番を生成し、
# np.random.permutationでランダム配列にする
rand_index = np.random.permutation(np.arange(len(X)))
# ランダム配列をインデックスとして渡し、Xとyをランダムに並べ替える
X = X[rand_index]
y = y[rand_index]
# 80%を訓練データに、残りをテストデータにする
X_train = X[:int(len(X)*0.8)]
y_train = to_categorical(y[:int(len(y)*0.8)])
X_test = X[int(len(X)*0.8):]
y_test = to_categorical(y[int(len(y)*0.8):])
転移学習
# 画像サイズをインプットデータにする
input_tensor = Input(shape = (150, 150, 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(Dropout(rate=0.3))
top_model.add(Dense(256, activation="relu"))
top_model.add(Dropout(rate=0.3))
top_model.add(Dense(128, activation="relu"))
top_model.add(Dropout(rate=0.2))
top_model.add(Dense(64, activation="relu"))
# softmax関数で8個の出力を渡す
top_model.add(Dense(8, activation="softmax"))
# 入力はvgg.input, 出力はtop_modelにvgg16の出力を入れたもの
# VGGの特徴抽出部分のみを使い、以降は自分で作成したモデルと結合
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))
# modelの16層目までのvgg16の特徴抽出部分は学習させないため、固定する
for layer in model.layers[:15]:
layer.trainable = False
# コンパイルする
model.compile(loss="categorical_crossentropy",
optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
metrics=["accuracy"])
# 訓練する
model.fit(X_train, y_train, batch_size=8, epochs=45)
テストデータの確認
# テストデータで評価する
# verbose:進行状況の表示モード.0=>表示なし,1=>プログレスバー,2=>各試行毎に一行出力
scores = model.evaluate(X_test, y_test, verbose=1)
print("Test loss:", scores[0])
print("Test accuracy:", scores[1])
>> 10/10 [==============================] - 8s 384ms/step - loss: 0.1534 - accuracy: 0.9619
>> Test loss: 0.15337002277374268
>> Test accuracy: 0.961904764175415
# 96.2%の成功率
モデルを使った予測を表示する関数
def pred_sanrio(img):
img = cv2.resize(img, (150, 150))
# model.predictでモデルの予測を取得する
# np.argmaxで予測の最大値のインデックスを取得する
pred = np.argmax(model.predict(np.array([img])))
if pred == 0:
return "Badtz-Maru"
elif pred == 1:
return "Cinnamoroll"
elif pred == 2:
return "Kero Kero Keroppi"
elif pred == 3:
return "Hello Kitty"
elif pred == 4:
return "Little Twin Stars"
elif pred == 5:
return "My Melody"
elif pred == 6:
return "Pompom Purin"
elif pred == 7:
return "Tuxedo Sam"
画像が適切に判断されているか確認
同じ処理は関数にまとめてもよかった。
for i in range(3):
img = cv2.imread(badtzmaru + path_badtzmaru[i])
# cv2.imreadはBGR、pltはRGBなので変換する
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.show()
print(pred_sanrio(img))
for i in range(3):
img = cv2.imread(cinnamon + path_cinnamon[i])
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.show()
print(pred_sanrio(img))
for i in range(3):
img = cv2.imread(keroppi + path_keroppi[i])
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.show()
print(pred_sanrio(img))
for i in range(3):
img = cv2.imread(kitty + path_kitty[i])
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.show()
print(pred_sanrio(img))
for i in range(3):
img = cv2.imread(LTS + path_LTS[i])
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.show()
print(pred_sanrio(img))
for i in range(3):
img = cv2.imread(mymelody + path_mymelody[i])
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.show()
print(pred_sanrio(img))
for i in range(3):
img = cv2.imread(pompom + path_pompom[i])
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.show()
print(pred_sanrio(img))
for i in range(3):
img = cv2.imread(sam + path_sam[i])
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.show()
print(pred_sanrio(img))
結果
Badtz-Maru
Cinnamoroll
Kero Kero Keroppi
Hello Kitty
Little Twin Stars
My Melody
Pompom Purin
Tuxedo Sam
正解 | 誤答 | |
---|---|---|
バツ丸 | 3 | 0 |
シナモロール | 2 | 1 |
ケロッピー | 3 | 0 |
キティ | 3 | 0 |
リトルツインスターズ | 2 | 1 |
マイメロ | 3 | 0 |
ポムポムプリン | 3 | 0 |
タキシードサム | 3 | 0 |
合計 | 22 | 2 |
確率: 83% |
考察
テストデータで96%の正解率は、目標の95%を超えた。
課題として、使用した画像枚数に差がある(タキシードサムとキティの差は3倍)、ハイパーパラメータの設定が難しいなど、改善点がみつかりました。
初めての画像診断としてはいい結果かなと思います。
最後の検証は各キャラクター3枚ずつしか使っていないので、枚数を増やせば正解率は上がると思います。
参考資料
画像水増し
OpenCVによるデータの水増し(lambda)
ImageDataGenerator
Kerasによる画像の水増し操作
KerasのImageDataGeneratorで学習用画像を水増しする方法
Kerasによるデータ拡張
NumPyでの画像のData Augmentationまとめ
kerasで学習させる
はじめてのニューラルネットワーク:分類問題の初歩
ModelクラスAPI
【python入門】os.listdirでファイル・ディレクトリの一覧を取得
NumPyのarange, linspaceの使い方(連番や等差数列を生成)
numpy.random.permutation – 配列の要素をランダムに並べ替えた新しい配列を生成
過学習と学習不足について知る
【Deep Learning】 Batch sizeをどうやって決めるかについてまとめる