Pengolahan Citra · Materi 13

Custom Dataset dan Training YOLO

Dataset berizin 100–200 gambar, anotasi YOLO, split scene anti-leakage, training dan evaluasi terpisah.

Google Colab · OpenCV · Pemahaman konsep → pengamatan → praktikum → analisis

Mode belajar: tulis kode sendiri. Copy/cut, seleksi teks dan klik kanan dibatasi pada halaman; pembatasan UI bukan jaminan anti-penyalinan mutlak.

1. Tujuan

  1. Membuat dataset 100–200 gambar berizin dengan 1–3 kelas dan anotasi normalized yang diaudit.
  2. Memvalidasi ZIP, class token, pasangan gambar-label serta koordinat sebelum training.
  3. Memisahkan train/val/test per scene dan menyusun data.yaml konsisten.
  4. Melatih yolov8n.pt serta menafsirkan precision, recall, mAP dan tanda overfitting.

Prasyarat: Modul 10–12; memahami bbox, kelas, confidence, IoU dan manajemen file Python.

2. Materi Pembahasan

Diagram alur; bukan klaim sebuah dataset atau training sudah tersedia.
Diagram alur; bukan klaim sebuah dataset atau training sudah tersedia.

2.1 Pertanyaan proyek dan izin data

Custom detection dimulai dari definisi kelas yang operasional, bukan langsung menekan tombol train. Kelompok memilih satu hingga tiga kategori yang dapat dibedakan secara visual dan menjelaskan aturan objek terpotong, tertutup, atau ambigu. Kumpulkan 100–200 gambar dengan variasi sudut, jarak, pencahayaan dan latar. Jumlah gambar saja tidak menjamin keragaman; seratus frame nyaris identik menawarkan informasi lebih sedikit daripada scene yang independen.

Gunakan foto buatan sendiri dengan persetujuan atau sumber yang izinnya sesuai. Catat sumber dan izin dalam LICENSE.md serta manifest tiap gambar. Hindari identitas pribadi yang tidak diperlukan. Program dapat memeriksa kolom izin tidak kosong, tetapi tidak dapat membuktikan lisensi sah. Tanggung jawab audit sumber dan dokumentasi tetap pada kelompok. Dataset kecil ini untuk latihan dan tidak otomatis layak penerapan berisiko tinggi.

2.2 Anotasi sebagai ground truth yang ditinjau

Satu objek target mendapatkan satu baris label: class_id x_center y_center width height. ID kelas adalah integer mulai nol mengikuti classes.json. Pusat x dan lebar dibagi lebar gambar; pusat y dan tinggi dibagi tinggi gambar. Koordinat dinormalisasi, ukuran harus positif, dan batas kotak harus tetap berada dalam gambar. Simpan gambar serta label dengan stem identik, misalnya scene01_001.jpg dan scene01_001.txt.

Anotasi harus mencakup seluruh objek target sesuai kebijakan, bukan hanya objek yang mudah dilihat. File kosong menyatakan tidak ada objek target, bukan belum selesai dilabeli. Contoh hanya menerima label kosong jika manifest menandai negative=true dan reviewed=true setelah pemeriksaan. Validasi numerik menangkap token keliru, NaN dan kotak di luar gambar, tetapi audit visual tetap diperlukan untuk kelas salah, objek terlewat dan bbox yang tidak rapat.

2.3 Pemisahan berdasarkan scene, bukan kebocoran

Train dipakai memperbarui bobot, validation memandu pemilihan model dan keputusan eksperimen, sedangkan test disimpan untuk evaluasi akhir. Gambar satu sesi, scene video, objek fisik yang sama berulang, atau rangkaian pengambilan yang sangat mirip perlu dikelompokkan pada satu split. Mengacak frame satu per satu dapat membuat model melihat latar dan objek hampir identik di train dan test sehingga angka evaluasi terlalu optimistis.

