mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
feat(image): image widget can take data from monitor_1d endpoint
This commit is contained in:
@ -164,7 +164,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
|
||||
self.d1 = self.dock.add_dock(name="dock_1", position="right")
|
||||
self.im = self.d1.add_widget("BECImageWidget")
|
||||
self.im.image("eiger")
|
||||
self.im.image("waveform", "1d")
|
||||
|
||||
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
|
||||
self.wf = self.d2.add_widget("BECWaveformWidget", row=0, col=0)
|
||||
|
@ -7,9 +7,18 @@ from collections import defaultdict
|
||||
from typing import Literal
|
||||
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtCore import QSize, Qt
|
||||
from qtpy.QtGui import QAction, QColor, QIcon
|
||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QSizePolicy, QToolBar, QToolButton, QWidget
|
||||
from qtpy.QtWidgets import (
|
||||
QComboBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMenu,
|
||||
QSizePolicy,
|
||||
QToolBar,
|
||||
QToolButton,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
import bec_widgets
|
||||
|
||||
@ -154,22 +163,52 @@ class WidgetAction(ToolBarAction):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, label: str | None = None, widget: QWidget = None):
|
||||
super().__init__()
|
||||
def __init__(self, label: str | None = None, widget: QWidget = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label = label
|
||||
self.widget = widget
|
||||
|
||||
def add_to_toolbar(self, toolbar, target):
|
||||
widget = QWidget()
|
||||
layout = QHBoxLayout(widget)
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
container = QWidget()
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(5)
|
||||
|
||||
if self.label is not None:
|
||||
label = QLabel(f"{self.label}")
|
||||
label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
layout.addWidget(label)
|
||||
self.widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
label_widget = QLabel(f"{self.label}")
|
||||
label_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
layout.addWidget(label_widget)
|
||||
|
||||
if isinstance(self.widget, QComboBox):
|
||||
self.widget.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
||||
|
||||
size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.widget.setSizePolicy(size_policy)
|
||||
|
||||
self.widget.setMinimumWidth(self.calculate_minimum_width(self.widget))
|
||||
|
||||
else:
|
||||
self.widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
|
||||
layout.addWidget(self.widget)
|
||||
toolbar.addWidget(widget)
|
||||
|
||||
toolbar.addWidget(container)
|
||||
|
||||
@staticmethod
|
||||
def calculate_minimum_width(combo_box: QComboBox) -> int:
|
||||
"""
|
||||
Calculate the minimum width required to display the longest item in the combo box.
|
||||
|
||||
Args:
|
||||
combo_box (QComboBox): The combo box to calculate the width for.
|
||||
|
||||
Returns:
|
||||
int: The calculated minimum width in pixels.
|
||||
"""
|
||||
font_metrics = combo_box.fontMetrics()
|
||||
max_width = max(font_metrics.width(combo_box.itemText(i)) for i in range(combo_box.count()))
|
||||
return max_width + 60
|
||||
|
||||
|
||||
class ExpandableMenuAction(ToolBarAction):
|
||||
|
@ -321,6 +321,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
self,
|
||||
image,
|
||||
monitor: str = None,
|
||||
monitor_type: Literal["1d", "2d"] = "2d",
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
@ -337,7 +338,13 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
data (np.ndarray): Custom data to display.
|
||||
"""
|
||||
if monitor is not None and data is None:
|
||||
image.image(monitor=monitor, color_map=color_map, vrange=vrange, color_bar=color_bar)
|
||||
image.image(
|
||||
monitor=monitor,
|
||||
monitor_type=monitor_type,
|
||||
color_map=color_map,
|
||||
vrange=vrange,
|
||||
color_bar=color_bar,
|
||||
)
|
||||
elif data is not None and monitor is None:
|
||||
image.add_custom_image(
|
||||
name="custom", data=data, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
@ -355,6 +362,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
def image(
|
||||
self,
|
||||
monitor: str = None,
|
||||
monitor_type: Literal["1d", "2d"] = "2d",
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
@ -393,6 +401,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
image = self._init_image(
|
||||
image=image,
|
||||
monitor=monitor,
|
||||
monitor_type=monitor_type,
|
||||
color_bar=color_bar,
|
||||
color_map=color_map,
|
||||
data=data,
|
||||
|
@ -7,10 +7,10 @@ import numpy as np
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from qtpy.QtCore import QThread
|
||||
from qtpy.QtCore import QThread, Slot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
# from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.utils import EntryValidator
|
||||
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem, ImageItemConfig
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import (
|
||||
@ -80,6 +80,8 @@ class BECImageShow(BECPlotBase):
|
||||
)
|
||||
# Get bec shortcuts dev, scans, queue, scan_storage, dap
|
||||
self.single_image = single_image
|
||||
self.image_type = "device_monitor_2d"
|
||||
self.scan_id = None
|
||||
self.get_bec_shortcuts()
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
self._images = defaultdict(dict)
|
||||
@ -105,7 +107,7 @@ class BECImageShow(BECPlotBase):
|
||||
|
||||
def find_image_by_monitor(self, item_id: str) -> BECImageItem:
|
||||
"""
|
||||
Find the widget by its gui_id.
|
||||
Find the image item by its gui_id.
|
||||
|
||||
Args:
|
||||
item_id(str): The gui_id of the widget.
|
||||
@ -230,6 +232,7 @@ class BECImageShow(BECPlotBase):
|
||||
def image(
|
||||
self,
|
||||
monitor: str,
|
||||
monitor_type: Literal["1d", "2d"] = "2d",
|
||||
color_map: Optional[str] = "magma",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "full",
|
||||
downsample: Optional[bool] = True,
|
||||
@ -243,6 +246,7 @@ class BECImageShow(BECPlotBase):
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor to display.
|
||||
monitor_type(Literal["1d","2d"]): The type of monitor to display.
|
||||
color_bar(Literal["simple","full"]): The type of color bar to display.
|
||||
color_map(str): The color map to use for the image.
|
||||
data(np.ndarray): Custom data to display.
|
||||
@ -251,7 +255,12 @@ class BECImageShow(BECPlotBase):
|
||||
Returns:
|
||||
BECImageItem: The image item.
|
||||
"""
|
||||
if monitor_type == "1d":
|
||||
image_source = "device_monitor_1d"
|
||||
self.image_type = "device_monitor_1d"
|
||||
elif monitor_type == "2d":
|
||||
image_source = "device_monitor_2d"
|
||||
self.image_type = "device_monitor_2d"
|
||||
|
||||
image_exits = self._check_image_id(monitor, self._images)
|
||||
if image_exits:
|
||||
@ -292,7 +301,6 @@ class BECImageShow(BECPlotBase):
|
||||
**kwargs,
|
||||
):
|
||||
image_source = "custom"
|
||||
# image_source = "device_monitor_2d"
|
||||
|
||||
image_exits = self._check_image_id(name, self._images)
|
||||
if image_exits:
|
||||
@ -516,10 +524,59 @@ class BECImageShow(BECPlotBase):
|
||||
"""
|
||||
data = msg["data"]
|
||||
device = msg["device"]
|
||||
if self.image_type == "device_monitor_1d":
|
||||
image = self._images["device_monitor_1d"][device]
|
||||
current_scan_id = metadata.get("scan_id", None)
|
||||
if current_scan_id is None:
|
||||
return
|
||||
if current_scan_id != self.scan_id:
|
||||
self.reset()
|
||||
self.scan_id = current_scan_id
|
||||
image.image_buffer_list = []
|
||||
image.max_len = 0
|
||||
image_buffer = self.adjust_image_buffer(image, data)
|
||||
image.raw_data = image_buffer
|
||||
self.process_image(device, image, image_buffer)
|
||||
elif self.image_type == "device_monitor_2d":
|
||||
image = self._images["device_monitor_2d"][device]
|
||||
image.raw_data = data
|
||||
self.process_image(device, image, data)
|
||||
|
||||
def adjust_image_buffer(self, image: BECImageItem, new_data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Adjusts the image buffer to accommodate the new data, ensuring that all rows have the same length.
|
||||
|
||||
Args:
|
||||
image: The image object (used to store buffer list and max_len).
|
||||
new_data (np.ndarray): The new incoming 1D waveform data.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The updated image buffer with adjusted shapes.
|
||||
"""
|
||||
new_len = new_data.shape[0]
|
||||
if not hasattr(image, "image_buffer_list"):
|
||||
image.image_buffer_list = []
|
||||
image.max_len = 0
|
||||
|
||||
if new_len > image.max_len:
|
||||
image.max_len = new_len
|
||||
for i in range(len(image.image_buffer_list)):
|
||||
wf = image.image_buffer_list[i]
|
||||
pad_width = image.max_len - wf.shape[0]
|
||||
if pad_width > 0:
|
||||
image.image_buffer_list[i] = np.pad(
|
||||
wf, (0, pad_width), mode="constant", constant_values=0
|
||||
)
|
||||
image.image_buffer_list.append(new_data)
|
||||
else:
|
||||
pad_width = image.max_len - new_len
|
||||
if pad_width > 0:
|
||||
new_data = np.pad(new_data, (0, pad_width), mode="constant", constant_values=0)
|
||||
image.image_buffer_list.append(new_data)
|
||||
|
||||
image_buffer = np.array(image.image_buffer_list)
|
||||
return image_buffer
|
||||
|
||||
@Slot(str, np.ndarray)
|
||||
def update_image(self, device: str, data: np.ndarray):
|
||||
"""
|
||||
@ -529,7 +586,7 @@ class BECImageShow(BECPlotBase):
|
||||
device(str): The name of the device.
|
||||
data(np.ndarray): The data to be updated.
|
||||
"""
|
||||
image_to_update = self._images["device_monitor_2d"][device]
|
||||
image_to_update = self._images[self.image_type][device]
|
||||
image_to_update.updateImage(data, autoLevels=image_to_update.config.autorange)
|
||||
|
||||
@Slot(str, ImageStats)
|
||||
@ -540,7 +597,7 @@ class BECImageShow(BECPlotBase):
|
||||
Args:
|
||||
stats(ImageStats): The statistics of the image.
|
||||
"""
|
||||
image_to_update = self._images["device_monitor_2d"][device]
|
||||
image_to_update = self._images[self.image_type][device]
|
||||
if image_to_update.config.autorange:
|
||||
image_to_update.auto_update_vrange(stats)
|
||||
|
||||
@ -553,7 +610,7 @@ class BECImageShow(BECPlotBase):
|
||||
data = image.raw_data
|
||||
self.process_image(image_id, image, data)
|
||||
|
||||
def _connect_device_monitor_2d(self, monitor: str):
|
||||
def _connect_device_monitor(self, monitor: str):
|
||||
"""
|
||||
Connect to the device monitor.
|
||||
|
||||
@ -566,12 +623,20 @@ class BECImageShow(BECPlotBase):
|
||||
except AttributeError:
|
||||
previous_monitor = None
|
||||
if previous_monitor and image_item.connected is True:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(previous_monitor)
|
||||
)
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(previous_monitor)
|
||||
)
|
||||
image_item.connected = False
|
||||
if monitor and image_item.connected is False:
|
||||
self.entry_validator.validate_monitor(monitor)
|
||||
if self.image_type == "device_monitor_1d":
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(monitor)
|
||||
)
|
||||
elif self.image_type == "device_monitor_2d":
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
|
||||
)
|
||||
@ -580,15 +645,14 @@ class BECImageShow(BECPlotBase):
|
||||
|
||||
def _add_image_object(
|
||||
self, source: str, name: str, config: ImageItemConfig, data=None
|
||||
) -> BECImageItem: # TODO fix types
|
||||
) -> BECImageItem:
|
||||
config.parent_id = self.gui_id
|
||||
if self.single_image is True and len(self.images) > 0:
|
||||
self.remove_image(0)
|
||||
image = BECImageItem(config=config, parent_image=self)
|
||||
self.plot_item.addItem(image)
|
||||
self._images[source][name] = image
|
||||
if source == "device_monitor_2d":
|
||||
self._connect_device_monitor_2d(config.monitor)
|
||||
self._connect_device_monitor(config.monitor)
|
||||
self.config.images[name] = config
|
||||
if data is not None:
|
||||
image.setImage(data)
|
||||
@ -673,6 +737,9 @@ class BECImageShow(BECPlotBase):
|
||||
"""
|
||||
image = self.find_image_by_monitor(image_id)
|
||||
if image:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(image.config.monitor)
|
||||
)
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(image.config.monitor)
|
||||
)
|
||||
@ -681,9 +748,9 @@ class BECImageShow(BECPlotBase):
|
||||
"""
|
||||
Clean up the widget.
|
||||
"""
|
||||
for monitor in self._images["device_monitor_2d"]:
|
||||
for monitor in self._images[self.image_type]:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(monitor)
|
||||
)
|
||||
self.images.clear()
|
||||
|
||||
|
@ -4,7 +4,7 @@ import sys
|
||||
from typing import Literal, Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import QComboBox, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
||||
@ -13,6 +13,7 @@ from bec_widgets.qt_utils.toolbar import (
|
||||
MaterialIconAction,
|
||||
ModularToolBar,
|
||||
SeparatorAction,
|
||||
WidgetAction,
|
||||
)
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
|
||||
@ -63,11 +64,14 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.fig = BECFigure()
|
||||
self.dim_combo_box = QComboBox()
|
||||
self.dim_combo_box.addItems(["1d", "2d"])
|
||||
self.toolbar = ModularToolBar(
|
||||
actions={
|
||||
"monitor": DeviceSelectionAction(
|
||||
"Monitor:", DeviceComboBox(device_filter="Device")
|
||||
),
|
||||
"monitor_type": WidgetAction(widget=self.dim_combo_box),
|
||||
"connect": MaterialIconAction(icon_name="link", tooltip="Connect Device"),
|
||||
"separator_0": SeparatorAction(),
|
||||
"save": MaterialIconAction(icon_name="save", tooltip="Open Export Dialog"),
|
||||
@ -163,7 +167,8 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
def _connect_action(self):
|
||||
monitor_combo = self.toolbar.widgets["monitor"].device_combobox
|
||||
monitor_name = monitor_combo.currentText()
|
||||
self.image(monitor_name)
|
||||
monitor_type = self.toolbar.widgets["monitor_type"].widget.currentText()
|
||||
self.image(monitor=monitor_name, monitor_type=monitor_type)
|
||||
monitor_combo.setStyleSheet("QComboBox { background-color: " "; }")
|
||||
|
||||
def show_axis_settings(self):
|
||||
@ -182,6 +187,7 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
def image(
|
||||
self,
|
||||
monitor: str,
|
||||
monitor_type: Optional[Literal["1d", "2d"]] = "2d",
|
||||
color_map: Optional[str] = "magma",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "full",
|
||||
downsample: Optional[bool] = True,
|
||||
@ -195,8 +201,14 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
self.toolbar.widgets["monitor"].device_combobox.setStyleSheet(
|
||||
"QComboBox {{ background-color: " "; }}"
|
||||
)
|
||||
if self.toolbar.widgets["monitor_type"].widget.currentText() != monitor_type:
|
||||
self.toolbar.widgets["monitor_type"].widget.setCurrentText(monitor_type)
|
||||
self.toolbar.widgets["monitor_type"].widget.setStyleSheet(
|
||||
"QComboBox {{ background-color: " "; }}"
|
||||
)
|
||||
return self._image.image(
|
||||
monitor=monitor,
|
||||
monitor_type=monitor_type,
|
||||
color_map=color_map,
|
||||
color_bar=color_bar,
|
||||
downsample=downsample,
|
||||
@ -486,6 +498,7 @@ def main(): # pragma: no cover
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = BECImageWidget()
|
||||
widget.image("waveform", "1d")
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
@ -4,62 +4,86 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The Image Widget is a versatile tool designed for visualizing 2D image data, such as camera images, in real-time. Directly integrated with the `BEC` framework, it can display live data streams from connected cameras or other image sources within the current `BEC` session. The widget provides advanced customization options for color maps and scale bars, allowing users to tailor the visualization to their specific needs.
|
||||
The Image Widget is a versatile tool designed for visualizing both 1D and 2D data, such as camera images or waveform data, in real-time. Directly integrated with the `BEC` framework, it can display live data streams from connected detectors or other data sources within the current `BEC` session. The widget provides advanced customization options for color maps and scale bars, allowing users to tailor the visualization to their specific needs.
|
||||
|
||||
## Key Features:
|
||||
- **Flexible Integration**: The widget can be integrated into both [`BECFigure`](user.widgets.bec_figure) and [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BECDesigner`.
|
||||
- **Live Data Visualization**: Real-time plotting of 2D image data from cameras or other image sources, provided that a data stream is available in the BEC session.
|
||||
- **Live Data Visualization**: Real-time plotting of both 1D and 2D data from detectors or other data sources, provided that a data stream is available in the BEC session.
|
||||
- **Support for Multiple Monitor Types**: The Image Widget supports different monitor types (`'1d'` and `'2d'`), allowing visualization of various data dimensions.
|
||||
- **Customizable Color Maps and Scale Bars**: Users can customize the appearance of images with various color maps and adjust scale bars to better interpret the visualized data.
|
||||
- **Real-time Image Processing**: Apply real-time image processing techniques directly within the widget to enhance the quality or analyze specific aspects of the images such as rotation, log scaling, and FFT.
|
||||
- **Data Export**: Export visualized image data to various formats such as PNG, TIFF, or H5 for further analysis or reporting.
|
||||
- **Real-time Image Processing**: Apply real-time image processing techniques directly within the widget to enhance the quality or analyze specific aspects of the data, such as rotation, logarithmic scaling, and Fast Fourier Transform (FFT).
|
||||
- **Data Export**: Export visualized data to various formats such as PNG, TIFF, or H5 for further analysis or reporting.
|
||||
- **Interactive Controls**: Offers interactive controls for zooming, panning, and adjusting the visual properties of the images on the fly.
|
||||
|
||||
## Monitor Types
|
||||
|
||||
The Image Widget can handle different types of data, specified by the `monitor_type` parameter:
|
||||
|
||||
- **1D Monitor (`monitor_type='1d'`)**: Used for visualizing 1D waveform data. The widget collects incoming 1D data arrays and constructs a 2D image by stacking them, adjusting for varying lengths if necessary.
|
||||
- **2D Monitor (`monitor_type='2d'`)**: Used for visualizing 2D image data directly from detectors like cameras.
|
||||
|
||||
By specifying the appropriate `monitor_type`, you can configure the Image Widget to handle data from different detectors and sources.
|
||||
|
||||

|
||||
````
|
||||
|
||||
````{tab} Examples - CLI
|
||||
|
||||
`ImageWidget` can be embedded in both [`BECFigure`](user.widgets.bec_figure) and [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BECDesigner`. However, the command-line API is the same for all cases.
|
||||
`ImageWidget` can be embedded in both [`BECFigure`](user.widgets.bec_figure) and [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BECDesigner`. The command-line API is the same for all cases.
|
||||
|
||||
## Example 1 - Adding Image Widget to BECFigure
|
||||
## Example 1 - Visualizing 2D Image Data from a Detector
|
||||
|
||||
In this example, we demonstrate how to add an `ImageWidget` to a [`BECFigure`](user.widgets.bec_figure) to visualize live data from a connected camera.
|
||||
In this example, we demonstrate how to add an `ImageWidget` to a [`BECFigure`](user.widgets.bec_figure) to visualize live 2D image data from a connected camera detector.
|
||||
|
||||
```python
|
||||
# Add a new dock with BECFigure widget
|
||||
fig = gui.add_dock().add_widget('BECFigure')
|
||||
|
||||
# Add an ImageWidget to the BECFigure
|
||||
img_widget = fig.image(source='eiger')
|
||||
img_widget.set_title("Camera Image Eiger")
|
||||
# Add an ImageWidget to the BECFigure for a 2D detector
|
||||
img_widget = fig.image(monitor='eiger', monitor_type='2d')
|
||||
img_widget.set_title("Camera Image - Eiger Detector")
|
||||
```
|
||||
|
||||
## Example 2 - Adding Image Widget as a Dock in BECDockArea
|
||||
## Example 2 - Visualizing 1D Waveform Data from a Detector
|
||||
|
||||
Adding `ImageWidget` into a [`BECDockArea`](user.widgets.bec_dock_area) is similar to adding any other widget. The widget has the same API as the one in [`BECFigure`](user.widgets.bec_figure); however, as an independent widget outside [`BECFigure`](user.widgets.bec_figure), it has its own toolbar, allowing users to configure the widget without needing CLI commands.
|
||||
This example demonstrates how to set up the Image Widget to visualize 1D waveform data from a detector, such as a line detector or a spectrometer. The widget will stack incoming 1D data arrays to construct a 2D image.
|
||||
|
||||
```python
|
||||
# Add an ImageWidget to the BECDockArea
|
||||
img_widget = gui.add_dock().add_widget('BECImageWidget')
|
||||
# Add an ImageWidget to the BECFigure for a 1D detector
|
||||
img_widget = fig.image(monitor='line_detector', monitor_type='1d')
|
||||
img_widget.set_title("Line Detector Data")
|
||||
|
||||
# Visualize live data from a camera with range from 0 to 100
|
||||
img_widget.image(source='eiger')
|
||||
# Optional: Set the color map and value range
|
||||
img_widget.set_colormap("plasma")
|
||||
img_widget.set_vrange(vmin=0, vmax=100)
|
||||
```
|
||||
|
||||
## Example 3 - Customizing Image Display
|
||||
## Example 3 - Adding Image Widget as a Dock in BECDockArea
|
||||
|
||||
Adding an `ImageWidget` into a [`BECDockArea`](user.widgets.bec_dock_area) is similar to adding any other widget. The widget has the same API as the one in [`BECFigure`](user.widgets.bec_figure); however, as an independent widget outside of `BECFigure`, it has its own toolbar, allowing users to configure the widget without needing CLI commands.
|
||||
|
||||
```python
|
||||
# Add an ImageWidget to the BECDockArea for a 2D detector
|
||||
img_widget = gui.add_dock().add_widget('BECImageWidget')
|
||||
|
||||
# Visualize live data from a camera with a specified value range
|
||||
img_widget.image(monitor='eiger', monitor_type='2d')
|
||||
img_widget.set_vrange(vmin=0, vmax=100)
|
||||
```
|
||||
|
||||
## Example 4 - Customizing Image Display
|
||||
|
||||
This example demonstrates how to customize the color map and scale bar for an image being visualized in an `ImageWidget`.
|
||||
|
||||
```python
|
||||
# Set the color map and adjust the scale bar range
|
||||
# Set the color map and adjust the value range
|
||||
img_widget.set_colormap("viridis")
|
||||
img_widget.set_vrange(vmin=10, vmax=200)
|
||||
```
|
||||
|
||||
## Example 4 - Real-time Image Processing
|
||||
## Example 5 - Real-time Image Processing
|
||||
|
||||
The `ImageWidget` provides real-time image processing capabilities, such as rotating, scaling, and applying FFT to the displayed images. The following example demonstrates how to rotate an image by 90 degrees, transpose it, and apply FFT.
|
||||
The `ImageWidget` provides real-time image processing capabilities, such as rotating, scaling, applying logarithmic scaling, and performing FFT on the displayed images. The following example demonstrates how to apply these transformations to an image.
|
||||
|
||||
```python
|
||||
# Rotate the image by 90 degrees
|
||||
@ -71,10 +95,35 @@ img_widget.set_transpose(enable=True)
|
||||
# Apply FFT to the image
|
||||
img_widget.set_fft(enable=True)
|
||||
|
||||
# Set the logarithmic scale for the image display
|
||||
# Set logarithmic scaling for the image display
|
||||
img_widget.set_log(enable=True)
|
||||
```
|
||||
|
||||
## Example 6 - Setting Up for Different Detectors
|
||||
|
||||
The Image Widget can be configured for different detectors by specifying the correct monitor name and monitor type. Here's how to set it up for various detectors:
|
||||
|
||||
### For a 2D Camera Detector (e.g., 'eiger')
|
||||
|
||||
```python
|
||||
# For a 2D camera detector
|
||||
img_widget = fig.image(monitor='eiger', monitor_type='2d')
|
||||
img_widget.set_title("Eiger Camera Image")
|
||||
```
|
||||
|
||||
### For a 1D Line Detector (e.g., 'waveform')
|
||||
|
||||
```python
|
||||
# For a 1D line detector
|
||||
img_widget = fig.image(monitor='waveform', monitor_type='1d')
|
||||
img_widget.set_title("Line Detector Data")
|
||||
```
|
||||
|
||||
```{note}
|
||||
Since the Image Widget does not have prior information about the shape of incoming data, it is essential to specify the correct `monitor_type` when setting up the widget. This ensures that the data is processed and displayed correctly.
|
||||
```
|
||||
|
||||
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
|
@ -139,6 +139,7 @@ DEVICES = [
|
||||
FakeDevice("bpm3a"),
|
||||
FakeDevice("bpm3i"),
|
||||
FakeDevice("eiger"),
|
||||
FakeDevice("waveform1d"),
|
||||
FakeDevice("async_device", readout_priority=ReadoutPriority.ASYNC),
|
||||
Positioner("test", limits=[-10, 10], read_value=2.0),
|
||||
Device("test_device"),
|
||||
|
@ -12,11 +12,6 @@ from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bec_image_show(bec_figure):
|
||||
yield bec_figure.image("eiger")
|
||||
|
||||
|
||||
def test_on_image_update(qtbot, mocked_client):
|
||||
bec_image_show = create_widget(qtbot, BECFigure, client=mocked_client).image("eiger")
|
||||
data = np.random.rand(100, 100)
|
||||
@ -61,3 +56,45 @@ def test_autorange_on_image_update(qtbot, mocked_client):
|
||||
vmin = max(np.mean(data) - 2 * np.std(data), 0)
|
||||
vmax = np.mean(data) + 2 * np.std(data)
|
||||
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()
|
||||
|
||||
|
||||
def test_on_image_update_variable_length(qtbot, mocked_client):
|
||||
"""
|
||||
Test the on_image_update slot with data arrays of varying lengths for 'device_monitor_1d' image type.
|
||||
"""
|
||||
# Create the widget and set image_type to 'device_monitor_1d'
|
||||
bec_image_show = create_widget(qtbot, BECFigure, client=mocked_client).image("waveform1d", "1d")
|
||||
|
||||
# Generate data arrays of varying lengths
|
||||
data_lengths = [10, 15, 12, 20, 5, 8, 1, 21]
|
||||
data_arrays = [np.random.rand(length) for length in data_lengths]
|
||||
|
||||
# Simulate sending messages with these data arrays
|
||||
device = "waveform1d"
|
||||
for data in data_arrays:
|
||||
msg = messages.DeviceMonitor1DMessage(
|
||||
device=device, data=data, metadata={"scan_id": "12345"}
|
||||
)
|
||||
bec_image_show.on_image_update(msg.content, msg.metadata)
|
||||
|
||||
# After processing all data, retrieve the image and its data
|
||||
img = bec_image_show.images[0]
|
||||
image_buffer = img.get_data()
|
||||
|
||||
# The image_buffer should be a 2D array with number of rows equal to number of data arrays
|
||||
# and number of columns equal to the maximum data length
|
||||
expected_num_rows = len(data_arrays)
|
||||
expected_num_cols = max(data_lengths)
|
||||
assert image_buffer.shape == (
|
||||
expected_num_rows,
|
||||
expected_num_cols,
|
||||
), f"Expected image buffer shape {(expected_num_rows, expected_num_cols)}, got {image_buffer.shape}"
|
||||
|
||||
# Check that each row in image_buffer corresponds to the padded data arrays
|
||||
for i, data in enumerate(data_arrays):
|
||||
padded_data = np.pad(
|
||||
data, (0, expected_num_cols - len(data)), mode="constant", constant_values=0
|
||||
)
|
||||
assert np.array_equal(
|
||||
image_buffer[i], padded_data
|
||||
), f"Row {i} in image buffer does not match expected padded data"
|
||||
|
@ -47,14 +47,19 @@ def test_image_widget_init(image_widget):
|
||||
# Toolbar Actions
|
||||
###################################
|
||||
def test_toolbar_connect_action(image_widget, mock_image, qtbot):
|
||||
combo = image_widget.toolbar.widgets["monitor"].device_combobox
|
||||
combo.setCurrentText("eiger")
|
||||
combo_device = image_widget.toolbar.widgets["monitor"].device_combobox
|
||||
combo_device.setCurrentText("eiger")
|
||||
qtbot.wait(200)
|
||||
assert combo.currentText() == "eiger"
|
||||
assert combo_device.currentText() == "eiger"
|
||||
combo_dim = image_widget.toolbar.widgets["monitor_type"].widget
|
||||
combo_dim.setCurrentText("2d")
|
||||
qtbot.wait(200)
|
||||
assert combo_dim.currentText() == "2d"
|
||||
action = image_widget.toolbar.widgets["connect"].action
|
||||
action.trigger()
|
||||
image_widget._image.image.assert_called_once_with(
|
||||
monitor="eiger",
|
||||
monitor_type="2d",
|
||||
color_map="magma",
|
||||
color_bar="full",
|
||||
downsample=True,
|
||||
@ -146,9 +151,10 @@ def test_image_toolbar_rotation(image_widget, mock_image):
|
||||
|
||||
|
||||
def test_image_set_image(image_widget, mock_image):
|
||||
image_widget.image(monitor="image")
|
||||
image_widget.image(monitor="image", monitor_type="2d")
|
||||
image_widget._image.image.assert_called_once_with(
|
||||
monitor="image",
|
||||
monitor_type="2d",
|
||||
color_map="magma",
|
||||
color_bar="full",
|
||||
downsample=True,
|
||||
|
@ -64,6 +64,7 @@ def test_device_input_combobox_init(device_input_combobox):
|
||||
"bpm3a",
|
||||
"bpm3i",
|
||||
"eiger",
|
||||
"waveform1d",
|
||||
"async_device",
|
||||
"test",
|
||||
"test_device",
|
||||
@ -150,6 +151,7 @@ def test_device_input_line_edit_init(device_input_line_edit):
|
||||
"bpm3a",
|
||||
"bpm3i",
|
||||
"eiger",
|
||||
"waveform1d",
|
||||
"async_device",
|
||||
"test",
|
||||
"test_device",
|
||||
|
Reference in New Issue
Block a user