From 85456704bddb9568c1064146bf76bb02373a4911 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 7 Feb 2024 15:22:33 +0100 Subject: [PATCH 01/10] update: add support for multi MNI transformations in BOLDWarper --- junifer/preprocess/bold_warper.py | 179 ++++++++++++++++++++++-------- 1 file changed, 135 insertions(+), 44 deletions(-) diff --git a/junifer/preprocess/bold_warper.py b/junifer/preprocess/bold_warper.py index 432fb6ff1..128e2cddf 100644 --- a/junifer/preprocess/bold_warper.py +++ b/junifer/preprocess/bold_warper.py @@ -13,8 +13,12 @@ from typing import ( Union, ) +import nibabel as nib + from ..api.decorators import register_preprocessor -from ..utils import logger, raise_error +from ..data import get_template, get_xfm +from ..pipeline import WorkDirManager +from ..utils import logger, raise_error, run_ext_cmd from .ants.ants_apply_transforms_warper import _AntsApplyTransformsWarper from .base import BasePreprocessor from .fsl.apply_warper import _ApplyWarper @@ -27,7 +31,13 @@ class BOLDWarper(BasePreprocessor): Parameters ---------- reference : str - The data type to use as reference for warping. + The data type to use as reference for warping, can be either a data + type like "T1w" or a template space like "MNI152NLin2009cAsym". + + Raises + ------ + ValueError + If ``reference`` is invalid. """ @@ -49,9 +59,25 @@ class BOLDWarper(BasePreprocessor): def __init__(self, reference: str) -> None: """Initialize the class.""" self.ref = reference - super().__init__( - on="BOLD", required_data_types=["BOLD", self.ref, "Warp"] - ) + # Initialize superclass based on reference + if self.ref == "T1w": + super().__init__( + on="BOLD", required_data_types=["BOLD", self.ref, "Warp"] + ) + elif self.ref in [ + "MNI152Lin", + "MNI152NLin6Sym", + "MNI152NLin6Asym", + "MNI152NLin2009aSym", + "MNI152NLin2009aAsym", + "MNI152NLin2009bSym", + "MNI152NLin2009bAsym", + "MNI152NLin2009cAsym", + "MNI152NLin2009cAsym", + ]: + super().__init__(on="BOLD", required_data_types=["BOLD"]) + else: + raise_error(f"Unknown reference: {self.ref}") def get_valid_inputs(self) -> List[str]: """Get valid data types for input. @@ -97,7 +123,8 @@ class BOLDWarper(BasePreprocessor): The BOLD input from the Junifer Data object. extra_input : dict, optional The other fields in the Junifer Data object. Must include the - ``Warp`` and ``ref`` value's keys. + ``Warp`` and ``ref`` value's keys if native space transformation is + needed. Returns ------- @@ -110,46 +137,110 @@ class BOLDWarper(BasePreprocessor): Raises ------ ValueError - If ``extra_input`` is None. + 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". + If warp / transformation file extension is not ".mat" or ".h5" + when transforming to native space. """ - logger.debug("Warping BOLD using BOLDWarper") - # Check for extra inputs - if extra_input is None: - raise_error( - f"No extra input provided, requires `Warp` and `{self.ref}` " - "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": - logger.debug("Using FSL with BOLDWarper") - # Initialize ApplyWarper for computation - apply_warper = _ApplyWarper(reference=self.ref, on="BOLD") - # Replace original BOLD data with warped BOLD data - _, input = apply_warper.preprocess( - input=input, - extra_input=extra_input, - ) - elif warp_file_ext == ".h5": - logger.debug("Using ANTs with BOLDWarper") - # Initialize AntsApplyTransformsWarper for computation - ants_apply_transforms_warper = _AntsApplyTransformsWarper( - reference=self.ref, on="BOLD" - ) - # Replace original BOLD data with warped BOLD data - _, input = ants_apply_transforms_warper.preprocess( - input=input, - extra_input=extra_input, - ) + logger.info(f"Warping BOLD to {self.ref} space using BOLDWarper") + # Transform to native space + if self.ref == "T1w": + # Check for extra inputs + if extra_input is None: + raise_error( + "No extra input provided, requires `Warp` and " + f"`{self.ref}` 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": + logger.debug("Using FSL with BOLDWarper") + # Initialize ApplyWarper for computation + apply_warper = _ApplyWarper(reference=self.ref, on="BOLD") + # Replace original BOLD data with warped BOLD data + _, input = apply_warper.preprocess( + input=input, + extra_input=extra_input, + ) + elif warp_file_ext == ".h5": + logger.debug("Using ANTs with BOLDWarper") + # Initialize AntsApplyTransformsWarper for computation + ants_apply_transforms_warper = _AntsApplyTransformsWarper( + reference=self.ref, on="BOLD" + ) + # Replace original BOLD data with warped BOLD data + _, input = ants_apply_transforms_warper.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: - raise_error( - msg=( - "Unknown warp / transformation file extension: " - f"{warp_file_ext}" - ), - klass=RuntimeError, - ) + # Check pre-requirements for space manipulation + if self.ref == input["space"]: + logger.info( + f"Skipped warping as the BOLD data is in {self.ref} space" + ) + else: + # Get xfm file + xfm_file_path = get_xfm(src=input["space"], dst=self.ref) + # Get template space image + template_space_img = get_template( + space=self.ref, + target_data=input, + extra_input=None, + ) + + # Create component-scoped tempdir + tempdir = WorkDirManager().get_tempdir(prefix="boldwarper") + # Create element-scoped tempdir so that warped BOLD is + # available later as nibabel stores file path reference for + # loading on computation + element_tempdir = WorkDirManager().get_element_tempdir( + prefix="boldwarper" + ) + + # Save template + template_space_img_path = tempdir / f"{self.ref}_T1w.nii.gz" + nib.save(template_space_img, template_space_img_path) + + # Create a tempfile for warped output + warped_bold_path = ( + element_tempdir + / f"bold_warped_from_{input['space']}_to_{self.ref}.nii.gz" + ) + + logger.debug( + f"Using ANTs to warp BOLD " + f"from {input['space']} to {self.ref}" + ) + # Set antsApplyTransforms command + apply_transforms_cmd = [ + "antsApplyTransforms", + "-d 3", + "-e 3", + "-n 'GenericLabel[NearestNeighbor]'", + f"-i {input['path'].resolve()}", + f"-r {template_space_img_path.resolve()}", + f"-t {xfm_file_path.resolve()}", + f"-o {warped_bold_path.resolve()}", + ] + # Call antsApplyTransforms + run_ext_cmd( + name="antsApplyTransforms", cmd=apply_transforms_cmd + ) + + # Modify target data + input["data"] = nib.load(warped_bold_path) + input["space"] = self.ref + return "BOLD", input -- 2.52.0 From 783c686ca55a4206e6d136da2f3dfdcc51112db7 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 7 Feb 2024 15:24:00 +0100 Subject: [PATCH 02/10] update: improve tests for BOLDWarper --- junifer/preprocess/tests/test_bold_warper.py | 75 +++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/junifer/preprocess/tests/test_bold_warper.py b/junifer/preprocess/tests/test_bold_warper.py index ac2d00720..ba91f5f0b 100644 --- a/junifer/preprocess/tests/test_bold_warper.py +++ b/junifer/preprocess/tests/test_bold_warper.py @@ -83,7 +83,7 @@ def test_BOLDWarper_get_output_type(input_: List[str]) -> None: socket.gethostname() != "juseless", reason="only for juseless", ) -def test_BOLDWarper_preprocess( +def test_BOLDWarper_preprocess_to_native( datagrabber: "BaseDataGrabber", element: Tuple[str, ...] ) -> None: """Test BOLDWarper preprocess. @@ -106,3 +106,76 @@ def test_BOLDWarper_preprocess( ) assert data_type == "BOLD" assert isinstance(data, dict) + + +@pytest.mark.parametrize( + "datagrabber, element", + "space", + [ + [ + DMCC13Benchmark( + types=["BOLD"], + sessions=["wave1bas"], + tasks=["Rest"], + phase_encodings=["AP"], + runs=["1"], + native_t1w=True, + ), + ("f9057kp", "wave1bas", "Rest", "AP", "1"), + "MNI152NLin2009aAsym", + ], + [ + DMCC13Benchmark( + types=["BOLD"], + sessions=["wave1bas"], + tasks=["Rest"], + phase_encodings=["AP"], + runs=["1"], + native_t1w=True, + ), + ("f9057kp", "wave1bas", "Rest", "AP", "1"), + "MNI152NLin6Asym", + ], + [ + DMCC13Benchmark( + types=["BOLD"], + sessions=["wave1bas"], + tasks=["Rest"], + phase_encodings=["AP"], + runs=["1"], + native_t1w=True, + ), + ("f9057kp", "wave1bas", "Rest", "AP", "1"), + "MNI152NLin2009cAsym", + ], + ], +) +@pytest.mark.skipif( + _check_ants() is False, reason="requires ANTs to be in PATH" +) +def test_BOLDWarper_preprocess_to_multi_mni( + datagrabber: "BaseDataGrabber", element: Tuple[str, ...], space: str +) -> None: + """Test BOLDWarper preprocess. + + 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]) + # Preprocess data + data_type, data = BOLDWarper(reference=space).preprocess( + input=element_data["BOLD"], + extra_input=element_data, + ) + assert data_type == "BOLD" + assert isinstance(data, dict) + assert data["space"] == space -- 2.52.0 From 9fd6e44c4e26aadfe1656d8ad0ac741386f163f1 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 8 Feb 2024 06:47:12 +0100 Subject: [PATCH 03/10] fix: change interpolation method for BOLDWarper to LanczosWindowedSinc --- junifer/preprocess/bold_warper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/preprocess/bold_warper.py b/junifer/preprocess/bold_warper.py index 128e2cddf..8d3cdb01f 100644 --- a/junifer/preprocess/bold_warper.py +++ b/junifer/preprocess/bold_warper.py @@ -228,7 +228,7 @@ class BOLDWarper(BasePreprocessor): "antsApplyTransforms", "-d 3", "-e 3", - "-n 'GenericLabel[NearestNeighbor]'", + "-n LanczosWindowedSinc", f"-i {input['path'].resolve()}", f"-r {template_space_img_path.resolve()}", f"-t {xfm_file_path.resolve()}", -- 2.52.0 From bf3bb878e84ff8529eb03dda9f74d75c21501380 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 8 Feb 2024 15:18:10 +0100 Subject: [PATCH 04/10] fix: correct parametrized parameters for BOLDWarper test --- junifer/preprocess/tests/test_bold_warper.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/junifer/preprocess/tests/test_bold_warper.py b/junifer/preprocess/tests/test_bold_warper.py index ba91f5f0b..7c7a9c8c6 100644 --- a/junifer/preprocess/tests/test_bold_warper.py +++ b/junifer/preprocess/tests/test_bold_warper.py @@ -109,8 +109,7 @@ def test_BOLDWarper_preprocess_to_native( @pytest.mark.parametrize( - "datagrabber, element", - "space", + "datagrabber, element, space", [ [ DMCC13Benchmark( -- 2.52.0 From 1bbe60bb4046a433281f0ccdf8053fb6cbac4e72 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 8 Feb 2024 15:21:49 +0100 Subject: [PATCH 05/10] chore: add changelog 302.enh --- docs/changes/newsfragments/302.enh | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changes/newsfragments/302.enh diff --git a/docs/changes/newsfragments/302.enh b/docs/changes/newsfragments/302.enh new file mode 100644 index 000000000..79bb91b30 --- /dev/null +++ b/docs/changes/newsfragments/302.enh @@ -0,0 +1 @@ +Allow :class:`.BOLDWarper` to warp BOLD data to other MNI spaces by `Synchon Mandal`_ -- 2.52.0 From 31037f733158beb4ebcabb319ee0da94096a5bff Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 13 Feb 2024 09:53:04 +0100 Subject: [PATCH 06/10] update: improve tests for BOLDWarper preprocess to multi MNI --- junifer/preprocess/tests/test_bold_warper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/junifer/preprocess/tests/test_bold_warper.py b/junifer/preprocess/tests/test_bold_warper.py index 7c7a9c8c6..b3d754ef9 100644 --- a/junifer/preprocess/tests/test_bold_warper.py +++ b/junifer/preprocess/tests/test_bold_warper.py @@ -118,7 +118,7 @@ def test_BOLDWarper_preprocess_to_native( tasks=["Rest"], phase_encodings=["AP"], runs=["1"], - native_t1w=True, + native_t1w=False, ), ("f9057kp", "wave1bas", "Rest", "AP", "1"), "MNI152NLin2009aAsym", @@ -130,7 +130,7 @@ def test_BOLDWarper_preprocess_to_native( tasks=["Rest"], phase_encodings=["AP"], runs=["1"], - native_t1w=True, + native_t1w=False, ), ("f9057kp", "wave1bas", "Rest", "AP", "1"), "MNI152NLin6Asym", @@ -142,7 +142,7 @@ def test_BOLDWarper_preprocess_to_native( tasks=["Rest"], phase_encodings=["AP"], runs=["1"], - native_t1w=True, + native_t1w=False, ), ("f9057kp", "wave1bas", "Rest", "AP", "1"), "MNI152NLin2009cAsym", -- 2.52.0 From 8952035c7cc62a30596a25875fe1c87a6488425f Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 16 Feb 2024 15:14:22 +0100 Subject: [PATCH 07/10] chore: raise error if BOLDWarper is not needed for warping --- junifer/preprocess/bold_warper.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/junifer/preprocess/bold_warper.py b/junifer/preprocess/bold_warper.py index 8d3cdb01f..8a8ee69ab 100644 --- a/junifer/preprocess/bold_warper.py +++ b/junifer/preprocess/bold_warper.py @@ -141,7 +141,9 @@ class BOLDWarper(BasePreprocessor): i.e., using "T1w" as reference. RuntimeError If warp / transformation file extension is not ".mat" or ".h5" - when transforming to native space. + when transforming to native space or + if the BOLD data is in the correct space and does not require + warping. """ logger.info(f"Warping BOLD to {self.ref} space using BOLDWarper") @@ -187,8 +189,13 @@ class BOLDWarper(BasePreprocessor): else: # Check pre-requirements for space manipulation if self.ref == input["space"]: - logger.info( - f"Skipped warping as the BOLD data is in {self.ref} space" + raise_error( + ( + f"Skipped warping as the BOLD data is in {self.ref} " + "space which would mean that you can remove the " + "BOLDWarper from the preprocess step." + ), + klass=RuntimeError, ) else: # Get xfm file -- 2.52.0 From 6db58feaeece8f647642d9d6e314894fff3f0162 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 16 Feb 2024 15:19:22 +0100 Subject: [PATCH 08/10] update: remove space restriction in BOLDWarper --- junifer/preprocess/bold_warper.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/junifer/preprocess/bold_warper.py b/junifer/preprocess/bold_warper.py index 8a8ee69ab..ddeeed93e 100644 --- a/junifer/preprocess/bold_warper.py +++ b/junifer/preprocess/bold_warper.py @@ -14,6 +14,7 @@ from typing import ( ) import nibabel as nib +from templateflow import api as tflow from ..api.decorators import register_preprocessor from ..data import get_template, get_xfm @@ -64,17 +65,7 @@ class BOLDWarper(BasePreprocessor): super().__init__( on="BOLD", required_data_types=["BOLD", self.ref, "Warp"] ) - elif self.ref in [ - "MNI152Lin", - "MNI152NLin6Sym", - "MNI152NLin6Asym", - "MNI152NLin2009aSym", - "MNI152NLin2009aAsym", - "MNI152NLin2009bSym", - "MNI152NLin2009bAsym", - "MNI152NLin2009cAsym", - "MNI152NLin2009cAsym", - ]: + elif self.ref in tflow.templates(): super().__init__(on="BOLD", required_data_types=["BOLD"]) else: raise_error(f"Unknown reference: {self.ref}") -- 2.52.0 From 5a4bf8c397181f8fbe083ff5f37c346c9542e3c9 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 21 Feb 2024 13:59:27 +0100 Subject: [PATCH 09/10] update: improve test in test_bold_warper.py --- junifer/preprocess/tests/test_bold_warper.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/junifer/preprocess/tests/test_bold_warper.py b/junifer/preprocess/tests/test_bold_warper.py index b3d754ef9..b65a0eaa6 100644 --- a/junifer/preprocess/tests/test_bold_warper.py +++ b/junifer/preprocess/tests/test_bold_warper.py @@ -7,6 +7,7 @@ import socket from typing import TYPE_CHECKING, List, Tuple import pytest +from numpy.testing import assert_array_equal, assert_raises from junifer.datagrabber import DataladHCP1200, DMCC13Benchmark from junifer.datareader import DefaultDataReader @@ -135,18 +136,6 @@ def test_BOLDWarper_preprocess_to_native( ("f9057kp", "wave1bas", "Rest", "AP", "1"), "MNI152NLin6Asym", ], - [ - DMCC13Benchmark( - types=["BOLD"], - sessions=["wave1bas"], - tasks=["Rest"], - phase_encodings=["AP"], - runs=["1"], - native_t1w=False, - ), - ("f9057kp", "wave1bas", "Rest", "AP", "1"), - "MNI152NLin2009cAsym", - ], ], ) @pytest.mark.skipif( @@ -170,6 +159,7 @@ def test_BOLDWarper_preprocess_to_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() # Preprocess data data_type, data = BOLDWarper(reference=space).preprocess( input=element_data["BOLD"], @@ -178,3 +168,5 @@ def test_BOLDWarper_preprocess_to_multi_mni( assert data_type == "BOLD" assert isinstance(data, dict) assert data["space"] == space + with assert_raises(AssertionError): + assert_array_equal(pre_xfm_data, data["data"]) -- 2.52.0 From 37a68ebe79b7e19d766a51d0b272645567ca4cc0 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 21 Feb 2024 15:11:52 +0100 Subject: [PATCH 10/10] chore: skip multi MNI warping via BOLDWarper test --- junifer/preprocess/tests/test_bold_warper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/junifer/preprocess/tests/test_bold_warper.py b/junifer/preprocess/tests/test_bold_warper.py index b65a0eaa6..58a26db14 100644 --- a/junifer/preprocess/tests/test_bold_warper.py +++ b/junifer/preprocess/tests/test_bold_warper.py @@ -141,6 +141,10 @@ def test_BOLDWarper_preprocess_to_native( @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_BOLDWarper_preprocess_to_multi_mni( datagrabber: "BaseDataGrabber", element: Tuple[str, ...], space: str ) -> None: -- 2.52.0