diff --git a/src/aare/gui/scan_logic/raster_grid_manager.py b/src/aare/gui/scan_logic/raster_grid_manager.py index 3ce894a9..9f53564c 100644 --- a/src/aare/gui/scan_logic/raster_grid_manager.py +++ b/src/aare/gui/scan_logic/raster_grid_manager.py @@ -1,7 +1,7 @@ import math from enum import Enum -from PySide6.QtCore import QObject, Signal, Slot, QPointF +from PySide6.QtCore import QObject, Signal, Slot, QPointF, QRectF, QLineF from PySide6.QtGui import QPainter, QPen, QColor, QBrush from PySide6.QtCore import Qt, QRect from typing import List, Tuple @@ -74,6 +74,7 @@ def normalize_angle(angle_deg: float) -> float: normalized_angle = math.fmod(angle_deg + 180, 360) - 180.0 return normalized_angle + class RasterGridManager(QObject): viewer_track_online = Signal() @@ -133,6 +134,54 @@ class RasterGridManager(QObject): return True return False + def _grid_pixel_geometry(self, grid: RasterGridRequest) -> tuple[float, float, float, float] | None: + if not self._is_grid_visible(grid): + return None + + start_anchor = grid.start_pxl(self.__geom) + start_picture = self.__geom.sample_to_picture(start_anchor) + cell = grid.grid_size_pxl(self.__geom) + + cell_w = float(cell.x) + cell_h = float(cell.y) + + if cell_w <= 0 or cell_h <= 0: + return None + + return float(start_picture.x), float(start_picture.y), cell_w, cell_h + + def _grid_bounds_rect(self, grid: RasterGridRequest) -> QRectF | None: + geo = self._grid_pixel_geometry(grid) + if geo is None: + return None + + start_x, start_y, cell_w, cell_h = geo + return QRectF( + start_x, + start_y, + cell_w * grid.n_x, + cell_h * grid.n_y, + ) + + def _visible_index_range( + self, + grid: RasterGridRequest, + visible_rect: QRectF | None, + start_x: float, + start_y: float, + cell_w: float, + cell_h: float, + ) -> tuple[int, int, int, int]: + if visible_rect is None or visible_rect.isEmpty(): + return 0, grid.n_x, 0, grid.n_y + + min_x = max(0, int(math.floor((visible_rect.left() - start_x) / cell_w)) - 1) + max_x = min(grid.n_x, int(math.ceil((visible_rect.right() - start_x) / cell_w)) + 1) + min_y = max(0, int(math.floor((visible_rect.top() - start_y) / cell_h)) - 1) + max_y = min(grid.n_y, int(math.ceil((visible_rect.bottom() - start_y) / cell_h)) + 1) + + return min_x, max_x, min_y, max_y + @Slot(RasterGridRequest) def update_active_grid_request(self, grid: RasterGridRequest): self.__active_grid.smargon_top_left = grid.smargon_top_left @@ -140,10 +189,12 @@ class RasterGridManager(QObject): self.__active_grid.grid_size_mm = grid.grid_size_mm self.__active_grid.n_x = grid.n_x self.__active_grid.n_y = grid.n_y - self.grid_scan_size_changed.emit(self.__active_grid.n_x, - self.__active_grid.n_y, - self.__active_grid.grid_size_mm.x, - self.__active_grid.grid_size_mm.y) + self.grid_scan_size_changed.emit( + self.__active_grid.n_x, + self.__active_grid.n_y, + self.__active_grid.grid_size_mm.x, + self.__active_grid.grid_size_mm.y, + ) @Slot(DAQStatusModel) def update_daq_status(self, s: DAQStatusModel): @@ -186,17 +237,18 @@ class RasterGridManager(QObject): ) self.__active_grid.omega_deg = self.__geom.omega_deg - self.grid_scan_size_changed.emit(self.__active_grid.n_x, - self.__active_grid.n_y, - self.__active_grid.grid_size_mm.x, - self.__active_grid.grid_size_mm.y) + self.grid_scan_size_changed.emit( + self.__active_grid.n_x, + self.__active_grid.n_y, + self.__active_grid.grid_size_mm.x, + self.__active_grid.grid_size_mm.y, + ) 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_top_left = SmargonCoordinate( @@ -209,9 +261,12 @@ class RasterGridManager(QObject): def clear_active_grid(self): self.__active_grid.n_x = 0 self.__active_grid.n_y = 0 - self.grid_scan_size_changed.emit(0,0, - self.__active_grid.grid_size_mm.x, - self.__active_grid.grid_size_mm.y) + self.grid_scan_size_changed.emit( + 0, + 0, + self.__active_grid.grid_size_mm.x, + self.__active_grid.grid_size_mm.y, + ) def get_grid_coord(self, grid: RasterGridRequest, point: QPointF) -> Tuple[int, int]: point_bl = self.__geom.picture_to_sample(Coordinate(x=point.x(), y=point.y())) @@ -363,22 +418,29 @@ class RasterGridManager(QObject): return obj.spots_ice / obj.spots_low_res def draw_grid(self, painter: QPainter, alpha: int = 127): - self._draw_grid(painter, self.__active_grid) + if alpha < 0 or alpha > 255: + return + + visible_rect = painter.clipBoundingRect() + if visible_rect.isEmpty(): + visible_rect = QRectF() + + self._draw_grid(painter, self.__active_grid, None, alpha, visible_rect) + for i in self.__completed_grids: - v = None match self.__metric: - case RasterGridMetric.BKG: - v = [obj.bkg for obj in i.result.images] + case RasterGridMetric.SPOTS_LOW_RES: + v = [obj.spots_low_res for obj in i.result.images] case RasterGridMetric.SPOTS: v = [obj.spots for obj in i.result.images] + case RasterGridMetric.BKG: + v = [obj.bkg for obj in i.result.images] case RasterGridMetric.INDEXING: v = [obj.index for obj in i.result.images] case RasterGridMetric.PR: - v = [obj.pr for obj in i.result.images] + v = [obj.index / max(obj.spots_low_res, 1) for obj in i.result.images] case RasterGridMetric.BFACTOR: v = [obj.b for obj in i.result.images] - case RasterGridMetric.SPOTS_LOW_RES: - v = [obj.spots_low_res for obj in i.result.images] case RasterGridMetric.RES: v = [obj.res for obj in i.result.images] case RasterGridMetric.SPOTS_ICE: @@ -387,43 +449,172 @@ class RasterGridManager(QObject): v = [self.spot_ice_ratio(obj) for obj in i.result.images] case RasterGridMetric.SPOTS_INDEXED: v = [obj.spots_indexed for obj in i.result.images] - self._draw_grid(painter, i.request, v, alpha) + self._draw_grid(painter, i.request, v, alpha, visible_rect) - def _draw_grid(self, painter: QPainter, grid: RasterGridRequest, values: List[float] | List[int] | None = None, - alpha : int = 127): + def _draw_grid( + self, + painter: QPainter, + grid: RasterGridRequest, + values: List[float] | List[int] | None = None, + alpha: int = 127, + visible_rect: QRectF | None = None, + ): if not self._is_grid_visible(grid): return if alpha < 0 or alpha > 255: return + if values is None: + if self._draw_active_grid_fast(painter, grid, visible_rect): + return + + geo = self._grid_pixel_geometry(grid) + if geo is None: + return + + start_x, start_y, cell_w, cell_h = geo + bounds = self._grid_bounds_rect(grid) + if bounds is None: + return + + if visible_rect is not None and not visible_rect.isEmpty() and not bounds.intersects(visible_rect): + return + + painter.save() + painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) + if values is not None: painter.setPen(Qt.PenStyle.NoPen) min_value = min((x for x in values if x is not None and not math.isnan(x)), default=0) max_value = max((x for x in values if x is not None and not math.isnan(x)), default=1) - if min_value == max_value: - diff = 1 - else: - diff = max_value - min_value - + diff = 1 if min_value == max_value else (max_value - min_value) else: - painter.setPen(QPen(QColor(114, 159, 207), 2, Qt.PenStyle.SolidLine)) + painter.setPen(QPen(QColor(114, 159, 207), 1, Qt.PenStyle.SolidLine)) min_value = 0 diff = 1 - g = grid.grid_size_pxl(self.__geom) - c0 = grid.start_pxl(self.__geom) - for x in range(grid.n_x): - for y in range(grid.n_y): - pxl = x + y * grid.n_x - if values is None or len(values) <= pxl or values[pxl] is None or math.isnan(values[pxl]) or values[pxl] < 0: - painter.setBrush(Qt.BrushStyle.NoBrush) - else: - painter.setBrush(QBrush(float_to_viridis_brush((values[pxl] - min_value) / diff, alpha=alpha))) - c = self.__geom.sample_to_picture(c0 + Coordinate(x=x * grid.grid_size_mm.x, y=y * grid.grid_size_mm.y)) - painter.drawRect(QRect(round(c.x), round(c.y), round(g.x), round(g.y))) + min_x, max_x, min_y, max_y = self._visible_index_range( + grid, visible_rect, start_x, start_y, cell_w, cell_h + ) + stride_x = 1 + stride_y = 1 + if values is not None: + min_fill_px = 2.0 + stride_x = max(1, int(math.ceil(min_fill_px / max(cell_w, 1e-9)))) + stride_y = max(1, int(math.ceil(min_fill_px / max(cell_h, 1e-9)))) + + for y in range(min_y, max_y, stride_y): + py = start_y + y * cell_h + draw_h = max(1.0, cell_h * stride_y) + + for x in range(min_x, max_x, stride_x): + px = start_x + x * cell_w + draw_w = max(1.0, cell_w * stride_x) + + if values is None: + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawRect(QRect(round(px), round(py), round(draw_w), round(draw_h))) + continue + + value = self._block_value( + values, + grid.n_x, + x, + min(x + stride_x, grid.n_x), + y, + min(y + stride_y, grid.n_y), + ) + if value is None: + continue + + brush = float_to_viridis_brush((value - min_value) / diff, alpha=alpha) + painter.fillRect(QRectF(px, py, draw_w, draw_h), brush) + + if values is None: + painter.setBrush(Qt.BrushStyle.NoBrush) + + painter.restore() + + def _draw_active_grid_fast( + self, + painter: QPainter, + grid: RasterGridRequest, + visible_rect: QRectF | None, + ) -> bool: + geo = self._grid_pixel_geometry(grid) + if geo is None: + return False + + start_x, start_y, cell_w, cell_h = geo + bounds = self._grid_bounds_rect(grid) + if bounds is None: + return False + + if visible_rect is not None and not visible_rect.isEmpty() and not bounds.intersects(visible_rect): + return True + + painter.save() + painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) + painter.setPen(QPen(QColor(114, 159, 207), 1, Qt.PenStyle.SolidLine)) painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawRect(bounds) + + min_spacing_px = 4.0 + stride_x = max(1, int(math.ceil(min_spacing_px / max(cell_w, 1e-9)))) + stride_y = max(1, int(math.ceil(min_spacing_px / max(cell_h, 1e-9)))) + + min_ix, max_ix, min_iy, max_iy = self._visible_index_range( + grid, visible_rect, start_x, start_y, cell_w, cell_h + ) + + vertical_lines: list[QLineF] = [] + horizontal_lines: list[QLineF] = [] + + x_start = max(1, min_ix) + x_end = min(grid.n_x, max_ix) + for x in range(x_start, x_end, stride_x): + px = start_x + x * cell_w + vertical_lines.append(QLineF(px, bounds.top(), px, bounds.bottom())) + + y_start = max(1, min_iy) + y_end = min(grid.n_y, max_iy) + for y in range(y_start, y_end, stride_y): + py = start_y + y * cell_h + horizontal_lines.append(QLineF(bounds.left(), py, bounds.right(), py)) + + if vertical_lines: + painter.drawLines(vertical_lines) + if horizontal_lines: + painter.drawLines(horizontal_lines) + + painter.restore() + return True + + def _block_value( + self, + values: List[float] | List[int], + grid_nx: int, + x0: int, + x1: int, + y0: int, + y1: int, + ) -> float | None: + best = None + for y in range(y0, y1): + row_offset = y * grid_nx + for x in range(x0, x1): + idx = row_offset + x + if idx >= len(values): + continue + value = values[idx] + if value is None or math.isnan(value) or value < 0: + continue + if best is None or value > best: + best = float(value) + return best + @Slot(float) def update_exposure_time(self, exp_time_s: float): self.__active_grid.exp_time_s = exp_time_s @@ -460,11 +651,8 @@ class RasterGridManager(QObject): if last_raster is not None and last_raster.result.file_prefix is not None: com = last_raster.centre_of_mass logger.info(f"COM: {com}") - if com: - cell = com.max_image - else: - cell = 0 - self.image_selected.emit("http://sls-gpu-001:8080",cell) #last_raster.result.file_prefix, 0) + cell = com.max_image if com else 0 + self.image_selected.emit("http://sls-gpu-001:8080", cell) self.completed_grid_updated.emit() @Slot(int, bool) diff --git a/src/aare/gui/widgets/camera_image.py b/src/aare/gui/widgets/camera_image.py index 8bfd3c10..6149701d 100644 --- a/src/aare/gui/widgets/camera_image.py +++ b/src/aare/gui/widgets/camera_image.py @@ -74,6 +74,8 @@ class SampleCameraImageLabel(QGraphicsView): self.__sam_cam = SampleCameraSettings(exposure=0.1, gain=100.0) self.__is_daq_busy = False self.__camera_available = True + self.__last_grid_update_ts = 0.0 + self.__grid_update_min_interval_s = 1.0 / 25.0 self.__geom = geom self.__bookmarks: SmargonBookmarkList = SmargonBookmarkList() @@ -118,7 +120,7 @@ class SampleCameraImageLabel(QGraphicsView): self.scene.addItem(self.pixmap_item) self.setFrameShape(QFrame.Shape.NoFrame) - self.setRenderHints(QPainter.RenderHint.Antialiasing) + self.setRenderHints(QPainter.RenderHint.TextAntialiasing) # Create a timer for throttling wheel events self.wheel_event_timer = QTimer() @@ -367,6 +369,11 @@ class SampleCameraImageLabel(QGraphicsView): def _update_grid(self): + now = time.monotonic() + if (now - self.__last_grid_update_ts) < self.__grid_update_min_interval_s: + return + self.__last_grid_update_ts = now + match self.__state: case SampleCameraImageState.DRAWING_RASTER_GRID: self.switch_raster_grid.emit() @@ -568,7 +575,7 @@ class SampleCameraImageLabel(QGraphicsView): # If no pixmap item exists (rare case), create one self.pixmap_item = QGraphicsPixmapItem(pixmap) self.scene.addItem(self.pixmap_item) - self.update() # Request an update to redraw the view + self.viewport().update() # Request an update to redraw the view @Slot(DAQStatusModel) def update_daq_status(self, s: DAQStatusModel):