From 1f563f688c71e78db4a4abd25663a8b059ad8b1f Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 13:04:16 +0100 Subject: [PATCH 01/22] update: introduce FSLWarper and ANTsWarper classes --- junifer/preprocess/warping/_ants_warper.py | 115 +++++++++++++++++++++ junifer/preprocess/warping/_fsl_warper.py | 109 +++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 junifer/preprocess/warping/_ants_warper.py create mode 100644 junifer/preprocess/warping/_fsl_warper.py diff --git a/junifer/preprocess/warping/_ants_warper.py b/junifer/preprocess/warping/_ants_warper.py new file mode 100644 index 000000000..9376ee64a --- /dev/null +++ b/junifer/preprocess/warping/_ants_warper.py @@ -0,0 +1,115 @@ +"""Provide class for space warping via ANTs antsApplyTransforms.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import ( + Any, + ClassVar, + Dict, + List, + Set, + Union, +) + +import nibabel as nib +import numpy as np + +from ...pipeline import WorkDirManager +from ...utils import logger, run_ext_cmd + + +class ANTsWarper: + """Class for space warping via ANTs antsApplyTransforms. + + This class uses ANTs' ``ResampleImage`` for resampling and + ``antsApplyTransforms`` for transformation. + + """ + + _EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [ + { + "name": "ants", + "commands": ["ResampleImage", "antsApplyTransforms"], + }, + ] + + _DEPENDENCIES: ClassVar[Set[str]] = {"numpy"} + + def preprocess( + self, + input: Dict[str, Any], + extra_input: Dict[str, Any], + ) -> Dict[str, Any]: + """Preprocess using ANTs. + + Parameters + ---------- + input : dict + A single input from the Junifer Data object in which to preprocess. + extra_input : dict + The other fields in the Junifer Data object. Should have ``T1w`` + and ``Warp`` data types. + + Returns + ------- + dict + The ``input`` dictionary with modified ``data`` and ``space`` key + values and new ``reference_path`` key whose value points to the + reference file used for warping. + + """ + logger.debug("Using ANTs for warping") + + # Get the min of the voxel sizes from input and use it as the + # resolution + resolution = np.min(input["data"].header.get_zooms()[:3]) + + # Create element-specific tempdir for storing post-warping assets + element_tempdir = WorkDirManager().get_element_tempdir( + prefix="ants_warper" + ) + + # Create a tempfile for resampled reference output + resample_image_out_path = ( + element_tempdir / "resampled_reference.nii.gz" + ) + # Set ResampleImage command + resample_image_cmd = [ + "ResampleImage", + "3", # image dimension + f"{extra_input['T1w']['path'].resolve()}", + f"{resample_image_out_path.resolve()}", + f"{resolution}x{resolution}x{resolution}", + "0", # option for spacing and not size + "3 3", # Lanczos windowed sinc + ] + # Call ResampleImage + run_ext_cmd(name="ResampleImage", cmd=resample_image_cmd) + + # Create a tempfile for warped output + apply_transforms_out_path = element_tempdir / "output.nii.gz" + # Set antsApplyTransforms command + apply_transforms_cmd = [ + "antsApplyTransforms", + "-d 3", + "-e 3", + "-n LanczosWindowedSinc", + f"-i {input['path'].resolve()}", + # use resampled reference + f"-r {resample_image_out_path.resolve()}", + f"-t {extra_input['Warp']['path'].resolve()}", + f"-o {apply_transforms_out_path.resolve()}", + ] + # Call antsApplyTransforms + run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd) + + # Load nifti + input["data"] = nib.load(apply_transforms_out_path) + # Save resampled reference path + input["reference_path"] = resample_image_out_path + + # Use reference input's space as warped input's space + input["space"] = extra_input["T1w"]["space"] + + return input diff --git a/junifer/preprocess/warping/_fsl_warper.py b/junifer/preprocess/warping/_fsl_warper.py new file mode 100644 index 000000000..0586005aa --- /dev/null +++ b/junifer/preprocess/warping/_fsl_warper.py @@ -0,0 +1,109 @@ +"""Provide class for space warping via FSL FLIRT.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import ( + Any, + ClassVar, + Dict, + List, + Set, + Union, +) + +import nibabel as nib +import numpy as np + +from ...pipeline import WorkDirManager +from ...utils import logger, run_ext_cmd + + +class FSLWarper: + """Class for space warping via FSL FLIRT. + + This class uses FSL FLIRT's ``flirt`` for resampling and ``applywarp`` for + transformation. + + """ + + _EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [ + { + "name": "fsl", + "commands": ["flirt", "applywarp"], + }, + ] + + _DEPENDENCIES: ClassVar[Set[str]] = {"numpy"} + + def preprocess( + self, + input: Dict[str, Any], + extra_input: Dict[str, Any], + ) -> Dict[str, Any]: + """Preprocess using FSL. + + Parameters + ---------- + input : dict + A single input from the Junifer Data object in which to preprocess. + extra_input : dict + The other fields in the Junifer Data object. Should have ``T1w`` + and ``Warp`` data types. + + Returns + ------- + dict + The ``input`` dictionary with modified ``data`` and ``space`` key + values and new ``reference_path`` key whose value points to the + reference file used for warping. + + """ + logger.debug("Using FSL for warping") + + # Get the min of the voxel sizes from input and use it as the + # resolution + resolution = np.min(input["data"].header.get_zooms()[:3]) + + # Create element-specific tempdir for storing post-warping assets + element_tempdir = WorkDirManager().get_element_tempdir( + prefix="fsl_warper" + ) + + # Create a tempfile for resampled reference output + flirt_out_path = element_tempdir / "resampled_reference.nii.gz" + # Set flirt command + flirt_cmd = [ + "flirt", + "-interp spline", + f"-in {extra_input['T1w']['path'].resolve()}", + f"-ref {extra_input['T1w']['path'].resolve()}", + f"-applyisoxfm {resolution}", + f"-out {flirt_out_path.resolve()}", + ] + # Call flirt + run_ext_cmd(name="flirt", cmd=flirt_cmd) + + # Create a tempfile for warped output + applywarp_out_path = element_tempdir / "output.nii.gz" + # Set applywarp command + applywarp_cmd = [ + "applywarp", + "--interp=spline", + f"-i {input['path'].resolve()}", + f"-r {flirt_out_path.resolve()}", # use resampled reference + f"-w {extra_input['Warp']['path'].resolve()}", + f"-o {applywarp_out_path.resolve()}", + ] + # Call applywarp + run_ext_cmd(name="applywarp", cmd=applywarp_cmd) + + # Load nifti + input["data"] = nib.load(applywarp_out_path) + # Save resampled reference path + input["reference_path"] = flirt_out_path + + # Use reference input's space as warped input's space + input["space"] = extra_input["T1w"]["space"] + + return input -- 2.52.0 From 2d627a0b554425a20f0f6e37e31cd5b3742ed5e0 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 13:05:25 +0100 Subject: [PATCH 02/22] feature: introduce SpaceWarper and add tests --- junifer/preprocess/__init__.py | 1 + junifer/preprocess/warping/__init__.py | 6 + junifer/preprocess/warping/space_warper.py | 258 ++++++++++++++++++ .../warping/tests/test_space_warper.py | 153 +++++++++++ 4 files changed, 418 insertions(+) create mode 100644 junifer/preprocess/warping/__init__.py create mode 100644 junifer/preprocess/warping/space_warper.py create mode 100644 junifer/preprocess/warping/tests/test_space_warper.py diff --git a/junifer/preprocess/__init__.py b/junifer/preprocess/__init__.py index 804433ecc..15042f0ee 100644 --- a/junifer/preprocess/__init__.py +++ b/junifer/preprocess/__init__.py @@ -8,3 +8,4 @@ from .base import BasePreprocessor from .confounds import fMRIPrepConfoundRemover from .bold_warper import BOLDWarper +from .warping import SpaceWarper diff --git a/junifer/preprocess/warping/__init__.py b/junifer/preprocess/warping/__init__.py new file mode 100644 index 000000000..1b7a3bc2b --- /dev/null +++ b/junifer/preprocess/warping/__init__.py @@ -0,0 +1,6 @@ +"""Provide imports for warping sub-package.""" + +# Authors: Synchon Mandal +# License: AGPL + +from .space_warper import SpaceWarper diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py new file mode 100644 index 000000000..70072db8f --- /dev/null +++ b/junifer/preprocess/warping/space_warper.py @@ -0,0 +1,258 @@ +"""Provide class for warping data to other template spaces.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union + +import nibabel as nib +from templateflow import api as tflow + +from ...api.decorators import register_preprocessor +from ...data import get_template, get_xfm +from ...pipeline import WorkDirManager +from ...utils import logger, raise_error, run_ext_cmd +from ..base import BasePreprocessor +from ._ants_warper import ANTsWarper +from ._fsl_warper import FSLWarper + + +__all__ = ["SpaceWarper"] + + +@register_preprocessor +class SpaceWarper(BasePreprocessor): + """Class for warping data to other template spaces. + + Parameters + ---------- + using : {"fsl", "ants"} + Implementation to use for warping: + + * "fsl" : Use FSL's ``applywarp`` + * "afni" : Use ANTs' ``antsApplyTransforms`` + + reference : str + The data type to use as reference for warping, can be either a data + type like ``"T1w"`` or a template space like ``"MNI152NLin2009cAsym"``. + on : {"T1w", "BOLD"} or list of the options + The data type to warp. + + Raises + ------ + ValueError + If ``using`` is invalid or + if ``reference`` is invalid. + + Notes + ----- + If you are setting ``reference`` to a template space like + "MNI152NLin2009cAsym", make sure ANTs is available for the + transformation else it will fail during runtime. It is tricky to validate + this beforehand and difficult to enforce this as a requirement, hence the + heads-up. + + """ + + _CONDITIONAL_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, Type]]]] = [ + { + "using": "fsl", + "depends_on": FSLWarper, + }, + { + "using": "ants", + "depends_on": ANTsWarper, + }, + ] + + def __init__( + self, using: str, reference: str, on: Union[List[str], str] + ) -> None: + """Initialize the class.""" + # Validate `using` parameter + valid_using = [dep["using"] for dep in self._CONDITIONAL_DEPENDENCIES] + if using not in valid_using: + raise_error( + f"Invalid value for `using`, should be one of: {valid_using}" + ) + self.using = using + self.reference = reference + # Set required data types based on reference and + # initialize superclass + if self.reference == "T1w": + required_data_types = [self.reference, "Warp"] + # Listify on + if not isinstance(on, list): + on = [on] + # Extend required data types + required_data_types.extend(on) + + super().__init__( + on=on, + required_data_types=required_data_types, + ) + elif self.reference in tflow.templates(): + super().__init__(on=on) + else: + raise_error(f"Unknown reference: {self.reference}") + + def get_valid_inputs(self) -> List[str]: + """Get valid data types for input. + + Returns + ------- + list of str + The list of data types that can be used as input for this + preprocessor. + + """ + return ["T1w", "BOLD"] + + def get_output_type(self, input_type: str) -> str: + """Get output type. + + Parameters + ---------- + input_type : str + The data type input to the preprocessor. + + Returns + ------- + str + The data type output by the preprocessor. + + """ + # Does not add any new keys + return input_type + + def preprocess( + self, + input: Dict[str, Any], + extra_input: Optional[Dict[str, Any]] = None, + ) -> Tuple[Dict[str, Any], Optional[Dict[str, Dict[str, Any]]]]: + """Preprocess. + + Parameters + ---------- + input : dict + The input from the Junifer Data object. + extra_input : dict, optional + The other fields in the Junifer Data object. + + Returns + ------- + dict + The computed result as dictionary. + None + Extra "helper" data types as dictionary to add to the Junifer Data + object. + + Raises + ------ + ValueError + If ``extra_input`` is None when transforming to native space + i.e., using ``"T1w"`` as reference. + RuntimeError + If warp / transformation file extension is not ".mat" or ".h5" + when transforming to native space or + if the data is in the correct space and does not require + warping. + + """ + logger.info(f"Warping to {self.reference} space using SpaceWarper") + # Transform to native space + if self.reference == "T1w": + # Check for extra inputs + if extra_input is None: + raise_error( + "No extra input provided, requires `Warp` and " + f"`{self.reference}` data types in particular." + ) + # Check for warp file type to use correct tool + warp_file_ext = extra_input["Warp"]["path"].suffix + if warp_file_ext == ".mat": + input = FSLWarper().preprocess( + input=input, + extra_input=extra_input, + ) + elif warp_file_ext == ".h5": + input = ANTsWarper().preprocess( + input=input, + extra_input=extra_input, + ) + else: + raise_error( + msg=( + "Unknown warp / transformation file extension: " + f"{warp_file_ext}" + ), + klass=RuntimeError, + ) + # Transform to template space + else: + # Check pre-requirements for space manipulation + if self.reference == input["space"]: + raise_error( + ( + "Skipped warping as the data is in " + f"{self.reference} space which would mean that you " + "can remove the SpaceWarper from the preprocess step." + ), + klass=RuntimeError, + ) + else: + # Get xfm file + xfm_file_path = get_xfm(src=input["space"], dst=self.reference) + # Get template space image + template_space_img = get_template( + space=self.reference, + target_data=input, + extra_input=None, + ) + + # Create component-scoped tempdir + tempdir = WorkDirManager().get_tempdir(prefix="space_warper") + # Create element-scoped tempdir so that warped data is + # available later as nibabel stores file path reference for + # loading on computation + element_tempdir = WorkDirManager().get_element_tempdir( + prefix="space_warper" + ) + + # Save template + template_space_img_path = ( + tempdir / f"{self.reference}_T1w.nii.gz" + ) + nib.save(template_space_img, template_space_img_path) + + # Create a tempfile for warped output + warped_output_path = element_tempdir / ( + f"data_warped_from_{input['space']}_to_" + f"{self.reference}.nii.gz" + ) + + logger.debug( + f"Using ANTs to warp data " + f"from {input['space']} to {self.reference}" + ) + # Set antsApplyTransforms command + apply_transforms_cmd = [ + "antsApplyTransforms", + "-d 3", + "-e 3", + "-n LanczosWindowedSinc", + f"-i {input['path'].resolve()}", + f"-r {template_space_img_path.resolve()}", + f"-t {xfm_file_path.resolve()}", + f"-o {warped_output_path.resolve()}", + ] + # Call antsApplyTransforms + run_ext_cmd( + name="antsApplyTransforms", cmd=apply_transforms_cmd + ) + + # Modify target data + input["data"] = nib.load(warped_output_path) + input["space"] = self.reference + + return input, None diff --git a/junifer/preprocess/warping/tests/test_space_warper.py b/junifer/preprocess/warping/tests/test_space_warper.py new file mode 100644 index 000000000..b82dfa806 --- /dev/null +++ b/junifer/preprocess/warping/tests/test_space_warper.py @@ -0,0 +1,153 @@ +"""Provide tests for SpaceWarper.""" + +# Authors: Synchon Mandal +# License: AGPL + +import socket +from typing import TYPE_CHECKING, Tuple + +import pytest +from numpy.testing import assert_array_equal, assert_raises + +from junifer.datagrabber import DataladHCP1200, DMCC13Benchmark +from junifer.datareader import DefaultDataReader +from junifer.pipeline.utils import _check_ants, _check_fsl +from junifer.preprocess import SpaceWarper + + +if TYPE_CHECKING: + from junifer.datagrabber import BaseDataGrabber + + +@pytest.mark.parametrize( + "datagrabber, element, using", + [ + [ + DMCC13Benchmark( + types=["BOLD", "T1w", "Warp"], + sessions=["wave1bas"], + tasks=["Rest"], + phase_encodings=["AP"], + runs=["1"], + native_t1w=True, + ), + ("f9057kp", "wave1bas", "Rest", "AP", "1"), + "ants", + ], + [ + DataladHCP1200( + tasks=["REST1"], + phase_encodings=["LR"], + ica_fix=True, + ), + ("100206", "REST1", "LR"), + "fsl", + ], + ], +) +@pytest.mark.skipif(_check_fsl() is False, reason="requires FSL to be in PATH") +@pytest.mark.skipif( + _check_ants() is False, reason="requires ANTs to be in PATH" +) +@pytest.mark.skipif( + socket.gethostname() != "juseless", + reason="only for juseless", +) +def test_SpaceWarper_native( + datagrabber: "BaseDataGrabber", element: Tuple[str, ...], using: str +) -> None: + """Test SpaceWarper for native space warping. + + Parameters + ---------- + datagrabber : DataGrabber-like object + The parametrized DataGrabber objects. + element : tuple of str + The parametrized elements. + using : str + The parametrized implementation method. + + """ + with datagrabber as dg: + # Read data + element_data = DefaultDataReader().fit_transform(dg[element]) + # Preprocess data + output, _ = SpaceWarper( + using=using, + reference="T1w", + on="BOLD", + ).preprocess( + input=element_data["BOLD"], + extra_input=element_data, + ) + # Check + assert isinstance(output, dict) + + +@pytest.mark.parametrize( + "datagrabber, element, space", + [ + [ + DMCC13Benchmark( + types=["T1w"], + sessions=["wave1bas"], + tasks=["Rest"], + phase_encodings=["AP"], + runs=["1"], + native_t1w=False, + ), + ("f9057kp", "wave1bas", "Rest", "AP", "1"), + "MNI152NLin2009aAsym", + ], + [ + DMCC13Benchmark( + types=["T1w"], + sessions=["wave1bas"], + tasks=["Rest"], + phase_encodings=["AP"], + runs=["1"], + native_t1w=False, + ), + ("f9057kp", "wave1bas", "Rest", "AP", "1"), + "MNI152NLin6Asym", + ], + ], +) +@pytest.mark.skipif( + _check_ants() is False, reason="requires ANTs to be in PATH" +) +def test_SpaceWarper_multi_mni( + datagrabber: "BaseDataGrabber", + element: Tuple[str, ...], + space: str, +) -> None: + """Test SpaceWarper for MNI space warping. + + Parameters + ---------- + datagrabber : DataGrabber-like object + The parametrized DataGrabber objects. + element : tuple of str + The parametrized elements. + space : str + The parametrized template space to transform to. + + """ + with datagrabber as dg: + # Read data + element_data = DefaultDataReader().fit_transform(dg[element]) + pre_xfm_data = element_data["BOLD"]["data"].get_fdata().copy() + # Preprocess data + output, _ = SpaceWarper( + using="ants", + reference=space, + on=["T1w"], + ).preprocess( + input=element_data["T1w"], + extra_input=element_data, + ) + # Checks + assert isinstance(output, dict) + assert output["space"] == space + with assert_raises(AssertionError): + assert_array_equal(pre_xfm_data, output["data"]) -- 2.52.0 From 8c23cf9cec869413414ec3aefb309f1250121a68 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 13:12:28 +0100 Subject: [PATCH 03/22] chore: add deprecation notice for BOLDWarper --- junifer/preprocess/bold_warper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/junifer/preprocess/bold_warper.py b/junifer/preprocess/bold_warper.py index 22aeaceb1..304f59bbc 100644 --- a/junifer/preprocess/bold_warper.py +++ b/junifer/preprocess/bold_warper.py @@ -30,6 +30,10 @@ from .fsl.apply_warper import _ApplyWarper class BOLDWarper(BasePreprocessor): """Class for warping BOLD NIfTI images. + .. deprecated:: 0.0.3 + `BOLDWarper` will be removed in v0.0.4, it is replaced by + `SpaceWarper` because the latter works also with T1w data. + Parameters ---------- using : {"fsl", "ants"} -- 2.52.0 From 875dbd9728b7fc862d2fefa75db40e0551f15137 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 13:20:38 +0100 Subject: [PATCH 04/22] chore: add changelogs 301.{feature,removal} --- docs/changes/newsfragments/301.feature | 1 + docs/changes/newsfragments/301.removal | 1 + 2 files changed, 2 insertions(+) create mode 100644 docs/changes/newsfragments/301.feature create mode 100644 docs/changes/newsfragments/301.removal diff --git a/docs/changes/newsfragments/301.feature b/docs/changes/newsfragments/301.feature new file mode 100644 index 000000000..caf3755d1 --- /dev/null +++ b/docs/changes/newsfragments/301.feature @@ -0,0 +1 @@ +Introduce :class:`.SpaceWarper` for warping ``T1w`` and ``BOLD`` data to other spaces by `Synchon Mandal`_ diff --git a/docs/changes/newsfragments/301.removal b/docs/changes/newsfragments/301.removal new file mode 100644 index 000000000..9b2c343ed --- /dev/null +++ b/docs/changes/newsfragments/301.removal @@ -0,0 +1 @@ +Deprecate :class:`.BOLDWarper` and mark for removal in v0.0.4 by `Synchon Mandal`_ -- 2.52.0 From 7be9a4293e51a19873ea65db7828c70510d9b307 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 13:47:20 +0100 Subject: [PATCH 05/22] chore: improve dependencies for ANTsWarper an FSLWarper --- junifer/preprocess/warping/_ants_warper.py | 2 +- junifer/preprocess/warping/_fsl_warper.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/junifer/preprocess/warping/_ants_warper.py b/junifer/preprocess/warping/_ants_warper.py index 9376ee64a..ba658dd78 100644 --- a/junifer/preprocess/warping/_ants_warper.py +++ b/junifer/preprocess/warping/_ants_warper.py @@ -34,7 +34,7 @@ class ANTsWarper: }, ] - _DEPENDENCIES: ClassVar[Set[str]] = {"numpy"} + _DEPENDENCIES: ClassVar[Set[str]] = {"numpy", "nibabel"} def preprocess( self, diff --git a/junifer/preprocess/warping/_fsl_warper.py b/junifer/preprocess/warping/_fsl_warper.py index 0586005aa..0dbb09649 100644 --- a/junifer/preprocess/warping/_fsl_warper.py +++ b/junifer/preprocess/warping/_fsl_warper.py @@ -34,7 +34,7 @@ class FSLWarper: }, ] - _DEPENDENCIES: ClassVar[Set[str]] = {"numpy"} + _DEPENDENCIES: ClassVar[Set[str]] = {"numpy", "nibabel"} def preprocess( self, -- 2.52.0 From 55562a5cc7a4cb37596d826e2086e4221dc8d31d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 13:47:57 +0100 Subject: [PATCH 06/22] update: add _DEPENDENCIES to SpaceWarper --- junifer/preprocess/warping/space_warper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index 70072db8f..d3f6ff6fc 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -3,7 +3,7 @@ # Authors: Synchon Mandal # License: AGPL -from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union +from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type, Union import nibabel as nib from templateflow import api as tflow @@ -65,6 +65,8 @@ class SpaceWarper(BasePreprocessor): }, ] + _DEPENDENCIES: ClassVar[Set[str]] = {"nibabel"} + def __init__( self, using: str, reference: str, on: Union[List[str], str] ) -> None: -- 2.52.0 From d276fef79ad5d4235133c578acd09a68c90b1808 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 13:48:30 +0100 Subject: [PATCH 07/22] refactor: use using parameter for conditional preprocessor check in SpaceWarper --- junifer/preprocess/warping/space_warper.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index d3f6ff6fc..758378ad7 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -155,9 +155,7 @@ class SpaceWarper(BasePreprocessor): If ``extra_input`` is None when transforming to native space i.e., using ``"T1w"`` as reference. RuntimeError - If warp / transformation file extension is not ".mat" or ".h5" - when transforming to native space or - if the data is in the correct space and does not require + If the data is in the correct space and does not require warping. """ @@ -170,26 +168,17 @@ class SpaceWarper(BasePreprocessor): "No extra input provided, requires `Warp` and " f"`{self.reference}` data types in particular." ) - # Check for warp file type to use correct tool - warp_file_ext = extra_input["Warp"]["path"].suffix - if warp_file_ext == ".mat": + # Conditional preprocessor + if self.using == "fsl": input = FSLWarper().preprocess( input=input, extra_input=extra_input, ) - elif warp_file_ext == ".h5": + elif self.using == "ants": input = ANTsWarper().preprocess( input=input, extra_input=extra_input, ) - else: - raise_error( - msg=( - "Unknown warp / transformation file extension: " - f"{warp_file_ext}" - ), - klass=RuntimeError, - ) # Transform to template space else: # Check pre-requirements for space manipulation -- 2.52.0 From c3128644191a9b84ffc85ff78fff2e53940caac6 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 14:13:30 +0100 Subject: [PATCH 08/22] update: add more supported data types for SpaceWarper --- junifer/preprocess/warping/space_warper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index 758378ad7..22cf1ed7c 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -108,7 +108,7 @@ class SpaceWarper(BasePreprocessor): preprocessor. """ - return ["T1w", "BOLD"] + return ["T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"] def get_output_type(self, input_type: str) -> str: """Get output type. -- 2.52.0 From 1e62f125de097e4a97050b4c34a54e9ea5d0c704 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 14:14:56 +0100 Subject: [PATCH 09/22] chore: update 301.feature --- docs/changes/newsfragments/301.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes/newsfragments/301.feature b/docs/changes/newsfragments/301.feature index caf3755d1..5ad08b386 100644 --- a/docs/changes/newsfragments/301.feature +++ b/docs/changes/newsfragments/301.feature @@ -1 +1 @@ -Introduce :class:`.SpaceWarper` for warping ``T1w`` and ``BOLD`` data to other spaces by `Synchon Mandal`_ +Introduce :class:`.SpaceWarper` for warping ``T1w``, ``BOLD``, ``VBM_GM``, ``VBM_WM``, ``fALFF``, ``GCOR`` and ``LCOR`` data to other spaces by `Synchon Mandal`_ -- 2.52.0 From d7b8d0504809e934d900a41f3c44681dae272c93 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 14:42:01 +0100 Subject: [PATCH 10/22] update: rename FSLWarper and ANTsWarper to FSLNativeWarper and ANTsNativeWarper respectively --- ..._ants_warper.py => _ants_native_warper.py} | 8 +++--- .../{_fsl_warper.py => _fsl_native_warper.py} | 10 ++++---- junifer/preprocess/warping/space_warper.py | 25 +++++++++++-------- 3 files changed, 24 insertions(+), 19 deletions(-) rename junifer/preprocess/warping/{_ants_warper.py => _ants_native_warper.py} (93%) rename junifer/preprocess/warping/{_fsl_warper.py => _fsl_native_warper.py} (92%) diff --git a/junifer/preprocess/warping/_ants_warper.py b/junifer/preprocess/warping/_ants_native_warper.py similarity index 93% rename from junifer/preprocess/warping/_ants_warper.py rename to junifer/preprocess/warping/_ants_native_warper.py index ba658dd78..2d4a64364 100644 --- a/junifer/preprocess/warping/_ants_warper.py +++ b/junifer/preprocess/warping/_ants_native_warper.py @@ -1,4 +1,4 @@ -"""Provide class for space warping via ANTs antsApplyTransforms.""" +"""Provide class for native space warping via ANTs antsApplyTransforms.""" # Authors: Synchon Mandal # License: AGPL @@ -19,8 +19,8 @@ from ...pipeline import WorkDirManager from ...utils import logger, run_ext_cmd -class ANTsWarper: - """Class for space warping via ANTs antsApplyTransforms. +class ANTsNativeWarper: + """Class for native space warping via ANTs antsApplyTransforms. This class uses ANTs' ``ResampleImage`` for resampling and ``antsApplyTransforms`` for transformation. @@ -59,7 +59,7 @@ class ANTsWarper: reference file used for warping. """ - logger.debug("Using ANTs for warping") + logger.debug("Using ANTs for native space warping") # Get the min of the voxel sizes from input and use it as the # resolution diff --git a/junifer/preprocess/warping/_fsl_warper.py b/junifer/preprocess/warping/_fsl_native_warper.py similarity index 92% rename from junifer/preprocess/warping/_fsl_warper.py rename to junifer/preprocess/warping/_fsl_native_warper.py index 0dbb09649..398fb8f95 100644 --- a/junifer/preprocess/warping/_fsl_warper.py +++ b/junifer/preprocess/warping/_fsl_native_warper.py @@ -1,4 +1,4 @@ -"""Provide class for space warping via FSL FLIRT.""" +"""Provide class for native space warping via FSL FLIRT.""" # Authors: Synchon Mandal # License: AGPL @@ -19,8 +19,8 @@ from ...pipeline import WorkDirManager from ...utils import logger, run_ext_cmd -class FSLWarper: - """Class for space warping via FSL FLIRT. +class FSLNativeWarper: + """Class for native space warping via FSL FLIRT. This class uses FSL FLIRT's ``flirt`` for resampling and ``applywarp`` for transformation. @@ -59,7 +59,7 @@ class FSLWarper: reference file used for warping. """ - logger.debug("Using FSL for warping") + logger.debug("Using FSL for native space warping") # Get the min of the voxel sizes from input and use it as the # resolution @@ -67,7 +67,7 @@ class FSLWarper: # Create element-specific tempdir for storing post-warping assets element_tempdir = WorkDirManager().get_element_tempdir( - prefix="fsl_warper" + prefix="fsl_native_warper" ) # Create a tempfile for resampled reference output diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index 22cf1ed7c..6df245eec 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -13,8 +13,8 @@ from ...data import get_template, get_xfm from ...pipeline import WorkDirManager from ...utils import logger, raise_error, run_ext_cmd from ..base import BasePreprocessor -from ._ants_warper import ANTsWarper -from ._fsl_warper import FSLWarper +from ._ants_native_warper import ANTsNativeWarper +from ._fsl_native_warper import FSLNativeWarper __all__ = ["SpaceWarper"] @@ -29,8 +29,9 @@ class SpaceWarper(BasePreprocessor): using : {"fsl", "ants"} Implementation to use for warping: - * "fsl" : Use FSL's ``applywarp`` - * "afni" : Use ANTs' ``antsApplyTransforms`` + * "fsl_native" : Use FSL's ``applywarp`` for native space warping + * "ants_native" : Use ANTs' ``antsApplyTransforms`` for native space + warping reference : str The data type to use as reference for warping, can be either a data @@ -56,8 +57,12 @@ class SpaceWarper(BasePreprocessor): _CONDITIONAL_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, Type]]]] = [ { - "using": "fsl", - "depends_on": FSLWarper, + "using": "fsl_native", + "depends_on": FSLNativeWarper, + }, + { + "using": "ants_native", + "depends_on": ANTsNativeWarper, }, { "using": "ants", @@ -169,13 +174,13 @@ class SpaceWarper(BasePreprocessor): f"`{self.reference}` data types in particular." ) # Conditional preprocessor - if self.using == "fsl": - input = FSLWarper().preprocess( + if self.using == "fsl_native": + input = FSLNativeWarper().preprocess( input=input, extra_input=extra_input, ) - elif self.using == "ants": - input = ANTsWarper().preprocess( + elif self.using == "ants_natice": + input = ANTsNativeWarper().preprocess( input=input, extra_input=extra_input, ) -- 2.52.0 From 4cec3cf3e71840101f5ef0b2aa322a8e00b7155c Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 14:43:16 +0100 Subject: [PATCH 11/22] update: introduce ANTsTemplateWarper --- .../warping/_ants_template_warper.py | 108 ++++++++++++++++++ junifer/preprocess/warping/space_warper.py | 84 +++----------- 2 files changed, 121 insertions(+), 71 deletions(-) create mode 100644 junifer/preprocess/warping/_ants_template_warper.py diff --git a/junifer/preprocess/warping/_ants_template_warper.py b/junifer/preprocess/warping/_ants_template_warper.py new file mode 100644 index 000000000..e821df859 --- /dev/null +++ b/junifer/preprocess/warping/_ants_template_warper.py @@ -0,0 +1,108 @@ +"""Provide class for template space warping via ANTs antsApplyTransforms.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import ( + Any, + ClassVar, + Dict, + List, + Set, + Union, +) + +import nibabel as nib + +from ...data import get_template, get_xfm +from ...pipeline import WorkDirManager +from ...utils import logger, run_ext_cmd + + +class ANTsTemplateWarper: + """Class for template space warping via ANTs antsApplyTransforms. + + This class uses ANTs' ``antsApplyTransforms`` for transformation. + + """ + + _EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [ + { + "name": "ants", + "commands": ["antsApplyTransforms"], + }, + ] + + _DEPENDENCIES: ClassVar[Set[str]] = {"nibabel"} + + def preprocess( + self, + input: Dict[str, Any], + dst: str, + ) -> Dict[str, Any]: + """Preprocess using ANTs. + + Parameters + ---------- + input : dict + A single input from the Junifer Data object in which to preprocess. + dst : str + Template space to warp to. + + Returns + ------- + dict + The ``input`` dictionary with modified ``data`` and ``space`` key + values. + + """ + logger.debug( + f"Using ANTs to warp data " f"from {input['space']} to {dst}" + ) + + # Get xfm file + xfm_file_path = get_xfm(src=input["space"], dst=dst) + # Get template space image + template_space_img = get_template( + space=dst, + target_data=input, + extra_input=None, + ) + + # Create component-scoped tempdir + tempdir = WorkDirManager().get_tempdir(prefix="ants_template_warper") + # Create element-scoped tempdir so that warped data is + # available later as nibabel stores file path reference for + # loading on computation + element_tempdir = WorkDirManager().get_element_tempdir( + prefix="ants_template_warper" + ) + + # Save template + template_space_img_path = tempdir / f"{dst}_T1w.nii.gz" + nib.save(template_space_img, template_space_img_path) + + # Create a tempfile for warped output + warped_output_path = element_tempdir / ( + f"data_warped_from_{input['space']}_to_" f"{dst}.nii.gz" + ) + + # Set antsApplyTransforms command + apply_transforms_cmd = [ + "antsApplyTransforms", + "-d 3", + "-e 3", + "-n LanczosWindowedSinc", + f"-i {input['path'].resolve()}", + f"-r {template_space_img_path.resolve()}", + f"-t {xfm_file_path.resolve()}", + f"-o {warped_output_path.resolve()}", + ] + # Call antsApplyTransforms + run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd) + + # Modify target data + input["data"] = nib.load(warped_output_path) + input["space"] = dst + + return input diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index 6df245eec..c366e45e0 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -3,17 +3,15 @@ # Authors: Synchon Mandal # License: AGPL -from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type, Union +from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union -import nibabel as nib from templateflow import api as tflow from ...api.decorators import register_preprocessor -from ...data import get_template, get_xfm -from ...pipeline import WorkDirManager -from ...utils import logger, raise_error, run_ext_cmd +from ...utils import logger, raise_error from ..base import BasePreprocessor from ._ants_native_warper import ANTsNativeWarper +from ._ants_template_warper import ANTsTemplateWarper from ._fsl_native_warper import FSLNativeWarper @@ -26,17 +24,20 @@ class SpaceWarper(BasePreprocessor): Parameters ---------- - using : {"fsl", "ants"} + using : {"fsl_native", "ants_native", "ants_template} Implementation to use for warping: * "fsl_native" : Use FSL's ``applywarp`` for native space warping * "ants_native" : Use ANTs' ``antsApplyTransforms`` for native space warping + * "ants_template" : Use ANTs' ``antsApplyTransforms`` for template + space warping reference : str The data type to use as reference for warping, can be either a data type like ``"T1w"`` or a template space like ``"MNI152NLin2009cAsym"``. - on : {"T1w", "BOLD"} or list of the options + Use ``"T1w"`` for native space warping and named templates for + template space warping. The data type to warp. Raises @@ -45,14 +46,6 @@ class SpaceWarper(BasePreprocessor): If ``using`` is invalid or if ``reference`` is invalid. - Notes - ----- - If you are setting ``reference`` to a template space like - "MNI152NLin2009cAsym", make sure ANTs is available for the - transformation else it will fail during runtime. It is tricky to validate - this beforehand and difficult to enforce this as a requirement, hence the - heads-up. - """ _CONDITIONAL_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, Type]]]] = [ @@ -65,13 +58,11 @@ class SpaceWarper(BasePreprocessor): "depends_on": ANTsNativeWarper, }, { - "using": "ants", - "depends_on": ANTsWarper, + "using": "ants_template", + "depends_on": ANTsTemplateWarper, }, ] - _DEPENDENCIES: ClassVar[Set[str]] = {"nibabel"} - def __init__( self, using: str, reference: str, on: Union[List[str], str] ) -> None: @@ -197,58 +188,9 @@ class SpaceWarper(BasePreprocessor): klass=RuntimeError, ) else: - # Get xfm file - xfm_file_path = get_xfm(src=input["space"], dst=self.reference) - # Get template space image - template_space_img = get_template( - space=self.reference, - target_data=input, - extra_input=None, + input = ANTsTemplateWarper().preprocess( + input=input, + dst=self.reference, ) - # Create component-scoped tempdir - tempdir = WorkDirManager().get_tempdir(prefix="space_warper") - # Create element-scoped tempdir so that warped data is - # available later as nibabel stores file path reference for - # loading on computation - element_tempdir = WorkDirManager().get_element_tempdir( - prefix="space_warper" - ) - - # Save template - template_space_img_path = ( - tempdir / f"{self.reference}_T1w.nii.gz" - ) - nib.save(template_space_img, template_space_img_path) - - # Create a tempfile for warped output - warped_output_path = element_tempdir / ( - f"data_warped_from_{input['space']}_to_" - f"{self.reference}.nii.gz" - ) - - logger.debug( - f"Using ANTs to warp data " - f"from {input['space']} to {self.reference}" - ) - # Set antsApplyTransforms command - apply_transforms_cmd = [ - "antsApplyTransforms", - "-d 3", - "-e 3", - "-n LanczosWindowedSinc", - f"-i {input['path'].resolve()}", - f"-r {template_space_img_path.resolve()}", - f"-t {xfm_file_path.resolve()}", - f"-o {warped_output_path.resolve()}", - ] - # Call antsApplyTransforms - run_ext_cmd( - name="antsApplyTransforms", cmd=apply_transforms_cmd - ) - - # Modify target data - input["data"] = nib.load(warped_output_path) - input["space"] = self.reference - return input, None -- 2.52.0 From 5194edc608a43a8cde230ffc7e47b574b8bc7aaa Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 14:43:34 +0100 Subject: [PATCH 12/22] chore: update docstring for SpaceWarper --- junifer/preprocess/warping/space_warper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index c366e45e0..9209f54a1 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -38,6 +38,8 @@ class SpaceWarper(BasePreprocessor): type like ``"T1w"`` or a template space like ``"MNI152NLin2009cAsym"``. Use ``"T1w"`` for native space warping and named templates for template space warping. + on : {"T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"} or list \ + of the options The data type to warp. Raises -- 2.52.0 From 5a76512ff18d93d0594b67036d20c4122f34c71a Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 14:48:17 +0100 Subject: [PATCH 13/22] fix: typo in SpaceWarper --- junifer/preprocess/warping/space_warper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index 9209f54a1..f71c6c1d0 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -172,7 +172,7 @@ class SpaceWarper(BasePreprocessor): input=input, extra_input=extra_input, ) - elif self.using == "ants_natice": + elif self.using == "ants_native": input = ANTsNativeWarper().preprocess( input=input, extra_input=extra_input, -- 2.52.0 From 7ca1565eb3111ba18c7524b0bced11c105297f3b Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 14:49:52 +0100 Subject: [PATCH 14/22] update: delete component tempdir after use in ANTsTemplateWarper --- junifer/preprocess/warping/_ants_template_warper.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/junifer/preprocess/warping/_ants_template_warper.py b/junifer/preprocess/warping/_ants_template_warper.py index e821df859..e6fd03417 100644 --- a/junifer/preprocess/warping/_ants_template_warper.py +++ b/junifer/preprocess/warping/_ants_template_warper.py @@ -101,6 +101,9 @@ class ANTsTemplateWarper: # Call antsApplyTransforms run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd) + # Delete tempdir + WorkDirManager().delete_tempdir(tempdir) + # Modify target data input["data"] = nib.load(warped_output_path) input["space"] = dst -- 2.52.0 From 2d75f448b77b4d7eb16511a26dd4898cb8ad0274 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 15:55:19 +0100 Subject: [PATCH 15/22] chore: fix tests for SpaceWarper --- junifer/preprocess/warping/tests/test_space_warper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/junifer/preprocess/warping/tests/test_space_warper.py b/junifer/preprocess/warping/tests/test_space_warper.py index b82dfa806..fbd225d41 100644 --- a/junifer/preprocess/warping/tests/test_space_warper.py +++ b/junifer/preprocess/warping/tests/test_space_warper.py @@ -32,7 +32,7 @@ if TYPE_CHECKING: native_t1w=True, ), ("f9057kp", "wave1bas", "Rest", "AP", "1"), - "ants", + "ants_native", ], [ DataladHCP1200( @@ -41,7 +41,7 @@ if TYPE_CHECKING: ica_fix=True, ), ("100206", "REST1", "LR"), - "fsl", + "fsl_native", ], ], ) @@ -139,7 +139,7 @@ def test_SpaceWarper_multi_mni( pre_xfm_data = element_data["BOLD"]["data"].get_fdata().copy() # Preprocess data output, _ = SpaceWarper( - using="ants", + using="ants_template", reference=space, on=["T1w"], ).preprocess( -- 2.52.0 From 7c740a2d54757213ccc6b56e5cbf670cc932ef79 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 15 Mar 2024 17:23:32 +0100 Subject: [PATCH 16/22] chore: correct data key access for SpaceWarper test --- junifer/preprocess/warping/tests/test_space_warper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/preprocess/warping/tests/test_space_warper.py b/junifer/preprocess/warping/tests/test_space_warper.py index fbd225d41..0950748e7 100644 --- a/junifer/preprocess/warping/tests/test_space_warper.py +++ b/junifer/preprocess/warping/tests/test_space_warper.py @@ -136,7 +136,7 @@ def test_SpaceWarper_multi_mni( with datagrabber as dg: # Read data element_data = DefaultDataReader().fit_transform(dg[element]) - pre_xfm_data = element_data["BOLD"]["data"].get_fdata().copy() + pre_xfm_data = element_data["T1w"]["data"].get_fdata().copy() # Preprocess data output, _ = SpaceWarper( using="ants_template", -- 2.52.0 From 54493ed3f13ad0f4ab4f6e77cacdc00da029ded9 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 18 Mar 2024 09:06:20 +0100 Subject: [PATCH 17/22] update: improve conditionals for SpaceWarper --- junifer/preprocess/warping/space_warper.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index f71c6c1d0..a6743b190 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -159,7 +159,10 @@ class SpaceWarper(BasePreprocessor): """ logger.info(f"Warping to {self.reference} space using SpaceWarper") # Transform to native space - if self.reference == "T1w": + if ( + self.using in ["fsl_native", "ants_native"] + and self.reference == "T1w" + ): # Check for extra inputs if extra_input is None: raise_error( @@ -178,7 +181,7 @@ class SpaceWarper(BasePreprocessor): extra_input=extra_input, ) # Transform to template space - else: + elif self.using == "ants_template" and self.reference != "T1w": # Check pre-requirements for space manipulation if self.reference == input["space"]: raise_error( @@ -189,7 +192,7 @@ class SpaceWarper(BasePreprocessor): ), klass=RuntimeError, ) - else: + if self.using == "ants_template": input = ANTsTemplateWarper().preprocess( input=input, dst=self.reference, -- 2.52.0 From 7bf50b8cc577f8a8872337631e6be6795ea1abb7 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 18 Mar 2024 14:37:46 +0100 Subject: [PATCH 18/22] update: revert warping helpers to FSLWarper and ANTsWarper with improvements --- .../preprocess/warping/_ants_native_warper.py | 115 ------------ .../warping/_ants_template_warper.py | 111 ------------ junifer/preprocess/warping/_ants_warper.py | 167 ++++++++++++++++++ .../{_fsl_native_warper.py => _fsl_warper.py} | 10 +- junifer/preprocess/warping/space_warper.py | 53 +++--- 5 files changed, 194 insertions(+), 262 deletions(-) delete mode 100644 junifer/preprocess/warping/_ants_native_warper.py delete mode 100644 junifer/preprocess/warping/_ants_template_warper.py create mode 100644 junifer/preprocess/warping/_ants_warper.py rename junifer/preprocess/warping/{_fsl_native_warper.py => _fsl_warper.py} (92%) diff --git a/junifer/preprocess/warping/_ants_native_warper.py b/junifer/preprocess/warping/_ants_native_warper.py deleted file mode 100644 index 2d4a64364..000000000 --- a/junifer/preprocess/warping/_ants_native_warper.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Provide class for native space warping via ANTs antsApplyTransforms.""" - -# Authors: Synchon Mandal -# License: AGPL - -from typing import ( - Any, - ClassVar, - Dict, - List, - Set, - Union, -) - -import nibabel as nib -import numpy as np - -from ...pipeline import WorkDirManager -from ...utils import logger, run_ext_cmd - - -class ANTsNativeWarper: - """Class for native space warping via ANTs antsApplyTransforms. - - This class uses ANTs' ``ResampleImage`` for resampling and - ``antsApplyTransforms`` for transformation. - - """ - - _EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [ - { - "name": "ants", - "commands": ["ResampleImage", "antsApplyTransforms"], - }, - ] - - _DEPENDENCIES: ClassVar[Set[str]] = {"numpy", "nibabel"} - - def preprocess( - self, - input: Dict[str, Any], - extra_input: Dict[str, Any], - ) -> Dict[str, Any]: - """Preprocess using ANTs. - - Parameters - ---------- - input : dict - A single input from the Junifer Data object in which to preprocess. - extra_input : dict - The other fields in the Junifer Data object. Should have ``T1w`` - and ``Warp`` data types. - - Returns - ------- - dict - The ``input`` dictionary with modified ``data`` and ``space`` key - values and new ``reference_path`` key whose value points to the - reference file used for warping. - - """ - logger.debug("Using ANTs for native space warping") - - # Get the min of the voxel sizes from input and use it as the - # resolution - resolution = np.min(input["data"].header.get_zooms()[:3]) - - # Create element-specific tempdir for storing post-warping assets - element_tempdir = WorkDirManager().get_element_tempdir( - prefix="ants_warper" - ) - - # Create a tempfile for resampled reference output - resample_image_out_path = ( - element_tempdir / "resampled_reference.nii.gz" - ) - # Set ResampleImage command - resample_image_cmd = [ - "ResampleImage", - "3", # image dimension - f"{extra_input['T1w']['path'].resolve()}", - f"{resample_image_out_path.resolve()}", - f"{resolution}x{resolution}x{resolution}", - "0", # option for spacing and not size - "3 3", # Lanczos windowed sinc - ] - # Call ResampleImage - run_ext_cmd(name="ResampleImage", cmd=resample_image_cmd) - - # Create a tempfile for warped output - apply_transforms_out_path = element_tempdir / "output.nii.gz" - # Set antsApplyTransforms command - apply_transforms_cmd = [ - "antsApplyTransforms", - "-d 3", - "-e 3", - "-n LanczosWindowedSinc", - f"-i {input['path'].resolve()}", - # use resampled reference - f"-r {resample_image_out_path.resolve()}", - f"-t {extra_input['Warp']['path'].resolve()}", - f"-o {apply_transforms_out_path.resolve()}", - ] - # Call antsApplyTransforms - run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd) - - # Load nifti - input["data"] = nib.load(apply_transforms_out_path) - # Save resampled reference path - input["reference_path"] = resample_image_out_path - - # Use reference input's space as warped input's space - input["space"] = extra_input["T1w"]["space"] - - return input diff --git a/junifer/preprocess/warping/_ants_template_warper.py b/junifer/preprocess/warping/_ants_template_warper.py deleted file mode 100644 index e6fd03417..000000000 --- a/junifer/preprocess/warping/_ants_template_warper.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Provide class for template space warping via ANTs antsApplyTransforms.""" - -# Authors: Synchon Mandal -# License: AGPL - -from typing import ( - Any, - ClassVar, - Dict, - List, - Set, - Union, -) - -import nibabel as nib - -from ...data import get_template, get_xfm -from ...pipeline import WorkDirManager -from ...utils import logger, run_ext_cmd - - -class ANTsTemplateWarper: - """Class for template space warping via ANTs antsApplyTransforms. - - This class uses ANTs' ``antsApplyTransforms`` for transformation. - - """ - - _EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [ - { - "name": "ants", - "commands": ["antsApplyTransforms"], - }, - ] - - _DEPENDENCIES: ClassVar[Set[str]] = {"nibabel"} - - def preprocess( - self, - input: Dict[str, Any], - dst: str, - ) -> Dict[str, Any]: - """Preprocess using ANTs. - - Parameters - ---------- - input : dict - A single input from the Junifer Data object in which to preprocess. - dst : str - Template space to warp to. - - Returns - ------- - dict - The ``input`` dictionary with modified ``data`` and ``space`` key - values. - - """ - logger.debug( - f"Using ANTs to warp data " f"from {input['space']} to {dst}" - ) - - # Get xfm file - xfm_file_path = get_xfm(src=input["space"], dst=dst) - # Get template space image - template_space_img = get_template( - space=dst, - target_data=input, - extra_input=None, - ) - - # Create component-scoped tempdir - tempdir = WorkDirManager().get_tempdir(prefix="ants_template_warper") - # Create element-scoped tempdir so that warped data is - # available later as nibabel stores file path reference for - # loading on computation - element_tempdir = WorkDirManager().get_element_tempdir( - prefix="ants_template_warper" - ) - - # Save template - template_space_img_path = tempdir / f"{dst}_T1w.nii.gz" - nib.save(template_space_img, template_space_img_path) - - # Create a tempfile for warped output - warped_output_path = element_tempdir / ( - f"data_warped_from_{input['space']}_to_" f"{dst}.nii.gz" - ) - - # Set antsApplyTransforms command - apply_transforms_cmd = [ - "antsApplyTransforms", - "-d 3", - "-e 3", - "-n LanczosWindowedSinc", - f"-i {input['path'].resolve()}", - f"-r {template_space_img_path.resolve()}", - f"-t {xfm_file_path.resolve()}", - f"-o {warped_output_path.resolve()}", - ] - # Call antsApplyTransforms - run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd) - - # Delete tempdir - WorkDirManager().delete_tempdir(tempdir) - - # Modify target data - input["data"] = nib.load(warped_output_path) - input["space"] = dst - - return input diff --git a/junifer/preprocess/warping/_ants_warper.py b/junifer/preprocess/warping/_ants_warper.py new file mode 100644 index 000000000..a78d67702 --- /dev/null +++ b/junifer/preprocess/warping/_ants_warper.py @@ -0,0 +1,167 @@ +"""Provide class for space warping via ANTs antsApplyTransforms.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import ( + Any, + ClassVar, + Dict, + List, + Set, + Union, +) + +import nibabel as nib +import numpy as np + +from ...data import get_template, get_xfm +from ...pipeline import WorkDirManager +from ...utils import logger, run_ext_cmd + + +class ANTsWarper: + """Class for space warping via ANTs antsApplyTransforms. + + This class uses ANTs' ``ResampleImage`` for resampling (if required) and + ``antsApplyTransforms`` for transformation. + + """ + + _EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [ + { + "name": "ants", + "commands": ["ResampleImage", "antsApplyTransforms"], + }, + ] + + _DEPENDENCIES: ClassVar[Set[str]] = {"numpy", "nibabel"} + + def preprocess( + self, + input: Dict[str, Any], + extra_input: Dict[str, Any], + reference: str, + ) -> Dict[str, Any]: + """Preprocess using ANTs. + + Parameters + ---------- + input : dict + A single input from the Junifer Data object in which to preprocess. + extra_input : dict + The other fields in the Junifer Data object. Should have ``T1w`` + and ``Warp`` data types. + reference : str + The data type or template space to use as reference for warping. + + Returns + ------- + dict + The ``input`` dictionary with modified ``data`` and ``space`` key + values and new ``reference_path`` key whose value points to the + reference file used for warping. + + """ + # Create element-specific tempdir for storing post-warping assets + element_tempdir = WorkDirManager().get_element_tempdir( + prefix="ants_warper" + ) + + # Native space warping + if reference == "T1w": + logger.debug("Using ANTs for space warping") + + # Get the min of the voxel sizes from input and use it as the + # resolution + resolution = np.min(input["data"].header.get_zooms()[:3]) + + # Create a tempfile for resampled reference output + resample_image_out_path = ( + element_tempdir / "resampled_reference.nii.gz" + ) + # Set ResampleImage command + resample_image_cmd = [ + "ResampleImage", + "3", # image dimension + f"{extra_input['T1w']['path'].resolve()}", + f"{resample_image_out_path.resolve()}", + f"{resolution}x{resolution}x{resolution}", + "0", # option for spacing and not size + "3 3", # Lanczos windowed sinc + ] + # Call ResampleImage + run_ext_cmd(name="ResampleImage", cmd=resample_image_cmd) + + # Create a tempfile for warped output + apply_transforms_out_path = element_tempdir / "output.nii.gz" + # Set antsApplyTransforms command + apply_transforms_cmd = [ + "antsApplyTransforms", + "-d 3", + "-e 3", + "-n LanczosWindowedSinc", + f"-i {input['path'].resolve()}", + # use resampled reference + f"-r {resample_image_out_path.resolve()}", + f"-t {extra_input['Warp']['path'].resolve()}", + f"-o {apply_transforms_out_path.resolve()}", + ] + # Call antsApplyTransforms + run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd) + + # Load nifti + input["data"] = nib.load(apply_transforms_out_path) + # Save resampled reference path + input["reference_path"] = resample_image_out_path + # Use reference input's space as warped input's space + input["space"] = extra_input["T1w"]["space"] + + # Template space warping + else: + logger.debug( + f"Using ANTs to warp data from {input['space']} to {reference}" + ) + + # Get xfm file + xfm_file_path = get_xfm(src=input["space"], dst=reference) + # Get template space image + template_space_img = get_template( + space=reference, + target_data=input, + extra_input=None, + ) + + # Create component-scoped tempdir + tempdir = WorkDirManager().get_tempdir(prefix="ants_warper") + # Save template + template_space_img_path = tempdir / f"{reference}_T1w.nii.gz" + nib.save(template_space_img, template_space_img_path) + + # Create a tempfile for warped output + warped_output_path = element_tempdir / ( + f"data_warped_from_{input['space']}_to_" f"{reference}.nii.gz" + ) + + # Set antsApplyTransforms command + apply_transforms_cmd = [ + "antsApplyTransforms", + "-d 3", + "-e 3", + "-n LanczosWindowedSinc", + f"-i {input['path'].resolve()}", + f"-r {template_space_img_path.resolve()}", + f"-t {xfm_file_path.resolve()}", + f"-o {warped_output_path.resolve()}", + ] + # Call antsApplyTransforms + run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd) + + # Delete tempdir + WorkDirManager().delete_tempdir(tempdir) + + # Modify target data + input["data"] = nib.load(warped_output_path) + input["space"] = reference + + return input diff --git a/junifer/preprocess/warping/_fsl_native_warper.py b/junifer/preprocess/warping/_fsl_warper.py similarity index 92% rename from junifer/preprocess/warping/_fsl_native_warper.py rename to junifer/preprocess/warping/_fsl_warper.py index 398fb8f95..32ded0e60 100644 --- a/junifer/preprocess/warping/_fsl_native_warper.py +++ b/junifer/preprocess/warping/_fsl_warper.py @@ -1,4 +1,4 @@ -"""Provide class for native space warping via FSL FLIRT.""" +"""Provide class for space warping via FSL FLIRT.""" # Authors: Synchon Mandal # License: AGPL @@ -19,8 +19,8 @@ from ...pipeline import WorkDirManager from ...utils import logger, run_ext_cmd -class FSLNativeWarper: - """Class for native space warping via FSL FLIRT. +class FSLWarper: + """Class for space warping via FSL FLIRT. This class uses FSL FLIRT's ``flirt`` for resampling and ``applywarp`` for transformation. @@ -59,7 +59,7 @@ class FSLNativeWarper: reference file used for warping. """ - logger.debug("Using FSL for native space warping") + logger.debug("Using FSL for space warping") # Get the min of the voxel sizes from input and use it as the # resolution @@ -67,7 +67,7 @@ class FSLNativeWarper: # Create element-specific tempdir for storing post-warping assets element_tempdir = WorkDirManager().get_element_tempdir( - prefix="fsl_native_warper" + prefix="fsl_warper" ) # Create a tempfile for resampled reference output diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index a6743b190..84cbb9c1d 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -10,9 +10,8 @@ from templateflow import api as tflow from ...api.decorators import register_preprocessor from ...utils import logger, raise_error from ..base import BasePreprocessor -from ._ants_native_warper import ANTsNativeWarper -from ._ants_template_warper import ANTsTemplateWarper -from ._fsl_native_warper import FSLNativeWarper +from ._ants_warper import ANTsWarper +from ._fsl_warper import FSLWarper __all__ = ["SpaceWarper"] @@ -24,14 +23,11 @@ class SpaceWarper(BasePreprocessor): Parameters ---------- - using : {"fsl_native", "ants_native", "ants_template} + using : {"fsl", "ants"} Implementation to use for warping: - * "fsl_native" : Use FSL's ``applywarp`` for native space warping - * "ants_native" : Use ANTs' ``antsApplyTransforms`` for native space - warping - * "ants_template" : Use ANTs' ``antsApplyTransforms`` for template - space warping + * "fsl" : Use FSL's ``applywarp`` + * "ants" : Use ANTs' ``antsApplyTransforms`` reference : str The data type to use as reference for warping, can be either a data @@ -52,16 +48,12 @@ class SpaceWarper(BasePreprocessor): _CONDITIONAL_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, Type]]]] = [ { - "using": "fsl_native", - "depends_on": FSLNativeWarper, + "using": "fsl", + "depends_on": FSLWarper, }, { - "using": "ants_native", - "depends_on": ANTsNativeWarper, - }, - { - "using": "ants_template", - "depends_on": ANTsTemplateWarper, + "using": "ants", + "depends_on": ANTsWarper, }, ] @@ -159,10 +151,7 @@ class SpaceWarper(BasePreprocessor): """ logger.info(f"Warping to {self.reference} space using SpaceWarper") # Transform to native space - if ( - self.using in ["fsl_native", "ants_native"] - and self.reference == "T1w" - ): + if self.using in ["fsl", "ants"] and self.reference == "T1w": # Check for extra inputs if extra_input is None: raise_error( @@ -170,18 +159,19 @@ class SpaceWarper(BasePreprocessor): f"`{self.reference}` data types in particular." ) # Conditional preprocessor - if self.using == "fsl_native": - input = FSLNativeWarper().preprocess( + if self.using == "fsl": + input = FSLWarper().preprocess( input=input, extra_input=extra_input, ) - elif self.using == "ants_native": - input = ANTsNativeWarper().preprocess( + elif self.using == "ants": + input = ANTsWarper().preprocess( input=input, extra_input=extra_input, + reference=self.reference, ) # Transform to template space - elif self.using == "ants_template" and self.reference != "T1w": + elif self.using == "ants" and self.reference != "T1w": # Check pre-requirements for space manipulation if self.reference == input["space"]: raise_error( @@ -192,10 +182,11 @@ class SpaceWarper(BasePreprocessor): ), klass=RuntimeError, ) - if self.using == "ants_template": - input = ANTsTemplateWarper().preprocess( - input=input, - dst=self.reference, - ) + + input = ANTsWarper().preprocess( + input=input, + extra_input={}, + reference=self.reference, + ) return input, None -- 2.52.0 From cc3c35acc7ef553a2e6d790da833a323a0b07575 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 18 Mar 2024 14:38:19 +0100 Subject: [PATCH 19/22] update: correct test parameters for SpaceWarper --- junifer/preprocess/warping/tests/test_space_warper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/junifer/preprocess/warping/tests/test_space_warper.py b/junifer/preprocess/warping/tests/test_space_warper.py index 0950748e7..0c03d1fb4 100644 --- a/junifer/preprocess/warping/tests/test_space_warper.py +++ b/junifer/preprocess/warping/tests/test_space_warper.py @@ -32,7 +32,7 @@ if TYPE_CHECKING: native_t1w=True, ), ("f9057kp", "wave1bas", "Rest", "AP", "1"), - "ants_native", + "ants", ], [ DataladHCP1200( @@ -41,7 +41,7 @@ if TYPE_CHECKING: ica_fix=True, ), ("100206", "REST1", "LR"), - "fsl_native", + "fsl", ], ], ) @@ -139,7 +139,7 @@ def test_SpaceWarper_multi_mni( pre_xfm_data = element_data["T1w"]["data"].get_fdata().copy() # Preprocess data output, _ = SpaceWarper( - using="ants_template", + using="ants", reference=space, on=["T1w"], ).preprocess( -- 2.52.0 From 2734615f332b7ce1818aad201af3c5a854535043 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 22 Mar 2024 11:26:54 +0100 Subject: [PATCH 20/22] update: handle use of FSL without T1w as reference for SpaceWarper --- junifer/preprocess/warping/space_warper.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index 84cbb9c1d..77b5e1e21 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -146,7 +146,8 @@ class SpaceWarper(BasePreprocessor): i.e., using ``"T1w"`` as reference. RuntimeError If the data is in the correct space and does not require - warping. + warping or + if FSL is used for template space warping. """ logger.info(f"Warping to {self.reference} space using SpaceWarper") @@ -170,7 +171,7 @@ class SpaceWarper(BasePreprocessor): extra_input=extra_input, reference=self.reference, ) - # Transform to template space + # Transform to template space with ANTs possible elif self.using == "ants" and self.reference != "T1w": # Check pre-requirements for space manipulation if self.reference == input["space"]: @@ -188,5 +189,14 @@ class SpaceWarper(BasePreprocessor): extra_input={}, reference=self.reference, ) + # Transform to template space with FSL not possible + elif self.using == "fsl" and self.reference != "T1w": + raise_error( + ( + f"Warping to {self.reference} space not possible with " + "FSL, use ANTs instead." + ), + klass=RuntimeError, + ) return input, None -- 2.52.0 From 555e856bcc142af957802f7abf1269694aee17bb Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 22 Mar 2024 11:27:27 +0100 Subject: [PATCH 21/22] update: improve error message when SpaceWarper shouldn't be used in the pipeline --- junifer/preprocess/warping/space_warper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index 77b5e1e21..18b9e9cca 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -177,9 +177,10 @@ class SpaceWarper(BasePreprocessor): if self.reference == input["space"]: raise_error( ( - "Skipped warping as the data is in " - f"{self.reference} space which would mean that you " - "can remove the SpaceWarper from the preprocess step." + f"The target data is in {self.reference} space " + "and thus warping will not be performed, hence you " + "should remove the SpaceWarper from the preprocess " + "step." ), klass=RuntimeError, ) -- 2.52.0 From 8a5bc77c642ec578657b1fad6b8d40d23e202c63 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 22 Mar 2024 11:28:05 +0100 Subject: [PATCH 22/22] update: add tests for SpaceWarper errors --- .../warping/tests/test_space_warper.py | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/junifer/preprocess/warping/tests/test_space_warper.py b/junifer/preprocess/warping/tests/test_space_warper.py index 0c03d1fb4..1d21d9fae 100644 --- a/junifer/preprocess/warping/tests/test_space_warper.py +++ b/junifer/preprocess/warping/tests/test_space_warper.py @@ -4,7 +4,7 @@ # License: AGPL import socket -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING, Tuple, Type import pytest from numpy.testing import assert_array_equal, assert_raises @@ -13,12 +13,57 @@ from junifer.datagrabber import DataladHCP1200, DMCC13Benchmark from junifer.datareader import DefaultDataReader from junifer.pipeline.utils import _check_ants, _check_fsl from junifer.preprocess import SpaceWarper +from junifer.testing.datagrabbers import PartlyCloudyTestingDataGrabber if TYPE_CHECKING: from junifer.datagrabber import BaseDataGrabber +@pytest.mark.parametrize( + "using, reference, error_type, error_msg", + [ + ("jam", "T1w", ValueError, "`using`"), + ("ants", "juice", ValueError, "reference"), + ("ants", "MNI152NLin2009cAsym", RuntimeError, "remove"), + ("fsl", "MNI152NLin2009cAsym", RuntimeError, "ANTs"), + ], +) +def test_SpaceWarper_errors( + using: str, + reference: str, + error_type: Type[Exception], + error_msg: str, +) -> None: + """Test SpaceWarper errors. + + Parameters + ---------- + using : str + The parametrized implementation method. + reference : str + The parametrized reference to use. + error_type : Exception-like object + The parametrized exception to check. + error_msg : str + The parametrized exception message to check. + + """ + with PartlyCloudyTestingDataGrabber() as dg: + # Read data + element_data = DefaultDataReader().fit_transform(dg["sub-01"]) + # Preprocess data + with pytest.raises(error_type, match=error_msg): + SpaceWarper( + using=using, + reference=reference, + on="BOLD", + ).preprocess( + input=element_data["BOLD"], + extra_input=element_data, + ) + + @pytest.mark.parametrize( "datagrabber, element, using", [ -- 2.52.0