noivan commited on
Commit
dd4fe16
·
1 Parent(s): dcf4ce9

fix: load model from HF Hub parts (no upload needed, base EfficientNetV2-S)

Browse files
Files changed (1) hide show
  1. app.py +154 -232
app.py CHANGED
@@ -1,14 +1,11 @@
1
  """
2
- CropDoc HF Space — CPU Optimized Inference (v40 architecture)
3
- ==============================================================
4
- - EfficientNetV2-S FT v2+v3+v4 ensemble (38 classes)
5
- - TTA: Center + HFlip (2-view, CPU-optimized, was 4-view on GPU)
6
- - Class calibration: TLB x4, SLS x1.5, TLM x2.0
7
- - No GrabCut (CPU too slow), no Gemma4 (GPU required)
8
- - Model loaded from HF Hub (noivan/cropdoc)
9
  """
10
 
11
- import os, time, json
12
  import gradio as gr
13
  import torch
14
  import torch.nn as nn
@@ -16,7 +13,10 @@ import numpy as np
16
  from PIL import Image
17
  from torchvision import transforms as T
18
  from torchvision.models import efficientnet_v2_s
19
- from huggingface_hub import hf_hub_download
 
 
 
20
 
21
  # ── 38-class label map ───────────────────────────────────────────────────────
