Green Doctor Deployer commited on
Commit
b495fe1
·
1 Parent(s): 9307f58

Finalized accuracy fixes and linked to production Hugging Face backend

Browse files
backend/__pycache__/app.cpython-312.pyc CHANGED
Binary files a/backend/__pycache__/app.cpython-312.pyc and b/backend/__pycache__/app.cpython-312.pyc differ
 
backend/__pycache__/model_manifest.cpython-312.pyc CHANGED
Binary files a/backend/__pycache__/model_manifest.cpython-312.pyc and b/backend/__pycache__/model_manifest.cpython-312.pyc differ
 
backend/app.py CHANGED
@@ -37,61 +37,59 @@ app.add_middleware(
37
  # --- GLOBAL MODELS ---
38
  GENERAL_EXPERT = None
39
  PLANT_SPECIALIST = None
40
- PEST_EXPERT = None
41
 
42
  from .model_manifest import get_seasonal_experts
43
 
44
  def get_experts():
45
  from transformers import pipeline
46
- global GENERAL_EXPERT, PLANT_SPECIALIST, PEST_EXPERT
47
 
48
- if GENERAL_EXPERT is None or PLANT_SPECIALIST is None or PEST_EXPERT is None:
49
  manifest = get_seasonal_experts()
50
  print(f"Loading Multi-Expert AI Pipeline...")
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  for entry in manifest:
53
  try:
54
- # Provide strict image processor fallback to correct the HuggingFace missing preprocessor_config crash
55
- if entry['id'] == 'specialist':
56
- from transformers import AutoModelForImageClassification
57
- import torch
58
- import numpy as np
59
-
60
- class SpecialistPipeline:
61
- def __init__(self, model_name):
62
- # Force HuggingFace rebuild
63
- self.model = AutoModelForImageClassification.from_pretrained(model_name).to(torch.float32)
64
- self.model.eval()
65
- self.torch = torch
66
-
67
- def manual_image_processor(self, image):
68
- # Completely replaces AutoImageProcessor so torchvision is never required
69
- img = image.convert("RGB").resize((224, 224))
70
- img_array = np.array(img).astype(np.float32) / 255.0
71
- # MobileNetV2 uses strictly 0.5 means/stds
72
- mean = np.array([0.5, 0.5, 0.5])
73
- std = np.array([0.5, 0.5, 0.5])
74
- img_array = (img_array - mean) / std
75
- # HWC to CHW format required by PyTorch
76
- img_array = img_array.transpose(2, 0, 1)
77
- # Explicitly force float32 unconditionally
78
- return self.torch.tensor(img_array, dtype=self.torch.float32).unsqueeze(0)
79
-
80
- def __call__(self, image):
81
- inputs = self.manual_image_processor(image)
82
- with self.torch.no_grad():
83
- outputs = self.model(inputs)
84
- probs = self.torch.nn.functional.softmax(outputs.logits, dim=-1)[0]
85
- predicted_idx = probs.argmax().item()
86
- return [{"label": self.model.config.id2label[predicted_idx], "score": probs[predicted_idx].item()}]
87
-
88
- pipe = SpecialistPipeline(entry['model'])
89
- else:
90
- pipe = pipeline("image-classification", model=entry['model'])
91
 
92
  if entry['id'] == 'generalist': GENERAL_EXPERT = pipe
93
  elif entry['id'] == 'specialist': PLANT_SPECIALIST = pipe
94
- elif entry['id'] == 'pest': PEST_EXPERT = pipe
95
  print(f"Expert [{entry['name']}] Loaded.")
96
  except Exception as e:
97
  print(f"Expert {entry['id']} Failed: {e}")
@@ -100,7 +98,7 @@ def get_experts():
100
  torch.set_grad_enabled(False)
101
  gc.collect()
102
 
103
- return GENERAL_EXPERT, PLANT_SPECIALIST, PEST_EXPERT
104
 
105
  def focalize_leaf(image_bytes):
106
  import numpy as np
@@ -151,12 +149,51 @@ def focalize_leaf(image_bytes):
151
  return Image.open(io.BytesIO(image_bytes)).convert('RGB'), 0.0
152
 
153
  # --- MAPPINGS ---
154
- # Generalist Map (Covers Rice, Corn, Wheat, Potato, Tomato, etc.)
155
  GENERALIST_MAP = {
156
  'Apple___Apple_scab': 'Apple - Scab',
157
  'Apple___Black_rot': 'Apple - Black Rot',
158
  'Apple___Cedar_apple_rust': 'Apple - Cedar Rust',
159
  'Apple___healthy': 'Apple - Healthy',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  'Corn___Common_Rust': 'Corn - Common Rust',
161
  'Corn___Gray_Leaf_Spot': 'Corn - Gray Leaf Spot',
162
  'Corn___Healthy': 'Corn - Healthy',
@@ -166,19 +203,10 @@ GENERALIST_MAP = {
166
  'Rice___Brown_Spot': 'Rice - Brown Spot',
167
  'Rice___Healthy': 'Rice - Healthy',
168
  'Rice___Leaf_Blast': 'Rice - Leaf Blast',
169
- 'Rice___Neck_Blast': 'Rice - Neck Blast',
170
- 'Tomato___Bacterial_Spot': 'Tomato - Bacterial Spot',
171
- 'Tomato___Early_Blight': 'Tomato - Early Blight',
172
- 'Tomato___Healthy': 'Tomato - Healthy',
173
- 'Tomato___Late_Blight': 'Tomato - Late Blight',
174
- 'Tomato___Leaf_Mold': 'Tomato - Leaf Mold',
175
- 'Tomato___Septoria_Leaf_Spot': 'Tomato - Septoria Spot',
176
- 'Tomato___Target_Spot': 'Tomato - Target Spot',
177
- 'Tomato___Yellow_Leaf_Curl_Virus': 'Tomato - Yellow Leaf Curl',
178
- 'Tomato___Mosaic_Virus': 'Tomato - Mosaic Virus',
179
  'Wheat___Brown_Rust': 'Wheat - Brown Rust',
180
  'Wheat___Healthy': 'Wheat - Healthy',
181
- 'Wheat___Yellow_Rust': 'Wheat - Yellow Rust'
 
182
  }
183
 
184
  # Specialist Map (PlantVillage 38 Classes)
@@ -235,7 +263,7 @@ async def predict(
235
  ):
236
  try:
237
  # Load Experts
238
- general_expert, plant_specialist, pest_expert = get_experts()
239
  contents = await file.read()
240
 
241
  # Quality Check
@@ -243,72 +271,104 @@ async def predict(
243
  if image is None:
244
  return {"class": "UNKNOWN", "confidence": 0.0, "ai_details": "No leaf detected (too low plant density).", "status": "success"}
245
 
246
- # STEP 1: Generalist Query (The "Broad Reach" model)
 
247
  gen_res = general_expert(image) if general_expert else []
 
248
  spec_res = plant_specialist(image) if plant_specialist else []
249
- pest_res = pest_expert(image) if pest_expert else []
250
-
251
- best_prediction = {"class": "UNKNOWN", "score": 0.0, "expert": "none"}
252
 
253
- # LOGIC:
254
- # 1. If Generalist is VERY confident in a specific disease, trust it.
255
- # 2. If Generalist detects a plant type but not sure of disease, ask Specialist.
256
- # 3. If Specialist is confident in a known class, trust Specialist (it has better granularity for those 38).
257
- # 4. If nothing is confident, try Pest.
258
  gen_class, gen_score = None, 0.0
259
  if gen_res:
260
  label = gen_res[0]['label']
261
- if GENERALIST_MAP.get(label):
262
- gen_class = GENERALIST_MAP.get(label)
263
- gen_score = gen_res[0]['score']
264
 
265
  spec_class, spec_score = None, 0.0
266
  if spec_res:
267
- s_label = spec_res[0]['label']
268
- if SPECIALIST_MAP.get(s_label):
269
- spec_class = SPECIALIST_MAP.get(s_label)
270
- spec_score = spec_res[0]['score']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
 
272
  best_prediction = {"class": "UNKNOWN", "score": 0.0, "expert": "none"}
273
 
274
- # Conflict Resolution Engine:
275
- best_prediction = None
276
-
277
- # Crops that the 38-class Specialist model is trained explicitly to handle.
278
- # If Generalist thinks it's one of these, we completely defer to the Specialist's diagnosis.
279
- specialist_crops = ["Tomato", "Potato", "Corn", "Apple", "Grape", "Peach", "Pepper", "Strawberry", "Cherry", "Blueberry", "Orange", "Raspberry", "Soybean", "Squash"]
280
-
281
- gen_crop_base = gen_class.split(' - ')[0] if gen_class else ""
282
 
 
283
  if gen_class and spec_class:
284
- if gen_crop_base in specialist_crops:
285
- # AGGRESSIVE OVERRIDE: 38-class model probabilities are naturally spread thin.
286
- # We do not require > 0.4 confidence if we know it's a Specialist crop.
287
- best_prediction = {"class": spec_class, "score": spec_score, "expert": "Specialist (Forced Override)"}
288
- elif spec_score > 0.4:
289
- best_prediction = {"class": spec_class, "score": spec_score, "expert": "Specialist"}
290
-
291
- # If both failed the threshold but we got a valid response, return the highest scoring one anyway as fallback
292
- if not best_prediction:
293
- if spec_score > gen_score * 1.5: # If specialist is very confident relatively
294
- best_prediction = {"class": spec_class, "score": spec_score, "expert": "Specialist"}
295
  else:
296
- best_prediction = {"class": gen_class or "UNKNOWN", "score": gen_score or 0.0, "expert": "Generalist"}
297
-
 
 
 
298
  elif spec_class:
299
- best_prediction = {"class": spec_class, "score": spec_score, "expert": "Specialist"}
300
  elif gen_class:
301
- best_prediction = {"class": gen_class, "score": gen_score, "expert": "Generalist"}
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
- # Fallback: Pest expert
304
- if pest_res:
305
- p_score = pest_res[0]['score']
306
- if p_score > 0.40 and p_score > best_prediction['score']:
307
- best_prediction = {"class": f"Pest: {pest_res[0]['label']}", "score": p_score, "expert": "Entomology"}
308
 
309
- # Final Handling for "Random Images"
310
- if best_prediction['score'] < 0.12:
311
- best_prediction = {"class": "UNKNOWN", "score": best_prediction['score'], "expert": "System Rejection"}
 
 
 
312
 
313
  final_class = best_prediction['class']
314
  print(f"Prediction: {final_class} ({best_prediction['score']*100:.1f}%) via {best_prediction['expert']}")
 
37
  # --- GLOBAL MODELS ---
38
  GENERAL_EXPERT = None
39
  PLANT_SPECIALIST = None
40
+ CEREAL_EXPERT = None
41
 
42
  from .model_manifest import get_seasonal_experts
43
 
44
  def get_experts():
45
  from transformers import pipeline
46
+ global GENERAL_EXPERT, PLANT_SPECIALIST, CEREAL_EXPERT
47
 
48
+ if GENERAL_EXPERT is None or PLANT_SPECIALIST is None or CEREAL_EXPERT is None:
49
  manifest = get_seasonal_experts()
50
  print(f"Loading Multi-Expert AI Pipeline...")
51
 
52
+ class ManualPipeline:
53
+ def __init__(self, model_name):
54
+ from transformers import AutoModelForImageClassification
55
+ self.model = AutoModelForImageClassification.from_pretrained(model_name).to(torch.float32)
56
+ self.model.eval()
57
+ self.torch = torch
58
+
59
+ def manual_image_processor(self, image):
60
+ import numpy as np
61
+ # Standardization for PlantVillage models (0.5 mean/std)
62
+ img = image.convert("RGB").resize((224, 224))
63
+ img_array = np.array(img).astype(np.float32) / 255.0
64
+ mean = np.array([0.5, 0.5, 0.5])
65
+ std = np.array([0.5, 0.5, 0.5])
66
+ img_array = (img_array - mean) / std
67
+ img_array = img_array.transpose(2, 0, 1)
68
+ return self.torch.tensor(img_array, dtype=self.torch.float32).unsqueeze(0)
69
+
70
+ def __call__(self, image):
71
+ inputs = self.manual_image_processor(image)
72
+ with self.torch.no_grad():
73
+ outputs = self.model(inputs)
74
+ probs = self.torch.nn.functional.softmax(outputs.logits, dim=-1)[0]
75
+ top_probs, top_indices = self.torch.topk(probs, 5)
76
+ results = []
77
+ for i in range(5):
78
+ idx = top_indices[i].item()
79
+ results.append({
80
+ "label": self.model.config.id2label[idx],
81
+ "score": top_probs[i].item()
82
+ })
83
+ return results
84
+
85
  for entry in manifest:
86
  try:
87
+ # All models in this pipeline (DeiT, MobileNet, ViT-tiny) use the same 224x224 0.5 normalization
88
+ pipe = ManualPipeline(entry['model'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  if entry['id'] == 'generalist': GENERAL_EXPERT = pipe
91
  elif entry['id'] == 'specialist': PLANT_SPECIALIST = pipe
92
+ elif entry['id'] == 'cereal': CEREAL_EXPERT = pipe
93
  print(f"Expert [{entry['name']}] Loaded.")
94
  except Exception as e:
95
  print(f"Expert {entry['id']} Failed: {e}")
 
98
  torch.set_grad_enabled(False)
99
  gc.collect()
100
 
101
+ return GENERAL_EXPERT, PLANT_SPECIALIST, CEREAL_EXPERT
102
 
103
  def focalize_leaf(image_bytes):
104
  import numpy as np
 
149
  return Image.open(io.BytesIO(image_bytes)).convert('RGB'), 0.0
150
 
151
  # --- MAPPINGS ---
152
+ # Generalist Map (DeiT Anchor - 38 Classes)
153
  GENERALIST_MAP = {
154
  'Apple___Apple_scab': 'Apple - Scab',
155
  'Apple___Black_rot': 'Apple - Black Rot',
156
  'Apple___Cedar_apple_rust': 'Apple - Cedar Rust',
157
  'Apple___healthy': 'Apple - Healthy',
158
+ 'Blueberry___healthy': 'Blueberry - Healthy',
159
+ 'Cherry___Powdery_mildew': 'Cherry - Powdery Mildew',
160
+ 'Cherry___healthy': 'Cherry - Healthy',
161
+ 'Corn___Cercospora_leaf_spot Gray_leaf_spot': 'Corn - Gray Leaf Spot',
162
+ 'Corn___Common_rust': 'Corn - Common Rust',
163
+ 'Corn___Northern_Leaf_Blight': 'Corn - Northern Leaf Blight',
164
+ 'Corn___healthy': 'Corn - Healthy',
165
+ 'Grape___Black_rot': 'Grape - Black Rot',
166
+ 'Grape___Esca_(Black_Measles)': 'Grape - Black Measles',
167
+ 'Grape___Leaf_blight_(Isariopsis_Leaf_Spot)': 'Grape - Leaf Blight',
168
+ 'Grape___healthy': 'Grape - Healthy',
169
+ 'Orange___Haunglongbing_(Citrus_greening)': 'Orange - Citrus Greening',
170
+ 'Peach___Bacterial_spot': 'Peach - Bacterial Spot',
171
+ 'Peach___healthy': 'Peach - Healthy',
172
+ 'Pepper,_bell___Bacterial_spot': 'Pepper - Bacterial Spot',
173
+ 'Pepper,_bell___healthy': 'Pepper - Healthy',
174
+ 'Potato___Early_blight': 'Potato - Early Blight',
175
+ 'Potato___Late_blight': 'Potato - Late Blight',
176
+ 'Potato___healthy': 'Potato - Healthy',
177
+ 'Raspberry___healthy': 'Raspberry - Healthy',
178
+ 'Soybean___healthy': 'Soybean - Healthy',
179
+ 'Squash___Powdery_mildew': 'Squash - Powdery Mildew',
180
+ 'Strawberry___Leaf_scorch': 'Strawberry - Leaf Scorch',
181
+ 'Strawberry___healthy': 'Strawberry - Healthy',
182
+ 'Tomato___Bacterial_spot': 'Tomato - Bacterial Spot',
183
+ 'Tomato___Early_blight': 'Tomato - Early Blight',
184
+ 'Tomato___Late_blight': 'Tomato - Late Blight',
185
+ 'Tomato___Leaf_Mold': 'Tomato - Leaf Mold',
186
+ 'Tomato___Septoria_leaf_spot': 'Tomato - Septoria Spot',
187
+ 'Tomato___Spider_mites Two-spotted_spider_mite': 'Tomato - Spider Mite',
188
+ 'Tomato___Target_Spot': 'Tomato - Target Spot',
189
+ 'Tomato___Tomato_Yellow_Leaf_Curl_Virus': 'Tomato - Yellow Leaf Curl',
190
+ 'Tomato___Tomato_mosaic_virus': 'Tomato - Mosaic Virus',
191
+ 'Tomato___healthy': 'Tomato - Healthy',
192
+ 'Background_without_leaves': 'INVALID'
193
+ }
194
+
195
+ # Cereal Specialist Map (Multi-Crop - Rice/Wheat/Corn)
196
+ CEREAL_MAP = {
197
  'Corn___Common_Rust': 'Corn - Common Rust',
198
  'Corn___Gray_Leaf_Spot': 'Corn - Gray Leaf Spot',
199
  'Corn___Healthy': 'Corn - Healthy',
 
203
  'Rice___Brown_Spot': 'Rice - Brown Spot',
204
  'Rice___Healthy': 'Rice - Healthy',
205
  'Rice___Leaf_Blast': 'Rice - Leaf Blast',
 
 
 
 
 
 
 
 
 
 
206
  'Wheat___Brown_Rust': 'Wheat - Brown Rust',
207
  'Wheat___Healthy': 'Wheat - Healthy',
208
+ 'Wheat___Yellow_Rust': 'Wheat - Yellow Rust',
209
+ 'Invalid': 'INVALID'
210
  }
211
 
212
  # Specialist Map (PlantVillage 38 Classes)
 
263
  ):
264
  try:
265
  # Load Experts
266
+ general_expert, plant_specialist, cereal_expert = get_experts()
267
  contents = await file.read()
268
 
269
  # Quality Check
 
271
  if image is None:
272
  return {"class": "UNKNOWN", "confidence": 0.0, "ai_details": "No leaf detected (too low plant density).", "status": "success"}
273
 
274
+ # STEP 1: Parallel Inference
275
+ # Generalist (ViT Anchor)
276
  gen_res = general_expert(image) if general_expert else []
277
+ # Specialist (Pathology Expert)
278
  spec_res = plant_specialist(image) if plant_specialist else []
279
+ # Cereal Expert (Fallback for Rice/Wheat)
280
+ cer_res = cereal_expert(image) if cereal_expert else []
 
281
 
282
+ # --- SPECIES CONSENSUS ENGINE ---
283
+ # We use the Generalist (ViT) to "anchor" the species because ViT is better at global shape.
 
 
 
284
  gen_class, gen_score = None, 0.0
285
  if gen_res:
286
  label = gen_res[0]['label']
287
+ gen_class = GENERALIST_MAP.get(label)
288
+ gen_score = gen_res[0]['score']
 
289
 
290
  spec_class, spec_score = None, 0.0
291
  if spec_res:
292
+ # Anchor Logic: If Generalist is confident in a species, prioritize Specialist's labels from THAT species.
293
+ gen_species = gen_class.split(' - ')[0] if gen_class and gen_class != "INVALID" else None
294
+
295
+ # --- HARDENED ANCHORING ---
296
+ best_spec_match = None
297
+ # Scan top 5 from specialist for a species match
298
+ for s in spec_res:
299
+ mapped = SPECIALIST_MAP.get(s['label'])
300
+ if mapped:
301
+ spec_species = mapped.split(' - ')[0]
302
+ # STRICK CONSTRAIN: If Anchor is very confident in a species, we ONLY accept that species from Specialist
303
+ if gen_species and gen_score > 0.4:
304
+ if spec_species == gen_species:
305
+ best_spec_match = (mapped, s['score'])
306
+ break
307
+ else:
308
+ # If anchor is weak, take the first mapped specialist result
309
+ best_spec_match = (mapped, s['score'])
310
+ break
311
+
312
+ if best_spec_match:
313
+ spec_class, spec_score = best_spec_match
314
+
315
+ cer_class, cer_score = None, 0.0
316
+ if cer_res:
317
+ label = cer_res[0]['label']
318
+ cer_class = CEREAL_MAP.get(label)
319
+ cer_score = cer_res[0]['score']
320
 
321
+ # --- CONFLICT RESOLUTION ---
322
  best_prediction = {"class": "UNKNOWN", "score": 0.0, "expert": "none"}
323
 
324
+ # 1. Check for Background/Invalid rejection
325
+ if gen_class == "INVALID" and gen_score > 0.6:
326
+ return {"class": "INVALID", "confidence": gen_score, "ai_details": "Image identified as non-plant background.", "status": "success"}
 
 
 
 
 
327
 
328
+ # 2. Priority 1: Specialist/Generalist Hybrid (Most Crops)
329
  if gen_class and spec_class:
330
+ gen_species = gen_class.split(' - ')[0] if gen_class != "INVALID" else None
331
+ spec_species = spec_class.split(' - ')[0]
332
+
333
+ if gen_species == spec_species:
334
+ # AGREEMENT: Use whichever score is higher, boosted by consensus
335
+ best_prediction = {"class": spec_class, "score": max(gen_score, spec_score) * 1.05, "expert": "Consensus (ViT+MobileNet)"}
336
+ elif gen_score > 0.6:
337
+ # DISAGREEMENT BUT ANCHOR IS CONFIDENT: Trust Anchor species identify
338
+ # If we forced a spec_match above and it failed, spec_class might be mismatched.
339
+ # We prioritize Anchor here to fix the Tomato/Pepper confusion.
340
+ best_prediction = {"class": gen_class, "score": gen_score, "expert": "Vision Anchor Overrule"}
341
  else:
342
+ # Low confidence consensus
343
+ if spec_score > gen_score:
344
+ best_prediction = {"class": spec_class, "score": spec_score, "expert": "Pathology Specialist"}
345
+ else:
346
+ best_prediction = {"class": gen_class, "score": gen_score, "expert": "Vision Anchor"}
347
  elif spec_class:
348
+ best_prediction = {"class": spec_class, "score": spec_score, "expert": "Pathology Specialist"}
349
  elif gen_class:
350
+ best_prediction = {"class": gen_class, "score": gen_score, "expert": "Vision Anchor"}
351
+
352
+ # 3. Priority 2: Cereal Specialist (Rice/Wheat)
353
+ if cer_class and "Rice" in cer_class or "Wheat" in cer_class:
354
+ if cer_score > 0.4 and cer_score > best_prediction['score']:
355
+ best_prediction = {"class": cer_class, "score": cer_score, "expert": "Cereal Specialist"}
356
+
357
+ # 4. Final Rejection for low confidence
358
+ if best_prediction['score'] < 0.15:
359
+ best_prediction = {"class": "UNKNOWN", "score": best_prediction['score'], "expert": "System Confidence Low"}
360
+
361
+ final_class = best_prediction['class']
362
+ print(f"Prediction: {final_class} ({best_prediction['score']*100:.1f}%) via {best_prediction['expert']}")
363
 
364
+ log_scan(final_class, float(best_prediction['score']), latitude, longitude)
 
 
 
 
365
 
366
+ return {
367
+ "class": final_class,
368
+ "confidence": min(float(best_prediction['score']), 1.0),
369
+ "ai_details": f"Analysis complete via {best_prediction['expert']} (Plant Density: {density:.1f}%)",
370
+ "status": "success"
371
+ }
372
 
373
  final_class = best_prediction['class']
374
  print(f"Prediction: {final_class} ({best_prediction['score']*100:.1f}%) via {best_prediction['expert']}")
backend/download_models.py CHANGED
@@ -5,13 +5,20 @@ import os
5
  os.environ["TORCH_FORCE_WEIGHTS_ONLY_LOAD"] = "0"
6
 
7
  def download_models():
8
- print("Downloading General Expert (Swin Transformer)...")
9
  try:
10
- pipeline("image-classification", model="microsoft/swin-tiny-patch4-window7-224")
11
  print("General Expert Downloaded!")
12
  except Exception as e:
13
  print(f"General Expert Failed: {e}")
14
 
 
 
 
 
 
 
 
15
  print("Downloading Rice Expert (ViT)...")
16
  try:
17
  pipeline("image-classification", model="wambugu71/crop_leaf_diseases_vit")
 
5
  os.environ["TORCH_FORCE_WEIGHTS_ONLY_LOAD"] = "0"
6
 
7
  def download_models():
8
+ print("Downloading General Expert (DeiT Anchor)...")
9
  try:
10
+ pipeline("image-classification", model="rescu/deit-base-patch16-224-finetuned-plantvillage")
11
  print("General Expert Downloaded!")
12
  except Exception as e:
13
  print(f"General Expert Failed: {e}")
14
 
15
+ print("Downloading Legacy Swin Expert...")
16
+ try:
17
+ pipeline("image-classification", model="microsoft/swin-tiny-patch4-window7-224")
18
+ print("Legacy Swin Downloaded!")
19
+ except Exception as e:
20
+ print(f"Swin Expert Failed: {e}")
21
+
22
  print("Downloading Rice Expert (ViT)...")
23
  try:
24
  pipeline("image-classification", model="wambugu71/crop_leaf_diseases_vit")
backend/model_manifest.py CHANGED
@@ -2,7 +2,8 @@ from datetime import datetime
2
 
3
  def get_seasonal_experts():
4
  experts = [
5
- {"id": "generalist", "name": "Generalist Expert", "model": "wambugu71/crop_leaf_diseases_vit"},
6
- {"id": "specialist", "name": "Plant Health Specialist", "model": "linkanjarad/mobilenet_v2_1.0_224-plant-disease-identification"}
 
7
  ]
8
  return experts
 
2
 
3
  def get_seasonal_experts():
4
  experts = [
5
+ {"id": "generalist", "name": "Vision Anchor (DeiT)", "model": "rescu/deit-base-patch16-224-finetuned-plantvillage"},
6
+ {"id": "specialist", "name": "Pathology Specialist", "model": "linkanjarad/mobilenet_v2_1.0_224-plant-disease-identification"},
7
+ {"id": "cereal", "name": "Cereal Specialist", "model": "wambugu71/crop_leaf_diseases_vit"}
8
  ]
9
  return experts
backend/verify_fix.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+
4
+ # Mock dependencies for testing logic without loading weights
5
+ class MockExpert:
6
+ def __init__(self, results):
7
+ self.results = results
8
+ def __call__(self, image):
9
+ return self.results
10
+
11
+ def test_consensus():
12
+ # Simulate Tomato leaf with Bacterial Spot
13
+ # Specialist misidentifies as Pepper
14
+ spec_results = [
15
+ {'label': 'Bell Pepper with Bacterial Spot', 'score': 0.957},
16
+ {'label': 'Tomato with Bacterial Spot', 'score': 0.030}
17
+ ]
18
+ # Generalist (ViT) identifies as Tomato
19
+ gen_results = [
20
+ {'label': 'Tomato___Bacterial_spot', 'score': 0.88}
21
+ ]
22
+
23
+ # Mappings (subset for test)
24
+ GENERALIST_MAP = {
25
+ 'Tomato___Bacterial_spot': 'Tomato - Bacterial Spot',
26
+ 'Pepper,_bell___Bacterial_spot': 'Pepper - Bacterial Spot'
27
+ }
28
+ SPECIALIST_MAP = {
29
+ "Bell Pepper with Bacterial Spot": "Pepper - Bacterial Spot",
30
+ "Tomato with Bacterial Spot": "Tomato - Bacterial Spot"
31
+ }
32
+
33
+ # Extract results
34
+ gen_class = GENERALIST_MAP.get(gen_results[0]['label'])
35
+ gen_score = gen_results[0]['score']
36
+ gen_species = gen_class.split(' - ')[0]
37
+
38
+ spec_class = None
39
+ spec_score = 0.0
40
+ best_spec_match = None
41
+
42
+ # Anchor Logic
43
+ for s in spec_results:
44
+ mapped = SPECIALIST_MAP.get(s['label'])
45
+ if mapped:
46
+ spec_species = mapped.split(' - ')[0]
47
+ if gen_species and spec_species == gen_species:
48
+ best_spec_match = (mapped, s['score'])
49
+ break
50
+
51
+ if not best_spec_match:
52
+ for s in spec_results:
53
+ if SPECIALIST_MAP.get(s['label']):
54
+ best_spec_match = (SPECIALIST_MAP.get(s['label']), s['score'])
55
+ break
56
+
57
+ spec_class, spec_score = best_spec_match
58
+
59
+ print(f"Generalist says: {gen_class} ({gen_score})")
60
+ print(f"Specialist says: {spec_class} ({spec_score})")
61
+
62
+ # Conflict Resolution
63
+ if gen_class and spec_class:
64
+ gen_species = gen_class.split(' - ')[0]
65
+ spec_species = spec_class.split(' - ')[0]
66
+
67
+ if gen_species == spec_species:
68
+ res = {"class": spec_class, "score": max(gen_score, spec_score) * 1.05, "expert": "Consensus"}
69
+ elif gen_score > 0.7:
70
+ res = {"class": spec_class, "score": spec_score, "expert": "Vision Anchor Overrule"}
71
+ else:
72
+ res = {"class": spec_class if spec_score > gen_score else gen_class, "score": max(spec_score, gen_score)}
73
+
74
+ print(f"FINAL RESULT: {res['class']} via {res['expert']}")
75
+ assert res['class'] == "Tomato - Bacterial Spot"
76
+ print("TEST PASSED: Consensus logic correctly chose Tomato even though Specialist preferred Pepper.")
77
+
78
+ if __name__ == "__main__":
79
+ test_consensus()
raspberry_pi/app_streamlit.py CHANGED
@@ -72,6 +72,9 @@ if interpreter:
72
  # Camera Control
73
  run_cam = st.toggle("Enable Camera", value=True)
74
  cap = cv2.VideoCapture(0)
 
 
 
75
 
76
  while run_cam:
77
  ret, frame = cap.read()
@@ -79,41 +82,43 @@ if interpreter:
79
  st.warning("Failed to access camera.")
80
  break
81
 
82
- # 1. Preprocess
83
- # Convert BGR (OpenCV) to RGB (Model Expectation)
84
- img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
85
- img = cv2.resize(img_rgb, (width, height))
86
- img = img.astype(np.float32) / 255.0
87
- input_data = np.expand_dims(img, axis=0)
88
-
89
- # 2. Predict
90
- interpreter.set_tensor(input_details[0]['index'], input_data)
91
- interpreter.invoke()
92
- output_data = interpreter.get_tensor(output_details[0]['index'])
93
-
94
- pred_idx = np.argmax(output_data[0])
95
- confidence = output_data[0][pred_idx]
96
-
97
- # 3. UI Update
98
- label = "Searching..."
99
- color = "white"
100
-
101
- if confidence > 0.4:
102
- label = labels[pred_idx]
103
- color = "green" if "Healthy" in label else "red"
104
-
105
  # Display frame
106
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
107
  frame_placeholder.image(frame_rgb, channels="RGB")
108
-
109
- # Display Stats
110
- res_placeholder.markdown(f"### Diagnosis: **:{color}[{label}]**")
111
- conf_placeholder.progress(float(confidence), text=f"Confidence: {confidence*100:.1f}%")
112
-
113
- if color == "green":
114
- status_placeholder.success("Plant looks healthy!")
115
- elif color == "red":
116
- status_placeholder.error("Warning: Disease potential detected.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  cap.release()
119
  else:
 
72
  # Camera Control
73
  run_cam = st.toggle("Enable Camera", value=True)
74
  cap = cv2.VideoCapture(0)
75
+
76
+ import requests
77
+ import io
78
 
79
  while run_cam:
80
  ret, frame = cap.read()
 
82
  st.warning("Failed to access camera.")
83
  break
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  # Display frame
86
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
87
  frame_placeholder.image(frame_rgb, channels="RGB")
88
+
89
+ # 1. Prepare image for API
90
+ _, buffer = cv2.imencode('.jpg', frame)
91
+ img_bytes = buffer.tobytes()
92
+
93
+ # 2. Call Multi-Expert Backend
94
+ try:
95
+ # Point to local backend (assuming it's running on the same machine/network)
96
+ backend_url = "http://localhost:10000/predict"
97
+ response = requests.post(backend_url, files={"file": ("image.jpg", img_bytes, "image/jpeg")})
98
+
99
+ if response.status_code == 200:
100
+ data = response.json()
101
+ label = data['class']
102
+ confidence = data['confidence']
103
+ ai_details = data.get('ai_details', 'Multi-Expert Analysis')
104
+
105
+ color = "green" if "Healthy" in label else "red"
106
+ if label == "UNKNOWN" or label == "INVALID": color = "white"
107
+
108
+ # 3. UI Update
109
+ res_placeholder.markdown(f"### Diagnosis: **:{color}[{label}]**")
110
+ conf_placeholder.progress(float(confidence), text=f"Confidence: {confidence*100:.1f}%")
111
+
112
+ if color == "green":
113
+ status_placeholder.success(f"Plant looks healthy! ({ai_details})")
114
+ elif color == "red":
115
+ status_placeholder.error(f"Warning: Disease detected! ({ai_details})")
116
+ else:
117
+ status_placeholder.info(f"System: {label} ({ai_details})")
118
+ else:
119
+ status_placeholder.warning("Backend server not responding...")
120
+ except Exception as e:
121
+ status_placeholder.error(f"Connection Error: Ensure backend is running. ({e})")
122
 
123
  cap.release()
124
  else:
services/aiService.js CHANGED
@@ -12,7 +12,7 @@ const HF_API_TOKEN = ""; // <-- PASTE YOUR HUGGING FACE TOKEN HERE
12
  const KINDWISE_API_URL = "https://api.plant.id/v2/health";
13
  const KINDWISE_API_TOKEN = ""; // <-- PASTE YOUR KINDWISE API KEY HERE (https://web.plant.id/)
14
 
15
- // Option C: Custom Backend (Hugging Face / Render)
16
  const BACKEND_API_URL = "https://rhamprassath-greendoctor-backend.hf.space/predict";
17
  // ----------------------------------------------------------------------
18
 
 
12
  const KINDWISE_API_URL = "https://api.plant.id/v2/health";
13
  const KINDWISE_API_TOKEN = ""; // <-- PASTE YOUR KINDWISE API KEY HERE (https://web.plant.id/)
14
 
15
+ // Option C: Custom Backend (Hugging Face Production)
16
  const BACKEND_API_URL = "https://rhamprassath-greendoctor-backend.hf.space/predict";
17
  // ----------------------------------------------------------------------
18