Contoh mencari split scene yang mendekati proporsi gambar 70/15/15 sambil menjaga semua kelas muncul pada tiap bagian. Proporsi aktual harus ditampilkan karena scene memiliki ukuran berbeda. Duplikat piksel identik ditolak, tetapi gambar nyaris sama tidak dijamin terdeteksi oleh hash; kelompok wajib meninjau grouping secara manual. Jika distribusi kelas tidak memungkinkan split, tambahkan scene independen, jangan memecah scene untuk mendapatkan angka yang lebih bagus.

2.4 Training dan konsistensi konfigurasi

ZIP diunggah dengan kontrak sederhana: folder images dan labels, daftar kelas, manifest, serta dokumen izin. Ekstraksi membatasi jalur dan ukuran agar arsip tidak menulis di luar direktori atau menghabiskan storage. Setelah validasi, program menyalin pasangan ke images/train, images/val, images/test dan folder labels sejajar. data.yaml dibuat dari konfigurasi yang sudah diperiksa, bukan mempercayai path dari arsip.

Fine-tuning memulai dari yolov8n.pt dan memperbarui bobot untuk kategori custom. Epoch, imgsz, batch dan device dicatat agar eksperimen dapat diulang. GPU disarankan; CPU tetap sah dengan waktu lebih panjang. Dataset kecil dapat cepat overfit sehingga 30 epoch adalah titik awal, bukan resep optimal. best.pt dipilih melalui validation internal training. Contoh tidak membuat label palsu dan tidak memakai coco8 sebagai pengganti tugas 100–200 gambar.

2.5 Metrik, kurva dan generalisasi

Precision mengukur bagian prediksi positif yang benar, sedangkan recall mengukur bagian objek ground truth yang berhasil ditemukan. Average precision merangkum hubungan precision–recall pada rentang skor. mAP50 menggunakan kecocokan IoU 0,50, sedangkan mAP50–95 merangkum beberapa threshold IoU dan lebih menuntut lokasi kotak yang akurat. Angka agregat harus dibaca bersama jumlah contoh, keterwakilan kelas, confusion matrix dan visualisasi kesalahan.

Loss train yang terus turun tetapi kurva validation memburuk dapat mengindikasikan overfitting, meskipun noise label atau pergeseran distribusi juga perlu diperiksa. Pilih perubahan eksperimen berdasarkan validation dan kunci keputusan sebelum test akhir. Jangan memilih threshold berulang kali dari test lalu menyebutnya evaluasi independen. Laporkan hasil nyata walaupun rendah, diskusikan keterbatasan dataset, dan tampilkan notebook dengan output training serta evaluasi yang dapat diperiksa.

Video pembelajaran sesuai bab

How to Train Ultralytics YOLOv8 models on Your Custom Dataset in Google Colab | Episode 3 — Ultralytics.

Fokus saat menonton: Ikuti anotasi dataset, konfigurasi dan training di Google Colab; bandingkan praktik split dengan pencegahan leakage pada bab ini.

Buka video di YouTube jika pemutar tidak tersedia

Video referensi berbahasa Inggris. Bila ada setup lokal, API atau versi model berbeda, pelajari konsepnya dan tetap gunakan alur Google Colab pada praktikum bab ini. Ketersediaan pemutar mengikuti YouTube, pemilik video dan jaringan.

Referensi dan sumber ilustrasi

Foto daun: Ανώνυμος Βικιπαιδιστής, Wikimedia Commons, CC BY 3.0; diubah ukuran/intensitas untuk ilustrasi. Foto bus: contoh aset Ultralytics. Foto astronaut: NASA melalui scikit-image. Panel diagram/simulasi ditandai pada caption; bukan hasil eksperimen yang dikarang. Hak penggunaan sumber tetap mengikuti sumber asli.

3. Praktikum Google Colab

Buka Google Colab

Buat notebook baru, simpan pada Drive milik sendiri, lalu isi identitas. Gunakan sel kode terpisah untuk setiap langkah dan jalankan berurutan dengan Shift + Enter. Tidak perlu mengunduh notebook contoh atau memasang Python lokal.

