GUI: Add beamline camera view
This commit is contained in:
1 parent
dd6d429e8d
commit
63d0acc227
3 files changed
+224
-17
No files matched your search
@@ -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()
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user