SID2000 commited on
Commit
6b579f3
·
verified ·
1 Parent(s): 06f74e9

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. Dockerfile +1 -1
  2. README.md +24 -22
  3. app.py +5 -0
  4. brain_mesh.py +344 -0
  5. pages/5_Brain_Viewer.py +220 -0
  6. requirements.txt +3 -0
Dockerfile CHANGED
@@ -3,7 +3,7 @@ FROM python:3.10-slim
3
  WORKDIR /app
4
 
5
  RUN apt-get update && apt-get install -y --no-install-recommends \
6
- build-essential \
7
  && rm -rf /var/lib/apt/lists/*
8
 
9
  COPY requirements.txt .
 
3
  WORKDIR /app
4
 
5
  RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ build-essential xvfb libgl1-mesa-glx libglib2.0-0 \
7
  && rm -rf /var/lib/apt/lists/*
8
 
9
  COPY requirements.txt .
README.md CHANGED
@@ -1,26 +1,28 @@
1
- ---
2
- title: CortexLab Dashboard
3
- emoji: 🧠
4
- colorFrom: purple
5
- colorTo: blue
6
- sdk: docker
7
- app_port: 7860
8
- license: cc-by-nc-4.0
9
- short_description: Interactive fMRI brain encoding analysis toolkit
10
- tags:
11
- - neuroscience
12
- - fmri
13
- - brain-alignment
14
- - streamlit
15
- ---
16
-
17
  # CortexLab Dashboard
18
 
19
- Interactive analysis dashboard for [CortexLab](https://github.com/siddhant-rajhans/cortexlab).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- - Brain Alignment Benchmark (RSA, CKA, Procrustes + statistical testing)
22
- - Cognitive Load Scorer (timeline, radar, comparison mode)
23
- - Temporal Dynamics (peak latency, lag correlation, sustained/transient)
24
- - ROI Connectivity (partial correlation, clustering, network graph)
25
 
26
- Runs on biologically realistic synthetic data. No GPU required.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # CortexLab Dashboard
2
 
3
+ Interactive analysis dashboard for [CortexLab](https://github.com/siddhant-rajhans/cortexlab) - multimodal fMRI brain encoding toolkit.
4
+
5
+ ## Pages
6
+
7
+ - **Brain Alignment Benchmark** - Compare AI model representations against brain responses (RSA, CKA, Procrustes)
8
+ - **Cognitive Load Scorer** - Visualize cognitive demand across visual, auditory, language, and executive dimensions
9
+ - **Temporal Dynamics** - Peak latency, lag correlations, sustained vs transient response decomposition
10
+ - **ROI Connectivity** - Correlation matrices, network clustering, degree centrality, graph visualization
11
+
12
+ ## Quick Start
13
+
14
+ ```bash
15
+ pip install -r requirements.txt
16
+ streamlit run Home.py
17
+ ```
18
+
19
+ Runs on **synthetic data** by default - no GPU or real fMRI data required.
20
+
21
+ ## Links
22
+
23
+ - [CortexLab Library](https://github.com/siddhant-rajhans/cortexlab)
24
+ - [CortexLab on HuggingFace](https://huggingface.co/SID2000/cortexlab)
25
 
26
+ ## License
 
 
 
27
 
28
+ CC BY-NC 4.0
app.py CHANGED
@@ -103,6 +103,11 @@ with col2:
103
  st.page_link("pages/4_Connectivity.py", label="ROI Connectivity", icon="🔗")
104
  st.caption("Partial correlation, modularity, betweenness centrality, dendrogram, network graph")
105
 
 
 
 
 
 
106
  # --- Analysis Log ---
107
  show_analysis_log()
108
 
 
103
  st.page_link("pages/4_Connectivity.py", label="ROI Connectivity", icon="🔗")
104
  st.caption("Partial correlation, modularity, betweenness centrality, dendrogram, network graph")
105
 
106
+ st.divider()
107
+ st.subheader("3D Visualization")
108
+ st.page_link("pages/5_Brain_Viewer.py", label="Interactive 3D Brain Viewer", icon="🧠")
109
+ st.caption("Rotatable brain surface with activation overlays, publication-quality multi-view panels, ROI highlighting, and modality-specific patterns")
110
+
111
  # --- Analysis Log ---
112
  show_analysis_log()
113
 
brain_mesh.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """3D brain mesh loading, data projection, and rendering utilities.
2
+
3
+ Supports both publication-quality multi-view panels (Plotly) and
4
+ interactive 3D exploration (PyVista/stpyvista with Plotly fallback).
5
+ """
6
+
7
+ import numpy as np
8
+ import streamlit as st
9
+ import plotly.graph_objects as go
10
+ from plotly.subplots import make_subplots
11
+
12
+ from utils import ROI_GROUPS
13
+
14
+ # --- Camera Presets ---
15
+
16
+ VIEWS = {
17
+ "Lateral Left": dict(eye=dict(x=-1.7, y=0, z=0.3), up=dict(x=0, y=0, z=1)),
18
+ "Lateral Right": dict(eye=dict(x=1.7, y=0, z=0.3), up=dict(x=0, y=0, z=1)),
19
+ "Medial": dict(eye=dict(x=1.5, y=0.3, z=0.2), up=dict(x=0, y=0, z=1)),
20
+ "Dorsal": dict(eye=dict(x=0, y=0, z=2.2), up=dict(x=0, y=1, z=0)),
21
+ "Ventral": dict(eye=dict(x=0, y=0, z=-2.2), up=dict(x=0, y=1, z=0)),
22
+ "Anterior": dict(eye=dict(x=0, y=1.7, z=0.3), up=dict(x=0, y=0, z=1)),
23
+ "Posterior": dict(eye=dict(x=0, y=-1.7, z=0.3), up=dict(x=0, y=0, z=1)),
24
+ }
25
+
26
+ # --- Modality activation patterns ---
27
+
28
+ ACTIVATION_PATTERNS = {
29
+ "visual": {
30
+ "V1": 1.0, "V2": 0.9, "V3": 0.8, "V4": 0.7,
31
+ "MT": 0.75, "MST": 0.65, "FFC": 0.6, "VVC": 0.55,
32
+ "A1": 0.05, "LBelt": 0.04, "44": 0.08, "45": 0.07,
33
+ "46": 0.25, "FEF": 0.35,
34
+ },
35
+ "auditory": {
36
+ "A1": 1.0, "LBelt": 0.9, "MBelt": 0.85, "PBelt": 0.8,
37
+ "A4": 0.7, "A5": 0.65,
38
+ "V1": 0.03, "44": 0.12, "45": 0.1, "TPOJ1": 0.25,
39
+ "46": 0.15,
40
+ },
41
+ "language": {
42
+ "44": 1.0, "45": 0.95, "IFJa": 0.85, "IFJp": 0.8,
43
+ "TPOJ1": 0.9, "TPOJ2": 0.85, "STV": 0.75, "PSL": 0.7,
44
+ "V1": 0.05, "A1": 0.25, "46": 0.45,
45
+ },
46
+ "multimodal": {
47
+ "V1": 0.6, "V2": 0.55, "MT": 0.5,
48
+ "A1": 0.6, "LBelt": 0.55,
49
+ "44": 0.55, "45": 0.5, "TPOJ1": 0.5,
50
+ "46": 0.35, "FEF": 0.3,
51
+ },
52
+ }
53
+
54
+
55
+ # --- Mesh Loading ---
56
+
57
+ @st.cache_resource
58
+ def load_fsaverage_mesh(hemi="left", resolution="fsaverage5"):
59
+ """Load fsaverage brain mesh via nilearn. Returns (coords, faces)."""
60
+ from nilearn.datasets import fetch_surf_fsaverage
61
+ from nilearn.surface import load_surf_mesh
62
+
63
+ fsaverage = fetch_surf_fsaverage(mesh=resolution)
64
+ key = f"pial_{hemi}"
65
+ coords, faces = load_surf_mesh(fsaverage[key])
66
+ return np.array(coords, dtype=np.float32), np.array(faces, dtype=np.int32)
67
+
68
+
69
+ @st.cache_resource
70
+ def load_sulcal_map(hemi="left", resolution="fsaverage5"):
71
+ """Load sulcal depth map for anatomical background."""
72
+ from nilearn.datasets import fetch_surf_fsaverage
73
+ from nilearn.surface import load_surf_data
74
+
75
+ fsaverage = fetch_surf_fsaverage(mesh=resolution)
76
+ sulc = load_surf_data(fsaverage[f"sulc_{hemi}"])
77
+ return np.array(sulc, dtype=np.float32)
78
+
79
+
80
+ # --- Data Projection ---
81
+
82
+ def generate_sample_activations(n_vertices, roi_indices, pattern="visual", seed=42):
83
+ """Generate demo activation data with modality-specific patterns.
84
+
85
+ Returns vertex-level activation array of shape (n_vertices,).
86
+ """
87
+ rng = np.random.default_rng(seed)
88
+ weights = ACTIVATION_PATTERNS.get(pattern, ACTIVATION_PATTERNS["visual"])
89
+
90
+ data = rng.standard_normal(n_vertices) * 0.05 # low baseline noise
91
+
92
+ for roi_name, vertices in roi_indices.items():
93
+ w = weights.get(roi_name, 0.02)
94
+ valid = vertices[vertices < n_vertices]
95
+ if len(valid) > 0:
96
+ # Smooth activation with per-vertex jitter
97
+ data[valid] = w + rng.standard_normal(len(valid)) * 0.05
98
+
99
+ return np.clip(data, 0, 1)
100
+
101
+
102
+ def highlight_rois(vertex_data, roi_indices, selected_rois, boost=1.5):
103
+ """Amplify activation in selected ROIs for visual highlighting."""
104
+ data = vertex_data.copy()
105
+ for roi in selected_rois:
106
+ if roi in roi_indices:
107
+ valid = roi_indices[roi]
108
+ valid = valid[valid < len(data)]
109
+ if len(valid) > 0:
110
+ data[valid] = np.clip(data[valid] * boost, 0, 1)
111
+ return data
112
+
113
+
114
+ def blend_with_sulcal(vertex_data, sulcal_map, data_opacity=0.85):
115
+ """Blend activation data with sulcal background for anatomical context."""
116
+ sulc_norm = (sulcal_map - sulcal_map.min()) / (sulcal_map.max() - sulcal_map.min() + 1e-8)
117
+ bg = 0.25 + sulc_norm * 0.3 # gray range 0.25-0.55
118
+
119
+ # Where activation is low, show more background
120
+ alpha = np.clip(vertex_data * 3, 0, data_opacity)
121
+ blended = alpha * vertex_data + (1 - alpha) * bg
122
+ return blended
123
+
124
+
125
+ # --- Plotly Rendering ---
126
+
127
+ def _make_mesh3d(coords, faces, vertex_data, cmap, vmin, vmax, opacity=1.0, name=""):
128
+ """Create a Plotly Mesh3d trace."""
129
+ return go.Mesh3d(
130
+ x=coords[:, 0], y=coords[:, 1], z=coords[:, 2],
131
+ i=faces[:, 0], j=faces[:, 1], k=faces[:, 2],
132
+ intensity=vertex_data,
133
+ intensitymode="vertex",
134
+ colorscale=cmap,
135
+ cmin=vmin, cmax=vmax,
136
+ opacity=opacity,
137
+ lighting=dict(ambient=0.4, diffuse=0.6, specular=0.3, roughness=0.5, fresnel=0.2),
138
+ lightposition=dict(x=100, y=200, z=300),
139
+ showscale=False,
140
+ name=name,
141
+ hovertemplate="Vertex: %{pointNumber}<br>Value: %{intensity:.3f}<extra></extra>",
142
+ )
143
+
144
+
145
+ def _scene_layout(camera, bg_color="#0E1117"):
146
+ """Create a Plotly 3D scene layout."""
147
+ return dict(
148
+ camera=camera,
149
+ xaxis=dict(visible=False),
150
+ yaxis=dict(visible=False),
151
+ zaxis=dict(visible=False),
152
+ bgcolor=bg_color,
153
+ aspectmode="data",
154
+ )
155
+
156
+
157
+ def render_publication_views(coords, faces, vertex_data, cmap="Hot", vmin=0, vmax=1, bg_color="#0E1117"):
158
+ """Render 4-panel publication-quality brain views.
159
+
160
+ Returns a Plotly figure with lateral left, lateral right, medial, and dorsal views.
161
+ """
162
+ view_keys = ["Lateral Left", "Lateral Right", "Medial", "Dorsal"]
163
+
164
+ fig = make_subplots(
165
+ rows=1, cols=4,
166
+ specs=[[{"type": "scene"}] * 4],
167
+ subplot_titles=view_keys,
168
+ horizontal_spacing=0.01,
169
+ )
170
+
171
+ for i, view_name in enumerate(view_keys, 1):
172
+ mesh = _make_mesh3d(coords, faces, vertex_data, cmap, vmin, vmax, name=view_name)
173
+ fig.add_trace(mesh, row=1, col=i)
174
+ fig.update_layout(**{f"scene{i if i > 1 else ''}": _scene_layout(VIEWS[view_name], bg_color)})
175
+
176
+ fig.update_layout(
177
+ height=350,
178
+ margin=dict(l=0, r=0, t=30, b=0),
179
+ paper_bgcolor=bg_color,
180
+ font=dict(color="white"),
181
+ showlegend=False,
182
+ )
183
+
184
+ # Add colorbar as a separate invisible trace
185
+ fig.add_trace(go.Mesh3d(
186
+ x=[0], y=[0], z=[0], i=[0], j=[0], k=[0],
187
+ intensity=[0], colorscale=cmap, cmin=vmin, cmax=vmax,
188
+ showscale=True,
189
+ colorbar=dict(
190
+ title=dict(text="Activation", side="right"),
191
+ len=0.8, thickness=15, x=1.02,
192
+ tickfont=dict(color="white"),
193
+ ),
194
+ opacity=0,
195
+ hoverinfo="none",
196
+ ))
197
+
198
+ return fig
199
+
200
+
201
+ def render_interactive_3d(coords, faces, vertex_data, cmap="Hot", vmin=0, vmax=1,
202
+ bg_color="#0E1117", initial_view="Lateral Left",
203
+ roi_indices=None, roi_labels=None, show_labels=False):
204
+ """Render an interactive rotatable 3D brain.
205
+
206
+ First attempts PyVista via stpyvista, falls back to Plotly mesh3d.
207
+ """
208
+ # Try stpyvista first
209
+ try:
210
+ return _render_pyvista(coords, faces, vertex_data, cmap, vmin, vmax,
211
+ bg_color, initial_view, roi_indices, show_labels)
212
+ except Exception:
213
+ pass
214
+
215
+ # Fallback: Plotly mesh3d (always works)
216
+ return _render_plotly(coords, faces, vertex_data, cmap, vmin, vmax,
217
+ bg_color, initial_view, roi_indices, roi_labels, show_labels)
218
+
219
+
220
+ def _render_pyvista(coords, faces, vertex_data, cmap, vmin, vmax,
221
+ bg_color, initial_view, roi_indices, show_labels):
222
+ """Render with PyVista via stpyvista."""
223
+ import pyvista as pv
224
+ from stpyvista import stpyvista
225
+ from stpyvista.utils import start_xvfb
226
+
227
+ if "IS_XVFB_RUNNING" not in st.session_state:
228
+ try:
229
+ start_xvfb()
230
+ except Exception:
231
+ pass
232
+ st.session_state.IS_XVFB_RUNNING = True
233
+
234
+ pv_faces = np.column_stack([np.full(len(faces), 3), faces]).ravel()
235
+ mesh = pv.PolyData(coords, pv_faces)
236
+ mesh.point_data["activation"] = vertex_data
237
+
238
+ cmap_map = {"Hot": "hot", "Inferno": "inferno", "Plasma": "plasma",
239
+ "Viridis": "viridis", "RdBu_r": "RdBu_r", "Coolwarm": "coolwarm"}
240
+ pv_cmap = cmap_map.get(cmap, "hot")
241
+
242
+ plotter = pv.Plotter(window_size=[900, 600], off_screen=True)
243
+ plotter.add_mesh(
244
+ mesh, scalars="activation", cmap=pv_cmap,
245
+ clim=[vmin, vmax], smooth_shading=True,
246
+ ambient=0.4, diffuse=0.6, specular=0.3,
247
+ show_scalar_bar=True,
248
+ )
249
+
250
+ if show_labels and roi_indices:
251
+ for name, vertices in roi_indices.items():
252
+ valid = vertices[vertices < len(coords)]
253
+ if len(valid) > 0:
254
+ center = coords[valid].mean(axis=0)
255
+ plotter.add_point_labels(
256
+ center.reshape(1, 3), [name],
257
+ font_size=10, shape_opacity=0.3,
258
+ text_color="white",
259
+ )
260
+
261
+ r, g, b = int(bg_color[1:3], 16), int(bg_color[3:5], 16), int(bg_color[5:7], 16)
262
+ plotter.background_color = (r / 255, g / 255, b / 255)
263
+
264
+ stpyvista(plotter, key="brain_3d_viewer")
265
+ return None # stpyvista renders directly
266
+
267
+
268
+ def _render_plotly(coords, faces, vertex_data, cmap, vmin, vmax,
269
+ bg_color, initial_view, roi_indices, roi_labels, show_labels):
270
+ """Render with Plotly mesh3d (fallback)."""
271
+ fig = go.Figure()
272
+
273
+ fig.add_trace(_make_mesh3d(coords, faces, vertex_data, cmap, vmin, vmax))
274
+
275
+ # Add ROI labels as scatter3d annotations
276
+ if show_labels and roi_indices:
277
+ label_x, label_y, label_z, label_text = [], [], [], []
278
+ for name, vertices in roi_indices.items():
279
+ valid = vertices[vertices < len(coords)]
280
+ if len(valid) > 0:
281
+ center = coords[valid].mean(axis=0)
282
+ label_x.append(center[0])
283
+ label_y.append(center[1])
284
+ label_z.append(center[2])
285
+ label_text.append(name)
286
+
287
+ fig.add_trace(go.Scatter3d(
288
+ x=label_x, y=label_y, z=label_z,
289
+ mode="text",
290
+ text=label_text,
291
+ textfont=dict(size=9, color="white"),
292
+ hoverinfo="text",
293
+ showlegend=False,
294
+ ))
295
+
296
+ camera = VIEWS.get(initial_view, VIEWS["Lateral Left"])
297
+ fig.update_layout(
298
+ scene=_scene_layout(camera, bg_color),
299
+ height=600,
300
+ margin=dict(l=0, r=0, t=0, b=0),
301
+ paper_bgcolor=bg_color,
302
+ )
303
+
304
+ return fig
305
+
306
+
307
+ # --- ROI Helpers ---
308
+
309
+ def make_vertex_roi_indices(n_vertices_per_roi=20):
310
+ """Create ROI -> vertex index mapping matching utils.make_roi_indices."""
311
+ from utils import ALL_ROIS
312
+ indices = {}
313
+ offset = 0
314
+ for roi in ALL_ROIS:
315
+ indices[roi] = np.arange(offset, offset + n_vertices_per_roi)
316
+ offset += n_vertices_per_roi
317
+ return indices, offset
318
+
319
+
320
+ def roi_summary_table(vertex_data, roi_indices, selected_rois):
321
+ """Compute summary stats for selected ROIs."""
322
+ import pandas as pd
323
+ rows = []
324
+ for roi in selected_rois:
325
+ if roi in roi_indices:
326
+ valid = roi_indices[roi]
327
+ valid = valid[valid < len(vertex_data)]
328
+ if len(valid) > 0:
329
+ vals = vertex_data[valid]
330
+ group = "Other"
331
+ for g, rois in ROI_GROUPS.items():
332
+ if roi in rois:
333
+ group = g
334
+ break
335
+ rows.append({
336
+ "ROI": roi,
337
+ "Group": group,
338
+ "Mean": float(vals.mean()),
339
+ "Std": float(vals.std()),
340
+ "Min": float(vals.min()),
341
+ "Max": float(vals.max()),
342
+ "Vertices": len(valid),
343
+ })
344
+ return pd.DataFrame(rows) if rows else None
pages/5_Brain_Viewer.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Interactive 3D Brain Viewer - Publication Quality + Explorer."""
2
+
3
+ import streamlit as st
4
+ import numpy as np
5
+ import plotly.graph_objects as go
6
+
7
+ from session import init_session, show_analysis_log, upload_npy_widget
8
+ from brain_mesh import (
9
+ load_fsaverage_mesh,
10
+ load_sulcal_map,
11
+ generate_sample_activations,
12
+ highlight_rois,
13
+ blend_with_sulcal,
14
+ render_publication_views,
15
+ render_interactive_3d,
16
+ roi_summary_table,
17
+ VIEWS,
18
+ ACTIVATION_PATTERNS,
19
+ )
20
+ from utils import ROI_GROUPS, make_roi_indices
21
+
22
+ st.set_page_config(page_title="3D Brain Viewer", page_icon="🧠", layout="wide")
23
+ init_session()
24
+ show_analysis_log()
25
+
26
+ st.title("🧠 Interactive 3D Brain Viewer")
27
+ st.markdown("Explore brain activation patterns on the cortical surface. Publication-quality multi-view panels + interactive 3D rotation.")
28
+
29
+ # --- Sidebar ---
30
+ with st.sidebar:
31
+ st.header("Brain Viewer")
32
+
33
+ hemi = st.selectbox("Hemisphere", ["left", "right"], index=0)
34
+ resolution = st.selectbox("Mesh resolution", ["fsaverage5", "fsaverage4"], index=0,
35
+ help="fsaverage5: 10k vertices (detailed). fsaverage4: 2.5k vertices (fast).")
36
+
37
+ st.subheader("Data")
38
+ data_source = st.radio("Data source", ["Sample activations", "From current analysis", "Upload .npy"])
39
+
40
+ if data_source == "Sample activations":
41
+ pattern = st.selectbox("Activation pattern", list(ACTIVATION_PATTERNS.keys()),
42
+ help="Modality-specific activation: visual lights up V1/V2/MT, language lights up Broca's/Wernicke's, etc.")
43
+ seed = st.number_input("Seed", value=42, min_value=0)
44
+
45
+ st.subheader("Appearance")
46
+ cmap = st.selectbox("Colormap", ["Hot", "Inferno", "Plasma", "Viridis", "RdBu_r", "Coolwarm"], index=0)
47
+ vmin, vmax = st.slider("Data range", 0.0, 1.0, (0.0, 1.0), 0.05)
48
+ bg_color = st.selectbox("Background", ["#0E1117", "#000000", "#1A1A2E"], index=0,
49
+ format_func=lambda x: {"#0E1117": "Dark", "#000000": "Black", "#1A1A2E": "Navy"}[x])
50
+
51
+ st.subheader("ROI Highlighting")
52
+ roi_groups_selected = st.multiselect("Region groups", list(ROI_GROUPS.keys()))
53
+ available_rois = []
54
+ for g in roi_groups_selected:
55
+ available_rois.extend(ROI_GROUPS[g])
56
+ selected_rois = st.multiselect("Specific ROIs", available_rois, default=available_rois[:5] if available_rois else [])
57
+ show_labels = st.checkbox("Show ROI labels", value=True)
58
+
59
+ # --- Load Mesh ---
60
+ with st.spinner(f"Loading {resolution} brain mesh ({hemi} hemisphere)..."):
61
+ coords, faces = load_fsaverage_mesh(hemi, resolution)
62
+ n_vertices = coords.shape[0]
63
+
64
+ # --- Load/Generate Data ---
65
+ roi_indices, _ = make_roi_indices()
66
+
67
+ # Map ROI indices to actual mesh vertices (scale to mesh size)
68
+ # Since our ROI indices are synthetic (0-580), map them proportionally to actual mesh
69
+ mesh_roi_indices = {}
70
+ for name, idx in roi_indices.items():
71
+ scaled = (idx * n_vertices // 580).astype(int)
72
+ scaled = scaled[scaled < n_vertices]
73
+ mesh_roi_indices[name] = scaled
74
+
75
+ if data_source == "Sample activations":
76
+ vertex_data = generate_sample_activations(n_vertices, mesh_roi_indices, pattern, seed)
77
+ elif data_source == "Upload .npy":
78
+ uploaded = upload_npy_widget(f"Upload vertex data (.npy, {n_vertices} vertices)", "brain_upload")
79
+ if uploaded is not None and len(uploaded) == n_vertices:
80
+ vertex_data = uploaded
81
+ elif uploaded is not None:
82
+ st.warning(f"Expected {n_vertices} vertices, got {len(uploaded)}. Using sample data.")
83
+ vertex_data = generate_sample_activations(n_vertices, mesh_roi_indices, "visual", 42)
84
+ else:
85
+ vertex_data = generate_sample_activations(n_vertices, mesh_roi_indices, "visual", 42)
86
+ elif data_source == "From current analysis":
87
+ preds = st.session_state.get("brain_predictions")
88
+ if preds is not None:
89
+ # Average across timepoints, take first n_vertices
90
+ avg = np.abs(preds).mean(axis=0)
91
+ if len(avg) >= n_vertices:
92
+ vertex_data = avg[:n_vertices]
93
+ else:
94
+ vertex_data = np.pad(avg, (0, n_vertices - len(avg)))
95
+ # Normalize to [0, 1]
96
+ vd_range = vertex_data.max() - vertex_data.min()
97
+ if vd_range > 0:
98
+ vertex_data = (vertex_data - vertex_data.min()) / vd_range
99
+ else:
100
+ st.info("No analysis data in session. Go to Home page to generate data, or use sample activations.")
101
+ vertex_data = generate_sample_activations(n_vertices, mesh_roi_indices, "visual", 42)
102
+
103
+ # Apply ROI highlighting
104
+ if selected_rois:
105
+ vertex_data = highlight_rois(vertex_data, mesh_roi_indices, selected_rois, boost=1.8)
106
+
107
+ # Blend with sulcal map for anatomical context
108
+ try:
109
+ sulc = load_sulcal_map(hemi, resolution)
110
+ vertex_data_display = blend_with_sulcal(vertex_data, sulc)
111
+ except Exception:
112
+ vertex_data_display = vertex_data
113
+
114
+ # --- Publication Views ---
115
+ st.subheader("Publication Views")
116
+ st.caption("Four standard neuroimaging views. Right-click any panel to save as image.")
117
+
118
+ fig_pub = render_publication_views(coords, faces, vertex_data_display, cmap, vmin, vmax, bg_color)
119
+ st.plotly_chart(fig_pub, use_container_width=True)
120
+
121
+ # --- Interactive 3D ---
122
+ st.divider()
123
+ st.subheader("Interactive 3D Explorer")
124
+ st.caption("Rotate: drag | Zoom: scroll | Pan: shift+drag")
125
+
126
+ col_view, col_space = st.columns([1, 3])
127
+ with col_view:
128
+ initial_view = st.selectbox("Initial view", list(VIEWS.keys()), index=0)
129
+
130
+ result = render_interactive_3d(
131
+ coords, faces, vertex_data_display, cmap, vmin, vmax,
132
+ bg_color, initial_view, mesh_roi_indices,
133
+ roi_labels=selected_rois, show_labels=show_labels,
134
+ )
135
+ if result is not None:
136
+ st.plotly_chart(result, use_container_width=True)
137
+
138
+ # --- ROI Summary ---
139
+ if selected_rois:
140
+ st.divider()
141
+ col_table, col_hist = st.columns([1, 1])
142
+
143
+ with col_table:
144
+ st.subheader("ROI Summary")
145
+ summary = roi_summary_table(vertex_data, mesh_roi_indices, selected_rois)
146
+ if summary is not None:
147
+ st.dataframe(
148
+ summary.style.format({"Mean": "{:.4f}", "Std": "{:.4f}", "Min": "{:.4f}", "Max": "{:.4f}"}),
149
+ use_container_width=True, hide_index=True,
150
+ )
151
+
152
+ with col_hist:
153
+ st.subheader("Activation Distribution")
154
+ fig_hist = go.Figure()
155
+ fig_hist.add_trace(go.Histogram(
156
+ x=vertex_data, nbinsx=50,
157
+ marker_color="rgba(108, 92, 231, 0.7)",
158
+ name="All vertices",
159
+ ))
160
+ # Overlay selected ROI distributions
161
+ group_colors = {"Visual": "#00D2FF", "Auditory": "#FF6B6B", "Language": "#A29BFE", "Executive": "#FFEAA7"}
162
+ for roi in selected_rois[:3]: # limit to 3 for clarity
163
+ if roi in mesh_roi_indices:
164
+ valid = mesh_roi_indices[roi]
165
+ valid = valid[valid < len(vertex_data)]
166
+ if len(valid) > 0:
167
+ group = "Other"
168
+ for g, rois in ROI_GROUPS.items():
169
+ if roi in rois:
170
+ group = g
171
+ break
172
+ fig_hist.add_trace(go.Histogram(
173
+ x=vertex_data[valid], nbinsx=20,
174
+ marker_color=group_colors.get(group, "#888"),
175
+ name=roi, opacity=0.6,
176
+ ))
177
+ fig_hist.update_layout(
178
+ xaxis_title="Activation", yaxis_title="Count",
179
+ height=350, template="plotly_dark",
180
+ barmode="overlay",
181
+ legend=dict(orientation="h", yanchor="bottom", y=1.02),
182
+ )
183
+ st.plotly_chart(fig_hist, use_container_width=True)
184
+
185
+ # --- Stats ---
186
+ st.divider()
187
+ col1, col2, col3, col4 = st.columns(4)
188
+ col1.metric("Vertices", f"{n_vertices:,}")
189
+ col2.metric("Mean Activation", f"{vertex_data.mean():.4f}")
190
+ col3.metric("Active Vertices", f"{(vertex_data > 0.1).sum():,} ({100 * (vertex_data > 0.1).mean():.0f}%)")
191
+ col4.metric("Peak", f"{vertex_data.max():.4f}")
192
+
193
+ # --- Methodology ---
194
+ with st.expander("About the 3D Brain Viewer", expanded=False):
195
+ st.markdown("""
196
+ **Surface Mesh**: The brain surface is the fsaverage template from FreeSurfer, loaded via
197
+ nilearn. fsaverage5 has 10,242 vertices per hemisphere; fsaverage4 has 2,562.
198
+
199
+ **Activation Overlay**: Vertex-level scalar data is projected onto the mesh surface as a
200
+ colormap. The data is blended with the sulcal depth map (anatomical grooves) to provide
201
+ spatial context.
202
+
203
+ **Sample Activations**: Modality-specific patterns assign activation weights to HCP MMP1.0
204
+ ROIs based on established functional neuroanatomy. Visual stimuli activate V1/V2/MT,
205
+ auditory stimuli activate A1/belt areas, language stimuli activate Broca's (area 44/45)
206
+ and Wernicke's (TPOJ1/2).
207
+
208
+ **ROI Highlighting**: Selected ROIs are amplified (1.8x) to make them visually distinct.
209
+ The summary table shows descriptive statistics for highlighted regions.
210
+
211
+ **Publication Views**: Four standard views (lateral left, lateral right, medial, dorsal)
212
+ match the conventions used in neuroimaging journals. Right-click to save individual panels.
213
+
214
+ **Interactive View**: Supports rotation (drag), zoom (scroll), and pan (shift+drag).
215
+ Uses PyVista when available, falls back to Plotly mesh3d.
216
+
217
+ **References**:
218
+ - Fischl, 2012, *NeuroImage* (FreeSurfer surface reconstruction)
219
+ - Glasser et al., 2016, *Nature* (HCP MMP1.0 parcellation)
220
+ """)
requirements.txt CHANGED
@@ -6,3 +6,6 @@ pandas>=2.0
6
  networkx>=3.2
7
  matplotlib>=3.8
8
  seaborn>=0.13
 
 
 
 
6
  networkx>=3.2
7
  matplotlib>=3.8
8
  seaborn>=0.13
9
+ nilearn>=0.11.0
10
+ pyvista>=0.47.0
11
+ stpyvista>=0.2.1