Identitas di bagian paling atas notebook: tambahkan sel Text/Markdown sebelum kode dan isi Nama serta NPM.
# Materi 13 — Pengolahan Citra
Nama: [Nama lengkap mahasiswa]
NPM: [NPM mahasiswa]
Kelas: [Kelas, jika diperlukan]
  1. Bekerja berkelompok; sepakati definisi 1–3 kelas dan aturan anotasi sebelum pengumpulan.
  2. Kumpulkan 100–200 gambar berizin, minimal 6 scene independen dan lebih baik 10 atau lebih; catat scene dan sumber.
  3. Anotasi seluruh target melalui alat anotasi pilihan dan ekspor format YOLO; review silang oleh anggota lain.
  4. Buat ZIP sesuai kontrak, tanpa folder induk tambahan; unggah dan jalankan validator sampai benar.
  5. Periksa beberapa label secara visual, laporan split aktual dan data.yaml sebelum train.
  6. Aktifkan GPU jika tersedia, jalankan training dan tampilkan output history serta validation.
  7. Kunci keputusan lalu evaluasi test; jelaskan overfit, kesalahan kelas dan keterbatasan generalisasi.
  8. Simpan semua output yang relevan dalam notebook dan kumpulkan satu link Viewer per kelompok.

Sel 1 — Instalasi Ultralytics

Jalankan sebagai sel tersendiri di Colab; tidak perlu meng-upgrade NumPy. Catat versi paket di sel berikutnya.

%pip install -q ultralytics

Sel 2 — Kontrak dataset dan upload ZIP

Siapkan ZIP berisi images/<nama>.jpg atau .png, labels/<stem>.txt, classes.json berupa list nama 1–3 kelas, manifest.csv, dan LICENSE.md. CSV wajib memiliki filename,scene_id,license,reviewed,negative. reviewed dan negative memakai true/false; label kosong hanya untuk negative=true yang sudah diperiksa. Jangan mengunggah gambar tanpa anotasi lalu menganggap label kosong sebagai ground truth.

from pathlib import Path, PurePosixPath
import csv, io, json, math, re, stat, zipfile, shutil, hashlib, uuid
from collections import Counter
import cv2, numpy as np, pandas as pd, matplotlib.pyplot as plt
from PIL import Image
import torch, ultralytics, yaml
from ultralytics import YOLO
from google.colab import files
from IPython.display import display
DEVICE = 0 if torch.cuda.is_available() else 'cpu'
print('Ultralytics:', ultralytics.__version__, 'device:', DEVICE)
print('Contoh struktur, bukan dataset: images/scene01_001.jpg ↔ labels/scene01_001.txt')
print('Format label: class_id x_center y_center width height (normalized).')
print('classes.json: list nama kelas berurutan, ID mulai 0.')
print('manifest.csv header: filename,scene_id,license,reviewed,negative')
uploaded_zip = files.upload()
if len(uploaded_zip) != 1:
    raise ValueError('Upload tepat satu ZIP dataset.')
zip_name, zip_bytes = next(iter(uploaded_zip.items()))
if Path(zip_name).suffix.lower() != '.zip' or not 0 < len(zip_bytes) <= 100*1024**2:
    raise ValueError('ZIP wajib tidak kosong dan maksimum 100 MB.')

Sel 3 — Ekstraksi aman dan validasi dataset

Tidak memakai extractall. Validasi membatasi jalur, symlink, file tak dikenal, jumlah berkas, ukuran terkompresi/aktual, label dan manifest. Validasi sintaks tidak membuktikan anotasi benar secara semantik atau izin sah: kelompok tetap harus mengaudit.

