S-4-G-4-R commited on
Commit
f8dd4c8
Β·
verified Β·
1 Parent(s): b4f632d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +369 -256
app.py CHANGED
@@ -8,44 +8,45 @@ import gradio as gr
8
  from huggingface_hub import hf_hub_download
9
 
10
  # ── Config ────────────────────────────────────────────────────────
11
- HF_REPO_ID = "your-hf-username/brain-tumor-efficientnet-b3" # <- change to your repo
12
- CKPT_FILE = "model.pt"
13
- DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
14
- MEAN = [0.485, 0.456, 0.406]
15
- STD = [0.229, 0.224, 0.225]
16
-
17
- ID_TO_LABEL = {
18
- 0: "Glioma",
19
- 1: "Meningioma",
20
- 2: "Pituitary Tumor",
21
- 3: "No Tumor",
22
- }
23
 
24
  CLASS_INFO = {
25
  "Glioma": {
26
- "color": "#ef4444",
27
- "desc": "A tumor that originates in the glial cells of the brain or spine. Gliomas account for about 30% of all brain tumors.",
 
 
28
  },
29
  "Meningioma": {
30
- "color": "#f97316",
31
- "desc": "A tumor that arises from the meninges, the membranes surrounding the brain and spinal cord. Usually benign and slow-growing.",
 
 
32
  },
33
  "Pituitary Tumor": {
34
- "color": "#a855f7",
35
- "desc": "A tumor in the pituitary gland at the base of the brain. Most are benign and can affect hormone regulation.",
 
 
36
  },
37
  "No Tumor": {
38
- "color": "#22c55e",
39
- "desc": "No tumor detected in the MRI scan. The brain tissue appears within normal parameters.",
 
 
40
  },
41
  }
42
 
43
- # ── Model definition (must match training code) ───────────────────
 
44
  class EfficientNetClassifier(nn.Module):
45
  def __init__(self, num_classes=4, dropout=0.4):
46
  super().__init__()
47
  self.backbone = efficientnet_b3(weights=None)
