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,524 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Image object definition
5
+ =======================
6
+
7
+ This module defines the main `ImageObj` class for representing 2D image data.
8
+
9
+ The `ImageObj` class provides:
10
+
11
+ - Data storage for 2D arrays with associated metadata
12
+ - Physical coordinate system with origin and pixel spacing
13
+ - Axis labeling and units
14
+ - Scale management (linear/logarithmic)
15
+ - DICOM template support
16
+ - ROI (Region of Interest) integration
17
+ - Coordinate conversion utilities (physical ↔ pixel)
18
+
19
+ This is the core class for image processing operations in Sigima.
20
+ """
21
+
22
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
23
+ # pylint: disable=duplicate-code
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ from collections.abc import Mapping
29
+ from typing import Any, Literal, Type
30
+
31
+ import guidata.dataset as gds
32
+ import numpy as np
33
+ from numpy import ma
34
+
35
+ from sigima.config import _
36
+ from sigima.objects import base
37
+ from sigima.objects.image.roi import ImageROI
38
+ from sigima.tools.datatypes import clip_astype
39
+
40
+
41
+ def to_builtin(obj) -> str | int | float | list | dict | np.ndarray | None:
42
+ """Convert an object implementing a numeric value or collection
43
+ into the corresponding builtin/NumPy type.
44
+
45
+ Return None if conversion fails."""
46
+ try:
47
+ return int(obj) if int(obj) == float(obj) else float(obj)
48
+ except (TypeError, ValueError):
49
+ pass
50
+ if isinstance(obj, str):
51
+ return obj
52
+ if hasattr(obj, "__iter__"):
53
+ try:
54
+ return list(obj)
55
+ except (TypeError, ValueError):
56
+ pass
57
+ if hasattr(obj, "__dict__"):
58
+ try:
59
+ return dict(obj.__dict__)
60
+ except (TypeError, ValueError):
61
+ pass
62
+ if isinstance(obj, np.ndarray):
63
+ return obj
64
+ return None
65
+
66
+
67
+ class ImageObj(gds.DataSet, base.BaseObj[ImageROI]):
68
+ """Image object"""
69
+
70
+ PREFIX = "i"
71
+ VALID_DTYPES = (
72
+ np.uint8,
73
+ np.uint16,
74
+ np.int16,
75
+ np.int32,
76
+ np.float32,
77
+ np.float64,
78
+ np.complex128,
79
+ )
80
+
81
+ def __init__(self, title=None, comment=None, icon=""):
82
+ """Constructor
83
+
84
+ Args:
85
+ title: title
86
+ comment: comment
87
+ icon: icon
88
+ """
89
+ gds.DataSet.__init__(self, title, comment, icon)
90
+ base.BaseObj.__init__(self)
91
+ self._dicom_template = None
92
+
93
+ @staticmethod
94
+ def get_roi_class() -> Type[ImageROI]:
95
+ """Return ROI class"""
96
+ # Import here to avoid circular imports
97
+
98
+ return ImageROI
99
+
100
+ def __add_metadata(self, key: str, value: Any) -> None:
101
+ """Add value to metadata if value can be converted into builtin/NumPy type
102
+
103
+ Args:
104
+ key: key
105
+ value: value
106
+ """
107
+ stored_val = to_builtin(value)
108
+ if stored_val is not None:
109
+ self.metadata[key] = stored_val
110
+
111
+ def __set_metadata_from(self, obj: Mapping | dict) -> None:
112
+ """Set metadata from object: dict-like (only string keys are considered)
113
+ or any other object (iterating over supported attributes)
114
+
115
+ Args:
116
+ obj: object
117
+ """
118
+ self.reset_metadata_to_defaults()
119
+ ptn = r"__[\S_]*__$"
120
+ if isinstance(obj, Mapping):
121
+ for key, value in obj.items():
122
+ if isinstance(key, str) and not re.match(ptn, key):
123
+ self.__add_metadata(key, value)
124
+ else:
125
+ for attrname in dir(obj):
126
+ if attrname != "GroupLength" and not re.match(ptn, attrname):
127
+ try:
128
+ attr = getattr(obj, attrname)
129
+ if not callable(attr) and attr:
130
+ self.__add_metadata(attrname, attr)
131
+ except AttributeError:
132
+ pass
133
+
134
+ @property
135
+ def dicom_template(self):
136
+ """Get DICOM template"""
137
+ return self._dicom_template
138
+
139
+ @dicom_template.setter
140
+ def dicom_template(self, template):
141
+ """Set DICOM template"""
142
+ if template is not None:
143
+ ipp = getattr(template, "ImagePositionPatient", None)
144
+ x0, y0 = 0.0, 0.0 if ipp is None else (float(ipp[0]), float(ipp[1]))
145
+ pxs = getattr(template, "PixelSpacing", None)
146
+ dx, dy = 1.0, 1.0 if pxs is None else (float(pxs[0]), float(pxs[1]))
147
+ self.set_uniform_coords(dx, dy, x0, y0)
148
+ self.__set_metadata_from(template)
149
+ self._dicom_template = template
150
+
151
+ _tabs = gds.BeginTabGroup("all")
152
+
153
+ _datag = gds.BeginGroup(_("Data"))
154
+ data = gds.FloatArrayItem(_("Data")) # type: ignore[assignment]
155
+ metadata = gds.DictItem(_("Metadata"), default={}) # type: ignore[assignment]
156
+ annotations = gds.StringItem(_("Annotations"), default="").set_prop(
157
+ "display",
158
+ hide=True,
159
+ ) # Annotations as a serialized JSON string # type: ignore[assignment]
160
+ _e_datag = gds.EndGroup(_("Data"))
161
+
162
+ def _compute_xmin(self) -> float:
163
+ """Compute Xmin"""
164
+ if self.data is None or self.data.size == 0:
165
+ return 0.0
166
+ if self.is_uniform_coords:
167
+ return self.x0
168
+ if self.xcoords is None or self.xcoords.size == 0:
169
+ return np.nan
170
+ return self.xcoords[0]
171
+
172
+ def _compute_xmax(self) -> float:
173
+ """Compute Xmax"""
174
+ if self.data is None or self.data.size == 0:
175
+ return 0.0
176
+ if self.is_uniform_coords:
177
+ return self.x0 + self.width - self.dx
178
+ if self.xcoords is None or self.xcoords.size == 0:
179
+ return np.nan
180
+ return self.xcoords[-1]
181
+
182
+ def _compute_ymin(self) -> float:
183
+ """Compute Ymin"""
184
+ if self.data is None or self.data.size == 0:
185
+ return 0.0
186
+ if self.is_uniform_coords:
187
+ return self.y0
188
+ if self.ycoords is None or self.ycoords.size == 0:
189
+ return np.nan
190
+ return self.ycoords[0]
191
+
192
+ def _compute_ymax(self) -> float:
193
+ """Compute Ymax"""
194
+ if self.data is None or self.data.size == 0:
195
+ return 0.0
196
+ if self.is_uniform_coords:
197
+ return self.y0 + self.height - self.dy
198
+ if self.ycoords is None or self.ycoords.size == 0:
199
+ return np.nan
200
+ return self.ycoords[-1]
201
+
202
+ _dxdyg = gds.BeginGroup(f"{_('Origin')} / {_('Pixel spacing')}")
203
+ _prop_uniform = gds.GetAttrProp("is_uniform_coords")
204
+ is_uniform_coords = gds.BoolItem(_("Uniform coordinates"), default=True).set_prop(
205
+ "display", store=_prop_uniform, active=False
206
+ )
207
+ _origin = gds.BeginGroup(_("Origin"))
208
+ x0 = gds.FloatItem("X<sub>0</sub>", default=0.0).set_prop(
209
+ "display", active=_prop_uniform
210
+ )
211
+ y0 = (
212
+ gds.FloatItem("Y<sub>0</sub>", default=0.0)
213
+ .set_prop("display", active=_prop_uniform)
214
+ .set_pos(col=1)
215
+ )
216
+ _e_origin = gds.EndGroup(_("Origin"))
217
+ _pixel_spacing = gds.BeginGroup(_("Pixel spacing"))
218
+ dx = gds.FloatItem("Δx", default=1.0).set_prop("display", active=_prop_uniform)
219
+ dy = (
220
+ gds.FloatItem("Δy", default=1.0)
221
+ .set_prop("display", active=_prop_uniform)
222
+ .set_pos(col=1)
223
+ )
224
+ _e_pixel_spacing = gds.EndGroup(_("Pixel spacing"))
225
+ _boundaries = gds.BeginGroup(_("Extent"))
226
+ xmin = gds.FloatItem("X<sub>MIN</sub>").set_computed(_compute_xmin)
227
+ xmax = gds.FloatItem("X<sub>MAX</sub>").set_pos(col=1).set_computed(_compute_xmax)
228
+ ymin = gds.FloatItem("Y<sub>MIN</sub>").set_computed(_compute_ymin)
229
+ ymax = gds.FloatItem("Y<sub>MAX</sub>").set_pos(col=1).set_computed(_compute_ymax)
230
+ _e_boundaries = gds.EndGroup(_("Extent"))
231
+ _e_dxdyg = gds.EndGroup(f"{_('Origin')} / {_('Pixel spacing')}")
232
+
233
+ _coordsg = gds.BeginGroup(_("Coordinates"))
234
+ xcoords = gds.FloatArrayItem(
235
+ _("X coordinates"),
236
+ default=np.array([], dtype=float),
237
+ ).set_prop("display", active=gds.NotProp(_prop_uniform)) # type: ignore[assignment]
238
+ ycoords = (
239
+ gds.FloatArrayItem(_("Y coordinates"), default=np.array([], dtype=float))
240
+ .set_prop("display", active=gds.NotProp(_prop_uniform))
241
+ .set_pos(col=1)
242
+ ) # type: ignore[assignment]
243
+ _e_coordsg = gds.EndGroup(_("Coordinates"))
244
+
245
+ def set_uniform_coords(
246
+ self, dx: float, dy: float, x0: float = 0.0, y0: float = 0.0
247
+ ) -> None:
248
+ """Set uniform coordinates and clear non-uniform arrays.
249
+
250
+ Args:
251
+ dx: pixel size along X-axis
252
+ dy: pixel size along Y-axis
253
+ x0: origin X-axis coordinate
254
+ y0: origin Y-axis coordinate
255
+ """
256
+ self.is_uniform_coords = True
257
+ self.xcoords = np.array([], dtype=float)
258
+ self.ycoords = np.array([], dtype=float)
259
+ self.dx, self.dy, self.x0, self.y0 = dx, dy, x0, y0
260
+
261
+ def set_coords(self, xcoords: np.ndarray, ycoords: np.ndarray) -> None:
262
+ """Set non-uniform coordinates.
263
+
264
+ Args:
265
+ xcoords: X coordinates
266
+ ycoords: Y coordinates
267
+ """
268
+ self.is_uniform_coords = False
269
+ self.xcoords = xcoords
270
+ self.ycoords = ycoords
271
+
272
+ def switch_coords_to(self, coords_type: Literal["uniform", "non-uniform"]) -> None:
273
+ """Switch coordinates to uniform or non-uniform representation.
274
+
275
+ If switching to uniform, the image pixel size and origin are computed from
276
+ the current non-uniform coordinates. If switching to non-uniform, the
277
+ corresponding coordinate arrays are generated from the current pixel size
278
+ and origin. If the current coordinates are already of the requested type,
279
+ no action is performed.
280
+
281
+ Args:
282
+ coords_type: 'uniform' or 'non-uniform'
283
+
284
+ Raises:
285
+ ValueError: If switching to uniform coordinates fails due to insufficient
286
+ non-uniform coordinates defined
287
+ """
288
+ if coords_type == "uniform" and not self.is_uniform_coords:
289
+ if self.xcoords.size >= 2 and self.ycoords.size >= 2:
290
+ x0, y0 = float(self.xcoords[0]), float(self.ycoords[0])
291
+ dx = float(self.xcoords[-1] - self.xcoords[0]) / (self.xcoords.size - 1)
292
+ dy = float(self.ycoords[-1] - self.ycoords[0]) / (self.ycoords.size - 1)
293
+ self.set_uniform_coords(dx, dy, x0, y0)
294
+ else:
295
+ raise ValueError(
296
+ "Cannot switch to uniform coordinates: "
297
+ "not enough non-uniform coordinates defined"
298
+ )
299
+ elif coords_type == "non-uniform" and self.is_uniform_coords:
300
+ shape = self.data.shape
301
+ xcoords = np.linspace(self.x0, self.x0 + self.dx * (shape[1] - 1), shape[1])
302
+ ycoords = np.linspace(self.y0, self.y0 + self.dy * (shape[0] - 1), shape[0])
303
+ self.set_coords(xcoords, ycoords)
304
+
305
+ _unitsg = gds.BeginGroup(_("Titles / Units"))
306
+ title = gds.StringItem(_("Image title"), default=_("Untitled"))
307
+ _tabs_u = gds.BeginTabGroup("units")
308
+ _unitsx = gds.BeginGroup(_("X-axis"))
309
+ xlabel = gds.StringItem(_("Title"), default="")
310
+ xunit = gds.StringItem(_("Unit"), default="")
311
+ _e_unitsx = gds.EndGroup(_("X-axis"))
312
+ _unitsy = gds.BeginGroup(_("Y-axis"))
313
+ ylabel = gds.StringItem(_("Title"), default="")
314
+ yunit = gds.StringItem(_("Unit"), default="")
315
+ _e_unitsy = gds.EndGroup(_("Y-axis"))
316
+ _unitsz = gds.BeginGroup(_("Z-axis"))
317
+ zlabel = gds.StringItem(_("Title"), default="")
318
+ zunit = gds.StringItem(_("Unit"), default="")
319
+ _e_unitsz = gds.EndGroup(_("Z-axis"))
320
+ _e_tabs_u = gds.EndTabGroup("units")
321
+ _e_unitsg = gds.EndGroup(_("Titles / Units"))
322
+
323
+ _scalesg = gds.BeginGroup(_("Scales"))
324
+ _prop_autoscale = gds.GetAttrProp("autoscale")
325
+ autoscale = gds.BoolItem(_("Auto scale"), default=True).set_prop(
326
+ "display", store=_prop_autoscale
327
+ )
328
+ _tabs_b = gds.BeginTabGroup("bounds")
329
+ _boundsx = gds.BeginGroup(_("X-axis"))
330
+ xscalelog = gds.BoolItem(_("Logarithmic scale"), default=False)
331
+ xscalemin = gds.FloatItem(_("Lower bound"), check=False).set_prop(
332
+ "display", active=gds.NotProp(_prop_autoscale)
333
+ )
334
+ xscalemax = gds.FloatItem(_("Upper bound"), check=False).set_prop(
335
+ "display", active=gds.NotProp(_prop_autoscale)
336
+ )
337
+ _e_boundsx = gds.EndGroup(_("X-axis"))
338
+ _boundsy = gds.BeginGroup(_("Y-axis"))
339
+ yscalelog = gds.BoolItem(_("Logarithmic scale"), default=False)
340
+ yscalemin = gds.FloatItem(_("Lower bound"), check=False).set_prop(
341
+ "display", active=gds.NotProp(_prop_autoscale)
342
+ )
343
+ yscalemax = gds.FloatItem(_("Upper bound"), check=False).set_prop(
344
+ "display", active=gds.NotProp(_prop_autoscale)
345
+ )
346
+ _e_boundsy = gds.EndGroup(_("Y-axis"))
347
+ _boundsz = gds.BeginGroup(_("LUT range"))
348
+ zscalemin = gds.FloatItem(_("Lower bound"), check=False)
349
+ zscalemax = gds.FloatItem(_("Upper bound"), check=False)
350
+ _e_boundsz = gds.EndGroup(_("LUT range"))
351
+ _e_tabs_b = gds.EndTabGroup("bounds")
352
+ _e_scalesg = gds.EndGroup(_("Scales"))
353
+
354
+ _e_tabs = gds.EndTabGroup("all")
355
+
356
+ @property
357
+ def width(self) -> float:
358
+ """Return image width, i.e. number of columns multiplied by pixel size"""
359
+ return self.data.shape[1] * self.dx
360
+
361
+ @property
362
+ def height(self) -> float:
363
+ """Return image height, i.e. number of rows multiplied by pixel size"""
364
+ return self.data.shape[0] * self.dy
365
+
366
+ @property
367
+ def xc(self) -> float:
368
+ """Return image center X-axis coordinate"""
369
+ return self.x0 + 0.5 * self.width
370
+
371
+ @property
372
+ def yc(self) -> float:
373
+ """Return image center Y-axis coordinate"""
374
+ return self.y0 + 0.5 * self.height
375
+
376
+ def get_data(self, roi_index: int | None = None) -> np.ndarray:
377
+ """
378
+ Return original data (if ROI is not defined or `roi_index` is None),
379
+ or ROI data (if both ROI and `roi_index` are defined).
380
+
381
+ Args:
382
+ roi_index: ROI index
383
+
384
+ Returns:
385
+ Masked data
386
+ """
387
+ if self.roi is None or roi_index is None:
388
+ view = self.data.view(ma.MaskedArray)
389
+ view.mask = np.isnan(self.data)
390
+ return view
391
+ single_roi = self.roi.get_single_roi(roi_index)
392
+ # pylint: disable=unbalanced-tuple-unpacking
393
+ x0, y0, x1, y1 = self.physical_to_indices(single_roi.get_bounding_box(self))
394
+ return self.get_masked_view()[y0:y1, x0:x1]
395
+
396
+ def copy(
397
+ self,
398
+ title: str | None = None,
399
+ dtype: np.dtype | None = None,
400
+ all_metadata: bool = False,
401
+ ) -> ImageObj:
402
+ """Copy object.
403
+
404
+ Args:
405
+ title: title
406
+ dtype: data type
407
+ all_metadata: if True, copy all metadata, otherwise only basic metadata
408
+
409
+ Returns:
410
+ Copied object
411
+ """
412
+ title = self.title if title is None else title
413
+ obj = ImageObj(title=title)
414
+ obj.title = title
415
+ obj.xlabel = self.xlabel
416
+ obj.ylabel = self.ylabel
417
+ obj.zlabel = self.zlabel
418
+ obj.xunit = self.xunit
419
+ obj.yunit = self.yunit
420
+ obj.zunit = self.zunit
421
+ obj.metadata = base.deepcopy_metadata(self.metadata, all_metadata=all_metadata)
422
+ obj.annotations = self.annotations
423
+ if self.data is not None:
424
+ obj.data = np.array(self.data, copy=True, dtype=dtype)
425
+ obj.is_uniform_coords = self.is_uniform_coords
426
+ if self.is_uniform_coords:
427
+ obj.dx = self.dx
428
+ obj.dy = self.dy
429
+ obj.x0 = self.x0
430
+ obj.y0 = self.y0
431
+ else:
432
+ obj.xcoords = np.array(self.xcoords, copy=True)
433
+ obj.ycoords = np.array(self.ycoords, copy=True)
434
+ obj.autoscale = self.autoscale
435
+ obj.xscalelog = self.xscalelog
436
+ obj.xscalemin = self.xscalemin
437
+ obj.xscalemax = self.xscalemax
438
+ obj.yscalelog = self.yscalelog
439
+ obj.yscalemin = self.yscalemin
440
+ obj.yscalemax = self.yscalemax
441
+ obj.zscalemin = self.zscalemin
442
+ obj.zscalemax = self.zscalemax
443
+ obj.dicom_template = self.dicom_template
444
+ return obj
445
+
446
+ def set_data_type(self, dtype: np.dtype) -> None:
447
+ """Change data type.
448
+ If data type is integer, clip values to the new data type's range, thus avoiding
449
+ overflow or underflow.
450
+
451
+ Args:
452
+ Data type
453
+ """
454
+ self.data = clip_astype(self.data, dtype)
455
+
456
+ def physical_to_indices(
457
+ self, coords: list[float], clip: bool = False, as_float: bool = False
458
+ ) -> list[int] | list[float]:
459
+ """Convert coordinates from physical (real world) to indices (pixel)
460
+
461
+ Args:
462
+ coords: flat list of physical coordinates [x0, y0, x1, y1, ...]
463
+ clip: if True, clip values to image boundaries
464
+ as_float: if True, return float indices (i.e. without rounding)
465
+
466
+ Returns:
467
+ Indices
468
+
469
+ Raises:
470
+ ValueError: if coords does not contain an even number of elements
471
+ """
472
+ if len(coords) % 2 != 0:
473
+ raise ValueError(
474
+ "coords must contain an even number of elements (x, y pairs)."
475
+ )
476
+ indices = np.array(coords, float)
477
+ if indices.size > 0:
478
+ if self.is_uniform_coords:
479
+ # Use existing uniform conversion
480
+ indices[::2] = (indices[::2] - self.x0) / self.dx
481
+ indices[1::2] = (indices[1::2] - self.y0) / self.dy
482
+ else:
483
+ # Use interpolation for non-uniform coordinates
484
+ x_indices = np.arange(len(self.xcoords))
485
+ y_indices = np.arange(len(self.ycoords))
486
+ indices[::2] = np.interp(indices[::2], self.xcoords, x_indices)
487
+ indices[1::2] = np.interp(indices[1::2], self.ycoords, y_indices)
488
+
489
+ if clip:
490
+ indices[::2] = np.clip(indices[::2], 0, self.data.shape[1] - 1)
491
+ indices[1::2] = np.clip(indices[1::2], 0, self.data.shape[0] - 1)
492
+ if as_float:
493
+ return indices.tolist()
494
+ return np.floor(indices + 0.5).astype(int).tolist()
495
+
496
+ def indices_to_physical(self, indices: list[float]) -> list[float]:
497
+ """Convert coordinates from indices to physical (real world)
498
+
499
+ Args:
500
+ indices: flat list of indices [x0, y0, x1, y1, ...]
501
+
502
+ Returns:
503
+ Coordinates
504
+
505
+ Raises:
506
+ ValueError: if indices does not contain an even number of elements
507
+ """
508
+ if len(indices) % 2 != 0:
509
+ raise ValueError(
510
+ "indices must contain an even number of elements (x, y pairs)."
511
+ )
512
+ coords = np.array(indices, float)
513
+ if coords.size > 0:
514
+ if self.is_uniform_coords:
515
+ # Use existing uniform conversion
516
+ coords[::2] = coords[::2] * self.dx + self.x0
517
+ coords[1::2] = coords[1::2] * self.dy + self.y0
518
+ else:
519
+ # Use interpolation for non-uniform coordinates
520
+ x_indices = np.arange(len(self.xcoords))
521
+ y_indices = np.arange(len(self.ycoords))
522
+ coords[::2] = np.interp(coords[::2], x_indices, self.xcoords)
523
+ coords[1::2] = np.interp(coords[1::2], y_indices, self.ycoords)
524
+ return coords.tolist()