itsLu commited on
Commit
5205645
·
0 Parent(s):

Initial commit

Browse files
.gitattributes ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ *.h5 filter=lfs diff=lfs merge=lfs -text
2
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
3
+ *.png filter=lfs diff=lfs merge=lfs -text
4
+ *.svg filter=lfs diff=lfs merge=lfs -text
5
+ *.jpg filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Backend (Flask) ---
2
+ FROM python:3.9-slim
3
+ WORKDIR /app
4
+
5
+ # Copy backend files
6
+ COPY app.py requirements.txt README.md ./
7
+ COPY models/ ./models/
8
+
9
+ # Copy static assets (brain examples, etc.) if present
10
+ COPY static/ ./static/
11
+
12
+ # Install Python deps
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ ENV PORT=7860
16
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: CerebroScan
3
+ emoji: 🧠
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
app.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ from dataclasses import dataclass
4
+ from typing import Dict, List, Tuple
5
+
6
+ import numpy as np
7
+ from flask import Flask, jsonify, request
8
+ from flask_cors import CORS
9
+
10
+ import tensorflow as tf
11
+ from tensorflow.keras.models import load_model
12
+
13
+
14
+ # ----------------------------
15
+ # Model definitions
16
+ # ----------------------------
17
+
18
+ @dataclass(frozen=True)
19
+ class ModelSpec:
20
+ id: str
21
+ display_name: str # what the user sees (friendly + technical)
22
+ filename: str # under ./models/
23
+ arch: str # "resnet" | "efficientnet"
24
+ img_size: int # input resolution
25
+ class_names: Tuple[str, ...] # output order used during training
26
+ recommended_threshold: float # per-model uncertainty cutoff (from notebooks)
27
+
28
+
29
+ # NOTE:
30
+ # Your training notebooks use *different* class ordering between the ResNet notebooks
31
+ # (sorted unique categories) and the EfficientNet notebook (explicit list).
32
+ # We keep per-model class order to avoid mislabeling probabilities.
33
+ RESNET_CLASS_ORDER = ("MildDemented", "ModerateDemented", "NonDemented", "VeryMildDemented")
34
+ EFFICIENTNET_CLASS_ORDER = ("NonDemented", "VeryMildDemented", "MildDemented", "ModerateDemented")
35
+
36
+ MODEL_SPECS: List[ModelSpec] = [
37
+ ModelSpec("atlas", "Atlas — ResNet-50", "resnet50.h5", "resnet", 224, RESNET_CLASS_ORDER, 0.95),
38
+ ModelSpec("orion", "Orion — ResNet-101", "resnet101.h5", "resnet", 224, RESNET_CLASS_ORDER, 0.95),
39
+ ModelSpec("pulse", "Pulse — EfficientNet-B2", "efficientnetb2.h5", "efficientnet", 260, EFFICIENTNET_CLASS_ORDER, 0.95),
40
+ ]
41
+
42
+
43
+
44
+ # ----------------------------
45
+ # Flask app
46
+ # ----------------------------
47
+
48
+ app = Flask(__name__)
49
+ CORS(app, resources={r"/api/*": {"origins": "*"}})
50
+
51
+ # Lazy-loaded models (load on first use). Keep only what we need in CPU Spaces.
52
+ _loaded_models: Dict[str, tf.keras.Model] = {}
53
+
54
+
55
+ def _get_spec(model_id: str) -> ModelSpec:
56
+ for s in MODEL_SPECS:
57
+ if s.id == model_id:
58
+ return s
59
+ raise KeyError(f"Unknown model_id: {model_id}")
60
+
61
+
62
+ def _get_preprocess_fn(arch: str):
63
+ if arch == "resnet":
64
+ from tensorflow.keras.applications.resnet50 import preprocess_input as resnet_preprocess
65
+ return resnet_preprocess
66
+ if arch == "efficientnet":
67
+ from tensorflow.keras.applications.efficientnet import preprocess_input as eff_preprocess
68
+ return eff_preprocess
69
+ raise ValueError(f"Unknown arch: {arch}")
70
+
71
+
72
+ def _load_model(spec: ModelSpec) -> tf.keras.Model:
73
+ if spec.id in _loaded_models:
74
+ return _loaded_models[spec.id]
75
+
76
+ model_path = os.path.join(os.path.dirname(__file__), "models", spec.filename)
77
+ if not os.path.exists(model_path):
78
+ raise FileNotFoundError(
79
+ f"Model file not found: {model_path}. "
80
+ f"Place it at models/{spec.filename} in your Space."
81
+ )
82
+
83
+ # CPU-friendly TF settings (small wins on free Spaces)
84
+ try:
85
+ tf.config.threading.set_intra_op_parallelism_threads(0)
86
+ tf.config.threading.set_inter_op_parallelism_threads(0)
87
+ except Exception:
88
+ pass
89
+
90
+ model = load_model(model_path, compile=False)
91
+ _loaded_models[spec.id] = model
92
+ return model
93
+
94
+
95
+ def _read_image(file_storage, img_size: int, preprocess_fn):
96
+ # Decode image
97
+ raw = file_storage.read()
98
+ image = tf.io.decode_image(raw, channels=3, expand_animations=False)
99
+ image = tf.image.resize(image, [img_size, img_size])
100
+ image = tf.cast(image, tf.float32)
101
+ image = preprocess_fn(image)
102
+ image = tf.expand_dims(image, axis=0) # [1, H, W, 3]
103
+ return image
104
+
105
+
106
+ def _predict(model: tf.keras.Model, image_tensor, class_names: Tuple[str, ...], threshold: float):
107
+ probs = model.predict(image_tensor, verbose=0)[0].astype(float)
108
+ probs = np.clip(probs, 0.0, 1.0)
109
+
110
+ best_idx = int(np.argmax(probs))
111
+ best_prob = float(np.max(probs))
112
+
113
+ # Add "Uncertain" post-hoc (not a model output class)
114
+ is_uncertain = best_prob < threshold
115
+
116
+ # Build response payload
117
+ by_class = [
118
+ {"id": name, "label": _pretty_label(name), "prob": float(probs[i])}
119
+ for i, name in enumerate(class_names)
120
+ ]
121
+ by_class.sort(key=lambda x: x["prob"], reverse=True)
122
+
123
+ return {
124
+ "prediction": {
125
+ "id": "Uncertain" if is_uncertain else class_names[best_idx],
126
+ "label": "Uncertain" if is_uncertain else _pretty_label(class_names[best_idx]),
127
+ "confidence": best_prob,
128
+ "threshold": threshold,
129
+ },
130
+ "probabilities": by_class,
131
+ }
132
+
133
+
134
+ def _pretty_label(name: str) -> str:
135
+ # Internal training labels -> user-facing labels (final wording)
136
+ mapping = {
137
+ "NonDemented": "Healthy",
138
+ "VeryMildDemented": "Very Mildly Demented",
139
+ "MildDemented": "Mildly Demented",
140
+ "ModerateDemented": "Moderately Demented",
141
+ # Post-hoc
142
+ "Uncertain": "Uncertain",
143
+ }
144
+ return mapping.get(name, name)
145
+
146
+
147
+ @app.get("/api/models")
148
+ def api_models():
149
+ return jsonify({
150
+ "models": [
151
+ {
152
+ "id": s.id,
153
+ "name": s.display_name,
154
+ "img_size": s.img_size,
155
+ "classes": [{"id": c, "label": _pretty_label(c)} for c in s.class_names],
156
+ "recommended_threshold": s.recommended_threshold,
157
+ }
158
+ for s in MODEL_SPECS
159
+ ],
160
+ "default_model_id": MODEL_SPECS[0].id,
161
+ })
162
+
163
+
164
+ @app.post("/api/classify")
165
+ def api_classify():
166
+ if "file" not in request.files:
167
+ return jsonify({"error": "No file uploaded (field name must be 'file')."}), 400
168
+
169
+ model_id = request.form.get("model_id", MODEL_SPECS[0].id)
170
+ spec = _get_spec(model_id)
171
+ # Threshold is model-specific and not user-adjustable
172
+ threshold = spec.recommended_threshold
173
+
174
+ try:
175
+ model = _load_model(spec)
176
+ preprocess_fn = _get_preprocess_fn(spec.arch)
177
+
178
+ image_tensor = _read_image(request.files["file"], spec.img_size, preprocess_fn)
179
+ payload = _predict(model, image_tensor, spec.class_names, threshold)
180
+
181
+ payload["model"] = {"id": spec.id, "name": spec.display_name}
182
+ return jsonify(payload)
183
+
184
+ except FileNotFoundError as e:
185
+ return jsonify({"error": str(e)}), 500
186
+ except Exception as e:
187
+ return jsonify({"error": f"Failed to classify image: {e}"}), 500
188
+
189
+
190
+ if __name__ == "__main__":
191
+ # Local dev: python app.py
192
+ # In Spaces (Dockerfile), gunicorn is used.
193
+ app.run(host="0.0.0.0", port=int(os.getenv("PORT", "7860")), debug=False)
models/README.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Place your model files here:
2
+ - resnet50.h5
3
+ - resnet101.h5
4
+ - efficientnetb2.h5
models/efficientnetb2.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:92ca0621bacfb11477e4242a4d38409b4f44a33483b5167dc5808e3413f7e243
3
+ size 102821032
models/resnet101.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:33946fb58dc03e58886e2b40fabff8ce545c6d75808b3ac87ab7d6b4cd75d39a
3
+ size 524986352
models/resnet50.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b892a36a277c2210a545eb8743425188a3ab8893196512be55ac5d7250dec445
3
+ size 296838384
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ tensorflow-cpu
4
+ numpy
5
+ pillow
6
+ h5py
7
+ gunicorn
static/assets/brain/PLACE_IMAGES_HERE.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Put these files here:
2
+ - supportedexample.jpg
3
+ - unsupportedexample.jpg
static/assets/brain/brain.svg ADDED