48
- in_features = self.backbone.classifier[1].in_features
49
  self.backbone.classifier = nn.Sequential(
50
  nn.Dropout(p=dropout, inplace=True),
51
  nn.Linear(in_features, 512),
@@ -58,22 +59,19 @@ class EfficientNetClassifier(nn.Module):
58
  return self.backbone(x)
59
 
60
 
61
- # ── Load model (cached after first download) ──────────────────────
62
  def load_model():
63
- ckpt_path = hf_hub_download(repo_id="S-4-G-4-R/brain-tumor-efficientnet-b3", filename=CKPT_FILE)
64
- ckpt = torch.load(ckpt_path, map_location=DEVICE, weights_only=False)
65
-
66
  n_classes = ckpt.get("num_classes", 4)
67
- img_size = ckpt.get("img_size", 300)
68
  id_to_label = {int(k): v for k, v in ckpt["id_to_label"].items()}
69
-
70
- model = EfficientNetClassifier(n_classes).to(DEVICE)
71
  model.load_state_dict(ckpt["model"])
72
  model.eval()
73
  return model, img_size, id_to_label
74
 
75
 
76
- print("Loading model...")
77
  model, IMG_SIZE, id_to_label = load_model()
78
  print(f"Model ready on {DEVICE}")
79
 
@@ -88,330 +86,445 @@ transform = transforms.Compose([
88
  @torch.no_grad()
89
  def predict(image: Image.Image):
90
  if image is None:
91
- return None, None
92
 
93
  tensor = transform(image.convert("RGB")).unsqueeze(0).to(DEVICE)
94
  logits = model(tensor)
95
  probs = torch.softmax(logits, dim=-1)[0]
96
 
97
- results = {
98
- id_to_label[i]: round(probs[i].item(), 4)
99
- for i in range(len(id_to_label))
100
- }
101
-
102
  top_label = max(results, key=results.get)
103
  top_prob = results[top_label]
104
 
105
- # Normalised label for CLASS_INFO lookup
106
- label_key = top_label.replace("pituitary", "Pituitary Tumor").strip()
107
- if label_key not in CLASS_INFO:
108
- # fallback: title-case match
109
- for k in CLASS_INFO:
110
- if k.lower() == top_label.lower():
111
- label_key = k
112
- break
113
 
114
- info = CLASS_INFO.get(label_key, CLASS_INFO.get(top_label, {}))
115
  color = info.get("color", "#ffffff")
 
 
116
  desc = info.get("desc", "")
117
 
118
- confidence_html = f"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  <div style="
120
- background: #0f0f0f;
121
- border: 1px solid #1e1e1e;
122
- border-radius: 12px;
123
- padding: 24px;
124
- font-family: 'DM Sans', sans-serif;
 
 
 
125
  ">
126
- <div style="margin-bottom: 20px;">
127
- <span style="
128
- font-size: 11px;
129
- font-weight: 600;
130
- letter-spacing: 0.12em;
131
- color: #555;
132
- text-transform: uppercase;
133
- ">Diagnosis</span>
134
- <div style="
135
- font-size: 28px;
136
- font-weight: 700;
137
- color: {color};
138
- margin-top: 6px;
139
- letter-spacing: -0.02em;
140
- ">{top_label}</div>
141
- <div style="
142
- font-size: 13px;
143
- color: #888;
144
- margin-top: 8px;
145
- line-height: 1.6;
146
- ">{desc}</div>
147
  </div>
148
 
149
- <div style="margin-bottom: 20px;">
150
- <div style="display:flex; justify-content:space-between; margin-bottom:6px;">
151
- <span style="font-size:12px; color:#555; letter-spacing:0.08em; text-transform:uppercase;">Confidence</span>
152
- <span style="font-size:14px; font-weight:700; color:{color};">{top_prob*100:.1f}%</span>
 
 
 
153
  </div>
154
- <div style="background:#1a1a1a; border-radius:4px; height:6px; overflow:hidden;">
155
- <div style="
156
- height:100%;
157
- width:{top_prob*100:.1f}%;
158
- background:{color};
159
- border-radius:4px;
160
- transition: width 0.6s ease;
161
- "></div>
162
  </div>
163
  </div>
164
 
 
165
  <div>
166
- <span style="font-size:11px; color:#555; letter-spacing:0.1em; text-transform:uppercase;">All class probabilities</span>
167
- <div style="margin-top:12px; display:flex; flex-direction:column; gap:10px;">
168
- """
169
-
170
- sorted_results = sorted(results.items(), key=lambda x: x[1], reverse=True)
171
- for label, prob in sorted_results:
172
- lkey = label
173
- for k in CLASS_INFO:
174
- if k.lower() == label.lower():
175
- lkey = k
176
- break
177
- c = CLASS_INFO.get(lkey, {}).get("color", "#444")
178
- is_top = label == top_label
179
- confidence_html += f"""
180
- <div>
181
- <div style="display:flex; justify-content:space-between; margin-bottom:4px;">
182
- <span style="
183
- font-size:13px;
184
- color: {'#fff' if is_top else '#888'};
185
- font-weight: {'600' if is_top else '400'};
186
- ">{label}</span>
187
- <span style="font-size:13px; color:{c}; font-weight:600;">{prob*100:.2f}%</span>
188
- </div>
189
- <div style="background:#1a1a1a; border-radius:3px; height:4px; overflow:hidden;">
190
- <div style="
191
- height:100%;
192
- width:{prob*100:.2f}%;
193
- background:{c};
194
- opacity:{'1' if is_top else '0.5'};
195
- border-radius:3px;
196
- "></div>
197
- </div>
198
- </div>
199
- """
200
-
201
- confidence_html += """
202
  </div>
 
203
  </div>
204
 
205
- <div style="
206
- margin-top: 20px;
207
- padding-top: 16px;
208
- border-top: 1px solid #1e1e1e;
209
- font-size: 11px;
210
- color: #444;
211
- text-align: center;
212
- ">
213
- For research use only. Not a medical diagnostic tool.
214
  </div>
215
  </div>
 
 
 
216
  """
 
217
 
218
- return results, confidence_html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
 
221
- # ── Custom CSS dark theme ─────────────────────────────────────────
222
  CSS = """
223
- @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap');
 
 
224
 
225
  :root {
226
- --bg-primary: #080808;
227
- --bg-secondary: #0f0f0f;
228
- --bg-card: #111111;
229
- --border: #1e1e1e;
230
- --accent: #6366f1;
231
- --text-primary: #f0f0f0;
232
- --text-muted: #555555;
 
233
  }
234
 
235
- body, .gradio-container {
236
- background: var(--bg-primary) !important;
237
- font-family: 'DM Sans', sans-serif !important;
238
- color: var(--text-primary) !important;
239
  }
240
 
241
  .gradio-container {
242
- max-width: 960px !important;
243
  margin: 0 auto !important;
 
244
  }
245
 
246
- /* Header */
247
- #header {
 
248
  text-align: center;
249
- padding: 48px 24px 32px;
250
  border-bottom: 1px solid var(--border);
251
- margin-bottom: 32px;
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  }
253
- #header h1 {
254
- font-size: 32px;
255
- font-weight: 700;
256
  letter-spacing: -0.04em;
257
- color: var(--text-primary);
258
- margin: 0 0 10px;
 
259
  }
260
- #header p {
 
261
  font-size: 14px;
262
- color: var(--text-muted);
263
  margin: 0;
264
- line-height: 1.6;
 
 
265
  }
