sigima 0.0.1.dev0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigima/__init__.py +142 -2
- sigima/client/__init__.py +105 -0
- sigima/client/base.py +780 -0
- sigima/client/remote.py +469 -0
- sigima/client/stub.py +814 -0
- sigima/client/utils.py +90 -0
- sigima/config.py +444 -0
- sigima/data/logo/Sigima.svg +135 -0
- sigima/data/tests/annotations.json +798 -0
- sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
- sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
- sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
- sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
- sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
- sigima/data/tests/curve_formats/bandwidth.txt +201 -0
- sigima/data/tests/curve_formats/boxcar.npy +0 -0
- sigima/data/tests/curve_formats/datetime.txt +1001 -0
- sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
- sigima/data/tests/curve_formats/fw1e2.txt +301 -0
- sigima/data/tests/curve_formats/fwhm.txt +319 -0
- sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
- sigima/data/tests/curve_formats/noised_saw.mat +0 -0
- sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
- sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
- sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
- sigima/data/tests/curve_formats/paracetamol.npy +0 -0
- sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
- sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
- sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
- sigima/data/tests/curve_formats/pulse1.npy +0 -0
- sigima/data/tests/curve_formats/pulse2.npy +0 -0
- sigima/data/tests/curve_formats/simple.txt +5 -0
- sigima/data/tests/curve_formats/spectrum.mca +2139 -0
- sigima/data/tests/curve_formats/square2.npy +0 -0
- sigima/data/tests/curve_formats/step.npy +0 -0
- sigima/data/tests/fabry-perot1.jpg +0 -0
- sigima/data/tests/fabry-perot2.jpg +0 -0
- sigima/data/tests/flower.npy +0 -0
- sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
- sigima/data/tests/image_formats/binary_image.npy +0 -0
- sigima/data/tests/image_formats/binary_image.png +0 -0
- sigima/data/tests/image_formats/centroid_test.npy +0 -0
- sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
- sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
- sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
- sigima/data/tests/image_formats/fiber.csv +480 -0
- sigima/data/tests/image_formats/fiber.jpg +0 -0
- sigima/data/tests/image_formats/fiber.png +0 -0
- sigima/data/tests/image_formats/fiber.txt +480 -0
- sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
- sigima/data/tests/image_formats/mr-brain.dcm +0 -0
- sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
- sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
- sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
- sigima/data/tests/image_formats/tiling.txt +10 -0
- sigima/data/tests/image_formats/uint16.tiff +0 -0
- sigima/data/tests/image_formats/uint8.tiff +0 -0
- sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
- sigima/enums.py +195 -0
- sigima/io/__init__.py +123 -0
- sigima/io/base.py +311 -0
- sigima/io/common/__init__.py +5 -0
- sigima/io/common/basename.py +164 -0
- sigima/io/common/converters.py +189 -0
- sigima/io/common/objmeta.py +181 -0
- sigima/io/common/textreader.py +58 -0
- sigima/io/convenience.py +157 -0
- sigima/io/enums.py +17 -0
- sigima/io/ftlab.py +395 -0
- sigima/io/image/__init__.py +9 -0
- sigima/io/image/base.py +177 -0
- sigima/io/image/formats.py +1016 -0
- sigima/io/image/funcs.py +414 -0
- sigima/io/signal/__init__.py +9 -0
- sigima/io/signal/base.py +129 -0
- sigima/io/signal/formats.py +290 -0
- sigima/io/signal/funcs.py +723 -0
- sigima/objects/__init__.py +260 -0
- sigima/objects/base.py +937 -0
- sigima/objects/image/__init__.py +88 -0
- sigima/objects/image/creation.py +556 -0
- sigima/objects/image/object.py +524 -0
- sigima/objects/image/roi.py +904 -0
- sigima/objects/scalar/__init__.py +57 -0
- sigima/objects/scalar/common.py +215 -0
- sigima/objects/scalar/geometry.py +502 -0
- sigima/objects/scalar/table.py +784 -0
- sigima/objects/shape.py +290 -0
- sigima/objects/signal/__init__.py +133 -0
- sigima/objects/signal/constants.py +27 -0
- sigima/objects/signal/creation.py +1428 -0
- sigima/objects/signal/object.py +444 -0
- sigima/objects/signal/roi.py +274 -0
- sigima/params.py +405 -0
- sigima/proc/__init__.py +96 -0
- sigima/proc/base.py +381 -0
- sigima/proc/decorator.py +330 -0
- sigima/proc/image/__init__.py +513 -0
- sigima/proc/image/arithmetic.py +335 -0
- sigima/proc/image/base.py +260 -0
- sigima/proc/image/detection.py +519 -0
- sigima/proc/image/edges.py +329 -0
- sigima/proc/image/exposure.py +406 -0
- sigima/proc/image/extraction.py +458 -0
- sigima/proc/image/filtering.py +219 -0
- sigima/proc/image/fourier.py +147 -0
- sigima/proc/image/geometry.py +661 -0
- sigima/proc/image/mathops.py +340 -0
- sigima/proc/image/measurement.py +195 -0
- sigima/proc/image/morphology.py +155 -0
- sigima/proc/image/noise.py +107 -0
- sigima/proc/image/preprocessing.py +182 -0
- sigima/proc/image/restoration.py +235 -0
- sigima/proc/image/threshold.py +217 -0
- sigima/proc/image/transformations.py +393 -0
- sigima/proc/signal/__init__.py +376 -0
- sigima/proc/signal/analysis.py +206 -0
- sigima/proc/signal/arithmetic.py +551 -0
- sigima/proc/signal/base.py +262 -0
- sigima/proc/signal/extraction.py +60 -0
- sigima/proc/signal/features.py +310 -0
- sigima/proc/signal/filtering.py +484 -0
- sigima/proc/signal/fitting.py +276 -0
- sigima/proc/signal/fourier.py +259 -0
- sigima/proc/signal/mathops.py +420 -0
- sigima/proc/signal/processing.py +580 -0
- sigima/proc/signal/stability.py +175 -0
- sigima/proc/title_formatting.py +227 -0
- sigima/proc/validation.py +272 -0
- sigima/tests/__init__.py +7 -0
- sigima/tests/common/__init__.py +0 -0
- sigima/tests/common/arithmeticparam_unit_test.py +26 -0
- sigima/tests/common/basename_unit_test.py +126 -0
- sigima/tests/common/client_unit_test.py +412 -0
- sigima/tests/common/converters_unit_test.py +77 -0
- sigima/tests/common/decorator_unit_test.py +176 -0
- sigima/tests/common/examples_unit_test.py +104 -0
- sigima/tests/common/kernel_normalization_unit_test.py +242 -0
- sigima/tests/common/roi_basic_unit_test.py +73 -0
- sigima/tests/common/roi_geometry_unit_test.py +171 -0
- sigima/tests/common/scalar_builder_unit_test.py +142 -0
- sigima/tests/common/scalar_unit_test.py +991 -0
- sigima/tests/common/shape_unit_test.py +183 -0
- sigima/tests/common/stat_unit_test.py +138 -0
- sigima/tests/common/title_formatting_unit_test.py +338 -0
- sigima/tests/common/tools_coordinates_unit_test.py +60 -0
- sigima/tests/common/transformations_unit_test.py +178 -0
- sigima/tests/common/validation_unit_test.py +205 -0
- sigima/tests/conftest.py +129 -0
- sigima/tests/data.py +998 -0
- sigima/tests/env.py +280 -0
- sigima/tests/guiutils.py +163 -0
- sigima/tests/helpers.py +532 -0
- sigima/tests/image/__init__.py +28 -0
- sigima/tests/image/binning_unit_test.py +128 -0
- sigima/tests/image/blob_detection_unit_test.py +312 -0
- sigima/tests/image/centroid_unit_test.py +170 -0
- sigima/tests/image/check_2d_array_unit_test.py +63 -0
- sigima/tests/image/contour_unit_test.py +172 -0
- sigima/tests/image/convolution_unit_test.py +178 -0
- sigima/tests/image/datatype_unit_test.py +67 -0
- sigima/tests/image/edges_unit_test.py +155 -0
- sigima/tests/image/enclosingcircle_unit_test.py +88 -0
- sigima/tests/image/exposure_unit_test.py +223 -0
- sigima/tests/image/fft2d_unit_test.py +189 -0
- sigima/tests/image/filtering_unit_test.py +166 -0
- sigima/tests/image/geometry_unit_test.py +654 -0
- sigima/tests/image/hough_circle_unit_test.py +147 -0
- sigima/tests/image/imageobj_unit_test.py +737 -0
- sigima/tests/image/morphology_unit_test.py +71 -0
- sigima/tests/image/noise_unit_test.py +57 -0
- sigima/tests/image/offset_correction_unit_test.py +72 -0
- sigima/tests/image/operation_unit_test.py +518 -0
- sigima/tests/image/peak2d_limits_unit_test.py +41 -0
- sigima/tests/image/peak2d_unit_test.py +133 -0
- sigima/tests/image/profile_unit_test.py +159 -0
- sigima/tests/image/projections_unit_test.py +121 -0
- sigima/tests/image/restoration_unit_test.py +141 -0
- sigima/tests/image/roi2dparam_unit_test.py +53 -0
- sigima/tests/image/roi_advanced_unit_test.py +588 -0
- sigima/tests/image/roi_grid_unit_test.py +279 -0
- sigima/tests/image/spectrum2d_unit_test.py +40 -0
- sigima/tests/image/threshold_unit_test.py +91 -0
- sigima/tests/io/__init__.py +0 -0
- sigima/tests/io/addnewformat_unit_test.py +125 -0
- sigima/tests/io/convenience_funcs_unit_test.py +470 -0
- sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
- sigima/tests/io/datetime_csv_unit_test.py +198 -0
- sigima/tests/io/imageio_formats_test.py +41 -0
- sigima/tests/io/ioregistry_unit_test.py +69 -0
- sigima/tests/io/objmeta_unit_test.py +87 -0
- sigima/tests/io/readobj_unit_test.py +130 -0
- sigima/tests/io/readwriteobj_unit_test.py +67 -0
- sigima/tests/signal/__init__.py +0 -0
- sigima/tests/signal/analysis_unit_test.py +135 -0
- sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
- sigima/tests/signal/convolution_unit_test.py +404 -0
- sigima/tests/signal/datetime_unit_test.py +176 -0
- sigima/tests/signal/fft1d_unit_test.py +303 -0
- sigima/tests/signal/filters_unit_test.py +403 -0
- sigima/tests/signal/fitting_unit_test.py +929 -0
- sigima/tests/signal/fwhm_unit_test.py +111 -0
- sigima/tests/signal/noise_unit_test.py +128 -0
- sigima/tests/signal/offset_correction_unit_test.py +34 -0
- sigima/tests/signal/operation_unit_test.py +489 -0
- sigima/tests/signal/peakdetection_unit_test.py +145 -0
- sigima/tests/signal/processing_unit_test.py +657 -0
- sigima/tests/signal/pulse/__init__.py +112 -0
- sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
- sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
- sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
- sigima/tests/signal/roi_advanced_unit_test.py +392 -0
- sigima/tests/signal/signalobj_unit_test.py +603 -0
- sigima/tests/signal/stability_unit_test.py +431 -0
- sigima/tests/signal/uncertainty_unit_test.py +611 -0
- sigima/tests/vistools.py +1030 -0
- sigima/tools/__init__.py +59 -0
- sigima/tools/checks.py +290 -0
- sigima/tools/coordinates.py +308 -0
- sigima/tools/datatypes.py +26 -0
- sigima/tools/image/__init__.py +97 -0
- sigima/tools/image/detection.py +451 -0
- sigima/tools/image/exposure.py +77 -0
- sigima/tools/image/extraction.py +48 -0
- sigima/tools/image/fourier.py +260 -0
- sigima/tools/image/geometry.py +190 -0
- sigima/tools/image/preprocessing.py +165 -0
- sigima/tools/signal/__init__.py +86 -0
- sigima/tools/signal/dynamic.py +254 -0
- sigima/tools/signal/features.py +135 -0
- sigima/tools/signal/filtering.py +171 -0
- sigima/tools/signal/fitting.py +1171 -0
- sigima/tools/signal/fourier.py +466 -0
- sigima/tools/signal/interpolation.py +70 -0
- sigima/tools/signal/peakdetection.py +126 -0
- sigima/tools/signal/pulse.py +1626 -0
- sigima/tools/signal/scaling.py +50 -0
- sigima/tools/signal/stability.py +258 -0
- sigima/tools/signal/windowing.py +90 -0
- sigima/worker.py +79 -0
- sigima-1.0.0.dist-info/METADATA +233 -0
- sigima-1.0.0.dist-info/RECORD +262 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
- sigima-0.0.1.dev0.dist-info/METADATA +0 -60
- sigima-0.0.1.dev0.dist-info/RECORD +0 -6
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Unit tests for exposure computation functions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
from skimage import exposure
|
|
12
|
+
|
|
13
|
+
import sigima.enums
|
|
14
|
+
import sigima.objects
|
|
15
|
+
import sigima.params
|
|
16
|
+
import sigima.proc.image
|
|
17
|
+
from sigima.tests.data import get_test_image
|
|
18
|
+
from sigima.tests.helpers import check_array_result, check_scalar_result
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.validation
|
|
22
|
+
def test_adjust_gamma() -> None:
|
|
23
|
+
"""Validation test for the image gamma adjustment processing."""
|
|
24
|
+
# See [1] in sigima\tests\image\__init__.py for more details about the validation.
|
|
25
|
+
src = get_test_image("flower.npy")
|
|
26
|
+
for gamma, gain in ((0.5, 1.0), (1.0, 2.0), (1.5, 0.5)):
|
|
27
|
+
p = sigima.params.AdjustGammaParam.create(gamma=gamma, gain=gain)
|
|
28
|
+
dst = sigima.proc.image.adjust_gamma(src, p)
|
|
29
|
+
exp = exposure.adjust_gamma(src.data, gamma=gamma, gain=gain)
|
|
30
|
+
check_array_result(f"AdjustGamma[gamma={gamma},gain={gain}]", dst.data, exp)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.validation
|
|
34
|
+
def test_adjust_log() -> None:
|
|
35
|
+
"""Validation test for the image logarithmic adjustment processing."""
|
|
36
|
+
# See [1] in sigima\tests\image\__init__.py for more details about the validation.
|
|
37
|
+
src = get_test_image("flower.npy")
|
|
38
|
+
for gain, inv in ((1.0, False), (2.0, True)):
|
|
39
|
+
p = sigima.params.AdjustLogParam.create(gain=gain, inv=inv)
|
|
40
|
+
dst = sigima.proc.image.adjust_log(src, p)
|
|
41
|
+
exp = exposure.adjust_log(src.data, gain=gain, inv=inv)
|
|
42
|
+
check_array_result(f"AdjustLog[gain={gain},inv={inv}]", dst.data, exp)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.validation
|
|
46
|
+
def test_adjust_sigmoid() -> None:
|
|
47
|
+
"""Validation test for the image sigmoid adjustment processing."""
|
|
48
|
+
# See [1] in sigima\tests\image\__init__.py for more details about the validation.
|
|
49
|
+
src = get_test_image("flower.npy")
|
|
50
|
+
for cutoff, gain, inv in ((0.5, 1.0, False), (0.25, 2.0, True)):
|
|
51
|
+
p = sigima.params.AdjustSigmoidParam.create(cutoff=cutoff, gain=gain, inv=inv)
|
|
52
|
+
dst = sigima.proc.image.adjust_sigmoid(src, p)
|
|
53
|
+
exp = exposure.adjust_sigmoid(src.data, cutoff=cutoff, gain=gain, inv=inv)
|
|
54
|
+
check_array_result(
|
|
55
|
+
f"AdjustSigmoid[cutoff={cutoff},gain={gain},inv={inv}]", dst.data, exp
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.validation
|
|
60
|
+
def test_rescale_intensity() -> None:
|
|
61
|
+
"""Validation test for the image intensity rescaling processing."""
|
|
62
|
+
# See [1] in sigima\tests\image\__init__.py for more details about the validation.
|
|
63
|
+
src = get_test_image("flower.npy")
|
|
64
|
+
p = sigima.params.RescaleIntensityParam.create(in_range="dtype", out_range="image")
|
|
65
|
+
dst = sigima.proc.image.rescale_intensity(src, p)
|
|
66
|
+
exp = exposure.rescale_intensity(
|
|
67
|
+
src.data, in_range=p.in_range, out_range=p.out_range
|
|
68
|
+
)
|
|
69
|
+
check_array_result("RescaleIntensity", dst.data, exp)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.mark.validation
|
|
73
|
+
def test_equalize_hist() -> None:
|
|
74
|
+
"""Validation test for the image histogram equalization processing."""
|
|
75
|
+
# See [1] in sigima\tests\image\__init__.py for more details about the validation.
|
|
76
|
+
src = get_test_image("flower.npy")
|
|
77
|
+
for nbins in (256, 512):
|
|
78
|
+
p = sigima.params.EqualizeHistParam.create(nbins=nbins)
|
|
79
|
+
dst = sigima.proc.image.equalize_hist(src, p)
|
|
80
|
+
exp = exposure.equalize_hist(src.data, nbins=nbins)
|
|
81
|
+
check_array_result(f"EqualizeHist[nbins={nbins}]", dst.data, exp)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.validation
|
|
85
|
+
def test_equalize_adapthist() -> None:
|
|
86
|
+
"""Validation test for the image adaptive histogram equalization processing."""
|
|
87
|
+
# See [1] in sigima\tests\image\__init__.py for more details about the validation.
|
|
88
|
+
src = get_test_image("flower.npy")
|
|
89
|
+
for clip_limit in (0.01, 0.1):
|
|
90
|
+
p = sigima.params.EqualizeAdaptHistParam.create(clip_limit=clip_limit)
|
|
91
|
+
dst = sigima.proc.image.equalize_adapthist(src, p)
|
|
92
|
+
exp = exposure.equalize_adapthist(src.data, clip_limit=clip_limit)
|
|
93
|
+
check_array_result(f"AdaptiveHist[clip_limit={clip_limit}]", dst.data, exp)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.mark.validation
|
|
97
|
+
def test_flatfield() -> None:
|
|
98
|
+
"""Validation test for the image flat-field correction processing."""
|
|
99
|
+
# See [1] in sigima\tests\image\__init__.py for more details about the validation.
|
|
100
|
+
src1 = get_test_image("flower.npy") # Raw data
|
|
101
|
+
src2 = get_test_image("flower.npy") # Flat field data (using same image as base)
|
|
102
|
+
|
|
103
|
+
# Modify flat field data to create realistic flat field variation
|
|
104
|
+
src2.data = src2.data.astype(float)
|
|
105
|
+
src2.data = src2.data / np.max(src2.data) * 100 + 50 # Scale to reasonable range
|
|
106
|
+
|
|
107
|
+
for threshold in (0.0, 10.0, 30.0):
|
|
108
|
+
p = sigima.params.FlatFieldParam.create(threshold=threshold)
|
|
109
|
+
dst = sigima.proc.image.flatfield(src1, src2, p)
|
|
110
|
+
|
|
111
|
+
# Compute expected result using the same algorithm as in sigima.tools.image
|
|
112
|
+
dtemp = np.array(src1.data, dtype=float, copy=True) * np.nanmean(src2.data)
|
|
113
|
+
dunif = np.array(src2.data, dtype=float, copy=True)
|
|
114
|
+
dunif[dunif == 0] = 1.0
|
|
115
|
+
dcorr_all = np.array(dtemp / dunif, dtype=src1.data.dtype)
|
|
116
|
+
exp = np.array(src1.data, copy=True)
|
|
117
|
+
exp[src1.data > threshold] = dcorr_all[src1.data > threshold]
|
|
118
|
+
|
|
119
|
+
check_array_result(f"FlatField[threshold={threshold}]", dst.data, exp)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.validation
|
|
123
|
+
def test_image_normalize() -> None:
|
|
124
|
+
"""Validation test for the image normalization processing."""
|
|
125
|
+
src = get_test_image("flower.npy")
|
|
126
|
+
src.data = np.array(src.data, dtype=float)
|
|
127
|
+
src.data[20:30, 20:30] = np.nan # Adding NaN values to the image
|
|
128
|
+
p = sigima.params.NormalizeParam()
|
|
129
|
+
|
|
130
|
+
# Given the fact that the normalization methods implementations are
|
|
131
|
+
# straightforward, we do not need to compare arrays with each other,
|
|
132
|
+
# we simply need to check if some properties are satisfied.
|
|
133
|
+
for method in sigima.enums.NormalizationMethod:
|
|
134
|
+
p.method = method
|
|
135
|
+
dst = sigima.proc.image.normalize(src, p)
|
|
136
|
+
title = f"Normalize[method='{p.method}']"
|
|
137
|
+
exp_min, exp_max = None, None
|
|
138
|
+
if p.method == sigima.enums.NormalizationMethod.MAXIMUM:
|
|
139
|
+
exp_min, exp_max = np.nanmin(src.data) / np.nanmax(src.data), 1.0
|
|
140
|
+
elif p.method == sigima.enums.NormalizationMethod.AMPLITUDE:
|
|
141
|
+
exp_min, exp_max = 0.0, 1.0
|
|
142
|
+
elif p.method == sigima.enums.NormalizationMethod.AREA:
|
|
143
|
+
area = np.nansum(src.data)
|
|
144
|
+
exp_min, exp_max = np.nanmin(src.data) / area, np.nanmax(src.data) / area
|
|
145
|
+
elif p.method == sigima.enums.NormalizationMethod.ENERGY:
|
|
146
|
+
energy = np.sqrt(np.nansum(np.abs(src.data) ** 2))
|
|
147
|
+
exp_min, exp_max = (
|
|
148
|
+
np.nanmin(src.data) / energy,
|
|
149
|
+
np.nanmax(src.data) / energy,
|
|
150
|
+
)
|
|
151
|
+
elif p.method == sigima.enums.NormalizationMethod.RMS:
|
|
152
|
+
rms = np.sqrt(np.nanmean(np.abs(src.data) ** 2))
|
|
153
|
+
exp_min, exp_max = np.nanmin(src.data) / rms, np.nanmax(src.data) / rms
|
|
154
|
+
check_scalar_result(f"{title}|min", np.nanmin(dst.data), exp_min)
|
|
155
|
+
check_scalar_result(f"{title}|max", np.nanmax(dst.data), exp_max)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.mark.validation
|
|
159
|
+
def test_image_clip() -> None:
|
|
160
|
+
"""Validation test for the image clipping processing."""
|
|
161
|
+
src = get_test_image("flower.npy")
|
|
162
|
+
p = sigima.params.ClipParam()
|
|
163
|
+
|
|
164
|
+
for lower, upper in ((float("-inf"), float("inf")), (50, 100)):
|
|
165
|
+
p.lower, p.upper = lower, upper
|
|
166
|
+
dst = sigima.proc.image.clip(src, p)
|
|
167
|
+
exp = np.clip(src.data, p.lower, p.upper)
|
|
168
|
+
check_array_result(f"Clip[{lower},{upper}]", dst.data, exp)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.validation
|
|
172
|
+
def test_image_histogram() -> None:
|
|
173
|
+
"""Validation test for the image histogram computation function."""
|
|
174
|
+
src = get_test_image("flower.npy")
|
|
175
|
+
for bins in (128, 256, 512):
|
|
176
|
+
for lower, upper in ((None, None), (50.0, 200.0)):
|
|
177
|
+
p = sigima.params.HistogramParam.create(bins=bins, lower=lower, upper=upper)
|
|
178
|
+
dst = sigima.proc.image.histogram(src, p)
|
|
179
|
+
|
|
180
|
+
# Get the actual data used for histogram computation
|
|
181
|
+
data = src.get_masked_view().compressed()
|
|
182
|
+
|
|
183
|
+
# Determine the range for numpy.histogram
|
|
184
|
+
hist_range = (p.lower, p.upper)
|
|
185
|
+
if p.lower is None:
|
|
186
|
+
hist_range = (np.min(data), hist_range[1])
|
|
187
|
+
if p.upper is None:
|
|
188
|
+
hist_range = (hist_range[0], np.max(data))
|
|
189
|
+
|
|
190
|
+
# Compute expected histogram using numpy.histogram
|
|
191
|
+
exp_y, bin_edges = np.histogram(data, bins=p.bins, range=hist_range)
|
|
192
|
+
exp_x = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
193
|
+
|
|
194
|
+
title = f"Histogram[bins={bins},lower={lower},upper={upper}]"
|
|
195
|
+
check_array_result(f"{title}|x", dst.x, exp_x)
|
|
196
|
+
check_array_result(f"{title}|y", dst.y, np.array(exp_y, dtype=float))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.validation
|
|
200
|
+
def test_image_offset_correction() -> None:
|
|
201
|
+
"""Validation test for the image offset correction processing."""
|
|
202
|
+
src = get_test_image("flower.npy")
|
|
203
|
+
# Defining the ROI that will be used to estimate the offset
|
|
204
|
+
p = sigima.objects.ROI2DParam.create(x0=0, y0=0, dx=50, dy=20)
|
|
205
|
+
dst = sigima.proc.image.offset_correction(src, p)
|
|
206
|
+
ix0, iy0 = int(p.x0), int(p.y0)
|
|
207
|
+
ix1, iy1 = int(p.x0 + p.dx), int(p.y0 + p.dy)
|
|
208
|
+
exp = src.data - np.mean(src.data[iy0:iy1, ix0:ix1])
|
|
209
|
+
check_array_result("OffsetCorrection", dst.data, exp)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
test_adjust_gamma()
|
|
214
|
+
test_adjust_log()
|
|
215
|
+
test_adjust_sigmoid()
|
|
216
|
+
test_rescale_intensity()
|
|
217
|
+
test_equalize_hist()
|
|
218
|
+
test_equalize_adapthist()
|
|
219
|
+
test_flatfield()
|
|
220
|
+
test_image_normalize()
|
|
221
|
+
test_image_clip()
|
|
222
|
+
test_image_histogram()
|
|
223
|
+
test_image_offset_correction()
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Image FFT unit test.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
8
|
+
# pylint: disable=duplicate-code
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
import sigima.objects
|
|
14
|
+
import sigima.params
|
|
15
|
+
import sigima.proc.image
|
|
16
|
+
import sigima.tests.data
|
|
17
|
+
import sigima.tools.image
|
|
18
|
+
from sigima.tests import guiutils
|
|
19
|
+
from sigima.tests.env import execenv
|
|
20
|
+
from sigima.tests.helpers import check_array_result, check_scalar_result
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.gui
|
|
24
|
+
def test_image_fft_interactive():
|
|
25
|
+
"""2D FFT interactive test."""
|
|
26
|
+
with guiutils.lazy_qt_app_context(force=True):
|
|
27
|
+
from sigima.tests import vistools # pylint: disable=import-outside-toplevel
|
|
28
|
+
|
|
29
|
+
# Create a 2D ring image
|
|
30
|
+
execenv.print("Generating 2D ring image...", end=" ")
|
|
31
|
+
data = sigima.tests.data.create_ring_image().data
|
|
32
|
+
execenv.print("OK")
|
|
33
|
+
|
|
34
|
+
# FFT
|
|
35
|
+
execenv.print("Computing FFT of image...", end=" ")
|
|
36
|
+
f = sigima.tools.image.fft2d(data)
|
|
37
|
+
data2 = sigima.tools.image.ifft2d(f)
|
|
38
|
+
execenv.print("OK")
|
|
39
|
+
execenv.print("Comparing original and FFT/iFFT images...", end=" ")
|
|
40
|
+
check_array_result(
|
|
41
|
+
"Image FFT/iFFT", np.array(data2.real, dtype=data.dtype), data, rtol=1e-3
|
|
42
|
+
)
|
|
43
|
+
execenv.print("OK")
|
|
44
|
+
|
|
45
|
+
images = [data, f.real, f.imag, np.abs(f), data2.real, data2.imag]
|
|
46
|
+
titles = ["Original", "Re(FFT)", "Im(FFT)", "Abs(FFT)", "Re(iFFT)", "Im(iFFT)"]
|
|
47
|
+
vistools.view_images_side_by_side(images, titles, rows=2, title="2D FFT/iFFT")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.validation
|
|
51
|
+
def test_image_zero_padding() -> None:
|
|
52
|
+
"""2D FFT zero padding validation test."""
|
|
53
|
+
ima1 = sigima.tests.data.create_checkerboard()
|
|
54
|
+
rows, cols = 2, 2
|
|
55
|
+
param = sigima.params.ZeroPadding2DParam.create(rows=rows, cols=cols)
|
|
56
|
+
assert param.strategy == "custom", (
|
|
57
|
+
f"Wrong default strategy: {param.strategy} (expected 'custom')"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Validate the zero padding with bottom-right position
|
|
61
|
+
param.position = "bottom-right"
|
|
62
|
+
ima2 = sigima.proc.image.zero_padding(ima1, param)
|
|
63
|
+
sh1, sh2 = ima1.data.shape, ima2.data.shape
|
|
64
|
+
exp_sh2 = (sh1[0] + rows, sh1[1] + cols)
|
|
65
|
+
execenv.print("Validating zero padding for bottom-right position...", end=" ")
|
|
66
|
+
assert sh2 == exp_sh2, f"Wrong shape: {sh2} (expected {exp_sh2})"
|
|
67
|
+
assert np.all(ima2.data[0 : sh1[0], 0 : sh1[1]] == ima1.data), (
|
|
68
|
+
"Altered data in original image area"
|
|
69
|
+
)
|
|
70
|
+
assert np.all(ima2.data[sh1[0] : sh2[0], sh1[1] : sh2[1]] == 0), (
|
|
71
|
+
"Altered data in padded area"
|
|
72
|
+
)
|
|
73
|
+
execenv.print("OK")
|
|
74
|
+
|
|
75
|
+
# Validate the zero padding with center position
|
|
76
|
+
param.position = "around"
|
|
77
|
+
ima3 = sigima.proc.image.zero_padding(ima1, param)
|
|
78
|
+
sh3 = ima3.data.shape
|
|
79
|
+
exp_sh3 = (sh1[0] + rows, sh1[1] + cols)
|
|
80
|
+
execenv.print("Validating zero padding for around position...", end=" ")
|
|
81
|
+
assert sh3 == exp_sh3, f"Wrong shape: {sh3} (expected {exp_sh3})"
|
|
82
|
+
assert np.all(
|
|
83
|
+
ima3.data[rows // 2 : sh1[0] + rows // 2, cols // 2 : sh1[1] + cols // 2]
|
|
84
|
+
== ima1.data
|
|
85
|
+
), "Altered data in original image area"
|
|
86
|
+
assert np.all(ima3.data[0 : rows // 2, :] == 0), "Altered data in padded area (top)"
|
|
87
|
+
assert np.all(ima3.data[sh1[0] + rows // 2 :, :] == 0), (
|
|
88
|
+
"Altered data in padded area (bottom)"
|
|
89
|
+
)
|
|
90
|
+
assert np.all(ima3.data[:, 0 : cols // 2] == 0), (
|
|
91
|
+
"Altered data in padded area (left)"
|
|
92
|
+
)
|
|
93
|
+
assert np.all(ima3.data[:, sh1[1] + cols // 2 :] == 0), (
|
|
94
|
+
"Altered data in padded area (right)"
|
|
95
|
+
)
|
|
96
|
+
execenv.print("OK")
|
|
97
|
+
|
|
98
|
+
# Validate zero padding with strategies other than custom size
|
|
99
|
+
# Image size is (200, 300) and the next power of 2 is (256, 512)
|
|
100
|
+
# The multiple of 64 is (256, 320)
|
|
101
|
+
ima4 = sigima.objects.create_image("", np.zeros((200, 300)))
|
|
102
|
+
for strategy, (exp_rows, exp_cols) in (
|
|
103
|
+
("next_pow2", (56, 212)),
|
|
104
|
+
("multiple_of_64", (56, 20)),
|
|
105
|
+
):
|
|
106
|
+
param = sigima.params.ZeroPadding2DParam.create(strategy=strategy)
|
|
107
|
+
param.update_from_obj(ima4)
|
|
108
|
+
assert param.rows == exp_rows, (
|
|
109
|
+
f"Wrong row number for '{param.strategy}' strategy: {param.rows}"
|
|
110
|
+
f" (expected {exp_rows})"
|
|
111
|
+
)
|
|
112
|
+
assert param.cols == exp_cols, (
|
|
113
|
+
f"Wrong column number for '{param.strategy}' strategy: {param.cols}"
|
|
114
|
+
f" (expected {exp_cols})"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.validation
|
|
119
|
+
def test_image_fft() -> None:
|
|
120
|
+
"""2D FFT validation test."""
|
|
121
|
+
ima1 = sigima.tests.data.create_checkerboard()
|
|
122
|
+
fft = sigima.proc.image.fft(ima1)
|
|
123
|
+
ifft = sigima.proc.image.ifft(fft)
|
|
124
|
+
|
|
125
|
+
# Check that the inverse FFT reconstructs the original image
|
|
126
|
+
check_array_result("Checkerboard image FFT/iFFT", ifft.data.real, ima1.data)
|
|
127
|
+
|
|
128
|
+
# Parseval's Theorem Validation
|
|
129
|
+
original_energy = np.sum(np.abs(ima1.data) ** 2)
|
|
130
|
+
transformed_energy = np.sum(np.abs(fft.data) ** 2) / (ima1.data.size)
|
|
131
|
+
check_scalar_result("Parseval's Theorem", transformed_energy, original_energy)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.mark.skip(reason="Already covered by the `test_image_fft` test.")
|
|
135
|
+
@pytest.mark.validation
|
|
136
|
+
def test_image_ifft() -> None:
|
|
137
|
+
"""2D iFFT validation test."""
|
|
138
|
+
# This is just a way of marking the iFFT test as a validation test because it is
|
|
139
|
+
# already covered by the FFT test above (there is no need to repeat the same test).
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.mark.validation
|
|
143
|
+
def test_image_magnitude_spectrum() -> None:
|
|
144
|
+
"""2D magnitude spectrum validation test."""
|
|
145
|
+
ima1 = sigima.tests.data.create_checkerboard()
|
|
146
|
+
fft = sigima.proc.image.fft(ima1)
|
|
147
|
+
param = sigima.params.SpectrumParam()
|
|
148
|
+
for decibel in (True, False):
|
|
149
|
+
param.decibel = decibel
|
|
150
|
+
mag = sigima.proc.image.magnitude_spectrum(ima1, param)
|
|
151
|
+
|
|
152
|
+
# Check that the magnitude spectrum is correct
|
|
153
|
+
exp = np.abs(fft.data)
|
|
154
|
+
check_array_result("Checkerboard image FFT magnitude spectrum", mag.data, exp)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.validation
|
|
158
|
+
def test_image_phase_spectrum() -> None:
|
|
159
|
+
"""2D phase spectrum validation test."""
|
|
160
|
+
ima1 = sigima.tests.data.create_checkerboard()
|
|
161
|
+
fft = sigima.proc.image.fft(ima1)
|
|
162
|
+
phase = sigima.proc.image.phase_spectrum(ima1)
|
|
163
|
+
|
|
164
|
+
# Check that the phase spectrum is correct
|
|
165
|
+
exp = np.rad2deg(np.angle(fft.data))
|
|
166
|
+
check_array_result("Checkerboard image FFT phase spectrum", phase.data, exp)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.validation
|
|
170
|
+
def test_image_psd() -> None:
|
|
171
|
+
"""2D Power Spectral Density validation test."""
|
|
172
|
+
ima1 = sigima.tests.data.create_checkerboard()
|
|
173
|
+
param = sigima.params.SpectrumParam()
|
|
174
|
+
for decibel in (True, False):
|
|
175
|
+
param.decibel = decibel
|
|
176
|
+
psd = sigima.proc.image.psd(ima1, param)
|
|
177
|
+
|
|
178
|
+
# Check that the PSD is correct
|
|
179
|
+
exp = np.abs(sigima.proc.image.fft(ima1).data) ** 2
|
|
180
|
+
check_array_result("Checkerboard image PSD", psd.data, exp)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
test_image_fft_interactive()
|
|
185
|
+
test_image_zero_padding()
|
|
186
|
+
test_image_fft()
|
|
187
|
+
test_image_magnitude_spectrum()
|
|
188
|
+
test_image_phase_spectrum()
|
|
189
|
+
test_image_psd()
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Unit tests for image filtering functions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
import scipy.ndimage as spi
|
|
12
|
+
import scipy.signal as sps
|
|
13
|
+
from skimage import filters
|
|
14
|
+
|
|
15
|
+
import sigima.enums
|
|
16
|
+
import sigima.params
|
|
17
|
+
import sigima.proc.image
|
|
18
|
+
import sigima.tools.image
|
|
19
|
+
from sigima.objects import ImageObj
|
|
20
|
+
from sigima.objects.image import create_image
|
|
21
|
+
from sigima.tests import guiutils
|
|
22
|
+
from sigima.tests.data import get_test_image
|
|
23
|
+
from sigima.tests.helpers import check_array_result, check_scalar_result
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.validation
|
|
27
|
+
def test_image_gaussian_filter() -> None:
|
|
28
|
+
"""Validation test for the image Gaussian filter processing."""
|
|
29
|
+
src = get_test_image("flower.npy")
|
|
30
|
+
for sigma in (10.0, 50.0):
|
|
31
|
+
p = sigima.params.GaussianParam.create(sigma=sigma)
|
|
32
|
+
dst = sigima.proc.image.gaussian_filter(src, p)
|
|
33
|
+
exp = spi.gaussian_filter(src.data, sigma=sigma)
|
|
34
|
+
check_array_result(f"GaussianFilter[sigma={sigma}]", dst.data, exp)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.validation
|
|
38
|
+
def test_image_moving_average() -> None:
|
|
39
|
+
"""Validation test for the image moving average processing."""
|
|
40
|
+
src = get_test_image("flower.npy")
|
|
41
|
+
p = sigima.params.MovingAverageParam.create(n=30)
|
|
42
|
+
for mode in sigima.enums.FilterMode:
|
|
43
|
+
p.mode = mode
|
|
44
|
+
dst = sigima.proc.image.moving_average(src, p)
|
|
45
|
+
exp = spi.uniform_filter(src.data, size=p.n, mode=mode.value)
|
|
46
|
+
check_array_result(f"MovingAvg[n={p.n},mode={p.mode}]", dst.data, exp)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.validation
|
|
50
|
+
def test_image_moving_median() -> None:
|
|
51
|
+
"""Validation test for the image moving median processing."""
|
|
52
|
+
src = get_test_image("flower.npy")
|
|
53
|
+
p = sigima.params.MovingMedianParam.create(n=5)
|
|
54
|
+
for mode in sigima.enums.FilterMode:
|
|
55
|
+
p.mode = mode
|
|
56
|
+
dst = sigima.proc.image.moving_median(src, p)
|
|
57
|
+
exp = spi.median_filter(src.data, size=p.n, mode=mode.value)
|
|
58
|
+
check_array_result(f"MovingMed[n={p.n},mode={p.mode}]", dst.data, exp)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.validation
|
|
62
|
+
def test_image_wiener() -> None:
|
|
63
|
+
"""Validation test for the image Wiener filter processing."""
|
|
64
|
+
src = get_test_image("flower.npy")
|
|
65
|
+
dst = sigima.proc.image.wiener(src)
|
|
66
|
+
exp = sps.wiener(src.data)
|
|
67
|
+
check_array_result("Wiener", dst.data, exp)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.validation
|
|
71
|
+
def test_butterworth() -> None:
|
|
72
|
+
"""Validation test for the image Butterworth filter processing."""
|
|
73
|
+
src = get_test_image("flower.npy")
|
|
74
|
+
p = sigima.params.ButterworthParam.create(order=2, cut_off=0.5, high_pass=False)
|
|
75
|
+
dst = sigima.proc.image.butterworth(src, p)
|
|
76
|
+
exp = filters.butterworth(src.data, p.cut_off, p.high_pass, p.order)
|
|
77
|
+
check_array_result(
|
|
78
|
+
f"Butterworth[order={p.order},cut_off={p.cut_off},high_pass={p.high_pass}]",
|
|
79
|
+
dst.data,
|
|
80
|
+
exp,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_clean_noisy_images(
|
|
85
|
+
shape: tuple[int, int] = (64, 64), noise_level: float = 0.4, freq: float = 0.05
|
|
86
|
+
) -> tuple[ImageObj, ImageObj]:
|
|
87
|
+
"""Generate a test image with a low-frequency signal and high-frequency noise.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
shape: Shape of the image (height, width).
|
|
91
|
+
noise_level: Standard deviation of the Gaussian noise.
|
|
92
|
+
freq: Frequency of the low-frequency signal.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Tuple of (clean, noisy) where:
|
|
96
|
+
- clean: The low-frequency component of the image.
|
|
97
|
+
- noisy: The noisy image with added Gaussian noise.
|
|
98
|
+
"""
|
|
99
|
+
# Low frequency signal: sinusoid with frequency in px^-1
|
|
100
|
+
x = np.arange(shape[1])
|
|
101
|
+
y = np.arange(shape[0])
|
|
102
|
+
x_matrice, y_matrice = np.meshgrid(x, y)
|
|
103
|
+
low_freq = 0.5 * (np.sin(2 * np.pi * freq * x_matrice)) + 0.5 * (
|
|
104
|
+
np.sin(2 * np.pi * freq * y_matrice)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# High frequency noise
|
|
108
|
+
rng = np.random.default_rng(seed=0)
|
|
109
|
+
high_freq_noise = noise_level * rng.standard_normal(size=shape)
|
|
110
|
+
|
|
111
|
+
# Sum of the two components
|
|
112
|
+
img = low_freq + high_freq_noise
|
|
113
|
+
return create_image("clean", low_freq), create_image("noisy", img)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@pytest.mark.validation
|
|
117
|
+
def test_gaussian_freq_filter() -> None:
|
|
118
|
+
"""Validation test for :py:func:`sigima.tools.image.gaussian_freq_filter`."""
|
|
119
|
+
clean, noisy = build_clean_noisy_images(freq=0.05)
|
|
120
|
+
param = sigima.proc.image.GaussianFreqFilterParam.create(f0=0.05, sigma=0.05)
|
|
121
|
+
filt = sigima.proc.image.gaussian_freq_filter(noisy, param)
|
|
122
|
+
clean_area = clean.data[10:-10, 10:-10]
|
|
123
|
+
guiutils.view_images_side_by_side_if_gui(
|
|
124
|
+
[clean, noisy, filt], titles=["Clean", "Noisy", "Filtered"]
|
|
125
|
+
)
|
|
126
|
+
mean_noise = float(np.mean(np.abs(clean_area - filt.data[10:-10, 10:-10])))
|
|
127
|
+
check_scalar_result(
|
|
128
|
+
"gaussian_freq_filter noise reduction", mean_noise, 0.0, atol=0.1
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_gaussian_freq_filter_constant_image() -> None:
|
|
133
|
+
"""Edge case: filtering a constant image must preserve the constant value
|
|
134
|
+
(DC component)."""
|
|
135
|
+
img_const = np.full((64, 64), fill_value=7.42)
|
|
136
|
+
zout = sigima.tools.image.gaussian_freq_filter(img_const, f0=0.0, sigma=0.05)
|
|
137
|
+
# Ignore borders
|
|
138
|
+
center = zout[10:-10, 10:-10]
|
|
139
|
+
# Assert that all values are (almost) equal to the original constant
|
|
140
|
+
assert np.allclose(center, 7.42, atol=1e-10), "Filtering constant image failed"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_gaussian_freq_filter_symmetry() -> None:
|
|
144
|
+
"""Test: filtering a symmetric image yields a symmetric result."""
|
|
145
|
+
# Create a symmetric image (e.g., a centered 2D Gaussian)
|
|
146
|
+
x = np.linspace(-1, 1, 64)
|
|
147
|
+
y = np.linspace(-1, 1, 64)
|
|
148
|
+
xv, yv = np.meshgrid(x, y)
|
|
149
|
+
img = np.exp(-(xv**2 + yv**2) / 0.1)
|
|
150
|
+
|
|
151
|
+
zout = sigima.tools.image.gaussian_freq_filter(img, f0=0.05, sigma=0.02)
|
|
152
|
+
# Symmetry check: image must be (almost) symmetric along both axes
|
|
153
|
+
assert np.allclose(zout, zout[::-1, :], atol=1e-10), "Vertical symmetry lost"
|
|
154
|
+
assert np.allclose(zout, zout[:, ::-1], atol=1e-10), "Horizontal symmetry lost"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
guiutils.enable_gui()
|
|
159
|
+
test_image_gaussian_filter()
|
|
160
|
+
test_image_moving_average()
|
|
161
|
+
test_image_moving_median()
|
|
162
|
+
test_image_wiener()
|
|
163
|
+
test_butterworth()
|
|
164
|
+
test_gaussian_freq_filter()
|
|
165
|
+
test_gaussian_freq_filter_constant_image()
|
|
166
|
+
test_gaussian_freq_filter_symmetry()
|