GUI: Add beamline camera view

This commit is contained in:
leonarski_f committed 2025-07-15 09:57:16 +02:00
1 parent dd6d429e8d
commit 63d0acc227
3 files changed
+224 -17

No files matched your search

+49 -17
View File
@@ -9,11 +9,11 @@ from PySide6.QtWidgets import (
QVBoxLayout,
QMessageBox,
QApplication,
QDockWidget)
QDockWidget, QTabWidget)
from aaredaqlib.coordinate import Coordinate, SmargonCoordinate
from aaredaqlib.diffraction_geometry import DiffractionGeometry
from aaredaqlib.models import SampleShortInfoList, TokenData
from aaredaqlib.models import SampleShortInfoList, TokenData, DAQStatusModel, BeamlineStateEnum
from aaredaqlib.sample_geometry import SampleGeometryModel
from aaregui.panels.beamline_controls import BeamlineControls
@@ -24,12 +24,14 @@ from aaregui.panels.tell_sample_panel import TellSamplePanel
from aaregui.scan_logic.raster_grid_manager import RasterGridManager
from aaregui.scan_logic.rotation_scan_manager import RotationScanManager
from aaregui.scan_logic.sample_mount_logic import SampleMountLogic
from aaregui.threads.axis_video_thread import VideoThread
from aaregui.threads.camera_thread import SampleCameraThread
from aaregui.threads.daq_worker import DAQWorker
from aaregui.threads.jfjoch_viewer import JFJochDBusClient
from aaregui.widgets.camera_image import SampleCameraImageLabel
from aaregui.widgets.no_wheel_scroll_area import NoWheelScrollArea
from aaregui.widgets.status_bar import StatusBar
from aaregui.widgets.video_image import VideoGraphicsView
class MainWindow(QMainWindow):
@@ -42,6 +44,7 @@ class MainWindow(QMainWindow):
super().__init__()
self.__base_url = base_url
self.__token = token
self.__mounting = False
self.viewer = JFJochDBusClient()
@@ -93,9 +96,19 @@ class MainWindow(QMainWindow):
collection_controls_scroll.setFixedWidth(self.data_collection.set_width + 10)
self.camera_image = SampleCameraImageLabel(geom=geom, raster=self.raster, parent=top_widget, default_image=default_image)
self.video_tab = QTabWidget(parent=top_widget)
top_widget_layout.addWidget(self.camera_image)
self.sample_camera = SampleCameraImageLabel(geom=geom, raster=self.raster, parent=top_widget, default_image=default_image)
self.beamline_view = VideoGraphicsView()
self.beamline_camera_thread = VideoThread(ip="x06da-axis-1.psi.ch")
self.beamline_camera_thread.frame_ready.connect(self.beamline_view.update_frame)
self.beamline_camera_thread.start()
self.video_tab.addTab(self.sample_camera, "Sample camera")
self.video_tab.addTab(self.beamline_view, "Beamline view")
top_widget_layout.addWidget(self.video_tab)
beamline_controls_scroll = NoWheelScrollArea(top_widget)
@@ -146,15 +159,15 @@ class MainWindow(QMainWindow):
self.beamline.loopctr.bounding_box.clicked.connect(self.daq.ml_bounding_box)
self.daq.raster_generated_by_ml.connect(self.raster.update_active_grid_request)
self.camera_image.smargon.connect(self.daq.move_smargon)
self.sample_camera.smargon.connect(self.daq.move_smargon)
self.beamline.smargon_panel.smargon.connect(self.daq.move_smargon)
self.camera_image.samcam_updated.connect(self.daq.samcam_settings)
self.sample_camera.samcam_updated.connect(self.daq.samcam_settings)
self.raster.omega.connect(self.daq.set_omega)
self.raster.smargon.connect(self.daq.move_smargon)
self.beamline.omega_panel.set_omega.connect(self.daq.set_omega)
self.camera_image.set_omega.connect(self.daq.set_omega)
self.sample_camera.set_omega.connect(self.daq.set_omega)
self.beamline.zoom_panel.zoom.connect(self.daq.zoom)
self.beamline.illumination_panel.light.connect(self.daq.light)
self.beamline.abr_tweak.abr_tweak.connect(self.daq.abr_tweak)
@@ -162,15 +175,15 @@ class MainWindow(QMainWindow):
self.beamline.abr_tweak.abr_goto_meas.connect(self.daq.abr_goto_meas)
self.beamline.beam_mark.beam_mark_clear.connect(self.daq.beam_mark_clear)
self.camera_image.update_beam_mark.connect(self.daq.beam_mark_add)
self.sample_camera.update_beam_mark.connect(self.daq.beam_mark_add)
self.beamline.beam_center.beam_center.connect(self.daq.beam_center)
self.beamline.beam_size.beam_size.connect(self.daq.beam_size_mm)
self.camera_image.load_image.connect(self.raster.load_image)
self.camera_image.switch_raster_grid.connect(self.data_collection.switch_to_raster)
self.sample_camera.load_image.connect(self.raster.load_image)
self.sample_camera.switch_raster_grid.connect(self.data_collection.switch_to_raster)
if zmq_addr is not None:
self.camera_thread = SampleCameraThread(zmq_url=zmq_addr)
self.camera_thread.camera_image.connect(self.camera_image.update_pixmap)
self.camera_thread.camera_image.connect(self.sample_camera.update_pixmap)
self.camera_thread.start()
else:
self.camera_thread = None
@@ -184,13 +197,13 @@ class MainWindow(QMainWindow):
# )
#
self.camera_image.autofocus.connect(self.daq.autofocus)
self.sample_camera.autofocus.connect(self.daq.autofocus)
self.camera_image.evaluate_grid.connect(self.raster.run_grid_scan)
self.sample_camera.evaluate_grid.connect(self.raster.run_grid_scan)
self.data_collection.raster.evaluate_grid.connect(self.raster.run_grid_scan)
self.camera_image.clear_evaluated_grids.connect(self.raster.clear_completed_grids)
self.camera_image.clear_grid.connect(self.raster.clear_active_grid)
self.sample_camera.clear_evaluated_grids.connect(self.raster.clear_completed_grids)
self.sample_camera.clear_grid.connect(self.raster.clear_active_grid)
self.daq.run_number_incremented.connect(self.data_collection.file_path_panel.increment_run_number)
self.job_list_panel.auto_scan.connect(self.daq.automated_scan)
@@ -205,7 +218,7 @@ class MainWindow(QMainWindow):
self.data_collection.raster.transmission_updated.connect(self.raster.update_transmission)
self.data_collection.raster.dtz_updated.connect(self.raster.update_dtz)
self.data_collection.raster.grid_metric_updated.connect(self.raster.metric)
self.data_collection.raster.raster_alpha_changed.connect(self.camera_image.raster_alpha)
self.data_collection.raster.raster_alpha_changed.connect(self.sample_camera.raster_alpha)
self.data_collection.cancel.connect(self.daq.cancel)
self.raster.grid_scan.connect(self.daq.raster_scan)
@@ -235,7 +248,7 @@ class MainWindow(QMainWindow):
self.daq.update.connect(self.data_collection.update_daq_status)
self.daq.update.connect(self.beamline.illumination_panel.update_daq_status)
self.daq.update.connect(self.raster.update_daq_status)
self.daq.update.connect(self.camera_image.update_daq_status)
self.daq.update.connect(self.sample_camera.update_daq_status)
self.daq.update.connect(self.tell_samples.update_daq_status)
self.daq.update.connect(self.beamline.abr_tweak.update_daq_status)
self.daq.update.connect(self.status_bar.update_daq_status)
@@ -300,6 +313,23 @@ class MainWindow(QMainWindow):
"Aare Macromolecular Crystallography GUI\nVersion: 1.0\nCopyright: Paul Scherrer Instiute 2024-2025",
)
@Slot()
def mount_view(self):
self.video_tab.setCurrentIndex(1)
@Slot()
def sample_view(self):
self.video_tab.setCurrentIndex(0)
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
if not self.__mounting and s.state == BeamlineStateEnum.RobotSampleExchange:
self.__mounting = True
self.video_tab.setCurrentIndex(1)
elif self.__mounting and s.state != BeamlineStateEnum.RobotSampleExchange:
self.__mounting = False
self.video_tab.setCurrentIndex(0)
@Slot(str)
def display_error(self, msg: str):
self.status_bar.setStyleSheet("color: red;")
@@ -308,3 +338,5 @@ class MainWindow(QMainWindow):
def cleanup(self):
if self.camera_thread is not None:
self.camera_thread.stop()
if self.beamline_camera_thread is not None:
self.beamline_camera_thread.stop()
@@ -0,0 +1,81 @@
import cv2
from PySide6.QtCore import QThread, Signal
from PySide6.QtGui import QImage
class VideoThread(QThread):
frame_ready = Signal(QImage)
error_occurred = Signal(str)
def __init__(self, ip: str):
super().__init__()
self.camera_ip = ip
self.running = False
self.cap = None
self.last_good_frame = None
def set_camera_ip(self, ip: str):
cv2.setLogLevel(cv2.LOG_LEVEL_ERROR)
self.camera_ip = ip
def run(self):
# RTSP URL without credentials
rtsp_url = f"rtsp://{self.camera_ip}:554/axis-media/media.amp?videocodec=h264&resolution=1280x720"
self.cap = cv2.VideoCapture(rtsp_url)
if not self.cap.isOpened():
self.error_occurred.emit(f"Could not connect to camera at {self.camera_ip}")
return
# Set buffer size to reduce latency
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
self.cap.set(cv2.CAP_PROP_FPS, 10) # Request 10 FPSs
self.running = True
while self.running:
ret, frame = self.cap.read()
if ret and frame is not None:
# Check if frame is valid (not corrupted)
if frame.shape[0] > 0 and frame.shape[1] > 0:
try:
# Convert BGR to RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Convert to QImage
height, width, channel = rgb_frame.shape
bytes_per_line = 3 * width
qt_image = QImage(rgb_frame.data, width, height, bytes_per_line, QImage.Format.Format_RGB888)
# Store as last good frame
self.last_good_frame = qt_image
self.frame_ready.emit(qt_image)
except Exception as e:
# If frame processing fails, use last good frame if available
if self.last_good_frame is not None:
self.frame_ready.emit(self.last_good_frame)
# Don't emit error for minor decoding issues, just continue
pass
else:
# Invalid frame dimensions, use last good frame if available
if self.last_good_frame is not None:
self.frame_ready.emit(self.last_good_frame)
else:
# Frame read failed, use last good frame if available
if self.last_good_frame is not None:
self.frame_ready.emit(self.last_good_frame)
# Small delay to prevent overwhelming the GUI
self.msleep(100) # ~10 FPS
def stop(self):
self.running = False
print("Video thread stop requested")
if self.cap:
self.cap.release()
self.quit()
print("Video thread stopped. Exiting...")
self.wait()
+94
View File
@@ -0,0 +1,94 @@
from PySide6.QtCore import Qt, Slot
from PySide6.QtGui import QPainter, QPixmap, QImage
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
class VideoGraphicsView(QGraphicsView):
def __init__(self):
super().__init__()
# Create scene
self.scene = QGraphicsScene()
self.setScene(self.scene)
# Create pixmap item for video frames
self.pixmap_item = QGraphicsPixmapItem()
self.scene.addItem(self.pixmap_item)
# Configure view
self.setRenderHint(self.renderHints() | QPainter.RenderHint.Antialiasing)
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
# Enable zooming and panning
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
# Initial settings
self.zoom_factor = 1.0
self.min_zoom = 0.1
self.max_zoom = 5.0
@Slot(QImage)
def update_frame(self, qt_image : QImage):
"""Update the video frame"""
pixmap = QPixmap.fromImage(qt_image)
self.pixmap_item.setPixmap(pixmap)
# Fit to view on first frame
if self.zoom_factor == 1.0:
self.fit_to_view()
def fit_to_view(self):
"""Fit the video to the view"""
if self.pixmap_item.pixmap().isNull():
return
self.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
self.zoom_factor = 1.0
def reset_zoom(self):
"""Reset zoom to 100%"""
self.resetTransform()
self.zoom_factor = 1.0
def zoom_in(self):
"""Zoom in"""
if self.zoom_factor < self.max_zoom:
factor = 1.25
self.scale(factor, factor)
self.zoom_factor *= factor
def zoom_out(self):
"""Zoom out"""
if self.zoom_factor > self.min_zoom:
factor = 0.8
self.scale(factor, factor)
self.zoom_factor *= factor
def wheelEvent(self, event):
"""Handle mouse wheel for zooming"""
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
# Zoom with Ctrl + wheel
if event.angleDelta().y() > 0:
self.zoom_in()
else:
self.zoom_out()
event.accept()
else:
# Normal scrolling
super().wheelEvent(event)
def keyPressEvent(self, event):
"""Handle keyboard shortcuts"""
if event.key() == Qt.Key.Key_F:
self.fit_to_view()
elif event.key() == Qt.Key.Key_R:
self.reset_zoom()
elif event.key() == Qt.Key.Key_Plus or event.key() == Qt.Key.Key_Equal:
self.zoom_in()
elif event.key() == Qt.Key.Key_Minus:
self.zoom_out()
else:
super().keyPressEvent(event)