Setiap proyek klasifikasi gambar dimulai dari pertanyaan yang sama: dari mana data berasal dan seperti apa isinya. Dataset foto daun nyata seperti PlantVillage berukuran puluhan gigabyte dan membutuhkan waktu unduhan serta pelatihan yang lama, jadi kita membangun dataset sintetis yang meniru struktur masalahnya — daun sehat dengan pola vena rapi versus daun berbintak dengan bercak gelap. Generator berjalan dalam hitungan detik, hasilnya identik setiap eksekusi, dan pola kodenya tetap berlaku ketika nanti Anda menukar data sintetis dengan foto nyata.
Membangun Dataset Sintetis #
Tiga fungsi inti pada data/generate_data.py membangun gambar dasar yang sama untuk kedua kelas, lalu berbeda pada gangguan yang ditambahkan:
def dasar_daun(rng):
"""Gambar dasar yang dipakai kedua kelas: latar, helai, dan vena."""
pusat_x = 7.5 + rng.uniform(-0.5, 0.5)
pusat_y = 7.5 + rng.uniform(-0.5, 0.5)
mask = mask_daun(pusat_x, pusat_y,
6.3 + rng.uniform(-0.4, 0.4),
7.2 + rng.uniform(-0.4, 0.4))
img = np.full((UKURAN, UKURAN), rng.uniform(18, 30), dtype=np.float64)
img[mask] = rng.uniform(140, 165) # helai daun lebih terang
vena = pola_vena()
img[vena & mask] = rng.uniform(195, 215) # vena paling terang
return img, mask, vena
def gambar_daun_sehat(rng):
"""Kelas 0: vena rapi; kadang ada 1-2 piksel noise gelap (bukan bercak)."""
img, mask, vena = dasar_daun(rng)
if rng.random() < 0.22: # noise gelap sesekali
for _ in range(int(rng.integers(1, 3))):
y, x = int(rng.integers(4, 12)), int(rng.integers(4, 12))
if mask[y, x] and not vena[y, x]:
img[y, x] -= rng.uniform(35, 60)
img += rng.normal(0, 5.0, (UKURAN, UKURAN))
return np.clip(img, 0, 255), 0, 0
def gambar_daun_berbintak(rng):
"""Kelas 1: vena yang sama + 1-4 bercak gelap bundar posisi seeded."""
img, mask, vena = dasar_daun(rng)
jumlah = int(rng.integers(1, 5)) # 1-4 bercak
kontras_rendah = rng.random() < 0.15 # kasus sulit: 1 bercak samar
if kontras_rendah:
jumlah = 1
terpasang = 0
percobaan = 0
while terpasang < jumlah and percobaan < 50:
percobaan += 1
y = int(rng.integers(2, 14))
x = int(rng.integers(2, 14))
if not mask[y, x] or vena[y, x]:
continue
radius = 1 if kontras_rendah else int(rng.choice([1, 2], p=[0.4, 0.6]))
nilai = rng.uniform(65, 85) if kontras_rendah else rng.uniform(38, 70)
yy, xx = np.mgrid[0:UKURAN, 0:UKURAN]
bercak = (xx - x) ** 2 + (yy - y) ** 2 <= radius ** 2
img[bercak & mask] = nilai
terpasang += 1
img += rng.normal(0, 5.0, (UKURAN, UKURAN))
return np.clip(img, 0, 255), 1, terpasangMari kita telaah fungsi di atas baris per baris:
rng.uniform(-0.5, 0.5)padadasar_daun: pusat dan ukuran helai daun digeser sedikit pada setiap gambar sehingga model tidak menghafal satu posisi tetap — variasi kecil yang meniru foto nyata yang jarang terpusat sempurna.img[mask] = rng.uniform(140, 165): mask elips memisahkan tiga tingkat kecerahan — latar gelap, helai daun, dan vena paling terang — sehingga struktur daun terbaca tanpa warna.rng.random() < 0.22padagambar_daun_sehat: sekitar 22 persen daun sehat membawa satu-dua piksel noise gelap yang menyerupai bercak sangat kecil. Gangguan halus ini membuat sebagian kecil daun sehat sulit dibedakan — dan akan muncul kembali sebagai tiga false positive pada analisis Bagian 5.kontras_rendah = rng.random() < 0.15: sekitar 15 persen daun berbintak hanya mendapat satu bercak dengan nilai piksel 65-85 (lebih samar daripada bercak biasa yang bernilai 38-70). Kasus sulit ini sengaja dipertahankan agar proyek memiliki kesalahan nyata untuk dianalisis pada Bagian 5.bercak = (xx - x) ** 2 + (yy - y) ** 2 <= radius ** 2: bercak digambar sebagai lingkaran — cakram piksel gelap beradius 1 atau 2 — pada posisi acak yang tetap berada di dalam helai daun.rng.normal(0, 5.0, ...): noise sensor ditambahkan terakhir agar kedua kelas tidak terlihat artifaktual sempurna;np.clip(0, 255)menjaga nilai tetap sahih untuk uint8.
Seluruh angka acak berasal dari satu generator np.random.default_rng(42), sehingga dataset yang dihasilkan identik pada setiap eksekusi di mesin mana pun — prasyarat hasil pelatihan yang dapat direproduksi.
Menjalankan Generator #
Dari folder proyek, dataset dibangun dengan satu perintah:
python3 data/generate_data.pyOutput yang Dihasilkan:
berkas : /tmp/daun_sintetis.npz
isi : 300 gambar 16x16 grayscale (150 sehat, 150 berbintak)
piksel : min 0, maks 229 (uint8 0-255)
bercak : 1-4 per daun berbintakBerkas .npz berukuran belasan kilobyte dan berisi tiga array sekaligus:
| Array | Bentuk | Isi |
|---|---|---|
X | (300, 16, 16) uint8 | intensitas piksel 0-255 |
y | (300,) int64 | label 0 = sehat, 1 = berbintak |
jumlah_bintak | (300,) int64 | metadata jumlah bercak per gambar |
Array metadata jumlah_bintak tidak dipakai model saat pelatihan — metadata ini menunggu perannya pada Bagian 5, ketika kita menguji hipotesis bahwa kesalahan model terkonsentrasi pada daun dengan sedikit bercak.
Memeriksa Isi Data #
Pemeriksaan ringkas memastikan data sesuai harapan sebelum melatih model — pola yang dibahas chapter Analisis Kesalahan sebagai langkah paling murah namun sering dilewatkan:
import numpy as np
data = np.load("/tmp/daun_sintetis.npz")
X, y, jumlah_bintak = data["X"], data["y"], data["jumlah_bintak"]
print("bentuk X :", X.shape, X.dtype)
print("label :", np.bincount(y).tolist(), "(0 = sehat, 1 = berbintak)")
print("bercak :", jumlah_bintak[y == 1].min(), "-",
jumlah_bintak[y == 1].max(), "per daun berbintak")
for indeks in (0, 200):
print(f"\ngambar #{indeks} (label {y[indeks]}, {jumlah_bintak[indeks]} bercak):")
for baris in X[indeks]:
print("".join("#" if v > 185 else ("+" if v > 120 else ("." if v > 60 else " "))
for v in baris))Output yang Dihasilkan:
bentuk X : (300, 16, 16) uint8
label : [150, 150] (0 = sehat, 1 = berbintak)
bercak : 1 - 4 per daun berbintak
gambar #0 (label 0, 0 bercak):
+++#++
++#+#+#++
++++###++++
++++++#+++++
+++#++#++#++
++++#+#+#++++
+++++###+++++
++++++#++++++
++++++#++++++
++++#+#+#+++
++++###++++
+++++#++++
++++#++++
+++#++
gambar #200 (label 1, 3 bercak):
++++#++
+++ +#+#+
+++ ##+++
++ ++++
++++ #++#++
+++ +#+#+++
++ #+++++
+++ +#++++.
++++ ++#+++..
+++++#+#+#...
+++++###++.
++++++#++++
+++++#+++
+++#+
Kedua kelas berjumlah sama (150 dan 150) sehingga akurasi sudah layak dipakai sebagai metrik utama — baseline tebakan acak berada di 0.50. Peta karakter memperlihatkan vena tengah dan cabang simetris pada daun sehat, serta tiga area gelap (karakter titik dan spasi) pada daun berbintak di posisi acak. Bercak gelap inilah satu-satunya sinyal pembeda antar kelas — persis seperti foto nyata, tempat gejala penyakit muncul sebagai area lokal, bukan perubahan seluruh gambar.
Data kini terkumpul dan sudah diperiksa: 300 gambar seimbang, dua kelas dengan satu sinyal pembeda, plus metadata jumlah bercak. Pada bagian berikutnya kita mengubah tumpukan uint8 ini menjadi tensor float yang siap dikonsumsi CNN.