266
- #header .badge {
267
- display: inline-block;
268
- font-family: 'DM Mono', monospace;
269
- font-size: 10px;
270
- letter-spacing: 0.12em;
271
- padding: 4px 10px;
272
- border: 1px solid #2a2a2a;
273
- border-radius: 4px;
274
- color: #666;
275
- margin-bottom: 16px;
276
- text-transform: uppercase;
277
  }
278
 
279
- /* Cards */
280
- .card {
281
- background: var(--bg-card) !important;
 
 
 
 
 
 
282
  border: 1px solid var(--border) !important;
283
- border-radius: 12px !important;
 
 
 
 
 
 
 
 
 
284
  }
285
 
286
- /* Upload zone */
287
- .upload-zone {
288
- border: 1.5px dashed #2a2a2a !important;
 
 
289
  border-radius: 12px !important;
290
- background: #0a0a0a !important;
291
- min-height: 280px !important;
292
- transition: border-color 0.2s ease;
293
  }
294
- .upload-zone:hover {
295
  border-color: var(--accent) !important;
296
  }
297
 
298
- /* Button */
299
- #run-btn {
 
 
300
  background: var(--accent) !important;
301
  border: none !important;
302
- border-radius: 8px !important;
303
  color: #fff !important;
304
- font-family: 'DM Sans', sans-serif !important;
305
  font-size: 14px !important;
306
- font-weight: 600 !important;
307
- letter-spacing: 0.04em !important;
308
- padding: 12px 28px !important;
309
  cursor: pointer !important;
310
- transition: opacity 0.2s !important;
311
- width: 100% !important;
 
 
 
 
 
 
 
312
  }
313
- #run-btn:hover { opacity: 0.88 !important; }
314
 
315
- /* Examples */
316
- .gr-samples-table td, .gr-samples-table th {
317
- background: var(--bg-secondary) !important;
318
- border-color: var(--border) !important;
319
- color: var(--text-primary) !important;
 
 
320
  }
321
 
322
- /* Labels */
323
- label span {
324
- font-family: 'DM Sans', sans-serif !important;
325
- font-size: 11px !important;
326
- font-weight: 600 !important;
327
- letter-spacing: 0.1em !important;
328
- text-transform: uppercase !important;
329
- color: var(--text-muted) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  }
331
 
332
- /* Hide default label on HTML output */
333
  .result-panel > label { display: none !important; }
 
334
 
335
- /* Footer */
336
  #footer {
337
  text-align: center;
338
- padding: 24px;
339
  border-top: 1px solid var(--border);
340
- margin-top: 32px;
341
  font-size: 12px;
342
- color: var(--text-muted);
 
343
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  """
345
 
346
- # ── UI ────────────────────────────────────────────────────────────
347
- with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="Brain Tumor MRI Classifier") as demo:
348
 
 
349
  gr.HTML("""
350
- <div id="header">
351
- <div class="badge">EfficientNet-B3 Β· 98.98% Val Acc</div>
352
- <h1>Brain Tumor MRI Classifier</h1>
353
- <p>Upload a brain MRI scan to classify into Glioma, Meningioma, Pituitary Tumor, or No Tumor.<br>
354
- Trained on Figshare + Kaggle Brain Tumor datasets Β· 8,211 training images.</p>
 
 
 
355
  </div>
