katelynhur commited on
Commit
8aa85bf
·
verified ·
1 Parent(s): 6151579

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +799 -239
app.py CHANGED
@@ -1,6 +1,4 @@
1
  from pathlib import Path
2
- import json
3
- import re
4
  import traceback
5
 
6
  import gradio as gr
@@ -9,7 +7,6 @@ import numpy as np
9
  import pandas as pd
10
  import plotly.graph_objects as go
11
 
12
-
13
  from model_adapter import (
14
  MODEL_SPECS,
15
  load_roi_model,
@@ -19,9 +16,11 @@ from model_adapter import (
19
  )
20
  from roi_extractor import extract_rois_from_t1
21
 
 
22
  APP_DIR = Path(__file__).resolve().parent
23
  MODELS_DIR = APP_DIR / "models"
24
 
 
25
  DISPLAY_CLASSES = [
26
  "No Impairment",
27
  "Very Mild Impairment",
@@ -37,16 +36,19 @@ CLASS_COLORS = {
37
  "Moderate Impairment": "#ef4444",
38
  }
39
 
 
40
  TASK_OPTIONS = [
41
  "Progression",
42
  "Development",
43
  ]
44
 
 
45
  MRI_STRATEGY_OPTIONS = [
46
  "Multi-Region ROI",
47
  "Hippocampal ROI",
48
  ]
49
 
 
50
  CORE_FEATURES = [
51
  "age_at_visit",
52
  "sex",
@@ -65,20 +67,29 @@ CORE_FEATURES = [
65
  "entorhinal_thickness_l",
66
  "entorhinal_thickness_r",
67
  ]
 
 
68
  DEFAULT_CORE_ROWS = [[name, ""] for name in CORE_FEATURES]
69
 
70
 
71
  PROJECT_MD = r"""
72
- # AD-Stage-Net v2: Multimodal Alzheimer’s Disease Prediction
73
 
74
- **AD-Stage-Net Multimodal** extends the original MRI classification project by combining
75
- anatomy-focused structural MRI with clinical and cognitive information.
76
 
77
- Upload a **whole-volume T1 MRI** (`.nii` / `.nii.gz`) to run either the hippocampal
78
- or multi-region multimodal model. The application automatically performs anatomical
79
- segmentation and ROI extraction before multimodal inference.
 
 
 
 
 
 
 
 
80
 
81
- **New multimodal MRI choices**
82
  - **Multi-Region ROI:** hippocampus + entorhinal cortex + amygdala + ventricles
83
  - **Hippocampal ROI:** focused hippocampal representation
84
 
@@ -86,31 +97,43 @@ segmentation and ROI extraction before multimodal inference.
86
  """
87
 
88
 
89
- # ---------------------------------------------------------------------
90
- # Shared display helpers
91
- # ---------------------------------------------------------------------
 
92
  def _to_float(value):
93
  if value is None:
94
  return np.nan
 
95
  if isinstance(value, str):
96
  s = value.strip()
 
97
  if not s:
98
  return np.nan
 
99
  low = s.lower()
 
100
  if low in {"m", "male"}:
101
  return 1.0
 
102
  if low in {"f", "female"}:
103
  return 0.0
 
104
  if low in {"yes", "true", "positive"}:
105
  return 1.0
 
106
  if low in {"no", "false", "negative"}:
107
  return 0.0
 
108
  try:
109
  return float(s)
 
110
  except ValueError:
111
  return np.nan
 
112
  try:
113
  return float(value)
 
114
  except Exception:
115
  return np.nan
116
 
@@ -118,109 +141,203 @@ def _to_float(value):
118
  def _core_table_to_dict(core_df):
119
  if core_df is None:
120
  return {}
 
121
  if not isinstance(core_df, pd.DataFrame):
122
- core_df = pd.DataFrame(core_df, columns=["Feature", "Value"])
123
- out = {}
 
 
 
 
 
124
  for _, row in core_df.iterrows():
125
  key = str(row.iloc[0]).strip()
 
126
  if key:
127
- out[key] = _to_float(row.iloc[1])
128
- return out
 
129
 
130
 
131
  def _read_extra_features(extra_csv):
132
  if extra_csv is None:
133
  return {}
 
134
  df = pd.read_csv(Path(extra_csv))
 
135
  if df.empty:
136
  return {}
137
 
138
- lower_cols = {str(c).strip().lower(): c for c in df.columns}
 
 
 
 
139
  if "feature" in lower_cols and "value" in lower_cols:
140
- fk = lower_cols["feature"]
141
- vk = lower_cols["value"]
 
142
  return {
143
- str(r[fk]).strip(): _to_float(r[vk])
144
- for _, r in df.iterrows()
145
- if str(r[fk]).strip()
146
  }
147
 
148
  row = df.iloc[0]
149
- return {str(c): _to_float(row[c]) for c in df.columns}
150
-
151
-
152
- def _make_prob_chart(probs):
153
- vals = [float(probs.get(c, 0.0)) * 100.0 for c in DISPLAY_CLASSES]
154
- fig = go.Figure(go.Bar(
155
- x=vals,
156
- y=DISPLAY_CLASSES,
157
- orientation="h",
158
- marker_color=[CLASS_COLORS[c] for c in DISPLAY_CLASSES],
159
- text=[f"{v:.1f}%" for v in vals],
160
- textposition="outside",
161
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  fig.update_layout(
163
  xaxis_title="Probability (%)",
164
  xaxis_range=[0, 105],
165
- height=270,
166
- margin=dict(l=15, r=20, t=15, b=25),
 
 
 
 
 
167
  plot_bgcolor="rgba(0,0,0,0)",
168
  paper_bgcolor="rgba(0,0,0,0)",
169
  )
 
170
  return fig
171
 
172
 
173
- def _stage_html(stage, confidence, model_name):
174
- color = CLASS_COLORS.get(stage, "#64748b")
 
 
 
 
 
 
 
 
 
175
  return f"""
176
- <div style="border:1px solid {color};border-radius:14px;padding:16px 18px;
177
- background:{color}12;text-align:center;">
178
- <div style="font-size:.78rem;opacity:.7;">Predicted Stage</div>
179
- <div style="font-size:1.55rem;font-weight:750;color:{color};margin-top:3px;">{stage}</div>
180
- <div style="font-size:.95rem;margin-top:4px;">{confidence*100:.1f}% confidence</div>
181
- <div style="font-size:.74rem;opacity:.62;margin-top:6px;">{model_name}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  </div>
183
  """
184
 
185
 
186
  def _softmax_or_normalize(raw):
187
- arr = np.asarray(raw, dtype=np.float64).reshape(-1)
188
- if len(arr) != 4:
189
- raise ValueError(f"Expected four outputs, got {np.asarray(raw).shape}.")
190
- if np.all(arr >= 0) and 0.98 <= arr.sum() <= 1.02:
191
- return arr / arr.sum()
192
- e = np.exp(arr - arr.max())
193
- return e / e.sum()
194
 
 
 
 
 
195
 
 
 
 
 
 
196
 
197
- def _is_nifti(path):
198
- name = str(path).lower()
199
- return name.endswith(".nii") or name.endswith(".nii.gz")
200
 
 
201
 
202
 
203
- # ---------------------------------------------------------------------
204
- # Task / MRI-strategy routing
205
- # ---------------------------------------------------------------------
206
- def _resolve_model_key(task_choice, mri_strategy):
207
- """
208
- Resolve the selected prediction task and MRI representation.
209
 
210
- Current Stage uses the existing deployment keys:
211
- - multi_region
212
- - hippocampal
 
213
 
214
- Progression and Development expect corresponding entries in
215
- model_config.json. This keeps the UI honest: the option is visible, but the
216
- app will not silently reuse the staging model for a different task.
217
- """
218
- strategy_key = (
219
- "multi_region"
220
- if mri_strategy.startswith("Multi-Region")
221
- else "hippocampal"
222
- )
223
 
 
 
224
 
225
  if task_choice == "Progression":
226
  return f"progression_{strategy_key}"
@@ -228,7 +345,9 @@ def _resolve_model_key(task_choice, mri_strategy):
228
  if task_choice == "Development":
229
  return f"development_{strategy_key}"
230
 
231
- raise gr.Error(f"Unknown prediction task: {task_choice}")
 
 
232
 
233
 
234
  def _task_output_description(task_choice):
@@ -247,304 +366,730 @@ def _task_output_description(task_choice):
247
  return ""
248
 
249
 
250
- # ---------------------------------------------------------------------
251
- # Multimodal inference
252
- # ---------------------------------------------------------------------
 
253
  def _required_files_message(model_key):
254
  spec = MODEL_SPECS[model_key]
 
255
  missing = []
256
- for filename in (spec["roi_checkpoint"], spec["fusion_model"]):
257
- if not (MODELS_DIR / filename).exists():
 
 
 
 
 
 
 
 
258
  missing.append(filename)
 
259
  if not missing:
260
  return ""
261
- return "Missing model file(s): " + ", ".join(f"models/{x}" for x in missing)
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
- def _build_feature_frame(booster, clinical_values, embedding, spec):
265
  feature_names = booster.feature_name()
266
- values = {name: np.nan for name in feature_names}
267
 
268
- for name, val in clinical_values.items():
 
 
 
 
 
269
  if name in values:
270
- values[name] = val
 
 
 
 
 
271
 
272
- configured = spec.get("embedding_feature_names", [])
273
- if configured:
274
- if len(configured) != len(embedding):
275
  raise ValueError(
276
- f"Configured embedding list has {len(configured)} features, "
277
- f"but the encoder returned {len(embedding)} values."
 
278
  )
279
- for name, val in zip(configured, embedding):
 
 
 
 
 
280
  if name in values:
281
- values[name] = float(val)
 
282
  else:
283
- prefixes = ("img_", "mri_", "emb_", "embedding_", "roi_")
284
- candidates = [n for n in feature_names if n.lower().startswith(prefixes)]
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  if len(candidates) != len(embedding):
286
  raise ValueError(
287
- "Could not map the MRI embedding into the LightGBM model. "
288
- "Fill embedding_feature_names in model_config.json with the exact "
289
- "training feature names."
 
290
  )
291
- for name, val in zip(candidates, embedding):
292
- values[name] = float(val)
293
 
294
- return pd.DataFrame([[values[n] for n in feature_names]], columns=feature_names)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
 
 
 
 
 
 
 
 
296
 
297
- def _run_multimodal(task_choice, mri_strategy, mri_path, core_df, extra_csv):
298
  if not _is_nifti(mri_path):
299
  raise gr.Error(
300
- "The hippocampal and multi-region models require a whole-volume "
301
- "T1 MRI in .nii or .nii.gz format."
302
  )
303
 
304
- model_key = _resolve_model_key(task_choice, mri_strategy)
 
 
 
305
 
306
  if model_key not in MODEL_SPECS:
307
  raise gr.Error(
308
- f"No deployment model is configured yet for '{task_choice}' with "
309
- f"'{mri_strategy}'. Add a '{model_key}' entry to model_config.json "
310
- "using the exact trained checkpoint and fusion model for that task. "
311
- "The app will not substitute the staging model."
312
  )
313
 
314
  spec = MODEL_SPECS[model_key]
315
 
316
- missing = _required_files_message(model_key)
 
 
 
317
  if missing:
318
  raise gr.Error(missing)
319
 
 
 
 
 
320
  roi_arrays, roi_qc = extract_rois_from_t1(
321
  mri_path,
322
- crop_shape=tuple(spec["crop_shape"]),
 
 
323
  device="cuda",
324
  )
325
 
326
- selected = {
327
- region: normalize_roi(
 
 
 
328
  roi_arrays[region],
329
- expected_shape=tuple(spec["crop_shape"]),
 
 
330
  )
331
- for region in spec["regions"]
332
- }
333
 
334
- encoder = load_roi_model(MODELS_DIR / spec["roi_checkpoint"])
335
- embedding = extract_embedding(encoder, selected, spec)
336
- booster = load_fusion_model(MODELS_DIR / spec["fusion_model"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
- clinical = _core_table_to_dict(core_df)
339
- clinical.update(_read_extra_features(extra_csv))
 
340
 
341
- X = _build_feature_frame(booster, clinical, embedding, spec)
342
- raw = booster.predict(X)
343
- raw_arr = np.asarray(raw)
344
- if raw_arr.ndim == 2:
345
- raw_arr = raw_arr[0]
346
- probs = _softmax_or_normalize(raw_arr)
347
 
348
- top_i = int(np.argmax(probs))
349
- stage = DISPLAY_CLASSES[top_i]
350
- confidence = float(probs[top_i])
351
- prob_map = dict(zip(DISPLAY_CLASSES, probs.tolist()))
 
352
 
353
- regions = ", ".join(spec["regions"])
354
- filled = int(np.sum(~pd.isna(X.iloc[0])))
355
- total = len(X.columns)
356
 
357
- qc = "; ".join(
358
- f"{r}: {roi_qc[r]['label_voxels']} voxels"
359
- for r in spec["regions"]
 
 
 
360
  )
361
 
362
  status = (
363
  f"**Prediction task:** {task_choice} \n"
364
  f"**Task meaning:** {_task_output_description(task_choice)} \n"
 
365
  f"**Input:** whole-volume T1 MRI \n"
366
- f"**Automatic processing:** FastSurfer → 64×64×64 ROI extraction \n"
367
  f"**Regions used:** {regions} \n"
368
- f"**Fusion features populated:** {filled}/{total} \n"
369
- f"**ROI QC:** {qc}"
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  )
371
- return stage, confidence, prob_map, status, X
372
 
373
 
374
- # ---------------------------------------------------------------------
375
- # Main ZeroGPU function
376
- # ---------------------------------------------------------------------
 
377
  @spaces.GPU(duration=300)
378
- def run_inference(task_choice, mri_strategy, mri_file, core_df, extra_csv):
 
 
 
 
 
 
 
379
  if mri_file is None:
380
- raise gr.Error("Please upload an MRI.")
 
 
381
 
382
  try:
383
- stage, confidence, probs, status, X = _run_multimodal(
384
- task_choice, mri_strategy, mri_file, core_df, extra_csv
 
 
 
 
 
 
 
 
 
 
 
385
  )
386
 
387
  return (
388
- _stage_html(stage, confidence, f"{task_choice} · {mri_strategy}"),
389
- _make_prob_chart(probs),
 
 
 
 
 
 
 
390
  status,
391
- X,
392
  )
 
393
  except gr.Error:
394
  raise
395
- except Exception as exc:
 
 
396
  traceback.print_exc()
397
- raise gr.Error(f"Inference failed: {exc}")
398
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
- def selection_help(task_choice, mri_strategy):
401
- if mri_strategy.startswith("Hippocampal"):
402
- region_text = "**Automatic ROI:** hippocampus"
403
  else:
 
404
  region_text = (
405
- "**Automatic ROIs:** hippocampus + entorhinal cortex + "
406
- "amygdala + ventricles"
407
  )
408
 
409
  if task_choice == "Progression":
410
- task_text = "**Task:** predict future disease progression"
 
 
 
 
411
  else:
 
412
  task_text = (
413
- "**Task:** predict future development of Alzheimer’s-related impairment"
 
414
  )
415
 
416
  return (
417
  f"{task_text} \n"
418
  "**Input:** whole-volume T1 MRI (`.nii` / `.nii.gz`) \n"
419
  f"{region_text} \n"
420
- "**Clinical/cognitive data:** used in fusion"
421
  )
422
 
423
 
 
 
 
 
424
  CSS = """
425
  .gradio-container {
426
- max-width: 1260px !important;
 
 
427
  }
 
428
  .hero-card {
429
  background: white;
430
  border: 1px solid #e5e7eb;
431
  border-radius: 14px;
432
- padding: 22px 26px;
433
- margin-bottom: 8px;
 
 
 
 
 
 
 
434
  }
435
- .input-card, .performance-card {
 
436
  border: 1px solid #e5e7eb;
437
  border-radius: 12px;
438
- padding: 12px;
 
439
  }
 
440
  #result-card {
441
- min-height: 120px;
 
 
 
 
442
  }
443
- footer {visibility: hidden;}
444
  """
445
 
446
 
447
- with gr.Blocks(theme=gr.themes.Soft(), css=CSS, title="AD-Stage-Net Multimodal") as demo:
448
- # Same top hero-card structure as the original app.
 
 
 
 
 
 
 
 
 
 
 
 
449
  with gr.Row():
 
450
  with gr.Column():
451
- gr.Markdown(PROJECT_MD, elem_classes=["hero-card"])
452
 
453
- # Same basic 3:2 main-page layout as the original app.
454
- with gr.Row():
455
- with gr.Column(scale=3):
456
- with gr.Row():
457
- with gr.Column():
458
- task_choice = gr.Dropdown(
459
- choices=TASK_OPTIONS,
460
- value="Progression",
461
- label="Prediction Task",
462
- allow_custom_value=False,
463
- )
464
 
465
- mri_strategy = gr.Dropdown(
466
- choices=MRI_STRATEGY_OPTIONS,
467
- value="Multi-Region ROI",
468
- label="MRI Strategy",
469
- allow_custom_value=False,
470
- )
471
 
472
- model_note = gr.Markdown(
473
- selection_help("Progression", "Multi-Region ROI")
474
- )
475
 
476
- extra_csv = gr.File(
477
- label="Optional cognitive-feature CSV",
478
- file_types=[".csv"],
479
- type="filepath",
480
- )
481
 
482
- with gr.Column():
483
- mri_file = gr.File(
484
- label="Upload whole-volume T1 MRI",
485
- file_types=[".nii", ".gz"],
486
- type="filepath",
487
- )
488
- gr.Markdown(
489
- "**Supported:** NIfTI whole-volume MRI (`.nii`, `.nii.gz`)."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  )
491
- clear_btn = gr.Button("Clear file")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
- gr.Markdown("### Clinical / Cognitive Information")
494
  gr.Markdown(
495
- "Used by the hippocampal and multi-region multimodal models. "
496
- "Leave unavailable measurements blank."
 
 
 
 
 
497
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  core_df = gr.Dataframe(
499
- headers=["Feature", "Value"],
500
- datatype=["str", "str"],
 
 
 
 
 
 
501
  value=DEFAULT_CORE_ROWS,
502
- row_count=(len(DEFAULT_CORE_ROWS), "fixed"),
503
- col_count=(2, "fixed"),
 
 
 
 
 
 
504
  interactive=True,
505
  label="Clinical / cognitive features",
 
 
 
 
 
 
 
 
 
506
  )
507
 
508
- run_btn = gr.Button("Run inference", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
 
510
- gr.Markdown("### Prediction")
511
- with gr.Row():
512
- stage_output = gr.HTML(elem_id="result-card")
513
- chart_output = gr.Plot()
514
- status_output = gr.Markdown()
515
 
516
- with gr.Accordion("Show exact multimodal fusion input", open=False):
517
- feature_frame = gr.Dataframe(interactive=False)
518
 
519
- with gr.Column(scale=2):
520
- gr.Markdown("""
521
- ### How the new pipeline works
522
 
523
- **Whole-volume T1 MRI**
524
- → automatic FastSurfer segmentation
525
- → automatic anatomical ROI extraction
526
- → MRI embedding
527
- → clinical/cognitive fusion
528
- → prediction
529
 
530
- The **multi-region** representation uses the hippocampus, entorhinal cortex,
531
- amygdala, and ventricles.
532
 
533
- The **hippocampal** representation uses the hippocampal ROI only.
534
 
535
- Clinical and cognitive information is combined with the MRI representation
536
- for the selected prediction task.
537
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
 
539
  task_choice.change(
540
  fn=selection_help,
541
- inputs=[task_choice, mri_strategy],
 
 
 
542
  outputs=model_note,
543
  )
544
 
545
  mri_strategy.change(
546
  fn=selection_help,
547
- inputs=[task_choice, mri_strategy],
 
 
 
548
  outputs=model_note,
549
  )
550
 
@@ -556,10 +1101,25 @@ for the selected prediction task.
556
 
557
  run_btn.click(
558
  fn=run_inference,
559
- inputs=[task_choice, mri_strategy, mri_file, core_df, extra_csv],
560
- outputs=[stage_output, chart_output, status_output, feature_frame],
 
 
 
 
 
 
 
 
 
 
 
561
  )
562
 
563
 
 
 
 
 
564
  if __name__ == "__main__":
565
- demo.launch()
 
1
  from pathlib import Path
 
 
2
  import traceback
3
 
4
  import gradio as gr
 
7
  import pandas as pd
8
  import plotly.graph_objects as go
9
 
 
10
  from model_adapter import (
11
  MODEL_SPECS,
12
  load_roi_model,
 
16
  )
17
  from roi_extractor import extract_rois_from_t1
18
 
19
+
20
  APP_DIR = Path(__file__).resolve().parent
21
  MODELS_DIR = APP_DIR / "models"
22
 
23
+
24
  DISPLAY_CLASSES = [
25
  "No Impairment",
26
  "Very Mild Impairment",
 
36
  "Moderate Impairment": "#ef4444",
37
  }
38
 
39
+
40
  TASK_OPTIONS = [
41
  "Progression",
42
  "Development",
43
  ]
44
 
45
+
46
  MRI_STRATEGY_OPTIONS = [
47
  "Multi-Region ROI",
48
  "Hippocampal ROI",
49
  ]
50
 
51
+
52
  CORE_FEATURES = [
53
  "age_at_visit",
54
  "sex",
 
67
  "entorhinal_thickness_l",
68
  "entorhinal_thickness_r",
69
  ]
70
+
71
+
72
  DEFAULT_CORE_ROWS = [[name, ""] for name in CORE_FEATURES]
73
 
74
 
75
  PROJECT_MD = r"""
76
+ # AD-Stage-Net: Multimodal Alzheimer’s Disease Prediction
77
 
78
+ **AD-Stage-Net Multimodal** combines structural MRI with clinical and cognitive
79
+ information to evaluate future Alzheimer’s disease outcomes.
80
 
81
+ **Original Staging Model:** <https://huggingface.co/spaces/katelynhur/AD-Stage-Net>
82
+
83
+ Upload a **whole-volume T1 MRI** (`.nii` / `.nii.gz`). The application automatically
84
+ performs anatomical segmentation and ROI extraction before multimodal inference.
85
+
86
+ ### Prediction Tasks
87
+
88
+ - **Progression** — predicts future disease progression
89
+ - **Development** — predicts future development of Alzheimer’s-related impairment
90
+
91
+ ### MRI Strategies
92
 
 
93
  - **Multi-Region ROI:** hippocampus + entorhinal cortex + amygdala + ventricles
94
  - **Hippocampal ROI:** focused hippocampal representation
95
 
 
97
  """
98
 
99
 
100
+ # ============================================================
101
+ # Basic helpers
102
+ # ============================================================
103
+
104
  def _to_float(value):
105
  if value is None:
106
  return np.nan
107
+
108
  if isinstance(value, str):
109
  s = value.strip()
110
+
111
  if not s:
112
  return np.nan
113
+
114
  low = s.lower()
115
+
116
  if low in {"m", "male"}:
117
  return 1.0
118
+
119
  if low in {"f", "female"}:
120
  return 0.0
121
+
122
  if low in {"yes", "true", "positive"}:
123
  return 1.0
124
+
125
  if low in {"no", "false", "negative"}:
126
  return 0.0
127
+
128
  try:
129
  return float(s)
130
+
131
  except ValueError:
132
  return np.nan
133
+
134
  try:
135
  return float(value)
136
+
137
  except Exception:
138
  return np.nan
139
 
 
141
  def _core_table_to_dict(core_df):
142
  if core_df is None:
143
  return {}
144
+
145
  if not isinstance(core_df, pd.DataFrame):
146
+ core_df = pd.DataFrame(
147
+ core_df,
148
+ columns=["Feature", "Value"],
149
+ )
150
+
151
+ output = {}
152
+
153
  for _, row in core_df.iterrows():
154
  key = str(row.iloc[0]).strip()
155
+
156
  if key:
157
+ output[key] = _to_float(row.iloc[1])
158
+
159
+ return output
160
 
161
 
162
  def _read_extra_features(extra_csv):
163
  if extra_csv is None:
164
  return {}
165
+
166
  df = pd.read_csv(Path(extra_csv))
167
+
168
  if df.empty:
169
  return {}
170
 
171
+ lower_cols = {
172
+ str(c).strip().lower(): c
173
+ for c in df.columns
174
+ }
175
+
176
  if "feature" in lower_cols and "value" in lower_cols:
177
+ feature_col = lower_cols["feature"]
178
+ value_col = lower_cols["value"]
179
+
180
  return {
181
+ str(row[feature_col]).strip(): _to_float(row[value_col])
182
+ for _, row in df.iterrows()
183
+ if str(row[feature_col]).strip()
184
  }
185
 
186
  row = df.iloc[0]
187
+
188
+ return {
189
+ str(column): _to_float(row[column])
190
+ for column in df.columns
191
+ }
192
+
193
+
194
+ def _is_nifti(path):
195
+ name = str(path).lower()
196
+
197
+ return (
198
+ name.endswith(".nii")
199
+ or name.endswith(".nii.gz")
200
+ )
201
+
202
+
203
+ # ============================================================
204
+ # Result display
205
+ # ============================================================
206
+
207
+ def _make_prob_chart(probabilities):
208
+ values = [
209
+ float(probabilities.get(stage, 0.0)) * 100
210
+ for stage in DISPLAY_CLASSES
211
+ ]
212
+
213
+ fig = go.Figure(
214
+ go.Bar(
215
+ x=values,
216
+ y=DISPLAY_CLASSES,
217
+ orientation="h",
218
+ marker_color=[
219
+ CLASS_COLORS[stage]
220
+ for stage in DISPLAY_CLASSES
221
+ ],
222
+ text=[
223
+ f"{value:.1f}%"
224
+ for value in values
225
+ ],
226
+ textposition="outside",
227
+ )
228
+ )
229
+
230
  fig.update_layout(
231
  xaxis_title="Probability (%)",
232
  xaxis_range=[0, 105],
233
+ height=280,
234
+ margin=dict(
235
+ l=20,
236
+ r=30,
237
+ t=20,
238
+ b=30,
239
+ ),
240
  plot_bgcolor="rgba(0,0,0,0)",
241
  paper_bgcolor="rgba(0,0,0,0)",
242
  )
243
+
244
  return fig
245
 
246
 
247
+ def _stage_html(
248
+ stage,
249
+ confidence,
250
+ task_name,
251
+ strategy_name,
252
+ ):
253
+ color = CLASS_COLORS.get(
254
+ stage,
255
+ "#64748b",
256
+ )
257
+
258
  return f"""
259
+ <div style="
260
+ border:1px solid {color};
261
+ border-radius:14px;
262
+ padding:18px 20px;
263
+ background:{color}12;
264
+ text-align:center;
265
+ min-height:120px;
266
+ ">
267
+
268
+ <div style="
269
+ font-size:.8rem;
270
+ opacity:.7;
271
+ ">
272
+ Predicted Result
273
+ </div>
274
+
275
+ <div style="
276
+ font-size:1.6rem;
277
+ font-weight:750;
278
+ color:{color};
279
+ margin-top:4px;
280
+ ">
281
+ {stage}
282
+ </div>
283
+
284
+ <div style="
285
+ font-size:1rem;
286
+ margin-top:5px;
287
+ ">
288
+ {confidence * 100:.1f}% confidence
289
+ </div>
290
+
291
+ <div style="
292
+ font-size:.78rem;
293
+ opacity:.65;
294
+ margin-top:7px;
295
+ ">
296
+ {task_name} · {strategy_name}
297
+ </div>
298
+
299
  </div>
300
  """
301
 
302
 
303
  def _softmax_or_normalize(raw):
304
+ arr = np.asarray(
305
+ raw,
306
+ dtype=np.float64,
307
+ ).reshape(-1)
 
 
 
308
 
309
+ if len(arr) != 4:
310
+ raise ValueError(
311
+ f"Expected four outputs, got {np.asarray(raw).shape}."
312
+ )
313
 
314
+ if (
315
+ np.all(arr >= 0)
316
+ and 0.98 <= arr.sum() <= 1.02
317
+ ):
318
+ return arr / arr.sum()
319
 
320
+ exp = np.exp(
321
+ arr - arr.max()
322
+ )
323
 
324
+ return exp / exp.sum()
325
 
326
 
327
+ # ============================================================
328
+ # Task + MRI strategy routing
329
+ # ============================================================
 
 
 
330
 
331
+ def _resolve_model_key(
332
+ task_choice,
333
+ mri_strategy,
334
+ ):
335
 
336
+ if mri_strategy == "Multi-Region ROI":
337
+ strategy_key = "multi_region"
 
 
 
 
 
 
 
338
 
339
+ else:
340
+ strategy_key = "hippocampal"
341
 
342
  if task_choice == "Progression":
343
  return f"progression_{strategy_key}"
 
345
  if task_choice == "Development":
346
  return f"development_{strategy_key}"
347
 
348
+ raise gr.Error(
349
+ f"Unknown prediction task: {task_choice}"
350
+ )
351
 
352
 
353
  def _task_output_description(task_choice):
 
366
  return ""
367
 
368
 
369
+ # ============================================================
370
+ # Model helpers
371
+ # ============================================================
372
+
373
  def _required_files_message(model_key):
374
  spec = MODEL_SPECS[model_key]
375
+
376
  missing = []
377
+
378
+ for filename in (
379
+ spec["roi_checkpoint"],
380
+ spec["fusion_model"],
381
+ ):
382
+
383
+ if not (
384
+ MODELS_DIR / filename
385
+ ).exists():
386
+
387
  missing.append(filename)
388
+
389
  if not missing:
390
  return ""
 
391
 
392
+ return (
393
+ "Missing model file(s): "
394
+ + ", ".join(
395
+ f"models/{filename}"
396
+ for filename in missing
397
+ )
398
+ )
399
+
400
+
401
+ def _build_feature_frame(
402
+ booster,
403
+ clinical_values,
404
+ embedding,
405
+ spec,
406
+ ):
407
 
 
408
  feature_names = booster.feature_name()
 
409
 
410
+ values = {
411
+ name: np.nan
412
+ for name in feature_names
413
+ }
414
+
415
+ for name, value in clinical_values.items():
416
  if name in values:
417
+ values[name] = value
418
+
419
+ configured_names = spec.get(
420
+ "embedding_feature_names",
421
+ [],
422
+ )
423
 
424
+ if configured_names:
425
+
426
+ if len(configured_names) != len(embedding):
427
  raise ValueError(
428
+ "Configured MRI embedding contains "
429
+ f"{len(configured_names)} names, but encoder returned "
430
+ f"{len(embedding)} values."
431
  )
432
+
433
+ for name, value in zip(
434
+ configured_names,
435
+ embedding,
436
+ ):
437
+
438
  if name in values:
439
+ values[name] = float(value)
440
+
441
  else:
442
+
443
+ prefixes = (
444
+ "img_",
445
+ "mri_",
446
+ "emb_",
447
+ "embedding_",
448
+ "roi_",
449
+ )
450
+
451
+ candidates = [
452
+ name
453
+ for name in feature_names
454
+ if name.lower().startswith(prefixes)
455
+ ]
456
+
457
  if len(candidates) != len(embedding):
458
  raise ValueError(
459
+ "Could not automatically map the MRI embedding "
460
+ "into the LightGBM model. Add the exact MRI "
461
+ "embedding feature names to embedding_feature_names "
462
+ "in model_config.json."
463
  )
 
 
464
 
465
+ for name, value in zip(
466
+ candidates,
467
+ embedding,
468
+ ):
469
+ values[name] = float(value)
470
+
471
+ return pd.DataFrame(
472
+ [
473
+ [
474
+ values[name]
475
+ for name in feature_names
476
+ ]
477
+ ],
478
+ columns=feature_names,
479
+ )
480
+
481
+
482
+ # ============================================================
483
+ # Multimodal inference
484
+ # ============================================================
485
 
486
+ def _run_multimodal(
487
+ task_choice,
488
+ mri_strategy,
489
+ mri_path,
490
+ core_df,
491
+ extra_csv,
492
+ ):
493
 
 
494
  if not _is_nifti(mri_path):
495
  raise gr.Error(
496
+ "Please upload a whole-volume T1 MRI "
497
+ "in .nii or .nii.gz format."
498
  )
499
 
500
+ model_key = _resolve_model_key(
501
+ task_choice,
502
+ mri_strategy,
503
+ )
504
 
505
  if model_key not in MODEL_SPECS:
506
  raise gr.Error(
507
+ f"No deployment model is configured for "
508
+ f"'{task_choice}' using '{mri_strategy}'. "
509
+ f"Add '{model_key}' to model_config.json "
510
+ "using the exact trained model files."
511
  )
512
 
513
  spec = MODEL_SPECS[model_key]
514
 
515
+ missing = _required_files_message(
516
+ model_key
517
+ )
518
+
519
  if missing:
520
  raise gr.Error(missing)
521
 
522
+ # --------------------------------------------------------
523
+ # Automatic segmentation + ROI extraction
524
+ # --------------------------------------------------------
525
+
526
  roi_arrays, roi_qc = extract_rois_from_t1(
527
  mri_path,
528
+ crop_shape=tuple(
529
+ spec["crop_shape"]
530
+ ),
531
  device="cuda",
532
  )
533
 
534
+ selected_rois = {}
535
+
536
+ for region in spec["regions"]:
537
+
538
+ selected_rois[region] = normalize_roi(
539
  roi_arrays[region],
540
+ expected_shape=tuple(
541
+ spec["crop_shape"]
542
+ ),
543
  )
 
 
544
 
545
+ # --------------------------------------------------------
546
+ # MRI encoder
547
+ # --------------------------------------------------------
548
+
549
+ encoder = load_roi_model(
550
+ MODELS_DIR
551
+ / spec["roi_checkpoint"]
552
+ )
553
+
554
+ embedding = extract_embedding(
555
+ encoder,
556
+ selected_rois,
557
+ spec,
558
+ )
559
+
560
+ # --------------------------------------------------------
561
+ # Clinical / cognitive fusion
562
+ # --------------------------------------------------------
563
+
564
+ booster = load_fusion_model(
565
+ MODELS_DIR
566
+ / spec["fusion_model"]
567
+ )
568
+
569
+ clinical_values = _core_table_to_dict(
570
+ core_df
571
+ )
572
+
573
+ clinical_values.update(
574
+ _read_extra_features(
575
+ extra_csv
576
+ )
577
+ )
578
+
579
+ X = _build_feature_frame(
580
+ booster,
581
+ clinical_values,
582
+ embedding,
583
+ spec,
584
+ )
585
+
586
+ raw_prediction = booster.predict(X)
587
+
588
+ raw_array = np.asarray(
589
+ raw_prediction
590
+ )
591
+
592
+ if raw_array.ndim == 2:
593
+ raw_array = raw_array[0]
594
+
595
+ probabilities = _softmax_or_normalize(
596
+ raw_array
597
+ )
598
+
599
+ probability_map = dict(
600
+ zip(
601
+ DISPLAY_CLASSES,
602
+ probabilities.tolist(),
603
+ )
604
+ )
605
+
606
+ top_index = int(
607
+ np.argmax(probabilities)
608
+ )
609
+
610
+ stage = DISPLAY_CLASSES[
611
+ top_index
612
+ ]
613
 
614
+ confidence = float(
615
+ probabilities[top_index]
616
+ )
617
 
618
+ regions = ", ".join(
619
+ spec["regions"]
620
+ )
 
 
 
621
 
622
+ filled_features = int(
623
+ np.sum(
624
+ ~pd.isna(X.iloc[0])
625
+ )
626
+ )
627
 
628
+ total_features = len(
629
+ X.columns
630
+ )
631
 
632
+ qc_text = "; ".join(
633
+ (
634
+ f"{region}: "
635
+ f"{roi_qc[region]['label_voxels']} voxels"
636
+ )
637
+ for region in spec["regions"]
638
  )
639
 
640
  status = (
641
  f"**Prediction task:** {task_choice} \n"
642
  f"**Task meaning:** {_task_output_description(task_choice)} \n"
643
+ f"**MRI strategy:** {mri_strategy} \n"
644
  f"**Input:** whole-volume T1 MRI \n"
645
+ f"**Automatic processing:** FastSurfer → ROI extraction \n"
646
  f"**Regions used:** {regions} \n"
647
+ f"**ROI size:** "
648
+ f"{spec['crop_shape'][0]}×"
649
+ f"{spec['crop_shape'][1]}×"
650
+ f"{spec['crop_shape'][2]} \n"
651
+ f"**Fusion features populated:** "
652
+ f"{filled_features}/{total_features} \n"
653
+ f"**ROI QC:** {qc_text}"
654
+ )
655
+
656
+ return (
657
+ stage,
658
+ confidence,
659
+ probability_map,
660
+ status,
661
+ X,
662
  )
 
663
 
664
 
665
+ # ============================================================
666
+ # ZeroGPU wrapper
667
+ # ============================================================
668
+
669
  @spaces.GPU(duration=300)
670
+ def run_inference(
671
+ task_choice,
672
+ mri_strategy,
673
+ mri_file,
674
+ core_df,
675
+ extra_csv,
676
+ ):
677
+
678
  if mri_file is None:
679
+ raise gr.Error(
680
+ "Please upload a whole-volume T1 MRI."
681
+ )
682
 
683
  try:
684
+
685
+ (
686
+ stage,
687
+ confidence,
688
+ probabilities,
689
+ status,
690
+ feature_frame,
691
+ ) = _run_multimodal(
692
+ task_choice,
693
+ mri_strategy,
694
+ mri_file,
695
+ core_df,
696
+ extra_csv,
697
  )
698
 
699
  return (
700
+ _stage_html(
701
+ stage,
702
+ confidence,
703
+ task_choice,
704
+ mri_strategy,
705
+ ),
706
+ _make_prob_chart(
707
+ probabilities
708
+ ),
709
  status,
710
+ feature_frame,
711
  )
712
+
713
  except gr.Error:
714
  raise
715
+
716
+ except Exception as error:
717
+
718
  traceback.print_exc()
 
719
 
720
+ raise gr.Error(
721
+ f"Inference failed: {error}"
722
+ )
723
+
724
+
725
+ # ============================================================
726
+ # Dynamic UI helper
727
+ # ============================================================
728
+
729
+ def selection_help(
730
+ task_choice,
731
+ mri_strategy,
732
+ ):
733
+
734
+ if mri_strategy == "Hippocampal ROI":
735
+
736
+ region_text = (
737
+ "**Automatic ROI:** hippocampus"
738
+ )
739
 
 
 
 
740
  else:
741
+
742
  region_text = (
743
+ "**Automatic ROIs:** hippocampus + "
744
+ "entorhinal cortex + amygdala + ventricles"
745
  )
746
 
747
  if task_choice == "Progression":
748
+
749
+ task_text = (
750
+ "**Task:** predict future disease progression"
751
+ )
752
+
753
  else:
754
+
755
  task_text = (
756
+ "**Task:** predict future development "
757
+ "of Alzheimer’s-related impairment"
758
  )
759
 
760
  return (
761
  f"{task_text} \n"
762
  "**Input:** whole-volume T1 MRI (`.nii` / `.nii.gz`) \n"
763
  f"{region_text} \n"
764
+ "**Clinical/cognitive data:** used in multimodal fusion"
765
  )
766
 
767
 
768
+ # ============================================================
769
+ # Styling
770
+ # ============================================================
771
+
772
  CSS = """
773
  .gradio-container {
774
+ max-width: 1600px !important;
775
+ width: 95vw !important;
776
+ margin: 0 auto !important;
777
  }
778
+
779
  .hero-card {
780
  background: white;
781
  border: 1px solid #e5e7eb;
782
  border-radius: 14px;
783
+ padding: 24px 28px;
784
+ margin-bottom: 12px;
785
+ }
786
+
787
+ .control-card {
788
+ border: 1px solid #e5e7eb;
789
+ border-radius: 12px;
790
+ padding: 18px;
791
+ background: white;
792
  }
793
+
794
+ .info-card {
795
  border: 1px solid #e5e7eb;
796
  border-radius: 12px;
797
+ padding: 18px 20px;
798
+ background: white;
799
  }
800
+
801
  #result-card {
802
+ min-height: 130px;
803
+ }
804
+
805
+ footer {
806
+ visibility: hidden;
807
  }
 
808
  """
809
 
810
 
811
+ # ============================================================
812
+ # App layout
813
+ # ============================================================
814
+
815
+ with gr.Blocks(
816
+ theme=gr.themes.Soft(),
817
+ css=CSS,
818
+ title="AD-Stage-Net Multimodal",
819
+ ) as demo:
820
+
821
+ # ========================================================
822
+ # Header
823
+ # ========================================================
824
+
825
  with gr.Row():
826
+
827
  with gr.Column():
 
828
 
829
+ gr.Markdown(
830
+ PROJECT_MD,
831
+ elem_classes=[
832
+ "hero-card"
833
+ ],
834
+ )
 
 
 
 
 
835
 
836
+ # ========================================================
837
+ # Main layout
838
+ # ========================================================
 
 
 
839
 
840
+ with gr.Row(
841
+ equal_height=False
842
+ ):
843
 
844
+ # ====================================================
845
+ # LEFT SIDE — inference controls
846
+ # ====================================================
 
 
847
 
848
+ with gr.Column(
849
+ scale=4,
850
+ min_width=650,
851
+ ):
852
+
853
+ # ------------------------------------------------
854
+ # Prediction controls
855
+ # ------------------------------------------------
856
+
857
+ gr.Markdown(
858
+ "## Run a Prediction"
859
+ )
860
+
861
+ with gr.Group(
862
+ elem_classes=[
863
+ "control-card"
864
+ ]
865
+ ):
866
+
867
+ task_choice = gr.Dropdown(
868
+ choices=TASK_OPTIONS,
869
+ value="Progression",
870
+ label="Prediction Task",
871
+ allow_custom_value=False,
872
+ )
873
+
874
+ mri_strategy = gr.Dropdown(
875
+ choices=MRI_STRATEGY_OPTIONS,
876
+ value="Multi-Region ROI",
877
+ label="MRI Strategy",
878
+ allow_custom_value=False,
879
+ )
880
+
881
+ model_note = gr.Markdown(
882
+ selection_help(
883
+ "Progression",
884
+ "Multi-Region ROI",
885
  )
886
+ )
887
+
888
+ # ------------------------------------------------
889
+ # MRI upload
890
+ # ------------------------------------------------
891
+
892
+ gr.Markdown(
893
+ "### 1. Upload MRI"
894
+ )
895
+
896
+ mri_file = gr.File(
897
+ label="Upload whole-volume T1 MRI",
898
+ file_types=[
899
+ ".nii",
900
+ ".gz",
901
+ ],
902
+ type="filepath",
903
+ height=180,
904
+ )
905
 
 
906
  gr.Markdown(
907
+ "**Supported:** NIfTI whole-volume MRI "
908
+ "(`.nii`, `.nii.gz`)."
909
+ )
910
+
911
+ clear_btn = gr.Button(
912
+ "Clear MRI",
913
+ variant="secondary",
914
  )
915
+
916
+ # ------------------------------------------------
917
+ # Clinical / cognitive information
918
+ # ------------------------------------------------
919
+
920
+ gr.Markdown(
921
+ "### 2. Clinical / Cognitive Information"
922
+ )
923
+
924
+ gr.Markdown(
925
+ "Enter the measurements available for the participant. "
926
+ "Leave unavailable values blank."
927
+ )
928
+
929
  core_df = gr.Dataframe(
930
+ headers=[
931
+ "Feature",
932
+ "Value",
933
+ ],
934
+ datatype=[
935
+ "str",
936
+ "str",
937
+ ],
938
  value=DEFAULT_CORE_ROWS,
939
+ row_count=(
940
+ len(DEFAULT_CORE_ROWS),
941
+ "fixed",
942
+ ),
943
+ col_count=(
944
+ 2,
945
+ "fixed",
946
+ ),
947
  interactive=True,
948
  label="Clinical / cognitive features",
949
+ height=420,
950
+ )
951
+
952
+ extra_csv = gr.File(
953
+ label="Optional complete cognitive-feature CSV",
954
+ file_types=[
955
+ ".csv"
956
+ ],
957
+ type="filepath",
958
  )
959
 
960
+ # ------------------------------------------------
961
+ # Run
962
+ # ------------------------------------------------
963
+
964
+ gr.Markdown(
965
+ "### 3. Run Model"
966
+ )
967
+
968
+ run_btn = gr.Button(
969
+ "Run inference",
970
+ variant="primary",
971
+ size="lg",
972
+ )
973
+
974
+ # ====================================================
975
+ # RIGHT SIDE — information
976
+ # ====================================================
977
+
978
+ with gr.Column(
979
+ scale=2,
980
+ min_width=350,
981
+ ):
982
+
983
+ with gr.Group(
984
+ elem_classes=[
985
+ "info-card"
986
+ ]
987
+ ):
988
 
989
+ gr.Markdown(
990
+ """
991
+ ## How the pipeline works
 
 
992
 
993
+ **Whole-volume T1 MRI**
 
994
 
995
+ ↓
 
 
996
 
997
+ **FastSurfer segmentation**
 
 
 
 
 
998
 
999
+ ↓
 
1000
 
1001
+ **Automatic ROI extraction**
1002
 
1003
+ ↓
1004
+
1005
+ **MRI embedding**
1006
+
1007
+ ↓
1008
+
1009
+ **Clinical + cognitive fusion**
1010
+
1011
+ ↓
1012
+
1013
+ **Prediction**
1014
+
1015
+ ---
1016
+
1017
+ ### Prediction Tasks
1018
+
1019
+ **Progression**
1020
+ Predicts future disease progression using information available at the current visit.
1021
+
1022
+ **Development**
1023
+ Predicts whether the participant will develop future Alzheimer’s-related impairment.
1024
+
1025
+ ---
1026
+
1027
+ ### MRI Strategies
1028
+
1029
+ **Multi-Region ROI**
1030
+ Uses the hippocampus, entorhinal cortex, amygdala, and ventricles.
1031
+
1032
+ **Hippocampal ROI**
1033
+ Uses the hippocampal region only.
1034
+ """
1035
+ )
1036
+
1037
+ # ========================================================
1038
+ # Prediction results — full width
1039
+ # ========================================================
1040
+
1041
+ gr.Markdown("---")
1042
+
1043
+ gr.Markdown(
1044
+ "## Prediction Results"
1045
+ )
1046
+
1047
+ with gr.Row():
1048
+
1049
+ with gr.Column(
1050
+ scale=2
1051
+ ):
1052
+
1053
+ stage_output = gr.HTML(
1054
+ elem_id="result-card"
1055
+ )
1056
+
1057
+ with gr.Column(
1058
+ scale=3
1059
+ ):
1060
+
1061
+ chart_output = gr.Plot()
1062
+
1063
+ status_output = gr.Markdown()
1064
+
1065
+ with gr.Accordion(
1066
+ "Show exact multimodal fusion input",
1067
+ open=False,
1068
+ ):
1069
+
1070
+ feature_frame = gr.Dataframe(
1071
+ interactive=False
1072
+ )
1073
+
1074
+ # ========================================================
1075
+ # Events
1076
+ # ========================================================
1077
 
1078
  task_choice.change(
1079
  fn=selection_help,
1080
+ inputs=[
1081
+ task_choice,
1082
+ mri_strategy,
1083
+ ],
1084
  outputs=model_note,
1085
  )
1086
 
1087
  mri_strategy.change(
1088
  fn=selection_help,
1089
+ inputs=[
1090
+ task_choice,
1091
+ mri_strategy,
1092
+ ],
1093
  outputs=model_note,
1094
  )
1095
 
 
1101
 
1102
  run_btn.click(
1103
  fn=run_inference,
1104
+ inputs=[
1105
+ task_choice,
1106
+ mri_strategy,
1107
+ mri_file,
1108
+ core_df,
1109
+ extra_csv,
1110
+ ],
1111
+ outputs=[
1112
+ stage_output,
1113
+ chart_output,
1114
+ status_output,
1115
+ feature_frame,
1116
+ ],
1117
  )
1118
 
1119
 
1120
+ # ============================================================
1121
+ # Launch
1122
+ # ============================================================
1123
+
1124
  if __name__ == "__main__":
1125
+ demo.launch()