Spaces:
Sleeping
Sleeping
| """ | |
| ============================================================================= | |
| AgriShield — Model Setup Script | |
| ============================================================================= | |
| This script does two things: | |
| 1. COPIES pre-trained DL models (CNN .h5 files from AgriGo sample project) | |
| into backend/models/DL_models/ | |
| 2. RETRAINS ML models (crop & fertilizer recommenders) from the CSV datasets | |
| in sample/AgriGo/AgriGo/dataset/ and saves them to backend/models/ML_models/ | |
| Optionally it can also RETRAIN the DL models from scratch using the augmented | |
| PlantVillage dataset (takes 30-90 min per crop on CPU / 5-15 min on GPU). | |
| Run: | |
| python setup_models.py # copy DL + retrain ML | |
| python setup_models.py --retrain # also retrain all DL models from scratch | |
| python setup_models.py --crop tomato --retrain # retrain one specific crop | |
| ============================================================================= | |
| """ | |
| import os | |
| import sys | |
| import shutil | |
| import pickle | |
| import argparse | |
| import numpy as np | |
| # ── Paths ────────────────────────────────────────────────────────────────── | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| WORKSPACE_DIR = os.path.dirname(BASE_DIR) | |
| AGRIGO_DIR = os.path.join(WORKSPACE_DIR, "sample", "AgriGo", "AgriGo") | |
| SRC_DL_DIR = os.path.join(AGRIGO_DIR, "models", "DL_models") | |
| SRC_ML_DIR = os.path.join(AGRIGO_DIR, "models", "ML_models") | |
| SRC_DATASET = os.path.join(AGRIGO_DIR, "dataset") | |
| # Primary augmented image dataset | |
| AUGMENTED_DATASET_TRAIN = os.path.join( | |
| WORKSPACE_DIR, "datasets", | |
| "New Plant Diseases Dataset(Augmented)", | |
| "New Plant Diseases Dataset(Augmented)", "train" | |
| ) | |
| AUGMENTED_DATASET_VALID = os.path.join( | |
| WORKSPACE_DIR, "datasets", | |
| "New Plant Diseases Dataset(Augmented)", | |
| "New Plant Diseases Dataset(Augmented)", "valid" | |
| ) | |
| # Destination — backend/models/ | |
| DST_MODELS_DIR = os.path.join(BASE_DIR, "models") | |
| DST_DL_DIR = os.path.join(DST_MODELS_DIR, "DL_models") | |
| DST_ML_DIR = os.path.join(DST_MODELS_DIR, "ML_models") | |
| os.makedirs(DST_DL_DIR, exist_ok=True) | |
| os.makedirs(DST_ML_DIR, exist_ok=True) | |
| # ── Crop → class mapping (matches main.py exactly) ───────────────────────── | |
| CROP_CLASS_MAP = { | |
| "apple": ["Apple___Apple_scab", "Apple___Black_rot", | |
| "Apple___Cedar_apple_rust", "Apple___healthy"], | |
| "cherry": ["Cherry_(including_sour)___Powdery_mildew", | |
| "Cherry_(including_sour)___healthy"], | |
| "corn": ["Corn_(maize)___Cercospora_leaf_spot Gray_leaf_spot", | |
| "Corn_(maize)___Common_rust_", | |
| "Corn_(maize)___Northern_Leaf_Blight", | |
| "Corn_(maize)___healthy"], | |
| "grape": ["Grape___Black_rot", "Grape___Esca_(Black_Measles)", | |
| "Grape___Leaf_blight_(Isariopsis_Leaf_Spot)", "Grape___healthy"], | |
| "patato": ["Potato___Early_blight", "Potato___Late_blight", "Potato___healthy"], | |
| "peach": ["Peach___Bacterial_spot", "Peach___healthy"], | |
| "pepper": ["Pepper,_bell___Bacterial_spot", "Pepper,_bell___healthy"], | |
| "strawberry": ["Strawberry___Leaf_scorch", "Strawberry___healthy"], | |
| "tomato": ["Tomato___Bacterial_spot", "Tomato___Early_blight", | |
| "Tomato___Late_blight", "Tomato___Leaf_Mold", | |
| "Tomato___Septoria_leaf_spot", | |
| "Tomato___Spider_mites Two-spotted_spider_mite", | |
| "Tomato___Target_Spot", | |
| "Tomato___Tomato_Yellow_Leaf_Curl_Virus", | |
| "Tomato___Tomato_mosaic_virus", "Tomato___healthy"], | |
| } | |
| ALL_CROPS = list(CROP_CLASS_MAP.keys()) | |
| # ============================================================================= | |
| # STEP 1 — Copy pre-trained DL models from AgriGo sample | |
| # ============================================================================= | |
| def copy_pretrained_dl_models(target_crops=None): | |
| """ | |
| Copies the pre-trained .h5 CNN models that ship with the AgriGo sample | |
| project into backend/models/DL_models/. | |
| These models were trained on PlantVillage with 224×224 input and achieve | |
| ~95% validation accuracy. | |
| """ | |
| print("\n" + "="*60) | |
| print(" STEP 1: Copying pre-trained DL models from AgriGo sample") | |
| print("="*60) | |
| if not os.path.isdir(SRC_DL_DIR): | |
| print(f"[ERROR] Source DL model directory not found:\n {SRC_DL_DIR}") | |
| return False | |
| crops_to_copy = target_crops if target_crops else ALL_CROPS | |
| copied = 0 | |
| for crop in crops_to_copy: | |
| src = os.path.join(SRC_DL_DIR, f"{crop}_model.h5") | |
| dst = os.path.join(DST_DL_DIR, f"{crop}_model.h5") | |
| if os.path.isfile(src): | |
| shutil.copy2(src, dst) | |
| size_mb = os.path.getsize(dst) / (1024 * 1024) | |
| print(f" ✓ {crop}_model.h5 ({size_mb:.1f} MB)") | |
| copied += 1 | |
| else: | |
| print(f" ✗ {crop}_model.h5 — NOT FOUND at {src}") | |
| print(f"\n Copied {copied}/{len(crops_to_copy)} DL models → {DST_DL_DIR}") | |
| return copied > 0 | |
| # ============================================================================= | |
| # STEP 2 — Retrain ML models (crop & fertilizer recommenders) | |
| # ============================================================================= | |
| def train_ml_models(): | |
| """ | |
| Retrains the crop recommendation and fertilizer recommendation | |
| Random Forest classifiers from the CSV datasets and saves them as | |
| .pkl files in backend/models/ML_models/. | |
| """ | |
| print("\n" + "="*60) | |
| print(" STEP 2: Training ML Recommendation Models") | |
| print("="*60) | |
| try: | |
| import pandas as pd | |
| from sklearn.preprocessing import StandardScaler | |
| from sklearn.ensemble import RandomForestClassifier | |
| from sklearn.model_selection import cross_val_score | |
| except ImportError as e: | |
| print(f"[ERROR] Missing Python package: {e}") | |
| print(" Run: pip install pandas scikit-learn") | |
| return False | |
| # ── 2a. Crop Recommender ───────────────────────────────────────────── | |
| print("\n [2a] Crop Recommender — loading dataset...") | |
| crop_csv = os.path.join(SRC_DATASET, "Crop_recommendation.csv") | |
| if not os.path.isfile(crop_csv): | |
| print(f" [ERROR] Not found: {crop_csv}") | |
| else: | |
| df = pd.read_csv(crop_csv) | |
| print(f" Loaded {len(df)} rows, columns: {list(df.columns)}") | |
| CROPS_LIST = [ | |
| 'apple', 'banana', 'blackgram', 'chickpea', 'coconut', 'coffee', | |
| 'cotton', 'grapes', 'jute', 'kidneybeans', 'lentil', 'maize', | |
| 'mango', 'mothbeans', 'mungbean', 'muskmelon', 'orange', 'papaya', | |
| 'pigeonpeas', 'pomegranate', 'rice', 'watermelon' | |
| ] | |
| X = df[['N', 'P', 'K', 'temperature', 'humidity', 'ph', 'rainfall']].values | |
| y = np.array([CROPS_LIST.index(c.lower().strip()) for c in df['label']]) | |
| scaler = StandardScaler() | |
| X_scaled = scaler.fit_transform(X) | |
| model = RandomForestClassifier( | |
| n_estimators=200, max_depth=None, | |
| min_samples_split=2, random_state=42, n_jobs=-1 | |
| ) | |
| model.fit(X_scaled, y) | |
| # Quick cross-val accuracy | |
| scores = cross_val_score(model, X_scaled, y, cv=5, scoring='accuracy') | |
| print(f" CV Accuracy: {scores.mean()*100:.2f}% ± {scores.std()*100:.2f}%") | |
| with open(os.path.join(DST_ML_DIR, "crop_scaler.pkl"), "wb") as f: | |
| pickle.dump(scaler, f) | |
| with open(os.path.join(DST_ML_DIR, "crop_model.pkl"), "wb") as f: | |
| pickle.dump(model, f) | |
| print(f" ✓ crop_model.pkl + crop_scaler.pkl → {DST_ML_DIR}") | |
| # ── 2b. Fertilizer Recommender ─────────────────────────────────────── | |
| print("\n [2b] Fertilizer Recommender — loading dataset...") | |
| fert_csv = os.path.join(SRC_DATASET, "Fertilizer Prediction.csv") | |
| if not os.path.isfile(fert_csv): | |
| print(f" [ERROR] Not found: {fert_csv}") | |
| else: | |
| df = pd.read_csv(fert_csv) | |
| df.columns = [c.strip() for c in df.columns] | |
| print(f" Loaded {len(df)} rows, columns: {list(df.columns)}") | |
| FERTILIZER_CLASSES = ['10-26-26', '14-35-14', '17-17-17', '20-20', '28-28', 'DAP', 'Urea'] | |
| SOIL_MAP = {'Black': 0, 'Clayey': 1, 'Loamy': 2, 'Red': 3, 'Sandy': 4} | |
| CROP_MAP = { | |
| 'Barley': 0, 'Cotton': 1, 'Ground Nuts': 2, 'Maize': 3, | |
| 'Millets': 4, 'Oil seeds': 5, 'Paddy': 6, 'Pulses': 7, | |
| 'Sugarcane': 8, 'Tobacco': 9, 'Wheat': 10 | |
| } | |
| X_num = df[['Temparature', 'Humidity', 'Moisture', | |
| 'Nitrogen', 'Phosphorous', 'Potassium']].values | |
| scaler = StandardScaler() | |
| X_num_scaled = scaler.fit_transform(X_num) | |
| soil_enc = np.array([SOIL_MAP[s.strip()] for s in df['Soil Type']]).reshape(-1, 1) | |
| crop_enc = np.array([CROP_MAP[c.strip()] for c in df['Crop Type']]).reshape(-1, 1) | |
| X_combined = np.concatenate([X_num_scaled, soil_enc, crop_enc], axis=1) | |
| y = np.array([FERTILIZER_CLASSES.index(f.strip()) for f in df['Fertilizer Name']]) | |
| model = RandomForestClassifier( | |
| n_estimators=200, max_depth=None, | |
| min_samples_split=2, random_state=42, n_jobs=-1 | |
| ) | |
| model.fit(X_combined, y) | |
| scores = cross_val_score(model, X_combined, y, cv=5, scoring='accuracy') | |
| print(f" CV Accuracy: {scores.mean()*100:.2f}% ± {scores.std()*100:.2f}%") | |
| with open(os.path.join(DST_ML_DIR, "fertilizer_scaler.pkl"), "wb") as f: | |
| pickle.dump(scaler, f) | |
| with open(os.path.join(DST_ML_DIR, "fertilizer_model.pkl"), "wb") as f: | |
| pickle.dump(model, f) | |
| print(f" ✓ fertilizer_model.pkl + fertilizer_scaler.pkl → {DST_ML_DIR}") | |
| return True | |
| # ============================================================================= | |
| # STEP 3 (OPTIONAL) — Retrain DL models from PlantVillage dataset | |
| # ============================================================================= | |
| def retrain_dl_model(crop_key: str): | |
| """ | |
| Trains a CNN (MobileNetV2 transfer learning) for a single crop from the | |
| augmented PlantVillage dataset. Input: 224×224, Output: per-crop softmax. | |
| Saves to backend/models/DL_models/{crop_key}_model.h5 | |
| """ | |
| print(f"\n [DL Retrain] Crop: {crop_key}") | |
| try: | |
| import tensorflow as tf | |
| except ImportError: | |
| print(" [ERROR] TensorFlow not installed. Run: pip install tensorflow") | |
| return False | |
| classes = CROP_CLASS_MAP[crop_key] | |
| num_classes = len(classes) | |
| IMG_SIZE = 224 | |
| BATCH = 32 | |
| EPOCHS = 15 | |
| # ── Build per-crop image generators from the augmented dataset ─────── | |
| if not os.path.isdir(AUGMENTED_DATASET_TRAIN): | |
| print(f" [ERROR] Dataset not found: {AUGMENTED_DATASET_TRAIN}") | |
| return False | |
| # Filter only the subdirs relevant to this crop | |
| train_dirs = [d for d in os.listdir(AUGMENTED_DATASET_TRAIN) if d in classes] | |
| if not train_dirs: | |
| print(f" [WARN] No matching class folders found in train dir for {crop_key}") | |
| print(f" Expected: {classes}") | |
| return False | |
| print(f" Classes found in dataset: {train_dirs}") | |
| # Create a temporary symlinked directory with only this crop's classes | |
| import tempfile, os | |
| tmp_train = tempfile.mkdtemp(prefix=f"dl_train_{crop_key}_") | |
| tmp_valid = tempfile.mkdtemp(prefix=f"dl_valid_{crop_key}_") | |
| try: | |
| for cls in classes: | |
| src_t = os.path.join(AUGMENTED_DATASET_TRAIN, cls) | |
| src_v = os.path.join(AUGMENTED_DATASET_VALID, cls) | |
| if os.path.isdir(src_t): | |
| # Use junctions on Windows to avoid copying gigabytes of data | |
| dst_t = os.path.join(tmp_train, cls) | |
| dst_v = os.path.join(tmp_valid, cls) | |
| try: | |
| os.symlink(src_t, dst_t) | |
| except (OSError, NotImplementedError): | |
| shutil.copytree(src_t, dst_t) | |
| if os.path.isdir(src_v): | |
| try: | |
| os.symlink(src_v, dst_v) | |
| except (OSError, NotImplementedError): | |
| shutil.copytree(src_v, dst_v) | |
| from tensorflow.keras.preprocessing.image import ImageDataGenerator | |
| from tensorflow.keras.applications import MobileNetV2 | |
| from tensorflow.keras import layers, models, callbacks | |
| datagen_train = ImageDataGenerator( | |
| rescale=1.0/255, | |
| rotation_range=15, | |
| width_shift_range=0.1, | |
| height_shift_range=0.1, | |
| horizontal_flip=True, | |
| zoom_range=0.1 | |
| ) | |
| datagen_valid = ImageDataGenerator(rescale=1.0/255) | |
| train_gen = datagen_train.flow_from_directory( | |
| tmp_train, target_size=(IMG_SIZE, IMG_SIZE), | |
| batch_size=BATCH, classes=classes, | |
| class_mode='categorical', shuffle=True | |
| ) | |
| valid_gen = datagen_valid.flow_from_directory( | |
| tmp_valid, target_size=(IMG_SIZE, IMG_SIZE), | |
| batch_size=BATCH, classes=classes, | |
| class_mode='categorical', shuffle=False | |
| ) | |
| if train_gen.samples == 0: | |
| print(f" [ERROR] No training images found for {crop_key}") | |
| return False | |
| print(f" Train samples: {train_gen.samples} | Valid samples: {valid_gen.samples}") | |
| # ── MobileNetV2 Transfer Learning ───────────────────────────────── | |
| base = MobileNetV2( | |
| input_shape=(IMG_SIZE, IMG_SIZE, 3), | |
| include_top=False, weights='imagenet' | |
| ) | |
| # Freeze base, train only top layers | |
| base.trainable = False | |
| model = models.Sequential([ | |
| base, | |
| layers.GlobalAveragePooling2D(), | |
| layers.BatchNormalization(), | |
| layers.Dense(256, activation='relu'), | |
| layers.Dropout(0.3), | |
| layers.Dense(num_classes, activation='softmax') | |
| ]) | |
| model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), | |
| loss='categorical_crossentropy', | |
| metrics=['accuracy'] | |
| ) | |
| model.summary() | |
| save_path = os.path.join(DST_DL_DIR, f"{crop_key}_model.h5") | |
| cbs = [ | |
| callbacks.ModelCheckpoint( | |
| save_path, monitor='val_accuracy', | |
| save_best_only=True, verbose=1 | |
| ), | |
| callbacks.EarlyStopping( | |
| monitor='val_loss', patience=4, | |
| restore_best_weights=True, verbose=1 | |
| ), | |
| callbacks.ReduceLROnPlateau( | |
| monitor='val_loss', factor=0.5, | |
| patience=2, min_lr=1e-6, verbose=1 | |
| ) | |
| ] | |
| print(f"\n Training {crop_key} model ({EPOCHS} epochs max)...") | |
| history = model.fit( | |
| train_gen, | |
| epochs=EPOCHS, | |
| validation_data=valid_gen, | |
| callbacks=cbs, | |
| verbose=1 | |
| ) | |
| # Fine-tune: unfreeze last 30 layers | |
| print("\n Fine-tuning last 30 layers of MobileNetV2...") | |
| base.trainable = True | |
| for layer in base.layers[:-30]: | |
| layer.trainable = False | |
| model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5), | |
| loss='categorical_crossentropy', | |
| metrics=['accuracy'] | |
| ) | |
| model.fit( | |
| train_gen, | |
| epochs=5, | |
| validation_data=valid_gen, | |
| callbacks=cbs, | |
| verbose=1 | |
| ) | |
| best_val_acc = max(history.history.get('val_accuracy', [0])) | |
| print(f"\n ✓ {crop_key}_model.h5 saved (best val_acc ≈ {best_val_acc*100:.1f}%)") | |
| return True | |
| finally: | |
| # Clean up temp symlink dirs | |
| for d in [tmp_train, tmp_valid]: | |
| try: | |
| shutil.rmtree(d, ignore_errors=True) | |
| except Exception: | |
| pass | |
| # ============================================================================= | |
| # Update main.py model paths to point to backend/models/ | |
| # ============================================================================= | |
| def update_main_py_paths(): | |
| """ | |
| Patches main.py so DL_MODELS_DIR and ML_MODELS_DIR point to | |
| backend/models/ instead of sample/AgriGo/... | |
| """ | |
| main_py = os.path.join(BASE_DIR, "main.py") | |
| if not os.path.isfile(main_py): | |
| return | |
| with open(main_py, "r", encoding="utf-8") as f: | |
| content = f.read() | |
| new_dl = 'DL_MODELS_DIR = os.path.join(BASE_DIR, "models", "DL_models")' | |
| new_ml = 'ML_MODELS_DIR = os.path.join(BASE_DIR, "models", "ML_models")' | |
| # Replace the old AgriGo-relative paths | |
| import re | |
| content = re.sub( | |
| r'DL_MODELS_DIR\s*=\s*os\.path\.join\(.*?DL_models.*?\)', | |
| new_dl, content | |
| ) | |
| content = re.sub( | |
| r'ML_MODELS_DIR\s*=\s*os\.path\.join\(.*?ML_models.*?\)', | |
| new_ml, content | |
| ) | |
| # Remove AGRIGO_DIR, SAMPLE_DIR lines if present and replace with comment | |
| content = re.sub(r'SAMPLE_DIR\s*=.*\n', '', content) | |
| content = re.sub(r'AGRIGO_DIR\s*=.*\n', '', content) | |
| with open(main_py, "w", encoding="utf-8") as f: | |
| f.write(content) | |
| print(f"\n ✓ Updated model paths in main.py → backend/models/") | |
| # ============================================================================= | |
| # Main Entry Point | |
| # ============================================================================= | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="AgriShield Model Setup — copy pre-trained DL + retrain ML models" | |
| ) | |
| parser.add_argument( | |
| "--retrain", action="store_true", | |
| help="Also retrain DL models from PlantVillage dataset (slow on CPU)" | |
| ) | |
| parser.add_argument( | |
| "--crop", type=str, default=None, | |
| help=f"Retrain a single crop only. Choices: {ALL_CROPS}" | |
| ) | |
| parser.add_argument( | |
| "--ml-only", action="store_true", | |
| help="Only retrain ML models, skip DL copy/train" | |
| ) | |
| args = parser.parse_args() | |
| print("\n" + "="*60) | |
| print(" AgriShield — Model Setup") | |
| print("="*60) | |
| print(f" Backend dir : {BASE_DIR}") | |
| print(f" Output dir : {DST_MODELS_DIR}") | |
| if not args.ml_only: | |
| if args.retrain: | |
| # Retrain DL from scratch | |
| crops_to_train = [args.crop] if args.crop else ALL_CROPS | |
| for crop in crops_to_train: | |
| if crop not in CROP_CLASS_MAP: | |
| print(f" [ERROR] Unknown crop '{crop}'. Choices: {ALL_CROPS}") | |
| continue | |
| retrain_dl_model(crop) | |
| else: | |
| # Copy pre-trained models (fast) | |
| copy_pretrained_dl_models() | |
| # Always retrain ML models (takes only seconds) | |
| train_ml_models() | |
| # Patch main.py paths | |
| update_main_py_paths() | |
| print("\n" + "="*60) | |
| print(" Setup complete! Models are in:") | |
| print(f" DL models : {DST_DL_DIR}") | |
| print(f" ML models : {DST_ML_DIR}") | |
| print("\n Start the backend with:") | |
| print(" uvicorn main:app --host 0.0.0.0 --port 8000 --reload") | |
| print("="*60 + "\n") | |
| if __name__ == "__main__": | |
| main() | |