yabplot 0.1.4__tar.gz → 0.2.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.1.4/yabplot.egg-info → yabplot-0.2.0}/PKG-INFO +39 -27
- {yabplot-0.1.4 → yabplot-0.2.0}/README.md +38 -26
- {yabplot-0.1.4 → yabplot-0.2.0}/pyproject.toml +2 -2
- {yabplot-0.1.4 → yabplot-0.2.0}/tests/test_smoke.py +3 -3
- {yabplot-0.1.4 → yabplot-0.2.0}/yabplot/__init__.py +2 -1
- yabplot-0.2.0/yabplot/atlas_builder.py +404 -0
- {yabplot-0.1.4 → yabplot-0.2.0}/yabplot/data/__init__.py +48 -1
- {yabplot-0.1.4 → yabplot-0.2.0}/yabplot/data/registry.txt +10 -2
- {yabplot-0.1.4 → yabplot-0.2.0}/yabplot/plotting.py +79 -56
- {yabplot-0.1.4 → yabplot-0.2.0}/yabplot/scene.py +17 -8
- {yabplot-0.1.4 → yabplot-0.2.0}/yabplot/utils.py +89 -1
- yabplot-0.2.0/yabplot/wrappers.py +36 -0
- {yabplot-0.1.4 → yabplot-0.2.0/yabplot.egg-info}/PKG-INFO +39 -27
- {yabplot-0.1.4 → yabplot-0.2.0}/yabplot.egg-info/SOURCES.txt +2 -0
- {yabplot-0.1.4 → yabplot-0.2.0}/LICENSE +0 -0
- {yabplot-0.1.4 → yabplot-0.2.0}/MANIFEST.in +0 -0
- {yabplot-0.1.4 → yabplot-0.2.0}/setup.cfg +0 -0
- {yabplot-0.1.4 → yabplot-0.2.0}/yabplot.egg-info/dependency_links.txt +0 -0
- {yabplot-0.1.4 → yabplot-0.2.0}/yabplot.egg-info/requires.txt +0 -0
- {yabplot-0.1.4 → yabplot-0.2.0}/yabplot.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yabplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: yet another brain plot
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -55,6 +55,9 @@ pip install yabplot --upgrade # to update
|
|
|
55
55
|
|
|
56
56
|
dependencies: python 3.11 with ipywidgets, nibabel, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
|
|
57
57
|
|
|
58
|
+
(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)
|
|
59
|
+
|
|
60
|
+
|
|
58
61
|
## quick start
|
|
59
62
|
|
|
60
63
|
please refer to the [documentation](https://teanijarv.github.io/yabplot/) for more comprehensive guides.
|
|
@@ -63,32 +66,41 @@ please refer to the [documentation](https://teanijarv.github.io/yabplot/) for mo
|
|
|
63
66
|
import yabplot as yab
|
|
64
67
|
import numpy as np
|
|
65
68
|
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# see
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
yab.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
69
|
+
# check that you have the latest version
|
|
70
|
+
print(yab.__version__)
|
|
71
|
+
|
|
72
|
+
# see available atlases and brain meshes
|
|
73
|
+
print(yab.get_available_resources())
|
|
74
|
+
|
|
75
|
+
# see the region names for a specific atlas
|
|
76
|
+
print(yab.get_atlas_regions(atlas='aseg', category='subcortical'))
|
|
77
|
+
|
|
78
|
+
# cortical surfaces
|
|
79
|
+
atlas = 'aparc'
|
|
80
|
+
dmap1 = {'L_lateraloccipital': 0.265, 'L_postcentral': 0.086, ...}
|
|
81
|
+
dmap2 = {'L_fusiform': 0.218, 'L_supramarginal': 0.119, ...}
|
|
82
|
+
yab.plot_cortical(data=dmap1, atlas=atlas, vminmax=[-0.1, 0.3],
|
|
83
|
+
bmesh_type='midthickness', views=['left_lateral', 'left_medial'],
|
|
84
|
+
figsize=(600, 300), cmap='viridis', proc_vertices='sharp')
|
|
85
|
+
yab.plot_cortical(data=dmap2, atlas=atlas, vminmax=[-0.1, 0.3],
|
|
86
|
+
bmesh_type='swm', views=['left_lateral', 'left_medial'],
|
|
87
|
+
figsize=(1200, 600), cmap='viridis', proc_vertices='sharp')
|
|
88
|
+
|
|
89
|
+
# subcortical structures
|
|
90
|
+
atlas = 'aseg'
|
|
91
|
+
regs = yab.get_atlas_regions(atlas=atlas, category='subcortical')
|
|
92
|
+
data = np.arange(1, len(regs)+1)
|
|
93
|
+
yab.plot_subcortical(data=data, atlas=atlas, vminmax=[2, 14],
|
|
94
|
+
views=['left_lateral', 'superior', 'right_lateral'],
|
|
95
|
+
bmesh_alpha=0.1, figsize=(600, 300), cmap='viridis')
|
|
96
|
+
|
|
97
|
+
# white matter bundles
|
|
98
|
+
atlas = 'xtract_tiny'
|
|
99
|
+
regs = yab.get_atlas_regions(atlas=atlas, category='tracts')
|
|
100
|
+
data = {reg: np.sin(i) for i, reg in enumerate(regs)}
|
|
101
|
+
yab.plot_tracts(data=data, atlas=atlas, style='matte',
|
|
102
|
+
views=['left_lateral', 'anterior', 'superior'], bmesh_type='pial',
|
|
103
|
+
bmesh_alpha=0.1, figsize=(1600, 800), cmap='viridis')
|
|
92
104
|
|
|
93
105
|
```
|
|
94
106
|
|
|
@@ -32,6 +32,9 @@ pip install yabplot --upgrade # to update
|
|
|
32
32
|
|
|
33
33
|
dependencies: python 3.11 with ipywidgets, nibabel, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
|
|
34
34
|
|
|
35
|
+
(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)
|
|
36
|
+
|
|
37
|
+
|
|
35
38
|
## quick start
|
|
36
39
|
|
|
37
40
|
please refer to the [documentation](https://teanijarv.github.io/yabplot/) for more comprehensive guides.
|
|
@@ -40,32 +43,41 @@ please refer to the [documentation](https://teanijarv.github.io/yabplot/) for mo
|
|
|
40
43
|
import yabplot as yab
|
|
41
44
|
import numpy as np
|
|
42
45
|
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# see
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
yab.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
# check that you have the latest version
|
|
47
|
+
print(yab.__version__)
|
|
48
|
+
|
|
49
|
+
# see available atlases and brain meshes
|
|
50
|
+
print(yab.get_available_resources())
|
|
51
|
+
|
|
52
|
+
# see the region names for a specific atlas
|
|
53
|
+
print(yab.get_atlas_regions(atlas='aseg', category='subcortical'))
|
|
54
|
+
|
|
55
|
+
# cortical surfaces
|
|
56
|
+
atlas = 'aparc'
|
|
57
|
+
dmap1 = {'L_lateraloccipital': 0.265, 'L_postcentral': 0.086, ...}
|
|
58
|
+
dmap2 = {'L_fusiform': 0.218, 'L_supramarginal': 0.119, ...}
|
|
59
|
+
yab.plot_cortical(data=dmap1, atlas=atlas, vminmax=[-0.1, 0.3],
|
|
60
|
+
bmesh_type='midthickness', views=['left_lateral', 'left_medial'],
|
|
61
|
+
figsize=(600, 300), cmap='viridis', proc_vertices='sharp')
|
|
62
|
+
yab.plot_cortical(data=dmap2, atlas=atlas, vminmax=[-0.1, 0.3],
|
|
63
|
+
bmesh_type='swm', views=['left_lateral', 'left_medial'],
|
|
64
|
+
figsize=(1200, 600), cmap='viridis', proc_vertices='sharp')
|
|
65
|
+
|
|
66
|
+
# subcortical structures
|
|
67
|
+
atlas = 'aseg'
|
|
68
|
+
regs = yab.get_atlas_regions(atlas=atlas, category='subcortical')
|
|
69
|
+
data = np.arange(1, len(regs)+1)
|
|
70
|
+
yab.plot_subcortical(data=data, atlas=atlas, vminmax=[2, 14],
|
|
71
|
+
views=['left_lateral', 'superior', 'right_lateral'],
|
|
72
|
+
bmesh_alpha=0.1, figsize=(600, 300), cmap='viridis')
|
|
73
|
+
|
|
74
|
+
# white matter bundles
|
|
75
|
+
atlas = 'xtract_tiny'
|
|
76
|
+
regs = yab.get_atlas_regions(atlas=atlas, category='tracts')
|
|
77
|
+
data = {reg: np.sin(i) for i, reg in enumerate(regs)}
|
|
78
|
+
yab.plot_tracts(data=data, atlas=atlas, style='matte',
|
|
79
|
+
views=['left_lateral', 'anterior', 'superior'], bmesh_type='pial',
|
|
80
|
+
bmesh_alpha=0.1, figsize=(1600, 800), cmap='viridis')
|
|
69
81
|
|
|
70
82
|
```
|
|
71
83
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "yabplot"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "yet another brain plot"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -29,4 +29,4 @@ where = ["."]
|
|
|
29
29
|
include = ["yabplot*"]
|
|
30
30
|
|
|
31
31
|
[tool.setuptools.package-data]
|
|
32
|
-
"yabplot" = ["*.txt", "*.json"]
|
|
32
|
+
"yabplot" = ["*.txt", "*.json"]
|
|
@@ -23,16 +23,16 @@ def test_plot_cortical():
|
|
|
23
23
|
"""
|
|
24
24
|
Integration test: Downloads 'aparc' and plots it.
|
|
25
25
|
"""
|
|
26
|
-
yab.plot_cortical(atlas='aparc', display_type=
|
|
26
|
+
yab.plot_cortical(atlas='aparc', display_type=None)
|
|
27
27
|
|
|
28
28
|
def test_plot_subcortical():
|
|
29
29
|
"""
|
|
30
30
|
Integration test: Downloads 'aseg' and plots it.
|
|
31
31
|
"""
|
|
32
|
-
yab.plot_subcortical(atlas='aseg', display_type=
|
|
32
|
+
yab.plot_subcortical(atlas='aseg', display_type=None)
|
|
33
33
|
|
|
34
34
|
def test_plot_tracts():
|
|
35
35
|
"""
|
|
36
36
|
Integration test: Downloads 'xtract_tiny' and plots it.
|
|
37
37
|
"""
|
|
38
|
-
yab.plot_tracts(atlas='xtract_tiny', display_type=
|
|
38
|
+
yab.plot_tracts(atlas='xtract_tiny', display_type=None)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from importlib.metadata import version, PackageNotFoundError
|
|
2
2
|
|
|
3
3
|
from .plotting import plot_cortical, plot_subcortical, plot_tracts, clear_tract_cache
|
|
4
|
-
from .data import get_available_resources, get_atlas_regions
|
|
4
|
+
from .data import get_available_resources, get_atlas_regions, get_surface_paths
|
|
5
|
+
from .atlas_builder import build_cortical_atlas, build_subcortical_atlas
|
|
5
6
|
|
|
6
7
|
try:
|
|
7
8
|
__version__ = version("yabplot")
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import glob
|
|
3
|
+
import numpy as np
|
|
4
|
+
import nibabel as nib
|
|
5
|
+
import scipy.sparse as sp
|
|
6
|
+
import pyvista as pv
|
|
7
|
+
from skimage import measure
|
|
8
|
+
from .wrappers import run_wb_import, run_wb_projection
|
|
9
|
+
from .data import get_surface_paths
|
|
10
|
+
from .plotting import plot_cortical, plot_subcortical
|
|
11
|
+
|
|
12
|
+
### CORTICAL
|
|
13
|
+
|
|
14
|
+
def _build_adjacency(surf_path, n_vert=32492):
|
|
15
|
+
"""internal helper to build surface adjacency matrix."""
|
|
16
|
+
surf = nib.load(surf_path)
|
|
17
|
+
faces = surf.darrays[1].data.astype(int)
|
|
18
|
+
edges = np.vstack([faces[:, [0, 1]], faces[:, [1, 2]], faces[:, [2, 0]]])
|
|
19
|
+
row, col = np.concatenate([edges[:, 0], edges[:, 1]]), np.concatenate([edges[:, 1], edges[:, 0]])
|
|
20
|
+
return sp.coo_matrix((np.ones(len(row), dtype=int), (row, col)), shape=(n_vert, n_vert)).tocsr()
|
|
21
|
+
|
|
22
|
+
def build_cortical_atlas(nii_path, wb_txt_path, out_dir, include_list=None, exclude_list=None, atlasname='atlas'):
|
|
23
|
+
"""
|
|
24
|
+
builds a custom yabplot cortical atlas from a volumetric NIfTI file.
|
|
25
|
+
|
|
26
|
+
projects a volumetric NIfTI atlas to standard fsLR32k surfaces using connectome workbench,
|
|
27
|
+
cleans the medial wall, and applies majority-vote boundary smoothing to remove voxel artifacts.
|
|
28
|
+
|
|
29
|
+
parameters
|
|
30
|
+
----------
|
|
31
|
+
nii_path : str
|
|
32
|
+
absolute path to the 3D NIfTI volume of the atlas.
|
|
33
|
+
wb_txt_path : str
|
|
34
|
+
absolute path to the text file formatted specifically for connectome workbench.
|
|
35
|
+
out_dir : str
|
|
36
|
+
directory where the final .csv map and .txt LUT will be saved.
|
|
37
|
+
include_list : list of str, optional
|
|
38
|
+
keywords of regions to strictly include. all other regions are ignored.
|
|
39
|
+
exclude_list : list of str, optional
|
|
40
|
+
keywords of regions to strictly exclude. all other regions are kept.
|
|
41
|
+
atlasname : str, optional
|
|
42
|
+
prefix name for the output files. default is 'atlas'.
|
|
43
|
+
|
|
44
|
+
raises
|
|
45
|
+
------
|
|
46
|
+
ValueError
|
|
47
|
+
if both include_list and exclude_list are provided.
|
|
48
|
+
"""
|
|
49
|
+
if include_list and exclude_list:
|
|
50
|
+
raise ValueError("please provide either 'include_list' or 'exclude_list', not both.")
|
|
51
|
+
|
|
52
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
# define intermediate and output paths
|
|
55
|
+
labeled_nii = os.path.join(out_dir, 'temp_labeled.nii.gz')
|
|
56
|
+
lh_gii = os.path.join(out_dir, 'lh_temp.label.gii')
|
|
57
|
+
rh_gii = os.path.join(out_dir, 'rh_temp.label.gii')
|
|
58
|
+
out_csv = os.path.join(out_dir, f'{atlasname}.csv')
|
|
59
|
+
out_lut = os.path.join(out_dir, f'{atlasname}.txt')
|
|
60
|
+
|
|
61
|
+
# fetch standard fsLR32k surfaces and masks via yabplot data system
|
|
62
|
+
print("fetching standard surfaces...")
|
|
63
|
+
lh_mid, rh_mid = get_surface_paths('midthickness', 'bmesh')
|
|
64
|
+
lh_white, rh_white = get_surface_paths('white', 'bmesh')
|
|
65
|
+
lh_pial, rh_pial = get_surface_paths('pial', 'bmesh')
|
|
66
|
+
lh_mask_path, rh_mask_path = get_surface_paths('nomedialwall', 'label')
|
|
67
|
+
|
|
68
|
+
# run wb_command wrappers
|
|
69
|
+
print("running volume-to-surface projection...")
|
|
70
|
+
run_wb_import(nii_path, wb_txt_path, labeled_nii)
|
|
71
|
+
run_wb_projection(labeled_nii, lh_mid, lh_gii, lh_white, lh_pial)
|
|
72
|
+
run_wb_projection(labeled_nii, rh_mid, rh_gii, rh_white, rh_pial)
|
|
73
|
+
|
|
74
|
+
# extract LUT and apply include/exclude filtering
|
|
75
|
+
labels_dict = nib.load(lh_gii).labeltable.get_labels_as_dict()
|
|
76
|
+
valid_ids = []
|
|
77
|
+
lut_dict = {}
|
|
78
|
+
|
|
79
|
+
for rid, name in labels_dict.items():
|
|
80
|
+
if rid == 0 or name == '???': continue
|
|
81
|
+
|
|
82
|
+
# filter logic
|
|
83
|
+
if include_list:
|
|
84
|
+
if not any(inc in name for inc in include_list):
|
|
85
|
+
continue
|
|
86
|
+
elif exclude_list:
|
|
87
|
+
if any(exc in name for exc in exclude_list):
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
clean_name = name.replace(' ', '_').replace('/', '-')
|
|
91
|
+
np.random.seed(rid)
|
|
92
|
+
r, g, b = np.random.randint(50, 255, 3)
|
|
93
|
+
|
|
94
|
+
# store the string in the dictionary instead of writing to a file yet
|
|
95
|
+
lut_dict[rid] = f"{rid} {clean_name} {r} {g} {b} 0"
|
|
96
|
+
valid_ids.append(rid)
|
|
97
|
+
|
|
98
|
+
print(f"found {len(valid_ids)} initial cortical regions. mapping and cleaning...")
|
|
99
|
+
|
|
100
|
+
# merge LH and RH, then apply masks
|
|
101
|
+
data = np.concatenate([
|
|
102
|
+
nib.load(lh_gii).darrays[0].data.astype(int).flatten(),
|
|
103
|
+
nib.load(rh_gii).darrays[0].data.astype(int).flatten()
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
mask = np.concatenate([
|
|
107
|
+
nib.load(lh_mask_path).darrays[0].data.astype(int).flatten() != 0,
|
|
108
|
+
nib.load(rh_mask_path).darrays[0].data.astype(int).flatten() != 0
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
data[~mask] = 0
|
|
112
|
+
data[~np.isin(data, valid_ids)] = 0
|
|
113
|
+
|
|
114
|
+
# build adjacency and run hole-filling
|
|
115
|
+
print("building surface adjacency and filling holes...")
|
|
116
|
+
adj = sp.block_diag((_build_adjacency(lh_mid), _build_adjacency(rh_mid))).tocsr()
|
|
117
|
+
adj.setdiag(1)
|
|
118
|
+
n_vert = len(data)
|
|
119
|
+
|
|
120
|
+
for _ in range(20):
|
|
121
|
+
holes = (data == 0) & mask
|
|
122
|
+
if not np.any(holes): break
|
|
123
|
+
|
|
124
|
+
unique, inv = np.unique(data, return_inverse=True)
|
|
125
|
+
one_hot = sp.coo_matrix((np.ones(n_vert), (np.arange(n_vert), inv)), shape=(n_vert, len(unique))).tocsr()
|
|
126
|
+
votes = (adj @ one_hot).toarray()
|
|
127
|
+
|
|
128
|
+
zero_idx = np.where(unique == 0)[0][0]
|
|
129
|
+
votes[:, zero_idx] = 0
|
|
130
|
+
|
|
131
|
+
winner = np.argmax(votes, axis=1)
|
|
132
|
+
fill_vals = unique[winner]
|
|
133
|
+
data[holes] = fill_vals[holes]
|
|
134
|
+
|
|
135
|
+
# smooth final boundaries
|
|
136
|
+
print("smoothing boundaries...")
|
|
137
|
+
for _ in range(10):
|
|
138
|
+
unique, inv = np.unique(data, return_inverse=True)
|
|
139
|
+
one_hot = sp.coo_matrix((np.ones(n_vert), (np.arange(n_vert), inv)), shape=(n_vert, len(unique))).tocsr()
|
|
140
|
+
winner = np.argmax((adj @ one_hot).toarray(), axis=1)
|
|
141
|
+
data = unique[winner]
|
|
142
|
+
data[~mask] = 0
|
|
143
|
+
|
|
144
|
+
# save the final vertex map
|
|
145
|
+
np.savetxt(out_csv, data, fmt='%i')
|
|
146
|
+
|
|
147
|
+
# find out which regions actually survived the smoothing/masking
|
|
148
|
+
surviving_ids = np.unique(data)
|
|
149
|
+
|
|
150
|
+
# filter the LUT lines to only include survivors
|
|
151
|
+
final_lines = []
|
|
152
|
+
dropped_count = 0
|
|
153
|
+
|
|
154
|
+
for rid, line_str in lut_dict.items():
|
|
155
|
+
if rid in surviving_ids:
|
|
156
|
+
final_lines.append(line_str)
|
|
157
|
+
else:
|
|
158
|
+
region_name = line_str.split()[1]
|
|
159
|
+
print(f"[WARNING] {region_name} (id {rid}) lost during smoothing/masking. dropping from lut.")
|
|
160
|
+
dropped_count += 1
|
|
161
|
+
|
|
162
|
+
# write the perfectly clean file
|
|
163
|
+
with open(out_lut, 'w') as f:
|
|
164
|
+
f.write("\n".join(final_lines))
|
|
165
|
+
|
|
166
|
+
print(f"final polished atlas saved to: {out_dir}")
|
|
167
|
+
print(f"saved {len(final_lines)} regions ({dropped_count} empty regions dropped).")
|
|
168
|
+
|
|
169
|
+
# cleanup intermediate Workbench files to save space
|
|
170
|
+
for temp_file in [labeled_nii, lh_gii, rh_gii]:
|
|
171
|
+
if os.path.exists(temp_file):
|
|
172
|
+
os.remove(temp_file)
|
|
173
|
+
|
|
174
|
+
def qc_custom_cortical_atlas(atlas_dir, atlasname='atlas'):
|
|
175
|
+
"""
|
|
176
|
+
generates a quality control report for a custom cortical atlas.
|
|
177
|
+
|
|
178
|
+
reads the generated vertex map and lookup table, counts the vertices for each region,
|
|
179
|
+
saves a summary text file, and generates individual static plots for every region
|
|
180
|
+
to help identify mapping dropouts or anatomical bleed.
|
|
181
|
+
|
|
182
|
+
parameters
|
|
183
|
+
----------
|
|
184
|
+
atlas_dir : str
|
|
185
|
+
absolute path to the custom atlas directory containing the .csv and .txt files.
|
|
186
|
+
atlasname : str, optional
|
|
187
|
+
prefix name of the files to check. default is 'atlas'.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
csv_path = os.path.join(atlas_dir, f"{atlasname}.csv")
|
|
191
|
+
lut_path = os.path.join(atlas_dir, f"{atlasname}.txt")
|
|
192
|
+
qc_dir = os.path.join(atlas_dir, "qc_report")
|
|
193
|
+
|
|
194
|
+
os.makedirs(qc_dir, exist_ok=True)
|
|
195
|
+
|
|
196
|
+
# load the mapped data and the lookup table
|
|
197
|
+
labels = np.loadtxt(csv_path).astype(int)
|
|
198
|
+
|
|
199
|
+
regions = {}
|
|
200
|
+
with open(lut_path, 'r') as f:
|
|
201
|
+
for line in f:
|
|
202
|
+
parts = line.strip().split()
|
|
203
|
+
if len(parts) >= 2:
|
|
204
|
+
regions[int(parts[0])] = parts[1]
|
|
205
|
+
|
|
206
|
+
print(f"starting qc for {len(regions)} regions...\n")
|
|
207
|
+
|
|
208
|
+
report_path = os.path.join(qc_dir, "_vertex_counts.txt")
|
|
209
|
+
|
|
210
|
+
with open(report_path, 'w') as f_out:
|
|
211
|
+
f_out.write("region_name\tid\tvertex_count\n")
|
|
212
|
+
f_out.write("-" * 40 + "\n")
|
|
213
|
+
|
|
214
|
+
for rid, name in regions.items():
|
|
215
|
+
count = np.sum(labels == rid)
|
|
216
|
+
f_out.write(f"{name}\t{rid}\t{count}\n")
|
|
217
|
+
|
|
218
|
+
print(f"[{name}] id: {rid} | vertices: {count}")
|
|
219
|
+
|
|
220
|
+
if count == 0:
|
|
221
|
+
print(f"[WARNING] {name} is empty! skipping plot.")
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
plot_file = os.path.join(qc_dir, f"{rid:03d}_{name}.png")
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
plot_cortical(
|
|
228
|
+
data={name: 1},
|
|
229
|
+
custom_atlas_path=atlas_dir,
|
|
230
|
+
cmap='binary',vminmax=[0, 1],
|
|
231
|
+
export_path=plot_file
|
|
232
|
+
)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
print(f" -> failed to plot {name}: {e}")
|
|
235
|
+
|
|
236
|
+
print(f"\nqc complete! check the '{qc_dir}' folder for the report and images.")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
### SUBCORTICAL
|
|
240
|
+
|
|
241
|
+
def build_subcortical_atlas(nii_path, labels_dict, out_dir, include_list=None, exclude_list=None,
|
|
242
|
+
smooth_i=15, smooth_f=0.6):
|
|
243
|
+
"""
|
|
244
|
+
extracts 3D subcortical meshes from a volumetric nifti atlas.
|
|
245
|
+
|
|
246
|
+
uses the marching cubes algorithm to generate 3D surface meshes for specific
|
|
247
|
+
regions, applies laplacian smoothing to remove voxel artifacts, and saves them as .vtk files.
|
|
248
|
+
|
|
249
|
+
parameters
|
|
250
|
+
----------
|
|
251
|
+
nii_path : str
|
|
252
|
+
absolute path to the 3D nifti volume.
|
|
253
|
+
labels_dict : dict
|
|
254
|
+
dictionary mapping integer region IDs to string names (e.g., {1: 'thalamus_l'}).
|
|
255
|
+
out_dir : str
|
|
256
|
+
directory where the .vtk mesh files will be saved.
|
|
257
|
+
include_list : list of str, optional
|
|
258
|
+
keywords of regions to strictly include. all other regions are ignored.
|
|
259
|
+
exclude_list : list of str, optional
|
|
260
|
+
keywords of regions to strictly exclude. all other regions are kept.
|
|
261
|
+
smooth_i : int, optional
|
|
262
|
+
number of iterations for laplacian mesh smoothing. default is 15.
|
|
263
|
+
smooth_f : float, optional
|
|
264
|
+
relaxation factor for laplacian mesh smoothing (0.0 to 1.0). default is 0.6.
|
|
265
|
+
|
|
266
|
+
raises
|
|
267
|
+
------
|
|
268
|
+
ValueError
|
|
269
|
+
if both include_list and exclude_list are provided.
|
|
270
|
+
"""
|
|
271
|
+
if include_list and exclude_list:
|
|
272
|
+
raise ValueError("please provide either 'include_list' or 'exclude_list', not both.")
|
|
273
|
+
|
|
274
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
275
|
+
|
|
276
|
+
# apply the include/exclude filters to the provided dictionary
|
|
277
|
+
targets = {}
|
|
278
|
+
for rid, name in labels_dict.items():
|
|
279
|
+
if include_list:
|
|
280
|
+
if not any(inc in name for inc in include_list):
|
|
281
|
+
continue
|
|
282
|
+
elif exclude_list:
|
|
283
|
+
if any(exc in name for exc in exclude_list):
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
targets[rid] = name
|
|
287
|
+
|
|
288
|
+
print(f"filtered down to {len(targets)} subcortical regions to extract.")
|
|
289
|
+
|
|
290
|
+
# load the nifti volume and its affine matrix
|
|
291
|
+
img = nib.load(nii_path)
|
|
292
|
+
data = img.get_fdata()
|
|
293
|
+
affine = img.affine
|
|
294
|
+
|
|
295
|
+
# loop through targets, extract meshes, and save
|
|
296
|
+
for rid, name in targets.items():
|
|
297
|
+
# create a binary mask for just this region
|
|
298
|
+
mask = (data == rid).astype(np.uint8)
|
|
299
|
+
|
|
300
|
+
# skip if empty
|
|
301
|
+
if np.sum(mask) == 0:
|
|
302
|
+
print(f"[WARNING] {name} is empty in the volume!")
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
print(f"extracting: {name} (id {rid})...")
|
|
306
|
+
|
|
307
|
+
# run marching cubes to get raw vertices and faces
|
|
308
|
+
verts, faces, normals, values = measure.marching_cubes(mask, level=0.5)
|
|
309
|
+
|
|
310
|
+
# apply the nifti affine matrix cleanly using nibabel
|
|
311
|
+
verts_mni = nib.affines.apply_affine(affine, verts)
|
|
312
|
+
|
|
313
|
+
# format faces for pyvista: [n_points, p1, p2, p3, n_points, p1, p2, p3...]
|
|
314
|
+
faces_pv = np.column_stack((np.full(len(faces), 3), faces)).flatten()
|
|
315
|
+
|
|
316
|
+
# create the 3d pyvista mesh
|
|
317
|
+
mesh = pv.PolyData(verts_mni, faces_pv)
|
|
318
|
+
|
|
319
|
+
# apply laplacian smoothing to melt away the blocky voxel edges
|
|
320
|
+
mesh = mesh.smooth(n_iter=smooth_i, relaxation_factor=smooth_f)
|
|
321
|
+
mesh.compute_normals(inplace=True)
|
|
322
|
+
|
|
323
|
+
# we remove super small structures which would not be visible
|
|
324
|
+
if mesh.n_points < 4 or abs(mesh.volume) < 0.01:
|
|
325
|
+
print(f"[WARNING] {name} is too small to form a 3D mesh (volume: {abs(mesh.volume):.4f} mm³). dropping from atlas.")
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# save as a vtk file
|
|
329
|
+
out_file = os.path.join(out_dir, f"{name}.vtk")
|
|
330
|
+
mesh.save(out_file)
|
|
331
|
+
|
|
332
|
+
print(f"\nsubcortical atlas successfully saved to: {out_dir}")
|
|
333
|
+
|
|
334
|
+
def qc_custom_subcortical_atlas(atlas_dir):
|
|
335
|
+
"""
|
|
336
|
+
generates a quality control report for a custom subcortical atlas.
|
|
337
|
+
|
|
338
|
+
reads the generated .vtk meshes, calculates their geometric properties
|
|
339
|
+
(vertices, faces, volume), saves a summary text file, and generates
|
|
340
|
+
individual static plots for every region to help identify corrupt meshes
|
|
341
|
+
or anatomical artifacts.
|
|
342
|
+
|
|
343
|
+
parameters
|
|
344
|
+
----------
|
|
345
|
+
atlas_dir : str
|
|
346
|
+
absolute path to the custom atlas directory containing the .vtk files.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
qc_dir = os.path.join(atlas_dir, "qc_report")
|
|
350
|
+
os.makedirs(qc_dir, exist_ok=True)
|
|
351
|
+
|
|
352
|
+
# find all vtk files in the atlas directory
|
|
353
|
+
vtk_files = glob.glob(os.path.join(atlas_dir, "*.vtk"))
|
|
354
|
+
|
|
355
|
+
if not vtk_files:
|
|
356
|
+
print(f"no .vtk files found in {atlas_dir}. cannot run qc.")
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
print(f"starting qc for {len(vtk_files)} subcortical meshes...\n")
|
|
360
|
+
|
|
361
|
+
report_path = os.path.join(qc_dir, "_mesh_properties.txt")
|
|
362
|
+
|
|
363
|
+
with open(report_path, 'w') as f_out:
|
|
364
|
+
# header for our text report
|
|
365
|
+
f_out.write("region_name\tvertices\tfaces\tvolume_mm3\n")
|
|
366
|
+
f_out.write("-" * 55 + "\n")
|
|
367
|
+
|
|
368
|
+
for vtk_path in sorted(vtk_files):
|
|
369
|
+
filename = os.path.basename(vtk_path)
|
|
370
|
+
region_name = os.path.splitext(filename)[0]
|
|
371
|
+
|
|
372
|
+
# read mesh to extract physical properties
|
|
373
|
+
try:
|
|
374
|
+
mesh = pv.read(vtk_path)
|
|
375
|
+
n_verts = mesh.n_points
|
|
376
|
+
n_faces = mesh.n_cells
|
|
377
|
+
volume = mesh.volume
|
|
378
|
+
except Exception as e:
|
|
379
|
+
print(f" -> error reading {filename}: {e}")
|
|
380
|
+
f_out.write(f"{region_name}\tERROR\tERROR\tERROR\n")
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
f_out.write(f"{region_name}\t{n_verts}\t{n_faces}\t{volume:.2f}\n")
|
|
384
|
+
print(f"[{region_name}] vertices: {n_verts} | volume: {volume:.1f} mm³")
|
|
385
|
+
|
|
386
|
+
# check for empty or severely corrupted meshes
|
|
387
|
+
if n_verts == 0:
|
|
388
|
+
print(f"[WARNING] {region_name} mesh is empty! skipping plot.")
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
plot_file = os.path.join(qc_dir, f"{region_name}.png")
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
plot_subcortical(
|
|
395
|
+
data={region_name: 1},
|
|
396
|
+
custom_atlas_path=atlas_dir,
|
|
397
|
+
cmap='binary', vminmax=[0, 1],
|
|
398
|
+
nan_alpha=0.2,
|
|
399
|
+
export_path=plot_file
|
|
400
|
+
)
|
|
401
|
+
except Exception as e:
|
|
402
|
+
print(f" -> failed to plot {region_name}: {e}")
|
|
403
|
+
|
|
404
|
+
print(f"\nqc complete! check the '{qc_dir}' folder for the report and images.")
|
|
@@ -70,6 +70,52 @@ def get_available_resources(category=None):
|
|
|
70
70
|
|
|
71
71
|
return all_resources
|
|
72
72
|
|
|
73
|
+
def get_surface_paths(name, category):
|
|
74
|
+
"""
|
|
75
|
+
Fetches and returns the paths to the Left and Right hemisphere files
|
|
76
|
+
for a given surface resource (meshes or labels).
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
name : str
|
|
81
|
+
Name of the resource (e.g., 'midthickness', 'nomedialwall').
|
|
82
|
+
category : str
|
|
83
|
+
Must be 'bmesh' or 'label'.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
tuple
|
|
88
|
+
(lh_path, rh_path) containing absolute paths to the files.
|
|
89
|
+
"""
|
|
90
|
+
if category not in ['bmesh', 'label']:
|
|
91
|
+
raise ValueError("Category must be 'bmesh' or 'label' to fetch surface paths.")
|
|
92
|
+
|
|
93
|
+
# Download/unpack the zip and get the folder path
|
|
94
|
+
directory = _resolve_resource_path(name, category)
|
|
95
|
+
|
|
96
|
+
lh_path = None
|
|
97
|
+
rh_path = None
|
|
98
|
+
|
|
99
|
+
# Traverse the unzipped directory to find L and R files
|
|
100
|
+
for root, dirs, files in os.walk(directory):
|
|
101
|
+
# Ignore hidden folders like .git or __MACOSX
|
|
102
|
+
dirs[:] = [d for d in dirs if not d.startswith(('.', '__'))]
|
|
103
|
+
for file in files:
|
|
104
|
+
# Ignore hidden files
|
|
105
|
+
if file.startswith('.'):
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# Robust checking for Left and Right hemisphere indicators
|
|
109
|
+
if '.L.' in file or '_L_' in file or 'hemi-L' in file:
|
|
110
|
+
lh_path = os.path.join(root, file)
|
|
111
|
+
elif '.R.' in file or '_R_' in file or 'hemi-R' in file:
|
|
112
|
+
rh_path = os.path.join(root, file)
|
|
113
|
+
|
|
114
|
+
if not lh_path or not rh_path:
|
|
115
|
+
raise FileNotFoundError(f"Could not locate both Left and Right hemisphere files for '{name}' in {directory}")
|
|
116
|
+
|
|
117
|
+
return lh_path, rh_path
|
|
118
|
+
|
|
73
119
|
def get_atlas_regions(atlas, category, custom_atlas_path=None):
|
|
74
120
|
"""
|
|
75
121
|
Returns the list of region names for a given atlas in the specific order
|
|
@@ -191,7 +237,8 @@ def _resolve_resource_path(name, category, custom_path=None):
|
|
|
191
237
|
'cortical': 'Cortical parcellations (vertices)',
|
|
192
238
|
'subcortical': 'Subcortical segmentations (volumes)',
|
|
193
239
|
'tracts': 'White matter bundles (tracts)',
|
|
194
|
-
'bmesh': 'Brain meshes'
|
|
240
|
+
'bmesh': 'Brain meshes',
|
|
241
|
+
'label': 'Surface labels'
|
|
195
242
|
}.get(category, category)
|
|
196
243
|
|
|
197
244
|
raise ValueError(
|
|
@@ -5,11 +5,14 @@ cortical-schaefer_200.zip sha256:065ee05afa1881a8884bdecee25a062ddd5fd6283bfeaab
|
|
|
5
5
|
cortical-schaefer_300.zip sha256:5430c66eaef58f008b9e20dd5c59c670c7f7154b02f4b310dda97250331b6eed https://osf.io/9djtr/download
|
|
6
6
|
cortical-schaefer_400.zip sha256:ae2b7011919d49e9930ee5aed1b19731180427f07f0d1c4497130dd9cd5e878d https://osf.io/pzmc5/download
|
|
7
7
|
cortical-schaefer_1000.zip sha256:a10448f101a874499d41bc62508c3af29120634955f48beb282ece9a4cc2bb0e https://osf.io/j9ygz/download
|
|
8
|
+
cortical-aal3.zip sha256:925e475e099b127925e20e6ec4d179b1a58971f2bc32b2a61f56a479f3e8d4fb https://osf.io/z9jxh/download
|
|
8
9
|
subcortical-aseg.zip sha256:a901a7fc6a39f9bdaf2ef2bafbcde1fbc085b225d99e6730c727650d9be047d5 https://osf.io/5cs7y/download
|
|
9
10
|
subcortical-brainnetome_sc.zip sha256:8301fdf6af109af52a2cf9b06d15486345d457b69cb86169ed38332a34a681c0 https://osf.io/2fsg5/download
|
|
10
11
|
subcortical-jhu.zip sha256:b0ca292589a9f041851dba8159bb605a4657323e4305ba1570a304944b28d0de https://osf.io/x5fhg/download
|
|
11
12
|
subcortical-musus100_dbn.zip sha256:1d865832a35570a8c67f79d5049b58c548d25cf4fe1853164c384193b6712e40 https://osf.io/eutmb/download
|
|
12
13
|
subcortical-tian2020_sc.zip sha256:8b5caf8bf0cdcf8e259a3532fbcb232f8746d68fc734ca9806d10e70cf8707a3 https://osf.io/jrvgp/download
|
|
14
|
+
subcortical-aal3.zip sha256:48abe400656d913cc459ee8766e373cc5eabbeb9e4f770aa7396da3d4a0ab3ca https://osf.io/39ebh/download
|
|
15
|
+
subcortical-aal3_nocer.zip sha256:b87de55861cdd18ddb78934964b0e0855f770560bdc878570fcbb8f27fefb674 https://osf.io/7jdxz/download
|
|
13
16
|
tracts-hcp1065_medium.zip sha256:366bb100074cf7b1e55586e5468653f0c342e42cfdd91dca6d99b6fde82f06ff https://osf.io/kjf8e/download
|
|
14
17
|
tracts-hcp1065_small.zip sha256:020f23059c3a20ee0dda8ad12cd3df99b9fe5d3b37e7a6b9ae34b8a52b4b4346 https://osf.io/ynpa5/download
|
|
15
18
|
tracts-hcp1065_tiny.zip sha256:54380d82f5cd234029c6fc011910e00f0ad859fdb6c22c6aaa6452d055449ae9 https://osf.io/jzk7p/download
|
|
@@ -17,5 +20,10 @@ tracts-xtract_large.zip sha256:e2d59e9f90b024018788af87e389f93b2fa9ac21360460196
|
|
|
17
20
|
tracts-xtract_medium.zip sha256:f095028d3dfc0f974ebef1feffe4006b5bac1366325a784ea2301c56f583b1e7 https://osf.io/7tbjg/download
|
|
18
21
|
tracts-xtract_small.zip sha256:9d3fe2c57acb87e8d13eb40a49a6c2c354dbe1a020122b693e9bf713e57cad5b https://osf.io/bmn3a/download
|
|
19
22
|
tracts-xtract_tiny.zip sha256:469f9ed8ed5ceb7f8e17c9a0a92f1491d540814f832ef56441a7539532111f41 https://osf.io/a73x2/download
|
|
20
|
-
bmesh-
|
|
21
|
-
bmesh-
|
|
23
|
+
bmesh-inflated.zip sha256:ba72f9e75f16fe767f6bbcec5c98381f6a20b2f5e2092505b4692844a1686461 https://osf.io/kfzjg/download
|
|
24
|
+
bmesh-midthickness.zip sha256:ef8209853e1dac8804a60b5cde0b653ef2f6c77b6c7536f7acaa69a46dbb06c0 https://osf.io/xaktf/download
|
|
25
|
+
bmesh-pial.zip sha256:5e36ba7883dd2a88485fc45c9166ed48e22db607d45655242961242b51f9f126 https://osf.io/knpg8/download
|
|
26
|
+
bmesh-swm.zip sha256:1a516c070cb63557751d841d5cf75772660872b89ec749ee19c23298d77fa807 https://osf.io/hpbc9/download
|
|
27
|
+
bmesh-very_inflated.zip sha256:8c33a00c718a47f0af118ce2c5804a2adf13d79be05c4665ca26582e3e8dd977 https://osf.io/xp9jr/download
|
|
28
|
+
bmesh-white.zip sha256:b885ac1a4dcdf9e241a97e8d3ca59d7b1c86d8a3ebe17df512993bc5c1c0354a https://osf.io/wfc5t/download
|
|
29
|
+
label-nomedialwall.zip sha256:03bd589b151814a1fc5e48236a78decf1a51791f04c3f4a521d2ed4fb214317b https://osf.io/2rgmc/download
|
|
@@ -14,7 +14,8 @@ from .data import (
|
|
|
14
14
|
from .utils import (
|
|
15
15
|
load_gii, load_gii2pv, prep_data,
|
|
16
16
|
generate_distinct_colors, parse_lut, map_values_to_surface,
|
|
17
|
-
|
|
17
|
+
get_puzzle_pieces, apply_internal_blur, apply_dilation,
|
|
18
|
+
get_smooth_mask, lines_from_streamlines, make_cortical_mesh
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
from .scene import (
|
|
@@ -26,8 +27,8 @@ from .scene import (
|
|
|
26
27
|
# --- plot for cortical surface ---
|
|
27
28
|
|
|
28
29
|
def plot_cortical(data=None, atlas=None, custom_atlas_path=None, views=None, layout=None,
|
|
29
|
-
figsize=(1000, 600), cmap='
|
|
30
|
-
nan_color=(1.0, 1.0, 1.0), style='default', zoom=1.2,
|
|
30
|
+
bmesh_type='midthickness', figsize=(1000, 600), cmap='coolwarm', vminmax=[None, None],
|
|
31
|
+
nan_color=(1.0, 1.0, 1.0), style='default', zoom=1.2, proc_vertices=None,
|
|
31
32
|
display_type='static', export_path=None):
|
|
32
33
|
"""
|
|
33
34
|
Visualize data on the cortical surface using a specified atlas.
|
|
@@ -54,6 +55,9 @@ def plot_cortical(data=None, atlas=None, custom_atlas_path=None, views=None, lay
|
|
|
54
55
|
or a dictionary of camera configurations. Defaults to all views.
|
|
55
56
|
layout : tuple (rows, cols), optional
|
|
56
57
|
Grid layout for subplots. If None, automatically calculated based on the number of views.
|
|
58
|
+
bmesh_type : str
|
|
59
|
+
Name of the background context brain mesh (e.g., 'midthickness', 'white', 'swm', etc).
|
|
60
|
+
Default is 'midthickness'.
|
|
57
61
|
figsize : tuple (width, height), optional
|
|
58
62
|
Window size in pixels. Default is (1000, 600).
|
|
59
63
|
cmap : str or matplotlib.colors.Colormap, optional
|
|
@@ -67,6 +71,11 @@ def plot_cortical(data=None, atlas=None, custom_atlas_path=None, views=None, lay
|
|
|
67
71
|
Lighting preset ('default', 'matte', 'glossy', 'sculpted', 'flat').
|
|
68
72
|
zoom : float, optional
|
|
69
73
|
Camera zoom level. >1.0 zooms in, <1.0 zooms out. Default is 1.2.
|
|
74
|
+
proc_vertices : str or None, optional
|
|
75
|
+
Whether to process the vertices edges according to geometry of bmesh.
|
|
76
|
+
Set to None to not perform (default).
|
|
77
|
+
'blur': Applies simple blurring between different color vertices (low performance impact).
|
|
78
|
+
'sharp': Applies sharpening of the resolution of different color vertices (high performance impact).
|
|
70
79
|
display_type : {'static', 'interactive', 'none'}, optional
|
|
71
80
|
'static': Returns a static image (good for notebooks).
|
|
72
81
|
'interactive': Opens an interactive viewer.
|
|
@@ -80,19 +89,18 @@ def plot_cortical(data=None, atlas=None, custom_atlas_path=None, views=None, lay
|
|
|
80
89
|
The plotter instance used for rendering.
|
|
81
90
|
"""
|
|
82
91
|
|
|
83
|
-
#
|
|
92
|
+
# atlas and categorical check
|
|
84
93
|
if atlas is None and custom_atlas_path is None:
|
|
85
94
|
atlas = 'aparc'
|
|
95
|
+
is_cat = (data is None)
|
|
86
96
|
|
|
87
97
|
# load brain mesh
|
|
88
|
-
bmesh_path = _resolve_resource_path(
|
|
89
|
-
lh_v, lh_f = load_gii(os.path.join(bmesh_path, '
|
|
90
|
-
rh_v, rh_f = load_gii(os.path.join(bmesh_path, '
|
|
98
|
+
bmesh_path = _resolve_resource_path(bmesh_type, 'bmesh')
|
|
99
|
+
lh_v, lh_f = load_gii(os.path.join(bmesh_path, f'fs_LR.32k.L.{bmesh_type}.surf.gii'))
|
|
100
|
+
rh_v, rh_f = load_gii(os.path.join(bmesh_path, f'fs_LR.32k.R.{bmesh_type}.surf.gii'))
|
|
91
101
|
|
|
92
|
-
# resolve atlas
|
|
102
|
+
# resolve atlas
|
|
93
103
|
atlas_dir = _resolve_resource_path(atlas, 'cortical', custom_path=custom_atlas_path)
|
|
94
|
-
|
|
95
|
-
# locate files
|
|
96
104
|
check_name = None if custom_atlas_path else atlas
|
|
97
105
|
csv_path, lut_path = _find_cortical_files(atlas_dir, strict_name=check_name)
|
|
98
106
|
|
|
@@ -102,28 +110,43 @@ def plot_cortical(data=None, atlas=None, custom_atlas_path=None, views=None, lay
|
|
|
102
110
|
|
|
103
111
|
# map data
|
|
104
112
|
all_vals = map_values_to_surface(data, tar_labels, lut_ids, lut_names)
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
lh_vals_raw = all_vals[:len(lh_v)]
|
|
114
|
+
rh_vals_raw = all_vals[len(lh_v):]
|
|
115
|
+
|
|
116
|
+
# vertices processing
|
|
117
|
+
results = []
|
|
118
|
+
for v, f, raw in [(lh_v, lh_f, lh_vals_raw), (rh_v, rh_f, rh_vals_raw)]:
|
|
119
|
+
if proc_vertices == 'sharp':
|
|
120
|
+
# razor-sharp puzzle mode
|
|
121
|
+
base, pieces = get_puzzle_pieces(v, f, raw)
|
|
122
|
+
results.append((base, pieces))
|
|
123
|
+
else:
|
|
124
|
+
# single clipper mode (blur or none)
|
|
125
|
+
v_proc = apply_internal_blur(f, raw, iterations=3, weight=0.3) if proc_vertices == 'blur' else raw
|
|
126
|
+
dilated = apply_dilation(f, v_proc, iterations=4)
|
|
127
|
+
o_guide = get_smooth_mask(f, np.where(np.isnan(raw), 0.0, 1.0), iterations=4)
|
|
128
|
+
|
|
129
|
+
mesh = make_cortical_mesh(v, f, dilated)
|
|
130
|
+
mesh['Slice_Mask'] = o_guide
|
|
131
|
+
data_p = mesh.clip_scalar(scalars='Slice_Mask', value=0.5, invert=False)
|
|
132
|
+
base_p = mesh.clip_scalar(scalars='Slice_Mask', value=0.5, invert=True)
|
|
133
|
+
if base_p.n_points > 0: base_p['Data'] = np.full(base_p.n_points, np.nan)
|
|
134
|
+
results.append((base_p, [data_p]))
|
|
135
|
+
(lh_base, lh_parts), (rh_base, rh_parts) = results
|
|
107
136
|
|
|
108
137
|
# setup colors
|
|
109
|
-
is_categorical = (data is None)
|
|
110
138
|
n_colors = 256
|
|
111
|
-
if
|
|
139
|
+
if is_cat:
|
|
112
140
|
_lut_colors = lut_colors.copy()
|
|
113
|
-
_lut_colors[0] = nan_color
|
|
141
|
+
_lut_colors[0] = nan_color # TODO: check nancolor
|
|
114
142
|
cmap = ListedColormap(_lut_colors)
|
|
115
143
|
n_colors = len(_lut_colors)
|
|
116
144
|
vmin, vmax = 0, max_id
|
|
117
145
|
else:
|
|
118
|
-
if cmap is None: cmap = 'RdYlBu_r'
|
|
119
146
|
vmin = vminmax[0] if vminmax[0] is not None else np.nanmin(all_vals)
|
|
120
147
|
vmax = vminmax[1] if vminmax[1] is not None else np.nanmax(all_vals)
|
|
121
148
|
|
|
122
|
-
#
|
|
123
|
-
lh_mesh = make_cortical_mesh(lh_v, lh_f, lh_vals)
|
|
124
|
-
rh_mesh = make_cortical_mesh(rh_v, rh_f, rh_vals)
|
|
125
|
-
|
|
126
|
-
# setup plotter
|
|
149
|
+
# plotter setup
|
|
127
150
|
sel_views = get_view_configs(views)
|
|
128
151
|
plotter, ncols, nrows = setup_plotter(sel_views, layout, figsize, display_type)
|
|
129
152
|
shading_params = get_shading_preset(style)
|
|
@@ -132,46 +155,46 @@ def plot_cortical(data=None, atlas=None, custom_atlas_path=None, views=None, lay
|
|
|
132
155
|
for i, (name, cfg) in enumerate(sel_views.items()):
|
|
133
156
|
plotter.subplot(i // ncols, i % ncols)
|
|
134
157
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if cfg['side'] in ['
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
view_bases = []
|
|
159
|
+
view_pieces = []
|
|
160
|
+
if cfg['side'] in ['L', 'both']:
|
|
161
|
+
if lh_base.n_points > 0: view_bases.append(lh_base)
|
|
162
|
+
view_pieces.extend(lh_parts)
|
|
163
|
+
if cfg['side'] in ['R', 'both']:
|
|
164
|
+
if rh_base.n_points > 0: view_bases.append(rh_base)
|
|
165
|
+
view_pieces.extend(rh_parts)
|
|
166
|
+
|
|
167
|
+
# brain meshes
|
|
168
|
+
for b_mesh in view_bases:
|
|
169
|
+
plotter.add_mesh(b_mesh, color=nan_color, smooth_shading=True, **shading_params)
|
|
170
|
+
|
|
171
|
+
# data vertices
|
|
172
|
+
for p_mesh in view_pieces:
|
|
173
|
+
if p_mesh.n_points == 0: continue
|
|
174
|
+
interp = (proc_vertices == 'blur') # only blur mode uses interpolation
|
|
175
|
+
|
|
140
176
|
actor = plotter.add_mesh(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
n_colors=n_colors,
|
|
146
|
-
nan_color=nan_color,
|
|
147
|
-
show_scalar_bar=False,
|
|
148
|
-
rgb=False,
|
|
149
|
-
smooth_shading=True,
|
|
150
|
-
show_edges=False,
|
|
151
|
-
interpolate_before_map=False,
|
|
152
|
-
**shading_params
|
|
153
|
-
)
|
|
177
|
+
p_mesh, scalars='Data', cmap=cmap, clim=(vmin, vmax),
|
|
178
|
+
n_colors=n_colors, nan_color=nan_color, show_scalar_bar=False,
|
|
179
|
+
smooth_shading=True, interpolate_before_map=interp, **shading_params
|
|
180
|
+
) # rgb=False, show_edges=False, interpolate_before_map=False,
|
|
154
181
|
if scalar_bar_mapper is None: scalar_bar_mapper = actor.mapper
|
|
155
182
|
|
|
156
183
|
set_camera(plotter, cfg, zoom=zoom)
|
|
157
184
|
plotter.hide_axes()
|
|
158
|
-
|
|
159
|
-
if not
|
|
185
|
+
|
|
186
|
+
if not is_cat and scalar_bar_mapper:
|
|
160
187
|
plotter.subplot(nrows - 1, 0)
|
|
161
|
-
plotter.add_scalar_bar(mapper=scalar_bar_mapper,
|
|
162
|
-
vertical=False, position_x=0.3, position_y=0.25,
|
|
163
|
-
height=0.5, width=0.4,color='black',
|
|
164
|
-
label_font_size=20)
|
|
188
|
+
plotter.add_scalar_bar(mapper=scalar_bar_mapper, vertical=False, position_x=0.3, position_y=0.25, height=0.5, width=0.4)
|
|
165
189
|
|
|
166
190
|
return finalize_plot(plotter, export_path, display_type)
|
|
167
191
|
|
|
168
192
|
|
|
169
|
-
|
|
170
193
|
# --- plot for subcortical structures ---
|
|
171
194
|
|
|
172
195
|
def plot_subcortical(data=None, atlas=None, custom_atlas_path=None, views=None, layout=None,
|
|
173
196
|
figsize=(1000, 600), cmap='coolwarm', vminmax=[None, None], nan_color='#cccccc',
|
|
174
|
-
nan_alpha=1.0, legend=False, style='default', bmesh_type='
|
|
197
|
+
nan_alpha=1.0, legend=False, style='default', bmesh_type='midthickness',
|
|
175
198
|
bmesh_alpha=0.1, bmesh_color='lightgray', zoom=1.2, display_type='static',
|
|
176
199
|
export_path=None, custom_atlas_proc=dict(smooth_i=15, smooth_f=0.6)):
|
|
177
200
|
"""
|
|
@@ -213,8 +236,8 @@ def plot_subcortical(data=None, atlas=None, custom_atlas_path=None, views=None,
|
|
|
213
236
|
style : str, optional
|
|
214
237
|
Lighting preset ('default', 'matte', 'glossy', 'sculpted', 'flat').
|
|
215
238
|
bmesh_type : str or None, optional
|
|
216
|
-
Name of the background context brain mesh (e.g., '
|
|
217
|
-
Set to None to hide the context brain.
|
|
239
|
+
Name of the background context brain mesh (e.g., 'midthickness', 'white', 'swm', etc).
|
|
240
|
+
Set to None to hide the context brain. Default is 'midthickness'.
|
|
218
241
|
bmesh_alpha : float, optional
|
|
219
242
|
Opacity of the context brain mesh. Default is 0.1.
|
|
220
243
|
bmesh_color : str, optional
|
|
@@ -246,8 +269,8 @@ def plot_subcortical(data=None, atlas=None, custom_atlas_path=None, views=None,
|
|
|
246
269
|
bmesh = {}
|
|
247
270
|
if bmesh_type:
|
|
248
271
|
bmesh_path = _resolve_resource_path(bmesh_type, 'bmesh')
|
|
249
|
-
for h in ['
|
|
250
|
-
fpath = os.path.join(bmesh_path, f'{
|
|
272
|
+
for h in ['L', 'R']:
|
|
273
|
+
fpath = os.path.join(bmesh_path, f'fs_LR.32k.{h}.{bmesh_type}.surf.gii')
|
|
251
274
|
if os.path.exists(fpath):
|
|
252
275
|
bmesh[h] = load_gii2pv(fpath)
|
|
253
276
|
|
|
@@ -371,7 +394,7 @@ def clear_tract_cache():
|
|
|
371
394
|
def plot_tracts(data=None, atlas=None, custom_atlas_path=None, views=None, layout=None,
|
|
372
395
|
figsize=(1000, 800), cmap='coolwarm', alpha=1.0, vminmax=[None, None],
|
|
373
396
|
nan_color='#BDBDBD', nan_alpha=1.0, legend=False, style='default',
|
|
374
|
-
bmesh_type='
|
|
397
|
+
bmesh_type='midthickness', bmesh_alpha=0.2, bmesh_color='lightgray',
|
|
375
398
|
zoom=1.2, orientation_coloring=False, display_type='static',
|
|
376
399
|
tract_kwargs=dict(render_lines_as_tubes=True, line_width=1.2),
|
|
377
400
|
export_path=None):
|
|
@@ -417,8 +440,8 @@ def plot_tracts(data=None, atlas=None, custom_atlas_path=None, views=None, layou
|
|
|
417
440
|
style : str, optional
|
|
418
441
|
Lighting preset ('default', 'matte', 'glossy', 'sculpted', 'flat').
|
|
419
442
|
bmesh_type : str or None, optional
|
|
420
|
-
Name of the background context brain mesh (e.g., '
|
|
421
|
-
Set to None to hide the context brain.
|
|
443
|
+
Name of the background context brain mesh (e.g., 'midthickness', 'white', 'swm', etc).
|
|
444
|
+
Set to None to hide the context brain. Default is 'midthickness'.
|
|
422
445
|
bmesh_alpha : float, optional
|
|
423
446
|
Opacity of the context brain mesh. Default is 0.2.
|
|
424
447
|
bmesh_color : str, optional
|
|
@@ -472,8 +495,8 @@ def plot_tracts(data=None, atlas=None, custom_atlas_path=None, views=None, layou
|
|
|
472
495
|
bmesh = {}
|
|
473
496
|
if bmesh_type:
|
|
474
497
|
bmesh_path = _resolve_resource_path(bmesh_type, 'bmesh')
|
|
475
|
-
for h in ['
|
|
476
|
-
fpath = os.path.join(bmesh_path, f'{
|
|
498
|
+
for h in ['L', 'R']:
|
|
499
|
+
fpath = os.path.join(bmesh_path, f'fs_LR.32k.{h}.{bmesh_type}.surf.gii')
|
|
477
500
|
if os.path.exists(fpath):
|
|
478
501
|
bmesh[h] = load_gii2pv(fpath)
|
|
479
502
|
|
|
@@ -91,7 +91,7 @@ def setup_plotter(sel_views, layout, figsize, display_type, needs_bottom_row=Tru
|
|
|
91
91
|
row_weights = None
|
|
92
92
|
|
|
93
93
|
plotter = pv.Plotter(shape=(nrows, ncols), groups=groups, row_weights=row_weights,
|
|
94
|
-
off_screen=(display_type=='
|
|
94
|
+
off_screen=(display_type=='object'), window_size=figsize, border=False)
|
|
95
95
|
plotter.set_background('white')
|
|
96
96
|
return plotter, ncols, nrows
|
|
97
97
|
|
|
@@ -101,7 +101,7 @@ def add_context_to_view(plotter, bmesh, view_side, alpha, color, **kwargs):
|
|
|
101
101
|
"""
|
|
102
102
|
if not bmesh: return
|
|
103
103
|
for h, mesh in bmesh.items():
|
|
104
|
-
if (view_side == 'L' and h == '
|
|
104
|
+
if (view_side == 'L' and h == 'L') or (view_side == 'R' and h == 'R'): continue
|
|
105
105
|
plotter.add_mesh(mesh, color=color, opacity=alpha,
|
|
106
106
|
smooth_shading=True, show_edges=False,
|
|
107
107
|
**kwargs)
|
|
@@ -115,10 +115,19 @@ def set_camera(plotter, view_cfg, zoom=1.0, distance=200):
|
|
|
115
115
|
plotter.camera.zoom(zoom)
|
|
116
116
|
|
|
117
117
|
def finalize_plot(plotter, export_path, display_type):
|
|
118
|
-
if export_path:
|
|
118
|
+
if export_path:
|
|
119
|
+
plotter.screenshot(export_path, transparent_background=True)
|
|
119
120
|
|
|
120
|
-
if display_type == 'static':
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
if display_type == 'static':
|
|
122
|
+
out = plotter.show(jupyter_backend='static')
|
|
123
|
+
plotter.close()
|
|
124
|
+
elif display_type == 'interactive':
|
|
125
|
+
out = plotter.show(jupyter_backend='trame')
|
|
126
|
+
elif display_type == 'object':
|
|
127
|
+
plotter.render()
|
|
128
|
+
return plotter
|
|
129
|
+
else:
|
|
130
|
+
plotter.close()
|
|
131
|
+
out = None
|
|
132
|
+
|
|
133
|
+
return out
|
|
@@ -3,6 +3,7 @@ import numpy as np
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
import nibabel as nib
|
|
5
5
|
import pyvista as pv
|
|
6
|
+
import scipy.sparse as sp
|
|
6
7
|
import matplotlib.pyplot as plt
|
|
7
8
|
from importlib.resources import files
|
|
8
9
|
|
|
@@ -143,6 +144,93 @@ def map_values_to_surface(data, target_labels, lut_ids, dense_lut_names):
|
|
|
143
144
|
lookup_table[valid_ids] = source_values
|
|
144
145
|
return lookup_table[target_labels]
|
|
145
146
|
|
|
147
|
+
def get_adj(faces, n_v):
|
|
148
|
+
"""build adjacency matrix from faces."""
|
|
149
|
+
row, col = [], []
|
|
150
|
+
for tri in faces:
|
|
151
|
+
row.extend([tri[0], tri[1], tri[2], tri[0], tri[1], tri[2]])
|
|
152
|
+
col.extend([tri[1], tri[2], tri[0], tri[2], tri[0], tri[1]])
|
|
153
|
+
adj = sp.csc_matrix((np.ones_like(row), (row, col)), shape=(n_v, n_v))
|
|
154
|
+
adj.data = np.ones_like(adj.data)
|
|
155
|
+
return adj
|
|
156
|
+
|
|
157
|
+
def get_smooth_mask(faces, data, iterations=4):
|
|
158
|
+
"""blur binary mask for guide of geometric slicing."""
|
|
159
|
+
n_v = len(data)
|
|
160
|
+
mask = data.astype(np.float64)
|
|
161
|
+
adj = get_adj(faces, n_v)
|
|
162
|
+
deg = np.array(adj.sum(axis=1)).flatten()
|
|
163
|
+
deg[deg == 0] = 1.0
|
|
164
|
+
for _ in range(iterations):
|
|
165
|
+
mask = (mask + (adj.dot(mask) / deg)) / 2.0
|
|
166
|
+
return mask
|
|
167
|
+
|
|
168
|
+
def apply_internal_blur(faces, data, iterations=1, weight=0.2):
|
|
169
|
+
"""blur data only on borders where different regions touch."""
|
|
170
|
+
data_out = np.copy(data)
|
|
171
|
+
n_v = len(data)
|
|
172
|
+
adj = get_adj(faces, n_v)
|
|
173
|
+
rows, cols = adj.nonzero()
|
|
174
|
+
valid = ~np.isnan(data_out)
|
|
175
|
+
diff = valid[rows] & valid[cols] & ~np.isclose(data_out[rows], data_out[cols], atol=1e-5)
|
|
176
|
+
b_verts = np.unique(rows[diff])
|
|
177
|
+
|
|
178
|
+
if len(b_verts) == 0: return data_out
|
|
179
|
+
|
|
180
|
+
for _ in range(iterations):
|
|
181
|
+
temp = np.nan_to_num(data_out, nan=0.0)
|
|
182
|
+
v_counts = adj.dot(valid.astype(float))
|
|
183
|
+
v_counts[v_counts == 0] = 1.0
|
|
184
|
+
n_mean = adj.dot(temp) / v_counts
|
|
185
|
+
data_out[b_verts] = (1 - weight) * data_out[b_verts] + weight * n_mean[b_verts]
|
|
186
|
+
return data_out
|
|
187
|
+
|
|
188
|
+
def apply_dilation(faces, data, iterations=4):
|
|
189
|
+
"""push values into NaN space to keep geometric cut pure."""
|
|
190
|
+
data_out = np.copy(data)
|
|
191
|
+
n_v = len(data)
|
|
192
|
+
adj = get_adj(faces, n_v)
|
|
193
|
+
for _ in range(iterations):
|
|
194
|
+
nan_m = np.isnan(data_out)
|
|
195
|
+
temp = np.nan_to_num(data_out, nan=0.0)
|
|
196
|
+
v_counts = adj.dot((~nan_m).astype(float))
|
|
197
|
+
s_neighbors = adj.dot(temp)
|
|
198
|
+
u_mask = nan_m & (v_counts > 0)
|
|
199
|
+
data_out[u_mask] = s_neighbors[u_mask] / v_counts[u_mask]
|
|
200
|
+
return data_out
|
|
201
|
+
|
|
202
|
+
def get_puzzle_pieces(v, f, raw_vals):
|
|
203
|
+
"""carve out geometric pieces with slight overlap to prevent gaps."""
|
|
204
|
+
pieces = []
|
|
205
|
+
valid_mask = ~np.isnan(raw_vals) & (raw_vals != 0.0)
|
|
206
|
+
u_vals = np.unique(raw_vals[valid_mask])
|
|
207
|
+
master = make_cortical_mesh(v, f, np.zeros_like(raw_vals))
|
|
208
|
+
|
|
209
|
+
for val in u_vals:
|
|
210
|
+
r_mask = np.where(raw_vals == val, 1.0, 0.0)
|
|
211
|
+
s_mask = get_smooth_mask(f, r_mask, iterations=4)
|
|
212
|
+
temp = master.copy()
|
|
213
|
+
temp['Slice_Mask'] = s_mask
|
|
214
|
+
# reduce search space
|
|
215
|
+
patch = temp.threshold(0.01, scalars='Slice_Mask')
|
|
216
|
+
if patch.n_points > 0:
|
|
217
|
+
# use 0.48 (slightly expanded) for pieces to seal cracks
|
|
218
|
+
piece = patch.clip_scalar(scalars='Slice_Mask', value=0.48, invert=False)
|
|
219
|
+
if piece.n_points > 0:
|
|
220
|
+
piece['Data'] = np.full(piece.n_points, val)
|
|
221
|
+
pieces.append(piece)
|
|
222
|
+
|
|
223
|
+
# slice base brain
|
|
224
|
+
all_mask = np.where(valid_mask, 1.0, 0.0)
|
|
225
|
+
s_all = get_smooth_mask(f, all_mask, iterations=4)
|
|
226
|
+
master['Slice_Mask'] = s_all
|
|
227
|
+
# use 0.52 (slightly contracted) for the hole to ensure colored pieces cover the edge
|
|
228
|
+
base_p = master.clip_scalar(scalars='Slice_Mask', value=0.52, invert=True)
|
|
229
|
+
if base_p.n_points > 0:
|
|
230
|
+
base_p['Data'] = np.full(base_p.n_points, np.nan)
|
|
231
|
+
|
|
232
|
+
return base_p, pieces
|
|
233
|
+
|
|
146
234
|
def lines_from_streamlines(streamlines):
|
|
147
235
|
if len(streamlines) == 0: return np.array([]), np.array([]), np.array([])
|
|
148
236
|
|
|
@@ -155,7 +243,7 @@ def lines_from_streamlines(streamlines):
|
|
|
155
243
|
cells.append(np.hstack([[length], np.arange(offset, offset + length)]))
|
|
156
244
|
lines = np.hstack(cells)
|
|
157
245
|
|
|
158
|
-
#
|
|
246
|
+
# calculate tangents
|
|
159
247
|
tangents = []
|
|
160
248
|
for s in streamlines:
|
|
161
249
|
if len(s) < 2:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import shutil
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
def check_workbench():
|
|
6
|
+
"""
|
|
7
|
+
Checks if Connectome Workbench (wb_command) is installed and available in PATH.
|
|
8
|
+
|
|
9
|
+
Raises
|
|
10
|
+
------
|
|
11
|
+
EnvironmentError
|
|
12
|
+
If wb_command is not found, providing instructions for installation.
|
|
13
|
+
"""
|
|
14
|
+
if shutil.which('wb_command') is None:
|
|
15
|
+
raise EnvironmentError(
|
|
16
|
+
"Connectome Workbench ('wb_command') was not found in your system PATH.\n"
|
|
17
|
+
"This is required for volume-to-surface projection (necessary for creating a custom cortical atlas).\n"
|
|
18
|
+
"Please download it from: https://humanconnectome.org/software/get-connectome-workbench\n"
|
|
19
|
+
"After installing, ensure the 'bin' folder is added to your PATH environment variable."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def run_wb_import(input_nii, label_list, output_nii):
|
|
23
|
+
"""Wrapper for wb_command -volume-label-import"""
|
|
24
|
+
check_workbench()
|
|
25
|
+
cmd = ["wb_command", "-volume-label-import", input_nii, label_list, output_nii]
|
|
26
|
+
subprocess.run(cmd, check=True)
|
|
27
|
+
|
|
28
|
+
def run_wb_projection(input_nii, midthickness, output_gii, white, pial):
|
|
29
|
+
"""Wrapper for wb_command -volume-label-to-surface-mapping (ribbon-constrained)"""
|
|
30
|
+
check_workbench()
|
|
31
|
+
cmd = [
|
|
32
|
+
"wb_command", "-volume-label-to-surface-mapping",
|
|
33
|
+
input_nii, midthickness, output_gii,
|
|
34
|
+
"-ribbon-constrained", white, pial
|
|
35
|
+
]
|
|
36
|
+
subprocess.run(cmd, check=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yabplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: yet another brain plot
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -55,6 +55,9 @@ pip install yabplot --upgrade # to update
|
|
|
55
55
|
|
|
56
56
|
dependencies: python 3.11 with ipywidgets, nibabel, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
|
|
57
57
|
|
|
58
|
+
(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)
|
|
59
|
+
|
|
60
|
+
|
|
58
61
|
## quick start
|
|
59
62
|
|
|
60
63
|
please refer to the [documentation](https://teanijarv.github.io/yabplot/) for more comprehensive guides.
|
|
@@ -63,32 +66,41 @@ please refer to the [documentation](https://teanijarv.github.io/yabplot/) for mo
|
|
|
63
66
|
import yabplot as yab
|
|
64
67
|
import numpy as np
|
|
65
68
|
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# see
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
yab.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
69
|
+
# check that you have the latest version
|
|
70
|
+
print(yab.__version__)
|
|
71
|
+
|
|
72
|
+
# see available atlases and brain meshes
|
|
73
|
+
print(yab.get_available_resources())
|
|
74
|
+
|
|
75
|
+
# see the region names for a specific atlas
|
|
76
|
+
print(yab.get_atlas_regions(atlas='aseg', category='subcortical'))
|
|
77
|
+
|
|
78
|
+
# cortical surfaces
|
|
79
|
+
atlas = 'aparc'
|
|
80
|
+
dmap1 = {'L_lateraloccipital': 0.265, 'L_postcentral': 0.086, ...}
|
|
81
|
+
dmap2 = {'L_fusiform': 0.218, 'L_supramarginal': 0.119, ...}
|
|
82
|
+
yab.plot_cortical(data=dmap1, atlas=atlas, vminmax=[-0.1, 0.3],
|
|
83
|
+
bmesh_type='midthickness', views=['left_lateral', 'left_medial'],
|
|
84
|
+
figsize=(600, 300), cmap='viridis', proc_vertices='sharp')
|
|
85
|
+
yab.plot_cortical(data=dmap2, atlas=atlas, vminmax=[-0.1, 0.3],
|
|
86
|
+
bmesh_type='swm', views=['left_lateral', 'left_medial'],
|
|
87
|
+
figsize=(1200, 600), cmap='viridis', proc_vertices='sharp')
|
|
88
|
+
|
|
89
|
+
# subcortical structures
|
|
90
|
+
atlas = 'aseg'
|
|
91
|
+
regs = yab.get_atlas_regions(atlas=atlas, category='subcortical')
|
|
92
|
+
data = np.arange(1, len(regs)+1)
|
|
93
|
+
yab.plot_subcortical(data=data, atlas=atlas, vminmax=[2, 14],
|
|
94
|
+
views=['left_lateral', 'superior', 'right_lateral'],
|
|
95
|
+
bmesh_alpha=0.1, figsize=(600, 300), cmap='viridis')
|
|
96
|
+
|
|
97
|
+
# white matter bundles
|
|
98
|
+
atlas = 'xtract_tiny'
|
|
99
|
+
regs = yab.get_atlas_regions(atlas=atlas, category='tracts')
|
|
100
|
+
data = {reg: np.sin(i) for i, reg in enumerate(regs)}
|
|
101
|
+
yab.plot_tracts(data=data, atlas=atlas, style='matte',
|
|
102
|
+
views=['left_lateral', 'anterior', 'superior'], bmesh_type='pial',
|
|
103
|
+
bmesh_alpha=0.1, figsize=(1600, 800), cmap='viridis')
|
|
92
104
|
|
|
93
105
|
```
|
|
94
106
|
|
|
@@ -4,9 +4,11 @@ README.md
|
|
|
4
4
|
pyproject.toml
|
|
5
5
|
tests/test_smoke.py
|
|
6
6
|
yabplot/__init__.py
|
|
7
|
+
yabplot/atlas_builder.py
|
|
7
8
|
yabplot/plotting.py
|
|
8
9
|
yabplot/scene.py
|
|
9
10
|
yabplot/utils.py
|
|
11
|
+
yabplot/wrappers.py
|
|
10
12
|
yabplot.egg-info/PKG-INFO
|
|
11
13
|
yabplot.egg-info/SOURCES.txt
|
|
12
14
|
yabplot.egg-info/dependency_links.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|