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 +0 -0
- backend/__pycache__/model_manifest.cpython-312.pyc +0 -0
- backend/app.py +162 -102
- backend/download_models.py +9 -2
- backend/model_manifest.py +3 -2
- backend/verify_fix.py +79 -0
- raspberry_pi/app_streamlit.py +37 -32
- services/aiService.js +1 -1
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 |
-
|
| 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,
|
| 47 |
|
| 48 |
-
if GENERAL_EXPERT is None or PLANT_SPECIALIST is None or
|
| 49 |
manifest = get_seasonal_experts()
|
| 50 |
print(f"Loading Multi-Expert AI Pipeline...")
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
for entry in manifest:
|
| 53 |
try:
|
| 54 |
-
#
|
| 55 |
-
|
| 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'] == '
|
| 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,
|
| 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 (
|
| 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,
|
| 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:
|
|
|
|
| 247 |
gen_res = general_expert(image) if general_expert else []
|
|
|
|
| 248 |
spec_res = plant_specialist(image) if plant_specialist else []
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
best_prediction = {"class": "UNKNOWN", "score": 0.0, "expert": "none"}
|
| 252 |
|
| 253 |
-
#
|
| 254 |
-
#
|
| 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 |
-
|
| 262 |
-
|
| 263 |
-
gen_score = gen_res[0]['score']
|
| 264 |
|
| 265 |
spec_class, spec_score = None, 0.0
|
| 266 |
if spec_res:
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
|
|
|
| 272 |
best_prediction = {"class": "UNKNOWN", "score": 0.0, "expert": "none"}
|
| 273 |
|
| 274 |
-
#
|
| 275 |
-
|
| 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
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
else:
|
| 296 |
-
|
| 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": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
|
| 303 |
-
|
| 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 |
-
|
| 310 |
-
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 9 |
try:
|
| 10 |
-
pipeline("image-classification", model="
|
| 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": "
|
| 6 |
-
{"id": "specialist", "name": "
|
|
|
|
| 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 |
-
#
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
|