セマンティックセグメンテーション(semantic segmentation)のサンプルコード解説。keras tensorflowを用いたコードで初心者向けです

データサイエンス

目次をクリックすると移動できます

はじめに

画像系AIの中で、最も高度な分析を行っているのが、セマンティックセグメンテーションです。画像内の各画素が何なのかを返してくれます。自動運転などの大規模なプロジェクトで使われていますね。

 今回は、比較的シンプルなセマンティックセグメンテーションのサンプルコードを初心者の方向けに解説していきます。

↓↓↓オリジナルはこちらです。tensorflow kerasですね。U-net のようなモデルを使っています。

Keras documentation: Image segmentation with a U-Net-like architecture
Keras documentation

google colaboで開くと、実行しながら確認出来て便利ですよ。

サンプルコード解説

データのダウンロード

!curl -O https://thor.robots.ox.ac.uk/~vgg/data/pets/images.tar.gz
!curl -O https://thor.robots.ox.ac.uk/~vgg/data/pets/annotations.tar.gz
!tar -xf images.tar.gz
!tar -xf annotations.tar.gz

データをダウンロードします。

google colabo上では先頭に!を忘れずに。

ローカルPCで実行される方は、!を外して、コマンドラインで実行してください。

画像データとマスク(正解の塗り絵画像)ファイルのパス設定

import os

input_dir = "images/"
target_dir = "annotations/trimaps/"
img_size = (160, 160)
num_classes = 3
batch_size = 32

input_img_paths = sorted(
    [
        os.path.join(input_dir, fname)
        for fname in os.listdir(input_dir)
        if fname.endswith(".jpg")
    ]
)
target_img_paths = sorted(
    [
        os.path.join(target_dir, fname)
        for fname in os.listdir(target_dir)
        if fname.endswith(".png") and not fname.startswith(".")
    ]
)

print("Number of samples:", len(input_img_paths))

for input_path, target_path in zip(input_img_paths[:10], target_img_paths[:10]):
    print(input_path, "|", target_path)

画像と答え(mask)のファイルパス設定です。

  • 3行目:画像ファイルパス
  • 4行目:アノテーション(マスク)ファイルパス
  • 5行目:画像サイズ160×160pix。小さめですね。。。実運用では使えないかな。
  • 6行目:クラス数 3。3種類の分類です
  • 7行目:バッチサイズ。一度にメモリにのせて計算させる画像データ数。
  • 9~14行目:画像ファイルのパス のリストを作成しています。
    12行目のos.listdir()でフォルダ内のファイルとフォルダ一覧を作成して、for分を回し
    13行目で最後が.jpgで終わるものだけ残します。
    11行目で、フォルダのパスとファイル名を合体します。
    ここは、glob使った方がいい気がしますね。。。。
  • 16~22行目:マスク画像のファイルパス のリストを作成しています。
    画像ファイルのパスと一緒ですね。
  • 24行目:サンプルファイルの数を表示します。
  • 26行目:画像とマスクのパスを10個まで、まとめてfor文回します。
  • 27行目:画像とマスクのパスを表示します。セットで。

画像を1個開いてみて、セグメンテーションの答えがどんな感じか確認します。

from IPython.display import Image, display
from tensorflow.keras.preprocessing.image import load_img
from PIL import ImageOps

# Display input image #7
display(Image(filename=input_img_paths[9]))

# Display auto-contrast version of corresponding target (per-pixel categories)
img = ImageOps.autocontrast(load_img(target_img_paths[9]))
display(img)
  • 1行目:IPythonの表示関係のライブラリをインポートします
  • 2行目:tensorflowの画像ロードライブラリをインポート
  • 3行目:PillowのImageOpsをインポート。画像補正関連ですね。
  • 6行目:9番目の画像を読み込んで表示します。
  • 9行目:9番目のマスク画像(正解を塗り絵した画像)を読み込みます。コントラスト自動調整を入れています。そのままだと見ずらいのですかね。
  • 10行目:マスク画像表示

データを引き出す便利なクラスを作成(データセットの準備)

from tensorflow import keras
import numpy as np
from tensorflow.keras.preprocessing.image import load_img


