junifer 0.0.6.dev175__py3-none-any.whl → 0.0.6.dev201__py3-none-any.whl
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.
- junifer/_version.py +2 -2
- junifer/data/__init__.pyi +17 -31
- junifer/data/_dispatch.py +251 -0
- junifer/data/coordinates/__init__.py +9 -0
- junifer/data/coordinates/__init__.pyi +5 -0
- junifer/data/coordinates/_ants_coordinates_warper.py +96 -0
- junifer/data/coordinates/_coordinates.py +356 -0
- junifer/data/coordinates/_fsl_coordinates_warper.py +83 -0
- junifer/data/{tests → coordinates/tests}/test_coordinates.py +25 -31
- junifer/data/masks/__init__.py +9 -0
- junifer/data/masks/__init__.pyi +6 -0
- junifer/data/masks/_ants_mask_warper.py +144 -0
- junifer/data/masks/_fsl_mask_warper.py +87 -0
- junifer/data/masks/_masks.py +624 -0
- junifer/data/{tests → masks/tests}/test_masks.py +63 -58
- junifer/data/parcellations/__init__.py +9 -0
- junifer/data/parcellations/__init__.pyi +6 -0
- junifer/data/parcellations/_ants_parcellation_warper.py +154 -0
- junifer/data/parcellations/_fsl_parcellation_warper.py +91 -0
- junifer/data/{parcellations.py → parcellations/_parcellations.py} +450 -473
- junifer/data/{tests → parcellations/tests}/test_parcellations.py +73 -81
- junifer/data/pipeline_data_registry_base.py +74 -0
- junifer/data/utils.py +4 -0
- junifer/markers/complexity/hurst_exponent.py +2 -2
- junifer/markers/complexity/multiscale_entropy_auc.py +2 -2
- junifer/markers/complexity/perm_entropy.py +2 -2
- junifer/markers/complexity/range_entropy.py +2 -2
- junifer/markers/complexity/range_entropy_auc.py +2 -2
- junifer/markers/complexity/sample_entropy.py +2 -2
- junifer/markers/complexity/weighted_perm_entropy.py +2 -2
- junifer/markers/ets_rss.py +2 -2
- junifer/markers/falff/falff_parcels.py +2 -2
- junifer/markers/falff/falff_spheres.py +2 -2
- junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +1 -1
- junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py +1 -1
- junifer/markers/functional_connectivity/functional_connectivity_parcels.py +1 -1
- junifer/markers/functional_connectivity/functional_connectivity_spheres.py +1 -1
- junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +3 -3
- junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +2 -2
- junifer/markers/parcel_aggregation.py +11 -7
- junifer/markers/reho/reho_parcels.py +2 -2
- junifer/markers/reho/reho_spheres.py +2 -2
- junifer/markers/sphere_aggregation.py +11 -7
- junifer/markers/temporal_snr/temporal_snr_parcels.py +2 -2
- junifer/markers/temporal_snr/temporal_snr_spheres.py +2 -2
- junifer/markers/tests/test_ets_rss.py +3 -3
- junifer/markers/tests/test_parcel_aggregation.py +24 -24
- junifer/markers/tests/test_sphere_aggregation.py +6 -6
- junifer/pipeline/pipeline_component_registry.py +1 -1
- junifer/preprocess/confounds/fmriprep_confound_remover.py +6 -3
- {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/METADATA +1 -1
- {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/RECORD +76 -62
- {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/WHEEL +1 -1
- junifer/data/coordinates.py +0 -408
- junifer/data/masks.py +0 -670
- /junifer/data/{VOIs → coordinates/VOIs}/meta/AutobiographicalMemory_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/CogAC_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/CogAR_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/DMNBuckner_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/Dosenbach2010_MNI_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/Empathy_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/Motor_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/MultiTask_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/PhysioStress_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/Power2011_MNI_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/Power2013_MNI_VOIs.tsv +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/Rew_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/Somatosensory_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/ToM_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/VigAtt_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/WM_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/eMDN_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/eSAD_VOIs.txt +0 -0
- /junifer/data/{VOIs → coordinates/VOIs}/meta/extDMN_VOIs.txt +0 -0
- {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/AUTHORS.rst +0 -0
- {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/LICENSE.md +0 -0
- {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/entry_points.txt +0 -0
- {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
"""
|
1
|
+
"""Provide class and function for parcellation registry and manipulation."""
|
2
2
|
|
3
3
|
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
4
4
|
# Vera Komeyer <v.komeyer@fz-juelich.de>
|
@@ -9,8 +9,8 @@ import io
|
|
9
9
|
import shutil
|
10
10
|
import tarfile
|
11
11
|
import tempfile
|
12
|
-
import typing
|
13
12
|
import zipfile
|
13
|
+
from itertools import product
|
14
14
|
from pathlib import Path
|
15
15
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
16
16
|
|
@@ -20,510 +20,487 @@ import numpy as np
|
|
20
20
|
import pandas as pd
|
21
21
|
from nilearn import datasets, image
|
22
22
|
|
23
|
-
from
|
24
|
-
from
|
25
|
-
from
|
26
|
-
from
|
23
|
+
from ...pipeline.singleton import singleton
|
24
|
+
from ...utils import logger, raise_error, warn_with_log
|
25
|
+
from ..pipeline_data_registry_base import BasePipelineDataRegistry
|
26
|
+
from ..utils import closest_resolution
|
27
|
+
from ._ants_parcellation_warper import ANTsParcellationWarper
|
28
|
+
from ._fsl_parcellation_warper import FSLParcellationWarper
|
27
29
|
|
28
30
|
|
29
31
|
if TYPE_CHECKING:
|
30
|
-
from nibabel import Nifti1Image
|
32
|
+
from nibabel.nifti1 import Nifti1Image
|
31
33
|
|
32
34
|
|
33
35
|
__all__ = [
|
34
|
-
"
|
35
|
-
"list_parcellations",
|
36
|
-
"get_parcellation",
|
37
|
-
"load_parcellation",
|
36
|
+
"ParcellationRegistry",
|
38
37
|
"merge_parcellations",
|
39
38
|
]
|
40
39
|
|
41
40
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
# Each entry is a dictionary that must contain at least the following keys:
|
46
|
-
# * 'family': the parcellation's family name (e.g. 'Schaefer', 'SUIT')
|
47
|
-
# * 'space': the parcellation's space (e.g., 'MNI', 'SUIT')
|
48
|
-
|
49
|
-
# Optional keys:
|
50
|
-
# * 'valid_resolutions': a list of valid resolutions for the parcellation
|
51
|
-
# (e.g. [1, 2])
|
52
|
-
|
53
|
-
# TODO: have separate dictionary for built-in
|
54
|
-
_available_parcellations: Dict[str, Dict[Any, Any]] = {
|
55
|
-
"SUITxSUIT": {"family": "SUIT", "space": "SUIT"},
|
56
|
-
"SUITxMNI": {"family": "SUIT", "space": "MNI152NLin6Asym"},
|
57
|
-
}
|
58
|
-
|
59
|
-
# Add Schaefer parcellation info
|
60
|
-
for n_rois in range(100, 1001, 100):
|
61
|
-
for t_net in [7, 17]:
|
62
|
-
t_name = f"Schaefer{n_rois}x{t_net}"
|
63
|
-
_available_parcellations[t_name] = {
|
64
|
-
"family": "Schaefer",
|
65
|
-
"n_rois": n_rois,
|
66
|
-
"yeo_networks": t_net,
|
67
|
-
"space": "MNI152NLin6Asym",
|
68
|
-
}
|
69
|
-
# Add Tian parcellation info
|
70
|
-
for scale in range(1, 5):
|
71
|
-
t_name = f"TianxS{scale}x7TxMNI6thgeneration"
|
72
|
-
_available_parcellations[t_name] = {
|
73
|
-
"family": "Tian",
|
74
|
-
"scale": scale,
|
75
|
-
"magneticfield": "7T",
|
76
|
-
"space": "MNI152NLin6Asym",
|
77
|
-
}
|
78
|
-
t_name = f"TianxS{scale}x3TxMNI6thgeneration"
|
79
|
-
_available_parcellations[t_name] = {
|
80
|
-
"family": "Tian",
|
81
|
-
"scale": scale,
|
82
|
-
"magneticfield": "3T",
|
83
|
-
"space": "MNI152NLin6Asym",
|
84
|
-
}
|
85
|
-
t_name = f"TianxS{scale}x3TxMNInonlinear2009cAsym"
|
86
|
-
_available_parcellations[t_name] = {
|
87
|
-
"family": "Tian",
|
88
|
-
"scale": scale,
|
89
|
-
"magneticfield": "3T",
|
90
|
-
"space": "MNI152NLin2009cAsym",
|
91
|
-
}
|
92
|
-
# Add AICHA parcellation info
|
93
|
-
for version in (1, 2):
|
94
|
-
_available_parcellations[f"AICHA_v{version}"] = {
|
95
|
-
"family": "AICHA",
|
96
|
-
"version": version,
|
97
|
-
"space": "IXI549Space",
|
98
|
-
}
|
99
|
-
# Add Shen parcellation info
|
100
|
-
for year in (2013, 2015, 2019):
|
101
|
-
if year == 2013:
|
102
|
-
for n_rois in (50, 100, 150):
|
103
|
-
_available_parcellations[f"Shen_{year}_{n_rois}"] = {
|
104
|
-
"family": "Shen",
|
105
|
-
"year": 2013,
|
106
|
-
"n_rois": n_rois,
|
107
|
-
"space": "MNI152NLin2009cAsym",
|
108
|
-
}
|
109
|
-
elif year == 2015:
|
110
|
-
_available_parcellations["Shen_2015_268"] = {
|
111
|
-
"family": "Shen",
|
112
|
-
"year": 2015,
|
113
|
-
"n_rois": 268,
|
114
|
-
"space": "MNI152NLin2009cAsym",
|
115
|
-
}
|
116
|
-
elif year == 2019:
|
117
|
-
_available_parcellations["Shen_2019_368"] = {
|
118
|
-
"family": "Shen",
|
119
|
-
"year": 2019,
|
120
|
-
"n_rois": 368,
|
121
|
-
"space": "MNI152NLin2009cAsym",
|
122
|
-
}
|
123
|
-
# Add Yan parcellation info
|
124
|
-
for n_rois in range(100, 1001, 100):
|
125
|
-
# Add Yeo networks
|
126
|
-
for yeo_network in [7, 17]:
|
127
|
-
_available_parcellations[f"Yan{n_rois}xYeo{yeo_network}"] = {
|
128
|
-
"family": "Yan",
|
129
|
-
"n_rois": n_rois,
|
130
|
-
"yeo_networks": yeo_network,
|
131
|
-
"space": "MNI152NLin6Asym",
|
132
|
-
}
|
133
|
-
# Add Kong networks
|
134
|
-
_available_parcellations[f"Yan{n_rois}xKong17"] = {
|
135
|
-
"family": "Yan",
|
136
|
-
"n_rois": n_rois,
|
137
|
-
"kong_networks": 17,
|
138
|
-
"space": "MNI152NLin6Asym",
|
139
|
-
}
|
140
|
-
# Add Brainnetome parcellation info
|
141
|
-
for threshold in [0, 25, 50]:
|
142
|
-
_available_parcellations[f"Brainnetome_thr{threshold}"] = {
|
143
|
-
"family": "Brainnetome",
|
144
|
-
"threshold": threshold,
|
145
|
-
"space": "MNI152NLin6Asym",
|
146
|
-
}
|
147
|
-
|
148
|
-
|
149
|
-
def register_parcellation(
|
150
|
-
name: str,
|
151
|
-
parcellation_path: Union[str, Path],
|
152
|
-
parcels_labels: List[str],
|
153
|
-
space: str,
|
154
|
-
overwrite: bool = False,
|
155
|
-
) -> None:
|
156
|
-
"""Register a custom user parcellation.
|
157
|
-
|
158
|
-
Parameters
|
159
|
-
----------
|
160
|
-
name : str
|
161
|
-
The name of the parcellation.
|
162
|
-
parcellation_path : str or pathlib.Path
|
163
|
-
The path to the parcellation file.
|
164
|
-
parcels_labels : list of str
|
165
|
-
The list of labels for the parcellation.
|
166
|
-
space : str
|
167
|
-
The template space of the parcellation, for e.g., "MNI152NLin6Asym".
|
168
|
-
overwrite : bool, optional
|
169
|
-
If True, overwrite an existing parcellation with the same name.
|
170
|
-
Does not apply to built-in parcellations (default False).
|
171
|
-
|
172
|
-
Raises
|
173
|
-
------
|
174
|
-
ValueError
|
175
|
-
If the parcellation name is already registered and overwrite is set to
|
176
|
-
False or if the parcellation name is a built-in parcellation.
|
177
|
-
|
178
|
-
"""
|
179
|
-
# Check for attempt of overwriting built-in parcellations
|
180
|
-
if name in _available_parcellations:
|
181
|
-
if overwrite is True:
|
182
|
-
logger.info(f"Overwriting {name} parcellation")
|
183
|
-
if (
|
184
|
-
_available_parcellations[name]["family"]
|
185
|
-
!= "CustomUserParcellation"
|
186
|
-
):
|
187
|
-
raise_error(
|
188
|
-
f"Cannot overwrite {name} parcellation. "
|
189
|
-
"It is a built-in parcellation."
|
190
|
-
)
|
191
|
-
else:
|
192
|
-
raise_error(
|
193
|
-
f"Parcellation {name} already registered. Set "
|
194
|
-
"`overwrite=True` to update its value."
|
195
|
-
)
|
196
|
-
# Convert str to Path
|
197
|
-
if not isinstance(parcellation_path, Path):
|
198
|
-
parcellation_path = Path(parcellation_path)
|
199
|
-
# Add user parcellation info
|
200
|
-
_available_parcellations[name] = {
|
201
|
-
"path": str(parcellation_path.absolute()),
|
202
|
-
"labels": parcels_labels,
|
203
|
-
"family": "CustomUserParcellation",
|
204
|
-
"space": space,
|
205
|
-
}
|
206
|
-
|
207
|
-
|
208
|
-
def list_parcellations() -> List[str]:
|
209
|
-
"""List all the available parcellations.
|
210
|
-
|
211
|
-
Returns
|
212
|
-
-------
|
213
|
-
list of str
|
214
|
-
A list with all available parcellations.
|
215
|
-
|
216
|
-
"""
|
217
|
-
return sorted(_available_parcellations.keys())
|
218
|
-
|
219
|
-
|
220
|
-
def get_parcellation(
|
221
|
-
parcellation: List[str],
|
222
|
-
target_data: Dict[str, Any],
|
223
|
-
extra_input: Optional[Dict[str, Any]] = None,
|
224
|
-
) -> Tuple["Nifti1Image", List[str]]:
|
225
|
-
"""Get parcellation, tailored for the target image.
|
41
|
+
@singleton
|
42
|
+
class ParcellationRegistry(BasePipelineDataRegistry):
|
43
|
+
"""Class for parcellation data registry.
|
226
44
|
|
227
|
-
|
228
|
-
|
229
|
-
parcellation : list of str
|
230
|
-
The name(s) of the parcellation(s).
|
231
|
-
target_data : dict
|
232
|
-
The corresponding item of the data object to which the parcellation
|
233
|
-
will be applied.
|
234
|
-
extra_input : dict, optional
|
235
|
-
The other fields in the data object. Useful for accessing other data
|
236
|
-
kinds that needs to be used in the computation of parcellations
|
237
|
-
(default None).
|
238
|
-
|
239
|
-
Returns
|
240
|
-
-------
|
241
|
-
Nifti1Image
|
242
|
-
The parcellation image.
|
243
|
-
list of str
|
244
|
-
Parcellation labels.
|
245
|
-
|
246
|
-
Raises
|
247
|
-
------
|
248
|
-
RuntimeError
|
249
|
-
If warp / transformation file extension is not ".mat" or ".h5".
|
250
|
-
ValueError
|
251
|
-
If ``extra_input`` is None when ``target_data``'s space is native.
|
45
|
+
This class is a singleton and is used for managing available parcellation
|
46
|
+
data in a centralized manner.
|
252
47
|
|
253
48
|
"""
|
254
|
-
# Check pre-requirements for space manipulation
|
255
|
-
target_space = target_data["space"]
|
256
|
-
# Set target standard space to target space
|
257
|
-
target_std_space = target_space
|
258
|
-
# Extra data type requirement check if target space is native
|
259
|
-
if target_space == "native":
|
260
|
-
# Check for extra inputs
|
261
|
-
if extra_input is None:
|
262
|
-
raise_error(
|
263
|
-
"No extra input provided, requires `Warp` and `T1w` "
|
264
|
-
"data types in particular for transformation to "
|
265
|
-
f"{target_data['space']} space for further computation."
|
266
|
-
)
|
267
|
-
# Set target standard space to warp file space source
|
268
|
-
target_std_space = extra_input["Warp"]["src"]
|
269
|
-
|
270
|
-
# Get the min of the voxels sizes and use it as the resolution
|
271
|
-
target_img = target_data["data"]
|
272
|
-
resolution = np.min(target_img.header.get_zooms()[:3])
|
273
|
-
|
274
|
-
# Create component-scoped tempdir
|
275
|
-
tempdir = WorkDirManager().get_tempdir(prefix="parcellations")
|
276
|
-
# Create element-scoped tempdir so that warped parcellation is
|
277
|
-
# available later as nibabel stores file path reference for
|
278
|
-
# loading on computation
|
279
|
-
element_tempdir = WorkDirManager().get_element_tempdir(
|
280
|
-
prefix="parcellations"
|
281
|
-
)
|
282
49
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
50
|
+
def __init__(self) -> None:
|
51
|
+
"""Initialize the class."""
|
52
|
+
# Each entry in registry is a dictionary that must contain at least
|
53
|
+
# the following keys:
|
54
|
+
# * 'family': the parcellation's family name (e.g., 'Schaefer', 'SUIT')
|
55
|
+
# * 'space': the parcellation's space (e.g., 'MNI', 'SUIT')
|
56
|
+
# and can also have optional key(s):
|
57
|
+
# * 'valid_resolutions': a list of valid resolutions for the
|
58
|
+
# parcellation (e.g., [1, 2])
|
59
|
+
# Make built-in and external dictionaries for validation later
|
60
|
+
self._builtin = {}
|
61
|
+
self._external = {}
|
62
|
+
|
63
|
+
# Add SUIT
|
64
|
+
self._builtin.update(
|
65
|
+
{
|
66
|
+
"SUITxSUIT": {"family": "SUIT", "space": "SUIT"},
|
67
|
+
"SUITxMNI": {"family": "SUIT", "space": "MNI152NLin6Asym"},
|
68
|
+
}
|
290
69
|
)
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
70
|
+
# Add Schaefer
|
71
|
+
for n_rois, t_net in product(range(100, 1001, 100), [7, 17]):
|
72
|
+
self._builtin.update(
|
73
|
+
{
|
74
|
+
f"Schaefer{n_rois}x{t_net}": {
|
75
|
+
"family": "Schaefer",
|
76
|
+
"n_rois": n_rois,
|
77
|
+
"yeo_networks": t_net,
|
78
|
+
"space": "MNI152NLin6Asym",
|
79
|
+
},
|
80
|
+
}
|
301
81
|
)
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
82
|
+
# Add Tian
|
83
|
+
for scale in range(1, 5):
|
84
|
+
self._builtin.update(
|
85
|
+
{
|
86
|
+
f"TianxS{scale}x7TxMNI6thgeneration": {
|
87
|
+
"family": "Tian",
|
88
|
+
"scale": scale,
|
89
|
+
"magneticfield": "7T",
|
90
|
+
"space": "MNI152NLin6Asym",
|
91
|
+
},
|
92
|
+
f"TianxS{scale}x3TxMNI6thgeneration": {
|
93
|
+
"family": "Tian",
|
94
|
+
"scale": scale,
|
95
|
+
"magneticfield": "3T",
|
96
|
+
"space": "MNI152NLin6Asym",
|
97
|
+
},
|
98
|
+
f"TianxS{scale}x3TxMNInonlinear2009cAsym": {
|
99
|
+
"family": "Tian",
|
100
|
+
"scale": scale,
|
101
|
+
"magneticfield": "3T",
|
102
|
+
"space": "MNI152NLin2009cAsym",
|
103
|
+
},
|
104
|
+
}
|
310
105
|
)
|
311
|
-
|
312
|
-
|
106
|
+
# Add AICHA
|
107
|
+
for version in (1, 2):
|
108
|
+
self._builtin.update(
|
109
|
+
{
|
110
|
+
f"AICHA_v{version}": {
|
111
|
+
"family": "AICHA",
|
112
|
+
"version": version,
|
113
|
+
"space": "IXI549Space",
|
114
|
+
},
|
115
|
+
}
|
313
116
|
)
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
117
|
+
# Add Shen
|
118
|
+
for year in (2013, 2015, 2019):
|
119
|
+
if year == 2013:
|
120
|
+
for n_rois in (50, 100, 150):
|
121
|
+
self._builtin.update(
|
122
|
+
{
|
123
|
+
f"Shen_{year}_{n_rois}": {
|
124
|
+
"family": "Shen",
|
125
|
+
"year": 2013,
|
126
|
+
"n_rois": n_rois,
|
127
|
+
"space": "MNI152NLin2009cAsym",
|
128
|
+
},
|
129
|
+
}
|
130
|
+
)
|
131
|
+
elif year == 2015:
|
132
|
+
self._builtin.update(
|
133
|
+
{
|
134
|
+
"Shen_2015_268": {
|
135
|
+
"family": "Shen",
|
136
|
+
"year": 2015,
|
137
|
+
"n_rois": 268,
|
138
|
+
"space": "MNI152NLin2009cAsym",
|
139
|
+
},
|
140
|
+
}
|
141
|
+
)
|
142
|
+
elif year == 2019:
|
143
|
+
self._builtin.update(
|
144
|
+
{
|
145
|
+
"Shen_2019_368": {
|
146
|
+
"family": "Shen",
|
147
|
+
"year": 2019,
|
148
|
+
"n_rois": 368,
|
149
|
+
"space": "MNI152NLin2009cAsym",
|
150
|
+
},
|
151
|
+
}
|
152
|
+
)
|
153
|
+
# Add Yan
|
154
|
+
for n_rois, yeo_network in product(range(100, 1001, 100), [7, 17]):
|
155
|
+
self._builtin.update(
|
156
|
+
{
|
157
|
+
f"Yan{n_rois}xYeo{yeo_network}": {
|
158
|
+
"family": "Yan",
|
159
|
+
"n_rois": n_rois,
|
160
|
+
"yeo_networks": yeo_network,
|
161
|
+
"space": "MNI152NLin6Asym",
|
162
|
+
},
|
163
|
+
}
|
318
164
|
)
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
165
|
+
self._builtin.update(
|
166
|
+
{
|
167
|
+
f"Yan{n_rois}xKong17": {
|
168
|
+
"family": "Yan",
|
169
|
+
"n_rois": n_rois,
|
170
|
+
"kong_networks": 17,
|
171
|
+
"space": "MNI152NLin6Asym",
|
172
|
+
},
|
173
|
+
}
|
174
|
+
)
|
175
|
+
# Add Brainnetome
|
176
|
+
for threshold in [0, 25, 50]:
|
177
|
+
self._builtin.update(
|
178
|
+
{
|
179
|
+
f"Brainnetome_thr{threshold}": {
|
180
|
+
"family": "Brainnetome",
|
181
|
+
"threshold": threshold,
|
182
|
+
"space": "MNI152NLin6Asym",
|
183
|
+
},
|
184
|
+
}
|
323
185
|
)
|
324
|
-
# Set antsApplyTransforms command
|
325
|
-
apply_transforms_cmd = [
|
326
|
-
"antsApplyTransforms",
|
327
|
-
"-d 3",
|
328
|
-
"-e 3",
|
329
|
-
"-n 'GenericLabel[NearestNeighbor]'",
|
330
|
-
f"-i {parcellation_path.resolve()}",
|
331
|
-
f"-r {target_std_space_template_path.resolve()}",
|
332
|
-
f"-t {xfm_file_path.resolve()}",
|
333
|
-
f"-o {warped_parcellation_path.resolve()}",
|
334
|
-
]
|
335
|
-
# Call antsApplyTransforms
|
336
|
-
run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd)
|
337
|
-
|
338
|
-
raw_img = nib.load(warped_parcellation_path)
|
339
|
-
# Remove extra dimension added by ANTs
|
340
|
-
img = image.math_img("np.squeeze(img)", img=raw_img)
|
341
|
-
|
342
|
-
# Resample parcellation to target image
|
343
|
-
img_to_merge = image.resample_to_img(
|
344
|
-
source_img=img,
|
345
|
-
target_img=target_img,
|
346
|
-
interpolation="nearest",
|
347
|
-
copy=True,
|
348
|
-
)
|
349
|
-
|
350
|
-
all_parcellations.append(img_to_merge)
|
351
|
-
all_labels.append(labels)
|
352
|
-
|
353
|
-
# Avoid merging if there is only one parcellation
|
354
|
-
if len(all_parcellations) == 1:
|
355
|
-
resampled_parcellation_img = all_parcellations[0]
|
356
|
-
labels = all_labels[0]
|
357
|
-
# Parcellations are already transformed to target standard space
|
358
|
-
else:
|
359
|
-
resampled_parcellation_img, labels = merge_parcellations(
|
360
|
-
parcellations_list=all_parcellations,
|
361
|
-
parcellations_names=parcellation,
|
362
|
-
labels_lists=all_labels,
|
363
|
-
)
|
364
|
-
|
365
|
-
# Warp parcellation if target space is native
|
366
|
-
if target_space == "native":
|
367
|
-
# Save parcellation image to a component-scoped tempfile
|
368
|
-
prewarp_parcellation_path = tempdir / "prewarp_parcellation.nii.gz"
|
369
|
-
nib.save(resampled_parcellation_img, prewarp_parcellation_path)
|
370
|
-
|
371
|
-
# Create an element-scoped tempfile for warped output
|
372
|
-
warped_parcellation_path = (
|
373
|
-
element_tempdir / "parcellation_warped.nii.gz"
|
374
|
-
)
|
375
186
|
|
376
|
-
#
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
187
|
+
# Set built-in to registry
|
188
|
+
self._registry = self._builtin
|
189
|
+
|
190
|
+
def register(
|
191
|
+
self,
|
192
|
+
name: str,
|
193
|
+
parcellation_path: Union[str, Path],
|
194
|
+
parcels_labels: List[str],
|
195
|
+
space: str,
|
196
|
+
overwrite: bool = False,
|
197
|
+
) -> None:
|
198
|
+
"""Register a custom user parcellation.
|
199
|
+
|
200
|
+
Parameters
|
201
|
+
----------
|
202
|
+
name : str
|
203
|
+
The name of the parcellation.
|
204
|
+
parcellation_path : str or pathlib.Path
|
205
|
+
The path to the parcellation file.
|
206
|
+
parcels_labels : list of str
|
207
|
+
The list of labels for the parcellation.
|
208
|
+
space : str
|
209
|
+
The template space of the parcellation, e.g., "MNI152NLin6Asym".
|
210
|
+
overwrite : bool, optional
|
211
|
+
If True, overwrite an existing parcellation with the same name.
|
212
|
+
Does not apply to built-in parcellations (default False).
|
213
|
+
|
214
|
+
Raises
|
215
|
+
------
|
216
|
+
ValueError
|
217
|
+
If the parcellation ``name`` is already registered and
|
218
|
+
``overwrite=False`` or
|
219
|
+
if the parcellation ``name`` is a built-in parcellation.
|
220
|
+
|
221
|
+
"""
|
222
|
+
# Check for attempt of overwriting built-in parcellations
|
223
|
+
if name in self._builtin:
|
224
|
+
if overwrite:
|
225
|
+
logger.info(f"Overwriting parcellation: {name}")
|
226
|
+
if self._registry[name]["family"] != "CustomUserParcellation":
|
227
|
+
raise_error(
|
228
|
+
f"Parcellation: {name} already registered as "
|
229
|
+
"built-in parcellation."
|
230
|
+
)
|
231
|
+
else:
|
232
|
+
raise_error(
|
233
|
+
f"Parcellation: {name} already registered. Set "
|
234
|
+
"`overwrite=True` to update its value."
|
235
|
+
)
|
236
|
+
# Convert str to Path
|
237
|
+
if not isinstance(parcellation_path, Path):
|
238
|
+
parcellation_path = Path(parcellation_path)
|
239
|
+
logger.info(f"Registering parcellation: {name}")
|
240
|
+
# Add user parcellation info
|
241
|
+
self._external[name] = {
|
242
|
+
"path": parcellation_path,
|
243
|
+
"labels": parcels_labels,
|
244
|
+
"family": "CustomUserParcellation",
|
245
|
+
"space": space,
|
246
|
+
}
|
247
|
+
# Update registry
|
248
|
+
self._registry[name] = {
|
249
|
+
"path": parcellation_path,
|
250
|
+
"labels": parcels_labels,
|
251
|
+
"family": "CustomUserParcellation",
|
252
|
+
"space": space,
|
253
|
+
}
|
409
254
|
|
410
|
-
|
255
|
+
def deregister(self, name: str) -> None:
|
256
|
+
"""De-register a custom user parcellation.
|
257
|
+
|
258
|
+
Parameters
|
259
|
+
----------
|
260
|
+
name : str
|
261
|
+
The name of the parcellation.
|
262
|
+
|
263
|
+
"""
|
264
|
+
logger.info(f"De-registering parcellation: {name}")
|
265
|
+
# Remove parcellation info
|
266
|
+
_ = self._external.pop(name)
|
267
|
+
# Update registry
|
268
|
+
_ = self._registry.pop(name)
|
269
|
+
|
270
|
+
def load(
|
271
|
+
self,
|
272
|
+
name: str,
|
273
|
+
parcellations_dir: Union[str, Path, None] = None,
|
274
|
+
resolution: Optional[float] = None,
|
275
|
+
path_only: bool = False,
|
276
|
+
) -> Tuple[Optional["Nifti1Image"], List[str], Path, str]:
|
277
|
+
"""Load parcellation and labels.
|
278
|
+
|
279
|
+
If it is a built-in parcellation and the file is not present in the
|
280
|
+
``parcellations_dir`` directory, it will be downloaded.
|
281
|
+
|
282
|
+
Parameters
|
283
|
+
----------
|
284
|
+
name : str
|
285
|
+
The name of the parcellation.
|
286
|
+
parcellations_dir : str or pathlib.Path, optional
|
287
|
+
Path where the parcellations files are stored. The default location
|
288
|
+
is "$HOME/junifer/data/parcellations" (default None).
|
289
|
+
resolution : float, optional
|
290
|
+
The desired resolution of the parcellation to load. If it is not
|
291
|
+
available, the closest resolution will be loaded. Preferably, use a
|
292
|
+
resolution higher than the desired one. By default, will load the
|
293
|
+
highest one (default None).
|
294
|
+
path_only : bool, optional
|
295
|
+
If True, the parcellation image will not be loaded (default False).
|
296
|
+
|
297
|
+
Returns
|
298
|
+
-------
|
299
|
+
Nifti1Image or None
|
300
|
+
Loaded parcellation image.
|
301
|
+
list of str
|
302
|
+
Parcellation labels.
|
303
|
+
pathlib.Path
|
304
|
+
File path to the parcellation image.
|
305
|
+
str
|
306
|
+
The space of the parcellation.
|
307
|
+
|
308
|
+
Raises
|
309
|
+
------
|
310
|
+
ValueError
|
311
|
+
If ``name`` is invalid or
|
312
|
+
if the parcellation values and labels
|
313
|
+
don't have equal dimension or if the value range is invalid.
|
314
|
+
|
315
|
+
"""
|
316
|
+
# Check for valid parcellation name
|
317
|
+
if name not in self._registry:
|
411
318
|
raise_error(
|
412
|
-
|
413
|
-
|
414
|
-
f"{warp_file_ext}"
|
415
|
-
),
|
416
|
-
klass=RuntimeError,
|
319
|
+
f"Parcellation: {name} not found. "
|
320
|
+
f"Valid options are: {self.list}"
|
417
321
|
)
|
418
322
|
|
419
|
-
#
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
def load_parcellation(
|
429
|
-
name: str,
|
430
|
-
parcellations_dir: Union[str, Path, None] = None,
|
431
|
-
resolution: Optional[float] = None,
|
432
|
-
path_only: bool = False,
|
433
|
-
) -> Tuple[Optional["Nifti1Image"], List[str], Path, str]:
|
434
|
-
"""Load a brain parcellation (including a label file).
|
435
|
-
|
436
|
-
If it is a built-in parcellation and the file is not present in the
|
437
|
-
``parcellations_dir`` directory, it will be downloaded.
|
438
|
-
|
439
|
-
Parameters
|
440
|
-
----------
|
441
|
-
name : str
|
442
|
-
The name of the parcellation. Check valid options by calling
|
443
|
-
:func:`.list_parcellations`.
|
444
|
-
parcellations_dir : str or pathlib.Path, optional
|
445
|
-
Path where the parcellations files are stored. The default location is
|
446
|
-
"$HOME/junifer/data/parcellations" (default None).
|
447
|
-
resolution : float, optional
|
448
|
-
The desired resolution of the parcellation to load. If it is not
|
449
|
-
available, the closest resolution will be loaded. Preferably, use a
|
450
|
-
resolution higher than the desired one. By default, will load the
|
451
|
-
highest one (default None).
|
452
|
-
path_only : bool, optional
|
453
|
-
If True, the parcellation image will not be loaded (default False).
|
323
|
+
# Copy parcellation definition to avoid edits in original object
|
324
|
+
parcellation_definition = self._registry[name].copy()
|
325
|
+
t_family = parcellation_definition.pop("family")
|
326
|
+
# Remove space conditionally
|
327
|
+
if t_family not in ["SUIT", "Tian"]:
|
328
|
+
space = parcellation_definition.pop("space")
|
329
|
+
else:
|
330
|
+
space = parcellation_definition["space"]
|
454
331
|
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
332
|
+
# Check if the parcellation family is custom or built-in
|
333
|
+
if t_family == "CustomUserParcellation":
|
334
|
+
parcellation_fname = Path(parcellation_definition["path"])
|
335
|
+
parcellation_labels = parcellation_definition["labels"]
|
336
|
+
else:
|
337
|
+
parcellation_fname, parcellation_labels = _retrieve_parcellation(
|
338
|
+
family=t_family,
|
339
|
+
parcellations_dir=parcellations_dir,
|
340
|
+
resolution=resolution,
|
341
|
+
**parcellation_definition,
|
342
|
+
)
|
465
343
|
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
344
|
+
# Load parcellation image and values
|
345
|
+
logger.info(f"Loading parcellation: {parcellation_fname.absolute()!s}")
|
346
|
+
parcellation_img = None
|
347
|
+
if not path_only:
|
348
|
+
# Load image via nibabel
|
349
|
+
parcellation_img = nib.load(parcellation_fname)
|
350
|
+
# Get unique values
|
351
|
+
parcel_values = np.unique(parcellation_img.get_fdata())
|
352
|
+
# Check for dimension
|
353
|
+
if len(parcel_values) - 1 != len(parcellation_labels):
|
354
|
+
raise_error(
|
355
|
+
f"Parcellation {name} has {len(parcel_values) - 1} "
|
356
|
+
f"parcels but {len(parcellation_labels)} labels."
|
357
|
+
)
|
358
|
+
# Sort values
|
359
|
+
parcel_values.sort()
|
360
|
+
# Check if value range is invalid
|
361
|
+
if np.any(np.diff(parcel_values) != 1):
|
362
|
+
raise_error(
|
363
|
+
f"Parcellation {name} must have all the values in the "
|
364
|
+
f"range [0, {len(parcel_values)}]"
|
365
|
+
)
|
471
366
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
367
|
+
return parcellation_img, parcellation_labels, parcellation_fname, space
|
368
|
+
|
369
|
+
def get(
|
370
|
+
self,
|
371
|
+
parcellations: Union[str, List[str]],
|
372
|
+
target_data: Dict[str, Any],
|
373
|
+
extra_input: Optional[Dict[str, Any]] = None,
|
374
|
+
) -> Tuple["Nifti1Image", List[str]]:
|
375
|
+
"""Get parcellation, tailored for the target image.
|
376
|
+
|
377
|
+
Parameters
|
378
|
+
----------
|
379
|
+
parcellations : str or list of str
|
380
|
+
The name(s) of the parcellation(s).
|
381
|
+
target_data : dict
|
382
|
+
The corresponding item of the data object to which the parcellation
|
383
|
+
will be applied.
|
384
|
+
extra_input : dict, optional
|
385
|
+
The other fields in the data object. Useful for accessing other
|
386
|
+
data kinds that needs to be used in the computation of
|
387
|
+
parcellations (default None).
|
388
|
+
|
389
|
+
Returns
|
390
|
+
-------
|
391
|
+
Nifti1Image
|
392
|
+
The parcellation image.
|
393
|
+
list of str
|
394
|
+
Parcellation labels.
|
395
|
+
|
396
|
+
Raises
|
397
|
+
------
|
398
|
+
RuntimeError
|
399
|
+
If warp / transformation file extension is not ".mat" or ".h5".
|
400
|
+
ValueError
|
401
|
+
If ``extra_input`` is None when ``target_data``'s space is native.
|
402
|
+
|
403
|
+
"""
|
404
|
+
# Check pre-requirements for space manipulation
|
405
|
+
target_space = target_data["space"]
|
406
|
+
# Set target standard space to target space
|
407
|
+
target_std_space = target_space
|
408
|
+
# Extra data type requirement check if target space is native
|
409
|
+
if target_space == "native":
|
410
|
+
# Check for extra inputs
|
411
|
+
if extra_input is None:
|
412
|
+
raise_error(
|
413
|
+
"No extra input provided, requires `Warp` and `T1w` "
|
414
|
+
"data types in particular for transformation to "
|
415
|
+
f"{target_data['space']} space for further computation."
|
416
|
+
)
|
417
|
+
# Set target standard space to warp file space source
|
418
|
+
target_std_space = extra_input["Warp"]["src"]
|
419
|
+
|
420
|
+
# Get the min of the voxels sizes and use it as the resolution
|
421
|
+
target_img = target_data["data"]
|
422
|
+
resolution = np.min(target_img.header.get_zooms()[:3])
|
423
|
+
|
424
|
+
# Convert parcellations to list if not already
|
425
|
+
if not isinstance(parcellations, list):
|
426
|
+
parcellations = [parcellations]
|
427
|
+
|
428
|
+
# Load the parcellations and labels
|
429
|
+
all_parcellations = []
|
430
|
+
all_labels = []
|
431
|
+
for name in parcellations:
|
432
|
+
img, labels, _, space = self.load(
|
433
|
+
name=name,
|
434
|
+
resolution=resolution,
|
435
|
+
)
|
479
436
|
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
437
|
+
# Convert parcellation spaces if required
|
438
|
+
if space != target_std_space:
|
439
|
+
raw_img = ANTsParcellationWarper().warp(
|
440
|
+
parcellation_name=name,
|
441
|
+
parcellation_img=img,
|
442
|
+
src=space,
|
443
|
+
dst=target_std_space,
|
444
|
+
target_data=target_data,
|
445
|
+
extra_input=None,
|
446
|
+
)
|
447
|
+
# Remove extra dimension added by ANTs
|
448
|
+
img = image.math_img("np.squeeze(img)", img=raw_img)
|
449
|
+
|
450
|
+
# Resample parcellation to target image
|
451
|
+
img_to_merge = image.resample_to_img(
|
452
|
+
source_img=img,
|
453
|
+
target_img=target_img,
|
454
|
+
interpolation="nearest",
|
455
|
+
copy=True,
|
456
|
+
)
|
488
457
|
|
489
|
-
|
490
|
-
|
491
|
-
parcellation_fname = Path(parcellation_definition["path"])
|
492
|
-
parcellation_labels = parcellation_definition["labels"]
|
493
|
-
else:
|
494
|
-
parcellation_fname, parcellation_labels = _retrieve_parcellation(
|
495
|
-
family=t_family,
|
496
|
-
parcellations_dir=parcellations_dir,
|
497
|
-
resolution=resolution,
|
498
|
-
**parcellation_definition,
|
499
|
-
)
|
458
|
+
all_parcellations.append(img_to_merge)
|
459
|
+
all_labels.append(labels)
|
500
460
|
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
#
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
raise_error(
|
512
|
-
f"Parcellation {name} has {len(parcel_values) - 1} parcels "
|
513
|
-
f"but {len(parcellation_labels)} labels."
|
514
|
-
)
|
515
|
-
# Sort values
|
516
|
-
parcel_values.sort()
|
517
|
-
# Check if value range is invalid
|
518
|
-
if np.any(np.diff(parcel_values) != 1):
|
519
|
-
raise_error(
|
520
|
-
f"Parcellation {name} must have all the values in the range "
|
521
|
-
f"[0, {len(parcel_values)}]."
|
461
|
+
# Avoid merging if there is only one parcellation
|
462
|
+
if len(all_parcellations) == 1:
|
463
|
+
resampled_parcellation_img = all_parcellations[0]
|
464
|
+
labels = all_labels[0]
|
465
|
+
# Parcellations are already transformed to target standard space
|
466
|
+
else:
|
467
|
+
resampled_parcellation_img, labels = merge_parcellations(
|
468
|
+
parcellations_list=all_parcellations,
|
469
|
+
parcellations_names=parcellations,
|
470
|
+
labels_lists=all_labels,
|
522
471
|
)
|
523
472
|
|
524
|
-
|
525
|
-
|
526
|
-
|
473
|
+
# Warp parcellation if target space is native
|
474
|
+
if target_space == "native":
|
475
|
+
# extra_input check done earlier
|
476
|
+
# Check for warp file type to use correct tool
|
477
|
+
warp_file_ext = extra_input["Warp"]["path"].suffix
|
478
|
+
if warp_file_ext == ".mat":
|
479
|
+
resampled_parcellation_img = FSLParcellationWarper().warp(
|
480
|
+
parcellation_name="native",
|
481
|
+
parcellation_img=resampled_parcellation_img,
|
482
|
+
target_data=target_data,
|
483
|
+
extra_input=extra_input,
|
484
|
+
)
|
485
|
+
elif warp_file_ext == ".h5":
|
486
|
+
resampled_parcellation_img = ANTsParcellationWarper().warp(
|
487
|
+
parcellation_name="native",
|
488
|
+
parcellation_img=resampled_parcellation_img,
|
489
|
+
src="",
|
490
|
+
dst="T1w",
|
491
|
+
target_data=target_data,
|
492
|
+
extra_input=extra_input,
|
493
|
+
)
|
494
|
+
else:
|
495
|
+
raise_error(
|
496
|
+
msg=(
|
497
|
+
"Unknown warp / transformation file extension: "
|
498
|
+
f"{warp_file_ext}"
|
499
|
+
),
|
500
|
+
klass=RuntimeError,
|
501
|
+
)
|
502
|
+
|
503
|
+
return resampled_parcellation_img, labels
|
527
504
|
|
528
505
|
|
529
506
|
def _retrieve_parcellation(
|