356
  """)
357
 
358
- with gr.Row(equal_height=True):
359
- with gr.Column(scale=1):
 
 
 
 
 
360
  image_input = gr.Image(
361
  type="pil",
362
- label="MRI Scan",
363
- elem_classes=["upload-zone"],
364
- height=300,
 
365
  )
366
- run_btn = gr.Button("Run Classification", elem_id="run-btn")
367
 
368
- with gr.Column(scale=1):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  result_html = gr.HTML(
370
- label="Result",
 
371
  elem_classes=["result-panel"],
372
- value="""
373
- <div style="
374
- background:#0f0f0f;
375
- border:1px solid #1e1e1e;
376
- border-radius:12px;
377
- padding:24px;
378
- height:300px;
379
- display:flex;
380
- align-items:center;
381
- justify-content:center;
382
- flex-direction:column;
383
- gap:12px;
384
- ">
385
- <div style="font-size:32px; opacity:0.15;">⬆</div>
386
- <div style="font-size:13px; color:#444; text-align:center; line-height:1.6;">
387
- Upload an MRI scan and click<br>Run Classification
388
- </div>
389
- </div>
390
- """,
391
  )
392
 
393
- # Hidden label output (used internally, not shown)
394
  label_output = gr.Label(visible=False)
395
 
396
- run_btn.click(
397
- fn=predict,
398
- inputs=[image_input],
399
- outputs=[label_output, result_html],
400
- )
401
- image_input.change(
402
- fn=predict,
403
- inputs=[image_input],
404
- outputs=[label_output, result_html],
405
- )
406
 
 
407
  gr.HTML("""
408
  <div id="footer">
409
- EfficientNet-B3 fine-tuned for brain tumor classification Β·
410
- <a href="https://huggingface.co/your-hf-username/brain-tumor-efficientnet-b3"
411
- style="color:#6366f1; text-decoration:none;">Model on Hugging Face</a>
412
- Β· For research use only
 
 
 
 
413
  </div>
414
  """)
415
 
 
416
  if __name__ == "__main__":
417
- demo.launch()
 
8
  from huggingface_hub import hf_hub_download
9
 
10
  # ── Config ────────────────────────────────────────────────────────
11
+ CKPT_FILE = "model.pt"
12
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
13
+ MEAN = [0.485, 0.456, 0.406]
14
+ STD = [0.229, 0.224, 0.225]
 
 
 
 
 
 
 
 
15
 
16
  CLASS_INFO = {
17
  "Glioma": {
18
+ "color": "#f87171",
19
+ "glow": "rgba(248,113,113,0.25)",
20
+ "icon": "πŸ”΄",
21
+ "desc": "Originates in glial cells of the brain or spine. Accounts for ~30% of all brain tumors and ~80% of malignant tumors.",
22
  },
23
  "Meningioma": {
24
+ "color": "#fb923c",
25
+ "glow": "rgba(251,146,60,0.25)",
26
+ "icon": "🟠",
27
+ "desc": "Arises from the meninges surrounding the brain and spinal cord. Usually benign and slow-growing.",
28
  },
29
  "Pituitary Tumor": {
30
+ "color": "#c084fc",
31
+ "glow": "rgba(192,132,252,0.25)",
32
+ "icon": "🟣",
33
+ "desc": "Located in the pituitary gland at the brain's base. Most are benign but can disrupt hormone regulation.",
34
  },
35
  "No Tumor": {
36
+ "color": "#4ade80",
37
+ "glow": "rgba(74,222,128,0.25)",
38
+ "icon": "🟒",
39
+ "desc": "No tumor detected. Brain tissue appears within normal parameters.",
40
  },
41
  }
42
 
43
+
44
+ # ── Model ─────────────────────────────────────────────────────────
45
  class EfficientNetClassifier(nn.Module):
46
  def __init__(self, num_classes=4, dropout=0.4):
47
  super().__init__()
48
  self.backbone = efficientnet_b3(weights=None)
49
+ in_features = self.backbone.classifier[1].in_features
50
  self.backbone.classifier = nn.Sequential(
51
  nn.Dropout(p=dropout, inplace=True),
52
  nn.Linear(in_features, 512),
 
59
  return self.backbone(x)
60
 
61
 
 
62
  def load_model():
63
+ ckpt_path = hf_hub_download(repo_id="S-4-G-4-R/brain-tumor-efficientnet-b3", filename=CKPT_FILE)
64
+ ckpt = torch.load(ckpt_path, map_location=DEVICE, weights_only=False)
 
65
  n_classes = ckpt.get("num_classes", 4)
66
+ img_size = ckpt.get("img_size", 300)
67
  id_to_label = {int(k): v for k, v in ckpt["id_to_label"].items()}
68
+ model = EfficientNetClassifier(n_classes).to(DEVICE)
 
69
  model.load_state_dict(ckpt["model"])
70
  model.eval()
71
  return model, img_size, id_to_label
72
 
73
 
74
+ print("Loading model…")
75
  model, IMG_SIZE, id_to_label = load_model()
76
  print(f"Model ready on {DEVICE}")
77
 
 
86
  @torch.no_grad()
87
  def predict(image: Image.Image):
88
  if image is None:
89
+ return None, _empty_state()
90
 
91
  tensor = transform(image.convert("RGB")).unsqueeze(0).to(DEVICE)
92
  logits = model(tensor)
93
  probs = torch.softmax(logits, dim=-1)[0]
94
 
95
+ results = {id_to_label[i]: round(probs[i].item(), 4) for i in range(len(id_to_label))}
 
 
 
 
96
  top_label = max(results, key=results.get)
97
  top_prob = results[top_label]
98
 
99
+ # Normalise key for CLASS_INFO lookup
100
+ label_key = top_label
101
+ for k in CLASS_INFO:
102
+ if k.lower() == top_label.lower():
103
+ label_key = k
104
+ break
 
 
105
 
106
+ info = CLASS_INFO.get(label_key, {})
107
  color = info.get("color", "#ffffff")
108
+ glow = info.get("glow", "rgba(255,255,255,0.1)")
109
+ icon = info.get("icon", "βšͺ")
110
  desc = info.get("desc", "")
111
 
112
+ # ── Probability bars ──────────────────────────────────────────
113
+ bars_html = ""
114
+ for lbl, prob in sorted(results.items(), key=lambda x: x[1], reverse=True):
115
+ lkey = lbl
116
+ for k in CLASS_INFO:
117
+ if k.lower() == lbl.lower():
118
+ lkey = k
119
+ break
120
+ c = CLASS_INFO.get(lkey, {}).get("color", "#555")
121
+ is_top = lbl == top_label
122
+ bars_html += f"""
123
+ <div style="margin-bottom:14px;">
124
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;">
125
+ <span style="font-size:13px;color:{'#e5e7eb' if is_top else '#6b7280'};
126
+ font-weight:{'600' if is_top else '400'};
127
+ font-family:'Space Grotesk',sans-serif;">
128
+ {CLASS_INFO.get(lkey,{}).get('icon','βšͺ')} {lbl}
129
+ </span>
130
+ <span style="font-size:13px;color:{c};font-weight:700;
131
+ font-family:'Space Grotesk',sans-serif;">{prob*100:.2f}%</span>
132
+ </div>
133
+ <div style="background:#1f2937;border-radius:99px;height:5px;overflow:hidden;">
134
+ <div style="height:100%;width:{prob*100:.2f}%;background:{c};
135
+ border-radius:99px;opacity:{'1' if is_top else '0.45'};
136
+ transition:width 0.7s cubic-bezier(0.4,0,0.2,1);"></div>
137
+ </div>
138
+ </div>"""
139
+
140
+ html = f"""
141
  <div style="
