diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 68d38e42..471b342a 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -32,6 +32,7 @@ from PySide6.QtGui import ( QColor, QCursor, QGuiApplication, + QImage, QKeySequence, QPalette, QPixmap, @@ -1608,16 +1609,20 @@ class MainWindow(QMainWindow): settings.setValue("samcam/compact_overlay_legend", overlay["compact_overlay_legend"]) settings.setValue("samcam/target_color", overlay["target_color"]) - @Slot(QPixmap) - def _on_sample_camera_frame(self, pixmap: QPixmap) -> None: + @Slot(QImage) + def _on_sample_camera_frame(self, image: QImage) -> None: """Hand one frame to whichever sample camera view is on screen. The subscriber emits from its own thread, so this runs queued on the GUI thread. Painting only the visible view costs one repaint per frame instead of three, and the acknowledgement at the end is what paces the subscriber to the rate the GUI can actually keep up with. + + The subscriber sends a QImage: QPixmap is a GUI-thread-only class, so + it is built here rather than in the worker. """ try: + pixmap = QPixmap.fromImage(image) for view in ( self.sample_camera, self.compact_sample_camera, diff --git a/src/aare/gui/threads/prediction_subscriber.py b/src/aare/gui/threads/prediction_subscriber.py index 70a60be2..1def221c 100644 --- a/src/aare/gui/threads/prediction_subscriber.py +++ b/src/aare/gui/threads/prediction_subscriber.py @@ -8,7 +8,7 @@ from aarecommon.config.logger import setup_logger from aarecommon.math.autofocus import focus_measure_edges from aarecommon.models.models import DAQStatusModel from PySide6.QtCore import QThread, Signal, Slot -from PySide6.QtGui import QImage, QPixmap +from PySide6.QtGui import QImage from aare.gui.constants import LOGGER_NAME @@ -18,7 +18,7 @@ logger = setup_logger(LOGGER_NAME) class PredictionSubscriber(QThread): prediction = Signal(dict) target_point = Signal(dict) - image = Signal(QPixmap) + image = Signal(QImage) focus_measure = Signal(float) fps_measure = Signal(float) camera_availability_changed = Signal(bool) @@ -146,11 +146,11 @@ class PredictionSubscriber(QThread): return None - def _rgb_to_pixmap(self, rgb: np.ndarray) -> QPixmap: - qimage = QImage(rgb.data, rgb.shape[1], rgb.shape[0], QImage.Format.Format_RGB888).copy() - return QPixmap.fromImage(qimage) + def _rgb_to_qimage(self, rgb: np.ndarray) -> QImage: + # .copy() because QImage does not own the numpy buffer it wraps. + return QImage(rgb.data, rgb.shape[1], rgb.shape[0], QImage.Format.Format_RGB888).copy() - def _emit_image(self, pixmap: QPixmap) -> None: + def _emit_image(self, image: QImage) -> None: """Emit a frame only while the GUI is keeping up, dropping it otherwise. `image` is a queued cross-thread signal and Qt's event queue has no @@ -169,7 +169,7 @@ class PredictionSubscriber(QThread): return self._frames_in_flight += 1 - self.image.emit(pixmap) + self.image.emit(image) def notify_frame_displayed(self) -> None: """Called from the GUI thread once a frame has been handed to the views.""" @@ -284,7 +284,7 @@ class PredictionSubscriber(QThread): self._set_camera_available(True) self._emit_focus_measure_if_enabled(rgb) if self.running: - self._emit_image(self._rgb_to_pixmap(rgb)) + self._emit_image(self._rgb_to_qimage(rgb)) elif self._emit_images: self._set_camera_available( False, "Sample camera feed unavailable: no frame header in zmq stream"