尾瀬に咲く花の画像を識別するアプリを作成する

はじめに

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

コロナで大好きな尾瀬に出かけられなくなってしまったので、尾瀬に咲く花をテーマにしました。識別できる種類は3種類のみです。

実行環境

・Python3.8.8
・Google Colaboratory
・Visual Studio Code

画像収集

まず事前準備として画像を収集します。
今回使用する花はミズバショウニッコウキスゲシラネアオイの3種類です。当初はBing APIを予定していましたが上手く使いこなせず、seleniumに変更し約70枚ずつ収集しました。

!apt-get update
!apt install chromium-chromedriver
!cp /usr/lib/chromium-browser/chromedriver /usr/bin
!pip install selenium

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
import requests
from bs4 import BeautifulSoup
import urllib
import time
import os

def scrollDownAllTheWay(driver):
   old_page = driver.page_source
   while True:
       for i in range(2):
           scrollDown(driver,200)#どの程度スクロールさせるか
           time.sleep(3)#読み込まないページの待機時間
       new_page = driver.page_source
       if new_page != old_page:
           old_page = new_page
       else:
           break
   return True

def scrollDown(driver, value):
   driver.execute_script("window.scrollBy(0,"+str(value)+")")

# ブラウザを開く
option = Options()
option.add_argument('--headless')
option.add_argument('--no-sandbox')
option.add_argument('--disable-dev-shm-usage')
driver = webdriver.Chrome('chromedriver',options=option)
# driver = webdriver.Chrome('chromedriver',options=option)  # ブラウザをバックグラウンドで起動(この行をコメントアウトすることでブラウザの動作を確認することができる)

# 対象のページを開く
driver.get("ページのURL")
time.sleep(3)  # レンダリングのため3秒待機

for i in range (20):
 scrollDownAllTheWay( driver)
 time.sleep(5)  # レンダリングのため3秒待機

# レンダリング後のhtmlを取得する
html = driver.page_source

# imgタグの要素を取得する
soup = BeautifulSoup(html, "lxml")
images = soup.find_all('img', class_='sw-Thumbnail__innerImage sw-Thumbnail__innerImage--width rapidnofollow')

# 画像ダウンロードのための関数を定義する
def download_file(url, path):
   try:
       with urllib.request.urlopen(url) as web_file, open(path, 'wb') as local_file:
           local_file.write(web_file.read())
   except urllib.error.URLError as e:
       print(e)

# 保存先ディレクトリがない場合作成する
directry = '保存先ディレクトリ'
if not os.path.isdir(directry):
   os.makedirs(directry)

# 画像のURLを取得し、ダウンロードを実施する
i = 1
for target in images:
   url = target['src']
   if not 'jpg' in url:
       continue
   path = directry + 'img_' + str(i) + '.jpeg'
   download_file(url, path)
   i += 1
print("finished")

画像1

画像の水増し

次に収集した画像がとても少ないので、水増しをしたいと思います。
コードはaidemyの講座を参考にしました。
水増し手法は下記の5通りを使用し約400枚ずつに増やしました。
・flip(反転)
・thr(閾値処理)
・filt(ぼかし)
・resize(モザイク)
・erode(収縮)

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.array([[1, 1, 1],
[1, 0, 1],
[1, 1, 1]], np.uint8)
   # オリジナルの画像データを配列に格納
   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)
   ])
  # 関数と画像を引数に、加工した画像を元と合わせて水増しする関数
   orig = []
   orig.extend(images)
   doubling_images = lambda f, imag: ([f(i) for i in imag])
   # methodsがTrueの関数で水増し
   for func in scratch[methods]:
     result = doubling_images(func, images)
     orig.extend(result)
   return orig
   # ----------------------------ここまで書いて下さい----------------------------
flower_dirs =["m","n","s"]
for dir_name in flower_dirs:
 # 画像の読み込み
 file_lists = os.listdir("/content/drive/MyDrive/app/"+dir_name+"/")
 flower_imgs = [] 
 for f in file_lists:
     f_img = cv2.imread("/content/drive/MyDrive/app/"+dir_name+"/"+f)
     flower_imgs.append(f_img)
 # 画像の水増し
 scratch_result = []
 for img in flower_imgs:
     scratch_flower_images = scratch_image(img)
     scratch_result.extend(scratch_flower_images)
 # 画像を保存するフォルダーを作成
 if not os.path.exists("scratch_image_"+dir_name):
     os.mkdir("scratch_image_"+dir_name)
 for num, im in enumerate(scratch_result):
     # まず保存先のディレクトリ"scratch_images/"を指定、番号を付けて保存
     cv2.imwrite("scratch_image_" + dir_name + "/" + str(num) + ".jpg" ,im) 

画像2

モデルの作成・学習①

