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.

python
"""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:

text
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:

  1. 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.
  2. 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.
  3. Baris 36 (urut): np.argsort(-skor) mengurutkan indeks deteksi dari skor tertinggi — urutan pemrosesan aturan pencocokan.
  4. Baris 40-52 (iou): fungsi IoU menghitung persegi panjang irisan (max untuk sisi kiri/atas, min untuk sisi kanan/bawah), lalu membaginya dengan luas gabungan dua kotak dikurangi irisan.
  5. 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.
  6. 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.
  7. Baris 83-102 (ap_dari_deteksi): urutan TP/FP diubah menjadi presisi dan recall kumulatif. Baris envelope membuat 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.
  8. 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.
  9. 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.

MetrikArtiRumus
Precision (deteksi)proporsi deteksi yang menimpa objek sungguhanTP / (TP + FP), TP = pasangan dengan IoU >= ambang
Recall (deteksi)proporsi ground truth yang tertangkapTP / jumlah ground truth
APluas di bawah kurva P-R (interpolasi)Σ (Δrecall) x presisi_envelope
mAPrata-rata AP lintas kelas, atau lintas ambangrata-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 .