junifer 0.0.3.dev186__py3-none-any.whl → 0.0.4__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 +14 -2
- junifer/api/cli.py +162 -17
- junifer/api/functions.py +87 -419
- junifer/api/parser.py +24 -0
- junifer/api/queue_context/__init__.py +8 -0
- junifer/api/queue_context/gnu_parallel_local_adapter.py +258 -0
- junifer/api/queue_context/htcondor_adapter.py +365 -0
- junifer/api/queue_context/queue_context_adapter.py +60 -0
- junifer/api/queue_context/tests/test_gnu_parallel_local_adapter.py +192 -0
- junifer/api/queue_context/tests/test_htcondor_adapter.py +257 -0
- junifer/api/res/afni/run_afni_docker.sh +6 -6
- junifer/api/res/ants/ResampleImage +3 -0
- junifer/api/res/ants/antsApplyTransforms +3 -0
- junifer/api/res/ants/antsApplyTransformsToPoints +3 -0
- junifer/api/res/ants/run_ants_docker.sh +39 -0
- junifer/api/res/fsl/applywarp +3 -0
- junifer/api/res/fsl/flirt +3 -0
- junifer/api/res/fsl/img2imgcoord +3 -0
- junifer/api/res/fsl/run_fsl_docker.sh +39 -0
- junifer/api/res/fsl/std2imgcoord +3 -0
- junifer/api/res/run_conda.sh +4 -4
- junifer/api/res/run_venv.sh +22 -0
- junifer/api/tests/data/partly_cloudy_agg_mean_tian.yml +16 -0
- junifer/api/tests/test_api_utils.py +21 -3
- junifer/api/tests/test_cli.py +232 -9
- junifer/api/tests/test_functions.py +211 -439
- junifer/api/tests/test_parser.py +1 -1
- junifer/configs/juseless/datagrabbers/aomic_id1000_vbm.py +6 -1
- junifer/configs/juseless/datagrabbers/camcan_vbm.py +6 -1
- junifer/configs/juseless/datagrabbers/ixi_vbm.py +6 -1
- junifer/configs/juseless/datagrabbers/tests/test_ucla.py +8 -8
- junifer/configs/juseless/datagrabbers/ucla.py +44 -26
- junifer/configs/juseless/datagrabbers/ukb_vbm.py +6 -1
- junifer/data/VOIs/meta/AutobiographicalMemory_VOIs.txt +23 -0
- junifer/data/VOIs/meta/Power2013_MNI_VOIs.tsv +264 -0
- junifer/data/__init__.py +4 -0
- junifer/data/coordinates.py +298 -31
- junifer/data/masks.py +360 -28
- junifer/data/parcellations.py +621 -188
- junifer/data/template_spaces.py +190 -0
- junifer/data/tests/test_coordinates.py +34 -3
- junifer/data/tests/test_data_utils.py +1 -0
- junifer/data/tests/test_masks.py +202 -86
- junifer/data/tests/test_parcellations.py +266 -55
- junifer/data/tests/test_template_spaces.py +104 -0
- junifer/data/utils.py +4 -2
- junifer/datagrabber/__init__.py +1 -0
- junifer/datagrabber/aomic/id1000.py +111 -70
- junifer/datagrabber/aomic/piop1.py +116 -53
- junifer/datagrabber/aomic/piop2.py +116 -53
- junifer/datagrabber/aomic/tests/test_id1000.py +27 -27
- junifer/datagrabber/aomic/tests/test_piop1.py +27 -27
- junifer/datagrabber/aomic/tests/test_piop2.py +27 -27
- junifer/datagrabber/base.py +62 -10
- junifer/datagrabber/datalad_base.py +0 -2
- junifer/datagrabber/dmcc13_benchmark.py +372 -0
- junifer/datagrabber/hcp1200/datalad_hcp1200.py +5 -0
- junifer/datagrabber/hcp1200/hcp1200.py +30 -13
- junifer/datagrabber/pattern.py +133 -27
- junifer/datagrabber/pattern_datalad.py +111 -13
- junifer/datagrabber/tests/test_base.py +57 -6
- junifer/datagrabber/tests/test_datagrabber_utils.py +204 -76
- junifer/datagrabber/tests/test_datalad_base.py +0 -6
- junifer/datagrabber/tests/test_dmcc13_benchmark.py +256 -0
- junifer/datagrabber/tests/test_multiple.py +43 -10
- junifer/datagrabber/tests/test_pattern.py +125 -178
- junifer/datagrabber/tests/test_pattern_datalad.py +44 -25
- junifer/datagrabber/utils.py +151 -16
- junifer/datareader/default.py +36 -10
- junifer/external/nilearn/junifer_nifti_spheres_masker.py +6 -0
- junifer/markers/base.py +25 -16
- junifer/markers/collection.py +35 -16
- junifer/markers/complexity/__init__.py +27 -0
- junifer/markers/complexity/complexity_base.py +149 -0
- junifer/markers/complexity/hurst_exponent.py +136 -0
- junifer/markers/complexity/multiscale_entropy_auc.py +140 -0
- junifer/markers/complexity/perm_entropy.py +132 -0
- junifer/markers/complexity/range_entropy.py +136 -0
- junifer/markers/complexity/range_entropy_auc.py +145 -0
- junifer/markers/complexity/sample_entropy.py +134 -0
- junifer/markers/complexity/tests/test_complexity_base.py +19 -0
- junifer/markers/complexity/tests/test_hurst_exponent.py +69 -0
- junifer/markers/complexity/tests/test_multiscale_entropy_auc.py +68 -0
- junifer/markers/complexity/tests/test_perm_entropy.py +68 -0
- junifer/markers/complexity/tests/test_range_entropy.py +69 -0
- junifer/markers/complexity/tests/test_range_entropy_auc.py +69 -0
- junifer/markers/complexity/tests/test_sample_entropy.py +68 -0
- junifer/markers/complexity/tests/test_weighted_perm_entropy.py +68 -0
- junifer/markers/complexity/weighted_perm_entropy.py +133 -0
- junifer/markers/falff/_afni_falff.py +153 -0
- junifer/markers/falff/_junifer_falff.py +142 -0
- junifer/markers/falff/falff_base.py +91 -84
- junifer/markers/falff/falff_parcels.py +61 -45
- junifer/markers/falff/falff_spheres.py +64 -48
- junifer/markers/falff/tests/test_falff_parcels.py +89 -121
- junifer/markers/falff/tests/test_falff_spheres.py +92 -127
- junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +1 -0
- junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +1 -0
- junifer/markers/functional_connectivity/functional_connectivity_base.py +1 -0
- junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py +46 -44
- junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +34 -39
- junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +40 -52
- junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +62 -70
- junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +99 -85
- junifer/markers/parcel_aggregation.py +60 -38
- junifer/markers/reho/_afni_reho.py +192 -0
- junifer/markers/reho/_junifer_reho.py +281 -0
- junifer/markers/reho/reho_base.py +69 -34
- junifer/markers/reho/reho_parcels.py +26 -16
- junifer/markers/reho/reho_spheres.py +23 -9
- junifer/markers/reho/tests/test_reho_parcels.py +93 -92
- junifer/markers/reho/tests/test_reho_spheres.py +88 -86
- junifer/markers/sphere_aggregation.py +54 -9
- junifer/markers/temporal_snr/temporal_snr_base.py +1 -0
- junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +38 -37
- junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +34 -38
- junifer/markers/tests/test_collection.py +43 -42
- junifer/markers/tests/test_ets_rss.py +29 -37
- junifer/markers/tests/test_parcel_aggregation.py +587 -468
- junifer/markers/tests/test_sphere_aggregation.py +209 -157
- junifer/markers/utils.py +2 -40
- junifer/onthefly/read_transform.py +13 -6
- junifer/pipeline/__init__.py +1 -0
- junifer/pipeline/pipeline_step_mixin.py +105 -41
- junifer/pipeline/registry.py +17 -0
- junifer/pipeline/singleton.py +45 -0
- junifer/pipeline/tests/test_pipeline_step_mixin.py +139 -51
- junifer/pipeline/tests/test_update_meta_mixin.py +1 -0
- junifer/pipeline/tests/test_workdir_manager.py +104 -0
- junifer/pipeline/update_meta_mixin.py +8 -2
- junifer/pipeline/utils.py +154 -15
- junifer/pipeline/workdir_manager.py +246 -0
- junifer/preprocess/__init__.py +3 -0
- junifer/preprocess/ants/__init__.py +4 -0
- junifer/preprocess/ants/ants_apply_transforms_warper.py +185 -0
- junifer/preprocess/ants/tests/test_ants_apply_transforms_warper.py +56 -0
- junifer/preprocess/base.py +96 -69
- junifer/preprocess/bold_warper.py +265 -0
- junifer/preprocess/confounds/fmriprep_confound_remover.py +91 -134
- junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +106 -111
- junifer/preprocess/fsl/__init__.py +4 -0
- junifer/preprocess/fsl/apply_warper.py +179 -0
- junifer/preprocess/fsl/tests/test_apply_warper.py +45 -0
- junifer/preprocess/tests/test_bold_warper.py +159 -0
- junifer/preprocess/tests/test_preprocess_base.py +6 -6
- junifer/preprocess/warping/__init__.py +6 -0
- junifer/preprocess/warping/_ants_warper.py +167 -0
- junifer/preprocess/warping/_fsl_warper.py +109 -0
- junifer/preprocess/warping/space_warper.py +213 -0
- junifer/preprocess/warping/tests/test_space_warper.py +198 -0
- junifer/stats.py +18 -4
- junifer/storage/base.py +9 -1
- junifer/storage/hdf5.py +8 -3
- junifer/storage/pandas_base.py +2 -1
- junifer/storage/sqlite.py +1 -0
- junifer/storage/tests/test_hdf5.py +2 -1
- junifer/storage/tests/test_sqlite.py +8 -8
- junifer/storage/tests/test_utils.py +6 -6
- junifer/storage/utils.py +1 -0
- junifer/testing/datagrabbers.py +11 -7
- junifer/testing/utils.py +1 -0
- junifer/tests/test_stats.py +2 -0
- junifer/utils/__init__.py +1 -0
- junifer/utils/helpers.py +53 -0
- junifer/utils/logging.py +14 -3
- junifer/utils/tests/test_helpers.py +35 -0
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/METADATA +59 -28
- junifer-0.0.4.dist-info/RECORD +257 -0
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/WHEEL +1 -1
- junifer/markers/falff/falff_estimator.py +0 -334
- junifer/markers/falff/tests/test_falff_estimator.py +0 -238
- junifer/markers/reho/reho_estimator.py +0 -515
- junifer/markers/reho/tests/test_reho_estimator.py +0 -260
- junifer-0.0.3.dev186.dist-info/RECORD +0 -199
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/AUTHORS.rst +0 -0
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/LICENSE.md +0 -0
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/entry_points.txt +0 -0
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/top_level.txt +0 -0
@@ -23,19 +23,25 @@ class UpdateMetaMixin:
|
|
23
23
|
The data object to update.
|
24
24
|
step_name : str
|
25
25
|
The name of the pipeline step.
|
26
|
+
|
26
27
|
"""
|
28
|
+
# Initialize empty dictionary for the step's metadata
|
27
29
|
t_meta = {}
|
30
|
+
# Set class name for the step
|
28
31
|
t_meta["class"] = self.__class__.__name__
|
32
|
+
# Add object variables to metadata if name doesn't start with "_"
|
29
33
|
for k, v in vars(self).items():
|
30
34
|
if not k.startswith("_"):
|
31
35
|
t_meta[k] = v
|
32
|
-
|
36
|
+
# Add "meta" to the step's local context dict
|
33
37
|
if "meta" not in input:
|
34
38
|
input["meta"] = {}
|
39
|
+
# Add step name
|
35
40
|
input["meta"][step_name] = t_meta
|
41
|
+
# Add step dependencies
|
36
42
|
if "dependencies" not in input["meta"]:
|
37
43
|
input["meta"]["dependencies"] = set()
|
38
|
-
|
44
|
+
# Update step dependencies
|
39
45
|
dependencies = getattr(self, "_DEPENDENCIES", set())
|
40
46
|
if dependencies is not None:
|
41
47
|
if not isinstance(dependencies, (set, list)):
|
junifer/pipeline/utils.py
CHANGED
@@ -10,16 +10,17 @@ from typing import Any, List, Optional
|
|
10
10
|
from junifer.utils.logging import raise_error, warn_with_log
|
11
11
|
|
12
12
|
|
13
|
-
def check_ext_dependencies(
|
13
|
+
def check_ext_dependencies(
|
14
|
+
name: str, optional: bool = False, **kwargs: Any
|
15
|
+
) -> bool:
|
14
16
|
"""Check if external dependency `name` is found if mandatory.
|
15
17
|
|
16
18
|
Parameters
|
17
19
|
----------
|
18
20
|
name : str
|
19
21
|
The name of the dependency.
|
20
|
-
optional : bool
|
21
|
-
Whether the dependency is optional
|
22
|
-
as optional, there should be an implementation provided with junfier.
|
22
|
+
optional : bool, optional
|
23
|
+
Whether the dependency is optional (default False).
|
23
24
|
**kwargs : dict
|
24
25
|
Extra keyword arguments.
|
25
26
|
|
@@ -28,33 +29,49 @@ def check_ext_dependencies(name: str, optional: bool, **kwargs: Any) -> bool:
|
|
28
29
|
bool
|
29
30
|
Whether the external dependency was found.
|
30
31
|
|
32
|
+
Raises
|
33
|
+
------
|
34
|
+
ValueError
|
35
|
+
If ``name`` is invalid.
|
36
|
+
RuntimeError
|
37
|
+
If ``name`` is mandatory and is not found.
|
38
|
+
|
31
39
|
"""
|
40
|
+
valid_ext_dependencies = ("afni", "fsl", "ants")
|
41
|
+
if name not in valid_ext_dependencies:
|
42
|
+
raise_error(
|
43
|
+
"Invalid value for `name`, should be one of: "
|
44
|
+
f"{valid_ext_dependencies}"
|
45
|
+
)
|
32
46
|
# Check for afni
|
33
47
|
if name == "afni":
|
34
48
|
found = _check_afni(**kwargs)
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
49
|
+
# Check for fsl
|
50
|
+
elif name == "fsl":
|
51
|
+
found = _check_fsl(**kwargs)
|
52
|
+
# Check for ants
|
53
|
+
elif name == "ants":
|
54
|
+
found = _check_ants(**kwargs)
|
55
|
+
|
42
56
|
# Check if the dependency is mandatory in case it's not found
|
43
57
|
if not found and not optional:
|
44
58
|
raise_error(
|
45
|
-
|
46
|
-
|
59
|
+
msg=(
|
60
|
+
f"{name} is not installed but is "
|
61
|
+
"required by one of the pipeline steps"
|
62
|
+
),
|
63
|
+
klass=RuntimeError,
|
47
64
|
)
|
48
65
|
return found
|
49
66
|
|
50
67
|
|
51
68
|
def _check_afni(commands: Optional[List[str]] = None) -> bool:
|
52
|
-
"""Check if
|
69
|
+
"""Check if AFNI is present in the system.
|
53
70
|
|
54
71
|
Parameters
|
55
72
|
----------
|
56
73
|
commands : list of str, optional
|
57
|
-
The commands to specifically check for from
|
74
|
+
The commands to specifically check for from AFNI. If None, only
|
58
75
|
the basic afni version would be looked up, else, would also
|
59
76
|
check for specific commands (default None).
|
60
77
|
|
@@ -106,3 +123,125 @@ def _check_afni(commands: Optional[List[str]] = None) -> bool:
|
|
106
123
|
f"{commands_found_results}"
|
107
124
|
)
|
108
125
|
return afni_found
|
126
|
+
|
127
|
+
|
128
|
+
def _check_fsl(commands: Optional[List[str]] = None) -> bool:
|
129
|
+
"""Check if FSL is present in the system.
|
130
|
+
|
131
|
+
Parameters
|
132
|
+
----------
|
133
|
+
commands : list of str, optional
|
134
|
+
The commands to specifically check for from FSL. If None, only
|
135
|
+
the basic FSL flirt version would be looked up, else, would also
|
136
|
+
check for specific commands (default None).
|
137
|
+
|
138
|
+
Returns
|
139
|
+
-------
|
140
|
+
bool
|
141
|
+
Whether FSL is found or not.
|
142
|
+
|
143
|
+
"""
|
144
|
+
completed_process = subprocess.run(
|
145
|
+
"flirt",
|
146
|
+
stdin=subprocess.DEVNULL,
|
147
|
+
stdout=subprocess.DEVNULL,
|
148
|
+
stderr=subprocess.STDOUT,
|
149
|
+
shell=True, # is unsafe but kept for resolution via PATH
|
150
|
+
check=False,
|
151
|
+
)
|
152
|
+
fsl_found = completed_process.returncode == 1
|
153
|
+
|
154
|
+
# Check for specific commands
|
155
|
+
if fsl_found and commands is not None:
|
156
|
+
if not isinstance(commands, list):
|
157
|
+
commands = [commands]
|
158
|
+
# Store command found results
|
159
|
+
commands_found_results = {}
|
160
|
+
# Set all commands found flag to True
|
161
|
+
all_commands_found = True
|
162
|
+
# Check commands' existence
|
163
|
+
for command in commands:
|
164
|
+
command_process = subprocess.run(
|
165
|
+
[command],
|
166
|
+
stdin=subprocess.DEVNULL,
|
167
|
+
stdout=subprocess.DEVNULL,
|
168
|
+
stderr=subprocess.STDOUT,
|
169
|
+
shell=True, # is unsafe but kept for resolution via PATH
|
170
|
+
check=False,
|
171
|
+
)
|
172
|
+
# FSL commands are incoherent with respect to status code hence a
|
173
|
+
# blanket to only look for no command found
|
174
|
+
command_found = command_process.returncode != 127
|
175
|
+
commands_found_results[command] = (
|
176
|
+
"found" if command_found else "not found"
|
177
|
+
)
|
178
|
+
# Set flag to trigger warning
|
179
|
+
all_commands_found = all_commands_found and command_found
|
180
|
+
# One or more commands were missing
|
181
|
+
if not all_commands_found:
|
182
|
+
warn_with_log(
|
183
|
+
"FSL is installed but some of the required commands "
|
184
|
+
"were not found. These are the results: "
|
185
|
+
f"{commands_found_results}"
|
186
|
+
)
|
187
|
+
return fsl_found
|
188
|
+
|
189
|
+
|
190
|
+
def _check_ants(commands: Optional[List[str]] = None) -> bool:
|
191
|
+
"""Check if ANTs is present in the system.
|
192
|
+
|
193
|
+
Parameters
|
194
|
+
----------
|
195
|
+
commands : list of str, optional
|
196
|
+
The commands to specifically check for from ANTs. If None, only
|
197
|
+
the basic ANTS help would be looked up, else, would also
|
198
|
+
check for specific commands (default None).
|
199
|
+
|
200
|
+
Returns
|
201
|
+
-------
|
202
|
+
bool
|
203
|
+
Whether ANTs is found or not.
|
204
|
+
|
205
|
+
"""
|
206
|
+
completed_process = subprocess.run(
|
207
|
+
"ANTS --help",
|
208
|
+
stdin=subprocess.DEVNULL,
|
209
|
+
stdout=subprocess.DEVNULL,
|
210
|
+
stderr=subprocess.STDOUT,
|
211
|
+
shell=True, # is unsafe but kept for resolution via PATH
|
212
|
+
check=False,
|
213
|
+
)
|
214
|
+
ants_found = completed_process.returncode == 0
|
215
|
+
|
216
|
+
# Check for specific commands
|
217
|
+
if ants_found and commands is not None:
|
218
|
+
if not isinstance(commands, list):
|
219
|
+
commands = [commands]
|
220
|
+
# Store command found results
|
221
|
+
commands_found_results = {}
|
222
|
+
# Set all commands found flag to True
|
223
|
+
all_commands_found = True
|
224
|
+
# Check commands' existence
|
225
|
+
for command in commands:
|
226
|
+
command_process = subprocess.run(
|
227
|
+
[command],
|
228
|
+
stdin=subprocess.DEVNULL,
|
229
|
+
stdout=subprocess.DEVNULL,
|
230
|
+
stderr=subprocess.STDOUT,
|
231
|
+
shell=True, # is unsafe but kept for resolution via PATH
|
232
|
+
check=False,
|
233
|
+
)
|
234
|
+
command_found = command_process.returncode == 0
|
235
|
+
commands_found_results[command] = (
|
236
|
+
"found" if command_found else "not found"
|
237
|
+
)
|
238
|
+
# Set flag to trigger warning
|
239
|
+
all_commands_found = all_commands_found and command_found
|
240
|
+
# One or more commands were missing
|
241
|
+
if not all_commands_found:
|
242
|
+
warn_with_log(
|
243
|
+
"ANTs is installed but some of the required commands "
|
244
|
+
"were not found. These are the results: "
|
245
|
+
f"{commands_found_results}"
|
246
|
+
)
|
247
|
+
return ants_found
|
@@ -0,0 +1,246 @@
|
|
1
|
+
"""Provide a work directory manager class to be used by pipeline components."""
|
2
|
+
|
3
|
+
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
4
|
+
# Federico Raimondo <f.raimondo@fz-juelich.de>
|
5
|
+
# License: AGPL
|
6
|
+
|
7
|
+
import shutil
|
8
|
+
import tempfile
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Optional, Union
|
11
|
+
|
12
|
+
from ..utils import logger
|
13
|
+
from .singleton import singleton
|
14
|
+
|
15
|
+
|
16
|
+
@singleton
|
17
|
+
class WorkDirManager:
|
18
|
+
"""Class for working directory manager.
|
19
|
+
|
20
|
+
This class is a singleton and is used for managing temporary and working
|
21
|
+
directories used across the pipeline by datagrabbers, preprocessors,
|
22
|
+
markers and so on. It maintains a single super-directory and provides
|
23
|
+
directories on-demand and cleans after itself thus keeping the user
|
24
|
+
filesystem clean.
|
25
|
+
|
26
|
+
Parameters
|
27
|
+
----------
|
28
|
+
workdir : str or pathlib.Path, optional
|
29
|
+
The path to the super-directory. If None, "TMPDIR/junifer" is used
|
30
|
+
where TMPDIR is the platform-dependent temporary directory.
|
31
|
+
|
32
|
+
Attributes
|
33
|
+
----------
|
34
|
+
workdir : pathlib.Path
|
35
|
+
The path to the working directory.
|
36
|
+
elementdir : pathlib.Path
|
37
|
+
The path to the element directory.
|
38
|
+
root_tempdir : pathlib.Path or None
|
39
|
+
The path to the root temporary directory.
|
40
|
+
|
41
|
+
"""
|
42
|
+
|
43
|
+
def __init__(self, workdir: Optional[Union[str, Path]] = None) -> None:
|
44
|
+
"""Initialize the class."""
|
45
|
+
self._workdir = Path(workdir) if isinstance(workdir, str) else workdir
|
46
|
+
self._elementdir = None
|
47
|
+
self._root_tempdir = None
|
48
|
+
|
49
|
+
self._set_default_workdir()
|
50
|
+
|
51
|
+
def _set_default_workdir(self) -> None:
|
52
|
+
"""Set the default working directory if not set already."""
|
53
|
+
# Check and set topmost level directory if not provided
|
54
|
+
if self._workdir is None:
|
55
|
+
self._workdir = Path(tempfile.gettempdir()) / "junifer"
|
56
|
+
# Create directory if not found
|
57
|
+
if not self._workdir.is_dir():
|
58
|
+
logger.debug(
|
59
|
+
"Creating working directory at "
|
60
|
+
f"{self._workdir.resolve()!s}"
|
61
|
+
)
|
62
|
+
self._workdir.mkdir(parents=True)
|
63
|
+
logger.debug(
|
64
|
+
f"Setting working directory to {self._workdir.resolve()!s}"
|
65
|
+
)
|
66
|
+
|
67
|
+
def __del__(self) -> None:
|
68
|
+
"""Destructor."""
|
69
|
+
self._cleanup()
|
70
|
+
|
71
|
+
def _cleanup(self) -> None:
|
72
|
+
"""Clean up the element and temporary directories."""
|
73
|
+
# Remove element directory
|
74
|
+
self.cleanup_elementdir()
|
75
|
+
# Remove root temporary directory
|
76
|
+
if self._root_tempdir is not None:
|
77
|
+
logger.debug(
|
78
|
+
"Deleting temporary directory at "
|
79
|
+
f"{self._root_tempdir.resolve()!s}"
|
80
|
+
)
|
81
|
+
shutil.rmtree(self._root_tempdir, ignore_errors=True)
|
82
|
+
self._root_tempdir = None
|
83
|
+
|
84
|
+
@property
|
85
|
+
def workdir(self) -> Path:
|
86
|
+
"""Get working directory."""
|
87
|
+
return self._workdir # type: ignore
|
88
|
+
|
89
|
+
@workdir.setter
|
90
|
+
def workdir(self, path: Union[str, Path]) -> None:
|
91
|
+
"""Set working directory.
|
92
|
+
|
93
|
+
The directory path is created if it doesn't exist yet.
|
94
|
+
|
95
|
+
Parameters
|
96
|
+
----------
|
97
|
+
path : str or pathlib.Path
|
98
|
+
The path to the working directory.
|
99
|
+
|
100
|
+
"""
|
101
|
+
# Check if existing working directory is same or not;
|
102
|
+
# if not, then clean up
|
103
|
+
if self._workdir != Path(path):
|
104
|
+
self._cleanup()
|
105
|
+
# Set working directory
|
106
|
+
self._workdir = Path(path)
|
107
|
+
logger.debug(
|
108
|
+
f"Changing working directory to {self._workdir.resolve()!s}"
|
109
|
+
)
|
110
|
+
# Create directory if not found
|
111
|
+
if not self._workdir.is_dir():
|
112
|
+
logger.debug(
|
113
|
+
f"Creating working directory at {self._workdir.resolve()!s}"
|
114
|
+
)
|
115
|
+
self._workdir.mkdir(parents=True)
|
116
|
+
|
117
|
+
@property
|
118
|
+
def elementdir(self) -> Path:
|
119
|
+
"""Get element directory."""
|
120
|
+
return self._elementdir # type: ignore
|
121
|
+
|
122
|
+
def get_element_tempdir(
|
123
|
+
self, prefix: Optional[str] = None, suffix: Optional[str] = None
|
124
|
+
) -> Path:
|
125
|
+
"""Get an element-scoped temporary directory.
|
126
|
+
|
127
|
+
This directory should be available only for the lifetime of an
|
128
|
+
element.
|
129
|
+
|
130
|
+
Parameters
|
131
|
+
----------
|
132
|
+
prefix : str, optional
|
133
|
+
The temporary directory prefix. If None, a default prefix is used
|
134
|
+
(default None).
|
135
|
+
suffix : str, optional
|
136
|
+
The temporary directory suffix. If None, no suffix is added
|
137
|
+
(default None).
|
138
|
+
|
139
|
+
Returns
|
140
|
+
-------
|
141
|
+
pathlib.Path
|
142
|
+
The path to the temporary directory.
|
143
|
+
|
144
|
+
"""
|
145
|
+
# Create element directory if not created already
|
146
|
+
if self._elementdir is None:
|
147
|
+
logger.debug(
|
148
|
+
"Setting up element directory under "
|
149
|
+
f"{self._workdir.resolve()!s}" # type: ignore
|
150
|
+
)
|
151
|
+
self._elementdir = Path(tempfile.mkdtemp(dir=self._workdir))
|
152
|
+
|
153
|
+
logger.debug(
|
154
|
+
"Creating element temporary directory at "
|
155
|
+
f"{self._elementdir.resolve()!s}"
|
156
|
+
)
|
157
|
+
return Path(
|
158
|
+
tempfile.mkdtemp(
|
159
|
+
dir=self._elementdir, prefix=prefix, suffix=suffix
|
160
|
+
)
|
161
|
+
)
|
162
|
+
|
163
|
+
def delete_element_tempdir(self, tempdir: Path) -> None:
|
164
|
+
"""Delete an element-scoped temporary directory.
|
165
|
+
|
166
|
+
Parameters
|
167
|
+
----------
|
168
|
+
tempdir : pathlib.Path
|
169
|
+
The temporary directory path to be deleted.
|
170
|
+
|
171
|
+
"""
|
172
|
+
logger.debug(f"Deleting element temporary directory at {tempdir}")
|
173
|
+
shutil.rmtree(tempdir, ignore_errors=True)
|
174
|
+
|
175
|
+
def cleanup_elementdir(self) -> None:
|
176
|
+
"""Clean up element directory.
|
177
|
+
|
178
|
+
It should preferably be used after fitting a marker or something
|
179
|
+
similar in the element-specific scope. If called between components,
|
180
|
+
can lead to required intermediate files not being found.
|
181
|
+
|
182
|
+
"""
|
183
|
+
if self._elementdir is not None:
|
184
|
+
logger.debug(
|
185
|
+
"Deleting element directory at "
|
186
|
+
f"{self._elementdir.resolve()!s}"
|
187
|
+
)
|
188
|
+
shutil.rmtree(self._elementdir, ignore_errors=True)
|
189
|
+
self._elementdir = None
|
190
|
+
|
191
|
+
@property
|
192
|
+
def root_tempdir(self) -> Optional[Path]:
|
193
|
+
"""Get root temporary directory."""
|
194
|
+
return self._root_tempdir
|
195
|
+
|
196
|
+
def get_tempdir(
|
197
|
+
self, prefix: Optional[str] = None, suffix: Optional[str] = None
|
198
|
+
) -> Path:
|
199
|
+
"""Get a component-scoped temporary directory.
|
200
|
+
|
201
|
+
This directory should be available only for the lifetime of a component
|
202
|
+
like a preprocessor or marker.
|
203
|
+
|
204
|
+
Parameters
|
205
|
+
----------
|
206
|
+
prefix : str, optional
|
207
|
+
The temporary directory prefix. If None, a default prefix is used
|
208
|
+
(default None).
|
209
|
+
suffix : str, optional
|
210
|
+
The temporary directory suffix. If None, no suffix is added
|
211
|
+
(default None).
|
212
|
+
|
213
|
+
Returns
|
214
|
+
-------
|
215
|
+
pathlib.Path
|
216
|
+
The path to the temporary directory.
|
217
|
+
|
218
|
+
"""
|
219
|
+
# Create root temporary directory if not created already
|
220
|
+
if self._root_tempdir is None:
|
221
|
+
logger.debug(
|
222
|
+
"Setting up temporary directory under "
|
223
|
+
f"{self._workdir.resolve()!s}" # type: ignore
|
224
|
+
)
|
225
|
+
self._root_tempdir = Path(tempfile.mkdtemp(dir=self._workdir))
|
226
|
+
|
227
|
+
logger.debug(
|
228
|
+
f"Creating temporary directory at {self._root_tempdir.resolve()!s}"
|
229
|
+
)
|
230
|
+
return Path(
|
231
|
+
tempfile.mkdtemp(
|
232
|
+
dir=self._root_tempdir, prefix=prefix, suffix=suffix
|
233
|
+
)
|
234
|
+
)
|
235
|
+
|
236
|
+
def delete_tempdir(self, tempdir: Path) -> None:
|
237
|
+
"""Delete a component-scoped temporary directory.
|
238
|
+
|
239
|
+
Parameters
|
240
|
+
----------
|
241
|
+
tempdir : pathlib.Path
|
242
|
+
The temporary directory path to be deleted.
|
243
|
+
|
244
|
+
"""
|
245
|
+
logger.debug(f"Deleting temporary directory at {tempdir}")
|
246
|
+
shutil.rmtree(tempdir, ignore_errors=True)
|
junifer/preprocess/__init__.py
CHANGED
@@ -2,7 +2,10 @@
|
|
2
2
|
|
3
3
|
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
4
4
|
# Leonard Sasse <l.sasse@fz-juelich.de>
|
5
|
+
# Synchon Mandal <s.mandal@fz-juelich.de>
|
5
6
|
# License: AGPL
|
6
7
|
|
7
8
|
from .base import BasePreprocessor
|
8
9
|
from .confounds import fMRIPrepConfoundRemover
|
10
|
+
from .bold_warper import BOLDWarper
|
11
|
+
from .warping import SpaceWarper
|
@@ -0,0 +1,185 @@
|
|
1
|
+
"""Provide class for warping via ANTs antsApplyTransforms."""
|
2
|
+
|
3
|
+
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
4
|
+
# License: AGPL
|
5
|
+
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import (
|
8
|
+
TYPE_CHECKING,
|
9
|
+
Any,
|
10
|
+
ClassVar,
|
11
|
+
Dict,
|
12
|
+
List,
|
13
|
+
Optional,
|
14
|
+
Tuple,
|
15
|
+
Union,
|
16
|
+
)
|
17
|
+
|
18
|
+
import nibabel as nib
|
19
|
+
import numpy as np
|
20
|
+
|
21
|
+
from ...pipeline import WorkDirManager
|
22
|
+
from ...utils import logger, raise_error, run_ext_cmd
|
23
|
+
|
24
|
+
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
from nibabel import Nifti1Image
|
27
|
+
|
28
|
+
|
29
|
+
class _AntsApplyTransformsWarper:
|
30
|
+
"""Class for warping NIfTI images via ANTs antsApplyTransforms.
|
31
|
+
|
32
|
+
Warps ANTs ``antsApplyTransforms``.
|
33
|
+
|
34
|
+
Parameters
|
35
|
+
----------
|
36
|
+
reference : str
|
37
|
+
The data type to use as reference for warping.
|
38
|
+
on : str
|
39
|
+
The data type to use for warping.
|
40
|
+
|
41
|
+
Raises
|
42
|
+
------
|
43
|
+
ValueError
|
44
|
+
If a list was passed for ``on``.
|
45
|
+
|
46
|
+
"""
|
47
|
+
|
48
|
+
_EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [
|
49
|
+
{
|
50
|
+
"name": "ants",
|
51
|
+
"commands": ["ResampleImage", "antsApplyTransforms"],
|
52
|
+
},
|
53
|
+
]
|
54
|
+
|
55
|
+
def __init__(self, reference: str, on: str) -> None:
|
56
|
+
"""Initialize the class."""
|
57
|
+
self.ref = reference
|
58
|
+
# Check only single data type is passed
|
59
|
+
if isinstance(on, list):
|
60
|
+
raise_error("Can only work on single data type, list was passed.")
|
61
|
+
self.on = on
|
62
|
+
|
63
|
+
def _run_apply_transforms(
|
64
|
+
self,
|
65
|
+
input_data: Dict,
|
66
|
+
ref_path: Path,
|
67
|
+
warp_path: Path,
|
68
|
+
) -> Tuple["Nifti1Image", Path]:
|
69
|
+
"""Run ``antsApplyTransforms``.
|
70
|
+
|
71
|
+
Parameters
|
72
|
+
----------
|
73
|
+
input_data : dict
|
74
|
+
The input data.
|
75
|
+
ref_path : pathlib.Path
|
76
|
+
The path to the reference file.
|
77
|
+
warp_path : pathlib.Path
|
78
|
+
The path to the warp file.
|
79
|
+
|
80
|
+
Returns
|
81
|
+
-------
|
82
|
+
Niimg-like object
|
83
|
+
The warped input image.
|
84
|
+
pathlib.Path
|
85
|
+
The path to the resampled reference image.
|
86
|
+
|
87
|
+
"""
|
88
|
+
# Get the min of the voxel sizes from input and use it as the
|
89
|
+
# resolution
|
90
|
+
resolution = np.min(input_data["data"].header.get_zooms()[:3])
|
91
|
+
|
92
|
+
# Create element-specific tempdir for storing post-warping assets
|
93
|
+
tempdir = WorkDirManager().get_element_tempdir(
|
94
|
+
prefix="applytransforms"
|
95
|
+
)
|
96
|
+
|
97
|
+
# Create a tempfile for resampled reference output
|
98
|
+
resample_image_out_path = tempdir / "reference_resampled.nii.gz"
|
99
|
+
# Set ResampleImage command
|
100
|
+
resample_image_cmd = [
|
101
|
+
"ResampleImage",
|
102
|
+
"3", # image dimension
|
103
|
+
f"{ref_path.resolve()}",
|
104
|
+
f"{resample_image_out_path.resolve()}",
|
105
|
+
f"{resolution}x{resolution}x{resolution}",
|
106
|
+
"0", # option for spacing and not size
|
107
|
+
"3 3", # Lanczos windowed sinc
|
108
|
+
]
|
109
|
+
# Call ResampleImage
|
110
|
+
run_ext_cmd(name="ResampleImage", cmd=resample_image_cmd)
|
111
|
+
|
112
|
+
# Create a tempfile for warped output
|
113
|
+
apply_transforms_out_path = tempdir / "input_warped.nii.gz"
|
114
|
+
# Set antsApplyTransforms command
|
115
|
+
apply_transforms_cmd = [
|
116
|
+
"antsApplyTransforms",
|
117
|
+
"-d 3",
|
118
|
+
"-e 3",
|
119
|
+
"-n LanczosWindowedSinc",
|
120
|
+
f"-i {input_data['path'].resolve()}",
|
121
|
+
# use resampled reference
|
122
|
+
f"-r {resample_image_out_path.resolve()}",
|
123
|
+
f"-t {warp_path.resolve()}",
|
124
|
+
f"-o {apply_transforms_out_path.resolve()}",
|
125
|
+
]
|
126
|
+
# Call antsApplyTransforms
|
127
|
+
run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd)
|
128
|
+
|
129
|
+
# Load nifti
|
130
|
+
output_img = nib.load(apply_transforms_out_path)
|
131
|
+
|
132
|
+
return output_img, resample_image_out_path # type: ignore
|
133
|
+
|
134
|
+
def preprocess(
|
135
|
+
self,
|
136
|
+
input: Dict[str, Any],
|
137
|
+
extra_input: Optional[Dict[str, Any]] = None,
|
138
|
+
) -> Tuple[str, Dict[str, Any]]:
|
139
|
+
"""Preprocess.
|
140
|
+
|
141
|
+
Parameters
|
142
|
+
----------
|
143
|
+
input : dict
|
144
|
+
A single input from the Junifer Data object in which to preprocess.
|
145
|
+
extra_input : dict, optional
|
146
|
+
The other fields in the Junifer Data object. Must include the
|
147
|
+
``Warp`` and ``ref`` value's keys.
|
148
|
+
|
149
|
+
Returns
|
150
|
+
-------
|
151
|
+
str
|
152
|
+
The key to store the output in the Junifer Data object.
|
153
|
+
dict
|
154
|
+
The computed result as dictionary. This will be stored in the
|
155
|
+
Junifer Data object under the key ``data`` of the data type.
|
156
|
+
|
157
|
+
Raises
|
158
|
+
------
|
159
|
+
ValueError
|
160
|
+
If ``extra_input`` is None.
|
161
|
+
|
162
|
+
"""
|
163
|
+
logger.debug("Warping via ANTs using antsApplyTransforms")
|
164
|
+
# Check for extra inputs
|
165
|
+
if extra_input is None:
|
166
|
+
raise_error(
|
167
|
+
f"No extra input provided, requires `Warp` and `{self.ref}` "
|
168
|
+
"data types in particular."
|
169
|
+
)
|
170
|
+
# Retrieve data type info to warp
|
171
|
+
to_warp_input = input
|
172
|
+
# Retrieve data type info to use as reference
|
173
|
+
ref_input = extra_input[self.ref]
|
174
|
+
# Retrieve Warp data
|
175
|
+
warp = extra_input["Warp"]
|
176
|
+
# Replace original data with warped data and add resampled reference
|
177
|
+
# path
|
178
|
+
input["data"], input["reference_path"] = self._run_apply_transforms(
|
179
|
+
input_data=to_warp_input,
|
180
|
+
ref_path=ref_input["path"],
|
181
|
+
warp_path=warp["path"],
|
182
|
+
)
|
183
|
+
# Use reference input's space as warped input's space
|
184
|
+
input["space"] = ref_input["space"]
|
185
|
+
return self.on, input
|