""" Plant Disease Detection — Gradio Web App (Chatbot Style) Deployed on Hugging Face Spaces (free CPU tier) Architecture: Custom ResNet-18 trained from scratch on PlantVillage dataset 38 classes | 99.17% test accuracy | 54,305 training images Features: • Chatbot-style interface for natural interaction • Batch image processing (multiple images at once) • CLAHE preprocessing (matches training pipeline exactly) • Top-3 predictions with confidence bars • Grad-CAM heatmap overlay • Disease treatment recommendations """ import os import json import warnings from datetime import datetime from typing import List, Tuple, Optional import numpy as np import cv2 from PIL import Image import gradio as gr import tensorflow as tf import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.cm as cm warnings.filterwarnings('ignore') os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # ───────────────────────────────────────────────────────────────────────────── # CONFIGURATION # ───────────────────────────────────────────────────────────────────────────── MODEL_PATH = 'model_best_0.9917_ep040_acc0.9917.keras' STATS_PATH = 'normalization_stats.json' TARGET_SIZE = (224, 224) # Class names — sorted alphabetical order (tf.keras default) CLASS_NAMES = [ 'Apple___Apple_scab', 'Apple___Black_rot', 'Apple___Cedar_apple_rust', 'Apple___healthy', 'Blueberry___healthy', 'Cherry_(including_sour)___Powdery_mildew', 'Cherry_(including_sour)___healthy', 'Corn_(maize)___Cercospora_leaf_spot Gray_leaf_spot', 'Corn_(maize)___Common_rust_', 'Corn_(maize)___Northern_Leaf_Blight', 'Corn_(maize)___healthy', 'Grape___Black_rot', 'Grape___Esca_(Black_Measles)', 'Grape___Leaf_blight_(Isariopsis_Leaf_Spot)', 'Grape___healthy', 'Orange___Haunglongbing_(Citrus_greening)', 'Peach___Bacterial_spot', 'Peach___healthy', 'Pepper,_bell___Bacterial_spot', 'Pepper,_bell___healthy', 'Potato___Early_blight', 'Potato___Late_blight', 'Potato___healthy', 'Raspberry___healthy', 'Soybean___healthy', 'Squash___Powdery_mildew', 'Strawberry___Leaf_scorch', 'Strawberry___healthy', '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', ] # Disease information with treatment recommendations DISEASE_INFO = { 'Apple___Apple_scab': { 'name': 'Apple Scab', 'pathogen': 'Venturia inaequalis (fungus)', 'symptoms': 'Dark, scabby lesions on leaves and fruit, yellowing leaves, premature leaf drop', 'treatment': 'Remove fallen leaves, apply fungicides (captan, mancozeb) during bud break, plant resistant varieties', 'severity': 'High' }, 'Apple___Black_rot': { 'name': 'Apple Black Rot', 'pathogen': 'Botryosphaeria obtusa (fungus)', 'symptoms': 'Brown leaf spots with concentric rings, fruit rot, cankers on branches', 'treatment': 'Prune infected branches, remove mummified fruit, apply fungicides during growing season', 'severity': 'High' }, 'Apple___Cedar_apple_rust': { 'name': 'Cedar Apple Rust', 'pathogen': 'Gymnosporangium juniperi-virginianae (fungus)', 'symptoms': 'Bright orange-yellow spots on leaves, gelatinous spore masses', 'treatment': 'Remove nearby juniper/cedar trees if possible, apply fungicides before infection', 'severity': 'Medium' }, 'Apple___healthy': { 'name': 'Healthy Apple', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Blueberry___healthy': { 'name': 'Healthy Blueberry', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Cherry_(including_sour)___Powdery_mildew': { 'name': 'Cherry Powdery Mildew', 'pathogen': 'Podosphaera clandestina (fungus)', 'symptoms': 'White powdery coating on leaves and shoots, leaf curling, stunted growth', 'treatment': 'Apply sulfur or potassium bicarbonate fungicides, ensure good air circulation', 'severity': 'Medium' }, 'Cherry_(including_sour)___healthy': { 'name': 'Healthy Cherry', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Corn_(maize)___Cercospora_leaf_spot Gray_leaf_spot': { 'name': 'Gray Leaf Spot', 'pathogen': 'Cercospora zeae-maydis (fungus)', 'symptoms': 'Rectangular gray-tan lesions on leaves, reduced photosynthesis', 'treatment': 'Crop rotation, resistant hybrids, apply strobilurin or triazole fungicides', 'severity': 'High' }, 'Corn_(maize)___Common_rust_': { 'name': 'Common Rust', 'pathogen': 'Puccinia sorghi (fungus)', 'symptoms': 'Brick-red pustules on both leaf surfaces, yellowing around pustules', 'treatment': 'Plant resistant hybrids, apply fungicides if severe', 'severity': 'Medium' }, 'Corn_(maize)___Northern_Leaf_Blight': { 'name': 'Northern Leaf Blight', 'pathogen': 'Exserohilum turcicum (fungus)', 'symptoms': 'Long elliptical gray-green lesions, cigar-shaped spots', 'treatment': 'Plant resistant hybrids, crop rotation, apply fungicides at tasseling', 'severity': 'High' }, 'Corn_(maize)___healthy': { 'name': 'Healthy Corn', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Grape___Black_rot': { 'name': 'Grape Black Rot', 'pathogen': 'Guignardia bidwellii (fungus)', 'symptoms': 'Brown leaf spots with black margins, fruit shriveling into mummies', 'treatment': 'Remove mummified fruit, apply fungicides during bloom, ensure good air circulation', 'severity': 'High' }, 'Grape___Esca_(Black_Measles)': { 'name': 'Grape Esca (Black Measles)', 'pathogen': 'Multiple wood-rotting fungi', 'symptoms': 'Tiger-stripe leaf pattern, wood decay, berry speckling', 'treatment': 'No cure available; prune infected wood, prevent wounds during pruning', 'severity': 'Very High' }, 'Grape___Leaf_blight_(Isariopsis_Leaf_Spot)': { 'name': 'Grape Leaf Blight', 'pathogen': 'Pseudocercospora vitis (fungus)', 'symptoms': 'Dark brown angular spots, premature leaf drop', 'treatment': 'Apply fungicides during growing season, remove infected leaves', 'severity': 'Medium' }, 'Grape___healthy': { 'name': 'Healthy Grape', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Orange___Haunglongbing_(Citrus_greening)': { 'name': 'Citrus Greening (HLB)', 'pathogen': 'Candidatus Liberibacter asiaticus (bacteria)', 'symptoms': 'Yellowing leaves, misshapen bitter fruit, tree decline', 'treatment': 'No cure; remove infected trees, control psyllid vectors, plant certified disease-free trees', 'severity': 'Critical' }, 'Peach___Bacterial_spot': { 'name': 'Peach Bacterial Spot', 'pathogen': 'Xanthomonas arboricola (bacteria)', 'symptoms': 'Water-soaked spots turning brown, premature leaf drop, fruit lesions', 'treatment': 'Copper-based bactericides, plant resistant varieties, avoid overhead irrigation', 'severity': 'High' }, 'Peach___healthy': { 'name': 'Healthy Peach', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Pepper,_bell___Bacterial_spot': { 'name': 'Bell Pepper Bacterial Spot', 'pathogen': 'Xanthomonas euvesicatoria (bacteria)', 'symptoms': 'Small water-soaked lesions with yellow halos on leaves and fruit', 'treatment': 'Copper-based bactericides, resistant varieties, crop rotation', 'severity': 'High' }, 'Pepper,_bell___healthy': { 'name': 'Healthy Bell Pepper', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Potato___Early_blight': { 'name': 'Potato Early Blight', 'pathogen': 'Alternaria solani (fungus)', 'symptoms': 'Concentric ring "target" lesions on older leaves, yellowing', 'treatment': 'Apply fungicides (chlorothalonil, mancozeb), remove infected debris, crop rotation', 'severity': 'High' }, 'Potato___Late_blight': { 'name': 'Potato Late Blight', 'pathogen': 'Phytophthora infestans (oomycete)', 'symptoms': 'Water-soaked lesions turning brown-black, white fungal growth under leaves', 'treatment': 'Apply fungicides preventively, remove infected plants, plant resistant varieties', 'severity': 'Critical' }, 'Potato___healthy': { 'name': 'Healthy Potato', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Raspberry___healthy': { 'name': 'Healthy Raspberry', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Soybean___healthy': { 'name': 'Healthy Soybean', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Squash___Powdery_mildew': { 'name': 'Squash Powdery Mildew', 'pathogen': 'Podosphaera xanthii (fungus)', 'symptoms': 'White powdery spots on upper leaf surface, yellowing, leaf death', 'treatment': 'Apply sulfur or potassium bicarbonate, ensure good air circulation, resistant varieties', 'severity': 'Medium' }, 'Strawberry___Leaf_scorch': { 'name': 'Strawberry Leaf Scorch', 'pathogen': 'Diplocarpon earlianum (fungus)', 'symptoms': 'Dark purple spots, leaf margins turn brown and scorched', 'treatment': 'Apply fungicides during wet periods, remove infected leaves, drip irrigation', 'severity': 'Medium' }, 'Strawberry___healthy': { 'name': 'Healthy Strawberry', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, 'Tomato___Bacterial_spot': { 'name': 'Tomato Bacterial Spot', 'pathogen': 'Xanthomonas species (bacteria)', 'symptoms': 'Small water-soaked circular spots with yellow halos', 'treatment': 'Copper-based bactericides, resistant varieties, avoid overhead irrigation', 'severity': 'High' }, 'Tomato___Early_blight': { 'name': 'Tomato Early Blight', 'pathogen': 'Alternaria solani (fungus)', 'symptoms': 'Target-like concentric ring lesions with yellow halo', 'treatment': 'Apply fungicides (chlorothalonil, copper), remove infected leaves, stake plants', 'severity': 'High' }, 'Tomato___Late_blight': { 'name': 'Tomato Late Blight', 'pathogen': 'Phytophthora infestans (oomycete)', 'symptoms': 'Dark water-soaked lesions, rapid plant death in humid conditions', 'treatment': 'Apply fungicides preventively, remove infected plants immediately, good drainage', 'severity': 'Critical' }, 'Tomato___Leaf_Mold': { 'name': 'Tomato Leaf Mold', 'pathogen': 'Passalora fulva (fungus)', 'symptoms': 'Pale green-yellow spots above, olive-grey mold below leaf', 'treatment': 'Improve ventilation, apply fungicides, resistant varieties for greenhouse', 'severity': 'Medium' }, 'Tomato___Septoria_leaf_spot': { 'name': 'Septoria Leaf Spot', 'pathogen': 'Septoria lycopersici (fungus)', 'symptoms': 'Circular spots with dark borders and gray centers with black dots', 'treatment': 'Remove infected leaves, apply fungicides, mulch to prevent splashing', 'severity': 'Medium' }, 'Tomato___Spider_mites Two-spotted_spider_mite': { 'name': 'Spider Mites', 'pathogen': 'Tetranychus urticae (arachnid pest)', 'symptoms': 'Stippled/bronzed leaves, fine webbing, leaf drop', 'treatment': 'Apply insecticidal soap, neem oil, or miticides; introduce predatory mites', 'severity': 'Medium' }, 'Tomato___Target_Spot': { 'name': 'Tomato Target Spot', 'pathogen': 'Corynespora cassiicola (fungus)', 'symptoms': 'Concentric ring lesions similar to early blight but darker', 'treatment': 'Apply fungicides (chlorothalonil, copper), remove infected debris', 'severity': 'Medium' }, 'Tomato___Tomato_Yellow_Leaf_Curl_Virus': { 'name': 'Tomato Yellow Leaf Curl Virus', 'pathogen': 'TYLCV (virus)', 'symptoms': 'Upward leaf curling, yellowing edges, stunted growth', 'treatment': 'No cure; control whiteflies with insecticides, use resistant varieties, row covers', 'severity': 'High' }, 'Tomato___Tomato_mosaic_virus': { 'name': 'Tomato Mosaic Virus', 'pathogen': 'ToMV (virus)', 'symptoms': 'Mottled light/dark green mosaic pattern, leaf distortion, reduced yield', 'treatment': 'No cure; remove infected plants, disinfect tools, wash hands after handling', 'severity': 'High' }, 'Tomato___healthy': { 'name': 'Healthy Tomato', 'pathogen': 'None', 'symptoms': 'No disease symptoms detected', 'treatment': 'Continue regular monitoring and preventive care', 'severity': 'None' }, } # Treatment recommendations by severity SEVERITY_COLORS = { 'None': '#22c55e', 'Low': '#84cc16', 'Medium': '#eab308', 'High': '#f97316', 'Very High': '#dc2626', 'Critical': '#991b1b' } # ───────────────────────────────────────────────────────────────────────────── # MODEL & STATS LOADING # ───────────────────────────────────────────────────────────────────────────── def load_model_and_stats(): """Load the trained model and normalization statistics.""" print('Loading model...') model = tf.keras.models.load_model(MODEL_PATH, compile=False) print(f'Model loaded: {model.count_params():,} parameters') with open(STATS_PATH) as f: stats = json.load(f) print(f'Norm stats loaded — mean: {[f"{v:.3f}" for v in stats["mean"]]}') return model, stats print('Initializing...') MODEL, STATS = load_model_and_stats() # ───────────────────────────────────────────────────────────────────────────── # PREPROCESSING # ───────────────────────────────────────────────────────────────────────────── def preprocess_image(pil_image: Image.Image) -> np.ndarray: """ Apply the same preprocessing used during training: 1. Convert to BGR (OpenCV convention) for CLAHE 2. Resize to 224×224 3. CLAHE on L channel in LAB colour space 4. Convert back to RGB numpy float32 in [0, 255] """ img_bgr = cv2.cvtColor(np.array(pil_image.convert('RGB')), cv2.COLOR_RGB2BGR) h, w = img_bgr.shape[:2] interp = cv2.INTER_AREA if (w > TARGET_SIZE[0] or h > TARGET_SIZE[1]) else cv2.INTER_CUBIC img_bgr = cv2.resize(img_bgr, TARGET_SIZE, interpolation=interp) lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) lab_eq = cv2.merge([clahe.apply(l), a, b]) img_rgb = cv2.cvtColor(lab_eq, cv2.COLOR_LAB2RGB) return img_rgb.astype(np.float32) # ───────────────────────────────────────────────────────────────────────────── # GRAD-CAM # ───────────────────────────────────────────────────────────────────────────── def find_last_conv_layer(model) -> str: """Locate the name of the last Conv2D layer before GAP.""" last_conv_name = None for layer in model.layers: if isinstance(layer, tf.keras.layers.Conv2D): last_conv_name = layer.name return last_conv_name def generate_gradcam(model, img_array: np.ndarray, class_idx: int) -> np.ndarray: """Generate a Grad-CAM heatmap for the given class index.""" last_conv_name = find_last_conv_layer(model) if last_conv_name is None: return np.zeros(TARGET_SIZE, dtype=np.float32) grad_model = tf.keras.models.Model( inputs=model.input, outputs=[model.get_layer(last_conv_name).output, model.output] ) input_tensor = tf.cast(img_array[np.newaxis, ...], tf.float32) with tf.GradientTape() as tape: conv_outputs, predictions = grad_model(input_tensor, training=False) tape.watch(conv_outputs) class_score = predictions[:, class_idx] grads = tape.gradient(class_score, conv_outputs) pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2)) conv_outputs = conv_outputs[0] heatmap = tf.reduce_sum(conv_outputs * pooled_grads, axis=-1) heatmap = tf.nn.relu(heatmap).numpy() if heatmap.max() > 1e-8: heatmap = heatmap / heatmap.max() heatmap = cv2.resize(heatmap, TARGET_SIZE) return heatmap.astype(np.float32) def overlay_gradcam(original_rgb: np.ndarray, heatmap: np.ndarray, alpha: float = 0.45) -> np.ndarray: """Blend the Grad-CAM heatmap onto the original image.""" heatmap_uint8 = np.uint8(255 * heatmap) heatmap_color = cv2.applyColorMap(heatmap_uint8, cv2.COLORMAP_JET) heatmap_rgb = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB) img_uint8 = np.clip(original_rgb, 0, 255).astype(np.uint8) superimposed = cv2.addWeighted(img_uint8, 1 - alpha, heatmap_rgb, alpha, 0) return superimposed.astype(np.uint8) def create_gradcam_figure(original_rgb: np.ndarray, heatmap: np.ndarray, class_name: str) -> Image.Image: """Create a 2-panel figure: original + overlay.""" overlay = overlay_gradcam(original_rgb, heatmap) fig, axes = plt.subplots(1, 2, figsize=(12, 5)) fig.patch.set_facecolor('#0f1a0f') axes[0].imshow(np.clip(original_rgb, 0, 255).astype(np.uint8)) axes[0].set_title('Original Image', color='#b8e8b0', fontsize=12, fontweight='bold') axes[0].axis('off') axes[0].set_facecolor('#0f1a0f') axes[1].imshow(overlay) axes[1].set_title(f'AI Attention — {class_name}', color='#b8e8b0', fontsize=12, fontweight='bold') axes[1].axis('off') axes[1].set_facecolor('#0f1a0f') plt.tight_layout() fig.canvas.draw() buf = fig.canvas.buffer_rgba() w_px, h_px = fig.canvas.get_width_height() pil_img = Image.frombytes('RGBA', (w_px, h_px), buf).convert('RGB') plt.close(fig) return pil_img # ───────────────────────────────────────────────────────────────────────────── # PREDICTION FUNCTION # ───────────────────────────────────────────────────────────────────────────── def predict_single(pil_image: Image.Image) -> dict: """Run inference on a single image and return detailed results.""" if pil_image is None: return None img_rgb = preprocess_image(pil_image) input_tensor = tf.cast(img_rgb[np.newaxis, ...], tf.float32) proba = MODEL(input_tensor, training=False).numpy()[0] top3_idx = np.argsort(proba)[::-1][:3] top3 = [(CLASS_NAMES[i], float(proba[i])) for i in top3_idx] top1_name = top3[0][0] top1_idx = top3_idx[0] top1_conf = top3[0][1] heatmap = generate_gradcam(MODEL, img_rgb, top1_idx) gradcam_fig = create_gradcam_figure(img_rgb, heatmap, top1_name) info = DISEASE_INFO.get(top1_name, {}) return { 'top1_name': top1_name, 'top1_conf': top1_conf, 'top3': top3, 'gradcam': gradcam_fig, 'info': info, 'preprocessed': Image.fromarray(np.clip(img_rgb, 0, 255).astype(np.uint8)) } def format_prediction_message(result: dict) -> str: """Format a single prediction as a chat message.""" if result is None: return "⚠️ Could not process image." top1_name = result['top1_name'] top1_conf = result['top1_conf'] info = result['info'] severity_color = SEVERITY_COLORS.get(info.get('severity', 'None'), '#ffffff') confidence_bar = '█' * int(top1_conf * 20) + '░' * (20 - int(top1_conf * 20)) message = f""" ### 🎯 **Prediction: {info.get('name', top1_name)}** | Metric | Value | |--------|-------| | **Confidence** | {top1_conf:.1%} `{confidence_bar}` | | **Severity** | ● {info.get('severity', 'N/A')} | | **Pathogen** | {info.get('pathogen', 'Unknown')} | **📋 Symptoms:** {info.get('symptoms', 'No description available.')} **💊 Treatment:** {info.get('treatment', 'No treatment information available.')} """ return message def process_images(images: List[Image.Image], chat_history: List) -> Tuple: """Process multiple images and update chat history.""" if not images: return chat_history, None, "Please select at least one image to analyse." for img in images: result = predict_single(img) message = format_prediction_message(result) # Add user's image thumbnail reference user_msg = f"📷 Analysing this leaf image..." # Add assistant's detailed response if result: gradcam_img = result['gradcam'] assistant_msg = message chat_history.append((user_msg, assistant_msg)) chat_history.append((None, (gradcam_img,))) # Show gradcam as image else: chat_history.append((user_msg, "⚠️ Could not process this image.")) return chat_history, None, "" def clear_chat() -> Tuple: """Clear chat history and reset state.""" return [], None, "" # ───────────────────────────────────────────────────────────────────────────── # GRADIO UI - CHATBOT STYLE # ───────────────────────────────────────────────────────────────────────────── CUSTOM_CSS = """ /* ── Theme Variables ── */ :root { --primary: #2d6a2d; --primary-hover: #3d8a3d; --accent: #7ec87e; --bg-dark: #0b130b; --bg-card: #111f11; --bg-chat: #0d160d; --bg-user: #1a3d1a; --bg-bot: #111f11; --text: #d4ead4; --text-dim: #8aaa8a; --border: #2a3d2a; --radius: 12px; } /* Page background */ .gradio-container { background: var(--bg-dark) !important; font-family: 'DM Sans', 'Inter', sans-serif !important; max-width: 1200px !important; margin: 0 auto !important; } /* Header */ .header-block { text-align: center; padding: 1.5rem 1rem; border-bottom: 1px solid var(--border); background: linear-gradient(180deg, var(--bg-card) 0%, var(--bg-dark) 100%); } .header-block h1 { font-size: 2.2rem; font-weight: 700; color: var(--accent); letter-spacing: -0.03em; margin: 0 0 0.5rem 0; } .header-block p { color: var(--text-dim); font-size: 0.95rem; margin: 0; } /* Chat container */ .chat-container { background: var(--bg-chat); border: 1px solid var(--border); border-radius: var(--radius); max-height: 600px; overflow-y: auto; } /* Chat messages */ .message-wrap { border-radius: var(--radius) !important; margin: 8px 12px; } .message-wrap.user { background: var(--bg-user) !important; border: 1px solid var(--border) !important; } .message-wrap.bot { background: var(--bg-bot) !important; border: 1px solid var(--border) !important; } .message { color: var(--text) !important; } .message.user { color: var(--text) !important; } .message.bot { color: var(--text) !important; } /* Markdown in chat */ .message.bot .prose { color: var(--text) !important; } .message.bot table { background: var(--bg-dark); border: 1px solid var(--border); border-radius: 8px; } .message.bot th { background: var(--bg-user); color: var(--accent) !important; } .message.bot td { color: var(--text) !important; } /* Input panel */ .input-panel { background: var(--bg-card); border: 2px dashed var(--border) !important; border-radius: var(--radius) !important; } .input-panel:hover { border-color: var(--accent) !important; } /* Buttons */ button.primary { background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; letter-spacing: 0.02em !important; transition: all 0.2s ease !important; box-shadow: 0 2px 8px rgba(45, 106, 45, 0.3) !important; } button.primary:hover { background: linear-gradient(135deg, var(--primary-hover) 0%, var(--primary) 100%) !important; transform: translateY(-1px) !important; box-shadow: 0 4px 12px rgba(45, 106, 45, 0.4) !important; } /* Footer */ .footer-block { text-align: center; padding: 1rem; color: var(--text-dim); font-size: 0.8rem; border-top: 1px solid var(--border); margin-top: 1rem; } /* Scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: var(--bg-dark); } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: var(--accent); } """ HEADER_MD = """