22
  ID2LABEL = {
@@ -42,261 +42,183 @@ ID2LABEL = {
42
  34: 'Tomato with Target Spot', 35: 'Tomato Yellow Leaf Curl Virus',
43
  36: 'Tomato Mosaic Virus', 37: 'Healthy Tomato Plant',
44
  }
45
- LABEL2ID = {v: k for k, v in ID2LABEL.items()}
46
-
47
- # ── Class calibration (v40 grid-search optimal) ──────────────────────────────
48
- CALIBRATION = {
49
- LABEL2ID['Tomato with Late Blight']: 4.00,
50
- LABEL2ID['Tomato with Septoria Leaf Spot']: 1.50,
51
- LABEL2ID['Tomato with Leaf Mold']: 2.00,
52
- }
53
 
54
- # ── Treatment / Info map ─────────────────────────────────────────────────────
55
  DISEASE_INFO = {
56
- 'Apple Scab': ('⚠️ Fungal disease', 'Apply fungicide (captan/myclobutanil) at bud break. Remove infected leaves.'),
57
- 'Apple with Black Rot': ('🔴 Fungal disease', 'Prune infected wood. Apply copper-based fungicide. Improve air circulation.'),
58
- 'Cedar Apple Rust': ('⚠️ Fungal disease', 'Remove nearby cedar/juniper trees. Apply protective fungicide in spring.'),
59
- 'Cherry with Powdery Mildew': ('⚠️ Fungal disease', 'Apply sulfur or potassium bicarbonate spray. Avoid overhead watering.'),
60
- 'Corn (Maize) with Cercospora and Gray Leaf Spot': ('⚠️ Fungal disease', 'Use resistant varieties. Apply strobilurin fungicide. Ensure good drainage.'),
61
- 'Corn (Maize) with Common Rust': ('⚠️ Fungal disease', 'Plant resistant hybrids. Apply fungicide at early signs. Scout regularly.'),
62
- 'Corn (Maize) with Northern Leaf Blight': ('🔴 Fungal disease', 'Use resistant varieties. Apply fungicide. Rotate crops.'),
63
- 'Grape with Black Rot': ('🔴 Fungal disease', 'Apply myclobutanil or mancozeb. Remove mummified berries. Prune for airflow.'),
64
- 'Grape with Esca (Black Measles)': ('🔴 Fungal disease', 'No cure known. Remove infected wood. Avoid large pruning wounds.'),
65
- 'Grape with Isariopsis Leaf Spot': ('⚠️ Fungal disease', 'Apply copper fungicide. Remove infected plant debris.'),
66
- 'Orange with Citrus Greening': ('🔴 Bacterial disease (HLB)', 'No cure. Remove infected trees. Control Asian citrus psyllid vector.'),
67
- 'Peach with Bacterial Spot': ('⚠️ Bacterial disease', 'Apply copper bactericide. Use resistant varieties. Avoid overhead irrigation.'),
68
- 'Bell Pepper with Bacterial Spot': ('⚠️ Bacterial disease', 'Apply copper-based bactericide. Use disease-free seed. Rotate crops.'),
69
- 'Potato with Early Blight': ('⚠️ Fungal disease', 'Apply chlorothalonil or mancozeb. Ensure proper irrigation. Remove infected foliage.'),
70
- 'Potato with Late Blight': ('🔴 Fungal disease (P. infestans)', 'Apply metalaxyl or cymoxanil IMMEDIATELY. Destroy infected plants. This spreads rapidly!'),
71
- 'Squash with Powdery Mildew': ('⚠️ Fungal disease', 'Apply potassium bicarbonate or neem oil. Plant resistant varieties. Ensure spacing.'),
72
- 'Strawberry with Leaf Scorch': ('⚠️ Fungal disease', 'Apply captan fungicide. Remove infected leaves. Improve air circulation.'),
73
- 'Tomato with Bacterial Spot': ('⚠️ Bacterial disease', 'Apply copper-based bactericide. Use certified disease-free transplants.'),
74
- 'Tomato with Early Blight': ('⚠️ Fungal disease', 'Apply chlorothalonil. Remove lower leaves. Mulch around plants.'),
75
- 'Tomato with Late Blight': ('🔴 Fungal disease (P. infestans)', 'Apply metalaxyl IMMEDIATELY. Remove all infected material. Very contagious!'),
76
- 'Tomato with Leaf Mold': ('⚠️ Fungal disease', 'Improve ventilation. Reduce humidity. Apply chlorothalonil or copper.'),
77
- 'Tomato with Septoria Leaf Spot': ('⚠️ Fungal disease', 'Apply chlorothalonil. Remove infected leaves. Avoid overhead watering.'),
78
- 'Tomato with Spider Mites or Two-spotted Spider Mite': ('⚠️ Pest infestation', 'Apply miticide or neem oil. Increase humidity. Introduce predatory mites.'),
79
- 'Tomato with Target Spot': ('⚠️ Fungal disease', 'Apply azoxystrobin or chlorothalonil. Remove infected leaves.'),
80
- 'Tomato Yellow Leaf Curl Virus': ('🔴 Viral disease (TYLCV)', 'No cure. Remove infected plants. Control whitefly vectors. Use resistant varieties.'),
81
- 'Tomato Mosaic Virus': ('🔴 Viral disease', 'No cure. Remove infected plants. Disinfect tools. Control aphid vectors.'),
82
  }
83
- HEALTHY_CROPS = {l for l in ID2LABEL.values() if 'Healthy' in l}
84
-
85
- # ── Transform (CPU-optimized, 2-view TTA) ────────────────────────────────────
86
- _TF_CENTER = T.Compose([
87
- T.Resize(256),
88
- T.CenterCrop(224),
89
- T.ToTensor(),
90
- T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
91
- ])
92
- _TF_HFLIP = T.Compose([
93
- T.Resize(256),
94
- T.CenterCrop(224),
95
- T.RandomHorizontalFlip(p=1.0),
96
- T.ToTensor(),
97
- T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
98
- ])
99
-
100
- # ── Model cache ───────────────────────────────────────────────────────────────
101
- _models: dict = {}
102
 
103
- def _build_effnet(num_classes: int = 38) -> nn.Module:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  m = efficientnet_v2_s(weights=None)
105
- m.classifier[1] = nn.Linear(m.classifier[1].in_features, num_classes)
 
 
 
 
 
106
  return m
107
 
108
- def _load_model(key: str, filename: str) -> nn.Module | None:
109
- if key in _models:
110
- return _models[key]
 
111
  try:
112
- path = hf_hub_download(repo_id="noivan/cropdoc", filename=filename)
113
- ckpt = torch.load(path, map_location="cpu", weights_only=False)
114
- if isinstance(ckpt, dict) and "model_state_dict" in ckpt:
115
- state = ckpt["model_state_dict"]
116
- nc = ckpt.get("num_classes", 38)
117
- else:
118
- state = ckpt
119
- nc = 38
120
- model = _build_effnet(nc)
121
- model.load_state_dict(state)
122
- model.eval()
123
- _models[key] = model
124
- print(f"✅ Loaded {key}")
125
- return model
126
  except Exception as e:
127
- print(f"⚠️ Failed to load {key}: {e}")
128
- return None
129
-
130
- def _predict_tta(img: Image.Image, model: nn.Module) -> np.ndarray:
131
- """2-view TTA (Center + HFlip), batched for CPU efficiency."""
132
- t1 = _TF_CENTER(img)
133
- t2 = _TF_HFLIP(img)
134
- batch = torch.stack([t1, t2]) # (2, 3, 224, 224)
135
- with torch.no_grad():
136
- logits = model(batch)
137
- probs = torch.softmax(logits, dim=1).mean(dim=0) # average over TTA
138
- return probs.numpy()
139
-
140
- def _ensemble_predict(img: Image.Image) -> np.ndarray | None:
141
- """FT v3+v4 ensemble (v3=0.40, v4=0.60). v2 optional if loaded."""
142
- ft_v2 = _models.get("ft_v2")
143
- ft_v3 = _models.get("ft_v3")
144
- ft_v4 = _models.get("ft_v4")
145
-
146
- if ft_v3 is None and ft_v4 is None:
147
- return None
148
-
149
- probs = None
150
- if ft_v2 is not None and ft_v3 is not None and ft_v4 is not None:
151
- p2 = _predict_tta(img, ft_v2)
152
- p3 = _predict_tta(img, ft_v3)
153
- p4 = _predict_tta(img, ft_v4)
154
- probs = 0.10 * p2 + 0.50 * p3 + 0.40 * p4
155
- elif ft_v3 is not None and ft_v4 is not None:
156
- p3 = _predict_tta(img, ft_v3)
157
- p4 = _predict_tta(img, ft_v4)
158
- probs = 0.40 * p3 + 0.60 * p4
159
- elif ft_v3 is not None:
160
- probs = _predict_tta(img, ft_v3)
161
- elif ft_v4 is not None:
162
- probs = _predict_tta(img, ft_v4)
163
-
164
- if probs is None:
165
- return None
166
-
167
- # Apply class calibration (v40)
168
- probs = probs.copy()
169
- for idx, mult in CALIBRATION.items():
170
- probs[idx] *= mult
171
-
172
- return probs
173
-
174
- # ── Lazy model loading on first inference ─────────────────────────────────────
175
- _initialized = False
176
-
177
- def _ensure_models():
178
- global _initialized
179
- if _initialized:
180
- return
181
- _initialized = True
182
- # Primary: ft_v3 + ft_v4 (most impactful)
183
- _load_model("ft_v3", "models/effnet_ft_v3.pt")
184
- _load_model("ft_v4", "models/effnet_ft_v4.pt")
185
- # Optional: ft_v2 (small weight 0.10)
186
- _load_model("ft_v2", "models/effnet_ft_plantdoc.pt")
187
-
188
- # ── Main inference function ───────────────────────────────────────────────────
189
- def diagnose(image: Image.Image) -> tuple[str, str, str, dict]:
190
- """
191
- Returns: (label, severity, recommendation, confidences_dict)
192
- """
193
- if image is None:
194
- return "No image", "", "Please upload an image.", {}
195
-
196
  t0 = time.time()
197
- _ensure_models()
198
-
199
- img = image.convert("RGB")
 
200
  if max(img.size) > 1024:
201
  img.thumbnail((1024, 1024), Image.LANCZOS)
202
-
203
- probs = _ensemble_predict(img)
204
- if probs is None:
205
- return "Model loading failed", "❌", "Please try again.", {}
206
-
207
- # Top-5 results
208
  top5 = sorted(enumerate(probs), key=lambda x: -x[1])[:5]
209
- top1_idx, top1_score = top5[0]
210
- top1_label = ID2LABEL.get(top1_idx, "Unknown")
211
-
212
  elapsed = time.time() - t0
213
-
214
- # Determine severity / recommendation
215
- if top1_label in HEALTHY_CROPS:
216
- severity = "✅ Healthy"
217
  rec = "Your plant looks healthy! Continue regular care and monitoring."
218
  else:
219
- info = DISEASE_INFO.get(top1_label)
220
- if info:
221
- severity, rec = info
222
- else:
223
- severity = "⚠️ Disease detected"
224
- rec = "Consult a local agricultural extension service."
225
-
226
- # Build confidence dict for display
227
- conf_dict = {ID2LABEL[i]: float(f"{s:.4f}") for i, s in top5}
228
-
229
- # Format label with confidence
230
- label_out = f"{top1_label} ({top1_score:.1%})"
231
- rec_out = f"{rec}\n\n⏱ Inference: {elapsed:.2f}s (CPU)"
232
-
233
- return label_out, severity, rec_out, conf_dict
234
 
235
  # ── Gradio UI ─────────────────────────────────────────────────────────────────
236
  CSS = """
237
  .gradio-container { max-width: 900px !important; margin: auto; }
238
- .title { text-align: center; color: #0d5c32; }
239
- .subtitle { text-align: center; color: #2a6040; margin-bottom: 1em; }
240
- #result-label { font-size: 1.3em; font-weight: bold; }
241
- .healthy { color: #007830; }
242
- .disease { color: #cc4400; }
243
  footer { display: none !important; }
244
  """
245
 
246
- EXAMPLES = [
247
- # These will be populated if example images are added to the repo
248
- ]
249
-
250
- def run_diagnose(img):
251
- if img is None:
252
- return "Upload an image first", "", "", {}
253
- label, severity, rec, conf = diagnose(img)
254
- return label, severity, rec, conf
255
-
256
  with gr.Blocks(css=CSS, title="🌿 CropDoc — Plant Disease AI") as demo:
257
  gr.HTML("""
258
- <div class="title">
259
- <h1>🌿 CropDoc</h1>
260
- <p class="subtitle">Plant Disease Diagnosis AI · 38 Disease Classes · EfficientNetV2-S Ensemble</p>
261
- </div>
262
  """)
263
 
264
  with gr.Row():
265
  with gr.Column(scale=1):
266
- img_input = gr.Image(type="pil", label="📷 Upload plant photo")
267
  btn = gr.Button("🔍 Diagnose", variant="primary", size="lg")
268
- gr.HTML("""
269
- <div style="font-size:12px;color:#555;margin-top:8px;">
270
- Supported crops: Tomato, Potato, Pepper, Apple, Grape, Corn, Peach, Strawberry, Squash, Orange, Cherry, Raspberry, Soybean, Blueberry<br>
271
- <b>Note:</b> Running on CPU — first inference ~5s (model loading), subsequent ~2s
272
- </div>
273
- """)
274
 
275
  with gr.Column(scale=1):
276
- out_label = gr.Textbox(label="🏷️ Diagnosis", elem_id="result-label")
277
- out_severity = gr.Textbox(label="⚡ Severity")
278
- out_rec = gr.Textbox(label="💊 Recommendation", lines=4)
279
- out_conf = gr.JSON(label="📊 Top-5 Confidence")
280
-
281
- btn.click(
282
- fn=run_diagnose,
283
- inputs=[img_input],
284
- outputs=[out_label, out_severity, out_rec, out_conf],
285
- )
286
- img_input.change(
287
- fn=run_diagnose,
288
- inputs=[img_input],
289
- outputs=[out_label, out_severity, out_rec, out_conf],
290
- )
291
-
292
- gr.HTML("""
293
- <hr style="margin-top:2em;">
294
- <div style="text-align:center;font-size:12px;color:#666;">
295
- <b>CropDoc</b> · EfficientNetV2-S fine-tuned on PlantDoc (61.6% accuracy) ·
296
- <a href="https://github.com/noivan0/cropdoc" target="_blank">GitHub</a> ·
297
- <a href="https://huggingface.co/noivan/cropdoc" target="_blank">Model Weights</a>
298
- </div>
299
- """)
300
 
301
  if __name__ == "__main__":
302
  demo.launch()
 
1
  """
2
+ CropDoc HF Space — CPU Inference using Hub-hosted model parts
3
+ =============================================================
4
+ Downloads & reconstructs cropdoc_efficientnet_v2 from noivan/cropdoc Hub.
5
+ Base model: EfficientNetV2-S (PlantVillage 99.91%, 38 classes)
 
 
 
6
  """
7
 
8
+ import os, sys, time, json, base64, glob, io, tempfile
9
  import gradio as gr
10
  import torch
11
  import torch.nn as nn
 
13
  from PIL import Image
14
  from torchvision import transforms as T
15
  from torchvision.models import efficientnet_v2_s
16
+ import requests
17
+ import urllib3
18
+
19
+ urllib3.disable_warnings()
20
 
21
  # ── 38-class label map ───────────────────────────────────────────────────────
22
  ID2LABEL = {
 
42
  34: 'Tomato with Target Spot', 35: 'Tomato Yellow Leaf Curl Virus',
43
  36: 'Tomato Mosaic Virus', 37: 'Healthy Tomato Plant',
44
  }
45
+ HEALTHY = {l for l in ID2LABEL.values() if 'Healthy' in l}
 
 
 
 
 
 
 
46
 
47
+ # ── Treatment info ───────────────────────────────────────────────────────────
48
  DISEASE_INFO = {
49
+ 'Apple Scab': ('⚠️ Fungal', 'Apply fungicide at bud break (captan/myclobutanil). Remove infected leaves.'),
50
+ 'Apple with Black Rot': ('🔴 Fungal', 'Prune infected wood. Apply copper fungicide. Improve air circulation.'),
51
+ 'Cedar Apple Rust': ('⚠️ Fungal', 'Remove nearby cedar trees. Apply protective fungicide in spring.'),
52
+ 'Cherry with Powdery Mildew': ('⚠️ Fungal', 'Apply sulfur spray. Avoid overhead watering.'),
53
+ 'Corn (Maize) with Cercospora and Gray Leaf Spot': ('⚠️ Fungal', 'Use resistant varieties. Apply strobilurin fungicide.'),
54
+ 'Corn (Maize) with Common Rust': ('⚠️ Fungal', 'Plant resistant hybrids. Apply fungicide at early signs.'),
55
+ 'Corn (Maize) with Northern Leaf Blight': ('🔴 Fungal', 'Use resistant varieties. Apply fungicide. Rotate crops.'),
56
+ 'Grape with Black Rot': ('🔴 Fungal', 'Apply myclobutanil. Remove mummified berries. Prune for airflow.'),
57
+ 'Grape with Esca (Black Measles)': ('🔴 Fungal', 'No cure known. Remove infected wood. Avoid large pruning wounds.'),
58
+ 'Grape with Isariopsis Leaf Spot': ('⚠️ Fungal', 'Apply copper fungicide. Remove infected debris.'),
59
+ 'Orange with Citrus Greening': ('🔴 Bacterial (HLB)', 'No cure. Remove infected trees. Control psyllid vector.'),
60
+ 'Peach with Bacterial Spot': ('⚠️ Bacterial', 'Apply copper bactericide. Use resistant varieties.'),
61
+ 'Bell Pepper with Bacterial Spot': ('⚠️ Bacterial', 'Apply copper bactericide. Use disease-free seed. Rotate crops.'),
62
+ 'Potato with Early Blight': ('⚠️ Fungal', 'Apply chlorothalonil. Ensure proper irrigation.'),
63
+ 'Potato with Late Blight': ('🔴 Fungal (P. infestans)', 'Apply metalaxyl IMMEDIATELY. Destroy infected plants. Spreads rapidly!'),
64
+ 'Squash with Powdery Mildew': ('⚠️ Fungal', 'Apply potassium bicarbonate or neem oil. Ensure plant spacing.'),
65
+ 'Strawberry with Leaf Scorch': ('⚠️ Fungal', 'Apply captan fungicide. Remove infected leaves.'),
66
+ 'Tomato with Bacterial Spot': ('⚠️ Bacterial', 'Apply copper bactericide. Use disease-free transplants.'),
67
+ 'Tomato with Early Blight': ('⚠️ Fungal', 'Apply chlorothalonil. Remove lower leaves. Mulch around plants.'),
68
+ 'Tomato with Late Blight': ('🔴 Fungal (P. infestans)', 'Apply metalaxyl IMMEDIATELY. Very contagious — remove all infected material!'),
69
+ 'Tomato with Leaf Mold': ('⚠️ Fungal', 'Improve ventilation. Reduce humidity. Apply chlorothalonil.'),
70
+ 'Tomato with Septoria Leaf Spot': ('⚠️ Fungal', 'Apply chlorothalonil. Remove infected leaves. Avoid overhead watering.'),
71
+ 'Tomato with Spider Mites or Two-spotted Spider Mite': ('⚠️ Pest', 'Apply miticide or neem oil. Increase humidity.'),
72
+ 'Tomato with Target Spot': ('⚠️ Fungal', 'Apply azoxystrobin. Remove infected leaves.'),
73
+ 'Tomato Yellow Leaf Curl Virus': ('🔴 Viral (TYLCV)', 'No cure. Remove infected plants. Control whitefly vectors.'),
74
+ 'Tomato Mosaic Virus': ('🔴 Viral', 'No cure. Remove infected plants. Disinfect tools.'),
75
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ # ── Model loading ─────────────────────────────────────────────────────────────
78
+ _model = None
79
+ _load_status = "Not loaded"
80
+
81
+ HF_BASE = "https://huggingface.co/noivan/cropdoc/resolve/main"
82
+ MODEL_PREFIX = "models/cropdoc_efficientnet_v2/model.pt"
83
+ N_PARTS = 16
84
+
85
+ def _download_and_reconstruct():
86
+ """Download model parts from HF Hub and reconstruct."""
87
+ global _load_status
88
+ _load_status = "Downloading model parts..."
89
+ print(_load_status, flush=True)
90
+
91
+ buf = io.BytesIO()
92
+ for i in range(N_PARTS):
93
+ part_url = f"{HF_BASE}/{MODEL_PREFIX}.part{i:04d}.json"
94
+ _load_status = f"Downloading part {i+1}/{N_PARTS}..."
95
+ print(_load_status, flush=True)
96
+
97
+ for attempt in range(3):
98
+ try:
99
+ r = requests.get(part_url, timeout=60)
100
+ r.raise_for_status()
101
+ chunk_data = r.json()
102
+ buf.write(base64.b64decode(chunk_data["data"]))
103
+ break
104
+ except Exception as e:
105
+ if attempt == 2:
106
+ raise RuntimeError(f"Failed to download part {i}: {e}")
107
+ time.sleep(2)
108
+
109
+ buf.seek(0)
110
+ _load_status = "Loading model weights..."
111
+ print(_load_status, flush=True)
112
+ ckpt = torch.load(buf, map_location="cpu", weights_only=False)
113
+ return ckpt
114
+
115
+ def _build_model(ckpt):
116
+ nc = ckpt.get("num_classes", 38)
117
  m = efficientnet_v2_s(weights=None)
118
+ m.classifier[1] = nn.Linear(m.classifier[1].in_features, nc)
119
+ if "model_state_dict" in ckpt:
120
+ m.load_state_dict(ckpt["model_state_dict"])
121
+ else:
122
+ m.load_state_dict(ckpt)
123
+ m.eval()
124
  return m
125
 
126
+ def ensure_model():
127
+ global _model, _load_status
128
+ if _model is not None:
129
+ return True
130
  try:
131
+ ckpt = _download_and_reconstruct()
132
+ _model = _build_model(ckpt)
133
+ _load_status = "✅ Model loaded (EfficientNetV2-S, 99.9% PlantVillage)"
134
+ print(_load_status, flush=True)
135
+ return True
 
 
 
 
 
 
 
 
 
136
  except Exception as e:
137
+ _load_status = f"❌ Load failed: {e}"
138
+ print(_load_status, flush=True)
139
+ return False
140
+
141
+ # ── Transforms ───────────────────────────────────────────────────────────────
142
+ _TF_C = T.Compose([T.Resize(256), T.CenterCrop(224), T.ToTensor(),
143
+ T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])])
144
+ _TF_H = T.Compose([T.Resize(256), T.CenterCrop(224),
145
+ T.RandomHorizontalFlip(1.0), T.ToTensor(),
146
+ T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])])
147
+
148
+ def predict(img: Image.Image):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  t0 = time.time()
150
+ if not ensure_model():
151
+ return "Model failed to load", "❌", _load_status, {}
152
+
153
+ img = img.convert("RGB")
154
  if max(img.size) > 1024:
