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,1030 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Visualization tools for `sigima` interactive tests (based on PlotPy)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import Generator, Literal
11
+
12
+ import numpy as np
13
+ import plotpy.tools
14
+ from guidata.qthelpers import exec_dialog as guidata_exec_dialog
15
+ from plotpy.builder import make
16
+ from plotpy.config import CONF
17
+ from plotpy.items import (
18
+ AnnotatedCircle,
19
+ AnnotatedEllipse,
20
+ AnnotatedPoint,
21
+ AnnotatedPolygon,
22
+ AnnotatedRectangle,
23
+ AnnotatedSegment,
24
+ AnnotatedShape,
25
+ AnnotatedXRange,
26
+ AnnotatedYRange,
27
+ CurveItem,
28
+ ImageItem,
29
+ LabelItem,
30
+ Marker,
31
+ MaskedImageItem,
32
+ MaskedXYImageItem,
33
+ )
34
+ from plotpy.plot import (
35
+ BasePlot,
36
+ BasePlotOptions,
37
+ PlotDialog,
38
+ PlotOptions,
39
+ SyncPlotDialog,
40
+ )
41
+ from plotpy.styles import LINESTYLES, ShapeParam
42
+ from qtpy import QtWidgets as QW
43
+
44
+ from sigima.config import _
45
+ from sigima.objects import (
46
+ CircularROI,
47
+ GeometryResult,
48
+ ImageObj,
49
+ KindShape,
50
+ PolygonalROI,
51
+ RectangularROI,
52
+ SegmentROI,
53
+ SignalObj,
54
+ )
55
+ from sigima.tests.helpers import get_default_test_name
56
+ from sigima.tools import coordinates
57
+
58
+ QAPP: QW.QApplication | None = None
59
+
60
+ WIDGETS: list[QW.QWidget] = []
61
+
62
+ CONF.set("plot", "title/font/size", 11)
63
+
64
+
65
+ def ensure_qapp() -> QW.QApplication:
66
+ """Ensure that a QApplication instance exists."""
67
+ global QAPP # pylint: disable=global-statement
68
+ if QAPP is None:
69
+ QAPP = QW.QApplication.instance()
70
+ if QAPP is None:
71
+ QAPP = QW.QApplication([]) # type: ignore[assignment]
72
+ return QAPP
73
+
74
+
75
+ def exec_dialog(dlg: QW.QDialog) -> None:
76
+ """Execute a dialog, supporting Sphinx-Gallery scraping."""
77
+ global WIDGETS # pylint: disable=global-statement,global-variable-not-assigned
78
+ gallery_building = os.getenv("SPHINX_GALLERY_BUILDING")
79
+ if gallery_building:
80
+ dlg.show()
81
+ WIDGETS.append(dlg)
82
+ else:
83
+ guidata_exec_dialog(dlg)
84
+
85
+
86
+ TEST_NB = {}
87
+
88
+ # Default image parameters
89
+ IMAGE_PARAMETERS = {
90
+ "interpolation": "nearest",
91
+ "eliminate_outliers": 0.1,
92
+ "colormap": "viridis",
93
+ }
94
+
95
+ #: Curve colors
96
+ COLORS = (
97
+ "#1f77b4", # muted blue
98
+ "#ff7f0e", # safety orange
99
+ "#2ca02c", # cooked asparagus green
100
+ "#d62728", # brick red
101
+ "#9467bd", # muted purple
102
+ "#8c564b", # chestnut brown
103
+ "#e377c2", # raspberry yogurt pink
104
+ "#7f7f7f", # gray
105
+ "#bcbd22", # curry yellow-green
106
+ "#17becf", # blue-teal
107
+ )
108
+
109
+
110
+ def style_generator() -> Generator[tuple[str, str], None, None]:
111
+ """Cycling through curve styles"""
112
+ while True:
113
+ for linestyle in LINESTYLES:
114
+ for color in COLORS:
115
+ yield (color, linestyle)
116
+
117
+
118
+ make.style = style_generator()
119
+
120
+
121
+ def get_name_title(name: str | None, title: str | None) -> tuple[str, str]:
122
+ """Return (default) widget name and title
123
+
124
+ Args:
125
+ name: Name of the widget, or None to use a default name
126
+ title: Title of the widget, or None to use a default title
127
+
128
+ Returns:
129
+ A tuple (name, title) where:
130
+ - `name` is the widget name, which is either the provided name or a default
131
+ - `title` is the widget title, which is either the provided title or a default
132
+ """
133
+ if name is None:
134
+ TEST_NB[name] = TEST_NB.setdefault(name, 0) + 1
135
+ name = get_default_test_name(f"{TEST_NB[name]:02d}")
136
+ if title is None:
137
+ title = f"{_('Test dialog')} `{name}`"
138
+ return name, title
139
+
140
+
141
+ def create_curve_dialog(
142
+ name: str | None = None,
143
+ title: str | None = None,
144
+ xlabel: str | None = None,
145
+ ylabel: str | None = None,
146
+ xunit: str | None = None,
147
+ yunit: str | None = None,
148
+ size: tuple[int, int] | None = None,
149
+ ) -> PlotDialog:
150
+ """Create Curve Dialog
151
+
152
+ Args:
153
+ name: Name of the dialog, or None to use a default name
154
+ title: Title of the dialog, or None to use a default title
155
+ xlabel: Label for the x-axis, or None for no label
156
+ ylabel: Label for the y-axis, or None for no label
157
+ xunit: Unit for the x-axis, or None for no unit
158
+ yunit: Unit for the y-axis, or None for no unit
159
+ size: Size of the dialog as a tuple (width, height), or None for default size
160
+
161
+ Returns:
162
+ A `PlotDialog` instance configured for curve plotting
163
+ """
164
+ name, title = get_name_title(name, title)
165
+ win = PlotDialog(
166
+ edit=False,
167
+ toolbar=True,
168
+ title=title,
169
+ options=PlotOptions(
170
+ title=title,
171
+ type="curve",
172
+ xlabel=xlabel,
173
+ ylabel=ylabel,
174
+ xunit=xunit,
175
+ yunit=yunit,
176
+ curve_antialiasing=True,
177
+ ),
178
+ size=(800, 600) if size is None else size,
179
+ )
180
+ win.setObjectName(name)
181
+ return win
182
+
183
+
184
+ def create_signal_segment(
185
+ x0: float,
186
+ y0: float,
187
+ x1: float,
188
+ y1: float,
189
+ label: str | None = None,
190
+ ) -> CurveItem:
191
+ """Create a signal segment item
192
+
193
+ Args:
194
+ x0: X-coordinate of the start point
195
+ y0: Y-coordinate of the start point
196
+ x1: X-coordinate of the end point
197
+ y1: Y-coordinate of the end point
198
+ label: Label for the segment, or None for no label
199
+
200
+ Returns:
201
+ A `CurveItem` representing the signal segment
202
+ """
203
+ item = make.annotated_segment(x0, y0, x1, y1, label, show_computations=False)
204
+ item.label.labelparam.bgalpha = 0.5
205
+ item.label.labelparam.anchor = "T"
206
+ item.label.labelparam.yc = 10
207
+ item.label.labelparam.update_item(item.label)
208
+ p: ShapeParam = item.shape.shapeparam
209
+ p.line.color = "#33ff00"
210
+ p.line.width = 5
211
+ p.symbol.facecolor = "#26be00"
212
+ p.symbol.edgecolor = "#33ff00"
213
+ p.symbol.marker = "Ellipse"
214
+ p.symbol.size = 11
215
+ p.update_item(item.shape)
216
+ item.set_movable(False)
217
+ item.set_resizable(False)
218
+ item.set_selectable(False)
219
+ return item
220
+
221
+
222
+ def create_cursor(
223
+ orientation: Literal["h", "v"], position: float, label: str
224
+ ) -> Marker:
225
+ """Create a horizontal or vertical cursor item
226
+
227
+ Args:
228
+ orientation: 'h' for horizontal cursor, 'v' for vertical cursor
229
+ position: Position of the cursor along the relevant axis
230
+ label: Label format string for the cursor
231
+
232
+ Returns:
233
+ A `Marker` representing the cursor
234
+ """
235
+ if orientation == "h":
236
+ cursor = make.hcursor(position, label=label)
237
+ elif orientation == "v":
238
+ cursor = make.vcursor(position, label=label)
239
+ else:
240
+ raise ValueError("Orientation must be 'h' or 'v'")
241
+ cursor.set_movable(False)
242
+ cursor.set_selectable(False)
243
+ cursor.markerparam.line.color = "#a7ff33"
244
+ cursor.markerparam.line.width = 3
245
+ cursor.markerparam.symbol.marker = "NoSymbol"
246
+ cursor.markerparam.text.textcolor = "#ffffff"
247
+ cursor.markerparam.text.background_color = "#000000"
248
+ cursor.markerparam.text.background_alpha = 0.5
249
+ cursor.markerparam.text.font.bold = True
250
+ cursor.markerparam.update_item(cursor)
251
+ return cursor
252
+
253
+
254
+ def create_range(
255
+ orientation: Literal["h", "v"], pos_min: float, pos_max: float, title: str
256
+ ) -> AnnotatedXRange | AnnotatedYRange:
257
+ """Create a horizontal or vertical range item
258
+
259
+ Args:
260
+ orientation: 'h' for horizontal range, 'v' for vertical range
261
+ pos_min: Minimum position of the range along the relevant axis
262
+ pos_max: Maximum position of the range along the relevant axis
263
+ title: Title for the range
264
+
265
+ Returns:
266
+ An `AnnotatedXRange` or `AnnotatedYRange` representing the range
267
+ """
268
+ if orientation == "h":
269
+ item = make.annotated_xrange(
270
+ pos_min, pos_max, title=title, show_computations=False
271
+ )
272
+ elif orientation == "v":
273
+ item = make.annotated_yrange(
274
+ pos_min, pos_max, title=title, show_computations=False
275
+ )
276
+ else:
277
+ raise ValueError("Orientation must be 'h' or 'v'")
278
+ item.label.labelparam.bgalpha = 0.5
279
+ item.label.labelparam.anchor = "L"
280
+ item.label.labelparam.xc = 20
281
+ item.label.labelparam.update_item(item.label)
282
+ item.set_movable(False)
283
+ item.set_resizable(False)
284
+ item.set_selectable(False)
285
+ return item
286
+
287
+
288
+ def create_label(text: str) -> LabelItem:
289
+ """Create a text label item
290
+
291
+ Args:
292
+ text: Text content of the label
293
+
294
+ Returns:
295
+ A `LabelItem` representing the text label
296
+ """
297
+ item = make.label(text, "TL", (0, 0), "TL")
298
+ return item
299
+
300
+
301
+ def __make_marker_item(x0: float, y0: float, fmt: str, title: str) -> Marker:
302
+ """Make marker item
303
+
304
+ Args:
305
+ x0: x coordinate
306
+ y0: y coordinate
307
+ fmt: numeric format (e.g. '%.3f')
308
+ title: title of the marker
309
+ """
310
+ if np.isnan(x0):
311
+ mstyle = "-"
312
+
313
+ def label(x, y): # pylint: disable=unused-argument
314
+ return (title + ": " + fmt) % y
315
+
316
+ elif np.isnan(y0):
317
+ mstyle = "|"
318
+
319
+ def label(x, y): # pylint: disable=unused-argument
320
+ return (title + ": " + fmt) % x
321
+
322
+ else:
323
+ mstyle = "+"
324
+ txt = title + ": (" + fmt + ", " + fmt + ")"
325
+
326
+ def label(x, y):
327
+ return txt % (x, y)
328
+
329
+ return make.marker(
330
+ position=(x0, y0),
331
+ markerstyle=mstyle,
332
+ label_cb=label,
333
+ linestyle="DashLine",
334
+ color="yellow",
335
+ )
336
+
337
+
338
+ def create_curve_item(
339
+ obj: SignalObj | tuple[np.ndarray, np.ndarray], title: str | None = None
340
+ ) -> CurveItem:
341
+ """Create a curve item from a SignalObj or (xdata, ydata) tuple
342
+
343
+ Args:
344
+ obj: Signal object or tuple of (xdata, ydata)
345
+ title: Title for the curve item
346
+
347
+ Returns:
348
+ A `CurveItem` representing the signal data
349
+ """
350
+ if isinstance(obj, (tuple, list)):
351
+ xdata, ydata = obj
352
+ title = title or ""
353
+ else:
354
+ assert obj.xydata is not None
355
+ xdata, ydata = obj.xydata
356
+ title = obj.title or title or ""
357
+ item = make.mcurve(xdata, ydata)
358
+ item.param.line.width = 1.25
359
+ item.param.update_item(item)
360
+ item.setTitle(title)
361
+ return item
362
+
363
+
364
+ def create_curve_roi_items(obj: SignalObj) -> list[AnnotatedXRange]:
365
+ """Create signal ROI items from a SignalObj
366
+
367
+ Args:
368
+ obj: Signal object
369
+
370
+ Returns:
371
+ A list of `AnnotatedXRange` items representing the ROIs
372
+ """
373
+ items = []
374
+ if obj.roi is not None and not obj.roi.is_empty():
375
+ for single_roi in obj.roi:
376
+ assert isinstance(single_roi, SegmentROI)
377
+ x0, x1 = single_roi.get_physical_coords(obj)
378
+ roi_item = make.annotated_xrange(x0, x1, single_roi.title)
379
+ roi_item.label.labelparam.anchor = "T"
380
+ roi_item.label.labelparam.xc = 20
381
+ roi_item.label.labelparam.update_item(roi_item.label)
382
+ # roi_item.set_style("plot", "shape/drag")
383
+ roi_item.set_movable(False)
384
+ roi_item.set_resizable(False)
385
+ roi_item.set_selectable(False)
386
+ items.append(roi_item)
387
+ return items
388
+
389
+
390
+ def create_image_item(
391
+ obj: ImageObj | np.ndarray, title: str | None = None, **kwargs
392
+ ) -> MaskedImageItem | MaskedXYImageItem:
393
+ """Create an image item from an ImageObj
394
+
395
+ Args:
396
+ obj: Image object or 2D numpy array
397
+ title: Title for the image item
398
+ **kwargs: Additional parameters for image display
399
+ (e.g., interpolation, colormap)
400
+
401
+ Returns:
402
+ A `MaskedImageItem` or `MaskedXYImageItem` representing the image
403
+ """
404
+ if isinstance(obj, ImageObj):
405
+ data = obj.data
406
+ mask = obj.maskdata
407
+ title = obj.title or title or ""
408
+ elif isinstance(obj, np.ndarray):
409
+ data = obj
410
+ mask = np.zeros_like(data, dtype=bool)
411
+ title = title or ""
412
+ else:
413
+ raise TypeError(f"Unsupported image type: {type(obj)}")
414
+ imparameters = IMAGE_PARAMETERS.copy()
415
+ imparameters.update(kwargs)
416
+ if isinstance(obj, ImageObj) and not obj.is_uniform_coords:
417
+ x, y = obj.xcoords, obj.ycoords
418
+ item = make.maskedxyimage(
419
+ x, y, data, mask, title=title, show_mask=True, **imparameters
420
+ )
421
+ else:
422
+ item = make.maskedimage(data, mask, title=title, show_mask=True, **imparameters)
423
+ if isinstance(obj, ImageObj):
424
+ x0, y0, dx, dy = obj.x0, obj.y0, obj.dx, obj.dy
425
+ item.param.xmin, item.param.xmax = x0, x0 + dx * data.shape[1]
426
+ item.param.ymin, item.param.ymax = y0, y0 + dy * data.shape[0]
427
+ item.param.update_item(item)
428
+ return item
429
+
430
+
431
+ def create_image_roi_items(obj: ImageObj) -> list[AnnotatedShape]:
432
+ """Create image ROI items from an ImageObj
433
+
434
+ Args:
435
+ obj: Image object
436
+
437
+ Returns:
438
+ A list of `AnnotatedShape` items representing the ROIs
439
+ """
440
+ items = []
441
+ if obj.roi is not None and not obj.roi.is_empty():
442
+ for single_roi in obj.roi:
443
+ if isinstance(single_roi, RectangularROI):
444
+ x0, y0, x1, y1 = single_roi.get_bounding_box(obj)
445
+ roi_item = make.annotated_rectangle(x0, y0, x1, y1, single_roi.title)
446
+ elif isinstance(single_roi, CircularROI):
447
+ xc, yc, r = single_roi.get_physical_coords(obj)
448
+ x0, y0, x1, y1 = coordinates.circle_to_diameter(xc, yc, r)
449
+ roi_item = make.annotated_circle(x0, y0, x1, y1, single_roi.title)
450
+ elif isinstance(single_roi, PolygonalROI):
451
+ coords = single_roi.get_physical_coords(obj)
452
+ points = np.array(coords).reshape(-1, 2)
453
+ roi_item = AnnotatedPolygon(points)
454
+ roi_item.annotationparam.title = single_roi.title
455
+ roi_item.set_style("plot", "shape/drag")
456
+ roi_item.annotationparam.update_item(roi_item)
457
+ items.append(roi_item)
458
+ return items
459
+
460
+
461
+ def create_plot_items_from_geometry(
462
+ result: GeometryResult,
463
+ ) -> list[
464
+ AnnotatedPoint
465
+ | Marker
466
+ | AnnotatedRectangle
467
+ | AnnotatedCircle
468
+ | AnnotatedSegment
469
+ | AnnotatedEllipse
470
+ | AnnotatedPolygon
471
+ ]:
472
+ """Create plot items from a GeometryResult object
473
+
474
+ Args:
475
+ result: The GeometryResult object to convert
476
+
477
+ Returns:
478
+ A list of plot items corresponding to the geometry result
479
+ """
480
+ items = []
481
+ for coords in result.coords:
482
+ title = result.title or ""
483
+ if result.kind == KindShape.POINT:
484
+ x0, y0 = coords
485
+ item = AnnotatedPoint(x0, y0)
486
+ sparam: ShapeParam = item.shape.shapeparam
487
+ sparam.symbol.marker = "Ellipse"
488
+ sparam.symbol.size = 6
489
+ sparam.sel_symbol.marker = "Ellipse"
490
+ sparam.sel_symbol.size = 6
491
+ aparam = item.annotationparam
492
+ aparam.title = title
493
+ sparam.update_item(item.shape)
494
+ aparam.update_item(item)
495
+ elif result.kind == KindShape.MARKER:
496
+ x0, y0 = coords
497
+ item = __make_marker_item(x0, y0, "%.3f", title)
498
+ elif result.kind == KindShape.RECTANGLE:
499
+ x0, y0, dx, dy = coords
500
+ item = make.annotated_rectangle(x0, y0, x0 + dx, y0 + dy, title=title)
501
+ elif result.kind == KindShape.CIRCLE:
502
+ xc, yc, r = coords
503
+ x0, y0, x1, y1 = coordinates.circle_to_diameter(xc, yc, r)
504
+ item = make.annotated_circle(x0, y0, x1, y1, title=title)
505
+ elif result.kind == KindShape.SEGMENT:
506
+ x0, y0, x1, y1 = coords
507
+ item = make.annotated_segment(x0, y0, x1, y1, title=title)
508
+ elif result.kind == KindShape.ELLIPSE:
509
+ xc, yc, a, b, t = coords
510
+ coords = coordinates.ellipse_to_diameters(xc, yc, a, b, t)
511
+ x0, y0, x1, y1, x2, y2, x3, y3 = coords
512
+ item = make.annotated_ellipse(x0, y0, x1, y1, x2, y2, x3, y3, title=title)
513
+ elif result.kind == KindShape.POLYGON:
514
+ x, y = coords[::2], coords[1::2]
515
+ item = make.polygon(x, y, title=title, closed=False)
516
+ else:
517
+ raise TypeError(f"Unsupported GeometryResult type: {type(result)}")
518
+ item.set_movable(False)
519
+ item.set_resizable(False)
520
+ item.set_selectable(False)
521
+
522
+ if isinstance(item, AnnotatedShape):
523
+ shapeparam: ShapeParam = item.shape.shapeparam
524
+ shapeparam.line.width = 2
525
+ shapeparam.update_item(item.shape)
526
+ item.annotationparam.show_computations = False
527
+ item.annotationparam.show_label = bool(title)
528
+ item.annotationparam.update_item(item)
529
+ item.label.labelparam.anchor = "T"
530
+ item.label.labelparam.yc = 10
531
+ item.label.labelparam.update_item(item.label)
532
+
533
+ items.append(item)
534
+
535
+ return items
536
+
537
+
538
+ def get_object_name_from_title(title: str, fallback: str) -> str:
539
+ """Generate a valid object name from a title string
540
+
541
+ Args:
542
+ title: The title string to convert
543
+ fallback: Fallback name to use if title is empty or invalid
544
+
545
+ Returns:
546
+ A valid object name derived from the title or the fallback name
547
+ """
548
+ if title:
549
+ obj_name = "".join(c if c.isalnum() else "_" for c in title)
550
+ if obj_name:
551
+ return obj_name
552
+ return fallback
553
+
554
+
555
+ def view_curve_items(
556
+ items: list[CurveItem],
557
+ name: str | None = None,
558
+ title: str | None = None,
559
+ xlabel: str | None = None,
560
+ ylabel: str | None = None,
561
+ xunit: str | None = None,
562
+ yunit: str | None = None,
563
+ add_legend: bool = True,
564
+ datetime_format: str | None = None,
565
+ object_name: str = "",
566
+ ) -> None:
567
+ """Create a curve dialog and plot items
568
+
569
+ Args:
570
+ items: List of `CurveItem` objects to plot
571
+ name: Name of the dialog, or None to use a default name
572
+ title: Title of the dialog, or None to use a default title
573
+ xlabel: Label for the x-axis, or None for no label
574
+ ylabel: Label for the y-axis, or None for no label
575
+ xunit: Unit for the x-axis, or None for no unit
576
+ yunit: Unit for the y-axis, or None for no unit
577
+ add_legend: Whether to add a legend to the plot, default is True
578
+ datetime_format: Datetime format for x-axis if x data is datetime, or None
579
+ object_name: Object name for the dialog (for screenshot functionality)
580
+ """
581
+ ensure_qapp()
582
+ win = create_curve_dialog(
583
+ name=name, title=title, xlabel=xlabel, ylabel=ylabel, xunit=xunit, yunit=yunit
584
+ )
585
+ win.setObjectName(object_name or get_object_name_from_title(title, "curve_dialog"))
586
+ plot = win.get_plot()
587
+ for item in items:
588
+ plot.add_item(item)
589
+ if add_legend:
590
+ plot.add_item(make.legend())
591
+ if datetime_format is not None:
592
+ plot.set_axis_datetime("bottom", format=datetime_format)
593
+ exec_dialog(win)
594
+ make.style = style_generator() # Reset style generator for next call
595
+
596
+
597
+ def view_curves(
598
+ data_or_objs: list[SignalObj | np.ndarray | tuple[np.ndarray, np.ndarray]]
599
+ | SignalObj
600
+ | np.ndarray
601
+ | tuple[np.ndarray, np.ndarray],
602
+ name: str | None = None,
603
+ title: str | None = None,
604
+ xlabel: str | None = None,
605
+ ylabel: str | None = None,
606
+ xunit: str | None = None,
607
+ yunit: str | None = None,
608
+ show_roi: bool = True,
609
+ object_name: str = "",
610
+ ) -> None:
611
+ """Create a curve dialog and plot curves
612
+
613
+ Args:
614
+ data_or_objs: Single `SignalObj` or `np.ndarray`, or a list/tuple of these,
615
+ or a list/tuple of (xdata, ydata) pairs
616
+ name: Name of the dialog, or None to use a default name
617
+ title: Title of the dialog, or None to use a default title
618
+ xlabel: Label for the x-axis, or None for no label
619
+ ylabel: Label for the y-axis, or None for no label
620
+ xunit: Unit for the x-axis, or None for no unit
621
+ yunit: Unit for the y-axis, or None for no unit
622
+ show_roi: Whether to show ROIs defined in `SignalObj` instances, default is True
623
+ (ignored if `data_or_objs` is not a `SignalObj`)
624
+ object_name: Object name for the dialog (for screenshot functionality)
625
+ """
626
+ ensure_qapp()
627
+ if isinstance(data_or_objs, (tuple, list)):
628
+ datalist = data_or_objs
629
+ else:
630
+ datalist = [data_or_objs]
631
+ items = []
632
+ datetime_format = None
633
+ for data_or_obj in datalist:
634
+ if isinstance(data_or_obj, SignalObj):
635
+ xlabel = xlabel or data_or_obj.xlabel or ""
636
+ ylabel = ylabel or data_or_obj.ylabel or ""
637
+ xunit = xunit or data_or_obj.xunit or ""
638
+ yunit = yunit or data_or_obj.yunit or ""
639
+ if data_or_obj.is_x_datetime():
640
+ datetime_format = data_or_obj.metadata.get("x_datetime_format")
641
+ if datetime_format is None:
642
+ unit = data_or_obj.xunit if data_or_obj.xunit else "s"
643
+ if unit in ("ns", "us", "ms"):
644
+ datetime_format = "%H:%M:%S.%f"
645
+ else:
646
+ datetime_format = "%H:%M:%S"
647
+ item = create_curve_item(data_or_obj)
648
+ if isinstance(data_or_obj, SignalObj) and show_roi:
649
+ items.extend(create_curve_roi_items(data_or_obj))
650
+ items.append(item)
651
+ view_curve_items(
652
+ items,
653
+ name=name,
654
+ title=title,
655
+ xlabel=xlabel,
656
+ ylabel=ylabel,
657
+ xunit=xunit,
658
+ yunit=yunit,
659
+ datetime_format=datetime_format,
660
+ object_name=object_name,
661
+ )
662
+ make.style = style_generator() # Reset style generator for next call
663
+
664
+
665
+ def create_image_dialog(
666
+ name: str | None = None,
667
+ title: str | None = None,
668
+ xlabel: str | None = None,
669
+ ylabel: str | None = None,
670
+ zlabel: str | None = None,
671
+ xunit: str | None = None,
672
+ yunit: str | None = None,
673
+ zunit: str | None = None,
674
+ size: tuple[int, int] | None = None,
675
+ object_name: str = "",
676
+ ) -> PlotDialog:
677
+ """Create Image Dialog
678
+
679
+ Args:
680
+ name: Name of the dialog, or None to use a default name
681
+ title: Title of the dialog, or None to use a default title
682
+ xlabel: Label for the x-axis, or None for no label
683
+ ylabel: Label for the y-axis, or None for no label
684
+ zlabel: Label for the z-axis (color scale), or None for no label
685
+ xunit: Unit for the x-axis, or None for no unit
686
+ yunit: Unit for the y-axis, or None for no unit
687
+ zunit: Unit for the z-axis (color scale), or None for no unit
688
+ size: Size of the dialog as a tuple (width, height), or None for default size
689
+ object_name: Object name for the dialog (for screenshot functionality)
690
+
691
+ Returns:
692
+ A `PlotDialog` instance configured for image plotting
693
+ """
694
+ ensure_qapp()
695
+ name, title = get_name_title(name, title)
696
+ win = PlotDialog(
697
+ edit=False,
698
+ toolbar=True,
699
+ title=title,
700
+ options=PlotOptions(
701
+ title=title,
702
+ type="image",
703
+ xlabel=xlabel,
704
+ ylabel=ylabel,
705
+ zlabel=zlabel,
706
+ xunit=xunit,
707
+ yunit=yunit,
708
+ zunit=zunit,
709
+ ),
710
+ size=(800, 600) if size is None else size,
711
+ )
712
+ win.setObjectName(object_name or name)
713
+ for toolklass in (
714
+ plotpy.tools.LabelTool,
715
+ plotpy.tools.VCursorTool,
716
+ plotpy.tools.HCursorTool,
717
+ plotpy.tools.XCursorTool,
718
+ plotpy.tools.AnnotatedRectangleTool,
719
+ plotpy.tools.AnnotatedCircleTool,
720
+ plotpy.tools.AnnotatedEllipseTool,
721
+ plotpy.tools.AnnotatedSegmentTool,
722
+ plotpy.tools.AnnotatedPointTool,
723
+ ):
724
+ win.get_manager().add_tool(toolklass, switch_to_default_tool=True)
725
+ return win
726
+
727
+
728
+ def view_image_items(
729
+ items: list[ImageItem],
730
+ name: str | None = None,
731
+ title: str | None = None,
732
+ xlabel: str | None = None,
733
+ ylabel: str | None = None,
734
+ zlabel: str | None = None,
735
+ xunit: str | None = None,
736
+ yunit: str | None = None,
737
+ zunit: str | None = None,
738
+ show_itemlist: bool = False,
739
+ object_name: str = "",
740
+ ) -> None:
741
+ """Create an image dialog and show items
742
+
743
+ Args:
744
+ items: List of `ImageItem` objects to display
745
+ name: Name of the dialog, or None to use a default name
746
+ title: Title of the dialog, or None to use a default title
747
+ xlabel: Label for the x-axis, or None for no label
748
+ ylabel: Label for the y-axis, or None for no label
749
+ zlabel: Label for the z-axis (color scale), or None for no label
750
+ xunit: Unit for the x-axis, or None for no unit
751
+ yunit: Unit for the y-axis, or None for no unit
752
+ zunit: Unit for the z-axis (color scale), or None for no unit
753
+ show_itemlist: Whether to show the item list panel in the dialog,
754
+ default is False
755
+ object_name: Object name for the dialog (for screenshot functionality)
756
+ """
757
+ ensure_qapp()
758
+ win = create_image_dialog(
759
+ name=name,
760
+ title=title,
761
+ xlabel=xlabel,
762
+ ylabel=ylabel,
763
+ zlabel=zlabel,
764
+ xunit=xunit,
765
+ yunit=yunit,
766
+ zunit=zunit,
767
+ object_name=object_name,
768
+ )
769
+ if show_itemlist:
770
+ win.manager.get_itemlist_panel().show()
771
+ plot = win.get_plot()
772
+ for item in items:
773
+ plot.add_item(item)
774
+ exec_dialog(win)
775
+
776
+
777
+ def view_images(
778
+ data_or_objs: list[ImageObj | np.ndarray] | ImageObj | np.ndarray,
779
+ name: str | None = None,
780
+ title: str | None = None,
781
+ xlabel: str | None = None,
782
+ ylabel: str | None = None,
783
+ zlabel: str | None = None,
784
+ xunit: str | None = None,
785
+ yunit: str | None = None,
786
+ zunit: str | None = None,
787
+ results: list[GeometryResult] | GeometryResult | None = None,
788
+ show_roi: bool = True,
789
+ object_name: str = "",
790
+ **kwargs,
791
+ ) -> None:
792
+ """Create an image dialog and show images
793
+
794
+ Args:
795
+ data_or_objs: Single `ImageObj` or `np.ndarray`, or a list/tuple of these
796
+ name: Name of the dialog, or None to use a default name
797
+ title: Title of the dialog, or None to use a default title
798
+ xlabel: Label for the x-axis, or None for no label
799
+ ylabel: Label for the y-axis, or None for no label
800
+ zlabel: Label for the z-axis (color scale), or None for no label
801
+ xunit: Unit for the x-axis, or None for no unit
802
+ yunit: Unit for the y-axis, or None for no unit
803
+ zunit: Unit for the z-axis (color scale), or None for no unit
804
+ results: Single `GeometryResult` or list of these to overlay on images, or None
805
+ if no overlay is needed.
806
+ show_roi: Whether to show ROIs defined in `ImageObj` instances, default is True
807
+ (ignored if `data_or_objs` is not a `ImageObj`)
808
+ object_name: Object name for the dialog (for screenshot functionality)
809
+ **kwargs: Additional keyword arguments to pass to `make.maskedimage()`
810
+ """
811
+ ensure_qapp()
812
+ if isinstance(data_or_objs, (tuple, list)):
813
+ datalist = data_or_objs
814
+ else:
815
+ datalist = [data_or_objs]
816
+ imparameters = IMAGE_PARAMETERS.copy()
817
+ imparameters.update(kwargs)
818
+ items = []
819
+ image_title: str | None = None
820
+ for data_or_obj in datalist:
821
+ if isinstance(data_or_obj, ImageObj):
822
+ data = data_or_obj.data
823
+ if data_or_obj.title:
824
+ image_title = data_or_obj.title
825
+ if data_or_obj.xlabel and xlabel is None:
826
+ xlabel = data_or_obj.xlabel
827
+ if data_or_obj.ylabel and ylabel is None:
828
+ ylabel = data_or_obj.ylabel
829
+ if data_or_obj.zlabel and zlabel is None:
830
+ zlabel = data_or_obj.zlabel
831
+ if data_or_obj.xunit and xunit is None:
832
+ xunit = data_or_obj.xunit
833
+ if data_or_obj.yunit and yunit is None:
834
+ yunit = data_or_obj.yunit
835
+ if data_or_obj.zunit and zunit is None:
836
+ zunit = data_or_obj.zunit
837
+ elif isinstance(data_or_obj, np.ndarray):
838
+ data = data_or_obj
839
+ else:
840
+ raise TypeError(f"Unsupported data type: {type(data_or_obj)}")
841
+ # Display real and imaginary parts of complex images.
842
+ assert data is not None
843
+ if np.issubdtype(data.dtype, np.complexfloating):
844
+ re_title = f"Re({image_title})" if image_title is not None else "Real"
845
+ im_title = f"Im({image_title})" if image_title is not None else "Imaginary"
846
+ items.append(create_image_item(data.real, title=re_title, **imparameters))
847
+ items.append(create_image_item(data.imag, title=im_title, **imparameters))
848
+ else:
849
+ items.append(
850
+ create_image_item(data_or_obj, title=image_title, **imparameters)
851
+ )
852
+ if isinstance(data_or_obj, ImageObj) and show_roi:
853
+ items.extend(create_image_roi_items(data_or_obj))
854
+ if results is not None:
855
+ if isinstance(results, GeometryResult):
856
+ results = [results]
857
+ if not isinstance(results, (list, tuple)):
858
+ raise TypeError(f"Unsupported results type: {type(results)}")
859
+ for res in results:
860
+ items.extend(create_plot_items_from_geometry(res))
861
+ view_image_items(
862
+ items,
863
+ name=name,
864
+ title=title,
865
+ xlabel=xlabel,
866
+ ylabel=ylabel,
867
+ zlabel=zlabel,
868
+ xunit=xunit,
869
+ yunit=yunit,
870
+ zunit=zunit,
871
+ object_name=object_name,
872
+ )
873
+
874
+
875
+ def view_curves_and_images(
876
+ data_or_objs: list[SignalObj | np.ndarray | ImageObj | np.ndarray],
877
+ name: str | None = None,
878
+ title: str | None = None,
879
+ xlabel: str | None = None,
880
+ ylabel: str | None = None,
881
+ zlabel: str | None = None,
882
+ xunit: str | None = None,
883
+ yunit: str | None = None,
884
+ zunit: str | None = None,
885
+ object_name: str = "",
886
+ ) -> None:
887
+ """View signals, then images in two successive dialogs
888
+
889
+ Args:
890
+ data_or_objs: List of `SignalObj`, `ImageObj`, `np.ndarray` or a mix of these
891
+ name: Name of the dialog, or None to use a default name
892
+ title: Title of the dialog, or None to use a default title
893
+ xlabel: Label for the x-axis, or None for no label
894
+ ylabel: Label for the y-axis, or None for no label
895
+ zlabel: Label for the z-axis (color scale), or None for no label
896
+ xunit: Unit for the x-axis, or None for no unit
897
+ yunit: Unit for the y-axis, or None for no unit
898
+ zunit: Unit for the z-axis (color scale), or None for no unit
899
+ object_name: Object name for the dialog (for screenshot functionality)
900
+ """
901
+ ensure_qapp()
902
+ if isinstance(data_or_objs, (tuple, list)):
903
+ objs = data_or_objs
904
+ else:
905
+ objs = [data_or_objs]
906
+ sig_objs = [obj for obj in objs if isinstance(obj, (SignalObj, np.ndarray))]
907
+ if sig_objs:
908
+ view_curves(
909
+ sig_objs,
910
+ name=name,
911
+ title=title,
912
+ xlabel=xlabel,
913
+ ylabel=ylabel,
914
+ xunit=xunit,
915
+ yunit=yunit,
916
+ object_name=f"{object_name}_curves",
917
+ )
918
+ ima_objs = [obj for obj in objs if isinstance(obj, (ImageObj, np.ndarray))]
919
+ if ima_objs:
920
+ view_images(
921
+ ima_objs,
922
+ name=name,
923
+ title=title,
924
+ xlabel=xlabel,
925
+ ylabel=ylabel,
926
+ zlabel=zlabel,
927
+ xunit=xunit,
928
+ yunit=yunit,
929
+ zunit=zunit,
930
+ object_name=f"{object_name}_images",
931
+ )
932
+
933
+
934
+ def __compute_grid(
935
+ num_objects: int, max_cols: int = 4, fixed_num_rows: int | None = None
936
+ ) -> tuple[int, int]:
937
+ """Compute number of rows and columns for a grid of images
938
+
939
+ Args:
940
+ num_objects: Total number of objects to display
941
+ max_cols: Maximum number of columns in the grid
942
+ fixed_num_rows: Fixed number of rows, if specified
943
+
944
+ Returns:
945
+ A tuple (num_rows, num_cols) representing the grid dimensions
946
+ """
947
+ num_cols = min(num_objects, max_cols)
948
+ if fixed_num_rows is not None:
949
+ num_rows = fixed_num_rows
950
+ num_cols = (num_objects + num_rows - 1) // num_rows
951
+ else:
952
+ num_rows = (num_objects + num_cols - 1) // num_cols
953
+ return num_rows, num_cols
954
+
955
+
956
+ def view_images_side_by_side(
957
+ images: list[ImageItem | np.ndarray | ImageObj],
958
+ titles: list[str] | None = None,
959
+ share_axes: bool = True,
960
+ rows: int | None = None,
961
+ maximized: bool = False,
962
+ title: str | None = None,
963
+ results: list[GeometryResult] | GeometryResult | None = None,
964
+ show_roi: bool = True,
965
+ object_name: str = "",
966
+ **kwargs,
967
+ ) -> None:
968
+ """Show sequence of images
969
+
970
+ Args:
971
+ images: List of `ImageItem`, `np.ndarray`, or `ImageObj` objects to display
972
+ titles: List of titles for each image
973
+ share_axes: Whether to share axes across plots, default is True
974
+ rows: Fixed number of rows in the grid, or None to compute automatically
975
+ maximized: Whether to show the dialog maximized, default is False
976
+ title: Title of the dialog, or None for a default title
977
+ results: Single `GeometryResult` or list of these to overlay on images, or None
978
+ if no overlay is needed.
979
+ show_roi: Whether to show ROIs defined in `ImageObj` instances, default is True
980
+ (ignored if `images` do not contain `ImageObj` instances)
981
+ object_name: Object name for the dialog widget (used for screenshot filename)
982
+ **kwargs: Additional keyword arguments to pass to `make.maskedimage()`
983
+ """
984
+ ensure_qapp()
985
+ # pylint: disable=too-many-nested-blocks
986
+ rows, cols = __compute_grid(len(images), fixed_num_rows=rows, max_cols=4)
987
+ dlg = SyncPlotDialog(title=title)
988
+ dlg.setObjectName(
989
+ object_name or get_object_name_from_title(title, "images_side_by_side")
990
+ )
991
+ imparameters = IMAGE_PARAMETERS.copy()
992
+ imparameters.update(kwargs)
993
+ if not isinstance(titles, (list, tuple)):
994
+ titles = [titles] * len(images)
995
+ elif len(titles) != len(images):
996
+ raise ValueError("Length of titles must match length of images")
997
+ if not isinstance(results, (list, tuple)):
998
+ results = [results] * len(images)
999
+ elif len(results) != len(images):
1000
+ raise ValueError("Length of results must match length of images")
1001
+ for idx, (img, result, imtitle) in enumerate(zip(images, results, titles)):
1002
+ row = idx // cols
1003
+ col = idx % cols
1004
+ imtitle = img.title if isinstance(img, ImageObj) else imtitle
1005
+ plot = BasePlot(options=BasePlotOptions(title=imtitle))
1006
+ other_items = []
1007
+ if isinstance(img, (MaskedImageItem, ImageItem)):
1008
+ item = img
1009
+ else:
1010
+ item = create_image_item(img, title=imtitle, **imparameters)
1011
+ if isinstance(img, ImageObj) and show_roi:
1012
+ other_items.extend(create_image_roi_items(img))
1013
+ plot.add_item(item)
1014
+ for other_item in other_items:
1015
+ plot.add_item(other_item)
1016
+ if result is not None:
1017
+ if not isinstance(result, GeometryResult):
1018
+ raise TypeError(f"Unsupported results type: {type(result)}")
1019
+ overlay_items = create_plot_items_from_geometry(result)
1020
+ for overlay_item in overlay_items:
1021
+ plot.add_item(overlay_item)
1022
+ dlg.add_plot(row, col, plot, sync=share_axes)
1023
+ dlg.finalize_configuration()
1024
+ if maximized:
1025
+ dlg.showMaximized()
1026
+ elif os.environ.get("QT_QPA_PLATFORM") == "offscreen":
1027
+ # Set explicit size for proper rendering in headless mode
1028
+ # Qt size hints don't work reliably without a display
1029
+ dlg.resize(20 + 440 * cols, 20 + 400 * rows)
1030
+ exec_dialog(dlg)