トマトの画像をマルチクラス分類してみた【Segmentation models】

AI実装

トマトの画像を収穫時期別にセマンティックセグメンテーションしていくタスクを紹介します。Segmentation modelsを用いて収穫時期にラベル付けした多クラスの分類していきます。

Segmentation modelsはTensorFlow==1.13を推奨していますが、今回はSegmentation modelsのコードをアップグレードしてTensorFlow2.6環境で実装していこうと思います。

2値分類については過去の記事で紹介していますので、よかったら併せてご覧ください↓。

学習環境

ローカル環境でGPUをつかって実装していきます。CUDA toolkit関連セットアップが分からない場合はこのサイトに細かく掲載されていますので確認してみてください。

PC環境

  • OS : Windows 10
  • CPU : AMD Ryzen7 5800
  • メモリ : 16GB
  • GPU : GeForce RTX3070 8GB

CUDA関連

  • CUDA : 11.4
  • cuDNN:8.2

Python

  • Python 3.7.10

ライブラリ

  • tensorflow-gpu 2.6.0
  • tensorflow-estimator 2.6.0
  • tensorboard 2.6.0
  • image-classifiers 1.0.0
  • efficientnet 1.0.0
  • その他必要に応じて

Segmentation modelsのコードをTensorFlow2へアップグレードする

アップグレードやり方を説明していきます。

  • Githubよりsegmentation modelsのソースコード↓をダウンロードしてください。ダウンロードしたらデスクトップへ解凍してください。
GitHub - qubvel/segmentation_models: Segmentation models with pretrained backbones. Keras and TensorFlow Keras.
Segmentation models with pretrained backbones. Keras and TensorFlow Keras. - GitHub - qubvel/segmentation_models: Segmentation models with pretrained backbones....
  • TensorFlow2.xをインストールした環境をアクティベートして↓のコードをAnacondaPromtなどで実行してください。保存するディレクトリをあらかじめつくらないように注意してください。
# segmentation_modelsをデスクトップへ置いた場合の例
cd desktop
tf_upgrade_v2 --intree segmentation_models --outtree segmentation_models_tf2 --reportfile report.txt

実行するとsegmentation_models_tf2というディレクトリが生成されると思います。ここを作業ディレクトリとしてつかっていきます。

アップグレードは別記事で紹介していますので詳しくは↓をご覧ください。

データセットの準備

独自のトマト画像へアノテーションしていきます。データセットの準備手順は↓です。

  • 独自の画像を用意
  • labelmeでアノテーションしてjsonファイルを作成
  • jsonファイルをセマンティックセグメンテーション用のデータセットへ変換

labelmeは↓からダウンロードしてください。上記処理に必要となります。

GitHub - wkentaro/labelme: Image Polygonal Annotation with Python (polygon, rectangle, circle, line, point and image-level flag annotation).
Image Polygonal Annotation with Python (polygon, rectangle, circle, line, point and image-level flag annotation). - wkentaro/labelme

labelmeでアノテーションしてjsonファイルを作成

labelmeのつかい方、jsonファイルの作成は↓に詳細がありますのでご覧ください。

今回のラベルは↓にしています。

  • 完熟した収穫時期    : A
  • もう少ししたら収穫時期 : B
  • 未熟          : C

jsonファイルをセマンティックセグメンテーション用のデータセットへ変換

セマンティックセグメンテーション用のデータセットであるラベリングしたpngファイルをつくっていきます。↓のようなペア画像です。

imgデータ(.jpg)
アノテーションデータ(.png)
  1. まずはimgデータ(.jpg)とアノテーションデータ(.json)ファイルをlabelmeの↓ディレクトリへ格納してください。
labelme
     -> examples
        -> semantic_segmentation
           -> train #名前は任意です
              1.jpg
              1.json
              2.jpg
              2.json
              3.jpg
              3.json
              ...
  1. semantic_segmentationディレクトリ下にlabet.txtというファイルがあると思います。これをアノテーションした内容へ変換します。今回の場合は↓になります。label.txtを↓へ書き換えて保存すればOKです 。labelmeでラベリングするときに生成されたモノでもOKです。
__ignore__
_background_
A
B
C
  1. AnacondaPromptなどで↓のコードを実行してください。
cd Desktop/labelme/examples/semantic_segmentation #labelmeをデスクトップへ保存した場合
python labelme2voc.py train data_dataset --labels labels.txt

実行すると変換が始まり、data_datasetというディレクトリが生成されると思います。中に「JPEGImage」「SegmentationClassPNG」が入っていればOKです。data_datasetディレクトリを segmentation_models_tf2 /dataディレクトリへ↓のように移動してください。

 segmentation_models_tf2
     -> data
        -> data_dataset

以上で準備は完了です。

アノテーションデータ(.png)について少し説明

データの準備とは関係ありませんが、多クラスのセマンティックセグメンテーションを実施するために、なぜアノテーションデータ(.png)が必要になるか簡単に説明します。

実はアノテーションデータは.pngで出力することに意味があります。このアノテーションデータはインデックスカラーをつかって作成されています。

