GUI: Raster grid manager - separated grid scan logic from camera image (there is still bug, when menu is closed, grid scan disappears)
Build and Publish / build (push) Successful in 22s

This commit is contained in:
2025-06-11 18:47:59 +02:00
parent 228d96bf7b
commit 0bd95d702b
4 changed files with 133 additions and 207 deletions
+7 -6
View File
@@ -15,9 +15,11 @@ from aaredaqlib.sample_geometry import SampleGeometryModel
from aaregui.panels.beamline_controls import BeamlineControls
from aaregui.panels.data_collection_settings import DataCollectionSettings
from aaregui.panels.raster_data_collection import RasterDataCollectionPanel
from aaregui.panels.sample_queue_panel import SampleQueuePanel
from aaregui.panels.status_panel import StatusPanel
from aaregui.panels.tell_sample_panel import TellSamplePanel
from aaregui.scan_logic.raster_grid_manager import RasterGridManager
from aaregui.threads.camera_thread import SampleCameraThread
from aaregui.threads.daq_worker import DAQWorker
from aaregui.widgets.camera_image import SampleCameraImageLabel
@@ -48,18 +50,19 @@ class MainWindow(QMainWindow):
top_widget.setLayout(top_widget_layout)
geom = SampleGeometryModel(beam_location_pxl=Coordinate(x=1000,y=1000),
pixel_in_mm=1,
pixel_in_mm=0.001,
aerotech=Coordinate(),
smargon=SmargonCoordinate(sh_mm=Coordinate(), phi_deg=0, chi_deg=0),
omega_deg=0,
beam_size_mm=Coordinate(x=1, y=1),
beam_size_mm=Coordinate(x=0.01, y=0.01),
aerotech_meas=Coordinate()
)
self.data_collection = DataCollectionSettings(s=geom, parent=top_widget)
top_widget_layout.addWidget(self.data_collection)
self.camera_image = SampleCameraImageLabel(geom=geom, parent=top_widget, default_image=default_image)
self.raster = RasterGridManager(geom=geom)
self.camera_image = SampleCameraImageLabel(geom=geom, raster=self.raster, parent=top_widget, default_image=default_image)
top_widget_layout.addWidget(self.camera_image)
@@ -164,9 +167,7 @@ class MainWindow(QMainWindow):
self.tell_samples.mount.connect(self.daq.mount)
self.tell_samples.unmount.connect(self.daq.unmount)
self.data_collection.raster.grid_size_updated.connect(
self.camera_image.update_raster_grid_size
)
self.data_collection.raster.grid_size_updated.connect(self.raster.update_grid_size)
self.status_bar.set_pgroup.connect(self.daq.set_pgroup)
self.status_bar.end_session.connect(self.daq.end_session)
self.status_bar.force_session.connect(self.daq.force_session)
@@ -63,18 +63,19 @@ class RasterGridManager(QObject):
def __init__(self, geom):
super().__init__()
self.__geom = geom
self.__sample = None
self.__start_point : QPointF = QPointF(0, 0)
self.__active_grid : RasterGridRequest = RasterGridRequest(
n_x= 0,
n_y= 0,
smargon = self.__geom.smargon,
grid_size_mm=Coordinate(x=0.1, y=0.1),
grid_size_mm=Coordinate(x=0.01, y=0.01),
omega_deg=self.__geom.omega_deg,
exp_time_s=1.0
)
self.__completed_grids : List[CompletedRasterGrid] = []
def is_grid_visible(self, grid: RasterGridRequest):
def _is_grid_visible(self, grid: RasterGridRequest):
if (
abs(normalize_angle(grid.omega_deg - self.__geom.omega_deg)) < 1.0
and abs(grid.smargon.phi_deg - self.__geom.smargon.phi_deg) < 1.0
@@ -89,21 +90,28 @@ class RasterGridManager(QObject):
def update_daq_status(self, s: DAQStatusModel):
self.__geom = s.geom
@Slot(QPointF, QPointF)
def update_raster_grid(self, start: QPointF, end: QPointF):
def resize_active_grid(self, end_point: QPointF):
self.update_active_grid(self.__start_point, end_point)
def update_active_grid(self, start: QPointF, end: QPointF):
delta_x = (end.x() - start.x()) * self.__geom.pixel_in_mm
delta_y = (end.y() - start.y()) * self.__geom.pixel_in_mm
self.__active_grid.n_x = round(abs(delta_x / self.__active_grid.grid_mm.x))
self.__active_grid.n_y = round(abs(delta_y / self.__active_grid.grid_mm.y))
self.__active_grid.n_x = round(abs(delta_x / self.__active_grid.grid_size_mm.x))
self.__active_grid.n_y = round(abs(delta_y / self.__active_grid.grid_size_mm.y))
c = self.__geom.picture_to_sample(Coordinate(x=start.x(), y=start.y()))
if delta_x < 0:
c.x -= self.__active_grid.n_x * self.__active_grid.grid_mm.x
c.x -= self.__active_grid.n_x * self.__active_grid.grid_size_mm.x
if delta_y < 0:
c.y -= self.__active_grid.n_y * self.__active_grid.grid_mm.y
c.y -= self.__active_grid.n_y * self.__active_grid.grid_size_mm.y
if delta_x < 0 or delta_y < 0:
c_pic = self.__geom.sample_to_picture(c)
self.__start_point = QPointF(c_pic.x, c_pic.y)
else:
self.__start_point = start
self.__active_grid.smargon = SmargonCoordinate(
sh_mm=self.__geom.beamline_to_smargon(c),
@@ -113,6 +121,34 @@ class RasterGridManager(QObject):
self.__active_grid.omega_deg = self.__geom.omega_deg
def move_active_grid(self, delta: QPointF):
if not self._is_grid_visible(self.__active_grid):
return
delta_pxl = Coordinate(x=delta.x(), y=delta.y())
delta_mm = self.__geom.smargon_nudge(delta_pxl * self.__geom.pixel_in_mm)
self.__active_grid.smargon = SmargonCoordinate(
sh_mm=self.__active_grid.smargon.sh_mm + delta_mm,
phi_deg=self.__geom.smargon.phi_deg,
chi_deg=self.__geom.smargon.chi_deg,
)
def clear_active_grid(self):
self.__active_grid.n_x = 0
self.__active_grid.n_y = 0
def is_part_of_active_grid(self, point: QPointF) -> bool:
if not self._is_grid_visible(self.__active_grid):
return False
point_bl = self.__geom.picture_to_sample(Coordinate(x=point.x(), y=point.y()))
delta = point_bl - self.__geom.smargon_to_beamline(self.__active_grid.smargon.sh_mm)
return (0 <= delta.x < self.__active_grid.n_x * self.__active_grid.grid_size_mm.x) and (
0 <= delta.y < self.__active_grid.n_y * self.__active_grid.grid_size_mm.y
)
@Slot(float)
def update_exposure_time(self, exp_time_s: float):
self.__active_grid.exp_time_s = exp_time_s
@@ -139,24 +175,26 @@ class RasterGridManager(QObject):
self._draw_grid(painter, i.request, i.spot_count)
def _draw_grid(self, painter: QPainter, grid: RasterGridRequest, values: List[float] | List[int] | None = None):
if not self.is_grid_visible(grid):
if not self._is_grid_visible(grid):
return
painter.setPen(QPen(QColor(114, 159, 207), 2, Qt.PenStyle.SolidLine))
min_value = min((x for x in values if not math.isnan(x)), default=0)
max_value = max((x for x in values if not math.isnan(x)), default=1)
if values is not None:
min_value = min((x for x in values if not math.isnan(x)), default=0)
max_value = max((x for x in values if not math.isnan(x)), default=1)
else:
min_value = 0
max_value = 1
g = grid.grid_size_pxl(self.__geom)
c0 = grid.start_pxl(self.__geom)
for x in range(self.__active_grid.n_x):
for y in range(self.__active_grid.n_y):
pxl = x + y * self.__active_grid.n_x
if len(values) <= pxl or math.isnan(values[pxl]):
if values is None or len(values) <= pxl or math.isnan(values[pxl]):
painter.setBrush(Qt.BrushStyle.NoBrush)
else:
painter.setBrush(QBrush(float_to_viridis_brush((values[pxl] - min_value) / (max_value - min_value), alpha=127)))
c = self.__geom.sample_to_picture(
c0 + Coordinate(x=x * grid.grid_size_mm.x,
y=y * grid.grid_size_mm.y)
+73 -186
View File
@@ -1,4 +1,4 @@
from typing import List
from enum import Enum
from PySide6.QtCore import Qt, QTimer, QRect, QPoint, Signal, Slot, QPointF
from PySide6.QtGui import (
@@ -26,7 +26,14 @@ from aaregui.models.bookmark import SmargonBookmarkList
from aaredaqlib.coordinate import Coordinate, SmargonCoordinate
from aaredaqlib.raster_grid import RasterGridRequest
from aaredaqlib.sample_geometry import SampleGeometryModel
from aaregui.scan_logic.raster_grid_manager import RasterGridManager
class SampleCameraImageState(Enum):
IDLE = 0
DRAWING_RASTER_GRID = 1
MOVING_RASTER_GRID = 2
RESIZE_RASTER_GRID = 3
BEAM_MARKING = 4
class SampleCameraImageLabel(QGraphicsView):
smargon = Signal(SmargonCoordinate)
@@ -41,29 +48,24 @@ class SampleCameraImageLabel(QGraphicsView):
def __init__(
self,
geom: SampleGeometryModel,
raster: RasterGridManager,
default_image: str | None,
parent=None,
):
super().__init__(parent)
self.__raster = []
self.__raster_mgr = raster
self.__state = SampleCameraImageState.IDLE
self.__geom = geom
self.__bookmarks: SmargonBookmarkList = SmargonBookmarkList()
self.__autoscale = True
self.__autoscale = False
self.__show_coords = False
self.__helical_start = SmargonCoordinate()
self.__helical_end = SmargonCoordinate()
self.drawing = False # Indicates whether the mouse is currently drawing
self.moving_raster_grid = None
self.right_click_grid = None
self.active_grid = None
self.drawn_grid = None
self.start_point = None # Starting point of the rectangle
self.end_point = None # Ending point of the rectangle
self.rectangles = [] # List of all drawn rectangles
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
@@ -75,7 +77,10 @@ class SampleCameraImageLabel(QGraphicsView):
self.pixmap_item = QGraphicsPixmapItem(pixmap)
self.scene.addItem(self.pixmap_item)
else:
self.pixmap_item = None
pixmap = QPixmap(2000, 2000)
pixmap.fill(Qt.GlobalColor.white)
self.pixmap_item = QGraphicsPixmapItem(pixmap)
self.scene.addItem(self.pixmap_item)
self.setFrameShape(QFrame.Shape.NoFrame)
self.setRenderHints(QPainter.RenderHint.Antialiasing)
@@ -91,28 +96,44 @@ class SampleCameraImageLabel(QGraphicsView):
self.left_click_hold_threshold = 200
self.right_click_hold_threshold = 200
# Default raster grid size
self.raster_grid_x = 0.02
self.raster_grid_y = 0.02
def drawForeground(self, painter, rect):
self.__draw_beam_center(painter)
self.__draw_grids(painter)
self.__raster_mgr.draw_grid(painter)
self.__draw_helical(painter)
def resizeEvent(self, event):
# Call the base class implementation of resizeEvent to maintain default behavior
super().resizeEvent(event)
self.__scaling()
def mousePressEvent(self, event):
pos = self.mapToScene(event.pos())
if event.button() == Qt.MouseButton.RightButton:
self.__start_drawing(pos)
self.click_timer.start(self.right_click_hold_threshold)
elif event.button() == Qt.MouseButton.LeftButton:
self.__start_moving(pos)
self.click_timer.start(self.left_click_hold_threshold)
self.start_point = self.mapToScene(event.pos())
print(self.__state)
match self.__state:
case SampleCameraImageState.BEAM_MARKING:
if event.button() == Qt.MouseButton.LeftButton:
self.update_beam_center.emit(self.start_point.x(), self.start_point.y())
case SampleCameraImageState.IDLE:
self.click_timer.start(self.right_click_hold_threshold)
if event.button() == Qt.MouseButton.RightButton:
if self.__raster_mgr.is_part_of_active_grid(self.start_point):
self.__state = SampleCameraImageState.RESIZE_RASTER_GRID
else:
self.__state = SampleCameraImageState.DRAWING_RASTER_GRID
elif event.button() == Qt.MouseButton.LeftButton:
if self.__raster_mgr.is_part_of_active_grid(self.start_point):
self.__state = SampleCameraImageState.MOVING_RASTER_GRID
def _update_grid(self):
pass
match self.__state:
case SampleCameraImageState.DRAWING_RASTER_GRID:
self.__raster_mgr.update_active_grid(self.start_point, self.end_point)
case SampleCameraImageState.MOVING_RASTER_GRID:
self.__raster_mgr.move_active_grid(self.end_point - self.start_point)
self.start_point = self.end_point
case SampleCameraImageState.RESIZE_RASTER_GRID:
self.__raster_mgr.resize_active_grid(self.end_point)
def mouseMoveEvent(self, event):
mouse_pos = self.mapToScene(event.pos())
@@ -122,11 +143,10 @@ class SampleCameraImageLabel(QGraphicsView):
self.mapToGlobal(event.pos()), f"{x:.0f}, {y:.0f} pxl", self
)
self.end_point = mouse_pos
if self.drawing and not self.click_timer.isActive():
self.__update_drawn_grid()
self.update()
elif self.moving_raster_grid is not None and not self.click_timer.isActive():
self.__update_moving_raster_grid()
if not self.click_timer.isActive():
self.end_point = self.mapToScene(event.pos())
self._update_grid()
self.update()
def mouseReleaseEvent(self, event):
@@ -134,14 +154,18 @@ class SampleCameraImageLabel(QGraphicsView):
if self.click_timer.isActive():
self.click_timer.stop()
self.__left_single_click(event)
self.__end_moving(event, self.click_timer.isActive())
else:
self._update_grid()
self.update()
if event.button() == Qt.MouseButton.RightButton:
if self.click_timer.isActive():
self.click_timer.stop()
self.__right_click_menu(event)
else:
self.__end_drawing(event)
self._update_grid()
self.update()
if self.__state != SampleCameraImageState.BEAM_MARKING:
self.__state = SampleCameraImageState.IDLE
def __right_click_menu(self, event):
self.drawing = False
@@ -166,9 +190,9 @@ class SampleCameraImageLabel(QGraphicsView):
delete_action = None
evaluate_action = None
self.active_grid = self.right_click_grid
scene_pos = self.mapToScene(event.pos())
if self.right_click_grid is not None:
if self.__raster_mgr.is_part_of_active_grid(scene_pos):
menu.addSection("Grid")
delete_action = menu.addAction("Delete")
evaluate_action = menu.addAction("Evaluate")
@@ -180,11 +204,10 @@ class SampleCameraImageLabel(QGraphicsView):
elif action == scale_action:
self.__autoscale = not self.__autoscale
self.__scaling()
elif self.right_click_grid is not None and action == delete_action:
self.__raster.remove(self.right_click_grid)
self.right_click_grid = None
elif self.right_click_grid is not None and action == evaluate_action:
self.run_raster.emit(self.right_click_grid)
elif action == delete_action:
self.__raster_mgr.clear_active_grid()
elif action == evaluate_action:
self.__raster_mgr.run_grid_scan()
elif action == grab_action:
self.__screenshot_with_dialog(overlay=False)
elif action == grab_with_overlay_action:
@@ -197,7 +220,6 @@ class SampleCameraImageLabel(QGraphicsView):
elif action == beam_mark_action:
c = self.mapToScene(event.pos())
self.update_beam_center.emit(c.x(), c.y())
self.active_grid = None
self.update()
def __screenshot_with_dialog(self, overlay: bool):
@@ -232,9 +254,7 @@ class SampleCameraImageLabel(QGraphicsView):
if self.pixmap_item.boundingRect().width() == 0:
ratio = 1.0
else:
ratio = (
self.viewport().size().width() / self.pixmap_item.boundingRect().width()
)
ratio = self.viewport().size().width() / self.pixmap_item.boundingRect().width()
if ratio < 0.2:
ratio = 0.2
@@ -246,11 +266,6 @@ class SampleCameraImageLabel(QGraphicsView):
matrix.scale(ratio, ratio)
self.setTransform(matrix)
@Slot(float, float)
def update_raster_grid_size(self, grid_size_x: float, grid_size_y: float):
self.raster_grid_x = grid_size_x
self.raster_grid_y = grid_size_y
@Slot(QPixmap)
def update_pixmap(self, pixmap: QPixmap):
if self.pixmap_item is not None: # Ensure pixmap_item exists
@@ -264,86 +279,19 @@ class SampleCameraImageLabel(QGraphicsView):
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
self.__geom = s.geom
# View is redrawn when new camera image will arrive
def __point_is_part_of_raster(self, pos) -> RasterGridRequest | None:
c = self.__geom.picture_to_sample(Coordinate(x=pos.x(), y=pos.y()))
for i in self.__raster:
if i is not None and i.point_sample_belongs_to_grid(self.__geom, c):
return i
return None
def __update_moving_raster_grid(self):
delta_pxl = Coordinate(
x=self.end_point.x() - self.start_point.x(),
y=self.end_point.y() - self.start_point.y(),
)
self.start_point = self.end_point
delta_mm = self.__geom.smargon_nudge(delta_pxl * self.__geom.pixel_in_mm)
self.moving_raster_grid.smargon = SmargonCoordinate(
sh_mm=self.moving_raster_grid.smargon.sh_mm + delta_mm,
phi_deg=self.__geom.smargon.phi_deg,
chi_deg=self.__geom.smargon.chi_deg,
)
def __left_single_click(self, event):
self.end_point = self.mapToScene(event.pos())
match self.__state:
case SampleCameraImageState.IDLE:
self.end_point = self.mapToScene(event.pos())
sample_coord = self.__geom.picture_to_sample(
Coordinate(x=self.end_point.x(), y=self.end_point.y())
)
smargon_coord = SmargonCoordinate(
sh_mm=self.__geom.beamline_to_smargon(sample_coord)
)
self.smargon.emit(smargon_coord)
def __start_moving(self, pos):
self.moving_raster_grid = self.__point_is_part_of_raster(pos)
self.active_grid = self.moving_raster_grid
self.start_point = pos # Record the starting position
self.end_point = self.start_point
def __end_moving(self, event, too_short: bool):
self.end_point = self.mapToScene(event.pos())
self.active_grid = None
if self.moving_raster_grid is not None and not too_short:
self.__update_moving_raster_grid()
self.moving_raster_grid = None
self.update()
def __start_drawing(self, pos):
self.drawing = True
self.right_click_grid = self.__point_is_part_of_raster(pos)
if self.right_click_grid is not None:
# Starting position is top-left corner of the old raster
old_start_pos = self.__geom.sample_to_picture(
self.right_click_grid.start_pxl(geom=self.__geom)
)
self.start_point = QPoint(int(old_start_pos.x), int(old_start_pos.y))
else:
self.start_point = pos
self.end_point = pos
def __end_drawing(self, event):
self.drawing = False
self.end_point = self.mapToScene(event.pos())
self.__update_drawn_grid()
if self.right_click_grid is not None:
self.__raster.remove(self.right_click_grid)
if self.drawn_grid is not None:
self.__raster.append(self.drawn_grid)
self.active_grid = None
self.right_click_grid = None
self.drawn_grid = None
self.update() # Repaint to permanently draw the rectangle
sample_coord = self.__geom.picture_to_sample(
Coordinate(x=self.end_point.x(), y=self.end_point.y())
)
smargon_coord = SmargonCoordinate(
sh_mm=self.__geom.beamline_to_smargon(sample_coord)
)
self.smargon.emit(smargon_coord)
def __draw_beam_center(self, painter: QPainter):
beam_size_pxl = self.__geom.beam_size_pxl
@@ -398,67 +346,6 @@ class SampleCameraImageLabel(QGraphicsView):
if start_pxl is not None and end_pxl is not None:
self.__draw_arrow(painter, start_pxl, end_pxl)
def __draw_grid(self, painter: QPainter, r: RasterGridRequest):
painter.setBrush(Qt.BrushStyle.NoBrush)
if r == self.active_grid:
painter.setPen(QPen(QColor(114, 159, 207), 2, Qt.PenStyle.SolidLine))
else:
painter.setPen(QPen(QColor(255, 0, 255), 2, Qt.PenStyle.SolidLine))
g = r.grid_size_pxl(self.__geom)
c0 = r.start_pxl(self.__geom)
for x in range(r.n_x):
for y in range(r.n_y):
c = self.__geom.sample_to_picture(
c0 + Coordinate(x=x * r.grid_size_mm.x, y=y * r.grid_size_mm.y)
)
painter.drawRect(QRect(int(c.x), int(c.y), int(g.x), int(g.y)))
def __draw_grids(self, painter):
for i in self.__raster:
if i is not None and i.grid_visible(self.__geom):
self.__draw_grid(painter, i)
if self.drawn_grid is not None:
self.__draw_grid(painter, self.drawn_grid)
def __update_drawn_grid(self):
if self.right_click_grid is None:
grid_mm = Coordinate(x=self.raster_grid_x, y=self.raster_grid_y)
else:
grid_mm = self.right_click_grid.grid_size_mm
delta_x = self.end_point.x() - self.start_point.x()
delta_y = self.end_point.y() - self.start_point.y()
n_x = round(abs(delta_x * self.__geom.pixel_in_mm / grid_mm.x))
n_y = round(abs(delta_y * self.__geom.pixel_in_mm / grid_mm.y))
c = self.__geom.picture_to_sample(
Coordinate(x=self.start_point.x(), y=self.start_point.y())
)
if delta_x < 0:
c.x -= n_x * grid_mm.x
if delta_y < 0:
c.y -= n_y * grid_mm.y
if n_x > 0 and n_y > 0:
self.drawn_grid = RasterGridRequest(
n_x=n_x,
n_y=n_y,
smargon=SmargonCoordinate(
sh_mm=self.__geom.beamline_to_smargon(c),
phi_deg=self.__geom.smargon.phi_deg,
chi_deg=self.__geom.smargon.chi_deg,
),
grid_size_mm=grid_mm,
omega_deg=self.__geom.omega_deg,
exp_time_s=1.0
)
self.active_grid = self.drawn_grid
else:
self.drawn_grid = None
@Slot(SmargonCoordinate)
def show_helical_start(self, pos: SmargonCoordinate):
self.__helical_start = pos