0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

fix(crosshair): adapted for 2D image

This commit is contained in:
2025-02-13 10:50:54 +01:00
parent 17f2dda977
commit a85402dde1
2 changed files with 76 additions and 18 deletions

View File

@ -1,4 +1,7 @@
from __future__ import annotations
from collections import defaultdict from collections import defaultdict
from typing import Any
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
@ -197,15 +200,18 @@ class Crosshair(QObject):
self.marker_2d = pg.ROI( self.marker_2d = pg.ROI(
[0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False [0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False
) )
self.marker_2d.skip_auto_range = True
self.plot_item.addItem(self.marker_2d) self.plot_item.addItem(self.marker_2d)
def snap_to_data(self, x, y) -> tuple[defaultdict[list], defaultdict[list]]: def snap_to_data(
self, x: float, y: float
) -> tuple[None, None] | tuple[defaultdict[Any, list], defaultdict[Any, list]]:
""" """
Finds the nearest data points to the given x and y coordinates. Finds the nearest data points to the given x and y coordinates.
Args: Args:
x: The x-coordinate of the mouse cursor x(float): The x-coordinate of the mouse cursor
y: The y-coordinate of the mouse cursor y(float): The y-coordinate of the mouse cursor
Returns: Returns:
tuple: x and y values snapped to the nearest data tuple: x and y values snapped to the nearest data
@ -235,7 +241,7 @@ class Crosshair(QObject):
y_values[name] = closest_y y_values[name] = closest_y
x_values[name] = closest_x x_values[name] = closest_x
elif isinstance(item, pg.ImageItem): # 2D plot elif isinstance(item, pg.ImageItem): # 2D plot
name = item.config.monitor name = item.config.monitor or str(id(item))
image_2d = item.image image_2d = item.image
# 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))
@ -320,7 +326,7 @@ class Crosshair(QObject):
) )
self.coordinatesChanged1D.emit(coordinate_to_emit) self.coordinatesChanged1D.emit(coordinate_to_emit)
elif isinstance(item, pg.ImageItem): elif isinstance(item, pg.ImageItem):
name = item.config.monitor name = item.config.monitor or str(id(item))
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
@ -374,7 +380,7 @@ class Crosshair(QObject):
) )
self.coordinatesClicked1D.emit(coordinate_to_emit) self.coordinatesClicked1D.emit(coordinate_to_emit)
elif isinstance(item, pg.ImageItem): elif isinstance(item, pg.ImageItem):
name = item.config.monitor name = item.config.monitor or str(id(item))
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
@ -418,9 +424,17 @@ class Crosshair(QObject):
""" """
x, y = pos x, y = pos
x_scaled, y_scaled = self.scale_emitted_coordinates(x, y) x_scaled, y_scaled = self.scale_emitted_coordinates(x, y)
text = f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})"
for item in self.items:
if isinstance(item, pg.ImageItem):
image = item.image
ix = int(np.clip(x, 0, image.shape[0] - 1))
iy = int(np.clip(y, 0, image.shape[1] - 1))
intensity = image[ix, iy]
text += f"\nIntensity: {intensity:.{self.precision}g}"
break
# Update coordinate label # Update coordinate label
self.coord_label.setText(f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})") self.coord_label.setText(text)
self.coord_label.setPos(x, y) self.coord_label.setPos(x, y)
self.coord_label.setVisible(True) self.coord_label.setVisible(True)
@ -436,6 +450,9 @@ class Crosshair(QObject):
self.clear_markers() self.clear_markers()
def cleanup(self): def cleanup(self):
if self.marker_2d is not None:
self.plot_item.removeItem(self.marker_2d)
self.marker_2d = 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)

View File

@ -4,9 +4,6 @@ import pytest
from qtpy.QtCore import QPointF, Qt from qtpy.QtCore import QPointF, Qt
from bec_widgets.utils import Crosshair from bec_widgets.utils import Crosshair
from bec_widgets.widgets.plots.image.image_widget import BECImageWidget
from .client_mocks import mocked_client
# pylint: disable = redefined-outer-name # pylint: disable = redefined-outer-name
@ -25,14 +22,20 @@ def plot_widget_with_crosshair(qtbot):
@pytest.fixture @pytest.fixture
def image_widget_with_crosshair(qtbot, mocked_client): def image_widget_with_crosshair(qtbot):
widget = BECImageWidget(client=mocked_client()) widget = pg.PlotWidget()
widget._image.add_custom_image(name="test", data=np.random.random((100, 200)))
widget._image.hook_crosshair()
qtbot.addWidget(widget) qtbot.addWidget(widget)
qtbot.waitExposed(widget) qtbot.waitExposed(widget)
yield widget._image.crosshair, widget._image.plot_item image_item = pg.ImageItem()
image_item.setImage(np.random.rand(100, 100))
image_item.config = type("obj", (object,), {"monitor": "test"})
widget.addItem(image_item)
plot_item = widget.getPlotItem()
crosshair = Crosshair(plot_item=plot_item, precision=3)
yield crosshair, plot_item
def test_mouse_moved_lines(plot_widget_with_crosshair): def test_mouse_moved_lines(plot_widget_with_crosshair):
@ -104,13 +107,13 @@ def test_mouse_moved_signals_2D(image_widget_with_crosshair):
crosshair.coordinatesChanged2D.connect(slot) crosshair.coordinatesChanged2D.connect(slot)
pos_in_view = QPointF(22.0, 55.0) pos_in_view = QPointF(21.0, 55.0)
pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view) pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene] event_mock = [pos_in_scene]
crosshair.mouse_moved(event_mock) crosshair.mouse_moved(event_mock)
assert emitted_values_2D == [("test", 22.0, 55.0)] assert emitted_values_2D == [("test", 21, 55)]
def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair): def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair):
@ -226,3 +229,41 @@ def test_crosshair_clicked_signal(qtbot, plot_widget_with_crosshair):
assert np.isclose(round(x, 1), 2) assert np.isclose(round(x, 1), 2)
assert np.isclose(round(y, 1), 5) assert np.isclose(round(y, 1), 5)
def test_update_coord_label_1D(plot_widget_with_crosshair):
crosshair, _ = plot_widget_with_crosshair
# Provide a test position
pos = (10, 20)
crosshair.update_coord_label(pos)
expected_text = f"({10:.3g}, {20:.3g})"
# Verify that the coordinate label shows only the 1D coordinates (no intensity line)
assert crosshair.coord_label.toPlainText() == expected_text
label_pos = crosshair.coord_label.pos()
assert np.isclose(label_pos.x(), 10)
assert np.isclose(label_pos.y(), 20)
assert crosshair.coord_label.isVisible()
def test_update_coord_label_2D(image_widget_with_crosshair):
crosshair, plot_item = image_widget_with_crosshair
known_image = np.array([[10, 20], [30, 40]], dtype=float)
for item in plot_item.items:
if isinstance(item, pg.ImageItem):
item.setImage(known_image)
pos = (0.5, 1.2)
crosshair.update_coord_label(pos)
ix = int(np.clip(0.5, 0, known_image.shape[0] - 1)) # 0
iy = int(np.clip(1.2, 0, known_image.shape[1] - 1)) # 1
intensity = known_image[ix, iy] # Expected: 20
expected_text = f"({0.5:.3g}, {1.2:.3g})\nIntensity: {intensity:.3g}"
assert crosshair.coord_label.toPlainText() == expected_text
label_pos = crosshair.coord_label.pos()
assert np.isclose(label_pos.x(), 0.5)
assert np.isclose(label_pos.y(), 1.2)
assert crosshair.coord_label.isVisible()