インデックスカラーとは色が順番に並んだテーブルのようになっています。0:黒、1:赤、2:緑のような形で表現されています。この先でも説明しますが.pngデータをインデックスカラーで読み込むことでクラス分けできる仕組みになっています。

↓に詳しく説明されています。興味ある方はご覧ください。

「画像変換101」#2: ダイレクトカラー画像とインデックスカラー画像 - OPTPiX Labs Blog
ここでは分かりやすく、ウェブの世界に限定して話しを進めたいと思います。 ウェブで使用される画像は「ダイレクトカラー画像」と「インデックスカラー画像」の 二種類に大別することができます。 ダイレクトカラー画像 ダイレクトカ … 続きを読む →

セマンティックセグメンテーションの実装

いよいよ実装していきます。全コードは一番↓に記載します。ポイントをピックアップして説明します。

  • フレームワークをkeras => tf.kerasで動かす

Segmentation modelsはデフォルトでkerasフレームワークをつかおうとしますが旧バージョンのkerasでしか上手く動かないので、TensorFlowに組み込まれたtf.kerasで動かします。↓のコードを入れればOKです。

import segmentation_models as sm
sm.set_framework('tf.keras')
sm.framework()
  • 教師データを”パレットモード”へ変換しインデックスカラーフォーマットとする

.pngファイルのインデックスカラーを利用しクラス分類するため次のコードでデータ変換します。

dir ="./data/data_dataset/SegmentationClassPNG"
files = glob.glob(dir+"/*.png")
for i, file in enumerate(files): 
    image = Image.open(file) #画像の読み込み
    image = image.convert("P") #パレットモード(インデックスカラー)へ変換
  • データの可視化を必ずやっておく

多クラスのセマンティックセグメンテーションを実施する場合、教師データ(Y_train)のデータ形状が↓の感じになり、そのままでは可視化ができません。

Y_train.shape

(***, 320, 224, 4)

きっちり教師データ(マスク)が作成されているか↓のコードで確認しましょう。Segmentation modelsのexampleコードです。

def visualize(**images):
    n = len(images)
    plt.figure(figsize=(32, 16))
    for i, (name, image) in enumerate(images.items()):
        plt.subplot(1, n, i + 1)
        plt.xticks([])
        plt.yticks([])
        plt.title(' '.join(name.split('_')).title())
        plt.imshow(image)
    plt.show()

i = 0 #何枚目の画像を確認するか変更できます

mask = Y_train[i] # get some sample
visualize(
    image=X_train[i], 
    background_mask_mask=mask[..., 0].squeeze(),
    A_mask=mask[..., 1].squeeze(),
    B_mask=mask[..., 2].squeeze(),
    C_mask=mask[..., 3].squeeze(),
)

ここできちんと狙い通りマスクされているかクラスとマスクを照らし合わせて確認してください。

  • 損失関数LossでDice係数/metricsでIoUを使用

多クラス分類の場合、通常はCategoricalCrossEntropyを用いると思いますが、セマンティックセグメンテーションではDice係数がよく用いられます。今回はDice係数を使用しています。

また評価指標としてはIoUを選択しています。CNNではAccuracyをつかうと思いますが、セマンティックセグメンテーションでAccuracyをつかうとすぐに100%付近まで精度が出てしまうので評価できません。IoUをつかうのが一般的です。コードは↓です。

dice_loss = sm.losses.DiceLoss()
metrics = [sm.metrics.IOUScore(threshold=0.5)]
model.compile(optimizer=opt, loss=dice_loss, metrics=metrics) 

Dice係数、IoUは↓のサイトに詳しく記載されていました。併せてご覧ください。

  • 学習条件
    • セマンティックセグメンテーションモデル:U-Net
    • バックボーン:Efficient Net B7
    • 学習済み重み:imagenet
    • Optimiser : Adam(learning_rate=1e-3)
    • Batch_size : 1
    • epochs : 100

学習結果

学習曲線は↓の感じです。

学習はうまく進んだと思います。Lossグラフより10エポックくらいから過学習気味になっていると思います。

セマンティックセグメンテーションはよく過学習します。一般的には画像枚数を増やして対応するのが良いのですが、私の経験ではある程度過学習しても学習を継続する方がいいです。学習を早くやめてしまうとノイズだらけで使いモノになりませんが、一方である程度過学習してても使えることがおおいです。ですが学習をやりすぎると汎用性が落ちますので、この辺りのさじ加減はCNNよりも難しいです。

結果を可視化します。見づらくてすみませんが、左3つのマスク画像が正解ラベル、右3つがセマンティックセグメンテーションモデルによる予測結果です。そこそこいい感じのモノになったのではないでしょうか。

皆さんも試してみてください。良いディープラーニングライフを👍

全コード

今回実装したコードを載せておきます。参考にしてください。

学習データセット

