bec-widgets 1.25.1__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. .gitlab-ci.yml +3 -5
  2. CHANGELOG.md +639 -0
  3. PKG-INFO +3 -3
  4. bec_widgets/__init__.py +4 -0
  5. bec_widgets/applications/bw_launch.py +23 -0
  6. bec_widgets/applications/launch_window.py +430 -0
  7. bec_widgets/assets/app_icons/auto_update.png +0 -0
  8. bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
  9. bec_widgets/cli/__init__.py +0 -1
  10. bec_widgets/cli/client.py +1779 -2064
  11. bec_widgets/cli/client_utils.py +346 -174
  12. bec_widgets/cli/generate_cli.py +143 -37
  13. bec_widgets/cli/rpc/rpc_base.py +152 -21
  14. bec_widgets/cli/rpc/rpc_register.py +113 -6
  15. bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
  16. bec_widgets/cli/server.py +125 -239
  17. bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
  18. bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
  19. bec_widgets/utils/bec_connector.py +190 -21
  20. bec_widgets/utils/bec_designer.py +7 -0
  21. bec_widgets/utils/bec_dispatcher.py +71 -4
  22. bec_widgets/utils/bec_plugin_helper.py +89 -0
  23. bec_widgets/utils/bec_signal_proxy.py +1 -1
  24. bec_widgets/utils/bec_widget.py +26 -10
  25. bec_widgets/utils/colors.py +1 -1
  26. bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
  27. bec_widgets/utils/container_utils.py +37 -12
  28. bec_widgets/utils/crosshair.py +25 -8
  29. bec_widgets/utils/entry_validator.py +3 -1
  30. bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
  31. bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
  32. bec_widgets/utils/forms_from_types/forms.py +182 -0
  33. bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
  34. bec_widgets/utils/generate_designer_plugin.py +40 -36
  35. bec_widgets/utils/linear_region_selector.py +2 -0
  36. bec_widgets/utils/name_utils.py +16 -0
  37. bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
  38. bec_widgets/utils/plot_indicator_items.py +2 -5
  39. bec_widgets/utils/plugin_utils.py +47 -1
  40. bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
  41. bec_widgets/utils/rpc_server.py +277 -0
  42. bec_widgets/utils/serialization.py +44 -0
  43. bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
  44. bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
  45. bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
  46. bec_widgets/utils/ui_loader.py +8 -8
  47. bec_widgets/utils/widget_io.py +166 -25
  48. bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
  49. bec_widgets/widgets/containers/dock/dock.py +157 -49
  50. bec_widgets/widgets/containers/dock/dock_area.py +188 -138
  51. bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
  52. bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
  53. bec_widgets/widgets/containers/main_window/main_window.py +189 -41
  54. bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
  55. bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
  56. bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
  57. bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
  58. bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
  59. bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +31 -13
  60. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +3 -1
  61. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
  62. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
  63. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
  64. bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
  65. bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
  66. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
  67. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
  68. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
  69. bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
  70. bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
  71. bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
  72. bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
  73. bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
  74. bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
  75. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
  76. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
  77. bec_widgets/widgets/editors/console/console.py +1 -1
  78. bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
  79. bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
  80. bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
  81. bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
  82. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
  83. bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
  84. bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
  85. bec_widgets/widgets/editors/text_box/text_box.py +2 -3
  86. bec_widgets/widgets/editors/website/website.py +2 -2
  87. bec_widgets/widgets/games/minesweeper.py +3 -2
  88. bec_widgets/widgets/plots/image/image.py +960 -0
  89. bec_widgets/widgets/plots/image/image.pyproject +1 -0
  90. bec_widgets/widgets/plots/image/image_item.py +279 -0
  91. bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
  92. bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
  93. bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
  94. bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
  95. bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
  96. bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
  97. bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
  98. bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
  99. bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
  100. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
  101. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
  102. bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
  103. bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
  104. bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
  105. bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
  106. bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
  107. bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
  108. bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
  109. bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
  110. bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
  111. bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
  112. bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
  113. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
  114. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
  115. bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
  116. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
  117. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
  118. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
  119. bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
  120. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
  121. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
  122. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
  123. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
  124. bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
  125. bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
  126. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
  127. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
  128. bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
  129. bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
  130. bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
  131. bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
  132. bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
  133. bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
  134. bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
  135. bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
  136. bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
  137. bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
  138. bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
  139. bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
  140. bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
  141. bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
  142. bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
  143. bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
  144. bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
  145. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/METADATA +3 -3
  146. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/RECORD +168 -153
  147. pyproject.toml +3 -3
  148. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
  149. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
  150. bec_widgets/applications/bec_app.py +0 -84
  151. bec_widgets/cli/auto_updates.py +0 -168
  152. bec_widgets/widgets/containers/figure/__init__.py +0 -1
  153. bec_widgets/widgets/containers/figure/figure.py +0 -796
  154. bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
  155. bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
  156. bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
  157. bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
  158. bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
  159. bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
  160. bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
  161. bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
  162. bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
  163. bec_widgets/widgets/plots/image/image_widget.py +0 -515
  164. bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
  165. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
  166. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
  167. bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
  168. bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
  169. bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
  170. bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
  171. bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
  172. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
  173. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
  174. bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
  175. bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
  176. /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
  177. /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
  178. /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
  179. /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
  180. /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
  181. /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
  182. /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
  183. /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
  184. /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
  185. /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
  186. /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
  187. /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
  188. /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
  189. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
  190. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
  191. /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
  192. /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
  193. /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
  194. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/WHEEL +0 -0
  195. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/entry_points.txt +0 -0
  196. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.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()
@@ -1,41 +1,39 @@
1
- import os
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.motor_map.motor_map_widget import BECMotorMapWidget
7
+ from bec_widgets.widgets.plots.image.image import Image
8
8
 
9
9
  DOM_XML = """
10
10
  <ui language='c++'>
11
- <widget class='BECMotorMapWidget' name='bec_motor_map_widget'>
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 = BECMotorMapWidget(parent)
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 "BEC Plots"
30
+ return "Plot Widgets"
33
31
 
34
32
  def icon(self):
35
- return designer_material_icon(BECMotorMapWidget.ICON_NAME)
33
+ return designer_material_icon(Image.ICON_NAME)
36
34
 
37
35
  def includeFile(self):
38
- return "bec_motor_map_widget"
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 "BECMotorMapWidget"
48
+ return "Image"
51
49
 
52
50
  def toolTip(self):
53
- return "BECMotorMapWidget"
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, Slot
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: Optional[bool] = Field(False, description="Whether to perform FFT on the monitor data.")
23
- log: Optional[bool] = Field(False, description="Whether to perform log on the monitor data.")
24
- center_of_mass: Optional[bool] = Field(
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
- rotation: Optional[int] = Field(
31
- None, description="The rotation angle of the monitor data before displaying."
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
- def __init__(self, config: ProcessingConfig = None):
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.rotation is not None:
140
- data = self.rotation(data, self.config.rotation)
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.bec_image_widget_plugin import BECImageWidgetPlugin
9
+ from bec_widgets.widgets.plots.image.image_plugin import ImagePlugin
10
10
 
11
- QPyDesignerCustomWidgetCollection.addCustomWidget(BECImageWidgetPlugin())
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)