Skip to content

3501 Add dict version SavitzkyGolaySmoothd #3502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/source/transforms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,14 @@ Intensity (Dict)
:members:
:special-members: __call__

`SavitzkyGolaySmoothd`
""""""""""""""""""""""
.. image:: https://github.com/Project-MONAI/DocImages/raw/main/transforms/SavitzkyGolaySmoothd.png
:alt: example of SavitzkyGolaySmoothd
.. autoclass:: SavitzkyGolaySmoothd
:members:
:special-members: __call__

`GaussianSmoothd`
"""""""""""""""""
.. image:: https://github.com/Project-MONAI/DocImages/raw/main/transforms/GaussianSmoothd.png
Expand Down
3 changes: 3 additions & 0 deletions monai/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@
RandStdShiftIntensityd,
RandStdShiftIntensityD,
RandStdShiftIntensityDict,
SavitzkyGolaySmoothd,
SavitzkyGolaySmoothD,
SavitzkyGolaySmoothDict,
ScaleIntensityd,
ScaleIntensityD,
ScaleIntensityDict,
Expand Down
42 changes: 42 additions & 0 deletions monai/transforms/intensity/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
RandScaleIntensity,
RandShiftIntensity,
RandStdShiftIntensity,
SavitzkyGolaySmooth,
ScaleIntensity,
ScaleIntensityRange,
ScaleIntensityRangePercentiles,
Expand Down Expand Up @@ -73,6 +74,7 @@
"RandAdjustContrastd",
"ScaleIntensityRangePercentilesd",
"MaskIntensityd",
"SavitzkyGolaySmoothd",
"GaussianSmoothd",
"RandGaussianSmoothd",
"GaussianSharpend",
Expand Down Expand Up @@ -115,6 +117,8 @@
"ScaleIntensityRangePercentilesDict",
"MaskIntensityD",
"MaskIntensityDict",
"SavitzkyGolaySmoothD",
"SavitzkyGolaySmoothDict",
"GaussianSmoothD",
"GaussianSmoothDict",
"RandGaussianSmoothD",
Expand Down Expand Up @@ -917,6 +921,43 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N
return d


class SavitzkyGolaySmoothd(MapTransform):
"""
Dictionary-based wrapper of :py:class:`monai.transforms.SavitzkyGolaySmooth`.

Args:
keys: keys of the corresponding items to be transformed.
See also: :py:class:`monai.transforms.compose.MapTransform`
window_length: length of the filter window, must be a positive odd integer.
order: order of the polynomial to fit to each window, must be less than ``window_length``.
axis: optional axis along which to apply the filter kernel. Default 1 (first spatial dimension).
mode: optional padding mode, passed to convolution class. ``'zeros'``, ``'reflect'``, ``'replicate'``
or ``'circular'``. default: ``'zeros'``. See ``torch.nn.Conv1d()`` for more information.
allow_missing_keys: don't raise exception if key is missing.

"""

backend = SavitzkyGolaySmooth.backend

def __init__(
self,
keys: KeysCollection,
window_length: int,
order: int,
axis: int = 1,
mode: str = "zeros",
allow_missing_keys: bool = False,
) -> None:
super().__init__(keys, allow_missing_keys)
self.converter = SavitzkyGolaySmooth(window_length=window_length, order=order, axis=axis, mode=mode)

def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]:
d = dict(data)
for key in self.key_iterator(d):
d[key] = self.converter(d[key])
return d


