mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
feat(image): roi plots with crosshair cuts added
This commit is contained in:
@ -85,7 +85,8 @@ class Crosshair(QObject):
|
|||||||
self.items = []
|
self.items = []
|
||||||
self.marker_moved_1d = {}
|
self.marker_moved_1d = {}
|
||||||
self.marker_clicked_1d = {}
|
self.marker_clicked_1d = {}
|
||||||
self.marker_2d = None
|
self.marker_2d_row = None
|
||||||
|
self.marker_2d_col = None
|
||||||
self.update_markers()
|
self.update_markers()
|
||||||
self.check_log()
|
self.check_log()
|
||||||
self.check_derivatives()
|
self.check_derivatives()
|
||||||
@ -195,13 +196,23 @@ class Crosshair(QObject):
|
|||||||
marker_clicked_list.append(marker_clicked)
|
marker_clicked_list.append(marker_clicked)
|
||||||
self.marker_clicked_1d[name] = marker_clicked_list
|
self.marker_clicked_1d[name] = marker_clicked_list
|
||||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||||
if self.marker_2d is not None:
|
if self.marker_2d_row is not None and self.marker_2d_col is not None:
|
||||||
continue
|
continue
|
||||||
self.marker_2d = pg.ROI(
|
# Create horizontal ROI for row highlighting
|
||||||
[0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False
|
if item.image is None:
|
||||||
|
continue
|
||||||
|
self.marker_2d_row = pg.ROI(
|
||||||
|
[0, 0], size=[item.image.shape[0], 1], pen=pg.mkPen("r", width=2), movable=False
|
||||||
)
|
)
|
||||||
self.marker_2d.skip_auto_range = True
|
self.marker_2d_row.skip_auto_range = True
|
||||||
self.plot_item.addItem(self.marker_2d)
|
self.plot_item.addItem(self.marker_2d_row)
|
||||||
|
|
||||||
|
# Create vertical ROI for column highlighting
|
||||||
|
self.marker_2d_col = pg.ROI(
|
||||||
|
[0, 0], size=[1, item.image.shape[1]], pen=pg.mkPen("r", width=2), movable=False
|
||||||
|
)
|
||||||
|
self.marker_2d_col.skip_auto_range = True
|
||||||
|
self.plot_item.addItem(self.marker_2d_col)
|
||||||
|
|
||||||
def snap_to_data(
|
def snap_to_data(
|
||||||
self, x: float, y: float
|
self, x: float, y: float
|
||||||
@ -243,6 +254,8 @@ class Crosshair(QObject):
|
|||||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||||
name = item.config.monitor or str(id(item))
|
name = item.config.monitor or str(id(item))
|
||||||
image_2d = item.image
|
image_2d = item.image
|
||||||
|
if image_2d is None:
|
||||||
|
continue
|
||||||
# Clip the x and y values to the image dimensions to avoid out of bounds errors
|
# Clip the x and y values to the image dimensions to avoid out of bounds errors
|
||||||
y_values[name] = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
y_values[name] = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
||||||
x_values[name] = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
x_values[name] = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
||||||
@ -330,7 +343,10 @@ class Crosshair(QObject):
|
|||||||
x, y = x_snap_values[name], y_snap_values[name]
|
x, y = x_snap_values[name], y_snap_values[name]
|
||||||
if x is None or y is None:
|
if x is None or y is None:
|
||||||
continue
|
continue
|
||||||
self.marker_2d.setPos([x, y])
|
# Set position of horizontal ROI (row)
|
||||||
|
self.marker_2d_row.setPos([0, y])
|
||||||
|
# Set position of vertical ROI (column)
|
||||||
|
self.marker_2d_col.setPos([x, 0])
|
||||||
coordinate_to_emit = (name, x, y)
|
coordinate_to_emit = (name, x, y)
|
||||||
self.coordinatesChanged2D.emit(coordinate_to_emit)
|
self.coordinatesChanged2D.emit(coordinate_to_emit)
|
||||||
else:
|
else:
|
||||||
@ -384,7 +400,10 @@ class Crosshair(QObject):
|
|||||||
x, y = x_snap_values[name], y_snap_values[name]
|
x, y = x_snap_values[name], y_snap_values[name]
|
||||||
if x is None or y is None:
|
if x is None or y is None:
|
||||||
continue
|
continue
|
||||||
self.marker_2d.setPos([x, y])
|
# Set position of horizontal ROI (row)
|
||||||
|
self.marker_2d_row.setPos([0, y])
|
||||||
|
# Set position of vertical ROI (column)
|
||||||
|
self.marker_2d_col.setPos([x, 0])
|
||||||
coordinate_to_emit = (name, x, y)
|
coordinate_to_emit = (name, x, y)
|
||||||
self.coordinatesClicked2D.emit(coordinate_to_emit)
|
self.coordinatesClicked2D.emit(coordinate_to_emit)
|
||||||
else:
|
else:
|
||||||
@ -428,6 +447,8 @@ class Crosshair(QObject):
|
|||||||
for item in self.items:
|
for item in self.items:
|
||||||
if isinstance(item, pg.ImageItem):
|
if isinstance(item, pg.ImageItem):
|
||||||
image = item.image
|
image = item.image
|
||||||
|
if image is None:
|
||||||
|
continue
|
||||||
ix = int(np.clip(x, 0, image.shape[0] - 1))
|
ix = int(np.clip(x, 0, image.shape[0] - 1))
|
||||||
iy = int(np.clip(y, 0, image.shape[1] - 1))
|
iy = int(np.clip(y, 0, image.shape[1] - 1))
|
||||||
intensity = image[ix, iy]
|
intensity = image[ix, iy]
|
||||||
@ -450,9 +471,12 @@ class Crosshair(QObject):
|
|||||||
self.clear_markers()
|
self.clear_markers()
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
if self.marker_2d is not None:
|
if self.marker_2d_row is not None:
|
||||||
self.plot_item.removeItem(self.marker_2d)
|
self.plot_item.removeItem(self.marker_2d_row)
|
||||||
self.marker_2d = None
|
self.marker_2d_row = None
|
||||||
|
if self.marker_2d_col is not None:
|
||||||
|
self.plot_item.removeItem(self.marker_2d_col)
|
||||||
|
self.marker_2d_col = None
|
||||||
self.plot_item.removeItem(self.v_line)
|
self.plot_item.removeItem(self.v_line)
|
||||||
self.plot_item.removeItem(self.h_line)
|
self.plot_item.removeItem(self.h_line)
|
||||||
self.plot_item.removeItem(self.coord_label)
|
self.plot_item.removeItem(self.coord_label)
|
||||||
|
@ -13,8 +13,10 @@ from qtpy.QtWidgets import QDialog, QVBoxLayout, QWidget
|
|||||||
from bec_widgets.utils import ConnectionConfig
|
from bec_widgets.utils import ConnectionConfig
|
||||||
from bec_widgets.utils.colors import Colors
|
from bec_widgets.utils.colors import Colors
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
from bec_widgets.utils.side_panel import SidePanel
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction, SwitchableToolBarAction
|
from bec_widgets.utils.toolbar import MaterialIconAction, SwitchableToolBarAction
|
||||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||||
|
from bec_widgets.widgets.plots.image.image_roi_plot import ImageROIPlot
|
||||||
from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
|
from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
|
||||||
from bec_widgets.widgets.plots.image.toolbar_bundles.image_selection import (
|
from bec_widgets.widgets.plots.image.toolbar_bundles.image_selection import (
|
||||||
MonitorSelectionToolbarBundle,
|
MonitorSelectionToolbarBundle,
|
||||||
@ -123,6 +125,7 @@ class Image(PlotBase):
|
|||||||
"rois",
|
"rois",
|
||||||
]
|
]
|
||||||
sync_colorbar_with_autorange = Signal()
|
sync_colorbar_with_autorange = Signal()
|
||||||
|
image_updated = Signal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -139,6 +142,8 @@ class Image(PlotBase):
|
|||||||
self._color_bar = None
|
self._color_bar = None
|
||||||
self._main_image = ImageItem()
|
self._main_image = ImageItem()
|
||||||
self.roi_controller = ROIController(colormap="viridis")
|
self.roi_controller = ROIController(colormap="viridis")
|
||||||
|
self.x_roi = None
|
||||||
|
self.y_roi = None
|
||||||
super().__init__(
|
super().__init__(
|
||||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||||
)
|
)
|
||||||
@ -150,24 +155,60 @@ class Image(PlotBase):
|
|||||||
# Default Color map to plasma
|
# Default Color map to plasma
|
||||||
self.color_map = "plasma"
|
self.color_map = "plasma"
|
||||||
|
|
||||||
|
# Initialize ROI plots and side panels
|
||||||
|
self._add_roi_plots()
|
||||||
|
|
||||||
self.roi_manager_dialog = None
|
self.roi_manager_dialog = None
|
||||||
|
|
||||||
|
# Refresh theme for ROI plots
|
||||||
|
self._update_theme()
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Widget Specific GUI interactions
|
# Widget Specific GUI interactions
|
||||||
################################################################################
|
################################################################################
|
||||||
|
def apply_theme(self, theme: str):
|
||||||
|
super().apply_theme(theme)
|
||||||
|
if self.x_roi is not None and self.y_roi is not None:
|
||||||
|
self.x_roi.apply_theme(theme)
|
||||||
|
self.y_roi.apply_theme(theme)
|
||||||
|
|
||||||
def _init_toolbar(self):
|
def _init_toolbar(self):
|
||||||
|
|
||||||
# add to the first position
|
# add to the first position
|
||||||
self.selection_bundle = MonitorSelectionToolbarBundle(
|
self.selection_bundle = MonitorSelectionToolbarBundle(
|
||||||
bundle_id="selection", target_widget=self
|
bundle_id="selection", target_widget=self
|
||||||
)
|
)
|
||||||
self.toolbar.add_bundle(self.selection_bundle, self)
|
self.toolbar.add_bundle(bundle=self.selection_bundle, target_widget=self)
|
||||||
|
|
||||||
super()._init_toolbar()
|
super()._init_toolbar()
|
||||||
|
|
||||||
# Image specific changes to PlotBase toolbar
|
# Image specific changes to PlotBase toolbar
|
||||||
self.toolbar.widgets["reset_legend"].action.setVisible(False)
|
self.toolbar.widgets["reset_legend"].action.setVisible(False)
|
||||||
|
|
||||||
|
# ROI Bundle replacement with switchable crosshair
|
||||||
|
self.toolbar.remove_bundle("roi")
|
||||||
|
crosshair = MaterialIconAction(
|
||||||
|
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
|
||||||
|
)
|
||||||
|
crosshair_roi = MaterialIconAction(
|
||||||
|
icon_name="my_location",
|
||||||
|
tooltip="Show Crosshair with ROI plots",
|
||||||
|
checkable=True,
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
crosshair_roi.action.toggled.connect(self.toggle_roi_panels)
|
||||||
|
crosshair.action.toggled.connect(self.toggle_crosshair)
|
||||||
|
switch_crosshair = SwitchableToolBarAction(
|
||||||
|
actions={"crosshair_simple": crosshair, "crosshair_roi": crosshair_roi},
|
||||||
|
initial_action="crosshair_simple",
|
||||||
|
tooltip="Crosshair",
|
||||||
|
checkable=True,
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
self.toolbar.add_action(
|
||||||
|
action_id="switch_crosshair", action=switch_crosshair, target_widget=self
|
||||||
|
)
|
||||||
|
|
||||||
# Lock aspect ratio button
|
# Lock aspect ratio button
|
||||||
self.lock_aspect_ratio_action = MaterialIconAction(
|
self.lock_aspect_ratio_action = MaterialIconAction(
|
||||||
icon_name="aspect_ratio", tooltip="Lock Aspect Ratio", checkable=True, parent=self
|
icon_name="aspect_ratio", tooltip="Lock Aspect Ratio", checkable=True, parent=self
|
||||||
@ -216,11 +257,8 @@ class Image(PlotBase):
|
|||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.toolbar.add_action_to_bundle(
|
self.toolbar.add_action(
|
||||||
bundle_id="roi",
|
action_id="autorange_image", action=self.autorange_switch, target_widget=self
|
||||||
action_id="autorange_image",
|
|
||||||
action=self.autorange_switch,
|
|
||||||
target_widget=self,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.autorange_mean_action.action.toggled.connect(
|
self.autorange_mean_action.action.toggled.connect(
|
||||||
@ -252,11 +290,8 @@ class Image(PlotBase):
|
|||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.toolbar.add_action_to_bundle(
|
self.toolbar.add_action(
|
||||||
bundle_id="roi",
|
action_id="switch_colorbar", action=self.colorbar_switch, target_widget=self
|
||||||
action_id="switch_colorbar",
|
|
||||||
action=self.colorbar_switch,
|
|
||||||
target_widget=self,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.simple_colorbar_action.action.toggled.connect(
|
self.simple_colorbar_action.action.toggled.connect(
|
||||||
@ -430,6 +465,101 @@ class Image(PlotBase):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("roi must be an int index or str name")
|
raise ValueError("roi must be an int index or str name")
|
||||||
|
|
||||||
|
def _add_roi_plots(self):
|
||||||
|
"""
|
||||||
|
Initialize the ROI plots and side panels.
|
||||||
|
"""
|
||||||
|
# Create ROI plot widgets
|
||||||
|
self.x_roi = ImageROIPlot(parent=self)
|
||||||
|
self.y_roi = ImageROIPlot(parent=self)
|
||||||
|
self.x_roi.apply_theme("dark")
|
||||||
|
self.y_roi.apply_theme("dark")
|
||||||
|
|
||||||
|
# Set titles for the plots
|
||||||
|
self.x_roi.plot_item.setTitle("X ROI")
|
||||||
|
self.y_roi.plot_item.setTitle("Y ROI")
|
||||||
|
|
||||||
|
# Create side panels
|
||||||
|
self.side_panel_x = SidePanel(
|
||||||
|
parent=self, orientation="bottom", panel_max_width=200, show_toolbar=False
|
||||||
|
)
|
||||||
|
self.side_panel_y = SidePanel(
|
||||||
|
parent=self, orientation="left", panel_max_width=200, show_toolbar=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add ROI plots to side panels
|
||||||
|
self.x_panel_index = self.side_panel_x.add_menu(widget=self.x_roi)
|
||||||
|
self.y_panel_index = self.side_panel_y.add_menu(widget=self.y_roi)
|
||||||
|
|
||||||
|
# # Add side panels to the layout
|
||||||
|
self.layout_manager.add_widget_relative(
|
||||||
|
self.side_panel_x, self.round_plot_widget, position="bottom", shift_direction="down"
|
||||||
|
)
|
||||||
|
self.layout_manager.add_widget_relative(
|
||||||
|
self.side_panel_y, self.round_plot_widget, position="left", shift_direction="right"
|
||||||
|
)
|
||||||
|
|
||||||
|
def toggle_roi_panels(self, checked: bool):
|
||||||
|
"""
|
||||||
|
Show or hide the ROI panels based on the test action toggle state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
checked (bool): Whether the test action is checked.
|
||||||
|
"""
|
||||||
|
if checked:
|
||||||
|
# Show the ROI panels
|
||||||
|
self.hook_crosshair()
|
||||||
|
self.side_panel_x.show_panel(self.x_panel_index)
|
||||||
|
self.side_panel_y.show_panel(self.y_panel_index)
|
||||||
|
self.crosshair.coordinatesChanged2D.connect(self.update_image_slices)
|
||||||
|
self.image_updated.connect(self.update_image_slices)
|
||||||
|
else:
|
||||||
|
self.unhook_crosshair()
|
||||||
|
# Hide the ROI panels
|
||||||
|
self.side_panel_x.hide_panel()
|
||||||
|
self.side_panel_y.hide_panel()
|
||||||
|
self.image_updated.disconnect(self.update_image_slices)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def update_image_slices(self, coordinates: tuple[int, int, int] = None):
|
||||||
|
"""
|
||||||
|
Update the image slices based on the crosshair position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coordinates(tuple): The coordinates of the crosshair.
|
||||||
|
"""
|
||||||
|
if coordinates is None:
|
||||||
|
# Try to get coordinates from crosshair position (like in crosshair mouse_moved)
|
||||||
|
if (
|
||||||
|
hasattr(self, "crosshair")
|
||||||
|
and hasattr(self.crosshair, "v_line")
|
||||||
|
and hasattr(self.crosshair, "h_line")
|
||||||
|
):
|
||||||
|
x = int(round(self.crosshair.v_line.value()))
|
||||||
|
y = int(round(self.crosshair.h_line.value()))
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
x = coordinates[1]
|
||||||
|
y = coordinates[2]
|
||||||
|
image = self._main_image.image
|
||||||
|
if image is None:
|
||||||
|
return
|
||||||
|
max_row, max_col = image.shape[0] - 1, image.shape[1] - 1
|
||||||
|
row, col = x, y
|
||||||
|
if not (0 <= row <= max_row and 0 <= col <= max_col):
|
||||||
|
return
|
||||||
|
# Horizontal slice
|
||||||
|
h_slice = image[:, col]
|
||||||
|
x_axis = np.arange(h_slice.shape[0])
|
||||||
|
self.x_roi.plot_item.clear()
|
||||||
|
self.x_roi.plot_item.plot(x_axis, h_slice, pen=pg.mkPen(self.x_roi.curve_color, width=3))
|
||||||
|
# Vertical slice
|
||||||
|
v_slice = image[row, :]
|
||||||
|
y_axis = np.arange(v_slice.shape[0])
|
||||||
|
self.y_roi.plot_item.clear()
|
||||||
|
self.y_roi.plot_item.plot(v_slice, y_axis, pen=pg.mkPen(self.y_roi.curve_color, width=3))
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Widget Specific Properties
|
# Widget Specific Properties
|
||||||
################################################################################
|
################################################################################
|
||||||
@ -984,6 +1114,7 @@ class Image(PlotBase):
|
|||||||
self._main_image.set_data(image_buffer)
|
self._main_image.set_data(image_buffer)
|
||||||
if self._color_bar is not None:
|
if self._color_bar is not None:
|
||||||
self._color_bar.blockSignals(False)
|
self._color_bar.blockSignals(False)
|
||||||
|
self.image_updated.emit()
|
||||||
|
|
||||||
def adjust_image_buffer(self, image: ImageItem, new_data: np.ndarray) -> np.ndarray:
|
def adjust_image_buffer(self, image: ImageItem, new_data: np.ndarray) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
@ -1035,6 +1166,7 @@ class Image(PlotBase):
|
|||||||
self._main_image.set_data(data)
|
self._main_image.set_data(data)
|
||||||
if self._color_bar is not None:
|
if self._color_bar is not None:
|
||||||
self._color_bar.blockSignals(False)
|
self._color_bar.blockSignals(False)
|
||||||
|
self.image_updated.emit()
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Clean up
|
# Clean up
|
||||||
@ -1090,6 +1222,10 @@ class Image(PlotBase):
|
|||||||
self.toolbar.widgets["monitor"].widget.close()
|
self.toolbar.widgets["monitor"].widget.close()
|
||||||
self.toolbar.widgets["monitor"].widget.deleteLater()
|
self.toolbar.widgets["monitor"].widget.deleteLater()
|
||||||
|
|
||||||
|
# ROI plots cleanup
|
||||||
|
self.x_roi.cleanup_pyqtgraph()
|
||||||
|
self.y_roi.cleanup_pyqtgraph()
|
||||||
|
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
37
bec_widgets/widgets/plots/image/image_roi_plot.py
Normal file
37
bec_widgets/widgets/plots/image/image_roi_plot.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
|
from bec_widgets.utils.round_frame import RoundedFrame
|
||||||
|
from bec_widgets.widgets.plots.plot_base import BECViewBox
|
||||||
|
|
||||||
|
|
||||||
|
class ImageROIPlot(RoundedFrame):
|
||||||
|
"""
|
||||||
|
A widget for displaying an image with a region of interest (ROI) overlay.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
|
self.content_widget = pg.GraphicsLayoutWidget(self)
|
||||||
|
self.layout.addWidget(self.content_widget)
|
||||||
|
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
||||||
|
self.content_widget.addItem(self.plot_item)
|
||||||
|
self.curve_color = "w"
|
||||||
|
|
||||||
|
self.apply_plot_widget_style()
|
||||||
|
|
||||||
|
def apply_theme(self, theme: str):
|
||||||
|
if theme == "dark":
|
||||||
|
self.curve_color = "w"
|
||||||
|
else:
|
||||||
|
self.curve_color = "k"
|
||||||
|
for curve in self.plot_item.curves:
|
||||||
|
curve.setPen(pg.mkPen(self.curve_color, width=3))
|
||||||
|
super().apply_theme(theme)
|
||||||
|
|
||||||
|
def cleanup_pyqtgraph(self):
|
||||||
|
"""Cleanup pyqtgraph items."""
|
||||||
|
self.plot_item.vb.menu.close()
|
||||||
|
self.plot_item.vb.menu.deleteLater()
|
||||||
|
self.plot_item.ctrlMenu.close()
|
||||||
|
self.plot_item.ctrlMenu.deleteLater()
|
@ -168,7 +168,7 @@ def test_image_data_update_1d(qtbot, mocked_client):
|
|||||||
|
|
||||||
def test_toolbar_actions_presence(qtbot, mocked_client):
|
def test_toolbar_actions_presence(qtbot, mocked_client):
|
||||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
assert "autorange_image" in bec_image_view.toolbar.bundles["roi"]
|
assert "autorange_image" in bec_image_view.toolbar.widgets
|
||||||
assert "lock_aspect_ratio" in bec_image_view.toolbar.bundles["mouse_interaction"]
|
assert "lock_aspect_ratio" in bec_image_view.toolbar.bundles["mouse_interaction"]
|
||||||
assert "processing" in bec_image_view.toolbar.bundles
|
assert "processing" in bec_image_view.toolbar.bundles
|
||||||
assert "selection" in bec_image_view.toolbar.bundles
|
assert "selection" in bec_image_view.toolbar.bundles
|
||||||
@ -414,3 +414,72 @@ def test_show_roi_manager_popup(qtbot, mocked_client):
|
|||||||
view.roi_manager_dialog.close()
|
view.roi_manager_dialog.close()
|
||||||
assert view.roi_manager_dialog is None
|
assert view.roi_manager_dialog is None
|
||||||
assert roi_action.isChecked() is False, "Icon should toggle off"
|
assert roi_action.isChecked() is False, "Icon should toggle off"
|
||||||
|
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# ROI Plots & Crosshair Switch
|
||||||
|
###################################
|
||||||
|
|
||||||
|
|
||||||
|
def test_crosshair_roi_panels_visibility(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Verify that enabling the ROI‑crosshair shows ROI panels and disabling hides them.
|
||||||
|
"""
|
||||||
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
|
switch = bec_image_view.toolbar.widgets["switch_crosshair"]
|
||||||
|
|
||||||
|
# Initially panels should be hidden
|
||||||
|
assert bec_image_view.side_panel_x.panel_height == 0
|
||||||
|
assert bec_image_view.side_panel_y.panel_width == 0
|
||||||
|
|
||||||
|
# Enable ROI crosshair
|
||||||
|
switch.actions["crosshair_roi"].action.trigger()
|
||||||
|
qtbot.wait(500)
|
||||||
|
|
||||||
|
# Panels must be visible
|
||||||
|
assert bec_image_view.side_panel_x.panel_height > 0
|
||||||
|
assert bec_image_view.side_panel_y.panel_width > 0
|
||||||
|
|
||||||
|
# Disable ROI crosshair
|
||||||
|
switch.actions["crosshair_roi"].action.trigger()
|
||||||
|
qtbot.wait(500)
|
||||||
|
|
||||||
|
# Panels hidden again
|
||||||
|
assert bec_image_view.side_panel_x.panel_height == 0
|
||||||
|
assert bec_image_view.side_panel_y.panel_width == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_roi_plot_data_from_image(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Check that ROI plots receive correct slice data from the 2D image.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
|
|
||||||
|
# Provide deterministic 2D data
|
||||||
|
test_data = np.arange(25).reshape(5, 5)
|
||||||
|
bec_image_view.on_image_update_2d({"data": test_data}, {})
|
||||||
|
|
||||||
|
# Activate ROI crosshair
|
||||||
|
switch = bec_image_view.toolbar.widgets["switch_crosshair"]
|
||||||
|
switch.actions["crosshair_roi"].action.trigger()
|
||||||
|
qtbot.wait(50)
|
||||||
|
|
||||||
|
# Simulate crosshair at row 2, col 3
|
||||||
|
bec_image_view.update_image_slices((0, 2, 3))
|
||||||
|
|
||||||
|
# Extract plotted data
|
||||||
|
x_items = bec_image_view.x_roi.plot_item.listDataItems()
|
||||||
|
y_items = bec_image_view.y_roi.plot_item.listDataItems()
|
||||||
|
|
||||||
|
assert len(x_items) == 1
|
||||||
|
assert len(y_items) == 1
|
||||||
|
|
||||||
|
# Vertical slice (column)
|
||||||
|
_, v_slice = x_items[0].getData()
|
||||||
|
np.testing.assert_array_equal(v_slice, test_data[:, 3])
|
||||||
|
|
||||||
|
# Horizontal slice (row)
|
||||||
|
h_slice, _ = y_items[0].getData()
|
||||||
|
np.testing.assert_array_equal(h_slice, test_data[2])
|
||||||
|
Reference in New Issue
Block a user