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,654 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Unit tests for geometry computation functions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+ import pytest
11
+ import scipy.ndimage as spi
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, iterate_noisy_images
18
+ from sigima.tests.env import execenv
19
+ from sigima.tests.helpers import check_array_result, check_scalar_result
20
+
21
+
22
+ @pytest.mark.validation
23
+ def test_image_translate() -> None:
24
+ """Image translation test."""
25
+ for dx, dy in [(10, 0), (0, 10), (-10, -10)]:
26
+ compfunc = sigima.proc.image.translate
27
+ execenv.print(f"*** Testing image translate: {compfunc.__name__}")
28
+ ima1 = list(iterate_noisy_images(size=128))[0]
29
+ ima2: sigima.objects.ImageObj = compfunc(
30
+ ima1, sigima.params.TranslateParam.create(dx=dx, dy=dy)
31
+ )
32
+ check_scalar_result("Image X translation", ima2.x0, ima1.x0 + dx)
33
+ check_scalar_result("Image Y translation", ima2.y0, ima1.y0 + dy)
34
+
35
+
36
+ def __generic_flip_check(compfunc: callable, expfunc: callable) -> None:
37
+ """Generic flip check function."""
38
+ execenv.print(f"*** Testing image flip: {compfunc.__name__}")
39
+ for ima1 in iterate_noisy_images(size=128):
40
+ execenv.print(f" {compfunc.__name__}({ima1.data.dtype}): ", end="")
41
+ ima2: sigima.objects.ImageObj = compfunc(ima1)
42
+ check_array_result("Image flip", ima2.data, expfunc(ima1.data))
43
+
44
+
45
+ @pytest.mark.validation
46
+ def test_image_fliph() -> None:
47
+ """Image horizontal flip test."""
48
+ __generic_flip_check(sigima.proc.image.fliph, np.fliplr)
49
+
50
+
51
+ @pytest.mark.validation
52
+ def test_image_flipv() -> None:
53
+ """Image vertical flip test."""
54
+ __generic_flip_check(sigima.proc.image.flipv, np.flipud)
55
+
56
+
57
+ def __generic_rotate_check(angle: int) -> None:
58
+ """Generic rotate check function."""
59
+ execenv.print(f"*** Testing image {angle}° rotation:")
60
+ for ima1 in iterate_noisy_images(size=128):
61
+ execenv.print(f" rotate{angle}({ima1.data.dtype}): ", end="")
62
+ ima2 = getattr(sigima.proc.image, f"rotate{angle}")(ima1)
63
+ check_array_result(
64
+ f"Image rotate{angle}", ima2.data, np.rot90(ima1.data, k=angle // 90)
65
+ )
66
+
67
+
68
+ @pytest.mark.validation
69
+ def test_image_rotate90() -> None:
70
+ """Image 90° rotation test."""
71
+ __generic_rotate_check(90)
72
+
73
+
74
+ @pytest.mark.validation
75
+ def test_image_rotate270() -> None:
76
+ """Image 270° rotation test."""
77
+ __generic_rotate_check(270)
78
+
79
+
80
+ def __get_test_image_with_roi() -> sigima.objects.ImageObj:
81
+ """Get a test image with a predefined ROI."""
82
+ ima = get_test_image("flower.npy")
83
+ ima.roi = sigima.objects.create_image_roi(
84
+ "rectangle", [10.0, 10.0, 50.0, 400.0], indices=False
85
+ )
86
+ return ima
87
+
88
+
89
+ def __check_roi_properties(
90
+ ima1: sigima.objects.ImageObj, ima2: sigima.objects.ImageObj
91
+ ) -> None:
92
+ """Check that the ROI properties are preserved after transformation."""
93
+ assert ima2.roi.single_rois[0].title == ima1.roi.single_rois[0].title
94
+ assert ima2.roi.single_rois[0].indices == ima1.roi.single_rois[0].indices
95
+
96
+
97
+ def test_roi_rotate90() -> None:
98
+ """Test 90° rotation with ROI transformation."""
99
+ ima = __get_test_image_with_roi()
100
+
101
+ # Apply 90° rotation
102
+ rotated = sigima.proc.image.rotate90(ima)
103
+
104
+ # Check that ROI coordinates were transformed correctly
105
+ # Original: [10, 10, 50, 400] -> Expected: [10, ima.height - 10 - 50, 400, 50]
106
+ expected_coords = np.array([10.0, ima.height - 60.0, 400.0, 50.0])
107
+ actual_coords = rotated.roi.single_rois[0].coords
108
+
109
+ assert np.allclose(actual_coords, expected_coords), (
110
+ f"ROI coordinates not transformed correctly. "
111
+ f"Expected {expected_coords}, got {actual_coords}"
112
+ )
113
+ __check_roi_properties(ima, rotated)
114
+
115
+
116
+ def test_roi_rotate270() -> None:
117
+ """Test 270° rotation with ROI transformation."""
118
+ ima = __get_test_image_with_roi()
119
+
120
+ # Apply 270° rotation
121
+ rotated = sigima.proc.image.rotate270(ima)
122
+
123
+ # Check that ROI coordinates were transformed correctly
124
+ # Original: [10, 10, 50, 400] -> Expected: [ima.width - 10 - 400, 10, 400, 50]
125
+ expected_coords = np.array([ima.width - 410.0, 10.0, 400.0, 50.0])
126
+ actual_coords = rotated.roi.single_rois[0].coords
127
+
128
+ assert np.allclose(actual_coords, expected_coords), (
129
+ f"ROI coordinates not transformed correctly. "
130
+ f"Expected {expected_coords}, got {actual_coords}"
131
+ )
132
+ __check_roi_properties(ima, rotated)
133
+
134
+
135
+ def test_roi_translation() -> None:
136
+ """Test translation with ROI transformation."""
137
+ ima = __get_test_image_with_roi()
138
+
139
+ # Apply translation
140
+ translated = sigima.proc.image.translate(
141
+ ima, sigima.params.TranslateParam.create(dx=10, dy=10)
142
+ )
143
+
144
+ # Check that ROI coordinates were transformed correctly
145
+ # Original: [10, 10, 50, 400] -> Expected: [20, 20, 50, 400]
146
+ expected_coords = np.array([20.0, 20.0, 50.0, 400.0])
147
+ actual_coords = translated.roi.single_rois[0].coords
148
+
149
+ assert np.allclose(actual_coords, expected_coords), (
150
+ f"ROI coordinates not transformed correctly. "
151
+ f"Expected {expected_coords}, got {actual_coords}"
152
+ )
153
+ __check_roi_properties(ima, translated)
154
+
155
+
156
+ @pytest.mark.validation
157
+ def test_image_rotate() -> None:
158
+ """Image rotation test."""
159
+ execenv.print("*** Testing image rotation:")
160
+ for ima1 in iterate_noisy_images(size=128):
161
+ for angle in (30.0, 45.0, 60.0, 120.0):
162
+ execenv.print(f" rotate{angle}({ima1.data.dtype}): ", end="")
163
+ ima2 = sigima.proc.image.rotate(
164
+ ima1, sigima.params.RotateParam.create(angle=angle)
165
+ )
166
+ exp = spi.rotate(ima1.data, angle, reshape=False)
167
+ check_array_result(f"Image rotate{angle}", ima2.data, exp)
168
+
169
+
170
+ @pytest.mark.validation
171
+ def test_image_transpose() -> None:
172
+ """Validation test for the image transpose processing."""
173
+ src = get_test_image("flower.npy")
174
+ dst = sigima.proc.image.transpose(src)
175
+ exp = np.swapaxes(src.data, 0, 1)
176
+ check_array_result("Transpose", dst.data, exp)
177
+
178
+
179
+ @pytest.mark.validation
180
+ def test_image_resampling() -> None:
181
+ """Image resampling test."""
182
+ execenv.print("*** Testing image resampling")
183
+
184
+ # Create a test image
185
+ ima1 = get_test_image(
186
+ "flower.npy"
187
+ ) # Test 1: Identity resampling (same dimensions and coordinate range)
188
+ p1 = sigima.params.Resampling2DParam.create(
189
+ mode="shape",
190
+ width=ima1.data.shape[1],
191
+ height=ima1.data.shape[0],
192
+ xmin=ima1.x0,
193
+ xmax=ima1.x0 + ima1.width,
194
+ ymin=ima1.y0,
195
+ ymax=ima1.y0 + ima1.height,
196
+ method=sigima.enums.Interpolation2DMethod.LINEAR,
197
+ )
198
+ dst1 = sigima.proc.image.resampling(ima1, p1)
199
+
200
+ # Should be very close to original (allowing for small interpolation differences)
201
+ check_scalar_result("Identity resampling X0", dst1.x0, ima1.x0)
202
+ check_scalar_result("Identity resampling Y0", dst1.y0, ima1.y0)
203
+ check_scalar_result(
204
+ "Identity resampling shape[0]", dst1.data.shape[0], ima1.data.shape[0]
205
+ )
206
+ check_scalar_result(
207
+ "Identity resampling shape[1]", dst1.data.shape[1], ima1.data.shape[1]
208
+ )
209
+
210
+ # Test 2: Downsample by factor of 2
211
+ p2 = sigima.params.Resampling2DParam.create(
212
+ mode="shape",
213
+ width=ima1.data.shape[1] // 2,
214
+ height=ima1.data.shape[0] // 2,
215
+ xmin=ima1.x0,
216
+ xmax=ima1.x0 + ima1.width,
217
+ ymin=ima1.y0,
218
+ ymax=ima1.y0 + ima1.height,
219
+ method=sigima.enums.Interpolation2DMethod.LINEAR,
220
+ )
221
+ dst2 = sigima.proc.image.resampling(ima1, p2)
222
+
223
+ check_scalar_result("Downsample X0", dst2.x0, ima1.x0)
224
+ check_scalar_result("Downsample Y0", dst2.y0, ima1.y0)
225
+ check_scalar_result(
226
+ "Downsample shape[0]", dst2.data.shape[0], ima1.data.shape[0] // 2
227
+ )
228
+ check_scalar_result(
229
+ "Downsample shape[1]", dst2.data.shape[1], ima1.data.shape[1] // 2
230
+ )
231
+
232
+ # Check that pixel sizes are adjusted correctly
233
+ expected_dx = ima1.dx * 2 if ima1.dx is not None else 2.0
234
+ expected_dy = ima1.dy * 2 if ima1.dy is not None else 2.0
235
+ check_scalar_result("Downsample dx", dst2.dx, expected_dx, rtol=1e-10)
236
+ check_scalar_result("Downsample dy", dst2.dy, expected_dy, rtol=1e-10)
237
+
238
+ # Test 3: Use pixel size mode
239
+ if ima1.dx is not None and ima1.dy is not None:
240
+ p3 = sigima.params.Resampling2DParam.create(
241
+ mode="dxy",
242
+ dx=ima1.dx * 1.5,
243
+ dy=ima1.dy * 1.5,
244
+ xmin=ima1.x0,
245
+ xmax=ima1.x0 + ima1.width,
246
+ ymin=ima1.y0,
247
+ ymax=ima1.y0 + ima1.height,
248
+ method=sigima.enums.Interpolation2DMethod.LINEAR,
249
+ )
250
+ dst3 = sigima.proc.image.resampling(ima1, p3)
251
+
252
+ check_scalar_result("Pixel size mode dx", dst3.dx, ima1.dx * 1.5, rtol=1e-10)
253
+ check_scalar_result("Pixel size mode dy", dst3.dy, ima1.dy * 1.5, rtol=1e-10)
254
+
255
+ # Test 4: Different interpolation methods
256
+ for method in sigima.enums.Interpolation2DMethod:
257
+ p4 = sigima.params.Resampling2DParam.create(
258
+ mode="shape",
259
+ width=ima1.data.shape[1] // 2,
260
+ height=ima1.data.shape[0] // 2,
261
+ xmin=ima1.x0,
262
+ xmax=ima1.x0 + ima1.width,
263
+ ymin=ima1.y0,
264
+ ymax=ima1.y0 + ima1.height,
265
+ method=method,
266
+ )
267
+ dst4 = sigima.proc.image.resampling(ima1, p4)
268
+
269
+ # Basic shape checks
270
+ check_scalar_result(
271
+ f"Method {method} shape[0]", dst4.data.shape[0], ima1.data.shape[0] // 2
272
+ )
273
+ check_scalar_result(
274
+ f"Method {method} shape[1]", dst4.data.shape[1], ima1.data.shape[1] // 2
275
+ )
276
+
277
+ # Test 5: fill_value parameter (out-of-bounds sampling)
278
+ execenv.print(" Testing fill_value parameter")
279
+
280
+ # Test 5a: Default behavior (fill_value=None should use NaN)
281
+ p5a = sigima.params.Resampling2DParam.create(
282
+ mode="shape",
283
+ width=20,
284
+ height=20,
285
+ xmin=600.0, # Outside image bounds
286
+ xmax=620.0,
287
+ ymin=600.0,
288
+ ymax=620.0,
289
+ method=sigima.enums.Interpolation2DMethod.LINEAR,
290
+ fill_value=None,
291
+ )
292
+ dst5a = sigima.proc.image.resampling(ima1, p5a)
293
+
294
+ # Should be all NaN since sampling outside image bounds
295
+ assert np.all(np.isnan(dst5a.data)), (
296
+ "Expected all NaN values for out-of-bounds sampling with fill_value=None"
297
+ )
298
+ assert dst5a.data.dtype == np.float64, "Expected float64 dtype for NaN result"
299
+
300
+ # Test 5b: Custom fill value
301
+ p5b = sigima.params.Resampling2DParam.create(
302
+ mode="shape",
303
+ width=20,
304
+ height=20,
305
+ xmin=600.0, # Outside image bounds
306
+ xmax=620.0,
307
+ ymin=600.0,
308
+ ymax=620.0,
309
+ method=sigima.enums.Interpolation2DMethod.LINEAR,
310
+ fill_value=123.0,
311
+ )
312
+ dst5b = sigima.proc.image.resampling(ima1, p5b)
313
+
314
+ # Should be all 123.0 since sampling outside image bounds
315
+ assert np.all(dst5b.data == 123.0), (
316
+ "Expected all fill values for out-of-bounds sampling"
317
+ )
318
+ assert dst5b.data.dtype == ima1.data.dtype, (
319
+ "Expected same dtype as input for numeric fill value"
320
+ )
321
+
322
+ # Test 5c: Partially outside (mix of real data and fill values)
323
+ p5c = sigima.params.Resampling2DParam.create(
324
+ mode="shape",
325
+ width=30,
326
+ height=30,
327
+ xmin=ima1.x0 + ima1.width - 10, # Partially outside
328
+ xmax=ima1.x0 + ima1.width + 20,
329
+ ymin=ima1.y0 + ima1.height - 10,
330
+ ymax=ima1.y0 + ima1.height + 20,
331
+ method=sigima.enums.Interpolation2DMethod.LINEAR,
332
+ fill_value=99.0,
333
+ )
334
+ dst5c = sigima.proc.image.resampling(ima1, p5c)
335
+
336
+ # Should have mix of values
337
+ fill_count = np.sum(dst5c.data == 99.0)
338
+ total_count = dst5c.data.size
339
+ assert fill_count > 0, "Expected some fill values for partially out-of-bounds"
340
+ assert fill_count < total_count, "Expected some real data values"
341
+
342
+ # Test 5d: Within bounds should not use fill value
343
+ p5d = sigima.params.Resampling2DParam.create(
344
+ mode="shape",
345
+ width=50,
346
+ height=50,
347
+ xmin=ima1.x0 + 50, # Within bounds
348
+ xmax=ima1.x0 + 100,
349
+ ymin=ima1.y0 + 50,
350
+ ymax=ima1.y0 + 100,
351
+ method=sigima.enums.Interpolation2DMethod.LINEAR,
352
+ fill_value=999.0,
353
+ )
354
+ dst5d = sigima.proc.image.resampling(ima1, p5d)
355
+
356
+ # Should not contain any fill values since all within bounds
357
+ assert not np.any(dst5d.data == 999.0), (
358
+ "No fill values expected for within-bounds sampling"
359
+ )
360
+
361
+
362
+ @pytest.mark.validation
363
+ def test_image_resize() -> None:
364
+ """Image resize test."""
365
+ execenv.print("*** Testing image resize")
366
+
367
+ # Test with different zoom factors
368
+ zoom_factors = [0.5, 2.0, 1.5, 0.75]
369
+
370
+ for ima1 in iterate_noisy_images(size=128):
371
+ execenv.print(f" Testing on {ima1.data.dtype} image")
372
+
373
+ for zoom in zoom_factors:
374
+ execenv.print(f" zoom={zoom}: ", end="")
375
+
376
+ # Test resize with default parameters
377
+ p = sigima.params.ResizeParam.create(zoom=zoom)
378
+ ima2 = sigima.proc.image.resize(ima1, p)
379
+
380
+ # Check that scipy.ndimage.zoom produces the same result
381
+ expected_data = spi.zoom(
382
+ ima1.data, zoom, order=3, mode="constant", cval=0.0, prefilter=True
383
+ )
384
+ check_array_result(f"Resize zoom={zoom}", ima2.data, expected_data)
385
+
386
+ # Check that pixel sizes are updated correctly
387
+ if ima1.dx is not None and ima1.dy is not None:
388
+ expected_dx = ima1.dx / zoom
389
+ expected_dy = ima1.dy / zoom
390
+ check_scalar_result(
391
+ f"Resize dx zoom={zoom}", ima2.dx, expected_dx, rtol=1e-10
392
+ )
393
+ check_scalar_result(
394
+ f"Resize dy zoom={zoom}", ima2.dy, expected_dy, rtol=1e-10
395
+ )
396
+
397
+ # Test different border modes and parameters
398
+ execenv.print(" Testing different border modes and parameters")
399
+ ima_test = get_test_image("flower.npy")
400
+
401
+ # Test different modes
402
+ for mode in sigima.enums.BorderMode:
403
+ execenv.print(f" mode={mode.name}: ", end="")
404
+ p = sigima.params.ResizeParam.create(zoom=1.5, mode=mode, cval=100.0)
405
+ ima_resized = sigima.proc.image.resize(ima_test, p)
406
+
407
+ # Compare with scipy implementation
408
+ expected_data = spi.zoom(
409
+ ima_test.data, 1.5, order=3, mode=mode.value, cval=100.0, prefilter=True
410
+ )
411
+ check_array_result(f"Resize mode={mode.name}", ima_resized.data, expected_data)
412
+
413
+ # Test different interpolation orders
414
+ execenv.print(" Testing different interpolation orders")
415
+ for order in [0, 1, 2, 3, 4, 5]:
416
+ execenv.print(f" order={order}: ", end="")
417
+ p = sigima.params.ResizeParam.create(zoom=1.3, order=order, prefilter=False)
418
+ ima_resized = sigima.proc.image.resize(ima_test, p)
419
+
420
+ # Compare with scipy implementation
421
+ expected_data = spi.zoom(
422
+ ima_test.data, 1.3, order=order, mode="constant", cval=0.0, prefilter=False
423
+ )
424
+ check_array_result(f"Resize order={order}", ima_resized.data, expected_data)
425
+
426
+ # Test with prefilter disabled
427
+ execenv.print(" Testing prefilter parameter")
428
+ for prefilter in [True, False]:
429
+ execenv.print(f" prefilter={prefilter}: ", end="")
430
+ p = sigima.params.ResizeParam.create(zoom=0.8, prefilter=prefilter)
431
+ ima_resized = sigima.proc.image.resize(ima_test, p)
432
+
433
+ # Compare with scipy implementation
434
+ expected_data = spi.zoom(
435
+ ima_test.data, 0.8, order=3, mode="constant", cval=0.0, prefilter=prefilter
436
+ )
437
+ check_array_result(
438
+ f"Resize prefilter={prefilter}", ima_resized.data, expected_data
439
+ )
440
+
441
+ # Test edge cases
442
+ execenv.print(" Testing edge cases")
443
+
444
+ # Test zoom=1.0 (identity)
445
+ p_identity = sigima.params.ResizeParam.create(zoom=1.0)
446
+ ima_identity = sigima.proc.image.resize(ima_test, p_identity)
447
+ check_array_result("Resize identity zoom=1.0", ima_identity.data, ima_test.data)
448
+
449
+ # Test very small zoom
450
+ p_small = sigima.params.ResizeParam.create(zoom=0.1)
451
+ ima_small = sigima.proc.image.resize(ima_test, p_small)
452
+ expected_small = spi.zoom(
453
+ ima_test.data, 0.1, order=3, mode="constant", cval=0.0, prefilter=True
454
+ )
455
+ check_array_result("Resize small zoom=0.1", ima_small.data, expected_small)
456
+
457
+ # Test large zoom
458
+ p_large = sigima.params.ResizeParam.create(zoom=5.0)
459
+ ima_large = sigima.proc.image.resize(ima_test, p_large)
460
+ expected_large = spi.zoom(
461
+ ima_test.data, 5.0, order=3, mode="constant", cval=0.0, prefilter=True
462
+ )
463
+ check_array_result("Resize large zoom=5.0", ima_large.data, expected_large)
464
+
465
+
466
+ @pytest.mark.validation
467
+ def test_set_uniform_coords() -> None:
468
+ """Test converting from non-uniform to uniform coordinates."""
469
+ execenv.print("*** Testing set_uniform_coords")
470
+
471
+ # Test 1: Create an image with non-uniform coordinates
472
+ execenv.print(" Testing non-uniform to uniform conversion")
473
+ ima1 = get_test_image("flower.npy")
474
+ nx, ny = ima1.data.shape[1], ima1.data.shape[0]
475
+
476
+ # Create non-uniform coordinates (e.g., quadratic spacing on y-axis)
477
+ xcoords = np.linspace(0.0, 10.0, nx)
478
+ ycoords = np.linspace(0.0, 8.0, ny) ** 2 # Non-uniform spacing
479
+ ima1.set_coords(xcoords, ycoords)
480
+
481
+ # Verify it's non-uniform
482
+ assert not ima1.is_uniform_coords, "Image should have non-uniform coordinates"
483
+
484
+ # Create parameter and update from object
485
+ p = sigima.params.UniformCoordsParam()
486
+ p.update_from_obj(ima1)
487
+
488
+ # Apply conversion
489
+ ima2 = sigima.proc.image.set_uniform_coords(ima1, p)
490
+
491
+ # Check that result has uniform coordinates
492
+ assert ima2.is_uniform_coords, "Result should have uniform coordinates"
493
+
494
+ # Check that the data is unchanged
495
+ check_array_result("Data preservation", ima2.data, ima1.data)
496
+
497
+ # Check that coordinate parameters were extracted correctly
498
+ expected_x0 = xcoords[0]
499
+ expected_y0 = ycoords[0]
500
+ expected_dx = (xcoords[-1] - xcoords[0]) / (nx - 1)
501
+ expected_dy = (ycoords[-1] - ycoords[0]) / (ny - 1)
502
+
503
+ check_scalar_result("X0 extraction", ima2.x0, expected_x0, atol=1e-10)
504
+ check_scalar_result("Y0 extraction", ima2.y0, expected_y0, atol=1e-10)
505
+ check_scalar_result("dx extraction", ima2.dx, expected_dx, atol=1e-10)
506
+ check_scalar_result("dy extraction", ima2.dy, expected_dy, atol=1e-10)
507
+
508
+ # Test 2: Converting already uniform coordinates (should preserve values)
509
+ execenv.print(" Testing uniform to uniform (identity)")
510
+ ima3 = get_test_image("flower.npy")
511
+ original_x0, original_y0 = ima3.x0, ima3.y0
512
+ original_dx, original_dy = ima3.dx, ima3.dy
513
+
514
+ p2 = sigima.params.UniformCoordsParam()
515
+ p2.update_from_obj(ima3)
516
+ ima4 = sigima.proc.image.set_uniform_coords(ima3, p2)
517
+
518
+ assert ima4.is_uniform_coords, "Result should have uniform coordinates"
519
+ check_array_result("Data preservation (uniform)", ima4.data, ima3.data)
520
+ check_scalar_result("X0 preservation", ima4.x0, original_x0, atol=1e-10)
521
+ check_scalar_result("Y0 preservation", ima4.y0, original_y0, atol=1e-10)
522
+ check_scalar_result("dx preservation", ima4.dx, original_dx, atol=1e-10)
523
+ check_scalar_result("dy preservation", ima4.dy, original_dy, atol=1e-10)
524
+
525
+ # Test 3: Manual parameter specification
526
+ execenv.print(" Testing manual parameter specification")
527
+ ima5 = get_test_image("flower.npy")
528
+ # Create non-uniform coordinates
529
+ ima5.set_coords(np.linspace(5.0, 15.0, nx), np.linspace(10.0, 20.0, ny))
530
+
531
+ p3 = sigima.params.UniformCoordsParam.create(x0=5.0, y0=10.0, dx=0.5, dy=0.25)
532
+ ima6 = sigima.proc.image.set_uniform_coords(ima5, p3)
533
+
534
+ assert ima6.is_uniform_coords, "Result should have uniform coordinates"
535
+ check_scalar_result("Manual X0", ima6.x0, 5.0, atol=1e-10)
536
+ check_scalar_result("Manual Y0", ima6.y0, 10.0, atol=1e-10)
537
+ check_scalar_result("Manual dx", ima6.dx, 0.5, atol=1e-10)
538
+ check_scalar_result("Manual dy", ima6.dy, 0.25, atol=1e-10)
539
+
540
+
541
+ @pytest.mark.validation
542
+ def test_image_calibration() -> None:
543
+ """Validation test for polynomial calibration."""
544
+ execenv.print("*** Testing calibration (polynomial)")
545
+
546
+ # Test 1: Z-axis polynomial calibration
547
+ execenv.print(" Testing Z-axis polynomial calibration")
548
+ src = get_test_image("flower.npy")
549
+ # Use smaller coefficients to avoid overflow with uint8 data (0-255 range)
550
+ p = sigima.params.XYZCalibrateParam.create(
551
+ axis="z", a0=10.0, a1=2.0, a2=0.001, a3=0.0
552
+ )
553
+ dst = sigima.proc.image.calibration(src, p)
554
+
555
+ # Verify polynomial transformation on data
556
+ src_data_float = src.data.astype(float)
557
+ expected_data = (
558
+ p.a0
559
+ + p.a1 * src_data_float
560
+ + p.a2 * src_data_float**2
561
+ + p.a3 * src_data_float**3
562
+ )
563
+ check_array_result("Z-axis polynomial", dst.data, expected_data)
564
+
565
+ # Coordinates should be unchanged
566
+ assert dst.is_uniform_coords
567
+ check_scalar_result("Z-calib: x0", dst.x0, src.x0)
568
+ check_scalar_result("Z-calib: y0", dst.y0, src.y0)
569
+ check_scalar_result("Z-calib: dx", dst.dx, src.dx)
570
+ check_scalar_result("Z-calib: dy", dst.dy, src.dy)
571
+
572
+ # Test 2: X-axis polynomial calibration (uniform → non-uniform)
573
+ execenv.print(" Testing X-axis polynomial (uniform → non-uniform)")
574
+ src2 = get_test_image("flower.npy")
575
+ src2.set_uniform_coords(dx=0.5, dy=0.5, x0=0.0, y0=0.0)
576
+ p2 = sigima.params.XYZCalibrateParam.create(
577
+ axis="x", a0=1.0, a1=2.0, a2=0.1, a3=0.0
578
+ )
579
+ dst2 = sigima.proc.image.calibration(src2, p2)
580
+
581
+ # After polynomial calibration on X, coordinates should become non-uniform
582
+ assert not dst2.is_uniform_coords, (
583
+ "X-axis polynomial should create non-uniform coords"
584
+ )
585
+
586
+ # Verify X coordinates transformation
587
+ x_uniform = src2.x0 + np.arange(src2.data.shape[1]) * src2.dx
588
+ expected_x = p2.a0 + p2.a1 * x_uniform + p2.a2 * x_uniform**2
589
+ check_array_result("X-axis polynomial coords", dst2.xcoords, expected_x)
590
+ # Check that Y coordinates were converted in non-uniform but unchanged
591
+ src2_ycoords = src2.y0 + np.arange(src2.data.shape[0]) * src2.dy
592
+ check_array_result("X-axis polynomial Y coords", dst2.ycoords, src2_ycoords)
593
+
594
+ # Data should be unchanged
595
+ check_array_result("X-calib: data preservation", dst2.data, src2.data)
596
+
597
+ # Test 3: Y-axis polynomial calibration (non-uniform → non-uniform)
598
+ execenv.print(" Testing Y-axis polynomial (non-uniform → non-uniform)")
599
+ src3 = get_test_image("flower.npy")
600
+ ny = src3.data.shape[0]
601
+ y_nonuniform = np.linspace(0.0, 10.0, ny)
602
+ src3.set_coords(None, y_nonuniform)
603
+
604
+ p3 = sigima.params.XYZCalibrateParam.create(
605
+ axis="y", a0=5.0, a1=1.0, a2=0.0, a3=0.05
606
+ )
607
+ dst3 = sigima.proc.image.calibration(src3, p3)
608
+
609
+ # Should still be non-uniform
610
+ assert not dst3.is_uniform_coords
611
+
612
+ # Verify Y coordinates transformation
613
+ expected_y = p3.a0 + p3.a1 * y_nonuniform + p3.a3 * y_nonuniform**3
614
+ check_array_result("Y-axis polynomial coords", dst3.ycoords, expected_y)
615
+
616
+ # Data should be unchanged
617
+ check_array_result("Y-calib: data preservation", dst3.data, src3.data)
618
+
619
+ # Test 4: Linear case (a2=a3=0, backward compatibility)
620
+ execenv.print(" Testing linear calibration (a2=a3=0)")
621
+ src4 = get_test_image("flower.npy")
622
+ p4 = sigima.params.XYZCalibrateParam.create(
623
+ axis="x", a0=0.5, a1=2.0, a2=0.0, a3=0.0
624
+ )
625
+ dst4 = sigima.proc.image.calibration(src4, p4)
626
+
627
+ # For linear case with uniform input, result should still be non-uniform
628
+ # because we always generate coordinate arrays
629
+ # Verify the transformation is correct
630
+ x_uniform = src4.x0 + np.arange(src4.data.shape[1]) * src4.dx
631
+ expected_x_linear = p4.a0 + p4.a1 * x_uniform
632
+ if dst4.is_uniform_coords:
633
+ # If implementation optimized to keep uniform coords
634
+ check_scalar_result("Linear x0", dst4.x0, expected_x_linear[0])
635
+ check_scalar_result(
636
+ "Linear dx", dst4.dx, expected_x_linear[1] - expected_x_linear[0]
637
+ )
638
+ else:
639
+ # If coordinates are non-uniform
640
+ check_array_result("Linear xcoords", dst4.xcoords, expected_x_linear)
641
+
642
+
643
+ if __name__ == "__main__":
644
+ test_image_fliph()
645
+ test_image_flipv()
646
+ test_image_rotate90()
647
+ test_image_rotate270()
648
+ test_image_rotate()
649
+ test_image_transpose()
650
+ test_image_resampling()
651
+ test_image_resize()
652
+ test_image_translate()
653
+ test_set_uniform_coords()
654
+ test_image_calibration()