155
  img.thumbnail((1024, 1024), Image.LANCZOS)
156
+
157
+ t1, t2 = _TF_C(img), _TF_H(img)
158
+ batch = torch.stack([t1, t2])
159
+ with torch.no_grad():
160
+ probs = torch.softmax(_model(batch), dim=1).mean(0).numpy()
161
+
162
  top5 = sorted(enumerate(probs), key=lambda x: -x[1])[:5]
163
+ idx, score = top5[0]
164
+ label = ID2LABEL.get(idx, "Unknown")
 
165
  elapsed = time.time() - t0
166
+
167
+ if label in HEALTHY:
168
+ sev = "✅ Healthy"
 
169
  rec = "Your plant looks healthy! Continue regular care and monitoring."
170
  else:
171
+ info = DISEASE_INFO.get(label, ("⚠️ Disease detected", "Consult a local agricultural extension service."))
172
+ sev, rec = info
173
+
174
+ conf = {ID2LABEL[i]: float(f"{s:.4f}") for i, s in top5}
175
+ label_out = f"{label} ({score:.1%})"
176
+ rec_out = f"{rec}\n\n⏱ {elapsed:.2f}s · CPU · EfficientNetV2-S"
177
+ return label_out, sev, rec_out, conf
 
 
 
 
 
 
 
 
178
 
