Pixedar commited on
Commit
bd4a96a
·
1 Parent(s): baed508

Deploy MindVisualizer live runtime

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +8 -0
  2. .gitattributes +3 -32
  3. .gitignore +4 -0
  4. Dockerfile +39 -0
  5. README.md +7 -6
  6. app.py +676 -0
  7. data/brain_alignment.json +28 -0
  8. data/extra_parcellation/README.md +53 -0
  9. data/extra_parcellation/combined_atlas.nii.gz +3 -0
  10. data/extra_parcellation/combined_atlas_labels.json +129 -0
  11. data/label_grid_cache.keys.json +1 -0
  12. data/label_grid_cache.npz +3 -0
  13. data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_entropy.bin +3 -0
  14. data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mean_xyz3.bin +3 -0
  15. data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_meta.json +1 -0
  16. data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu1_xyz3.bin +3 -0
  17. data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu2_xyz3.bin +3 -0
  18. data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu3_xyz3.bin +3 -0
  19. data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu4_xyz3.bin +3 -0
  20. data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu5_xyz3.bin +3 -0
  21. data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu6_xyz3.bin +3 -0
  22. data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_pi_xyz6.bin +3 -0
  23. data/mdn/mdn_particles_rdcim_teacher_edge_hq_training_points.npy +3 -0
  24. data/meshes/.keep +1 -0
  25. data/meshes_obj/10153.obj +3 -0
  26. data/meshes_obj/10154.obj +3 -0
  27. data/meshes_obj/10155.obj +3 -0
  28. data/meshes_obj/10156.obj +3 -0
  29. data/meshes_obj/10157.obj +3 -0
  30. data/meshes_obj/10158.obj +3 -0
  31. data/meshes_obj/10159.obj +3 -0
  32. data/meshes_obj/10292.obj +3 -0
  33. data/meshes_obj/10306.obj +3 -0
  34. data/meshes_obj/10307.obj +3 -0
  35. data/meshes_obj/10308.obj +3 -0
  36. data/meshes_obj/10311.obj +3 -0
  37. data/meshes_obj/10313.obj +3 -0
  38. data/meshes_obj/10327.obj +3 -0
  39. data/meshes_obj/10329.obj +3 -0
  40. data/meshes_obj/10330.obj +3 -0
  41. data/meshes_obj/10331.obj +3 -0
  42. data/meshes_obj/10332.obj +3 -0
  43. data/meshes_obj/10333.obj +3 -0
  44. data/meshes_obj/10334.obj +3 -0
  45. data/meshes_obj/10335.obj +3 -0
  46. data/meshes_obj/10336.obj +3 -0
  47. data/meshes_obj/10337.obj +3 -0
  48. data/meshes_obj/10338.obj +3 -0
  49. data/meshes_obj/10339.obj +3 -0
  50. data/meshes_obj/10342.obj +3 -0
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .idea
3
+ .venv
4
+ __pycache__
5
+ *.pyc
6
+ *.pyo
7
+ .env
8
+ data/meshes
.gitattributes CHANGED
@@ -1,35 +1,6 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
  *.npy filter=lfs diff=lfs merge=lfs -text
15
  *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.obj filter=lfs diff=lfs merge=lfs -text
 
2
  *.bin filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
3
  *.npy filter=lfs diff=lfs merge=lfs -text
4
  *.npz filter=lfs diff=lfs merge=lfs -text
