Model object detection mengeluarkan dua hal sekaligus: kotak deteksi dan skor keyakinan. Keberhasilan sebuah deteksi tidak lagi cukup dijawab “benar atau salah” seperti pada klasifikasi, karena jawabannya bergantung pada seberapa tepat kotak menimpa objek. Chapter ini membangun metrik evaluasi deteksi dari nol: pencocokan deteksi dengan ground truth memakai IoU, tabel kumulatif TP/FP, Average Precision lewat interpolasi kurva precision-recall, dan mAP pada sapuan ambang 0.5:0.95 ala COCO. Seluruh perhitungan kita jalankan pada satu contoh kecil yang angkanya bisa ditelusuri dengan kalkulator.
Precision dan Recall Versi Deteksi #
Pada Metrik Klasifikasi , TP dihitung dari kesamaan label. Pada deteksi objek, sebuah deteksi hanya dihitung TP bila dua syarat terpenuhi: kelasnya benar dan IoU dengan salah satu ground truth melampaui ambang (umumnya 0.5). IoU sudah kita kuasai pada chapter Bounding Box , jadi di sini langsung dipakai sebagai alat pencocokan. Dua aturan tambahan wajib dipahami:
- Satu ground truth hanya boleh dicocokkan dengan satu deteksi — deteksi kedua pada objek yang sama dihitung FP (kotak ganda).
- Pencocokan dilakukan dari skor tertinggi ke terendah, sehingga deteksi paling percaya diri berhak memilih pasangan lebih dulu.
Precision versi deteksi berarti proporsi deteksi yang benar-benar menimpa objek, sedangkan recall berarti proporsi ground truth yang berhasil ditangkap. Karena deteksi datang berurutan menurut skor, keduanya membentuk kurva: makin banyak deteksi kita terima, recall naik tetapi precision biasanya turun.
Eksperimen: Enam Deteksi pada Satu Kelas #
Berkas hitung_ap.py menyiapkan tiga ground truth dan enam deteksi satu kelas pada satu gambar, lalu menjalankan seluruh rantai perhitungan sampai mAP. Keenam deteksi sengaja dibuat beragam: ada yang pas (IoU 1.00), ada yang longgar (IoU 0.72 dan 0.81), ada kotak ganda, dan ada yang jatuh di latar belakang kosong.
"""Average Precision (AP) untuk object detection, dihitung dari nol.
Alur: 3 ground truth + 6 deteksi satu kelas -> matriks IoU -> pencocokan
rakus (greedy) pada ambang 0.5 -> tabel kumulatif TP/FP -> presisi-recall
-> interpolasi -> [email protected] -> sapuan ambang 0.5:0.95 -> mAP.
"""
import numpy as np
# ---------- 1. Data: satu kelas "kucing" pada satu gambar ----------
# Ground truth: 3 kotak (x1, y1, x2, y2)
gt = np.array(
[
[10, 10, 50, 50], # GT-1 kiri atas
[60, 10, 100, 50], # GT-2 kanan atas
[10, 60, 50, 100], # GT-3 kiri bawah
],
dtype=float,
)
# Deteksi model: (x1, y1, x2, y2, skor)
det = np.array(
[
[12, 12, 48, 48, 0.95], # D1 menimpa GT-1 (IoU 0.81)
[200, 200, 240, 240, 0.90], # D2 latar belakang kosong
[64, 14, 98, 48, 0.85], # D3 menimpa GT-2 (IoU 0.72)
[10, 60, 50, 100, 0.75], # D4 persis GT-3 (IoU 1.00)
[15, 15, 45, 45, 0.60], # D5 kotak ganda di atas GT-1
[55, 55, 75, 75, 0.40], # D6 salah lokasi
],
dtype=float,
)
kotak = det[:, :4]
skor = det[:, 4]
urut = np.argsort(-skor) # deteksi diproses dari skor tertinggi
# ---------- 2. IoU pasangan kotak ----------
def iou(a, b):
"""IoU satu pasang kotak (x1, y1, x2, y2)."""
x1 = max(a[0], b[0])
y1 = max(a[1], b[1])
x2 = min(a[2], b[2])
y2 = min(a[3], b[3])
irisan = max(0.0, x2 - x1) * max(0.0, y2 - y1)
gabungan = (
(a[2] - a[0]) * (a[3] - a[1])
+ (b[2] - b[0]) * (b[3] - b[1])
- irisan
)
return irisan / gabungan if gabungan > 0 else 0.0
matriks_iou = np.array([[iou(d, g) for g in gt] for d in kotak])
print("matriks IoU (baris = deteksi D1..D6, kolom = GT-1..GT-3):")
print(np.round(matriks_iou, 2))
print()
# ---------- 3. Pencocokan rakus pada satu ambang ----------
def cocokkan(ambang):
"""Greedy matching: deteksi urut skor; GT terbaik yang belum terpakai."""
terpakai = np.zeros(len(gt), dtype=bool)
hasil = [] # (indeks_deteksi, indeks_gt atau -1, iou_terbaik)
for i in urut:
kandidat = np.where(~terpakai)[0]
if len(kandidat) == 0:
hasil.append((i, -1, matriks_iou[i].max() if len(gt) else 0.0))
continue
j = kandidat[np.argmax(matriks_iou[i, kandidat])]
if matriks_iou[i, j] >= ambang:
terpakai[j] = True
hasil.append((i, j, matriks_iou[i, j]))
else:
hasil.append((i, -1, matriks_iou[i].max()))
return hasil
# ---------- 4. AP dari urutan TP/FP ----------
def ap_dari_deteksi(ambang):
"""AP satu kelas dengan interpolasi semua titik (gaya COCO)."""
hasil = cocokkan(ambang)
tp = np.array([1.0 if j >= 0 else 0.0 for _, j, _ in hasil])
tp_kum = np.cumsum(tp)
fp_kum = np.cumsum(1.0 - tp)
presisi = tp_kum / (tp_kum + fp_kum)
recall = tp_kum / len(gt)
# presisi envelope: tiap titik diganti maksimum presisi di kanannya
envelope = presisi.copy()
for k in range(len(envelope) - 2, -1, -1):
envelope[k] = max(envelope[k], envelope[k + 1])
# luas di bawah kurva P-R bertingkat
ap = 0.0
sebelum = 0.0
for k in range(len(envelope)):
if recall[k] > sebelum:
ap += (recall[k] - sebelum) * envelope[k]
sebelum = recall[k]
return ap, tp, presisi, envelope, recall, hasil
# ---------- 5. Tabel kumulatif pada ambang 0.5 ----------
ap05, tp, presisi, envelope, recall, hasil = ap_dari_deteksi(0.5)
tp_kum = np.cumsum(tp)
fp_kum = np.cumsum(1.0 - tp)
print("pencocokan ambang IoU 0.5 (diproses dari skor tertinggi):")
print(f"{'det':>4} {'skor':>5} {'GT':>6} {'IoU':>5} {'TP/FP':>6} "
f"{'TP kum':>6} {'FP kum':>6} {'presisi':>8} {'recall':>7}")
for posisi, (i, j, _) in enumerate(hasil):
label = f"GT-{j + 1}" if j >= 0 else "-"
status = "TP" if j >= 0 else "FP"
print(f"{'D' + str(i + 1):>4} {skor[i]:5.2f} {label:>6} "
f"{matriks_iou[i].max():5.2f} {status:>6} "
f"{int(tp_kum[posisi]):6d} {int(fp_kum[posisi]):6d} "
f"{presisi[posisi]:8.3f} {recall[posisi]:7.3f}")
print()
print("presisi mentah vs presisi hasil interpolasi (envelope):")
print(f"{'recall':>7} {'mentah':>7} {'envelope':>8}")
for k in range(len(presisi)):
print(f"{recall[k]:7.3f} {presisi[k]:7.3f} {envelope[k]:8.3f}")
print(f"[email protected] = {ap05:.4f} (luas di bawah kurva P-R bertingkat)")
print()
# ---------- 6. Sapuan ambang 0.5:0.95 ----------
print("sapuan ambang COCO (10 nilai, langkah 0.05):")
total = 0.0
for ambang in np.arange(0.5, 0.96, 0.05):
nilai, _, _, _, _, _ = ap_dari_deteksi(round(ambang, 2))
total += nilai
print(f" ambang {ambang:.2f}: AP = {nilai:.4f}")
peta_rata = total / 10
print(f"mAP@[0.5:0.95] = {peta_rata:.4f}")
print()
print("mAP turun ketika ambang IoU diperketat: D1 (IoU 0.81) mulai")
print("ditolak di ambang 0.85, D3 (IoU 0.72) mulai ditolak di 0.75.")Output yang Dihasilkan:
matriks IoU (baris = deteksi D1..D6, kolom = GT-1..GT-3):
[[0.81 0. 0. ]
[0. 0. 0. ]
[0. 0.72 0. ]
[0. 0. 1. ]
[0.56 0. 0. ]
[0. 0. 0. ]]
pencocokan ambang IoU 0.5 (diproses dari skor tertinggi):
det skor GT IoU TP/FP TP kum FP kum presisi recall
D1 0.95 GT-1 0.81 TP 1 0 1.000 0.333
D2 0.90 - 0.00 FP 1 1 0.500 0.333
D3 0.85 GT-2 0.72 TP 2 1 0.667 0.667
D4 0.75 GT-3 1.00 TP 3 1 0.750 1.000
D5 0.60 - 0.56 FP 3 2 0.600 1.000
D6 0.40 - 0.00 FP 3 3 0.500 1.000
presisi mentah vs presisi hasil interpolasi (envelope):
recall mentah envelope
0.333 1.000 1.000
0.333 0.500 0.750
0.667 0.667 0.750
1.000 0.750 0.750
1.000 0.600 0.600
1.000 0.500 0.500
[email protected] = 0.8333 (luas di bawah kurva P-R bertingkat)
sapuan ambang COCO (10 nilai, langkah 0.05):
ambang 0.50: AP = 0.8333
ambang 0.55: AP = 0.8333
ambang 0.60: AP = 0.8333
ambang 0.65: AP = 0.8333
ambang 0.70: AP = 0.8333
ambang 0.75: AP = 0.5000
ambang 0.80: AP = 0.5000
ambang 0.85: AP = 0.0833
ambang 0.90: AP = 0.0833
ambang 0.95: AP = 0.0833
mAP@[0.5:0.95] = 0.5417
mAP turun ketika ambang IoU diperketat: D1 (IoU 0.81) mulai
ditolak di ambang 0.85, D3 (IoU 0.72) mulai ditolak di 0.75.Mari kita telaah kode di atas baris per baris:
- Baris 12-19 (
gt): tiga kotak ground truth dalam format(x1, y1, x2, y2)— sudut kiri atas dan kanan bawah, konvensi yang sama dengan chapter Bounding Box. - Baris 22-32 (
det): enam deteksi dengan kolom kelima berisi skor keyakinan model — D1 menimpa GT-1 (IoU 0.81), D3 menimpa GT-2 (0.72), dan D5 kotak ganda di atas GT-1 yang sudah diambil D1. - Baris 36 (
urut):np.argsort(-skor)mengurutkan indeks deteksi dari skor tertinggi — urutan pemrosesan aturan pencocokan. - Baris 40-52 (
iou): fungsi IoU menghitung persegi panjang irisan (maxuntuk sisi kiri/atas,minuntuk sisi kanan/bawah), lalu membaginya dengan luas gabungan dua kotak dikurangi irisan. - Baris 55-57: matriks IoU 6x3 dicetak penuh — baris D4 hanya berisi nilai 1.00 pada GT-3, tanda kotak deteksi persis menimpa ground truth.
- Baris 62-77 (
cocokkan): pencocokan rakus. Untuk tiap deteksi (urut skor), ground truth dengan IoU terbesar yang belum terpakai dipilih; bila IoU-nya melampaui ambang, pasangan menjadi TP dan ground truth ditandai terpakai — deteksi lain yang datang belakangan ke ground truth sama otomatis menjadi FP. - Baris 83-102 (
ap_dari_deteksi): urutan TP/FP diubah menjadi presisi dan recall kumulatif. Barisenvelopemembuat salinan presisi lalu mengganti tiap titik dengan maksimum presisi di kanannya — inilah interpolasi yang membuat kurva monoton turun. Luas di bawah kurva bertingkat dijumlahkan pada baris 96-101. - Baris 106-125: tabel kumulatif ambang 0.5 menunjukkan presisi mentah turun ke 0.500 pada D2, tetapi tabel kedua memperlihatkan envelope menaikkannya menjadi 0.750 — presisi 0.500 itu “diganti” oleh titik D4 pada recall lebih tinggi.
- Baris 129-137: sapuan ambang 0.50 sampai 0.95 menghitung AP sepuluh kali lalu merata-ratakannya menjadi mAP. AP turun ke 0.5000 begitu ambang melewati IoU D3 (0.72), lalu ke 0.0833 begitu melewati IoU D1 (0.81) — hanya D4 dengan IoU sempurna yang bertahan.
Dari Kurva P-R ke Average Precision #
Average Precision (AP) adalah luas di bawah kurva precision-recall — satu angka yang merangkum seluruh trade-off. Interpolasi envelope dipakai karena kurva P-R mentah bergerigi: precision bisa turun sementara ketika recall naik. Dengan mengganti tiap titik oleh maksimum presisi di kanannya, kurva menjadi monoton dan hasilnya stabil terhadap getaran kecil. Pada contoh kita, [email protected]
bernilai 0.8333: segmen recall 0 sampai 0.333 menyumbang luas 0.333 x 1.000, lalu dua segmen berikutnya masing-masing 0.333 x 0.750.
| Metrik | Arti | Rumus |
|---|---|---|
| Precision (deteksi) | proporsi deteksi yang menimpa objek sungguhan | TP / (TP + FP), TP = pasangan dengan IoU >= ambang |
| Recall (deteksi) | proporsi ground truth yang tertangkap | TP / jumlah ground truth |
| AP | luas di bawah kurva P-R (interpolasi) | Σ (Δrecall) x presisi_envelope |
| mAP | rata-rata AP lintas kelas, atau lintas ambang | rata-rata AP untuk tiap kelas / ambang |
mAP dan Ambang 0.5:0.95 #
Satu angka [email protected]
masih memanjakan model: kotak dengan IoU 0.51 dan kotak dengan IoU 0.95 sama-sama dianggap TP. Kompetisi COCO karena itu merata-ratakan AP pada sepuluh ambang dari 0.50 hingga 0.95 — notasi mAP@[0.5:0.95]. Pada contoh kita nilainya 0.5417, jauh di bawah [email protected]
sebesar 0.8333, karena dua deteksi dengan kotak longgar mulai ditolak begitu ambang diperketat. Metrik multi-ambang ini memaksa model menghasilkan kotak yang benar-benar presisi, bukan sekadar “kira-kira menimpa”.
Dengan kotak dan skornya kini terukur adil, pertanyaan berikutnya memindahkan target dari kotak ke piksel: bagaimana menilai model yang melabeli setiap piksel gambar? Chapter Semantic Segmentation
melanjutkan ke metrik tingkat piksel. Praktik evaluasi deteksi dengan torchvision bisa dipelajari di tutorial deteksi objek PyTorch
; protokol resmi COCO dirinci di cocodataset.org
.