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:
@ -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)
|
||||||
|
@ -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()
|
||||||
|
Reference in New Issue
Block a user