junifer 0.0.6.dev154__py3-none-any.whl → 0.0.6.dev194__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/__init__.pyi +2 -0
- junifer/_version.py +2 -2
- junifer/api/decorators.py +6 -11
- junifer/api/functions.py +74 -62
- junifer/api/tests/test_functions.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/datagrabber/aomic/piop2.py +1 -1
- 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/__init__.pyi +2 -2
- junifer/pipeline/pipeline_component_registry.py +299 -0
- junifer/pipeline/tests/test_pipeline_component_registry.py +201 -0
- junifer/preprocess/confounds/fmriprep_confound_remover.py +6 -3
- junifer/testing/__init__.pyi +2 -2
- junifer/testing/registry.py +4 -7
- junifer/testing/tests/test_testing_registry.py +9 -17
- {junifer-0.0.6.dev154.dist-info → junifer-0.0.6.dev194.dist-info}/METADATA +1 -1
- {junifer-0.0.6.dev154.dist-info → junifer-0.0.6.dev194.dist-info}/RECORD +86 -72
- {junifer-0.0.6.dev154.dist-info → junifer-0.0.6.dev194.dist-info}/WHEEL +1 -1
- junifer/data/coordinates.py +0 -408
- junifer/data/masks.py +0 -670
- junifer/pipeline/registry.py +0 -245
- junifer/pipeline/tests/test_registry.py +0 -150
- /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.dev154.dist-info → junifer-0.0.6.dev194.dist-info}/AUTHORS.rst +0 -0
- {junifer-0.0.6.dev154.dist-info → junifer-0.0.6.dev194.dist-info}/LICENSE.md +0 -0
- {junifer-0.0.6.dev154.dist-info → junifer-0.0.6.dev194.dist-info}/entry_points.txt +0 -0
- {junifer-0.0.6.dev154.dist-info → junifer-0.0.6.dev194.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
"""Provide class for coordinates space warping via ANTs."""
|
2
|
+
|
3
|
+
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
4
|
+
# License: AGPL
|
5
|
+
|
6
|
+
from typing import Any, Dict
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
from numpy.typing import ArrayLike
|
10
|
+
|
11
|
+
from ...pipeline import WorkDirManager
|
12
|
+
from ...utils import logger, run_ext_cmd
|
13
|
+
|
14
|
+
|
15
|
+
__all__ = ["ANTsCoordinatesWarper"]
|
16
|
+
|
17
|
+
|
18
|
+
class ANTsCoordinatesWarper:
|
19
|
+
"""Class for coordinates space warping via ANTs.
|
20
|
+
|
21
|
+
This class uses ANTs ``antsApplyTransformsToPoints`` for transformation.
|
22
|
+
|
23
|
+
"""
|
24
|
+
|
25
|
+
def warp(
|
26
|
+
self,
|
27
|
+
seeds: ArrayLike,
|
28
|
+
target_data: Dict[str, Any],
|
29
|
+
extra_input: Dict[str, Any],
|
30
|
+
) -> ArrayLike:
|
31
|
+
"""Warp ``seeds`` to correct space.
|
32
|
+
|
33
|
+
Parameters
|
34
|
+
----------
|
35
|
+
seeds : array-like
|
36
|
+
The coordinates to transform.
|
37
|
+
target_data : dict
|
38
|
+
The corresponding item of the data object to which the coordinates
|
39
|
+
will be applied.
|
40
|
+
extra_input : dict, optional
|
41
|
+
The other fields in the data object. Useful for accessing other
|
42
|
+
data kinds that needs to be used in the computation of coordinates
|
43
|
+
(default None).
|
44
|
+
|
45
|
+
Returns
|
46
|
+
-------
|
47
|
+
numpy.ndarray
|
48
|
+
The transformed coordinates.
|
49
|
+
|
50
|
+
"""
|
51
|
+
logger.debug("Using ANTs for coordinates transformation")
|
52
|
+
|
53
|
+
# Create element-specific tempdir for storing post-warping assets
|
54
|
+
element_tempdir = WorkDirManager().get_element_tempdir(
|
55
|
+
prefix="ants_coordinates_warper"
|
56
|
+
)
|
57
|
+
|
58
|
+
# Save existing coordinates to a tempfile
|
59
|
+
pretransform_coordinates_path = (
|
60
|
+
element_tempdir / "pretransform_coordinates.txt"
|
61
|
+
)
|
62
|
+
np.savetxt(
|
63
|
+
pretransform_coordinates_path,
|
64
|
+
seeds,
|
65
|
+
delimiter=",",
|
66
|
+
# Add header while saving to make ANTs work
|
67
|
+
header="x,y,z",
|
68
|
+
)
|
69
|
+
|
70
|
+
# Create a tempfile for transformed coordinates output
|
71
|
+
transformed_coords_path = (
|
72
|
+
element_tempdir / "coordinates_transformed.txt"
|
73
|
+
)
|
74
|
+
# Set antsApplyTransformsToPoints command
|
75
|
+
apply_transforms_to_points_cmd = [
|
76
|
+
"antsApplyTransformsToPoints",
|
77
|
+
"-d 3",
|
78
|
+
"-p 1",
|
79
|
+
"-f 0",
|
80
|
+
f"-i {pretransform_coordinates_path.resolve()}",
|
81
|
+
f"-o {transformed_coords_path.resolve()}",
|
82
|
+
f"-t {extra_input['Warp']['path'].resolve()};",
|
83
|
+
]
|
84
|
+
# Call antsApplyTransformsToPoints
|
85
|
+
run_ext_cmd(
|
86
|
+
name="antsApplyTransformsToPoints",
|
87
|
+
cmd=apply_transforms_to_points_cmd,
|
88
|
+
)
|
89
|
+
|
90
|
+
# Load coordinates
|
91
|
+
return np.loadtxt(
|
92
|
+
# Skip header when reading
|
93
|
+
transformed_coords_path,
|
94
|
+
delimiter=",",
|
95
|
+
skiprows=1,
|
96
|
+
)
|
@@ -0,0 +1,356 @@
|
|
1
|
+
"""Provide a class for centralized coordinates data registry."""
|
2
|
+
|
3
|
+
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
4
|
+
# Synchon Mandal <s.mandal@fz-juelich.de>
|
5
|
+
# License: AGPL
|
6
|
+
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple
|
9
|
+
|
10
|
+
import numpy as np
|
11
|
+
import pandas as pd
|
12
|
+
from numpy.typing import ArrayLike
|
13
|
+
|
14
|
+
from ...pipeline.singleton import singleton
|
15
|
+
from ...utils import logger, raise_error
|
16
|
+
from ..pipeline_data_registry_base import BasePipelineDataRegistry
|
17
|
+
from ._ants_coordinates_warper import ANTsCoordinatesWarper
|
18
|
+
from ._fsl_coordinates_warper import FSLCoordinatesWarper
|
19
|
+
|
20
|
+
|
21
|
+
__all__ = ["CoordinatesRegistry"]
|
22
|
+
|
23
|
+
|
24
|
+
@singleton
|
25
|
+
class CoordinatesRegistry(BasePipelineDataRegistry):
|
26
|
+
"""Class for coordinates data registry.
|
27
|
+
|
28
|
+
This class is a singleton and is used for managing available coordinates
|
29
|
+
data in a centralized manner.
|
30
|
+
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(self) -> None:
|
34
|
+
"""Initialize the class."""
|
35
|
+
# Each entry in registry is a dictionary that must contain at least
|
36
|
+
# the following keys:
|
37
|
+
# * 'space': the coordinates' space (e.g., 'MNI')
|
38
|
+
# The built-in coordinates are files that are shipped with the package
|
39
|
+
# in the data/VOIs directory. The user can also register their own
|
40
|
+
# coordinates, which will be stored as numpy arrays in the dictionary.
|
41
|
+
# Make built-in and external dictionaries for validation later
|
42
|
+
self._builtin = {}
|
43
|
+
self._external = {}
|
44
|
+
|
45
|
+
# Path to the metadata of the VOIs
|
46
|
+
_vois_meta_path = Path(__file__).parent / "VOIs" / "meta"
|
47
|
+
|
48
|
+
self._builtin = {
|
49
|
+
"CogAC": {
|
50
|
+
"path": _vois_meta_path / "CogAC_VOIs.txt",
|
51
|
+
"space": "MNI",
|
52
|
+
},
|
53
|
+
"CogAR": {
|
54
|
+
"path": _vois_meta_path / "CogAR_VOIs.txt",
|
55
|
+
"space": "MNI",
|
56
|
+
},
|
57
|
+
"DMNBuckner": {
|
58
|
+
"path": _vois_meta_path / "DMNBuckner_VOIs.txt",
|
59
|
+
"space": "MNI",
|
60
|
+
},
|
61
|
+
"eMDN": {
|
62
|
+
"path": _vois_meta_path / "eMDN_VOIs.txt",
|
63
|
+
"space": "MNI",
|
64
|
+
},
|
65
|
+
"Empathy": {
|
66
|
+
"path": _vois_meta_path / "Empathy_VOIs.txt",
|
67
|
+
"space": "MNI",
|
68
|
+
},
|
69
|
+
"eSAD": {
|
70
|
+
"path": _vois_meta_path / "eSAD_VOIs.txt",
|
71
|
+
"space": "MNI",
|
72
|
+
},
|
73
|
+
"extDMN": {
|
74
|
+
"path": _vois_meta_path / "extDMN_VOIs.txt",
|
75
|
+
"space": "MNI",
|
76
|
+
},
|
77
|
+
"Motor": {
|
78
|
+
"path": _vois_meta_path / "Motor_VOIs.txt",
|
79
|
+
"space": "MNI",
|
80
|
+
},
|
81
|
+
"MultiTask": {
|
82
|
+
"path": _vois_meta_path / "MultiTask_VOIs.txt",
|
83
|
+
"space": "MNI",
|
84
|
+
},
|
85
|
+
"PhysioStress": {
|
86
|
+
"path": _vois_meta_path / "PhysioStress_VOIs.txt",
|
87
|
+
"space": "MNI",
|
88
|
+
},
|
89
|
+
"Rew": {
|
90
|
+
"path": _vois_meta_path / "Rew_VOIs.txt",
|
91
|
+
"space": "MNI",
|
92
|
+
},
|
93
|
+
"Somatosensory": {
|
94
|
+
"path": _vois_meta_path / "Somatosensory_VOIs.txt",
|
95
|
+
"space": "MNI",
|
96
|
+
},
|
97
|
+
"ToM": {
|
98
|
+
"path": _vois_meta_path / "ToM_VOIs.txt",
|
99
|
+
"space": "MNI",
|
100
|
+
},
|
101
|
+
"VigAtt": {
|
102
|
+
"path": _vois_meta_path / "VigAtt_VOIs.txt",
|
103
|
+
"space": "MNI",
|
104
|
+
},
|
105
|
+
"WM": {
|
106
|
+
"path": _vois_meta_path / "WM_VOIs.txt",
|
107
|
+
"space": "MNI",
|
108
|
+
},
|
109
|
+
"Power": {
|
110
|
+
"path": _vois_meta_path / "Power2011_MNI_VOIs.txt",
|
111
|
+
"space": "MNI",
|
112
|
+
},
|
113
|
+
"Power2011": {
|
114
|
+
"path": _vois_meta_path / "Power2011_MNI_VOIs.txt",
|
115
|
+
"space": "MNI",
|
116
|
+
},
|
117
|
+
"Dosenbach": {
|
118
|
+
"path": _vois_meta_path / "Dosenbach2010_MNI_VOIs.txt",
|
119
|
+
"space": "MNI",
|
120
|
+
},
|
121
|
+
"Power2013": {
|
122
|
+
"path": _vois_meta_path / "Power2013_MNI_VOIs.tsv",
|
123
|
+
"space": "MNI",
|
124
|
+
},
|
125
|
+
"AutobiographicalMemory": {
|
126
|
+
"path": _vois_meta_path / "AutobiographicalMemory_VOIs.txt",
|
127
|
+
"space": "MNI",
|
128
|
+
},
|
129
|
+
}
|
130
|
+
|
131
|
+
# Set built-in to registry
|
132
|
+
self._registry = self._builtin
|
133
|
+
|
134
|
+
def register(
|
135
|
+
self,
|
136
|
+
name: str,
|
137
|
+
coordinates: ArrayLike,
|
138
|
+
voi_names: List[str],
|
139
|
+
space: str,
|
140
|
+
overwrite: Optional[bool] = False,
|
141
|
+
) -> None:
|
142
|
+
"""Register a custom user coordinates.
|
143
|
+
|
144
|
+
Parameters
|
145
|
+
----------
|
146
|
+
name : str
|
147
|
+
The name of the coordinates.
|
148
|
+
coordinates : numpy.ndarray
|
149
|
+
The coordinates. This should be a 2-dimensional array with three
|
150
|
+
columns. Each row corresponds to a volume-of-interest (VOI) and
|
151
|
+
each column corresponds to a spatial dimension (i.e. x, y, and
|
152
|
+
z-coordinates).
|
153
|
+
voi_names : list of str
|
154
|
+
The names of the VOIs.
|
155
|
+
space : str
|
156
|
+
The space of the coordinates, e.g., "MNI".
|
157
|
+
overwrite : bool, optional
|
158
|
+
If True, overwrite an existing list of coordinates with the same
|
159
|
+
name. Does not apply to built-in coordinates (default False).
|
160
|
+
|
161
|
+
Raises
|
162
|
+
------
|
163
|
+
ValueError
|
164
|
+
If the coordinates ``name`` is already registered and
|
165
|
+
``overwrite=False`` or
|
166
|
+
if the coordinates ``name`` is a built-in coordinates or
|
167
|
+
if the ``coordinates`` is not a 2D array or
|
168
|
+
if coordinate value does not have 3 components or
|
169
|
+
if the ``voi_names`` shape does not match the
|
170
|
+
``coordinates`` shape.
|
171
|
+
TypeError
|
172
|
+
If ``coordinates`` is not a ``numpy.ndarray``.
|
173
|
+
|
174
|
+
"""
|
175
|
+
# Check for attempt of overwriting built-in coordinates
|
176
|
+
if name in self._builtin:
|
177
|
+
if isinstance(self._registry[name].get("path"), Path):
|
178
|
+
raise_error(
|
179
|
+
f"Coordinates: {name} already registered as built-in "
|
180
|
+
"coordinates."
|
181
|
+
)
|
182
|
+
if overwrite:
|
183
|
+
logger.info(f"Overwriting coordinates: {name}")
|
184
|
+
else:
|
185
|
+
raise_error(
|
186
|
+
f"Coordinates: {name} already registered. "
|
187
|
+
"Set `overwrite=True` to update its value."
|
188
|
+
)
|
189
|
+
|
190
|
+
if not isinstance(coordinates, np.ndarray):
|
191
|
+
raise_error(
|
192
|
+
"Coordinates must be a `numpy.ndarray`, "
|
193
|
+
f"not {type(coordinates)}.",
|
194
|
+
klass=TypeError,
|
195
|
+
)
|
196
|
+
if coordinates.ndim != 2:
|
197
|
+
raise_error(
|
198
|
+
f"Coordinates must be a 2D array, not {coordinates.ndim}D."
|
199
|
+
)
|
200
|
+
if coordinates.shape[1] != 3:
|
201
|
+
raise_error(
|
202
|
+
"Each coordinate must have 3 values, "
|
203
|
+
f"not {coordinates.shape[1]}"
|
204
|
+
)
|
205
|
+
if len(voi_names) != coordinates.shape[0]:
|
206
|
+
raise_error(
|
207
|
+
f"Length of `voi_names` ({len(voi_names)}) does not match the "
|
208
|
+
f"number of `coordinates` ({coordinates.shape[0]})."
|
209
|
+
)
|
210
|
+
logger.info(f"Registering coordinates: {name}")
|
211
|
+
# Add coordinates info
|
212
|
+
self._external[name] = {
|
213
|
+
"coords": coordinates,
|
214
|
+
"voi_names": voi_names,
|
215
|
+
"space": space,
|
216
|
+
}
|
217
|
+
# Update registry
|
218
|
+
self._registry[name] = {
|
219
|
+
"coords": coordinates,
|
220
|
+
"voi_names": voi_names,
|
221
|
+
"space": space,
|
222
|
+
}
|
223
|
+
|
224
|
+
def deregister(self, name: str) -> None:
|
225
|
+
"""De-register a custom user coordinates.
|
226
|
+
|
227
|
+
Parameters
|
228
|
+
----------
|
229
|
+
name : str
|
230
|
+
The name of the coordinates.
|
231
|
+
|
232
|
+
"""
|
233
|
+
logger.info(f"De-registering coordinates: {name}")
|
234
|
+
# Remove coordinates info
|
235
|
+
_ = self._external.pop(name)
|
236
|
+
# Update registry
|
237
|
+
_ = self._registry.pop(name)
|
238
|
+
|
239
|
+
def load(self, name: str) -> Tuple[ArrayLike, List[str], str]:
|
240
|
+
"""Load coordinates.
|
241
|
+
|
242
|
+
Parameters
|
243
|
+
----------
|
244
|
+
name : str
|
245
|
+
The name of the coordinates.
|
246
|
+
|
247
|
+
Returns
|
248
|
+
-------
|
249
|
+
numpy.ndarray
|
250
|
+
The coordinates.
|
251
|
+
list of str
|
252
|
+
The names of the VOIs.
|
253
|
+
str
|
254
|
+
The space of the coordinates.
|
255
|
+
|
256
|
+
Raises
|
257
|
+
------
|
258
|
+
ValueError
|
259
|
+
If ``name`` is invalid.
|
260
|
+
|
261
|
+
"""
|
262
|
+
# Check for valid coordinates name
|
263
|
+
if name not in self._registry:
|
264
|
+
raise_error(
|
265
|
+
f"Coordinates: {name} not found. "
|
266
|
+
f"Valid options are: {self.list}"
|
267
|
+
)
|
268
|
+
# Load coordinates
|
269
|
+
t_coord = self._registry[name]
|
270
|
+
# Load data
|
271
|
+
if isinstance(t_coord.get("path"), Path):
|
272
|
+
logger.debug(f"Loading coordinates {t_coord['path'].absolute()!s}")
|
273
|
+
# Load via pandas
|
274
|
+
df_coords = pd.read_csv(t_coord["path"], sep="\t", header=None)
|
275
|
+
# Convert dataframe to numpy ndarray
|
276
|
+
coords = df_coords.iloc[:, [0, 1, 2]].to_numpy()
|
277
|
+
# Get label names
|
278
|
+
names = list(df_coords.iloc[:, [3]].values[:, 0])
|
279
|
+
else:
|
280
|
+
coords = t_coord["coords"]
|
281
|
+
names = t_coord["voi_names"]
|
282
|
+
|
283
|
+
return coords, names, t_coord["space"]
|
284
|
+
|
285
|
+
def get(
|
286
|
+
self,
|
287
|
+
coords: str,
|
288
|
+
target_data: Dict[str, Any],
|
289
|
+
extra_input: Optional[Dict[str, Any]] = None,
|
290
|
+
) -> Tuple[ArrayLike, List[str]]:
|
291
|
+
"""Get coordinates, tailored for the target data.
|
292
|
+
|
293
|
+
Parameters
|
294
|
+
----------
|
295
|
+
coords : str
|
296
|
+
The name of the coordinates.
|
297
|
+
target_data : dict
|
298
|
+
The corresponding item of the data object to which the coordinates
|
299
|
+
will be applied.
|
300
|
+
extra_input : dict, optional
|
301
|
+
The other fields in the data object. Useful for accessing other
|
302
|
+
data kinds that needs to be used in the computation of coordinates
|
303
|
+
(default None).
|
304
|
+
|
305
|
+
Returns
|
306
|
+
-------
|
307
|
+
numpy.ndarray
|
308
|
+
The coordinates.
|
309
|
+
list of str
|
310
|
+
The names of the VOIs.
|
311
|
+
|
312
|
+
Raises
|
313
|
+
------
|
314
|
+
RuntimeError
|
315
|
+
If warp / transformation file extension is not ".mat" or ".h5".
|
316
|
+
ValueError
|
317
|
+
If ``extra_input`` is None when ``target_data``'s space is native.
|
318
|
+
|
319
|
+
"""
|
320
|
+
# Load the coordinates
|
321
|
+
seeds, labels, _ = self.load(name=coords)
|
322
|
+
|
323
|
+
# Transform coordinate if target data is native
|
324
|
+
if target_data["space"] == "native":
|
325
|
+
# Check for extra inputs
|
326
|
+
if extra_input is None:
|
327
|
+
raise_error(
|
328
|
+
"No extra input provided, requires `Warp` and `T1w` "
|
329
|
+
"data types in particular for transformation to "
|
330
|
+
f"{target_data['space']} space for further computation."
|
331
|
+
)
|
332
|
+
|
333
|
+
# Check for warp file type to use correct tool
|
334
|
+
warp_file_ext = extra_input["Warp"]["path"].suffix
|
335
|
+
if warp_file_ext == ".mat":
|
336
|
+
seeds = FSLCoordinatesWarper().warp(
|
337
|
+
seeds=seeds,
|
338
|
+
target_data=target_data,
|
339
|
+
extra_input=extra_input,
|
340
|
+
)
|
341
|
+
elif warp_file_ext == ".h5":
|
342
|
+
seeds = ANTsCoordinatesWarper().warp(
|
343
|
+
seeds=seeds,
|
344
|
+
target_data=target_data,
|
345
|
+
extra_input=extra_input,
|
346
|
+
)
|
347
|
+
else:
|
348
|
+
raise_error(
|
349
|
+
msg=(
|
350
|
+
"Unknown warp / transformation file extension: "
|
351
|
+
f"{warp_file_ext}"
|
352
|
+
),
|
353
|
+
klass=RuntimeError,
|
354
|
+
)
|
355
|
+
|
356
|
+
return seeds, labels
|
@@ -0,0 +1,83 @@
|
|
1
|
+
"""Provide class for coordinates space warping via FSL FLIRT."""
|
2
|
+
|
3
|
+
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
4
|
+
# License: AGPL
|
5
|
+
|
6
|
+
from typing import Any, Dict
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
from numpy.typing import ArrayLike
|
10
|
+
|
11
|
+
from ...pipeline import WorkDirManager
|
12
|
+
from ...utils import logger, run_ext_cmd
|
13
|
+
|
14
|
+
|
15
|
+
__all__ = ["FSLCoordinatesWarper"]
|
16
|
+
|
17
|
+
|
18
|
+
class FSLCoordinatesWarper:
|
19
|
+
"""Class for coordinates space warping via FSL FLIRT.
|
20
|
+
|
21
|
+
This class uses FSL FLIRT's ``img2imgcoord`` for transformation.
|
22
|
+
|
23
|
+
"""
|
24
|
+
|
25
|
+
def warp(
|
26
|
+
self,
|
27
|
+
seeds: ArrayLike,
|
28
|
+
target_data: Dict[str, Any],
|
29
|
+
extra_input: Dict[str, Any],
|
30
|
+
) -> ArrayLike:
|
31
|
+
"""Warp ``seeds`` to correct space.
|
32
|
+
|
33
|
+
Parameters
|
34
|
+
----------
|
35
|
+
seeds : array-like
|
36
|
+
The coordinates to transform.
|
37
|
+
target_data : dict
|
38
|
+
The corresponding item of the data object to which the coordinates
|
39
|
+
will be applied.
|
40
|
+
extra_input : dict, optional
|
41
|
+
The other fields in the data object. Useful for accessing other
|
42
|
+
data kinds that needs to be used in the computation of coordinates
|
43
|
+
(default None).
|
44
|
+
|
45
|
+
Returns
|
46
|
+
-------
|
47
|
+
numpy.ndarray
|
48
|
+
The transformed coordinates.
|
49
|
+
|
50
|
+
"""
|
51
|
+
logger.debug("Using FSL for coordinates transformation")
|
52
|
+
|
53
|
+
# Create element-specific tempdir for storing post-warping assets
|
54
|
+
element_tempdir = WorkDirManager().get_element_tempdir(
|
55
|
+
prefix="fsl_coordinates_warper"
|
56
|
+
)
|
57
|
+
|
58
|
+
# Save existing coordinates to a tempfile
|
59
|
+
pretransform_coordinates_path = (
|
60
|
+
element_tempdir / "pretransform_coordinates.txt"
|
61
|
+
)
|
62
|
+
np.savetxt(pretransform_coordinates_path, seeds)
|
63
|
+
|
64
|
+
# Create a tempfile for transformed coordinates output
|
65
|
+
transformed_coords_path = (
|
66
|
+
element_tempdir / "coordinates_transformed.txt"
|
67
|
+
)
|
68
|
+
# Set img2imgcoord command
|
69
|
+
img2imgcoord_cmd = [
|
70
|
+
"cat",
|
71
|
+
f"{pretransform_coordinates_path.resolve()}",
|
72
|
+
"| img2imgcoord -mm",
|
73
|
+
f"-src {target_data['path'].resolve()}",
|
74
|
+
f"-dest {target_data['reference_path'].resolve()}",
|
75
|
+
f"-warp {extra_input['Warp']['path'].resolve()}",
|
76
|
+
f"> {transformed_coords_path.resolve()};",
|
77
|
+
f"sed -i 1d {transformed_coords_path.resolve()}",
|
78
|
+
]
|
79
|
+
# Call img2imgcoord
|
80
|
+
run_ext_cmd(name="img2imgcoord", cmd=img2imgcoord_cmd)
|
81
|
+
|
82
|
+
# Load coordinates
|
83
|
+
return np.loadtxt(transformed_coords_path)
|
@@ -1,26 +1,22 @@
|
|
1
1
|
"""Provide tests for coordinates."""
|
2
2
|
|
3
3
|
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
4
|
+
# Synchon Mandal <s.mandal@fz-juelich.de>
|
4
5
|
# License: AGPL
|
5
6
|
|
6
7
|
import numpy as np
|
7
8
|
import pytest
|
8
9
|
from numpy.testing import assert_array_equal
|
9
10
|
|
10
|
-
from junifer.data
|
11
|
-
get_coordinates,
|
12
|
-
list_coordinates,
|
13
|
-
load_coordinates,
|
14
|
-
register_coordinates,
|
15
|
-
)
|
11
|
+
from junifer.data import CoordinatesRegistry
|
16
12
|
from junifer.datareader import DefaultDataReader
|
17
13
|
from junifer.testing.datagrabbers import OasisVBMTestingDataGrabber
|
18
14
|
|
19
15
|
|
20
|
-
def
|
16
|
+
def test_register_built_in_check() -> None:
|
21
17
|
"""Test coordinates registration check for built-in coordinates."""
|
22
18
|
with pytest.raises(ValueError, match=r"built-in"):
|
23
|
-
|
19
|
+
CoordinatesRegistry().register(
|
24
20
|
name="DMNBuckner",
|
25
21
|
coordinates=np.zeros(2),
|
26
22
|
voi_names=["1", "2"],
|
@@ -29,9 +25,9 @@ def test_register_coordinates_built_in_check() -> None:
|
|
29
25
|
)
|
30
26
|
|
31
27
|
|
32
|
-
def
|
28
|
+
def test_register_overwrite() -> None:
|
33
29
|
"""Test coordinates registration check for overwriting."""
|
34
|
-
|
30
|
+
CoordinatesRegistry().register(
|
35
31
|
name="MyList",
|
36
32
|
coordinates=np.zeros((2, 3)),
|
37
33
|
voi_names=["roi1", "roi2"],
|
@@ -39,14 +35,14 @@ def test_register_coordinates_overwrite() -> None:
|
|
39
35
|
overwrite=True,
|
40
36
|
)
|
41
37
|
with pytest.raises(ValueError, match=r"already registered"):
|
42
|
-
|
38
|
+
CoordinatesRegistry().register(
|
43
39
|
name="MyList",
|
44
40
|
coordinates=np.ones((2, 3)),
|
45
41
|
voi_names=["roi2", "roi3"],
|
46
42
|
space="MNI",
|
47
43
|
)
|
48
44
|
|
49
|
-
|
45
|
+
CoordinatesRegistry().register(
|
50
46
|
name="MyList",
|
51
47
|
coordinates=np.ones((2, 3)),
|
52
48
|
voi_names=["roi2", "roi3"],
|
@@ -54,16 +50,16 @@ def test_register_coordinates_overwrite() -> None:
|
|
54
50
|
overwrite=True,
|
55
51
|
)
|
56
52
|
|
57
|
-
coord, names, space =
|
53
|
+
coord, names, space = CoordinatesRegistry().load("MyList")
|
58
54
|
assert_array_equal(coord, np.ones((2, 3)))
|
59
55
|
assert names == ["roi2", "roi3"]
|
60
56
|
assert space == "MNI"
|
61
57
|
|
62
58
|
|
63
|
-
def
|
59
|
+
def test_register_valid_input() -> None:
|
64
60
|
"""Test coordinates registration check for valid input."""
|
65
61
|
with pytest.raises(TypeError, match=r"numpy.ndarray"):
|
66
|
-
|
62
|
+
CoordinatesRegistry().register(
|
67
63
|
name="MyList",
|
68
64
|
coordinates=[1, 2],
|
69
65
|
voi_names=["roi1", "roi2"],
|
@@ -71,7 +67,7 @@ def test_register_coordinates_valid_input() -> None:
|
|
71
67
|
overwrite=True,
|
72
68
|
)
|
73
69
|
with pytest.raises(ValueError, match=r"2D array"):
|
74
|
-
|
70
|
+
CoordinatesRegistry().register(
|
75
71
|
name="MyList",
|
76
72
|
coordinates=np.zeros((2, 3, 4)),
|
77
73
|
voi_names=["roi1", "roi2"],
|
@@ -80,7 +76,7 @@ def test_register_coordinates_valid_input() -> None:
|
|
80
76
|
)
|
81
77
|
|
82
78
|
with pytest.raises(ValueError, match=r"3 values"):
|
83
|
-
|
79
|
+
CoordinatesRegistry().register(
|
84
80
|
name="MyList",
|
85
81
|
coordinates=np.zeros((2, 4)),
|
86
82
|
voi_names=["roi1", "roi2"],
|
@@ -88,7 +84,7 @@ def test_register_coordinates_valid_input() -> None:
|
|
88
84
|
overwrite=True,
|
89
85
|
)
|
90
86
|
with pytest.raises(ValueError, match=r"voi_names"):
|
91
|
-
|
87
|
+
CoordinatesRegistry().register(
|
92
88
|
name="MyList",
|
93
89
|
coordinates=np.zeros((2, 3)),
|
94
90
|
voi_names=["roi1", "roi2", "roi3"],
|
@@ -97,30 +93,28 @@ def test_register_coordinates_valid_input() -> None:
|
|
97
93
|
)
|
98
94
|
|
99
95
|
|
100
|
-
def
|
96
|
+
def test_list() -> None:
|
101
97
|
"""Test listing of available coordinates."""
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
assert "VigAtt" in available_coordinates
|
106
|
-
assert "WM" in available_coordinates
|
98
|
+
assert {"DMNBuckner", "MultiTask", "VigAtt", "WM"}.issubset(
|
99
|
+
set(CoordinatesRegistry().list)
|
100
|
+
)
|
107
101
|
|
108
102
|
|
109
|
-
def
|
103
|
+
def test_load() -> None:
|
110
104
|
"""Test loading coordinates from file."""
|
111
|
-
coord, names, space =
|
105
|
+
coord, names, space = CoordinatesRegistry().load("DMNBuckner")
|
112
106
|
assert coord.shape == (6, 3) # type: ignore
|
113
107
|
assert names == ["PCC", "MPFC", "lAG", "rAG", "lHF", "rHF"]
|
114
108
|
assert space == "MNI"
|
115
109
|
|
116
110
|
|
117
|
-
def
|
111
|
+
def test_load_nonexisting() -> None:
|
118
112
|
"""Test loading coordinates that not exist."""
|
119
113
|
with pytest.raises(ValueError, match=r"not found"):
|
120
|
-
|
114
|
+
CoordinatesRegistry().load("NonExisting")
|
121
115
|
|
122
116
|
|
123
|
-
def
|
117
|
+
def test_get() -> None:
|
124
118
|
"""Test tailored coordinates fetch."""
|
125
119
|
reader = DefaultDataReader()
|
126
120
|
with OasisVBMTestingDataGrabber() as dg:
|
@@ -128,11 +122,11 @@ def test_get_coordinates() -> None:
|
|
128
122
|
element_data = reader.fit_transform(element)
|
129
123
|
vbm_gm = element_data["VBM_GM"]
|
130
124
|
# Get tailored coordinates
|
131
|
-
tailored_coords, tailored_labels =
|
125
|
+
tailored_coords, tailored_labels = CoordinatesRegistry().get(
|
132
126
|
coords="DMNBuckner", target_data=vbm_gm
|
133
127
|
)
|
134
128
|
# Get raw coordinates
|
135
|
-
raw_coords, raw_labels, _ =
|
129
|
+
raw_coords, raw_labels, _ = CoordinatesRegistry().load("DMNBuckner")
|
136
130
|
# Both tailored and raw should be same for now
|
137
131
|
assert_array_equal(tailored_coords, raw_coords)
|
138
132
|
assert tailored_labels == raw_labels
|