yabplot 0.4.0__tar.gz → 0.5.1__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.5.1/CITATION.cff +21 -0
- yabplot-0.5.1/CODE_OF_CONDUCT.md +27 -0
- yabplot-0.5.1/CONTRIBUTING.md +34 -0
- yabplot-0.5.1/MANIFEST.in +9 -0
- {yabplot-0.4.0/yabplot.egg-info → yabplot-0.5.1}/PKG-INFO +42 -29
- {yabplot-0.4.0 → yabplot-0.5.1}/README.md +40 -28
- {yabplot-0.4.0 → yabplot-0.5.1}/pyproject.toml +7 -1
- yabplot-0.5.1/tests/conftest.py +51 -0
- yabplot-0.5.1/tests/test_data.py +41 -0
- yabplot-0.5.1/tests/test_mapping.py +78 -0
- yabplot-0.5.1/tests/test_mesh.py +24 -0
- yabplot-0.5.1/tests/test_plotting.py +93 -0
- yabplot-0.5.1/tests/test_projection.py +25 -0
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot/__init__.py +3 -2
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot/atlas_builder.py +3 -3
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot/mesh.py +125 -24
- yabplot-0.5.1/yabplot/plotting.py +1458 -0
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot/projection.py +34 -28
- yabplot-0.5.1/yabplot/scene.py +336 -0
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot/utils.py +17 -15
- {yabplot-0.4.0 → yabplot-0.5.1/yabplot.egg-info}/PKG-INFO +42 -29
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot.egg-info/SOURCES.txt +9 -1
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot.egg-info/requires.txt +1 -0
- yabplot-0.4.0/MANIFEST.in +0 -4
- yabplot-0.4.0/tests/test_smoke.py +0 -80
- 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.1}/LICENSE +0 -0
- {yabplot-0.4.0 → yabplot-0.5.1}/setup.cfg +0 -0
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot/data/__init__.py +0 -0
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot/data/registry.txt +0 -0
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot/wrappers.py +0 -0
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot.egg-info/dependency_links.txt +0 -0
- {yabplot-0.4.0 → yabplot-0.5.1}/yabplot.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use yabplot in your research, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Anijärv"
|
|
5
|
+
given-names: "Toomas Erik"
|
|
6
|
+
orcid: "https://orcid.org/0000-0002-3650-4230"
|
|
7
|
+
title: "yabplot: yet another brain plot for unified neuroimaging visualization in Python"
|
|
8
|
+
doi: 10.5281/zenodo.18237144
|
|
9
|
+
version: 0.5.1
|
|
10
|
+
url: "https://github.com/teanijarv/yabplot"
|
|
11
|
+
preferred-citation:
|
|
12
|
+
type: software
|
|
13
|
+
authors:
|
|
14
|
+
- family-names: "Anijärv"
|
|
15
|
+
given-names: "Toomas Erik"
|
|
16
|
+
orcid: "https://orcid.org/0000-0002-3650-4230"
|
|
17
|
+
title: "yabplot: yet another brain plot for unified neuroimaging visualization in Python"
|
|
18
|
+
year: 2026
|
|
19
|
+
doi: 10.5281/zenodo.18237144
|
|
20
|
+
url: "https://github.com/teanijarv/yabplot"
|
|
21
|
+
abstract: "An open-source Python package for 3D neuroimaging visualization."
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# code of conduct
|
|
2
|
+
|
|
3
|
+
## our pledge
|
|
4
|
+
in the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
5
|
+
|
|
6
|
+
## our standards
|
|
7
|
+
examples of behavior that contributes to creating a positive environment include:
|
|
8
|
+
* using welcoming and inclusive language
|
|
9
|
+
* being respectful of differing viewpoints and experiences
|
|
10
|
+
* gracefully accepting constructive criticism
|
|
11
|
+
* focusing on what is best for the community
|
|
12
|
+
* showing empathy towards other community members
|
|
13
|
+
|
|
14
|
+
examples of unacceptable behavior by participants include:
|
|
15
|
+
* the use of sexualized language or imagery and unwelcome sexual attention or advances
|
|
16
|
+
* trolling, insulting/derogatory comments, and personal or political attacks
|
|
17
|
+
* public or private harassment
|
|
18
|
+
* publishing others' private information, such as a physical or electronic address, without explicit permission
|
|
19
|
+
* other conduct which could reasonably be considered inappropriate in a professional setting
|
|
20
|
+
|
|
21
|
+
## our responsibilities
|
|
22
|
+
as the maintainers of this project, we are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
|
23
|
+
|
|
24
|
+
we have the right and responsibility to remove, edit, or reject comments, commits, code, docs edits, issues, and other contributions that are not aligned to this code of conduct, or to ban temporarily or permanently any contributor for other behaviors that we deem inappropriate, threatening, offensive, or harmful.
|
|
25
|
+
|
|
26
|
+
## enforcement
|
|
27
|
+
instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting us at teanijarv@pm.me. all complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. we are obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# contributing to yabplot
|
|
2
|
+
|
|
3
|
+
thank you for your interest in contributing to `yabplot`! we welcome contributions from the neuroimaging and open-source communities.
|
|
4
|
+
|
|
5
|
+
## how to contribute
|
|
6
|
+
|
|
7
|
+
### reporting bugs
|
|
8
|
+
- use the [github issue tracker](https://github.com/teanijarv/yabplot/issues) to report bugs.
|
|
9
|
+
- include a minimal reproducible example and information about your environment (os, python version, package versions).
|
|
10
|
+
|
|
11
|
+
### suggesting enhancements
|
|
12
|
+
- open an issue describing the proposed feature and why it would be useful for the community.
|
|
13
|
+
|
|
14
|
+
### pull requests
|
|
15
|
+
1. fork the repository and create your branch from `main`.
|
|
16
|
+
2. if you've added code that should be tested, add tests in the `tests/` directory.
|
|
17
|
+
3. if you've changed API, update the documentation.
|
|
18
|
+
4. ensure the test suite passes: `uv run pytest tests/`
|
|
19
|
+
5. submit a pull request with a clear description of the changes.
|
|
20
|
+
|
|
21
|
+
## development setup
|
|
22
|
+
|
|
23
|
+
we use `uv` for dependency management. to set up your development environment:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git clone https://github.com/teanijarv/yabplot.git
|
|
27
|
+
cd yabplot
|
|
28
|
+
uv venv
|
|
29
|
+
uv pip install -e ".[docs]"
|
|
30
|
+
uv pip install pytest
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## community
|
|
34
|
+
by contributing, you agree to abide by our [code of conduct](CODE_OF_CONDUCT.md).
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yabplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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.1"
|
|
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",
|
|
@@ -31,3 +32,8 @@ include = ["yabplot*"]
|
|
|
31
32
|
|
|
32
33
|
[tool.setuptools.package-data]
|
|
33
34
|
"yabplot" = ["*.txt", "*.json"]
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=9.0.3",
|
|
39
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import numpy as np
|
|
3
|
+
import nibabel as nib
|
|
4
|
+
import pyvista as pv
|
|
5
|
+
|
|
6
|
+
pv.OFF_SCREEN = True
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def synthetic_nifti(tmp_path):
|
|
10
|
+
"""Generates a simple 3D NIfTI file with a sphere of high intensity."""
|
|
11
|
+
shape = (20, 20, 20)
|
|
12
|
+
data = np.zeros(shape)
|
|
13
|
+
|
|
14
|
+
# create a simple sphere
|
|
15
|
+
cz, cy, cx = 10, 10, 10
|
|
16
|
+
r = 5
|
|
17
|
+
z, y, x = np.ogrid[:shape[0], :shape[1], :shape[2]]
|
|
18
|
+
mask = (z - cz)**2 + (y - cy)**2 + (x - cx)**2 <= r**2
|
|
19
|
+
data[mask] = 10.0
|
|
20
|
+
|
|
21
|
+
affine = np.eye(4)
|
|
22
|
+
img = nib.Nifti1Image(data, affine)
|
|
23
|
+
file_path = tmp_path / "synthetic.nii.gz"
|
|
24
|
+
nib.save(img, file_path)
|
|
25
|
+
return str(file_path)
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def synthetic_nifti_4d(tmp_path):
|
|
29
|
+
"""Generates a 4D NIfTI file."""
|
|
30
|
+
shape = (20, 20, 20, 3)
|
|
31
|
+
data = np.random.rand(*shape)
|
|
32
|
+
affine = np.eye(4)
|
|
33
|
+
img = nib.Nifti1Image(data, affine)
|
|
34
|
+
file_path = tmp_path / "synthetic_4d.nii.gz"
|
|
35
|
+
nib.save(img, file_path)
|
|
36
|
+
return str(file_path)
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def synthetic_tractogram(tmp_path):
|
|
40
|
+
"""Generates a simple tractogram."""
|
|
41
|
+
from nibabel.streamlines.tractogram import Tractogram
|
|
42
|
+
from nibabel.streamlines.trk import TrkFile
|
|
43
|
+
|
|
44
|
+
streamlines = [
|
|
45
|
+
np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2]], dtype=np.float32),
|
|
46
|
+
np.array([[10, 10, 10], [11, 10, 10], [12, 10, 10]], dtype=np.float32)
|
|
47
|
+
]
|
|
48
|
+
tractogram = Tractogram(streamlines, affine_to_rasmm=np.eye(4))
|
|
49
|
+
file_path = tmp_path / "synthetic.trk"
|
|
50
|
+
TrkFile(tractogram).save(file_path)
|
|
51
|
+
return str(file_path)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from yabplot.data import get_available_resources, get_atlas_regions
|
|
3
|
+
|
|
4
|
+
def test_get_available_resources():
|
|
5
|
+
"""Verify that all resource categories exist and contain data."""
|
|
6
|
+
# all categories
|
|
7
|
+
res_all = get_available_resources(None)
|
|
8
|
+
assert isinstance(res_all, dict)
|
|
9
|
+
|
|
10
|
+
expected_categories = ['cortical', 'subcortical', 'tracts', 'bmesh']
|
|
11
|
+
for cat in expected_categories:
|
|
12
|
+
assert cat in res_all, f"expected category {cat} to be in resources"
|
|
13
|
+
assert len(res_all[cat]) > 0
|
|
14
|
+
|
|
15
|
+
# specific category
|
|
16
|
+
res_cortical = get_available_resources('cortical')
|
|
17
|
+
assert isinstance(res_cortical, list)
|
|
18
|
+
assert 'aparc' in res_cortical
|
|
19
|
+
|
|
20
|
+
def test_get_atlas_regions_cortical():
|
|
21
|
+
"""Verify that cortical atlas regions are correctly retrieved."""
|
|
22
|
+
regions = get_atlas_regions('aparc', 'cortical')
|
|
23
|
+
assert isinstance(regions, list)
|
|
24
|
+
assert len(regions) > 0
|
|
25
|
+
|
|
26
|
+
def test_get_atlas_regions_subcortical():
|
|
27
|
+
"""Verify that subcortical atlas regions are correctly retrieved."""
|
|
28
|
+
regions = get_atlas_regions('aseg', 'subcortical')
|
|
29
|
+
assert isinstance(regions, list)
|
|
30
|
+
assert len(regions) > 0
|
|
31
|
+
|
|
32
|
+
def test_get_atlas_regions_tracts():
|
|
33
|
+
"""Verify that tract atlas regions are correctly retrieved."""
|
|
34
|
+
regions = get_atlas_regions('xtract_tiny', 'tracts')
|
|
35
|
+
assert isinstance(regions, list)
|
|
36
|
+
assert len(regions) > 0
|
|
37
|
+
|
|
38
|
+
def test_get_atlas_regions_invalid():
|
|
39
|
+
"""Verify that invalid categories return an empty list gracefully."""
|
|
40
|
+
regions = get_atlas_regions('aparc', 'invalid_category')
|
|
41
|
+
assert regions == []
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from yabplot.mesh import map_values_to_surface
|
|
5
|
+
from yabplot.utils import prep_data
|
|
6
|
+
|
|
7
|
+
def test_prep_data_array():
|
|
8
|
+
"""Verify that 1D arrays are correctly mapped to a dictionary by position."""
|
|
9
|
+
regions = ['A', 'B', 'C']
|
|
10
|
+
data = [1, 2, 3]
|
|
11
|
+
result = prep_data(data, regions, 'test_atlas', 'cortical')
|
|
12
|
+
assert isinstance(result, dict)
|
|
13
|
+
assert result['A'] == 1
|
|
14
|
+
assert result['C'] == 3
|
|
15
|
+
|
|
16
|
+
def test_prep_data_dict():
|
|
17
|
+
"""Verify that dictionaries are passed through correctly."""
|
|
18
|
+
regions = ['A', 'B', 'C']
|
|
19
|
+
data = {'A': 10, 'B': 20}
|
|
20
|
+
result = prep_data(data, regions, 'test_atlas', 'cortical')
|
|
21
|
+
assert isinstance(result, dict)
|
|
22
|
+
assert result['A'] == 10
|
|
23
|
+
assert result['B'] == 20
|
|
24
|
+
|
|
25
|
+
def test_prep_data_series():
|
|
26
|
+
"""Verify that pandas Series are correctly converted using their index."""
|
|
27
|
+
regions = ['A', 'B', 'C']
|
|
28
|
+
data = pd.Series([100, 200], index=['A', 'C'])
|
|
29
|
+
result = prep_data(data, regions, 'test_atlas', 'cortical')
|
|
30
|
+
assert result['A'] == 100
|
|
31
|
+
assert result['C'] == 200
|
|
32
|
+
|
|
33
|
+
def test_prep_data_dataframe():
|
|
34
|
+
"""Verify that pandas DataFrames are correctly handled by index or column mapping."""
|
|
35
|
+
regions = ['A', 'B', 'C']
|
|
36
|
+
# 1-col dataframe will use index as labels
|
|
37
|
+
data_1col = pd.DataFrame({'val': [5, 6]}, index=['B', 'A'])
|
|
38
|
+
result_1col = prep_data(data_1col, regions, 'test_atlas', 'cortical')
|
|
39
|
+
assert result_1col['A'] == 6
|
|
40
|
+
assert result_1col['B'] == 5
|
|
41
|
+
|
|
42
|
+
# 2-col dataframe will use first column as labels and second as values
|
|
43
|
+
df2 = pd.DataFrame({'Region': ['B', 'A'], 'Value': [50, 60]})
|
|
44
|
+
result_2col = prep_data(df2, regions, 'test_atlas', 'cortical')
|
|
45
|
+
assert result_2col['A'] == 60
|
|
46
|
+
assert result_2col['B'] == 50
|
|
47
|
+
|
|
48
|
+
def test_prep_data_length_mismatch():
|
|
49
|
+
"""Verify that an array length mismatch raises a ValueError."""
|
|
50
|
+
regions = ['A', 'B', 'C']
|
|
51
|
+
data = [1, 2] # length mismatch
|
|
52
|
+
with pytest.raises(ValueError, match="Data length mismatch"):
|
|
53
|
+
prep_data(data, regions, 'test_atlas', 'cortical')
|
|
54
|
+
|
|
55
|
+
def test_map_values_to_surface():
|
|
56
|
+
"""Verify that atlas scalar mapping correctly handles data injection and NaNs."""
|
|
57
|
+
target_labels = np.array([0, 1, 2, 1, 0, 3])
|
|
58
|
+
lut_ids = np.array([0, 1, 2, 3])
|
|
59
|
+
dense_lut_names = ['Region0', 'Region1', 'Region2', 'Region3']
|
|
60
|
+
|
|
61
|
+
# dict input
|
|
62
|
+
data_dict = {'Region1': 10.0, 'Region2': 20.0}
|
|
63
|
+
res_dict = map_values_to_surface(data_dict, target_labels, lut_ids, dense_lut_names)
|
|
64
|
+
assert np.isnan(res_dict[0]) # region 0 mapped to NaN
|
|
65
|
+
assert res_dict[1] == 10.0 # region 1 mapped to 10
|
|
66
|
+
assert res_dict[2] == 20.0 # region 2 mapped to 20
|
|
67
|
+
assert np.isnan(res_dict[5]) # target 3 -> region 3 mapped to NaN
|
|
68
|
+
|
|
69
|
+
# array input
|
|
70
|
+
data_arr = [0.0, 1.0, 2.0, 3.0]
|
|
71
|
+
res_arr = map_values_to_surface(data_arr, target_labels, lut_ids, dense_lut_names)
|
|
72
|
+
assert res_arr[0] == 0.0
|
|
73
|
+
assert res_arr[1] == 1.0
|
|
74
|
+
assert res_arr[5] == 3.0
|
|
75
|
+
|
|
76
|
+
# mismatch length array
|
|
77
|
+
with pytest.raises(ValueError):
|
|
78
|
+
map_values_to_surface([1.0, 2.0], target_labels, lut_ids, dense_lut_names)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pyvista as pv
|
|
4
|
+
from yabplot.mesh import load_nii_as_mesh, make_cortical_mesh
|
|
5
|
+
|
|
6
|
+
def test_load_nii_as_mesh(synthetic_nifti):
|
|
7
|
+
"""Verify that marching cubes mesh extraction works on a NIfTI volume."""
|
|
8
|
+
mesh = load_nii_as_mesh(synthetic_nifti, threshold=5.0, blur_sigma=0, smooth_i=0)
|
|
9
|
+
assert isinstance(mesh, pv.PolyData)
|
|
10
|
+
assert mesh.n_points > 0
|
|
11
|
+
assert mesh.n_cells > 0
|
|
12
|
+
|
|
13
|
+
def test_make_cortical_mesh():
|
|
14
|
+
"""Verify that a manual mesh can be constructed from vertex and face arrays."""
|
|
15
|
+
verts = np.array([[0,0,0], [1,0,0], [0,1,0], [0,0,1]], dtype=np.float32)
|
|
16
|
+
faces = np.array([[0,1,2], [0,1,3], [0,2,3], [1,2,3]], dtype=np.int64)
|
|
17
|
+
scalars = np.array([1, 2, 3, 4], dtype=np.float32)
|
|
18
|
+
mesh = make_cortical_mesh(verts, faces, scalars, scalar_name='TestData')
|
|
19
|
+
|
|
20
|
+
assert isinstance(mesh, pv.PolyData)
|
|
21
|
+
assert mesh.n_points == 4
|
|
22
|
+
assert mesh.n_cells == 4
|
|
23
|
+
assert 'TestData' in mesh.point_data
|
|
24
|
+
assert np.all(mesh.point_data['TestData'] == scalars)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import os
|
|
3
|
+
import numpy as np
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import yabplot as yab
|
|
6
|
+
import pyvista as pv
|
|
7
|
+
|
|
8
|
+
# tell PyVista to run in "off-screen" mode so it doesn't try to open a real window
|
|
9
|
+
pv.OFF_SCREEN = True
|
|
10
|
+
|
|
11
|
+
def test_version():
|
|
12
|
+
"""Check that the package has a version string."""
|
|
13
|
+
assert yab.__version__ is not None
|
|
14
|
+
|
|
15
|
+
def test_none_returns_empty_dict():
|
|
16
|
+
"""Verify that passing None disables the background mesh."""
|
|
17
|
+
result = yab.mesh.load_bmesh(None)
|
|
18
|
+
assert result == {}
|
|
19
|
+
|
|
20
|
+
def test_dict_passthrough():
|
|
21
|
+
"""Verify that custom dictionary keys for hemispheres are properly sanitized."""
|
|
22
|
+
mesh_l = pv.Sphere()
|
|
23
|
+
mesh_r = pv.Cube()
|
|
24
|
+
mesh_other = pv.Cone()
|
|
25
|
+
d = {'left': mesh_l, 'RIGHT': mesh_r, 'other': mesh_other}
|
|
26
|
+
result = yab.mesh.load_bmesh(d)
|
|
27
|
+
expected = {'L': mesh_l, 'R': mesh_r, 'other': mesh_other}
|
|
28
|
+
assert result == expected
|
|
29
|
+
|
|
30
|
+
def test_polydata_wrapped_in_both():
|
|
31
|
+
"""Verify that passing a single PyVista mesh safely wraps it."""
|
|
32
|
+
mesh = pv.Sphere()
|
|
33
|
+
result = yab.mesh.load_bmesh(mesh)
|
|
34
|
+
assert 'both' in result
|
|
35
|
+
assert result['both'] is mesh
|
|
36
|
+
|
|
37
|
+
def test_plotter_instantiation():
|
|
38
|
+
"""Smoke test: can we create a Plotter without crashing?"""
|
|
39
|
+
plotter = pv.Plotter(off_screen=True)
|
|
40
|
+
plotter.add_mesh(pv.Sphere())
|
|
41
|
+
plotter.show()
|
|
42
|
+
plotter.close()
|
|
43
|
+
|
|
44
|
+
def test_plot_cortical():
|
|
45
|
+
"""Smoke test: verify that plot_cortical renders with standard atlas."""
|
|
46
|
+
yab.plot_cortical(atlas='aparc', display_type='matplotlib')
|
|
47
|
+
|
|
48
|
+
def test_plot_subcortical():
|
|
49
|
+
"""Smoke test: verify that plot_subcortical renders with standard atlas."""
|
|
50
|
+
yab.plot_subcortical(atlas='aseg', display_type='matplotlib')
|
|
51
|
+
|
|
52
|
+
def test_plot_tracts():
|
|
53
|
+
"""Smoke test: verify that plot_tracts renders with standard atlas."""
|
|
54
|
+
yab.plot_tracts(atlas='xtract_tiny', display_type='matplotlib')
|
|
55
|
+
|
|
56
|
+
def test_plot_vertexwise():
|
|
57
|
+
"""Smoke test: verify that plot_vertexwise renders with custom meshes."""
|
|
58
|
+
lh = pv.Sphere()
|
|
59
|
+
rh = pv.Sphere()
|
|
60
|
+
lh['Data'] = np.random.rand(lh.n_points)
|
|
61
|
+
rh['Data'] = np.random.rand(rh.n_points)
|
|
62
|
+
yab.plot_vertexwise(lh, rh, display_type='matplotlib')
|
|
63
|
+
|
|
64
|
+
def test_plot_voxelwise(synthetic_nifti):
|
|
65
|
+
"""Smoke test: verify that plot_voxelwise renders with synthetic volume."""
|
|
66
|
+
yab.plot_voxelwise(synthetic_nifti, threshold=5.0, blur_sigma=0, display_type='matplotlib')
|
|
67
|
+
|
|
68
|
+
def test_plot_connectome():
|
|
69
|
+
"""Smoke test: verify that plot_connectome renders both nodes-only and matrix modes."""
|
|
70
|
+
yab.plot_connectome(atlas='aparc', display_type='matplotlib')
|
|
71
|
+
# plotting with a dummy matrix
|
|
72
|
+
regions = yab.get_atlas_regions('aparc', 'cortical')
|
|
73
|
+
n = len(regions)
|
|
74
|
+
matrix = np.random.rand(n, n)
|
|
75
|
+
yab.plot_connectome(matrix=matrix, atlas='aparc', edge_threshold=0.5, display_type='matplotlib')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_matplotlib_ax_compatibility():
|
|
79
|
+
"""Verify that plotting on a provided Matplotlib axis works correctly."""
|
|
80
|
+
fig, ax = plt.subplots()
|
|
81
|
+
ret_ax = yab.plot_cortical(atlas='aparc', ax=ax, display_type='matplotlib')
|
|
82
|
+
assert ret_ax is ax
|
|
83
|
+
# verify ax is usable
|
|
84
|
+
ret_ax.set_title("Test Title")
|
|
85
|
+
assert ret_ax.get_title() == "Test Title"
|
|
86
|
+
plt.close(fig)
|
|
87
|
+
|
|
88
|
+
def test_export_path(tmp_path):
|
|
89
|
+
"""Verify that export_path correctly saves a file to disk."""
|
|
90
|
+
out_file = tmp_path / "test_export.png"
|
|
91
|
+
yab.plot_cortical(atlas='aparc', display_type='matplotlib', export_path=str(out_file))
|
|
92
|
+
assert out_file.exists()
|
|
93
|
+
assert out_file.stat().st_size > 0
|