0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21: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 typing import Any
import numpy as np
import pyqtgraph as pg
@ -197,15 +200,18 @@ class Crosshair(QObject):
self.marker_2d = pg.ROI(
[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)
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.
Args:
x: The x-coordinate of the mouse cursor
y: The y-coordinate of the mouse cursor
x(float): The x-coordinate of the mouse cursor
y(float): The y-coordinate of the mouse cursor
Returns:
tuple: x and y values snapped to the nearest data
@ -235,7 +241,7 @@ class Crosshair(QObject):
y_values[name] = closest_y
x_values[name] = closest_x
elif isinstance(item, pg.ImageItem): # 2D plot
name = item.config.monitor
name = item.config.monitor or str(id(item))
image_2d = item.image
# 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))
@ -320,7 +326,7 @@ class Crosshair(QObject):
)
self.coordinatesChanged1D.emit(coordinate_to_emit)
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]
if x is None or y is None:
continue
@ -374,7 +380,7 @@ class Crosshair(QObject):
)
self.coordinatesClicked1D.emit(coordinate_to_emit)
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]
if x is None or y is None:
continue
@ -418,9 +424,17 @@ class Crosshair(QObject):
"""
x, y = pos
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
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.setVisible(True)
@ -436,6 +450,9 @@ class Crosshair(QObject):
self.clear_markers()
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.h_line)
self.plot_item.removeItem(self.coord_label)

View File

@ -4,9 +4,6 @@ import pytest
from qtpy.QtCore import QPointF, Qt
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
@ -25,14 +22,20 @@ def plot_widget_with_crosshair(qtbot):
@pytest.fixture
def image_widget_with_crosshair(qtbot, mocked_client):
widget = BECImageWidget(client=mocked_client())
widget._image.add_custom_image(name="test", data=np.random.random((100, 200)))
widget._image.hook_crosshair()
def image_widget_with_crosshair(qtbot):
widget = pg.PlotWidget()
qtbot.addWidget(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):
@ -104,13 +107,13 @@ def test_mouse_moved_signals_2D(image_widget_with_crosshair):
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)
event_mock = [pos_in_scene]
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):
@ -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(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()