import cv2 import numpy as np import os from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix import matplotlib.pyplot as plt import seaborn as sns import joblib from sklearn.preprocessing import LabelEncoder def extract_color_histogram(image, bins=(8, 8, 8)): """ Ekstraksi fitur Color Histogram HSV dari gambar Parameters: image: gambar dalam format BGR (OpenCV) bins: jumlah bin untuk setiap channel HSV Returns: features: array fitur histogram yang telah dinormalisasi """ # Konversi dari BGR ke HSV image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # Hitung dan normalisasi histogram untuk setiap channel HSV features = [] for i in range(3): hist = cv2.calcHist([image_hsv], [i], None, [bins[i]], [0, 256]) hist = cv2.normalize(hist, hist).flatten() features.append(hist) # Gabungkan semua histogram features = np.concatenate(features) return features def plot_confusion_matrix(y_true, y_pred, class_names, save_path='confusion_matrix.png'): """ Menampilkan confusion matrix dalam bentuk heatmap dan menyimpannya sebagai file PNG Parameters: y_true: label aktual y_pred: label prediksi class_names: list nama kelas save_path: path untuk menyimpan file gambar """ # Hitung confusion matrix cm = confusion_matrix(y_true, y_pred) # Buat figure plt.figure(figsize=(10, 8)) # Buat heatmap dengan seaborn sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names, cbar_kws={'label': 'Jumlah Sampel'}, square=True, linewidths=0.5, annot_kws={'size': 12, 'weight': 'bold'}) # Set title dan labels plt.title('Confusion Matrix - Klasifikasi Tingkat Kematangan Tomat', fontsize=16, fontweight='bold', pad=20) plt.xlabel('Kelas Prediksi', fontsize=12, fontweight='bold') plt.ylabel('Kelas Aktual', fontsize=12, fontweight='bold') # Rotate labels untuk better readability plt.xticks(rotation=45, ha='right') plt.yticks(rotation=0) # Add text summary total_samples = len(y_true) accuracy = np.mean(y_true == y_pred) * 100 plt.figtext(0.5, 0.02, f'Total Sampel: {total_samples} | Akurasi: {accuracy:.2f}%', ha='center', fontsize=11, style='italic') # Adjust layout plt.tight_layout() # Save sebagai PNG dengan high quality plt.savefig(save_path, dpi=300, bbox_inches='tight', facecolor='white') # Tampilkan plot plt.show() # Print summary print(f"\nConfusion Matrix telah disimpan sebagai: {save_path}") print(f"Ukuran gambar: 300 DPI") return cm def load_dataset(dataset_path): """ Membaca seluruh gambar dari folder dataset dan melakukan ekstraksi fitur Parameters: dataset_path: path ke folder dataset Returns: features: array fitur dari semua gambar labels: array label dari semua gambar class_names: nama kelas """ features = [] labels = [] class_names = [] # Dapatkan semua folder kelas classes = [d for d in os.listdir(dataset_path) if os.path.isdir(os.path.join(dataset_path, d))] classes.sort() # Urutkan untuk konsistensi print(f"Ditemukan {len(classes)} kelas: {classes}") for class_idx, class_name in enumerate(classes): class_path = os.path.join(dataset_path, class_name) class_names.append(class_name) # Dapatkan semua file gambar dalam folder kelas image_files = [f for f in os.listdir(class_path) if f.lower().endswith(('.png', '.jpg', '.jpeg'))] print(f"Memproses kelas '{class_name}': {len(image_files)} gambar") for image_file in image_files: image_path = os.path.join(class_path, image_file) try: # Baca gambar image = cv2.imread(image_path) if image is None: print(f"Warning: Tidak dapat membaca gambar {image_path}") continue # Ekstraksi fitur feature = extract_color_histogram(image) features.append(feature) labels.append(class_idx) except Exception as e: print(f"Error memproses {image_path}: {e}") continue return np.array(features), np.array(labels), class_names def main(): """ Fungsi utama untuk menjalankan proses klasifikasi tingkat kematangan tomat """ print("=" * 60) print("KLASIFIKASI TINGKAT KEMATANGAN TOMAT") print("Menggunakan Random Forest dan Color Histogram HSV") print("=" * 60) # Path ke dataset dataset_path = "." # Cek apakah folder dataset ada if not os.path.exists(dataset_path): print(f"Error: Folder '{dataset_path}' tidak ditemukan!") print("Pastikan folder dataset ada dengan struktur:") print("./") print(" matang/") print(" mentah/") print(" setengah_matang/") return # Load dataset dan ekstraksi fitur print("\n1. MEMBACA DATASET DAN EKSTRAKSI FITUR") print("-" * 40) features, labels, class_names = load_dataset(dataset_path) print(f"\nTotal gambar yang berhasil diproses: {len(features)}") print(f"Dimensi fitur per gambar: {features.shape[1]}") # Split dataset menjadi training dan testing (80:20) print("\n2. SPLIT DATASET") print("-" * 40) X_train, X_test, y_train, y_test = train_test_split( features, labels, test_size=0.2, random_state=42, stratify=labels ) # Tampilkan jumlah data training dan testing print(f"Jumlah data training: {len(X_train)}") print(f"Jumlah data testing: {len(X_test)}") print(f"Rasio training:testing = {len(X_train)/(len(X_train)+len(X_test)):.1f}:{len(X_test)/(len(X_train)+len(X_test)):.1f}") # Tampilkan distribusi kelas print("\nDistribusi kelas pada data training:") for i, class_name in enumerate(class_names): count = np.sum(y_train == i) print(f" {class_name}: {count} gambar") print("\nDistribusi kelas pada data testing:") for i, class_name in enumerate(class_names): count = np.sum(y_test == i) print(f" {class_name}: {count} gambar") # Training model Random Forest print("\n3. TRAINING MODEL RANDOM FOREST") print("-" * 40) # Inisialisasi model rf_model = RandomForestClassifier( n_estimators=100, random_state=42, max_depth=10 ) # Training model rf_model.fit(X_train, y_train) print("Model Random Forest telah selesai training!") # Evaluasi model print("\n4. EVALUASI MODEL") print("-" * 40) # Prediksi pada data testing y_pred = rf_model.predict(X_test) # Tampilkan classification report print("\nClassification Report:") print(classification_report(y_test, y_pred, target_names=class_names)) # Tampilkan confusion matrix dengan visualisasi yang lebih baik print("\nConfusion Matrix:") cm = plot_confusion_matrix(y_test, y_pred, class_names, 'confusion_matrix_tomat.png') print(cm) # Tampilkan akurasi accuracy = np.mean(y_pred == y_test) print(f"\nAkurasi model: {accuracy:.4f} ({accuracy*100:.2f}%)") # Feature importance print("\n5. FEATURE IMPORTANCE") print("-" * 40) # Dapatkan feature importance importances = rf_model.feature_importances_ # Kelompokkan berdasarkan channel RGB bins_per_channel = len(importances) // 3 r_importance = np.sum(importances[:bins_per_channel]) g_importance = np.sum(importances[bins_per_channel:2*bins_per_channel]) b_importance = np.sum(importances[2*bins_per_channel:]) print(f"Importance channel R: {r_importance:.4f} ({r_importance*100:.2f}%)") print(f"Importance channel G: {g_importance:.4f} ({g_importance*100:.2f}%)") print(f"Importance channel B: {b_importance:.4f} ({b_importance*100:.2f}%)") # Simpan model print("\n6. MENYIMPAN MODEL") print("-" * 40) # Buat folder models jika belum ada models_folder = "models" if not os.path.exists(models_folder): os.makedirs(models_folder) print(f"Folder '{models_folder}' dibuat") # Buat label encoder label_encoder = LabelEncoder() label_encoder.fit(class_names) # Simpan model try: model_file = os.path.join(models_folder, "tomat_classifier.pkl") joblib.dump(rf_model, model_file) print(f"Model berhasil disimpan: {model_file}") # Simpan label encoder encoder_file = os.path.join(models_folder, "label_encoder.pkl") joblib.dump(label_encoder, encoder_file) print(f"Label encoder berhasil disimpan: {encoder_file}") # Simpan metadata metadata = { 'model_type': 'RandomForestClassifier', 'n_estimators': 100, 'max_depth': 10, 'n_features': features.shape[1], 'classes': class_names, 'accuracy': accuracy, 'histogram_bins': (8, 8, 8) } metadata_file = os.path.join(models_folder, "metadata.pkl") joblib.dump(metadata, metadata_file) print(f"Metadata berhasil disimpan: {metadata_file}") except Exception as e: print(f"Error menyimpan model: {e}") print("\n" + "=" * 60) print("PROSES KLASIFIKASI SELESAI!") print("=" * 60) if __name__ == "__main__": main()