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,661 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Geometry computation module
5
+ ---------------------------
6
+
7
+ This module implements geometric transformations and manipulations for images,
8
+ such as rotations, flips, resizing, axis swapping, binning, and padding.
9
+
10
+ Main features include:
11
+
12
+ - Rotation by arbitrary or fixed angles
13
+ - Horizontal and vertical flipping
14
+ - Resizing and binning of images
15
+ - Axis swapping and zero padding
16
+
17
+ These functions are useful for preparing and augmenting image data.
18
+ """
19
+
20
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
21
+
22
+ # Note:
23
+ # ----
24
+ # - All `guidata.dataset.DataSet` parameter classes must also be imported
25
+ # in the `sigima.params` module.
26
+ # - All functions decorated by `computation_function` must be imported in the upper
27
+ # level `sigima.proc.image` module.
28
+
29
+ from __future__ import annotations
30
+
31
+ import guidata.dataset as gds
32
+ import numpy as np
33
+ import scipy.ndimage as spi
34
+
35
+ from sigima.config import _
36
+ from sigima.enums import BorderMode, Interpolation2DMethod
37
+ from sigima.objects.image import ImageObj
38
+ from sigima.proc.base import dst_1_to_1
39
+ from sigima.proc.decorator import computation_function
40
+ from sigima.proc.image.base import restore_data_outside_roi
41
+ from sigima.proc.image.transformations import transformer
42
+
43
+ # NOTE: Only parameter classes DEFINED in this module should be included in __all__.
44
+ # Parameter classes imported from other modules (like sigima.proc.base) should NOT
45
+ # be re-exported to avoid Sphinx cross-reference conflicts. The sigima.params module
46
+ # serves as the central API point that imports and re-exports all parameter classes.
47
+ __all__ = [
48
+ "Resampling2DParam",
49
+ "ResizeParam",
50
+ "RotateParam",
51
+ "TranslateParam",
52
+ "UniformCoordsParam",
53
+ "XYZCalibrateParam",
54
+ "calibration",
55
+ "fliph",
56
+ "flipv",
57
+ "resampling",
58
+ "resize",
59
+ "rotate",
60
+ "rotate90",
61
+ "rotate270",
62
+ "set_uniform_coords",
63
+ "translate",
64
+ "transpose",
65
+ ]
66
+
67
+
68
+ class TranslateParam(gds.DataSet):
69
+ """Translate parameters"""
70
+
71
+ dx = gds.FloatItem(_("X translation"), default=0.0)
72
+ dy = gds.FloatItem(_("Y translation"), default=0.0)
73
+
74
+
75
+ @computation_function()
76
+ def translate(src: ImageObj, p: TranslateParam) -> ImageObj:
77
+ """Translate data with :py:func:`scipy.ndimage.shift`
78
+
79
+ Args:
80
+ src: input image object
81
+ p: parameters
82
+
83
+ Returns:
84
+ Output image object
85
+ """
86
+ dst = dst_1_to_1(src, "translate", f"dx={p.dx}, dy={p.dy}")
87
+ if src.is_uniform_coords:
88
+ dst.set_uniform_coords(dst.dx, dst.dy, dst.x0 + p.dx, dst.y0 + p.dy)
89
+ else:
90
+ dst.set_coords(src.xcoords + p.dx, src.ycoords + p.dy)
91
+ transformer.transform_roi(dst, "translate", dx=p.dx, dy=p.dy)
92
+ return dst
93
+
94
+
95
+ class RotateParam(gds.DataSet):
96
+ """Rotate parameters"""
97
+
98
+ prop = gds.ValueProp(False)
99
+
100
+ angle = gds.FloatItem(f"{_('Angle')} (°)", default=0.0)
101
+ mode = gds.ChoiceItem(_("Mode"), BorderMode, default=BorderMode.CONSTANT)
102
+ cval = gds.FloatItem(
103
+ _("cval"),
104
+ default=0.0,
105
+ help=_(
106
+ "Value used for points outside the "
107
+ "boundaries of the input if mode is "
108
+ "'constant'"
109
+ ),
110
+ )
111
+ reshape = gds.BoolItem(
112
+ _("Reshape the output array"),
113
+ default=False,
114
+ help=_(
115
+ "Reshape the output array "
116
+ "so that the input array is "
117
+ "contained completely in the output"
118
+ ),
119
+ )
120
+ prefilter = gds.BoolItem(_("Prefilter the input image"), default=True).set_prop(
121
+ "display", store=prop
122
+ )
123
+ order = gds.IntItem(
124
+ _("Order"),
125
+ default=3,
126
+ min=0,
127
+ max=5,
128
+ help=_("Spline interpolation order"),
129
+ ).set_prop("display", active=prop)
130
+
131
+
132
+ @computation_function()
133
+ def rotate(src: ImageObj, p: RotateParam) -> ImageObj:
134
+ """Rotate data with :py:func:`scipy.ndimage.rotate`
135
+
136
+ Args:
137
+ src: input image object
138
+ p: parameters
139
+
140
+ Returns:
141
+ Output image object
142
+ """
143
+ dst = dst_1_to_1(src, "rotate", f"α={p.angle:.3f}°, mode='{p.mode}'")
144
+ dst.data = spi.rotate(
145
+ src.data,
146
+ p.angle,
147
+ reshape=p.reshape,
148
+ order=p.order,
149
+ mode=p.mode,
150
+ cval=p.cval,
151
+ prefilter=p.prefilter,
152
+ )
153
+ dst.roi = None # Reset ROI as it may change after rotation
154
+ return dst
155
+
156
+
157
+ @computation_function()
158
+ def rotate90(src: ImageObj) -> ImageObj:
159
+ """Rotate data 90° with :py:func:`numpy.rot90`
160
+
161
+ Args:
162
+ src: input image object
163
+
164
+ Returns:
165
+ Output image object
166
+ """
167
+ dst = dst_1_to_1(src, "rotate90")
168
+ dst.data = np.rot90(src.data)
169
+ transformer.transform_roi(dst, "rotate", angle=-np.pi / 2, center=(dst.xc, dst.yc))
170
+ return dst
171
+
172
+
173
+ @computation_function()
174
+ def rotate270(src: ImageObj) -> ImageObj:
175
+ """Rotate data 270° with :py:func:`numpy.rot90`
176
+
177
+ Args:
178
+ src: input image object
179
+
180
+ Returns:
181
+ Output image object
182
+ """
183
+ dst = dst_1_to_1(src, "rotate270")
184
+ dst.data = np.rot90(src.data, 3)
185
+ transformer.transform_roi(dst, "rotate", angle=np.pi / 2, center=(dst.xc, dst.yc))
186
+ return dst
187
+
188
+
189
+ @computation_function()
190
+ def fliph(src: ImageObj) -> ImageObj:
191
+ """Flip data horizontally with :py:func:`numpy.fliplr`
192
+
193
+ Args:
194
+ src: input image object
195
+
196
+ Returns:
197
+ Output image object
198
+ """
199
+ dst = dst_1_to_1(src, "fliph")
200
+ dst.data = np.fliplr(src.data)
201
+ transformer.transform_roi(dst, "fliph", cx=dst.xc)
202
+ return dst
203
+
204
+
205
+ @computation_function()
206
+ def flipv(src: ImageObj) -> ImageObj:
207
+ """Flip data vertically with :py:func:`numpy.flipud`
208
+
209
+ Args:
210
+ src: input image object
211
+
212
+ Returns:
213
+ Output image object
214
+ """
215
+ dst = dst_1_to_1(src, "flipv")
216
+ dst.data = np.flipud(src.data)
217
+ transformer.transform_roi(dst, "flipv", cy=dst.yc)
218
+ return dst
219
+
220
+
221
+ class ResizeParam(gds.DataSet):
222
+ """Resize parameters"""
223
+
224
+ prop = gds.ValueProp(False)
225
+
226
+ zoom = gds.FloatItem(_("Zoom"), default=1.0)
227
+ mode = gds.ChoiceItem(_("Mode"), BorderMode, default=BorderMode.CONSTANT)
228
+ cval = gds.FloatItem(
229
+ _("cval"),
230
+ default=0.0,
231
+ help=_(
232
+ "Value used for points outside the "
233
+ "boundaries of the input if mode is "
234
+ "'constant'"
235
+ ),
236
+ )
237
+ prefilter = gds.BoolItem(_("Prefilter the input image"), default=True).set_prop(
238
+ "display", store=prop
239
+ )
240
+ order = gds.IntItem(
241
+ _("Order"),
242
+ default=3,
243
+ min=0,
244
+ max=5,
245
+ help=_("Spline interpolation order"),
246
+ ).set_prop("display", active=prop)
247
+
248
+
249
+ @computation_function()
250
+ def resize(src: ImageObj, p: ResizeParam) -> ImageObj:
251
+ """Zooming function with :py:func:`scipy.ndimage.zoom`
252
+
253
+ Args:
254
+ src: input image object
255
+ p: parameters
256
+
257
+ Returns:
258
+ Output image object
259
+
260
+ Raises:
261
+ ValueError: if source image has non-uniform coordinates
262
+ """
263
+ if not src.is_uniform_coords:
264
+ raise ValueError("Source image must have uniform coordinates for resampling")
265
+ mode = p.mode
266
+ dst = dst_1_to_1(src, "resize", f"zoom={p.zoom:.3f}")
267
+ dst.data = spi.zoom(
268
+ src.data,
269
+ p.zoom,
270
+ order=p.order,
271
+ mode=mode,
272
+ cval=p.cval,
273
+ prefilter=p.prefilter,
274
+ )
275
+ if not np.isnan(dst.dx) and not np.isnan(dst.dy):
276
+ dst.set_uniform_coords(dst.dx / p.zoom, dst.dy / p.zoom, dst.x0, dst.y0)
277
+ return dst
278
+
279
+
280
+ @computation_function()
281
+ def transpose(src: ImageObj) -> ImageObj:
282
+ """Transpose image with :py:func:`numpy.transpose`.
283
+
284
+ Args:
285
+ src: Input image object.
286
+
287
+ Returns:
288
+ Output image object.
289
+ """
290
+ dst = dst_1_to_1(src, "transpose")
291
+ dst.data = np.transpose(src.data)
292
+ dst.xlabel = src.ylabel
293
+ dst.ylabel = src.xlabel
294
+ dst.xunit = src.yunit
295
+ dst.yunit = src.xunit
296
+ if src.is_uniform_coords:
297
+ dst.set_uniform_coords(src.dy, src.dx, src.y0, src.x0)
298
+ else:
299
+ dst.set_coords(src.ycoords, src.xcoords)
300
+ transformer.transform_roi(dst, "transpose")
301
+ return dst
302
+
303
+
304
+ class Resampling2DParam(gds.DataSet):
305
+ """Resample parameters for 2D images"""
306
+
307
+ # Output coordinate system
308
+ xmin = gds.FloatItem(
309
+ "X<sub>min</sub>",
310
+ default=None,
311
+ allow_none=True,
312
+ help=_("Minimum X-coordinate of the output image"),
313
+ )
314
+ xmax = gds.FloatItem(
315
+ "X<sub>max</sub>",
316
+ default=None,
317
+ allow_none=True,
318
+ help=_("Maximum X-coordinate of the output image"),
319
+ )
320
+ ymin = gds.FloatItem(
321
+ "Y<sub>min</sub>",
322
+ default=None,
323
+ allow_none=True,
324
+ help=_("Minimum Y-coordinate of the output image"),
325
+ )
326
+ ymax = gds.FloatItem(
327
+ "Y<sub>max</sub>",
328
+ default=None,
329
+ allow_none=True,
330
+ help=_("Maximum Y-coordinate of the output image"),
331
+ )
332
+
333
+ # Mode selection
334
+ _prop = gds.GetAttrProp("mode")
335
+ _modes = (("dxy", _("Pixel size")), ("shape", _("Output shape")))
336
+ mode = gds.ChoiceItem(_("Mode"), _modes, default="shape", radio=True).set_prop(
337
+ "display", store=_prop
338
+ )
339
+
340
+ # Pixel size mode parameters
341
+ dx = gds.FloatItem(
342
+ "ΔX", default=None, allow_none=True, help=_("Pixel size in X direction")
343
+ ).set_prop("display", active=gds.FuncProp(_prop, lambda x: x == "dxy"))
344
+ dy = gds.FloatItem(
345
+ "ΔY", default=None, allow_none=True, help=_("Pixel size in Y direction")
346
+ ).set_prop("display", active=gds.FuncProp(_prop, lambda x: x == "dxy"))
347
+
348
+ # Shape mode parameters
349
+ width = gds.IntItem(
350
+ _("Width"),
351
+ default=None,
352
+ allow_none=True,
353
+ help=_("Output image width in pixels"),
354
+ ).set_prop("display", active=gds.FuncProp(_prop, lambda x: x == "shape"))
355
+ height = gds.IntItem(
356
+ _("Height"),
357
+ default=None,
358
+ allow_none=True,
359
+ help=_("Output image height in pixels"),
360
+ ).set_prop("display", active=gds.FuncProp(_prop, lambda x: x == "shape"))
361
+
362
+ # Interpolation parameters
363
+ method = gds.ChoiceItem(
364
+ _("Interpolation method"),
365
+ Interpolation2DMethod,
366
+ default=Interpolation2DMethod.LINEAR,
367
+ )
368
+ fill_value = gds.FloatItem(
369
+ _("Fill value"),
370
+ default=None,
371
+ help=_(
372
+ "Value to use for points outside the input image domain. "
373
+ "If None, uses NaN for extrapolation."
374
+ ),
375
+ check=False,
376
+ )
377
+
378
+ def update_from_obj(self, obj: ImageObj) -> None:
379
+ """Update parameters from an image object."""
380
+ if self.xmin is None:
381
+ self.xmin = obj.x0
382
+ if self.xmax is None:
383
+ self.xmax = obj.x0 + obj.width
384
+ if self.ymin is None:
385
+ self.ymin = obj.y0
386
+ if self.ymax is None:
387
+ self.ymax = obj.y0 + obj.height
388
+ if self.dx is None:
389
+ self.dx = obj.dx
390
+ if self.dy is None:
391
+ self.dy = obj.dy
392
+ if self.width is None:
393
+ self.width = obj.data.shape[1]
394
+ if self.height is None:
395
+ self.height = obj.data.shape[0]
396
+
397
+
398
+ @computation_function()
399
+ def resampling(src: ImageObj, p: Resampling2DParam) -> ImageObj:
400
+ """Resample image to new coordinate grid using interpolation
401
+
402
+ Args:
403
+ src: source image
404
+ p: resampling parameters
405
+
406
+ Returns:
407
+ Resampled image object
408
+
409
+ Raises:
410
+ ValueError: if source image has non-uniform coordinates
411
+ """
412
+ if not src.is_uniform_coords:
413
+ raise ValueError("Source image must have uniform coordinates for resampling")
414
+
415
+ # Set output range - use source image bounds if not specified
416
+ output_xmin = p.xmin if p.xmin is not None else src.x0
417
+ output_xmax = p.xmax if p.xmax is not None else src.x0 + src.width
418
+ output_ymin = p.ymin if p.ymin is not None else src.y0
419
+ output_ymax = p.ymax if p.ymax is not None else src.y0 + src.height
420
+
421
+ # Calculate output grid dimensions and spacing
422
+ output_width_phys = output_xmax - output_xmin
423
+ output_height_phys = output_ymax - output_ymin
424
+
425
+ # Determine output grid parameters
426
+ method: Interpolation2DMethod = p.method
427
+ if p.mode == "dxy":
428
+ # Calculate dimensions from pixel sizes
429
+ if p.dx is None or p.dy is None:
430
+ raise ValueError("dx and dy must be specified in pixel size mode")
431
+ output_width = int(np.ceil(output_width_phys / p.dx))
432
+ output_height = int(np.ceil(output_height_phys / p.dy))
433
+ output_dx = p.dx
434
+ output_dy = p.dy
435
+ fill_suffix = f", fill_value={p.fill_value}" if p.fill_value is not None else ""
436
+ suffix = f"method={method.value}, dx={p.dx:.3f}, dy={p.dy:.3f}{fill_suffix}"
437
+ else:
438
+ # Use specified shape
439
+ if p.width is None or p.height is None:
440
+ raise ValueError("width and height must be specified in shape mode")
441
+ output_width = p.width
442
+ output_height = p.height
443
+ output_dx = output_width_phys / p.width if p.width > 0 else src.dx
444
+ output_dy = output_height_phys / p.height if p.height > 0 else src.dy
445
+ fill_suffix = f", fill_value={p.fill_value}" if p.fill_value is not None else ""
446
+ suffix = f"method={method.value}, size=({p.width}x{p.height}){fill_suffix}"
447
+
448
+ # Create destination image
449
+ dst = dst_1_to_1(src, "resample", suffix)
450
+
451
+ # Output coordinates (physical) - ensure we sample pixel centers, not boundaries
452
+ # For an image spanning [xmin, xmax], we want to sample at pixel centers
453
+ # The pixel centers should be distributed within the range,
454
+ # not including the exact endpoints
455
+ if output_width > 1:
456
+ out_x = np.linspace(
457
+ output_xmin + output_dx / 2, output_xmax - output_dx / 2, output_width
458
+ )
459
+ else:
460
+ out_x = np.array([(output_xmin + output_xmax) / 2])
461
+
462
+ if output_height > 1:
463
+ out_y = np.linspace(
464
+ output_ymin + output_dy / 2, output_ymax - output_dy / 2, output_height
465
+ )
466
+ else:
467
+ out_y = np.array([(output_ymin + output_ymax) / 2])
468
+
469
+ # Create meshgrids
470
+ out_X, out_Y = np.meshgrid(out_x, out_y, indexing="xy")
471
+
472
+ # Convert interpolation method to scipy parameter
473
+ if method == Interpolation2DMethod.LINEAR:
474
+ order = 1
475
+ elif method == Interpolation2DMethod.CUBIC:
476
+ order = 3
477
+ elif method == Interpolation2DMethod.NEAREST:
478
+ order = 0
479
+ else:
480
+ order = 1 # fallback to linear
481
+
482
+ # Convert physical coordinates to source image indices
483
+ src_i = (out_X - src.x0) / src.dx
484
+ src_j = (out_Y - src.y0) / src.dy
485
+
486
+ # Perform interpolation using map_coordinates
487
+ # Note: map_coordinates expects (j, i) order (row, col)
488
+ coordinates = np.array([src_j.ravel(), src_i.ravel()])
489
+
490
+ # Determine fill value for interpolation
491
+ cval = p.fill_value if p.fill_value is not None else np.nan
492
+
493
+ # For NaN fill values, we need to work with float data to preserve NaN
494
+ # Convert to float if necessary to allow NaN representation
495
+ if np.isnan(cval) and not np.issubdtype(src.data.dtype, np.floating):
496
+ input_data = src.data.astype(np.float64)
497
+ else:
498
+ input_data = src.data
499
+
500
+ # Interpolate
501
+ resampled_data = spi.map_coordinates(
502
+ input_data, coordinates, order=order, mode="constant", cval=cval, prefilter=True
503
+ ).reshape(output_height, output_width)
504
+
505
+ # Set output data and coordinate system
506
+ dst.data = resampled_data
507
+ dst.set_uniform_coords(output_dx, output_dy, output_xmin, output_ymin)
508
+
509
+ return dst
510
+
511
+
512
+ class UniformCoordsParam(gds.DataSet):
513
+ """Uniform coordinates parameters"""
514
+
515
+ x0 = gds.FloatItem("X<sub>0</sub>", default=0.0, help=_("Origin X-axis coordinate"))
516
+ y0 = gds.FloatItem("Y<sub>0</sub>", default=0.0, help=_("Origin Y-axis coordinate"))
517
+ dx = gds.FloatItem("Δx", default=1.0, help=_("Pixel size along X-axis"))
518
+ dy = gds.FloatItem("Δy", default=1.0, help=_("Pixel size along Y-axis"))
519
+
520
+ def update_from_obj(self, obj: ImageObj) -> None:
521
+ """Update default values from image object's non-uniform coordinates.
522
+
523
+ This method extracts uniform coordinate approximations from non-uniform
524
+ coordinate arrays, handling numerical precision issues that may arise
525
+ from arrays created using linspace.
526
+
527
+ Args:
528
+ obj: Image object with non-uniform coordinates
529
+ """
530
+ if obj.is_uniform_coords:
531
+ # Already uniform, just copy the values
532
+ self.x0 = obj.x0
533
+ self.y0 = obj.y0
534
+ self.dx = obj.dx
535
+ self.dy = obj.dy
536
+ else:
537
+ # Extract from non-uniform coordinates
538
+ if obj.xcoords is not None and len(obj.xcoords) >= 2:
539
+ self.x0 = float(obj.xcoords[0])
540
+ # Calculate dx with rounding to handle numerical precision
541
+ dx_raw = (obj.xcoords[-1] - obj.xcoords[0]) / (len(obj.xcoords) - 1)
542
+ # Round to reasonable precision (12 decimal places)
543
+ self.dx = float(np.round(dx_raw, 12))
544
+ else:
545
+ self.x0 = 0.0
546
+ self.dx = 1.0
547
+
548
+ if obj.ycoords is not None and len(obj.ycoords) >= 2:
549
+ self.y0 = float(obj.ycoords[0])
550
+ # Calculate dy with rounding to handle numerical precision
551
+ dy_raw = (obj.ycoords[-1] - obj.ycoords[0]) / (len(obj.ycoords) - 1)
552
+ # Round to reasonable precision (12 decimal places)
553
+ self.dy = float(np.round(dy_raw, 12))
554
+ else:
555
+ self.y0 = 0.0
556
+ self.dy = 1.0
557
+
558
+
559
+ @computation_function()
560
+ def set_uniform_coords(src: ImageObj, p: UniformCoordsParam) -> ImageObj:
561
+ """Convert image to uniform coordinate system
562
+
563
+ Args:
564
+ src: input image object
565
+ p: uniform coordinates parameters
566
+
567
+ Returns:
568
+ Output image object with uniform coordinates
569
+ """
570
+ dst = dst_1_to_1(src, "uniform_coords", f"dx={p.dx}, dy={p.dy}")
571
+ dst.set_uniform_coords(p.dx, p.dy, p.x0, p.y0)
572
+ return dst
573
+
574
+
575
+ class XYZCalibrateParam(gds.DataSet):
576
+ """Image polynomial calibration parameters"""
577
+
578
+ axes = (("x", _("X-axis")), ("y", _("Y-axis")), ("z", _("Z-axis")))
579
+ axis = gds.ChoiceItem(_("Calibrate"), axes, default="z")
580
+ a0 = gds.FloatItem("a<sub>0</sub>", default=0.0, help=_("Constant term"))
581
+ a1 = gds.FloatItem("a<sub>1</sub>", default=1.0, help=_("Linear term"))
582
+ a2 = gds.FloatItem("a<sub>2</sub>", default=0.0, help=_("Quadratic term"))
583
+ a3 = gds.FloatItem("a<sub>3</sub>", default=0.0, help=_("Cubic term"))
584
+
585
+
586
+ @computation_function()
587
+ def calibration(src: ImageObj, p: XYZCalibrateParam) -> ImageObj:
588
+ """Compute polynomial calibration
589
+
590
+ Applies polynomial transformation: dst = a0 + a1*src + a2*src² + a3*src³
591
+
592
+ Args:
593
+ src: input image object
594
+ p: calibration parameters
595
+
596
+ Returns:
597
+ Output image object
598
+ """
599
+ # Build polynomial description for metadata
600
+ terms = []
601
+ if p.a0 != 0.0:
602
+ terms.append(f"{p.a0}")
603
+ if p.a1 != 0.0:
604
+ terms.append(f"{p.a1}*{p.axis}" if p.a1 != 1.0 else p.axis)
605
+ if p.a2 != 0.0:
606
+ terms.append(f"{p.a2}*{p.axis}²")
607
+ if p.a3 != 0.0:
608
+ terms.append(f"{p.a3}*{p.axis}³")
609
+ poly_str = "+".join(terms) if terms else "0"
610
+
611
+ dst = dst_1_to_1(src, "calibration", f"{p.axis}={poly_str}")
612
+
613
+ shape = src.data.shape
614
+
615
+ if p.axis == "z":
616
+ # Apply polynomial to data values
617
+ data = src.data.astype(float)
618
+ dst.data = p.a0 + p.a1 * data + p.a2 * data**2 + p.a3 * data**3
619
+ restore_data_outside_roi(dst, src)
620
+ elif p.axis == "x":
621
+ # For X-axis, polynomial calibration requires non-uniform coordinates
622
+ # (unless it's linear but we don't special case that here)
623
+ if src.is_uniform_coords:
624
+ # Generate uniform coordinates array
625
+ x_uniform = src.x0 + np.arange(src.data.shape[1]) * src.dx
626
+ # Apply polynomial transformation
627
+ x_new = p.a0 + p.a1 * x_uniform + p.a2 * x_uniform**2 + p.a3 * x_uniform**3
628
+ # Set non-uniform coordinates
629
+ ycoords = np.linspace(src.y0, src.y0 + src.dy * (shape[0] - 1), shape[0])
630
+ dst.set_coords(x_new, ycoords)
631
+ else:
632
+ # Apply polynomial to existing non-uniform coordinates
633
+ x_new = (
634
+ p.a0
635
+ + p.a1 * src.xcoords
636
+ + p.a2 * src.xcoords**2
637
+ + p.a3 * src.xcoords**3
638
+ )
639
+ dst.set_coords(x_new, dst.ycoords)
640
+ elif p.axis == "y":
641
+ # For Y-axis, polynomial calibration requires non-uniform coordinates
642
+ if src.is_uniform_coords:
643
+ # Generate uniform coordinates array
644
+ y_uniform = src.y0 + np.arange(src.data.shape[0]) * src.dy
645
+ # Apply polynomial transformation
646
+ y_new = p.a0 + p.a1 * y_uniform + p.a2 * y_uniform**2 + p.a3 * y_uniform**3
647
+ # Set non-uniform coordinates
648
+ xcoords = np.linspace(src.x0, src.x0 + src.dx * (shape[1] - 1), shape[1])
649
+ dst.set_coords(xcoords, y_new)
650
+ else:
651
+ # Apply polynomial to existing non-uniform coordinates
652
+ y_new = (
653
+ p.a0
654
+ + p.a1 * src.ycoords
655
+ + p.a2 * src.ycoords**2
656
+ + p.a3 * src.ycoords**3
657
+ )
658
+ dst.set_coords(dst.xcoords, y_new)
659
+ else: # Should not happen
660
+ raise ValueError(f"Unknown axis: {p.axis}") # pragma: no cover
661
+ return dst