AIによる青果物の鮮度識別アプリ

# はじめに
はじめまして。AIやDX、プログラミングに興味を持ち、AidemyさんのAIアプリ開発講座を受講している管理栄養士です。
成果物として食品の検品をするようなアプリを作りたいと思い、その作成した過程について書きたいと思います。

# 本記事の概要
青果物5種類（りんご・バナナ・ピーマン・オレンジ・トマト）が新鮮か腐敗しているかを識別する下記のアプリを作成しました。
![fruits-vegetables-freshness.herokuapp.com_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2731161/6c987d10-6b62-7e06-cb45-54932ab76683.png)

# 実行環境
MacBook Air (M1, 2020)(Monterey12.4)
Google Colaboratory

# 作成したプログラムについて
### データの準備
データセットを使ってみたかったので、いくつかのサイトを見てみて[kaggle](https://www.kaggle.com/datasets)で以下をダウンロードしました。

https://www.kaggle.com/datasets/raghavrpotdar/fresh-and-stale-images-of-fruits-and-vegetables

このデータセットには、りんご、バナナ、ゴーヤ、ピーマン、オレンジ、トマトの6種類のそれぞれ新鮮なものと古いものの2つのカテゴリの画像が入っています。
ゴーヤの写真は、海外の品種なのか日本のスーパーに売っているものと比べて短くて丸っこく、形状がわりと異なって見えたため、うまく画像判定できない可能性を考えて除外しました。
青果物５種で写真は合計約14,000枚でした。
読み込んだ画像のディレクトリ構造は以下の添付画像の通りです。
![スクリーンショット 2022-07-17 23.37.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2731161/c14af700-4195-6a32-e49d-4c298e97001d.png)
画像への正解ラベルをつけ方や学習モデルなど、講師の方にアドバイスいただき、
Aidemy卒業生の方の下記のブログを参考にさせていただきました。

https://qiita.com/KK_Ken/items/97f382c632e93eb7a239

ラベルの付け方として、tf.kerasのフォルダ名から自動的にラベルをつける関数image_dataset_from_directoryも教えていただきました。
[Tensorflowチュートリアル](https://colab.research.google.com/github/tensorflow/docs-l10n/blob/master/site/ja/tutorials/images/classification.ipynb?hl=ja)

今回は卒業生の方のfor文を参考にさせていただきました。
```python
# 正解ラベル格納リスト
labels = []
for i,f_v_name in enumerate(fru_vege):
    numbers = len(make_path(f_v_name))
    labels += [i]*numbers
```
### モデルの構築
青果物の種類と鮮度という2つのフェーズの判別を行いたいわけですが、
１度の学習でまとめていけるのか、それとも２段階の学習モデルにするべきなのかと考えましたが、とりあえず試しに1回の学習でやってみました。

モデルは、精度向上のため、学習済みモデル[VGG16](https://keras.io/ja/applications/#vgg16)を使って新たなモデルの学習を行う転移学習を用いました。
<details><summary>VGG16の詳細についてはこちらをクリック</summary>

VGGモデルは、2014年のILSVRCという大規模な画像認識のコンペティションで2位になった、オックスフォード大学VGG(Visual Geometry Group)チームが作成したネットワークモデル。小さいフィルターを使った畳み込みを2〜4回連続で行いさらにプーリングする、というのを繰り返し、かなり層を深くしてあるのが特徴。VGGモデルには、重みがある層(畳み込み層と全結合層)を16層重ねたものと19層重ねたものがあり、それぞれVGG16やVGG19と呼ばれる。1000クラスの分類モデルなので出力ユニットは1000個あるが、最後の全結合層は使わずに途中までの層を特徴抽出のための層として使うことで、転移学習に用いることができる。また、入力画像のサイズも気にする必要はありません。

</details>

```python
# VGG16のインスタンス生成
input_tensor = Input(shape = (50, 50, 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(0.1))
top_model.add(BatchNormalization())
top_model.add(Dense(512, activation = 'relu'))
top_model.add(Dropout(0.1))
top_model.add(BatchNormalization())
top_model.add(Dense(10, activation = 'softmax'))

# モデルの連結
model = Model(inputs = vgg16.input, outputs = top_model(vgg16.output))

# VGG16の重みの固定
for layer in model.layers[:15]:
    layer.trainable = False
```
### モデルの学習・評価
```python
history = model.fit(X_train, y_train, validation_data = (X_test, y_test), batch_size =128, epochs =40)
```
結果、正解率は90%を超えました！
AIってエラい!!:sparkles:
epoch数は37くらいでほぼ正解率が頭打ちのようだったので、40にしました。
![スクリーンショット 2022-07-20 0.27.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2731161/6c986404-524a-1990-c2d1-3bc5e87baf20.png)
ネットワーク構造のユニット数とDropoutのrate、学習のbatch_sizeもチューニングし、最終的に正解率は98％になりました:blush:



<details><summary>最終的なコードはこちらをクリック</summary>

```python
import os
from google.colab import files
import cv2
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input, BatchNormalization
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers

!sudo apt install tree
!tree -d /content/drive/MyDrive/archive

#パスに使用する用の青果物の名前リスト
fru_vege=['新鮮なりんご','新鮮なバナナ','新鮮なピーマン','新鮮なオレンジ','新鮮なトマト','古いりんご','古いバナナ','古いピーマン','古いオレンジ','古いトマト']

# 画像ファイルのパス作成関数
def make_path(dir):
    path_name = os.listdir('/content/drive/MyDrive/archive/{}/'.format(dir))
    return path_name

# 画像ファイルの一時保管用リスト
pre_list = [[] for i in range(10)]

# 画像ファイルの読み込み    
for n, f_v_name in zip(range(10), fru_vege):
    numbers = len(make_path(f_v_name))
    for i in range(numbers):
        img = cv2.imread('/content/drive/MyDrive/archive/{}/'.format(f_v_name) + make_path(f_v_name)[i])
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        img = cv2.resize(img, (50, 50))
        pre_list[n].append(img)

# 青果物の種類ごとに分けられていた画像ファイルを1つのリストに統合する
img_list = []
for i in range(10):
    img_list += pre_list[i]

# 正解ラベル格納リスト
labels = []
for i,f_v_name in enumerate(fru_vege):
    numbers = len(make_path(f_v_name))
    labels += [i]*numbers

# 画像データをnumpy配列に変換
X = np.array(img_list)
y = np.array(labels)

# 画像データをシャッフル
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)

# VGG16のインスタンス生成
input_tensor = Input(shape = (50, 50, 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(0.1))
top_model.add(BatchNormalization())
top_model.add(Dense(512, activation = 'relu'))
top_model.add(Dropout(0.1))
top_model.add(BatchNormalization())
top_model.add(Dense(10, activation = 'softmax'))

# モデルの連結
model = Model(inputs = vgg16.input, outputs = top_model(vgg16.output))

# 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'])

# 学習の実行
history = model.fit(X_train, y_train, validation_data = (X_test, y_test), batch_size =128, epochs =40)

#resultsディレクトリを作成
result_dir = 'results'
if not os.path.exists(result_dir):
    os.mkdir(result_dir)

# 学習モデルを保存
model.save(os.path.join(result_dir, "f_vmodel.h5"))
files.download( '/content/results/f_vmodel.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()
```
</details>


# Webページの作成（HTML・CSS・FLASK）
画像を挿入したかったので、[pixabay](https://pixabay.com/ja/)というサイトでおしゃれな雰囲気の野菜の画像をダウンロードしました。
識別できる青果物の種類が写真と違うじゃないか！という点は
雰囲気ということで、ご容赦ください。

<details><summary>HTMLのコードはこちらをクリック</summary>

```rb
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./static/stylesheet.css">
</head>
<header>
    <h1>青果物の腐敗識別</h1>
</header>

<body>
    <div class="main">
        <img class="title" src="./static/vegetables.jpg">
        <h2>AIが青果物が新鮮か腐敗しているかの判別をします</h2>
        <p class='img_send'>青果物（りんご・バナナ・ピーマン・オレンジ・トマト）の画像を送信してください</p>
        <form method="POST" enctype="multipart/form-data">
            <input class="file_choose" type="file" name="file">
            <input class="btn" value="送信" type="submit">
        </form>
        <div class="answer">{{answer}}</div>
    </div>

    <footer>
        <small>&copy; 2022 T.Horii</small>
    </footer>
    
</body>

</html>
```
</details>

<details><summary>CSSのコードはこちらをクリック</summary>

```rb
header {
    background-color: #432f2f;
    height: 60px;
    margin: -8px;
    text-align:left;
}

h1{
    color: #fff;
}

img{
    width: 100%;
    height: auto;
}

body{
    background-color:#a2d7dd
}

h2{
    color: #444444;
    text-align: center;
}

p {
    color: #444444;
    margin: 50px 0px 30px 0px;
    text-align: center;
}

.answer {
    color: #444444;
    margin: 70px 0px 30px 0px;
    text-align: center;
}

form {
    text-align: center;
}

footer {
    background-color: #432f2f;
    height: 30px;
    margin: -2px;
    text-align:right;
}

small {
    color: #fff;
}
```
</details>

<details><summary>FLASKのコードはこちらをクリック</summary>

```rb
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 = ["新鮮なりんご","新鮮なバナナ","新鮮なピーマン","新鮮なオレンジ","新鮮なトマト","古いりんご","古いバナナ","古いピーマン","古いオレンジ","古いトマト"]
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('./f_vmodel.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()
            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)
```
</details>

# 今後の活用
参考にさせていただいたコードの精度とデータセットの汎用性が高かったのか、精度の良いモデルにできたと思います。
さらに精度を向上させるためには、ぼかし画像を水増しすれば不明瞭な画像にも対応できるのかもしれません。
また、青果物の種類や鮮度の段階の画像を増やせば、腐敗前にアプリ等でアラートでお知らせしてくれる冷蔵庫もできるんじゃないか!?と思いました。適切な青果物の保存方法をおすすめしてくれる機能などもあるともっと便利な気がします。
それから、Webページのデザインは、さらにHTMLやCSSを学習すればブラッシュアップできる気がします。


# おわりに
アプリが完成した時は、達成感とともにとても嬉しかったです:blush::sparkles:
コードの初歩的な所で躓いたことも何度もありましたが、親切に教えてくださった講師の皆様へ心より感謝申し上げます。本当にありがとうございました。
憧れのIT・AIに強い栄養士を目指して、データサイエンスや自然言語処理なども学んでいきたいと思います。