class OxfordPets(keras.utils.Sequence):
    """Helper to iterate over the data (as Numpy arrays)."""

    def __init__(self, batch_size, img_size, input_img_paths, target_img_paths):
        self.batch_size = batch_size
        self.img_size = img_size
        self.input_img_paths = input_img_paths
        self.target_img_paths = target_img_paths

    def __len__(self):
        return len(self.target_img_paths) // self.batch_size

    def __getitem__(self, idx):
        """Returns tuple (input, target) correspond to batch #idx."""
        i = idx * self.batch_size
        batch_input_img_paths = self.input_img_paths[i : i + self.batch_size]
        batch_target_img_paths = self.target_img_paths[i : i + self.batch_size]
        x = np.zeros((self.batch_size,) + self.img_size + (3,), dtype="float32")
        for j, path in enumerate(batch_input_img_paths):
            img = load_img(path, target_size=self.img_size)
            x[j] = img
        y = np.zeros((self.batch_size,) + self.img_size + (1,), dtype="uint8")
        for j, path in enumerate(batch_target_img_paths):
            img = load_img(path, target_size=self.img_size, color_mode="grayscale")
            y[j] = np.expand_dims(img, 2)
            # Ground truth labels are 1, 2, 3. Subtract one to make them 0, 1, 2:
            y[j] -= 1
        return x, yd

ここでは、画像と回答(マスク画像)を呼び出すクラスを作成しています。データセットの準備ですね。まだ、kerasやtensorflowでsegmentation向けのジェネレータは実装されていないようですね。。。

  • 1~3行目:tensorflwo,numpy,tensorflowの画像読み込みライブラリインポート
  • 6行目:クラス定義。オックスフォードペット?ですかね?
  • 9~13行目:変数初期値定義。
    9行目:バッチサイズ
    10行目:画像サイズ
    11行目:画像データのパスリスト
    12行目:マスク画像のパスリスト
  • 15~16行目:バッチ数を返す関数定義
  • 18行目:特殊メソッド定義。画像のidを入力すると、画像データとマスク画像を返します。
  • 20行目:画像の番号にします
  • 21行目:今のバッチにおける、画像のパスリスト。例えばバッチ数32なら、32個の画像分のパスです。
  • 22行目:今のバッチにおける、ターゲット(マスク)の画像パスリスト。
  • 23行目:numpy形式で、箱を用意しておきます。中身は全部ゼロです。
  • 24行目:画像のパスリストでfor分を回します。パスをひとつづ取り出します。
  • 25行目:画像をロードします。引数target_sizeで画像サイズをそろえます。
  • 26行目:画像の箱、xに入れておきます
  • 27行目:マスク画像を入れておく箱を、numpy形式で作成します。中身はすべてゼロです。
  • 28行目:マスク画像のパスリストから、for分を回して、ひとつづパスを取り出します。
  • 29行目:マスク画像を読み込みます。target_sizeで画像をそろえて、グレイスケールで読み込みます。
  • 30行目:マスク画像はグレイスケールの2次元データなので、chの次元を追加してから、マスク画像の箱に入れておきます。
  • 32行目:マスク画像は全部から1引いておきます。もともと1,2,3の3クラスなのですが、pythonだけに0,1,2にするため。。。
  • 33行目:入力画像の箱(バッチ分の画像)と、マスク画像の箱(バッチ分のマスク)を返します。

Xceptionの構造をもつU-NETモデルを構築

from tensorflow.keras import layers