def validate_dataset_zip(payload, base=Path('/content')):
    if not 0 < len(payload) <= 100 * 1024**2:
        raise ValueError('Ukuran ZIP di luar batas.')
    root = Path(base) / ('dataset_raw_' + uuid.uuid4().hex[:10])
    root.mkdir(parents=True, exist_ok=False)
    max_total = 300 * 1024**2
    actual_total = 0
    try:
        with zipfile.ZipFile(io.BytesIO(payload)) as archive:
            members = archive.infolist()
            if len(members) > 1000: raise ValueError('Terlalu banyak entri ZIP.')
            seen, declared_total = set(), 0
            for entry in members:
                name = entry.filename
                p = PurePosixPath(name)
                if (not name or '\\' in name or '\x00' in name or p.is_absolute()
                    or ':' in name or any(part in {'..', '.'} for part in name.split('/'))):
                    raise ValueError(f'Jalur ZIP berbahaya: {name}')
                mode = entry.external_attr >> 16
                if stat.S_ISLNK(mode) or entry.flag_bits & 1:
                    raise ValueError('Symlink atau ZIP terenkripsi ditolak.')
                if name in seen: raise ValueError('Nama entri duplikat.')
                seen.add(name)
                if entry.is_dir():
                    if name not in {'images/', 'labels/'}:
                        raise ValueError('Direktori ZIP tidak sesuai kontrak.')
                    continue
                parts = p.parts
                allowed = name in {'classes.json','manifest.csv','LICENSE.md'}
                if len(parts) == 2 and parts[0] in {'images','labels'}:
                    if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,100}', parts[1]):
                        raise ValueError('Nama file wajib sederhana tanpa subdirektori.')
                    ext = p.suffix.lower()
                    allowed = ext in ({'.jpg','.jpeg','.png'} if parts[0]=='images' else {'.txt'})
                if not allowed: raise ValueError(f'File tidak diizinkan: {name}')
                cap = 20*1024**2 if parts[0]=='images' else 2*1024**2
                if entry.file_size > cap or entry.file_size < 0:
                    raise ValueError('File terlalu besar.')
                if entry.file_size > 1024**2 and entry.file_size/max(1,entry.compress_size) > 200:
                    raise ValueError('Rasio kompresi mencurigakan.')
                declared_total += entry.file_size
                if declared_total > max_total: raise ValueError('Ukuran ekspansi ZIP berlebih.')
                destination = root.joinpath(*parts).resolve()
                if root.resolve() not in destination.parents:
                    raise ValueError('Zip Slip terdeteksi.')
                destination.parent.mkdir(parents=True,exist_ok=True)
                actual_file = 0
                with archive.open(entry) as source, destination.open('xb') as target:
                    while True:
                        block = source.read(1024*1024)
                        if not block: break
                        actual_file += len(block); actual_total += len(block)
                        if actual_file > cap or actual_total > max_total:
                            raise ValueError('Ukuran aktual ekspansi melewati batas.')
                        target.write(block)
                if actual_file != entry.file_size: raise ValueError('Ukuran entri tidak konsisten.')
        required = [root/'classes.json',root/'manifest.csv',root/'LICENSE.md']
        if any(not p.is_file() for p in required): raise ValueError('Metadata wajib tidak lengkap.')
        names = json.loads((root/'classes.json').read_text(encoding='utf-8'))
        if (not isinstance(names,list) or not 1 <= len(names) <= 3
            or any(not isinstance(n,str) or not n.strip() or len(n)>60
                   or any(ord(ch)<32 for ch in n) for n in names)
            or len(set(names)) != len(names)):
            raise ValueError('Nama kelas harus list 1–3 nama unik nonkosong.')
        if not (root/'LICENSE.md').read_text(encoding='utf-8').strip():
            raise ValueError('LICENSE.md harus menjelaskan sumber dan izin.')
        with (root/'manifest.csv').open(encoding='utf-8',newline='') as handle:
            reader = csv.DictReader(handle)
            fields = {'filename','scene_id','license','reviewed','negative'}
            if reader.fieldnames is None or not fields <= set(reader.fieldnames):
                raise ValueError('Header manifest tidak lengkap.')
            records = list(reader)
        image_paths = sorted((root/'images').glob('*'))
        if not 100 <= len(image_paths) <= 200:
            raise ValueError('Tugas wajib 100–200 gambar, bukan smoke test.')
        manifest_names = [r['filename'] for r in records]
        if len(set(manifest_names)) != len(records) or set(manifest_names) != {p.name for p in image_paths}:
            raise ValueError('Manifest harus tepat satu baris per gambar.')
        stems = [p.stem for p in image_paths]
        if len(set(stems)) != len(stems): raise ValueError('Stem gambar harus unik.')
        if {p.name for p in (root/'labels').glob('*')} != {s+'.txt' for s in stems}:
            raise ValueError('Label wajib berpasangan tepat dengan gambar, tanpa label ekstra.')
        by_name = {r['filename']: r for r in records}
        pixel_hashes, inventory = set(), []
        class_totals = Counter()
        for image_path in image_paths:
            r = by_name[image_path.name]
            if (not r['scene_id'].strip() or not r['license'].strip()
                or r['reviewed'].strip().lower() != 'true'
                or r['negative'].strip().lower() not in {'true','false'}):
                raise ValueError('Scene/izin/review/negative belum valid.')
            with Image.open(image_path) as header:
                if header.width * header.height > 20_000_000:
                    raise ValueError('Gambar melebihi 20 megapiksel.')
                header.verify()
            image = cv2.imread(str(image_path))
            if image is None: raise ValueError(f'Decode gagal: {image_path.name}')
            digest = hashlib.sha256(str(image.shape).encode()+image.tobytes()).hexdigest()
            if digest in pixel_hashes: raise ValueError('Gambar duplikat identik ditolak.')
            pixel_hashes.add(digest)
            label = root/'labels'/(image_path.stem+'.txt')
            lines = [line for line in label.read_text(encoding='utf-8').splitlines() if line.strip()]
            is_negative = r['negative'].strip().lower() == 'true'
            if bool(lines) == is_negative:
                raise ValueError('Label kosong hanya untuk negatif yang sudah ditinjau; negatif tidak boleh memiliki bbox.')
            counts = Counter()
            for line in lines:
                tokens = line.split()
                if len(tokens)!=5 or not re.fullmatch(r'[0-9]+',tokens[0]):
                    raise ValueError(f'Token class/label salah: {label.name}')
                class_id = int(tokens[0])
                if not 0 <= class_id < len(names): raise ValueError('Class ID di luar names.')
                x,y,w,h = map(float,tokens[1:])
                if (not all(math.isfinite(v) for v in (x,y,w,h))
                    or not 0 <= x <= 1 or not 0 <= y <= 1
                    or not 0 < w <= 1 or not 0 < h <= 1
                    or x-w/2 < -1e-6 or y-h/2 < -1e-6
                    or x+w/2 > 1+1e-6 or y+h/2 > 1+1e-6):
                    raise ValueError(f'Koordinat normalized tidak valid: {label.name}')
                counts[class_id] += 1; class_totals[class_id] += 1
            inventory.append({'filename':image_path.name,'scene_id':r['scene_id'].strip(),
                              'license':r['license'],'negative':is_negative,
                              'boxes':sum(counts.values()),'class_counts':dict(counts)})
        if set(class_totals) != set(range(len(names))):
            raise ValueError('Setiap kelas wajib punya anotasi positif.')
        if len({r['scene_id'] for r in inventory}) < 6:
            raise ValueError('Butuh minimal 6 scene independen untuk train/val/test; disarankan 10 atau lebih.')
        return root, names, inventory
    except Exception:
        shutil.rmtree(root,ignore_errors=True)
        raise
