cullamatmf123 commited on
Commit
1280171
Β·
verified Β·
1 Parent(s): 6fc7c55

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +326 -0
app.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import logging
4
+ import traceback
5
+ import warnings
6
+ import re
7
+
8
+ warnings.filterwarnings("ignore")
9
+
10
+ import numpy as np
11
+ import gradio as gr
12
+ from PIL import Image, ImageDraw, ImageFont
13
+
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format="%(asctime)s [%(levelname)s] %(name)s β€” %(message)s",
17
+ handlers=[logging.StreamHandler(sys.stdout)],
18
+ )
19
+ logger = logging.getLogger("cocoscan-classifier")
20
+ logger.info("Logger initialised.")
21
+ logger.info(f"Gradio version: {gr.__version__}")
22
+
23
+ MODEL_PATH = "best.pt"
24
+ model = None
25
+
26
+ try:
27
+ from ultralytics import YOLO
28
+
29
+ if os.path.exists(MODEL_PATH):
30
+ model = YOLO(MODEL_PATH)
31
+ logger.info(f"Model loaded: {MODEL_PATH}")
32
+ else:
33
+ logger.warning(f"best.pt not found at '{MODEL_PATH}'. Running in fallback mode.")
34
+ except Exception as e:
35
+ logger.error(f"Failed to load model: {e}\n{traceback.format_exc()}")
36
+ model = None
37
+
38
+ DEFAULT_CLASSES = [
39
+ "unspecified",
40
+ "crb infestation",
41
+ "unhealthy",
42
+ "oryctes rhinoceros",
43
+ "healthy",
44
+ ]
45
+
46
+ CONFIDENCE_THRESHOLDS = {
47
+ "healthy": 0.50,
48
+ "oryctes rhinoceros": 0.45,
49
+ "unhealthy": 0.45,
50
+ "crb infestation": 0.45,
51
+ "unspecified": 0.30,
52
+ }
53
+
54
+ CLASS_COLORS = {
55
+ "healthy": "#2ecc71",
56
+ "crb infestation": "#e74c3c",
57
+ "unspecified": "#f39c12",
58
+ "oryctes rhinoceros": "#8e44ad",
59
+ "unhealthy": "#3498db",
60
+ }
61
+
62
+ NAME_NORMALIZATION = {
63
+ "unknown": "unspecified",
64
+ "other-pest-damage": "unhealthy",
65
+ "other_pest_damage": "unhealthy",
66
+ "oryctes-rhinoceros": "oryctes rhinoceros",
67
+ "oryctes_rhinoceros": "oryctes rhinoceros",
68
+ }
69
+
70
+
71
+ def normalize_class_name(name: str) -> str:
72
+ if not isinstance(name, str):
73
+ return str(name)
74
+ key = name.strip().lower()
75
+ key = re.sub(r"\s+", " ", key)
76
+ return NAME_NORMALIZATION.get(key, key)
77
+
78
+
79
+ def health_cascade(probs: dict) -> tuple:
80
+ ranked = sorted(probs.items(), key=lambda x: x[1], reverse=True)
81
+ if not ranked:
82
+ return "unspecified", 0.0
83
+
84
+ for cls_name, conf in ranked:
85
+ threshold = CONFIDENCE_THRESHOLDS.get(cls_name, 0.30)
86
+ if conf >= threshold:
87
+ return cls_name, conf
88
+ return ranked[0]
89
+
90
+
91
+ def multi_run_predict(image: Image.Image, runs: int = 3) -> dict:
92
+ if model is None:
93
+ return {}
94
+
95
+ accumulated = {}
96
+ imgsz_list = [224, 256, 192]
97
+
98
+ for i in range(runs):
99
+ imgsz = imgsz_list[i % len(imgsz_list)]
100
+ try:
101
+ result = model(image, imgsz=imgsz, verbose=False)[0]
102
+ names = result.names
103
+ probs = result.probs.data.cpu().numpy()
104
+
105
+ for idx, prob in enumerate(probs):
106
+ cls_name = names.get(idx, f"class_{idx}")
107
+ cls_name = normalize_class_name(cls_name)
108
+ accumulated[cls_name] = accumulated.get(cls_name, 0.0) + float(prob)
109
+ except Exception as e:
110
+ logger.warning(f"Run {i+1} failed: {e}")
111
+ continue
112
+
113
+ if not accumulated:
114
+ return {}
115
+
116
+ averaged = {k: v / runs for k, v in accumulated.items()}
117
+
118
+ for c in DEFAULT_CLASSES:
119
+ averaged.setdefault(c, 0.0)
120
+
121
+ return averaged
122
+
123
+
124
+ def predict_classification(image: Image.Image) -> dict:
125
+ if image is None:
126
+ return {
127
+ "success": False,
128
+ "class": "unspecified",
129
+ "confidence": 0.0,
130
+ "all_probs": {},
131
+ "message": "No image provided.",
132
+ }
133
+
134
+ image = image.convert("RGB")
135
+
136
+ if model is None:
137
+ return {
138
+ "success": True,
139
+ "class": "unspecified",
140
+ "confidence": 0.0,
141
+ "all_probs": {c: 0.0 for c in DEFAULT_CLASSES},
142
+ "message": "Model not available. Please put best.pt beside app.py and restart.",
143
+ }
144
+
145
+ try:
146
+ avg_probs = multi_run_predict(image, runs=3)
147
+ if not avg_probs:
148
+ raise ValueError("No probabilities returned from model.")
149
+
150
+ predicted_class, confidence = health_cascade(avg_probs)
151
+ predicted_class = normalize_class_name(predicted_class)
152
+
153
+ logger.info(f"Prediction: {predicted_class} ({confidence:.4f})")
154
+
155
+ return {
156
+ "success": True,
157
+ "class": predicted_class,
158
+ "confidence": round(float(confidence), 4),
159
+ "all_probs": {k: round(float(v), 4) for k, v in avg_probs.items()},
160
+ "message": "Classification successful.",
161
+ }
162
+ except Exception as e:
163
+ logger.error(f"Prediction error: {e}\n{traceback.format_exc()}")
164
+ return {
165
+ "success": True,
166
+ "class": "unspecified",
167
+ "confidence": 0.0,
168
+ "all_probs": {c: 0.0 for c in DEFAULT_CLASSES},
169
+ "message": f"Prediction failed: {str(e)}",
170
+ }
171
+
172
+
173
+ def _escape_html(s: str) -> str:
174
+ return str(s).replace("&", "&​amp;").replace("<", "&​lt;").replace(">", "&​gt;")
175
+
176
+
177
+ def predict_on_image(input_image):
178
+ if input_image is None:
179
+ blank = Image.new("RGB", (420, 220), color="#1a1a2e")
180
+ draw = ImageDraw.Draw(blank)
181
+ draw.text((80, 100), "Please upload an image.", fill="white")
182
+ return blank, "<div style='color:#fff;'>No image uploaded.</div>"
183
+
184
+ if isinstance(input_image, np.ndarray):
185
+ pil_image = Image.fromarray(input_image.astype(np.uint8))
186
+ elif isinstance(input_image, Image.Image):
187
+ pil_image = input_image
188
+ else:
189
+ pil_image = Image.fromarray(np.array(input_image).astype(np.uint8))
190
+
191
+ result = predict_classification(pil_image)
192
+ cls_name = normalize_class_name(result["class"])
193
+ confidence = float(result["confidence"])
194
+ all_probs = result.get("all_probs", {}) or {}
195
+ message = result.get("message", "")
196
+
197
+ img_display = pil_image.convert("RGB").copy()
198
+ w, h = img_display.size
199
+ draw = ImageDraw.Draw(img_display)
200
+
201
+ bar_h = max(50, h // 8)
202
+ bar_color = CLASS_COLORS.get(cls_name, "#888888")
203
+ draw.rectangle([0, h - bar_h, w, h], fill=bar_color)
204
+
205
+ label = f"{cls_name.upper()} {confidence * 100:.1f}%"
206
+ try:
207
+ font = ImageFont.truetype(
208
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
209
+ max(14, bar_h // 2),
210
+ )
211
+ except Exception:
212
+ font = ImageFont.load_default()
213
+
214
+ bbox = draw.textbbox((0, 0), label, font=font)
215
+ text_w = bbox[2] - bbox[0]
216
+ text_h = bbox[3] - bbox[1]
217
+ text_x = (w - text_w) // 2
218
+ text_y = h - bar_h + (bar_h - text_h) // 2
219
+ draw.text((text_x, text_y), label, fill="white", font=font)
220
+
221
+ emoji = {
222
+ "healthy": "βœ…",
223
+ "crb infestation": "❌",
224
+ "unspecified": "⚠️",
225
+ "oryctes rhinoceros": "πŸͺ²",
226
+ "unhealthy": "πŸ›",
227
+ }.get(cls_name, "πŸ”")
228
+
229
+ lines = [
230
+ f"{emoji} Predicted Class : {cls_name.upper()}",
231
+ f"Confidence : {confidence * 100:.2f}%",
232
+ "",
233
+ "── All Class Probabilities ──",
234
+ ]
235
+
236
+ # Show probabilities in a stable order (the 5 classes first), then any extras
237
+ seen = set()
238
+ for c in DEFAULT_CLASSES:
239
+ p = float(all_probs.get(c, 0.0))
240
+ bar = "β–ˆ" * int(max(0.0, min(1.0, p)) * 20)
241
+ lines.append(f" {c:<18} {p * 100:5.1f}% {bar}")
242
+ seen.add(c)
243
+
244
+ extras = [(k, v) for k, v in all_probs.items() if k not in seen]
245
+ for c, p in sorted(
246
+ extras, key=lambda x: float(x[1]) if x[1] is not None else 0.0, reverse=True
247
+ ):
248
+ try:
249
+ p = float(p)
250
+ except Exception:
251
+ p = 0.0
252
+ bar = "β–ˆ" * int(max(0.0, min(1.0, p)) * 20)
253
+ lines.append(f" {str(c):<18} {p * 100:5.1f}% {bar}")
254
+
255
+ lines += ["", f"Info: {message}"]
256
+
257
+ text_color = CLASS_COLORS.get(cls_name, "#ffffff")
258
+ safe_lines = "<br>".join(_escape_html(line) for line in lines)
259
+
260
+ html = f"""
261
+ <div style="
262
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
263
+ white-space: normal;
264
+ line-height: 1.35;
265
+ color: {text_color};
266
+ ">
267
+ {safe_lines}
268
+ </div>
269
+ """
270
+ return img_display, html
271
+
272
+
273
+ with gr.Blocks(title="Capstone CocoScan β€” 5-Class Classifier") as demo:
274
+ gr.HTML(
275
+ """
276
+ <div style="text-align:center; padding:16px 0;">
277
+ <h1>Capstone CocoScan β€” 5-Class Classifier</h1>
278
+ <p>Upload an image to classify it into:</p>
279
+ <p>
280
+ <b>UNSPECIFIED</b>,
281
+ <b>CRB INFESTATION</b>,
282
+ <b>UNHEALTHY</b>,
283
+ <b>ORYCTES RHINOCEROS</b>,
284
+ <b>HEALTHY</b>
285
+ </p>
286
+ <p style="color:#888; font-size:13px;">
287
+ Model: YOLOv8-cls Β· 5 classes Β· Multi-run averaging
288
+ </p>
289
+ </div>
290
+ """
291
+ )
292
+
293
+ with gr.Row():
294
+ with gr.Column(scale=1):
295
+ input_image = gr.Image(
296
+ label="Upload Image",
297
+ type="numpy",
298
+ height=350,
299
+ )
300
+ classify_btn = gr.Button(
301
+ value="Classify",
302
+ variant="primary",
303
+ )
304
+
305
+ with gr.Column(scale=1):
306
+ output_image = gr.Image(
307
+ label="Result",
308
+ type="pil",
309
+ height=350,
310
+ )
311
+ output_text = gr.HTML(label="Details")
312
+
313
+ classify_btn.click(
314
+ fn=predict_on_image,
315
+ inputs=input_image,
316
+ outputs=[output_image, output_text],
317
+ )
318
+
319
+ input_image.change(
320
+ fn=predict_on_image,
321
+ inputs=input_image,
322
+ outputs=[output_image, output_text],
323
+ )
324
+
325
+ if __name__ == "__main__":
326
+ demo.queue().launch(server_name="0.0.0.0", server_port=7860)