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,260 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Fourier analysis module
5
+ -----------------------
6
+
7
+ This module provides 2D Fourier transform utilities and frequency domain operations
8
+ for image processing.
9
+
10
+ Features include:
11
+
12
+ - 2D FFT/IFFT functions with optional shifting
13
+ - Spectral analysis (magnitude spectrum, phase spectrum, power spectral density)
14
+ - Frequency domain filtering and deconvolution
15
+ - Zero padding utilities for FFT operations
16
+
17
+ These tools support various frequency domain image processing operations
18
+ including filtering, spectral analysis, and deconvolution.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import warnings
24
+
25
+ import numpy as np
26
+ import scipy.signal as sps
27
+
28
+ from sigima.tools.checks import check_2d_array, normalize_kernel
29
+
30
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
31
+
32
+
33
+ @check_2d_array
34
+ def fft2d(z: np.ndarray, shift: bool = True) -> np.ndarray:
35
+ """Compute FFT of complex array `z`
36
+
37
+ Args:
38
+ z: Input data
39
+ shift: Shift zero frequency to center (default: True)
40
+
41
+ Returns:
42
+ FFT of input data
43
+ """
44
+ z1 = np.fft.fft2(z)
45
+ if shift:
46
+ z1 = np.fft.fftshift(z1)
47
+ return z1
48
+
49
+
50
+ @check_2d_array
51
+ def ifft2d(z: np.ndarray, shift: bool = True) -> np.ndarray:
52
+ """Compute inverse FFT of complex array `z`
53
+
54
+ Args:
55
+ z: Input data
56
+ shift: Shift zero frequency to center (default: True)
57
+
58
+ Returns:
59
+ Inverse FFT of input data
60
+ """
61
+ if shift:
62
+ z = np.fft.ifftshift(z)
63
+ z1 = np.fft.ifft2(z)
64
+ return z1
65
+
66
+
67
+ @check_2d_array
68
+ def magnitude_spectrum(z: np.ndarray, log_scale: bool = False) -> np.ndarray:
69
+ """Compute magnitude spectrum of complex array `z`
70
+
71
+ Args:
72
+ z: Input data
73
+ log_scale: Use log scale (default: False)
74
+
75
+ Returns:
76
+ Magnitude spectrum of input data
77
+ """
78
+ z1 = np.abs(fft2d(z))
79
+ if log_scale:
80
+ z1 = 20 * np.log10(z1.clip(1e-10))
81
+ return z1
82
+
83
+
84
+ @check_2d_array
85
+ def phase_spectrum(z: np.ndarray) -> np.ndarray:
86
+ """Compute phase spectrum of complex array `z`
87
+
88
+ Args:
89
+ z: Input data
90
+
91
+ Returns:
92
+ Phase spectrum of input data (in degrees)
93
+ """
94
+ return np.rad2deg(np.angle(fft2d(z)))
95
+
96
+
97
+ @check_2d_array
98
+ def psd(z: np.ndarray, log_scale: bool = False) -> np.ndarray:
99
+ """Compute power spectral density of complex array `z`
100
+
101
+ Args:
102
+ z: Input data
103
+ log_scale: Use log scale (default: False)
104
+
105
+ Returns:
106
+ Power spectral density of input data
107
+ """
108
+ z1 = np.abs(fft2d(z)) ** 2
109
+ if log_scale:
110
+ z1 = 10 * np.log10(z1.clip(1e-10))
111
+ return z1
112
+
113
+
114
+ @check_2d_array
115
+ def gaussian_freq_filter(
116
+ data: np.ndarray, f0: float = 0.1, sigma: float = 0.05
117
+ ) -> np.ndarray:
118
+ """
119
+ Apply a 2D Gaussian bandpass filter in the frequency domain to an image.
120
+
121
+ This function performs a 2D Fast Fourier Transform (FFT) on the input image,
122
+ applies a Gaussian filter centered at frequency `f0` with standard deviation `sigma`
123
+ (both expressed in cycles per pixel), and then transforms the result back to the
124
+ spatial domain.
125
+
126
+ Args:
127
+ data: Input image data.
128
+ f0: Center frequency of the Gaussian filter (cycles/pixel).
129
+ sigma: Standard deviation of the Gaussian filter (cycles/pixel).
130
+
131
+ Returns:
132
+ The filtered image.
133
+ """
134
+ n, m = data.shape
135
+ fx = np.fft.fftshift(np.fft.fftfreq(m, d=1))
136
+ fy = np.fft.fftshift(np.fft.fftfreq(n, d=1))
137
+ fx_grid, fy_grid = np.meshgrid(fx, fy)
138
+ freq_radius = np.hypot(fx_grid, fy_grid)
139
+
140
+ # Create the 2D Gaussian bandpass filter
141
+ gaussian_filter = np.exp(-0.5 * ((freq_radius - f0) / sigma) ** 2)
142
+
143
+ # Apply FFT, filter in frequency domain, and inverse FFT
144
+ fft_data = fft2d(data, shift=True)
145
+ filtered_fft = fft_data * gaussian_filter
146
+ zout = ifft2d(filtered_fft, shift=True)
147
+ return zout.real
148
+
149
+
150
+ @check_2d_array(non_constant=True)
151
+ def convolve(
152
+ data: np.ndarray,
153
+ kernel: np.ndarray,
154
+ normalize_kernel_flag: bool = True,
155
+ ) -> np.ndarray:
156
+ """
157
+ Perform 2D convolution with a kernel using scipy.signal.convolve.
158
+
159
+ This function adds optional kernel normalization to the standard scipy convolution.
160
+
161
+ Args:
162
+ data: Input image (2D array).
163
+ kernel: Convolution kernel.
164
+ normalize_kernel_flag: If True, normalize kernel so that ``kernel.sum() == 1``
165
+ to preserve image brightness.
166
+
167
+ Returns:
168
+ Convolved image (same shape as input).
169
+
170
+ Raises:
171
+ ValueError: If kernel is empty or null.
172
+ """
173
+ if kernel.size == 0 or not np.any(kernel):
174
+ raise ValueError("Convolution kernel cannot be null.")
175
+
176
+ # Optionally normalize the kernel
177
+ if normalize_kernel_flag:
178
+ kernel = normalize_kernel(kernel)
179
+
180
+ # Use scipy.signal.convolve with 'same' mode to preserve image size
181
+ return sps.convolve(data, kernel, mode="same", method="auto")
182
+
183
+
184
+ @check_2d_array(non_constant=True)
185
+ def deconvolve(
186
+ data: np.ndarray,
187
+ kernel: np.ndarray,
188
+ reg: float = 0.0,
189
+ boundary: str = "edge",
190
+ normalize_kernel_flag: bool = True,
191
+ ) -> np.ndarray:
192
+ """
193
+ Perform 2D FFT deconvolution with correct 'same' geometry (no shift).
194
+
195
+ The kernel (PSF) must be centered (impulse at center for identity kernel).
196
+ Odd kernel sizes are recommended.
197
+
198
+ Args:
199
+ data: Input image (2D array).
200
+ kernel: Point Spread Function (PSF), centered.
201
+ reg: Regularization parameter (if >0, Wiener/Tikhonov inverse:
202
+ ``H* / (|H|^2 + reg))``.
203
+ boundary: Padding mode ('edge' for constant plateau,
204
+ 'reflect' for symmetric mirror).
205
+ normalize_kernel_flag: If True, normalize kernel so that ``kernel.sum() == 1``
206
+ to preserve image brightness.
207
+
208
+ Returns:
209
+ Deconvolved image (same shape as input).
210
+
211
+ Raises:
212
+ ValueError: If kernel is empty or null.
213
+ """
214
+ if kernel.size == 0 or not np.any(kernel):
215
+ raise ValueError("Deconvolution kernel cannot be null.")
216
+
217
+ # Optionally normalize the kernel
218
+ if normalize_kernel_flag:
219
+ kernel = normalize_kernel(kernel)
220
+
221
+ H, W = data.shape
222
+ kh, kw = kernel.shape
223
+
224
+ if kh % 2 == 0 or kw % 2 == 0:
225
+ # Warning for even-sized kernels (off-by-one in centered FFT)
226
+ warnings.warn(
227
+ "Deconvolution kernel should have odd dimensions for centered FFT."
228
+ )
229
+
230
+ # Symmetric padding for centered 'same' convolution
231
+ top = kh // 2
232
+ bottom = kh - 1 - top
233
+ left = kw // 2
234
+ right = kw - 1 - left
235
+ data_pad = np.pad(data, ((top, bottom), (left, right)), mode=boundary)
236
+ Hp, Wp = data_pad.shape # = H+kh-1, W+kw-1
237
+
238
+ # Centered PSF to OTF conversion (avoid off-by-one for even sizes)
239
+ kernel_pad = np.zeros_like(data_pad, dtype=float)
240
+ r0 = Hp // 2 - kh // 2
241
+ c0 = Wp // 2 - kw // 2
242
+ kernel_pad[r0 : r0 + kh, c0 : c0 + kw] = kernel
243
+ H_otf = np.fft.fft2(np.fft.ifftshift(kernel_pad)) # center → (0,0)
244
+
245
+ # FFT of padded image (no shift)
246
+ Z = np.fft.fft2(data_pad)
247
+
248
+ # Frequency domain inversion
249
+ if reg > 0.0:
250
+ Hc = np.conj(H_otf)
251
+ X = Z * Hc / (np.abs(H_otf) ** 2 + float(reg))
252
+ else:
253
+ eps = 1e-12
254
+ X = Z / (H_otf + eps)
255
+
256
+ data_true_pad = np.fft.ifft2(X).real
257
+
258
+ # Central crop to restore original geometry
259
+ out = data_true_pad[top : top + H, left : left + W]
260
+ return out
@@ -0,0 +1,190 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Geometric analysis module
5
+ -------------------------
6
+
7
+ This module provides functions for geometric analysis of images, including
8
+ centroid detection, shape fitting, and spatial measurements.
9
+
10
+ Features include:
11
+
12
+ - Various centroid detection algorithms (Fourier-based, projected profile,
13
+ automatic selection)
14
+ - Enclosing circle calculation for thresholded regions
15
+ - Radial profile extraction around specified centers
16
+ - Absolute level calculation from relative thresholds
17
+
18
+ These tools support precise geometric measurements and shape analysis
19
+ for scientific and technical image analysis applications.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Literal
25
+
26
+ import numpy as np
27
+ from skimage import measure
28
+
29
+ from sigima.tools.checks import check_2d_array
30
+ from sigima.tools.image.preprocessing import get_absolute_level
31
+
32
+
33
+ @check_2d_array
34
+ def get_centroid_fourier(data: np.ndarray) -> tuple[float, float]:
35
+ """Return image centroid using Fourier algorithm
36
+
37
+ Args:
38
+ data: Input data
39
+
40
+ Returns:
41
+ Centroid coordinates (row, col)
42
+ """
43
+ # Fourier transform method as discussed by Weisshaar et al.
44
+ # (http://www.mnd-umwelttechnik.fh-wiesbaden.de/pig/weisshaar_u5.pdf)
45
+ rows, cols = data.shape
46
+ if rows == 1 or cols == 1:
47
+ return 0, 0
48
+
49
+ i = np.arange(0, rows).reshape(1, rows)
50
+ sin_a = np.sin((i - 1) * 2 * np.pi / (rows - 1)).T
51
+ cos_a = np.cos((i - 1) * 2 * np.pi / (rows - 1)).T
52
+
53
+ j = np.arange(0, cols).reshape(cols, 1)
54
+ sin_b = np.sin((j - 1) * 2 * np.pi / (cols - 1)).T
55
+ cos_b = np.cos((j - 1) * 2 * np.pi / (cols - 1)).T
56
+
57
+ a = np.nansum((cos_a * data))
58
+ b = np.nansum((sin_a * data))
59
+ c = np.nansum((data * cos_b))
60
+ d = np.nansum((data * sin_b))
61
+
62
+ rphi = (0 if b > 0 else 2 * np.pi) if a > 0 else np.pi
63
+ cphi = (0 if d > 0 else 2 * np.pi) if c > 0 else np.pi
64
+
65
+ if a * c == 0.0:
66
+ return 0, 0
67
+
68
+ row = (np.arctan(b / a) + rphi) * (rows - 1) / (2 * np.pi) + 1
69
+ col = (np.arctan(d / c) + cphi) * (cols - 1) / (2 * np.pi) + 1
70
+
71
+ row = np.nan if row is np.ma.masked else row
72
+ col = np.nan if col is np.ma.masked else col
73
+
74
+ return row, col
75
+
76
+
77
+ @check_2d_array
78
+ def get_projected_profile_centroid(
79
+ data: np.ndarray, smooth_ratio: float = 1 / 40, method: str = "median"
80
+ ) -> tuple[float, float]:
81
+ """
82
+ Estimate centroid from smoothed 1D projections.
83
+
84
+ Args:
85
+ data: 2D image array
86
+ smooth_ratio: Ratio of smoothing window size (default: 1/40)
87
+ method: 'median' (default) or 'barycenter'
88
+
89
+ Returns:
90
+ (y, x) coordinates
91
+ """
92
+ x_profile = data.sum(axis=0)
93
+ y_profile = data.sum(axis=1)
94
+ window_size = max(1, int(min(data.shape) * smooth_ratio))
95
+ kernel = np.ones(window_size) / window_size
96
+ x_profile = np.convolve(x_profile, kernel, mode="same")
97
+ y_profile = np.convolve(y_profile, kernel, mode="same")
98
+ x_profile -= np.min(x_profile)
99
+ y_profile -= np.min(y_profile)
100
+
101
+ if method == "median":
102
+ x_integral = np.cumsum(x_profile)
103
+ y_integral = np.cumsum(y_profile)
104
+ x_center = np.interp(
105
+ 0.5 * x_integral[-1], x_integral, np.arange(len(x_integral))
106
+ )
107
+ y_center = np.interp(
108
+ 0.5 * y_integral[-1], y_integral, np.arange(len(y_integral))
109
+ )
110
+ elif method == "barycenter": # pragma: no cover
111
+ # (ignored for coverage because median gives better results)
112
+ x_center = np.sum(np.arange(len(x_profile)) * x_profile) / np.sum(x_profile)
113
+ y_center = np.sum(np.arange(len(y_profile)) * y_profile) / np.sum(y_profile)
114
+ else:
115
+ raise ValueError("Unknown method: choose 'median' or 'barycenter'")
116
+
117
+ return float(y_center), float(x_center)
118
+
119
+
120
+ @check_2d_array
121
+ def get_centroid_auto(
122
+ data: np.ndarray,
123
+ return_method: bool = False,
124
+ ) -> tuple[float, float] | tuple[float, float, Literal["fourier", "skimage"]]:
125
+ """
126
+ Automatically select the most reliable centroid estimation method:
127
+ - Prefer Fourier if it is more consistent with the projected median.
128
+ - Fallback to scikit-image centroid if Fourier is less coherent.
129
+
130
+ Args:
131
+ data: 2D image array.
132
+ return_method: If True, also return the name of the selected method.
133
+
134
+ Returns:
135
+ (row, col): Estimated centroid coordinates (float).
136
+ Optionally, the selected method as string: "fourier" or "skimage".
137
+ """
138
+ try:
139
+ row_f, col_f = get_centroid_fourier(data)
140
+ except Exception: # pylint: disable=broad-except
141
+ row_f, col_f = float("nan"), float("nan")
142
+
143
+ row_m, col_m = get_projected_profile_centroid(data, method="median")
144
+ row_s, col_s = measure.centroid(data)
145
+
146
+ dist_f = np.hypot(row_f - row_m, col_f - col_m)
147
+ dist_s = np.hypot(row_s - row_m, col_s - col_m)
148
+
149
+ if not (np.isnan(row_f) or np.isnan(col_f)) and dist_f < dist_s:
150
+ result = (row_f, col_f)
151
+ method = "fourier"
152
+ else:
153
+ result = (row_s, col_s)
154
+ method = "skimage"
155
+
156
+ return result + (method,) if return_method else result
157
+
158
+
159
+ @check_2d_array(non_constant=True)
160
+ def get_enclosing_circle(
161
+ data: np.ndarray, level: float = 0.5
162
+ ) -> tuple[int, int, float]:
163
+ """Return (x, y, radius) for the circle contour enclosing image
164
+ values above threshold relative level (.5 means FWHM)
165
+
166
+ Args:
167
+ data: Input data
168
+ level: Relative level (default: 0.5)
169
+
170
+ Returns:
171
+ A tuple (x, y, radius)
172
+
173
+ Raises:
174
+ ValueError: No contour was found
175
+ """
176
+ data_th = data.copy()
177
+ data_th[data <= get_absolute_level(data, level)] = 0.0
178
+ contours = measure.find_contours(data_th)
179
+ model = measure.CircleModel()
180
+ result = None
181
+ max_radius = 1.0
182
+ for contour in contours:
183
+ if model.estimate(contour):
184
+ yc, xc, radius = model.params
185
+ if radius > max_radius:
186
+ result = (int(xc), int(yc), radius)
187
+ max_radius = radius
188
+ if result is None:
189
+ raise ValueError("No contour was found")
190
+ return result
@@ -0,0 +1,165 @@
1
+ """
2
+ Signal/Image Preprocessing
3
+ --------------------------
4
+
5
+ This module contains utility functions for preprocessing and transforming image data:
6
+
7
+ - Binning and scaling operations
8
+ - Zero padding for Fourier analysis
9
+ - Utility functions for data transformation
10
+
11
+ .. note::
12
+ All functions in this module are also available directly in the parent
13
+ `sigima.tools.image` package.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Literal
19
+
20
+ import numpy as np
21
+ import scipy.spatial as spt
22
+ from numpy import ma
23
+
24
+ from sigima.enums import BinningOperation
25
+ from sigima.tools.checks import check_2d_array
26
+
27
+
28
+ def get_absolute_level(data: np.ndarray, level: float) -> float:
29
+ """Get absolute level from relative level
30
+
31
+ Args:
32
+ data: Input data
33
+ level: Relative level (0.0 to 1.0)
34
+
35
+ Returns:
36
+ Absolute level
37
+
38
+ Raises:
39
+ ValueError: If level is not a float between 0.0 and 1.0
40
+ """
41
+ if not isinstance(level, (int, float)) or level < 0.0 or level > 1.0:
42
+ raise ValueError("Level must be a number between 0.0 and 1.0")
43
+ return np.nanmin(data) + level * (np.nanmax(data) - np.nanmin(data))
44
+
45
+
46
+ def distance_matrix(coords: list) -> np.ndarray:
47
+ """Return distance matrix from coords
48
+
49
+ Args:
50
+ coords: List of coordinates
51
+
52
+ Returns:
53
+ Distance matrix
54
+ """
55
+ return np.triu(spt.distance.cdist(coords, coords, "euclidean"))
56
+
57
+
58
+ @check_2d_array
59
+ def binning(
60
+ data: np.ndarray,
61
+ sx: int,
62
+ sy: int,
63
+ operation: BinningOperation | str,
64
+ dtype=None,
65
+ ) -> np.ndarray:
66
+ """Perform image pixel binning
67
+
68
+ Args:
69
+ data: Input data
70
+ sx: Binning size along x (number of pixels to bin together)
71
+ sy: Binning size along y (number of pixels to bin together)
72
+ operation: Binning operation
73
+ dtype: Output data type (default: None, i.e. same as input)
74
+
75
+ Returns:
76
+ Binned data
77
+ """
78
+ # Convert enum to string value if needed
79
+ if isinstance(operation, BinningOperation):
80
+ operation = operation.value
81
+
82
+ ny, nx = data.shape
83
+ shape = (ny // sy, sy, nx // sx, sx)
84
+ try:
85
+ bdata = data[: ny - ny % sy, : nx - nx % sx].reshape(shape)
86
+ except ValueError as err:
87
+ raise ValueError("Binning is not a multiple of image dimensions") from err
88
+ if operation == "sum":
89
+ bdata = np.array(bdata, dtype=float).sum(axis=(-1, 1))
90
+ elif operation == "average":
91
+ bdata = bdata.mean(axis=(-1, 1))
92
+ elif operation == "median":
93
+ bdata = ma.median(bdata, axis=(-1, 1))
94
+ elif operation == "min":
95
+ bdata = bdata.min(axis=(-1, 1))
96
+ elif operation == "max":
97
+ bdata = bdata.max(axis=(-1, 1))
98
+ else:
99
+ valid = ", ".join(op.value for op in BinningOperation)
100
+ raise ValueError(f"Invalid operation {operation} (valid values: {valid})")
101
+ return np.array(bdata, dtype=data.dtype if dtype is None else np.dtype(dtype))
102
+
103
+
104
+ @check_2d_array(non_constant=True)
105
+ def scale_data_to_min_max(
106
+ data: np.ndarray, zmin: float | int, zmax: float | int
107
+ ) -> np.ndarray:
108
+ """Scale array `data` to fit [zmin, zmax] dynamic range
109
+
110
+ Args:
111
+ data: Input data
112
+ zmin: Minimum value of output data
113
+ zmax: Maximum value of output data
114
+
115
+ Returns:
116
+ Scaled data
117
+ """
118
+ dmin, dmax = np.nanmin(data), np.nanmax(data)
119
+ if dmin == zmin and dmax == zmax:
120
+ return data
121
+ fdata = np.array(data, dtype=float)
122
+ fdata -= dmin
123
+ fdata *= float(zmax - zmin) / (dmax - dmin)
124
+ fdata += float(zmin)
125
+ return np.array(fdata, data.dtype)
126
+
127
+
128
+ @check_2d_array
129
+ def zero_padding(
130
+ data: np.ndarray,
131
+ rows: int = 0,
132
+ cols: int = 0,
133
+ position: Literal["bottom-right", "around"] = "bottom-right",
134
+ ) -> np.ndarray:
135
+ """
136
+ Zero-pad a 2D image by adding rows and/or columns.
137
+
138
+ Args:
139
+ data: 2D input image (grayscale)
140
+ rows: Number of rows to add in total (default: 0)
141
+ cols: Number of columns to add in total (default: 0)
142
+ position: Padding placement strategy:
143
+ - "bottom-right": all padding is added to the bottom and right
144
+ - "around": padding is split equally on top/bottom and left/right
145
+
146
+ Returns:
147
+ The padded 2D image as a NumPy array.
148
+
149
+ Raises:
150
+ ValueError: If the input is not a 2D array or if padding values are negative.
151
+ """
152
+ if rows < 0 or cols < 0:
153
+ raise ValueError("Padding values must be non-negative")
154
+
155
+ if position == "bottom-right":
156
+ pad_width = ((0, rows), (0, cols))
157
+ elif position == "around":
158
+ pad_width = (
159
+ (rows // 2, rows - rows // 2),
160
+ (cols // 2, cols - cols // 2),
161
+ )
162
+ else:
163
+ raise ValueError(f"Invalid position: {position}")
164
+
165
+ return np.pad(data, pad_width, mode="constant", constant_values=0)
@@ -0,0 +1,86 @@
1
+ """
2
+ Signal Processing Tools (:mod:`sigima.tools.signal`)
3
+ ----------------------------------------------------
4
+
5
+ This package contains signal processing tools, which are organized into subpackages
6
+ according to their purpose:
7
+
8
+ - :mod:`sigima.tools.signal.dynamic`: Dynamic parameter extraction
9
+ - :mod:`sigima.tools.signal.features`: Feature extraction algorithms
10
+ - :mod:`sigima.tools.signal.pulse`: Curve fitting models
11
+ - :mod:`sigima.tools.signal.fourier`: Fourier analysis algorithms
12
+ - :mod:`sigima.tools.signal.interpolation`: Interpolation algorithms
13
+ - :mod:`sigima.tools.signal.peakdetection`: Peak detection algorithms
14
+ - :mod:`sigima.tools.signal.pulse`: Pulse analysis algorithms
15
+ - :mod:`sigima.tools.signal.scaling`: Scaling algorithms
16
+ - :mod:`sigima.tools.signal.stability`: Stability analysis algorithms
17
+ - :mod:`sigima.tools.signal.windowing`: Windowing algorithms
18
+
19
+
20
+ Dynamic Parameter Extraction
21
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
22
+
23
+ .. automodule:: sigima.tools.signal.dynamic
24
+ :members:
25
+
26
+ Feature Extraction Algorithms
27
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28
+
29
+ .. automodule:: sigima.tools.signal.features
30
+ :members:
31
+
32
+ Filtering Functions
33
+ ^^^^^^^^^^^^^^^^^^^
34
+
35
+ .. automodule:: sigima.tools.signal.filtering
36
+ :members:
37
+
38
+ Curve Fitting Models
39
+ ^^^^^^^^^^^^^^^^^^^^
40
+
41
+ .. automodule:: sigima.tools.signal.fitting
42
+ :members:
43
+
44
+ Fourier Analysis Algorithms
45
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
46
+
47
+ .. automodule:: sigima.tools.signal.fourier
48
+ :members:
49
+
50
+ Interpolation Algorithms
51
+ ^^^^^^^^^^^^^^^^^^^^^^^^
52
+
53
+ .. automodule:: sigima.tools.signal.interpolation
54
+ :members:
55
+
56
+ Peak Detection Algorithms
57
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
58
+
59
+ .. automodule:: sigima.tools.signal.peakdetection
60
+ :members:
61
+
62
+ Pulse Analysis Algorithms
63
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
64
+
65
+ .. automodule:: sigima.tools.signal.pulse
66
+ :members:
67
+
68
+ Scaling Algorithms
69
+ ^^^^^^^^^^^^^^^^^^
70
+
71
+ .. automodule:: sigima.tools.signal.scaling
72
+ :members:
73
+
74
+ Stability Analysis Algorithms
75
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
76
+
77
+ .. automodule:: sigima.tools.signal.stability
78
+ :members:
79
+
80
+ Windowing Algorithms
81
+ ^^^^^^^^^^^^^^^^^^^^
82
+
83
+ .. automodule:: sigima.tools.signal.windowing
84
+ :members:
85
+
86
+ """