RAW, CLASS_NAMES, inventory = validate_dataset_zip(zip_bytes)
display(pd.DataFrame([{k:v for k,v in r.items() if k!='class_counts'} for r in inventory]).head())
print('Gambar tervalidasi:',len(inventory),'Kelas:',CLASS_NAMES)
# Audit visual anotasi: kotak ini ground truth yang diunggah, bukan prediksi model.
for record in inventory[:3]:
    name = record['filename']
    image = cv2.imread(str(RAW/'images'/name))
    h,w = image.shape[:2]
    label_lines = (RAW/'labels'/(Path(name).stem+'.txt')).read_text().splitlines()
    for line in label_lines:
        if not line.strip(): continue
        tokens = line.split(); idx = int(tokens[0])
        x,y,bw,bh = map(float,tokens[1:])
        left,top = round((x-bw/2)*w), round((y-bh/2)*h)
        right,bottom = round((x+bw/2)*w), round((y+bh/2)*h)
        cv2.rectangle(image,(left,top),(right,bottom),(0,255,0),2)
        cv2.putText(image,CLASS_NAMES[idx],(left,max(12,top)),
                    cv2.FONT_HERSHEY_SIMPLEX,0.5,(0,255,0),1)
    plt.figure(figsize=(9,6)); plt.imshow(cv2.cvtColor(image,cv2.COLOR_BGR2RGB))
    plt.title(f"Audit label: {name}, negative={record['negative']}")
    plt.axis('off'); plt.show()

