Model deteksi belajar dari pasangan (gambar, label) — dan labelnya lebih kaya daripada klasifikasi: selain kelas, setiap objek perlu kotak pembatas. Menyediakan dataset berlabel bounding box biasanya berbiaya tinggi karena anotasinya manual, sebagaimana dibahas pada chapter Dataset Labeling . Proyek ini menempuh jalan yang bisa diulang di CPU: gambar 32x32 dibangkitkan sintetis dari generator NumPy berbenih, sehingga labelnya lahir bersama gambar secara gratis — pola data sintetis yang diulas pada chapter Synthetic Data .

Satu Gambar, Satu Label Lengkap #

Fungsi buat_gambar di src/data_deteksi.py menarik satu blok persegi ke dalam latar derau:

python src/data_deteksi.py — pembangkit satu gambar
def buat_gambar(rng: np.random.Generator):
    """Satu gambar sintetis beserta label kelas dan bounding box."""
    kelas = int(rng.integers(0, 3))
    sisi = UKURAN_KELAS[kelas]
    x0 = int(rng.integers(1, UKURAN_GAMBAR - sisi + 1))
    y0 = int(rng.integers(1, UKURAN_GAMBAR - sisi + 1))
    gambar = np.clip(rng.normal(0.12, 0.04,
                                (UKURAN_GAMBAR, UKURAN_GAMBAR)), 0.0, 1.0)
    blok = np.clip(rng.normal(INTENSITAS_KELAS[kelas], 0.03, (sisi, sisi)),
                   0.0, 1.0)
    gambar[y0:y0 + sisi, x0:x0 + sisi] = blok
    kotak = (float(x0), float(y0), float(sisi), float(sisi))
    return gambar.astype(np.float32), kelas, kotak

Tiga kelas dibuat mudah dibedakan lewat dua sinyal sekaligus: ukuran blok (4, 6, atau 8 piksel) dan intensitasnya (0.90, 0.60, atau 0.35) — identitas ganda ini memberi model jalur belajar yang jelas tanpa membuat tugas menjadi remeh. Posisi pojok kiri atas (x0, y0) diambil acak dengan batas sehingga blok selalu berada penuh di dalam kanvas. Label yang dikembalikan berformat (kelas, x, y, lebar, tinggi) — format xywh yang sama dengan konvensi pada chapter Bounding Box .

Fungsi buat_dataset memanggil generator ini berulang kali dengan np.random.default_rng(benih), sehingga dataset dengan benih sama selalu identik. Skrip data/generate_data.py merangkumnya menjadi tiga subset dan memverifikasi hasilnya:

bash
python3 data/generate_data.py

Output yang Dihasilkan:

text
== pembangkitan dataset deteksi objek ==
latih     : 480 gambar (benih 11), kelas [165, 152, 163]
validasi  : 120 gambar (benih 22), kelas [41, 40, 39]
uji       :  60 gambar (benih 77), kelas [21, 18, 21]

berkas npz      : /tmp/dl_deteksi_demo/dataset.npz
ukuran          : 2181581 byte
termuat identik : True

statistik piksel (latih):
  min 0.000 max 0.994 rata-rata 0.134
nama kelas : kecil, sedang, besar
dataset di /tmp sengaja tidak disimpan di bundle karena
dapat dibangun ulang identik kapan pun dari benih.

Ketiga subset memakai benih berbeda (11, 22, 77) sehingga tidak ada gambar yang tumpang-tindih antara latih, validasi, dan uji — pengaman pembagian data yang dibahas pada chapter Pembagian Data Training, Validasi, dan Testing . Distribusi kelas yang hampir seimbang (165-152-163 pada latih) membuat akurasi dan IoU rata-rata langsung layak dibandingkan tanpa penyesuaian bobot kelas.

Mengukur Kesesuaian Dua Kotak #

Kualitas deteksi diukur dengan Intersection over Union — luas irisan dua kotak dibagi luas gabungannya. Fungsinya hidup di src/data_deteksi.py dan menjadi bahasa bersama seluruh proyek:

python src/data_deteksi.py — fungsi iou
def iou(a, b) -> float:
    """IoU dua kotak format (x, y, lebar, tinggi), boleh pecahan."""
    ax1, ay1 = a[0], a[1]
    ax2, ay2 = a[0] + a[2], a[1] + a[3]
    bx1, by1 = b[0], b[1]
    bx2, by2 = b[0] + b[2], b[1] + b[3]
    lebar = max(0.0, min(ax2, bx2) - max(ax1, bx1))
    tinggi = max(0.0, min(ay2, by2) - max(ay1, by1))
    irisan = lebar * tinggi
    gabungan = a[2] * a[3] + b[2] * b[3] - irisan
    return irisan / gabungan if gabungan > 0 else 0.0

Perhitungannya bekerja pada kontinu, bukan piksel bulat: max(0.0, ...) menjaga irisan tidak bernilai negatif saat dua kotak tidak bersentuhan, dan penjaga gabungan > 0 menghindari pembagian nol. Pratinjau python3 src/data_deteksi.py memperlihatkan perilakunya sekaligus isi dataset:

text
== pratinjau dataset sintetis ==
bentuk X            : (6, 1, 32, 32)
distribusi kelas    : [3, 2, 1] (kecil, sedang, besar)
sampel 0: kelas 0 (kecil), kotak (4,23,4,4), rata-rata blok 0.90 vs latar 0.13
sampel 1: kelas 1 (sedang), kotak (19,17,6,6), rata-rata blok 0.60 vs latar 0.14
sampel 2: kelas 1 (sedang), kotak (4,12,6,6), rata-rata blok 0.61 vs latar 0.14
iou kotak [0] vs [0]: 1.00
iou kotak [0] vs [1]: 0.00

Tiga sampel yang ditampilkan sudah memuat tiga informasi penting. Bentuk X adalah (6, 1, 32, 32) — satu kanal abu-abu per gambar, format batch yang langsung siap masuk nn.Conv2d. Rata-rata intensitas blok (0.90, 0.60, 0.61) kontras terhadap latar (0.13, 0.14), menegaskan bahwa sinyal objek memang bisa dibaca dari piksel. Dua baris terakhir memverifikasi fungsi IoU pada kasus batas: kotak terhadap dirinya sendiri bernilai tepat 1.00, sedangkan dua kotak yang tidak bersentuhan bernilai 0.00 — kasus uji yang sama diperiksa otomatis oleh tests/test_iou.py.

Dataset dan bahasa ukurnya kini siap. Bagian berikutnya merancang arsitektur yang memetakan setiap gambar menjadi sepasang keluaran: skor tiga kelas dan empat angka kotak.