bec-widgets 1.25.1__py3-none-any.whl → 2.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.
- .gitlab-ci.yml +3 -5
- CHANGELOG.md +631 -0
- PKG-INFO +3 -3
- bec_widgets/__init__.py +4 -0
- bec_widgets/applications/bw_launch.py +23 -0
- bec_widgets/applications/launch_window.py +430 -0
- bec_widgets/assets/app_icons/auto_update.png +0 -0
- bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
- bec_widgets/cli/__init__.py +0 -1
- bec_widgets/cli/client.py +1779 -2064
- bec_widgets/cli/client_utils.py +346 -174
- bec_widgets/cli/generate_cli.py +143 -37
- bec_widgets/cli/rpc/rpc_base.py +152 -21
- bec_widgets/cli/rpc/rpc_register.py +113 -6
- bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
- bec_widgets/cli/server.py +125 -239
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
- bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
- bec_widgets/utils/bec_connector.py +190 -21
- bec_widgets/utils/bec_designer.py +7 -0
- bec_widgets/utils/bec_dispatcher.py +71 -4
- bec_widgets/utils/bec_plugin_helper.py +89 -0
- bec_widgets/utils/bec_signal_proxy.py +1 -1
- bec_widgets/utils/bec_widget.py +26 -10
- bec_widgets/utils/colors.py +1 -1
- bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
- bec_widgets/utils/container_utils.py +37 -12
- bec_widgets/utils/crosshair.py +25 -8
- bec_widgets/utils/entry_validator.py +3 -1
- bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
- bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
- bec_widgets/utils/forms_from_types/forms.py +182 -0
- bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
- bec_widgets/utils/generate_designer_plugin.py +40 -36
- bec_widgets/utils/linear_region_selector.py +2 -0
- bec_widgets/utils/name_utils.py +16 -0
- bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
- bec_widgets/utils/plot_indicator_items.py +2 -5
- bec_widgets/utils/plugin_utils.py +47 -1
- bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
- bec_widgets/utils/rpc_server.py +277 -0
- bec_widgets/utils/serialization.py +44 -0
- bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
- bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
- bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
- bec_widgets/utils/ui_loader.py +8 -8
- bec_widgets/utils/widget_io.py +166 -25
- bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
- bec_widgets/widgets/containers/dock/dock.py +157 -49
- bec_widgets/widgets/containers/dock/dock_area.py +186 -138
- bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
- bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
- bec_widgets/widgets/containers/main_window/main_window.py +189 -41
- bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
- bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
- bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
- bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
- bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
- bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +31 -13
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +3 -1
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
- bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
- bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
- bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
- bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
- bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
- bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
- bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
- bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
- bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
- bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
- bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
- bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
- bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
- bec_widgets/widgets/editors/console/console.py +1 -1
- bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
- bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
- bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
- bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
- bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
- bec_widgets/widgets/editors/text_box/text_box.py +2 -3
- bec_widgets/widgets/editors/website/website.py +2 -2
- bec_widgets/widgets/games/minesweeper.py +3 -2
- bec_widgets/widgets/plots/image/image.py +960 -0
- bec_widgets/widgets/plots/image/image.pyproject +1 -0
- bec_widgets/widgets/plots/image/image_item.py +279 -0
- bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
- bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
- bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
- bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
- bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
- bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
- bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
- bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
- bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
- bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
- bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
- bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
- bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
- bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
- bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
- bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
- bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
- bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
- bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
- bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
- bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
- bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
- bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
- bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
- bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
- bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
- bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
- bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
- bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
- bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
- bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
- bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
- bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
- bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
- bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
- bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
- bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
- bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
- bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
- bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
- bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
- bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
- bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
- bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +168 -153
- pyproject.toml +3 -3
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
- bec_widgets/applications/bec_app.py +0 -84
- bec_widgets/cli/auto_updates.py +0 -168
- bec_widgets/widgets/containers/figure/__init__.py +0 -1
- bec_widgets/widgets/containers/figure/figure.py +0 -796
- bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
- bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
- bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
- bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
- bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
- bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
- bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
- bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
- bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
- bec_widgets/widgets/plots/image/image_widget.py +0 -515
- bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
- bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
- bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
- bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
- bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
- bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
- bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
- bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
- bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
- bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
- /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
- /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
- /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
- /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
- /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
- /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
{'files': ['image.py']}
|
@@ -0,0 +1,279 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Literal, Optional
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
import pyqtgraph as pg
|
7
|
+
from bec_lib.logger import bec_logger
|
8
|
+
from pydantic import Field, ValidationError, field_validator
|
9
|
+
from qtpy.QtCore import Signal
|
10
|
+
|
11
|
+
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
|
12
|
+
from bec_widgets.widgets.plots.image.image_processor import (
|
13
|
+
ImageProcessor,
|
14
|
+
ImageStats,
|
15
|
+
ProcessingConfig,
|
16
|
+
)
|
17
|
+
|
18
|
+
logger = bec_logger.logger
|
19
|
+
|
20
|
+
|
21
|
+
# noinspection PyDataclass
|
22
|
+
class ImageItemConfig(ConnectionConfig): # TODO review config
|
23
|
+
parent_id: str | None = Field(None, description="The parent plot of the image.")
|
24
|
+
monitor: str | None = Field(None, description="The name of the monitor.")
|
25
|
+
monitor_type: Literal["1d", "2d", "auto"] = Field("auto", description="The type of monitor.")
|
26
|
+
source: str | None = Field(None, description="The source of the curve.")
|
27
|
+
color_map: str | None = Field("plasma", description="The color map of the image.")
|
28
|
+
downsample: bool | None = Field(True, description="Whether to downsample the image.")
|
29
|
+
opacity: float | None = Field(1.0, description="The opacity of the image.")
|
30
|
+
v_range: tuple[float | int, float | int] | None = Field(
|
31
|
+
None, description="The range of the color bar. If None, the range is automatically set."
|
32
|
+
)
|
33
|
+
autorange: bool | None = Field(True, description="Whether to autorange the color bar.")
|
34
|
+
autorange_mode: Literal["max", "mean"] = Field(
|
35
|
+
"mean", description="Whether to use the mean of the image for autoscaling."
|
36
|
+
)
|
37
|
+
processing: ProcessingConfig = Field(
|
38
|
+
default_factory=ProcessingConfig, description="The post processing of the image."
|
39
|
+
)
|
40
|
+
|
41
|
+
model_config: dict = {"validate_assignment": True}
|
42
|
+
_validate_color_map = field_validator("color_map")(Colors.validate_color_map)
|
43
|
+
|
44
|
+
|
45
|
+
class ImageItem(BECConnector, pg.ImageItem):
|
46
|
+
RPC = True
|
47
|
+
USER_ACCESS = [
|
48
|
+
"color_map",
|
49
|
+
"color_map.setter",
|
50
|
+
"v_range",
|
51
|
+
"v_range.setter",
|
52
|
+
"v_min",
|
53
|
+
"v_min.setter",
|
54
|
+
"v_max",
|
55
|
+
"v_max.setter",
|
56
|
+
"autorange",
|
57
|
+
"autorange.setter",
|
58
|
+
"autorange_mode",
|
59
|
+
"autorange_mode.setter",
|
60
|
+
"fft",
|
61
|
+
"fft.setter",
|
62
|
+
"log",
|
63
|
+
"log.setter",
|
64
|
+
"rotation",
|
65
|
+
"rotation.setter",
|
66
|
+
"transpose",
|
67
|
+
"transpose.setter",
|
68
|
+
"get_data",
|
69
|
+
]
|
70
|
+
|
71
|
+
vRangeChangedManually = Signal(tuple)
|
72
|
+
|
73
|
+
def __init__(
|
74
|
+
self,
|
75
|
+
config: Optional[ImageItemConfig] = None,
|
76
|
+
gui_id: Optional[str] = None,
|
77
|
+
parent_image=None,
|
78
|
+
**kwargs,
|
79
|
+
):
|
80
|
+
if config is None:
|
81
|
+
config = ImageItemConfig(widget_class=self.__class__.__name__)
|
82
|
+
self.config = config
|
83
|
+
else:
|
84
|
+
self.config = config
|
85
|
+
if parent_image is not None:
|
86
|
+
self.set_parent(parent_image)
|
87
|
+
else:
|
88
|
+
self.parent_image = None
|
89
|
+
super().__init__(config=config, gui_id=gui_id, **kwargs)
|
90
|
+
|
91
|
+
self.raw_data = None
|
92
|
+
self.buffer = []
|
93
|
+
self.max_len = 0
|
94
|
+
|
95
|
+
# Image processor will handle any setting of data
|
96
|
+
self._image_processor = ImageProcessor(config=self.config.processing)
|
97
|
+
|
98
|
+
def set_parent(self, parent: BECConnector):
|
99
|
+
self.parent_image = parent
|
100
|
+
|
101
|
+
def parent(self):
|
102
|
+
return self.parent_image
|
103
|
+
|
104
|
+
def set_data(self, data: np.ndarray):
|
105
|
+
self.raw_data = data
|
106
|
+
self._process_image()
|
107
|
+
|
108
|
+
################################################################################
|
109
|
+
# Properties
|
110
|
+
@property
|
111
|
+
def color_map(self) -> str:
|
112
|
+
"""Get the current color map."""
|
113
|
+
return self.config.color_map
|
114
|
+
|
115
|
+
@color_map.setter
|
116
|
+
def color_map(self, value: str):
|
117
|
+
"""Set a new color map."""
|
118
|
+
try:
|
119
|
+
self.config.color_map = value
|
120
|
+
self.setColorMap(value)
|
121
|
+
except ValidationError:
|
122
|
+
logger.error(f"Invalid colormap '{value}' provided.")
|
123
|
+
|
124
|
+
@property
|
125
|
+
def v_range(self) -> tuple[float, float]:
|
126
|
+
"""
|
127
|
+
Get the color intensity range of the image.
|
128
|
+
"""
|
129
|
+
if self.levels is not None:
|
130
|
+
return tuple(float(x) for x in self.levels)
|
131
|
+
return 0.0, 1.0
|
132
|
+
|
133
|
+
@v_range.setter
|
134
|
+
def v_range(self, vrange: tuple[float, float]):
|
135
|
+
"""
|
136
|
+
Set the color intensity range of the image.
|
137
|
+
"""
|
138
|
+
self.set_v_range(vrange, disable_autorange=True)
|
139
|
+
|
140
|
+
def set_v_range(self, vrange: tuple[float, float], disable_autorange=True):
|
141
|
+
if disable_autorange:
|
142
|
+
self.config.autorange = False
|
143
|
+
self.vRangeChangedManually.emit(vrange)
|
144
|
+
self.setLevels(vrange)
|
145
|
+
self.config.v_range = vrange
|
146
|
+
|
147
|
+
@property
|
148
|
+
def v_min(self) -> float:
|
149
|
+
return self.v_range[0]
|
150
|
+
|
151
|
+
@v_min.setter
|
152
|
+
def v_min(self, value: float):
|
153
|
+
self.v_range = (value, self.v_range[1])
|
154
|
+
|
155
|
+
@property
|
156
|
+
def v_max(self) -> float:
|
157
|
+
return self.v_range[1]
|
158
|
+
|
159
|
+
@v_max.setter
|
160
|
+
def v_max(self, value: float):
|
161
|
+
self.v_range = (self.v_range[0], value)
|
162
|
+
|
163
|
+
################################################################################
|
164
|
+
# Autorange Logic
|
165
|
+
|
166
|
+
@property
|
167
|
+
def autorange(self) -> bool:
|
168
|
+
return self.config.autorange
|
169
|
+
|
170
|
+
@autorange.setter
|
171
|
+
def autorange(self, value: bool):
|
172
|
+
self.config.autorange = value
|
173
|
+
if value:
|
174
|
+
self.apply_autorange()
|
175
|
+
|
176
|
+
@property
|
177
|
+
def autorange_mode(self) -> str:
|
178
|
+
return self.config.autorange_mode
|
179
|
+
|
180
|
+
@autorange_mode.setter
|
181
|
+
def autorange_mode(self, mode: str):
|
182
|
+
self.config.autorange_mode = mode
|
183
|
+
if self.autorange:
|
184
|
+
self.apply_autorange()
|
185
|
+
|
186
|
+
def apply_autorange(self):
|
187
|
+
if self.raw_data is None:
|
188
|
+
return
|
189
|
+
data = self.image
|
190
|
+
if data is None:
|
191
|
+
data = self.raw_data
|
192
|
+
stats = ImageStats.from_data(data)
|
193
|
+
self.auto_update_vrange(stats)
|
194
|
+
|
195
|
+
def auto_update_vrange(self, stats: ImageStats) -> None:
|
196
|
+
"""Update the v_range based on the stats of the image."""
|
197
|
+
fumble_factor = 2
|
198
|
+
if self.config.autorange_mode == "mean":
|
199
|
+
vmin = max(stats.mean - fumble_factor * stats.std, 0)
|
200
|
+
vmax = stats.mean + fumble_factor * stats.std
|
201
|
+
elif self.config.autorange_mode == "max":
|
202
|
+
vmin, vmax = stats.minimum, stats.maximum
|
203
|
+
else:
|
204
|
+
return
|
205
|
+
self.set_v_range(vrange=(vmin, vmax), disable_autorange=False)
|
206
|
+
|
207
|
+
################################################################################
|
208
|
+
# Data Processing Logic
|
209
|
+
|
210
|
+
def _process_image(self):
|
211
|
+
"""
|
212
|
+
Reprocess the current raw data and update the image display.
|
213
|
+
"""
|
214
|
+
if self.raw_data is not None:
|
215
|
+
autorange = self.config.autorange
|
216
|
+
self._image_processor.set_config(self.config.processing)
|
217
|
+
processed_data = self._image_processor.process_image(self.raw_data)
|
218
|
+
self.setImage(processed_data, autoLevels=False)
|
219
|
+
self.autorange = autorange
|
220
|
+
|
221
|
+
@property
|
222
|
+
def fft(self) -> bool:
|
223
|
+
"""Get or set whether FFT postprocessing is enabled."""
|
224
|
+
return self.config.processing.fft
|
225
|
+
|
226
|
+
@fft.setter
|
227
|
+
def fft(self, enable: bool):
|
228
|
+
self.config.processing.fft = enable
|
229
|
+
self._process_image()
|
230
|
+
|
231
|
+
@property
|
232
|
+
def log(self) -> bool:
|
233
|
+
"""Get or set whether logarithmic scaling is applied."""
|
234
|
+
return self.config.processing.log
|
235
|
+
|
236
|
+
@log.setter
|
237
|
+
def log(self, enable: bool):
|
238
|
+
self.config.processing.log = enable
|
239
|
+
self._process_image()
|
240
|
+
|
241
|
+
@property
|
242
|
+
def num_rotation_90(self) -> Optional[int]:
|
243
|
+
"""Get or set the number of 90° rotations to apply."""
|
244
|
+
return self.config.processing.num_rotation_90
|
245
|
+
|
246
|
+
@num_rotation_90.setter
|
247
|
+
def num_rotation_90(self, value: Optional[int]):
|
248
|
+
self.config.processing.num_rotation_90 = value
|
249
|
+
self._process_image()
|
250
|
+
|
251
|
+
@property
|
252
|
+
def transpose(self) -> bool:
|
253
|
+
"""Get or set whether the image is transposed."""
|
254
|
+
return self.config.processing.transpose
|
255
|
+
|
256
|
+
@transpose.setter
|
257
|
+
def transpose(self, enable: bool):
|
258
|
+
self.config.processing.transpose = enable
|
259
|
+
self._process_image()
|
260
|
+
|
261
|
+
################################################################################
|
262
|
+
# Export
|
263
|
+
def get_data(self) -> np.ndarray:
|
264
|
+
"""
|
265
|
+
Get the data of the image.
|
266
|
+
Returns:
|
267
|
+
np.ndarray: The data of the image.
|
268
|
+
"""
|
269
|
+
return self.image
|
270
|
+
|
271
|
+
def clear(self):
|
272
|
+
super().clear()
|
273
|
+
self.raw_data = None
|
274
|
+
self.buffer = []
|
275
|
+
self.max_len = 0
|
276
|
+
|
277
|
+
def remove(self):
|
278
|
+
self.parent().disconnect_monitor(self.config.monitor)
|
279
|
+
self.clear()
|
bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py}
RENAMED
@@ -1,41 +1,39 @@
|
|
1
|
-
|
1
|
+
# Copyright (C) 2022 The Qt Company Ltd.
|
2
|
+
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
2
3
|
|
3
4
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
4
5
|
|
5
|
-
import bec_widgets
|
6
6
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
7
|
-
from bec_widgets.widgets.plots.
|
7
|
+
from bec_widgets.widgets.plots.image.image import Image
|
8
8
|
|
9
9
|
DOM_XML = """
|
10
10
|
<ui language='c++'>
|
11
|
-
<widget class='
|
11
|
+
<widget class='Image' name='image'>
|
12
12
|
</widget>
|
13
13
|
</ui>
|
14
14
|
"""
|
15
15
|
|
16
|
-
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
17
16
|
|
18
|
-
|
19
|
-
class BECMotorMapWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
17
|
+
class ImagePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
20
18
|
def __init__(self):
|
21
19
|
super().__init__()
|
22
20
|
self._form_editor = None
|
23
21
|
|
24
22
|
def createWidget(self, parent):
|
25
|
-
t =
|
23
|
+
t = Image(parent)
|
26
24
|
return t
|
27
25
|
|
28
26
|
def domXml(self):
|
29
27
|
return DOM_XML
|
30
28
|
|
31
29
|
def group(self):
|
32
|
-
return "
|
30
|
+
return "Plot Widgets"
|
33
31
|
|
34
32
|
def icon(self):
|
35
|
-
return designer_material_icon(
|
33
|
+
return designer_material_icon(Image.ICON_NAME)
|
36
34
|
|
37
35
|
def includeFile(self):
|
38
|
-
return "
|
36
|
+
return "image"
|
39
37
|
|
40
38
|
def initialize(self, form_editor):
|
41
39
|
self._form_editor = form_editor
|
@@ -47,10 +45,10 @@ class BECMotorMapWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
|
|
47
45
|
return self._form_editor is not None
|
48
46
|
|
49
47
|
def name(self):
|
50
|
-
return "
|
48
|
+
return "Image"
|
51
49
|
|
52
50
|
def toolTip(self):
|
53
|
-
return "
|
51
|
+
return "Image"
|
54
52
|
|
55
53
|
def whatsThis(self):
|
56
54
|
return self.toolTip()
|
@@ -1,11 +1,10 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from dataclasses import dataclass
|
4
|
-
from typing import Optional
|
5
4
|
|
6
5
|
import numpy as np
|
7
6
|
from pydantic import BaseModel, Field
|
8
|
-
from qtpy.QtCore import QObject, Signal
|
7
|
+
from qtpy.QtCore import QObject, Signal
|
9
8
|
|
10
9
|
|
11
10
|
@dataclass
|
@@ -17,35 +16,51 @@ class ImageStats:
|
|
17
16
|
mean: float
|
18
17
|
std: float
|
19
18
|
|
19
|
+
@classmethod
|
20
|
+
def from_data(cls, data: np.ndarray) -> ImageStats:
|
21
|
+
"""
|
22
|
+
Get the statistics of the image data.
|
20
23
|
|
24
|
+
Args:
|
25
|
+
data(np.ndarray): The image data.
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
ImageStats: The statistics of the image data.
|
29
|
+
"""
|
30
|
+
return cls(maximum=np.max(data), minimum=np.min(data), mean=np.mean(data), std=np.std(data))
|
31
|
+
|
32
|
+
|
33
|
+
# noinspection PyDataclass
|
21
34
|
class ProcessingConfig(BaseModel):
|
22
|
-
fft:
|
23
|
-
log:
|
24
|
-
|
25
|
-
False, description="Whether to calculate the center of mass of the monitor data."
|
26
|
-
)
|
27
|
-
transpose: Optional[bool] = Field(
|
35
|
+
fft: bool = Field(False, description="Whether to perform FFT on the monitor data.")
|
36
|
+
log: bool = Field(False, description="Whether to perform log on the monitor data.")
|
37
|
+
transpose: bool = Field(
|
28
38
|
False, description="Whether to transpose the monitor data before displaying."
|
29
39
|
)
|
30
|
-
|
31
|
-
|
40
|
+
num_rotation_90: int = Field(
|
41
|
+
0, description="The rotation angle of the monitor data before displaying."
|
32
42
|
)
|
33
|
-
model_config: dict = {"validate_assignment": True}
|
34
43
|
stats: ImageStats = Field(
|
35
44
|
ImageStats(maximum=0, minimum=0, mean=0, std=0),
|
36
45
|
description="The statistics of the image data.",
|
37
46
|
)
|
38
47
|
|
48
|
+
model_config: dict = {"validate_assignment": True}
|
49
|
+
|
39
50
|
|
40
|
-
class ImageProcessor:
|
51
|
+
class ImageProcessor(QObject):
|
41
52
|
"""
|
42
53
|
Class for processing the image data.
|
43
54
|
"""
|
44
55
|
|
45
|
-
|
56
|
+
image_processed = Signal(np.ndarray)
|
57
|
+
|
58
|
+
def __init__(self, parent=None, config: ProcessingConfig = None):
|
59
|
+
super().__init__(parent=parent)
|
46
60
|
if config is None:
|
47
61
|
config = ProcessingConfig()
|
48
62
|
self.config = config
|
63
|
+
self._current_thread = None
|
49
64
|
|
50
65
|
def set_config(self, config: ProcessingConfig):
|
51
66
|
"""
|
@@ -109,9 +124,6 @@ class ImageProcessor:
|
|
109
124
|
data_offset = data + offset
|
110
125
|
return np.log10(data_offset)
|
111
126
|
|
112
|
-
# def center_of_mass(self, data: np.ndarray) -> tuple: # TODO check functionality
|
113
|
-
# return np.unravel_index(np.argmax(data), data.shape)
|
114
|
-
|
115
127
|
def update_image_stats(self, data: np.ndarray) -> None:
|
116
128
|
"""Get the statistics of the image data.
|
117
129
|
|
@@ -125,59 +137,14 @@ class ImageProcessor:
|
|
125
137
|
self.config.stats.std = np.std(data)
|
126
138
|
|
127
139
|
def process_image(self, data: np.ndarray) -> np.ndarray:
|
128
|
-
"""
|
129
|
-
Process the data according to the configuration.
|
130
|
-
|
131
|
-
Args:
|
132
|
-
data(np.ndarray): The data to be processed.
|
133
|
-
|
134
|
-
Returns:
|
135
|
-
np.ndarray: The processed data.
|
136
|
-
"""
|
140
|
+
"""Core processing logic without threading overhead."""
|
137
141
|
if self.config.fft:
|
138
142
|
data = self.FFT(data)
|
139
|
-
if self.config.
|
140
|
-
data = self.rotation(data, self.config.
|
143
|
+
if self.config.num_rotation_90 is not None:
|
144
|
+
data = self.rotation(data, self.config.num_rotation_90)
|
141
145
|
if self.config.transpose:
|
142
146
|
data = self.transpose(data)
|
143
147
|
if self.config.log:
|
144
148
|
data = self.log(data)
|
145
149
|
self.update_image_stats(data)
|
146
150
|
return data
|
147
|
-
|
148
|
-
|
149
|
-
class ProcessorWorker(QObject):
|
150
|
-
"""
|
151
|
-
Worker for processing the image data.
|
152
|
-
"""
|
153
|
-
|
154
|
-
processed = Signal(str, np.ndarray)
|
155
|
-
stats = Signal(str, ImageStats)
|
156
|
-
stopRequested = Signal()
|
157
|
-
finished = Signal()
|
158
|
-
|
159
|
-
def __init__(self, processor):
|
160
|
-
super().__init__()
|
161
|
-
self.processor = processor
|
162
|
-
self._isRunning = False
|
163
|
-
self.stopRequested.connect(self.stop)
|
164
|
-
|
165
|
-
@Slot(str, np.ndarray)
|
166
|
-
def process_image(self, device: str, image: np.ndarray):
|
167
|
-
"""
|
168
|
-
Process the image data.
|
169
|
-
|
170
|
-
Args:
|
171
|
-
device(str): The name of the device.
|
172
|
-
image(np.ndarray): The image data.
|
173
|
-
"""
|
174
|
-
self._isRunning = True
|
175
|
-
processed_image = self.processor.process_image(image)
|
176
|
-
self._isRunning = False
|
177
|
-
if not self._isRunning:
|
178
|
-
self.processed.emit(device, processed_image)
|
179
|
-
self.stats.emit(self.processor.config.stats)
|
180
|
-
self.finished.emit()
|
181
|
-
|
182
|
-
def stop(self):
|
183
|
-
self._isRunning = False
|
@@ -6,9 +6,9 @@ def main(): # pragma: no cover
|
|
6
6
|
return
|
7
7
|
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
8
8
|
|
9
|
-
from bec_widgets.widgets.plots.image.
|
9
|
+
from bec_widgets.widgets.plots.image.image_plugin import ImagePlugin
|
10
10
|
|
11
|
-
QPyDesignerCustomWidgetCollection.addCustomWidget(
|
11
|
+
QPyDesignerCustomWidgetCollection.addCustomWidget(ImagePlugin())
|
12
12
|
|
13
13
|
|
14
14
|
if __name__ == "__main__": # pragma: no cover
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from bec_lib.device import ReadoutPriority
|
2
|
+
from qtpy.QtCore import Qt
|
3
|
+
from qtpy.QtWidgets import QComboBox, QStyledItemDelegate
|
4
|
+
|
5
|
+
from bec_widgets.utils.error_popups import SafeSlot
|
6
|
+
from bec_widgets.utils.toolbar import ToolbarBundle, WidgetAction
|
7
|
+
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
|
8
|
+
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
9
|
+
|
10
|
+
|
11
|
+
class NoCheckDelegate(QStyledItemDelegate):
|
12
|
+
"""To reduce space in combo boxes by removing the checkmark."""
|
13
|
+
|
14
|
+
def initStyleOption(self, option, index):
|
15
|
+
super().initStyleOption(option, index)
|
16
|
+
# Remove any check indicator
|
17
|
+
option.checkState = Qt.Unchecked
|
18
|
+
|
19
|
+
|
20
|
+
class MonitorSelectionToolbarBundle(ToolbarBundle):
|
21
|
+
"""
|
22
|
+
A bundle of actions for a toolbar that controls monitor selection on a plot.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self, bundle_id="device_selection", target_widget=None, **kwargs):
|
26
|
+
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
27
|
+
self.target_widget = target_widget
|
28
|
+
|
29
|
+
# 1) Device combo box
|
30
|
+
self.device_combo_box = DeviceComboBox(
|
31
|
+
parent=self.target_widget,
|
32
|
+
device_filter=BECDeviceFilter.DEVICE,
|
33
|
+
readout_priority_filter=[ReadoutPriority.ASYNC],
|
34
|
+
)
|
35
|
+
self.device_combo_box.addItem("", None)
|
36
|
+
self.device_combo_box.setCurrentText("")
|
37
|
+
self.device_combo_box.setToolTip("Select Device")
|
38
|
+
self.device_combo_box.setItemDelegate(NoCheckDelegate(self.device_combo_box))
|
39
|
+
|
40
|
+
self.add_action("monitor", WidgetAction(widget=self.device_combo_box, adjust_size=True))
|
41
|
+
|
42
|
+
# 2) Dimension combo box
|
43
|
+
self.dim_combo_box = QComboBox(parent=self.target_widget)
|
44
|
+
self.dim_combo_box.addItems(["auto", "1d", "2d"])
|
45
|
+
self.dim_combo_box.setCurrentText("auto")
|
46
|
+
self.dim_combo_box.setToolTip("Monitor Dimension")
|
47
|
+
self.dim_combo_box.setFixedWidth(60)
|
48
|
+
self.dim_combo_box.setItemDelegate(NoCheckDelegate(self.dim_combo_box))
|
49
|
+
|
50
|
+
self.add_action("dim_combo", WidgetAction(widget=self.dim_combo_box, adjust_size=True))
|
51
|
+
|
52
|
+
# Connect slots, a device will be connected upon change of any combobox
|
53
|
+
self.device_combo_box.currentTextChanged.connect(lambda: self.connect_monitor())
|
54
|
+
self.dim_combo_box.currentTextChanged.connect(lambda: self.connect_monitor())
|
55
|
+
|
56
|
+
@SafeSlot()
|
57
|
+
def connect_monitor(self):
|
58
|
+
dim = self.dim_combo_box.currentText()
|
59
|
+
self.target_widget.image(monitor=self.device_combo_box.currentText(), monitor_type=dim)
|
@@ -0,0 +1,79 @@
|
|
1
|
+
from bec_widgets.utils.error_popups import SafeSlot
|
2
|
+
from bec_widgets.utils.toolbar import MaterialIconAction, ToolbarBundle
|
3
|
+
|
4
|
+
|
5
|
+
class ImageProcessingToolbarBundle(ToolbarBundle):
|
6
|
+
"""
|
7
|
+
A bundle of actions for a toolbar that controls processing of monitor.
|
8
|
+
"""
|
9
|
+
|
10
|
+
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
11
|
+
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
12
|
+
self.target_widget = target_widget
|
13
|
+
|
14
|
+
self.fft = MaterialIconAction(icon_name="fft", tooltip="Toggle FFT", checkable=True)
|
15
|
+
self.log = MaterialIconAction(icon_name="log_scale", tooltip="Toggle Log", checkable=True)
|
16
|
+
self.transpose = MaterialIconAction(
|
17
|
+
icon_name="transform", tooltip="Transpose Image", checkable=True
|
18
|
+
)
|
19
|
+
self.right = MaterialIconAction(
|
20
|
+
icon_name="rotate_right", tooltip="Rotate image clockwise by 90 deg"
|
21
|
+
)
|
22
|
+
self.left = MaterialIconAction(
|
23
|
+
icon_name="rotate_left", tooltip="Rotate image counterclockwise by 90 deg"
|
24
|
+
)
|
25
|
+
self.reset = MaterialIconAction(icon_name="reset_settings", tooltip="Reset Image Settings")
|
26
|
+
|
27
|
+
self.add_action("fft", self.fft)
|
28
|
+
self.add_action("log", self.log)
|
29
|
+
self.add_action("transpose", self.transpose)
|
30
|
+
self.add_action("rotate_right", self.right)
|
31
|
+
self.add_action("rotate_left", self.left)
|
32
|
+
self.add_action("reset", self.reset)
|
33
|
+
|
34
|
+
self.fft.action.triggered.connect(self.toggle_fft)
|
35
|
+
self.log.action.triggered.connect(self.toggle_log)
|
36
|
+
self.transpose.action.triggered.connect(self.toggle_transpose)
|
37
|
+
self.right.action.triggered.connect(self.rotate_right)
|
38
|
+
self.left.action.triggered.connect(self.rotate_left)
|
39
|
+
self.reset.action.triggered.connect(self.reset_settings)
|
40
|
+
|
41
|
+
@SafeSlot()
|
42
|
+
def toggle_fft(self):
|
43
|
+
checked = self.fft.action.isChecked()
|
44
|
+
self.target_widget.fft = checked
|
45
|
+
|
46
|
+
@SafeSlot()
|
47
|
+
def toggle_log(self):
|
48
|
+
checked = self.log.action.isChecked()
|
49
|
+
self.target_widget.log = checked
|
50
|
+
|
51
|
+
@SafeSlot()
|
52
|
+
def toggle_transpose(self):
|
53
|
+
checked = self.transpose.action.isChecked()
|
54
|
+
self.target_widget.transpose = checked
|
55
|
+
|
56
|
+
@SafeSlot()
|
57
|
+
def rotate_right(self):
|
58
|
+
if self.target_widget.num_rotation_90 is None:
|
59
|
+
return
|
60
|
+
rotation = (self.target_widget.num_rotation_90 - 1) % 4
|
61
|
+
self.target_widget.num_rotation_90 = rotation
|
62
|
+
|
63
|
+
@SafeSlot()
|
64
|
+
def rotate_left(self):
|
65
|
+
if self.target_widget.num_rotation_90 is None:
|
66
|
+
return
|
67
|
+
rotation = (self.target_widget.num_rotation_90 + 1) % 4
|
68
|
+
self.target_widget.num_rotation_90 = rotation
|
69
|
+
|
70
|
+
@SafeSlot()
|
71
|
+
def reset_settings(self):
|
72
|
+
self.target_widget.fft = False
|
73
|
+
self.target_widget.log = False
|
74
|
+
self.target_widget.transpose = False
|
75
|
+
self.target_widget.num_rotation_90 = 0
|
76
|
+
|
77
|
+
self.fft.action.setChecked(False)
|
78
|
+
self.log.action.setChecked(False)
|
79
|
+
self.transpose.action.setChecked(False)
|