Spaces:
Sleeping
Sleeping
change main.py and added json
Browse files- disease_guide.json +119 -0
- main.py +351 -42
- requirements.txt +0 -0
- todo.md +43 -0
disease_guide.json
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Apple___Apple_scab": {
|
| 3 |
+
"disease_name": "Apple Scab",
|
| 4 |
+
"common_names": ["Apple scab"],
|
| 5 |
+
"crop": "Apple",
|
| 6 |
+
"description": "A fungal disease caused by Venturia inaequalis that affects the leaves and fruit of apple trees, resulting in olive-green to black velvety lesions.",
|
| 7 |
+
"symptoms": [
|
| 8 |
+
"Dark, scabby lesions on leaves and fruit",
|
| 9 |
+
"Premature leaf drop"
|
| 10 |
+
],
|
| 11 |
+
"cause": "Fungal",
|
| 12 |
+
"treatment": [
|
| 13 |
+
"Apply fungicides like captan or myclobutanil",
|
| 14 |
+
"Remove infected leaves and fruit"
|
| 15 |
+
],
|
| 16 |
+
"prevention": [
|
| 17 |
+
"Prune trees for better air circulation",
|
| 18 |
+
"Plant resistant varieties"
|
| 19 |
+
],
|
| 20 |
+
"image_urls": ["https://yourcdn.com/apple_scab_1.jpg"],
|
| 21 |
+
"management_tips": "Monitor trees during wet seasons and apply fungicide as needed.",
|
| 22 |
+
"risk_level": "High",
|
| 23 |
+
"sprayer_intervals": "Every 7-10 days during wet periods",
|
| 24 |
+
"localized_tips": "Ensure proper pruning to reduce humidity",
|
| 25 |
+
"type": "Fungal",
|
| 26 |
+
"external_resources": [
|
| 27 |
+
{
|
| 28 |
+
"title": "Apple Scab Management",
|
| 29 |
+
"url": "https://example.com/apple-scab"
|
| 30 |
+
}
|
| 31 |
+
]
|
| 32 |
+
},
|
| 33 |
+
"Apple___Black_rot": {
|
| 34 |
+
"disease_name": "Black Rot",
|
| 35 |
+
"common_names": ["Apple black rot"],
|
| 36 |
+
"crop": "Apple",
|
| 37 |
+
"description": "Caused by the fungus Botryosphaeria obtusa, black rot affects the fruit, leaves, and bark of apple trees.",
|
| 38 |
+
"symptoms": [
|
| 39 |
+
"Circular brown lesions on fruit",
|
| 40 |
+
"Purple-bordered leaf spots"
|
| 41 |
+
],
|
| 42 |
+
"cause": "Fungal",
|
| 43 |
+
"treatment": [
|
| 44 |
+
"Remove and destroy infected branches and fruit",
|
| 45 |
+
"Apply fungicides in early spring"
|
| 46 |
+
],
|
| 47 |
+
"prevention": ["Sanitize pruning tools", "Clear fallen debris"],
|
| 48 |
+
"image_urls": ["https://yourcdn.com/apple_black_rot_1.jpg"],
|
| 49 |
+
"management_tips": "Clean up mummified fruits and dead wood from the orchard.",
|
| 50 |
+
"risk_level": "Medium",
|
| 51 |
+
"sprayer_intervals": "Every 10-14 days",
|
| 52 |
+
"localized_tips": "Remove mummified fruit and prune affected branches",
|
| 53 |
+
"type": "Fungal",
|
| 54 |
+
"external_resources": []
|
| 55 |
+
},
|
| 56 |
+
"Apple___Cedar_apple_rust": {
|
| 57 |
+
"disease_name": "Cedar Apple Rust",
|
| 58 |
+
"common_names": ["Cedar rust"],
|
| 59 |
+
"crop": "Apple",
|
| 60 |
+
"description": "A fungal disease requiring both apple and cedar (juniper) hosts to complete its lifecycle.",
|
| 61 |
+
"symptoms": ["Bright orange spots on leaves", "Galls on cedar trees"],
|
| 62 |
+
"cause": "Fungal",
|
| 63 |
+
"treatment": [
|
| 64 |
+
"Apply fungicide such as mancozeb",
|
| 65 |
+
"Remove nearby cedar trees if possible"
|
| 66 |
+
],
|
| 67 |
+
"prevention": [
|
| 68 |
+
"Use resistant varieties",
|
| 69 |
+
"Space trees to increase airflow"
|
| 70 |
+
],
|
| 71 |
+
"image_urls": ["https://yourcdn.com/cedar_apple_rust_1.jpg"],
|
| 72 |
+
"management_tips": "Monitor both apple and cedar hosts for symptoms and apply fungicide preventively.",
|
| 73 |
+
"risk_level": "Medium",
|
| 74 |
+
"sprayer_intervals": "Every 7-10 days during infection periods",
|
| 75 |
+
"localized_tips": "Avoid planting near cedar trees",
|
| 76 |
+
"type": "Fungal",
|
| 77 |
+
"external_resources": []
|
| 78 |
+
},
|
| 79 |
+
"Apple___healthy": {
|
| 80 |
+
"disease_name": null,
|
| 81 |
+
"common_names": [],
|
| 82 |
+
"crop": "Apple",
|
| 83 |
+
"description": "Healthy plant — no disease detected.",
|
| 84 |
+
"symptoms": [],
|
| 85 |
+
"cause": null,
|
| 86 |
+
"treatment": [],
|
| 87 |
+
"prevention": ["Maintain good cultural practices"],
|
| 88 |
+
"image_urls": ["https://yourcdn.com/apple_healthy_1.jpg"],
|
| 89 |
+
"management_tips": "Regular inspection and proper irrigation help maintain plant health.",
|
| 90 |
+
"risk_level": "Low",
|
| 91 |
+
"sprayer_intervals": "Preventive fungicide once per month",
|
| 92 |
+
"localized_tips": "Keep foliage dry by watering at the base",
|
| 93 |
+
"type": "None",
|
| 94 |
+
"external_resources": []
|
| 95 |
+
},
|
| 96 |
+
"Tomato___Late_blight": {
|
| 97 |
+
"disease_name": "Late Blight",
|
| 98 |
+
"common_names": ["Tomato late blight"],
|
| 99 |
+
"crop": "Tomato",
|
| 100 |
+
"description": "A serious fungal disease caused by Phytophthora infestans, affecting leaves, stems, and fruit.",
|
| 101 |
+
"symptoms": [
|
| 102 |
+
"Large, dark brown blotches with green halos on leaves",
|
| 103 |
+
"Fruits develop firm, dark lesions"
|
| 104 |
+
],
|
| 105 |
+
"cause": "Fungal",
|
| 106 |
+
"treatment": [
|
| 107 |
+
"Apply fungicide like chlorothalonil or mancozeb",
|
| 108 |
+
"Remove infected plants"
|
| 109 |
+
],
|
| 110 |
+
"prevention": ["Use resistant varieties", "Rotate crops annually"],
|
| 111 |
+
"image_urls": ["https://yourcdn.com/tomato_late_blight.jpg"],
|
| 112 |
+
"management_tips": "Avoid overhead watering to reduce moisture on leaves.",
|
| 113 |
+
"risk_level": "High",
|
| 114 |
+
"sprayer_intervals": "Every 7 days during high humidity",
|
| 115 |
+
"localized_tips": "Improve drainage and spacing to reduce humidity",
|
| 116 |
+
"type": "Fungal",
|
| 117 |
+
"external_resources": []
|
| 118 |
+
}
|
| 119 |
+
}
|
main.py
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
import logging
|
| 3 |
-
from typing import Optional
|
| 4 |
from contextlib import asynccontextmanager
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
import numpy as np
|
| 7 |
import tensorflow as tf
|
| 8 |
-
from fastapi import FastAPI, File, UploadFile, HTTPException, status
|
| 9 |
from PIL import Image
|
| 10 |
import io
|
| 11 |
from huggingface_hub import hf_hub_download
|
| 12 |
-
from pydantic import BaseModel
|
| 13 |
|
| 14 |
# Configure logging
|
| 15 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -20,6 +25,8 @@ HF_MODEL_REPO: str = os.getenv("HF_MODEL_REPO", "yasyn14/smart-leaf-model")
|
|
| 20 |
HF_MODEL_FILENAME: str = os.getenv("HF_MODEL_FILENAME", "best_model_32epochs.keras")
|
| 21 |
HF_CACHE_DIR: str = os.getenv("HF_HOME", "/home/appuser/huggingface")
|
| 22 |
IMAGE_SIZE: tuple = (300, 300)
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# Plant disease class names
|
| 25 |
CLASS_NAMES = [
|
|
@@ -38,36 +45,131 @@ CLASS_NAMES = [
|
|
| 38 |
'Tomato___healthy'
|
| 39 |
]
|
| 40 |
|
| 41 |
-
# Clean class names for better display
|
| 42 |
-
CLEAN_CLASS_NAMES = [name.replace('___', ' - ').replace('_', ' ') for name in CLASS_NAMES]
|
| 43 |
-
|
| 44 |
# HTTP Status Messages
|
| 45 |
HTTP_MESSAGES = {
|
| 46 |
"MODEL_NOT_LOADED": "Model not loaded. Please check server logs.",
|
| 47 |
"INVALID_FILE_TYPE": "File must be an image",
|
|
|
|
| 48 |
"PREDICTION_FAILED": "Prediction failed: {error}",
|
| 49 |
"IMAGE_PROCESSING_FAILED": "Error preprocessing image: {error}",
|
| 50 |
"MODEL_LOAD_SUCCESS": "Model loaded successfully",
|
| 51 |
-
"MODEL_LOAD_FAILED": "Failed to load model"
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
# Global model variable
|
| 55 |
model: Optional[tf.keras.Model] = None
|
|
|
|
| 56 |
|
| 57 |
# Response models
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
class PredictionResponse(BaseModel):
|
| 59 |
success: bool
|
| 60 |
predicted_class: str
|
| 61 |
-
clean_class_name: str
|
| 62 |
confidence: float
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
| 64 |
message: str
|
| 65 |
|
| 66 |
class HealthResponse(BaseModel):
|
| 67 |
status: str
|
| 68 |
model_loaded: bool
|
|
|
|
|
|
|
| 69 |
message: str
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
def download_model_from_hf() -> str:
|
| 72 |
"""Download model from Hugging Face Hub"""
|
| 73 |
try:
|
|
@@ -84,31 +186,51 @@ def download_model_from_hf() -> str:
|
|
| 84 |
raise
|
| 85 |
|
| 86 |
def load_model() -> tf.keras.Model:
|
| 87 |
-
"""Load the Keras model from Hugging Face"""
|
| 88 |
try:
|
| 89 |
model_path = download_model_from_hf()
|
| 90 |
loaded_model = tf.keras.models.load_model(model_path)
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
return loaded_model
|
| 93 |
except Exception as e:
|
| 94 |
logger.error(f"Failed to load model: {str(e)}")
|
| 95 |
raise
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
def preprocess_image(image_bytes: bytes) -> np.ndarray:
|
| 98 |
-
"""Preprocess image for model prediction"""
|
| 99 |
try:
|
| 100 |
-
#
|
|
|
|
|
|
|
|
|
|
| 101 |
image = Image.open(io.BytesIO(image_bytes))
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
# Convert to RGB if needed
|
| 104 |
if image.mode != 'RGB':
|
| 105 |
image = image.convert('RGB')
|
| 106 |
|
| 107 |
-
# Resize image
|
| 108 |
-
image = image.resize(IMAGE_SIZE)
|
| 109 |
|
| 110 |
# Convert to numpy array and normalize
|
| 111 |
-
img_array = np.array(image) / 255.0
|
| 112 |
|
| 113 |
# Add batch dimension
|
| 114 |
img_array = np.expand_dims(img_array, axis=0)
|
|
@@ -116,11 +238,14 @@ def preprocess_image(image_bytes: bytes) -> np.ndarray:
|
|
| 116 |
return img_array
|
| 117 |
except Exception as e:
|
| 118 |
logger.error(f"Error preprocessing image: {str(e)}")
|
| 119 |
-
raise
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
def predict_image(image_bytes: bytes) -> PredictionResponse:
|
| 122 |
-
"""Make prediction for the uploaded image"""
|
| 123 |
-
global model
|
| 124 |
|
| 125 |
if model is None:
|
| 126 |
raise HTTPException(
|
|
@@ -137,25 +262,48 @@ def predict_image(image_bytes: bytes) -> PredictionResponse:
|
|
| 137 |
predicted_class_idx = np.argmax(predictions[0])
|
| 138 |
confidence = float(predictions[0][predicted_class_idx])
|
| 139 |
|
| 140 |
-
# Get
|
| 141 |
predicted_class = CLASS_NAMES[predicted_class_idx]
|
| 142 |
-
|
|
|
|
| 143 |
|
| 144 |
-
#
|
|
|
|
| 145 |
all_predictions = {
|
| 146 |
-
|
| 147 |
-
for
|
| 148 |
}
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
return PredictionResponse(
|
| 151 |
success=True,
|
| 152 |
predicted_class=predicted_class,
|
| 153 |
-
clean_class_name=
|
| 154 |
confidence=confidence,
|
|
|
|
| 155 |
all_predictions=all_predictions,
|
| 156 |
-
|
|
|
|
|
|
|
| 157 |
)
|
| 158 |
|
|
|
|
|
|
|
| 159 |
except Exception as e:
|
| 160 |
logger.error(f"Prediction failed: {str(e)}")
|
| 161 |
raise HTTPException(
|
|
@@ -165,6 +313,8 @@ def predict_image(image_bytes: bytes) -> PredictionResponse:
|
|
| 165 |
|
| 166 |
def is_image_file(filename: str) -> bool:
|
| 167 |
"""Check if file is an image based on extension"""
|
|
|
|
|
|
|
| 168 |
image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'}
|
| 169 |
return any(filename.lower().endswith(ext) for ext in image_extensions)
|
| 170 |
|
|
@@ -172,9 +322,14 @@ def is_image_file(filename: str) -> bool:
|
|
| 172 |
async def lifespan(app: FastAPI):
|
| 173 |
"""Handle startup and shutdown events"""
|
| 174 |
# Startup
|
| 175 |
-
global model
|
| 176 |
try:
|
| 177 |
-
logger.info("Starting up... Loading model")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
model = load_model()
|
| 179 |
|
| 180 |
# Pre-warm the model with a dummy prediction
|
|
@@ -183,7 +338,7 @@ async def lifespan(app: FastAPI):
|
|
| 183 |
logger.info("Model pre-warmed successfully")
|
| 184 |
|
| 185 |
except Exception as e:
|
| 186 |
-
logger.error(f"Failed to
|
| 187 |
model = None
|
| 188 |
|
| 189 |
yield
|
|
@@ -194,9 +349,20 @@ async def lifespan(app: FastAPI):
|
|
| 194 |
# Create FastAPI app
|
| 195 |
app = FastAPI(
|
| 196 |
title="Plant Disease Prediction API",
|
| 197 |
-
description="API for predicting plant diseases from
|
| 198 |
-
version="
|
| 199 |
-
lifespan=lifespan
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
)
|
| 201 |
|
| 202 |
@app.get("/", response_model=HealthResponse)
|
|
@@ -205,6 +371,8 @@ async def root():
|
|
| 205 |
return HealthResponse(
|
| 206 |
status="running",
|
| 207 |
model_loaded=model is not None,
|
|
|
|
|
|
|
| 208 |
message="Plant Disease Prediction API is running"
|
| 209 |
)
|
| 210 |
|
|
@@ -214,6 +382,8 @@ async def health_check():
|
|
| 214 |
return HealthResponse(
|
| 215 |
status="healthy" if model is not None else "unhealthy",
|
| 216 |
model_loaded=model is not None,
|
|
|
|
|
|
|
| 217 |
message=HTTP_MESSAGES["MODEL_LOAD_SUCCESS"] if model is not None else HTTP_MESSAGES["MODEL_NOT_LOADED"]
|
| 218 |
)
|
| 219 |
|
|
@@ -222,12 +392,18 @@ async def predict_plant_disease(file: UploadFile = File(...)):
|
|
| 222 |
"""
|
| 223 |
Predict plant disease from uploaded image
|
| 224 |
|
| 225 |
-
- **file**: Single image file to analyze
|
| 226 |
|
| 227 |
-
Returns prediction with confidence score
|
| 228 |
"""
|
| 229 |
|
| 230 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
if not is_image_file(file.filename):
|
| 232 |
raise HTTPException(
|
| 233 |
status_code=status.HTTP_400_BAD_REQUEST,
|
|
@@ -251,15 +427,148 @@ async def predict_plant_disease(file: UploadFile = File(...)):
|
|
| 251 |
detail=HTTP_MESSAGES["IMAGE_PROCESSING_FAILED"].format(error=str(e))
|
| 252 |
)
|
| 253 |
|
| 254 |
-
@app.get("/
|
| 255 |
-
async def
|
| 256 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
return {
|
| 258 |
-
"
|
| 259 |
-
"
|
| 260 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
}
|
| 262 |
|
| 263 |
if __name__ == "__main__":
|
| 264 |
import uvicorn
|
| 265 |
-
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|
|
| 1 |
+
import json
|
| 2 |
import os
|
| 3 |
import logging
|
| 4 |
+
from typing import Dict, Optional, Any, List
|
| 5 |
from contextlib import asynccontextmanager
|
| 6 |
+
from rapidfuzz import process, fuzz
|
| 7 |
+
|
| 8 |
+
from fastapi.responses import JSONResponse
|
| 9 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
|
| 11 |
import numpy as np
|
| 12 |
import tensorflow as tf
|
| 13 |
+
from fastapi import FastAPI, File, Path, Query, UploadFile, HTTPException, status
|
| 14 |
from PIL import Image
|
| 15 |
import io
|
| 16 |
from huggingface_hub import hf_hub_download
|
| 17 |
+
from pydantic import BaseModel, Field
|
| 18 |
|
| 19 |
# Configure logging
|
| 20 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 25 |
HF_MODEL_FILENAME: str = os.getenv("HF_MODEL_FILENAME", "best_model_32epochs.keras")
|
| 26 |
HF_CACHE_DIR: str = os.getenv("HF_HOME", "/home/appuser/huggingface")
|
| 27 |
IMAGE_SIZE: tuple = (300, 300)
|
| 28 |
+
MAX_FILE_SIZE_MB: int = 10
|
| 29 |
+
CONFIDENCE_THRESHOLD: float = 0.5
|
| 30 |
|
| 31 |
# Plant disease class names
|
| 32 |
CLASS_NAMES = [
|
|
|
|
| 45 |
'Tomato___healthy'
|
| 46 |
]
|
| 47 |
|
|
|
|
|
|
|
|
|
|
| 48 |
# HTTP Status Messages
|
| 49 |
HTTP_MESSAGES = {
|
| 50 |
"MODEL_NOT_LOADED": "Model not loaded. Please check server logs.",
|
| 51 |
"INVALID_FILE_TYPE": "File must be an image",
|
| 52 |
+
"FILE_TOO_LARGE": f"File size exceeds {MAX_FILE_SIZE_MB}MB limit",
|
| 53 |
"PREDICTION_FAILED": "Prediction failed: {error}",
|
| 54 |
"IMAGE_PROCESSING_FAILED": "Error preprocessing image: {error}",
|
| 55 |
"MODEL_LOAD_SUCCESS": "Model loaded successfully",
|
| 56 |
+
"MODEL_LOAD_FAILED": "Failed to load model",
|
| 57 |
+
"LOW_CONFIDENCE": "Prediction confidence is low. Please try a clearer image."
|
| 58 |
}
|
| 59 |
|
| 60 |
# Global model variable
|
| 61 |
model: Optional[tf.keras.Model] = None
|
| 62 |
+
disease_guide: Dict[str, Dict[str, Any]] = {}
|
| 63 |
|
| 64 |
# Response models
|
| 65 |
+
class DiseaseInfo(BaseModel):
|
| 66 |
+
disease_name: Optional[str]
|
| 67 |
+
common_names: List[str] = []
|
| 68 |
+
crop: str
|
| 69 |
+
description: str
|
| 70 |
+
symptoms: List[str] = []
|
| 71 |
+
cause: Optional[str]
|
| 72 |
+
treatment: List[str] = []
|
| 73 |
+
prevention: List[str] = []
|
| 74 |
+
management_tips: str = ""
|
| 75 |
+
risk_level: str = "Unknown"
|
| 76 |
+
sprayer_intervals: str = ""
|
| 77 |
+
localized_tips: str = ""
|
| 78 |
+
type: str = "Unknown"
|
| 79 |
+
external_resources: List[Dict[str, str]] = []
|
| 80 |
+
|
| 81 |
class PredictionResponse(BaseModel):
|
| 82 |
success: bool
|
| 83 |
predicted_class: str
|
| 84 |
+
clean_class_name: str = Field(description="Human-readable class name")
|
| 85 |
confidence: float
|
| 86 |
+
confidence_level: str = Field(description="High/Medium/Low confidence level")
|
| 87 |
+
all_predictions: Dict[str, float] = Field(description="Top 5 predictions with confidence scores")
|
| 88 |
+
disease_info: DiseaseInfo
|
| 89 |
+
recommendations: List[str] = Field(description="Action recommendations based on prediction")
|
| 90 |
message: str
|
| 91 |
|
| 92 |
class HealthResponse(BaseModel):
|
| 93 |
status: str
|
| 94 |
model_loaded: bool
|
| 95 |
+
total_classes: int
|
| 96 |
+
available_diseases: int
|
| 97 |
message: str
|
| 98 |
|
| 99 |
+
class SearchResult(BaseModel):
|
| 100 |
+
class_name: str
|
| 101 |
+
disease_info: DiseaseInfo
|
| 102 |
+
relevance_score: Optional[float] = None
|
| 103 |
+
|
| 104 |
+
class SearchResponse(BaseModel):
|
| 105 |
+
results: List[SearchResult]
|
| 106 |
+
suggestions: List[SearchResult] = []
|
| 107 |
+
total_results: int
|
| 108 |
+
message: str = ""
|
| 109 |
+
|
| 110 |
+
def load_disease_guide() -> Dict[str, Dict[str, Any]]:
|
| 111 |
+
"""Load disease guide from JSON file with error handling"""
|
| 112 |
+
try:
|
| 113 |
+
guide_path = "disease_guide.json"
|
| 114 |
+
if not os.path.exists(guide_path):
|
| 115 |
+
logger.warning(f"Disease guide file not found at {guide_path}")
|
| 116 |
+
return {}
|
| 117 |
+
|
| 118 |
+
with open(guide_path, 'r', encoding='utf-8') as f:
|
| 119 |
+
guide = json.load(f)
|
| 120 |
+
|
| 121 |
+
logger.info(f"Loaded disease guide with {len(guide)} entries")
|
| 122 |
+
return guide
|
| 123 |
+
except Exception as e:
|
| 124 |
+
logger.error(f"Failed to load disease guide: {str(e)}")
|
| 125 |
+
return {}
|
| 126 |
+
|
| 127 |
+
def clean_class_name(class_name: str) -> str:
|
| 128 |
+
"""Convert class name to human-readable format"""
|
| 129 |
+
# Replace underscores with spaces and clean up formatting
|
| 130 |
+
cleaned = class_name.replace('___', ' - ').replace('_', ' ')
|
| 131 |
+
# Handle special cases
|
| 132 |
+
cleaned = cleaned.replace('(including sour)', '(including sour)')
|
| 133 |
+
cleaned = cleaned.replace('Two-spotted spider mite', 'Two-spotted spider mite')
|
| 134 |
+
return cleaned.title()
|
| 135 |
+
|
| 136 |
+
def get_confidence_level(confidence: float) -> str:
|
| 137 |
+
"""Categorize confidence level"""
|
| 138 |
+
if confidence >= 0.8:
|
| 139 |
+
return "High"
|
| 140 |
+
elif confidence >= 0.6:
|
| 141 |
+
return "Medium"
|
| 142 |
+
else:
|
| 143 |
+
return "Low"
|
| 144 |
+
|
| 145 |
+
def get_recommendations(predicted_class: str, confidence: float, disease_info: Dict[str, Any]) -> List[str]:
|
| 146 |
+
"""Generate actionable recommendations based on prediction"""
|
| 147 |
+
recommendations = []
|
| 148 |
+
|
| 149 |
+
if confidence < CONFIDENCE_THRESHOLD:
|
| 150 |
+
recommendations.append("Consider taking a clearer, well-lit photo for better accuracy")
|
| 151 |
+
recommendations.append("Ensure the leaf fills most of the frame")
|
| 152 |
+
|
| 153 |
+
if disease_info.get("disease_name"):
|
| 154 |
+
# Disease detected
|
| 155 |
+
recommendations.extend([
|
| 156 |
+
f"Immediate action: {disease_info.get('treatment', ['Consult agricultural expert'])[0] if disease_info.get('treatment') else 'Consult agricultural expert'}",
|
| 157 |
+
"Isolate affected plants to prevent spread",
|
| 158 |
+
"Monitor other plants for similar symptoms"
|
| 159 |
+
])
|
| 160 |
+
|
| 161 |
+
if disease_info.get("risk_level") == "High":
|
| 162 |
+
recommendations.insert(0, "⚠️ HIGH RISK: Take immediate action to prevent crop loss")
|
| 163 |
+
else:
|
| 164 |
+
# Healthy plant
|
| 165 |
+
recommendations.extend([
|
| 166 |
+
"Plant appears healthy - continue current care routine",
|
| 167 |
+
"Monitor regularly for any changes",
|
| 168 |
+
"Maintain preventive measures"
|
| 169 |
+
])
|
| 170 |
+
|
| 171 |
+
return recommendations
|
| 172 |
+
|
| 173 |
def download_model_from_hf() -> str:
|
| 174 |
"""Download model from Hugging Face Hub"""
|
| 175 |
try:
|
|
|
|
| 186 |
raise
|
| 187 |
|
| 188 |
def load_model() -> tf.keras.Model:
|
| 189 |
+
"""Load the Keras model from Hugging Face with optimization"""
|
| 190 |
try:
|
| 191 |
model_path = download_model_from_hf()
|
| 192 |
loaded_model = tf.keras.models.load_model(model_path)
|
| 193 |
+
|
| 194 |
+
# Compile model for inference optimization
|
| 195 |
+
loaded_model.compile(optimizer='adam', loss='sparse_categorical_crossentropy')
|
| 196 |
+
|
| 197 |
+
logger.info("Model loaded and compiled successfully")
|
| 198 |
return loaded_model
|
| 199 |
except Exception as e:
|
| 200 |
logger.error(f"Failed to load model: {str(e)}")
|
| 201 |
raise
|
| 202 |
|
| 203 |
+
def validate_file_size(file_size: int) -> None:
|
| 204 |
+
"""Validate uploaded file size"""
|
| 205 |
+
max_size_bytes = MAX_FILE_SIZE_MB * 1024 * 1024
|
| 206 |
+
if file_size > max_size_bytes:
|
| 207 |
+
raise HTTPException(
|
| 208 |
+
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
| 209 |
+
detail=HTTP_MESSAGES["FILE_TOO_LARGE"]
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
def preprocess_image(image_bytes: bytes) -> np.ndarray:
|
| 213 |
+
"""Preprocess image for model prediction with enhanced error handling"""
|
| 214 |
try:
|
| 215 |
+
# Validate file size
|
| 216 |
+
validate_file_size(len(image_bytes))
|
| 217 |
+
|
| 218 |
+
# Open and validate image
|
| 219 |
image = Image.open(io.BytesIO(image_bytes))
|
| 220 |
|
| 221 |
+
# Validate image format
|
| 222 |
+
if image.format not in ['JPEG', 'PNG', 'BMP', 'TIFF', 'WEBP']:
|
| 223 |
+
raise ValueError(f"Unsupported image format: {image.format}")
|
| 224 |
+
|
| 225 |
# Convert to RGB if needed
|
| 226 |
if image.mode != 'RGB':
|
| 227 |
image = image.convert('RGB')
|
| 228 |
|
| 229 |
+
# Resize image with high-quality resampling
|
| 230 |
+
image = image.resize(IMAGE_SIZE, Image.Resampling.LANCZOS)
|
| 231 |
|
| 232 |
# Convert to numpy array and normalize
|
| 233 |
+
img_array = np.array(image, dtype=np.float32) / 255.0
|
| 234 |
|
| 235 |
# Add batch dimension
|
| 236 |
img_array = np.expand_dims(img_array, axis=0)
|
|
|
|
| 238 |
return img_array
|
| 239 |
except Exception as e:
|
| 240 |
logger.error(f"Error preprocessing image: {str(e)}")
|
| 241 |
+
raise HTTPException(
|
| 242 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 243 |
+
detail=HTTP_MESSAGES["IMAGE_PROCESSING_FAILED"].format(error=str(e))
|
| 244 |
+
)
|
| 245 |
|
| 246 |
def predict_image(image_bytes: bytes) -> PredictionResponse:
|
| 247 |
+
"""Make prediction for the uploaded image with enhanced response"""
|
| 248 |
+
global model, disease_guide
|
| 249 |
|
| 250 |
if model is None:
|
| 251 |
raise HTTPException(
|
|
|
|
| 262 |
predicted_class_idx = np.argmax(predictions[0])
|
| 263 |
confidence = float(predictions[0][predicted_class_idx])
|
| 264 |
|
| 265 |
+
# Get predicted class
|
| 266 |
predicted_class = CLASS_NAMES[predicted_class_idx]
|
| 267 |
+
clean_name = clean_class_name(predicted_class)
|
| 268 |
+
confidence_level = get_confidence_level(confidence)
|
| 269 |
|
| 270 |
+
# Get top 5 predictions
|
| 271 |
+
top_indices = np.argsort(predictions[0])[-5:][::-1]
|
| 272 |
all_predictions = {
|
| 273 |
+
clean_class_name(CLASS_NAMES[idx]): float(predictions[0][idx])
|
| 274 |
+
for idx in top_indices
|
| 275 |
}
|
| 276 |
|
| 277 |
+
# Get disease information
|
| 278 |
+
disease_data = disease_guide.get(predicted_class, {})
|
| 279 |
+
disease_info = DiseaseInfo(**disease_data) if disease_data else DiseaseInfo(
|
| 280 |
+
crop="Unknown",
|
| 281 |
+
description="No information available for this class"
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
# Generate recommendations
|
| 285 |
+
recommendations = get_recommendations(predicted_class, confidence, disease_data)
|
| 286 |
+
|
| 287 |
+
# Determine message based on confidence
|
| 288 |
+
if confidence < CONFIDENCE_THRESHOLD:
|
| 289 |
+
message = HTTP_MESSAGES["LOW_CONFIDENCE"]
|
| 290 |
+
else:
|
| 291 |
+
message = "Prediction completed successfully"
|
| 292 |
+
|
| 293 |
return PredictionResponse(
|
| 294 |
success=True,
|
| 295 |
predicted_class=predicted_class,
|
| 296 |
+
clean_class_name=clean_name,
|
| 297 |
confidence=confidence,
|
| 298 |
+
confidence_level=confidence_level,
|
| 299 |
all_predictions=all_predictions,
|
| 300 |
+
disease_info=disease_info,
|
| 301 |
+
recommendations=recommendations,
|
| 302 |
+
message=message
|
| 303 |
)
|
| 304 |
|
| 305 |
+
except HTTPException:
|
| 306 |
+
raise
|
| 307 |
except Exception as e:
|
| 308 |
logger.error(f"Prediction failed: {str(e)}")
|
| 309 |
raise HTTPException(
|
|
|
|
| 313 |
|
| 314 |
def is_image_file(filename: str) -> bool:
|
| 315 |
"""Check if file is an image based on extension"""
|
| 316 |
+
if not filename:
|
| 317 |
+
return False
|
| 318 |
image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'}
|
| 319 |
return any(filename.lower().endswith(ext) for ext in image_extensions)
|
| 320 |
|
|
|
|
| 322 |
async def lifespan(app: FastAPI):
|
| 323 |
"""Handle startup and shutdown events"""
|
| 324 |
# Startup
|
| 325 |
+
global model, disease_guide
|
| 326 |
try:
|
| 327 |
+
logger.info("Starting up... Loading disease guide and model")
|
| 328 |
+
|
| 329 |
+
# Load disease guide
|
| 330 |
+
disease_guide = load_disease_guide()
|
| 331 |
+
|
| 332 |
+
# Load model
|
| 333 |
model = load_model()
|
| 334 |
|
| 335 |
# Pre-warm the model with a dummy prediction
|
|
|
|
| 338 |
logger.info("Model pre-warmed successfully")
|
| 339 |
|
| 340 |
except Exception as e:
|
| 341 |
+
logger.error(f"Failed to initialize during startup: {str(e)}")
|
| 342 |
model = None
|
| 343 |
|
| 344 |
yield
|
|
|
|
| 349 |
# Create FastAPI app
|
| 350 |
app = FastAPI(
|
| 351 |
title="Plant Disease Prediction API",
|
| 352 |
+
description="API for predicting plant diseases from leaf images using deep learning",
|
| 353 |
+
version="2.0.0",
|
| 354 |
+
lifespan=lifespan,
|
| 355 |
+
docs_url="/docs",
|
| 356 |
+
redoc_url="/redoc"
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
# Add CORS middleware
|
| 360 |
+
app.add_middleware(
|
| 361 |
+
CORSMiddleware,
|
| 362 |
+
allow_origins=["*"], # Configure appropriately for production
|
| 363 |
+
allow_credentials=True,
|
| 364 |
+
allow_methods=["*"],
|
| 365 |
+
allow_headers=["*"],
|
| 366 |
)
|
| 367 |
|
| 368 |
@app.get("/", response_model=HealthResponse)
|
|
|
|
| 371 |
return HealthResponse(
|
| 372 |
status="running",
|
| 373 |
model_loaded=model is not None,
|
| 374 |
+
total_classes=len(CLASS_NAMES),
|
| 375 |
+
available_diseases=len([d for d in disease_guide.values() if d.get("disease_name")]),
|
| 376 |
message="Plant Disease Prediction API is running"
|
| 377 |
)
|
| 378 |
|
|
|
|
| 382 |
return HealthResponse(
|
| 383 |
status="healthy" if model is not None else "unhealthy",
|
| 384 |
model_loaded=model is not None,
|
| 385 |
+
total_classes=len(CLASS_NAMES),
|
| 386 |
+
available_diseases=len([d for d in disease_guide.values() if d.get("disease_name")]),
|
| 387 |
message=HTTP_MESSAGES["MODEL_LOAD_SUCCESS"] if model is not None else HTTP_MESSAGES["MODEL_NOT_LOADED"]
|
| 388 |
)
|
| 389 |
|
|
|
|
| 392 |
"""
|
| 393 |
Predict plant disease from uploaded image
|
| 394 |
|
| 395 |
+
- **file**: Single image file to analyze (max 10MB)
|
| 396 |
|
| 397 |
+
Returns comprehensive prediction with confidence score, disease information, and recommendations
|
| 398 |
"""
|
| 399 |
|
| 400 |
+
# Validate file
|
| 401 |
+
if not file.filename:
|
| 402 |
+
raise HTTPException(
|
| 403 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 404 |
+
detail="No filename provided"
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
if not is_image_file(file.filename):
|
| 408 |
raise HTTPException(
|
| 409 |
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
| 427 |
detail=HTTP_MESSAGES["IMAGE_PROCESSING_FAILED"].format(error=str(e))
|
| 428 |
)
|
| 429 |
|
| 430 |
+
@app.get("/diseases", response_model=List[SearchResult])
|
| 431 |
+
async def get_all_plant_diseases(
|
| 432 |
+
crop: Optional[str] = Query(None, description="Filter by crop name (e.g. Apple, Tomato)"),
|
| 433 |
+
disease_type: Optional[str] = Query(None, description="Filter by disease type (Fungal, Bacterial, Viral)"),
|
| 434 |
+
risk_level: Optional[str] = Query(None, description="Filter by risk level (High, Medium, Low)")
|
| 435 |
+
):
|
| 436 |
+
"""
|
| 437 |
+
Get all plant diseases with optional filtering
|
| 438 |
+
"""
|
| 439 |
+
diseases = []
|
| 440 |
+
|
| 441 |
+
for class_name, info in disease_guide.items():
|
| 442 |
+
if not info.get("disease_name"):
|
| 443 |
+
continue # Skip healthy entries
|
| 444 |
+
|
| 445 |
+
# Apply filters
|
| 446 |
+
if crop and info.get("crop", "").lower() != crop.lower():
|
| 447 |
+
continue
|
| 448 |
+
if disease_type and info.get("type", "").lower() != disease_type.lower():
|
| 449 |
+
continue
|
| 450 |
+
if risk_level and info.get("risk_level", "").lower() != risk_level.lower():
|
| 451 |
+
continue
|
| 452 |
+
|
| 453 |
+
diseases.append(SearchResult(
|
| 454 |
+
class_name=class_name,
|
| 455 |
+
disease_info=DiseaseInfo(**info)
|
| 456 |
+
))
|
| 457 |
+
|
| 458 |
+
return diseases
|
| 459 |
+
|
| 460 |
+
@app.get("/search", response_model=SearchResponse)
|
| 461 |
+
async def search_diseases(
|
| 462 |
+
query: str = Query(..., min_length=1, description="Search term"),
|
| 463 |
+
limit: int = Query(10, ge=1, le=50, description="Maximum number of results")
|
| 464 |
+
):
|
| 465 |
+
"""
|
| 466 |
+
Search plant diseases with fuzzy matching and relevance scoring
|
| 467 |
+
"""
|
| 468 |
+
query_lower = query.lower()
|
| 469 |
+
exact_matches = []
|
| 470 |
+
fuzzy_candidates = []
|
| 471 |
+
|
| 472 |
+
for class_name, info in disease_guide.items():
|
| 473 |
+
if not info.get("disease_name"):
|
| 474 |
+
continue
|
| 475 |
+
|
| 476 |
+
# Build searchable text
|
| 477 |
+
searchable_text = " ".join([
|
| 478 |
+
class_name,
|
| 479 |
+
info.get("disease_name", ""),
|
| 480 |
+
info.get("description", ""),
|
| 481 |
+
info.get("crop", ""),
|
| 482 |
+
info.get("type", ""),
|
| 483 |
+
" ".join(info.get("symptoms", [])),
|
| 484 |
+
" ".join(info.get("common_names", []))
|
| 485 |
+
]).lower()
|
| 486 |
+
|
| 487 |
+
# Check for exact substring matches
|
| 488 |
+
if query_lower in searchable_text:
|
| 489 |
+
exact_matches.append(SearchResult(
|
| 490 |
+
class_name=class_name,
|
| 491 |
+
disease_info=DiseaseInfo(**info)
|
| 492 |
+
))
|
| 493 |
+
else:
|
| 494 |
+
fuzzy_candidates.append((class_name, info, searchable_text))
|
| 495 |
+
|
| 496 |
+
# If we have exact matches, return them
|
| 497 |
+
if exact_matches:
|
| 498 |
+
return SearchResponse(
|
| 499 |
+
results=exact_matches[:limit],
|
| 500 |
+
total_results=len(exact_matches),
|
| 501 |
+
message=f"Found {len(exact_matches)} exact matches"
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
# Fuzzy search on candidates
|
| 505 |
+
search_texts = [text for _, _, text in fuzzy_candidates]
|
| 506 |
+
fuzzy_matches = process.extract(
|
| 507 |
+
query, search_texts, scorer=fuzz.token_sort_ratio, limit=limit
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
suggestions = []
|
| 511 |
+
for match_text, score, idx in fuzzy_matches:
|
| 512 |
+
if score > 60: # Minimum relevance threshold
|
| 513 |
+
class_name, info, _ = fuzzy_candidates[idx]
|
| 514 |
+
suggestions.append(SearchResult(
|
| 515 |
+
class_name=class_name,
|
| 516 |
+
disease_info=DiseaseInfo(**info),
|
| 517 |
+
relevance_score=score
|
| 518 |
+
))
|
| 519 |
+
|
| 520 |
+
return SearchResponse(
|
| 521 |
+
results=[],
|
| 522 |
+
suggestions=suggestions,
|
| 523 |
+
total_results=len(suggestions),
|
| 524 |
+
message="No exact matches found. Showing relevant suggestions." if suggestions else "No matches found."
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
@app.get("/diseases/{class_name}", response_model=SearchResult)
|
| 528 |
+
async def get_disease_by_class_name(
|
| 529 |
+
class_name: str = Path(..., description="Exact class name, e.g. Apple___Apple_scab")
|
| 530 |
+
):
|
| 531 |
+
"""
|
| 532 |
+
Retrieve detailed information for a specific disease class
|
| 533 |
+
"""
|
| 534 |
+
disease_data = disease_guide.get(class_name)
|
| 535 |
+
|
| 536 |
+
if not disease_data:
|
| 537 |
+
raise HTTPException(
|
| 538 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 539 |
+
detail=f"Disease with class name '{class_name}' not found."
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
return SearchResult(
|
| 543 |
+
class_name=class_name,
|
| 544 |
+
disease_info=DiseaseInfo(**disease_data)
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
@app.get("/stats")
|
| 548 |
+
async def get_api_stats():
|
| 549 |
+
"""Get API statistics and supported classes"""
|
| 550 |
+
crops = set()
|
| 551 |
+
disease_types = set()
|
| 552 |
+
risk_levels = set()
|
| 553 |
+
|
| 554 |
+
for info in disease_guide.values():
|
| 555 |
+
if info.get("crop"):
|
| 556 |
+
crops.add(info["crop"])
|
| 557 |
+
if info.get("type"):
|
| 558 |
+
disease_types.add(info["type"])
|
| 559 |
+
if info.get("risk_level"):
|
| 560 |
+
risk_levels.add(info["risk_level"])
|
| 561 |
+
|
| 562 |
return {
|
| 563 |
+
"total_classes": len(CLASS_NAMES),
|
| 564 |
+
"diseases_in_guide": len([d for d in disease_guide.values() if d.get("disease_name")]),
|
| 565 |
+
"healthy_classes": len([d for d in disease_guide.values() if not d.get("disease_name")]),
|
| 566 |
+
"supported_crops": sorted(list(crops)),
|
| 567 |
+
"disease_types": sorted(list(disease_types)),
|
| 568 |
+
"risk_levels": sorted(list(risk_levels)),
|
| 569 |
+
"model_loaded": model is not None
|
| 570 |
}
|
| 571 |
|
| 572 |
if __name__ == "__main__":
|
| 573 |
import uvicorn
|
| 574 |
+
uvicorn.run(app, host="0.0.0.0", port=8000, reload=False)
|
requirements.txt
CHANGED
|
Binary files a/requirements.txt and b/requirements.txt differ
|
|
|
todo.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## ✅ Completed
|
| 2 |
+
|
| 3 |
+
- [x] Apple\_\_\_Apple_scab
|
| 4 |
+
- [x] Apple\_\_\_Black_rot
|
| 5 |
+
- [x] Apple\_\_\_Cedar_apple_rust
|
| 6 |
+
- [x] Apple\_\_\_healthy
|
| 7 |
+
- [x] Tomato\_\_\_Late_blight
|
| 8 |
+
|
| 9 |
+
## ⏳ To Do
|
| 10 |
+
|
| 11 |
+
- [ ] Blueberry\_\_\_healthy
|
| 12 |
+
- [ ] Cherry\_(including_sour)\_\_\_Powdery_mildew
|
| 13 |
+
- [ ] Cherry\_(including_sour)\_\_\_healthy
|
| 14 |
+
- [ ] Corn\_(maize)\_\_\_Cercospora_leaf_spot Gray_leaf_spot
|
| 15 |
+
- [ ] Corn\_(maize)_\_\_Common_rust_
|
| 16 |
+
- [ ] Corn\_(maize)\_\_\_Northern_Leaf_Blight
|
| 17 |
+
- [ ] Corn\_(maize)\_\_\_healthy
|
| 18 |
+
- [ ] Grape\_\_\_Black_rot
|
| 19 |
+
- [ ] Grape*\_\_Esca*(Black_Measles)
|
| 20 |
+
- [ ] Grape*\_\_Leaf_blight*(Isariopsis_Leaf_Spot)
|
| 21 |
+
- [ ] Grape\_\_\_healthy
|
| 22 |
+
- [ ] Orange*\_\_Haunglongbing*(Citrus_greening)
|
| 23 |
+
- [ ] Peach\_\_\_Bacterial_spot
|
| 24 |
+
- [ ] Peach\_\_\_healthy
|
| 25 |
+
- [ ] Pepper,\_bell\_\_\_Bacterial_spot
|
| 26 |
+
- [ ] Pepper,\_bell\_\_\_healthy
|
| 27 |
+
- [ ] Potato\_\_\_Early_blight
|
| 28 |
+
- [ ] Potato\_\_\_Late_blight
|
| 29 |
+
- [ ] Potato\_\_\_healthy
|
| 30 |
+
- [ ] Raspberry\_\_\_healthy
|
| 31 |
+
- [ ] Soybean\_\_\_healthy
|
| 32 |
+
- [ ] Squash\_\_\_Powdery_mildew
|
| 33 |
+
- [ ] Strawberry\_\_\_Leaf_scorch
|
| 34 |
+
- [ ] Strawberry\_\_\_healthy
|
| 35 |
+
- [ ] Tomato\_\_\_Bacterial_spot
|
| 36 |
+
- [ ] Tomato\_\_\_Early_blight
|
| 37 |
+
- [ ] Tomato\_\_\_Leaf_Mold
|
| 38 |
+
- [ ] Tomato\_\_\_Septoria_leaf_spot
|
| 39 |
+
- [ ] Tomato\_\_\_Spider_mites Two-spotted_spider_mite
|
| 40 |
+
- [ ] Tomato\_\_\_Target_Spot
|
| 41 |
+
- [ ] Tomato\_\_\_Tomato_Yellow_Leaf_Curl_Virus
|
| 42 |
+
- [ ] Tomato\_\_\_Tomato_mosaic_virus
|
| 43 |
+
- [ ] Tomato\_\_\_healthy
|