CNN (Convolutional Neural Network、畳み込みニューラルネットワーク)と呼ばれる、画像認識に広く使われるディープニューラルネットワークを用います。
今回はkerasを使用し、中間層にはランプ関数、出力層にはソフトマックス関数を使用します。Kerasとはニューラルネットワークライブラリの1つです。
ランプ関数は計算が簡単で中間層が多いディープラーニングでも安定して学習ができ、ソフトマックス関数は分類問題で使われ、 複数ある出力の合計を 1 にしてくれる関数です。
上記関数の詳細はこちらをご参照ください⇒https://blog.apar.jp/deep-learning/12249/

from keras.datasets import mnist
from keras.layers import Dense, Dropout, Flatten, Activation
from keras.layers import Conv2D, MaxPooling2D
from keras.models import Sequential, load_model
from keras.utils.np_utils import to_categorical
from keras.utils.vis_utils import plot_model
import numpy as np
import matplotlib.pyplot as plt
 #モデルの保存 
import os
from google.colab import files

import cv2

from sklearn.model_selection import train_test_split


img_all = np.load('画像フォルダ')

X = img_all["x"]
y = img_all["y"]

# データの分割
X_train, X_test, y_train, y_test = train_test_split(X, y,test_size = 0.2, random_state = 42)

# ラベルデータをone-hotベクトルに変更する
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# モデルの定義
model = Sequential()
model.add(Conv2D(filters=32, kernel_size=(3, 3),input_shape=(50,50,3)))
model.add(Activation('relu'))
model.add(Conv2D(filters=64, kernel_size=(3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(3))
model.add(Activation('softmax'))


model.compile(loss='categorical_crossentropy',
             optimizer='adadelta',
             metrics=['accuracy'])

model.fit(X_train, y_train,
         batch_size=128,
         epochs=50,
         verbose=1,
         validation_data=(X_test, y_test))

# 精度の評価
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

# データの可視化(検証データの先頭の10枚)
for i in range(10):
   plt.subplot(2, 5, i+1)
   plt.imshow(X_test[i])
plt.suptitle("10 images of test data",fontsize=20)
plt.show()

# 予測(検証データの先頭の10枚)
pred = np.argmax(model.predict(X_test[0:10]), axis=1)
print(pred)

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

# files.download( '保存先ディレクトリ' ) 

画像が少なくTest accuracyが0.6と低いですので、次は転移学習を試したいと思います。

画像3

モデルの作成・学習②

2回目は転移学習に変更してみます。
こちらのコードもaidemyの男女識別の講座を参考にしました。
転移学習とはすでに大量のデータを使ってある程度学習しているモデルをベースに、自分で層を追加して学習を行う方法です。
1回目同様にランプ関数とソフトマックス関数を使用します。

# モデルにvggを使います
input_tensor = Input(shape=(50, 50, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)
# vggのoutputを受け取り、3クラス分類する層を定義します
# その際中間層を下のようにいくつか入れると精度が上がります
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(3, 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'])
# 学習を行います
model.fit(X_train, y_train, batch_size=100, epochs=10, validation_data=(X_test, y_test))
# 画像を一枚受け取り、どの花かを判定する関数
def pred_flower(img):
   img = cv2.resize(img, (50, 50))
   pred = np.argmax(model.predict(np.array([img])))
   if pred == 0:
       return 'ミズバショウ'
   elif pred == 1:
       return 'ニッコウキスゲ'
   else:
       return 'シラネアオイ'
   
# pred_flower関数に花の写真を渡して予測します
y_len = np.arange(0,len(y_test))
rand_num = np.random.choice(y_len,5,replace = False)
for i in range(5):
   img = X_test[rand_num[i]]
   # b,g,r = cv2.split(img) 
   # img = cv2.merge([r,g,b])
   plt.imshow(img)
   plt.show()
   print(pred_flower(img))

# 重みを保存
model.save(os.path.join(result_dir, 'model_VGG.h5'))

files.download( '保存先ディレクトリ' ) 

結果はval accuracy0.9でしたのでこちらでモデル完成とします。

画像4

アプリ

アプリもaidemyの講座を参考に作成しました。
見た目はシンプルですが、初心者の私にとってはHTMLもCSSもとても難しかったです。背景画像設定にはとても時間がかかりました。

画像5

まとめ

今回はアプリを完成させることで精一杯で、残念ながら勉強したことをフル活用はできませんでした。
画像収集からモデルの作成・学習までは今回の手法に限らず様々な手法がありますので、色々試してみたいと思います。
また、骨格検知や行動分析、自然言語処理も勉強して今後の仕事に役立てたいと思います。
aidemyのカウンセラーの方にはいつも丁寧に教えていただき、大変感謝しております。本当にありがとうございました。

参考サイト

画像収集:https://qiita.com/kbanchi1111/items/c494976fe1f7d4a53605



この記事が気に入ったらサポートをしてみませんか?