5
+ *.gz filter=lfs diff=lfs merge=lfs -text
6
+ *.ply filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .env
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive \
4
+ DISPLAY=:99 \
5
+ LIBGL_ALWAYS_SOFTWARE=1 \
6
+ MESA_GL_VERSION_OVERRIDE=3.3 \
7
+ PIP_NO_CACHE_DIR=1 \
8
+ PYTHONUNBUFFERED=1
9
+
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ ca-certificates \
12
+ libegl1 \
13
+ libgl1 \
14
+ libgl1-mesa-dri \
15
+ libglib2.0-0 \
16
+ libglu1-mesa \
17
+ libsm6 \
18
+ libx11-6 \
19
+ libxext6 \
20
+ libxrender1 \
21
+ libxt6 \
22
+ xvfb \
23
+ && rm -rf /var/lib/apt/lists/*
24
+
25
+ WORKDIR /app/mindVisualizer
26
+ COPY . /app/mindVisualizer
27
+
28
+ RUN python -m pip install --upgrade pip setuptools wheel && \
29
+ python -m pip install \
30
+ numpy scipy vtk \
31
+ langchain langchain-openai langchain-community \
32
+ faiss-cpu python-dotenv certifi tiktoken openai \
33
+ brainglobe-atlasapi nibabel nilearn scikit-image \
34
+ trame trame-vtk trame-vuetify && \
35
+ chmod +x /app/mindVisualizer/start.sh
36
+
37
+ EXPOSE 7860
38
+
39
+ CMD ["/app/mindVisualizer/start.sh"]
README.md CHANGED
@@ -1,10 +1,11 @@
1
- ---
2
- title: Mindvisualizer Vtk
3
- emoji: 😻
4
- colorFrom: purple
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
+ ---
2
+ title: MindVisualizer Live
3
+ colorFrom: green
4
+ colorTo: blue
 
5
  sdk: docker
6
  pinned: false
7
  ---
8
 
9
+ # MindVisualizer Live
10
+
11
+ Runs the Python/VTK MindVisualizer runtime through a Trame web transport.
app.py ADDED
@@ -0,0 +1,676 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """HF-safe live MindVisualizer wrapper.
3
+
4
+ This is a web transport for the Python/VTK runtime. It imports the original
5
+ project modules for field loading, particle helpers, mesh overlay, probes, and
6
+ GPT analysis instead of reimplementing the algorithms in TypeScript.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import asyncio
13
+ import os
14
+ import sys
15
+ import threading
16
+ from pathlib import Path
17
+
18
+ import numpy as np
19
+ import vtk
20
+ from scipy.spatial import cKDTree
21
+ from vtkmodules.util.numpy_support import numpy_to_vtk
22
+ from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
23
+
24
+ PROJECT_ROOT = Path(__file__).resolve().parent
25
+ DATA_DIR = PROJECT_ROOT / "data"
26
+ MDN_DIR = DATA_DIR / "mdn"
27
+ MESH_DIR = DATA_DIR / "meshes"
28
+ ALIGNMENT_FILE = DATA_DIR / "brain_alignment.json"
29
+ DEFAULT_META = MDN_DIR / "mdn_particles_rdcim_teacher_edge_hq_grid125_meta.json"
30
+ DEFAULT_OOS = MDN_DIR / "mdn_particles_rdcim_teacher_edge_hq_training_points.npy"
31
+
32
+ if str(PROJECT_ROOT) not in sys.path:
33
+ sys.path.insert(0, str(PROJECT_ROOT))
34
+
35
+ from src.colormaps import bicolor_white_center, rainbow_rgb01, turbo_rgb01
36
+ from src.field_loader import TriLinearSampler, load_field, load_points_any
37
+ from src.main import (
38
+ VtkTextOverlay,
39
+ add_window_legend,
40
+ build_cloud,
41
+ compute_fraction_mask,
42
+ densify_oos_surface,
43
+ lattice_positions,
44
+ wrap_inside,
45
+ )
46
+ from src.mesh_overlay import FlowMeshOverlay
47
+ from src.probe import ProbeSystem
48
+ from src.region_analyzer import analyze_probe_path, analyze_with_gpt
49
+
50
+
51
+ def ensure_ssl() -> None:
52
+ try:
53
+ import certifi
54
+
55
+ cert_file = certifi.where()
56
+ for name in ("SSL_CERT_FILE", "REQUESTS_CA_BUNDLE"):
57
+ current = os.environ.get(name, "")
58
+ if not current or not os.path.isfile(current):
59
+ os.environ[name] = cert_file
60
+ except Exception:
61
+ pass
62
+
63
+
64
+ class LiveMindVisualizer:
65
+ def __init__(self, args: argparse.Namespace):
66
+ self.args = args
67
+ self.rng = np.random.default_rng(2025)
68
+ self.paused = False
69
+ self.field_mode = "mean"
70
+ self.colour_mode = "SPEED"
71
+ self.cmap_mode = "TURBO"
72
+ self.filter_mode = "OFF"
73
+ self.filter_top = True
74
+ self.filter_frac = 1.0
75
+ self.clip_on = False
76
+ self.clip_frac = 1.0
77
+ self.mdn_alpha = 1.0
78
+ self.oos_visible = False
79
+ self.probe_busy = False
80
+ self.gpt_pending: str | None = None
81
+ self._load_data()
82
+ self._setup_vtk()
83
+
84
+ def _load_data(self) -> None:
85
+ self.field = load_field(self.args.meta)
86
+ self.G = self.field["G"]
87
+ self.amin = self.field["amin"]
88
+ self.amax = self.field["amax"]
89
+ self.V_mean = self.field["mean"]
90
+ self.MUS = self.field["mus"]
91
+ self.PI = self.field["pi"]
92
+ self.ENT = self.field["ENT"]
93
+ self.K = len(self.MUS)
94
+
95
+ self.sampler_mean = TriLinearSampler(self.V_mean, self.amin, self.amax)
96
+ self.diag = float(np.linalg.norm(self.amax - self.amin))
97
+ self.target_step = self.args.max_step_frac * max(self.diag, 1e-6)
98
+ self.vmax_mean = self._interior_percentile(self.V_mean)
99
+
100
+ self.samplers_comp = [TriLinearSampler(mu, self.amin, self.amax) for mu in self.MUS]
101
+ self.sampler_gated = None
102
+ self.V_gated = None
103
+ if self.PI is not None and self.PI.ndim == 4 and self.PI.shape[-1] == self.K and self.K > 0:
104
+ idx = np.argmax(self.PI, axis=-1)
105
+ self.V_gated = np.zeros_like(self.MUS[0], dtype=np.float32)
106
+ for k_i in range(self.K):
107
+ self.V_gated += self.MUS[k_i] * (idx == k_i)[..., None].astype(np.float32)
108
+ self.sampler_gated = TriLinearSampler(self.V_gated, self.amin, self.amax)
109
+
110
+ self.field_modes = ["mean"]
111
+ if self.sampler_gated is not None:
112
+ self.field_modes.append("gated")
113
+ self.field_modes += [f"comp{i + 1}" for i in range(self.K)]
114
+
115
+ self.sampler_by_name = {"mean": self.sampler_mean}
116
+ if self.sampler_gated is not None:
117
+ self.sampler_by_name["gated"] = self.sampler_gated
118
+ for i, sampler in enumerate(self.samplers_comp):
119
+ self.sampler_by_name[f"comp{i + 1}"] = sampler
120
+
121
+ self.vmax_by_name = {"mean": self.vmax_mean}
122
+ if self.V_gated is not None:
123
+ self.vmax_by_name["gated"] = self._interior_percentile(self.V_gated)
124
+ for i, mu in enumerate(self.MUS):
125
+ self.vmax_by_name[f"comp{i + 1}"] = self._interior_percentile(mu)
126
+
127
+ self.ent_min = self.ent_max = 0.0
128
+ if self.ENT is not None:
129
+ self.ent_min = float(np.min(self.ENT))
130
+ self.ent_max = float(np.max(self.ENT))
131
+
132
+ self.oos_pts = None
133
+ if self.args.oos is not None and Path(self.args.oos).exists():
134
+ self.oos_pts = load_points_any(Path(self.args.oos)).astype(np.float32)
135
+
136
+ self._seed_particles()
137
+ self._prepare_oos_distance()
138
+
139
+ def _interior_percentile(self, vec: np.ndarray, q: float = 100.0) -> float:
140
+ mag = np.linalg.norm(vec.reshape(-1, 3), axis=1)
141
+ nz = mag[mag > 0]
142
+ return float(np.percentile(nz, q)) if nz.size else 1.0
143
+
144
+ def _seed_particles(self) -> None:
145
+ overlap_radius = float(self.args.overlap_frac) * max(self.diag, 1e-9)
146
+ overlap_sigma = float(self.args.overlap_jitter) * max(self.diag, 1e-9)
147
+ self.seed_mode = "grid"
148
+ self.seed_points = None
149
+ self.n_axis = None
150
+
151
+ train = self.field["TRAIN"]
152
+ if train is not None and self.oos_pts is not None and len(train) > 0 and len(self.oos_pts) > 0:
153
+ try:
154
+ tree = cKDTree(train.astype(np.float32))
155
+ dists, _ = tree.query(self.oos_pts.astype(np.float32), k=1)
156
+ candidates = self.oos_pts[dists <= overlap_radius]
157
+ if candidates.size > 0:
158
+ self.seed_mode = "overlap"
159
+ if self.args.overlap_limit and len(candidates) > self.args.overlap_limit:
160
+ idx = self.rng.choice(len(candidates), size=int(self.args.overlap_limit), replace=False)
161
+ candidates = candidates[idx]
162
+ self.seed_points = candidates.astype(np.float32)
163
+ except Exception as exc:
164
+ print("[seed] overlap failed:", exc)
165
+
166
+ if self.seed_mode == "overlap" and self.seed_points is not None:
167
+ if self.args.oos_seed_limit > 0 and len(self.seed_points) > self.args.oos_seed_limit:
168
+ idx = self.rng.choice(len(self.seed_points), size=self.args.oos_seed_limit, replace=False)
169
+ self.seed_points = self.seed_points[idx]
170
+ if self.args.oos_fill_count > 0 and self.oos_pts is not None:
171
+ extras = densify_oos_surface(
172
+ base_pts=self.seed_points,
173
+ full_oos_pts=self.oos_pts,
174
+ amin=self.amin,
175
+ amax=self.amax,
176
+ extra_count=self.args.oos_fill_count,
177
+ )
178
+ if extras is not None and len(extras) > 0:
179
+ self.seed_points = np.concatenate([self.seed_points, extras], axis=0).astype(np.float32)
180
+
181
+ if self.seed_mode == "overlap" and self.seed_points is not None:
182
+ if overlap_sigma > 0:
183
+ jitter = self.rng.standard_normal(self.seed_points.shape).astype(np.float32) * overlap_sigma
184
+ self.P0 = np.clip(self.seed_points + jitter, self.amin, self.amax)
185
+ else:
186
+ self.P0 = np.clip(self.seed_points, self.amin, self.amax)
187
+ else:
188
+ self.n_axis = max(2, self.G // max(1, self.args.stride))
189
+ self.P0 = lattice_positions(
190
+ self.n_axis,
191
+ self.amin,
192
+ self.amax,
193
+ margin=0.02,
194
+ jitter=self.args.respawn_jitter,
195
+ seed=2025,
196
+ )
197
+
198
+ self.P = self.P0.copy()
199
+ self.Np = len(self.P)
200
+ self.base_ttl_lo = max(8, 60 // 2)
201
+ self.base_ttl_hi = 60
202
+ self.ttl_scale = 1.0
203
+ self.ttl = self._sample_ttl(self.Np)
204
+ self.ages = self.rng.integers(0, np.maximum(1, self.ttl), size=self.Np, dtype=np.int32)
205
+ self.ent_prev = self.sampler_mean.sample_scalar(self.ENT, self.P).astype(np.float32) if self.ENT is not None else None
206
+ self.ent_d_ema = np.zeros(self.Np, np.float32) if self.ENT is not None else None
207
+
208
+ def _sample_ttl(self, n: int) -> np.ndarray:
209
+ base = self.rng.integers(self.base_ttl_lo, self.base_ttl_hi + 1, size=int(n))
210
+ return np.maximum(1, np.round(base * self.ttl_scale)).astype(np.int32)
211
+
212
+ def _prepare_oos_distance(self) -> None:
213
+ self.OOS_DIST = None
214
+ self.sampler_oosdist = None
215
+ self.far_oos_params = None
216
+ if self.args.no_far_from_oos or self.oos_pts is None or len(self.oos_pts) == 0:
217
+ return
218
+ Ng = int(max(8, self.args.oos_dist_grid))
219
+ tree_oos = cKDTree(self.oos_pts.astype(np.float32))
220
+ gridP = lattice_positions(Ng, self.amin, self.amax, margin=0.0, jitter=0.0, seed=0)
221
+ dists, _ = tree_oos.query(gridP, k=1)
222
+ self.OOS_DIST = dists.reshape(Ng, Ng, Ng).astype(np.float32)
223
+ self.sampler_oosdist = TriLinearSampler(np.zeros((Ng, Ng, Ng, 3), np.float32), self.amin, self.amax)
224
+ self.far_oos_params = {
225
+ "d0": float(self.args.oos_dist_thresh_frac) * self.diag,
226
+ "gamma": float(self.args.oos_dist_gamma),
227
+ "boost": float(self.args.oos_death_boost),
228
+ }
229
+
230
+ def _setup_vtk(self) -> None:
231
+ init_rgba = np.tile(np.array([[255, 255, 255, 48]], np.uint8), (self.Np, 1))
232
+ self.points, self.color_arr, self.poly, self.p_actor = build_cloud(self.P, point_size=2.0, rgba=init_rgba)
233
+
234
+ self.ren = vtk.vtkRenderer()
235
+ self.ren.SetBackground(0, 0, 0)
236
+ self.ren.AddActor(self.p_actor)
237
+
238
+ self.win = vtk.vtkRenderWindow()
239
+ self.win.SetOffScreenRendering(1)
240
+ self.win.AddRenderer(self.ren)
241
+ self.win.SetSize(self.args.window_size[0], self.args.window_size[1])
242
+ self.win.SetWindowName("mindVisualizer Live")
243
+
244
+ self.iren = vtk.vtkRenderWindowInteractor()
245
+ self.iren.SetRenderWindow(self.win)
246
+ self.iren.SetInteractorStyle(vtkInteractorStyleTrackballCamera())
247
+ self.iren.Initialize()
248
+
249
+ self.o_actor = None
250
+ if self.oos_pts is not None and len(self.oos_pts) > 0:
251
+ rgb_oos = np.tile(np.array([[255, 255, 255]], np.uint8), (len(self.oos_pts), 1))
252
+ oos_rgba = np.concatenate([rgb_oos, np.full((len(self.oos_pts), 1), 72, np.uint8)], axis=1)
253
+ _, _, _, self.o_actor = build_cloud(self.oos_pts, point_size=1.5, rgba=oos_rgba, opacity=72 / 255.0)
254
+
255
+ self.flow_mesh = None
256
+ try:
257
+ self.flow_mesh = FlowMeshOverlay(ren=self.ren, win=self.win, mesh_dir=MESH_DIR, alignment_file=ALIGNMENT_FILE)
258
+ grid_cache = DATA_DIR / "label_grid_cache.npz"
259
+ if not self.flow_mesh.load_label_grid(grid_cache):
260
+ self.flow_mesh.build_label_grid()
261
+ self.flow_mesh.save_label_grid(grid_cache)
262
+ if self.flow_mesh.get_all_region_keys():
263
+ self.flow_mesh.cycle(+1)
264
+ except Exception as exc:
265
+ print("[flow mesh] disabled:", exc)
266
+
267
+ self.text_overlay = VtkTextOverlay(self.ren)
268
+ self.probe_sys = ProbeSystem(self.ren, self.win, self.amin, self.amax)
269
+ if self.flow_mesh is not None:
270
+ self.probe_sys.set_mesh_overlay(self.flow_mesh)
271
+ self.probe_sys.set_multi_count(self.args.multi_probe)
272
+ if self.args.branching and self.PI is not None:
273
+ self.probe_sys.set_branching(True, pi_field=self.PI, mus_samplers=self.samplers_comp)
274
+
275
+ add_window_legend(
276
+ self.ren,
277
+ [
278
+ "mindVisualizer live Python/VTK",
279
+ "space pause | f field | v colour | m cmap | q/w mesh | hide",
280
+ "[ ] dt | +/- speed | 1/2 lifetime | 7/8 alpha | y/u/z/x filter",
281
+ "o OOS | j clip | a/d frac | probe | analyze | clear",
282
+ ],
283
+ font_px=12,
284
+ )
285
+ self.ren.ResetCamera()
286
+ for _ in range(12):
287
+ V = self.sampler_mean.sample_vec(self.P)
288
+ self.P = wrap_inside(
289
+ self.P + V * (self.args.dt * self.args.speed_scale) * (self.target_step / max(self.vmax_mean, 1e-9)),
290
+ self.amin,
291
+ self.amax,
292
+ margin_frac=self.args.margin,
293
+ )
294
+ if self.ENT is not None:
295
+ self.ent_prev = self.sampler_mean.sample_scalar(self.ENT, self.P).astype(np.float32)
296
+ self.ent_d_ema[:] = 0.0
297
+ self.win.Render()
298
+
299
+ def current_sampler(self) -> TriLinearSampler:
300
+ return self.sampler_by_name.get(self.field_mode, self.sampler_mean)
301
+
302
+ def vmax_for_mode(self) -> float:
303
+ return self.vmax_by_name.get(self.field_mode, self.vmax_mean)
304
+
305
+ def map_rgb_from_t(self, t01: np.ndarray) -> np.ndarray:
306
+ return rainbow_rgb01(t01) if self.cmap_mode == "RAINBOW" else turbo_rgb01(t01)
307
+
308
+ def apply_colors(self, speed_vals: np.ndarray, P_world: np.ndarray, dent_vals=None, vis_mask=None) -> None:
309
+ eps = 1e-12
310
+ if self.colour_mode == "ENTROPY" and self.ENT is not None:
311
+ scal = self.sampler_mean.sample_scalar(self.ENT, P_world)
312
+ t = (scal - self.ent_min) / max(self.ent_max - self.ent_min, eps)
313
+ base_rgb = self.map_rgb_from_t(np.clip(t, 0, 1))
314
+ elif self.colour_mode == "DELTA_ENTROPY" and dent_vals is not None:
315
+ a = np.abs(dent_vals)
316
+ scale = float(np.percentile(a, 97)) if a.size else 1.0
317
+ if not np.isfinite(scale) or scale <= eps:
318
+ scale = 1.0
319
+ base_rgb = self.map_rgb_from_t(np.clip(a / scale, 0, 1))
320
+ elif self.colour_mode == "DIR_DELTA_ENTROPY" and dent_vals is not None:
321
+ base_rgb = bicolor_white_center(dent_vals)
322
+ else:
323
+ smin = float(np.min(speed_vals)) if speed_vals.size else 0.0
324
+ smax = float(np.max(speed_vals)) if speed_vals.size else 1.0
325
+ t = (speed_vals - smin) / max(smax - smin, eps)
326
+ base_rgb = self.map_rgb_from_t(np.clip(t, 0, 1))
327
+
328
+ alpha = int(np.clip(255.0 * self.mdn_alpha, 5, 255))
329
+ a = np.full(len(base_rgb), alpha, np.uint8)
330
+ if vis_mask is not None:
331
+ a = a.copy()
332
+ a[~vis_mask] = 0
333
+ rgba = np.concatenate([base_rgb, a[:, None]], axis=1).astype(np.uint8)
334
+ self.color_arr.DeepCopy(numpy_to_vtk(rgba, deep=True))
335
+ self.color_arr.Modified()
336
+ self.poly.Modified()
337
+
338
+ def step(self) -> None:
339
+ if self.gpt_pending is not None:
340
+ result = self.gpt_pending
341
+ self.gpt_pending = None
342
+ self.probe_busy = False
343
+ self.text_overlay.show_gpt(result)
344
+ self.text_overlay.add_log("GPT analysis complete")
345
+
346
+ if self.paused:
347
+ self.win.Render()
348
+ return
349
+
350
+ sampler = self.current_sampler()
351
+ Vraw = sampler.sample_vec(self.P)
352
+ vmx = float(self.vmax_for_mode())
353
+ if self.clip_on:
354
+ thr = float(max(1e-9, self.clip_frac * vmx))
355
+ speed = np.linalg.norm(Vraw, axis=1)
356
+ k = np.minimum(1.0, thr / (speed + 1e-9)).astype(np.float32)
357
+ Vstep = Vraw * k[:, None]
358
+ eff_vmax = min(vmx, thr)
359
+ else:
360
+ Vstep = Vraw
361
+ eff_vmax = vmx
362
+
363
+ step_size = self.args.dt * self.args.speed_scale * (self.target_step / max(eff_vmax, 1e-9))
364
+ self.P = wrap_inside(self.P + Vstep * step_size, self.amin, self.amax, margin_frac=self.args.margin)
365
+
366
+ dent = None
367
+ if self.ENT is not None:
368
+ ent_now = self.sampler_mean.sample_scalar(self.ENT, self.P).astype(np.float32)
369
+ d = ent_now - self.ent_prev
370
+ self.ent_d_ema = (1.0 - 0.2) * self.ent_d_ema + 0.2 * d
371
+ self.ent_prev = ent_now
372
+ dent = self.ent_d_ema
373
+
374
+ age_inc = np.ones(len(self.P), np.int32)
375
+ if self.sampler_oosdist is not None and self.OOS_DIST is not None and self.far_oos_params is not None:
376
+ d_oos = self.sampler_oosdist.sample_scalar(self.OOS_DIST, self.P).astype(np.float32)
377
+ d0 = max(1e-9, self.far_oos_params["d0"])
378
+ t = np.clip((d_oos - d0) / d0, 0.0, 1.0)
379
+ gate = np.power(t, float(max(0.1, self.far_oos_params["gamma"]))).astype(np.float32)
380
+ age_inc += np.floor(gate * float(max(0.0, self.far_oos_params["boost"])) + 1e-9).astype(np.int32)
381
+ self.ages += age_inc
382
+
383
+ dead = self.ages >= self.ttl
384
+ if np.any(dead):
385
+ if self.seed_mode == "overlap" and self.seed_points is not None and len(self.seed_points) > 0:
386
+ sel = self.rng.integers(0, len(self.seed_points), size=dead.sum())
387
+ base = self.seed_points[sel]
388
+ if self.args.overlap_jitter > 0:
389
+ sigma = float(self.args.overlap_jitter) * max(self.diag, 1e-9)
390
+ jitter = self.rng.standard_normal((dead.sum(), 3)).astype(np.float32) * sigma
391
+ self.P[dead] = np.clip(base + jitter, self.amin, self.amax)
392
+ else:
393
+ self.P[dead] = np.clip(base, self.amin, self.amax)
394
+ else:
395
+ jitter_world = (self.amax - self.amin) * (self.args.respawn_jitter / max((self.n_axis or 2) - 1, 1))
396
+ jitter = (self.rng.random((dead.sum(), 3)).astype(np.float32) - 0.5) * 2.0 * jitter_world
397
+ self.P[dead] = np.clip(self.P0[dead] + jitter, self.amin, self.amax)
398
+ self.ages[dead] = 0
399
+ self.ttl[dead] = self._sample_ttl(dead.sum())
400
+ if self.ENT is not None:
401
+ self.ent_prev[dead] = self.sampler_mean.sample_scalar(self.ENT, self.P[dead]).astype(np.float32)
402
+ self.ent_d_ema[dead] = 0.0
403
+
404
+ if self.probe_sys.active:
405
+ self.probe_sys.step(sampler, step_size)
406
+
407
+ speed = np.linalg.norm(Vstep, axis=1)
408
+ if self.filter_mode == "OFF":
409
+ vis_mask = np.ones(len(self.P), dtype=bool)
410
+ else:
411
+ vis_mask = compute_fraction_mask(speed, self.filter_frac, top=self.filter_top)
412
+
413
+ self.points.SetData(numpy_to_vtk(self.P, deep=True))
414
+ self.points.Modified()
415
+ self.apply_colors(speed, self.P, dent_vals=dent, vis_mask=vis_mask)
416
+ self.win.Render()
417
+
418
+ def cycle_field(self) -> str:
419
+ idx = self.field_modes.index(self.field_mode)
420
+ self.field_mode = self.field_modes[(idx + 1) % len(self.field_modes)]
421
+ return self.field_mode
422
+
423
+ def cycle_colour(self) -> str:
424
+ modes = ["SPEED"]
425
+ if self.ENT is not None:
426
+ modes += ["ENTROPY", "DELTA_ENTROPY", "DIR_DELTA_ENTROPY"]
427
+ idx = modes.index(self.colour_mode) if self.colour_mode in modes else 0
428
+ self.colour_mode = modes[(idx + 1) % len(modes)]
429
+ return self.colour_mode
430
+
431
+ def place_probe_center(self) -> None:
432
+ pos = ((self.amin + self.amax) * 0.5).astype(np.float32)
433
+ self.probe_sys.place(pos)
434
+ self.text_overlay.add_log("Probe placed at brain center")
435
+
436
+ def analyze_probe(self, use_rag: bool) -> None:
437
+ if not self.probe_sys.active or self.flow_mesh is None or self.probe_busy:
438
+ self.text_overlay.add_log("No probe ready to analyze")
439
+ return
440
+ self.probe_sys.freeze()
441
+ snapshots = []
442
+ for probe in self.probe_sys.get_all_probes():
443
+ if not probe.path or len(probe.path) < 3:
444
+ continue
445
+ snapshots.append(
446
+ {
447
+ "path": probe.get_path_array().copy(),
448
+ "field_mags": probe.get_field_mags_array().copy(),
449
+ "ghost": probe.ghost,
450
+ "label": probe.label,
451
+ }
452
+ )
453
+ if not snapshots:
454
+ self.text_overlay.add_log("No probe data")
455
+ return
456
+ self.probe_busy = True
457
+ self.text_overlay.add_log("Analyzing probe path...")
458
+ threading.Thread(target=self._analyze_async, args=(snapshots, use_rag), daemon=True).start()
459
+
460
+ def _analyze_async(self, snapshots, use_rag: bool) -> None:
461
+ try:
462
+ transitions = []
463
+ for snap in snapshots:
464
+ transitions.extend(
465
+ analyze_probe_path(
466
+ snap["path"],
467
+ self.flow_mesh,
468
+ sample_every=5,
469
+ field_mags=snap["field_mags"] if len(snap["field_mags"]) == len(snap["path"]) else None,
470
+ entropy_sampler=self.sampler_mean if self.ENT is not None else None,
471
+ entropy_field=self.ENT,
472
+ )
473
+ )
474
+ if not transitions:
475
+ self.gpt_pending = "No brain regions detected along probe path."
476
+ return
477
+ self.gpt_pending = analyze_with_gpt(transitions, use_rag=use_rag, model=self.args.model, debug=False)
478
+ except Exception as exc:
479
+ self.gpt_pending = f"[GPT ERROR] {exc}"
480
+
481
+ @property
482
+ def status(self) -> str:
483
+ mesh_count = len(self.flow_mesh.get_all_region_keys()) if self.flow_mesh else 0
484
+ return (
485
+ f"{self.field_mode} | {self.colour_mode} | "
486
+ f"{self.Np:,} particles | {mesh_count} meshes | "
487
+ f"{'paused' if self.paused else 'running'}"
488
+ )
489
+
490
+
491
+ def build_arg_parser() -> argparse.ArgumentParser:
492
+ parser = argparse.ArgumentParser(description="MindVisualizer Trame live server")
493
+ parser.add_argument("--host", default="0.0.0.0")
494
+ parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "7860")))
495
+ parser.add_argument("--meta", type=Path, default=DEFAULT_META)
496
+ parser.add_argument("--oos", type=Path, default=DEFAULT_OOS)
497
+ parser.add_argument("--fps", type=int, default=int(os.environ.get("MINDVIS_FPS", "18")))
498
+ parser.add_argument("--stride", type=int, default=3)
499
+ parser.add_argument("--dt", type=float, default=1.0)
500
+ parser.add_argument("--max-step-frac", type=float, default=0.01)
501
+ parser.add_argument("--speed-scale", type=float, default=1.0)
502
+ parser.add_argument("--margin", type=float, default=0.0)
503
+ parser.add_argument("--respawn-jitter", type=float, default=0.015)
504
+ parser.add_argument("--oos-seed-limit", type=int, default=50000)
505
+ parser.add_argument("--oos-fill-count", type=int, default=10000)
506
+ parser.add_argument("--overlap-frac", type=float, default=0.01)
507
+ parser.add_argument("--overlap-jitter", type=float, default=0.002)
508
+ parser.add_argument("--overlap-limit", type=int, default=200000)
509
+ parser.add_argument("--no-far-from-oos", action="store_true")
510
+ parser.add_argument("--oos-dist-grid", type=int, default=96)
511
+ parser.add_argument("--oos-dist-thresh-frac", type=float, default=0.009)
512
+ parser.add_argument("--oos-dist-gamma", type=float, default=3.0)
513
+ parser.add_argument("--oos-death-boost", type=float, default=8.0)
514
+ parser.add_argument("--multi-probe", type=int, default=1)
515
+ parser.add_argument("--branching", action="store_true")
516
+ parser.add_argument("--window-size", type=int, nargs=2, default=[1320, 820])
517
+ parser.add_argument("--model", default=os.environ.get("OPENAI_MODEL", "gpt-5.4-mini"))
518
+ parser.add_argument("--rag", action="store_true")
519
+ return parser
520
+
521
+
522
+ def main() -> None:
523
+ ensure_ssl()
524
+ args = build_arg_parser().parse_args()
525
+
526
+ from trame.app import get_server
527
+ from trame.ui.vuetify3 import SinglePageLayout
528
+ from trame.widgets import html, vtk as vtk_widgets, vuetify3
529
+
530
+ app = LiveMindVisualizer(args)
531
+ server = get_server(client_type="vue3")
532
+ state, ctrl = server.state, server.controller
533
+ state.trame__title = "MindVisualizer Live"
534
+ state.status = app.status
535
+
536
+ def refresh_status() -> None:
537
+ state.status = app.status
538
+ state.flush()
539
+
540
+ def update_view() -> None:
541
+ if hasattr(ctrl, "view_update"):
542
+ ctrl.view_update()
543
+ refresh_status()
544
+
545
+ @ctrl.add("toggle_pause")
546
+ def toggle_pause(**_):
547
+ app.paused = not app.paused
548
+ update_view()
549
+
550
+ @ctrl.add("cycle_field")
551
+ def cycle_field(**_):
552
+ app.cycle_field()
553
+ update_view()
554
+
555
+ @ctrl.add("cycle_colour")
556
+ def cycle_colour(**_):
557
+ app.cycle_colour()
558
+ update_view()
559
+
560
+ @ctrl.add("cycle_cmap")
561
+ def cycle_cmap(**_):
562
+ app.cmap_mode = "RAINBOW" if app.cmap_mode == "TURBO" else "TURBO"
563
+ update_view()
564
+
565
+ @ctrl.add("mesh_prev")
566
+ def mesh_prev(**_):
567
+ if app.flow_mesh:
568
+ app.flow_mesh.cycle(-1)
569
+ update_view()
570
+
571
+ @ctrl.add("mesh_next")
572
+ def mesh_next(**_):
573
+ if app.flow_mesh:
574
+ app.flow_mesh.cycle(+1)
575
+ update_view()
576
+
577
+ @ctrl.add("mesh_hide")
578
+ def mesh_hide(**_):
579
+ if app.flow_mesh:
580
+ app.flow_mesh.hide()
581
+ update_view()
582
+
583
+ @ctrl.add("mesh_alpha_down")
584
+ def mesh_alpha_down(**_):
585
+ if app.flow_mesh:
586
+ app.flow_mesh.set_opacity(1.0 / 1.25)
587
+ update_view()
588
+
589
+ @ctrl.add("mesh_alpha_up")
590
+ def mesh_alpha_up(**_):
591
+ if app.flow_mesh:
592
+ app.flow_mesh.set_opacity(1.25)
593
+ update_view()
594
+
595
+ @ctrl.add("toggle_oos")
596
+ def toggle_oos(**_):
597
+ if app.o_actor is not None:
598
+ if app.oos_visible:
599
+ app.ren.RemoveActor(app.o_actor)
600
+ else:
601
+ app.ren.AddActor(app.o_actor)
602
+ app.oos_visible = not app.oos_visible
603
+ update_view()
604
+
605
+ @ctrl.add("place_probe")
606
+ def place_probe(**_):
607
+ app.place_probe_center()
608
+ update_view()
609
+
610
+ @ctrl.add("analyze_probe")
611
+ def analyze_probe(**_):
612
+ app.analyze_probe(use_rag=args.rag)
613
+ update_view()
614
+
615
+ @ctrl.add("clear_probe")
616
+ def clear_probe(**_):
617
+ app.probe_sys.clear()
618
+ app.text_overlay.clear_log()
619
+ app.text_overlay.hide_gpt()
620
+ update_view()
621
+
622
+ @ctrl.add("reset_camera")
623
+ def reset_camera(**_):
624
+ if hasattr(ctrl, "view_reset_camera"):
625
+ ctrl.view_reset_camera()
626
+ update_view()
627
+
628
+ async def animation_loop():
629
+ delay = 1.0 / max(1, int(args.fps))
630
+ while True:
631
+ app.step()
632
+ if hasattr(ctrl, "view_update"):
633
+ ctrl.view_update()
634
+ state.status = app.status
635
+ state.flush()
636
+ await asyncio.sleep(delay)
637
+
638
+ ctrl.on_server_ready.add(lambda **_: asyncio.create_task(animation_loop()))
639
+
640
+ with SinglePageLayout(server) as layout:
641
+ layout.title.set_text("MindVisualizer Live")
642
+ with layout.toolbar:
643
+ vuetify3.VBtn("Pause", click=ctrl.toggle_pause, density="compact", variant="tonal")
644
+ vuetify3.VBtn("Field", click=ctrl.cycle_field, density="compact", variant="tonal")
645
+ vuetify3.VBtn("Colour", click=ctrl.cycle_colour, density="compact", variant="tonal")
646
+ vuetify3.VBtn("Cmap", click=ctrl.cycle_cmap, density="compact", variant="tonal")
647
+ vuetify3.VDivider(vertical=True, classes="mx-2")
648
+ vuetify3.VBtn("Mesh -", click=ctrl.mesh_prev, density="compact", variant="tonal")
649
+ vuetify3.VBtn("Mesh +", click=ctrl.mesh_next, density="compact", variant="tonal")
650
+ vuetify3.VBtn("Hide", click=ctrl.mesh_hide, density="compact", variant="tonal")
651
+ vuetify3.VBtn("Alpha -", click=ctrl.mesh_alpha_down, density="compact", variant="tonal")
652
+ vuetify3.VBtn("Alpha +", click=ctrl.mesh_alpha_up, density="compact", variant="tonal")
653
+ vuetify3.VDivider(vertical=True, classes="mx-2")
654
+ vuetify3.VBtn("OOS", click=ctrl.toggle_oos, density="compact", variant="tonal")
655
+ vuetify3.VBtn("Probe", click=ctrl.place_probe, density="compact", variant="tonal")
656
+ vuetify3.VBtn("Analyze", click=ctrl.analyze_probe, density="compact", variant="tonal")
657
+ vuetify3.VBtn("Clear", click=ctrl.clear_probe, density="compact", variant="tonal")
658
+ vuetify3.VSpacer()
659
+ html.Div("{{ status }}", classes="text-caption text-medium-emphasis")
660
+
661
+ with layout.content:
662
+ with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height", style="background: #000;"):
663
+ view = vtk_widgets.VtkRemoteView(
664
+ app.win,
665
+ ref="view",
666
+ interactive_ratio=0.65,
667
+ interactive_quality=55,
668
+ )
669
+ ctrl.view_update = view.update
670
+ ctrl.view_reset_camera = view.reset_camera
671
+
672
+ server.start(host=args.host, port=args.port)
673
+
674
+
675
+ if __name__ == "__main__":
676
+ main()
data/brain_alignment.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "matrix": [
3
+ [
4
+ -0.03483789662196803,
5
+ -0.1654855793300339,
6
+ 0.9305766368497694,
7
+ -68.4377205759064
8
+ ],
9
+ [
10
+ -0.9422190580860553,
11
+ -0.06752555868994387,
12
+ -0.047281905216520856,
13
+ 106.73199000287599
14
+ ],
15
+ [
16
+ 0.0747101104018069,
17
+ -0.9287770749837014,
18
+ -0.16236864674663903,
19
+ 115.80117022013123
20
+ ],
21
+ [
22
+ 0.0,
23
+ 0.0,
24
+ 0.0,
25
+ 1.0
26
+ ]
27
+ ]
28
+ }
data/extra_parcellation/README.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Extra Parcellation Data
2
+
3
+ Provides fine-grained subregion labels alongside the primary Allen atlas meshes.
4
+
5
+ ## Setup (recommended)
6
+
7
+ Run the setup script to build the combined atlas automatically:
8
+
9
+ ```bash
10
+ python scripts/setup_extra_parcellation.py
11
+ ```
12
+
13
+ This downloads and merges three complementary MNI-space atlases via nilearn:
14
+
15
+ | Layer | Source | Regions | Role |
16
+ |-------|--------|---------|------|
17
+ | 1 | Harvard-Oxford Cortical | 48 | Broad cortical coverage |
18
+ | 2 | Harvard-Oxford Subcortical | 17 | Thalamus, putamen, caudate, hippocampus, amygdala, etc. |
19
+ | 3 | **Julich-Brain** (highest priority) | 62 | Cytoarchitectonic: motor BA4a/4p, somatosensory BA1-3, visual V1-V5, auditory, Broca's, hippocampal subfields |
20
+
21
+ **Total: 127 labeled regions**, Julich-Brain labels override coarser Harvard-Oxford
22
+ where available.
23
+
24
+ Output files (auto-detected by flow mode):
25
+ - `combined_atlas.nii.gz` — NIfTI volume (~700KB)
26
+ - `combined_atlas_labels.json` — label ID to name map
27
+
28
+ ## How it works
29
+
30
+ The combined atlas is in MNI152 1mm space. The Allen atlas scene space is
31
+ approximately MNI-aligned, so probe coordinates can be used directly for lookups.
32
+ When the exact voxel is unlabeled, a nearby-search fallback finds the closest
33
+ labeled region within a few mm.
34
+
35
+ When both the primary Allen mesh and the extra parcellation cover a probe position,
36
+ both are highlighted: the Allen mesh in red, the extra parcellation subregion in
37
+ orange. The subregion name is included in the LLM context as additional detail.
38
+
39
+ ## Custom atlas
40
+
41
+ You can also provide any NIfTI atlas in MNI space:
42
+
43
+ ```bash
44
+ python examples/flow_probe_example.py \
45
+ --extra-parcellation my_atlas.nii.gz \
46
+ --extra-parcellation-labels my_atlas_labels.json
47
+ ```
48
+
49
+ ## Requirements
50
+
51
+ ```bash
52
+ pip install nilearn nibabel scikit-image
53
+ ```
data/extra_parcellation/combined_atlas.nii.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:68b7dbb0b9949e0e3021d60db9d6f1499615560cba955e0a2f6ab2e41962d25a
3
+ size 693799
data/extra_parcellation/combined_atlas_labels.json ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "1": "Frontal Pole",
3
+ "2": "Insular Cortex",
4
+ "3": "Superior Frontal Gyrus",
5
+ "4": "Middle Frontal Gyrus",
6
+ "5": "Inferior Frontal Gyrus, pars triangularis",
7
+ "6": "Inferior Frontal Gyrus, pars opercularis",
8
+ "7": "Precentral Gyrus",
9
+ "8": "Temporal Pole",
10
+ "9": "Superior Temporal Gyrus, anterior division",
11
+ "10": "Superior Temporal Gyrus, posterior division",
12
+ "11": "Middle Temporal Gyrus, anterior division",
13
+ "12": "Middle Temporal Gyrus, posterior division",
14
+ "13": "Middle Temporal Gyrus, temporooccipital part",
15
+ "14": "Inferior Temporal Gyrus, anterior division",
16
+ "15": "Inferior Temporal Gyrus, posterior division",
17
+ "16": "Inferior Temporal Gyrus, temporooccipital part",
18
+ "17": "Postcentral Gyrus",
19
+ "18": "Superior Parietal Lobule",
20
+ "19": "Supramarginal Gyrus, anterior division",
21
+ "20": "Supramarginal Gyrus, posterior division",
22
+ "21": "Angular Gyrus",
23
+ "22": "Lateral Occipital Cortex, superior division",
24
+ "23": "Lateral Occipital Cortex, inferior division",
25
+ "24": "Intracalcarine Cortex",
26
+ "25": "Frontal Medial Cortex",
27
+ "26": "Juxtapositional Lobule Cortex (formerly Supplementary Motor Cortex)",
28
+ "27": "Subcallosal Cortex",
29
+ "28": "Paracingulate Gyrus",
30
+ "29": "Cingulate Gyrus, anterior division",
31
+ "30": "Cingulate Gyrus, posterior division",
32
+ "31": "Precuneous Cortex",
33
+ "32": "Cuneal Cortex",
34
+ "33": "Frontal Orbital Cortex",
35
+ "34": "Parahippocampal Gyrus, anterior division",
36
+ "35": "Parahippocampal Gyrus, posterior division",
37
+ "36": "Lingual Gyrus",
38
+ "37": "Temporal Fusiform Cortex, anterior division",
39
+ "38": "Temporal Fusiform Cortex, posterior division",
40
+ "39": "Temporal Occipital Fusiform Cortex",
41
+ "40": "Occipital Fusiform Gyrus",
42
+ "41": "Frontal Opercular Cortex",
43
+ "42": "Central Opercular Cortex",
44
+ "43": "Parietal Opercular Cortex",
45
+ "44": "Planum Polare",
46
+ "45": "Heschl's Gyrus (includes H1 and H2)",
47
+ "46": "Planum Temporale",
48
+ "47": "Supracalcarine Cortex",
49
+ "48": "Occipital Pole",
50
+ "103": "Left Lateral Ventricle",
51
+ "104": "Left Thalamus",
52
+ "105": "Left Caudate",
53
+ "106": "Left Putamen",
54
+ "107": "Left Pallidum",
55
+ "108": "Brain-Stem",
56
+ "109": "Left Hippocampus",
57
+ "110": "Left Amygdala",
58
+ "111": "Left Accumbens",
59
+ "114": "Right Lateral Ventricle",
60
+ "115": "Right Thalamus",
61
+ "116": "Right Caudate",
62
+ "117": "Right Putamen",
63
+ "118": "Right Pallidum",
64
+ "119": "Right Hippocampus",
65
+ "120": "Right Amygdala",
66
+ "121": "Right Accumbens",
67
+ "201": "Amygdala_centromedial group",
68
+ "202": "Amygdala_laterobasal group",
69
+ "203": "Amygdala_superficial group",
70
+ "204": "Anterior intra-parietal sulcus hIP1",
71
+ "205": "Anterior intra-parietal sulcus hIP2",
72
+ "206": "Anterior intra-parietal sulcus hIP3",
73
+ "207": "Broca's area BA44",
74
+ "208": "Broca's area BA45",
75
+ "209": "Hippocampus cornu ammonis",
76
+ "210": "Hippocampus dentate gyrus",
77
+ "211": "Hippocampus entorhinal cortex",
78
+ "212": "Hippocampus hippocampal-amygdaloid transition area",
79
+ "213": "Hippocampus subiculum",
80
+ "214": "Inferior parietal lobule PF",
81
+ "215": "Inferior parietal lobule PFcm",
82
+ "216": "Inferior parietal lobule PFm",
83
+ "217": "Inferior parietal lobule PFop",
84
+ "218": "Inferior parietal lobule PFt",
85
+ "219": "Inferior parietal lobule PGp",
86
+ "220": "Inferior parietal lobule Pga",
87
+ "221": "Insula Id1",
88
+ "222": "Insula Ig1",
89
+ "223": "Insula Ig2",
90
+ "224": "Lateral geniculate body",
91
+ "225": "Mamillary body",
92
+ "226": "Medial geniculate body",
93
+ "227": "Premotor cortex BA6",
94
+ "228": "Primary auditory cortex TE1.0",
95
+ "229": "Primary auditory cortex TE1.1",
96
+ "230": "Primary auditory cortex TE1.2",
97
+ "231": "Primary motor cortex BA4a",
98
+ "232": "Primary motor cortex BA4p",
99
+ "233": "Primary somatosensory cortex BA1",
100
+ "234": "Primary somatosensory cortex BA2",
101
+ "235": "Primary somatosensory cortex BA3a",
102
+ "236": "Primary somatosensory cortex BA3b",
103
+ "237": "Secondary somatosensory cortex / Parietal operculum OP1",
104
+ "238": "Secondary somatosensory cortex / Parietal operculum OP2",
105
+ "239": "Secondary somatosensory cortex / Parietal operculum OP3",
106
+ "240": "Secondary somatosensory cortex / Parietal operculum OP4",
107
+ "241": "Superior parietal lobule 5Ci",
108
+ "242": "Superior parietal lobule 5L",
109
+ "243": "Superior parietal lobule 5M",
110
+ "244": "Superior parietal lobule 7A",
111
+ "245": "Superior parietal lobule 7M",
112
+ "246": "Superior parietal lobule 7P",
113
+ "247": "Superior parietal lobule 7PC",
114
+ "248": "Visual cortex V1 BA17",
115
+ "249": "Visual cortex V2 BA18",
116
+ "250": "Visual cortex V3V",
117
+ "251": "Visual cortex V4",
118
+ "252": "Visual cortex V5",
119
+ "253": "WM: Acoustic radiation",
120
+ "254": "WM: Callosal body",
121
+ "255": "WM: Cingulum",
122
+ "256": "WM: Corticospinal tract",
123
+ "257": "WM: Fornix",
124
+ "258": "WM: Inferior occipito-frontal fascicle",
125
+ "259": "WM: Optic radiation",
126
+ "260": "WM: Superior longitudinal fascicle",
127
+ "261": "WM: Superior occipito-frontal fascicle",
128
+ "262": "WM: Uncinate fascicle"
129
+ }
data/label_grid_cache.keys.json ADDED
@@ -0,0 +1 @@
 
 
1
+ ["", "10159", "10292", "10306", "10307", "10308", "10311", "10313", "10327", "10329", "10330", "10331", "10332", "10333", "10334", "10335", "10336", "10337", "10338", "10339", "10342", "10343", "10344", "10346", "10349", "10361", "10383", "10384", "10389", "10390", "10465", "10466", "10467", "10595", "10596", "10597", "10598", "10599", "10600", "10602", "10648", "10649", "10650", "10651", "10653", "10654", "10655", "10656", "10657", "10658", "10659", "10660", "10661", "10662", "10669", "12114", "12115", "12116", "12117", "12118", "12119", "12121", "12122", "12123", "12124", "12125", "12126", "12127", "12132", "12133", "12136", "12137", "12138", "12140", "12142", "12143", "12144", "12145", "12146", "12147", "12148", "12149", "12150", "12151", "12152", "12153", "12154", "12155", "12156", "12157", "12158", "12159", "12160", "12162", "12163", "12164", "12165", "12166", "12170", "12171", "12173", "12174", "12176", "12177", "12178", "12179", "12181", "12195", "12291", "12292", "12305", "12330", "12354", "12369", "12384", "12390", "12405", "12416", "12535", "12538", "12805", "12826", "13229", "13230", "146034872", "146034876", "146034884", "146034888", "266441657"]
data/label_grid_cache.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ab71af20799b4cefb9986791337fcb609105b4653141cde9285d486cbfaa4791
3
+ size 101223
data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_entropy.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2dc61d9b05f915dad3e8ade5e5d9e5f1df27983cbeaf34c0232010ab7e0008d7
3
+ size 7812500
data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mean_xyz3.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:34525f6bf4a68ac06f87801aa180b57cfea10c8c8559ea67d23d66ea54865109
3
+ size 23437500
data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_meta.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"grid":125,"axis_min":[-69.5,-104.5625,-54.25],"axis_max":[70.0625,76.09375,81.8125],"files":{"mean_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mean_xyz3.bin","pi":"mdn_particles_rdcim_teacher_edge_hq_grid125_pi_xyz6.bin","entropy":"mdn_particles_rdcim_teacher_edge_hq_grid125_entropy.bin","mu1_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu1_xyz3.bin","mu2_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu2_xyz3.bin","mu3_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu3_xyz3.bin","mu4_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu4_xyz3.bin","mu5_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu5_xyz3.bin","mu6_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu6_xyz3.bin"},"dtype":"float32","layout":"C-order (x,y,z,3) for *_xyz3; (x,y,z,K) for pi_xyzK; (x,y,z) for entropy","endianness":"little","mixture_mean_field":true,"model":{"quality":"hq","components":6},"mdn_source":"particles/rdcim_teacher_edge","bounds_mode":"full_brain","mode":"field","notes":"Trained on teacher segments (XYZ→ΔS) with optional rDCM conditioning and ROI embeddings","training_points_npy":"mdn_particles_rdcim_teacher_edge_hq_training_points.npy"}
data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu1_xyz3.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:44c45d1ccd4c2afb936d7a4622493b1d363df9664f6ce53cb26cd5bd892b79e8
3
+ size 23437500
data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu2_xyz3.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2f1b9505a5ae611528425d57b09387e35dd921c20377a3b232a4267133b19baf
3
+ size 23437500
data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu3_xyz3.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53b0999649dfccf31f8726695c4d5a738009f408389a16954144be89d2a58c87
3
+ size 23437500
data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu4_xyz3.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:afd81e6a49823e2ddb42f8e4256f2bf58f268f5fbf83d95fbf0dc0f0eedef559
3
+ size 23437500
data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu5_xyz3.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:68c1f5eceb934435f58ea77c3b623abb114256cfc23affaa2dba1a202139a4aa
3
+ size 23437500
data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu6_xyz3.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9827f9f7a923994c84e1a44773fc2fe88dd91fd7905e718083dc752d24a31f4d
3
+ size 23437500
data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_pi_xyz6.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ffb8a88b783e3a9aefeb8b7c14cd925769fcd3b4ef7d9ca029e4a8f71cd25265
3
+ size 46875000
data/mdn/mdn_particles_rdcim_teacher_edge_hq_training_points.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f4ac819327e772ab01ed85b81150d2a28111df1ba4ebd9b2b3af200a011849b
3
+ size 14400128
data/meshes/.keep ADDED
@@ -0,0 +1 @@
 
 
1
+ placeholder so code paths that check data/meshes exist keep working; actual OBJ meshes are in data/meshes_obj.
data/meshes_obj/10153.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:03245194c13e6081c41f81bfafb018f9aa819af57642f1729d6830db236912cb
3
+ size 9316827
data/meshes_obj/10154.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0026eafd072ba11e02f3f4eab7f768ebcf89f32f5a0144fa38d8774576a1baff
3
+ size 13523828
data/meshes_obj/10155.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:89e07d4dc266bd9c69404da5b1ce8a1ea23dd2a41560e30f350b3c49a23e4c65
3
+ size 13523828
data/meshes_obj/10156.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8dd907bcc3ed5f654395b9b2bec702f2f00355d6e7a9d208e18e01766f2782eb
3
+ size 12090622
data/meshes_obj/10157.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c1432ccc092ed2e7b1495e506a11179420fcfb0919519824bb35329ee8171002
3
+ size 3291242
data/meshes_obj/10158.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:be09e3ec39b5cacb24af0e9f7d8e69086fd35853899daea7729bf7f048bb5c6d
3
+ size 1992516
data/meshes_obj/10159.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aa7c48344b0af6f41f069450f319e9c360ef8b45fe8f36dded24cb45e7ba0aef
3
+ size 149484
data/meshes_obj/10292.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2c4b4771c184c4512cfadef75b152e0bcbe9d816a62102d7b93349c967d0d955
3
+ size 112083
data/meshes_obj/10306.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aef8de5471788784415bdd77b44e5c532cabc79f5fc3f3c4be37e9dd6c5222e8
3
+ size 112083
data/meshes_obj/10307.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:25a071fd3a955681fcd64f6d28fd917c921e9558b792f622649f93ebace11aa6
3
+ size 19205
data/meshes_obj/10308.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:83c6c01f3d02a6980403f340692930dbe47339c03784b4c6a41d6cc8bcf45dd2
3
+ size 28542
data/meshes_obj/10311.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6e94088b6a00b31e0077ecb75f28d21621f8b8f4ae1a033b76756713b66737fe
3
+ size 57711
data/meshes_obj/10313.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:22a69c74d7c982a8372ac63c49df436bf8bc0fc1edb401710c4939c85fd9866f
3
+ size 48263
data/meshes_obj/10327.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:61602d7e56749544c6773467d7b8af22aa7788de7640de8c0b18e6fc9630239a
3
+ size 48263
data/meshes_obj/10329.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f145efd0531e791a5f2059f89a4c649a83b12ffee4bd06648569cc36396bbd3c
3
+ size 17345
data/meshes_obj/10330.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8d96c21416298bac87134b1f41f762ac122854cd66a93f53bd97a8d833f7b99c
3
+ size 36168
data/meshes_obj/10331.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1e092fe926797259b4568d792ea0439daf36297ab5c9946a689892abe4c3b9aa
3
+ size 1853173
data/meshes_obj/10332.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c5030b54f80cad2316a5cc57db2649ac44e38d31d61677bda1710ebb29e1418f
3
+ size 1027691
data/meshes_obj/10333.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:df7e6a6ae97116b644f5090984b56ddcf5da574b5387c22bb8c554f9fff089ce
3
+ size 949235
data/meshes_obj/10334.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a16d8d4d1fcb2cc0ac4be610e5f200c6efd910b3de982492432f563c39f6c2b9
3
+ size 498360
data/meshes_obj/10335.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6f4c64f991bb22248463cf2415ab33c6cf89235bb85fa26425672958c6362708
3
+ size 225016
data/meshes_obj/10336.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:88b46eb320fe5c91aa1da562277d436caaabace45b0547a1b0d71e13cfa86d81
3
+ size 157144
data/meshes_obj/10337.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f60b0f68b013cfca7c50fc2afd63db26e64098d8e6476990d7cbc339613d68eb
3
+ size 162134
data/meshes_obj/10338.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a913a6bd57ddc20209c2a7d0c873705c234eb0f116ae0dfbb7fa88a7074d017
3
+ size 374785
data/meshes_obj/10339.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0521f9f55fd1c10d2d9560fff80cb2385a6a9fab003bea1c524e4db1a154bcbb
3
+ size 74905
data/meshes_obj/10342.obj ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0864087632dbee936a66dd7d0ab48f208db7fc57118bdef6a1b37e0dbe38b162
3
+ size 142593