142
+ background:linear-gradient(145deg,#0d1117,#111827);
143
+ border:1px solid #1f2937;
144
+ border-radius:16px;
145
+ padding:28px;
146
+ font-family:'Space Grotesk',sans-serif;
147
+ height:100%;
148
+ box-sizing:border-box;
149
+ animation: fadeIn 0.4s ease;
150
  ">
151
+ <!-- Diagnosis card -->
152
+ <div style="
153
+ background:linear-gradient(135deg,{glow},{glow.replace('0.25','0.08')});
154
+ border:1px solid {color}33;
155
+ border-radius:12px;
156
+ padding:20px;
157
+ margin-bottom:24px;
158
+ box-shadow: 0 0 32px {glow};
159
+ ">
160
+ <div style="font-size:11px;letter-spacing:0.14em;color:#6b7280;
161
+ text-transform:uppercase;margin-bottom:8px;">
162
+ πŸ”¬ Diagnosis
163
+ </div>
164
+ <div style="font-size:30px;font-weight:700;color:{color};
165
+ letter-spacing:-0.03em;margin-bottom:6px;">
166
+ {icon} {top_label}
167
+ </div>
168
+ <div style="font-size:13px;color:#9ca3af;line-height:1.65;">{desc}</div>
 
 
 
169
  </div>
170
 
171
+ <!-- Confidence meter -->
172
+ <div style="margin-bottom:24px;">
173
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
174
+ <span style="font-size:11px;letter-spacing:0.12em;color:#6b7280;text-transform:uppercase;">
175
+ πŸ“Š Confidence
176
+ </span>
177
+ <span style="font-size:22px;font-weight:800;color:{color};">{top_prob*100:.1f}%</span>
178
  </div>
179
+ <div style="background:#1f2937;border-radius:99px;height:8px;overflow:hidden;">
180
+ <div style="height:100%;width:{top_prob*100:.1f}%;
181
+ background:linear-gradient(90deg,{color}99,{color});
182
+ border-radius:99px;
183
+ box-shadow:0 0 12px {color}66;
184
+ transition:width 0.7s cubic-bezier(0.4,0,0.2,1);"></div>
 
 
185
  </div>
186
  </div>
187
 
188
+ <!-- All probabilities -->
189
  <div>
190
+ <div style="font-size:11px;letter-spacing:0.12em;color:#6b7280;
191
+ text-transform:uppercase;margin-bottom:14px;">
192
+ πŸ“ˆ All Classes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  </div>
194
+ {bars_html}
195
  </div>
196
 
197
+ <!-- Disclaimer -->
198
+ <div style="margin-top:20px;padding-top:16px;border-top:1px solid #1f2937;
199
+ font-size:11px;color:#374151;text-align:center;line-height:1.5;">
200
+ ⚠️ For research use only · Not a clinical diagnostic tool
 
 
 
 
 
201
  </div>
202
  </div>
203
+ <style>
204
+ @keyframes fadeIn {{ from {{opacity:0;transform:translateY(6px)}} to {{opacity:1;transform:translateY(0)}} }}
205
+ </style>
206
  """
207
+ return results, html
208
 
209
+
210
+ def _empty_state():
211
+ return """
212
+ <div style="
213
+ background:linear-gradient(145deg,#0d1117,#111827);
214
+ border:1px solid #1f2937;
215
+ border-radius:16px;
216
+ padding:28px;
217
+ display:flex;
218
+ flex-direction:column;
219
+ align-items:center;
220
+ justify-content:center;
221
+ gap:16px;
222
+ min-height:340px;
223
+ box-sizing:border-box;
224
+ font-family:'Space Grotesk',sans-serif;
225
+ ">
226
+ <div style="font-size:52px;opacity:0.18;">🧠</div>
227
+ <div style="font-size:16px;font-weight:600;color:#374151;letter-spacing:-0.01em;">
228
+ Awaiting MRI scan
229
+ </div>
230
+ <div style="font-size:13px;color:#374151;text-align:center;line-height:1.6;max-width:240px;">
231
+ Upload or drag-and-drop a brain MRI image on the left to see the classification result here.
232
+ </div>
233
+ </div>"""
234
 
235
 
236
+ # ── CSS ───────────────────────────────────────────────────────────
237
  CSS = """
238
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800&family=Space+Mono:wght@400;700&display=swap');
239
+
240
+ *, *::before, *::after { box-sizing: border-box; }
241
 
242
  :root {
243
+ --bg: #080c12;
244
+ --surface: #0d1117;
245
+ --border: #1f2937;
246
+ --accent: #6366f1;
247
+ --muted: #6b7280;
248
+ --text: #e5e7eb;
249
+ --font: 'Space Grotesk', sans-serif;
250
+ --mono: 'Space Mono', monospace;
251
  }
252
 
253
+ html, body, .gradio-container {
254
+ background: var(--bg) !important;
255
+ font-family: var(--font) !important;
256
+ color: var(--text) !important;
257
  }
258
 
259
  .gradio-container {
260
+ max-width: 1100px !important;
261
  margin: 0 auto !important;
262
+ padding: 0 16px !important;
263
  }
264
 
265
+ /* ── Header ── */
266
+ #hero {
267
+ padding: 44px 8px 36px;
268
  text-align: center;
 
269
  border-bottom: 1px solid var(--border);
270
+ margin-bottom: 36px;
271
+ }
272
+ #hero .pill {
273
+ display: inline-block;
274
+ font-family: var(--mono);
275
+ font-size: 10px;
276
+ letter-spacing: 0.15em;
277
+ text-transform: uppercase;
278
+ padding: 5px 14px;
279
+ border: 1px solid #2a3a4a;
280
+ border-radius: 99px;
281
+ color: #4b6a8a;
282
+ margin-bottom: 20px;
283
+ background: #0a131e;
284
  }
