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,279 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """Image grid ROI unit tests"""
4
+
5
+ from __future__ import annotations
6
+
7
+ import os.path as osp
8
+ from copy import deepcopy
9
+
10
+ import guidata.dataset as gds
11
+ import numpy as np
12
+ from numpy.testing import assert_array_equal
13
+ from pytest import approx
14
+
15
+ from sigima.io import read_roi_grid, write_roi_grid
16
+ from sigima.objects import ImageObj, ImageROI, create_image
17
+ from sigima.proc.image.extraction import (
18
+ Direction,
19
+ ROIGridParam,
20
+ extract_roi,
21
+ generate_image_grid_roi,
22
+ )
23
+ from sigima.tests.data import create_grid_of_gaussian_images
24
+ from sigima.tests.helpers import WorkdirRestoringTempDir
25
+
26
+
27
+ def _roi_by_title(roi: list[ImageROI], title: str) -> ImageROI:
28
+ """Get ROI by title."""
29
+ for r in roi:
30
+ if getattr(r, "title", None) == title:
31
+ return r
32
+ raise KeyError(title)
33
+
34
+
35
+ def test_roi_grid_basic_geometry() -> None:
36
+ """2x2 grid, 50% size, centered (50% translations)."""
37
+ img = create_grid_of_gaussian_images() # synthetic image with known geometry
38
+ p = ROIGridParam()
39
+ p.nx = p.ny = 2
40
+ p.xsize = p.ysize = 50
41
+ p.xtranslation = p.ytranslation = 50
42
+ p.xdirection = p.ydirection = Direction.INCREASING
43
+ p.base_name = "ROI"
44
+ p.name_pattern = "{base}({r},{c})"
45
+
46
+ # src.roi must stay untouched (pure builder)
47
+ assert img.roi is None
48
+
49
+ roi = generate_image_grid_roi(img, p)
50
+
51
+ # 4 rectangles created
52
+ items = list(roi)
53
+ assert len(items) == 4
54
+
55
+ # Titles present
56
+ titles = {r.title for r in items}
57
+ assert {"ROI(1,1)", "ROI(1,2)", "ROI(2,1)", "ROI(2,2)"} <= titles
58
+
59
+ # Check one rectangle’s geometry (top-left label)
60
+ r11 = _roi_by_title(roi, "ROI(1,1)")
61
+ _x0, _y0, dx, dy = r11.get_physical_coords(img) # uses indices=False path
62
+ # Each cell: width/2 by height/2; ROI takes 50% of that
63
+ assert dx == approx((img.width / 2) * 0.5)
64
+ assert dy == approx((img.height / 2) * 0.5)
65
+
66
+ # Source image must still be unmodified
67
+ assert img.roi is None
68
+
69
+
70
+ def test_labeling_changes_with_direction_but_geometry_set_is_invariant() -> None:
71
+ """Flipping directions relabels cells
72
+ but the set of rectangles (geometry) stays the same."""
73
+ img = create_grid_of_gaussian_images()
74
+ base = ROIGridParam()
75
+ base.nx = base.ny = 2
76
+ base.xsize = base.ysize = 50
77
+ base.xtranslation = base.ytranslation = 50
78
+ base.base_name = "ROI"
79
+ base.name_pattern = "{base}({r},{c})"
80
+
81
+ # Increasing both
82
+ p_inc = deepcopy(base)
83
+ p_inc.xdirection = p_inc.ydirection = Direction.INCREASING
84
+ roi_inc = generate_image_grid_roi(img, p_inc)
85
+ geoms_inc = sorted(
86
+ (r.get_physical_coords(img) for r in roi_inc), key=lambda t: (t[0], t[1])
87
+ )
88
+
89
+ # Decreasing both
90
+ p_dec = deepcopy(base)
91
+ p_dec.xdirection = p_dec.ydirection = Direction.DECREASING
92
+ roi_dec = generate_image_grid_roi(img, p_dec)
93
+ geoms_dec = sorted(
94
+ (r.get_physical_coords(img) for r in roi_dec), key=lambda t: (t[0], t[1])
95
+ )
96
+
97
+ # Same rectangles, just different titles
98
+ for (x0a, y0a, dxa, dya), (x0b, y0b, dxb, dyb) in zip(geoms_inc, geoms_dec):
99
+ assert x0a == approx(x0b)
100
+ assert y0a == approx(y0b)
101
+ assert dxa == approx(dxb)
102
+ assert dya == approx(dyb)
103
+
104
+
105
+ def test_translation_semantics_delta() -> None:
106
+ """Changing translation by +10% moves rectangles by 10% of image size."""
107
+ img = create_grid_of_gaussian_images()
108
+ p1 = ROIGridParam()
109
+ p1.nx = p1.ny = 2
110
+ p1.xsize = p1.ysize = 50
111
+ p1.xtranslation = p1.ytranslation = 50 # centered
112
+ p1.xdirection = p1.ydirection = Direction.INCREASING
113
+
114
+ p2 = deepcopy(p1)
115
+ p2.xtranslation = 60 # +10% shift in X
116
+
117
+ roi1 = generate_image_grid_roi(img, p1)
118
+ roi2 = generate_image_grid_roi(img, p2)
119
+
120
+ r11_1 = _roi_by_title(roi1, "ROI(1,1)")
121
+ r11_2 = _roi_by_title(roi2, "ROI(1,1)")
122
+
123
+ x0_1, y0_1, dx1, dy1 = r11_1.get_physical_coords(img)
124
+ x0_2, y0_2, dx2, dy2 = r11_2.get_physical_coords(img)
125
+
126
+ # Width should be unchanged; position should shift by exactly 10% of image width
127
+ assert dx1 == approx(dx2)
128
+ assert dy1 == approx(dy2)
129
+ assert (x0_2 - x0_1) == approx(0.10 * img.width)
130
+ assert (y0_2 - y0_1) == approx(0.00 * img.height)
131
+
132
+
133
+ def test_invalid_name_pattern_falls_back() -> None:
134
+ """Malformed pattern should not break: titles fall back to 'ROI(r,c)'."""
135
+ img = create_grid_of_gaussian_images()
136
+ p = ROIGridParam()
137
+ p.nx = p.ny = 1
138
+ p.xsize = p.ysize = 50
139
+ p.xtranslation = p.ytranslation = 50
140
+ p.xdirection = p.ydirection = Direction.INCREASING
141
+ p.base_name = "ANY"
142
+ p.name_pattern = "{this_will_raise}" # invalid placeholders
143
+
144
+ roi = generate_image_grid_roi(img, p)
145
+ titles = [r.title for r in roi]
146
+ assert titles == ["ROI(1,1)"] # see fallback in implementation
147
+
148
+
149
+ def test_zero_size_is_allowed_currently() -> None:
150
+ """Current behavior: 0% sizes produce degenerate rectangles (dx==0 or dy==0)."""
151
+ img = create_grid_of_gaussian_images()
152
+ p = ROIGridParam()
153
+ p.nx = p.ny = 2
154
+ p.xsize = 0
155
+ p.ysize = 50
156
+ p.xtranslation = p.ytranslation = 50
157
+
158
+ roi = generate_image_grid_roi(img, p)
159
+ # All ROIs exist; all have dx == 0
160
+ for r in roi:
161
+ _x0, _y0, dx, dy = r.get_physical_coords(img)
162
+ assert dx == approx(0.0)
163
+ assert dy > 0.0
164
+
165
+
166
+ def _make_positional_image(h=6, w=9, dx=1.0, dy=1.0, x0=0.0, y0=0.0) -> ImageObj:
167
+ """
168
+ pixel(y, x) = 1000*y + x (strictly monotone in both axes)
169
+ Choosing H=6, W=9 allows clean 2x3 tiling (cell=3x3).
170
+ """
171
+ data = np.add.outer(
172
+ 1000 * np.arange(h, dtype=np.int32), np.arange(w, dtype=np.int32)
173
+ )
174
+ img = create_image("positional", data)
175
+ img.set_uniform_coords(dx, dy, x0, y0)
176
+ img.roi = None
177
+ return img
178
+
179
+
180
+ def test_roi_grid_extract_matches_pattern() -> None:
181
+ """Test that the extracted ROIs match the expected pattern."""
182
+ img = _make_positional_image(h=6, w=9) # 6x9 → ny=2, nx=3 → cells are 3x3
183
+
184
+ # Build a full-coverage grid: each ROI == cell (no gaps/overlaps)
185
+ p = ROIGridParam()
186
+ p.nx, p.ny = 3, 2
187
+ p.xsize = p.ysize = 100 # ROI size == cell size
188
+ p.xtranslation = p.ytranslation = 50 # centered on each cell
189
+ p.xdirection = p.ydirection = Direction.INCREASING
190
+ p.base_name = "ROI"
191
+ p.name_pattern = "{base}({r},{c})"
192
+
193
+ roi = generate_image_grid_roi(img, p) # pure builder, no mutation
194
+ # Sanity: full coverage, 3*2 cells, all 3x3
195
+
196
+ params = roi.to_params(img)
197
+ for rparam in params:
198
+ # Reference window from indices
199
+ x0, y0, x1, y1 = rparam.get_bounding_box_indices(img)
200
+ ref = img.data[y0:y1, x0:x1]
201
+
202
+ # Extract via computation function
203
+ extracted = extract_roi(img, rparam)
204
+ out = extracted.data
205
+
206
+ # 1) Pixel-exact equality
207
+ assert_array_equal(out, ref)
208
+
209
+ # 2) Dimensions are the expected 3x3
210
+ assert out.shape == (3, 3)
211
+
212
+ # 3) Physical origin is consistent with bounding box (dx=dy=1, x0=y0=0)
213
+ px0, py0, _px1, _py1 = rparam.get_bounding_box_physical()
214
+ assert extracted.x0 == px0
215
+ assert extracted.y0 == py0
216
+
217
+
218
+ def test_roi_grid_extract_with_translation() -> None:
219
+ """Shift the grid by +10% in X (of full image width) and verify that each ROI
220
+ moves accordingly — the extracted content matches the shifted reference.
221
+ """
222
+ img = _make_positional_image(h=6, w=9) # width=9 → +10% shift = 0.9 pixel
223
+
224
+ # Base centered grid (2x3)
225
+ p1 = ROIGridParam()
226
+ p1.nx, p1.ny = 3, 2
227
+ p1.xsize = p1.ysize = 100
228
+ p1.xtranslation = p1.ytranslation = 50
229
+ p1.xdirection = p1.ydirection = Direction.INCREASING
230
+ p1.base_name = "ROI"
231
+ p1.name_pattern = "{base}({r},{c})"
232
+
233
+ # Shifted grid (+10% in X)
234
+ p2 = deepcopy(p1)
235
+ p2.xtranslation = 60
236
+
237
+ roi1 = generate_image_grid_roi(img, p1)
238
+ roi2 = generate_image_grid_roi(img, p2)
239
+
240
+ # Compare first ROI of each row/col "logically" (same label), but expect
241
+ # a one-pixel shift when rounding indices. To avoid rounding heuristics,
242
+ # we compare against each ROI's own bounding box-derived slice.
243
+ roiparams1, roiparams2 = roi1.to_params(img), roi2.to_params(img)
244
+ for rp1, rp2 in zip(roiparams1, roiparams2):
245
+ ref1 = img.data[
246
+ rp1.get_bounding_box_indices(img)[1] : rp1.get_bounding_box_indices(img)[3],
247
+ rp1.get_bounding_box_indices(img)[0] : rp1.get_bounding_box_indices(img)[2],
248
+ ]
249
+ ref2 = img.data[
250
+ rp2.get_bounding_box_indices(img)[1] : rp2.get_bounding_box_indices(img)[3],
251
+ rp2.get_bounding_box_indices(img)[0] : rp2.get_bounding_box_indices(img)[2],
252
+ ]
253
+ out1 = extract_roi(img, rp1).data
254
+ out2 = extract_roi(img, rp2).data
255
+ # Both extractions must match their own references exactly
256
+ assert_array_equal(out1, ref1)
257
+ assert_array_equal(out2, ref2)
258
+
259
+
260
+ def test_roi_grid_import_export() -> None:
261
+ """Test the import and export of ROI grids."""
262
+ p = ROIGridParam()
263
+ p.nx, p.ny = 3, 2
264
+ p.xsize = p.ysize = 100
265
+ p.xtranslation = p.ytranslation = 50
266
+ p.xdirection = p.ydirection = Direction.INCREASING
267
+ p.base_name = "ROI"
268
+ p.name_pattern = "{base}({r},{c})"
269
+
270
+ with WorkdirRestoringTempDir() as temp_dir:
271
+ path = osp.join(temp_dir, "test_roi_grid.json")
272
+ write_roi_grid(path, p)
273
+ new_p = read_roi_grid(path)
274
+
275
+ gds.assert_datasets_equal(new_p, p, "Imported ROI grid does not match original")
276
+
277
+
278
+ if __name__ == "__main__":
279
+ test_roi_grid_import_export()
@@ -0,0 +1,40 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Image spectrum unit test.
5
+ """
6
+
7
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
8
+ # pylint: disable=duplicate-code
9
+
10
+ import pytest
11
+
12
+ import sigima.tools.image
13
+ from sigima.tests import guiutils
14
+ from sigima.tests.data import get_test_image
15
+
16
+
17
+ @pytest.mark.gui
18
+ def test_image_spectrum_interactive():
19
+ """Interactive test of the magnitude/phase/power spectrum of an image."""
20
+ with guiutils.lazy_qt_app_context(force=True):
21
+ # pylint: disable=import-outside-toplevel
22
+ from sigima.tests.vistools import view_images_side_by_side
23
+
24
+ obj = get_test_image("NF 180338201.scor-data")
25
+ data = obj.data
26
+ ms = sigima.tools.image.magnitude_spectrum(data, log_scale=True)
27
+ ps = sigima.tools.image.phase_spectrum(data)
28
+ psd = sigima.tools.image.psd(data, log_scale=True)
29
+ images = [data, ms, ps, psd]
30
+ titles = [
31
+ "Original",
32
+ "Magnitude spectrum",
33
+ "Phase spectrum",
34
+ "Power spectral density",
35
+ ]
36
+ view_images_side_by_side(images, titles, rows=2, title="Image spectrum")
37
+
38
+
39
+ if __name__ == "__main__":
40
+ test_image_spectrum_interactive()
@@ -0,0 +1,91 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Unit tests for thresholding computation functions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import pytest
10
+ from skimage import filters, util
11
+
12
+ import sigima.params
13
+ import sigima.proc.image
14
+ from sigima.tests.data import get_test_image
15
+ from sigima.tests.helpers import check_array_result
16
+
17
+
18
+ @pytest.mark.validation
19
+ def test_threshold() -> None:
20
+ """Validation test for the image threshold processing."""
21
+ src = get_test_image("flower.npy")
22
+ p = sigima.params.ThresholdParam.create(value=100.0)
23
+ dst = sigima.proc.image.threshold(src, p)
24
+ exp = util.img_as_ubyte(src.data > p.value)
25
+ check_array_result(f"Threshold[{p.value}]", dst.data, exp)
26
+
27
+
28
+ def __generic_threshold_validation(method: str) -> None:
29
+ """Generic test for thresholding methods."""
30
+ # See [1] in sigima\tests\image\__init__.py for more details about the validation.
31
+ src = get_test_image("flower.npy")
32
+ dst = sigima.proc.image.threshold(
33
+ src, sigima.params.ThresholdParam.create(method=method)
34
+ )
35
+ exp = util.img_as_ubyte(
36
+ src.data > getattr(filters, f"threshold_{method}")(src.data)
37
+ )
38
+ check_array_result(f"Threshold{method.capitalize()}", dst.data, exp)
39
+
40
+
41
+ @pytest.mark.validation
42
+ def test_threshold_isodata() -> None:
43
+ """Validation test for the image threshold Isodata processing."""
44
+ __generic_threshold_validation("isodata")
45
+
46
+
47
+ @pytest.mark.validation
48
+ def test_threshold_li() -> None:
49
+ """Validation test for the image threshold Li processing."""
50
+ __generic_threshold_validation("li")
51
+
52
+
53
+ @pytest.mark.validation
54
+ def test_threshold_mean() -> None:
55
+ """Validation test for the image threshold Mean processing."""
56
+ __generic_threshold_validation("mean")
57
+
58
+
59
+ @pytest.mark.validation
60
+ def test_threshold_minimum() -> None:
61
+ """Validation test for the image threshold Minimum processing."""
62
+ __generic_threshold_validation("minimum")
63
+
64
+
65
+ @pytest.mark.validation
66
+ def test_threshold_otsu() -> None:
67
+ """Validation test for the image threshold Otsu processing."""
68
+ __generic_threshold_validation("otsu")
69
+
70
+
71
+ @pytest.mark.validation
72
+ def test_threshold_triangle() -> None:
73
+ """Validation test for the image threshold Triangle processing."""
74
+ __generic_threshold_validation("triangle")
75
+
76
+
77
+ @pytest.mark.validation
78
+ def test_threshold_yen() -> None:
79
+ """Validation test for the image threshold Yen processing."""
80
+ __generic_threshold_validation("yen")
81
+
82
+
83
+ if __name__ == "__main__":
84
+ test_threshold()
85
+ test_threshold_isodata()
86
+ test_threshold_li()
87
+ test_threshold_mean()
88
+ test_threshold_minimum()
89
+ test_threshold_otsu()
90
+ test_threshold_triangle()
91
+ test_threshold_yen()
File without changes
@@ -0,0 +1,125 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Test adding new I/O formats
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Type
10
+
11
+ import numpy as np
12
+
13
+ from sigima.io import ImageIORegistry, SignalIORegistry
14
+ from sigima.io.base import FormatInfo
15
+ from sigima.io.image.base import SingleImageFormatBase
16
+ from sigima.io.signal.base import SignalFormatBase
17
+ from sigima.tests.env import execenv
18
+
19
+
20
+ def _get_image_format_number() -> int:
21
+ """Get the number of standard image formats"""
22
+ return len(ImageIORegistry.get_formats())
23
+
24
+
25
+ def _add_image_format() -> Type[SingleImageFormatBase]:
26
+ """Add a new image format to the registry"""
27
+
28
+ class MyImageFormat(SingleImageFormatBase):
29
+ """Object representing MyImageFormat image file type"""
30
+
31
+ FORMAT_INFO = FormatInfo(
32
+ name="MyImageFormat",
33
+ extensions="*.myimg",
34
+ readable=True,
35
+ writeable=False,
36
+ )
37
+
38
+ @staticmethod
39
+ def read_data(filename: str) -> np.ndarray:
40
+ """Read data and return it
41
+
42
+ Args:
43
+ filename (str): path to MyImageFormat file
44
+
45
+ Returns:
46
+ np.ndarray: image data
47
+ """
48
+ # Implement reading logic here
49
+
50
+ return MyImageFormat
51
+
52
+
53
+ def test_add_image_format() -> None:
54
+ """Test adding a new image format"""
55
+ n1 = _get_image_format_number()
56
+ execenv.print(f"Number of standard image formats: {n1}")
57
+ execenv.print("Adding MyImageFormat... ", end="")
58
+ image_class = _add_image_format()
59
+ n2 = _get_image_format_number()
60
+ assert n2 == n1 + 1, "Image format was not added correctly"
61
+ execenv.print("OK")
62
+ execenv.print(f"New number of image formats: {n2}")
63
+ assert (
64
+ sum(isinstance(fmt, image_class) for fmt in ImageIORegistry.get_formats()) == 1
65
+ )
66
+ finfo = image_class.FORMAT_INFO
67
+ finfo_str = "\n".join([(" " * 4) + line for line in str(finfo).splitlines()])
68
+ assert finfo_str in ImageIORegistry.get_format_info(mode="text")
69
+
70
+
71
+ def _get_signal_format_number() -> int:
72
+ """Get the number of standard signal formats"""
73
+ return len(SignalIORegistry.get_formats())
74
+
75
+
76
+ def _add_signal_format() -> Type[SignalFormatBase]:
77
+ """Add a new signal format to the registry"""
78
+
79
+ class MySignalFormat(SignalFormatBase):
80
+ """Object representing MySignalFormat signal file type"""
81
+
82
+ FORMAT_INFO = FormatInfo(
83
+ name="MySignalFormat",
84
+ extensions="*.mysig",
85
+ readable=True,
86
+ writeable=False,
87
+ )
88
+
89
+ def read_xydata(self, filename: str) -> np.ndarray:
90
+ """Read data and metadata from file, write metadata to object, return xydata
91
+
92
+ Args:
93
+ filename: Name of file to read
94
+
95
+ Returns:
96
+ NumPy array xydata
97
+ """
98
+ # Implement reading logic here
99
+ print(f"Reading data from {filename}")
100
+
101
+ return MySignalFormat
102
+
103
+
104
+ def test_add_signal_format() -> None:
105
+ """Test adding a new signal format"""
106
+ n1 = _get_signal_format_number()
107
+ execenv.print(f"Number of standard signal formats: {n1}")
108
+ execenv.print("Adding MySignalFormat... ", end="")
109
+ signal_class = _add_signal_format()
110
+ n2 = _get_signal_format_number()
111
+ assert n2 == n1 + 1, "Signal format was not added correctly"
112
+ execenv.print("OK")
113
+ execenv.print(f"New number of signal formats: {n2}")
114
+ assert (
115
+ sum(isinstance(fmt, signal_class) for fmt in SignalIORegistry.get_formats())
116
+ == 1
117
+ )
118
+ finfo = signal_class.FORMAT_INFO
119
+ finfo_str = "\n".join([(" " * 4) + line for line in str(finfo).splitlines()])
120
+ assert finfo_str in SignalIORegistry.get_format_info(mode="text")
121
+
122
+
123
+ if __name__ == "__main__":
124
+ test_add_image_format()
125
+ test_add_signal_format()