#ライブラリインポート
import tensorflow as tf
import segmentation_models as sm
sm.set_framework('tf.keras')
sm.framework()
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
import numpy as np
import matplotlib.pyplot as plt
import glob
from PIL import Image
from tensorflow.python.keras.utils import np_utils
from sklearn.model_selection import train_test_split
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

#データセット
x_image_size = 224 #インプット画像のサイズ
y_image_size = 320 #インプット画像のサイズ
X = []
Y = []
#画像データ
dir ="./data/data_dataset/JPEGImages"
files = glob.glob(dir+"/*.jpg")
for i, file in enumerate(files): 
    image = Image.open(file) #画像の読み込み
    image = image.convert("RGB") #RGBに変換
    image = image.resize((x_image_size, y_image_size)) #サイズ変更
    data = np.asarray(image) #画像データを配列へ変更
    X.append(data) #画像データを繰返し追加
X = np.array(X)
#アノテーションデータ(マスクデータ)
dir ="./data/data_dataset/SegmentationClassPNG"
files = glob.glob(dir+"/*.png")
for i, file in enumerate(files): 
    image = Image.open(file) #画像の読み込み
    image = image.convert("P") #パレットモード(インデックスカラー)へ変換
    image = image.resize((x_image_size, y_image_size)) #サイズ変更
    data = np.asarray(image) #画像データを配列へ変更
    Y.append(data) #画像データを繰返し追加
Y= np_utils.to_categorical(Y,4)
X_train, X_valid, Y_train, Y_valid = train_test_split(X, Y , test_size=0.2)

教師データの可視化

def visualize(**images):
    n = len(images)
    plt.figure(figsize=(32, 16))
    for i, (name, image) in enumerate(images.items()):
        plt.subplot(1, n, i + 1)
        plt.xticks([])
        plt.yticks([])
        plt.title(' '.join(name.split('_')).title())
        plt.imshow(image)
    plt.show()

i = 0 #何枚目の画像を確認するか変更できます

mask = Y_train[i] # get some sample
visualize(
    image=X_train[i], 
    background_mask_mask=mask[..., 0].squeeze(),
    A_mask=mask[..., 1].squeeze(),
    B_mask=mask[..., 2].squeeze(),
    C_mask=mask[..., 3].squeeze(),
)

セマンティックセグメンテーションモデルの定義

BACKBORN = 'efficientnetb7'
preprocess_input = sm.get_preprocessing(BACKBORN)
model = sm.Unet(BACKBORN, 
                encoder_weights='imagenet', 
                classes = 4, 
                encoder_freeze = True,
                input_shape=(320, 224, 3))

#Dice係数/IoU
opt = tf.keras.optimizers.Adam(learning_rate=1e-3)
dice_loss = sm.losses.DiceLoss()
metrics = [sm.metrics.IOUScore(threshold=0.5)]
model.compile(optimizer=opt, loss=dice_loss, metrics=metrics) 

#チェックポイント作成
checkpoint_filepath = './tmp/checkpoint'
model_checkpoint_callback = tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_filepath,
    save_weights_only=True,
    monitor='val_iou_score',
    mode='max',
    save_best_only=True)

#学習
history =model.fit(X_train, Y_train, batch_size=1, epochs=100, 
                   validation_data=(X_valid, Y_valid),
                   callbacks=[model_checkpoint_callback])

結果の可視化

#学習曲線
score = model.evaluate(X, Y, verbose=0)
print("evaluate loss: {0[0]}\nevaluate acc: {0[1]}".format(score))
fig, ax = plt.subplots(1, 2, figsize=(12, 6))
ax[0].set_title('Training performance (Loss)')
ax[0].plot(history.epoch, history.history['loss'], label='loss')
ax[0].plot(history.epoch, history.history['val_loss'], label='val_loss')
ax[0].set(xlabel='Epoch', ylabel='Loss')
ax[0].legend()

ax[1].set_title('Training performance (iou_score)')
ax[1].plot(history.epoch, history.history['iou_score'], label='iou_score')
ax[1].plot(history.epoch, history.history['val_iou_score'], label='val_iou_csore')
ax[1].set(xlabel='Epoch', ylabel='iou_score')
ax[1].legend(loc='best')
#予測結果の可視化
n = 3 #結果の表示数
ids = np.random.choice(np.arange(len(X_valid)), size=n)
for i in ids:   
    image = X_valid[i]
    gt_mask = Y_valid[i]
    image = np.expand_dims(image, axis=0)
    pr_mask = model.predict(image)  
    
    visualize(
        image=image.squeeze(),
        A_Ground_Truth=gt_mask[..., 1].squeeze(),
        B_Ground_Truth=gt_mask[..., 2].squeeze(),
        C_Ground_Truth=gt_mask[..., 3].squeeze(),
        #background_mask=pr_mask[..., 0].squeeze(),
        A_Prediction=pr_mask[..., 1].squeeze(),
        B_Prediction=pr_mask[..., 2].squeeze(),
        C_Prediction=pr_mask[..., 3].squeeze(),
    )

関連図書

↓の本を読んでディープラーニングを勉強しました。ご参考に👍

コメント

タイトルとURLをコピーしました