def get_model(img_size, num_classes):
    inputs = keras.Input(shape=img_size + (3,))

    ### [First half of the network: downsampling inputs] ###

    # Entry block
    x = layers.Conv2D(32, 3, strides=2, padding="same")(inputs)
    x = layers.BatchNormalization()(x)
    x = layers.Activation("relu")(x)

    previous_block_activation = x  # Set aside residual

    # Blocks 1, 2, 3 are identical apart from the feature depth.
    for filters in [64, 128, 256]:
        x = layers.Activation("relu")(x)
        x = layers.SeparableConv2D(filters, 3, padding="same")(x)
        x = layers.BatchNormalization()(x)

        x = layers.Activation("relu")(x)
        x = layers.SeparableConv2D(filters, 3, padding="same")(x)
        x = layers.BatchNormalization()(x)

        x = layers.MaxPooling2D(3, strides=2, padding="same")(x)

        # Project residual
        residual = layers.Conv2D(filters, 1, strides=2, padding="same")(
            previous_block_activation
        )
        x = layers.add([x, residual])  # Add back residual
        previous_block_activation = x  # Set aside next residual

    ### [Second half of the network: upsampling inputs] ###

    for filters in [256, 128, 64, 32]:
        x = layers.Activation("relu")(x)
        x = layers.Conv2DTranspose(filters, 3, padding="same")(x)
        x = layers.BatchNormalization()(x)

        x = layers.Activation("relu")(x)
        x = layers.Conv2DTranspose(filters, 3, padding="same")(x)
        x = layers.BatchNormalization()(x)

        x = layers.UpSampling2D(2)(x)

        # Project residual
        residual = layers.UpSampling2D(2)(previous_block_activation)
        residual = layers.Conv2D(filters, 1, padding="same")(residual)
        x = layers.add([x, residual])  # Add back residual
        previous_block_activation = x  # Set aside next residual

    # Add a per-pixel classification layer
    outputs = layers.Conv2D(num_classes, 3, activation="softmax", padding="same")(x)

    # Define the model
    model = keras.Model(inputs, outputs)
    return model


# Free up RAM in case the model definition cells were run multiple times
keras.backend.clear_session()

# Build model
model = get_model(img_size, num_classes)
model.summary()

本題のモデル構築です。モデル構造を理解しつつ、美しさを味わいましょう。

  • 1行目:tensorflowのレイヤーをインポート
  • 4行目:モデル構築関数。引数は、画像のサイズとクラスの数です。
  • 5行目:画像入力層です。サイズを変えています
  • ここから、ダウンサンプリング。画像を小さくしていきます。
  • 10行目:畳み込み層
  • 11行目:バッチノーマリゼーション(バッチごとの正規化)
  • 12行目:活性化関数。レルーですね
  • 14行目:出力をresidualとしてとっておいて。分岐みたいなものです。
  • 17行目:フィルタサイズ変えて3回繰り返します。
  • 18行目:活性化関数
  • 19行目:ちょっと特殊な畳み込みを行います。xceptionで使うものです。
  • 20行目:バッチごとの正規化
  • 22~24行目:もう1セット。活性化関数、特殊な畳み込み、バッチごとの正規化。
  • 26行目:マックスプーリング
  • 29~31行目:residual層を畳み込みで作ります。14行目で分岐させてとっておいた入力使います。
  • 32行目:本流と、支流(residual)を合体
  • 33行目:また、分岐用に出力をとておきます。
  • ここからは、アップサンプリングです。画像をもとのサイズに戻していきます。
  • 37行目:4種類のフィルタ種類でfor文回します
  • 38行目:活性化関数
  • 39行目:転置畳み込み。
  • 40行目:バッチごとの正規化
  • 42~44行目:もう1セット、活性化関数、転置畳み込み、バッチごとの正規化
  • 46行目:アップサンプリング。画像を2倍にします。
  • 49行目:residual層。33行目でとっておいた、支流を入力として、2倍にアップサンプリング。
  • 50行目:1回畳み込んで
  • 51行目:主流と、支流(residual)合体
  • 52行目:支流を保存しておく。
  • 55行目:softmaxでつないで、各画素値が、3クラス分類の確率になるようにする。
  • 58行目:モデルを作っておきます。引数が、入力層と出力層のところ
  • 63行目:メモリを開放するおまじない
  • 66行目:モデルクラスを作成
  • 67行目:モデルのサマリ表示

データの一部をValidationデータに分けます。

import random

# Split our img paths into a training and a validation set
val_samples = 1000
random.Random(1337).shuffle(input_img_paths)
random.Random(1337).shuffle(target_img_paths)
train_input_img_paths = input_img_paths[:-val_samples]
train_target_img_paths = target_img_paths[:-val_samples]
val_input_img_paths = input_img_paths[-val_samples:]
val_target_img_paths = target_img_paths[-val_samples:]

# Instantiate data Sequences for each split
train_gen = OxfordPets(
    batch_size, img_size, train_input_img_paths, train_target_img_paths
)
val_gen = OxfordPets(batch_size, img_size, val_input_img_paths, val_target_img_paths)