285
+ #hero h1 {
286
+ font-size: clamp(26px, 5vw, 42px);
287
+ font-weight: 800;
288
  letter-spacing: -0.04em;
289
+ color: #f1f5f9;
290
+ margin: 0 0 12px;
291
+ line-height: 1.1;
292
  }
293
+ #hero h1 span { color: #6366f1; }
294
+ #hero p {
295
  font-size: 14px;
296
+ color: var(--muted);
297
  margin: 0;
298
+ line-height: 1.7;
299
+ max-width: 520px;
300
+ margin: 0 auto;
301
  }
302
+
303
+ /* ── Two-column wrapper ── */
304
+ #main-row {
305
+ display: grid !important;
306
+ grid-template-columns: 1fr 1fr !important;
307
+ gap: 20px !important;
308
+ align-items: start !important;
 
 
 
 
309
  }
310
 
311
+ @media (max-width: 700px) {
312
+ #main-row {
313
+ grid-template-columns: 1fr !important;
314
+ }
315
+ }
316
+
317
+ /* ── Left panel ── */
318
+ #upload-panel {
319
+ background: var(--surface) !important;
320
  border: 1px solid var(--border) !important;
321
+ border-radius: 16px !important;
322
+ padding: 24px !important;
323
+ }
324
+ #upload-panel .panel-label {
325
+ font-size: 11px;
326
+ letter-spacing: 0.14em;
327
+ text-transform: uppercase;
328
+ color: var(--muted);
329
+ margin-bottom: 16px;
330
+ font-family: var(--mono);
331
  }
