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.
- {yabplot-0.4.0/yabplot.egg-info → yabplot-0.5.0}/PKG-INFO +42 -29
- {yabplot-0.4.0 → yabplot-0.5.0}/README.md +40 -28
- {yabplot-0.4.0 → yabplot-0.5.0}/pyproject.toml +2 -1
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot/__init__.py +3 -2
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot/atlas_builder.py +3 -3
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot/mesh.py +125 -24
- yabplot-0.5.0/yabplot/plotting.py +1458 -0
- yabplot-0.5.0/yabplot/scene.py +336 -0
- {yabplot-0.4.0 → yabplot-0.5.0/yabplot.egg-info}/PKG-INFO +42 -29
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot.egg-info/requires.txt +1 -0
- yabplot-0.4.0/yabplot/plotting.py +0 -726
- yabplot-0.4.0/yabplot/scene.py +0 -184
- {yabplot-0.4.0 → yabplot-0.5.0}/LICENSE +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/MANIFEST.in +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/setup.cfg +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/tests/test_smoke.py +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot/data/__init__.py +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot/data/registry.txt +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot/projection.py +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot/utils.py +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot/wrappers.py +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot.egg-info/SOURCES.txt +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot.egg-info/dependency_links.txt +0 -0
- {yabplot-0.4.0 → yabplot-0.5.0}/yabplot.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yabplot
|
|
3
|
-
Version: 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
|
[](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml)
|
|
32
33
|
[](https://doi.org/10.5281/zenodo.18237144)
|
|
33
34
|
|
|
34
|
-
**yabplot** is a Python library for creating
|
|
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
|
-
* **
|
|
41
|
-
* **
|
|
42
|
-
* **
|
|
43
|
-
* **custom atlases:**
|
|
44
|
-
* **
|
|
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=[
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
#
|
|
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=[-
|
|
103
|
-
views=['left_lateral', 'left_medial']
|
|
117
|
+
ax = yab.plot_vertexwise(lh_mesh, rh_mesh, cmap='viridis', vminmax=[-10, 10],
|
|
118
|
+
views=['left_lateral', 'left_medial'])
|
|
104
119
|
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-

|
|
116
129
|
|
|
117
130
|
## acknowledgements
|
|
118
131
|
|
|
@@ -7,17 +7,18 @@
|
|
|
7
7
|
[](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml)
|
|
8
8
|
[](https://doi.org/10.5281/zenodo.18237144)
|
|
9
9
|
|
|
10
|
-
**yabplot** is a Python library for creating
|
|
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
|
-
* **
|
|
17
|
-
* **
|
|
18
|
-
* **
|
|
19
|
-
* **custom atlases:**
|
|
20
|
-
* **
|
|
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=[
|
|
60
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
#
|
|
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=[-
|
|
79
|
-
views=['left_lateral', 'left_medial']
|
|
92
|
+
ax = yab.plot_vertexwise(lh_mesh, rh_mesh, cmap='viridis', vminmax=[-10, 10],
|
|
93
|
+
views=['left_lateral', 'left_medial'])
|
|
80
94
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-

|
|
92
104
|
|
|
93
105
|
## acknowledgements
|
|
94
106
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "yabplot"
|
|
3
|
-
version = "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
|
-
|
|
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
|
-
"""
|
|
209
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|