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.
Files changed (78) hide show
  1. junifer/_version.py +2 -2
  2. junifer/data/__init__.pyi +17 -31
  3. junifer/data/_dispatch.py +251 -0
  4. junifer/data/coordinates/__init__.py +9 -0
  5. junifer/data/coordinates/__init__.pyi +5 -0
  6. junifer/data/coordinates/_ants_coordinates_warper.py +96 -0
  7. junifer/data/coordinates/_coordinates.py +356 -0
  8. junifer/data/coordinates/_fsl_coordinates_warper.py +83 -0
  9. junifer/data/{tests → coordinates/tests}/test_coordinates.py +25 -31
  10. junifer/data/masks/__init__.py +9 -0
  11. junifer/data/masks/__init__.pyi +6 -0
  12. junifer/data/masks/_ants_mask_warper.py +144 -0
  13. junifer/data/masks/_fsl_mask_warper.py +87 -0
  14. junifer/data/masks/_masks.py +624 -0
  15. junifer/data/{tests → masks/tests}/test_masks.py +63 -58
  16. junifer/data/parcellations/__init__.py +9 -0
  17. junifer/data/parcellations/__init__.pyi +6 -0
  18. junifer/data/parcellations/_ants_parcellation_warper.py +154 -0
  19. junifer/data/parcellations/_fsl_parcellation_warper.py +91 -0
  20. junifer/data/{parcellations.py → parcellations/_parcellations.py} +450 -473
  21. junifer/data/{tests → parcellations/tests}/test_parcellations.py +73 -81
  22. junifer/data/pipeline_data_registry_base.py +74 -0
  23. junifer/data/utils.py +4 -0
  24. junifer/markers/complexity/hurst_exponent.py +2 -2
  25. junifer/markers/complexity/multiscale_entropy_auc.py +2 -2
  26. junifer/markers/complexity/perm_entropy.py +2 -2
  27. junifer/markers/complexity/range_entropy.py +2 -2
  28. junifer/markers/complexity/range_entropy_auc.py +2 -2
  29. junifer/markers/complexity/sample_entropy.py +2 -2
  30. junifer/markers/complexity/weighted_perm_entropy.py +2 -2
  31. junifer/markers/ets_rss.py +2 -2
  32. junifer/markers/falff/falff_parcels.py +2 -2
  33. junifer/markers/falff/falff_spheres.py +2 -2
  34. junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +1 -1
  35. junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py +1 -1
  36. junifer/markers/functional_connectivity/functional_connectivity_parcels.py +1 -1
  37. junifer/markers/functional_connectivity/functional_connectivity_spheres.py +1 -1
  38. junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +3 -3
  39. junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +2 -2
  40. junifer/markers/parcel_aggregation.py +11 -7
  41. junifer/markers/reho/reho_parcels.py +2 -2
  42. junifer/markers/reho/reho_spheres.py +2 -2
  43. junifer/markers/sphere_aggregation.py +11 -7
  44. junifer/markers/temporal_snr/temporal_snr_parcels.py +2 -2
  45. junifer/markers/temporal_snr/temporal_snr_spheres.py +2 -2
  46. junifer/markers/tests/test_ets_rss.py +3 -3
  47. junifer/markers/tests/test_parcel_aggregation.py +24 -24
  48. junifer/markers/tests/test_sphere_aggregation.py +6 -6
  49. junifer/pipeline/pipeline_component_registry.py +1 -1
  50. junifer/preprocess/confounds/fmriprep_confound_remover.py +6 -3
  51. {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/METADATA +1 -1
  52. {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/RECORD +76 -62
  53. {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/WHEEL +1 -1
  54. junifer/data/coordinates.py +0 -408
  55. junifer/data/masks.py +0 -670
  56. /junifer/data/{VOIs → coordinates/VOIs}/meta/AutobiographicalMemory_VOIs.txt +0 -0
  57. /junifer/data/{VOIs → coordinates/VOIs}/meta/CogAC_VOIs.txt +0 -0
  58. /junifer/data/{VOIs → coordinates/VOIs}/meta/CogAR_VOIs.txt +0 -0
  59. /junifer/data/{VOIs → coordinates/VOIs}/meta/DMNBuckner_VOIs.txt +0 -0
  60. /junifer/data/{VOIs → coordinates/VOIs}/meta/Dosenbach2010_MNI_VOIs.txt +0 -0
  61. /junifer/data/{VOIs → coordinates/VOIs}/meta/Empathy_VOIs.txt +0 -0
  62. /junifer/data/{VOIs → coordinates/VOIs}/meta/Motor_VOIs.txt +0 -0
  63. /junifer/data/{VOIs → coordinates/VOIs}/meta/MultiTask_VOIs.txt +0 -0
  64. /junifer/data/{VOIs → coordinates/VOIs}/meta/PhysioStress_VOIs.txt +0 -0
  65. /junifer/data/{VOIs → coordinates/VOIs}/meta/Power2011_MNI_VOIs.txt +0 -0
  66. /junifer/data/{VOIs → coordinates/VOIs}/meta/Power2013_MNI_VOIs.tsv +0 -0
  67. /junifer/data/{VOIs → coordinates/VOIs}/meta/Rew_VOIs.txt +0 -0
  68. /junifer/data/{VOIs → coordinates/VOIs}/meta/Somatosensory_VOIs.txt +0 -0
  69. /junifer/data/{VOIs → coordinates/VOIs}/meta/ToM_VOIs.txt +0 -0
  70. /junifer/data/{VOIs → coordinates/VOIs}/meta/VigAtt_VOIs.txt +0 -0
  71. /junifer/data/{VOIs → coordinates/VOIs}/meta/WM_VOIs.txt +0 -0
  72. /junifer/data/{VOIs → coordinates/VOIs}/meta/eMDN_VOIs.txt +0 -0
  73. /junifer/data/{VOIs → coordinates/VOIs}/meta/eSAD_VOIs.txt +0 -0
  74. /junifer/data/{VOIs → coordinates/VOIs}/meta/extDMN_VOIs.txt +0 -0
  75. {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/AUTHORS.rst +0 -0
  76. {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/LICENSE.md +0 -0
  77. {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/entry_points.txt +0 -0
  78. {junifer-0.0.6.dev175.dist-info → junifer-0.0.6.dev201.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- """Functions for parcellation manipulation."""
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 ..pipeline import WorkDirManager
24
- from ..utils import logger, raise_error, run_ext_cmd, warn_with_log
25
- from .template_spaces import get_template, get_xfm
26
- from .utils import closest_resolution
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
- "register_parcellation",
35
- "list_parcellations",
36
- "get_parcellation",
37
- "load_parcellation",
36
+ "ParcellationRegistry",
38
37
  "merge_parcellations",
39
38
  ]
40
39
 
41
40
 
42
- # A dictionary containing all supported parcellations and their respective
43
- # valid parameters.
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
- Parameters
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
- # Load the parcellations
284
- all_parcellations = []
285
- all_labels = []
286
- for name in parcellation:
287
- img, labels, _, space = load_parcellation(
288
- name=name,
289
- resolution=resolution,
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
- # Convert parcellation spaces if required
293
- if space != target_std_space:
294
- # Get xfm file
295
- xfm_file_path = get_xfm(src=space, dst=target_std_space)
296
- # Get target standard space template
297
- target_std_space_template_img = get_template(
298
- space=target_std_space,
299
- target_data=target_data,
300
- extra_input=extra_input,
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
- # Save parcellation image to a component-scoped tempfile
304
- parcellation_path = tempdir / f"{name}.nii.gz"
305
- nib.save(img, parcellation_path)
306
-
307
- # Save template
308
- target_std_space_template_path = (
309
- tempdir / f"{target_std_space}_T1w_{resolution}.nii.gz"
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
- nib.save(
312
- target_std_space_template_img, target_std_space_template_path
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
- # Set warped parcellation path
316
- warped_parcellation_path = element_tempdir / (
317
- f"{name}_warped_from_{space}_to_" f"{target_std_space}.nii.gz"
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
- logger.debug(
321
- f"Using ANTs to warp {name} "
322
- f"from {space} to {target_std_space}"
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
- # Check for warp file type to use correct tool
377
- warp_file_ext = extra_input["Warp"]["path"].suffix
378
- if warp_file_ext == ".mat":
379
- logger.debug("Using FSL for parcellation warping")
380
- # Set applywarp command
381
- applywarp_cmd = [
382
- "applywarp",
383
- "--interp=nn",
384
- f"-i {prewarp_parcellation_path.resolve()}",
385
- # use resampled reference
386
- f"-r {target_data['reference_path'].resolve()}",
387
- f"-w {extra_input['Warp']['path'].resolve()}",
388
- f"-o {warped_parcellation_path.resolve()}",
389
- ]
390
- # Call applywarp
391
- run_ext_cmd(name="applywarp", cmd=applywarp_cmd)
392
-
393
- elif warp_file_ext == ".h5":
394
- logger.debug("Using ANTs for parcellation warping")
395
- # Set antsApplyTransforms command
396
- apply_transforms_cmd = [
397
- "antsApplyTransforms",
398
- "-d 3",
399
- "-e 3",
400
- "-n 'GenericLabel[NearestNeighbor]'",
401
- f"-i {prewarp_parcellation_path.resolve()}",
402
- # use resampled reference
403
- f"-r {target_data['reference_path'].resolve()}",
404
- f"-t {extra_input['Warp']['path'].resolve()}",
405
- f"-o {warped_parcellation_path.resolve()}",
406
- ]
407
- # Call antsApplyTransforms
408
- run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd)
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
- else:
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
- msg=(
413
- "Unknown warp / transformation file extension: "
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
- # Load nifti
420
- resampled_parcellation_img = nib.load(warped_parcellation_path)
421
-
422
- # Delete tempdir
423
- WorkDirManager().delete_tempdir(tempdir)
424
-
425
- return resampled_parcellation_img, labels # type: ignore
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
- Returns
456
- -------
457
- Nifti1Image or None
458
- Loaded parcellation image.
459
- list of str
460
- Parcellation labels.
461
- pathlib.Path
462
- File path to the parcellation image.
463
- str
464
- The space of the parcellation.
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
- Raises
467
- ------
468
- ValueError
469
- If ``name`` is invalid or if the parcellation values and labels
470
- don't have equal dimension or if the value range is invalid.
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
- # Check for valid parcellation name
474
- if name not in _available_parcellations:
475
- raise_error(
476
- f"Parcellation {name} not found. "
477
- f"Valid options are: {list_parcellations()}"
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
- # Copy parcellation definition to avoid edits in original object
481
- parcellation_definition = _available_parcellations[name].copy()
482
- t_family = parcellation_definition.pop("family")
483
- # Remove space conditionally
484
- if t_family not in ["SUIT", "Tian"]:
485
- space = parcellation_definition.pop("space")
486
- else:
487
- space = parcellation_definition["space"]
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
- # Check if the parcellation family is custom or built-in
490
- if t_family == "CustomUserParcellation":
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
- # Load parcellation image and values
502
- logger.info(f"Loading parcellation {parcellation_fname.absolute()!s}")
503
- parcellation_img = None
504
- if path_only is False:
505
- # Load image via nibabel
506
- parcellation_img = nib.load(parcellation_fname)
507
- # Get unique values
508
- parcel_values = np.unique(parcellation_img.get_fdata())
509
- # Check for dimension
510
- if len(parcel_values) - 1 != len(parcellation_labels):
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
- # Type-cast to remove errors
525
- parcellation_img = typing.cast("Nifti1Image", parcellation_img)
526
- return parcellation_img, parcellation_labels, parcellation_fname, space
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(