画像認識を用いたトマトの葉の病気診断アプリ作成

# はじめに
はじめまして。新しいことを始めようと最近機械学習を学び始めたプログラマーです。
この度、Aidemyさんの機械学習講座の課題として、ブログの作成をいたします。
内容は、画像認識を使ってトマト葉の病気の診断をやってみるというものです。
時間の都合もあり今回はkaggleにあるトマト葉の病気のサンプル画像を用いてモデルの構築を行いました。
それでは早速本題に入ります。よろしくお願いいたします。

# 内容・・・
1.画像収集
2.モデルの作成
3.学習データと検証データの作成
4.モデルの学習と保存
5.HTML
6.main.py
7.実装結果

# 実行環境
・Visual Studio Code
・Google Colaboratory

# 作成課題
[Tomato_Leaf Disease Diagnosis](https://tomato-leaf-identification-app.herokuapp.com/)

# 1:画像収集
今回は時間の都合もありkaggle以下URLにある画像を利用させていただきました。
[Tomato|Kaggle](https://www.kaggle.com/datasets/noulam/tomato)
コードの中で、このサイトから直接画像を取得できればよかったのですが、今回はそれができなかったので、その方法を書いておきます。
1.上記サイトの右上にあるDownloadボタンを押下。
2.ダウンロードしたフォルダを解凍。
3.以下URLを参考にマイドライブにアップロードする。
    [Googleドライブヘルプ](https://support.google.com/drive/answer/2424368?hl=ja&co=GENIE.Platform%3DDesktop&oco=0)
4.Google Colaboratoryでドライブをマウント
```ruby
from google.colab import drive
drive.mount('/content/drive')
```
上記手順でGoogle Colaboratory上で画像を取得できるようになります。

# 2:モデルの作成
```ruby
from tensorflow.keras.layers import Input, Dense, Flatten
from tensorflow.keras import Model
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.preprocessing import image
from tensorflow.keras.models import Sequential
import numpy as np
import pandas as pd

img_size = 224
image_size = [img_size, img_size]
# vgg16インタンス作成
vgg = VGG16(input_shape = image_size + [3], weights = 'imagenet', include_top =  False)

# vgg16による特徴抽出部分の重みを固定
for layer in vgg.layers:
    layer.trainable = False
    
# マウントしたドライブから画像フォルダを取得
from glob import glob
folders = glob('/content/drive/MyDrive/archive/New Plant Diseases Dataset(Augmented)/New Plant Diseases Dataset(Augmented)/train/*')
folders

# モデル作成
x = Flatten()(vgg.output)
prediction = Dense(len(folders), activation = 'softmax')(x)
# モデルの連結
model = Model(inputs = vgg.input, outputs = prediction)
model.summary()

model.compile(loss = 'categorical_crossentropy', optimizer = 'adam', metrics = ['accuracy'])
```
今回はVGG16を使い転移学習を行っています。
自分理解の範囲でコードの説明をしていきます。

## コード①
```ruby
img_size = 224
image_size = [img_size, img_size]
```
上記、
・読み込む画像のサイズを設定します。

## コード②
```ruby
# モデル作成
x = Flatten()(vgg.output)
prediction = Dense(len(folders), activation = 'softmax')(x)
# モデルの連結
model = Model(inputs = vgg.input, outputs = prediction)
model.summary()

model.compile(loss = 'categorical_crossentropy', optimizer = 'adam', metrics = ['accuracy'])
```
上記、
・ サンプルで使用しているトマト葉の病気の種類が10種類(foldersの数)なので適用したモデルを作成する。
・ 作成したモデルと既存のVGG16モデルを連結する。
・「損失関数（loss function）」と「最適化（Optimization）」で、学習内容を構築。
・ lossの値はつまり失敗値なので「0」に近いほどよい。
・ categorical_crossentrは分類問題を解くために用いる損失関数
・ to_categoricalはOne-hot-vectorを求めるための関数。
・ optimizerに指定した「adam」は各パラメーターの変化率を計算するアルゴリズム。

### 参考文献：
[categorical_crossentropyとsparse_categorical_crossentropyの違い【Keras】](https://engineeeer.com/keras-categorical-crossentropy-sparse-categorical-crossentropy/)
[深層学習の最適化アルゴリズムまとめ](https://data-analysis-stats.jp/%E6%B7%B1%E5%B1%9E%E5%AD%A6%E7%BF%92/%E6%B7%B1%E5%B1%A4%E5%AD%A6%E7%BF%92%E3%81%AE%E6%9C%80%E9%81%A9%E5%8C%96%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%81%BE%E3%81%A8%E3%82%81/)

# 3:学習データと検証データの作成
環境によってfoldersから取り出すフォルダの順番が上記コードと違う可能性があるため、下記コードを実行する前に次のコードでフォルダの並び順を確認することを推奨する。
```
# 取り出したフォルダ名を出力
folders
```

```ruby
###############################################################################
# 斑点病の学習データ
###############################################################################
# 該当するフォルダのファイルを取得(斑点病)
folders1 = glob(folders[0] + '/*.JPG')
img_TargetSpot = []
train_range = 1200
# rangeの数だけ画像ファイルを取得(斑点病)
import cv2
for i in range(train_range):
    img = cv2.imread(folders1[i])
    img = cv2.resize(img, (img_size, img_size))
    img_TargetSpot.append(img)


###############################################################################
# モザイク病の学習データ
###############################################################################
# 該当するフォルダのファイルを取得(モザイク病)
folders2 = glob(folders[1] + '/*.JPG')
img_MosaicVirus = []
for i in range(train_range):
    img = cv2.imread(folders2[i])
    img = cv2.resize(img, (img_size, img_size))
    img_MosaicVirus.append(img)


###############################################################################
# 葉巻病の学習データ
###############################################################################
# 該当するフォルダのファイルを取得(葉巻病)
folders3 = glob(folders[2] + '/*.JPG')
img_LeafCurlVirus = []
for i in range(train_range):
    img = cv2.imread(folders3[i])
    img = cv2.resize(img, (img_size, img_size))
    img_LeafCurlVirus.append(img)


###############################################################################
# ハダニ被害の学習データ
###############################################################################
# 該当するフォルダのファイルを取得(ハダニ被害)
folders4 = glob(folders[3] + '/*.JPG')
img_TwoSpottedSpiderMite = []
for i in range(train_range):
    img = cv2.imread(folders4[i])
    img = cv2.resize(img, (img_size, img_size))
    img_TwoSpottedSpiderMite.append(img)


###############################################################################
# 葉かび病の学習データ
###############################################################################
# 該当するフォルダのファイルを取得(葉かび病)
folders5 = glob(folders[4] + '/*.JPG')
img_LeafMold = []
for i in range(train_range):
    img = cv2.imread(folders5[i])
    img = cv2.resize(img, (img_size, img_size))
    img_LeafMold.append(img)


###############################################################################
# 白星病の学習データ
###############################################################################
# 該当するフォルダのファイルを取得(白星病)
folders6 = glob(folders[5] + '/*.JPG')
img_SeptoriaLeafSpot = []
for i in range(train_range):
    img = cv2.imread(folders6[i])
    img = cv2.resize(img, (img_size, img_size))
    img_SeptoriaLeafSpot.append(img)


###############################################################################
# 斑点細菌病の学習データ
###############################################################################
# 該当するフォルダのファイルを取得(斑点細菌病)
folders7 = glob(folders[6] + '/*.JPG')
img_BacterialSpot = []
for i in range(train_range):
    img = cv2.imread(folders7[i])
    img = cv2.resize(img, (img_size, img_size))
    img_BacterialSpot.append(img)


###############################################################################
# 輪紋病の学習データ
###############################################################################
# 該当するフォルダのファイルを取得(輪紋病)
folders8 = glob(folders[7] + '/*.JPG')
img_EarlyBlight = []
for i in range(train_range):
    img = cv2.imread(folders8[i])
    img = cv2.resize(img, (img_size, img_size))
    img_EarlyBlight.append(img)


###############################################################################
# 疫病の学習データ
###############################################################################
# 該当するフォルダのファイルを取得(疫病)
folders9 = glob(folders[8] + '/*.JPG')
img_LateBlight = []
for i in range(train_range):
    img = cv2.imread(folders9[i])
    img = cv2.resize(img, (img_size, img_size))
    img_LateBlight.append(img)


###############################################################################
# 健康態の学習データ
###############################################################################
# 該当するフォルダのファイルを取得(健康態)
folders10 = glob(folders[9] + '/*.JPG')
img_Healthyt = []
for i in range(train_range):
    img = cv2.imread(folders10[i])
    img = cv2.resize(img, (img_size, img_size))
    img_Healthyt.append(img)


# 学習データ作成
X_train = []
X_train.extend(img_TargetSpot)
X_train.extend(img_MosaicVirus)
X_train.extend(img_LeafCurlVirus)
X_train.extend(img_TwoSpottedSpiderMite)
X_train.extend(img_LeafMold)
X_train.extend(img_SeptoriaLeafSpot)
X_train.extend(img_BacterialSpot)
X_train.extend(img_EarlyBlight)
X_train.extend(img_LateBlight)
X_train.extend(img_Healthyt)

# 正解ラベルを作成
y_train = []
i = 0
while i < 10:
  y_train.extend([i] * train_range)
  i += 1


#検証データ作成
X_test = []
# 画像フォルダ取得
test_folders = glob('/content/drive/MyDrive/archive/New Plant Diseases Dataset(Augmented)/New Plant Diseases Dataset(Augmented)/valid/*')
test_folders
test_range = 400

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 学習データと同様に検証データを作成
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

```
学習データは各フォルダ内から特定枚数の画像を取り込み作成しています。
コードの説明をしていきます。

## コード①
```ruby
# 該当するフォルダのファイルを取得(斑点病)
folders1 = glob(folders[0] + '/*.JPG')
img_TargetSpot = []
train_range = 1200
# rangeの数だけ画像ファイルを取得(斑点病)
import cv2
for i in range(train_range):
    img = cv2.imread(folders1[i])
    img = cv2.resize(img, (img_size, img_size))
    img_TargetSpot.append(img)
```
上記、
・foldersからJPGデータを取り出す。
・img_TargetSpot = []でからの配列を作成。
・train_range = 1200で指定した数の画像を取得(今回は1200)
・OpenCVで画像を取得していく。
・10種類の分類すべてでこの作業を行う。

### 参考文献:
[PythonにおけるOpenCVのインストール方法について現役エンジニアが解説【初心者向け】](https://techacademy.jp/magazine/51404)

## コード②
```ruby
# 学習データ作成
X_train = []
X_train.extend(img_TargetSpot)
X_train.extend(img_MosaicVirus)
X_train.extend(img_LeafCurlVirus)
X_train.extend(img_TwoSpottedSpiderMite)
X_train.extend(img_LeafMold)
X_train.extend(img_SeptoriaLeafSpot)
X_train.extend(img_BacterialSpot)
X_train.extend(img_EarlyBlight)
X_train.extend(img_LateBlight)
X_train.extend(img_Healthyt)
```
上記、
・X_train = []で空の学習データの配列を作成。
・X_train.extend(各学習データ)で各学習データを格納。

## コード③
```ruby
# 正解ラベルを作成
y_train = []
i = 0
while i < 10:
  y_train.extend([i] * train_range)
  i += 1
```
上記、
・y_train = []で空の正解ラベルの配列を作成。
・y_train.extend([i] * train_range)でラベルを作成。
正解ラベルは斑点病=1,モザイク病=2のように数字形式で割り当てる。

## コード④
```ruby
#検証データ作成
X_test = []
# 画像フォルダ取得
test_folders = glob('/content/drive/MyDrive/archive/New Plant Diseases Dataset(Augmented)/New Plant Diseases Dataset(Augmented)/valid/*')
```
上記、
・glob()でフォルダを指定しなおす。(使用する画像を格納するフォルダが学習データのものと異なるため)
・学習データと同様に検証データを作成。

# 4:モデルの学習と保存
```ruby
X_train = np.array(X_train)
y_train = np.array(y_train)
X_test = np.array(X_test)
y_test = np.array(y_test)

from tensorflow.keras.utils import to_categorical
# 正解ラベルをone-hotの形にします
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# モデルの学習
model.fit(X_train, y_train, batch_size=100, epochs=40, verbose=1)

score = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', score[0])
print('Test accuracy:', score[1])


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

files.download( '/content/results/model.h5' )
```
モデルの学習を行い、重みを保存します。
コードの説明をしていきます。

## コード①
```ruby
X_train = np.array(X_train)
y_train = np.array(y_train)
X_test = np.array(X_test)
y_test = np.array(y_test)

from tensorflow.keras.utils import to_categorical
# 正解ラベルをone-hotの形にします
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
```
上記、
・学習の為に各配列をnp.arrayの形に整形します。
・y_train = to_categorical(y_train)でクラス ベクトル (整数) をバイナリ クラス行列に変換します。
・y_testも同様に変換。

### 参考文献:
[tf.keras.utils.to_categorical ](https://www.tensorflow.org/api_docs/python/tf/keras/utils/to_categorical)

## コード②
```ruby
# モデルの学習
model.fit(X_train, y_train, batch_size=100, epochs=40, verbose=1)

score = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', score[0])
print('Test accuracy:', score[1])


import os
from google.colab import files
#resultsディレクトリを作成
result_dir = 'results'
if not os.path.exists(result_dir):
    os.mkdir(result_dir)
# 重みを保存
model.save(os.path.join(result_dir, 'model.h5'))
```
上記、
・model.fit()でモデルの学習を行います。
・今回は評価が90%の辺りで重みを保存しました。

# 5:HTML作成(参考までに)
コードの参考は、AidemyのFlask入門です。
```ruby
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Tomato_Leaf Disease Diagnosis</title>
    <link rel="stylesheet" href="./static/stylesheet.css">
</head>
<body>
    <header>   
        <img class="header_img" src="../static/sample_img.jpg" alt="tomato_leaf disease" widht="24px" height="17px">
        <a class="header-logo" href="#">Tomato_Leaf Disease Diagnosis</a>
    </header>

    <div class="main">    
        <h2> AIが送信されたトマト葉の画像から病気の診断を行います。</h2>
        <p>診断したい画像を選択してください。</p>
        <form method="POST" enctype="multipart/form-data">
            <input class="file_choose" type="file" name="file">
            <input class="btn" value="submit!" type="submit">
        </form>
        <div class="answer">
            <font color="red">{{answer}}</font>
        </div>
    </div>

    <footer>
        <img class="footer_img" src="../static/sample_img.jpg" alt="tomato_leaf disease" widht="24px" height="17px">
        <small>&copy; 2019 Aidemy, inc.</small>   
    </footer>
</body>
</html>
```

# 6:main.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 = ["0","1","2","3","4","5","6","7","8","9"]
leaf_classes = ["斑点病", "モザイク病", "葉巻病", "ハダニ被害", "葉かび病", "白星病", "斑点細菌病", "輪紋病", "疫病", "健康態"]
image_size = 224

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('./model.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, target_size=(image_size,image_size))
            img = image.img_to_array(img)
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()

            if predicted == 9:
                pred_answer = "これは「" + leaf_classes[predicted] + "」です。"
            else:
                pred_answer = "これは「" + leaf_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)
```
# 7:実装結果
画像にもよりますが、診断結果を表示できるようになりました。
![アプリ実行.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787057/5e38b73c-ff73-df95-2f0e-2bedde336bee.png)
・学習に用いる画像のサイズを大きくする。
・model.add()でモデルの正規化行う。
をすることでより正確な診断を行える可能性があります。

# まとめ
画像認識を用いたAIアプリの作成ができるようになりたいということ。またこれからも増えていくであろう農畜産へのAIの導入に興味を持ったことから、今回、「画像認識を用いたトマトの葉の病気診断アプリ」を作成してみました。
試作という意味では悪くないものでしたが、step5でも書いたように
・学習に用いる画像のサイズを大きくする。
・model.add()でモデルの正規化行う。
など、まだまだ、精度を上げることは可能だと感じています。
また、農畜産の分野へのAIの導入を考えると、もっと複雑な処理が必要となってくるでしょう。
今回のアプリ作成で、その入り口に触れられるだけの知識は得られたと思います。
今後はこの知識を基に、畜産分野へのAIの導入に参画するべく、機械学習、AIに関する知識を増やしていけたらと考えています。