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
@@ -7,11 +7,11 @@
|
|
7
7
|
from typing import Any, ClassVar, Dict, List, Optional, Set, Union
|
8
8
|
|
9
9
|
import numpy as np
|
10
|
-
from nilearn.image import math_img
|
10
|
+
from nilearn.image import math_img
|
11
11
|
from nilearn.maskers import NiftiMasker
|
12
12
|
|
13
13
|
from ..api.decorators import register_marker
|
14
|
-
from ..data import get_mask,
|
14
|
+
from ..data import get_mask, get_parcellation
|
15
15
|
from ..stats import get_aggfunc_by_name
|
16
16
|
from ..utils import logger, raise_error, warn_with_log
|
17
17
|
from .base import BaseMarker
|
@@ -42,13 +42,20 @@ class ParcelAggregation(BaseMarker):
|
|
42
42
|
The specification of the masks to apply to regions before extracting
|
43
43
|
signals. Check :ref:`Using Masks <using_masks>` for more details.
|
44
44
|
If None, will not apply any mask (default None).
|
45
|
-
on : {"T1w", "BOLD", "VBM_GM", "VBM_WM", "
|
46
|
-
|
45
|
+
on : {"T1w", "T2w", "BOLD", "VBM_GM", "VBM_WM", "VBM_CSF", "fALFF", \
|
46
|
+
"GCOR", "LCOR"} or list of the options, optional
|
47
47
|
The data types to apply the marker to. If None, will work on all
|
48
48
|
available data (default None).
|
49
49
|
name : str, optional
|
50
50
|
The name of the marker. If None, will use the class name (default
|
51
51
|
None).
|
52
|
+
|
53
|
+
Raises
|
54
|
+
------
|
55
|
+
ValueError
|
56
|
+
If ``time_method`` is specified for non-BOLD data or if
|
57
|
+
``time_method_params`` is not None when ``time_method`` is None.
|
58
|
+
|
52
59
|
"""
|
53
60
|
|
54
61
|
_DEPENDENCIES: ClassVar[Set[str]] = {"nilearn", "numpy"}
|
@@ -95,7 +102,17 @@ class ParcelAggregation(BaseMarker):
|
|
95
102
|
The list of data types that can be used as input for this marker.
|
96
103
|
|
97
104
|
"""
|
98
|
-
return [
|
105
|
+
return [
|
106
|
+
"T1w",
|
107
|
+
"T2w",
|
108
|
+
"BOLD",
|
109
|
+
"VBM_GM",
|
110
|
+
"VBM_WM",
|
111
|
+
"VBM_CSF",
|
112
|
+
"fALFF",
|
113
|
+
"GCOR",
|
114
|
+
"LCOR",
|
115
|
+
]
|
99
116
|
|
100
117
|
def get_output_type(self, input_type: str) -> str:
|
101
118
|
"""Get output type.
|
@@ -110,14 +127,26 @@ class ParcelAggregation(BaseMarker):
|
|
110
127
|
str
|
111
128
|
The storage type output by the marker.
|
112
129
|
|
130
|
+
Raises
|
131
|
+
------
|
132
|
+
ValueError
|
133
|
+
If the ``input_type`` is invalid.
|
134
|
+
|
113
135
|
"""
|
114
136
|
|
115
|
-
if input_type in [
|
137
|
+
if input_type in [
|
138
|
+
"VBM_GM",
|
139
|
+
"VBM_WM",
|
140
|
+
"VBM_CSF",
|
141
|
+
"fALFF",
|
142
|
+
"GCOR",
|
143
|
+
"LCOR",
|
144
|
+
]:
|
116
145
|
return "vector"
|
117
146
|
elif input_type == "BOLD":
|
118
147
|
return "timeseries"
|
119
148
|
else:
|
120
|
-
|
149
|
+
raise_error(f"Unknown input kind for {input_type}")
|
121
150
|
|
122
151
|
def compute(
|
123
152
|
self, input: Dict[str, Any], extra_input: Optional[Dict] = None
|
@@ -145,62 +174,53 @@ class ParcelAggregation(BaseMarker):
|
|
145
174
|
* ``data`` : the actual computed values as a numpy.ndarray
|
146
175
|
* ``col_names`` : the column labels for the computed values as list
|
147
176
|
|
177
|
+
Warns
|
178
|
+
-----
|
179
|
+
RuntimeWarning
|
180
|
+
If time aggregation is required but only time point is available.
|
181
|
+
|
148
182
|
"""
|
149
183
|
t_input_img = input["data"]
|
150
184
|
logger.debug(f"Parcel aggregation using {self.method}")
|
185
|
+
# Get aggregation function
|
151
186
|
agg_func = get_aggfunc_by_name(
|
152
187
|
name=self.method, func_params=self.method_params
|
153
188
|
)
|
154
|
-
# Get the min of the voxels sizes and use it as the resolution
|
155
|
-
resolution = np.min(t_input_img.header.get_zooms()[:3])
|
156
|
-
|
157
|
-
# Load the parcellations
|
158
|
-
all_parcelations = []
|
159
|
-
all_labels = []
|
160
|
-
for t_parc_name in self.parcellation:
|
161
|
-
t_parcellation, t_labels, _ = load_parcellation(
|
162
|
-
name=t_parc_name, resolution=resolution
|
163
|
-
)
|
164
|
-
# Resample all of them to the image
|
165
|
-
t_parcellation_img_res = resample_to_img(
|
166
|
-
t_parcellation, t_input_img, interpolation="nearest", copy=True
|
167
|
-
)
|
168
|
-
all_parcelations.append(t_parcellation_img_res)
|
169
|
-
all_labels.append(t_labels)
|
170
189
|
|
171
|
-
#
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
parcellation_img_res, labels = merge_parcellations(
|
178
|
-
all_parcelations, self.parcellation, all_labels
|
179
|
-
)
|
190
|
+
# Get parcellation tailored to target image
|
191
|
+
parcellation_img, labels = get_parcellation(
|
192
|
+
parcellation=self.parcellation,
|
193
|
+
target_data=input,
|
194
|
+
extra_input=extra_input,
|
195
|
+
)
|
180
196
|
|
181
|
-
|
197
|
+
# Get binarized parcellation image for masking
|
198
|
+
parcellation_bin = math_img("img != 0", img=parcellation_img)
|
182
199
|
|
200
|
+
# Load mask
|
183
201
|
if self.masks is not None:
|
184
202
|
logger.debug(f"Masking with {self.masks}")
|
203
|
+
# Get tailored mask
|
185
204
|
mask_img = get_mask(
|
186
205
|
masks=self.masks, target_data=input, extra_input=extra_input
|
187
206
|
)
|
188
|
-
|
207
|
+
# Get "logical and" version of parcellation and mask
|
189
208
|
parcellation_bin = math_img(
|
190
209
|
"np.logical_and(img, mask)",
|
191
210
|
img=parcellation_bin,
|
192
211
|
mask=mask_img,
|
193
212
|
)
|
194
213
|
|
214
|
+
# Initialize masker
|
195
215
|
logger.debug("Masking")
|
196
216
|
masker = NiftiMasker(
|
197
217
|
parcellation_bin, target_affine=t_input_img.affine
|
198
|
-
)
|
199
|
-
|
218
|
+
)
|
200
219
|
# Mask the input data and the parcellation
|
201
220
|
data = masker.fit_transform(t_input_img)
|
202
|
-
parcellation_values =
|
203
|
-
|
221
|
+
parcellation_values = np.squeeze(
|
222
|
+
masker.transform(parcellation_img)
|
223
|
+
).astype(int)
|
204
224
|
|
205
225
|
# Get the values for each parcel and apply agg function
|
206
226
|
logger.debug("Computing ROI means")
|
@@ -214,6 +234,7 @@ class ParcelAggregation(BaseMarker):
|
|
214
234
|
|
215
235
|
out_values = np.array(out_values).T
|
216
236
|
|
237
|
+
# Apply time dimension aggregation if required
|
217
238
|
if self.time_method is not None:
|
218
239
|
if out_values.shape[0] > 1:
|
219
240
|
logger.debug("Aggregating time dimension")
|
@@ -226,5 +247,6 @@ class ParcelAggregation(BaseMarker):
|
|
226
247
|
"No time dimension to aggregate as only one time point is "
|
227
248
|
"available."
|
228
249
|
)
|
250
|
+
# Format the output
|
229
251
|
out = {"data": out_values, "col_names": labels}
|
230
252
|
return out
|
@@ -0,0 +1,192 @@
|
|
1
|
+
"""Provide class for computing regional homogeneity (ReHo) using AFNI."""
|
2
|
+
|
3
|
+
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
4
|
+
# License: AGPL
|
5
|
+
|
6
|
+
from functools import lru_cache
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import (
|
9
|
+
TYPE_CHECKING,
|
10
|
+
ClassVar,
|
11
|
+
Dict,
|
12
|
+
List,
|
13
|
+
Optional,
|
14
|
+
Tuple,
|
15
|
+
Union,
|
16
|
+
)
|
17
|
+
|
18
|
+
import nibabel as nib
|
19
|
+
|
20
|
+
from ...pipeline import WorkDirManager
|
21
|
+
from ...pipeline.singleton import singleton
|
22
|
+
from ...utils import logger, run_ext_cmd
|
23
|
+
|
24
|
+
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
from nibabel import Nifti1Image
|
27
|
+
|
28
|
+
|
29
|
+
@singleton
|
30
|
+
class AFNIReHo:
|
31
|
+
"""Class for computing ReHo using AFNI.
|
32
|
+
|
33
|
+
This class uses AFNI's 3dReHo to compute ReHo. It's designed as a singleton
|
34
|
+
with caching for efficient computation.
|
35
|
+
|
36
|
+
"""
|
37
|
+
|
38
|
+
_EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [
|
39
|
+
{
|
40
|
+
"name": "afni",
|
41
|
+
"commands": ["3dReHo", "3dAFNItoNIFTI"],
|
42
|
+
},
|
43
|
+
]
|
44
|
+
|
45
|
+
def __del__(self) -> None:
|
46
|
+
"""Terminate the class."""
|
47
|
+
# Clear the computation cache
|
48
|
+
logger.debug("Clearing cache for ReHo computation via AFNI")
|
49
|
+
self.compute.cache_clear()
|
50
|
+
|
51
|
+
@lru_cache(maxsize=None, typed=True)
|
52
|
+
def compute(
|
53
|
+
self,
|
54
|
+
data: "Nifti1Image",
|
55
|
+
nneigh: int = 27,
|
56
|
+
neigh_rad: Optional[float] = None,
|
57
|
+
neigh_x: Optional[float] = None,
|
58
|
+
neigh_y: Optional[float] = None,
|
59
|
+
neigh_z: Optional[float] = None,
|
60
|
+
box_rad: Optional[int] = None,
|
61
|
+
box_x: Optional[int] = None,
|
62
|
+
box_y: Optional[int] = None,
|
63
|
+
box_z: Optional[int] = None,
|
64
|
+
) -> Tuple["Nifti1Image", Path]:
|
65
|
+
"""Compute ReHo map.
|
66
|
+
|
67
|
+
Parameters
|
68
|
+
----------
|
69
|
+
data : 4D Niimg-like object
|
70
|
+
Images to process.
|
71
|
+
nneigh : {7, 19, 27}, optional
|
72
|
+
Number of voxels in the neighbourhood, inclusive. Can be:
|
73
|
+
|
74
|
+
* 7 : for facewise neighbours only
|
75
|
+
* 19 : for face- and edge-wise nieghbours
|
76
|
+
* 27 : for face-, edge-, and node-wise neighbors
|
77
|
+
|
78
|
+
(default 27).
|
79
|
+
neigh_rad : positive float, optional
|
80
|
+
The radius of a desired neighbourhood (default None).
|
81
|
+
neigh_x : positive float, optional
|
82
|
+
The semi-radius for x-axis of ellipsoidal volumes (default None).
|
83
|
+
neigh_y : positive float, optional
|
84
|
+
The semi-radius for y-axis of ellipsoidal volumes (default None).
|
85
|
+
neigh_z : positive float, optional
|
86
|
+
The semi-radius for z-axis of ellipsoidal volumes (default None).
|
87
|
+
box_rad : positive int, optional
|
88
|
+
The number of voxels outward in a given cardinal direction for a
|
89
|
+
cubic box centered on a given voxel (default None).
|
90
|
+
box_x : positive int, optional
|
91
|
+
The number of voxels for +/- x-axis of cuboidal volumes
|
92
|
+
(default None).
|
93
|
+
box_y : positive int, optional
|
94
|
+
The number of voxels for +/- y-axis of cuboidal volumes
|
95
|
+
(default None).
|
96
|
+
box_z : positive int, optional
|
97
|
+
The number of voxels for +/- z-axis of cuboidal volumes
|
98
|
+
(default None).
|
99
|
+
|
100
|
+
Returns
|
101
|
+
-------
|
102
|
+
Niimg-like object
|
103
|
+
The ReHo map as NIfTI.
|
104
|
+
pathlib.Path
|
105
|
+
The path to the ReHo map as NIfTI.
|
106
|
+
|
107
|
+
Notes
|
108
|
+
-----
|
109
|
+
For more information on the publication, please check [1]_ , and for
|
110
|
+
3dReHo help check:
|
111
|
+
https://afni.nimh.nih.gov/pub/dist/doc/program_help/3dReHo.html
|
112
|
+
|
113
|
+
Please note that that you cannot mix ``box_*`` and ``neigh_*``
|
114
|
+
arguments. The arguments are prioritized by their order in the function
|
115
|
+
signature.
|
116
|
+
|
117
|
+
As the process also depends on the conversion of AFNI files to NIFTI
|
118
|
+
via afni's 3dAFNItoNIFTI, the help for that can be found at:
|
119
|
+
https://afni.nimh.nih.gov/pub/dist/doc/program_help/3dAFNItoNIFTI.html
|
120
|
+
|
121
|
+
References
|
122
|
+
----------
|
123
|
+
.. [1] Taylor, P.A., & Saad, Z.S. (2013).
|
124
|
+
FATCAT: (An Efficient) Functional And Tractographic Connectivity
|
125
|
+
Analysis Toolbox.
|
126
|
+
Brain connectivity, Volume 3(5), Pages 523-35.
|
127
|
+
https://doi.org/10.1089/brain.2013.0154
|
128
|
+
|
129
|
+
"""
|
130
|
+
logger.debug("Creating cache for ReHo computation via AFNI")
|
131
|
+
|
132
|
+
# Create component-scoped tempdir
|
133
|
+
tempdir = WorkDirManager().get_tempdir(prefix="afni_reho")
|
134
|
+
|
135
|
+
# Save target data to a component-scoped tempfile
|
136
|
+
nifti_in_file_path = tempdir / "input.nii" # needs to be .nii
|
137
|
+
nib.save(data, nifti_in_file_path)
|
138
|
+
|
139
|
+
# Set 3dReHo command
|
140
|
+
reho_out_path_prefix = tempdir / "reho"
|
141
|
+
reho_cmd = [
|
142
|
+
"3dReHo",
|
143
|
+
f"-prefix {reho_out_path_prefix.resolve()}",
|
144
|
+
f"-inset {nifti_in_file_path.resolve()}",
|
145
|
+
]
|
146
|
+
# Check ellipsoidal / cuboidal volume arguments
|
147
|
+
if neigh_rad:
|
148
|
+
reho_cmd.append(f"-neigh_RAD {neigh_rad}")
|
149
|
+
elif neigh_x and neigh_y and neigh_z:
|
150
|
+
reho_cmd.extend(
|
151
|
+
[
|
152
|
+
f"-neigh_X {neigh_x}",
|
153
|
+
f"-neigh_Y {neigh_y}",
|
154
|
+
f"-neigh_Z {neigh_z}",
|
155
|
+
]
|
156
|
+
)
|
157
|
+
elif box_rad:
|
158
|
+
reho_cmd.append(f"-box_RAD {box_rad}")
|
159
|
+
elif box_x and box_y and box_z:
|
160
|
+
reho_cmd.extend(
|
161
|
+
[f"-box_X {box_x}", f"-box_Y {box_y}", f"-box_Z {box_z}"]
|
162
|
+
)
|
163
|
+
else:
|
164
|
+
reho_cmd.append(f"-nneigh {nneigh}")
|
165
|
+
# Call 3dReHo
|
166
|
+
run_ext_cmd(name="3dReHo", cmd=reho_cmd)
|
167
|
+
|
168
|
+
# Create element-scoped tempdir so that the ReHo map is
|
169
|
+
# available later as nibabel stores file path reference for
|
170
|
+
# loading on computation
|
171
|
+
element_tempdir = WorkDirManager().get_element_tempdir(
|
172
|
+
prefix="afni_reho"
|
173
|
+
)
|
174
|
+
# Convert afni to nifti
|
175
|
+
reho_afni_to_nifti_out_path = (
|
176
|
+
element_tempdir / "output.nii" # needs to be .nii
|
177
|
+
)
|
178
|
+
convert_cmd = [
|
179
|
+
"3dAFNItoNIFTI",
|
180
|
+
f"-prefix {reho_afni_to_nifti_out_path.resolve()}",
|
181
|
+
f"{reho_out_path_prefix}+tlrc.BRIK",
|
182
|
+
]
|
183
|
+
# Call 3dAFNItoNIFTI
|
184
|
+
run_ext_cmd(name="3dAFNItoNIFTI", cmd=convert_cmd)
|
185
|
+
|
186
|
+
# Load nifti
|
187
|
+
output_data = nib.load(reho_afni_to_nifti_out_path)
|
188
|
+
|
189
|
+
# Delete tempdir
|
190
|
+
WorkDirManager().delete_tempdir(tempdir)
|
191
|
+
|
192
|
+
return output_data, reho_afni_to_nifti_out_path # type: ignore
|
@@ -0,0 +1,281 @@
|
|
1
|
+
"""Provide class for computing regional homogeneity (ReHo) using junifer."""
|
2
|
+
|
3
|
+
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
4
|
+
# License: AGPL
|
5
|
+
|
6
|
+
from functools import lru_cache
|
7
|
+
from itertools import product
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import (
|
10
|
+
TYPE_CHECKING,
|
11
|
+
ClassVar,
|
12
|
+
Set,
|
13
|
+
Tuple,
|
14
|
+
)
|
15
|
+
|
16
|
+
import nibabel as nib
|
17
|
+
import numpy as np
|
18
|
+
import scipy as sp
|
19
|
+
from nilearn import image as nimg
|
20
|
+
from nilearn import masking as nmask
|
21
|
+
|
22
|
+
from ...pipeline import WorkDirManager
|
23
|
+
from ...pipeline.singleton import singleton
|
24
|
+
from ...utils import logger, raise_error
|
25
|
+
|
26
|
+
|
27
|
+
if TYPE_CHECKING:
|
28
|
+
from nibabel import Nifti1Image
|
29
|
+
|
30
|
+
|
31
|
+
@singleton
|
32
|
+
class JuniferReHo:
|
33
|
+
"""Class for computing ReHo using junifer.
|
34
|
+
|
35
|
+
It's designed as a singleton with caching for efficient computation.
|
36
|
+
|
37
|
+
"""
|
38
|
+
|
39
|
+
_DEPENDENCIES: ClassVar[Set[str]] = {"numpy", "nilearn", "scipy"}
|
40
|
+
|
41
|
+
def __del__(self) -> None:
|
42
|
+
"""Terminate the class."""
|
43
|
+
# Clear the computation cache
|
44
|
+
logger.debug("Clearing cache for ReHo computation via junifer")
|
45
|
+
self.compute.cache_clear()
|
46
|
+
|
47
|
+
@lru_cache(maxsize=None, typed=True)
|
48
|
+
def compute(
|
49
|
+
self,
|
50
|
+
data: "Nifti1Image",
|
51
|
+
nneigh: int = 27,
|
52
|
+
) -> Tuple["Nifti1Image", Path]:
|
53
|
+
"""Compute ReHo map.
|
54
|
+
|
55
|
+
Parameters
|
56
|
+
----------
|
57
|
+
data : 4D Niimg-like object
|
58
|
+
Images to process.
|
59
|
+
nneigh : {7, 19, 27, 125}, optional
|
60
|
+
Number of voxels in the neighbourhood, inclusive. Can be:
|
61
|
+
|
62
|
+
* 7 : for facewise neighbours only
|
63
|
+
* 19 : for face- and edge-wise nieghbours
|
64
|
+
* 27 : for face-, edge-, and node-wise neighbors
|
65
|
+
* 125 : for 5x5 cuboidal volume
|
66
|
+
|
67
|
+
(default 27).
|
68
|
+
|
69
|
+
Returns
|
70
|
+
-------
|
71
|
+
Niimg-like object
|
72
|
+
The ReHo map as NIfTI.
|
73
|
+
pathlib.Path
|
74
|
+
The path to the ReHo map as NIfTI.
|
75
|
+
|
76
|
+
Raises
|
77
|
+
------
|
78
|
+
ValueError
|
79
|
+
If ``nneigh`` is invalid.
|
80
|
+
|
81
|
+
"""
|
82
|
+
valid_nneigh = (7, 19, 27, 125)
|
83
|
+
if nneigh not in valid_nneigh:
|
84
|
+
raise_error(
|
85
|
+
f"Invalid value for `nneigh`, should be one of: {valid_nneigh}"
|
86
|
+
)
|
87
|
+
|
88
|
+
logger.debug("Creating cache for ReHo computation via junifer")
|
89
|
+
|
90
|
+
# Get scan data
|
91
|
+
niimg_data = data.get_fdata()
|
92
|
+
# Get scan dimensions
|
93
|
+
n_x, n_y, n_z, _ = niimg_data.shape
|
94
|
+
|
95
|
+
# Get rank of every voxel across time series
|
96
|
+
ranks_niimg_data = sp.stats.rankdata(niimg_data, axis=-1)
|
97
|
+
|
98
|
+
# Initialize 3D array to store tied rank correction for every voxel
|
99
|
+
tied_rank_corrections = np.zeros((n_x, n_y, n_z), dtype=np.float64)
|
100
|
+
# Calculate tied rank correction for every voxel
|
101
|
+
for i_x, i_y, i_z in product(range(n_x), range(n_y), range(n_z)):
|
102
|
+
# Calculate tied rank count for every voxel across time series
|
103
|
+
_, tie_count = np.unique(
|
104
|
+
ranks_niimg_data[i_x, i_y, i_z, :],
|
105
|
+
return_counts=True,
|
106
|
+
)
|
107
|
+
# Calculate and store tied rank correction for every voxel across
|
108
|
+
# timeseries
|
109
|
+
tied_rank_corrections[i_x, i_y, i_z] = np.sum(
|
110
|
+
tie_count**3 - tie_count
|
111
|
+
)
|
112
|
+
|
113
|
+
# Initialize 3D array to store reho map
|
114
|
+
reho_map = np.ones((n_x, n_y, n_z), dtype=np.float32)
|
115
|
+
|
116
|
+
# TODO(synchon): this will give incorrect results if
|
117
|
+
# template doesn't match, hence needs to be changed
|
118
|
+
# after #299 is merged
|
119
|
+
# Calculate whole brain mask
|
120
|
+
mni152_whole_brain_mask = nmask.compute_brain_mask(
|
121
|
+
target_img=data,
|
122
|
+
threshold=0.5,
|
123
|
+
mask_type="whole-brain",
|
124
|
+
)
|
125
|
+
# Convert 0 / 1 array to bool
|
126
|
+
logical_mni152_whole_brain_mask = (
|
127
|
+
mni152_whole_brain_mask.get_fdata().astype(bool)
|
128
|
+
)
|
129
|
+
|
130
|
+
# Create mask cluster and set start and end indices
|
131
|
+
if nneigh in (7, 19, 27):
|
132
|
+
mask_cluster = np.ones((3, 3, 3))
|
133
|
+
|
134
|
+
if nneigh == 7:
|
135
|
+
mask_cluster[0, 0, 0] = 0
|
136
|
+
mask_cluster[0, 1, 0] = 0
|
137
|
+
mask_cluster[0, 2, 0] = 0
|
138
|
+
mask_cluster[0, 0, 1] = 0
|
139
|
+
mask_cluster[0, 2, 1] = 0
|
140
|
+
mask_cluster[0, 0, 2] = 0
|
141
|
+
mask_cluster[0, 1, 2] = 0
|
142
|
+
mask_cluster[0, 2, 2] = 0
|
143
|
+
mask_cluster[1, 0, 0] = 0
|
144
|
+
mask_cluster[1, 2, 0] = 0
|
145
|
+
mask_cluster[1, 0, 2] = 0
|
146
|
+
mask_cluster[1, 2, 2] = 0
|
147
|
+
mask_cluster[2, 0, 0] = 0
|
148
|
+
mask_cluster[2, 1, 0] = 0
|
149
|
+
mask_cluster[2, 2, 0] = 0
|
150
|
+
mask_cluster[2, 0, 1] = 0
|
151
|
+
mask_cluster[2, 2, 1] = 0
|
152
|
+
mask_cluster[2, 0, 2] = 0
|
153
|
+
mask_cluster[2, 1, 2] = 0
|
154
|
+
mask_cluster[2, 2, 2] = 0
|
155
|
+
|
156
|
+
elif nneigh == 19:
|
157
|
+
mask_cluster[0, 0, 0] = 0
|
158
|
+
mask_cluster[0, 2, 0] = 0
|
159
|
+
mask_cluster[2, 0, 0] = 0
|
160
|
+
mask_cluster[2, 2, 0] = 0
|
161
|
+
mask_cluster[0, 0, 2] = 0
|
162
|
+
mask_cluster[0, 2, 2] = 0
|
163
|
+
mask_cluster[2, 0, 2] = 0
|
164
|
+
mask_cluster[2, 2, 2] = 0
|
165
|
+
|
166
|
+
start_idx = 1
|
167
|
+
end_idx = 2
|
168
|
+
|
169
|
+
elif nneigh == 125:
|
170
|
+
mask_cluster = np.ones((5, 5, 5))
|
171
|
+
start_idx = 2
|
172
|
+
end_idx = 3
|
173
|
+
|
174
|
+
# Convert 0 / 1 array to bool
|
175
|
+
logical_mask_cluster = mask_cluster.astype(bool)
|
176
|
+
|
177
|
+
for i, j, k in product(
|
178
|
+
range(start_idx, n_x - (end_idx - 1)),
|
179
|
+
range(start_idx, n_y - (end_idx - 1)),
|
180
|
+
range(start_idx, n_z - (end_idx - 1)),
|
181
|
+
):
|
182
|
+
# Get mask only for neighbourhood
|
183
|
+
logical_neighbourhood_mni152_whole_brain_mask = (
|
184
|
+
logical_mni152_whole_brain_mask[
|
185
|
+
i - start_idx : i + end_idx,
|
186
|
+
j - start_idx : j + end_idx,
|
187
|
+
k - start_idx : k + end_idx,
|
188
|
+
]
|
189
|
+
)
|
190
|
+
# Perform logical AND to get neighbourhood mask;
|
191
|
+
# done to take care of brain boundaries
|
192
|
+
neighbourhood_mask = (
|
193
|
+
logical_mask_cluster
|
194
|
+
& logical_neighbourhood_mni152_whole_brain_mask
|
195
|
+
)
|
196
|
+
# Continue if voxel is restricted by mask
|
197
|
+
if neighbourhood_mask[1, 1, 1] == 0:
|
198
|
+
continue
|
199
|
+
|
200
|
+
# Get ranks for the neighbourhood
|
201
|
+
neighbourhood_ranks = ranks_niimg_data[
|
202
|
+
i - start_idx : i + end_idx,
|
203
|
+
j - start_idx : j + end_idx,
|
204
|
+
k - start_idx : k + end_idx,
|
205
|
+
:,
|
206
|
+
]
|
207
|
+
# Get tied ranks corrections for the neighbourhood
|
208
|
+
neighbourhood_tied_ranks_corrections = tied_rank_corrections[
|
209
|
+
i - start_idx : i + end_idx,
|
210
|
+
j - start_idx : j + end_idx,
|
211
|
+
k - start_idx : k + end_idx,
|
212
|
+
]
|
213
|
+
# Mask neighbourhood ranks
|
214
|
+
masked_neighbourhood_ranks = neighbourhood_ranks[
|
215
|
+
logical_mask_cluster, :
|
216
|
+
]
|
217
|
+
# Mask tied ranks corrections for the neighbourhood
|
218
|
+
masked_tied_rank_corrections = (
|
219
|
+
neighbourhood_tied_ranks_corrections[logical_mask_cluster]
|
220
|
+
)
|
221
|
+
# Calculate KCC
|
222
|
+
reho_map[i, j, k] = _kendall_w_reho(
|
223
|
+
timeseries_ranks=masked_neighbourhood_ranks,
|
224
|
+
tied_rank_corrections=masked_tied_rank_corrections,
|
225
|
+
)
|
226
|
+
|
227
|
+
# Create new image like target image
|
228
|
+
output_data = nimg.new_img_like(
|
229
|
+
ref_niimg=data,
|
230
|
+
data=reho_map,
|
231
|
+
copy_header=False,
|
232
|
+
)
|
233
|
+
|
234
|
+
# Create element-scoped tempdir so that the ReHo map is
|
235
|
+
# available later as nibabel stores file path reference for
|
236
|
+
# loading on computation
|
237
|
+
element_tempdir = WorkDirManager().get_element_tempdir(
|
238
|
+
prefix="junifer_reho"
|
239
|
+
)
|
240
|
+
output_path = element_tempdir / "output.nii.gz"
|
241
|
+
# Save computed data to file
|
242
|
+
nib.save(output_data, output_path)
|
243
|
+
|
244
|
+
return output_data, output_path # type: ignore
|
245
|
+
|
246
|
+
|
247
|
+
def _kendall_w_reho(
|
248
|
+
timeseries_ranks: np.ndarray, tied_rank_corrections: np.ndarray
|
249
|
+
) -> float:
|
250
|
+
"""Calculate Kendall's coefficient of concordance (KCC) for ReHo map.
|
251
|
+
|
252
|
+
..note:: This function should only be used to calculate KCC for a ReHo map.
|
253
|
+
For general use, check out ``junifer.stats.kendall_w``.
|
254
|
+
|
255
|
+
Parameters
|
256
|
+
----------
|
257
|
+
timeseries_ranks : 2D numpy.ndarray
|
258
|
+
A matrix of ranks of a subset subject's brain voxels.
|
259
|
+
tied_rank_corrections : 3D numpy.ndarray
|
260
|
+
A 3D array consisting of the tied rank corrections for the ranks
|
261
|
+
of a subset subject's brain voxels.
|
262
|
+
|
263
|
+
Returns
|
264
|
+
-------
|
265
|
+
float
|
266
|
+
Kendall's W (KCC) of the given timeseries matrix.
|
267
|
+
|
268
|
+
"""
|
269
|
+
m, n = timeseries_ranks.shape # annotators X items
|
270
|
+
|
271
|
+
numerator = (12 * np.sum(np.square(np.sum(timeseries_ranks, axis=0)))) - (
|
272
|
+
3 * m**2 * n * (n + 1) ** 2
|
273
|
+
)
|
274
|
+
denominator = (m**2 * n * (n**2 - 1)) - (m * np.sum(tied_rank_corrections))
|
275
|
+
|
276
|
+
if denominator == 0:
|
277
|
+
kcc = 1.0
|
278
|
+
else:
|
279
|
+
kcc = numerator / denominator
|
280
|
+
|
281
|
+
return kcc
|