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,217 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Threshold computation module
5
+ ----------------------------
6
+
7
+ This module provides various thresholding techniques for image segmentation.
8
+ Thresholding is a simple yet effective method to separate objects from the background
9
+ in an image by converting it into a binary image based on a specified threshold value.
10
+ """
11
+
12
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
13
+
14
+ # Note:
15
+ # ----
16
+ # - All `guidata.dataset.DataSet` parameter classes must also be imported
17
+ # in the `sigima.params` module.
18
+ # - All functions decorated by `computation_function` must be imported in the upper
19
+ # level `sigima.proc.image` module.
20
+
21
+ from __future__ import annotations
22
+
23
+ import guidata.dataset as gds
24
+ import skimage.util
25
+ from skimage import filters
26
+
27
+ from sigima.config import _
28
+ from sigima.objects.image import ImageObj
29
+ from sigima.proc.base import dst_1_to_1
30
+ from sigima.proc.decorator import computation_function
31
+ from sigima.proc.image.base import restore_data_outside_roi
32
+
33
+ # NOTE: Only parameter classes DEFINED in this module should be included in __all__.
34
+ # Parameter classes imported from other modules (like sigima.proc.base) should NOT
35
+ # be re-exported to avoid Sphinx cross-reference conflicts. The sigima.params module
36
+ # serves as the central API point that imports and re-exports all parameter classes.
37
+ __all__ = [
38
+ "ThresholdParam",
39
+ "threshold",
40
+ "threshold_isodata",
41
+ "threshold_li",
42
+ "threshold_mean",
43
+ "threshold_minimum",
44
+ "threshold_otsu",
45
+ "threshold_triangle",
46
+ "threshold_yen",
47
+ ]
48
+
49
+
50
+ class ThresholdParam(gds.DataSet):
51
+ """Histogram threshold parameters"""
52
+
53
+ methods = (
54
+ ("manual", _("Manual")),
55
+ ("isodata", "ISODATA"),
56
+ ("li", "Li"),
57
+ ("mean", _("Mean")),
58
+ ("minimum", _("Minimum")),
59
+ ("otsu", "Otsu"),
60
+ ("triangle", _("Triangle")),
61
+ ("yen", "Yen"),
62
+ )
63
+
64
+ _method_prop = gds.GetAttrProp("method")
65
+ method = gds.ChoiceItem(_("Threshold method"), methods, default="manual").set_prop(
66
+ "display", store=_method_prop
67
+ )
68
+ bins = gds.IntItem(_("Number of bins"), default=256, min=1).set_prop(
69
+ "display",
70
+ active=gds.FuncProp(_method_prop, lambda x: x not in ("li", "mean", "manual")),
71
+ )
72
+ value = gds.FloatItem(_("Threshold value"), default=0.0).set_prop(
73
+ "display", active=gds.FuncProp(_method_prop, lambda x: x == "manual")
74
+ )
75
+ operation = gds.ChoiceItem(
76
+ _("Operation"),
77
+ ((">", _("Greater than")), ("<", _("Less than"))),
78
+ default=">",
79
+ )
80
+
81
+
82
+ @computation_function()
83
+ def threshold(src: ImageObj, p: ThresholdParam) -> ImageObj:
84
+ """Compute the threshold, using one of the available algorithms:
85
+
86
+ - Manual: a fixed threshold value
87
+ - ISODATA: :py:func:`skimage.filters.threshold_isodata`
88
+ - Li: :py:func:`skimage.filters.threshold_li`
89
+ - Mean: :py:func:`skimage.filters.threshold_mean`
90
+ - Minimum: :py:func:`skimage.filters.threshold_minimum`
91
+ - Otsu: :py:func:`skimage.filters.threshold_otsu`
92
+ - Triangle: :py:func:`skimage.filters.threshold_triangle`
93
+ - Yen: :py:func:`skimage.filters.threshold_yen`
94
+
95
+ Args:
96
+ src: input image object
97
+ p: parameters
98
+
99
+ Returns:
100
+ Output image object
101
+ """
102
+ if p.method == "manual":
103
+ suffix = f"value={p.value}"
104
+ value = p.value
105
+ else:
106
+ suffix = f"method={p.method}"
107
+ if p.method not in ("li", "mean"):
108
+ suffix += f", nbins={p.bins}"
109
+ func = getattr(filters, f"threshold_{p.method}")
110
+ args = [] if p.method in ("li", "mean") else [p.bins]
111
+ value = func(src.data, *args)
112
+ suffix += f", op='{p.operation}'"
113
+ dst = dst_1_to_1(src, "threshold", suffix)
114
+ data = src.data > value if p.operation == ">" else src.data < value
115
+ dst.data = skimage.util.img_as_ubyte(data)
116
+ dst.zscalemin, dst.zscalemax = 0, 255 # LUT range
117
+ dst.set_metadata_option("colormap", "gray")
118
+ restore_data_outside_roi(dst, src)
119
+ return dst
120
+
121
+
122
+ @computation_function()
123
+ def threshold_isodata(src: ImageObj) -> ImageObj:
124
+ """Compute the threshold using the Isodata algorithm with default parameters,
125
+ see :py:func:`skimage.filters.threshold_isodata`
126
+
127
+ Args:
128
+ src: input image object
129
+
130
+ Returns:
131
+ Output image object
132
+ """
133
+ return threshold(src, ThresholdParam.create(method="isodata"))
134
+
135
+
136
+ @computation_function()
137
+ def threshold_li(src: ImageObj) -> ImageObj:
138
+ """Compute the threshold using the Li algorithm with default parameters,
139
+ see :py:func:`skimage.filters.threshold_li`
140
+
141
+ Args:
142
+ src: input image object
143
+
144
+ Returns:
145
+ Output image object
146
+ """
147
+ return threshold(src, ThresholdParam.create(method="li"))
148
+
149
+
150
+ @computation_function()
151
+ def threshold_mean(src: ImageObj) -> ImageObj:
152
+ """Compute the threshold using the Mean algorithm,
153
+ see :py:func:`skimage.filters.threshold_mean`
154
+
155
+ Args:
156
+ src: input image object
157
+
158
+ Returns:
159
+ Output image object
160
+ """
161
+ return threshold(src, ThresholdParam.create(method="mean"))
162
+
163
+
164
+ @computation_function()
165
+ def threshold_minimum(src: ImageObj) -> ImageObj:
166
+ """Compute the threshold using the Minimum algorithm with default parameters,
167
+ see :py:func:`skimage.filters.threshold_minimum`
168
+
169
+ Args:
170
+ src: input image object
171
+
172
+ Returns:
173
+ Output image object
174
+ """
175
+ return threshold(src, ThresholdParam.create(method="minimum"))
176
+
177
+
178
+ @computation_function()
179
+ def threshold_otsu(src: ImageObj) -> ImageObj:
180
+ """Compute the threshold using the Otsu algorithm with default parameters,
181
+ see :py:func:`skimage.filters.threshold_otsu`
182
+
183
+ Args:
184
+ src: input image object
185
+
186
+ Returns:
187
+ Output image object
188
+ """
189
+ return threshold(src, ThresholdParam.create(method="otsu"))
190
+
191
+
192
+ @computation_function()
193
+ def threshold_triangle(src: ImageObj) -> ImageObj:
194
+ """Compute the threshold using the Triangle algorithm with default parameters,
195
+ see :py:func:`skimage.filters.threshold_triangle`
196
+
197
+ Args:
198
+ src: input image object
199
+
200
+ Returns:
201
+ Output image object
202
+ """
203
+ return threshold(src, ThresholdParam.create(method="triangle"))
204
+
205
+
206
+ @computation_function()
207
+ def threshold_yen(src: ImageObj) -> ImageObj:
208
+ """Compute the threshold using the Yen algorithm with default parameters,
209
+ see :py:func:`skimage.filters.threshold_yen`
210
+
211
+ Args:
212
+ src: input image object
213
+
214
+ Returns:
215
+ Output image object
216
+ """
217
+ return threshold(src, ThresholdParam.create(method="yen"))
@@ -0,0 +1,393 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Geometry transformations module
5
+ ===============================
6
+
7
+ This module provides a unified interface for applying geometric transformations
8
+ to both geometry results (:class:`sigima.objects.GeometryResult`) and ROI objects
9
+ using the shape coordinate system (:mod:`sigima.objects.shape`).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ import numpy as np
17
+
18
+ from sigima.objects.scalar import GeometryResult, KindShape
19
+ from sigima.objects.shape import (
20
+ CircleCoordinates,
21
+ EllipseCoordinates,
22
+ PointCoordinates,
23
+ PolygonCoordinates,
24
+ RectangleCoordinates,
25
+ SegmentCoordinates,
26
+ )
27
+
28
+ if TYPE_CHECKING:
29
+ from sigima.objects import CircularROI, ImageObj, PolygonalROI, RectangularROI
30
+
31
+
32
+ __all__ = [
33
+ "GeometryTransformer",
34
+ "transformer",
35
+ ]
36
+
37
+
38
+ class GeometryTransformer:
39
+ """
40
+ Singleton class for applying transformations to geometry objects.
41
+
42
+ Provides a unified interface for transforming both GeometryResult and ROI
43
+ objects using the shape coordinate system.
44
+ """
45
+
46
+ _instance: GeometryTransformer | None = None
47
+
48
+ def __new__(cls) -> GeometryTransformer:
49
+ """Ensure singleton pattern."""
50
+ if cls._instance is None:
51
+ cls._instance = super().__new__(cls)
52
+ cls._instance._initialized = False
53
+ return cls._instance
54
+
55
+ def __init__(self) -> None:
56
+ """Initialize the transformer (only once due to singleton)."""
57
+ if self._initialized: # pylint: disable=access-member-before-definition
58
+ return
59
+
60
+ # Mapping from GeometryResult kinds to shape coordinate classes
61
+ self._geometry_shape_map: dict[
62
+ KindShape,
63
+ type[
64
+ RectangleCoordinates
65
+ | CircleCoordinates
66
+ | PolygonCoordinates
67
+ | SegmentCoordinates
68
+ ],
69
+ ] = {
70
+ KindShape.POINT: PointCoordinates,
71
+ KindShape.RECTANGLE: RectangleCoordinates,
72
+ KindShape.CIRCLE: CircleCoordinates,
73
+ KindShape.ELLIPSE: EllipseCoordinates,
74
+ KindShape.POLYGON: PolygonCoordinates,
75
+ KindShape.SEGMENT: SegmentCoordinates,
76
+ KindShape.MARKER: PointCoordinates,
77
+ }
78
+
79
+ # Mapping from ROI types to shape coordinate classes (lazy loaded)
80
+ self._roi_shape_map: dict[type, type] = {}
81
+
82
+ self._initialized = True
83
+
84
+ def _get_roi_shape_map(
85
+ self,
86
+ ) -> dict[
87
+ type[CircularROI | RectangularROI | PolygonalROI],
88
+ type[RectangleCoordinates | CircleCoordinates | PolygonCoordinates],
89
+ ]:
90
+ """Lazy load ROI shape mapping to avoid circular imports."""
91
+ if not self._roi_shape_map:
92
+ # pylint: disable=import-outside-toplevel
93
+ from sigima.objects.image import CircularROI, PolygonalROI, RectangularROI
94
+
95
+ self._roi_shape_map = {
96
+ RectangularROI: RectangleCoordinates,
97
+ CircularROI: CircleCoordinates,
98
+ PolygonalROI: PolygonCoordinates,
99
+ }
100
+ return self._roi_shape_map
101
+
102
+ def transform_geometry(
103
+ self, geometry: GeometryResult, operation: str, **kwargs: Any
104
+ ) -> GeometryResult:
105
+ """
106
+ Transform a GeometryResult and return a new one.
107
+
108
+ Args:
109
+ geometry: The GeometryResult to transform.
110
+ operation: Operation name ('rotate', 'translate', 'fliph', 'flipv',
111
+ 'transpose', 'scale').
112
+ **kwargs: Operation-specific parameters.
113
+
114
+ Returns:
115
+ New GeometryResult with transformed coordinates.
116
+
117
+ Raises:
118
+ ValueError: If operation is unknown or geometry kind is unsupported.
119
+ """
120
+ coord_class = self._geometry_shape_map.get(geometry.kind)
121
+ if coord_class is None:
122
+ raise ValueError(f"Unsupported geometry kind: {geometry.kind}")
123
+
124
+ # Transform each row of coordinates
125
+ transformed_coords = []
126
+ for row in geometry.coords:
127
+ # Create coordinate object for this row
128
+ shape_coords = coord_class(row.copy())
129
+
130
+ # Apply transformation
131
+ self._apply_operation(shape_coords, operation, **kwargs)
132
+
133
+ transformed_coords.append(shape_coords.data)
134
+
135
+ # Create new GeometryResult with transformed coordinates
136
+ return GeometryResult(
137
+ title=geometry.title,
138
+ kind=geometry.kind,
139
+ coords=np.array(transformed_coords),
140
+ roi_indices=(
141
+ geometry.roi_indices.copy()
142
+ if geometry.roi_indices is not None
143
+ else None
144
+ ),
145
+ attrs=geometry.attrs.copy(),
146
+ )
147
+
148
+ def transform_single_roi(
149
+ self,
150
+ single_roi: RectangularROI | CircularROI | PolygonalROI,
151
+ operation: str,
152
+ **kwargs: Any,
153
+ ) -> None:
154
+ """
155
+ Transform ROI coordinates inplace.
156
+
157
+ Args:
158
+ single_roi: ROI object with .coords attribute.
159
+ operation: Operation name.
160
+ **kwargs: Operation-specific parameters.
161
+
162
+ Raises:
163
+ ValueError: If ROI type is unsupported or operation is unknown.
164
+ """
165
+ roi_shape_map = self._get_roi_shape_map()
166
+ coord_class = roi_shape_map.get(type(single_roi))
167
+ if coord_class is None:
168
+ raise ValueError(f"Unsupported ROI type: {type(single_roi)}")
169
+
170
+ # Create shape coordinates and transform
171
+ shape_coords = coord_class(single_roi.coords.copy())
172
+ self._apply_operation(shape_coords, operation, **kwargs)
173
+
174
+ # Update ROI coordinates inplace
175
+ single_roi.coords[:] = shape_coords.data
176
+
177
+ def transform_roi(self, image: ImageObj, operation: str, **kwargs: Any) -> None:
178
+ """
179
+ Transform all ROI coordinates in an ImageObj inplace.
180
+
181
+ Args:
182
+ image: Image object whose ROI coordinates will be transformed.
183
+ operation: Operation name.
184
+ **kwargs: Operation-specific parameters.
185
+ """
186
+ if image.roi is None or image.roi.is_empty():
187
+ return
188
+
189
+ # Import here to avoid circular imports
190
+ # pylint: disable=import-outside-toplevel
191
+ from sigima.objects.image import ImageROI
192
+
193
+ # Determine ROI type and set up appropriate classes
194
+ new_roi = ImageROI()
195
+
196
+ # Transform each single ROI
197
+ for single_roi in image.roi.single_rois:
198
+ coords = single_roi.coords.copy()
199
+ roi_class = single_roi.__class__
200
+
201
+ # Create shape coordinates and transform
202
+ roi_shape_map = self._get_roi_shape_map()
203
+ coord_class = roi_shape_map.get(roi_class)
204
+ if coord_class is None:
205
+ raise ValueError(f"Unsupported ROI type: {roi_class}")
206
+
207
+ shape_coords = coord_class(coords)
208
+ self._apply_operation(shape_coords, operation, **kwargs)
209
+
210
+ new_coords = shape_coords.data
211
+ new_single_roi = roi_class(new_coords, single_roi.indices, single_roi.title)
212
+ new_roi.add_roi(new_single_roi)
213
+
214
+ image.roi = new_roi
215
+
216
+ def _apply_operation(
217
+ self,
218
+ shape_coords: (
219
+ PointCoordinates
220
+ | RectangleCoordinates
221
+ | CircleCoordinates
222
+ | EllipseCoordinates
223
+ | PolygonCoordinates
224
+ | SegmentCoordinates
225
+ ),
226
+ operation: str,
227
+ **kwargs: Any,
228
+ ) -> None:
229
+ """
230
+ Apply the specified operation to shape coordinates.
231
+
232
+ Args:
233
+ shape_coords: Shape coordinate object to transform.
234
+ operation: Operation name.
235
+ **kwargs: Operation-specific parameters.
236
+
237
+ Raises:
238
+ ValueError: If operation is unknown.
239
+ """
240
+ if operation == "rotate":
241
+ angle = kwargs.get("angle", 0)
242
+ center = kwargs.get("center", (0, 0))
243
+ shape_coords.rotate(angle, center)
244
+ elif operation == "translate":
245
+ dx = kwargs.get("dx", 0)
246
+ dy = kwargs.get("dy", 0)
247
+ shape_coords.translate(dx, dy)
248
+ elif operation == "fliph":
249
+ cx = kwargs.get("cx", 0.0)
250
+ shape_coords.fliph(cx)
251
+ elif operation == "flipv":
252
+ cy = kwargs.get("cy", 0.0)
253
+ shape_coords.flipv(cy)
254
+ elif operation == "transpose":
255
+ shape_coords.transpose()
256
+ elif operation == "scale":
257
+ sx = kwargs.get("sx", 1.0)
258
+ sy = kwargs.get("sy", 1.0)
259
+ center = kwargs.get("center", (0, 0))
260
+ shape_coords.scale(sx, sy, center)
261
+ else:
262
+ raise ValueError(f"Unknown operation: {operation}")
263
+
264
+ # Convenience methods for common operations
265
+ def rotate(
266
+ self,
267
+ obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
268
+ angle: float,
269
+ center: tuple[float, float],
270
+ ) -> GeometryResult | None:
271
+ """
272
+ Rotate geometry or ROI by given angle around center.
273
+
274
+ Args:
275
+ obj: GeometryResult or single ROI object.
276
+ angle: Rotation angle in radians.
277
+ center: Center of rotation (x, y).
278
+
279
+ Returns:
280
+ New GeometryResult if input was GeometryResult, None if ROI (inplace).
281
+ """
282
+ if isinstance(obj, GeometryResult):
283
+ return self.transform_geometry(obj, "rotate", angle=angle, center=center)
284
+ self.transform_single_roi(obj, "rotate", angle=angle, center=center)
285
+ return None
286
+
287
+ def translate(
288
+ self,
289
+ obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
290
+ dx: float,
291
+ dy: float,
292
+ ) -> GeometryResult | None:
293
+ """
294
+ Translate geometry or ROI by given offset.
295
+
296
+ Args:
297
+ obj: GeometryResult or single ROI object.
298
+ dx: Translation in x direction.
299
+ dy: Translation in y direction.
300
+
301
+ Returns:
302
+ New GeometryResult if input was GeometryResult, None if ROI (inplace).
303
+ """
304
+ if isinstance(obj, GeometryResult):
305
+ return self.transform_geometry(obj, "translate", dx=dx, dy=dy)
306
+ self.transform_single_roi(obj, "translate", dx=dx, dy=dy)
307
+ return None
308
+
309
+ def fliph(
310
+ self,
311
+ obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
312
+ cx: float,
313
+ ) -> GeometryResult | None:
314
+ """
315
+ Flip geometry or ROI horizontally around given x-coordinate.
316
+
317
+ Args:
318
+ obj: GeometryResult or single ROI object.
319
+ cx: X-coordinate of flip axis.
320
+
321
+ Returns:
322
+ New GeometryResult if input was GeometryResult, None if ROI (inplace).
323
+ """
324
+ if isinstance(obj, GeometryResult):
325
+ return self.transform_geometry(obj, "fliph", cx=cx)
326
+ self.transform_single_roi(obj, "fliph", cx=cx)
327
+ return None
328
+
329
+ def flipv(
330
+ self,
331
+ obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
332
+ cy: float,
333
+ ) -> GeometryResult | None:
334
+ """
335
+ Flip geometry or ROI vertically around given y-coordinate.
336
+
337
+ Args:
338
+ obj: GeometryResult or single ROI object.
339
+ cy: Y-coordinate of flip axis.
340
+
341
+ Returns:
342
+ New GeometryResult if input was GeometryResult, None if ROI (inplace).
343
+ """
344
+ if isinstance(obj, GeometryResult):
345
+ return self.transform_geometry(obj, "flipv", cy=cy)
346
+ self.transform_single_roi(obj, "flipv", cy=cy)
347
+ return None
348
+
349
+ def transpose(
350
+ self, obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI
351
+ ) -> GeometryResult | None:
352
+ """
353
+ Transpose geometry or ROI (swap x and y coordinates).
354
+
355
+ Args:
356
+ obj: GeometryResult or single ROI object.
357
+
358
+ Returns:
359
+ New GeometryResult if input was GeometryResult, None if ROI (inplace).
360
+ """
361
+ if isinstance(obj, GeometryResult):
362
+ return self.transform_geometry(obj, "transpose")
363
+ self.transform_single_roi(obj, "transpose")
364
+ return None
365
+
366
+ def scale(
367
+ self,
368
+ obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
369
+ sx: float,
370
+ sy: float,
371
+ center: tuple[float, float],
372
+ ) -> GeometryResult | None:
373
+ """
374
+ Scale geometry or ROI by given factors around center.
375
+
376
+ Args:
377
+ obj: GeometryResult or single ROI object.
378
+ sx: Scale factor in x direction.
379
+ sy: Scale factor in y direction.
380
+ center: Center of scaling (x, y).
381
+
382
+ Returns:
383
+ New GeometryResult if input was GeometryResult, None if ROI (inplace).
384
+ """
385
+ if isinstance(obj, GeometryResult):
386
+ return self.transform_geometry(obj, "scale", sx=sx, sy=sy, center=center)
387
+ self.transform_single_roi(obj, "scale", sx=sx, sy=sy, center=center)
388
+ return None
389
+
390
+
391
+ #: Global singleton instance of GeometryTransformer for applying geometric
392
+ #: transformations to geometry results and ROI objects.
393
+ transformer = GeometryTransformer()