yabplot 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yabplot
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: yet another brain plot
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
8
  Requires-Dist: cmocean>=4.0.3
9
9
  Requires-Dist: ipywidgets>=8.1.8
10
+ Requires-Dist: matplotlib>=3.10.7
10
11
  Requires-Dist: nibabel>=5.3.2
11
12
  Requires-Dist: pandas>=2.3.3
12
13
  Requires-Dist: pooch>=1.8.2
@@ -31,17 +32,18 @@ Dynamic: license-file
31
32
  [![Tests](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml/badge.svg)](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml)
32
33
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.18237144.svg)](https://doi.org/10.5281/zenodo.18237144)
33
34
 
34
- **yabplot** is a Python library for creating beautiful, publication-quality 3D brain visualizations. it supports plotting cortical regions (+ vertexwise), subcortical structures, and white matter bundles.
35
+ **yabplot** is a Python library for creating publication-quality 3D brain visualizations. it provides a unified interface for cortical regions, subcortical structures, white matter bundles, connectomes, voxel-wise maps, and vertex-wise maps.
35
36
 
36
- the idea is simple. while there are already amazing visualization tools available, they often focus on specific domains—using one tool for white matter tracts and another for cortical surfaces inevitably leads to inconsistent styles. i wanted a unified, simple-to-use tool that enables me (and hopefully others) to perform most brain visualizations in a single place. recognizing that neuroscience evolves daily, i designed **yabplot** to be modular: it supports standard pre-packaged atlases out of the box, but easily accepts any custom parcellation or tractography dataset you might need.
37
+ the idea is simple. while there are already amazing visualization tools available, they often focus on specific domains—using one tool for white matter tracts and another for cortical surfaces inevitably leads to inconsistent styles. i wanted a unified, simple-to-use tool that enables me (and hopefully others) to perform most brain visualizations in a single place. recognizing that neuroscience evolves daily, i designed **yabplot** to be modular: it supports standard pre-packaged atlases out of the box, but easily accepts any custom parcellation or tractography dataset you might need. moreover, it enables to plot volumetric data either voxel-wise or by projecting the data to cortical surface or white matter tracts.
37
38
 
38
39
  ## features
39
40
 
40
- * **pre-existing atlases:** access many commonly used atlases (schaefer2018, brainnetome, aparc, aseg, musus100, xtract, etc) on demand.
41
- * **vertexwise plotting:** project volume (.nii) to cortical surface and plot.
42
- * **simple to use:** plug-n-play functions for cortex, subcortex, and tracts with a unified API.
43
- * **custom atlases:** easily use your own parcellations, segmentations (.nii/.gii), or tractograms (.trk).
44
- * **flexible inputs:** accepts data as dictionaries (for partial mapping) or arrays (for strict mapping).
41
+ * **unified plotting API:** plot cortical regions, vertex-wise maps, subcortical structures, voxel-wise maps, white-matter tracts, and connectomes with a consistent interface.
42
+ * **pre-packaged resources:** access commonly used atlases and meshes on demand, including schaefer, brainnetome, aparc, aseg, musus100, and xtract atlases.
43
+ * **flexible data mapping:** pass data as arrays for strict ordering or dictionaries for partial/name-based mapping.
44
+ * **custom atlases:** build and use custom cortical parcellations, subcortical segmentations, and tractography datasets.
45
+ * **volume projection:** project nifti images to cortical surfaces or tractograms for vertex-wise and tractometry visualizations, or plot the volumes voxel-wise.
46
+ * **publication-oriented output:** generate static figures, saved images, and interactive 3D views.
45
47
 
46
48
  ## installation
47
49
 
@@ -55,7 +57,7 @@ pip install yabplot # to install
55
57
  pip install yabplot --upgrade # to update
56
58
  ```
57
59
 
58
- dependencies: python 3.11 with ipywidgets, nibabel, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
60
+ dependencies: python 3.11 with ipywidgets, nibabel, matplotlib, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
59
61
 
60
62
  (Connectome Workbench (`wb_command`) is a requirement to create custom cortical atlases unless you plan to only use pre-loaded atlases; see more in docs)
61
63
 
@@ -77,42 +79,53 @@ print(yab.get_available_resources())
77
79
  # see the region names for a specific atlas
78
80
  print(yab.get_atlas_regions(atlas='aseg', category='subcortical'))
79
81
 
80
- # cortical surface regions
82
+ # plotting cortical surface regions
81
83
  atlas = 'aparc'
82
- dmap1 = {'L_lateraloccipital': 0.265, 'L_postcentral': 0.086, ...}
83
- yab.plot_cortical(data=dmap1, atlas=atlas, vminmax=[-0.1, 0.3],
84
- bmesh='midthickness', views=['left_lateral', 'left_medial'],
85
- figsize=(600, 300), cmap='viridis')
84
+ dmap1 = {'L_lateraloccipital': 0.265, 'L_postcentral': 0.086}
85
+ ax = yab.plot_cortical(data=dmap1, atlas=atlas, vminmax=[0.0, 0.3], cmap='viridis',
86
+ bmesh='midthickness', views=['left_lateral', 'left_medial'])
86
87
 
87
- # subcortical structures
88
+ # plotting subcortical regions
88
89
  atlas = 'aseg'
89
90
  regs = yab.get_atlas_regions(atlas=atlas, category='subcortical')
90
91
  data = np.arange(1, len(regs)+1)
91
- yab.plot_subcortical(data=data, atlas=atlas, vminmax=[2, 14],
92
- views=['left_lateral', 'superior', 'right_lateral'],
93
- bmesh_alpha=0.1, figsize=(600, 300), cmap='viridis')
92
+ ax = yab.plot_subcortical(data=data, atlas=atlas, vminmax=[2, 14],
93
+ views=['left_lateral', 'superior', 'right_lateral'],
94
+ bmesh_alpha=0.1, cmap='plasma')
94
95
 
95
- # vertex-wise surface
96
+ # plotting white matter tracts
97
+ atlas = 'xtract_medium'
98
+ regs = yab.get_atlas_regions(atlas=atlas, category='tracts')
99
+ data = {reg: np.sin(i) for i, reg in enumerate(regs)}
100
+ ax = yab.plot_tracts(data=data, atlas=atlas, style='matte', cmap='coolwarm',
101
+ views=['left_lateral', 'anterior', 'superior'], bmesh='pial')
102
+
103
+ # plotting connectome
104
+ data = np.random.rand(400, 400)
105
+ ax = yab.plot_connectome(matrix=data, atlas='schaefer400',
106
+ edge_cmap='dense', node_cmap='binary', edge_threshold='95%',
107
+ views=['left_lateral', 'superior', 'posterior']
108
+ )
109
+
110
+ # volume projection to cortical surface and vertexwise plotting
96
111
  threshold = 4
97
112
  b_lh_path, b_rh_path = yab.data.get_surface_paths('midthickness', 'bmesh')
98
113
  lh_data, rh_data = yab.project_vol2surf('path/to/yourdata.nii.gz', bmesh='midthickness')
99
114
  lh_data = np.where(lh_data > threshold, lh_data, np.nan)
100
115
  rh_data = np.where(rh_data > threshold, rh_data, np.nan)
101
116
  lh_mesh, rh_mesh = yab.load_vertexwise_mesh(b_lh_path, b_rh_path, lh_data, rh_data)
102
- yab.plot_vertexwise(lh_mesh, rh_mesh, cmap='viridis', vminmax=[-4, 9],
103
- views=['left_lateral', 'left_medial'], figsize=(600, 300))
117
+ ax = yab.plot_vertexwise(lh_mesh, rh_mesh, cmap='viridis', vminmax=[-10, 10],
118
+ views=['left_lateral', 'left_medial'])
104
119
 
105
- # white matter bundles
106
- atlas = 'xtract_tiny'
107
- regs = yab.get_atlas_regions(atlas=atlas, category='tracts')
108
- data = {reg: np.sin(i) for i, reg in enumerate(regs)}
109
- yab.plot_tracts(data=data, atlas=atlas, style='matte',
110
- views=['left_lateral', 'anterior', 'superior'], bmesh='pial',
111
- bmesh_alpha=0.1, figsize=(1600, 800), cmap='viridis')
120
+ # plotting volume voxel-wise
121
+ threshold = '99.5%'
122
+ nii_path = 'path/to/yourdata.nii.gz'
123
+ ax = yab.plot_voxelwise(nii_path, threshold='99%', cmap='Reds',
124
+ views=['left_lateral', 'superior', 'anterior'])
112
125
 
113
126
  ```
114
127
 
115
- ![examples](docs/assets/examples.png)
128
+ ![examples](docs/assets/overview.png)
116
129
 
117
130
  ## acknowledgements
118
131
 
@@ -7,17 +7,18 @@
7
7
  [![Tests](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml/badge.svg)](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml)
8
8
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.18237144.svg)](https://doi.org/10.5281/zenodo.18237144)
9
9
 
10
- **yabplot** is a Python library for creating beautiful, publication-quality 3D brain visualizations. it supports plotting cortical regions (+ vertexwise), subcortical structures, and white matter bundles.
10
+ **yabplot** is a Python library for creating publication-quality 3D brain visualizations. it provides a unified interface for cortical regions, subcortical structures, white matter bundles, connectomes, voxel-wise maps, and vertex-wise maps.
11
11
 
12
- the idea is simple. while there are already amazing visualization tools available, they often focus on specific domains—using one tool for white matter tracts and another for cortical surfaces inevitably leads to inconsistent styles. i wanted a unified, simple-to-use tool that enables me (and hopefully others) to perform most brain visualizations in a single place. recognizing that neuroscience evolves daily, i designed **yabplot** to be modular: it supports standard pre-packaged atlases out of the box, but easily accepts any custom parcellation or tractography dataset you might need.
12
+ the idea is simple. while there are already amazing visualization tools available, they often focus on specific domains—using one tool for white matter tracts and another for cortical surfaces inevitably leads to inconsistent styles. i wanted a unified, simple-to-use tool that enables me (and hopefully others) to perform most brain visualizations in a single place. recognizing that neuroscience evolves daily, i designed **yabplot** to be modular: it supports standard pre-packaged atlases out of the box, but easily accepts any custom parcellation or tractography dataset you might need. moreover, it enables to plot volumetric data either voxel-wise or by projecting the data to cortical surface or white matter tracts.
13
13
 
14
14
  ## features
15
15
 
16
- * **pre-existing atlases:** access many commonly used atlases (schaefer2018, brainnetome, aparc, aseg, musus100, xtract, etc) on demand.
17
- * **vertexwise plotting:** project volume (.nii) to cortical surface and plot.
18
- * **simple to use:** plug-n-play functions for cortex, subcortex, and tracts with a unified API.
19
- * **custom atlases:** easily use your own parcellations, segmentations (.nii/.gii), or tractograms (.trk).
20
- * **flexible inputs:** accepts data as dictionaries (for partial mapping) or arrays (for strict mapping).
16
+ * **unified plotting API:** plot cortical regions, vertex-wise maps, subcortical structures, voxel-wise maps, white-matter tracts, and connectomes with a consistent interface.
17
+ * **pre-packaged resources:** access commonly used atlases and meshes on demand, including schaefer, brainnetome, aparc, aseg, musus100, and xtract atlases.
18
+ * **flexible data mapping:** pass data as arrays for strict ordering or dictionaries for partial/name-based mapping.
19
+ * **custom atlases:** build and use custom cortical parcellations, subcortical segmentations, and tractography datasets.
20
+ * **volume projection:** project nifti images to cortical surfaces or tractograms for vertex-wise and tractometry visualizations, or plot the volumes voxel-wise.
21
+ * **publication-oriented output:** generate static figures, saved images, and interactive 3D views.
21
22
 
22
23
  ## installation
23
24
 
@@ -31,7 +32,7 @@ pip install yabplot # to install
31
32
  pip install yabplot --upgrade # to update
32
33
  ```
33
34
 
34
- dependencies: python 3.11 with ipywidgets, nibabel, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
35
+ dependencies: python 3.11 with ipywidgets, nibabel, matplotlib, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
35
36
 
36
37
  (Connectome Workbench (`wb_command`) is a requirement to create custom cortical atlases unless you plan to only use pre-loaded atlases; see more in docs)
37
38
 
@@ -53,42 +54,53 @@ print(yab.get_available_resources())
53
54
  # see the region names for a specific atlas
54
55
  print(yab.get_atlas_regions(atlas='aseg', category='subcortical'))
55
56
 
56
- # cortical surface regions
57
+ # plotting cortical surface regions
57
58
  atlas = 'aparc'
58
- dmap1 = {'L_lateraloccipital': 0.265, 'L_postcentral': 0.086, ...}
59
- yab.plot_cortical(data=dmap1, atlas=atlas, vminmax=[-0.1, 0.3],
60
- bmesh='midthickness', views=['left_lateral', 'left_medial'],
61
- figsize=(600, 300), cmap='viridis')
59
+ dmap1 = {'L_lateraloccipital': 0.265, 'L_postcentral': 0.086}
60
+ ax = yab.plot_cortical(data=dmap1, atlas=atlas, vminmax=[0.0, 0.3], cmap='viridis',
61
+ bmesh='midthickness', views=['left_lateral', 'left_medial'])
62
62
 
63
- # subcortical structures
63
+ # plotting subcortical regions
64
64
  atlas = 'aseg'
65
65
  regs = yab.get_atlas_regions(atlas=atlas, category='subcortical')
66
66
  data = np.arange(1, len(regs)+1)
67
- yab.plot_subcortical(data=data, atlas=atlas, vminmax=[2, 14],
68
- views=['left_lateral', 'superior', 'right_lateral'],
69
- bmesh_alpha=0.1, figsize=(600, 300), cmap='viridis')
67
+ ax = yab.plot_subcortical(data=data, atlas=atlas, vminmax=[2, 14],
68
+ views=['left_lateral', 'superior', 'right_lateral'],
69
+ bmesh_alpha=0.1, cmap='plasma')
70
70
 
71
- # vertex-wise surface
71
+ # plotting white matter tracts
72
+ atlas = 'xtract_medium'
73
+ regs = yab.get_atlas_regions(atlas=atlas, category='tracts')
74
+ data = {reg: np.sin(i) for i, reg in enumerate(regs)}
75
+ ax = yab.plot_tracts(data=data, atlas=atlas, style='matte', cmap='coolwarm',
76
+ views=['left_lateral', 'anterior', 'superior'], bmesh='pial')
77
+
78
+ # plotting connectome
79
+ data = np.random.rand(400, 400)
80
+ ax = yab.plot_connectome(matrix=data, atlas='schaefer400',
81
+ edge_cmap='dense', node_cmap='binary', edge_threshold='95%',
82
+ views=['left_lateral', 'superior', 'posterior']
83
+ )
84
+
85
+ # volume projection to cortical surface and vertexwise plotting
72
86
  threshold = 4
73
87
  b_lh_path, b_rh_path = yab.data.get_surface_paths('midthickness', 'bmesh')
74
88
  lh_data, rh_data = yab.project_vol2surf('path/to/yourdata.nii.gz', bmesh='midthickness')
75
89
  lh_data = np.where(lh_data > threshold, lh_data, np.nan)
76
90
  rh_data = np.where(rh_data > threshold, rh_data, np.nan)
77
91
  lh_mesh, rh_mesh = yab.load_vertexwise_mesh(b_lh_path, b_rh_path, lh_data, rh_data)
78
- yab.plot_vertexwise(lh_mesh, rh_mesh, cmap='viridis', vminmax=[-4, 9],
79
- views=['left_lateral', 'left_medial'], figsize=(600, 300))
92
+ ax = yab.plot_vertexwise(lh_mesh, rh_mesh, cmap='viridis', vminmax=[-10, 10],
93
+ views=['left_lateral', 'left_medial'])
80
94
 
81
- # white matter bundles
82
- atlas = 'xtract_tiny'
83
- regs = yab.get_atlas_regions(atlas=atlas, category='tracts')
84
- data = {reg: np.sin(i) for i, reg in enumerate(regs)}
85
- yab.plot_tracts(data=data, atlas=atlas, style='matte',
86
- views=['left_lateral', 'anterior', 'superior'], bmesh='pial',
87
- bmesh_alpha=0.1, figsize=(1600, 800), cmap='viridis')
95
+ # plotting volume voxel-wise
96
+ threshold = '99.5%'
97
+ nii_path = 'path/to/yourdata.nii.gz'
98
+ ax = yab.plot_voxelwise(nii_path, threshold='99%', cmap='Reds',
99
+ views=['left_lateral', 'superior', 'anterior'])
88
100
 
89
101
  ```
90
102
 
91
- ![examples](docs/assets/examples.png)
103
+ ![examples](docs/assets/overview.png)
92
104
 
93
105
  ## acknowledgements
94
106
 
@@ -1,12 +1,13 @@
1
1
  [project]
2
2
  name = "yabplot"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "yet another brain plot"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
8
  "cmocean>=4.0.3",
9
9
  "ipywidgets>=8.1.8",
10
+ "matplotlib>=3.10.7",
10
11
  "nibabel>=5.3.2",
11
12
  "pandas>=2.3.3",
12
13
  "pooch>=1.8.2",
@@ -2,7 +2,8 @@ from importlib.metadata import version, PackageNotFoundError
2
2
 
3
3
  from .plotting import (
4
4
  plot_cortical, plot_subcortical, plot_tracts,
5
- clear_tract_cache, plot_vertexwise
5
+ clear_cache, plot_vertexwise, plot_connectome,
6
+ plot_voxelwise
6
7
  )
7
8
  from .data import (
8
9
  get_available_resources, get_atlas_regions
@@ -11,7 +12,7 @@ from .atlas_builder import (
11
12
  build_cortical_atlas, build_subcortical_atlas
12
13
  )
13
14
  from .mesh import (
14
- load_vertexwise_mesh, make_cortical_mesh
15
+ load_vertexwise_mesh, make_cortical_mesh, load_nii_as_mesh
15
16
  )
16
17
  from .projection import (
17
18
  project_vol2surf, project_vol2tract, project_vol2tract_atlas
@@ -224,10 +224,10 @@ def qc_custom_cortical_atlas(atlas_dir, atlasname='atlas'):
224
224
  plot_file = os.path.join(qc_dir, f"{rid:03d}_{name}.png")
225
225
 
226
226
  try:
227
- plot_cortical(
227
+ ax = plot_cortical(
228
228
  data={name: 1},
229
229
  custom_atlas_path=atlas_dir,
230
- cmap='binary',vminmax=[0, 1],
230
+ cmap='binary', vminmax=[0, 1],
231
231
  export_path=plot_file
232
232
  )
233
233
  except Exception as e:
@@ -397,7 +397,7 @@ def qc_custom_subcortical_atlas(atlas_dir):
397
397
  plot_file = os.path.join(qc_dir, f"{region_name}.png")
398
398
 
399
399
  try:
400
- plot_subcortical(
400
+ ax = plot_subcortical(
401
401
  data={region_name: 1},
402
402
  custom_atlas_path=atlas_dir,
403
403
  cmap='binary', vminmax=[0, 1],
@@ -1,6 +1,12 @@
1
+ import warnings
2
+
3
+ import nibabel as nib
1
4
  import numpy as np
2
5
  import pyvista as pv
3
6
  import scipy.sparse as sp
7
+ from scipy.ndimage import gaussian_filter
8
+ from skimage import measure
9
+ from concurrent.futures import ThreadPoolExecutor
4
10
 
5
11
  def load_bmesh(bmesh):
6
12
  """
@@ -104,6 +110,76 @@ def load_vertexwise_mesh(lh_mesh_path, rh_mesh_path, lh_data, rh_data, scalar_na
104
110
  rh = make_cortical_mesh(*load_gii(rh_mesh_path), rh_data, scalar_name)
105
111
  return lh, rh
106
112
 
113
+ def load_nii_as_mesh(
114
+ nii_path,
115
+ threshold=0.5,
116
+ blur_sigma=1.5,
117
+ smooth_i=10,
118
+ smooth_f=0.1
119
+ ):
120
+ """
121
+ Build a surface mesh from a 3D NIfTI volume using marching cubes, with optional Gaussian blurring and mesh smoothing.
122
+
123
+ Parameters
124
+ ----------
125
+ nii_path : str
126
+ Absolute path to a NIfTI file representing a 3D volume. If 4D, only the first volume will be used.
127
+ threshold : float, optional
128
+ Threshold applied after optional blur. Voxels ``> threshold`` are kept.
129
+ blur_sigma : float, optional
130
+ Gaussian blur (voxel units) before thresholding.
131
+ smooth_i : int, optional
132
+ Number of PyVista smoothing iterations after surface extraction.
133
+ smooth_f : float, optional
134
+ Relaxation factor for mesh smoothing.
135
+
136
+ Returns
137
+ -------
138
+ mesh : pyvista.PolyData
139
+ The extracted and smoothed surface mesh ready for plotting.
140
+ """
141
+
142
+ img = nib.load(nii_path)
143
+ vol = img.get_fdata()
144
+
145
+ if vol.ndim > 3:
146
+ warnings.warn(
147
+ f"[WARNING] detected {vol.ndim}d nifti volume. using the first volume (index 0)."
148
+ )
149
+ vol = vol[..., 0]
150
+
151
+ vol = np.nan_to_num(vol, nan=0.0)
152
+
153
+ if blur_sigma and blur_sigma > 0:
154
+ vol = gaussian_filter(vol, sigma=float(blur_sigma))
155
+
156
+ mask = vol > float(threshold)
157
+
158
+ if not np.any(mask):
159
+ raise ValueError("Mask is empty after thresholding. Adjust threshold/blur_sigma.")
160
+
161
+ verts_vox, faces, _, _ = measure.marching_cubes(mask.astype(np.float32), level=0.5)
162
+ verts_world = nib.affines.apply_affine(img.affine, verts_vox)
163
+
164
+ faces_pv = np.hstack([
165
+ np.full((faces.shape[0], 1), 3, dtype=np.int64),
166
+ faces.astype(np.int64)
167
+ ]).ravel()
168
+ mesh = pv.PolyData(verts_world.astype(np.float32), faces_pv)
169
+
170
+ if smooth_i and smooth_i > 0:
171
+ mesh = mesh.smooth(n_iter=int(smooth_i), relaxation_factor=float(smooth_f))
172
+
173
+ if mesh.n_points == 0:
174
+ raise ValueError("Extracted mesh has no vertices. Check input mask and parameters.")
175
+
176
+ # fill topological holes in the extracted meshes
177
+ try:
178
+ mesh = mesh.fill_holes(1000)
179
+ except Exception as e:
180
+ warnings.warn(f"Mesh hole filling failed: {e}. Continuing with unfilled meshes.")
181
+
182
+ return mesh
107
183
 
108
184
  def map_values_to_surface(data, target_labels, lut_ids, dense_lut_names):
109
185
  """maps data to vertices."""
@@ -204,35 +280,60 @@ def apply_dilation(faces, data, iterations=4):
204
280
  return data_out
205
281
 
206
282
 
283
+ def get_smooth_masks_vectorized(faces, n_v, r_masks, iterations=4):
284
+ adj = get_adj(faces, n_v)
285
+ deg = np.array(adj.sum(axis=1)).flatten()
286
+ deg[deg == 0] = 1.0
287
+ mask = r_masks.astype(np.float64)
288
+ for _ in range(iterations):
289
+ mask = (mask + (adj.dot(mask) / deg[:, None])) / 2.0
290
+ return mask
291
+
207
292
  def get_puzzle_pieces(v, f, raw_vals):
208
- """carve out geometric pieces with slight overlap to prevent gaps."""
209
- pieces = []
293
+ """
294
+ Creates sharp boundaries without gaps by calculating smooth probability fields
295
+ and interpolating them onto a highly subdivided continuous mesh.
296
+ """
210
297
  valid_mask = ~np.isnan(raw_vals) & (raw_vals != 0.0)
211
298
  u_vals = np.unique(raw_vals[valid_mask])
299
+
300
+ # if no data, skip
301
+ if len(u_vals) == 0:
302
+ master = make_cortical_mesh(v, f, np.full(len(v), np.nan))
303
+ return pv.PolyData(), [master]
304
+
305
+ n_v = len(v)
306
+ n_k = len(u_vals)
307
+
308
+ # vectorized smoothing
309
+ r_masks = np.zeros((n_v, n_k + 1), dtype=np.float64)
310
+ r_masks[:, 0] = np.where(~valid_mask, 1.0, 0.0) # medial wall
311
+ for i, val in enumerate(u_vals):
312
+ r_masks[:, i+1] = np.where(raw_vals == val, 1.0, 0.0)
313
+
314
+ s_masks = get_smooth_masks_vectorized(f, n_v, r_masks, iterations=4)
315
+
212
316
  master = make_cortical_mesh(v, f, np.zeros_like(raw_vals))
213
-
214
- for val in u_vals:
215
- r_mask = np.where(raw_vals == val, 1.0, 0.0)
216
- s_mask = get_smooth_mask(f, r_mask, iterations=4)
217
- temp = master.copy()
218
- temp['Slice_Mask'] = s_mask
219
- # reduce search space
220
- patch = temp.threshold(0.01, scalars='Slice_Mask')
221
- if patch.n_points > 0:
222
- # use 0.48 (slightly expanded) for pieces to seal cracks
223
- piece = patch.clip_scalar(scalars='Slice_Mask', value=0.48, invert=False)
224
- if piece.n_points > 0:
225
- piece['Data'] = np.full(piece.n_points, val)
226
- pieces.append(piece)
317
+ master['Masks'] = s_masks
318
+
319
+ # subdivide to increase resolution (2 levels = 16x faces)
320
+ sub = master.subdivide(2, subfilter='linear')
321
+
322
+ # assign labels to high-res vertices
323
+ interp_masks = sub['Masks']
324
+ best_class = np.argmax(interp_masks, axis=1)
325
+ new_data = np.full(sub.n_points, np.nan)
326
+ valid_idx = best_class > 0
327
+
328
+ # map the argmax indices back to their original scalar values
329
+ new_data[valid_idx] = u_vals[best_class[valid_idx] - 1]
330
+ sub['Data'] = new_data
331
+
332
+ # clean up multi-component array to save memory
333
+ del sub.point_data['Masks']
227
334
 
228
- # slice base brain
229
- all_mask = np.where(valid_mask, 1.0, 0.0)
230
- s_all = get_smooth_mask(f, all_mask, iterations=4)
231
- master['Slice_Mask'] = s_all
232
- # use 0.52 (slightly contracted) for the hole to ensure colored pieces cover the edge
233
- base_p = master.clip_scalar(scalars='Slice_Mask', value=0.52, invert=True)
234
- if base_p.n_points > 0:
235
- base_p['Data'] = np.full(base_p.n_points, np.nan)
335
+ base_p = pv.PolyData()
336
+ pieces = [sub]
236
337
 
237
338
  return base_p, pieces
238
339