学習データと検証データに分けるところです

  • 1行目:乱数のライブラリインポート
  • 4行目:検証データは100個
  • 5行目:乱数で、入力画像のパスたちをシャッフル
  • 6行目:乱数で、答え(マスク画像)のパスたちをシャッフル。シード同一だから、画像と答えはセットだよ。
  • 7行目:前の方が学習データのパス。画像
  • 8行目:前の方が学習データのパス。マスク
  • 9行目:後ろ100個が検証データのパス。画像
  • 10行目:後ろ100個が検証データのパス。マスク
  • 13~15行目:学習画像のジェネレータを作成。自作クラスが活躍しています!
  • 16行目:検証画像のジェネレータを作成。こちらも自作クラスが活躍しています!

model学習

# Configure the model for training.
# We use the "sparse" version of categorical_crossentropy
# because our target data is integers.
model.compile(optimizer="rmsprop", loss="sparse_categorical_crossentropy")

callbacks = [
    keras.callbacks.ModelCheckpoint("oxford_segmentation.h5", save_best_only=True)
]

# Train the model, doing validation at the end of each epoch.
epochs = 15
model.fit(train_gen, epochs=epochs, validation_data=val_gen, callbacks=callbacks)

学習です。

  • 4行目:モデルコンパイル。optimizerは”rmsprop”。lossは”sparce categorical crossentropy”です。ターゲットデータ(マスク画像)が整数なので、sparceでよいそうです。
  • 6~8行目:コールバックを定義。ベスト解が出たら、重みを保存しておきます。
  • 11行目:エポック数は15。そんなもんですかね。。。もう少し増やした方が。。。
  • 12行目:学習実行。画像と答えはジェネレータで入れています。

予測結果を可視化

# Generate predictions for all images in the validation set

val_gen = OxfordPets(batch_size, img_size, val_input_img_paths, val_target_img_paths)
val_preds = model.predict(val_gen)


def display_mask(i):
    """Quick utility to display a model's prediction."""
    mask = np.argmax(val_preds[i], axis=-1)
    mask = np.expand_dims(mask, axis=-1)
    img = ImageOps.autocontrast(keras.preprocessing.image.array_to_img(mask))
    display(img)


# Display results for validation image #10
i = 10

# Display input image
display(Image(filename=val_input_img_paths[i]))

# Display ground-truth target mask
img = ImageOps.autocontrast(load_img(val_target_img_paths[i]))
display(img)

# Display mask predicted by our model
display_mask(i)  # Note that the model only sees inputs at 150x150.

学習したモデルで、予測して結果を可視化します

  • 3行目:検証データのジェネレータを作成して
  • 4行目:学習済みのモデルを使って予測
  • 7行目:予測結果 各画素をクラス分けしたものを表示する関数
  • 9行目:マスクは、確率が一番大きかったクラス
  • 10行目:次元を1次元増やして。chの次元を追加します。
  • 11行目:マスクデータを画像データに変換。kerasのarray_to_imageというメソッドを使っています。Pillowのautocontrastでコントラストを自動調整いています。
  • 12行目:画像表示
  • 16行目:検証データの10番目をつかうよ
  • 19行目:推論につかった入力画像を表示
  • 22行目:正解のマスク画像を読み込んで。。。
  • 23行目:表示
  • 26行目:予測結果を表示

まとめ

今回は、segmentationのサンプルコードに解説を加えてみました。segmentationはアノテーション(色塗り)作業が大変であまり活用が進みませんでしたが、ラベリングツールが改善されたこともあり、徐々に活用例が増え始めています。

 みなさんの、コードの理解におやくにたてれば光栄です。

最後までお付き合いいただいた、勉強熱心な皆さんへは、よろしければ、こちらの記事もいかがでしょう

tensorflow keras vision transformerによる物体検知のサンプルコード解説。 初心者向けです。
画像認識の主流となりつつなるアルゴリズム、Vision Transformerですが、物体検知(object detection)タスクへの利用も提案されています。今回は、Tensorflwo kerasを用いて、ViTを物体検出へ適用したサンプルコードを初心者向けに解説します。
タイトルとURLをコピーしました