#!/usr/bin/env python3 """ train_from_icml.py ================== Extracts features DIRECTLY from raw ICML folder images and retrains the SVM. Folder labeling (by name): Contains 'post' (any case) → Post_Defoliation Contains 'pre' (any case) → Pre_Defoliation Features used (overflow-safe): Std_ExG – std of Excess Green Index (high = leafy canopy) Log_RBR – log(1 + mean(R/B)) (safe, was overflowing to 500 000+) Mean_B – mean Blue channel (POST fields have higher blue) ExG_pos_frac – fraction of pixels where ExG > 0 (strong leaf indicator) """ import os, sys, warnings import numpy as np import pandas as pd import joblib import cv2 from skimage.feature import graycomatrix, graycoprops from skimage.measure import shannon_entropy from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.metrics import classification_report, confusion_matrix warnings.filterwarnings("ignore") ICML_ROOT = '/Volumes/T9/ICML' HERE = os.path.dirname(os.path.abspath(__file__)) OUT_PKL = os.path.join(HERE, 'model.pkl') QML_FEATURES = ['Std_ExG', 'Log_RBR', 'Mean_B', 'ExG_pos_frac'] VALID_EXT = {'.jpg', '.jpeg', '.png', '.tif', '.tiff', '.JPG', '.JPEG', '.PNG'} LIMIT_PER_FOLDER = None # set to e.g. 200 for quick test def label_from_folder(folder_name: str) -> str | None: fn = folder_name.lower() if 'post' in fn: return 'Post_Defoliation' if 'pre' in fn: return 'Pre_Defoliation' return None def extract_features(img_bgr: np.ndarray) -> dict | None: """Safe feature extraction matching app.py's extract_features().""" try: img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB).astype(float) / 255.0 R, G, B = img_rgb[:,:,0], img_rgb[:,:,1], img_rgb[:,:,2] ExG = 2*G - R - B RBR = R / (B + 1e-6) img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) h, w = img_gray.shape if h > 512 or w > 512: img_gray = cv2.resize(img_gray, (min(w, 512), min(h, 512))) glcm = graycomatrix(img_gray, distances=[1], angles=[0], levels=256, symmetric=True, normed=True) return { 'Std_ExG': float(np.std(ExG)), 'Log_RBR': float(np.log1p(np.mean(RBR))), 'Mean_B': float(np.mean(B)), 'ExG_pos_frac': float(np.mean(ExG > 0)), # extras stored for analysis but not used in SVM 'Mean_ExG': float(np.mean(ExG)), 'Mean_G': float(np.mean(G)), 'Mean_R': float(np.mean(R)), 'Correlation': float(graycoprops(glcm, 'correlation')[0, 0]), } except Exception as e: return None def load_dataset(): records = [] for folder in sorted(os.listdir(ICML_ROOT)): folder_path = os.path.join(ICML_ROOT, folder) if not os.path.isdir(folder_path): continue label = label_from_folder(folder) if label is None: print(f" [SKIP] {folder} — no pre/post in name") continue files = [f for f in os.listdir(folder_path) if os.path.splitext(f)[1] in VALID_EXT] if LIMIT_PER_FOLDER: files = files[:LIMIT_PER_FOLDER] print(f" [{label[:4].upper()}] {folder}: {len(files)} images") for fname in files: img = cv2.imread(os.path.join(folder_path, fname)) if img is None: continue feats = extract_features(img) if feats is None: continue feats['Label'] = label feats['Folder'] = folder records.append(feats) return pd.DataFrame(records) def main(): print("=" * 60) print(" Training from real ICML UAV images") print("=" * 60) print("\nScanning ICML folders …") df = load_dataset() if df.empty: print("ERROR: No data loaded. Check ICML_ROOT path.") sys.exit(1) print(f"\nLoaded {len(df)} images") print(df.groupby('Label').size().to_string()) # Feature distributions (quick sanity check) print("\n=== Feature Means by Class ===") for feat in QML_FEATURES: for label in ['Pre_Defoliation', 'Post_Defoliation']: sub = df[df['Label'] == label][feat] print(f" {label[:4]} {feat}: {sub.mean():.5f} ± {sub.std():.5f}") # Encode labels X = df[QML_FEATURES].values y = (df['Label'] == 'Pre_Defoliation').astype(int).values # 1=Pre, 0=Post # Build pipeline pipeline = Pipeline([ ('scaler', StandardScaler()), ('svm', SVC(kernel='rbf', C=10, gamma='scale', probability=True, random_state=42)) ]) # 5-fold CV print("\nRunning 5-fold stratified CV …") cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) accs = cross_val_score(pipeline, X, y, cv=cv, scoring='accuracy') print(f" CV Accuracy: {accs.mean():.4f} ± {accs.std():.4f}") # Train on full data print("\nFitting on full dataset …") pipeline.fit(X, y) preds = pipeline.predict(X) print("\n=== Classification Report (train set) ===") print(classification_report(y, preds, target_names=['Post_Defoliation', 'Pre_Defoliation'])) print("=== Confusion Matrix ===") cm = confusion_matrix(y, preds) print(f" Predicted Post Pre") print(f" True Post: {cm[0,0]:4d} {cm[0,1]:4d}") print(f" True Pre: {cm[1,0]:4d} {cm[1,1]:4d}") # Quick test on known images print("\n=== Quick generalisation check (unseen images) ===") tests = [ ('/Volumes/T9/ICML/Part_one_pre_def_rgb/DJI_20250929095743_0311_D.JPG', 'Pre_Defoliation'), ('/Volumes/T9/ICML/Post_def_rgb_part1/DJI_20250929124149_0029_D.JPG', 'Post_Defoliation'), ('/Volumes/T9/ICML/205_Post_Def_rgb/DJI_20250929124505_0127_D.JPG', 'Post_Defoliation'), ('/Volumes/T9/ICML/part 2_pre_def_rgb/DJI_20250929093936_0722_D.JPG', 'Pre_Defoliation'), ] for path, true_label in tests: img = cv2.imread(path) if img is None: print(f" [SKIP] {os.path.basename(path)}") continue f = extract_features(img) x = np.array([[f[k] for k in QML_FEATURES]]) probs = pipeline.predict_proba(x)[0] pred = 'Pre_Defoliation' if np.argmax(probs) == 1 else 'Post_Defoliation' ok = '✅' if pred == true_label else '❌' print(f" {ok} {os.path.basename(path)}") print(f" True={true_label[:4]} Pred={pred[:4]} " f"Post={probs[0]*100:.1f}% Pre={probs[1]*100:.1f}%") # Save joblib.dump(pipeline, OUT_PKL) print(f"\n✅ Model saved → {OUT_PKL}") if __name__ == '__main__': main()