GUI: Improved performance of GUI with large reaster grids by throttle Raster grid drawing to 25 Hz, change geometry of the drawn grid only to imrpove speed - draw less lines particulalrly when cells are less than 2 px.

This commit is contained in:
2026-04-22 11:27:57 +02:00
parent c05969a02b
commit 3c4a500b4c
2 changed files with 242 additions and 47 deletions
+233 -45
View File
@@ -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)
+9 -2
View File
@@ -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):