Git LFS Details

  • SHA256: 117ee61def3c5d5c8158bc1787cb5515f4da680dffefc106d4bfd312ace3f7c3
  • Pointer size: 128 Bytes
  • Size of remote file: 540 Bytes
static/assets/brain/supportedexample.jpg ADDED

Git LFS Details

  • SHA256: e781f3619b0021e39ded4ea45a2252bdc06ca3227cfbefd44d9f99bb5785876e
  • Pointer size: 131 Bytes
  • Size of remote file: 129 kB
static/assets/brain/unsupportedexample.jpg ADDED

Git LFS Details

  • SHA256: 8796fd3b447a8babe7c8693abf00bf0a3237833dd8802e66bd4c9c68c591440c
  • Pointer size: 129 Bytes
  • Size of remote file: 3.42 kB
static/assets/images/header.png ADDED

Git LFS Details

  • SHA256: 04c4d24fe909758ebf7e295464521d684d9602aa9b2f2b2394fc9e1443048e75
  • Pointer size: 130 Bytes
  • Size of remote file: 50.7 kB
static/assets/images/team/asem.jpg ADDED

Git LFS Details

  • SHA256: 7205eec76b59918e144962dc2ddeb6b14b3fb3870373bcb469d70691a6528137
  • Pointer size: 132 Bytes
  • Size of remote file: 2.19 MB