Sel 4 — Split berdasarkan scene dan buat data.yaml

Seluruh scene masuk satu split. Pencarian deterministik mencoba mendekati 70/15/15 berdasarkan gambar dan memastikan tiap kelas muncul di setiap split. Jika tidak ada kandidat, tambah scene/keterwakilan kelas; jangan pecah scene untuk memperbaiki metrik.

import random

def split_by_scene(inventory, class_names, seed=42):
    groups = sorted({r['scene_id'] for r in inventory})
    n = len(groups)
    n_val = max(1, round(0.15*n)); n_test = max(1, round(0.15*n))
    rng = random.Random(seed)
    best = None
    for _ in range(500):
        order = groups.copy(); rng.shuffle(order)
        group_sets = {'val': set(order[:n_val]),
                      'test': set(order[n_val:n_val+n_test]),
                      'train': set(order[n_val+n_test:])}
        slices = {s:[r for r in inventory if r['scene_id'] in gs]
                  for s,gs in group_sets.items()}
        if any(not part for part in slices.values()): continue
        covered = {s:set(k for r in part for k in r['class_counts'])
                   for s,part in slices.items()}
        if any(ids != set(range(len(class_names))) for ids in covered.values()): continue
        score = sum(abs(len(slices[s])/len(inventory)-target)
                    for s,target in [('train',0.7),('val',0.15),('test',0.15)])
        if best is None or score < best[0]: best = (score, slices, group_sets)
    if best is None:
        raise ValueError('Tidak ditemukan split scene dengan semua kelas; tambah data independen.')
    _, slices, group_sets = best
    assert not (group_sets['train'] & group_sets['val'] or group_sets['train'] & group_sets['test']
                or group_sets['val'] & group_sets['test'])
    assert sum(map(len,slices.values())) == len(inventory)
    return slices

slices = split_by_scene(inventory, CLASS_NAMES)
DATASET = Path('/content') / ('dataset_yolo_' + uuid.uuid4().hex[:10])
for split, records in slices.items():
    image_dir = DATASET/'images'/split; label_dir = DATASET/'labels'/split
    image_dir.mkdir(parents=True,exist_ok=True); label_dir.mkdir(parents=True,exist_ok=True)
    for record in records:
        name = record['filename']
        shutil.copy2(RAW/'images'/name, image_dir/name)
        shutil.copy2(RAW/'labels'/(Path(name).stem+'.txt'), label_dir/(Path(name).stem+'.txt'))
    if len(list(image_dir.glob('*'))) != len(records): raise RuntimeError('Jumlah split salah.')
config = {'path':str(DATASET.resolve()),'train':'images/train','val':'images/val',
          'test':'images/test','names':dict(enumerate(CLASS_NAMES))}
DATA_YAML = DATASET/'data.yaml'
DATA_YAML.write_text(yaml.safe_dump(config,sort_keys=False,allow_unicode=True),encoding='utf-8')
report_rows = []
for split, records in slices.items():
    totals = Counter()
    for r in records: totals.update(r['class_counts'])
    report_rows.append({'split':split,'images':len(records),
        'scenes':len({r['scene_id'] for r in records}),
        **{name:totals[i] for i,name in enumerate(CLASS_NAMES)}})
