yasyn14 commited on
Commit
e37f7e7
·
1 Parent(s): 82bac8f

change main.py and added json

Browse files
Files changed (4) hide show
  1. disease_guide.json +119 -0
  2. main.py +351 -42
  3. requirements.txt +0 -0
  4. 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
- all_predictions: dict
 
 
 
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
- logger.info("Model loaded successfully")
 
 
 
 
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
- # Open image from bytes
 
 
 
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 class names
141
  predicted_class = CLASS_NAMES[predicted_class_idx]
142
- clean_class_name = CLEAN_CLASS_NAMES[predicted_class_idx]
 
143
 
144
- # Create all predictions dictionary
 
145
  all_predictions = {
146
- CLEAN_CLASS_NAMES[i]: float(predictions[0][i])
147
- for i in range(len(CLASS_NAMES))
148
  }
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  return PredictionResponse(
151
  success=True,
152
  predicted_class=predicted_class,
153
- clean_class_name=clean_class_name,
154
  confidence=confidence,
 
155
  all_predictions=all_predictions,
156
- message="Image processed successfully"
 
 
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 load model during startup: {str(e)}")
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 a single leaf image using deep learning",
198
- version="1.0.0",
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 for the image
228
  """
229
 
230
- # Check if file is an image
 
 
 
 
 
 
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("/classes")
255
- async def get_classes():
256
- """Get all available plant disease classes"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  return {
258
- "classes": CLASS_NAMES,
259
- "clean_classes": CLEAN_CLASS_NAMES,
260
- "total_classes": len(CLASS_NAMES)
 
 
 
 
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