Klasifikasi menjawab “apa yang ada di gambar”, deteksi menambahkan “di kotak mana”, tetapi keduanya masih memperlakukan objek sebagai persegi panjang. Semantic segmentation menurunkan satuan keputusan sampai ke piksel: setiap piksel mendapat label kelas, sehingga tepi objek, lubang, dan bentuk mengikuti kontur aslinya. Pada chapter ini kita bedah pola encoder-decoder dengan skip connection yang menjadi dasar U-Net, dua loss yang paling sering dipakai (cross-entropy per piksel dan Dice), lalu menghitung sendiri pixel accuracy, IoU per kelas, dan Dice dengan NumPy pada mask digit yang dibuat dari dataset digits.
Klasifikasi Per Piksel, Bukan Per Gambar #
Model segmentasi pada dasarnya adalah jaringan klasifikasi yang dijalankan serentak pada seluruh piksel: untuk piksel di baris r, kolom c, model memprediksi distribusi probabilitas kelas. Keluarannya bukan vektor, melainkan tensor (H, W, C) — satu skor per kelas untuk setiap piksel, lalu argmax pada sumbu kelas menghasilkan mask akhir. Karena keluarannya berukuran sama dengan masukan, arsitektur harus bisa mengekstraksi konteks (jalan ke resolusi rendah) lalu kembali ke resolusi penuh.
| Aspek | Klasifikasi | Object detection | Semantic segmentation |
|---|---|---|---|
| Unit keputusan | satu gambar | satu kotak kandidat | satu piksel |
| Keluaran | label + probabilitas | daftar kotak + skor | mask (H, W) berlabel |
| Metrik utama | akurasi, F1 | mAP | IoU per kelas, Dice |
| Contoh model | ResNet classifier | YOLO, Faster R-CNN | U-Net, DeepLab |
Encoder-Decoder dan Skip Connection #
Pola encoder-decoder bekerja seperti kompresi lalu dekompresi. Encoder menumpuk blok convolution dan pooling yang mengecilkan resolusi sambil menaikkan jumlah channel — persis jalur CNN yang kita kenal dari chapter Feature Map . Decoder membalik arah: menaikkan resolusi kembali dengan upsample, lalu menghaluskan hasilnya dengan convolution. Bagian tersempit di tengah (resolusi terkecil) menyimpan ringkasan konteks global.
Upsample adalah kebalikan pooling: nilai satu sel disalin ke blok 2x2 (nearest neighbor) atau disisipkan (bilinear). Transposed convolution membuat versi yang bisa dilatih — bobotnya menentukan bagaimana tiap nilai “disebar” ke sel tetangga — dengan catatan pemilihan kernel yang buruk menimbulkan pola papan catur pada mask. Komponen kunci U-Net adalah skip connection (garis putus-putus pada diagram): feature map encoder digabungkan ke feature map decoder beresolusi sama, sehingga detail tepi yang hilang saat pooling dikembalikan saat menyusun mask.
Loss: Cross-Entropy Per Piksel dan Dice #
Cross-entropy pada segmentasi adalah rata-rata cross-entropy klasifikasi di seluruh piksel: H = -rata-rata log p(y_r,c) untuk label y tiap piksel. Masalahnya, piksel latar biasanya jauh lebih banyak daripada piksel objek, sehingga loss didominasi kelas mayoritas. Loss Dice melengkapi dengan mengukur tumpang tindih langsung antara prediksi P dan ground truth G:
| Loss | Rumus | Karakter |
|---|---|---|
| Cross-entropy piksel | -rata-rata log p(y) per piksel | stabil, tapi bias ke kelas mayoritas |
| Dice (hard) | `2 x | P ∩ G |
| Dice (soft) | 2 x Σ p·g / (Σ p² + Σ g²) | bisa dipakai di dalam gradien |
Kombinasi keduanya (rata-rata berbobot) merupakan praktik umum: cross-entropy menjaga kestabilan awal pelatihan, Dice mendorong kontur yang rapi pada kelas kecil.
Eksperimen NumPy: Mask Digit dan Tiga Metrik #
Berkas evaluasi_segmentasi.py membangun ground truth dari 8 citra digits — piksel tinta menjadi kelas “depan” — lalu mensimulasikan prediksi dengan blur 3x3 dan ambang. Simulasi ini meniru perilaku model nyata: mask hasil sedikit melebar dan menutup celah goresan, cukup tidak sempurna untuk membuat metrik bekerja.
"""Metrik semantic segmentation: pixel accuracy, IoU per kelas, dan Dice.
Alur: 8 citra digits -> mask ground truth (piksel tinta) -> simulasi
prediksi (blur 3x3 lalu ambang) -> bandingkan piksel demi piksel ->
pixel accuracy, IoU per kelas, mean IoU, Dice.
"""
import numpy as np
from sklearn.datasets import load_digits
digits = load_digits()
gambar = digits.images[:8].astype(float) # 8 citra 8x8, nilai 0..16
# ---------- 1. Mask ground truth: piksel tinta = kelas depan ----------
gt = (gambar > 0).astype(int) # 1 = depan (goresan digit), 0 = latar
# ---------- 2. Simulasi prediksi: blur 3x3 lalu ambang ----------
def blur3x3(a):
"""Rata-rata 3x3 pada dua sumbu terakhir, padding tepi (mode edge)."""
pad = [(1, 1)] * a.ndim
if a.ndim == 3: # batch (N, H, W): sumbu batch tidak di-pad
pad[0] = (0, 0)
t = np.pad(a, pad, mode="edge")
jumlah = np.zeros_like(a)
h, w = a.shape[-2], a.shape[-1]
for dy in (-1, 0, 1):
for dx in (-1, 0, 1):
jumlah = jumlah + t[..., 1 + dy: 1 + dy + h, 1 + dx: 1 + dx + w]
return jumlah / 9.0
pred = (blur3x3(gambar) > 4.0).astype(int) # ambang 4.0 -> mask sedikit melebar
# Mask prediksi sengaja dibuat tidak sempurna supaya metrik terlihat bekerja:
# blur merata-ratakan tinta ke tetangga, ambang 4.0 lalu menutup beberapa
# celah di dalam goresan (over-segmentation ringan di tepi).
# ---------- 3. Visualisasi piksel untuk citra pertama ----------
def tampil(mask):
return "\n".join("".join("#" if v else "." for v in baris) for baris in mask)
print(f"citra ke-0 (angka {digits.target[0]})")
print("ground truth:")
print(tampil(gt[0]))
print("prediksi (blur 3x3 lalu ambang):")
print(tampil(pred[0]))
print()
# ---------- 4. Pixel accuracy ----------
benar = (pred == gt)
akurasi = benar.sum() / gt.size
print(f"total piksel : {gt.size} (8 citra x 8 x 8)")
print(f"piksel benar : {int(benar.sum())}")
print(f"pixel accuracy : {akurasi:.4f}")
print()
# ---------- 5. IoU per kelas dan mean IoU ----------
def iou_kelas(y_true, y_pred, kelas):
t = y_true == kelas
p = y_pred == kelas
irisan = np.logical_and(t, p).sum()
gabungan = np.logical_or(t, p).sum()
return irisan / gabungan if gabungan > 0 else float("nan")
iou_bg = iou_kelas(gt, pred, 0)
iou_fg = iou_kelas(gt, pred, 1)
print(f"IoU latar (0) : {iou_bg:.4f}")
print(f"IoU depan (1) : {iou_fg:.4f}")
print(f"mean IoU : {(iou_bg + iou_fg) / 2:.4f}")
print()
# ---------- 6. Dice untuk kelas depan ----------
t = gt == 1
p = pred == 1
irisan = np.logical_and(t, p).sum()
dice = 2 * irisan / (t.sum() + p.sum())
print(f"|GT depan| : {int(t.sum())} piksel")
print(f"|pred depan| : {int(p.sum())} piksel")
print(f"|irisan| : {int(irisan)} piksel")
print(f"Dice depan : {dice:.4f}")
print()
# Dice dan IoU pun hubungan tetap: Dice = 2*IoU / (1 + IoU)
print(f"cek hubungan : 2*IoU/(1+IoU) = {2 * iou_fg / (1 + iou_fg):.4f} "
f"(sama dengan Dice)")Output yang Dihasilkan:
citra ke-0 (angka 0)
ground truth:
..####..
..#####.
.###.##.
.##..##.
.##..##.
.##.###.
.#####..
..###...
prediksi (blur 3x3 lalu ambang):
..####..
..#####.
.######.
.###.##.
.##..##.
.######.
.#####..
..####..
total piksel : 512 (8 citra x 8 x 8)
piksel benar : 470
pixel accuracy : 0.9180
IoU latar (0) : 0.8456
IoU depan (1) : 0.8511
mean IoU : 0.8483
|GT depan| : 254 piksel
|pred depan| : 268 piksel
|irisan| : 240 piksel
Dice depan : 0.9195
cek hubungan : 2*IoU/(1+IoU) = 0.9195 (sama dengan Dice)Mari kita telaah kode di atas baris per baris:
- Baris 11-12: delapan citra digits (8x8, nilai 0-16) dimuat; hanya 8 yang dipakai supaya tabel keluaran tetap ringkas.
- Baris 16 (
gt): mask ground truth dibentuk dengan ambang sederhana> 0— piksel bernilai tinta menjadi kelas 1 (depan), sisanya kelas 0 (latar). - Baris 20-31 (
blur3x3): filter rata-rata 3x3 ditulis manual dengannp.padmode"edge"dan penjumlahan sembilan potongan matriks — mekanisme yang sama dengan konvolusi tanpa bobot terlatih. - Baris 34 (
pred): prediksi disimulasikan sebagaiblur3x3(gambar) > 4.0. Tinta yang tadinya tajam menyebar ke tetangga, sehingga ambang 4.0 menghasilkan mask yang menutup celah dan sedikit melebar — lihat baris ke-2 dan ke-5 mask prediksi yang menutup lubang di dalam angka 0. - Baris 42-50 (
tampil): mask dicetak sebagai peta karakter#dan.agar perbedaan piksel terlihat langsung tanpa gambar. - Baris 55-59: pixel accuracy =
470/512 = 0.9180— proporsi piksel yang labelnya benar, dihitung menyatukan kedua kelas. - Baris 64-76 (
iou_kelas): IoU kelaskmembandingkan irisan dan gabungan mask biner kelas tersebut. Kelas depan mencetak 0.8511, kelas latar 0.8456, mean IoU 0.8483. - Baris 82-88: Dice kelas depan menghitung
2 x 240 / (254 + 268) = 0.9195. Baris terakhir memverifikasi hubungan tetapDice = 2·IoU/(1+IoU)— dua metrik ini monoton satu sama lain, hanya skalanya berbeda.
Segmentasi semantik melabeli kelas, tetapi masih membingungkan dua objek sekelas: dua kucing menjadi satu gumpalan “kucing”. Chapter berikutnya, Instance Segmentation , memisahkan tiap objek menjadi mask tersendiri — kita mulai dari trik connected components lalu lihat mengapa Mask R-CNN diperlukan. Katalog model segmentasi siap pakai tersedia di dokumentasi torchvision .