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,458 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Extraction computation module
5
+ -----------------------------
6
+
7
+ This module provides functions to extract sub-regions
8
+ and intensity profiles from images.
9
+
10
+ Main features include:
11
+
12
+ - Extraction of regions of interest (ROIs)
13
+ - Extraction of line, segment, average, and radial intensity profiles
14
+
15
+ These functions are useful for isolating specific image zones and for analyzing signal
16
+ intensity along defined paths.
17
+ """
18
+
19
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
20
+
21
+ # Note:
22
+ # ----
23
+ # - All `guidata.dataset.DataSet` parameter classes must also be imported
24
+ # in the `sigima.params` module.
25
+ # - All functions decorated by `computation_function` must be imported in the upper
26
+ # level `sigima.proc.image` module.
27
+
28
+ from __future__ import annotations
29
+
30
+ from typing import Callable
31
+
32
+ import guidata.dataset as gds
33
+ import numpy as np
34
+ from numpy import ma
35
+
36
+ import sigima.tools.image
37
+ from sigima.config import _
38
+ from sigima.objects.image import ImageObj, ImageROI, RectangularROI, ROI2DParam
39
+ from sigima.objects.signal import SignalObj
40
+ from sigima.proc.base import dst_1_to_1
41
+ from sigima.proc.decorator import computation_function
42
+ from sigima.proc.image.base import dst_1_to_1_signal
43
+
44
+ # NOTE: Only parameter classes DEFINED in this module should be included in __all__.
45
+ # Parameter classes imported from other modules (like sigima.proc.base) should NOT
46
+ # be re-exported to avoid Sphinx cross-reference conflicts. The sigima.params module
47
+ # serves as the central API point that imports and re-exports all parameter classes.
48
+ __all__ = [
49
+ "AverageProfileParam",
50
+ "LineProfileParam",
51
+ "ROIGridParam",
52
+ "RadialProfileParam",
53
+ "SegmentProfileParam",
54
+ "average_profile",
55
+ "extract_roi",
56
+ "extract_rois",
57
+ "generate_image_grid_roi",
58
+ "line_profile",
59
+ "radial_profile",
60
+ "segment_profile",
61
+ ]
62
+
63
+
64
+ @computation_function()
65
+ def extract_rois(src: ImageObj, params: list[ROI2DParam]) -> ImageObj:
66
+ """Extract multiple regions of interest from data
67
+
68
+ Args:
69
+ src: input image object
70
+ params: list of ROI parameters
71
+
72
+ Returns:
73
+ Output image object
74
+ """
75
+ # Initialize ix0, iy0 with maximum values:
76
+ iy0, ix0 = iymax, ixmax = src.data.shape
77
+ # Initialize ix1, iy1 with minimum values:
78
+ iy1, ix1 = iymin, ixmin = 0, 0
79
+ for p in params:
80
+ x0i, y0i, x1i, y1i = p.get_bounding_box_indices(src)
81
+ ix0, iy0, ix1, iy1 = min(ix0, x0i), min(iy0, y0i), max(ix1, x1i), max(iy1, y1i)
82
+ ix0, iy0 = max(ix0, ixmin), max(iy0, iymin)
83
+ ix1, iy1 = min(ix1, ixmax), min(iy1, iymax)
84
+
85
+ suffix = None
86
+ if len(params) == 1:
87
+ p = params[0]
88
+ suffix = p.get_suffix()
89
+ dst = dst_1_to_1(src, "extract_rois", suffix)
90
+ if src.is_uniform_coords:
91
+ dst.set_uniform_coords(
92
+ dst.dx, dst.dy, dst.x0 + ix0 * src.dx, dst.y0 + iy0 * src.dy
93
+ )
94
+ else:
95
+ dst.set_coords(src.xcoords[iy0:iy1], src.ycoords[ix0:ix1])
96
+ dst.roi = None
97
+
98
+ src2 = src.copy()
99
+ src2.roi = ImageROI.from_params(src2, params)
100
+ src2.data[src2.maskdata] = 0
101
+ dst.data = src2.data[iy0:iy1, ix0:ix1]
102
+ return dst
103
+
104
+
105
+ @computation_function()
106
+ def extract_roi(src: ImageObj, p: ROI2DParam) -> ImageObj:
107
+ """Extract single ROI
108
+
109
+ Args:
110
+ src: input image object
111
+ p: ROI parameters
112
+
113
+ Returns:
114
+ Output image object
115
+ """
116
+ dst = dst_1_to_1(src, "extract_roi", p.get_suffix())
117
+ dst.data = p.get_data(src).copy()
118
+ dst.roi = p.get_extracted_roi(src)
119
+ x0, y0, _x1, _y1 = p.get_bounding_box_physical()
120
+ if src.is_uniform_coords:
121
+ dst.set_uniform_coords(dst.dx, dst.dy, dst.x0 + x0, dst.y0 + y0)
122
+ else:
123
+ dst.set_coords(src.xcoords + x0, src.ycoords + y0)
124
+ return dst
125
+
126
+
127
+ class Direction(gds.LabeledEnum):
128
+ """Direction choice"""
129
+
130
+ INCREASING = "increasing", _("increasing")
131
+ DECREASING = "decreasing", _("decreasing")
132
+
133
+
134
+ class ROIGridParam(gds.DataSet):
135
+ """ROI Grid parameters"""
136
+
137
+ # optional Python-level hook, no Qt
138
+ on_geometry_changed: Callable | None = None
139
+
140
+ # pylint: disable=unused-argument
141
+ def geometry_changed(self, item, value) -> None:
142
+ """Notify host (if any) that geometry changed."""
143
+ if callable(self.on_geometry_changed):
144
+ self.on_geometry_changed() # pylint: disable=not-callable
145
+
146
+ _b_group0 = gds.BeginGroup(_("Geometry"))
147
+ ny = gds.IntItem(f"N<sub>y</sub> ({_('rows')})", default=3, nonzero=True).set_prop(
148
+ "display", callback=geometry_changed
149
+ )
150
+ nx = (
151
+ gds.IntItem(f"N<sub>x</sub> ({_('columns')})", default=3, nonzero=True)
152
+ .set_prop("display", callback=geometry_changed)
153
+ .set_pos(col=1)
154
+ )
155
+ xtranslation = gds.IntItem(
156
+ _("X translation"),
157
+ default=50,
158
+ min=0,
159
+ max=100,
160
+ unit="%",
161
+ slider=True,
162
+ ).set_prop("display", callback=geometry_changed)
163
+ ytranslation = gds.IntItem(
164
+ _("Y translation"),
165
+ default=50,
166
+ min=0,
167
+ max=100,
168
+ unit="%",
169
+ slider=True,
170
+ ).set_prop("display", callback=geometry_changed)
171
+ xsize = gds.IntItem(
172
+ f"X size ({_('column size')})",
173
+ default=50,
174
+ min=0,
175
+ max=100,
176
+ unit="%",
177
+ slider=True,
178
+ ).set_prop("display", callback=geometry_changed)
179
+ ysize = gds.IntItem(
180
+ f"Y size ({_('row size')})",
181
+ default=50,
182
+ min=0,
183
+ max=100,
184
+ unit="%",
185
+ slider=True,
186
+ ).set_prop("display", callback=geometry_changed)
187
+ _e_group0 = gds.EndGroup(_("Geometry"))
188
+ _b_group1 = gds.BeginGroup(_("ROI titles"))
189
+ base_name = gds.StringItem(_("Base name"), default="ROI").set_prop(
190
+ "display", callback=geometry_changed
191
+ )
192
+ name_pattern = gds.StringItem(
193
+ _("Name pattern"), default="{base}({r},{c})"
194
+ ).set_prop("display", callback=geometry_changed)
195
+ xdirection = gds.ChoiceItem(_("X direction"), Direction).set_prop(
196
+ "display", callback=geometry_changed
197
+ )
198
+ ydirection = (
199
+ gds.ChoiceItem(_("Y direction"), Direction)
200
+ .set_prop("display", callback=geometry_changed)
201
+ .set_pos(col=1)
202
+ )
203
+ _e_group1 = gds.EndGroup(_("ROI titles"))
204
+
205
+
206
+ def generate_image_grid_roi(src: ImageObj, p: ROIGridParam) -> ImageROI:
207
+ """Create a grid of rectangular ROIs from an image object.
208
+
209
+ Args:
210
+ obj: The image object to create the ROI for.
211
+ p: ROIGridParam object containing the grid parameters.
212
+
213
+ Returns:
214
+ The created ROI object.
215
+ """
216
+ dx_cell = src.width / p.nx
217
+ dy_cell = src.height / p.ny
218
+ dx = dx_cell * p.xsize / 100.0
219
+ dy = dy_cell * p.ysize / 100.0
220
+ xtrans = src.width * (p.xtranslation - 50.0) / 100.0
221
+ ytrans = src.height * (p.ytranslation - 50.0) / 100.0
222
+ lbl_rows = range(p.ny)
223
+ if p.ydirection == Direction.DECREASING:
224
+ lbl_rows = range(p.ny - 1, -1, -1)
225
+ lbl_cols = range(p.nx)
226
+ if p.xdirection == Direction.DECREASING:
227
+ lbl_cols = range(p.nx - 1, -1, -1)
228
+ ptn: str = p.name_pattern
229
+ roi = ImageROI()
230
+ for ir in range(p.ny):
231
+ for ic in range(p.nx):
232
+ x0 = src.x0 + (ic + 0.5) * dx_cell + xtrans - 0.5 * dx
233
+ y0 = src.y0 + (ir + 0.5) * dy_cell + ytrans - 0.5 * dy
234
+ nir, nic = lbl_rows[ir], lbl_cols[ic]
235
+ try:
236
+ title = ptn.format(base=p.base_name, r=nir + 1, c=nic + 1)
237
+ except Exception: # pylint: disable=broad-except
238
+ title = f"ROI({nir + 1},{nic + 1})"
239
+ roi.add_roi(RectangularROI([x0, y0, dx, dy], indices=False, title=title))
240
+ return roi
241
+
242
+
243
+ class LineProfileParam(gds.DataSet):
244
+ """Horizontal or vertical profile parameters"""
245
+
246
+ _prop = gds.GetAttrProp("direction")
247
+ _directions = (("horizontal", _("horizontal")), ("vertical", _("vertical")))
248
+ direction = gds.ChoiceItem(_("Direction"), _directions, radio=True).set_prop(
249
+ "display", store=_prop
250
+ )
251
+ row = gds.IntItem(_("Row"), default=0, min=0).set_prop(
252
+ "display", active=gds.FuncProp(_prop, lambda x: x == "horizontal")
253
+ )
254
+ col = gds.IntItem(_("Column"), default=0, min=0).set_prop(
255
+ "display", active=gds.FuncProp(_prop, lambda x: x == "vertical")
256
+ )
257
+
258
+
259
+ @computation_function()
260
+ def line_profile(src: ImageObj, p: LineProfileParam) -> SignalObj:
261
+ """Compute horizontal or vertical profile
262
+
263
+ Args:
264
+ src: input image object
265
+ p: parameters
266
+
267
+ Returns:
268
+ Signal object with the profile
269
+ """
270
+ data = src.get_masked_view()
271
+ p.row = min(p.row, data.shape[0] - 1)
272
+ p.col = min(p.col, data.shape[1] - 1)
273
+ if p.direction == "horizontal":
274
+ suffix, shape_index, pdata = f"row={p.row}", 1, data[p.row, :]
275
+ else:
276
+ suffix, shape_index, pdata = f"col={p.col}", 0, data[:, p.col]
277
+ pdata: ma.MaskedArray
278
+ x = np.arange(data.shape[shape_index])[~pdata.mask]
279
+ y = np.array(pdata, dtype=float)[~pdata.mask]
280
+ dst = dst_1_to_1_signal(src, "profile", suffix)
281
+ dst.set_xydata(x, y)
282
+ return dst
283
+
284
+
285
+ class SegmentProfileParam(gds.DataSet):
286
+ """Segment profile parameters"""
287
+
288
+ row1 = gds.IntItem(_("Start row"), default=0, min=0)
289
+ col1 = gds.IntItem(_("Start column"), default=0, min=0)
290
+ row2 = gds.IntItem(_("End row"), default=0, min=0)
291
+ col2 = gds.IntItem(_("End column"), default=0, min=0)
292
+
293
+
294
+ def csline(data: np.ndarray, row0, col0, row1, col1) -> tuple[np.ndarray, np.ndarray]:
295
+ """Return intensity profile of data along a line
296
+
297
+ Args:
298
+ data: 2D array
299
+ row0, col0: start point
300
+ row1, col1: end point
301
+ """
302
+ # Keep coordinates inside the image
303
+ row0 = max(0, min(row0, data.shape[0] - 1))
304
+ col0 = max(0, min(col0, data.shape[1] - 1))
305
+ row1 = max(0, min(row1, data.shape[0] - 1))
306
+ col1 = max(0, min(col1, data.shape[1] - 1))
307
+ # Keep coordinates in the right order
308
+ row0, row1 = min(row0, row1), max(row0, row1)
309
+ col0, col1 = min(col0, col1), max(col0, col1)
310
+ # Extract the line
311
+ line = np.zeros((2, max(abs(row1 - row0), abs(col1 - col0)) + 1), dtype=int)
312
+ line[0, :] = np.linspace(row0, row1, line.shape[1]).astype(int)
313
+ line[1, :] = np.linspace(col0, col1, line.shape[1]).astype(int)
314
+ # Interpolate the line
315
+ y = np.ma.array(data[line[0], line[1]], float).filled(np.nan)
316
+ x = np.arange(y.size)
317
+ return x, y
318
+
319
+
320
+ @computation_function()
321
+ def segment_profile(src: ImageObj, p: SegmentProfileParam) -> SignalObj:
322
+ """Compute segment profile
323
+
324
+ Args:
325
+ src: input image object
326
+ p: parameters
327
+
328
+ Returns:
329
+ Signal object with the segment profile
330
+ """
331
+ data = src.get_masked_view()
332
+ p.row1 = min(p.row1, data.shape[0] - 1)
333
+ p.col1 = min(p.col1, data.shape[1] - 1)
334
+ p.row2 = min(p.row2, data.shape[0] - 1)
335
+ p.col2 = min(p.col2, data.shape[1] - 1)
336
+ suffix = f"({p.row1}, {p.col1})-({p.row2}, {p.col2})"
337
+ x, y = csline(data, p.row1, p.col1, p.row2, p.col2)
338
+ x, y = x[~np.isnan(y)], y[~np.isnan(y)] # Remove NaN values
339
+ dst = dst_1_to_1_signal(src, "segment_profile", suffix)
340
+ dst.set_xydata(np.array(x, dtype=float), np.array(y, dtype=float))
341
+ return dst
342
+
343
+
344
+ class AverageProfileParam(gds.DataSet):
345
+ """Average horizontal or vertical profile parameters"""
346
+
347
+ _directions = (("horizontal", _("horizontal")), ("vertical", _("vertical")))
348
+ direction = gds.ChoiceItem(_("Direction"), _directions, radio=True)
349
+ _hgroup_begin = gds.BeginGroup(_("Profile rectangular area"))
350
+ row1 = gds.IntItem(_("Row 1"), default=0, min=0)
351
+ row2 = gds.IntItem(_("Row 2"), default=-1, min=-1)
352
+ col1 = gds.IntItem(_("Column 1"), default=0, min=0)
353
+ col2 = gds.IntItem(_("Column 2"), default=-1, min=-1)
354
+ _hgroup_end = gds.EndGroup(_("Profile rectangular area"))
355
+
356
+
357
+ @computation_function()
358
+ def average_profile(src: ImageObj, p: AverageProfileParam) -> SignalObj:
359
+ """Compute horizontal or vertical average profile
360
+
361
+ Args:
362
+ src: input image object
363
+ p: parameters
364
+
365
+ Returns:
366
+ Signal object with the average profile
367
+ """
368
+ data = src.get_masked_view()
369
+ if p.row2 == -1:
370
+ p.row2 = data.shape[0] - 1
371
+ if p.col2 == -1:
372
+ p.col2 = data.shape[1] - 1
373
+ if p.row1 > p.row2:
374
+ p.row1, p.row2 = p.row2, p.row1
375
+ if p.col1 > p.col2:
376
+ p.col1, p.col2 = p.col2, p.col1
377
+ p.row1 = min(p.row1, data.shape[0] - 1)
378
+ p.row2 = min(p.row2, data.shape[0] - 1)
379
+ p.col1 = min(p.col1, data.shape[1] - 1)
380
+ p.col2 = min(p.col2, data.shape[1] - 1)
381
+ suffix = f"{p.direction}, rows=[{p.row1}, {p.row2}], cols=[{p.col1}, {p.col2}]"
382
+ if p.direction == "horizontal":
383
+ x, axis = np.arange(p.col1, p.col2 + 1), 0
384
+ else:
385
+ x, axis = np.arange(p.row1, p.row2 + 1), 1
386
+ y = ma.mean(data[p.row1 : p.row2 + 1, p.col1 : p.col2 + 1], axis=axis)
387
+ dst = dst_1_to_1_signal(src, "average_profile", suffix)
388
+ dst.set_xydata(x, y)
389
+ return dst
390
+
391
+
392
+ class RadialProfileParam(gds.DataSet):
393
+ """Radial profile parameters"""
394
+
395
+ def __init__(self, *args, **kwargs) -> None:
396
+ super().__init__(*args, **kwargs)
397
+ self.__obj: ImageObj | None = None
398
+
399
+ def update_from_obj(self, obj: ImageObj) -> None:
400
+ """Update parameters from image"""
401
+ self.__obj = obj
402
+ self.x0 = obj.xc
403
+ self.y0 = obj.yc
404
+
405
+ def choice_callback(self, item, value): # pylint: disable=unused-argument
406
+ """Callback for choice item"""
407
+ if value == "centroid":
408
+ self.y0, self.x0 = sigima.tools.image.get_centroid_fourier(
409
+ self.__obj.get_masked_view()
410
+ )
411
+ elif value == "center":
412
+ self.x0, self.y0 = self.__obj.xc, self.__obj.yc
413
+
414
+ _prop = gds.GetAttrProp("center")
415
+ center = gds.ChoiceItem(
416
+ _("Center position"),
417
+ (
418
+ ("centroid", _("Image centroid")),
419
+ ("center", _("Image center")),
420
+ ("user", _("User-defined")),
421
+ ),
422
+ default="centroid",
423
+ ).set_prop("display", store=_prop, callback=choice_callback)
424
+
425
+ _func_prop = gds.FuncProp(_prop, lambda x: x == "user")
426
+ _xyl = "<sub>" + _("Center") + "</sub>"
427
+ x0 = gds.FloatItem(f"X{_xyl}", default=0.0, unit="pixel").set_prop(
428
+ "display", active=_func_prop
429
+ )
430
+ y0 = gds.FloatItem(f"Y{_xyl}", default=0.0, unit="pixel").set_prop(
431
+ "display", active=_func_prop
432
+ )
433
+
434
+
435
+ @computation_function()
436
+ def radial_profile(src: ImageObj, p: RadialProfileParam) -> SignalObj:
437
+ """Compute radial profile around the centroid
438
+ with :py:func:`sigima.tools.image.get_radial_profile`
439
+
440
+ Args:
441
+ src: input image object
442
+ p: parameters
443
+
444
+ Returns:
445
+ Signal object with the radial profile
446
+ """
447
+ data = src.get_masked_view()
448
+ if p.center == "centroid":
449
+ y0, x0 = sigima.tools.image.get_centroid_fourier(data)
450
+ elif p.center == "center":
451
+ x0, y0 = src.xc, src.yc
452
+ else:
453
+ x0, y0 = p.x0, p.y0
454
+ suffix = f"center=({x0:.3f}, {y0:.3f})"
455
+ dst = dst_1_to_1_signal(src, "radial_profile", suffix)
456
+ x, y = sigima.tools.image.get_radial_profile(data, (x0, y0))
457
+ dst.set_xydata(x, y)
458
+ return dst
@@ -0,0 +1,219 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """Filtering computation module.
4
+ --------------------------------
5
+
6
+ This module provides spatial and frequency-based filtering operations for images.
7
+ Filtering functions are essential for enhancing image quality and removing noise.
8
+
9
+ Main features include:
10
+ * Gaussian, median, moving average and Wiener filters
11
+ * Butterworth and frequency domain Gaussian filters.
12
+
13
+ Filtering functions are essential for enhancing image quality
14
+ and removing noise prior to further analysis.
15
+ """
16
+
17
+ # pylint: disable=invalid-name # Allows short names like x, y...
18
+
19
+ # Note:
20
+ # ----
21
+ # - All `guidata.dataset.DataSet` parameter classes must also be imported in the
22
+ # `sigima.params` module.
23
+ # - All functions decorated with `computation_function` must be imported in the upper
24
+ # level `sigima.proc.image` module.
25
+
26
+ from __future__ import annotations
27
+
28
+ import guidata.dataset as gds # type: ignore[import]
29
+ import scipy.ndimage as spi # type: ignore[import]
30
+ import scipy.signal as sps # type: ignore[import]
31
+ from skimage import filters # type: ignore[import]
32
+
33
+ import sigima.tools.image
34
+ from sigima.config import _
35
+ from sigima.objects.image import ImageObj
36
+ from sigima.proc.base import (
37
+ GaussianParam,
38
+ MovingAverageParam,
39
+ MovingMedianParam,
40
+ dst_1_to_1,
41
+ )
42
+ from sigima.proc.decorator import computation_function
43
+ from sigima.proc.image.base import Wrap1to1Func, restore_data_outside_roi
44
+
45
+ # NOTE: Only parameter classes DEFINED in this module should be included in __all__.
46
+ # Parameter classes imported from other modules (like sigima.proc.base) should NOT
47
+ # be re-exported to avoid Sphinx cross-reference conflicts. The sigima.params module
48
+ # serves as the central API point that imports and re-exports all parameter classes.
49
+ __all__ = [
50
+ "ButterworthParam",
51
+ "GaussianFreqFilterParam",
52
+ "butterworth",
53
+ "gaussian_filter",
54
+ "gaussian_freq_filter",
55
+ "moving_average",
56
+ "moving_median",
57
+ "wiener",
58
+ ]
59
+
60
+ # MARK: Noise reduction filters
61
+
62
+
63
+ @computation_function()
64
+ def gaussian_filter(src: ImageObj, p: GaussianParam) -> ImageObj:
65
+ """Compute gaussian filter with :py:func:`scipy.ndimage.gaussian_filter`.
66
+
67
+ Args:
68
+ src: Input image object.
69
+ p: Parameters.
70
+
71
+ Returns:
72
+ Output image object.
73
+ """
74
+ return Wrap1to1Func(spi.gaussian_filter, sigma=p.sigma)(src)
75
+
76
+
77
+ @computation_function()
78
+ def moving_average(src: ImageObj, p: MovingAverageParam) -> ImageObj:
79
+ """Compute moving average with :py:func:`scipy.ndimage.uniform_filter`.
80
+
81
+ Args:
82
+ src: Input image object.
83
+ p: Parameters.
84
+
85
+ Returns:
86
+ Output image object.
87
+ """
88
+ return Wrap1to1Func(
89
+ spi.uniform_filter, size=p.n, mode=p.mode, func_name="moving_average"
90
+ )(src)
91
+
92
+
93
+ @computation_function()
94
+ def moving_median(src: ImageObj, p: MovingMedianParam) -> ImageObj:
95
+ """Compute moving median with :py:func:`scipy.ndimage.median_filter`.
96
+
97
+ Args:
98
+ src: Input image object.
99
+ p: Parameters.
100
+
101
+ Returns:
102
+ Output image object.
103
+ """
104
+ return Wrap1to1Func(
105
+ spi.median_filter, size=p.n, mode=p.mode, func_name="moving_median"
106
+ )(src)
107
+
108
+
109
+ @computation_function()
110
+ def wiener(src: ImageObj) -> ImageObj:
111
+ """Compute Wiener filter with :py:func:`scipy.signal.wiener`
112
+
113
+ Args:
114
+ src: Input image object.
115
+
116
+ Returns:
117
+ Output image object.
118
+ """
119
+ return Wrap1to1Func(sps.wiener)(src)
120
+
121
+
122
+ class ButterworthParam(gds.DataSet):
123
+ """Butterworth filter parameters."""
124
+
125
+ cut_off = gds.FloatItem(
126
+ _("Cut-off frequency ratio"),
127
+ default=0.005,
128
+ min=0.0,
129
+ max=0.5,
130
+ help=_("Cut-off frequency ratio"),
131
+ )
132
+ high_pass = gds.BoolItem(
133
+ _("High-pass filter"),
134
+ default=False,
135
+ help=_("If True, apply high-pass filter instead of low-pass"),
136
+ )
137
+ order = gds.IntItem(
138
+ _("Order"),
139
+ default=2,
140
+ min=1,
141
+ help=_("Order of the Butterworth filter"),
142
+ )
143
+
144
+
145
+ # MARK: Frequency filters
146
+
147
+
148
+ @computation_function()
149
+ def butterworth(src: ImageObj, p: ButterworthParam) -> ImageObj:
150
+ """Compute Butterworth filter with :py:func:`skimage.filters.butterworth`.
151
+
152
+ Args:
153
+ src: Input image object.
154
+ p: Parameters.
155
+
156
+ Returns:
157
+ Output image object.
158
+ """
159
+ dst = dst_1_to_1(
160
+ src,
161
+ "butterworth",
162
+ f"cut_off={p.cut_off:.3f}, order={p.order}, high_pass={p.high_pass}",
163
+ )
164
+ dst.data = filters.butterworth(src.data, p.cut_off, p.high_pass, p.order)
165
+ restore_data_outside_roi(dst, src)
166
+ return dst
167
+
168
+
169
+ class GaussianFreqFilterParam(GaussianParam):
170
+ """Parameters for Gaussian filter applied in the frequency domain."""
171
+
172
+ sigma = gds.FloatItem(
173
+ "σ",
174
+ default=1.0,
175
+ unit="pixel⁻¹",
176
+ min=0.0,
177
+ help=_("Standard deviation of the Gaussian filter"),
178
+ )
179
+ f0 = gds.FloatItem(
180
+ _("Center frequency"),
181
+ default=1.0,
182
+ unit="pixel⁻¹",
183
+ min=0.0,
184
+ help=_("Center frequency of the Gaussian filter"),
185
+ )
186
+ sigma = gds.FloatItem(
187
+ "σ",
188
+ default=0.5,
189
+ unit="pixels⁻¹",
190
+ min=0.0,
191
+ help=_("Standard deviation of the Gaussian filter"),
192
+ )
193
+ ifft_result_type = gds.ChoiceItem(
194
+ _("Inverse FFT result"),
195
+ (("real", _("Real part")), ("abs", _("Absolute value"))),
196
+ default="real",
197
+ help=_("How to return the inverse FFT result"),
198
+ )
199
+
200
+
201
+ @computation_function()
202
+ def gaussian_freq_filter(src: ImageObj, p: GaussianFreqFilterParam) -> ImageObj:
203
+ """Apply a Gaussian filter in the frequency domain.
204
+
205
+ Args:
206
+ src: Source image object.
207
+ p: Parameters.
208
+
209
+ Returns:
210
+ Output image object.
211
+ """
212
+ dst = dst_1_to_1(
213
+ src,
214
+ "frequency_domain_gaussian_filter",
215
+ f"sigma={p.sigma:.3f}, f0={p.f0:.3f}",
216
+ )
217
+ dst.data = sigima.tools.image.gaussian_freq_filter(src.data, p.f0, p.sigma)
218
+ restore_data_outside_roi(dst, src)
219
+ return dst