332
 
333
+ /* Gradio image component dark styling */
334
+ .upload-wrap .svelte-1ipelgc,
335
+ .upload-wrap [data-testid="image"] {
336
+ background: #080c12 !important;
337
+ border: 1.5px dashed #2a3a4a !important;
338
  border-radius: 12px !important;
339
+ min-height: 260px !important;
340
+ transition: border-color 0.25s;
 
341
  }
342
+ .upload-wrap [data-testid="image"]:hover {
343
  border-color: var(--accent) !important;
344
  }
345
 
346
+ /* ── Classify button ── */
347
+ #classify-btn {
348
+ margin-top: 14px !important;
349
+ width: 100% !important;
350
  background: var(--accent) !important;
351
  border: none !important;
352
+ border-radius: 10px !important;
353
  color: #fff !important;
354
+ font-family: var(--font) !important;
355
  font-size: 14px !important;
356
+ font-weight: 700 !important;
357
+ letter-spacing: 0.06em !important;
358
+ padding: 13px 0 !important;
359
  cursor: pointer !important;
360
+ transition: opacity 0.2s, transform 0.15s !important;
361
+ box-shadow: 0 0 24px rgba(99,102,241,0.35) !important;
362
+ }
363
+ #classify-btn:hover {
364
+ opacity: 0.88 !important;
365
+ transform: translateY(-1px) !important;
366
+ }
367
+ #classify-btn:active {
368
+ transform: translateY(0) !important;
369
  }
 
370
 
371
+ /* ── Upload hint text ── */
372
+ #upload-hint {
373
+ font-size: 12px;
374
+ color: #374151;
375
+ text-align: center;
376
+ margin-top: 10px;
377
+ line-height: 1.6;
378
  }
379
 
380
+ /* ── Stats strip ── */
381
+ #stats-strip {
382
+ display: flex;
383
+ gap: 12px;
384
+ margin-top: 16px;
385
+ }
386
+ .stat-chip {
387
+ flex: 1;
388
+ background: #0a131e;
389
+ border: 1px solid #1a2535;
390
+ border-radius: 8px;
391
+ padding: 10px 12px;
392
+ text-align: center;
393
+ }
394
+ .stat-chip .val {
395
+ font-size: 16px;
396
+ font-weight: 800;
397
+ color: #6366f1;
398
+ font-family: var(--mono);
399
+ display: block;
400
+ letter-spacing: -0.02em;
401
+ }
402
+ .stat-chip .lbl {
403
+ font-size: 10px;
404
+ color: #374151;
405
+ text-transform: uppercase;
406
+ letter-spacing: 0.1em;
407
+ margin-top: 2px;
408
+ display: block;
409
  }
410
 
411
+ /* ── Right panel / result ── */
412
  .result-panel > label { display: none !important; }
413
+ #result-col { align-self: stretch; }
414
 
415
+ /* ── Footer ── */
416
  #footer {
417
  text-align: center;
418
+ padding: 28px 16px;
419
  border-top: 1px solid var(--border);
420
+ margin-top: 36px;
421
  font-size: 12px;
422
+ color: #2d3748;
423
+ line-height: 1.8;
424
  }
