S-4-G-4-R commited on
Commit
06a1fde
Β·
verified Β·
1 Parent(s): a996225

Initial Commit

Browse files
Files changed (3) hide show
  1. README.md +119 -7
  2. app.py +417 -0
  3. requirements.txt +6 -0
README.md CHANGED
@@ -1,14 +1,126 @@
1
  ---
2
- title: Brain Tumor Detection
3
- emoji: πŸƒ
4
- colorFrom: red
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.10.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: Takes a MRI Scan Image and classifies it to tumor detection
 
 
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Brain Tumor MRI Classifier
3
+ emoji: 🧠
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 5.29.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
+ tags:
12
+ - medical-imaging
13
+ - brain-tumor
14
+ - efficientnet
15
+ - image-classification
16
+ - pytorch
17
+ - mri
18
  ---
19
 
20
+ # Brain Tumor MRI Classifier β€” EfficientNet-B3
21
+
22
+ A fine-tuned **EfficientNet-B3** model for 4-class brain tumor classification from MRI scans, achieving **98.98% validation accuracy** and **0.9896 macro F1**.
23
+
24
+ ## Classes
25
+
26
+ | Class | Description |
27
+ |---|---|
28
+ | Glioma | Tumor originating in glial cells of the brain or spine |
29
+ | Meningioma | Tumor arising from the meninges surrounding the brain |
30
+ | Pituitary Tumor | Tumor in the pituitary gland at the base of the brain |
31
+ | No Tumor | No tumor detected in the MRI scan |
32
+
33
+ ## Model
34
+
35
+ - **Architecture**: EfficientNet-B3 (ImageNet pretrained) with custom classification head
36
+ - **Head**: `Dropout β†’ Linear(1536, 512) β†’ SiLU β†’ Dropout β†’ Linear(512, 4)`
37
+ - **Input size**: 300 Γ— 300
38
+ - **Training**: Two-phase β€” backbone frozen for 5 epochs (head LR 1e-3), then full fine-tune with differential LR (backbone 1e-4, head 1e-3)
39
+ - **Schedule**: Cosine decay with 3-epoch linear warmup
40
+ - **Loss**: Class-weighted cross-entropy
41
+
42
+ ## Weights
43
+
44
+ The model weights (`model.pt`) are hosted in this repository and downloaded automatically on first run via `huggingface_hub`.
45
+
46
+ To download manually:
47
+
48
+ ```python
49
+ from huggingface_hub import hf_hub_download
50
+ ckpt_path = hf_hub_download(repo_id="your-hf-username/brain-tumor-efficientnet-b3", filename="model.pt")
51
+ ```
52
+
53
+ ## Dataset
54
+
55
+ Trained on a merged dataset from two sources:
56
+
57
+ - **Figshare Brain Tumor Dataset** β€” glioma, meningioma, pituitary MRI scans
58
+ - **Kaggle Brain Tumor MRI Dataset** β€” 4-class dataset with glioma, meningioma, pituitary, no tumor
59
+
60
+ | Split | Images |
61
+ |---|---|
62
+ | Train | 8,211 |
63
+ | Validation | 2,053 |
64
+
65
+ ## Results
66
+
67
+ | Metric | Score |
68
+ |---|---|
69
+ | Accuracy | 0.9898 |
70
+ | Macro F1 | 0.9896 |
71
+ | Weighted F1 | 0.9898 |
72
+
73
+ Per-class F1: Glioma 0.9915 Β· Meningioma 0.9832 Β· No Tumor 0.9903 Β· Pituitary 0.9935
74
+
75
+ ## Usage
76
+
77
+ ```python
78
+ import torch
79
+ import torch.nn as nn
80
+ from torchvision import transforms
81
+ from torchvision.models import efficientnet_b3
82
+ from huggingface_hub import hf_hub_download
83
+ from PIL import Image
84
+
85
+ class EfficientNetClassifier(nn.Module):
86
+ def __init__(self, num_classes=4, dropout=0.4):
87
+ super().__init__()
88
+ self.backbone = efficientnet_b3(weights=None)
89
+ in_features = self.backbone.classifier[1].in_features
90
+ self.backbone.classifier = nn.Sequential(
91
+ nn.Dropout(p=dropout, inplace=True),
92
+ nn.Linear(in_features, 512),
93
+ nn.SiLU(),
94
+ nn.Dropout(p=dropout / 2),
95
+ nn.Linear(512, num_classes),
96
+ )
97
+ def forward(self, x):
98
+ return self.backbone(x)
99
+
100
+ # Load
101
+ ckpt_path = hf_hub_download("your-hf-username/brain-tumor-efficientnet-b3", "model.pt")
102
+ ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=False)
103
+ id_to_label = {int(k): v for k, v in ckpt["id_to_label"].items()}
104
+
105
+ model = EfficientNetClassifier()
106
+ model.load_state_dict(ckpt["model"])
107
+ model.eval()
108
+
109
+ # Infer
110
+ transform = transforms.Compose([
111
+ transforms.Resize((300, 300)),
112
+ transforms.ToTensor(),
113
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
114
+ ])
115
+
116
+ img = Image.open("mri_scan.jpg").convert("RGB")
117
+ with torch.no_grad():
118
+ probs = torch.softmax(model(transform(img).unsqueeze(0)), dim=-1)[0]
119
+ pred = id_to_label[probs.argmax().item()]
120
+
121
+ print(pred)
122
+ ```
123
+
124
+ ## Disclaimer
125
+
126
+ This model is intended for **research purposes only** and is not a certified medical diagnostic tool. Do not use for clinical decision-making.
app.py ADDED
@@ -0,0 +1,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn as nn
4
+ from torchvision import transforms
5
+ from torchvision.models import efficientnet_b3
6
+ from PIL import Image
7
+ 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),
52
+ nn.SiLU(),
53
+ nn.Dropout(p=dropout / 2),
54
+ nn.Linear(512, num_classes),
55
+ )
56
+
57
+ def forward(self, x):
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=HF_REPO_ID, 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
+
80
+ transform = transforms.Compose([
81
+ transforms.Resize((IMG_SIZE, IMG_SIZE)),
82
+ transforms.ToTensor(),
83
+ transforms.Normalize(MEAN, STD),
84
+ ])
85
+
86
+
87
+ # ── Inference ─────────────────────────────────────────────────────
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()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ torch==2.10.0
2
+ torchvision==0.25.0
3
+ gradio==5.29.0
4
+ huggingface_hub==1.4.1
5
+ pillow==12.1.1
6
+ numpy==2.4.2