class GaussianSmoothd(MapTransform):
"""
Dictionary-based wrapper of :py:class:`monai.transforms.GaussianSmooth`.
Expand Down Expand Up @@ -1626,6 +1667,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N
RandAdjustContrastD = RandAdjustContrastDict = RandAdjustContrastd
ScaleIntensityRangePercentilesD = ScaleIntensityRangePercentilesDict = ScaleIntensityRangePercentilesd
MaskIntensityD = MaskIntensityDict = MaskIntensityd
SavitzkyGolaySmoothD = SavitzkyGolaySmoothDict = SavitzkyGolaySmoothd
GaussianSmoothD = GaussianSmoothDict = GaussianSmoothd
RandGaussianSmoothD = RandGaussianSmoothDict = RandGaussianSmoothd
GaussianSharpenD = GaussianSharpenDict = GaussianSharpend
Expand Down
2 changes: 2 additions & 0 deletions monai/transforms/utils_create_transform_ims.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
RandScaleIntensityd,
RandShiftIntensityd,
RandStdShiftIntensityd,
SavitzkyGolaySmoothd,
ScaleIntensityRanged,
ScaleIntensityRangePercentilesd,
ShiftIntensityd,
Expand Down Expand Up @@ -526,6 +527,7 @@ def create_transform_im(
create_transform_im(RandRicianNoise, dict(prob=1.0, mean=1, std=0.5), data)
create_transform_im(RandRicianNoised, dict(keys=CommonKeys.IMAGE, prob=1.0, mean=1, std=0.5), data)
create_transform_im(SavitzkyGolaySmooth, dict(window_length=5, order=1), data)
create_transform_im(SavitzkyGolaySmoothd, dict(keys=CommonKeys.IMAGE, window_length=5, order=1), data)
create_transform_im(GibbsNoise, dict(alpha=0.8), data)
create_transform_im(GibbsNoised, dict(keys=CommonKeys.IMAGE, alpha=0.8), data)
create_transform_im(RandGibbsNoise, dict(prob=1.0, alpha=(0.6, 0.8)), data)
Expand Down
12 changes: 3 additions & 9 deletions tests/test_savitzky_golay_smooth.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,9 @@


class TestSavitzkyGolaySmooth(unittest.TestCase):
@parameterized.expand([TEST_CASE_SINGLE_VALUE, TEST_CASE_2D_AXIS_2, TEST_CASE_SINE_SMOOTH])
def test_value(self, arguments, image, expected_data, atol):
for p in TEST_NDARRAYS:
result = SavitzkyGolaySmooth(**arguments)(p(image.astype(np.float32)))
torch.testing.assert_allclose(result, p(expected_data.astype(np.float32)), rtol=1e-4, atol=atol)


class TestSavitzkyGolaySmoothREP(unittest.TestCase):
@parameterized.expand([TEST_CASE_SINGLE_VALUE_REP])
@parameterized.expand(
[TEST_CASE_SINGLE_VALUE, TEST_CASE_2D_AXIS_2, TEST_CASE_SINE_SMOOTH, TEST_CASE_SINGLE_VALUE_REP]
)
def test_value(self, arguments, image, expected_data, atol):
for p in TEST_NDARRAYS:
result = SavitzkyGolaySmooth(**arguments)(p(image.astype(np.float32)))
Expand Down
72 changes: 72 additions & 0 deletions tests/test_savitzky_golay_smoothd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2020 MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest

import numpy as np
import torch
from parameterized import parameterized

from monai.transforms import SavitzkyGolaySmoothd
from tests.utils import TEST_NDARRAYS

# Zero-padding trivial tests

TEST_CASE_SINGLE_VALUE = [
{"keys": "img", "window_length": 3, "order": 1},
np.expand_dims(np.array([1.0]), 0), # Input data: Single value
np.expand_dims(np.array([1 / 3]), 0), # Expected output: With a window length of 3 and polyorder 1
# output should be equal to mean of 0, 1 and 0 = 1/3 (because input will be zero-padded and a linear fit performed)
1e-5, # absolute tolerance
]

TEST_CASE_2D_AXIS_2 = [
{"keys": "img", "window_length": 3, "order": 1, "axis": 2}, # along axis 2 (second spatial dim)
np.expand_dims(np.ones((2, 3)), 0),
np.expand_dims(np.array([[2 / 3, 1.0, 2 / 3], [2 / 3, 1.0, 2 / 3]]), 0),
1e-5, # absolute tolerance
]

# Replicated-padding trivial tests

TEST_CASE_SINGLE_VALUE_REP = [
{"keys": "img", "window_length": 3, "order": 1, "mode": "replicate"},
np.expand_dims(np.array([1.0]), 0), # Input data: Single value
np.expand_dims(np.array([1.0]), 0), # Expected output: With a window length of 3 and polyorder 1
# output will be equal to mean of [1, 1, 1] = 1 (input will be nearest-neighbour-padded and a linear fit performed)
1e-5, # absolute tolerance
]

# Sine smoothing

TEST_CASE_SINE_SMOOTH = [
{"keys": "img", "window_length": 3, "order": 1},
# Sine wave with period equal to savgol window length (windowed to reduce edge effects).
np.expand_dims(np.sin(2 * np.pi * 1 / 3 * np.arange(100)) * np.hanning(100), 0),
# Should be smoothed out to zeros
np.expand_dims(np.zeros(100), 0),
# tolerance chosen by examining output of SciPy.signal.savgol_filter() when provided the above input
2e-2, # absolute tolerance
]


class TestSavitzkyGolaySmoothd(unittest.TestCase):
@parameterized.expand(
[TEST_CASE_SINGLE_VALUE, TEST_CASE_2D_AXIS_2, TEST_CASE_SINE_SMOOTH, TEST_CASE_SINGLE_VALUE_REP]
)
def test_value(self, arguments, image, expected_data, atol):
for p in TEST_NDARRAYS:
result = SavitzkyGolaySmoothd(**arguments)({"img": p(image.astype(np.float32))})["img"]
torch.testing.assert_allclose(result, p(expected_data.astype(np.float32)), rtol=1e-4, atol=atol)


if __name__ == "__main__":
unittest.main()