From e251e2f7949245477be33914414d913b1c5ff2d5 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 18 Mar 2024 13:26:16 +0100 Subject: [PATCH] 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 000000000..2e02ea222 --- /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 afefe439c..16f71a394 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 eb0d274b7..6fe66c55b 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)