なつめの実の良し悪しを見分けるWebアプリ

:::note info
このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています。
:::

# はじめに

ここではなつめの実を食用に適するか判定するソフトについて紹介します。

**作成したアプリのコード**

https://github.com/YoheiKunita/natsume_app

## なつめの紹介
　2023年の秋、家の畑に生えているなつめを収穫しました。なつめの木といえば街路樹や植木として全国に植えられていますが、一般的にはなつめの実が食べられることはないそうです。原産国の中国では主に干した実を砂糖漬けにしたり、餡にして食べられているそうです。

　日本で食用としているのは、私が住んでいる飛騨地方のみで、半熟の実を甘露煮にして食べるのがポピュラーです。頻繁に食べるわけではないですが、全国的に食べられていると思っていたので意外でした。マイナーな郷土料理と聞くとなんだか愛着が湧いてきます。秋のシーズンになると、なつめの実は道の駅などで販売されます。１袋１キロ入りで８００円前後と、そこそこ高値で売れるので根強い人気があるみたいですね。

http://gifu-kiwami.jp/products/57/

## なつめを食べるまで
　食べるまでになかなか手間がかかります。まず枝の選定を兼ねて枝ごと切り落とした後、実を一粒ずつ外します。葉っぱと実を分けたのち、虫食いや割れた実をより分けます。特に今年は道の駅へ出品したので、一粒ずつ入念により分けました。ここまでが出荷前の作業です。購入した家庭では、実に残っているヘタをつまようじで取ります。料理法としては砂糖で甘辛く煮ます。好みに応じて醤油を入れる場合もあるようです。
 
　このように最初の収穫から食べられるようになるまで、さまざまな手間がかかります。畑の隅に生えている木１本程度でしたが、全部で１２kg、１袋１キロの中に700～800粒ほど入っていたので、全部で1万粒くらい収穫できました。袋詰めまでで丸１日はかかったと思われます。特に悪くなっている実の選定が大変でした。
　
# 目標
　紹介した通り食べるまでに手間がかかるため、なんとか効率化できないかなと考えました。特に実の選別は１粒ずつ手で行う必要があるのでとても時間がかかります。そこでAidemy様の最後の課題として実の選別を行うアプリを作成することにしました。

　目標として、なつめの実が１つ写った画像から実の良し悪しを判定するアプリとしました。アプリはブラウザ上で動作し、ユーザーがなつめの実が写った画像をアップロードすると判定を行うものを作ります。

# 実行環境
* Html, CSS
* Render
* tensorflow 2.14
* python 
* keras 2.14
 
