fix: build the sample camera QPixmap on the GUI thread
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Failing after 38s
CI / test (3.12) (pull_request) Successful in 1m0s
CI / test (3.13) (pull_request) Successful in 1m2s
CI / test (3.14) (pull_request) Successful in 1m1s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m8s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m14s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m19s
CI / test-with-coverage (pull_request) Successful in 1m26s
CI / coverage-analysis (pull_request) Failing after 3s
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Failing after 38s
CI / test (3.12) (pull_request) Successful in 1m0s
CI / test (3.13) (pull_request) Successful in 1m2s
CI / test (3.14) (pull_request) Successful in 1m1s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m8s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m14s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m19s
CI / test-with-coverage (pull_request) Successful in 1m26s
CI / coverage-analysis (pull_request) Failing after 3s
QPixmap is a GUI-thread-only class in Qt; the worker was constructing one per frame via QPixmap.fromImage. It happens to work with the raster backend, which is why this has not bitten us, but it is not supported and the guarantee is not ours to rely on. The subscriber now emits QImage — which is explicitly safe to build and move between threads — and _on_sample_camera_frame converts once on the GUI thread before handing the pixmap to the visible view. No extra copy: the .copy() that detaches the QImage from the numpy buffer was already there. Verified end to end against a real PUB socket: the payload delivered is QImage, the slot runs only on the main thread, and frames still decode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVqbXPoeHyqrQq5Vc8EcYP
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user