Chapter Object Detection berakhir pada satu benda: kotak berkoordinat yang melingkupi objek. Benda sederhana itu ternyata menyimpan banyak jebakan — format koordinat yang berbeda antar pustaka, konvensi inklusif versus eksklusif, dan kebutuhan mengukur seberapa mirip dua kotak. Chapter ini membedah bounding box sampai ke unitnya: konvensi koordinat piksel, tiga format yang beredar di ekosistem, Intersection over Union (IoU) sebagai penggaris kemiripan, dan Non-Maximum Suppression (NMS) yang memakai IoU untuk merapikan prediksi. Seluruh mekanisme dijalankan dengan angka konkret yang bisa dihitung ulang dengan tangan.
Koordinat Piksel dan Tiga Format Box #
Dalam representasi gambar digital, titik asal (0, 0) berada di pojok kiri atas: sumbu x bergerak ke kanan sepanjang kolom, sumbu y bergerak ke bawah sepanjang baris — kebalikan dari koordinat matematika biasa. Kita memakai konvensi inklusif: box (20, 10, 100, 50) menjangkau piksel kolom 20 sampai 100 dan baris 10 sampai 50, sehingga lebarnya 81 piksel, bukan 80. Dari konvensi yang sama, tiga format lazim dibaca:
| Format | Isi | Konversi dari xyxy | Khas dipakai di |
|---|---|---|---|
xyxy | x1, y1, x2, y2 sudut kiri-atas dan kanan-bawah | — | antarmuka PyTorch, banyak dataset |
xywh | x, y pojok kiri-atas + lebar + tinggi | w = x2-x1+1, h = y2-y1+1 | anotasi COCO, matplotlib.patches |
xywh normalized | keempat nilai dibagi lebar/tinggi gambar, rentang 0-1 | bagi ukuran gambar | berkas label YOLO |
Format normalized juga menuntut kehati-hatian presisi: lebar 81 pada gambar lebar 200 adalah 0.405, yang dibulatkan menjadi 0.41 — pembulatan di berkas label bisa menggeser box saat dikonversi kembali.
IoU: Penggaris Kemiripan Dua Box #
IoU menjawab “seberapa mirip dua kotak” dengan satu angka di rentang 0-1: luas irisan dibagi luas gabungan. Dua box identik memberi 1.0, dua box terpisah memberi 0.0, dan seluruh keadaan di antaranya terletak pada proporsi tumpang tindih. Karena sifatnya yang ternormalisasi, IoU dipakai di mana-mana: menyaring deteksi ganda di NMS, menilai kualitas lokalisasi terhadap ground truth, dan menjadi dasar ambang benar-salah pada metrik evaluasi.
Praktik: Konversi, IoU, dan NMS dalam Satu Program #
Berkas iou_dan_nms.py merangkai ketiganya: konversi tiga format dengan uji pulang-pergi, IoU skalar untuk lima kasus plus versi vektor NumPy yang menghitung seluruh kasus sekaligus, dan NMS yang memangkas empat kandidat menjadi dua deteksi:
"""Format bounding box, IoU, dan NMS: skalar Python + vektor NumPy.
Bagian 1: konversi tiga format box (xyxy, xywh, normalized) dan uji
pulang-pergi. Bagian 2: IoU skalar untuk lima kasus -- tumpang tindih
sempurna, tidak bersentuhan, satu memuat satunya, sebagian bergeser,
dan ground truth vs prediksi -- lalu versi vektor NumPy yang menghitung
seluruh kasus sekaligus. Bagian 3: NMS langkah demi langkah pada
4 box kandidat yang tersisa 2, lalu tiap deteksi dinilai IoU terhadap
ground truth dengan ambang 0.50 sebagai petunjuk TP/FP.
"""
import numpy as np
def ke_xywh(b_xyxy):
"""xyxy -> xywh: sudut kanan bawah diganti lebar dan tinggi."""
x1, y1, x2, y2 = b_xyxy
return (x1, y1, x2 - x1 + 1, y2 - y1 + 1)
def ke_xyxy(b_xywh):
"""xywh -> xyxy: lebar dan tinggi diganti sudut kanan bawah."""
x, y, w, h = b_xywh
return (x, y, x + w - 1, y + h - 1)
def ke_normalized(b_xywh, lebar_gambar, tinggi_gambar):
"""xywh -> normalized: semua koordinat dibagi ukuran gambar."""
x, y, w, h = b_xywh
return (x / lebar_gambar, y / tinggi_gambar, w / lebar_gambar, h / tinggi_gambar)
# ---------- 1. Satu box, tiga format ----------
LEBAR, TINGGI = 200, 100
xyxy = (20, 10, 100, 50) # (x1, y1, x2, y2) inklusif
xywh = ke_xywh(xyxy)
norm = ke_normalized(xywh, LEBAR, TINGGI)
print(f"gambar {LEBAR}x{TINGGI}, satu box dalam tiga format:")
print(f" xyxy : {xyxy}")
print(f" xywh : {xywh}")
print(f" normalized : ({norm[0]:.2f}, {norm[1]:.2f}, "
f"{norm[2]:.2f}, {norm[3]:.2f})")
print(f" xyxy -> xywh -> xyxy pulih utuh: {ke_xyxy(ke_xywh(xyxy)) == xyxy}")
# ---------- 2. IoU: skalar lalu vektor ----------
def luas(b):
"""Luas satu box xyxy inklusif."""
return (b[2] - b[0] + 1) * (b[3] - b[1] + 1)
def iou(a, b):
"""IoU dua box xyxy inklusif: luas irisan dibagi luas gabungan."""
x1, y1 = max(a[0], b[0]), max(a[1], b[1])
x2, y2 = min(a[2], b[2]), min(a[3], b[3])
irisan = max(0, x2 - x1 + 1) * max(0, y2 - y1 + 1)
return irisan / (luas(a) + luas(b) - irisan)
def iou_vektor(A, B):
"""IoU vektor NumPy: dua kumpulan box (N, 4) -> N skor."""
x1 = np.maximum(A[:, 0], B[:, 0])
y1 = np.maximum(A[:, 1], B[:, 1])
x2 = np.minimum(A[:, 2], B[:, 2])
y2 = np.minimum(A[:, 3], B[:, 3])
irisan = np.clip(x2 - x1 + 1, 0, None) * np.clip(y2 - y1 + 1, 0, None)
luas_A = (A[:, 2] - A[:, 0] + 1) * (A[:, 3] - A[:, 1] + 1)
luas_B = (B[:, 2] - B[:, 0] + 1) * (B[:, 3] - B[:, 1] + 1)
return irisan / (luas_A + luas_B - irisan)
kasus = [
("tumpang tindih sempurna", (10, 10, 50, 90), (10, 10, 50, 90)),
("tidak bersentuhan", (10, 10, 50, 90), (60, 20, 120, 80)),
("satu memuat satunya", (0, 0, 99, 99), (20, 10, 59, 69)),
("sebagian bergeser", (0, 0, 99, 99), (50, 0, 149, 99)),
("ground truth vs prediksi", (10, 10, 50, 90), (8, 8, 52, 92)),
]
print("\nIoU lima kasus:")
for nama, a, b in kasus:
print(f" {nama:<26s} A={a} B={b} -> IoU {iou(a, b):.3f}")
A = np.array([k[1] for k in kasus], dtype=float)
B = np.array([k[2] for k in kasus], dtype=float)
skalar = np.array([iou(k[1], k[2]) for k in kasus])
print(f"versi vektor NumPy identik dengan skalar: "
f"{np.array_equal(iou_vektor(A, B), skalar)}")
# ---------- 3. NMS: 4 kandidat menjadi 2 deteksi ----------
kandidat = [
{"kelas": "orang", "conf": 0.90, "box": (8, 8, 52, 92)},
{"kelas": "orang", "conf": 0.80, "box": (12, 10, 54, 88)},
{"kelas": "orang", "conf": 0.60, "box": (5, 5, 45, 85)},
{"kelas": "mobil", "conf": 0.85, "box": (58, 18, 122, 82)},
]
AMBANG_NMS = 0.50
urut = sorted(kandidat, key=lambda k: -k["conf"])
tersimpan = []
print(f"\nNMS (ambang IoU {AMBANG_NMS:.2f}), urut conf turun:")
for k in urut:
ket = "disimpan"
for t in tersimpan:
skor = iou(k["box"], t["box"])
if skor > AMBANG_NMS:
ket = f"ditekan (IoU {skor:.2f} dgn box conf {t['conf']:.2f})"
break
print(f" box {k['box']} conf {k['conf']:.2f} kelas {k['kelas']:>5s} "
f"-> {ket}")
if ket == "disimpan":
tersimpan.append(k)
# ---------- 4. Deteksi akhir vs ground truth ----------
gt = {"orang": (10, 10, 50, 90), "mobil": (60, 20, 120, 80)}
AMBANG_TP = 0.50
print(f"\ndeteksi akhir: {len(tersimpan)} box "
f"(dari {len(kandidat)} kandidat)")
for d in tersimpan:
skor = iou(d["box"], gt[d["kelas"]])
status = "TP" if skor >= AMBANG_TP else "FP"
print(f" kelas {d['kelas']:>5s} | box {d['box']} | conf {d['conf']:.2f} "
f"| IoU vs GT {skor:.3f} | {status} (ambang {AMBANG_TP:.2f})")Output yang Dihasilkan:
gambar 200x100, satu box dalam tiga format:
xyxy : (20, 10, 100, 50)
xywh : (20, 10, 81, 41)
normalized : (0.10, 0.10, 0.41, 0.41)
xyxy -> xywh -> xyxy pulih utuh: True
IoU lima kasus:
tumpang tindih sempurna A=(10, 10, 50, 90) B=(10, 10, 50, 90) -> IoU 1.000
tidak bersentuhan A=(10, 10, 50, 90) B=(60, 20, 120, 80) -> IoU 0.000
satu memuat satunya A=(0, 0, 99, 99) B=(20, 10, 59, 69) -> IoU 0.240
sebagian bergeser A=(0, 0, 99, 99) B=(50, 0, 149, 99) -> IoU 0.333
ground truth vs prediksi A=(10, 10, 50, 90) B=(8, 8, 52, 92) -> IoU 0.868
versi vektor NumPy identik dengan skalar: True
NMS (ambang IoU 0.50), urut conf turun:
box (8, 8, 52, 92) conf 0.90 kelas orang -> disimpan
box (58, 18, 122, 82) conf 0.85 kelas mobil -> disimpan
box (12, 10, 54, 88) conf 0.80 kelas orang -> ditekan (IoU 0.81 dgn box conf 0.90)
box (5, 5, 45, 85) conf 0.60 kelas orang -> ditekan (IoU 0.71 dgn box conf 0.90)
deteksi akhir: 2 box (dari 4 kandidat)
kelas orang | box (8, 8, 52, 92) | conf 0.90 | IoU vs GT 0.868 | TP (ambang 0.50)
kelas mobil | box (58, 18, 122, 82) | conf 0.85 | IoU vs GT 0.881 | TP (ambang 0.50)Mari kita telaah kode di atas baris per baris:
- Baris 15-27 (tiga fungsi konversi):
ke_xywhmengganti sudut kanan-bawah dengan lebar-tinggi memakai+1inklusif;ke_xyxymelakukan kebalikannya;ke_normalizedmembagi keempat nilai xywh dengan ukuran gambar. - Baris 34-41: satu box
(20, 10, 100, 50)dicetak dalam ketiga format; baris terakhir membuktikan konversi pulang-pergixyxy → xywh → xyxytidak kehilangan informasi. - Baris 48-58 (
iouskalar): irisan dihitung dari perpotongan sisi —maxuntuk sisi kiri-atas,minuntuk sisi kanan-bawah,max(0, ...)menangani box yang tidak bersentuhan; hasil dibagi gabungan dua luas dikurangi irisan. - Baris 61-70 (
iou_vektor): rumus yang sama dikerjakan serentak padaNpasang box —np.maximumdannp.clipmenggantikanmaxskalar; inilah bentuk yang dipakai saat mengevaluasi ratusan deteksi sekaligus. - Baris 73-88: lima kasus dihitung dua kali — skalar lewat loop, lalu vektor lewat
np.array— dannp.array_equalmemastikan keduanya identik. - Baris 92-113 (NMS): kandidat diurutkan turun berdasarkan
conf; tiap kandidat dibandingkan hanya dengan box yang sudah disimpan; IoU melebihi ambang berarti kandidat adalah deteksi ganda dari objek yang sama sehingga ditekan. - Baris 116-124: dua deteksi yang selamat dinilai terhadap ground truth; IoU 0.868 dan 0.881 keduanya melampaui ambang 0.50 sehingga berstatus TP — petunjuk menuju mekanika metrik evaluasi.
Membaca Hasil: Angka yang Bisa Dihitung Tangan #
Keluaran IoU layak diverifikasi manual. Kasus “sebagian bergeser”: dua box 100x100 berselisih setengah sisi — irisan 50x100 = 5000, gabungan 15000, IoU tepat 0.333. Kasus “satu memuat satunya”: box kecil 40x60 = 2400 sepenuhnya di dalam box besar 10000, jadi IoU 2400/10000 = 0.240 — nilai tertinggi yang mungkin ketika satu box memuat satunya adalah rasio luas kedua box. NMS memperlihatkan pola khasnya: tiga box orang saling tumpang tindih (IoU 0.71-0.81), hanya conf tertinggi 0.90 yang bertahan, sementara box mobil dibiarkan karena tidak bersentuhan sama sekali dengan box orang.
Format box, IoU, dan ambang TP kini menjadi kosakata lengkap kita. Pertanyaan berikutnya adalah pertanyaan evaluasi menyeluruh: bagaimana merangkum ratusan deteksi dan ground truth menjadi satu angka kualitas model — tugas chapter Metrik Object Detection .