Klasifikasi memberi satu label untuk seluruh gambar; object detection memberi label sekaligus lokasi untuk setiap objek di dalamnya. Satu foto jalan bisa memuat mobil, motor, dan pejalan kaki — masing-masing perlu kelas dan kotak posisinya sendiri. Chapter ini membongkar anatomi detektor modern: bagaimana grid dan anchor box mengubah satu gambar menjadi sekumpulan prediksi, bagaimana dua keluarga arsitektur membagi strategi, dan bagaimana NMS merapikan prediksi ganda. Seluruh mekanisme lalu dijalankan pada demo NumPy murni di atas gambar sintetis 16x16.

Dari Satu Label ke Banyak Kotak #

Detection menjawab dua pertanyaan sekaligus per objek: apa (klasifikasi) dan di mana (lokalisasi). Keluaran sebuah detektor bukan lagi satu vektor probabilitas, melainkan daftar deteksi — tiap deteksi memuat kelas, skor keyakinan, dan bounding box berupa koordinat persegi yang melingkupi objek. Jumlah deteksi tidak diketahui di depan: gambar ramai bisa memuat puluhan objek, gambar sepi bisa kosong. Ketidakpastian jumlah inilah yang membuat arsitektur detection lebih rumit daripada klasifikasi murni dari chapter Klasifikasi Gambar .

Dua Keluarga Detektor #

Strategi utama terbagi dua keluarga. One-stage menembakkan prediksi langsung dari grid gambar — cepat, cocok untuk kendaraan otonom dan video real-time. Two-stage menyaring kandidat wilayah lebih dulu lewat region proposal, lalu mengklasifikasikan tiap kandidat di tahap kedua — secara historis unggul pada akurasi, dengan harga kecepatan.

AspekOne-stage (YOLO, SSD)Two-stage (keluarga R-CNN)
Alursatu jaringan memprediksi langsung dari tiap sel gridtahap 1 usulkan region, tahap 2 klasifikasi tiap region
Kecepatansangat cepat, ramah real-timelebih lambat, dua tahap berurutan
Akurasikompetitif; objek kecil ditangani anchor beragamsecara historis unggul pada presisi lokalisasi
ContohYOLO, SSD, RetinaNetFaster R-CNN, Mask R-CNN

Anatomi Detektor: Grid, Anchor, dan Offset #

Ide inti one-stage: bagi gambar menjadi grid, lalu minta tiap sel bertanggung jawab mendeteksi objek yang pusatnya berada di dalam sel. Setiap sel mengeluarkan tiga prediksi sekaligus: skor objectness (apakah ada objek di sini), offset box terhadap anchor default, dan distribusi kelas. Anchor box adalah kotak berukuran dan rasio bawaan — jaringan tidak meramal koordinat dari nol, melainkan mengoreksi anchor terdekat; kumpulan anchor beragam menangkap bentuk berbeda dari orang berdiri sampai mobil sedan. Pipeline lengkapnya:

Prediksi mentah selalu berlebih: sel-sel tetangga di sekitar satu objek sama-sama yakin, sehingga satu objek terdeteksi berkali-kali. Non-Maximum Suppression (NMS) merapikannya — urutkan berdasar skor, simpan tertinggi, tekan sisanya bila tumpang tindih melewati ambang IoU.

Demo Mekanisme dengan NumPy Murni #

Berkas deteksi_mini.py memainkan seluruh anatomi tersebut tanpa pelatihan: gambar sintetis 16x16 berisi dua objek, grid 4x4, skor objectness dari fraksi piksel terang per sel, box dari piksel terang dalam jendela 9x9 di sekitar pusat sel (memainkan peran anchor), kelas dari rasio bentuk box (memainkan peran classifier head), lalu NMS:

python
"""Mekanisme object detection: grid, prediksi per sel, dan NMS.

Gambar sintetis 16x16 memuat dua objek: kuadrat 5x5 dan batang 11x3.
Grid 4x4 membagi gambar menjadi sel 4x4; tiap sel menghasilkan skor
objectness (fraksi piksel terang di sel), box prediksi (box terketat
piksel terang dalam jendela 9x9 -- memainkan peran anchor prior), dan
kelas dari rasio lebar/tinggi box -- memainkan peran classifier head.
Kandidat lalu disaring NMS berbasis IoU. Tanpa pelatihan: seluruhnya
mekanisme.
"""

import numpy as np

UKURAN = 16
GRID = 4
SEL = UKURAN // GRID          # 4 piksel per sel grid
JENDELA = 9                   # rentang pandang per sel (peran anchor)
AMBANG_OBJECTNESS = 0.20
AMBANG_NMS = 0.40

# ---------- 1. Gambar sintetis + ground truth ----------
img = np.zeros((UKURAN, UKURAN))
img[1:6, 1:6] = 1.0           # objek A: kuadrat 5x5
img[11:14, 2:13] = 1.0        # objek B: batang 11x3

