Skip to content

Commit

Permalink
feature: introduce FSLSmoothing
Browse files Browse the repository at this point in the history
  • Loading branch information
synchon committed Apr 5, 2024
1 parent 429d506 commit e251e2f
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 2 deletions.
116 changes: 116 additions & 0 deletions junifer/preprocess/smoothing/_fsl_smoothing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Provide class for smoothing via FSL."""

# Authors: Synchon Mandal <[email protected]>
# 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
19 changes: 18 additions & 1 deletion junifer/preprocess/smoothing/smoothing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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.
Expand Down Expand Up @@ -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]]]] = [
Expand All @@ -65,6 +76,10 @@ class Smoothing(BasePreprocessor):
"using": "afni",
"depends_on": AFNISmoothing,
},
{
"using": "fsl",
"depends_on": FSLSmoothing,
},
]

def __init__(
Expand Down Expand Up @@ -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"],
Expand Down
29 changes: 28 additions & 1 deletion junifer/preprocess/smoothing/tests/test_smoothing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

0 comments on commit e251e2f

Please sign in to comment.