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.
- sigima/__init__.py +142 -2
- sigima/client/__init__.py +105 -0
- sigima/client/base.py +780 -0
- sigima/client/remote.py +469 -0
- sigima/client/stub.py +814 -0
- sigima/client/utils.py +90 -0
- sigima/config.py +444 -0
- sigima/data/logo/Sigima.svg +135 -0
- sigima/data/tests/annotations.json +798 -0
- sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
- sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
- sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
- sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
- sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
- sigima/data/tests/curve_formats/bandwidth.txt +201 -0
- sigima/data/tests/curve_formats/boxcar.npy +0 -0
- sigima/data/tests/curve_formats/datetime.txt +1001 -0
- sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
- sigima/data/tests/curve_formats/fw1e2.txt +301 -0
- sigima/data/tests/curve_formats/fwhm.txt +319 -0
- sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
- sigima/data/tests/curve_formats/noised_saw.mat +0 -0
- sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
- sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
- sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
- sigima/data/tests/curve_formats/paracetamol.npy +0 -0
- sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
- sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
- sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
- sigima/data/tests/curve_formats/pulse1.npy +0 -0
- sigima/data/tests/curve_formats/pulse2.npy +0 -0
- sigima/data/tests/curve_formats/simple.txt +5 -0
- sigima/data/tests/curve_formats/spectrum.mca +2139 -0
- sigima/data/tests/curve_formats/square2.npy +0 -0
- sigima/data/tests/curve_formats/step.npy +0 -0
- sigima/data/tests/fabry-perot1.jpg +0 -0
- sigima/data/tests/fabry-perot2.jpg +0 -0
- sigima/data/tests/flower.npy +0 -0
- sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
- sigima/data/tests/image_formats/binary_image.npy +0 -0
- sigima/data/tests/image_formats/binary_image.png +0 -0
- sigima/data/tests/image_formats/centroid_test.npy +0 -0
- sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
- sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
- sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
- sigima/data/tests/image_formats/fiber.csv +480 -0
- sigima/data/tests/image_formats/fiber.jpg +0 -0
- sigima/data/tests/image_formats/fiber.png +0 -0
- sigima/data/tests/image_formats/fiber.txt +480 -0
- sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
- sigima/data/tests/image_formats/mr-brain.dcm +0 -0
- sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
- sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
- sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
- sigima/data/tests/image_formats/tiling.txt +10 -0
- sigima/data/tests/image_formats/uint16.tiff +0 -0
- sigima/data/tests/image_formats/uint8.tiff +0 -0
- sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
- sigima/enums.py +195 -0
- sigima/io/__init__.py +123 -0
- sigima/io/base.py +311 -0
- sigima/io/common/__init__.py +5 -0
- sigima/io/common/basename.py +164 -0
- sigima/io/common/converters.py +189 -0
- sigima/io/common/objmeta.py +181 -0
- sigima/io/common/textreader.py +58 -0
- sigima/io/convenience.py +157 -0
- sigima/io/enums.py +17 -0
- sigima/io/ftlab.py +395 -0
- sigima/io/image/__init__.py +9 -0
- sigima/io/image/base.py +177 -0
- sigima/io/image/formats.py +1016 -0
- sigima/io/image/funcs.py +414 -0
- sigima/io/signal/__init__.py +9 -0
- sigima/io/signal/base.py +129 -0
- sigima/io/signal/formats.py +290 -0
- sigima/io/signal/funcs.py +723 -0
- sigima/objects/__init__.py +260 -0
- sigima/objects/base.py +937 -0
- sigima/objects/image/__init__.py +88 -0
- sigima/objects/image/creation.py +556 -0
- sigima/objects/image/object.py +524 -0
- sigima/objects/image/roi.py +904 -0
- sigima/objects/scalar/__init__.py +57 -0
- sigima/objects/scalar/common.py +215 -0
- sigima/objects/scalar/geometry.py +502 -0
- sigima/objects/scalar/table.py +784 -0
- sigima/objects/shape.py +290 -0
- sigima/objects/signal/__init__.py +133 -0
- sigima/objects/signal/constants.py +27 -0
- sigima/objects/signal/creation.py +1428 -0
- sigima/objects/signal/object.py +444 -0
- sigima/objects/signal/roi.py +274 -0
- sigima/params.py +405 -0
- sigima/proc/__init__.py +96 -0
- sigima/proc/base.py +381 -0
- sigima/proc/decorator.py +330 -0
- sigima/proc/image/__init__.py +513 -0
- sigima/proc/image/arithmetic.py +335 -0
- sigima/proc/image/base.py +260 -0
- sigima/proc/image/detection.py +519 -0
- sigima/proc/image/edges.py +329 -0
- sigima/proc/image/exposure.py +406 -0
- sigima/proc/image/extraction.py +458 -0
- sigima/proc/image/filtering.py +219 -0
- sigima/proc/image/fourier.py +147 -0
- sigima/proc/image/geometry.py +661 -0
- sigima/proc/image/mathops.py +340 -0
- sigima/proc/image/measurement.py +195 -0
- sigima/proc/image/morphology.py +155 -0
- sigima/proc/image/noise.py +107 -0
- sigima/proc/image/preprocessing.py +182 -0
- sigima/proc/image/restoration.py +235 -0
- sigima/proc/image/threshold.py +217 -0
- sigima/proc/image/transformations.py +393 -0
- sigima/proc/signal/__init__.py +376 -0
- sigima/proc/signal/analysis.py +206 -0
- sigima/proc/signal/arithmetic.py +551 -0
- sigima/proc/signal/base.py +262 -0
- sigima/proc/signal/extraction.py +60 -0
- sigima/proc/signal/features.py +310 -0
- sigima/proc/signal/filtering.py +484 -0
- sigima/proc/signal/fitting.py +276 -0
- sigima/proc/signal/fourier.py +259 -0
- sigima/proc/signal/mathops.py +420 -0
- sigima/proc/signal/processing.py +580 -0
- sigima/proc/signal/stability.py +175 -0
- sigima/proc/title_formatting.py +227 -0
- sigima/proc/validation.py +272 -0
- sigima/tests/__init__.py +7 -0
- sigima/tests/common/__init__.py +0 -0
- sigima/tests/common/arithmeticparam_unit_test.py +26 -0
- sigima/tests/common/basename_unit_test.py +126 -0
- sigima/tests/common/client_unit_test.py +412 -0
- sigima/tests/common/converters_unit_test.py +77 -0
- sigima/tests/common/decorator_unit_test.py +176 -0
- sigima/tests/common/examples_unit_test.py +104 -0
- sigima/tests/common/kernel_normalization_unit_test.py +242 -0
- sigima/tests/common/roi_basic_unit_test.py +73 -0
- sigima/tests/common/roi_geometry_unit_test.py +171 -0
- sigima/tests/common/scalar_builder_unit_test.py +142 -0
- sigima/tests/common/scalar_unit_test.py +991 -0
- sigima/tests/common/shape_unit_test.py +183 -0
- sigima/tests/common/stat_unit_test.py +138 -0
- sigima/tests/common/title_formatting_unit_test.py +338 -0
- sigima/tests/common/tools_coordinates_unit_test.py +60 -0
- sigima/tests/common/transformations_unit_test.py +178 -0
- sigima/tests/common/validation_unit_test.py +205 -0
- sigima/tests/conftest.py +129 -0
- sigima/tests/data.py +998 -0
- sigima/tests/env.py +280 -0
- sigima/tests/guiutils.py +163 -0
- sigima/tests/helpers.py +532 -0
- sigima/tests/image/__init__.py +28 -0
- sigima/tests/image/binning_unit_test.py +128 -0
- sigima/tests/image/blob_detection_unit_test.py +312 -0
- sigima/tests/image/centroid_unit_test.py +170 -0
- sigima/tests/image/check_2d_array_unit_test.py +63 -0
- sigima/tests/image/contour_unit_test.py +172 -0
- sigima/tests/image/convolution_unit_test.py +178 -0
- sigima/tests/image/datatype_unit_test.py +67 -0
- sigima/tests/image/edges_unit_test.py +155 -0
- sigima/tests/image/enclosingcircle_unit_test.py +88 -0
- sigima/tests/image/exposure_unit_test.py +223 -0
- sigima/tests/image/fft2d_unit_test.py +189 -0
- sigima/tests/image/filtering_unit_test.py +166 -0
- sigima/tests/image/geometry_unit_test.py +654 -0
- sigima/tests/image/hough_circle_unit_test.py +147 -0
- sigima/tests/image/imageobj_unit_test.py +737 -0
- sigima/tests/image/morphology_unit_test.py +71 -0
- sigima/tests/image/noise_unit_test.py +57 -0
- sigima/tests/image/offset_correction_unit_test.py +72 -0
- sigima/tests/image/operation_unit_test.py +518 -0
- sigima/tests/image/peak2d_limits_unit_test.py +41 -0
- sigima/tests/image/peak2d_unit_test.py +133 -0
- sigima/tests/image/profile_unit_test.py +159 -0
- sigima/tests/image/projections_unit_test.py +121 -0
- sigima/tests/image/restoration_unit_test.py +141 -0
- sigima/tests/image/roi2dparam_unit_test.py +53 -0
- sigima/tests/image/roi_advanced_unit_test.py +588 -0
- sigima/tests/image/roi_grid_unit_test.py +279 -0
- sigima/tests/image/spectrum2d_unit_test.py +40 -0
- sigima/tests/image/threshold_unit_test.py +91 -0
- sigima/tests/io/__init__.py +0 -0
- sigima/tests/io/addnewformat_unit_test.py +125 -0
- sigima/tests/io/convenience_funcs_unit_test.py +470 -0
- sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
- sigima/tests/io/datetime_csv_unit_test.py +198 -0
- sigima/tests/io/imageio_formats_test.py +41 -0
- sigima/tests/io/ioregistry_unit_test.py +69 -0
- sigima/tests/io/objmeta_unit_test.py +87 -0
- sigima/tests/io/readobj_unit_test.py +130 -0
- sigima/tests/io/readwriteobj_unit_test.py +67 -0
- sigima/tests/signal/__init__.py +0 -0
- sigima/tests/signal/analysis_unit_test.py +135 -0
- sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
- sigima/tests/signal/convolution_unit_test.py +404 -0
- sigima/tests/signal/datetime_unit_test.py +176 -0
- sigima/tests/signal/fft1d_unit_test.py +303 -0
- sigima/tests/signal/filters_unit_test.py +403 -0
- sigima/tests/signal/fitting_unit_test.py +929 -0
- sigima/tests/signal/fwhm_unit_test.py +111 -0
- sigima/tests/signal/noise_unit_test.py +128 -0
- sigima/tests/signal/offset_correction_unit_test.py +34 -0
- sigima/tests/signal/operation_unit_test.py +489 -0
- sigima/tests/signal/peakdetection_unit_test.py +145 -0
- sigima/tests/signal/processing_unit_test.py +657 -0
- sigima/tests/signal/pulse/__init__.py +112 -0
- sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
- sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
- sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
- sigima/tests/signal/roi_advanced_unit_test.py +392 -0
- sigima/tests/signal/signalobj_unit_test.py +603 -0
- sigima/tests/signal/stability_unit_test.py +431 -0
- sigima/tests/signal/uncertainty_unit_test.py +611 -0
- sigima/tests/vistools.py +1030 -0
- sigima/tools/__init__.py +59 -0
- sigima/tools/checks.py +290 -0
- sigima/tools/coordinates.py +308 -0
- sigima/tools/datatypes.py +26 -0
- sigima/tools/image/__init__.py +97 -0
- sigima/tools/image/detection.py +451 -0
- sigima/tools/image/exposure.py +77 -0
- sigima/tools/image/extraction.py +48 -0
- sigima/tools/image/fourier.py +260 -0
- sigima/tools/image/geometry.py +190 -0
- sigima/tools/image/preprocessing.py +165 -0
- sigima/tools/signal/__init__.py +86 -0
- sigima/tools/signal/dynamic.py +254 -0
- sigima/tools/signal/features.py +135 -0
- sigima/tools/signal/filtering.py +171 -0
- sigima/tools/signal/fitting.py +1171 -0
- sigima/tools/signal/fourier.py +466 -0
- sigima/tools/signal/interpolation.py +70 -0
- sigima/tools/signal/peakdetection.py +126 -0
- sigima/tools/signal/pulse.py +1626 -0
- sigima/tools/signal/scaling.py +50 -0
- sigima/tools/signal/stability.py +258 -0
- sigima/tools/signal/windowing.py +90 -0
- sigima/worker.py +79 -0
- sigima-1.0.0.dist-info/METADATA +233 -0
- sigima-1.0.0.dist-info/RECORD +262 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
- sigima-0.0.1.dev0.dist-info/METADATA +0 -60
- sigima-0.0.1.dev0.dist-info/RECORD +0 -6
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
sigima/tests/vistools.py
ADDED
|
@@ -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)
|