# アプリの紹介
①教師用データの作成
②kerasを用いて判別モデルを作成（「男女識別(深層学習応用)」を変更)
③HTML＋CSSでアプリを作成(「Flask入門」を変更。③も同じ。）
④Render上にデプロイ

※プログラムはAidemy様の講義で作成したものを変更して作りました。

**男女識別(深層学習発展)**

https://premium.aidemy.jp/courses/6100

**Flask入門**

https://premium.aidemy.jp/courses/1030

## ①教師データの作成
　収穫したナツメの実を食用に適するものと適さないものに分別し、スマホで撮影します。
 Ⅰ．ナツメの実を大量に撮影する、Ⅱ．学習用データに加工するの２段階に分けて作業を行いました。

### Ⅰ．ナツメの実を大量に撮影する
　教師用データになつめの実を大量に撮影する必要があります。のちの処理を効率よく進めるためになつめの実を規則正しく、効率よく並べることを考えました。
　なつめはたわら型なので桝形に区切った枠に転がしてあげれば大まかに並べることができるので、細かい節を手で行えば一度に複数の実を撮影することができます。今回は20cm四方の板に木の棒を等間隔に貼り付け、分割処理をしやすいように枠に青色のラインを引いたものを作成しました。（下図左）6列×6行で1回につき36個のなつめの実を撮影することができます。
　また、学習用データとしてはナツメとカメラの距離・角度はできるだけ一定にすることが望ましいので、撮影用の台として植木鉢用のプラスチック棚を用意しました。（下図右）棚の上段にスマホを置き、下段に板をセットすれば撮影準備完了です。あとはひたすらなつめをセットしては撮影していきます。
　今回は良い実を14枚、悪い実を9枚撮影しました。1粒に換算するとよい実で504枚、悪い実で324枚となります。後ほどデータ量が少ないことが分かりましたが、出荷まで時間がなかったためあまり多く撮影できませんでした。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577668/a155d734-76da-d41a-69e9-509e5835fa64.jpeg">

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577668/8cd59df7-c6fe-a10a-2e0e-733fe899ca05.jpeg">

### Ⅱ．学習用データに加工する
　①の画像からなつめの実を1粒につき1枚の画像データに分けます。今回はOpenCVを使用して画像を等間隔に分割するプログラムを作成しました。アルゴリズムはいたって単純で、読み込んだ画像を等間隔に６×６に分離して保存します。下処理として分割する前に撮影した画像から余分な部分をトリミングしておきます。実行すると1枚の元画像(下図左)から教師用データ(下図右)36枚を作成することができました。
 
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577668/5c7fd138-6a4b-97ef-dda4-6236d1060458.jpeg">

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577668/e25ea155-7e32-9633-4de7-894c16a457a8.jpeg">

<details><summary>クリックしてコードを表示</summary>

```python:natsume_image_split.py
import os
from pathlib import Path
import matplotlib.pyplot as plt
import cv2
import numpy as np
from tkinter import filedialog

#画像の分割
def image_spliter(output_path, ind, file_name):
    # 画像ファイルの読み込み
    img = cv2.imread(file_name)

    # 画像の表示
    rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # bgr順→rgb順
    plt.imshow(rgb_img)
    plt.show()

    rows = 6  # 行数
    cols = 6  # 列数
    chunks = []
    for row_img in np.array_split(img, rows, axis=0):
        for chunk in np.array_split(row_img, cols, axis=1):
            chunks.append(chunk)

    # 画像の保存
    for i, chunk in enumerate(chunks):
        save_path = f"{output_path}natsume_{ind:02d}_{i:03d}.jpg"
        cv2.imwrite(str(save_path), chunk)
        print(file_name, save_path)

path = os.getcwd()
output_path = f"{path}\\aidemy_git\image_split\output\\"
input_path = f"{path}\\aidemy_git\image_split\input\\"

typ = [('JPEG', '*.jpg;*.jpeg;')]
fle = filedialog.askopenfilenames(filetypes = typ, initialdir = input_path) 

#画像の分割を実行
for i , file_name in enumerate(fle):
    image_spliter(output_path, i, file_name)
```
</details>

以下のURL参考にしました。

https://pystyle.info/opencv-split-and-concat-images/

## ②kerasを用いて判別モデルを作成
　Aidemy様の講座にて作成した男女識別モデルを変更して、出力が1ビットの判別モデルを作成しました。良い例として500枚、悪い例として288枚の画像を用意して、全体の80%を学習用データ、残りをテスト用データにしました。また、VGG16を転移学習します。正答率0.6～0.8でモデル作成ができました。

<details><summary>クリックしてコードを表示</summary>

```python:make_natsume_model.py
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
from keras.api._v2 import keras
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

os.chdir(Path(f'C:/Users/water/aidemy_git/image_split'))
print("current_directry:" + os.getcwd())

# お使いの仮想環境のディレクトリ構造等によってファイルパスは異なります。
path_good = os.listdir('./output/good/')
path_bad  = os.listdir('./output/bad/')

img_good = []
img_bad = []
img_w = 150
img_h = 150

for i in range(len(path_good)):
    img = cv2.imread('./output/good/' + path_good[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (img_w,img_h))
    img_good.append(img)

for i in range(len(path_bad)):
    img = cv2.imread('./output/bad/' + path_bad[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (img_w,img_h))
    img_bad.append(img)

X = np.array(img_good+img_bad)
y = np.array([0]*len(img_good) + [1]*len(img_bad)) #教師データ 男性:0 女性:1

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):]

# vgg16のインスタンスの生成
input_tensor = Input(shape=(img_w,img_h, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

# vgg16のインスタンスの生成
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dense(8, activation='relu'))
top_model.add(Dense(1, activation='sigmoid'))

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

# vgg16の重みの固定
for layer in model.layers[:19]:
    layer.trainable = False

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.SGD(learning_rate=1e-4, momentum=0.9),
              metrics=['accuracy'])

model.fit(X_train, y_train, batch_size=10, epochs=1)

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

def check_natsume(img):
    #変換したデータをモデルに渡して予測する
    reshaped_img = img.reshape(1, img_w, img_h,3)
    if 1 == np.round(model.predict(reshaped_img)):
        print("result predict natsume:良いナツメです") 
    else:
        print("result predict natsume:悪いナツメです") 


# pred_gender関数に写真を渡して予測します。例の場合は悪いナツメ
for i in range(5):
    img = cv2.imread('./output/good/' + path_good[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (img_w,img_h))
    #plt.imshow(img)
    #plt.show()
    check_natsume(img)

model.summary()

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

# 重みを保存
model.save(os.path.join(result_dir, 'natsume_model.h5'))
```
</details>

## ③HTML＋CSSでアプリを作成
　Flask入門で作成したmnistを利用した手書き文字識別ソフトを変更して作成しました。「ファイルを選択」で画像をアップロード、「Submit!」で判別の実行、結果を画面下部に表示します。

**動作画面**
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577668/b2bfddad-25e0-e339-268f-4f79a65416a7.png">

## ④Render上にデプロイ
　③で作成したコードをGitHubへアップロードし、Render上でデプロイしました。課題で作成したコードから表示名称等を変更してあります。

**デプロイしたRender画面**
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577668/ac0a6f8a-0495-a68d-d9a8-6f7ee8cacc26.png">


**アプリへのリンク** ※サーバーが停止しているため動作しません
https://natsume-app.onrender.com/

# 考察・反省
　教師データの用意、機械学習のモデル作成、その他さまざまなフレームワークやサービスを実際に使ういい機会になりました。認識精度に関しては教師データが不足していたため芳しくない結果になったのが残念でした。
* モデル構築～アプリ実装まで一通り実装できた。
* 正答率が60～80%と実用には程遠い。
* モデルが古いため精度が悪かった。
* 教師データの数が少ないため判別精度が悪くなってしまった。

# 今後の展望
基本的なことは一通りできたので、精度の向上や機能の拡張に挑戦していきたいです。
* 教師データの数を増やして正答率を上げる
* ImageDataGeneratorを使用してデータの水増しを行う
* モデルを新しいものに変更する
* 良い実の等級を分類して判定
* 悪い実のデータを増やして悪い実の分析(割れ、虫食い、熟しすぎ など）
* アプリの見た目や機能の充実（複数の実を一度に判定など）