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,532 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Module providing test utilities
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import atexit
10
+ import functools
11
+ import os
12
+ import os.path as osp
13
+ import pathlib
14
+ import subprocess
15
+ import sys
16
+ import tempfile
17
+ import warnings
18
+ from collections.abc import Callable
19
+ from typing import Any, Generator
20
+
21
+ import numpy as np
22
+ from guidata.configtools import get_module_data_path
23
+
24
+ from sigima.config import MOD_NAME
25
+ from sigima.io.image import ImageIORegistry
26
+ from sigima.io.signal import SignalIORegistry
27
+ from sigima.objects.image import ImageObj
28
+ from sigima.objects.signal import SignalObj
29
+ from sigima.tests.env import execenv
30
+
31
+ TST_PATH = []
32
+
33
+
34
+ def get_test_paths() -> list[str]:
35
+ """Return the list of test data paths"""
36
+ return TST_PATH
37
+
38
+
39
+ def add_test_path(path: str) -> None:
40
+ """Appends test data path, after normalizing it and making it absolute.
41
+ Do nothing if the path is already in the list.
42
+
43
+ Args:
44
+ Path to add to the list of test data paths
45
+
46
+ Raises:
47
+ FileNotFoundError: if the path does not exist
48
+ """
49
+ path = osp.abspath(osp.normpath(path))
50
+ if path not in TST_PATH:
51
+ if not osp.exists(path):
52
+ raise FileNotFoundError(f"Test data path does not exist: {path}")
53
+ TST_PATH.append(path)
54
+
55
+
56
+ def add_test_path_from_env(envvar: str) -> None:
57
+ """Appends test data path from environment variable (fails silently)"""
58
+ # Note: this function is used in third-party plugins
59
+ path = os.environ.get(envvar)
60
+ if path:
61
+ add_test_path(path)
62
+
63
+
64
+ # Add test data files and folders pointed by `SIGIMA_DATA` environment variable:
65
+ add_test_path_from_env("SIGIMA_DATA")
66
+
67
+
68
+ def add_test_module_path(modname: str, relpath: str) -> None:
69
+ """
70
+ Appends test data path relative to a module name.
71
+ Used to add module local data that resides in a module directory
72
+ but will be shipped under sys.prefix / share/ ...
73
+
74
+ modname must be the name of an already imported module as found in
75
+ sys.modules
76
+ """
77
+ add_test_path(get_module_data_path(modname, relpath=relpath))
78
+
79
+
80
+ # Add test data files and folders for the Sigima module:
81
+ add_test_module_path(MOD_NAME, osp.join("data", "tests"))
82
+
83
+
84
+ def get_test_fnames(pattern: str, in_folder: str | None = None) -> list[str]:
85
+ """
86
+ Return the absolute path list to test files with specified pattern
87
+
88
+ Pattern may be a file name (basename), a wildcard (e.g. *.txt)...
89
+
90
+ Args:
91
+ pattern: pattern to match
92
+ in_folder: folder to search in, in test data path (default: None,
93
+ search in all test data paths)
94
+ """
95
+ pathlist = []
96
+ for pth in [osp.join(TST_PATH[0], in_folder)] if in_folder else TST_PATH:
97
+ pathlist += sorted(pathlib.Path(pth).rglob(pattern))
98
+ if not pathlist:
99
+ raise FileNotFoundError(f"Test file(s) {pattern} not found")
100
+ return [str(path) for path in pathlist]
101
+
102
+
103
+ def read_test_objects(
104
+ registry: SignalIORegistry | ImageIORegistry,
105
+ pattern: str = "*.*",
106
+ in_folder: str | None = None,
107
+ ) -> Generator[tuple[str, ImageObj | None] | tuple[str, SignalObj | None], None, None]:
108
+ """Read test images and yield their file names and objects
109
+
110
+ Args:
111
+ registry: I/O registry to use
112
+ pattern: File name pattern to match
113
+ in_folder: Folder to search for test files
114
+
115
+ Yields:
116
+ Tuple of file name and object (or None if not implemented)
117
+ """
118
+ if registry is ImageIORegistry:
119
+ in_folder = in_folder or "image_formats"
120
+ elif registry is SignalIORegistry:
121
+ in_folder = in_folder or "curve_formats"
122
+ else:
123
+ raise ValueError(f"Unsupported registry type: {registry}")
124
+ fnames = get_test_fnames(pattern, in_folder)
125
+ for fname in fnames:
126
+ try:
127
+ obj = registry.read(fname)[0]
128
+ yield fname, obj
129
+ except NotImplementedError:
130
+ yield fname, None
131
+
132
+
133
+ def try_open_test_data(title: str, pattern: str) -> Callable:
134
+ """Decorator handling test data opening"""
135
+
136
+ def try_open_test_data_decorator(func: Callable) -> Callable:
137
+ """Decorator handling test data opening"""
138
+
139
+ @functools.wraps(func)
140
+ def func_wrapper(*args, **kwargs) -> None:
141
+ """Decorator wrapper function"""
142
+ execenv.print(title + ":")
143
+ execenv.print("-" * len(title))
144
+ try:
145
+ for fname in get_test_fnames(pattern):
146
+ execenv.print(f"=> Opening: {fname}")
147
+ func(fname, title, *args, **kwargs)
148
+ except FileNotFoundError:
149
+ execenv.print(f" No test data available for {pattern}")
150
+ finally:
151
+ execenv.print(os.linesep)
152
+
153
+ return func_wrapper
154
+
155
+ return try_open_test_data_decorator
156
+
157
+
158
+ def get_default_test_name(suffix: str | None = None) -> str:
159
+ """Return default test name based on script name"""
160
+ name = osp.splitext(osp.basename(sys.argv[0]))[0]
161
+ if suffix is not None:
162
+ name += "_" + suffix
163
+ return name
164
+
165
+
166
+ def get_output_data_path(extension: str, suffix: str | None = None) -> str:
167
+ """Return full path for data file with extension, generated by a test script"""
168
+ name = get_default_test_name(suffix)
169
+ return osp.join(TST_PATH[0], f"{name}.{extension}")
170
+
171
+
172
+ def reduce_path(filename: str) -> str:
173
+ """Reduce a file path to a relative path
174
+
175
+ Args:
176
+ filename: path to reduce
177
+
178
+ Returns:
179
+ Relative path to the file, relative to its parent directory
180
+ """
181
+ return osp.relpath(filename, osp.join(osp.dirname(filename), osp.pardir))
182
+
183
+
184
+ class WorkdirRestoringTempDir(tempfile.TemporaryDirectory):
185
+ """Enhanced temporary directory with working directory preservation.
186
+
187
+ A subclass of :py:class:`tempfile.TemporaryDirectory` that:
188
+
189
+ * Preserves and automatically restores the working directory during cleanup
190
+ * Handles common cleanup errors silently (PermissionError, RecursionError)
191
+
192
+ Example::
193
+
194
+ with WorkdirRestoringTempDir() as tmpdir:
195
+ os.chdir(tmpdir) # Directory change is automatically reverted at exit
196
+ """
197
+
198
+ def __init__(self) -> None:
199
+ super().__init__()
200
+ self.__cwd = os.getcwd()
201
+
202
+ def cleanup(self) -> None:
203
+ """Clean up temporary directory, restore working directory, ignore errors."""
204
+ os.chdir(self.__cwd)
205
+ try:
206
+ super().cleanup()
207
+ except (PermissionError, RecursionError):
208
+ pass
209
+
210
+
211
+ def get_temporary_directory() -> str:
212
+ """Return path to a temporary directory, and clean-up at exit"""
213
+ tmp = WorkdirRestoringTempDir()
214
+ atexit.register(tmp.cleanup)
215
+ return tmp.name
216
+
217
+
218
+ def exec_script(
219
+ path: str,
220
+ wait: bool = True,
221
+ args: list[str] = None,
222
+ env: dict[str, str] | None = None,
223
+ verbose: bool = False,
224
+ ) -> subprocess.Popen | None:
225
+ """Run test script.
226
+
227
+ Args:
228
+ path: path to script
229
+ wait: wait for script to finish
230
+ args: arguments to pass to script
231
+ env: environment variables to pass to script
232
+ verbose: if True, print command and output
233
+
234
+ Returns:
235
+ subprocess.Popen object if wait is False, None otherwise
236
+ """
237
+ stderr = subprocess.DEVNULL if execenv.unattended else None
238
+ # pylint: disable=consider-using-with
239
+ if verbose:
240
+ command = [sys.executable, path] + ([] if args is None else args)
241
+ proc = subprocess.Popen(
242
+ command,
243
+ stdout=subprocess.PIPE,
244
+ stderr=subprocess.PIPE,
245
+ env=env,
246
+ text=True,
247
+ )
248
+ else:
249
+ command = [sys.executable, '"' + path + '"'] + ([] if args is None else args)
250
+ proc = subprocess.Popen(" ".join(command), shell=True, stderr=stderr, env=env)
251
+ if wait:
252
+ if verbose:
253
+ stdout, stderr = proc.communicate()
254
+ print("Command:", " ".join(command))
255
+ print("Return code:", proc.returncode)
256
+ print("---- STDOUT ----\n", stdout)
257
+ print("---- STDERR ----\n", stderr)
258
+ return None
259
+ proc.wait()
260
+ return proc
261
+
262
+
263
+ def get_script_output(
264
+ path: str, args: list[str] = None, env: dict[str, str] | None = None
265
+ ) -> str:
266
+ """Run test script and return its output.
267
+
268
+ Args:
269
+ path (str): path to script
270
+ args (list): arguments to pass to script
271
+ env (dict): environment variables to pass to script
272
+
273
+ Returns:
274
+ str: script output
275
+ """
276
+ command = [sys.executable, '"' + path + '"'] + ([] if args is None else args)
277
+ result = subprocess.run(
278
+ " ".join(command), capture_output=True, text=True, env=env, check=False
279
+ )
280
+ return result.stdout.strip()
281
+
282
+
283
+ def compare_lists(
284
+ list1: list, list2: list, level: int = 1, raise_on_diff: bool = False
285
+ ) -> tuple[bool, list[str]]:
286
+ """Compare two lists
287
+
288
+ Args:
289
+ list1: first list
290
+ list2: second list
291
+ level: recursion level
292
+ raise_on_diff: if True, raise an AssertionError on difference (default: False)
293
+
294
+ Returns:
295
+ A tuple (same, diff) where `same` is True if lists are the same,
296
+ False otherwise, and `diff` is a list of differences found
297
+
298
+ Raises:
299
+ AssertionError: if raise_on_diff is True and lists are different
300
+ """
301
+ same = True
302
+ prefix = " " * level
303
+ diff = []
304
+ # Check for length mismatch
305
+ if len(list1) != len(list2):
306
+ same = False
307
+ diff += [f"{prefix}Lists have different lengths: {len(list1)} != {len(list2)}"]
308
+ for idx, (elem1, elem2) in enumerate(zip(list1, list2)):
309
+ execenv.print(f"{prefix}Checking element {idx}...", end=" ")
310
+ if isinstance(elem1, (list, tuple)):
311
+ execenv.print("")
312
+ cl_same, cl_diff = compare_lists(elem1, elem2, level + 1)
313
+ diff += cl_diff
314
+ same = same and cl_same
315
+ elif isinstance(elem1, dict):
316
+ execenv.print("")
317
+ cm_same, cm_diff = compare_metadata(elem1, elem2, level + 1)
318
+ diff += cm_diff
319
+ same = same and cm_same
320
+ else:
321
+ same_value = str(elem1) == str(elem2)
322
+ if not same_value:
323
+ diff += [
324
+ f"{prefix}Different values for element {idx}: {elem1} != {elem2}"
325
+ ]
326
+ same = same and same_value
327
+ execenv.print("OK" if same_value else "KO")
328
+ if diff:
329
+ all_diff = os.linesep.join(diff)
330
+ if raise_on_diff:
331
+ raise AssertionError(all_diff)
332
+ execenv.print("Lists are different:")
333
+ execenv.print(all_diff)
334
+ return same, diff
335
+
336
+
337
+ def compare_metadata(
338
+ dict1: dict[str, Any],
339
+ dict2: dict[str, Any],
340
+ level: int = 1,
341
+ raise_on_diff: bool = False,
342
+ ) -> tuple[bool, list[str]]:
343
+ """Compare metadata dictionaries without private elements
344
+
345
+ Args:
346
+ dict1: first dictionary, exclusively with string keys
347
+ dict2: second dictionary, exclusively with string keys
348
+ level: recursion level
349
+ raise_on_diff: if True, raise an AssertionError on difference (default: False)
350
+
351
+ Returns:
352
+ A tuple (same, diff) where `same` is True if dictionaries are the same,
353
+ False otherwise, and `diff` is a list of differences found
354
+
355
+ Raises:
356
+ AssertionError: if raise_on_diff is True and metadata is different
357
+ """
358
+ dict_a, dict_b = dict1.copy(), dict2.copy()
359
+ for dict_ in (dict_a, dict_b):
360
+ for key in list(dict_.keys()):
361
+ if key.startswith("__"):
362
+ dict_.pop(key)
363
+ same = True
364
+ prefix = " " * level
365
+ diff = []
366
+ # Check for keys only in dict_a
367
+ for key in dict_a:
368
+ if key not in dict_b:
369
+ same = False
370
+ diff += [f"{prefix}Key {key} found in first dict but not in second"]
371
+ continue
372
+ val_a, val_b = dict_a[key], dict_b[key]
373
+ execenv.print(f"{prefix}Checking key {key}...", end=" ")
374
+ if isinstance(val_a, dict):
375
+ execenv.print("")
376
+ cm_same, cm_diff = compare_metadata(val_a, val_b, level + 1)
377
+ diff += cm_diff
378
+ same = same and cm_same
379
+ elif isinstance(val_a, (list, tuple)):
380
+ execenv.print("")
381
+ cl_same, cl_diff = compare_lists(val_a, val_b, level + 1)
382
+ diff += cl_diff
383
+ same = same and cl_same
384
+ else:
385
+ same_value = str(val_a) == str(val_b)
386
+ if not same_value:
387
+ diff += [f"{prefix}Different values for key {key}: {val_a} != {val_b}"]
388
+ same = same and same_value
389
+ execenv.print("OK" if same_value else "KO")
390
+ # Check for keys only in dict_b
391
+ for key in dict_b:
392
+ if key not in dict_a:
393
+ same = False
394
+ diff += [f"{prefix}Key {key} found in second dict but not in first"]
395
+ if diff:
396
+ all_diff = os.linesep.join(diff)
397
+ if raise_on_diff:
398
+ raise AssertionError(all_diff)
399
+ execenv.print("Dictionaries are different:")
400
+ execenv.print(all_diff)
401
+ return same, diff
402
+
403
+
404
+ def __evaluate_func_safely(func: Callable, fallback: float | int = np.nan) -> Any:
405
+ """Evaluate function, ignore warnings and exceptions.
406
+
407
+ Args:
408
+ func: function to evaluate
409
+ fallback: value to return if function raises an exception (default: np.nan)
410
+
411
+ Returns:
412
+ Function result, or fallback value if function raises an exception
413
+ """
414
+ with warnings.catch_warnings():
415
+ warnings.simplefilter("ignore")
416
+ try:
417
+ return func()
418
+ except Exception: # pylint: disable=broad-except
419
+ return fallback
420
+
421
+
422
+ def __array_to_str(data: np.ndarray) -> str:
423
+ """Return a compact description of the array properties.
424
+
425
+ Args:
426
+ data: input array
427
+
428
+ Returns:
429
+ String describing array dimensions, dtype, min/max, mean, std, sum
430
+ """
431
+ dims = "×".join(str(dim) for dim in data.shape)
432
+ efs = __evaluate_func_safely
433
+ return (
434
+ f"{dims},{data.dtype},"
435
+ f"{efs(data.min):.2g}→{efs(data.max):.2g},"
436
+ f"µ={efs(data.mean):.2g},σ={efs(data.std):.2g},∑={efs(data.sum):.2g}"
437
+ )
438
+
439
+
440
+ def check_array_result(
441
+ title: str,
442
+ res: np.ndarray,
443
+ exp: np.ndarray,
444
+ rtol: float = 1.0e-5,
445
+ atol: float = 1.0e-8,
446
+ similar: bool = False,
447
+ sort: bool = False,
448
+ verbose: bool = True,
449
+ ) -> None:
450
+ """Assert that two arrays are almost equal.
451
+
452
+ Args:
453
+ title: title of the test
454
+ res: result array
455
+ exp: expected array
456
+ rtol: relative tolerance for comparison
457
+ atol: absolute tolerance for comparison
458
+ similar: if True, arrays are compared exclusively using their textual
459
+ global representation (e.g. '824,float64,-0.00012→0.036,µ=0.018')
460
+ sort: if True, sort arrays before comparison (default: False)
461
+ verbose: if True, print detailed result (default: True)
462
+
463
+ Raises:
464
+ AssertionError: if arrays are not almost equal or have different dtypes
465
+ """
466
+ if sort:
467
+ res = np.sort(np.array(res, copy=True), axis=None)
468
+ exp = np.sort(np.array(exp, copy=True), axis=None)
469
+ restxt = f"{title}: {__array_to_str(res)} (expected: {__array_to_str(exp)})"
470
+ if verbose:
471
+ execenv.print(restxt)
472
+ assert res.shape == exp.shape, f"{restxt} - Different shapes"
473
+ try:
474
+ if similar:
475
+ assert __array_to_str(res) == __array_to_str(exp), restxt
476
+ else:
477
+ assert np.allclose(res, exp, rtol=rtol, atol=atol, equal_nan=True), restxt
478
+ except AssertionError as exc:
479
+ raise AssertionError(restxt) from exc
480
+ assert res.dtype == exp.dtype, f"{restxt} - Different dtypes"
481
+
482
+
483
+ def check_scalar_result(
484
+ title: str,
485
+ res: float,
486
+ exp: float | tuple[float, ...],
487
+ rtol: float = 1.0e-5,
488
+ atol: float = 1.0e-8,
489
+ verbose: bool = True,
490
+ ) -> None:
491
+ """Assert that two scalars are almost equal.
492
+
493
+ Args:
494
+ title: title of the test
495
+ res: result value
496
+ exp: expected value or tuple of expected values
497
+ rtol: relative tolerance for comparison
498
+ atol: absolute tolerance for comparison
499
+ verbose: if True, print detailed result (default: True)
500
+
501
+ Raises:
502
+ AssertionError: if values are not almost equal or if expected is not a scalar
503
+ or tuple
504
+ """
505
+ restxt = f"{title}: {res} (expected: {exp}) ± {rtol * abs(exp) + atol:.2g}"
506
+ if verbose:
507
+ execenv.print(restxt)
508
+ if isinstance(exp, tuple):
509
+ assert any(np.isclose(res, exp_val, rtol=rtol, atol=atol) for exp_val in exp), (
510
+ restxt
511
+ )
512
+ else:
513
+ assert np.isclose(res, exp, rtol=rtol, atol=atol), restxt
514
+
515
+
516
+ def print_obj_data_dimensions(obj: SignalObj | ImageObj, indent: int = 0) -> None:
517
+ """Print data array shape for the given signal or image object,
518
+ including ROI data if available.
519
+
520
+ Args:
521
+ obj: Signal or image object to print data dimensions for.
522
+ indent: Indentation level for printing (default: 0)
523
+ """
524
+ indent_str = " " * indent
525
+ execenv.print(f"{indent_str}Accessing object '{obj.title}':")
526
+ execenv.print(f"{indent_str} data: {__array_to_str(obj.data)}")
527
+ if obj.roi is not None:
528
+ for idx in range(len(obj.roi)):
529
+ roi_data = obj.get_data(idx)
530
+ if isinstance(obj, SignalObj):
531
+ roi_data = roi_data[1] # y data
532
+ execenv.print(f"{indent_str} ROI[{idx}]: {__array_to_str(roi_data)}")
@@ -0,0 +1,28 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Unit tests for image features
5
+ -----------------------------
6
+
7
+ [1] Implementation note regarding scikit-image methods
8
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9
+
10
+ The following note applies to:
11
+ - thresholding methods (isodata, li, mean, minimum, otsu, triangle, yen)
12
+ - exposure methods (adjust_gamma, adjust_log, adjust_sigmoid, rescale_intensity,
13
+ equalize_hist, equalize_adapthist)
14
+ - restoration methods (denoise_tv, denoise_bilateral, denoise_wavelet)
15
+ - morphology methods (white_tophat, black_tophat, erosion, dilation, opening, closing)
16
+ - edge detection methods (canny, roberts, prewitt, sobel, scharr, farid, laplace)
17
+
18
+ The thresholding, morphological, and edge detection methods are implemented
19
+ in the scikit-image library: those algorithms are considered to be validated,
20
+ so we can use them as reference.
21
+ As a consequence, the only purpose of the associated validation tests is to check
22
+ if the methods are correctly called and if the results are consistent with
23
+ the reference implementation.
24
+
25
+ In other words, we are not testing the correctness of the algorithms, but
26
+ the correctness of the interface between the Sigima and the scikit-image
27
+ libraries.
28
+ """
@@ -0,0 +1,128 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Image pixel binning computation test
5
+ """
6
+
7
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
8
+ # pylint: disable=duplicate-code
9
+
10
+ import time
11
+
12
+ import numpy as np
13
+ import pytest
14
+ from numpy import ma
15
+
16
+ import sigima.params
17
+ import sigima.proc.image
18
+ from sigima.enums import BinningOperation
19
+ from sigima.tests import guiutils
20
+ from sigima.tests.data import get_test_image
21
+ from sigima.tests.env import execenv
22
+ from sigima.tools.image import binning
23
+
24
+
25
+ def compare_binning_images(data: ma.MaskedArray) -> None:
26
+ """Compare binning images
27
+
28
+ Args:
29
+ data: Image data
30
+ """
31
+ # pylint: disable=import-outside-toplevel
32
+ from plotpy.builder import make
33
+
34
+ from sigima.tests.vistools import view_image_items
35
+
36
+ items = []
37
+ items += [make.image(data, interpolation="nearest", eliminate_outliers=2.0)]
38
+ # Computing pixel binning
39
+ oa_t0 = time.time()
40
+ for ix in range(1, 5):
41
+ sx = 2**ix
42
+ for iy in range(1, 5):
43
+ sy = 2**iy
44
+ for operation in BinningOperation:
45
+ t0 = time.time()
46
+ bdata = binning(data, sx=sx, sy=sy, operation=operation)
47
+ title = f"[{sx}x{sy},{operation.value}]"
48
+ item = make.image(
49
+ bdata,
50
+ title=title,
51
+ interpolation="nearest",
52
+ eliminate_outliers=2.0,
53
+ xdata=[0, data.shape[1]],
54
+ ydata=[0, data.shape[0]],
55
+ )
56
+ item.hide()
57
+ items.append(item)
58
+ dt = time.time() - t0
59
+ execenv.print(f" {title}: {int(dt * 1e3):d} ms")
60
+ oa_dt = time.time() - oa_t0
61
+ execenv.print(f" Overall calculation time: {int(oa_dt * 1e3):d} ms")
62
+ view_image_items(items, title="Binning test", show_itemlist=True)
63
+
64
+
65
+ @pytest.mark.gui
66
+ def test_binning_interactive() -> None:
67
+ """Test binning computation and show results"""
68
+ with guiutils.lazy_qt_app_context(force=True):
69
+ data = get_test_image("*.scor-data").data[:500, :500]
70
+ execenv.print(f"Data[dtype={data.dtype},shape={data.shape}]")
71
+ compare_binning_images(data.view(ma.MaskedArray))
72
+
73
+
74
+ @pytest.mark.validation
75
+ def test_binning() -> None:
76
+ """Validation test for binning computation"""
77
+ # Implementation note:
78
+ # ---------------------
79
+ #
80
+ # Pixel binning algorithm is validated graphically by comparing the results of
81
+ # different binning operations and sizes: that is the purpose of the
82
+ # `test_binning_graphically`` function.
83
+ # Formal validation is not possible without reimplementation of the algorithm
84
+ # here, which would be redundant and proove nothing. Instead, as a complementary
85
+ # test, we only validate some basic properties of the binning algorithm:
86
+ # - The output shape is correct
87
+ # - The output data type is correct
88
+ # - Some basic properties of the output data are correct (e.g. min, max, mean)
89
+
90
+ src = get_test_image("*.scor-data")
91
+ src.data = data = np.array(src.data[:500, :500], dtype=float)
92
+ ny, nx = data.shape
93
+
94
+ p = sigima.params.BinningParam()
95
+ for operation in BinningOperation:
96
+ p.operation = operation
97
+ for sx in range(2, 3):
98
+ for sy in range(2, 5):
99
+ p.sx = sx
100
+ p.sy = sy
101
+ rdata = data[: ny - (ny % sy), : nx - (nx % sx)]
102
+ dst = sigima.proc.image.binning(src, p)
103
+ bdata = dst.data
104
+ assert bdata.shape == (data.shape[0] // sy, data.shape[1] // sx)
105
+ assert bdata.dtype == data.dtype
106
+ if operation == "min":
107
+ assert bdata.min() == rdata.min()
108
+ elif operation == "max":
109
+ assert bdata.max() == rdata.max()
110
+ elif operation == "sum":
111
+ assert bdata.sum() == rdata.sum()
112
+ elif operation == "average":
113
+ assert bdata.mean() == rdata.mean()
114
+ for src_dtype in (float, np.uint8, np.uint16, np.int16):
115
+ src.data = data = np.array(src.data[:500, :500], dtype=src_dtype)
116
+ for dtype_str in p.dtypes:
117
+ p.dtype_str = dtype_str
118
+ dst = sigima.proc.image.binning(src, p)
119
+ bdata = dst.data
120
+ if dtype_str == "dtype":
121
+ assert bdata.dtype is data.dtype
122
+ else:
123
+ assert bdata.dtype is np.dtype(dtype_str)
124
+
125
+
126
+ if __name__ == "__main__":
127
+ test_binning_interactive()
128
+ test_binning()