split_report = pd.DataFrame(report_rows)
display(split_report)
print(DATA_YAML.read_text())
split_report.to_csv(DATASET/'split_report.csv',index=False)
pd.DataFrame([{**{k:v for k,v in r.items() if k!='class_counts'},'split':s}
              for s,records in slices.items() for r in records]).to_csv(DATASET/'split_manifest.csv',index=False)

Sel 5 — Fine-tuning baseline

GPU disarankan; CPU sah tetapi bisa lambat. Run 30 epoch adalah awal eksperimen, bukan jaminan kualitas. Jangan menggunakan test untuk memilih threshold atau epoch.

run_id = 'custom_' + uuid.uuid4().hex[:8]
train_model = YOLO('yolov8n.pt')
train_results = train_model.train(data=str(DATA_YAML), epochs=30, imgsz=640,
    batch=4, device=DEVICE, workers=2, seed=42, deterministic=True,
    patience=10, project='/content/runs_custom', name=run_id, exist_ok=False,
    plots=True, cache=False)
RUN_DIR = Path(train_model.trainer.save_dir)
BEST = RUN_DIR/'weights'/'best.pt'
if not BEST.is_file(): raise RuntimeError('Training belum menghasilkan best.pt.')
print('Run:',RUN_DIR,'Bobot terbaik:',BEST)

Sel 6 — Evaluasi validation dan test terpisah

Pilih keputusan eksperimen berdasarkan validation. Jalankan test setelah model/threshold dikunci; jangan berulang kali memilih model dari angka test.

custom_model = YOLO(str(BEST))
val_metrics = custom_model.val(data=str(DATA_YAML), split='val', device=DEVICE,
                              imgsz=640, plots=True, project=str(RUN_DIR), name='val_final')
test_metrics = custom_model.val(data=str(DATA_YAML), split='test', device=DEVICE,
                               imgsz=640, plots=True, project=str(RUN_DIR), name='test_final')
metric_rows = []
for split, metrics in [('val',val_metrics),('test',test_metrics)]:
    metric_rows.append({'split':split,'precision_mean':float(metrics.box.mp),
                       'recall_mean':float(metrics.box.mr),
                       'mAP50':float(metrics.box.map50),'mAP50_95':float(metrics.box.map)})
display(pd.DataFrame(metric_rows))
pd.DataFrame(metric_rows).to_csv(RUN_DIR/'metrics_summary.csv',index=False)
history = pd.read_csv(RUN_DIR/'results.csv')
history.columns = history.columns.str.strip()
display(history.tail())
loss_cols = [c for c in history.columns if 'box_loss' in c]
if loss_cols:
    history.plot(x='epoch', y=loss_cols, figsize=(10,4), grid=True)
    plt.ylabel('Loss'); plt.show()

Sel 7 — Audit prediksi pada gambar test

Tampilkan beberapa gambar test menggunakan bobot terbaik dan kelas custom. Tidak menyatakan metrik tinggi sebelum hasil run tersedia.

for record in slices['test'][:5]:
    path = DATASET/'images'/'test'/record['filename']
    image = cv2.imread(str(path))
    if image is None: raise ValueError('Gambar test tidak dapat dibaca.')
    result = custom_model.predict(image, conf=0.25, device=DEVICE, verbose=False)[0]
    annotated = result.plot()
    plt.figure(figsize=(10,6)); plt.imshow(cv2.cvtColor(annotated,cv2.COLOR_BGR2RGB))
    plt.title(record['filename']); plt.axis('off'); plt.show()
print('Periksa bbox terhadap label yang diaudit; jangan menganggap prediksi sebagai ground truth.')

Jika terjadi error

