画像判定による自動車の車種識別を実践してみた

#はじめに

この記事は Aidemy Premium plan AIアプリ開発コースの最終成果物として AIを用いた画像認識アプリを開発する過程を記録したものです。

自分は車関係の仕事をしているため自動車の画像で車種を判別するためのアプリの制作を行いました。

#目次
[1. 実行環境](#1-実行環境)
[2. 画像の水増し](#2-画像の水増し)
[3. CNNモデル作成および学習](#3-CNNモデル作成および学習)
[4. HTMLとFlaskコードの作成](#4-HTMLとFlaskコードの作成)
[5. アプリの動作確認](#5-アプリの動作確認)
[6. Herokuへのデプロイ](#6-Herokuへのデプロイ)
[7. 反省](#7.-反省)

#1. 実行環境
Python 3.8.7
Tensorflow 2.5.0
Windows10 (第10世代Corei7) 
Visual Studio Code

#2. 画像の水増し
学習のために使用した画像はすべて Googleの画像検索から同車種のものを数枚ピックアップして保存しました。
自動車はマイナーチェンジ等で外観が微妙に異なるものがあり、スクレイピングではこれらが混ざってしまう恐れがあると考え、元の画像はシンプルに自分で集めて画像の水増し作業で学習用の画像を増やすことにしました。
（本当はそれでもうまくスクレイピングを活用するべきだったかなと思っています。。。）

具体的なコードは以下の通りです。

```py
import os

import numpy as np
import matplotlib.pyplot as plt
import cv2


def scratch_image(img, flip=True, thr=True, filt=True, resize=True, erode=True):

    # 水増しの手法を配列にまとめる
    methods = [flip, thr, filt, resize, erode]
    # 画像のサイズを習得、ぼかしに使うフィルターの作成
    img_size = img.shape
    filter1 = np.ones((3, 3))
    # オリジナルの画像データを配列に格納
    images = [img]
    # 手法に用いる関数
    scratch = np.array([
        lambda x: cv2.flip(x, 1),
        lambda x: cv2.threshold(x, 100, 255, cv2.THRESH_TOZERO)[1],
        lambda x: cv2.GaussianBlur(x, (5, 5), 0),
        lambda x: cv2.resize(cv2.resize(
                        x, (img_size[1] // 5, img_size[0] // 5)
                    ),(img_size[1], img_size[0])),
        lambda x: cv2.erode(x, filter1)
    ])
    # 関数と画像を引数に、加工した画像を元と合わせて水増しする関数
    
    doubling_images = lambda f, imag: (imag + [f(i) for i in imag])
    # methodsがTrueの関数で水増し
    for func in scratch[methods]:
        images = doubling_images(func, images)
    
    return images
    
# 画像の読み込み

img = cv2.imread("C:/Users/narno/Desktop/Aidemy app/Picture/XC40/XC40_1.jpg")

# 画像の水増し
scratch_images = scratch_image(img)

# 画像を保存するフォルダーを作成
if not os.path.exists("scratch_images_1"):
    os.mkdir("scratch_images_1")

for num, im in enumerate(scratch_images):
    # まず保存先のディレクトリ"scratch_images/"を指定、番号を付けて保存
    cv2.imwrite("scratch_images_1/" + str(num) + ".jpg" ,im) 
```

こちらのコードを用いて画像の水増しを行い学習用の画像を集めました。


#3. CNNモデル作成および学習

続いてCNNモデルの作成ですが、こちらはAidemyの講義で作成したコードを参考に作成しています。

```py
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers


path_Q5 = os.listdir('./Q5_Data/')
path_XC40 = os.listdir('./XC40_Data/')

img_Q5 = []
img_XC40 = []

for i in range(len(path_Q5)):
    img = cv2.imread('./Q5_Data/' + path_Q5[i])
    img = cv2.resize(img, (50,50))
    img_Q5.append(img)

for i in range(len(path_XC40)):
    img = cv2.imread('./XC40_Data/' + path_XC40[i])
    img = cv2.resize(img, (50,50))
    img_XC40.append(img)
    
X = np.array(img_Q5 + img_XC40)
y =  np.array([0]*len(img_Q5) + [1]*len(img_XC40))

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の形にします
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# モデルにvggを使います
input_tensor = Input(shape=(50, 50, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

# vggのoutputを受け取り、2クラス分類する層を定義します
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(2, activation='softmax'))

# vggと、top_modelを連結します
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

# vggの層の重みを変更不能にします
for layer in model.layers[:19]:
    layer.trainable = False

# コンパイルします
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=100, epochs=10, validation_data=(X_test, y_test))

# 画像を一枚受け取り、車の車種を判定する関数
def pred_gender(img):
    img = cv2.resize(img, (50, 50))
    pred = np.argmax(model.predict(np.array([img])))
    if pred == 0:
        return 'Q5'
    else:
        return 'XC40'

# pred_gender関数に車の写真を渡して車種を予測します
img = cv2.imread('./Q5_Data/' + path_Q5[0])
b,g,r = cv2.split(img) 
img = cv2.merge([r,g,b])
plt.imshow(img)
plt.show()
print(pred_gender(img))

model.summary()

#resultsディレクトリを作成
result_dir = 'results'
if not os.path.exists(result_dir):
    os.mkdir(result_dir)
# 重みを保存
model.save('Car.h5')



# 正解率の可視化
plt.plot(history.history['accuracy'], label='acc', ls='-')
plt.plot(history.history['val_accuracy'], label='val_acc', ls='-')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(loc='best')
plt.show()
```
以下が学習結果になります。
今回は比較的わかりやすい画像を使用したこともあり、良好な結果になっています。
転移学習に結果はepoch 7でも正解率100%にできていたのでepochは 10も必要なかったかなと思います。

また今回はAidemyの講義内容をベースにコードを作成していることもあり、Dropoutや活性化関数をいじらなくてもある程度いい結果が出てしまったので、今後車種等を増やしてよりアプリとしての7有用性を高めていくためにこれらのパラメーターをいじってどのような変化が起きるのか試してみたいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1899045/69c67259-9bf2-b2c8-64bc-110423e8c0c6.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1899045/a9acb1ad-3f3a-625f-6361-b6ed63fa1936.png)


#4. HTMLとFlaskコードの作成

3までにアプリの中身は出来上がったので、これからWebサイトを作成していきます。
こちらもAidemyの講義内容をベースに作成してみました。
ヘッダーやフッターについても画像を用いておしゃれにしたかったのですが
自分の美的センスのなさに絶望したため今回はアプリとして機能させることを優先させました。

```py
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


classes = ["Q5","XC40"]
image_size = 50

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

            #受け取った画像を読み込み、np形式に変換
            img = image.load_img(filepath, grayscale=False, target_size=(image_size,image_size))
            img = image.img_to_array(img)
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()
            pred_answer = "これは " + classes[predicted] + " です"
            

            return render_template("index.html",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)

```
このコードで問題なくWebページの作成はできました。

#5. アプリの動作確認

上記のコードを実行すると以下のようなWebページになりました。
（うーん。。。なんとも無機質。。。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1899045/fd6aed14-81d5-e837-71cc-0247091a648a.png)

とりあえず適当な写真をアップデートしてみます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1899045/8d93b858-76fb-0d6c-a9ae-881a30ff5de3.png)

アプリの動作は問題なくできていそうですね。

#6. Herokuへのデプロイ
つづいてHerokuへのデプロイを実行していきます。

デプロイの時の注意事項としては、アプリを格納しているフォルダ内にあるファイルすべてをアップロードしてしまうので、ファイルの中身を整理しておく必要があることをしり少し戸惑いましたが、Aidemyのチューターさんにお力添えをいただきながらなんとかデプロイを完了させることができました。


#7. 反省
プログラミングもAIも初心者の私ですがAidemyの教材を使用することで簡単なアプリの制作についての知識は身に着けることができたかなと感じています。

ですが、思いのほか勉強に費やせる時間が少なく自分の目指していたレベルに到達できなかった気がすることが心残りです。

今後は今回制作したものをベースにアプリを改良していこうと思います。
具体的には、
　１．識別できる車種を増やす
　２．Webページのレイアウトをきれいにする（ヘッダー追加など）
　３．判定後に車の写真とメーカーのWebページのリンクが表示されるようにする
など自分で改良を進めていくつもりです。

最後にAidemyの方々に初心者の私にも親切にサポートいただきましたこと感謝申し上げます。