Pythonで簡単な画像をセグメンテーションした後に領域の周長を求める話

※全文無料です。

1. 画像データセットの用意

とりあえずセマンティックセグメンテーションを行うためのデータセットを用意しましょう。64*64ピクセルの黒背景の上に、['▲','●','■']の中からランダムに2つ配置して画像化します(返すのはndarrayですが)。片方にはノイズを乗せておきましょう。メモリが不安なら画像として出力処理を入れてください。

from PIL import Image, ImageDraw, ImageFont
import numpy as np

symbols = np.array(['▲','●','■'])

def im_gen():
   font = ImageFont.truetype('times.ttf', np.random.randint(12, 20))
   im = Image.new("L",(64,64),0)
   draw = ImageDraw.Draw(im)
   draw.text(np.random.randint(0, 20, 2), ''.join(np.random.choice(symbols)), font=font, fill=255)
   draw.text(np.random.randint(25, 50, 2), ''.join(np.random.choice(symbols)), font=font, fill=255)
   return np.array(im)

def noise(x):
   y = np.where(x < 100, x + 100, x) - np.random.randint(0, 100, 64*64).reshape(64,64)
   return y

label_data = []
image_data = []

for _ in range(10000):
   im = im_gen()
   label_data.append(im)
   image_data.append(noise(im))

label_data = np.array(label_data)
image_data = np.array(image_data)

さて、これを実行すると次のような画像のペアが1万組出力されます。Image.fromarray()で1枚ずつ見ることができます。

Image.fromarray(np.uint8(label_data[0]))
Image.fromarray(np.uint8(image_data[0]))

画像1

これが確認できればデータセットの作成は終了です。

2. セマンティックセグメンテーション

TensorFlowでノイズ付きの画像からノイズ無しの画像を取り出すニューラルネットワークを構成します。その前にまずはデータセットを少し加工しましょう。

import tensorflow as tf

images = tf.reshape(image_data/255.0, [-1, 64, 64, 1])
labels = tf.reshape(label_data/255.0, [-1, 64, 64, 1])

これでさきほど出力したndarrayが全てテンソルになりました。ついでにピクセルごとの輝度も0~1の範囲に収まるように255で割って正規化しています。
次にモデルを組んでいきましょう。

im_input = tf.keras.layers.Input(shape=(None, None, 1))

conv1 = tf.keras.layers.Conv2D(16, (5, 5), padding='same')(im_input)
conv1 = tf.keras.layers.BatchNormalization()(conv1)
conv1 = tf.keras.layers.Activation('relu')(conv1)

conv2 = tf.keras.layers.Conv2D(32, (5, 5), padding='same')(conv1)
conv2 = tf.keras.layers.BatchNormalization()(conv2)
conv2 = tf.keras.layers.Activation('relu')(conv2)

train_out = tf.keras.layers.Conv2D(1, (1, 1), activation='sigmoid', padding='same')(conv2)

model = tf.keras.models.Model(inputs = im_input, outputs = train_out)

model.summary()

インプット層の下に畳み込み層→Batch Normalization→活性化関数ReLUの順で2回並べて、2値分類なのでアウトプット層は活性化関数Sigmoidを使っています。組んだモデルをコンパイルします。

adam = tf.keras.optimizers.Adam(lr=0.0001, epsilon=1e-06)
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=[tf.keras.metrics.binary_accuracy])

2値分類なのでロス関数はbinary crossentropyです。オプティマイザーはAdamで問題ないでしょう。

import datetime