Zip Slip, symlink, ukuran berlebih atau file asing ditolak.
Ekspor ulang hanya file sesuai kontrak, tanpa path absolut, ../ atau kode di dalam arsip; jangan melemahkan validator.
Class token atau normalized bbox salah.
Periksa ekspor YOLO, ID integer 0..n-1 dan normalisasi berdasarkan lebar/tinggi asli.
Label kosong untuk gambar positif.
Lengkapi anotasi seluruh objek; negative=true hanya setelah gambar benar-benar ditinjau sebagai negatif.
Tidak ada split scene yang memenuhi semua kelas.
Tambahkan scene berkelas langka; jangan mengacak frame agar leakage terjadi.
CUDA out of memory.
Turunkan batch ke 2 atau 1 dan restart runtime jika perlu; pertahankan pencatatan parameter.

Runtime Colab dan file upload bersifat sementara. Simpan output penting pada notebook/Drive sebelum sesi berakhir. Jika runtime direset, jalankan ulang sel dari awal. Gunakan media berizin dan hindari unggahan data sensitif tanpa izin.

4. Tugas 13

Tugas kelompok: membuat dataset custom berizin 100–200 gambar dengan 1–3 kelas, melakukan anotasi dan training YOLO serta evaluasi anti-leakage.

Ketentuan pengerjaan

  1. Tetapkan kelas, kebijakan anotasi, sumber/izin data dan peran anggota pada notebook.
  2. Dataset 100–200 gambar, 1–3 kelas, scene independen dan label yang direview; tidak menggunakan coco8 sebagai pemenuhan jumlah.
  3. Jalankan validasi ZIP/path/ukuran, class token, label normalized, pasangan gambar-label dan negatif sah.
  4. Tampilkan ringkasan train/val/test aktual per scene dan kelas serta isi data.yaml.
  5. Training baseline yolov8n.pt, catat versi/device/seed/epoch/batch, tampilkan history dan lokasi best.pt.
  6. Tampilkan metrik validation dan test, contoh prediksi test serta analisis overfit/kegagalan yang jujur.
  7. Kumpulkan hanya SATU link Google Colab per kelompok, akses Viewer; identitas semua anggota pada Markdown atas, output dan analisis terlihat. Tidak mengumpulkan ZIP, bobot, PDF atau screenshot terpisah.

Pertanyaan analisis

  1. Bagaimana scene_id ditetapkan untuk mencegah near-duplicate leakage?
  2. Apa bukti label kosong adalah negatif sah?
  3. Apa beda mAP50 dan mAP50–95 serta mengapa hasil per kelas perlu dibaca?
  4. Apa indikasi overfit dan perubahan apa yang dipilih hanya dari validation?
Identitas di bagian paling atas notebook: tambahkan sel Text/Markdown sebelum kode dan isi Nama serta NPM.
# Materi 13 — Pengolahan Citra
Nama: [Nama lengkap mahasiswa]
NPM: [NPM mahasiswa]
Kelas: [Kelas, jika diperlukan]

Pengumpulan

Kumpulkan satu link Google Colab saja. Nama dan NPM wajib berada pada sel Markdown paling atas. Sertakan kode, output yang tersimpan/terlihat serta analisis di notebook; tidak perlu mengumpulkan file .ipynb, ZIP, PDF atau screenshot terpisah.

  1. Jalankan sel dengan konfigurasi valid dan simpan notebook beserta output. Pastikan opsi menghilangkan output saat menyimpan tidak aktif untuk versi pengumpulan.
  2. Bagikan sebagai Viewer/Pelihat kepada dosen. Gunakan akses link jika diizinkan institusi; bila dibatasi, beri akses khusus akun dosen. Jangan memberi izin edit publik.
  3. Periksa akses dari akun penerima. Kirim URL notebook Colab, bukan folder Drive atau runtime sementara.
AspekBobotIndikator
Data, izin dan anotasi35%100–200 gambar, 1–3 kelas, izin serta review jelas.
Validasi dan split25%Validasi aman dan split scene dengan laporan jumlah aktual.
Training dan evaluasi25%Run nyata, history, validation/test dan best.pt ditunjukkan.
Analisis dan kerja kelompok15%Kegagalan, leakage, overfit dan pembagian kontribusi dijelaskan.