quantum_Harvest / train_from_icml.py
Harshitha M
Create clean Hugging Face Space snapshot
25e2276
Raw
History Blame Contribute Delete
6.98 kB
#!/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()