179
  # ── Gradio UI ─────────────────────────────────────────────────────────────────
180
  CSS = """
181
  .gradio-container { max-width: 900px !important; margin: auto; }
182
+ h1 { text-align: center; color: #0d5c32; }
183
+ .subtitle { text-align: center; color: #2a6040; margin-bottom: 1em; font-size: 14px; }
 
 
 
184
  footer { display: none !important; }
185
  """
186
 
 
 
 
 
 
 
 
 
 
 
187
  with gr.Blocks(css=CSS, title="🌿 CropDoc — Plant Disease AI") as demo:
188
  gr.HTML("""
189
+ <h1>🌿 CropDoc</h1>
190
+ <p class="subtitle">Plant Disease Diagnosis AI · 38 Classes · EfficientNetV2-S (99.9% accuracy)</p>
 
 
191
  """)
192
 
193
  with gr.Row():
194
  with gr.Column(scale=1):
195
+ img_in = gr.Image(type="pil", label="📷 Upload plant photo")
196
  btn = gr.Button("🔍 Diagnose", variant="primary", size="lg")
197
+ gr.HTML("""<div style="font-size:12px;color:#777;margin-top:8px;">
198
+ <b>Supported crops:</b> Tomato, Potato, Pepper, Apple, Grape, Corn, Peach, Strawberry, Squash, Orange, Cherry, Raspberry, Soybean, Blueberry<br>
199
+ <b>Note:</b> First run downloads model (~80MB). Subsequent inferences are fast (~1–2s CPU).
200
+ </div>""")
 
 
201
 
202
  with gr.Column(scale=1):
203
+ out_label = gr.Textbox(label="🏷️ Diagnosis")
204
+ out_sev = gr.Textbox(label="⚡ Severity")
205
+ out_rec = gr.Textbox(label="💊 Recommendation", lines=4)
206
+ out_conf = gr.JSON(label="📊 Top-5 Confidence")
207
+
208
+ def run(img):
209
+ if img is None:
210
+ return "Upload an image first", "", "", {}
211
+ return predict(img)
212
+
213
+ btn.click(run, [img_in], [out_label, out_sev, out_rec, out_conf])
214
+ img_in.change(run, [img_in], [out_label, out_sev, out_rec, out_conf])
215
+
216
+ gr.HTML("""<hr style="margin-top:2em;">
217
+ <div style="text-align:center;font-size:12px;color:#888;">
218
+ CropDoc · EfficientNetV2-S · PlantVillage 99.9% · PlantDoc 61.6% ·
219
+ <a href="https://github.com/noivan0/cropdoc">GitHub</a> ·
220
+ <a href="https://huggingface.co/noivan/cropdoc">Model</a>
221
+ </div>""")
 
 
 
 
 
222
 
223
  if __name__ == "__main__":
224
  demo.launch()