static/assets/images/team/fatma.jpg ADDED

Git LFS Details

  • SHA256: 356379bac8a1c6f9969f5233cc2692d42081484360137331f051bf2205e18882
  • Pointer size: 131 Bytes
  • Size of remote file: 191 kB
static/assets/images/team/gehad.jpg ADDED

Git LFS Details

  • SHA256: 4aefefdcf0e8bce9db5a809409beec460a6457dfb2bd6195cb1d85e8e2c7ed2f
  • Pointer size: 131 Bytes
  • Size of remote file: 441 kB
static/assets/images/team/heba.jpg ADDED

Git LFS Details

  • SHA256: 4b35ceb8306aa16ce34409a68d5b0a45fd092ed979243f23fabbdcea1a992acc
  • Pointer size: 131 Bytes
  • Size of remote file: 349 kB
static/assets/images/team/sameh.jpg ADDED

Git LFS Details

  • SHA256: f5f51e2c260ddad90feb0a360abd1755a015fc9c77771c357270f48c57d22dbf
  • Pointer size: 130 Bytes
  • Size of remote file: 85.9 kB
templates/index.html ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>CerebroScan</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
8
+ <style>
9
+ :root{
10
+ --bg:#0b1220;
11
+ --card:#0f1b33;
12
+ --card2:#0c172e;
13
+ --text:#e8eefc;
14
+ --muted:#a9b7d0;
15
+ --border:rgba(255,255,255,.12);
16
+ --primary:#4f7cff;
17
+ --ring:rgba(79,124,255,.35);
18
+ }
19
+
20
+ :root[data-theme="light"]{
21
+ --bg:#f6f7fb;
22
+ --card:#ffffff;
23
+ --card2:#ffffff;
24
+ --text:#0c1222;
25
+ --muted:#44506a;
26
+ --border:rgba(0,0,0,.12);
27
+ --primary:#355dff;
28
+ --ring:rgba(53,93,255,.25);
29
+ }
30
+
31
+ *{box-sizing:border-box}
32
+ body{
33
+ margin:0;
34
+ font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
35
+ background:var(--bg);
36
+ color:var(--text);
37
+ }
38
+
39
+ header{
40
+ display:flex;
41
+ justify-content:space-between;
42
+ align-items:center;
43
+ padding:20px 28px;
44
+ }
45
+
46
+ .brand{display:flex;gap:12px;align-items:center}
47
+ .brand img{width:40px;height:40px}
48
+
49
+ .chip{
50
+ padding:8px 12px;
51
+ border-radius:999px;
52
+ background:var(--card);
53
+ border:1px solid var(--border);
54
+ cursor:pointer;
55
+ }
56
+
57
+ nav{
58
+ padding:0 28px 18px;
59
+ display:flex;
60
+ gap:10px;
61
+ }
62
+
63
+ .tab{
64
+ padding:8px 16px;
65
+ border-radius:999px;
66
+ border:1px solid var(--border);
67
+ background:rgba(255,255,255,.06);
68
+ cursor:pointer;
69
+ }
70
+
71
+ .tab.active{
72
+ background:rgba(79,124,255,.18);
73
+ box-shadow:0 0 0 4px var(--ring);
74
+ }
75
+
76
+ .section{display:none}
77
+ .section.show{display:block}
78
+
79
+ main{
80
+ display:grid;
81
+ grid-template-columns:1.2fr 1fr;
82
+ gap:24px;
83
+ padding:0 28px 28px;
84
+ }
85
+ @media(max-width:900px){main{grid-template-columns:1fr}}
86
+
87
+ .card{
88
+ background:linear-gradient(180deg,var(--card),var(--card2));
89
+ border-radius:18px;
90
+ border:1px solid var(--border);
91
+ padding:22px;
92
+ }
93
+
94
+ h2{margin:0 0 12px;font-size:20px}
95
+
96
+ .controls{
97
+ display:flex;
98
+ gap:12px;
99
+ flex-wrap:wrap;
100
+ align-items:center;
101
+ }
102
+
103
+ .select-wrap{position:relative}
104
+ .select-wrap::after{
105
+ content:"▾";
106
+ position:absolute;
107
+ right:14px;
108
+ top:50%;
109
+ transform:translateY(-50%);
110
+ pointer-events:none;
111
+ color:var(--muted);
112
+ }
113
+
114
+ select{
115
+ appearance:none;
116
+ padding:10px 40px 10px 14px;
117
+ border-radius:12px;
118
+ border:1px solid var(--border);
119
+ background:rgba(255,255,255,.06);
120
+ color:var(--text);
121
+ font-weight:600;
122
+ min-width:260px;
123
+ }
124
+
125
+ .file-btn{
126
+ padding:10px 14px;
127
+ border-radius:12px;
128
+ border:1px dashed var(--border);
129
+ cursor:pointer;
130
+ background:rgba(255,255,255,.04);
131
+ }
132
+
133
+ button.primary{
134
+ padding:10px 18px;
135
+ border-radius:12px;
136
+ border:none;
137
+ background:linear-gradient(180deg,var(--primary),#2f62ff);
138
+ color:white;
139
+ font-weight:700;
140
+ cursor:pointer;
141
+ }
142
+
143
+ button.secondary{
144
+ padding:10px 14px;
145
+ border-radius:12px;
146
+ border:1px solid var(--border);
147
+ background:rgba(255,255,255,.04);
148
+ cursor:pointer;
149
+ }
150
+
151
+ button:disabled{opacity:.6}
152
+
153
+ .upload-box{
154
+ margin-top:14px;
155
+ padding:18px;
156
+ border:2px dashed var(--border);
157
+ border-radius:14px;
158
+ text-align:center;
159
+ color:var(--muted);
160
+ }
161
+
162
+ .preview{margin-top:14px;background:black;border-radius:14px;overflow:hidden}
163
+ .preview.hidden{display:none}
164
+ .preview img{width:100%;max-height:360px;object-fit:contain}
165
+
166
+ .result{margin-top:16px}
167
+ .result-title{font-size:22px;font-weight:800}
168
+ .result-msg{color:var(--muted);margin-top:6px}
169
+
170
+ .examples .ex{
171
+ margin-top:12px;
172
+ border:1px solid var(--border);
173
+ border-radius:14px;
174
+ overflow:hidden;
175
+ }
176
+ .examples .lbl{padding:10px;font-weight:800;border-bottom:1px solid var(--border)}
177
+ .examples .desc{padding:0 10px 10px;color:var(--muted)}
178
+ .examples img{width:100%;max-height:280px;object-fit:contain;background:black}
179
+
180
+ .team{
181
+ display:grid;
182
+ grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
183
+ gap:14px;
184
+ }
185
+ .member{
186
+ display:flex;
187
+ gap:12px;
188
+ align-items:center;
189
+ padding:10px;
190
+ border:1px solid var(--border);
191
+ border-radius:14px;
192
+ }
193
+ .member img{width:48px;height:48px;border-radius:50%;cursor:pointer}
194
+
195
+ footer{
196
+ padding:14px 28px;
197
+ border-top:1px solid var(--border);
198
+ font-size:13px;
199
+ color:var(--muted);
200
+ }
201
+
202
+ /* modal */
203
+ .modal-backdrop{
204
+ position:fixed;
205
+ inset:0;
206
+ background:rgba(0,0,0,.6);
207
+ display:none;
208
+ align-items:center;
209
+ justify-content:center;
210
+ }
211
+ .modal-backdrop.show{display:flex}
212
+ .modal{
213
+ background:var(--card);
214
+ padding:18px;
215
+ border-radius:16px;
216
+ border:1px solid var(--border);
217
+ width:420px;
218
+ }
219
+ </style>
220
+ </head>
221
+
222
+ <body>
223
+
224
+ <header>
225
+ <div class="brand">
226
+ <img src="{{ url_for('static', filename='assets/brain/brain.svg') }}">
227
+ <div>
228
+ <strong>CerebroScan</strong><br>
229
+ <small style="color:var(--muted)">AI-assisted MRI screening (demo)</small>
230
+ </div>
231
+ </div>
232
+ <button id="themeToggle" class="chip">☀️</button>
233
+ </header>
234
+
235
+ <nav>
236
+ <button class="tab active" onclick="showSection('scan',this)">Scan</button>
237
+ <button class="tab" onclick="showSection('info',this)">Info</button>
238
+ </nav>
239
+
240
+ <section id="scan" class="section show">
241
+ <main>
242
+ <div class="card">
243
+ <h2>New scan</h2>
244
+
245
+ <div class="controls">
246
+ <span class="select-wrap"><select id="modelSelect"></select></span>
247
+
248
+ <label class="file-btn">
249
+ Choose image
250
+ <input type="file" id="fileInput" accept="image/*" hidden>
251
+ </label>
252
+
253
+ <button class="primary" id="runBtn" onclick="runScan()">Run scan</button>
254
+ <button class="secondary" id="newBtn" style="display:none" onclick="newScan()">New scan</button>
255
+ </div>
256
+
257
+ <div class="upload-box" id="dropZone">
258
+ Drag & drop or paste (Ctrl+V) an MRI image
259
+ </div>
260
+
261
+ <div class="preview hidden" id="previewBox">
262
+ <img id="previewImg">
263
+ </div>
264
+
265
+ <div class="result">
266
+ <div id="resultTitle" class="result-title">—</div>
267
+ <div id="resultMsg" class="result-msg">Upload an image to begin.</div>
268
+ <button id="likelyBtn" class="secondary" style="display:none;margin-top:10px" onclick="showLikely()">Show most likely result</button>
269
+ </div>
270
+ </div>
271
+
272
+ <div class="card">
273
+ <h2>Examples</h2>
274
+ <div class="examples">
275
+ <div class="ex">
276
+ <div class="lbl">Supported example</div>
277
+ <div class="desc">Brain-only MRI slice (no skull visible).</div>
278
+ <img src="{{ url_for('static', filename='assets/brain/supportedexample.jpg') }}">
279
+ </div>
280
+ <div class="ex">
281
+ <div class="lbl">Unsupported example</div>
282
+ <div class="desc">Skull visible — please upload a brain-only slice.</div>
283
+ <img src="{{ url_for('static', filename='assets/brain/unsupportedexample.jpg') }}">
284
+ </div>
285
+ </div>
286
+ </div>
287
+ </main>
288
+ </section>
289
+
290
+ <section id="info" class="section">
291
+ <main>
292
+ <div class="card">
293
+ <h2>About CerebroScan</h2>
294
+ <p>Educational demo for Alzheimer’s stage classification. Low confidence → <b>Uncertain</b>.</p>
295
+
296
+ <h2>Under the supervision of:</h2>
297
+ <p>
298
+ Prof. Muhammad Sayed Hammad<br>
299
+ Eng. Heidi Ahmed
300
+ </p>
301
+
302
+ <h2>Team</h2>
303
+ <div class="team">
304
+ <div class="member"><img src="{{ url_for('static', filename='assets/images/team/fatma.jpg') }}" onclick="window.open('https://www.linkedin.com/in/fatma-al-zahraa-emad-326b64234/')"><span>Fatma Al-Zahraa Emad</span></div>
305
+ <div class="member"><img src="{{ url_for('static', filename='assets/images/team/gehad.jpg') }}" onclick="window.open('https://www.linkedin.com/in/gehad-mohamed-2a4946252/')"><span>Gehad Mohamed</span></div>
306
+ <div class="member"><img src="{{ url_for('static', filename='assets/images/team/heba.jpg') }}" onclick="window.open('https://www.linkedin.com/in/hebatullah-elgazoly-308ab2243/')"><span>Hebatullah El Gazoly</span></div>
307
+ <div class="member"><img src="{{ url_for('static', filename='assets/images/team/asem.jpg') }}" onclick="window.open('https://www.linkedin.com/in/mohamedasem318/')"><span>Mohamed Assem</span></div>
308
+ <div class="member"><img src="{{ url_for('static', filename='assets/images/team/sameh.jpg') }}" onclick="window.open('https://www.linkedin.com/in/muhamedsameh/')"><span>Mohamed Sameh</span></div>
309
+ </div>
310
+ </div>
311
+ </main>
312
+ </section>
313
+
314
+ <footer>
315
+ Contact: <a href="mailto:mohamedasem318@gmail.com">Mohamed Assem</a> •
316
+ <a href="mailto:mohamed.sameh8103@gmail.com">Mohamed Sameh</a><br>
317
+ Educational demo — not medical advice
318
+ </footer>
319
+
320
+ <div class="modal-backdrop" id="modalBackdrop" onclick="closeModalIfBackdrop(event)">
321
+ <div class="modal">
322
+ <h3>Most likely result</h3>
323
+ <p id="modalLabel"></p>
324
+ <p id="modalProb"></p>
325
+ <button class="secondary" onclick="closeModal()">Close</button>
326
+ </div>
327
+ </div>
328
+
329
+ <script>
330
+ function showSection(id,btn){
331
+ document.querySelectorAll('.section').forEach(s=>s.classList.remove('show'));
332
+ document.getElementById(id).classList.add('show');
333
+ document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
334
+ btn.classList.add('active');
335
+ }
336
+
337
+ function setTheme(t){
338
+ document.documentElement.dataset.theme=t;
339
+ localStorage.setItem("theme",t);
340
+ themeToggle.textContent=t==="dark"?"☀️":"🌙";
341
+ }
342
+ themeToggle.onclick=()=>setTheme(document.documentElement.dataset.theme==="dark"?"light":"dark");
343
+ setTheme(localStorage.getItem("theme")||"dark");
344
+
345
+ let currentFile=null,lastMostLikely=null;
346
+ const previewBox=document.getElementById("previewBox");
347
+ const previewImg=document.getElementById("previewImg");
348
+
349
+ function setFile(f){
350
+ currentFile=f;
351
+ previewImg.src=URL.createObjectURL(f);
352
+ previewBox.classList.remove("hidden");
353
+ resultTitle.textContent="—";
354
+ resultMsg.textContent="Ready to run scan.";
355
+ likelyBtn.style.display="none";
356
+ newBtn.style.display="none";
357
+ lastMostLikely=null;
358
+ }
359
+
360
+ fileInput.onchange=e=>e.target.files[0]&&setFile(e.target.files[0]);
361
+
362
+ dropZone.ondragover=e=>{e.preventDefault()};
363
+ dropZone.ondrop=e=>{
364
+ e.preventDefault();
365
+ e.dataTransfer.files[0]&&setFile(e.dataTransfer.files[0]);
366
+ };
367
+
368
+ window.addEventListener("paste",e=>{
369
+ for(const i of e.clipboardData.items){
370
+ if(i.type.startsWith("image/")){setFile(i.getAsFile());break;}
371
+ }
372
+ });
373
+
374
+ async function loadModels(){
375
+ const r=await fetch("/api/models");
376
+ const d=await r.json();
377
+ modelSelect.innerHTML="";
378
+ d.models.forEach(m=>{
379
+ const o=document.createElement("option");
380
+ o.value=m.id;o.textContent=m.name;
381
+ modelSelect.appendChild(o);
382
+ });
383
+ modelSelect.value=d.default_model_id;
384
+ }
385
+
386
+ async function runScan(){
387
+ if(!currentFile){resultMsg.textContent="Upload an image first.";return;}
388
+ runBtn.disabled=true;
389
+ modelSelect.disabled=true;
390
+ resultTitle.textContent="Running…";
391
+ resultMsg.textContent="Analyzing image…";
392
+ likelyBtn.style.display="none";
393
+ lastMostLikely=null;
394
+
395
+ try{
396
+ const fd=new FormData();
397
+ fd.append("file",currentFile);
398
+ fd.append("model_id",modelSelect.value);
399
+ const r=await fetch("/api/classify",{method:"POST",body:fd});
400
+ const d=await r.json();
401
+
402
+ if(d.prediction.label==="Uncertain"){
403
+ resultTitle.textContent="Uncertain";
404
+ resultMsg.textContent="Consult a professional.";
405
+ const probs=[...(d.probabilities||[])].sort((a,b)=>b.prob-a.prob);
406
+ if(probs[0]){
407
+ lastMostLikely=probs[0];
408
+ likelyBtn.style.display="inline-block";
409
+ }
410
+ }else{
411
+ resultTitle.textContent=d.prediction.label;
412
+ resultMsg.textContent=`Confidence: ${(d.prediction.confidence*100).toFixed(1)}%`;
413
+ likelyBtn.style.display="none";
414
+ lastMostLikely=null;
415
+ }
416
+ newBtn.style.display="inline-block";
417
+ }catch(e){
418
+ resultTitle.textContent="Sorry";
419
+ resultMsg.textContent="Something went wrong.";
420
+ }finally{
421
+ runBtn.disabled=false;
422
+ modelSelect.disabled=false;
423
+ }
424
+ }
425
+
426
+ function newScan(){
427
+ currentFile=null;
428
+ fileInput.value="";
429
+ previewBox.classList.add("hidden");
430
+ previewImg.src="";
431
+ resultTitle.textContent="—";
432
+ resultMsg.textContent="Upload an image to begin.";
433
+ likelyBtn.style.display="none";
434
+ newBtn.style.display="none";
435
+ lastMostLikely=null;
436
+ }
437
+
438
+ function showLikely(){
439
+ if(!lastMostLikely)return;
440
+ modalLabel.textContent=lastMostLikely.label;
441
+ modalProb.textContent=`Probability: ${(lastMostLikely.prob*100).toFixed(1)}%`;
442
+ modalBackdrop.classList.add("show");
443
+ }
444
+ function closeModal(){modalBackdrop.classList.remove("show")}
445
+ function closeModalIfBackdrop(e){if(e.target.id==="modalBackdrop")closeModal()}
446
+
447
+ loadModels();
448
+ </script>
449
+
450
+ </body>
451
+ </html>