create log experiment
#1
by abrarwahid - opened
- .gitignore +5 -15
- README.md +63 -432
- config.py +20 -6
- outputs/tables/Laporan_Prediksi_SIFT_KMEANS.csv +5 -5
- outputs/tables/cluster_metrics_all.csv +6 -10
- outputs/tables/cluster_stability.csv +2 -2
- outputs/tables/efficiency_raw.csv +0 -0
- outputs/tables/efficiency_summary.csv +2 -2
- outputs/tables/final_summary.json +3 -3
- outputs/tables/manifest_summary.csv +0 -0
- run_pipeline.py +57 -184
- src/bovw.py +4 -3
- src/clustering.py +2 -1
- src/evaluation.py +3 -1
- src/feature_extraction.py +5 -1
- src/preprocessing.py +5 -4
- src/segmentation.py +2 -2
- src/utils.py +1 -1
- src/visualization.py +8 -5
.gitignore
CHANGED
|
@@ -16,25 +16,14 @@ test.py
|
|
| 16 |
|
| 17 |
.env
|
| 18 |
outputs/interim/
|
| 19 |
-
outputs-publik/interim/
|
| 20 |
-
|
| 21 |
outputs/logs/
|
| 22 |
-
outputs-publik/logs/
|
| 23 |
-
|
| 24 |
outputs/figures/
|
| 25 |
-
outputs-
|
| 26 |
-
|
| 27 |
outputs/visualized_segments/
|
| 28 |
-
outputs-publik/visualized_segments/
|
| 29 |
|
| 30 |
-
archives/
|
| 31 |
|
| 32 |
-
outputs-publik
|
| 33 |
|
| 34 |
-
|
| 35 |
-
outputs-denoise/
|
| 36 |
-
outputs-bima/
|
| 37 |
-
data/data-publik/
|
| 38 |
data/dataset_root/
|
| 39 |
data/01_raw/*
|
| 40 |
data/02_processed/*
|
|
@@ -52,7 +41,8 @@ pertahankan folder dengan file kosong .gitkeep
|
|
| 52 |
|
| 53 |
|
| 54 |
|
|
|
|
| 55 |
/results
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
|
|
|
| 16 |
|
| 17 |
.env
|
| 18 |
outputs/interim/
|
|
|
|
|
|
|
| 19 |
outputs/logs/
|
|
|
|
|
|
|
| 20 |
outputs/figures/
|
| 21 |
+
outputs-v1/interim/
|
| 22 |
+
outputs-v1/logs/
|
| 23 |
outputs/visualized_segments/
|
|
|
|
| 24 |
|
|
|
|
| 25 |
|
|
|
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
data/dataset_root/
|
| 28 |
data/01_raw/*
|
| 29 |
data/02_processed/*
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
|
| 44 |
+
<<<<<<< HEAD
|
| 45 |
/results
|
| 46 |
|
| 47 |
+
=======
|
| 48 |
+
>>>>>>> bdb5b47fc05627063c1ade15705b9dca12b5f889
|
README.md
CHANGED
|
@@ -13,420 +13,46 @@ tags:
|
|
| 13 |
- beef-grading
|
| 14 |
---
|
| 15 |
|
| 16 |
-
# Pipeline Beef Research BIMA: Deteksi Mutu Kesegaran Daging Sapi Multidomain
|
| 17 |
-
|
| 18 |
-
## Daftar Isi
|
| 19 |
-
|
| 20 |
-
1. [Latar Belakang](#1-latar-belakang)
|
| 21 |
-
2. [Tujuan Penelitian](#2-tujuan-penelitian)
|
| 22 |
-
3. [Dataset](#3-dataset)
|
| 23 |
-
4. [Metodologi](#4-metodologi)
|
| 24 |
-
5. [Arsitektur Pipeline](#5-arsitektur-pipeline)
|
| 25 |
-
6. [Tahapan Pipeline (Detail Teknis)](#6-tahapan-pipeline-detail-teknis)
|
| 26 |
-
7. [Hasil dan Evaluasi](#7-hasil-dan-evaluasi)
|
| 27 |
-
8. [Struktur Output](#8-struktur-output)
|
| 28 |
-
9. [Setup dan Instalasi](#9-setup-dan-instalasi)
|
| 29 |
-
10. [Cara Menjalankan Pipeline](#10-cara-menjalankan-pipeline)
|
| 30 |
-
11. [Struktur Proyek](#11-struktur-proyek)
|
| 31 |
-
|
| 32 |
-
---
|
| 33 |
-
|
| 34 |
-
## 1. Latar Belakang
|
| 35 |
-
|
| 36 |
-
Penilaian mutu dan kesegaran daging sapi secara konvensional masih mengandalkan inspeksi visual oleh tenaga ahli—sebuah proses yang tidak skalabel, subjektif, dan rentan terhadap inkonsistensi antar penilai. Di sisi lain, pendekatan *machine learning* supervised (SVM, KNN, CNN) membutuhkan dataset berlabel dalam jumlah besar yang mahal dan memakan waktu untuk dikumpulkan, terutama di konteks penelitian lapangan (RPH dan UMKM).
|
| 37 |
-
|
| 38 |
-
Proyek ini menawarkan pendekatan alternatif: pipeline **sepenuhnya unsupervised** berbasis computer vision yang mampu memisahkan citra daging sapi ke dalam dua klaster mutu (Segar / Tidak Segar) **tanpa memerlukan label training satu pun**. Sistem ini dirancang untuk berjalan lintas domain (multi-domain), yaitu dapat menangani dataset dari kondisi pengambilan gambar yang berbeda (latar belakang hitam dari RPH Day-1 dan karton biru dari UMKM Day-2) tanpa memerlukan penyesuaian aturan warna yang berbeda per domain.
|
| 39 |
-
|
| 40 |
-
---
|
| 41 |
-
|
| 42 |
-
## 2. Tujuan Penelitian
|
| 43 |
-
|
| 44 |
-
- Membangun pipeline deteksi mutu kesegaran daging sapi berbasis unsupervised learning yang tidak memerlukan proses labeling supervised.
|
| 45 |
-
- Membandingkan kinerja dua algoritma ekstraksi fitur lokal: **SIFT** (*Scale-Invariant Feature Transform*) dan **SURF** (*Speeded-Up Robust Features*).
|
| 46 |
-
- Mengembangkan representasi citra berbasis **Bags of Visual Words (BoVW)** yang diperkaya dengan momen warna HSV (fusi fitur tekstur + warna).
|
| 47 |
-
- Mengevaluasi kualitas pemisahan klaster menggunakan metrik validasi internal (tanpa label) dan eksternal (opsional, bila label tersedia).
|
| 48 |
-
- Menghasilkan model produksi yang dapat digunakan untuk klasifikasi otomatis gambar daging baru.
|
| 49 |
-
|
| 50 |
-
---
|
| 51 |
-
|
| 52 |
-
## 3. Dataset
|
| 53 |
-
|
| 54 |
-
Dataset terdiri dari citra daging sapi yang dikumpulkan dari dua domain berbeda:
|
| 55 |
-
|
| 56 |
-
| Domain | Kondisi Pengambilan | Latar Belakang | Label Temporal |
|
| 57 |
-
|--------|---------------------|----------------|----------------|
|
| 58 |
-
| **DAY-1** | RPH (Rumah Potong Hewan), hari pertama pasca penyembelihan | Kain hitam | Segar |
|
| 59 |
-
| **DAY-2** | UMKM/pasar, hari berikutnya | Karton biru muda | Mendekati tidak segar |
|
| 60 |
-
|
| 61 |
-
### Konvensi Penamaan File
|
| 62 |
-
|
| 63 |
-
```
|
| 64 |
-
[HARI]_[DOMAIN]_[KODE]_[NOMOR].JPG
|
| 65 |
-
Contoh: DAY-1_RPH_SIR_001.JPG
|
| 66 |
-
```
|
| 67 |
-
|
| 68 |
-
- `HARI`: `DAY-1` atau `DAY-2`
|
| 69 |
-
- `DOMAIN`: kode lokasi pengambilan gambar
|
| 70 |
-
- `KODE`: jenis potongan daging (misal `SIR` untuk sirloin)
|
| 71 |
-
- `NOMOR`: nomor urut citra
|
| 72 |
-
|
| 73 |
-
### Label Ground Truth (Opsional)
|
| 74 |
-
|
| 75 |
-
File `data/labels.csv` dengan kolom `filename,label` (nilai: `segar` / `tidak_segar`) bersifat **opsional** dan **tidak digunakan untuk training**. Label ini hanya dipakai untuk validasi eksternal pasca-hoc (menghitung ARI, NMI, dan Purity) guna mengukur seberapa baik klaster otomatis bersesuaian dengan penilaian pakar.
|
| 76 |
-
|
| 77 |
-
---
|
| 78 |
-
|
| 79 |
-
## 4. Metodologi
|
| 80 |
-
|
| 81 |
-
### Prinsip Dasar: Unsupervised Sepenuhnya
|
| 82 |
-
|
| 83 |
-
Pipeline ini **tidak menggunakan label apapun dalam proses pelatihan**. Seluruh pemisahan mutu dilakukan secara otonom berdasarkan struktur visual inheren dari citra daging. Pendekatan ini dipilih karena:
|
| 84 |
-
|
| 85 |
-
1. **Data berlabel sulit dikumpulkan** di lapangan (membutuhkan ahli peternakan).
|
| 86 |
-
2. **Validitas lintas domain** — fitur tekstur dan warna permukaan daging yang terurai seharusnya universal, tidak bergantung pada latar belakang foto.
|
| 87 |
-
3. **Skalabilitas** — sistem dapat langsung diterapkan pada dataset baru tanpa proses re-labeling.
|
| 88 |
-
|
| 89 |
-
### Alur Metodologi
|
| 90 |
-
|
| 91 |
-
```
|
| 92 |
-
Citra Mentah
|
| 93 |
-
│
|
| 94 |
-
▼
|
| 95 |
-
Preprocessing & Segmentasi ROI
|
| 96 |
-
(Resize → Denoising → HSV Thresholding → Morfologi → Bounding Box Crop)
|
| 97 |
-
│
|
| 98 |
-
▼
|
| 99 |
-
Ekstraksi Fitur Lokal
|
| 100 |
-
(SIFT / SURF, dibatasi pada area mask daging)
|
| 101 |
-
│
|
| 102 |
-
├── Descriptor lokal (128-dim SIFT / 64-dim SURF)
|
| 103 |
-
└── Momen HSV (Mean, Std, Skewness per channel H/S/V → 9-dim)
|
| 104 |
-
│
|
| 105 |
-
▼
|
| 106 |
-
Bags of Visual Words (BoVW)
|
| 107 |
-
(MiniBatchKMeans codebook → Histogram Hellinger → Fusi HSV)
|
| 108 |
-
│
|
| 109 |
-
▼
|
| 110 |
-
Reduksi Dimensi (PCA)
|
| 111 |
-
│
|
| 112 |
-
▼
|
| 113 |
-
Clustering Unsupervised
|
| 114 |
-
(K-Means / GMM, k=2)
|
| 115 |
-
│
|
| 116 |
-
▼
|
| 117 |
-
Evaluasi & Komparasi
|
| 118 |
-
(Metrik internal + eksternal opsional + visualisasi PCA/t-SNE)
|
| 119 |
-
```
|
| 120 |
-
|
| 121 |
-
### Keputusan Desain Kunci
|
| 122 |
-
|
| 123 |
-
#### Segmentasi ROI Agnostik Domain
|
| 124 |
-
|
| 125 |
-
Segmentasi menggunakan **Strict HSV Thresholding** pada rentang pigmen mioglobin (H: 0–30 dan 160–180, S > 30, V > 25), diikuti **Margin Annihilation** (menghapus 12% tepi citra untuk mengeliminasi sisa label/stiker), lalu morfologi ellips (close + open) untuk menutup porositas tanpa mendistorsi batas. Pendekatan ini bekerja untuk kedua domain (hitam dan biru) tanpa aturan warna yang berbeda per domain.
|
| 126 |
-
|
| 127 |
-
#### Root-BoVW (Transformasi Hellinger)
|
| 128 |
-
|
| 129 |
-
Histogram BoVW dinormalisasi menggunakan **transformasi Hellinger** (√(histogram/total)) bukan L2 biasa. Ini menghasilkan vektor dengan norma L2 = 1 dan mengurangi sensitivitas terhadap frekuensi kata visual yang sangat dominan—ekuivalen dengan menggunakan kernel Hellinger pada SVM, namun bekerja langsung di ruang Euclidean sehingga K-Means dan GMM dapat diterapkan secara efektif.
|
| 130 |
-
|
| 131 |
-
#### Fusi Warna HSV yang Terkalibrasi
|
| 132 |
-
|
| 133 |
-
Momen warna HSV (mean, std, skewness untuk channel H, S, dan V = 9 dimensi) difusikan ke histogram BoVW setelah dua tahap normalisasi:
|
| 134 |
-
1. **MinMaxScaler** (fit di seluruh dataset, bukan per citra)
|
| 135 |
-
2. **L2-normalize per baris** agar norma-L2 = 1, setara dengan blok tekstur Hellinger
|
| 136 |
-
|
| 137 |
-
Dengan demikian, parameter `HSV_FUSION_WEIGHT` benar-benar merepresentasikan "bobot proporsi" yang terkendali: nilai 3.0 berarti kontribusi warna diberi bobot 3× lipat kontribusi tekstur.
|
| 138 |
-
|
| 139 |
-
#### Perbandingan Algoritma yang Adil (Ruang Fitur Seragam)
|
| 140 |
-
|
| 141 |
-
Semua algoritma clustering (K-Means dan GMM) dijalankan **di ruang fitur yang sama setelah PCA** (bukan K-Means di data mentah sementara GMM di PCA). Ini memastikan perbandingan murni pada level algoritma, bukan perbandingan ruang fitur yang tercampur.
|
| 142 |
-
|
| 143 |
-
#### Uji Stabilitas Klaster
|
| 144 |
-
|
| 145 |
-
Untuk menghindari *metric-chasing* (melaporkan Silhouette dari satu kali fit yang berpotensi terlalu optimis), pipeline menjalankan **10 kali subsample 80% data** dan melaporkan Silhouette mean ± std serta ARI vs klaster data penuh. ARI mendekati 1 berarti klaster stabil terhadap perturbasi subsampling.
|
| 146 |
-
|
| 147 |
-
---
|
| 148 |
-
|
| 149 |
-
## 5. Arsitektur Pipeline
|
| 150 |
-
|
| 151 |
-
Pipeline terdiri dari 8 tahap yang dapat dijalankan secara terpisah (dengan cache pickle antar tahap) atau sekaligus:
|
| 152 |
-
|
| 153 |
-
```
|
| 154 |
-
┌─────────────────────────────────────────────────────────────┐
|
| 155 |
-
│ run_pipeline.py │
|
| 156 |
-
│ │
|
| 157 |
-
│ step_preprocess → step_extract → step_bovw │
|
| 158 |
-
│ │ │ │ │
|
| 159 |
-
│ manifest.pkl descriptors_*.pkl histograms_*.pkl │
|
| 160 |
-
│ masks/*.npz timing_*.pkl codebooks_*.pkl │
|
| 161 |
-
│ processed/*.npz hsv_*.pkl │
|
| 162 |
-
│ │
|
| 163 |
-
│ step_cluster → step_evaluate → step_export │
|
| 164 |
-
│ │ │ │ │
|
| 165 |
-
│ cluster_labels.pkl figures/ Laporan_Prediksi.csv │
|
| 166 |
-
│ cluster_metrics.csv tables/ │
|
| 167 |
-
│ │
|
| 168 |
-
│ step_tune → step_build_production_model │
|
| 169 |
-
│ │ │ │
|
| 170 |
-
│ tuning_results.csv meat_grading_model.joblib │
|
| 171 |
-
│ │
|
| 172 |
-
│ step_batch_experiment │
|
| 173 |
-
│ │ │
|
| 174 |
-
│ archives/ (14 skenario + Master_Log.csv) │
|
| 175 |
-
└─────────────────────────────────────────────────────────────┘
|
| 176 |
-
```
|
| 177 |
-
|
| 178 |
-
---
|
| 179 |
-
|
| 180 |
-
## 6. Tahapan Pipeline (Detail Teknis)
|
| 181 |
-
|
| 182 |
-
### Tahap 1 — Preprocessing & Segmentasi ROI (`src/preprocessing.py`, `src/segmentation.py`)
|
| 183 |
-
|
| 184 |
-
**Alur per citra:**
|
| 185 |
-
1. **Resize** mempertahankan aspek rasio, sisi terpanjang menjadi 800px (`RESIZE_MAX_SIDE`).
|
| 186 |
-
2. **Denoising** menggunakan `fastNlMeansDenoisingColored` (opsional, diaktifkan jika `DENOISE_H` bukan `None`).
|
| 187 |
-
3. **Segmentasi ROI** — Strict HSV Thresholding pada rentang mioglobin + Margin Annihilation 12% + morfologi ellips 11×11 + pemilihan kontur terbaik berdasarkan skor `area / (dist_dari_pusat + 1)`.
|
| 188 |
-
4. **QA Visual** — menyimpan gambar berdampingan (asli + kontur merah | daging tersegmentasi di atas latar putih) ke `outputs/visualized_segments/`.
|
| 189 |
-
5. **Bounding Box Crop** — memotong citra ke bounding box foreground dengan padding 1% agar detektor tepi SURF tidak terpotong.
|
| 190 |
-
6. **Konversi** ke grayscale (untuk SIFT/SURF) dan HSV (untuk momen warna).
|
| 191 |
-
|
| 192 |
-
**Output:** `outputs/interim/processed/*.npz` (gray, hsv, mask per citra), `manifest.pkl`.
|
| 193 |
-
|
| 194 |
-
### Tahap 2 — Ekstraksi Fitur Lokal (`src/feature_extraction.py`)
|
| 195 |
-
|
| 196 |
-
- **SIFT**: `cv2.xfeatures2d.SIFT_create`, descriptor 128-dimensi.
|
| 197 |
-
- **SURF**: `cv2.xfeatures2d.SURF_create`, Hessian threshold = 500 (hasil tuning), descriptor 64-dimensi.
|
| 198 |
-
- Keypoint dibatasi **hanya pada area mask** menggunakan parameter `mask` di `detectAndCompute`.
|
| 199 |
-
- Descriptor di-cap maksimum 800 per citra (subsample acak) untuk menjaga efisiensi codebook.
|
| 200 |
-
- **Momen HSV**: Mean, Std, dan Skewness untuk channel H, S, dan V → vektor 9-dimensi per citra.
|
| 201 |
-
- Waktu ekstraksi per citra direkam untuk perbandingan efisiensi komputasi.
|
| 202 |
-
|
| 203 |
-
**Output:** `descriptors_sift.pkl`, `descriptors_surf.pkl`, `hsv_sift.pkl`, `hsv_surf.pkl`, `timing_*.pkl`.
|
| 204 |
-
|
| 205 |
-
### Tahap 3 — Bags of Visual Words (`src/bovw.py`)
|
| 206 |
-
|
| 207 |
-
- **Sampling descriptor**: Maksimum 200.000 descriptor dari seluruh dataset digabung untuk melatih codebook (menghindari memory error untuk dataset besar).
|
| 208 |
-
- **Pembangunan codebook**: `MiniBatchKMeans` dengan batch size 2000, `n_init=10`, `max_iter=200`, untuk ukuran k ∈ {50, 100, 200}.
|
| 209 |
-
- **Komputasi histogram**: Setiap citra dikuantisasi ke dalam histogram k-bin menggunakan `codebook.predict(descriptors)`, lalu dinormalisasi dengan transformasi Hellinger.
|
| 210 |
-
- **Fusi warna HSV**: Jika `USE_COLOR_FUSION=True`, vektor HSV dinormalisasi (MinMax → L2-norm) lalu dikali `HSV_FUSION_WEIGHT` dan di-concatenate ke histogram BoVW.
|
| 211 |
-
|
| 212 |
-
**Output:** `codebooks_*.pkl` (dict k → MiniBatchKMeans), `histograms_*.pkl` (dict k → matrix N×(k+9)).
|
| 213 |
-
|
| 214 |
-
### Tahap 4 — Clustering Unsupervised (`src/clustering.py`)
|
| 215 |
-
|
| 216 |
-
**4a. Sensitivitas Ukuran Codebook** (ruang fitur mentah, K-Means saja):
|
| 217 |
-
- Menguji pengaruh granularitas codebook (k=50, 100, 200) terhadap kualitas klaster.
|
| 218 |
-
- Ditandai `feature_space="raw"` agar tidak tercampur dengan analisis perbandingan algoritma.
|
| 219 |
-
|
| 220 |
-
**4b. Perbandingan Algoritma Clustering** (ruang fitur PCA, codebook k=200):
|
| 221 |
-
- PCA diaplikasikan ke data default codebook, mempertahankan hingga 30 komponen utama.
|
| 222 |
-
- K-Means (`n_init=20`) dan GMM (`covariance_type="full"`, `n_init=10`) dibandingkan di ruang PCA yang sama.
|
| 223 |
-
- Label hasil clustering disimpan untuk visualisasi dan ekspor.
|
| 224 |
-
|
| 225 |
-
**4c. Uji Stabilitas Klaster**:
|
| 226 |
-
- 10 iterasi subsample 80%, masing-masing dijalankan K-Means independen.
|
| 227 |
-
- Melaporkan Silhouette mean ± std dan ARI vs klaster data penuh.
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
### Tahap 5 — Evaluasi & Visualisasi (`src/evaluation.py`, `src/visualization.py`)
|
| 232 |
-
|
| 233 |
-
**Metrik Validasi Internal** (tanpa label):
|
| 234 |
-
| Metrik | Interpretasi | Arah Optimal |
|
| 235 |
-
|--------|-------------|--------------|
|
| 236 |
-
| **Silhouette Score** | Kerapatan intra-klaster vs. separasi antar-klaster, rentang [-1, 1] | ↑ Lebih tinggi lebih baik |
|
| 237 |
-
| **Davies-Bouldin Index** | Rata-rata similaritas tiap klaster dengan klaster paling mirip | ↓ Lebih rendah lebih baik |
|
| 238 |
-
| **Calinski-Harabasz Index** | Rasio dispersi antar-klaster / intra-klaster | ↑ Lebih tinggi lebih baik |
|
| 239 |
-
|
| 240 |
-
**Metrik Validasi Eksternal** (bila `labels.csv` tersedia):
|
| 241 |
-
| Metrik | Interpretasi |
|
| 242 |
-
|--------|-------------|
|
| 243 |
-
| **ARI** (Adjusted Rand Index) | Kesesuaian klaster dengan ground truth, dikoreksi untuk chance, rentang [-1, 1] |
|
| 244 |
-
| **NMI** (Normalized Mutual Info) | Informasi mutual antara klaster dan ground truth, rentang [0, 1] |
|
| 245 |
-
| **Purity** | Proporsi anggota klaster yang berasal dari kelas mayoritas |
|
| 246 |
-
|
| 247 |
-
**Visualisasi yang dihasilkan:**
|
| 248 |
-
- Proyeksi PCA 2D dan t-SNE 2D dari klaster terbaik.
|
| 249 |
-
- Bar chart Silhouette, Davies-Bouldin, dan Calinski-Harabasz per kombinasi (fitur × algoritma).
|
| 250 |
-
- Ablation Study: dampak PCA terhadap Silhouette Score (raw vs. PCA space).
|
| 251 |
-
- Boxplot efisiensi komputasi (waktu ekstraksi dan jumlah keypoint).
|
| 252 |
-
- Radar Chart multidimensional SIFT vs. SURF (performa + efisiensi).
|
| 253 |
-
- Visual proof keypoint SIFT vs. SURF pada citra sampel.
|
| 254 |
-
- Stacked bar chart distribusi klaster per hari (temporal degradation).
|
| 255 |
-
|
| 256 |
-
### Tahap 6 — Hyperparameter Tuning (`step_tune`)
|
| 257 |
-
|
| 258 |
-
Grid search terhadap:
|
| 259 |
-
- **HSV Fusion Weight**: [2.5, 2.8, 3.0, 3.2, 3.5]
|
| 260 |
-
- **PCA Components**: [15, 20, 25, 30, 35]
|
| 261 |
-
- **GMM Covariance Type**: ['full', 'tied', 'diag']
|
| 262 |
-
|
| 263 |
-
Total 75 kombinasi diuji, diurutkan berdasarkan Silhouette Score tertinggi lalu Davies-Bouldin terendah. Konfigurasi terbaik kemudian diuji stabilitasnya (10× subsample) dan hasilnya **dilaporkan sebagai mean ± std**, bukan nilai mentah dari pencarian, untuk menghindari bias optimistis.
|
| 264 |
-
|
| 265 |
-
> **Peringatan metodologis**: Silhouette Score hasil grid search tidak boleh dilaporkan langsung sebagai bukti separabilitas karena merupakan target optimisasi itu sendiri. Selalu gunakan angka stabilitas (mean ± std dari subsample berulang).
|
| 266 |
-
|
| 267 |
-
### Tahap 7 — Ekspor Prediksi (`step_export`)
|
| 268 |
-
|
| 269 |
-
Menghasilkan tabel prediksi per citra dari model terbaik (SIFT + K-Means) dalam format CSV yang siap dibuka di Excel, mencakup: nama file, domain asal, hari ke-, jenis potongan, ID klaster, dan asumsi kondisi (Klaster A / Klaster B).
|
| 270 |
-
|
| 271 |
-
### Tahap 8 — Pembuatan Model Produksi (`step_build_production_model`)
|
| 272 |
-
|
| 273 |
-
Merakit dan menyimpan seluruh komponen pipeline ke dalam satu file `.joblib`:
|
| 274 |
-
|
| 275 |
-
```python
|
| 276 |
-
model_package = {
|
| 277 |
-
"codebook": MiniBatchKMeans, # Kamus visual BoVW
|
| 278 |
-
"scaler": MinMaxScaler, # Normalisasi HSV
|
| 279 |
-
"pca": PCA, # Reduksi dimensi
|
| 280 |
-
"gmm": GaussianMixture, # Classifier akhir
|
| 281 |
-
"hsv_weight": float, # Bobot fusi warna
|
| 282 |
-
"metadata": dict # Konfigurasi eksperimen
|
| 283 |
-
}
|
| 284 |
-
```
|
| 285 |
-
|
| 286 |
-
Model ini dapat dimuat dan digunakan untuk klasifikasi citra daging baru tanpa menjalankan ulang seluruh pipeline.
|
| 287 |
-
|
| 288 |
-
---
|
| 289 |
-
|
| 290 |
-
## 7. Hasil dan Evaluasi
|
| 291 |
-
|
| 292 |
-
### Komparasi SIFT vs. SURF
|
| 293 |
-
|
| 294 |
-
| Aspek | SIFT | SURF |
|
| 295 |
-
|-------|------|------|
|
| 296 |
-
| **Dimensi Descriptor** | 128-dim | 64-dim |
|
| 297 |
-
| **Kecepatan Ekstraksi** | Lebih lambat | ~2–3× lebih cepat |
|
| 298 |
-
| **Jumlah Keypoint** | Lebih banyak (threshold berbasis DoG) | Bergantung Hessian threshold (500 optimal) |
|
| 299 |
-
| **Separabilitas Klaster** | Silhouette umumnya lebih tinggi di ruang PCA | Silhouette kompetitif setelah fusi HSV |
|
| 300 |
-
| **Keunggulan** | Akurasi separasi tekstur | Efisiensi komputasi |
|
| 301 |
-
|
| 302 |
-
### Dampak Komponen Pipeline (Ablation Study)
|
| 303 |
-
|
| 304 |
-
| Komponen | Dampak pada Silhouette |
|
| 305 |
-
|----------|----------------------|
|
| 306 |
-
| Raw BoVW tanpa PCA | Baseline (rendah akibat *curse of dimensionality*) |
|
| 307 |
-
| + PCA (30 komponen) | Peningkatan signifikan |
|
| 308 |
-
| + Fusi HSV (weight=3.0) | Peningkatan tambahan — warna permukaan merupakan indikator kesegaran yang kuat |
|
| 309 |
-
| + Normalisasi L2 HSV | Memastikan bobot HSV terkendali dan dapat diinterpretasi |
|
| 310 |
-
|
| 311 |
-
### Uji Stabilitas (Laporan yang Jujur)
|
| 312 |
-
|
| 313 |
-
Kualitas klaster dilaporkan sebagai:
|
| 314 |
-
- **Silhouette mean ± std** dari 10× subsample 80%
|
| 315 |
-
- **ARI vs klaster data penuh** — nilai mendekati 1 menunjukkan struktur klaster yang stabil, bukan artefak dari satu kali fitting
|
| 316 |
-
|
| 317 |
-
### Sensitivitas Ukuran Codebook
|
| 318 |
-
|
| 319 |
-
Ukuran codebook k ∈ {50, 100, 200} diuji. Secara umum, k=200 memberikan representasi yang lebih granular dan Silhouette Score yang lebih baik. Perbedaan antara k=100 dan k=200 biasanya tidak dramatis, menunjukkan saturasi representasi.
|
| 320 |
-
|
| 321 |
-
---
|
| 322 |
-
|
| 323 |
-
## 8. Struktur Output
|
| 324 |
-
|
| 325 |
-
```
|
| 326 |
-
outputs/
|
| 327 |
-
├── interim/ # Cache pickle antar tahap (dapat dihapus & diulang)
|
| 328 |
-
│ ├── manifest.pkl # Metadata seluruh citra (hari/domain/kode/label)
|
| 329 |
-
│ ├── processed/ # *.npz per citra (gray, hsv, mask)
|
| 330 |
-
│ ├── masks/ # Mask ROI terpisah (*.npz)
|
| 331 |
-
│ ├── descriptors_sift.pkl
|
| 332 |
-
│ ├── descriptors_surf.pkl
|
| 333 |
-
│ ├── hsv_sift.pkl
|
| 334 |
-
│ ├── hsv_surf.pkl
|
| 335 |
-
│ ├── codebooks_sift.pkl
|
| 336 |
-
│ ├── codebooks_surf.pkl
|
| 337 |
-
│ ├── histograms_sift.pkl
|
| 338 |
-
│ ├── histograms_surf.pkl
|
| 339 |
-
│ ├── timing_sift.pkl
|
| 340 |
-
│ ├── timing_surf.pkl
|
| 341 |
-
│ └── cluster_labels.pkl
|
| 342 |
-
├── figures/ # Seluruh grafik (PCA, t-SNE, radar, boxplot, dsb.)
|
| 343 |
-
├── tables/ # Seluruh tabel hasil (CSV)
|
| 344 |
-
│ ├── manifest_summary.csv
|
| 345 |
-
│ ├── efficiency_raw.csv
|
| 346 |
-
│ ├── efficiency_summary.csv
|
| 347 |
-
│ ├── cluster_metrics_all.csv
|
| 348 |
-
│ ├── cluster_stability.csv
|
| 349 |
-
│ ├── tuning_results_deep_search.csv
|
| 350 |
-
│ ├── Laporan_Prediksi_SIFT_KMEANS.csv
|
| 351 |
-
│ └── final_summary.json
|
| 352 |
-
├── visualized_segments/ # Hasil QA segmentasi (asli | tersegmentasi)
|
| 353 |
-
├── meat_grading_model.joblib # Model produksi siap pakai
|
| 354 |
-
└── logs/
|
| 355 |
-
└── pipeline.log
|
| 356 |
-
```
|
| 357 |
-
|
| 358 |
-
```
|
| 359 |
-
archives/
|
| 360 |
-
└── DATA-BIMA_EXP-YYYYMMDD-XX/ # Satu folder per skenario eksperimen batch
|
| 361 |
-
├── code_snapshot/ # Snapshot kode saat eksperimen berjalan
|
| 362 |
-
└── results/
|
| 363 |
-
└── evaluation_metrics.json
|
| 364 |
-
Master_Log_DATA-BIMA_YYYYMMDD_HHMMSS.csv # Rekapitulasi 14 skenario
|
| 365 |
-
```
|
| 366 |
-
|
| 367 |
-
---
|
| 368 |
-
|
| 369 |
-
## 9. Setup dan Instalasi
|
| 370 |
-
|
| 371 |
-
### Prasyarat
|
| 372 |
|
| 373 |
-
|
| 374 |
-
|
|
|
|
| 375 |
|
| 376 |
-
##
|
| 377 |
|
| 378 |
```bash
|
| 379 |
-
# Install
|
| 380 |
uv python install 3.7
|
| 381 |
|
| 382 |
# Buat & sync environment sesuai pyproject.toml
|
| 383 |
uv sync
|
| 384 |
|
| 385 |
-
#
|
| 386 |
uv run python -c "import cv2; s=cv2.xfeatures2d.SIFT_create(); f=cv2.xfeatures2d.SURF_create(); print(True, True)"
|
| 387 |
```
|
| 388 |
|
| 389 |
-
>
|
| 390 |
-
>
|
| 391 |
-
>
|
| 392 |
-
> uv sync
|
| 393 |
-
> ```
|
| 394 |
-
|
| 395 |
-
### Dependensi Utama
|
| 396 |
-
|
| 397 |
-
| Paket | Versi | Fungsi |
|
| 398 |
-
|-------|-------|--------|
|
| 399 |
-
| `opencv-contrib-python` | 3.4.2.16 | SIFT, SURF (`xfeatures2d`) |
|
| 400 |
-
| `scikit-learn` | ≥1.0.2 | KMeans, GMM, PCA, metrik |
|
| 401 |
-
| `numpy` | ≥1.21.6 | Operasi matriks |
|
| 402 |
-
| `pandas` | ≥1.1.5 | Tabel hasil |
|
| 403 |
-
| `matplotlib` | ≥3.5.3 | Visualisasi |
|
| 404 |
-
| `seaborn` | ≥0.12.2 | Visualisasi statistik |
|
| 405 |
-
|
| 406 |
-
---
|
| 407 |
-
|
| 408 |
-
## 10. Cara Menjalankan Pipeline
|
| 409 |
-
|
| 410 |
-
### Persiapan Data
|
| 411 |
|
| 412 |
-
|
| 413 |
-
2. Letakkan di `data/dataset_root/` atau ubah `RAW_DATA_DIR` di `config.py`.
|
| 414 |
-
3. (Opsional) Siapkan `data/labels.csv` dengan kolom `filename,label` untuk validasi eksternal.
|
| 415 |
|
| 416 |
-
|
|
|
|
|
|
|
| 417 |
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
| 419 |
|
| 420 |
-
|
| 421 |
-
DATASET_DOMAIN = "DATA-BIMA" # Nama domain untuk penamaan arsip
|
| 422 |
-
RAW_DATA_DIR = "data/dataset_root"
|
| 423 |
-
USE_COLOR_FUSION = True # Aktifkan fusi momen HSV
|
| 424 |
-
HSV_FUSION_WEIGHT = 3.0 # Bobot kontribusi warna vs. tekstur
|
| 425 |
-
DEFAULT_CODEBOOK_SIZE = 200 # Ukuran codebook utama
|
| 426 |
-
SURF_HESSIAN_THRESHOLD = 500 # Threshold detektor SURF
|
| 427 |
-
```
|
| 428 |
|
| 429 |
-
|
|
|
|
| 430 |
|
| 431 |
```bash
|
| 432 |
uv run python run_pipeline.py --step preprocess
|
|
@@ -434,49 +60,54 @@ uv run python run_pipeline.py --step extract
|
|
| 434 |
uv run python run_pipeline.py --step bovw
|
| 435 |
uv run python run_pipeline.py --step cluster
|
| 436 |
uv run python run_pipeline.py --step evaluate
|
| 437 |
-
uv run python run_pipeline.py --step export
|
| 438 |
-
uv run python run_pipeline.py --step build_model
|
| 439 |
-
```
|
| 440 |
-
|
| 441 |
-
### Menjalankan Semua Tahap Sekaligus
|
| 442 |
|
| 443 |
-
|
| 444 |
uv run python run_pipeline.py --step all
|
| 445 |
```
|
| 446 |
|
| 447 |
-
|
| 448 |
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
|
| 451 |
-
|
| 452 |
-
# Grid search hyperparameter (lama, ~75 kombinasi × step_bovw)
|
| 453 |
-
uv run python run_pipeline.py --step tune
|
| 454 |
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
-
|
|
|
|
|
|
|
| 460 |
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
| 462 |
|
| 463 |
-
``
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
│ ├── preprocessing.py # Resize, denoising, konversi, crop
|
| 473 |
-
│ ├── segmentation.py # HSV thresholding & morfologi ROI
|
| 474 |
-
│ ├── feature_extraction.py # SIFT/SURF + momen HSV
|
| 475 |
-
│ ├── bovw.py # Codebook & histogram Hellinger
|
| 476 |
-
│ ├── clustering.py # K-Means, GMM, metrik validasi
|
| 477 |
-
│ ├── evaluation.py # Tabel efisiensi & komparasi fitur
|
| 478 |
-
│ ├── visualization.py # Semua fungsi plot
|
| 479 |
-
│ └── utils.py # Logger, I/O pickle/JSON, utils umum
|
| 480 |
-
├── outputs/ # Semua hasil pipeline (di-generate otomatis)
|
| 481 |
-
└── archives/ # Arsip eksperimen batch
|
| 482 |
-
```
|
|
|
|
| 13 |
- beef-grading
|
| 14 |
---
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
# Pipeline Beef Research BIMA: Deteksi Mutu Kesegaran Daging Sapi Multidomain
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
Pipeline **unsupervised** (SIFT/SURF -> Bags of Visual Words -> Clustering) untuk
|
| 20 |
+
dataset citra daging sapi multidomain (RPH & UMKM), seluruh proses klasifikasi mutu dilakukan tanpa pelatihan supervised
|
| 21 |
+
(tanpa SVM/KNN), murni berbasis unsupervised learning.
|
| 22 |
|
| 23 |
+
## 1. Setup Environment (uv, Python 3.7, OpenCV 3.4.2.16)
|
| 24 |
|
| 25 |
```bash
|
| 26 |
+
# Install python 3.7 lewat uv (jika belum ada di mesin)
|
| 27 |
uv python install 3.7
|
| 28 |
|
| 29 |
# Buat & sync environment sesuai pyproject.toml
|
| 30 |
uv sync
|
| 31 |
|
| 32 |
+
# (opsional) verifikasi SIFT & SURF tersedia (harus tercetak True True)
|
| 33 |
uv run python -c "import cv2; s=cv2.xfeatures2d.SIFT_create(); f=cv2.xfeatures2d.SURF_create(); print(True, True)"
|
| 34 |
```
|
| 35 |
|
| 36 |
+
> Catatan: jika `uv python install 3.7` gagal karena python-build-standalone tidak
|
| 37 |
+
> lagi menyediakan build 3.7 di platform Anda, gunakan pyenv/conda untuk
|
| 38 |
+
> menyediakan interpreter 3.7, lalu arahkan uv ke sana dengan
|
| 39 |
+
> `uv venv --python /path/to/python3.7` sebelum `uv sync`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
## 2. Siapkan Data
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
Susun dataset mentah sesuai struktur pada README dataset (folder `DAY-1/`, `DAY-2/`,
|
| 44 |
+
penamaan `[HARI]_[DOMAIN]_[KODE]_[NOMOR].JPG`). Atur path pada `config.py`
|
| 45 |
+
(`RAW_DATA_DIR`).
|
| 46 |
|
| 47 |
+
Jika Anda sudah punya sebagian label ground truth (dari pelabelan awal tim
|
| 48 |
+
peneliti / Peternakan), siapkan file CSV opsional `labels.csv` dengan kolom
|
| 49 |
+
`filename,label` (label: `segar` / `tidak_segar`) — dipakai HANYA untuk
|
| 50 |
+
validasi eksternal klaster (ARI/NMI/Purity), bukan untuk training.
|
| 51 |
|
| 52 |
+
## 3. Menjalankan Pipeline
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
+
Pipeline terbagi menjadi 5 tahap, masing-masing bisa dijalankan terpisah
|
| 55 |
+
(hasil antara di-cache di `outputs/interim/`) atau sekaligus dengan `all`.
|
| 56 |
|
| 57 |
```bash
|
| 58 |
uv run python run_pipeline.py --step preprocess
|
|
|
|
| 60 |
uv run python run_pipeline.py --step bovw
|
| 61 |
uv run python run_pipeline.py --step cluster
|
| 62 |
uv run python run_pipeline.py --step evaluate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
# atau jalankan semua tahap sekaligus
|
| 65 |
uv run python run_pipeline.py --step all
|
| 66 |
```
|
| 67 |
|
| 68 |
+
## 4. Struktur Output
|
| 69 |
|
| 70 |
+
```
|
| 71 |
+
outputs/
|
| 72 |
+
├── interim/ # cache pickle antar tahap (boleh dihapus & rerun)
|
| 73 |
+
│ ├── manifest.pkl # metadata semua citra (hari/domain/kode/label)
|
| 74 |
+
│ ├── masks/ # mask ROI hasil segmentasi (npz per citra)
|
| 75 |
+
│ ├── descriptors_sift.pkl
|
| 76 |
+
│ ├── descriptors_surf.pkl
|
| 77 |
+
│ ├── codebook_sift.pkl
|
| 78 |
+
│ ├── codebook_surf.pkl
|
| 79 |
+
│ ├── histograms_sift.npy
|
| 80 |
+
│ └── histograms_surf.npy
|
| 81 |
+
├── figures/ # semua grafik (PCA/TSNE, perbandingan metrik, dsb.)
|
| 82 |
+
├── tables/ # semua tabel hasil (CSV) siap ditempel ke laporan
|
| 83 |
+
└── logs/
|
| 84 |
+
└── pipeline.log
|
| 85 |
+
```
|
| 86 |
|
| 87 |
+
## 5. Tahapan Pipeline (ringkas)
|
|
|
|
|
|
|
| 88 |
|
| 89 |
+
1. **Preprocessing & Segmentasi ROI** (`src/preprocessing.py`, `src/segmentation.py`)
|
| 90 |
+
Resize, denoising, konversi HSV, dan **segmentasi latar belakang berbasis
|
| 91 |
+
flood-fill dari tepi citra** (bekerja untuk backdrop hitam Day-1 maupun
|
| 92 |
+
karton biru muda Day-2 tanpa perlu aturan warna berbeda per hari) sehingga
|
| 93 |
+
fitur yang diekstraksi terfokus pada permukaan daging, bukan latar
|
| 94 |
+
belakang yang kebetulan berkorelasi dengan hari/tahap kesegaran.
|
| 95 |
|
| 96 |
+
2. **Ekstraksi Fitur Lokal** (`src/feature_extraction.py`)
|
| 97 |
+
SIFT dan SURF dijalankan terpisah, keypoint dibatasi hanya pada area mask
|
| 98 |
+
ROI daging (parameter `mask` di `detectAndCompute`).
|
| 99 |
|
| 100 |
+
3. **Bags of Visual Words** (`src/bovw.py`)
|
| 101 |
+
Codebook dibangun dengan `MiniBatchKMeans` (beberapa ukuran cluster diuji:
|
| 102 |
+
50/100/150/200 sesuai indikator capaian proposal), setiap citra direpresentasikan
|
| 103 |
+
sebagai histogram fitur ternormalisasi.
|
| 104 |
|
| 105 |
+
4. **Clustering Unsupervised** (`src/clustering.py`)
|
| 106 |
+
K-Means (k=2) dan Gaussian Mixture dibandingkan
|
| 107 |
+
untuk membagi citra ke 2 klaster akhir (Segar/Tidak Segar).
|
| 108 |
+
|
| 109 |
+
5. **Evaluasi & Komparasi SIFT vs SURF** (`src/evaluation.py`, `src/visualization.py`)
|
| 110 |
+
Validasi internal (Silhouette, Davies-Bouldin, Calinski-Harabasz), validasi
|
| 111 |
+
eksternal opsional (ARI/NMI/Purity bila ada `labels.csv`), efisiensi
|
| 112 |
+
komputasi (waktu ekstraksi & jumlah keypoint rata-rata), serta visualisasi
|
| 113 |
+
PCA/t-SNE dari histogram BoVW.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
| 1 |
"""
|
| 2 |
-
Konfigurasi pipeline
|
|
|
|
| 3 |
"""
|
| 4 |
import os
|
| 5 |
|
|
|
|
| 6 |
# PATH DIREKTORI
|
|
|
|
| 7 |
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
RAW_DATA_DIR = os.path.join(PROJECT_ROOT, "data", "dataset_root") #Atur juga data sumber nya
|
| 12 |
LABELS_CSV = os.path.join(PROJECT_ROOT, "data", "labels.csv")
|
| 13 |
|
| 14 |
OUTPUT_DIR = os.path.join(PROJECT_ROOT, "outputs")
|
|
@@ -25,7 +26,9 @@ SEGMENTED_VIEW_DIR = os.path.join(OUTPUT_DIR, "visualized_segments")
|
|
| 25 |
IMAGE_EXTENSIONS = (".jpg", ".jpeg", ".JPG", ".JPEG")
|
| 26 |
FILENAME_REGEX = r"^(DAY-\d+)_([A-Za-z]+)_([A-Za-z]+)_(\d+)\.\w+$"
|
| 27 |
|
|
|
|
| 28 |
# PREPROCESSING
|
|
|
|
| 29 |
RESIZE_MAX_SIDE = 800
|
| 30 |
|
| 31 |
DENOISE_H = None
|
|
@@ -37,7 +40,9 @@ FLOODFILL_TOLERANCE = 12
|
|
| 37 |
FLOODFILL_MORPH_KERNEL = 7
|
| 38 |
MIN_FOREGROUND_AREA_RATIO = 0.05
|
| 39 |
|
|
|
|
| 40 |
# EKSTRAKSI FITUR & FUSI
|
|
|
|
| 41 |
SIFT_N_FEATURES = 0
|
| 42 |
SURF_HESSIAN_THRESHOLD = 500 # Nilai default optimal dari hasil tuning
|
| 43 |
MAX_DESCRIPTORS_PER_IMAGE = 800
|
|
@@ -46,20 +51,29 @@ MAX_DESCRIPTORS_PER_IMAGE = 800
|
|
| 46 |
USE_COLOR_FUSION = True
|
| 47 |
HSV_FUSION_WEIGHT = 3.0 # Nilai default optimal dari hasil tuning
|
| 48 |
|
|
|
|
| 49 |
# BAGS OF VISUAL WORDS
|
| 50 |
-
|
|
|
|
| 51 |
DEFAULT_CODEBOOK_SIZE = 200 # Nilai default optimal dari hasil tuning
|
| 52 |
MAX_DESCRIPTORS_FOR_CODEBOOK = 200_000
|
| 53 |
MINIBATCH_KMEANS_BATCH_SIZE = 2000
|
| 54 |
|
|
|
|
| 55 |
# CLUSTERING (K-Means / PCA)
|
|
|
|
| 56 |
N_CLUSTERS_FINAL = 2
|
| 57 |
RANDOM_STATE = 42
|
| 58 |
|
|
|
|
| 59 |
# LOGGING
|
|
|
|
| 60 |
LOG_LEVEL = "INFO"
|
| 61 |
|
| 62 |
-
#
|
|
|
|
|
|
|
|
|
|
| 63 |
TUNING_HESSIAN_THRESHOLDS = [400, 500, 600]
|
| 64 |
TUNING_HSV_WEIGHTS = [2.0, 2.5, 3.0]
|
| 65 |
TUNING_CODEBOOK_SIZES = [50, 100, 200]
|
|
|
|
| 1 |
"""
|
| 2 |
+
Konfigurasi terpusat pipeline computer vision deteksi mutu kesegaran daging sapi.
|
| 3 |
+
Ubah nilai di sini sesuai kebutuhan; jangan ubah logika di dalam modul src/.
|
| 4 |
"""
|
| 5 |
import os
|
| 6 |
|
| 7 |
+
# ---------------------------------------------------------------------------
|
| 8 |
# PATH DIREKTORI
|
| 9 |
+
# ---------------------------------------------------------------------------
|
| 10 |
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
| 11 |
|
| 12 |
+
RAW_DATA_DIR = os.path.join(PROJECT_ROOT, "data", "dataset_root")
|
|
|
|
|
|
|
| 13 |
LABELS_CSV = os.path.join(PROJECT_ROOT, "data", "labels.csv")
|
| 14 |
|
| 15 |
OUTPUT_DIR = os.path.join(PROJECT_ROOT, "outputs")
|
|
|
|
| 26 |
IMAGE_EXTENSIONS = (".jpg", ".jpeg", ".JPG", ".JPEG")
|
| 27 |
FILENAME_REGEX = r"^(DAY-\d+)_([A-Za-z]+)_([A-Za-z]+)_(\d+)\.\w+$"
|
| 28 |
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
# PREPROCESSING
|
| 31 |
+
# ---------------------------------------------------------------------------
|
| 32 |
RESIZE_MAX_SIDE = 800
|
| 33 |
|
| 34 |
DENOISE_H = None
|
|
|
|
| 40 |
FLOODFILL_MORPH_KERNEL = 7
|
| 41 |
MIN_FOREGROUND_AREA_RATIO = 0.05
|
| 42 |
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
# EKSTRAKSI FITUR & FUSI
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
SIFT_N_FEATURES = 0
|
| 47 |
SURF_HESSIAN_THRESHOLD = 500 # Nilai default optimal dari hasil tuning
|
| 48 |
MAX_DESCRIPTORS_PER_IMAGE = 800
|
|
|
|
| 51 |
USE_COLOR_FUSION = True
|
| 52 |
HSV_FUSION_WEIGHT = 3.0 # Nilai default optimal dari hasil tuning
|
| 53 |
|
| 54 |
+
# ---------------------------------------------------------------------------
|
| 55 |
# BAGS OF VISUAL WORDS
|
| 56 |
+
# ---------------------------------------------------------------------------
|
| 57 |
+
CODEBOOK_SIZES = [200]
|
| 58 |
DEFAULT_CODEBOOK_SIZE = 200 # Nilai default optimal dari hasil tuning
|
| 59 |
MAX_DESCRIPTORS_FOR_CODEBOOK = 200_000
|
| 60 |
MINIBATCH_KMEANS_BATCH_SIZE = 2000
|
| 61 |
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
# CLUSTERING (K-Means / PCA)
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
N_CLUSTERS_FINAL = 2
|
| 66 |
RANDOM_STATE = 42
|
| 67 |
|
| 68 |
+
# ---------------------------------------------------------------------------
|
| 69 |
# LOGGING
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
LOG_LEVEL = "INFO"
|
| 72 |
|
| 73 |
+
# ---------------------------------------------------------------------------
|
| 74 |
+
# HYPERPARAMETER TUNING OTOMATIS (NATIVE)
|
| 75 |
+
# ---------------------------------------------------------------------------
|
| 76 |
+
# Rentang angka yang akan diuji secara otomatis saat menjalankan --step tune
|
| 77 |
TUNING_HESSIAN_THRESHOLDS = [400, 500, 600]
|
| 78 |
TUNING_HSV_WEIGHTS = [2.0, 2.5, 3.0]
|
| 79 |
TUNING_CODEBOOK_SIZES = [50, 100, 200]
|
outputs/tables/Laporan_Prediksi_SIFT_KMEANS.csv
CHANGED
|
@@ -1037,7 +1037,7 @@ DAY-1_UMKM_SKL_093.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
|
| 1037 |
DAY-1_UMKM_SKL_094.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
| 1038 |
DAY-1_UMKM_SKL_095.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
| 1039 |
DAY-1_UMKM_SKL_096.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
| 1040 |
-
DAY-1_UMKM_SKL_097.JPG,UMKM,DAY-1,SKL,
|
| 1041 |
DAY-1_UMKM_SKL_098.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
| 1042 |
DAY-1_UMKM_SKL_099.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
| 1043 |
DAY-1_UMKM_SKL_100.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
|
@@ -1821,7 +1821,7 @@ DAY-2_UMKM_PDS_060.JPG,UMKM,DAY-2,PDS,0,Klaster A
|
|
| 1821 |
DAY-2_UMKM_PDS_061.JPG,UMKM,DAY-2,PDS,1,Klaster B
|
| 1822 |
DAY-2_UMKM_PDS_062.JPG,UMKM,DAY-2,PDS,0,Klaster A
|
| 1823 |
DAY-2_UMKM_PDS_063.JPG,UMKM,DAY-2,PDS,1,Klaster B
|
| 1824 |
-
DAY-2_UMKM_PDS_064.JPG,UMKM,DAY-2,PDS,
|
| 1825 |
DAY-2_UMKM_PDS_065.JPG,UMKM,DAY-2,PDS,1,Klaster B
|
| 1826 |
DAY-2_UMKM_PDS_066.JPG,UMKM,DAY-2,PDS,0,Klaster A
|
| 1827 |
DAY-2_UMKM_PDS_067.JPG,UMKM,DAY-2,PDS,1,Klaster B
|
|
@@ -2017,7 +2017,7 @@ DAY-2_UMKM_PNT_116.JPG,UMKM,DAY-2,PNT,1,Klaster B
|
|
| 2017 |
DAY-2_UMKM_PNT_117.JPG,UMKM,DAY-2,PNT,1,Klaster B
|
| 2018 |
DAY-2_UMKM_PNT_118.JPG,UMKM,DAY-2,PNT,0,Klaster A
|
| 2019 |
DAY-2_UMKM_PNT_119.JPG,UMKM,DAY-2,PNT,1,Klaster B
|
| 2020 |
-
DAY-2_UMKM_PNT_120.JPG,UMKM,DAY-2,PNT,
|
| 2021 |
DAY-2_UMKM_PNT_121.JPG,UMKM,DAY-2,PNT,1,Klaster B
|
| 2022 |
DAY-2_UMKM_PNT_122.JPG,UMKM,DAY-2,PNT,0,Klaster A
|
| 2023 |
DAY-2_UMKM_PNT_123.JPG,UMKM,DAY-2,PNT,1,Klaster B
|
|
@@ -2247,7 +2247,7 @@ DAY-2_UMKM_SKL_066.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
|
| 2247 |
DAY-2_UMKM_SKL_067.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
| 2248 |
DAY-2_UMKM_SKL_068.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
| 2249 |
DAY-2_UMKM_SKL_069.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
| 2250 |
-
DAY-2_UMKM_SKL_070.JPG,UMKM,DAY-2,SKL,
|
| 2251 |
DAY-2_UMKM_SKL_071.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
| 2252 |
DAY-2_UMKM_SKL_072.JPG,UMKM,DAY-2,SKL,0,Klaster A
|
| 2253 |
DAY-2_UMKM_SKL_073.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
|
@@ -2452,6 +2452,6 @@ DAY-2_UMKM_SMC_134.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
|
| 2452 |
DAY-2_UMKM_SMC_135.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
| 2453 |
DAY-2_UMKM_SMC_136.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
| 2454 |
DAY-2_UMKM_SMC_137.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
| 2455 |
-
DAY-2_UMKM_SMC_138.JPG,UMKM,DAY-2,SMC,
|
| 2456 |
DAY-2_UMKM_SMC_139.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
| 2457 |
DAY-2_UMKM_SMC_140.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
|
|
|
| 1037 |
DAY-1_UMKM_SKL_094.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
| 1038 |
DAY-1_UMKM_SKL_095.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
| 1039 |
DAY-1_UMKM_SKL_096.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
| 1040 |
+
DAY-1_UMKM_SKL_097.JPG,UMKM,DAY-1,SKL,1,Klaster B
|
| 1041 |
DAY-1_UMKM_SKL_098.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
| 1042 |
DAY-1_UMKM_SKL_099.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
| 1043 |
DAY-1_UMKM_SKL_100.JPG,UMKM,DAY-1,SKL,0,Klaster A
|
|
|
|
| 1821 |
DAY-2_UMKM_PDS_061.JPG,UMKM,DAY-2,PDS,1,Klaster B
|
| 1822 |
DAY-2_UMKM_PDS_062.JPG,UMKM,DAY-2,PDS,0,Klaster A
|
| 1823 |
DAY-2_UMKM_PDS_063.JPG,UMKM,DAY-2,PDS,1,Klaster B
|
| 1824 |
+
DAY-2_UMKM_PDS_064.JPG,UMKM,DAY-2,PDS,1,Klaster B
|
| 1825 |
DAY-2_UMKM_PDS_065.JPG,UMKM,DAY-2,PDS,1,Klaster B
|
| 1826 |
DAY-2_UMKM_PDS_066.JPG,UMKM,DAY-2,PDS,0,Klaster A
|
| 1827 |
DAY-2_UMKM_PDS_067.JPG,UMKM,DAY-2,PDS,1,Klaster B
|
|
|
|
| 2017 |
DAY-2_UMKM_PNT_117.JPG,UMKM,DAY-2,PNT,1,Klaster B
|
| 2018 |
DAY-2_UMKM_PNT_118.JPG,UMKM,DAY-2,PNT,0,Klaster A
|
| 2019 |
DAY-2_UMKM_PNT_119.JPG,UMKM,DAY-2,PNT,1,Klaster B
|
| 2020 |
+
DAY-2_UMKM_PNT_120.JPG,UMKM,DAY-2,PNT,0,Klaster A
|
| 2021 |
DAY-2_UMKM_PNT_121.JPG,UMKM,DAY-2,PNT,1,Klaster B
|
| 2022 |
DAY-2_UMKM_PNT_122.JPG,UMKM,DAY-2,PNT,0,Klaster A
|
| 2023 |
DAY-2_UMKM_PNT_123.JPG,UMKM,DAY-2,PNT,1,Klaster B
|
|
|
|
| 2247 |
DAY-2_UMKM_SKL_067.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
| 2248 |
DAY-2_UMKM_SKL_068.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
| 2249 |
DAY-2_UMKM_SKL_069.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
| 2250 |
+
DAY-2_UMKM_SKL_070.JPG,UMKM,DAY-2,SKL,0,Klaster A
|
| 2251 |
DAY-2_UMKM_SKL_071.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
| 2252 |
DAY-2_UMKM_SKL_072.JPG,UMKM,DAY-2,SKL,0,Klaster A
|
| 2253 |
DAY-2_UMKM_SKL_073.JPG,UMKM,DAY-2,SKL,1,Klaster B
|
|
|
|
| 2452 |
DAY-2_UMKM_SMC_135.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
| 2453 |
DAY-2_UMKM_SMC_136.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
| 2454 |
DAY-2_UMKM_SMC_137.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
| 2455 |
+
DAY-2_UMKM_SMC_138.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
| 2456 |
DAY-2_UMKM_SMC_139.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
| 2457 |
DAY-2_UMKM_SMC_140.JPG,UMKM,DAY-2,SMC,1,Klaster B
|
outputs/tables/cluster_metrics_all.csv
CHANGED
|
@@ -1,11 +1,7 @@
|
|
| 1 |
silhouette_score,davies_bouldin_score,calinski_harabasz_score,feature,method,codebook_size,feature_space
|
| 2 |
-
0.
|
| 3 |
-
0.
|
| 4 |
-
0.
|
| 5 |
-
0.
|
| 6 |
-
0.
|
| 7 |
-
0.
|
| 8 |
-
0.266449511051178,1.5571960138509717,972.1834943598637,SURF,kmeans,100,raw
|
| 9 |
-
0.24493154883384705,1.646778363458069,872.3269812624878,SURF,kmeans,200,raw
|
| 10 |
-
0.3165423274040222,1.37358422721392,1211.4946023395398,SURF,kmeans,200,pca
|
| 11 |
-
0.2951558530330658,1.4791946371612006,1047.482893527336,SURF,gmm,200,pca
|
|
|
|
| 1 |
silhouette_score,davies_bouldin_score,calinski_harabasz_score,feature,method,codebook_size,feature_space
|
| 2 |
+
0.2769351303577423,1.5145757851244719,1026.650511427009,SIFT,kmeans,200,raw
|
| 3 |
+
0.3407447636127472,1.2889466499830886,1367.2222491156222,SIFT,kmeans,200,pca
|
| 4 |
+
0.3261227607727051,1.312133462623333,1273.1930209170655,SIFT,gmm,200,pca
|
| 5 |
+
0.2449309080839157,1.6467811072725456,872.3243306465555,SURF,kmeans,200,raw
|
| 6 |
+
0.316542387008667,1.373584476226374,1211.4935094315822,SURF,kmeans,200,pca
|
| 7 |
+
0.29515576362609863,1.4791957406926373,1047.4810568345908,SURF,gmm,200,pca
|
|
|
|
|
|
|
|
|
|
|
|
outputs/tables/cluster_stability.csv
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
feature,silhouette_mean,silhouette_std,ari_vs_full_mean,ari_vs_full_std,n_trials
|
| 2 |
-
SIFT,0.
|
| 3 |
-
SURF,0.
|
|
|
|
| 1 |
feature,silhouette_mean,silhouette_std,ari_vs_full_mean,ari_vs_full_std,n_trials
|
| 2 |
+
SIFT,0.3403042733669281,0.002450848583654707,0.9896803679035703,0.013573475599350439,10
|
| 3 |
+
SURF,0.3187896698713303,0.006372772695515273,0.9385743852568675,0.15141701917397363,10
|
outputs/tables/efficiency_raw.csv
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
outputs/tables/efficiency_summary.csv
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
method,mean_elapsed_sec,median_elapsed_sec,std_elapsed_sec,mean_n_keypoints,median_n_keypoints,std_n_keypoints,n_images
|
| 2 |
-
SIFT,0.
|
| 3 |
-
SURF,0.
|
|
|
|
| 1 |
method,mean_elapsed_sec,median_elapsed_sec,std_elapsed_sec,mean_n_keypoints,median_n_keypoints,std_n_keypoints,n_images
|
| 2 |
+
SIFT,0.0044723340390882705,0.003919449999983726,0.0023356877672534916,128.60138436482086,90.0,122.43336731418978,2456
|
| 3 |
+
SURF,0.0022748484934848137,0.002071800000010171,0.004352626171909472,56.10260586319218,37.0,60.453230961928284,2456
|
outputs/tables/final_summary.json
CHANGED
|
@@ -2,12 +2,12 @@
|
|
| 2 |
"best_combination_by_silhouette": {
|
| 3 |
"feature": "SIFT",
|
| 4 |
"clustering_method": "kmeans",
|
| 5 |
-
"silhouette_score": 0.
|
| 6 |
},
|
| 7 |
"best_feature_overall_avg_silhouette": "SIFT",
|
| 8 |
"avg_silhouette_per_feature": {
|
| 9 |
-
"SIFT": 0.
|
| 10 |
-
"SURF": 0.
|
| 11 |
},
|
| 12 |
"codebook_size_used_for_main_comparison": 200
|
| 13 |
}
|
|
|
|
| 2 |
"best_combination_by_silhouette": {
|
| 3 |
"feature": "SIFT",
|
| 4 |
"clustering_method": "kmeans",
|
| 5 |
+
"silhouette_score": 0.3407447636127472
|
| 6 |
},
|
| 7 |
"best_feature_overall_avg_silhouette": "SIFT",
|
| 8 |
"avg_silhouette_per_feature": {
|
| 9 |
+
"SIFT": 0.33343376219272614,
|
| 10 |
+
"SURF": 0.3058490753173828
|
| 11 |
},
|
| 12 |
"codebook_size_used_for_main_comparison": 200
|
| 13 |
}
|
outputs/tables/manifest_summary.csv
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
run_pipeline.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
Orkestrator pipeline unsupervised computer vision untuk deteksi mutu
|
| 4 |
kesegaran daging sapi multidomain (SIFT/SURF -> BoVW -> Clustering).
|
| 5 |
|
| 6 |
-
|
| 7 |
uv run python run_pipeline.py --step preprocess
|
| 8 |
uv run python run_pipeline.py --step extract
|
| 9 |
uv run python run_pipeline.py --step bovw
|
|
@@ -14,10 +14,8 @@ Code pemakaian:
|
|
| 14 |
"""
|
| 15 |
import argparse
|
| 16 |
import os
|
| 17 |
-
import shutil
|
| 18 |
import time
|
| 19 |
import json
|
| 20 |
-
import datetime
|
| 21 |
|
| 22 |
import numpy as np
|
| 23 |
import pandas as pd
|
|
@@ -25,7 +23,6 @@ from tqdm import tqdm
|
|
| 25 |
from sklearn.preprocessing import MinMaxScaler
|
| 26 |
from sklearn.decomposition import PCA
|
| 27 |
from sklearn.mixture import GaussianMixture
|
| 28 |
-
from typing import Dict, Any
|
| 29 |
import joblib
|
| 30 |
|
| 31 |
import config
|
|
@@ -34,9 +31,11 @@ from src import bovw, clustering, evaluation, feature_extraction, preprocessing,
|
|
| 34 |
logger = utils.get_logger("run_pipeline")
|
| 35 |
|
| 36 |
|
|
|
|
| 37 |
# TAHAP 1: PREPROCESSING & SEGMENTASI ROI
|
|
|
|
| 38 |
def step_preprocess():
|
| 39 |
-
logger.info("TAHAP 1
|
| 40 |
utils.ensure_dirs()
|
| 41 |
|
| 42 |
image_paths = utils.list_images(config.RAW_DATA_DIR, config.IMAGE_EXTENSIONS)
|
|
@@ -82,9 +81,11 @@ def step_preprocess():
|
|
| 82 |
logger.info("Selesai preprocessing: %d sukses.", len(manifest))
|
| 83 |
|
| 84 |
|
|
|
|
| 85 |
# TAHAP 2: EKSTRAKSI FITUR LOKAL (SIFT & SURF)
|
|
|
|
| 86 |
def step_extract():
|
| 87 |
-
logger.info("TAHAP 2
|
| 88 |
manifest = utils.load_pickle(os.path.join(config.INTERIM_DIR, "manifest.pkl"))
|
| 89 |
|
| 90 |
for method in ("sift", "surf"):
|
|
@@ -118,9 +119,11 @@ def step_extract():
|
|
| 118 |
evaluation.save_table(evaluation.summarize_efficiency(df_eff), "efficiency_summary.csv")
|
| 119 |
|
| 120 |
|
|
|
|
| 121 |
# TAHAP 3: BAGS OF VISUAL WORDS & EARLY FUSION
|
|
|
|
| 122 |
def step_bovw():
|
| 123 |
-
logger.info("TAHAP 3
|
| 124 |
manifest = utils.load_pickle(os.path.join(config.INTERIM_DIR, "manifest.pkl"))
|
| 125 |
filenames = [e["filename"] for e in manifest]
|
| 126 |
|
|
@@ -144,7 +147,7 @@ def step_bovw():
|
|
| 144 |
hsv_dict[f_name] = scaler.transform(hsv_dict[f_name].reshape(1, -1))[0]
|
| 145 |
|
| 146 |
# ----------------------------------------------------------
|
| 147 |
-
# MinMaxScaler saja TIDAK cukup menyamakan skala
|
| 148 |
# dengan blok tekstur. bovw_hist (Hellinger) punya norma-L2 = 1
|
| 149 |
# per baris (properti transformasi sqrt(L1-normalized)),
|
| 150 |
# sedangkan vektor HSV hasil MinMax bisa punya norma-L2
|
|
@@ -155,7 +158,7 @@ def step_bovw():
|
|
| 155 |
# yang dijalankan setelahnya (fitur bervarians besar otomatis
|
| 156 |
# mendominasi komponen utama).
|
| 157 |
#
|
| 158 |
-
# normalisasi setiap vektor HSV ke norma-L2 = 1 SETELAH
|
| 159 |
# MinMax, baru dikalikan HSV_FUSION_WEIGHT. Sekarang
|
| 160 |
# HSV_FUSION_WEIGHT=1.0 berarti "kontribusi warna setara
|
| 161 |
# dengan tekstur", weight=3.0 berarti "warna diberi bobot 3x
|
|
@@ -204,9 +207,11 @@ def step_bovw():
|
|
| 204 |
utils.save_pickle(histograms, os.path.join(config.INTERIM_DIR, f"histograms_{method}.pkl"))
|
| 205 |
|
| 206 |
|
|
|
|
| 207 |
# TAHAP 4: CLUSTERING UNSUPERVISED DENGAN PCA
|
|
|
|
| 208 |
def step_cluster():
|
| 209 |
-
logger.info("TAHAP 4
|
| 210 |
manifest = utils.load_pickle(os.path.join(config.INTERIM_DIR, "manifest.pkl"))
|
| 211 |
true_labels = [e.get("label") for e in manifest]
|
| 212 |
has_ground_truth = all(l is not None for l in true_labels) and len(true_labels) > 0
|
|
@@ -318,9 +323,12 @@ def step_cluster():
|
|
| 318 |
evaluation.save_table(pd.DataFrame(stability_rows), "cluster_stability.csv")
|
| 319 |
|
| 320 |
|
|
|
|
| 321 |
# TAHAP 5: EVALUASI & KOMPARASI SIFT vs SURF
|
|
|
|
| 322 |
def step_evaluate():
|
| 323 |
-
logger.info("TAHAP 5
|
|
|
|
| 324 |
manifest_path = os.path.join(config.INTERIM_DIR, "manifest.pkl")
|
| 325 |
if os.path.exists(manifest_path):
|
| 326 |
manifest = utils.load_pickle(manifest_path)
|
|
@@ -339,13 +347,14 @@ def step_evaluate():
|
|
| 339 |
df_results = pd.read_csv(os.path.join(config.TABLES_DIR, "cluster_metrics_all.csv"))
|
| 340 |
df_eff = pd.read_csv(os.path.join(config.TABLES_DIR, "efficiency_raw.csv"))
|
| 341 |
|
|
|
|
| 342 |
logger.info("Menghasilkan visualisasi Ablation Study (PCA Impact)...")
|
| 343 |
visualization.plot_pca_ablation_impact(
|
| 344 |
df_results,
|
| 345 |
save_name="pca_impact_ablation.png"
|
| 346 |
)
|
| 347 |
-
|
| 348 |
-
logger.info("Menghasilkan Radar Chart
|
| 349 |
visualization.plot_overall_radar_comparison(df_results, df_eff)
|
| 350 |
|
| 351 |
df_default = df_results[
|
|
@@ -406,14 +415,17 @@ def step_evaluate():
|
|
| 406 |
logger.info("Ringkasan akhir disimpan ke outputs/tables/final_summary.json")
|
| 407 |
|
| 408 |
|
|
|
|
| 409 |
# TAHAP 6: HYPERPARAMETER TUNING NATIVE (DEEP SEARCH)
|
|
|
|
| 410 |
def step_tune():
|
| 411 |
-
logger.info("TAHAP 6
|
| 412 |
|
| 413 |
-
#
|
| 414 |
tuning_pca_components = [15, 20, 25, 30, 35]
|
| 415 |
tuning_gmm_covariances = ['full', 'tied', 'diag']
|
| 416 |
|
|
|
|
| 417 |
config.SURF_HESSIAN_THRESHOLD = 500
|
| 418 |
config.CODEBOOK_SIZES = [200]
|
| 419 |
config.DEFAULT_CODEBOOK_SIZE = 200
|
|
@@ -485,7 +497,15 @@ def step_tune():
|
|
| 485 |
print(df_results.head(1).to_string(index=False))
|
| 486 |
print("="*60)
|
| 487 |
|
| 488 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
logger.warning(
|
| 490 |
"PERINGATAN METODOLOGIS: SILHOUETTE_SCORE pada tabel deep search "
|
| 491 |
"adalah hasil PENCARIAN yang memaksimalkan metrik itu sendiri -> bias "
|
|
@@ -538,7 +558,7 @@ def step_tune():
|
|
| 538 |
|
| 539 |
|
| 540 |
def step_export():
|
| 541 |
-
logger.info("TAHAP 7
|
| 542 |
|
| 543 |
# 1. Memuat metadata gambar dan hasil klasterisasi
|
| 544 |
manifest_path = os.path.join(config.INTERIM_DIR, "manifest.pkl")
|
|
@@ -584,171 +604,24 @@ def step_export():
|
|
| 584 |
print(df_report.head(10).to_string(index=False))
|
| 585 |
print("="*70)
|
| 586 |
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
df_report,
|
| 596 |
-
save_name=f"temporal_cluster_{config.DATASET_DOMAIN}.png"
|
| 597 |
-
)
|
| 598 |
-
logger.info("Grafik distribusi klaster otonom tersimpan di direktori figures.")
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
def _execute_single_scenario(sc: Dict[str, Any], dict_sift: dict, dict_surf: dict,
|
| 602 |
-
code_dir: str, results_dir: str) -> dict:
|
| 603 |
-
"""
|
| 604 |
-
Helper function (Private Method):
|
| 605 |
-
Mengeksekusi satu iterasi eksperimen otonom (ekstraksi matriks laten,
|
| 606 |
-
reduksi dimensi, dan klastering) serta mengembalikan matriks evaluasinya.
|
| 607 |
-
"""
|
| 608 |
-
# 1. Penentuan target dictionary berdasarkan skenario
|
| 609 |
-
target_dict = dict_sift if sc["feat"] == "SIFT" else dict_surf
|
| 610 |
-
X_current = target_dict[sc["k"]].copy()
|
| 611 |
-
|
| 612 |
-
# 2. Operasi Ablasi Fusi Warna (HSV)
|
| 613 |
-
if not sc["use_hsv"]:
|
| 614 |
-
X_current = X_current[:, :-9]
|
| 615 |
-
|
| 616 |
-
# 3. Operasi Reduksi Dimensi (PCA)
|
| 617 |
-
if sc["space"] == "pca":
|
| 618 |
-
# Mencegah komponen PCA melampaui dimensi sampel
|
| 619 |
-
n_comp = min(sc["pca_comp"], X_current.shape[1] - 1)
|
| 620 |
-
pca = PCA(n_components=n_comp, random_state=config.RANDOM_STATE)
|
| 621 |
-
X_current = pca.fit_transform(X_current)
|
| 622 |
-
|
| 623 |
-
# 4. Pemanggilan Algoritma Klastering Otonom
|
| 624 |
-
if sc["algo"] == "kmeans":
|
| 625 |
-
labels, _ = clustering.run_kmeans(X_current)
|
| 626 |
-
else:
|
| 627 |
-
labels, _ = clustering.run_gmm(X_current)
|
| 628 |
-
|
| 629 |
-
# 5. Kalkulasi Metrik Separabilitas Spasial
|
| 630 |
-
metrics = clustering.internal_validation(X_current, labels)
|
| 631 |
-
|
| 632 |
-
return metrics
|
| 633 |
-
|
| 634 |
|
| 635 |
-
#EKSPERIMEN SENSITIVITAS ANALISIS
|
| 636 |
-
def step_batch_experiment() -> None:
|
| 637 |
-
logger.info("=== TAHAP EKSPERIMEN BATCH & SENSITIVITY ANALYSIS ===")
|
| 638 |
-
|
| 639 |
-
sift_path = os.path.join(config.INTERIM_DIR, "histograms_sift.pkl")
|
| 640 |
-
surf_path = os.path.join(config.INTERIM_DIR, "histograms_surf.pkl")
|
| 641 |
-
efficiency_path = os.path.join(config.TABLES_DIR, "efficiency_summary.csv")
|
| 642 |
-
|
| 643 |
-
if not os.path.exists(sift_path) or not os.path.exists(efficiency_path):
|
| 644 |
-
logger.warning("Fitur dasar atau data efisiensi belum ada. Harap jalankan ekstraksi terlebih dahulu.")
|
| 645 |
-
return
|
| 646 |
-
|
| 647 |
-
dict_sift = utils.load_pickle(sift_path)
|
| 648 |
-
dict_surf = utils.load_pickle(surf_path)
|
| 649 |
-
|
| 650 |
-
# 1. Pemuatan dan Pemetaan Data Efisiensi
|
| 651 |
-
df_eff = pd.read_csv(efficiency_path)
|
| 652 |
-
df_eff['method'] = df_eff['method'].str.upper()
|
| 653 |
-
|
| 654 |
-
eff_lookup = {}
|
| 655 |
-
for _, row in df_eff.iterrows():
|
| 656 |
-
eff_lookup[row['method']] = {
|
| 657 |
-
'elapsed_sec': round(row['mean_elapsed_sec'], 4),
|
| 658 |
-
'std_sec': round(row['std_elapsed_sec'], 4),
|
| 659 |
-
'n_keypoints': int(round(row['mean_n_keypoints'], 0))
|
| 660 |
-
}
|
| 661 |
-
|
| 662 |
-
# 2. Definisi 14 Skenario Eksperimen (SINTA 1/2 Standard)
|
| 663 |
-
scenarios = [
|
| 664 |
-
{"id": "EXP-001", "feat": "SIFT", "k": 200, "use_hsv": True, "space": "raw", "algo": "kmeans", "pca_comp": None},
|
| 665 |
-
{"id": "EXP-002", "feat": "SURF", "k": 200, "use_hsv": True, "space": "raw", "algo": "kmeans", "pca_comp": None},
|
| 666 |
-
{"id": "EXP-003", "feat": "SIFT", "k": 200, "use_hsv": True, "space": "pca", "algo": "kmeans", "pca_comp": 30},
|
| 667 |
-
{"id": "EXP-004", "feat": "SURF", "k": 200, "use_hsv": True, "space": "pca", "algo": "kmeans", "pca_comp": 30},
|
| 668 |
-
{"id": "EXP-005", "feat": "SIFT", "k": 200, "use_hsv": True, "space": "pca", "algo": "gmm", "pca_comp": 30},
|
| 669 |
-
{"id": "EXP-006", "feat": "SURF", "k": 200, "use_hsv": True, "space": "pca", "algo": "gmm", "pca_comp": 30},
|
| 670 |
-
{"id": "EXP-007", "feat": "SIFT", "k": 50, "use_hsv": True, "space": "pca", "algo": "kmeans", "pca_comp": 30},
|
| 671 |
-
{"id": "EXP-008", "feat": "SIFT", "k": 100, "use_hsv": True, "space": "pca", "algo": "kmeans", "pca_comp": 30},
|
| 672 |
-
{"id": "EXP-009", "feat": "SURF", "k": 50, "use_hsv": True, "space": "pca", "algo": "kmeans", "pca_comp": 30},
|
| 673 |
-
{"id": "EXP-010", "feat": "SURF", "k": 100, "use_hsv": True, "space": "pca", "algo": "kmeans", "pca_comp": 30},
|
| 674 |
-
{"id": "EXP-011", "feat": "SIFT", "k": 200, "use_hsv": False, "space": "pca", "algo": "kmeans", "pca_comp": 30},
|
| 675 |
-
{"id": "EXP-012", "feat": "SURF", "k": 200, "use_hsv": False, "space": "pca", "algo": "kmeans", "pca_comp": 30},
|
| 676 |
-
{"id": "EXP-013", "feat": "SIFT", "k": 200, "use_hsv": False, "space": "raw", "algo": "kmeans", "pca_comp": None},
|
| 677 |
-
{"id": "EXP-014", "feat": "SURF", "k": 200, "use_hsv": False, "space": "raw", "algo": "kmeans", "pca_comp": None},
|
| 678 |
-
]
|
| 679 |
-
|
| 680 |
-
experiment_logs = []
|
| 681 |
-
base_archive_path = os.path.join(os.getcwd(), "archives")
|
| 682 |
-
os.makedirs(base_archive_path, exist_ok=True)
|
| 683 |
-
|
| 684 |
-
# Menghasilkan format tanggal (YYYYMMDD) untuk penomoran ID
|
| 685 |
-
date_str = datetime.datetime.now().strftime("%Y%m%d")
|
| 686 |
-
# Mempertahankan timestamp detik untuk nama file Master Log
|
| 687 |
-
timestamp_full = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 688 |
-
|
| 689 |
-
# Menggunakan enumerate untuk menghasilkan sekuens otomatis (01, 02, dst.)
|
| 690 |
-
for idx, sc in enumerate(scenarios, start=1):
|
| 691 |
-
# Membentuk ID Eksperimen: EXP-YYYYMMDD-XX
|
| 692 |
-
seq_str = f"{idx:02d}"
|
| 693 |
-
exp_id = f"EXP-{date_str}-{seq_str}"
|
| 694 |
-
|
| 695 |
-
# Menimpa 'id' lama agar ID baru terekam di Master Log
|
| 696 |
-
sc["id"] = exp_id
|
| 697 |
-
|
| 698 |
-
extractor = sc["feat"]
|
| 699 |
-
logger.info(f"Mengeksekusi {exp_id}: {extractor} k={sc['k']} HSV={sc['use_hsv']} {sc['space'].upper()} {sc['algo'].upper()}")
|
| 700 |
-
|
| 701 |
-
# A. Pembuatan Direktori Arsip otomatis
|
| 702 |
-
folder_name = f"{config.DATASET_DOMAIN}_{exp_id}"
|
| 703 |
-
exp_dir = os.path.join(base_archive_path, folder_name)
|
| 704 |
-
code_dir, results_dir = os.path.join(exp_dir, "code_snapshot"), os.path.join(exp_dir, "results")
|
| 705 |
-
os.makedirs(code_dir, exist_ok=True)
|
| 706 |
-
os.makedirs(results_dir, exist_ok=True)
|
| 707 |
-
|
| 708 |
-
# B. Snapshot Source Code
|
| 709 |
-
for file in os.listdir("."):
|
| 710 |
-
if file.endswith(".py") or file.endswith(".json"):
|
| 711 |
-
shutil.copy(file, code_dir)
|
| 712 |
-
if os.path.exists("src"):
|
| 713 |
-
shutil.copytree("src", os.path.join(code_dir, "src"))
|
| 714 |
-
|
| 715 |
-
# C. Eksekusi Skenario Komputasi
|
| 716 |
-
metrics = _execute_single_scenario(sc, dict_sift, dict_surf, code_dir, results_dir)
|
| 717 |
-
|
| 718 |
-
# D. Integrasi Metrik Efisiensi ke dalam Log
|
| 719 |
-
eff_data = eff_lookup.get(extractor, {'elapsed_sec': 0, 'std_sec': 0, 'n_keypoints': 0})
|
| 720 |
-
metrics["efficiency"] = eff_data
|
| 721 |
-
|
| 722 |
-
# Memperbarui JSON di dalam folder arsip
|
| 723 |
-
with open(os.path.join(results_dir, "evaluation_metrics.json"), "w") as f:
|
| 724 |
-
json.dump(metrics, f, indent=4)
|
| 725 |
-
|
| 726 |
-
# E. Pencatatan ke Master Log Tabular
|
| 727 |
-
experiment_logs.append({
|
| 728 |
-
"ID Eksperimen": exp_id,
|
| 729 |
-
"Ekstraktor": extractor,
|
| 730 |
-
"Konfigurasi": f"{extractor} k={sc['k']} HSV={sc['use_hsv']}",
|
| 731 |
-
"Ruang & Algo": f"{sc['space'].upper()} + {sc['algo'].upper()}",
|
| 732 |
-
"Silhouette (↑)": round(metrics.get("silhouette_score", 0), 4),
|
| 733 |
-
"Davies-Bouldin (↓)": round(metrics.get("davies_bouldin_score", 0), 4),
|
| 734 |
-
"Calinski-Harabasz (↑)": round(metrics.get("calinski_harabasz_score", 0), 2),
|
| 735 |
-
"Waktu Ekstraksi (det/citra) (↓)": eff_data["elapsed_sec"],
|
| 736 |
-
"Std Dev Waktu (±)": eff_data["std_sec"],
|
| 737 |
-
"Rata-rata Keypoint": eff_data["n_keypoints"],
|
| 738 |
-
"Folder Arsip": folder_name
|
| 739 |
-
})
|
| 740 |
-
|
| 741 |
-
# 3. Ekspor Tabel Rekapitulasi
|
| 742 |
-
df_log = pd.DataFrame(experiment_logs)
|
| 743 |
-
main_log_path = os.path.join(base_archive_path, f"Master_Log_{config.DATASET_DOMAIN}_{timestamp_full}.csv")
|
| 744 |
-
df_log.to_csv(main_log_path, index=False)
|
| 745 |
-
logger.info(f"Eksperimen komprehensif terintegrasi selesai. Log tersimpan di: {main_log_path}")
|
| 746 |
|
| 747 |
-
#
|
|
|
|
|
|
|
| 748 |
def step_build_production_model():
|
| 749 |
-
logger.info("TAHAP 8
|
| 750 |
|
| 751 |
-
# 1. Tentukan parameter terbaik dari hasil
|
|
|
|
| 752 |
logger.info("Mengambil konfigurasi terbaik dari file CSV...")
|
| 753 |
tuning_csv_path = os.path.join(config.TABLES_DIR, "tuning_results_deep_search.csv")
|
| 754 |
|
|
@@ -778,10 +651,9 @@ def step_build_production_model():
|
|
| 778 |
codebook = codebooks[best_codebook_size]
|
| 779 |
filenames = list(descriptors_dict.keys())
|
| 780 |
|
| 781 |
-
|
| 782 |
-
HARUS sama persis dengan step_bovw, atau model produksi tidak konsisten
|
| 783 |
-
dengan hasil eksperimen yang dilaporkan
|
| 784 |
-
|
| 785 |
logger.info("Melatih ulang MinMaxScaler...")
|
| 786 |
raw_hsv_values = list(hsv_dict.values())
|
| 787 |
scaler = MinMaxScaler()
|
|
@@ -813,7 +685,7 @@ def step_build_production_model():
|
|
| 813 |
logger.info("Melatih GMM Classifier...")
|
| 814 |
gmm = GaussianMixture(n_components=2, covariance_type=best_gmm_covariance,
|
| 815 |
random_state=config.RANDOM_STATE, n_init=10)
|
| 816 |
-
gmm.fit(X_pca)
|
| 817 |
|
| 818 |
# 6. BUNGKUS MENJADI SATU FILE MODEL
|
| 819 |
model_package = {
|
|
@@ -835,7 +707,9 @@ def step_build_production_model():
|
|
| 835 |
logger.info("Model Produksi berhasil disimpan di: %s", model_path)
|
| 836 |
logger.info("Model ini siap dimuat (di-load) untuk aplikasi klasifikasi otomatis tahun depan!")
|
| 837 |
|
|
|
|
| 838 |
# ENTRY POINT
|
|
|
|
| 839 |
STEPS = {
|
| 840 |
"preprocess": step_preprocess,
|
| 841 |
"extract": step_extract,
|
|
@@ -845,7 +719,6 @@ STEPS = {
|
|
| 845 |
"tune": step_tune,
|
| 846 |
"export": step_export,
|
| 847 |
"build_model": step_build_production_model,
|
| 848 |
-
"batch_log": step_batch_experiment,
|
| 849 |
}
|
| 850 |
|
| 851 |
def main():
|
|
|
|
| 3 |
Orkestrator pipeline unsupervised computer vision untuk deteksi mutu
|
| 4 |
kesegaran daging sapi multidomain (SIFT/SURF -> BoVW -> Clustering).
|
| 5 |
|
| 6 |
+
Contoh pemakaian:
|
| 7 |
uv run python run_pipeline.py --step preprocess
|
| 8 |
uv run python run_pipeline.py --step extract
|
| 9 |
uv run python run_pipeline.py --step bovw
|
|
|
|
| 14 |
"""
|
| 15 |
import argparse
|
| 16 |
import os
|
|
|
|
| 17 |
import time
|
| 18 |
import json
|
|
|
|
| 19 |
|
| 20 |
import numpy as np
|
| 21 |
import pandas as pd
|
|
|
|
| 23 |
from sklearn.preprocessing import MinMaxScaler
|
| 24 |
from sklearn.decomposition import PCA
|
| 25 |
from sklearn.mixture import GaussianMixture
|
|
|
|
| 26 |
import joblib
|
| 27 |
|
| 28 |
import config
|
|
|
|
| 31 |
logger = utils.get_logger("run_pipeline")
|
| 32 |
|
| 33 |
|
| 34 |
+
# ---------------------------------------------------------------------------
|
| 35 |
# TAHAP 1: PREPROCESSING & SEGMENTASI ROI
|
| 36 |
+
# ---------------------------------------------------------------------------
|
| 37 |
def step_preprocess():
|
| 38 |
+
logger.info("=== TAHAP 1: PREPROCESSING & SEGMENTASI ROI ===")
|
| 39 |
utils.ensure_dirs()
|
| 40 |
|
| 41 |
image_paths = utils.list_images(config.RAW_DATA_DIR, config.IMAGE_EXTENSIONS)
|
|
|
|
| 81 |
logger.info("Selesai preprocessing: %d sukses.", len(manifest))
|
| 82 |
|
| 83 |
|
| 84 |
+
# ---------------------------------------------------------------------------
|
| 85 |
# TAHAP 2: EKSTRAKSI FITUR LOKAL (SIFT & SURF)
|
| 86 |
+
# ---------------------------------------------------------------------------
|
| 87 |
def step_extract():
|
| 88 |
+
logger.info("=== TAHAP 2: EKSTRAKSI FITUR LOKAL (SIFT & SURF) ===")
|
| 89 |
manifest = utils.load_pickle(os.path.join(config.INTERIM_DIR, "manifest.pkl"))
|
| 90 |
|
| 91 |
for method in ("sift", "surf"):
|
|
|
|
| 119 |
evaluation.save_table(evaluation.summarize_efficiency(df_eff), "efficiency_summary.csv")
|
| 120 |
|
| 121 |
|
| 122 |
+
# ---------------------------------------------------------------------------
|
| 123 |
# TAHAP 3: BAGS OF VISUAL WORDS & EARLY FUSION
|
| 124 |
+
# ---------------------------------------------------------------------------
|
| 125 |
def step_bovw():
|
| 126 |
+
logger.info("=== TAHAP 3: BAGS OF VISUAL WORDS & EARLY FUSION ===")
|
| 127 |
manifest = utils.load_pickle(os.path.join(config.INTERIM_DIR, "manifest.pkl"))
|
| 128 |
filenames = [e["filename"] for e in manifest]
|
| 129 |
|
|
|
|
| 147 |
hsv_dict[f_name] = scaler.transform(hsv_dict[f_name].reshape(1, -1))[0]
|
| 148 |
|
| 149 |
# ----------------------------------------------------------
|
| 150 |
+
# PERBAIKAN: MinMaxScaler saja TIDAK cukup menyamakan skala
|
| 151 |
# dengan blok tekstur. bovw_hist (Hellinger) punya norma-L2 = 1
|
| 152 |
# per baris (properti transformasi sqrt(L1-normalized)),
|
| 153 |
# sedangkan vektor HSV hasil MinMax bisa punya norma-L2
|
|
|
|
| 158 |
# yang dijalankan setelahnya (fitur bervarians besar otomatis
|
| 159 |
# mendominasi komponen utama).
|
| 160 |
#
|
| 161 |
+
# Solusi: normalisasi setiap vektor HSV ke norma-L2 = 1 SETELAH
|
| 162 |
# MinMax, baru dikalikan HSV_FUSION_WEIGHT. Sekarang
|
| 163 |
# HSV_FUSION_WEIGHT=1.0 berarti "kontribusi warna setara
|
| 164 |
# dengan tekstur", weight=3.0 berarti "warna diberi bobot 3x
|
|
|
|
| 207 |
utils.save_pickle(histograms, os.path.join(config.INTERIM_DIR, f"histograms_{method}.pkl"))
|
| 208 |
|
| 209 |
|
| 210 |
+
# ---------------------------------------------------------------------------
|
| 211 |
# TAHAP 4: CLUSTERING UNSUPERVISED DENGAN PCA
|
| 212 |
+
# ---------------------------------------------------------------------------
|
| 213 |
def step_cluster():
|
| 214 |
+
logger.info("=== TAHAP 4: CLUSTERING UNSUPERVISED ===")
|
| 215 |
manifest = utils.load_pickle(os.path.join(config.INTERIM_DIR, "manifest.pkl"))
|
| 216 |
true_labels = [e.get("label") for e in manifest]
|
| 217 |
has_ground_truth = all(l is not None for l in true_labels) and len(true_labels) > 0
|
|
|
|
| 323 |
evaluation.save_table(pd.DataFrame(stability_rows), "cluster_stability.csv")
|
| 324 |
|
| 325 |
|
| 326 |
+
# ---------------------------------------------------------------------------
|
| 327 |
# TAHAP 5: EVALUASI & KOMPARASI SIFT vs SURF
|
| 328 |
+
# ---------------------------------------------------------------------------
|
| 329 |
def step_evaluate():
|
| 330 |
+
logger.info("=== TAHAP 5: EVALUASI & KOMPARASI SIFT vs SURF ===")
|
| 331 |
+
logger.info("Menghasilkan bukti visual deteksi Keypoint untuk manuskrip...")
|
| 332 |
manifest_path = os.path.join(config.INTERIM_DIR, "manifest.pkl")
|
| 333 |
if os.path.exists(manifest_path):
|
| 334 |
manifest = utils.load_pickle(manifest_path)
|
|
|
|
| 347 |
df_results = pd.read_csv(os.path.join(config.TABLES_DIR, "cluster_metrics_all.csv"))
|
| 348 |
df_eff = pd.read_csv(os.path.join(config.TABLES_DIR, "efficiency_raw.csv"))
|
| 349 |
|
| 350 |
+
# --- [INJEKSI KODE BARU: GRAFIK ABLATION PCA] ---
|
| 351 |
logger.info("Menghasilkan visualisasi Ablation Study (PCA Impact)...")
|
| 352 |
visualization.plot_pca_ablation_impact(
|
| 353 |
df_results,
|
| 354 |
save_name="pca_impact_ablation.png"
|
| 355 |
)
|
| 356 |
+
#baru buat keseluruhan
|
| 357 |
+
logger.info("Menghasilkan Radar Chart Komparasi Keseluruhan...")
|
| 358 |
visualization.plot_overall_radar_comparison(df_results, df_eff)
|
| 359 |
|
| 360 |
df_default = df_results[
|
|
|
|
| 415 |
logger.info("Ringkasan akhir disimpan ke outputs/tables/final_summary.json")
|
| 416 |
|
| 417 |
|
| 418 |
+
# ---------------------------------------------------------------------------
|
| 419 |
# TAHAP 6: HYPERPARAMETER TUNING NATIVE (DEEP SEARCH)
|
| 420 |
+
# ---------------------------------------------------------------------------
|
| 421 |
def step_tune():
|
| 422 |
+
logger.info("=== TAHAP 6: HYPERPARAMETER TUNING (DEEP SEARCH) ===")
|
| 423 |
|
| 424 |
+
# Kumpulan parameter baru untuk memaksimalkan Silhouette > 0.5
|
| 425 |
tuning_pca_components = [15, 20, 25, 30, 35]
|
| 426 |
tuning_gmm_covariances = ['full', 'tied', 'diag']
|
| 427 |
|
| 428 |
+
# Kita fokuskan Hessian di angka optimal sebelumnya untuk menghemat waktu
|
| 429 |
config.SURF_HESSIAN_THRESHOLD = 500
|
| 430 |
config.CODEBOOK_SIZES = [200]
|
| 431 |
config.DEFAULT_CODEBOOK_SIZE = 200
|
|
|
|
| 497 |
print(df_results.head(1).to_string(index=False))
|
| 498 |
print("="*60)
|
| 499 |
|
| 500 |
+
# ------------------------------------------------------------------
|
| 501 |
+
# PERINGATAN METODOLOGIS + UJI STABILITAS KONFIGURASI TERBAIK
|
| 502 |
+
# ------------------------------------------------------------------
|
| 503 |
+
# Silhouette tertinggi dari grid search di atas TIDAK BOLEH langsung
|
| 504 |
+
# dilaporkan sebagai bukti separabilitas klaster - angka itu dipilih
|
| 505 |
+
# KARENA memaksimalkan metrik yang sama, jadi pasti bias optimis
|
| 506 |
+
# (circular: mencari lalu melaporkan metrik yang sama). Untuk laporan,
|
| 507 |
+
# gunakan silhouette_mean +/- std dari uji stabilitas di bawah, BUKAN
|
| 508 |
+
# SILHOUETTE_SCORE mentah dari baris teratas tabel deep search.
|
| 509 |
logger.warning(
|
| 510 |
"PERINGATAN METODOLOGIS: SILHOUETTE_SCORE pada tabel deep search "
|
| 511 |
"adalah hasil PENCARIAN yang memaksimalkan metrik itu sendiri -> bias "
|
|
|
|
| 558 |
|
| 559 |
|
| 560 |
def step_export():
|
| 561 |
+
logger.info("=== TAHAP 7: EKSPOR TABEL PREDIKSI ===")
|
| 562 |
|
| 563 |
# 1. Memuat metadata gambar dan hasil klasterisasi
|
| 564 |
manifest_path = os.path.join(config.INTERIM_DIR, "manifest.pkl")
|
|
|
|
| 604 |
print(df_report.head(10).to_string(index=False))
|
| 605 |
print("="*70)
|
| 606 |
|
| 607 |
+
# --- [INJEKSI KODE BARU: GRAFIK DISTRIBUSI TEMPORAL] ---
|
| 608 |
+
logger.info("Menghasilkan visualisasi Distribusi Temporal (DAY-1 vs DAY-2)...")
|
| 609 |
+
visualization.plot_temporal_distribution(
|
| 610 |
+
df_report,
|
| 611 |
+
save_name="temporal_cluster_distribution.png"
|
| 612 |
+
)
|
| 613 |
+
logger.info("Grafik distribusi klaster otonom tersimpan di direktori figures.")
|
| 614 |
+
# --- [AKHIR INJEKSI KODE BARU] ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 616 |
|
| 617 |
+
# ---------------------------------------------------------------------------
|
| 618 |
+
# TAHAP 8: PEMBUATAN MODEL PRODUKSI (DEPLOYMENT)
|
| 619 |
+
# ---------------------------------------------------------------------------
|
| 620 |
def step_build_production_model():
|
| 621 |
+
logger.info("=== TAHAP 8: MERAKIT MODEL PRODUKSI ===")
|
| 622 |
|
| 623 |
+
# 1. Tentukan parameter terbaik dari hasil eksperimenmu
|
| 624 |
+
# 1. Membaca parameter terbaik dari hasil Deep Search secara otomatis
|
| 625 |
logger.info("Mengambil konfigurasi terbaik dari file CSV...")
|
| 626 |
tuning_csv_path = os.path.join(config.TABLES_DIR, "tuning_results_deep_search.csv")
|
| 627 |
|
|
|
|
| 651 |
codebook = codebooks[best_codebook_size]
|
| 652 |
filenames = list(descriptors_dict.keys())
|
| 653 |
|
| 654 |
+
# 2. Buat ulang scaler warna HSV (MinMax lalu L2-normalize per baris -
|
| 655 |
+
# HARUS sama persis dengan step_bovw, atau model produksi tidak konsisten
|
| 656 |
+
# dengan hasil eksperimen yang dilaporkan)
|
|
|
|
| 657 |
logger.info("Melatih ulang MinMaxScaler...")
|
| 658 |
raw_hsv_values = list(hsv_dict.values())
|
| 659 |
scaler = MinMaxScaler()
|
|
|
|
| 685 |
logger.info("Melatih GMM Classifier...")
|
| 686 |
gmm = GaussianMixture(n_components=2, covariance_type=best_gmm_covariance,
|
| 687 |
random_state=config.RANDOM_STATE, n_init=10)
|
| 688 |
+
gmm.fit(X_pca) # Kita memanggil .fit() untuk menyimpan statenya
|
| 689 |
|
| 690 |
# 6. BUNGKUS MENJADI SATU FILE MODEL
|
| 691 |
model_package = {
|
|
|
|
| 707 |
logger.info("Model Produksi berhasil disimpan di: %s", model_path)
|
| 708 |
logger.info("Model ini siap dimuat (di-load) untuk aplikasi klasifikasi otomatis tahun depan!")
|
| 709 |
|
| 710 |
+
# ---------------------------------------------------------------------------
|
| 711 |
# ENTRY POINT
|
| 712 |
+
# ---------------------------------------------------------------------------
|
| 713 |
STEPS = {
|
| 714 |
"preprocess": step_preprocess,
|
| 715 |
"extract": step_extract,
|
|
|
|
| 719 |
"tune": step_tune,
|
| 720 |
"export": step_export,
|
| 721 |
"build_model": step_build_production_model,
|
|
|
|
| 722 |
}
|
| 723 |
|
| 724 |
def main():
|
src/bovw.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Bags of Visual Words
|
| 2 |
import numpy as np
|
| 3 |
from sklearn.cluster import MiniBatchKMeans
|
| 4 |
|
|
@@ -8,7 +8,8 @@ import config
|
|
| 8 |
def sample_descriptors_for_codebook(all_descriptors_list, max_total, random_state):
|
| 9 |
"""
|
| 10 |
Menggabungkan descriptor dari seluruh citra lalu men-subsample hingga
|
| 11 |
-
maksimum `max_total` baris agar pelatihan codebook tetap efisien
|
|
|
|
| 12 |
"""
|
| 13 |
stacked = np.vstack(
|
| 14 |
[d for d in all_descriptors_list if d is not None and len(d) > 0]
|
|
@@ -58,7 +59,7 @@ def compute_histogram(descriptors, codebook, normalize="hellinger"):
|
|
| 58 |
if total > 0:
|
| 59 |
histogram = histogram / total
|
| 60 |
elif normalize == "hellinger":
|
| 61 |
-
# Transformasi Root-BoVW
|
| 62 |
total = histogram.sum()
|
| 63 |
if total > 0:
|
| 64 |
histogram = histogram / total
|
|
|
|
| 1 |
+
"""Bags of Visual Words: pembentukan codebook (MiniBatchKMeans) & histogram fitur."""
|
| 2 |
import numpy as np
|
| 3 |
from sklearn.cluster import MiniBatchKMeans
|
| 4 |
|
|
|
|
| 8 |
def sample_descriptors_for_codebook(all_descriptors_list, max_total, random_state):
|
| 9 |
"""
|
| 10 |
Menggabungkan descriptor dari seluruh citra lalu men-subsample hingga
|
| 11 |
+
maksimum `max_total` baris agar pelatihan codebook tetap efisien memori
|
| 12 |
+
untuk dataset beribu-ribu citra.
|
| 13 |
"""
|
| 14 |
stacked = np.vstack(
|
| 15 |
[d for d in all_descriptors_list if d is not None and len(d) > 0]
|
|
|
|
| 59 |
if total > 0:
|
| 60 |
histogram = histogram / total
|
| 61 |
elif normalize == "hellinger":
|
| 62 |
+
# Transformasi Root-BoVW (Sangat superior untuk deteksi tekstur)
|
| 63 |
total = histogram.sum()
|
| 64 |
if total > 0:
|
| 65 |
histogram = histogram / total
|
src/clustering.py
CHANGED
|
@@ -28,7 +28,8 @@ def run_gmm(X, k=None, random_state=None):
|
|
| 28 |
labels = model.fit_predict(X)
|
| 29 |
return labels, model
|
| 30 |
|
| 31 |
-
#
|
|
|
|
| 32 |
CLUSTERING_METHODS = {
|
| 33 |
"kmeans": run_kmeans,
|
| 34 |
"gmm": run_gmm,
|
|
|
|
| 28 |
labels = model.fit_predict(X)
|
| 29 |
return labels, model
|
| 30 |
|
| 31 |
+
# Pipeline utama akan membaca dictionary ini secara dinamis
|
| 32 |
+
# Sekarang hanya terdaftar dua algoritma
|
| 33 |
CLUSTERING_METHODS = {
|
| 34 |
"kmeans": run_kmeans,
|
| 35 |
"gmm": run_gmm,
|
src/evaluation.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
"""Komparasi SIFT vs SURF: efisiensi komputasi & kualitas separabilitas klaster."""
|
| 2 |
import os
|
|
|
|
| 3 |
import pandas as pd
|
|
|
|
| 4 |
import config
|
| 5 |
|
| 6 |
|
|
@@ -38,7 +40,7 @@ def summarize_efficiency(df_efficiency):
|
|
| 38 |
def build_cluster_metrics_table(results):
|
| 39 |
"""
|
| 40 |
results: list of dict, masing-masing hasil satu kombinasi
|
| 41 |
-
{feature: "SIFT"/"SURF", method: "kmeans"/"gmm",
|
| 42 |
codebook_size: int, **internal_metrics, **external_metrics(optional)}
|
| 43 |
"""
|
| 44 |
return pd.DataFrame(results)
|
|
|
|
| 1 |
"""Komparasi SIFT vs SURF: efisiensi komputasi & kualitas separabilitas klaster."""
|
| 2 |
import os
|
| 3 |
+
|
| 4 |
import pandas as pd
|
| 5 |
+
|
| 6 |
import config
|
| 7 |
|
| 8 |
|
|
|
|
| 40 |
def build_cluster_metrics_table(results):
|
| 41 |
"""
|
| 42 |
results: list of dict, masing-masing hasil satu kombinasi
|
| 43 |
+
{feature: "SIFT"/"SURF", method: "kmeans"/"gmm"/"agglomerative",
|
| 44 |
codebook_size: int, **internal_metrics, **external_metrics(optional)}
|
| 45 |
"""
|
| 46 |
return pd.DataFrame(results)
|
src/feature_extraction.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
| 1 |
"""
|
| 2 |
-
Ekstraksi fitur SIFT & SURF, serta momen HSV, dibatasi pada area ROI daging (mask).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
"""
|
| 4 |
import time
|
| 5 |
import cv2
|
|
|
|
| 1 |
"""
|
| 2 |
+
Ekstraksi fitur lokal SIFT & SURF, serta momen HSV, dibatasi pada area ROI daging (mask).
|
| 3 |
+
|
| 4 |
+
Membutuhkan opencv-contrib-python versi yang dikompilasi dengan
|
| 5 |
+
OPENCV_ENABLE_NONFREE=ON (mis. 3.4.2.16) agar cv2.xfeatures2d.SIFT_create()
|
| 6 |
+
dan cv2.xfeatures2d.SURF_create() tersedia.
|
| 7 |
"""
|
| 8 |
import time
|
| 9 |
import cv2
|
src/preprocessing.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Preprocessing: resize, denoising, segmentasi ROI, QA visual, dan Bounding Box Crop."""
|
| 2 |
import os
|
| 3 |
import cv2
|
| 4 |
import numpy as np
|
|
@@ -32,19 +32,20 @@ def crop_to_bounding_box(image, mask, pad_ratio=0.01):
|
|
| 32 |
Mengeliminasi redundansi dimensi latar belakang untuk menjaga kemurnian
|
| 33 |
ekstraksi fitur spasial (SURF) dan momen statistik warna (HSV).
|
| 34 |
"""
|
| 35 |
-
#1.Ekstraksi koordinat absolut dari piksel foreground murni
|
| 36 |
coords = cv2.findNonZero(mask)
|
| 37 |
|
| 38 |
# Fallback jika mask secara anomali kosong
|
| 39 |
if coords is None:
|
| 40 |
return image, mask
|
| 41 |
|
| 42 |
-
#2.Kalkulasi bounding box yang presisi mengelilingi piksel aktif
|
| 43 |
x, y, w, h_rect = cv2.boundingRect(coords)
|
| 44 |
|
| 45 |
H, W = image.shape[:2]
|
| 46 |
|
| 47 |
-
#3.Margin ketat (1% dari dimensi objek)
|
|
|
|
| 48 |
pad_x, pad_y = int(w * pad_ratio), int(h_rect * pad_ratio)
|
| 49 |
|
| 50 |
x1, y1 = max(0, x - pad_x), max(0, y - pad_y)
|
|
|
|
| 1 |
+
"""Preprocessing citra: resize, denoising, segmentasi ROI, QA visual, dan Bounding Box Crop."""
|
| 2 |
import os
|
| 3 |
import cv2
|
| 4 |
import numpy as np
|
|
|
|
| 32 |
Mengeliminasi redundansi dimensi latar belakang untuk menjaga kemurnian
|
| 33 |
ekstraksi fitur spasial (SURF) dan momen statistik warna (HSV).
|
| 34 |
"""
|
| 35 |
+
# 1. Ekstraksi koordinat absolut dari piksel foreground murni
|
| 36 |
coords = cv2.findNonZero(mask)
|
| 37 |
|
| 38 |
# Fallback jika mask secara anomali kosong
|
| 39 |
if coords is None:
|
| 40 |
return image, mask
|
| 41 |
|
| 42 |
+
# 2. Kalkulasi bounding box yang presisi mengelilingi piksel aktif
|
| 43 |
x, y, w, h_rect = cv2.boundingRect(coords)
|
| 44 |
|
| 45 |
H, W = image.shape[:2]
|
| 46 |
|
| 47 |
+
# 3. Margin sangat ketat (1% dari dimensi objek)
|
| 48 |
+
# untuk menyisakan sedikit ruang agar detektor tepi SURF tidak terpotong
|
| 49 |
pad_x, pad_y = int(w * pad_ratio), int(h_rect * pad_ratio)
|
| 50 |
|
| 51 |
x1, y1 = max(0, x - pad_x), max(0, y - pad_y)
|
src/segmentation.py
CHANGED
|
@@ -13,11 +13,11 @@ def segment_meat_roi(image_bgr, min_area_ratio=0.015):
|
|
| 13 |
h, w = image_bgr.shape[:2]
|
| 14 |
total_area = float(h * w)
|
| 15 |
|
| 16 |
-
# 1.Konversi ke HSV dengan reduksi blur untuk menjaga ketegasan batas tepi
|
| 17 |
blurred = cv2.GaussianBlur(image_bgr, (7, 7), 0)
|
| 18 |
hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)
|
| 19 |
|
| 20 |
-
# 2.Strict Spectrum Isolation
|
| 21 |
# S > 50 dan V > 40 membuang warna netral (hitam/abu-abu latar & putih stiker)
|
| 22 |
# H: 0-20 & 160-180 murni mengunci pigmen mioglobin (merah/pink/cokelat)
|
| 23 |
mask1 = cv2.inRange(hsv, np.array([0, 30, 25]), np.array([30, 255, 255]))
|
|
|
|
| 13 |
h, w = image_bgr.shape[:2]
|
| 14 |
total_area = float(h * w)
|
| 15 |
|
| 16 |
+
# 1. Konversi ke HSV dengan reduksi blur untuk menjaga ketegasan batas tepi
|
| 17 |
blurred = cv2.GaussianBlur(image_bgr, (7, 7), 0)
|
| 18 |
hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)
|
| 19 |
|
| 20 |
+
# 2. Strict Spectrum Isolation
|
| 21 |
# S > 50 dan V > 40 membuang warna netral (hitam/abu-abu latar & putih stiker)
|
| 22 |
# H: 0-20 & 160-180 murni mengunci pigmen mioglobin (merah/pink/cokelat)
|
| 23 |
mask1 = cv2.inRange(hsv, np.array([0, 30, 25]), np.array([30, 255, 255]))
|
src/utils.py
CHANGED
|
@@ -15,7 +15,7 @@ def get_logger(name="pipeline"):
|
|
| 15 |
os.makedirs(config.LOGS_DIR, exist_ok=True)
|
| 16 |
logger = logging.getLogger(name)
|
| 17 |
if logger.handlers:
|
| 18 |
-
return logger
|
| 19 |
|
| 20 |
logger.setLevel(getattr(logging, config.LOG_LEVEL, logging.INFO))
|
| 21 |
fmt = logging.Formatter(
|
|
|
|
| 15 |
os.makedirs(config.LOGS_DIR, exist_ok=True)
|
| 16 |
logger = logging.getLogger(name)
|
| 17 |
if logger.handlers:
|
| 18 |
+
return logger # sudah dikonfigurasi sebelumnya, hindari duplikasi handler
|
| 19 |
|
| 20 |
logger.setLevel(getattr(logging, config.LOG_LEVEL, logging.INFO))
|
| 21 |
fmt = logging.Formatter(
|
src/visualization.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
Visualisasi hasil: reduksi dimensi, evaluasi metrik, dan analisis distribusi.
|
|
|
|
|
|
|
| 3 |
"""
|
| 4 |
import os
|
| 5 |
import matplotlib
|
| 6 |
-
matplotlib.use("Agg")
|
| 7 |
import matplotlib.pyplot as plt
|
| 8 |
import numpy as np
|
| 9 |
import pandas as pd
|
|
@@ -17,7 +19,7 @@ from sklearn.preprocessing import MinMaxScaler
|
|
| 17 |
import config
|
| 18 |
|
| 19 |
def set_academic_style():
|
| 20 |
-
"""Mengatur parameter global Matplotlib"""
|
| 21 |
plt.rcParams.update({
|
| 22 |
"font.family": "serif",
|
| 23 |
"font.serif": ["Times New Roman", "DejaVu Serif"],
|
|
@@ -37,18 +39,19 @@ def set_academic_style():
|
|
| 37 |
"axes.spines.right": False
|
| 38 |
})
|
| 39 |
|
| 40 |
-
# Panggil styling
|
| 41 |
set_academic_style()
|
| 42 |
|
| 43 |
|
| 44 |
def plot_embedding_2d(X, labels, method_name, save_name, title=None):
|
| 45 |
-
"""Plot reduksi dimensi PCA"""
|
| 46 |
os.makedirs(config.FIGURES_DIR, exist_ok=True)
|
| 47 |
pca = PCA(n_components=2, random_state=config.RANDOM_STATE)
|
| 48 |
embedding = pca.fit_transform(X)
|
| 49 |
|
| 50 |
fig, ax = plt.subplots(figsize=(7, 6))
|
| 51 |
|
|
|
|
| 52 |
scatter = ax.scatter(
|
| 53 |
embedding[:, 0], embedding[:, 1], c=labels, cmap="Set1",
|
| 54 |
s=25, alpha=0.8, edgecolors="white", linewidth=0.5
|
|
@@ -325,7 +328,7 @@ def plot_overall_radar_comparison(df_metrics, df_efficiency, save_name="overall_
|
|
| 325 |
]
|
| 326 |
|
| 327 |
# 4. Normalisasi Min-Max (0-1) agar bisa di-plot di sumbu yang sama
|
| 328 |
-
# Untuk DB dan Time, balik (invers) sebelum dinormalisasi agar nilai tertinggi = terbaik
|
| 329 |
raw_data = np.array([sift_raw, surf_raw])
|
| 330 |
|
| 331 |
# Inversi kolom DB (indeks 3) dan Time (indeks 4)
|
|
|
|
| 1 |
"""
|
| 2 |
Visualisasi hasil: reduksi dimensi, evaluasi metrik, dan analisis distribusi.
|
| 3 |
+
Standar output disesuaikan untuk publikasi akademik (SINTA/Scopus) dengan
|
| 4 |
+
resolusi 300 DPI dan gaya minimalis-kontras tinggi.
|
| 5 |
"""
|
| 6 |
import os
|
| 7 |
import matplotlib
|
| 8 |
+
matplotlib.use("Agg") # Headless mode
|
| 9 |
import matplotlib.pyplot as plt
|
| 10 |
import numpy as np
|
| 11 |
import pandas as pd
|
|
|
|
| 19 |
import config
|
| 20 |
|
| 21 |
def set_academic_style():
|
| 22 |
+
"""Mengatur parameter global Matplotlib untuk standar jurnal akademik."""
|
| 23 |
plt.rcParams.update({
|
| 24 |
"font.family": "serif",
|
| 25 |
"font.serif": ["Times New Roman", "DejaVu Serif"],
|
|
|
|
| 39 |
"axes.spines.right": False
|
| 40 |
})
|
| 41 |
|
| 42 |
+
# Panggil styling di awal
|
| 43 |
set_academic_style()
|
| 44 |
|
| 45 |
|
| 46 |
def plot_embedding_2d(X, labels, method_name, save_name, title=None):
|
| 47 |
+
"""Plot reduksi dimensi PCA dengan standar visual akademik."""
|
| 48 |
os.makedirs(config.FIGURES_DIR, exist_ok=True)
|
| 49 |
pca = PCA(n_components=2, random_state=config.RANDOM_STATE)
|
| 50 |
embedding = pca.fit_transform(X)
|
| 51 |
|
| 52 |
fig, ax = plt.subplots(figsize=(7, 6))
|
| 53 |
|
| 54 |
+
# Menggunakan colormap yang ramah buta warna dan elegan
|
| 55 |
scatter = ax.scatter(
|
| 56 |
embedding[:, 0], embedding[:, 1], c=labels, cmap="Set1",
|
| 57 |
s=25, alpha=0.8, edgecolors="white", linewidth=0.5
|
|
|
|
| 328 |
]
|
| 329 |
|
| 330 |
# 4. Normalisasi Min-Max (0-1) agar bisa di-plot di sumbu yang sama
|
| 331 |
+
# Untuk DB dan Time, kita balik (invers) sebelum dinormalisasi agar nilai tertinggi = terbaik
|
| 332 |
raw_data = np.array([sift_raw, surf_raw])
|
| 333 |
|
| 334 |
# Inversi kolom DB (indeks 3) dan Time (indeks 4)
|