create log experiment

#1
.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-publik/figures/
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
- archives-1/
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
- **Output:** `cluster_labels.pkl`, `cluster_metrics_all.csv`, `cluster_stability.csv`.
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
- - Python 3.7 (wajib — OpenCV 3.4.2.16 dengan SIFT/SURF tersedia hanya di Python 3.7)
374
- - [uv](https://github.com/astral-sh/uv) (package manager)
 
375
 
376
- ### Instalasi
377
 
378
  ```bash
379
- # Install Python 3.7 lewat uv (jika belum ada di mesin)
380
  uv python install 3.7
381
 
382
  # Buat & sync environment sesuai pyproject.toml
383
  uv sync
384
 
385
- # Verifikasi SIFT & SURF tersedia (harus mencetak True True)
386
  uv run python -c "import cv2; s=cv2.xfeatures2d.SIFT_create(); f=cv2.xfeatures2d.SURF_create(); print(True, True)"
387
  ```
388
 
389
- > **Catatan:** Jika `uv python install 3.7` gagal karena python-build-standalone tidak lagi menyediakan build 3.7 di platform Anda, gunakan pyenv/conda untuk menyediakan interpreter 3.7, lalu arahkan uv ke sana:
390
- > ```bash
391
- > uv venv --python /path/to/python3.7
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
- 1. Susun dataset sesuai konvensi penamaan (`DAY-1_[DOMAIN]_[KODE]_[NOMOR].JPG`).
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
- ### Konfigurasi
 
 
417
 
418
- Edit `config.py` sesuai kebutuhan:
 
 
 
419
 
420
- ```python
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
- ### Menjalankan Tahap per Tahap
 
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
- ```bash
444
  uv run python run_pipeline.py --step all
445
  ```
446
 
447
- > Perintah `--step all` menjalankan semua tahap kecuali `tune` (untuk menghindari grid search yang lama secara tidak sengaja).
448
 
449
- ### Tahap Opsional
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
 
451
- ```bash
452
- # Grid search hyperparameter (lama, ~75 kombinasi × step_bovw)
453
- uv run python run_pipeline.py --step tune
454
 
455
- # 14 skenario eksperimen batch dengan arsip otomatis
456
- uv run python run_pipeline.py --step batch_log
457
- ```
 
 
 
458
 
459
- ---
 
 
460
 
461
- ## 11. Struktur Proyek
 
 
 
462
 
463
- ```
464
- beefresearch-bima/
465
- ├── config.py # Konfigurasi sentral pipeline
466
- ├── run_pipeline.py # Orkestrator utama (semua step ada di sini)
467
- ├── pyproject.toml # Dependensi & metadata proyek
468
- ├── data/
469
- ├── dataset_root/ # Dataset mentah (diatur di config.py)
470
- └── labels.csv # Ground truth opsional
471
- ├── src/
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 utama.
 
3
  """
4
  import os
5
 
 
6
  # PATH DIREKTORI
 
7
  PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
8
 
9
- DATASET_DOMAIN = "DATA-BIMA" # Ubah sesuai domain dataset yang digunakan
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
- CODEBOOK_SIZES = [50, 100, 200]
 
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
- # HYPERPARAMETER TUNING OTOMATIS (NATIVE) diuji secara otomatis saat menjalankan --step tune
 
 
 
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,0,Klaster A
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,0,Klaster A
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,1,Klaster B
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,1,Klaster B
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,0,Klaster A
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.3273361027240753,1.3393923655280175,1275.8714948781799,SIFT,kmeans,50,raw
3
- 0.30371972918510437,1.4179403992073865,1155.3381185938072,SIFT,kmeans,100,raw
4
- 0.2761237323284149,1.519173743772955,1020.7523554216446,SIFT,kmeans,200,raw
5
- 0.3402327299118042,1.2915196253700496,1361.5232738374064,SIFT,kmeans,200,pca
6
- 0.3259091377258301,1.3138622678954146,1269.7095677771042,SIFT,gmm,200,pca
7
- 0.29011809825897217,1.4702439943859047,1079.6358132925886,SURF,kmeans,50,raw
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.3397314935922623,0.0024768677542831774,0.99006899750333,0.010907261921736588,10
3
- SURF,0.31878958344459535,0.006372579916181219,0.9385743852568675,0.15141701917397363,10
 
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.0036894152202715568,0.0035862605000147596,0.0016130202402580723,128.5969055374593,90.0,122.42829721038306,2456
3
- SURF,0.00276825313436675,0.0025465785001870245,0.0013585822474636924,56.10260586319218,37.0,60.453230961928284,2456
 
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.3402327299118042
6
  },
7
  "best_feature_overall_avg_silhouette": "SIFT",
8
  "avg_silhouette_per_feature": {
9
- "SIFT": 0.33307093381881714,
10
- "SURF": 0.305849090218544
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
- Code 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,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 - PREPROCESSING & SEGMENTASI ROI")
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 - EKSTRAKSI FITUR LOKAL (SIFT & SURF)")
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 - BAGS OF VISUAL WORDS & EARLY FUSION")
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 - CLUSTERING UNSUPERVISED")
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 - EVALUASI & KOMPARASI SIFT vs SURF")
 
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 untuk Komparasi Keseluruhan")
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 - HYPERPARAMETER TUNING (DEEP SEARCH)")
412
 
413
- # Tuning parameter baru untuk memaksimalkan Silhouette > 0.5
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 - EKSPOR TABEL PREDIKSI")
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
- if df_report['Hari Ke-'].isnull().all():
588
- logger.warning(
589
- "Dataset eksternal (publik) terdeteksi. Metadata temporal ('Hari Ke-') tidak "
590
- "tersedia. Pembuatan grafik Distribusi Temporal secara otomatis dilewati."
591
- )
592
- else:
593
- logger.info("Menghasilkan visualisasi Distribusi Temporal (DAY-1 vs DAY-2)...")
594
- visualization.plot_temporal_distribution(
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
- # TAHAP 8: PEMBUATAN MODEL PRODUKSI
 
 
748
  def step_build_production_model():
749
- logger.info("TAHAP 8 - MERAKIT MODEL PRODUKSI")
750
 
751
- # 1. Tentukan parameter terbaik dari hasil eksperimen
 
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
- """ 2. Buat ulang scaler warna HSV (MinMax lalu L2-normalize per baris -
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 - pembentukan codebook (MiniBatchKMeans) & histogram fitur."""
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
- #algoritma yang dipake
 
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) untuk menyisakan sedikit ruang agar detektor tepi SURF tidak terpotong
 
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)