From 55f34076100db579271626248ee2309a31f0d125 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 8 Mar 2024 14:18:01 +0100 Subject: [PATCH 01/12] feature: introduce SmoothingBase --- junifer/preprocess/smoothing/__init__.py | 4 ++ .../preprocess/smoothing/smoothing_base.py | 56 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 junifer/preprocess/smoothing/__init__.py create mode 100644 junifer/preprocess/smoothing/smoothing_base.py diff --git a/junifer/preprocess/smoothing/__init__.py b/junifer/preprocess/smoothing/__init__.py new file mode 100644 index 0000000000..88b947217d --- /dev/null +++ b/junifer/preprocess/smoothing/__init__.py @@ -0,0 +1,4 @@ +"""Provide imports for smoothing sub-package.""" + +# Authors: Synchon Mandal +# License: AGPL diff --git a/junifer/preprocess/smoothing/smoothing_base.py b/junifer/preprocess/smoothing/smoothing_base.py new file mode 100644 index 0000000000..3cd9c1cb8e --- /dev/null +++ b/junifer/preprocess/smoothing/smoothing_base.py @@ -0,0 +1,56 @@ +"""Provide base class for smoothing.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import List, Optional, Union + +from ..base import BasePreprocessor + + +__all__ = ["SmoothingBase"] + + +class SmoothingBase(BasePreprocessor): + """Base class for smoothing. + + Parameters + ---------- + on : {"T1w", "T2w", "BOLD"} or list of the options or None + The data type to apply smoothing to. If None, will apply to all + available data types. + + """ + + def __init__(self, on: Optional[Union[List[str], str]]) -> None: + """Initialize the class.""" + super().__init__(on=on, required_data_types=on) + + 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", "T2w", "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 From 9780d3042fce4c4a54ffd9a6183656ab1fad4403 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 8 Mar 2024 14:19:06 +0100 Subject: [PATCH 02/12] feature: introduce NilearnSmoothing --- junifer/preprocess/__init__.py | 1 + junifer/preprocess/smoothing/__init__.py | 2 + .../preprocess/smoothing/nilearn_smoothing.py | 90 +++++++++++++++++++ .../smoothing/tests/test_nilearn_smoothing.py | 55 ++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 junifer/preprocess/smoothing/nilearn_smoothing.py create mode 100644 junifer/preprocess/smoothing/tests/test_nilearn_smoothing.py diff --git a/junifer/preprocess/__init__.py b/junifer/preprocess/__init__.py index 15042f0eec..9f2b40b70c 100644 --- a/junifer/preprocess/__init__.py +++ b/junifer/preprocess/__init__.py @@ -9,3 +9,4 @@ from .confounds import fMRIPrepConfoundRemover from .bold_warper import BOLDWarper from .warping import SpaceWarper +from .smoothing import NilearnSmoothing diff --git a/junifer/preprocess/smoothing/__init__.py b/junifer/preprocess/smoothing/__init__.py index 88b947217d..68567513d7 100644 --- a/junifer/preprocess/smoothing/__init__.py +++ b/junifer/preprocess/smoothing/__init__.py @@ -2,3 +2,5 @@ # Authors: Synchon Mandal # License: AGPL + +from .nilearn_smoothing import NilearnSmoothing diff --git a/junifer/preprocess/smoothing/nilearn_smoothing.py b/junifer/preprocess/smoothing/nilearn_smoothing.py new file mode 100644 index 0000000000..242365fdc2 --- /dev/null +++ b/junifer/preprocess/smoothing/nilearn_smoothing.py @@ -0,0 +1,90 @@ +"""Provide class for smoothing via nilearn.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import ( + Any, + ClassVar, + Dict, + List, + Literal, + Optional, + Set, + Tuple, + Union, +) + +from nilearn import image as nimg +from numpy.typing import ArrayLike + +from ...api.decorators import register_preprocessor +from ...utils import logger +from .smoothing_base import SmoothingBase + + +__all__ = ["NilearnSmoothing"] + + +@register_preprocessor +class NilearnSmoothing(SmoothingBase): + """Class for smoothing via nilearn. + + Parameters + ---------- + fwhm : scalar, ``numpy.ndarray``, tuple or list of scalar, "fast" or None + Smoothing strength, as a full-width at half maximum, in millimeters: + + * If nonzero scalar, width is identical in all 3 directions. + * If ``numpy.ndarray``, tuple, or list, it must have 3 elements, giving + the FWHM along each axis. If any of the elements is 0 or None, + smoothing is not performed along that axis. + * If ``"fast"``, a fast smoothing will be performed with a filter + ``[0.2, 1, 0.2]`` in each direction and a normalisation to preserve + the local average value. + * If None, no filtering is performed (useful when just removal of + non-finite values is needed). + + on : {"T1w", "T2w", "BOLD"} or list of the options or None + The data type to apply smoothing to. If None, will apply to all + available data types (default None). + + """ + + _DEPENDENCIES: ClassVar[Set[str]] = {"nilearn"} + + def __init__( + self, + fwhm: Union[int, float, ArrayLike, Literal["fast"], None], + on: Optional[Union[List[str], str]] = None, + ) -> None: + """Initialize the class.""" + self.fwhm = fwhm + super().__init__(on=on) + + 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. + + """ + logger.info("Smoothing using NilearnSmoothing") + input["data"] = nimg.smooth_img(imgs=input["data"], fwhm=self.fwhm) + return input, None diff --git a/junifer/preprocess/smoothing/tests/test_nilearn_smoothing.py b/junifer/preprocess/smoothing/tests/test_nilearn_smoothing.py new file mode 100644 index 0000000000..180148a580 --- /dev/null +++ b/junifer/preprocess/smoothing/tests/test_nilearn_smoothing.py @@ -0,0 +1,55 @@ +"""Provide tests for NilearnSmoothing.""" + +# Authors: Synchon Mandal +# License: AGPL + + +import pytest + +from junifer.datareader import DefaultDataReader +from junifer.preprocess import NilearnSmoothing +from junifer.testing.datagrabbers import SPMAuditoryTestingDataGrabber + + +def test_NilearnSmoothing_init() -> None: + """Test NilearnSmoothing init.""" + smoothing = NilearnSmoothing(fwhm=None) + assert smoothing._on == ["T1w", "T2w", "BOLD"] + + +def test_NilearnSmoothing_get_valid_input() -> None: + """Test NilearnSmoothing get_valid_inputs.""" + smoothing = NilearnSmoothing(fwhm=None) + assert smoothing.get_valid_inputs() == ["T1w", "T2w", "BOLD"] + + +def test_NilearnSmoothing_get_output_type() -> None: + """Test NilearnSmoothing get_output_type.""" + smoothing = NilearnSmoothing(fwhm=None) + assert smoothing.get_output_type("BOLD") == "BOLD" + + +@pytest.mark.parametrize( + "data_type", + [ + "T1w", + "BOLD", + ], +) +def test_NilearnSmoothing_preprocess(data_type: str) -> None: + """Test NilearnSmoothing preprocess. + + Parameters + ---------- + data_type : str + The parametrized data type. + + """ + with SPMAuditoryTestingDataGrabber() as dg: + # Read data + element_data = DefaultDataReader().fit_transform(dg["sub001"]) + # Preprocess data + data, _ = NilearnSmoothing(fwhm="fast").preprocess( + input=element_data[data_type] + ) + assert isinstance(data, dict) From c0cd1f9812f0384a8a1857d1495c4b1c33e0e09d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 14 Mar 2024 14:35:11 +0100 Subject: [PATCH 03/12] refactor: migrate SmoothingBase to Smoothing making it the concrete class --- junifer/preprocess/smoothing/smoothing.py | 145 ++++++++++++++++++ .../preprocess/smoothing/smoothing_base.py | 56 ------- 2 files changed, 145 insertions(+), 56 deletions(-) create mode 100644 junifer/preprocess/smoothing/smoothing.py delete mode 100644 junifer/preprocess/smoothing/smoothing_base.py diff --git a/junifer/preprocess/smoothing/smoothing.py b/junifer/preprocess/smoothing/smoothing.py new file mode 100644 index 0000000000..a4fda9f956 --- /dev/null +++ b/junifer/preprocess/smoothing/smoothing.py @@ -0,0 +1,145 @@ +"""Provide class for smoothing.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union + +from ...api.decorators import register_preprocessor +from ...utils import logger, raise_error +from ..base import BasePreprocessor +from ._nilearn_smoothing import NilearnSmoothing + + +__all__ = ["Smoothing"] + + +@register_preprocessor +class Smoothing(BasePreprocessor): + """Class for smoothing. + + Parameters + ---------- + using : {"nilearn"} + Implementation to use for smoothing: + + * "nilearn" : Use :func`nilearn.image.smooth_img` + + smoothing_params : dict, optional + Extra parameters for smoothing as a dictionary (default None). + If ``using="nilearn"``, then the valid keys are: + + * ``fmhw`` : scalar, ``numpy.ndarray``, tuple or list of scalar, \ + "fast" or None + Smoothing strength, as a full-width at half maximum, in + millimeters: + + - If nonzero scalar, width is identical in all 3 directions. + - If ``numpy.ndarray``, tuple, or list, it must have 3 elements, + giving the FWHM along each axis. If any of the elements is 0 or + None, smoothing is not performed along that axis. + - If ``"fast"``, a fast smoothing will be performed with a filter + ``[0.2, 1, 0.2]`` in each direction and a normalisation to + preserve the local average value. + - If None, no filtering is performed (useful when just removal of + non-finite values is needed). + + on : {"T1w", "T2w", "BOLD"} or list of the options or None, optional + The data type to apply smoothing to. If None, will apply to all + available data types (default None). + + """ + + _CONDITIONAL_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, Type]]]] = [ + { + "using": "nilearn", + "depends_on": NilearnSmoothing, + }, + ] + + def __init__( + self, + using: str, + smoothing_params: Optional[Dict] = None, + on: Optional[Union[List[str], str]] = None, + ) -> 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.smoothing_params = ( + smoothing_params if smoothing_params is not None else {} + ) + super().__init__(on=on, required_data_types=on) + + 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", "T2w", "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. + + """ + logger.debug("Smoothing") + + # Conditional preprocessor + if self.using == "nilearn": + preprocessor = NilearnSmoothing() + # Smooth + output = preprocessor.preprocess( # type: ignore + data=input["data"], + **self.smoothing_params, + ) + + # Modify target data + input["data"] = output + + return input, None diff --git a/junifer/preprocess/smoothing/smoothing_base.py b/junifer/preprocess/smoothing/smoothing_base.py deleted file mode 100644 index 3cd9c1cb8e..0000000000 --- a/junifer/preprocess/smoothing/smoothing_base.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Provide base class for smoothing.""" - -# Authors: Synchon Mandal -# License: AGPL - -from typing import List, Optional, Union - -from ..base import BasePreprocessor - - -__all__ = ["SmoothingBase"] - - -class SmoothingBase(BasePreprocessor): - """Base class for smoothing. - - Parameters - ---------- - on : {"T1w", "T2w", "BOLD"} or list of the options or None - The data type to apply smoothing to. If None, will apply to all - available data types. - - """ - - def __init__(self, on: Optional[Union[List[str], str]]) -> None: - """Initialize the class.""" - super().__init__(on=on, required_data_types=on) - - 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", "T2w", "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 From 8075d4f66826bbe089a2d9b5af614eb17c9b2c13 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 14 Mar 2024 14:36:27 +0100 Subject: [PATCH 04/12] update: migrate NilearnSmoothing from a concrete preprocessor to helper class --- .../smoothing/_nilearn_smoothing.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 junifer/preprocess/smoothing/_nilearn_smoothing.py diff --git a/junifer/preprocess/smoothing/_nilearn_smoothing.py b/junifer/preprocess/smoothing/_nilearn_smoothing.py new file mode 100644 index 0000000000..30f5716055 --- /dev/null +++ b/junifer/preprocess/smoothing/_nilearn_smoothing.py @@ -0,0 +1,69 @@ +"""Provide class for smoothing via nilearn.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import ( + TYPE_CHECKING, + ClassVar, + Literal, + Set, + Union, +) + +from nilearn import image as nimg +from numpy.typing import ArrayLike + +from ...utils import logger + + +if TYPE_CHECKING: + from nibabel import Nifti1Image + + +__all__ = ["NilearnSmoothing"] + + +class NilearnSmoothing: + """Class for smoothing via nilearn. + + This class uses :func:`nilearn.image.smooth_img` to smooth image(s). + + """ + + _DEPENDENCIES: ClassVar[Set[str]] = {"nilearn"} + + def preprocess( + self, + data: "Nifti1Image", + fwhm: Union[int, float, ArrayLike, Literal["fast"], None], + ) -> "Nifti1Image": + """Preprocess using nilearn. + + Parameters + ---------- + data : Niimg-like object + Image(s) to preprocess. + fwhm : scalar, ``numpy.ndarray``, tuple or list of scalar, "fast" or \ + None + Smoothing strength, as a full-width at half maximum, in + millimeters: + + * If nonzero scalar, width is identical in all 3 directions. + * If ``numpy.ndarray``, tuple, or list, it must have 3 elements, + giving the FWHM along each axis. If any of the elements is 0 or + None, smoothing is not performed along that axis. + * If ``"fast"``, a fast smoothing will be performed with a filter + ``[0.2, 1, 0.2]`` in each direction and a normalisation to + preserve the local average value. + * If None, no filtering is performed (useful when just removal of + non-finite values is needed). + + Returns + ------- + Niimg-like object + The preprocessed image(s). + + """ + logger.info("Smoothing using nilearn") + return nimg.smooth_img(imgs=data, fwhm=fwhm) # type: ignore From 6e705a344e19a78e3b4e2737265d02865af4db45 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 14 Mar 2024 14:37:57 +0100 Subject: [PATCH 05/12] chore: remove old NilearnSmoothing class and its imports --- junifer/preprocess/__init__.py | 2 +- junifer/preprocess/smoothing/__init__.py | 2 +- .../preprocess/smoothing/nilearn_smoothing.py | 90 ------------------- .../smoothing/tests/test_nilearn_smoothing.py | 55 ------------ 4 files changed, 2 insertions(+), 147 deletions(-) delete mode 100644 junifer/preprocess/smoothing/nilearn_smoothing.py delete mode 100644 junifer/preprocess/smoothing/tests/test_nilearn_smoothing.py diff --git a/junifer/preprocess/__init__.py b/junifer/preprocess/__init__.py index 9f2b40b70c..d60c6efddb 100644 --- a/junifer/preprocess/__init__.py +++ b/junifer/preprocess/__init__.py @@ -9,4 +9,4 @@ from .confounds import fMRIPrepConfoundRemover from .bold_warper import BOLDWarper from .warping import SpaceWarper -from .smoothing import NilearnSmoothing +from .smoothing import Smoothing diff --git a/junifer/preprocess/smoothing/__init__.py b/junifer/preprocess/smoothing/__init__.py index 68567513d7..0236a63d1e 100644 --- a/junifer/preprocess/smoothing/__init__.py +++ b/junifer/preprocess/smoothing/__init__.py @@ -3,4 +3,4 @@ # Authors: Synchon Mandal # License: AGPL -from .nilearn_smoothing import NilearnSmoothing +from .smoothing import Smoothing diff --git a/junifer/preprocess/smoothing/nilearn_smoothing.py b/junifer/preprocess/smoothing/nilearn_smoothing.py deleted file mode 100644 index 242365fdc2..0000000000 --- a/junifer/preprocess/smoothing/nilearn_smoothing.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Provide class for smoothing via nilearn.""" - -# Authors: Synchon Mandal -# License: AGPL - -from typing import ( - Any, - ClassVar, - Dict, - List, - Literal, - Optional, - Set, - Tuple, - Union, -) - -from nilearn import image as nimg -from numpy.typing import ArrayLike - -from ...api.decorators import register_preprocessor -from ...utils import logger -from .smoothing_base import SmoothingBase - - -__all__ = ["NilearnSmoothing"] - - -@register_preprocessor -class NilearnSmoothing(SmoothingBase): - """Class for smoothing via nilearn. - - Parameters - ---------- - fwhm : scalar, ``numpy.ndarray``, tuple or list of scalar, "fast" or None - Smoothing strength, as a full-width at half maximum, in millimeters: - - * If nonzero scalar, width is identical in all 3 directions. - * If ``numpy.ndarray``, tuple, or list, it must have 3 elements, giving - the FWHM along each axis. If any of the elements is 0 or None, - smoothing is not performed along that axis. - * If ``"fast"``, a fast smoothing will be performed with a filter - ``[0.2, 1, 0.2]`` in each direction and a normalisation to preserve - the local average value. - * If None, no filtering is performed (useful when just removal of - non-finite values is needed). - - on : {"T1w", "T2w", "BOLD"} or list of the options or None - The data type to apply smoothing to. If None, will apply to all - available data types (default None). - - """ - - _DEPENDENCIES: ClassVar[Set[str]] = {"nilearn"} - - def __init__( - self, - fwhm: Union[int, float, ArrayLike, Literal["fast"], None], - on: Optional[Union[List[str], str]] = None, - ) -> None: - """Initialize the class.""" - self.fwhm = fwhm - super().__init__(on=on) - - 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. - - """ - logger.info("Smoothing using NilearnSmoothing") - input["data"] = nimg.smooth_img(imgs=input["data"], fwhm=self.fwhm) - return input, None diff --git a/junifer/preprocess/smoothing/tests/test_nilearn_smoothing.py b/junifer/preprocess/smoothing/tests/test_nilearn_smoothing.py deleted file mode 100644 index 180148a580..0000000000 --- a/junifer/preprocess/smoothing/tests/test_nilearn_smoothing.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Provide tests for NilearnSmoothing.""" - -# Authors: Synchon Mandal -# License: AGPL - - -import pytest - -from junifer.datareader import DefaultDataReader -from junifer.preprocess import NilearnSmoothing -from junifer.testing.datagrabbers import SPMAuditoryTestingDataGrabber - - -def test_NilearnSmoothing_init() -> None: - """Test NilearnSmoothing init.""" - smoothing = NilearnSmoothing(fwhm=None) - assert smoothing._on == ["T1w", "T2w", "BOLD"] - - -def test_NilearnSmoothing_get_valid_input() -> None: - """Test NilearnSmoothing get_valid_inputs.""" - smoothing = NilearnSmoothing(fwhm=None) - assert smoothing.get_valid_inputs() == ["T1w", "T2w", "BOLD"] - - -def test_NilearnSmoothing_get_output_type() -> None: - """Test NilearnSmoothing get_output_type.""" - smoothing = NilearnSmoothing(fwhm=None) - assert smoothing.get_output_type("BOLD") == "BOLD" - - -@pytest.mark.parametrize( - "data_type", - [ - "T1w", - "BOLD", - ], -) -def test_NilearnSmoothing_preprocess(data_type: str) -> None: - """Test NilearnSmoothing preprocess. - - Parameters - ---------- - data_type : str - The parametrized data type. - - """ - with SPMAuditoryTestingDataGrabber() as dg: - # Read data - element_data = DefaultDataReader().fit_transform(dg["sub001"]) - # Preprocess data - data, _ = NilearnSmoothing(fwhm="fast").preprocess( - input=element_data[data_type] - ) - assert isinstance(data, dict) From 2ea647bce1d797f4b543ca5ac7c4f075b5095ccd Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 14 Mar 2024 14:38:42 +0100 Subject: [PATCH 06/12] update: add tests for Smoothing --- .../smoothing/tests/test_smoothing.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 junifer/preprocess/smoothing/tests/test_smoothing.py diff --git a/junifer/preprocess/smoothing/tests/test_smoothing.py b/junifer/preprocess/smoothing/tests/test_smoothing.py new file mode 100644 index 0000000000..31dde43d0f --- /dev/null +++ b/junifer/preprocess/smoothing/tests/test_smoothing.py @@ -0,0 +1,37 @@ +"""Provide tests for Smoothing.""" + +# Authors: Synchon Mandal +# License: AGPL + + +import pytest + +from junifer.datareader import DefaultDataReader +from junifer.preprocess import Smoothing +from junifer.testing.datagrabbers import SPMAuditoryTestingDataGrabber + + +@pytest.mark.parametrize( + "data_type", + ["T1w", "BOLD", None], +) +def test_Smoothing(data_type: str) -> None: + """Test Smoothing. + + Parameters + ---------- + data_type : str + The parametrized data type. + + """ + with SPMAuditoryTestingDataGrabber() as dg: + # Read data + element_data = DefaultDataReader().fit_transform(dg["sub001"]) + # Preprocess data + data, _ = Smoothing( + using="nilearn", + smoothing_params={"fwhm": "fast"}, + on=data_type, + ).fit_transform(element_data) + + assert isinstance(data, dict) From dde728cc9fb83fafe6ba1543e5e99248049f3387 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 14 Mar 2024 15:06:08 +0100 Subject: [PATCH 07/12] update: make on argument positional in Smoothing --- junifer/preprocess/smoothing/smoothing.py | 10 ++++------ junifer/preprocess/smoothing/tests/test_smoothing.py | 8 ++++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/junifer/preprocess/smoothing/smoothing.py b/junifer/preprocess/smoothing/smoothing.py index a4fda9f956..71e5168620 100644 --- a/junifer/preprocess/smoothing/smoothing.py +++ b/junifer/preprocess/smoothing/smoothing.py @@ -25,6 +25,8 @@ class Smoothing(BasePreprocessor): * "nilearn" : Use :func`nilearn.image.smooth_img` + on : {"T1w", "T2w", "BOLD"} or list of the options + The data type to apply smoothing to. smoothing_params : dict, optional Extra parameters for smoothing as a dictionary (default None). If ``using="nilearn"``, then the valid keys are: @@ -44,10 +46,6 @@ class Smoothing(BasePreprocessor): - If None, no filtering is performed (useful when just removal of non-finite values is needed). - on : {"T1w", "T2w", "BOLD"} or list of the options or None, optional - The data type to apply smoothing to. If None, will apply to all - available data types (default None). - """ _CONDITIONAL_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, Type]]]] = [ @@ -60,8 +58,8 @@ class Smoothing(BasePreprocessor): def __init__( self, using: str, + on: Union[List[str], str], smoothing_params: Optional[Dict] = None, - on: Optional[Union[List[str], str]] = None, ) -> None: """Initialize the class.""" # Validate `using` parameter @@ -74,7 +72,7 @@ def __init__( self.smoothing_params = ( smoothing_params if smoothing_params is not None else {} ) - super().__init__(on=on, required_data_types=on) + super().__init__(on=on) def get_valid_inputs(self) -> List[str]: """Get valid data types for input. diff --git a/junifer/preprocess/smoothing/tests/test_smoothing.py b/junifer/preprocess/smoothing/tests/test_smoothing.py index 31dde43d0f..180bc8f9d9 100644 --- a/junifer/preprocess/smoothing/tests/test_smoothing.py +++ b/junifer/preprocess/smoothing/tests/test_smoothing.py @@ -13,7 +13,7 @@ @pytest.mark.parametrize( "data_type", - ["T1w", "BOLD", None], + ["T1w", "BOLD"], ) def test_Smoothing(data_type: str) -> None: """Test Smoothing. @@ -28,10 +28,10 @@ def test_Smoothing(data_type: str) -> None: # Read data element_data = DefaultDataReader().fit_transform(dg["sub001"]) # Preprocess data - data, _ = Smoothing( + output = Smoothing( using="nilearn", - smoothing_params={"fwhm": "fast"}, on=data_type, + smoothing_params={"fwhm": "fast"}, ).fit_transform(element_data) - assert isinstance(data, dict) + assert isinstance(output, dict) From cdf928f664222f32a3f3e943cf3e8da06f4621dd Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 18 Mar 2024 11:01:04 +0100 Subject: [PATCH 08/12] chore: fix docstring ref for Smoothing --- junifer/preprocess/smoothing/smoothing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/preprocess/smoothing/smoothing.py b/junifer/preprocess/smoothing/smoothing.py index 71e5168620..62af5b917b 100644 --- a/junifer/preprocess/smoothing/smoothing.py +++ b/junifer/preprocess/smoothing/smoothing.py @@ -23,7 +23,7 @@ class Smoothing(BasePreprocessor): using : {"nilearn"} Implementation to use for smoothing: - * "nilearn" : Use :func`nilearn.image.smooth_img` + * "nilearn" : Use :func:`nilearn.image.smooth_img` on : {"T1w", "T2w", "BOLD"} or list of the options The data type to apply smoothing to. From 6c8496882fbb999c1436de3a479349fd84561a9d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 18 Mar 2024 11:01:45 +0100 Subject: [PATCH 09/12] feature: introduce AFNISmoothing --- .../preprocess/smoothing/_afni_smoothing.py | 119 ++++++++++++++++++ junifer/preprocess/smoothing/smoothing.py | 16 ++- .../smoothing/tests/test_smoothing.py | 34 ++++- 3 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 junifer/preprocess/smoothing/_afni_smoothing.py diff --git a/junifer/preprocess/smoothing/_afni_smoothing.py b/junifer/preprocess/smoothing/_afni_smoothing.py new file mode 100644 index 0000000000..50fd27161b --- /dev/null +++ b/junifer/preprocess/smoothing/_afni_smoothing.py @@ -0,0 +1,119 @@ +"""Provide class for smoothing via AFNI.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import ( + TYPE_CHECKING, + ClassVar, + Dict, + List, + Set, + Union, +) + +import nibabel as nib + +from ...pipeline import WorkDirManager +from ...utils import logger, run_ext_cmd + + +if TYPE_CHECKING: + from nibabel import Nifti1Image + + +__all__ = ["AFNISmoothing"] + + +class AFNISmoothing: + """Class for smoothing via AFNI. + + This class uses AFNI's 3dBlurToFWHM. + + """ + + _EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [ + { + "name": "afni", + "commands": ["3dBlurToFWHM"], + }, + ] + + _DEPENDENCIES: ClassVar[Set[str]] = {"nibabel"} + + def preprocess( + self, + data: "Nifti1Image", + fwhm: Union[int, float], + ) -> "Nifti1Image": + """Preprocess using AFNI. + + Parameters + ---------- + data : Niimg-like object + Image(s) to preprocess. + fwhm : int or float + Smooth until the value. AFNI estimates the smoothing and then + applies smoothing to reach ``fwhm``. + + Returns + ------- + Niimg-like object + The preprocessed image(s). + + Notes + ----- + For more information on ``3dBlurToFWHM``, check: + https://afni.nimh.nih.gov/pub/dist/doc/program_help/3dBlurToFWHM.html + + As the process also depends on the conversion of AFNI files to NIfTI + via AFNI's ``3dAFNItoNIFTI``, the help for that can be found at: + https://afni.nimh.nih.gov/pub/dist/doc/program_help/3dAFNItoNIFTI.html + + """ + logger.info("Smoothing using AFNI") + + # Create component-scoped tempdir + tempdir = WorkDirManager().get_tempdir(prefix="afni_smoothing") + + # Save target data to a component-scoped tempfile + nifti_in_file_path = tempdir / "input.nii" # needs to be .nii + nib.save(data, nifti_in_file_path) + + # Set 3dBlurToFWHM command + blur_out_path_prefix = tempdir / "blur" + blur_cmd = [ + "3dBlurToFWHM", + f"-input {nifti_in_file_path.resolve()}", + f"-prefix {blur_out_path_prefix.resolve()}", + "-automask", + f"-FWHM {fwhm}", + ] + # Call 3dBlurToFWHM + run_ext_cmd(name="3dBlurToFWHM", cmd=blur_cmd) + + # Create element-scoped tempdir so that the blurred output is + # available later as nibabel stores file path reference for + # loading on computation + element_tempdir = WorkDirManager().get_element_tempdir( + prefix="afni_blur" + ) + # Convert afni to nifti + blur_afni_to_nifti_out_path = ( + element_tempdir / "output.nii" # needs to be .nii + ) + convert_cmd = [ + "3dAFNItoNIFTI", + f"-prefix {blur_afni_to_nifti_out_path.resolve()}", + f"{blur_out_path_prefix}+tlrc.BRIK", + ] + # Call 3dAFNItoNIFTI + run_ext_cmd(name="3dAFNItoNIFTI", cmd=convert_cmd) + + # Load nifti + output_data = nib.load(blur_afni_to_nifti_out_path) + + # Delete tempdir + WorkDirManager().delete_tempdir(tempdir) + + return output_data # type: ignore diff --git a/junifer/preprocess/smoothing/smoothing.py b/junifer/preprocess/smoothing/smoothing.py index 62af5b917b..afefe439c1 100644 --- a/junifer/preprocess/smoothing/smoothing.py +++ b/junifer/preprocess/smoothing/smoothing.py @@ -8,6 +8,7 @@ from ...api.decorators import register_preprocessor from ...utils import logger, raise_error from ..base import BasePreprocessor +from ._afni_smoothing import AFNISmoothing from ._nilearn_smoothing import NilearnSmoothing @@ -20,10 +21,11 @@ class Smoothing(BasePreprocessor): Parameters ---------- - using : {"nilearn"} + using : {"nilearn", "afni"} Implementation to use for smoothing: * "nilearn" : Use :func:`nilearn.image.smooth_img` + * "afni" : Use AFNI's ``3dBlurToFWHM`` on : {"T1w", "T2w", "BOLD"} or list of the options The data type to apply smoothing to. @@ -46,6 +48,12 @@ class Smoothing(BasePreprocessor): - If None, no filtering is performed (useful when just removal of non-finite values is needed). + else if ``using="afni"``, then the valid keys are: + + * ``fwhm`` : int or float + Smooth until the value. AFNI estimates the smoothing and then + applies smoothing to reach ``fwhm``. + """ _CONDITIONAL_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, Type]]]] = [ @@ -53,6 +61,10 @@ class Smoothing(BasePreprocessor): "using": "nilearn", "depends_on": NilearnSmoothing, }, + { + "using": "afni", + "depends_on": AFNISmoothing, + }, ] def __init__( @@ -131,6 +143,8 @@ def preprocess( # Conditional preprocessor if self.using == "nilearn": preprocessor = NilearnSmoothing() + elif self.using == "afni": + preprocessor = AFNISmoothing() # Smooth output = preprocessor.preprocess( # type: ignore data=input["data"], diff --git a/junifer/preprocess/smoothing/tests/test_smoothing.py b/junifer/preprocess/smoothing/tests/test_smoothing.py index 180bc8f9d9..eb0d274b7b 100644 --- a/junifer/preprocess/smoothing/tests/test_smoothing.py +++ b/junifer/preprocess/smoothing/tests/test_smoothing.py @@ -7,6 +7,7 @@ import pytest from junifer.datareader import DefaultDataReader +from junifer.pipeline.utils import _check_afni from junifer.preprocess import Smoothing from junifer.testing.datagrabbers import SPMAuditoryTestingDataGrabber @@ -15,8 +16,8 @@ "data_type", ["T1w", "BOLD"], ) -def test_Smoothing(data_type: str) -> None: - """Test Smoothing. +def test_Smoothing_nilearn(data_type: str) -> None: + """Test Smoothing using nilearn. Parameters ---------- @@ -35,3 +36,32 @@ def test_Smoothing(data_type: str) -> None: ).fit_transform(element_data) assert isinstance(output, dict) + + +@pytest.mark.parametrize( + "data_type", + ["T1w", "BOLD"], +) +@pytest.mark.skipif( + _check_afni() is False, reason="requires AFNI to be in PATH" +) +def test_Smoothing_afni(data_type: str) -> None: + """Test Smoothing using AFNI. + + Parameters + ---------- + data_type : str + The parametrized data type. + + """ + with SPMAuditoryTestingDataGrabber() as dg: + # Read data + element_data = DefaultDataReader().fit_transform(dg["sub001"]) + # Preprocess data + output = Smoothing( + using="afni", + on=data_type, + smoothing_params={"fwhm": 3}, + ).fit_transform(element_data) + + assert isinstance(output, dict) From 7fa1d1fd3bcc6d7e7f72657b5389981e2071aba2 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 18 Mar 2024 11:02:16 +0100 Subject: [PATCH 10/12] chore: fix docstring alignment for NilearnSmoothing --- junifer/preprocess/smoothing/_nilearn_smoothing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/junifer/preprocess/smoothing/_nilearn_smoothing.py b/junifer/preprocess/smoothing/_nilearn_smoothing.py index 30f5716055..1ef0498ee0 100644 --- a/junifer/preprocess/smoothing/_nilearn_smoothing.py +++ b/junifer/preprocess/smoothing/_nilearn_smoothing.py @@ -61,8 +61,8 @@ def preprocess( Returns ------- - Niimg-like object - The preprocessed image(s). + Niimg-like object + The preprocessed image(s). """ logger.info("Smoothing using nilearn") From 84f5056eb52f71c7025a26623d215a77d1b9b348 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 18 Mar 2024 13:26:16 +0100 Subject: [PATCH 11/12] feature: introduce FSLSmoothing --- .../preprocess/smoothing/_fsl_smoothing.py | 116 ++++++++++++++++++ junifer/preprocess/smoothing/smoothing.py | 19 ++- .../smoothing/tests/test_smoothing.py | 29 ++++- 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 junifer/preprocess/smoothing/_fsl_smoothing.py diff --git a/junifer/preprocess/smoothing/_fsl_smoothing.py b/junifer/preprocess/smoothing/_fsl_smoothing.py new file mode 100644 index 0000000000..2e02ea2226 --- /dev/null +++ b/junifer/preprocess/smoothing/_fsl_smoothing.py @@ -0,0 +1,116 @@ +"""Provide class for smoothing via FSL.""" + +# Authors: Synchon Mandal +# License: AGPL + +from typing import ( + TYPE_CHECKING, + ClassVar, + Dict, + List, + Set, + Union, +) + +import nibabel as nib + +from ...pipeline import WorkDirManager +from ...utils import logger, run_ext_cmd + + +if TYPE_CHECKING: + from nibabel import Nifti1Image + + +__all__ = ["FSLSmoothing"] + + +class FSLSmoothing: + """Class for smoothing via FSL. + + This class uses FSL's susan. + + """ + + _EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [ + { + "name": "fsl", + "commands": ["susan"], + }, + ] + + _DEPENDENCIES: ClassVar[Set[str]] = {"nibabel"} + + def preprocess( + self, + data: "Nifti1Image", + brightness_threshold: float, + fwhm: float, + ) -> "Nifti1Image": + """Preprocess using FSL. + + Parameters + ---------- + data : Niimg-like object + Image(s) to preprocess. + brightness_threshold : float + Threshold to discriminate between noise and the underlying image. + The value should be set greater than the noise level and less than + the contrast of the underlying image. + fwhm : float + Spatial extent of smoothing. + + Returns + ------- + Niimg-like object + The preprocessed image(s). + + Notes + ----- + For more information on ``SUSAN``, check [1]_ + + References + ---------- + .. [1] Smith, S.M. and Brady, J.M. (1997). + SUSAN - a new approach to low level image processing. + International Journal of Computer Vision, Volume 23(1), + Pages 45-78. + + """ + logger.info("Smoothing using FSL") + + # Create component-scoped tempdir + tempdir = WorkDirManager().get_tempdir(prefix="fsl_smoothing") + + # Save target data to a component-scoped tempfile + nifti_in_file_path = tempdir / "input.nii.gz" + nib.save(data, nifti_in_file_path) + + # Create element-scoped tempdir so that the output is + # available later as nibabel stores file path reference for + # loading on computation + element_tempdir = WorkDirManager().get_element_tempdir( + prefix="fsl_susan" + ) + susan_out_path = element_tempdir / "output.nii.gz" + # Set susan command + susan_cmd = [ + "susan", + f"{nifti_in_file_path.resolve()}", + f"{brightness_threshold}", + f"{fwhm}", + "3", # dimension + "1", # use median when no neighbourhood is found + "0", # use input image to find USAN + f"{susan_out_path.resolve()}", + ] + # Call susan + run_ext_cmd(name="susan", cmd=susan_cmd) + + # Load nifti + output_data = nib.load(susan_out_path) + + # Delete tempdir + WorkDirManager().delete_tempdir(tempdir) + + return output_data # type: ignore diff --git a/junifer/preprocess/smoothing/smoothing.py b/junifer/preprocess/smoothing/smoothing.py index afefe439c1..16f71a3943 100644 --- a/junifer/preprocess/smoothing/smoothing.py +++ b/junifer/preprocess/smoothing/smoothing.py @@ -9,6 +9,7 @@ from ...utils import logger, raise_error from ..base import BasePreprocessor from ._afni_smoothing import AFNISmoothing +from ._fsl_smoothing import FSLSmoothing from ._nilearn_smoothing import NilearnSmoothing @@ -21,11 +22,12 @@ class Smoothing(BasePreprocessor): Parameters ---------- - using : {"nilearn", "afni"} + using : {"nilearn", "afni", "fsl"} Implementation to use for smoothing: * "nilearn" : Use :func:`nilearn.image.smooth_img` * "afni" : Use AFNI's ``3dBlurToFWHM`` + * "fsl" : Use FSL SUSAN's ``susan`` on : {"T1w", "T2w", "BOLD"} or list of the options The data type to apply smoothing to. @@ -54,6 +56,15 @@ class Smoothing(BasePreprocessor): Smooth until the value. AFNI estimates the smoothing and then applies smoothing to reach ``fwhm``. + else if ``using="fsl"``, then the valid keys are: + + * ``brightness_threshold`` : float + Threshold to discriminate between noise and the underlying image. + The value should be set greater than the noise level and less than + the contrast of the underlying image. + * ``fwhm`` : float + Spatial extent of smoothing. + """ _CONDITIONAL_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, Type]]]] = [ @@ -65,6 +76,10 @@ class Smoothing(BasePreprocessor): "using": "afni", "depends_on": AFNISmoothing, }, + { + "using": "fsl", + "depends_on": FSLSmoothing, + }, ] def __init__( @@ -145,6 +160,8 @@ def preprocess( preprocessor = NilearnSmoothing() elif self.using == "afni": preprocessor = AFNISmoothing() + elif self.using == "fsl": + preprocessor = FSLSmoothing() # Smooth output = preprocessor.preprocess( # type: ignore data=input["data"], diff --git a/junifer/preprocess/smoothing/tests/test_smoothing.py b/junifer/preprocess/smoothing/tests/test_smoothing.py index eb0d274b7b..6fe66c55b2 100644 --- a/junifer/preprocess/smoothing/tests/test_smoothing.py +++ b/junifer/preprocess/smoothing/tests/test_smoothing.py @@ -7,7 +7,7 @@ import pytest from junifer.datareader import DefaultDataReader -from junifer.pipeline.utils import _check_afni +from junifer.pipeline.utils import _check_afni, _check_fsl from junifer.preprocess import Smoothing from junifer.testing.datagrabbers import SPMAuditoryTestingDataGrabber @@ -65,3 +65,30 @@ def test_Smoothing_afni(data_type: str) -> None: ).fit_transform(element_data) assert isinstance(output, dict) + + +@pytest.mark.parametrize( + "data_type", + ["T1w", "BOLD"], +) +@pytest.mark.skipif(_check_fsl() is False, reason="requires FSL to be in PATH") +def test_Smoothing_fsl(data_type: str) -> None: + """Test Smoothing using FSL. + + Parameters + ---------- + data_type : str + The parametrized data type. + + """ + with SPMAuditoryTestingDataGrabber() as dg: + # Read data + element_data = DefaultDataReader().fit_transform(dg["sub001"]) + # Preprocess data + output = Smoothing( + using="fsl", + on=data_type, + smoothing_params={"brightness_threshold": 10.0, "fwhm": 3.0}, + ).fit_transform(element_data) + + assert isinstance(output, dict) From 9054ff4b90e5a1b3766f7079ab4c697afec06d08 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 18 Mar 2024 13:31:14 +0100 Subject: [PATCH 12/12] chore: add changelog 161.feature --- docs/changes/newsfragments/161.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changes/newsfragments/161.feature diff --git a/docs/changes/newsfragments/161.feature b/docs/changes/newsfragments/161.feature new file mode 100644 index 0000000000..6c1e3742e5 --- /dev/null +++ b/docs/changes/newsfragments/161.feature @@ -0,0 +1 @@ +Introduce :class:`.Smoothing` for smoothing / blurring images as a preprocessing step by `Synchon Mandal`_