425
+ #footer a { color: #4b6a8a; text-decoration: none; }
426
+ #footer a:hover { color: var(--accent); }
427
+
428
+ /* ── Gradio internal overrides ── */
429
+ label span {
430
+ font-family: var(--font) !important;
431
+ font-size: 11px !important;
432
+ font-weight: 600 !important;
433
+ letter-spacing: 0.1em !important;
434
+ text-transform: uppercase !important;
435
+ color: var(--muted) !important;
436
+ }
437
+
438
+ /* Remove default gradio row gaps */
439
+ .gr-row { gap: 0 !important; }
440
  """
441
 
442
+ # ── Gradio UI ─────────────────────────────────────────────────────
443
+ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="NeuroScan Β· Brain Tumor MRI Classifier") as demo:
444
 
445
+ # ── Hero ──────────────────────────────────────────────────────
446
  gr.HTML("""
447
+ <div id="hero">
448
+ <div class="pill">⚑ EfficientNet-B3 &nbsp;·&nbsp; 98.98% Val Acc &nbsp;·&nbsp; 4 Classes</div>
449
+ <h1>🧠 Neuro<span>Scan</span></h1>
450
+ <p>
451
+ AI-powered brain tumor detection from MRI scans.<br>
452
+ Classifies <strong style="color:#e5e7eb;">Glioma Β· Meningioma Β· Pituitary Tumor Β· No Tumor</strong><br>
453
+ in seconds β€” just upload your scan below.
454
+ </p>
455
  </div>
456
  """)
457
 
458
+ # ── Main two-column layout ─────────────────────────────────────
459
+ with gr.Row(elem_id="main-row"):
460
+
461
+ # ── Left: Upload panel ────────────────────────────────────
462
+ with gr.Column(elem_id="upload-panel", scale=1):
463
+ gr.HTML('<div class="panel-label">πŸ“€ Upload MRI Scan</div>')
464
+
465
  image_input = gr.Image(
466
  type="pil",
467
+ label="",
468
+ elem_classes=["upload-wrap"],
469
+ height=280,
470
+ show_label=False,
471
  )
 
472
 
473
+ gr.HTML("""
474
+ <div id="upload-hint">
475
+ πŸ–ΌοΈ Drag & drop or click to browse<br>
476
+ Supports <code style="color:#4b6a8a;">JPG Β· PNG Β· WEBP</code> &nbsp;Β·&nbsp; Axial / coronal / sagittal views
477
+ </div>
478
+ """)
479
+
480
+ run_btn = gr.Button("πŸ” Classify MRI Scan", elem_id="classify-btn")
481
+
482
+ gr.HTML("""
483
+ <div id="stats-strip">
484
+ <div class="stat-chip">
485
+ <span class="val">8.2K</span>
486
+ <span class="lbl">Train Images</span>
487
+ </div>
488
+ <div class="stat-chip">
489
+ <span class="val">98.98%</span>
490
+ <span class="lbl">Val Accuracy</span>
491
+ </div>
492
+ <div class="stat-chip">
493
+ <span class="val">4</span>
494
+ <span class="lbl">Classes</span>
495
+ </div>
496
+ </div>
497
+ """)
498
+
499
+ # ── Right: Result panel ───────────────────────────────────
500
+ with gr.Column(elem_id="result-col", scale=1):
501
  result_html = gr.HTML(
502
+ value=_empty_state(),
503
+ label="",
504
  elem_classes=["result-panel"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  )
506
 
507
+ # Hidden label output (internal use)
508
  label_output = gr.Label(visible=False)
509
 
510
+ # ── Event bindings ─────────────────────────────────────────────
511
+ run_btn.click(fn=predict, inputs=[image_input], outputs=[label_output, result_html])
512
+ image_input.change(fn=predict, inputs=[image_input], outputs=[label_output, result_html])
 
 
 
 
 
 
 
513
 
514
+ # ── Footer ────────────────────────────────────────────────────
515
  gr.HTML("""
516
  <div id="footer">
517
+ πŸ”¬ <strong style="color:#374151;">NeuroScan</strong> &nbsp;Β·&nbsp;
518
+ EfficientNet-B3 fine-tuned on Figshare + Kaggle Brain Tumor datasets &nbsp;Β·&nbsp;
519
+ <a href="https://huggingface.co/S-4-G-4-R/brain-tumor-efficientnet-b3" target="_blank">
520
+ πŸ€— Model on Hugging Face
521
+ </a>
522
+ <br>
523
+ ⚠️ This tool is intended for research and educational purposes only.
524
+ It is <strong>not</strong> a substitute for clinical diagnosis.
525
  </div>
526
  """)
527
 
528
+
529
  if __name__ == "__main__":
530
+ demo.launch()