ImageDownloaderで画像を収集して、サンリオキャラの画像診断してみた

サンリオキャラクターを画像認識するAIを作りました。

## ImageDownloader
Chromeの拡張機能でWebサイトの画像をダウンロードできる
([公式](https://chrome.google.com/webstore/detail/image-downloader/cnpniohnfphhjihaiiggeabnkjhpaldj?hl=ja&))からchromeに追加をクリックして入手

### 分類するキャラクター
無料の画像サイトから拝借しました。
#### バツ丸
![84741762_202x291.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/6359cf43-ac8a-42f8-31b6-5489ee976b42.jpeg)
#### シナモロール
![4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/b5c4caa1-1500-f5bd-0a29-f86429a9163a.jpeg)
#### ケロッピー
![878856781.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/a021f6e4-1fb7-c66b-f2c5-24d77c8b6e79.jpeg)
#### ハローキティ
![84741725_220x146.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/3ae9f7c7-d484-87e3-107b-9910b67f0940.jpeg)
#### リトルツインスターズ
![5467289.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/4eeaacae-90ea-485b-bdbb-0b5be0724172.png)
#### マイメロディ
![68789739.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/e970f997-dc7b-3ae7-52d7-ad7ae73ad364.jpeg)
#### ポムポムプリン
![987895824.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/f37972cd-8ab5-9939-e24c-6fdcdc817d33.jpeg)
#### タキシードサム
![95867738.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/96668aba-2e73-aa20-5803-680a4f335acf.jpeg)

## 画像水増し
ダウンロードした画像だけでは少ないので、水増しする
kerasを使っても、cv2を使ってもどちらでも可

```python:kerasによる画像水増し
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")
```
```python:cv2による水増し
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)

```
## 画像サイズ調整
画像を読み込むときにもサイズ調整するので、ここではしてもしなくてもよい。

```python:画像サイズ調整
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で学習させる
```python
# 必要なライブラリのインポート
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
```
### 画像ファイルの読み込み
```python
# 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)
```
### 訓練データとテストデータを作成
```python
# 各画像をまとめて配列にする
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):])
```
### 転移学習
```python
# 画像サイズをインプットデータにする
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)
```
### テストデータの確認
```python
# テストデータで評価する
# 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%の成功率
```
### モデルを使った予測を表示する関数
```python
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"
```
### 画像が適切に判断されているか確認
同じ処理は関数にまとめてもよかった。

```python
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))
```
### 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/795c5974-2aaa-ca5e-8e66-21ee93047787.png)
Badtz-Maru
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/5ef597eb-fefa-0579-272e-edc3a7309831.png)
Cinnamoroll
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/50d6bf45-a681-f2ce-a927-9e4fdcf94856.png)
Kero Kero Keroppi
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/344f52b2-ad29-a5e1-1a98-e38218f68630.png)
Hello Kitty
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/3c7eabb4-25fb-7bb6-6f13-6045681d5b78.png)
Little Twin Stars
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/f28b0091-ca68-e285-90e8-6d1daf8d476c.png)
My Melody
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/fe9e7385-fd56-5665-9dc6-a1be621a00b1.png)
Pompom Purin
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683664/ebbc5eb3-28ae-5044-c614-753ace31599e.png)
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）](https://qiita.com/FujiedaTaro/items/1196df74d5d12dd9fe5d)
[ImageDataGenerator](https://keras.io/ja/preprocessing/image/)
[Kerasによる画像の水増し操作](https://shirakonotempura.hatenablog.com/entry/2019/01/09/080534)
[KerasのImageDataGeneratorで学習用画像を水増しする方法](https://intellectual-curiosity.tokyo/2019/07/03/keras%E3%81%AEimagedatagenerator%E3%81%A7%E5%AD%A6%E7%BF%92%E7%94%A8%E7%94%BB%E5%83%8F%E3%82%92%E6%B0%B4%E5%A2%97%E3%81%97%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/)
[Kerasによるデータ拡張](https://aidiary.hatenablog.com/category/Keras?page=1483535144)
[NumPyでの画像のData Augmentationまとめ](https://www.kumilog.net/entry/numpy-data-augmentation)


### kerasで学習させる
[はじめてのニューラルネットワーク：分類問題の初歩](https://www.tensorflow.org/tutorials/keras/classification?hl=ja)
[ModelクラスAPI](https://keras.io/ja/models/model/)
[【python入門】os.listdirでファイル・ディレクトリの一覧を取得](https://www.sejuku.net/blog/63568)
[NumPyのarange, linspaceの使い方（連番や等差数列を生成）](https://note.nkmk.me/python-numpy-arange-linspace/)
[numpy.random.permutation – 配列の要素をランダムに並べ替えた新しい配列を生成](https://www.headboost.jp/numpy-random-permutation/)
[過学習と学習不足について知る](https://www.tensorflow.org/tutorials/keras/overfit_and_underfit?hl=ja)
[【Deep Learning】 Batch sizeをどうやって決めるかについてまとめる](https://www.st-hakky-blog.com/entry/2017/11/16/161805)