model.fit(x=images, y=labels, batch_size=16, epochs=3, verbose=1, validation_split=0.1, shuffle=True, steps_per_epoch=9000//16, validation_steps=1000//16)
model.save('saved_model/' + datetime.datetime.now().strftime('%Y%m%d-%H%M%S'))

#Epoch 1/3
#562/562 [==============================] - 40s 71ms/step - loss: 0.0059 - binary_accuracy: 0.9894 - val_loss: 0.0054 - val_binary_accuracy: 0.9894
#Epoch 2/3
#562/562 [==============================] - 39s 70ms/step - loss: 0.0054 - binary_accuracy: 0.9895 - val_loss: 0.0053 - val_binary_accuracy: 0.9894
#Epoch 3/3
#562/562 [==============================] - 40s 71ms/step - loss: 0.0053 - binary_accuracy: 0.9894 - val_loss: 0.0053 - val_binary_accuracy: 0.9894
#INFO:tensorflow:Assets written to: saved_model/20210309-132235\assets

実際に学習させた後にモデルをセーブします。データが単純なのでエポック数は3もあれば十分です。もし、ここでプログラムが動かない場合には、

import os
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'

とすればGPUが無効になってCPUのみを使うようになるので、まずはこれを試してみましょう。これでダメならメモリが足りていない可能性が高いです。
では、モデルを使ってセグメンテーションしてみましょう。

pred_data = []

for _ in range(20):
   im = im_gen()
   pred_data.append(noise(im))

pred_data = np.array(pred_data)

predictions = model.predict(pred_data/255.0)

Image.fromarray(np.uint8(predictions.reshape(-1,64,64)[0]*255))
Image.fromarray(np.uint8(pred_data[0]))

とすれば、ノイズ画像とセグメンテーション後の画像のペアが得られます。

画像2

今回は単純なデータで行いましたが、畳み込みニューラルネットワークを用いた全てのセマンティックセグメンテーションの基本的な流れはこれと同じです。どんどん使っていきましょう。

3. 領域の周長を求める

まず、今後の説明を楽にするために、セグメンテーション後の画像を白黒の2値化しましょう。

def w_or_b(x):
   y = np.where(x < 127, 0, 255)
   return y

pred_image = np.uint8(w_or_b(predictions.reshape(-1,64,64)[0]*255)/255)

さて、今回求めたいものは2つの図形の周長を足した合計です。そのために、まずは画像の元となる配列に前処理を行います。

pred_image = np.repeat(np.repeat(pred_image, 2, axis=1), 2, axis=0)
pred_image = np.block([np.zeros((128, 1), dtype='uint8'), pred_image])
pred_image = np.block([pred_image, np.zeros((128, 1), dtype='int16')])
pred_image = np.insert(pred_image, 0, np.zeros((1, 128+2), dtype='int16'), axis=0)
pred_image = np.insert(pred_image, 128+1, np.zeros((1, 128+2), dtype='int16'), axis=0)

画像を縦横2倍に引き延ばした後、画像の外側に黒色のピクセルを1ピクセルずつ追加しています。こうすることで、境界が重なっているときや、画像のふちに図形が重なっているときでも領域が求まります。
続いて輪郭を定義します。

for i in range(1, 128+2):
   for j in range(1, 128+2):
       count=0
       if pred_image[j][i]==0 and pred_image[j+1][i]==1:
           count+=1
       if pred_image[j][i]==0 and pred_image[j][i+1]==1:
           count+=1
       if pred_image[j][i]==0 and pred_image[j-1][i]==1:
           count+=1
       if pred_image[j][i]==0 and pred_image[j][i-1]==1:
           count+=1
       if count>=1:
           pred_image[j][i]=-1
       if count==2:
           pred_image[j][i]=-2

わかりやすいようにfor文の2重ループで書きましたが、実際に使う場合には通常通り速い表記を使ってください。このプログラムでは、背景ピクセルの隣に1ピクセル白色があるときに配列の要素を-1とし、隣に2ピクセル白色があるときに配列の要素を-2とします。
このようにすれば、pred_imageの0未満の要素を合計して絶対値を取ったものの半分が、2つの図形の周の長さの合計になります。

r = abs(sum(pred_image[pred_image<0]))//2
# r = 82

ここから先は

0字

¥ 100

この記事が気に入ったらチップで応援してみませんか?