Konsep dan Implementasi U-Net model untuk Image Segmentation dengan Tensorflow
Machine learning menjadi salah satu tren teknologi pada tahun 2023, termasuk deep learning maupun computer vision. Image segmentation merupakan salah satu teknik dari computer vision, teknik ini cukup terbilang powerfull karena sudah digunakan pada tugas yang cukup rumit seperti self-driving car, robotic, sampai tumor detection pada dunia medis. Pada artikel ini kita akan coba memahami cara kerja salah satu arsitektur deep learning untuk image segmentation
Apa itu Image Segmentation
Pada dasarnya computer vision memiliki tiga teknik utama, yaitu: classification, object detection, dan image segmentation. Perbedaan dari ketiga teknik tersebut bisa dilihat pada gambar di bawah ini:
Pada classification, model hanya bisa mengetahui bahwa gambar itu adalah gambar kucing. Pada object detection model bisa mengetahui di mana lokasi kucing pada gambar tersebut, sedangkan image segmentation model dapat benar-benar membedakan yang mana kucing dan yang mana bukan kucing (background, benda, dll). Oleh karena itu, cara kerja image segmentation sebenarnya adalah klasifikasi dalam kelas “pixel”. Ada beberapa cara untuk melakukan teknik image segementation, bisa dengan teknik machine learning klasik seperti k-means clustering atau teknik deep learning seperti U-net.
U-Net
U-net adalah model image segmentation yang ditemukan oleh Olaf Ronneberger dari University of Freiburg Jerman pada tahun 2015. Pada dasarnya U-net merupakan fully convolutional network (FCN); jadi tidak ada dense layer sama sekali, berbeda dengan fully connected layer yang hanya terdiri dari dense layer. Selain digunakan untuk image segmentation, FCN juga dapat digunakan untuk image super resolution. Lalu kenapa model ini dinamakan dengan U-net? Karena bentuk arsitekturnya seperti huruf “U”.
Gambar di atas merupakan bentuk dari arsitektur U-net. Berikut adalah karakteristik model tersebut berdasarkan papernya: - Model tersebut dibagi menjadi dua bagian, yaitu encoder (contracting path) dan decoder (expansive path). Tugas dari encoder adalah melakukan konvolusi dan ekstrasi fitur. Sedangkan decoder melakukan transpose convolution dan menunjukan ‘yang mana objek tersebut pada gambar’, sehingga output dari model tersebut berupa hasil segmentasi. - Setial levelnya terjadi dua kali konvolusi, ukuran kernel sebesar 3x3, dengan ReLu sebagai fungsi aktivasi.
- Menggunakan Max pooling dengan ukuran 2x2
- Pada bagian decoder menggunakan transpose convolution dengan ukuran 2x2
- Garis abu-abu pada gambar di atas juga sering disebut skip connections, atau concatenate jika kita implementasikan dengan tensorflow. Fungsinya adalah menambah informasi yang mungkin hilang saat kita melakukan Max pooling pada bagian encoder.
Berdasarkan papernya, padding yang dipakai saat proses konvolusi adalah “unpadded padding_”_, artinya ukuran gambar akan berkurang setiap kali melakukan konvolusi. Tapi pada saat kita implementasikan dengan tensorflow, kita bisa mengubah paddingnya menjadi ‘same’ agar ukuran gambar tidak berubah setelah konvolusi.
Nah kalau kita lihat arsitektur di atas, U-net memang tidak mengandung dense layer sama sekali. Kalau kalian mau mungkin kalian bisa ganti bagian decoder dengan dense layer untuk tugas klasifikasi gambar biasa
Metrics Untuk Image Segmentation
Jika kita membuat sebuah model machine learning atau deep learning, pasti kita akan menggunakan sebuah ‘metrics’ untuk mengukur performa dari model machine learning yang sudah kita latih. Biasanya kita mengunakan metrics accuracy, precision, dan recall untuk tugas machine learning untuk klasifikasi. Pada kasus image segmentation, metrics yang digunakan agak berbeda dengan metrics pada klasifikasi biasa; pada kasus ini kita menggunakan pixel accuracy, IoU, dan dice coefficient.
Pixel Accuracy
Seperti yang sudah dijelaskan di atas, konsep dari image segmentation adalah klasifikasi pada kelas “pixel”. Seperti yang kita ketahui, rumus dari accuracy adalah sebagai berikut:
Artinya jika semakin banyak pixel yang benar diprediksi, maka accuracy nya akan tinggi. Tapi metrics ini tidak terlalu berguna untuk kasus image segmentation, kenapa? Coba kita lihat ilustrasi di bawah > TP = pixel yang berhasil disegmentasi dengan benar. > > TN = pixel yang tidak tersegmentasi, dan itu memang bukan bagian dari foreground (bagian yang ingin disegmentasi) > > FP = pixel yang salah meng-segmentasi > > FN = pixel foreground tetapi tidak ter-segmentasi
Pada bagian berwarna putih pada gambar menunjukan foreground atau gambar yang ingin kita segmentasi (class positive), warna hitam menunjukan background yang kita ingin bagian ini tidak tersegmentasi (class negative), dan bagian berwarna hijau menunjukan hasil prediksi atau segmentasi.
Nah kalau kita lihat, mungkin 90% dari gambar tersebut didominasi oleh background berwarna hitam dan statusnya adalah TN (true negative). Sehingga kalau kita menggunakan accuracy sebagai metrics hasilnya pasti akan besar. Padahal kalau kita lihat, bagian foreground yang tersegmentasi dengan benar (TP) hanya sekitar ¼. Kasus ini merupakan kasus imbalanced class, di mana proporsi setiap class nya sangat tidak seimbang karena pixel hitam (negative) jauh lebih banyak dan mendominasi daripada pixel putih (positive). Sehingga jika kita menggunakan metrics ini, kemungkinan akan menghasilkan interpretasi yang salah.
Intersection over Union (IoU)
IoU bisa menjadi alternatif metrics selain accuracy. Dengan memakai metrics ini, kita ingin memperbesar nilai Area of Overlap (intersection) atau true positive dan memperkecil nilai union. Jadi semakin besar nilai IoU, maka performa model akan semakin bagus. Selain untuk image segmentation, IoU juga dipakai sebagai metrics untuk mengukur performa object detection. Secara matematis rumus dari IoU dapat ditulis sebagai berikut:
Hal yang perlu diingat, output dari deep learning bukan berupa nilai diskrit; melainkan nilai probabilitas dari nol sampai satu. Oleh karena itu kita tidak bisa memakai rumus di atas. Kita bisa memakai rumus di bawah ini:
Dice Coefficient
Dice coeffient sebenarnya sama dengan F1 score pada kasus klasifikasi biasa. Rumusnya bisa ditulis sebagai berikut:
Sangat sederhana bukan? Nah mari kita buktikan jika hasil dari dice coefficient sama dengan F1 score dengan code di bawah ini. Sebelumnya coba kita review sedikit tentang F1 score:
TP = 100
TN = 120
FP = 60
FN = 75
precision = TP/(TP+FP)
recall = TP/(TP+FN)
f1_score = 2*precision*recall / (precision+recall)
intersection = TP
union = FP+TP+FN
dice = 2*intersection/(union+intersection)
print(f"Dice == f1_score : {dice == f1_score}")
# Output
# Dice == f1_score : True
Loss Function
Untuk loss functionnya sangat sederhana, cukup kita ambil angka satu dan kita kurangkan dengan nilai dice coeffient. Jika loss nya kecil, maka nilai dice coefficient nya akan besar. Begitu juga dengan sebaliknya.
Implementasi Tensorflow
Let’s get to the code. Kita akan menggunakan dataset untuk deteksi keretakan pada bangunan dan jalan, untuk data kita ambil dari kaggle. Pertama-tama, coba kita lihat sample datanya.
import os
import cv2
import matplotlib.pyplot as plt
# Dataset Folder
ASK_PATH = 'crack_segmentation_dataset/masks'
mask_files = os.listdir(MASK_PATH)
TRAIN_PATH = 'crack_segmentation_dataset/images'
train_files = os.listdir(TRAIN_PATH)
# Show image sample
for i in range(8,12):
plt.figure(figsize=(12,12))
plt.subplot(1,3,1)
plt.imshow(cv2.imread(f"{TRAIN_PATH}/{train_files[i]}"))
plt.title('Gambar')
plt.subplot(1,3,2)
plt.imshow(cv2.imread(f"{MASK_PATH}/{mask_files[i]}"))
plt.title('Label/Mask')
plt.show()
Hasilnya adalah sebagai berikut:
Untuk image segmentation, kita perlu dua jenis gambar: gambar aslinya dan gambar mask atau label yang menunjukan bagian mana pada gambar yang ingin disegmentasi. Bagaimana cara labelnya? mau tidak mau harus manual 😊. Ada software untuk me-label dan anotasi data, contohnya adalah label studio. Kalian bisa lihat-lihat di sini https://labelstud.io/
Ok, sekarang kita akan membuat model U-net nya. Cukup ikuti arsitektur modelnya dari paper
import tensorflow as tf
from tensorflow.keras import Input
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.models import Model, load_model, save_model
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from tensorflow.keras.layers import Input, Activation, BatchNormalization, Dropout, Lambda, Conv2D, Conv2DTranspose, MaxPooling2D, concatenate
def unet(input_size=(256,256,3)):
inputs = Input(input_size)
# ENCODER
conv1 = Conv2D(64, (3, 3), padding='same')(inputs) #Kalian bisa eksperimen dengan cara mengubah jumlah kernelnya
bn1 = Activation('relu')(conv1)
conv1 = Conv2D(64, (3, 3), padding='same')(bn1)
bn1 = BatchNormalization(axis=3)(conv1)
bn1 = Activation('relu')(bn1)
pool1 = MaxPooling2D(pool_size=(2, 2))(bn1)
conv2 = Conv2D(128, (3, 3), padding='same')(pool1)
bn2 = Activation('relu')(conv2)
conv2 = Conv2D(128, (3, 3), padding='same')(bn2)
bn2 = BatchNormalization(axis=3)(conv2)
bn2 = Activation('relu')(bn2)
pool2 = MaxPooling2D(pool_size=(2, 2))(bn2)
conv3 = Conv2D(256, (3, 3), padding='same')(pool2)
bn3 = Activation('relu')(conv3)
conv3 = Conv2D(256, (3, 3), padding='same')(bn3)
bn3 = BatchNormalization(axis=3)(conv3)
bn3 = Activation('relu')(bn3)
pool3 = MaxPooling2D(pool_size=(2, 2))(bn3)
conv4 = Conv2D(512, (3, 3), padding='same')(pool3)
bn4 = Activation('relu')(conv4)
conv4 = Conv2D(512, (3, 3), padding='same')(bn4)
bn4 = BatchNormalization(axis=3)(conv4)
bn4 = Activation('relu')(bn4)
pool4 = MaxPooling2D(pool_size=(2, 2))(bn4)
conv5 = Conv2D(1024, (3, 3), padding='same')(pool4)
bn5 = Activation('relu')(conv5)
conv5 = Conv2D(1024, (3, 3), padding='same')(bn5)
bn5 = BatchNormalization(axis=3)(conv5)
bn5 = Activation('relu')(bn5)
# DECODER
up6 = Conv2DTranspose(512, (2, 2), strides=(2, 2), padding='same')(bn5)
up6 = concatenate([up6,conv4], axis=3)
conv6 = Conv2D(512, (3, 3), padding='same')(up6)
bn6 = Activation('relu')(conv6)
conv6 = Conv2D(512, (3, 3), padding='same')(bn6)
bn6 = BatchNormalization(axis=3)(conv6)
bn6 = Activation('relu')(bn6)
up7 = Conv2DTranspose(256, (2, 2), strides=(2, 2), padding='same')(bn6)
up7 = concatenate([up7,conv3], axis=3)
conv7 = Conv2D(256, (3, 3), padding='same')(up7)
bn7 = Activation('relu')(conv7)
conv7 = Conv2D(256, (3, 3), padding='same')(bn7)
bn7 = BatchNormalization(axis=3)(conv7)
bn7 = Activation('relu')(bn7)
up8 = Conv2DTranspose(128, (2, 2), strides=(2, 2), padding='same')(bn7)
up8 = concatenate([up8,conv2], axis=3)
conv8 = Conv2D(128, (3, 3), padding='same')(up8)
bn8 = Activation('relu')(conv8)
conv8 = Conv2D(128, (3, 3), padding='same')(bn8)
bn8 = BatchNormalization(axis=3)(conv8)
bn8 = Activation('relu')(bn8)
up9 = Conv2DTranspose(64, (2, 2), strides=(2, 2), padding='same')(bn8)
up9 = concatenate([up9,conv1], axis=3)
conv9 = Conv2D(64, (3, 3), padding='same')(up9)
bn9 = Activation('relu')(conv9)
conv9 = Conv2D(64, (3, 3), padding='same')(bn9)
bn9 = BatchNormalization(axis=3)(conv9)
bn9 = Activation('relu')(bn9)
conv10 = Conv2D(1, (1, 1), activation='sigmoid')(bn9)
return Model(inputs=[inputs], outputs=[conv10])
Ukuran gambar asli adalah 448448, di sini kita ubah ke ukuran 256256. Padding kita set ke “same” agar ukuran gambar setelah konvolusi tidak berubah. Agak berbeda dengan implementasi di paper, but it’s ok
Kita lanjut untuk membuat metrics dan loss functions.
from tensorflow.keras import backend as K
def dice_coef(y_true, y_pred):
y_true = K.flatten(y_true)
y_pred = K.flatten(y_pred)
intersection = K.sum(y_true * y_pred)
coef = ((2 *intersection) / (K.sum(y_true) + K.sum(y_pred)))
return coef
def dice_loss(y_true, y_pred):
return 1 - dice_coef(y_true, y_pred)
def iou(y_true, y_pred):
intersection = K.sum(y_true * y_pred)
iou_score = intersection / ((K.sum(y_true + y_pred)) - intersection)
return iou_score
Setelah itu kita akan training modelnya. Cukup gunakan 30 epochs, karena training U-net cukup lama daripada training CNN biasa.
opt = tf.keras.optimizers.legacy.Adam(lr=1e-4)
model = unet(input_size=(256, 256, 3))
model.compile(optimizer=opt, loss=dice_loss, metrics=["binary_accuracy", iou, dice_coef])
# User data generator for train_gen and test_gen
history = model.fit(train_gen,
epochs=30,
validation_data=test_gen)
Hampir memakan waktu 4 jam untuk 30 epochs😊 Sekarang coba kita visualisasikan plot IoU, dice coefficient, dan lossnya.
plt.plot(history.epoch, history.history["val_iou"], label='IoU')
plt.plot(history.epoch, history.history["val_dice_coef"] ,label='Dice Coef')
plt.legend()
plt.show()
Tidak terlalu memuaskan. Untuk IoU mencapai 0.5-an dan Dice Coefficientnya mencapai 0,7. Mungkin kita bisa tambah lagi epochnya untuk menaikan performanya. Sekarang coba kita lihat hasil prediksinya.
Ternyata hasilnya lumayan memuaskan😁 Sekarang coba kita ambil gambar random dari google.
original = cv2.imread('tes.jpg')
sample = cv2.resize(original, (im_height, im_width))
sample = sample / 255
sample = sample[np.newaxis, :, :, :]
pred = model.predict(sample) #predict
fig, (ax1,ax2,ax3) = plt.subplots(1,3, figsize=(15,5))
ax1.imshow(np.squeeze(sample))
ax1.set_title('Image')
pred_img = np.squeeze(pred) > .5
pred_img = pred_img.astype(int)
pred_img = addChannel(pred_img.astype(int), 0,0,255)
ax2.imshow(pred_img)
ax2.set_title('Predicted')
ax3.imshow(np.squeeze(sample))
ax3.imshow(pred_img, alpha=0.7)
ax3.set_title('Merged')
plt.plot()
Dan beginilah hasil prediksinya:
Ternyata dengan 30 epoch kita sudah memiliki hasil yang lumayan bagus
Ok, segitu dulu tentang image segmentation. Semoga mudah dipahami dan bermanfaat😊.