gt = {
    "kuadrat": (1, 1, 5, 5),   # (x1, y1, x2, y2) inklusif
    "batang": (2, 11, 12, 13),
}

print("gambar sintetis 16x16 ('#' = piksel objek):")
for baris in img:
    print("  " + "".join("#" if v > 0 else "." for v in baris))
print(f"\nobjek A: kuadrat 5x5 | GT = {gt['kuadrat']}")
print(f"objek B: batang 11x3 | GT = {gt['batang']}")


def iou(a, b):
    """IoU dua box (x1, y1, x2, y2) inklusif."""
    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)
    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)


def kelas_dari_rasio(box):
    """Kelas sederhana dari rasio lebar/tinggi (peran classifier head)."""
    lebar = box[2] - box[0] + 1
    tinggi = box[3] - box[1] + 1
    return "batang" if lebar / tinggi >= 1.5 else "kuadrat"


# ---------- 2. Prediksi per sel grid ----------
def prediksi_sel(baris_sel, kolom_sel):
    """Skor objectness, box, dan kelas untuk satu sel grid."""
    r0, c0 = baris_sel * SEL, kolom_sel * SEL
    objectness = img[r0:r0 + SEL, c0:c0 + SEL].mean()

    # jendela 9x9 di sekitar pusat sel: peran anchor prior
    pr, pc = r0 + SEL // 2, c0 + SEL // 2
    wr0 = max(0, pr - JENDELA // 2)
    wr1 = min(UKURAN, pr + JENDELA // 2 + 1)
    wc0 = max(0, pc - JENDELA // 2)
    wc1 = min(UKURAN, pc + JENDELA // 2 + 1)
    terang = img[wr0:wr1, wc0:wc1] > 0.5

    box = None
    if terang.any():
        baris, kolom = np.nonzero(terang)
        box = (int(kolom.min()) + wc0, int(baris.min()) + wr0,
               int(kolom.max()) + wc0, int(baris.max()) + wr0)
    return objectness, box


kandidat = []
for i in range(GRID):
    for j in range(GRID):
        skor, box = prediksi_sel(i, j)
        if box is not None and skor >= AMBANG_OBJECTNESS:
            kandidat.append({"sel": (i, j), "obj": skor, "box": box,
                             "kelas": kelas_dari_rasio(box)})

print(f"\nkandidat mentah (objectness >= {AMBANG_OBJECTNESS:.2f}): "
      f"{len(kandidat)} sel")
print(f"{'sel':>8s} {'objectness':>11s} {'box (x1,y1,x2,y2)':>19s} "
      f"{'rasio':>6s} {'kelas':>8s}")
for k in kandidat:
    b = k["box"]
    rasio = (b[2] - b[0] + 1) / (b[3] - b[1] + 1)
    teks_box = f"({b[0]},{b[1]},{b[2]},{b[3]})"
    print(f"{str(k['sel']):>8s} {k['obj']:>11.2f} {teks_box:>19s} "
          f"{rasio:>6.2f} {k['kelas']:>8s}")

# ---------- 3. NMS: tekan deteksi ganda ----------
urut = sorted(kandidat, key=lambda k: (-k["obj"], k["sel"][0], k["sel"][1]))
tersimpan = []
print(f"\nNMS (ambang IoU {AMBANG_NMS:.2f}), berurutan dari objectness tertinggi:")
for k in urut:
    status = "disimpan"
    for t in tersimpan:
        skor = iou(k["box"], t["box"])
        if skor > AMBANG_NMS:
            status = f"ditekan (IoU {skor:.2f} dgn sel {t['sel']})"
            break
    print(f"  sel {str(k['sel']):>6s} conf {k['obj']:.2f} "
          f"box {k['box']} -> {status}")
    if status == "disimpan":
        tersimpan.append(k)

# ---------- 4. Deteksi akhir vs ground truth ----------
print(f"\ndeteksi akhir: {len(tersimpan)} objek")
for d in tersimpan:
    skor = iou(d["box"], gt[d["kelas"]])
    print(f"  kelas {d['kelas']:>7s} | box {d['box']} | conf {d['obj']:.2f} "
          f"| IoU vs GT {skor:.3f}")

Output yang Dihasilkan:

text
gambar sintetis 16x16 ('#' = piksel objek):
  ................
  .#####..........
  .#####..........
  .#####..........
  .#####..........
  .#####..........
  ................
  ................
  ................
  ................
  ................
  ..###########...
  ..###########...
  ..###########...
  ................
  ................

objek A: kuadrat 5x5 | GT = (1, 1, 5, 5)
objek B: batang 11x3 | GT = (2, 11, 12, 13)

kandidat mentah (objectness >= 0.20): 9 sel
     sel  objectness   box (x1,y1,x2,y2)  rasio    kelas
  (0, 0)        0.56           (1,1,5,5)   1.00  kuadrat
  (0, 1)        0.38           (2,1,5,5)   0.80  kuadrat
  (1, 0)        0.38           (1,2,5,5)   1.25  kuadrat
  (1, 1)        0.25           (2,2,5,5)   1.00  kuadrat
  (2, 1)        0.25        (2,11,10,13)   3.00   batang
  (2, 2)        0.25        (6,11,12,13)   2.33   batang
  (3, 0)        0.25         (2,11,6,13)   1.67   batang
  (3, 1)        0.50        (2,11,10,13)   3.00   batang
  (3, 2)        0.50        (6,11,12,13)   2.33   batang

NMS (ambang IoU 0.40), berurutan dari objectness tertinggi:
  sel (0, 0) conf 0.56 box (1, 1, 5, 5) -> disimpan
  sel (3, 1) conf 0.50 box (2, 11, 10, 13) -> disimpan
  sel (3, 2) conf 0.50 box (6, 11, 12, 13) -> ditekan (IoU 0.45 dgn sel (3, 1))
  sel (0, 1) conf 0.38 box (2, 1, 5, 5) -> ditekan (IoU 0.80 dgn sel (0, 0))
  sel (1, 0) conf 0.38 box (1, 2, 5, 5) -> ditekan (IoU 0.80 dgn sel (0, 0))
  sel (1, 1) conf 0.25 box (2, 2, 5, 5) -> ditekan (IoU 0.64 dgn sel (0, 0))
  sel (2, 1) conf 0.25 box (2, 11, 10, 13) -> ditekan (IoU 1.00 dgn sel (3, 1))
  sel (2, 2) conf 0.25 box (6, 11, 12, 13) -> ditekan (IoU 0.45 dgn sel (3, 1))
  sel (3, 0) conf 0.25 box (2, 11, 6, 13) -> ditekan (IoU 0.56 dgn sel (3, 1))

deteksi akhir: 2 objek
  kelas kuadrat | box (1, 1, 5, 5) | conf 0.56 | IoU vs GT 1.000
  kelas  batang | box (2, 11, 10, 13) | conf 0.50 | IoU vs GT 0.818

Mari kita telaah kode di atas baris per baris:

  1. Baris 14-19 (konstanta): UKURAN 16, GRID 4, SEL 4 — grid 4x4 membagi gambar menjadi 16 sel berukuran 4x4 piksel; JENDELA 9 adalah rentang pandang tiap sel; dua ambang mengendalikan penyaringan.
  2. Baris 22-29: gambar sintetis dibangun dari dua blok piksel; gt menyimpan ground truth tiap objek dalam format (x1, y1, x2, y2) inklusif.
  3. Baris 38-45 (iou): irisan dihitung dari perpotongan koordinat, +1 muncul karena koordinat inklusif; IoU membagi irisan dengan gabungan — matematika lengkapnya dibahas tuntas di chapter Bounding Box .
  4. Baris 48-52 (kelas_dari_rasio): pengganti classifier head — rasio lebar/tinggi box memisahkan batang (memanjang) dari kuadrat.
  5. Baris 56-74 (prediksi_sel): inti anatomi per sel — objectness dari rata-rata piksel sel; jendela 9x9 dipusatkan di pusat sel lalu dipangkas tepi gambar, memainkan peran anchor prior; box diambil dari np.nonzero piksel terang dalam jendela — padanan offset regression pada detektor nyata.
  6. Baris 77-83: seluruh 16 sel dipindai; sel dengan objectness di bawah ambang dibuang, sisanya menjadi kandidat lengkap dengan kelas.
  7. Baris 97-110 (NMS): kandidat diurutkan turun berdasar objectness (tie-break per posisi sel agar deterministik); tiap kandidat dibandingkan dengan deteksi yang sudah disimpan — bila IoU melebihi ambang, kandidat ditekan sebagai deteksi ganda.
  8. Baris 113-117: deteksi akhir dibandingkan dengan ground truth lewat IoU — angka kualitas lokalisasi tiap deteksi.

Membaca Hasil: Sembilan Kandidat Menjadi Dua Deteksi #

Keluaran memperlihatkan tiga perilaku khas detection. Pertama, prediksi berlebih: objek kuadrat memicu empat sel tetangga, batang memicu lima — sembilan kandidat untuk dua objek. Kedua, NMS bekerja persis seperti janjinya: sel (2,1) yang boxnya identik dengan sel (3,1) ditekan pada IoU 1.00, sel-sel tetangga kuadrat ditekan pada IoU 0.64-0.80. Ketiga, kualitas deteksi tidak seragam: kuadrat terdeteksi sempurna (IoU 1.000) karena muat dalam satu jendela, sedangkan batang terpotong (IoU 0.818) karena lebarnya 11 piksel melebihi jangkauan jendela 9 — padanan nyata dari objek yang lebih besar dari receptive field, yang pada detektor sungguhan ditangani anchor berukuran besar.

Deteksi akhir kita membaca kualitasnya lewat IoU terhadap ground truth. Sebelum jauh ke metrik evaluasi lengkap, alat ukurnya perlu dibedah lebih dulu: format koordinat box, konversi antarformat, IoU, dan NMS langkah demi langkah — semuanya menunggu di chapter Bounding Box .