🌿 Plant Disease Detective

AI-powered diagnosis • 38 disease classes • 99.17% accuracy • Grad-CAM visualisation

""" FOOTER_MD = """ """ WELCOME_MESSAGE = """ ### 👋 Hello! I'm your Plant Disease Detective! I can help you identify diseases in plant leaves using AI. Here's how to use me: 1. **📷 Upload images** - Click the upload button or drag & drop leaf images 2. **🔬 Click "Analyse Leaves"** - I'll analyse each image 3. **📊 View results** - Get diagnosis, confidence, and treatment recommendations **💡 Tips for best results:** - Use clear, well-lit photos of single leaves - Make sure the affected area is visible - Try one of the example images below Ready to start? Upload an image! 🚀 """ def get_example_images(): """Get list of example images from the examples directory.""" example_dir = 'examples' if not os.path.exists(example_dir): return [] examples = [] for filename in sorted(os.listdir(example_dir))[:24]: # Show first 24 if filename.endswith(('.jpg', '.jpeg', '.png')): examples.append(os.path.join(example_dir, filename)) return examples # Build Gradio interface def create_app(): with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Base()) as demo: gr.HTML(HEADER_MD) # State to track chat history chat_state = gr.State([]) with gr.Row(): # Left column: Input with gr.Column(scale=1, min_width=350): gr.Markdown("### 📷 Upload Leaf Images") image_input = gr.Image( type='pil', label='Drop leaf image here or click to upload', elem_classes='input-panel', height=280, ) analyse_btn = gr.Button( '🔬 Analyse Leaves', variant='primary', size='lg', ) clear_btn = gr.Button( '🗑️ Clear Chat', variant='secondary', size='md', ) gr.Markdown(""" **Supported formats:** JPG, PNG **Max file size:** 10MB **Batch:** Multiple images supported """) # Right column: Chat output with gr.Column(scale=2): gr.Markdown("### 💬 Analysis Results") chatbot = gr.Chatbot( label='Disease Analysis', elem_classes='chat-container', height=550, show_copy_button=True, ) status_text = gr.Textbox( label='Status', interactive=False, visible=False, ) # Examples section example_images = get_example_images() if example_images: gr.Markdown("### 🌟 Try Example Images") examples = gr.Gallery( value=example_images, label='Click to load example', columns=6, height='auto', object_fit='cover', ) gr.HTML(FOOTER_MD) # Event handlers def on_image_upload(img): if img is not None: return [img], None return [], None def on_example_click(img_path): if os.path.exists(img_path): return Image.open(img_path), None return None, None def analyse_current_image(img, history): """Analyse a single image and return updated history.""" if img is None: return history, "No image to analyse." result = predict_single(img) message = format_prediction_message(result) user_msg = "📷 Analysing this leaf image..." if result: gradcam_img = result['gradcam'] history.append((user_msg, message)) history.append((None, (gradcam_img,))) else: history.append((user_msg, "⚠️ Could not process this image.")) return history, "" def clear_all(): return [], None, "" # Wire up events analyse_btn.click( fn=analyse_current_image, inputs=[image_input, chatbot], outputs=[chatbot, status_text], ) clear_btn.click( fn=clear_all, inputs=[], outputs=[chatbot, image_input, status_text], ) # Example gallery click handler if example_images: examples.select( fn=on_example_click, inputs=[examples], outputs=[image_input, status_text], ) return demo if __name__ == '__main__': demo = create_app() demo.launch(server_name='0.0.0.0', server_port=7860)