From 336ca1bdcbc7f365d5a35bee134de8aa2fe82727 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:39:23 +0000 Subject: [PATCH 1/4] Initial plan From ba485fd12f99a0de7a5b678ea9e8df1cbf1bbe61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:45:43 +0000 Subject: [PATCH 2/4] Add pipeline-compatible noise reduction module and tests Agent-Logs-Url: https://github.com/SINTEF/pyopia/sessions/eb7b9975-61d8-461a-85e6-1672c3a1084d Co-authored-by: nepstad <152277+nepstad@users.noreply.github.com> --- pyopia/noise.py | 81 ++++++++++++++++++++++++++++++++++++++ pyopia/tests/test_noise.py | 66 +++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 pyopia/noise.py create mode 100644 pyopia/tests/test_noise.py diff --git a/pyopia/noise.py b/pyopia/noise.py new file mode 100644 index 0000000..9887d89 --- /dev/null +++ b/pyopia/noise.py @@ -0,0 +1,81 @@ +''' +Noise reduction module for optional preprocessing steps in the pipeline. +''' +from skimage import exposure +from skimage import filters + + +class ReduceNoise(): + ''' + :class:`pyopia.pipeline` compatible class for optional noise reduction. + + Required keys in :class:`pyopia.pipeline.Data`: + - key given by ``image_source`` + + Parameters + ---------- + method : str, optional + Noise reduction method. Supported values are ``'gaussian'`` and ``'clahe'``. + Defaults to ``'gaussian'``. + image_source : str, optional + Key in Pipeline.data containing the input image. Defaults to ``'im_corrected'``. + output_key : str | None, optional + Key where the filtered image will be stored. If ``None``, the image is updated in place + (same key as ``image_source``). Defaults to ``None``. + gaussian_sigma : float, optional + Gaussian sigma when ``method='gaussian'``. Defaults to ``1.0``. + clahe_clip_limit : float, optional + CLAHE clip limit when ``method='clahe'``. Defaults to ``0.01``. + clahe_nbins : int, optional + Number of bins for CLAHE when ``method='clahe'``. Defaults to ``256``. + + Returns + ------- + data : :class:`pyopia.pipeline.Data` + containing filtered image in ``output_key``. + + Example pipeline use + -------------------- + + .. code-block:: toml + + [steps.noisereduction] + pipeline_class = 'pyopia.noise.ReduceNoise' + method = 'gaussian' + image_source = 'imraw' + output_key = 'imraw' + gaussian_sigma = 1.0 + ''' + + def __init__(self, + method='gaussian', + image_source='im_corrected', + output_key=None, + gaussian_sigma=1.0, + clahe_clip_limit=0.01, + clahe_nbins=256): + + self.method = method + self.image_source = image_source + self.output_key = image_source if output_key is None else output_key + self.gaussian_sigma = gaussian_sigma + self.clahe_clip_limit = clahe_clip_limit + self.clahe_nbins = clahe_nbins + + def __call__(self, data): + im = data[self.image_source] + + if self.method == 'gaussian': + data[self.output_key] = filters.gaussian(im, + sigma=self.gaussian_sigma, + preserve_range=True, + channel_axis=-1 if im.ndim == 3 else None) + elif self.method == 'clahe': + data[self.output_key] = exposure.equalize_adapthist(im, + clip_limit=self.clahe_clip_limit, + nbins=self.clahe_nbins) + else: + raise ValueError(f"Unknown noise reduction method '{self.method}'. " + "Expected one of: 'gaussian', 'clahe'.") + + return data diff --git a/pyopia/tests/test_noise.py b/pyopia/tests/test_noise.py new file mode 100644 index 0000000..57fca3f --- /dev/null +++ b/pyopia/tests/test_noise.py @@ -0,0 +1,66 @@ +import numpy as np +import pytest + +from pyopia.noise import ReduceNoise +from pyopia.pipeline import Pipeline + + +class _Data(dict): + pass + + +def test_reduce_noise_gaussian_smooths_image(): + data = _Data() + data['imraw'] = np.zeros((5, 5), dtype=float) + data['imraw'][2, 2] = 1.0 + + reducer = ReduceNoise(method='gaussian', image_source='imraw', output_key='im_denoised', gaussian_sigma=1.0) + out = reducer(data) + + assert out['im_denoised'].shape == data['imraw'].shape + assert out['im_denoised'][2, 2] < 1.0 + assert out['im_denoised'][2, 2] > 0.0 + + +def test_reduce_noise_clahe_returns_contrast_enhanced_float_image(): + data = _Data() + data['im_corrected'] = np.full((8, 8), 0.4, dtype=float) + data['im_corrected'][2:6, 2:6] = 0.6 + + reducer = ReduceNoise(method='clahe', image_source='im_corrected') + out = reducer(data) + + assert out['im_corrected'].shape == data['im_corrected'].shape + assert np.issubdtype(out['im_corrected'].dtype, np.floating) + assert out['im_corrected'].min() >= 0.0 + assert out['im_corrected'].max() <= 1.0 + + +def test_reduce_noise_invalid_method_raises_value_error(): + data = _Data(im_corrected=np.ones((4, 4), dtype=float)) + + reducer = ReduceNoise(method='invalid', image_source='im_corrected') + + with pytest.raises(ValueError, match='Unknown noise reduction method'): + reducer(data) + + +def test_reduce_noise_can_run_as_pipeline_step(): + settings = { + 'general': {'raw_files': None}, + 'steps': { + 'noisereduction': { + 'pipeline_class': 'pyopia.noise.ReduceNoise', + 'method': 'gaussian', + 'image_source': 'im_corrected', + 'gaussian_sigma': 0.5, + } + } + } + pipeline = Pipeline(settings=settings, initial_steps='') + pipeline.data['im_corrected'] = np.zeros((6, 6), dtype=float) + pipeline.data['im_corrected'][3, 3] = 1.0 + + pipeline.run_step('noisereduction') + + assert pipeline.data['im_corrected'][3, 3] < 1.0 From bf94e9b3f27fc01baad824398c0de89230748da1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:01:44 +0000 Subject: [PATCH 3/4] Address review feedback for noise module defaults and docs Agent-Logs-Url: https://github.com/SINTEF/pyopia/sessions/fbbe847c-b7de-4832-becc-5b8e1a82b5af Co-authored-by: nepstad <152277+nepstad@users.noreply.github.com> --- docs/_toc.yml | 1 + docs/notebooks/noise_reduction.md | 44 +++++++++++++++++++++++++++++++ pyopia/noise.py | 6 ++--- pyopia/tests/test_noise.py | 11 ++++---- 4 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 docs/notebooks/noise_reduction.md diff --git a/docs/_toc.yml b/docs/_toc.yml index beffedd..5d5c95f 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -22,6 +22,7 @@ parts: chapters: - file: notebooks/pipeline_step_by_step - file: notebooks/background_correction + - file: notebooks/noise_reduction - file: notebooks/exploring_pipeline_data - file: notebooks/montaging - caption: Code docs diff --git a/docs/notebooks/noise_reduction.md b/docs/notebooks/noise_reduction.md new file mode 100644 index 0000000..aa5b6fb --- /dev/null +++ b/docs/notebooks/noise_reduction.md @@ -0,0 +1,44 @@ +# Noise reduction + +For particularly noisy image data, you can add an optional noise reduction step in the pipeline. +This can be inserted anywhere it is useful, for example between image loading and background correction, +or just before segmentation. + +## Example: Gaussian noise reduction before background correction + +```toml +[steps.load] +pipeline_class = "pyopia.instrument.silcam.SilCamLoad" + +[steps.noisereduction] +pipeline_class = "pyopia.noise.ReduceNoise" +method = "gaussian" +image_source = "imraw" +gaussian_sigma = 1.0 + +[steps.correctbackground] +pipeline_class = "pyopia.background.CorrectBackgroundAccurate" +bgshift_function = "accurate" +average_window = 5 +image_source = "im_denoised" +``` + +## Example: CLAHE before segmentation + +```toml +[steps.imageprep] +pipeline_class = "pyopia.instrument.silcam.ImagePrep" +image_level = "im_corrected" + +[steps.noisereduction] +pipeline_class = "pyopia.noise.ReduceNoise" +method = "clahe" +image_source = "im_minimum" +clahe_clip_limit = 0.01 +clahe_nbins = 256 + +[steps.segmentation] +pipeline_class = "pyopia.process.Segment" +threshold = 0.85 +segment_source = "im_denoised" +``` diff --git a/pyopia/noise.py b/pyopia/noise.py index 9887d89..3a70a79 100644 --- a/pyopia/noise.py +++ b/pyopia/noise.py @@ -21,7 +21,7 @@ class ReduceNoise(): Key in Pipeline.data containing the input image. Defaults to ``'im_corrected'``. output_key : str | None, optional Key where the filtered image will be stored. If ``None``, the image is updated in place - (same key as ``image_source``). Defaults to ``None``. + in ``'im_denoised'``. Defaults to ``None``. gaussian_sigma : float, optional Gaussian sigma when ``method='gaussian'``. Defaults to ``1.0``. clahe_clip_limit : float, optional @@ -43,7 +43,7 @@ class ReduceNoise(): pipeline_class = 'pyopia.noise.ReduceNoise' method = 'gaussian' image_source = 'imraw' - output_key = 'imraw' + output_key = 'im_denoised' gaussian_sigma = 1.0 ''' @@ -57,7 +57,7 @@ def __init__(self, self.method = method self.image_source = image_source - self.output_key = image_source if output_key is None else output_key + self.output_key = 'im_denoised' if output_key is None else output_key self.gaussian_sigma = gaussian_sigma self.clahe_clip_limit = clahe_clip_limit self.clahe_nbins = clahe_nbins diff --git a/pyopia/tests/test_noise.py b/pyopia/tests/test_noise.py index 57fca3f..2088c45 100644 --- a/pyopia/tests/test_noise.py +++ b/pyopia/tests/test_noise.py @@ -30,10 +30,10 @@ def test_reduce_noise_clahe_returns_contrast_enhanced_float_image(): reducer = ReduceNoise(method='clahe', image_source='im_corrected') out = reducer(data) - assert out['im_corrected'].shape == data['im_corrected'].shape - assert np.issubdtype(out['im_corrected'].dtype, np.floating) - assert out['im_corrected'].min() >= 0.0 - assert out['im_corrected'].max() <= 1.0 + assert out['im_denoised'].shape == data['im_corrected'].shape + assert np.issubdtype(out['im_denoised'].dtype, np.floating) + assert out['im_denoised'].min() >= 0.0 + assert out['im_denoised'].max() <= 1.0 def test_reduce_noise_invalid_method_raises_value_error(): @@ -63,4 +63,5 @@ def test_reduce_noise_can_run_as_pipeline_step(): pipeline.run_step('noisereduction') - assert pipeline.data['im_corrected'][3, 3] < 1.0 + assert pipeline.data['im_denoised'][3, 3] < 1.0 + assert pipeline.data['im_corrected'][3, 3] == 1.0 From 4b9c9f76250ba33ba6a9b9404cf1deaf5e9d8166 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:13:02 +0000 Subject: [PATCH 4/4] Clarify output_key None behavior in ReduceNoise docs Agent-Logs-Url: https://github.com/SINTEF/pyopia/sessions/8cae7831-2989-45d8-8080-f70698ded5ae Co-authored-by: nepstad <152277+nepstad@users.noreply.github.com> --- pyopia/noise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyopia/noise.py b/pyopia/noise.py index 3a70a79..80f9286 100644 --- a/pyopia/noise.py +++ b/pyopia/noise.py @@ -20,8 +20,8 @@ class ReduceNoise(): image_source : str, optional Key in Pipeline.data containing the input image. Defaults to ``'im_corrected'``. output_key : str | None, optional - Key where the filtered image will be stored. If ``None``, the image is updated in place - in ``'im_denoised'``. Defaults to ``None``. + Key where the filtered image will be stored. If ``None``, the filtered image is stored + in ``'im_denoised'`` (i.e. not in place). Defaults to ``None``. gaussian_sigma : float, optional Gaussian sigma when ``method='gaussian'``. Defaults to ``1.0``. clahe_clip_limit : float, optional