cropdiseasedetect3d / setup_models.py
Rishav Goswami
Deploy FastAPI backend to Hugging Face
5bda5ea
Raw
History Blame Contribute Delete
19.7 kB
"""
=============================================================================
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()