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.
Files changed (264) hide show
  1. sigima/__init__.py +142 -2
  2. sigima/client/__init__.py +105 -0
  3. sigima/client/base.py +780 -0
  4. sigima/client/remote.py +469 -0
  5. sigima/client/stub.py +814 -0
  6. sigima/client/utils.py +90 -0
  7. sigima/config.py +444 -0
  8. sigima/data/logo/Sigima.svg +135 -0
  9. sigima/data/tests/annotations.json +798 -0
  10. sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
  11. sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
  12. sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
  13. sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
  14. sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
  15. sigima/data/tests/curve_formats/bandwidth.txt +201 -0
  16. sigima/data/tests/curve_formats/boxcar.npy +0 -0
  17. sigima/data/tests/curve_formats/datetime.txt +1001 -0
  18. sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
  19. sigima/data/tests/curve_formats/fw1e2.txt +301 -0
  20. sigima/data/tests/curve_formats/fwhm.txt +319 -0
  21. sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
  22. sigima/data/tests/curve_formats/noised_saw.mat +0 -0
  23. sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
  24. sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
  25. sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
  26. sigima/data/tests/curve_formats/paracetamol.npy +0 -0
  27. sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
  28. sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
  29. sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
  30. sigima/data/tests/curve_formats/pulse1.npy +0 -0
  31. sigima/data/tests/curve_formats/pulse2.npy +0 -0
  32. sigima/data/tests/curve_formats/simple.txt +5 -0
  33. sigima/data/tests/curve_formats/spectrum.mca +2139 -0
  34. sigima/data/tests/curve_formats/square2.npy +0 -0
  35. sigima/data/tests/curve_formats/step.npy +0 -0
  36. sigima/data/tests/fabry-perot1.jpg +0 -0
  37. sigima/data/tests/fabry-perot2.jpg +0 -0
  38. sigima/data/tests/flower.npy +0 -0
  39. sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
  40. sigima/data/tests/image_formats/binary_image.npy +0 -0
  41. sigima/data/tests/image_formats/binary_image.png +0 -0
  42. sigima/data/tests/image_formats/centroid_test.npy +0 -0
  43. sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
  44. sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
  45. sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
  46. sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
  47. sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
  48. sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
  49. sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
  50. sigima/data/tests/image_formats/fiber.csv +480 -0
  51. sigima/data/tests/image_formats/fiber.jpg +0 -0
  52. sigima/data/tests/image_formats/fiber.png +0 -0
  53. sigima/data/tests/image_formats/fiber.txt +480 -0
  54. sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
  55. sigima/data/tests/image_formats/mr-brain.dcm +0 -0
  56. sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
  57. sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
  58. sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
  59. sigima/data/tests/image_formats/tiling.txt +10 -0
  60. sigima/data/tests/image_formats/uint16.tiff +0 -0
  61. sigima/data/tests/image_formats/uint8.tiff +0 -0
  62. sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
  63. sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
  64. sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
  65. sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
  66. sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
  67. sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
  68. sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
  69. sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
  70. sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
  71. sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
  72. sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
  73. sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
  74. sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
  75. sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
  76. sigima/enums.py +195 -0
  77. sigima/io/__init__.py +123 -0
  78. sigima/io/base.py +311 -0
  79. sigima/io/common/__init__.py +5 -0
  80. sigima/io/common/basename.py +164 -0
  81. sigima/io/common/converters.py +189 -0
  82. sigima/io/common/objmeta.py +181 -0
  83. sigima/io/common/textreader.py +58 -0
  84. sigima/io/convenience.py +157 -0
  85. sigima/io/enums.py +17 -0
  86. sigima/io/ftlab.py +395 -0
  87. sigima/io/image/__init__.py +9 -0
  88. sigima/io/image/base.py +177 -0
  89. sigima/io/image/formats.py +1016 -0
  90. sigima/io/image/funcs.py +414 -0
  91. sigima/io/signal/__init__.py +9 -0
  92. sigima/io/signal/base.py +129 -0
  93. sigima/io/signal/formats.py +290 -0
  94. sigima/io/signal/funcs.py +723 -0
  95. sigima/objects/__init__.py +260 -0
  96. sigima/objects/base.py +937 -0
  97. sigima/objects/image/__init__.py +88 -0
  98. sigima/objects/image/creation.py +556 -0
  99. sigima/objects/image/object.py +524 -0
  100. sigima/objects/image/roi.py +904 -0
  101. sigima/objects/scalar/__init__.py +57 -0
  102. sigima/objects/scalar/common.py +215 -0
  103. sigima/objects/scalar/geometry.py +502 -0
  104. sigima/objects/scalar/table.py +784 -0
  105. sigima/objects/shape.py +290 -0
  106. sigima/objects/signal/__init__.py +133 -0
  107. sigima/objects/signal/constants.py +27 -0
  108. sigima/objects/signal/creation.py +1428 -0
  109. sigima/objects/signal/object.py +444 -0
  110. sigima/objects/signal/roi.py +274 -0
  111. sigima/params.py +405 -0
  112. sigima/proc/__init__.py +96 -0
  113. sigima/proc/base.py +381 -0
  114. sigima/proc/decorator.py +330 -0
  115. sigima/proc/image/__init__.py +513 -0
  116. sigima/proc/image/arithmetic.py +335 -0
  117. sigima/proc/image/base.py +260 -0
  118. sigima/proc/image/detection.py +519 -0
  119. sigima/proc/image/edges.py +329 -0
  120. sigima/proc/image/exposure.py +406 -0
  121. sigima/proc/image/extraction.py +458 -0
  122. sigima/proc/image/filtering.py +219 -0
  123. sigima/proc/image/fourier.py +147 -0
  124. sigima/proc/image/geometry.py +661 -0
  125. sigima/proc/image/mathops.py +340 -0
  126. sigima/proc/image/measurement.py +195 -0
  127. sigima/proc/image/morphology.py +155 -0
  128. sigima/proc/image/noise.py +107 -0
  129. sigima/proc/image/preprocessing.py +182 -0
  130. sigima/proc/image/restoration.py +235 -0
  131. sigima/proc/image/threshold.py +217 -0
  132. sigima/proc/image/transformations.py +393 -0
  133. sigima/proc/signal/__init__.py +376 -0
  134. sigima/proc/signal/analysis.py +206 -0
  135. sigima/proc/signal/arithmetic.py +551 -0
  136. sigima/proc/signal/base.py +262 -0
  137. sigima/proc/signal/extraction.py +60 -0
  138. sigima/proc/signal/features.py +310 -0
  139. sigima/proc/signal/filtering.py +484 -0
  140. sigima/proc/signal/fitting.py +276 -0
  141. sigima/proc/signal/fourier.py +259 -0
  142. sigima/proc/signal/mathops.py +420 -0
  143. sigima/proc/signal/processing.py +580 -0
  144. sigima/proc/signal/stability.py +175 -0
  145. sigima/proc/title_formatting.py +227 -0
  146. sigima/proc/validation.py +272 -0
  147. sigima/tests/__init__.py +7 -0
  148. sigima/tests/common/__init__.py +0 -0
  149. sigima/tests/common/arithmeticparam_unit_test.py +26 -0
  150. sigima/tests/common/basename_unit_test.py +126 -0
  151. sigima/tests/common/client_unit_test.py +412 -0
  152. sigima/tests/common/converters_unit_test.py +77 -0
  153. sigima/tests/common/decorator_unit_test.py +176 -0
  154. sigima/tests/common/examples_unit_test.py +104 -0
  155. sigima/tests/common/kernel_normalization_unit_test.py +242 -0
  156. sigima/tests/common/roi_basic_unit_test.py +73 -0
  157. sigima/tests/common/roi_geometry_unit_test.py +171 -0
  158. sigima/tests/common/scalar_builder_unit_test.py +142 -0
  159. sigima/tests/common/scalar_unit_test.py +991 -0
  160. sigima/tests/common/shape_unit_test.py +183 -0
  161. sigima/tests/common/stat_unit_test.py +138 -0
  162. sigima/tests/common/title_formatting_unit_test.py +338 -0
  163. sigima/tests/common/tools_coordinates_unit_test.py +60 -0
  164. sigima/tests/common/transformations_unit_test.py +178 -0
  165. sigima/tests/common/validation_unit_test.py +205 -0
  166. sigima/tests/conftest.py +129 -0
  167. sigima/tests/data.py +998 -0
  168. sigima/tests/env.py +280 -0
  169. sigima/tests/guiutils.py +163 -0
  170. sigima/tests/helpers.py +532 -0
  171. sigima/tests/image/__init__.py +28 -0
  172. sigima/tests/image/binning_unit_test.py +128 -0
  173. sigima/tests/image/blob_detection_unit_test.py +312 -0
  174. sigima/tests/image/centroid_unit_test.py +170 -0
  175. sigima/tests/image/check_2d_array_unit_test.py +63 -0
  176. sigima/tests/image/contour_unit_test.py +172 -0
  177. sigima/tests/image/convolution_unit_test.py +178 -0
  178. sigima/tests/image/datatype_unit_test.py +67 -0
  179. sigima/tests/image/edges_unit_test.py +155 -0
  180. sigima/tests/image/enclosingcircle_unit_test.py +88 -0
  181. sigima/tests/image/exposure_unit_test.py +223 -0
  182. sigima/tests/image/fft2d_unit_test.py +189 -0
  183. sigima/tests/image/filtering_unit_test.py +166 -0
  184. sigima/tests/image/geometry_unit_test.py +654 -0
  185. sigima/tests/image/hough_circle_unit_test.py +147 -0
  186. sigima/tests/image/imageobj_unit_test.py +737 -0
  187. sigima/tests/image/morphology_unit_test.py +71 -0
  188. sigima/tests/image/noise_unit_test.py +57 -0
  189. sigima/tests/image/offset_correction_unit_test.py +72 -0
  190. sigima/tests/image/operation_unit_test.py +518 -0
  191. sigima/tests/image/peak2d_limits_unit_test.py +41 -0
  192. sigima/tests/image/peak2d_unit_test.py +133 -0
  193. sigima/tests/image/profile_unit_test.py +159 -0
  194. sigima/tests/image/projections_unit_test.py +121 -0
  195. sigima/tests/image/restoration_unit_test.py +141 -0
  196. sigima/tests/image/roi2dparam_unit_test.py +53 -0
  197. sigima/tests/image/roi_advanced_unit_test.py +588 -0
  198. sigima/tests/image/roi_grid_unit_test.py +279 -0
  199. sigima/tests/image/spectrum2d_unit_test.py +40 -0
  200. sigima/tests/image/threshold_unit_test.py +91 -0
  201. sigima/tests/io/__init__.py +0 -0
  202. sigima/tests/io/addnewformat_unit_test.py +125 -0
  203. sigima/tests/io/convenience_funcs_unit_test.py +470 -0
  204. sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
  205. sigima/tests/io/datetime_csv_unit_test.py +198 -0
  206. sigima/tests/io/imageio_formats_test.py +41 -0
  207. sigima/tests/io/ioregistry_unit_test.py +69 -0
  208. sigima/tests/io/objmeta_unit_test.py +87 -0
  209. sigima/tests/io/readobj_unit_test.py +130 -0
  210. sigima/tests/io/readwriteobj_unit_test.py +67 -0
  211. sigima/tests/signal/__init__.py +0 -0
  212. sigima/tests/signal/analysis_unit_test.py +135 -0
  213. sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
  214. sigima/tests/signal/convolution_unit_test.py +404 -0
  215. sigima/tests/signal/datetime_unit_test.py +176 -0
  216. sigima/tests/signal/fft1d_unit_test.py +303 -0
  217. sigima/tests/signal/filters_unit_test.py +403 -0
  218. sigima/tests/signal/fitting_unit_test.py +929 -0
  219. sigima/tests/signal/fwhm_unit_test.py +111 -0
  220. sigima/tests/signal/noise_unit_test.py +128 -0
  221. sigima/tests/signal/offset_correction_unit_test.py +34 -0
  222. sigima/tests/signal/operation_unit_test.py +489 -0
  223. sigima/tests/signal/peakdetection_unit_test.py +145 -0
  224. sigima/tests/signal/processing_unit_test.py +657 -0
  225. sigima/tests/signal/pulse/__init__.py +112 -0
  226. sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
  227. sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
  228. sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
  229. sigima/tests/signal/roi_advanced_unit_test.py +392 -0
  230. sigima/tests/signal/signalobj_unit_test.py +603 -0
  231. sigima/tests/signal/stability_unit_test.py +431 -0
  232. sigima/tests/signal/uncertainty_unit_test.py +611 -0
  233. sigima/tests/vistools.py +1030 -0
  234. sigima/tools/__init__.py +59 -0
  235. sigima/tools/checks.py +290 -0
  236. sigima/tools/coordinates.py +308 -0
  237. sigima/tools/datatypes.py +26 -0
  238. sigima/tools/image/__init__.py +97 -0
  239. sigima/tools/image/detection.py +451 -0
  240. sigima/tools/image/exposure.py +77 -0
  241. sigima/tools/image/extraction.py +48 -0
  242. sigima/tools/image/fourier.py +260 -0
  243. sigima/tools/image/geometry.py +190 -0
  244. sigima/tools/image/preprocessing.py +165 -0
  245. sigima/tools/signal/__init__.py +86 -0
  246. sigima/tools/signal/dynamic.py +254 -0
  247. sigima/tools/signal/features.py +135 -0
  248. sigima/tools/signal/filtering.py +171 -0
  249. sigima/tools/signal/fitting.py +1171 -0
  250. sigima/tools/signal/fourier.py +466 -0
  251. sigima/tools/signal/interpolation.py +70 -0
  252. sigima/tools/signal/peakdetection.py +126 -0
  253. sigima/tools/signal/pulse.py +1626 -0
  254. sigima/tools/signal/scaling.py +50 -0
  255. sigima/tools/signal/stability.py +258 -0
  256. sigima/tools/signal/windowing.py +90 -0
  257. sigima/worker.py +79 -0
  258. sigima-1.0.0.dist-info/METADATA +233 -0
  259. sigima-1.0.0.dist-info/RECORD +262 -0
  260. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
  261. sigima-0.0.1.dev0.dist-info/METADATA +0 -60
  262. sigima-0.0.1.dev0.dist-info/RECORD +0 -6
  263. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
  264. {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()