Source code for pyccapt.control.gui.gui_cameras

import sys
from pathlib import Path

import numpy as np
import pyqtgraph as pg
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import pyqtSignal, QObject, QThread
from PyQt6.QtGui import QPixmap

# Local module and scripts
from pyccapt.control.core import runtime
from pyccapt.control.devices import camera
from pyccapt.control.gui import tooltips
from pyccapt.control.usb_switch import usb_switch


[docs] class Ui_Cameras_Alignment(object): def __init__(self, variables, conf, SignalEmitter): """ Initialize the UiCamerasAlignment class. Args: variables: Global experiment variables. conf: Configuration data. SignalEmitter: Signal emitter for communication. """ # Cameras default to auto-exposure (Continuous) on startup; the # button text describes the action it performs ("Auto Exposure # Time Off" → click to switch into manual mode). The flag mirrors # the worker's `exposure_auto` state. self.auto_exposure_time_flag = True self.conf = conf self.emitter = SignalEmitter self.variables = variables
[docs] def setupUi(self, Cameras_Alignment): """ Set up the GUI for the Cameras Alignment window. Args: Cameras_Alignment: Returns: None """ Cameras_Alignment.setObjectName("Cameras_Alignment") Cameras_Alignment.resize(1210, 938) self.gridLayout_5 = QtWidgets.QGridLayout(Cameras_Alignment) self.gridLayout_5.setObjectName("gridLayout_5") self.gridLayout_4 = QtWidgets.QGridLayout() self.gridLayout_4.setObjectName("gridLayout_4") self.verticalLayout = QtWidgets.QVBoxLayout() self.verticalLayout.setObjectName("verticalLayout") self.gridLayout = QtWidgets.QGridLayout() self.gridLayout.setObjectName("gridLayout") self.label_208 = QtWidgets.QLabel(parent=Cameras_Alignment) font = QtGui.QFont() font.setBold(True) self.label_208.setFont(font) self.label_208.setObjectName("label_208") self.gridLayout.addWidget(self.label_208, 5, 0, 1, 1) ### # self.cam_s_d = QtWidgets.QLabel(parent=Cameras_alignment) self.cam_s_d = pg.ImageView(parent=Cameras_Alignment) self.cam_s_d.adjustSize() self.cam_s_d.ui.histogram.hide() self.cam_s_d.ui.roiBtn.hide() self.cam_s_d.ui.menuBtn.hide() self.cam_s_d.setObjectName("cam_s_o") ### sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(2) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.cam_s_d.sizePolicy().hasHeightForWidth()) self.cam_s_d.setSizePolicy(sizePolicy) self.cam_s_d.setMinimumSize(QtCore.QSize(600, 250)) self.cam_s_d.setMaximumSize(QtCore.QSize(16777215, 16777215)) self.cam_s_d.setStyleSheet( "QWidget{\n" " border: 2px solid gray;\n" " }\n" " " ) # self.cam_s_d.setText("") self.cam_s_d.setObjectName("cam_s_d") self.gridLayout.addWidget(self.cam_s_d, 3, 4, 1, 1) self.label_209 = QtWidgets.QLabel(parent=Cameras_Alignment) font = QtGui.QFont() font.setBold(True) self.label_209.setFont(font) self.label_209.setObjectName("label_209") self.gridLayout.addWidget(self.label_209, 5, 4, 1, 1) ### # self.cam_b_d = QtWidgets.QLabel(parent=Cameras_alignment) self.cam_b_d = pg.ImageView(parent=Cameras_Alignment) self.cam_b_d.adjustSize() self.cam_b_d.ui.histogram.hide() self.cam_b_d.ui.roiBtn.hide() self.cam_b_d.ui.menuBtn.hide() ### sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(2) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.cam_b_d.sizePolicy().hasHeightForWidth()) self.cam_b_d.setSizePolicy(sizePolicy) self.cam_b_d.setMinimumSize(QtCore.QSize(600, 250)) self.cam_b_d.setMaximumSize(QtCore.QSize(16777215, 16777215)) self.cam_b_d.setStyleSheet( "QWidget{\n" " border: 2px solid gray;\n" " }\n" " " ) # self.cam_b_d.setText("") self.cam_b_d.setObjectName("cam_b_d") self.gridLayout.addWidget(self.cam_b_d, 6, 4, 1, 1) self.label_204 = QtWidgets.QLabel(parent=Cameras_Alignment) font = QtGui.QFont() font.setBold(True) self.label_204.setFont(font) self.label_204.setObjectName("label_204") self.gridLayout.addWidget(self.label_204, 2, 4, 1, 1) ### # self.cam_s_o = QtWidgets.QLabel(parent=Cameras_alignment) self.cam_s_o = pg.ImageView(parent=Cameras_Alignment) self.cam_s_o.adjustSize() self.cam_s_o.ui.histogram.hide() self.cam_s_o.ui.roiBtn.hide() self.cam_s_o.ui.menuBtn.hide() self.cam_s_o.setObjectName("cam_s_o") ### sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.cam_s_o.sizePolicy().hasHeightForWidth()) self.cam_s_o.setSizePolicy(sizePolicy) self.cam_s_o.setMinimumSize(QtCore.QSize(250, 250)) self.cam_s_o.setStyleSheet( "QWidget{\n" " border: 2px solid gray;\n" " }\n" " " ) # self.cam_s_o.setText("") self.cam_s_o.setObjectName("cam_s_o") self.gridLayout.addWidget(self.cam_s_o, 3, 0, 1, 4) self.label_203 = QtWidgets.QLabel(parent=Cameras_Alignment) font = QtGui.QFont() font.setBold(True) self.label_203.setFont(font) self.label_203.setObjectName("label_203") self.gridLayout.addWidget(self.label_203, 2, 0, 1, 1) ### # self.cam_b_o = QtWidgets.QLabel(parent=Cameras_alignment) self.cam_b_o = pg.ImageView(parent=Cameras_Alignment) self.cam_b_o.adjustSize() self.cam_b_o.ui.histogram.hide() self.cam_b_o.ui.roiBtn.hide() self.cam_b_o.ui.menuBtn.hide() self.cam_b_o.setObjectName("cam_s_o") ### sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.cam_b_o.sizePolicy().hasHeightForWidth()) self.cam_b_o.setSizePolicy(sizePolicy) self.cam_b_o.setMinimumSize(QtCore.QSize(250, 250)) self.cam_b_o.setMaximumSize(QtCore.QSize(16777215, 16777215)) self.cam_b_o.setStyleSheet( "QWidget{\n" " border: 2px solid gray;\n" " }\n" " " ) # self.cam_b_o.setText("") self.cam_b_o.setObjectName("cam_b_o") self.gridLayout.addWidget(self.cam_b_o, 6, 0, 1, 4) self.label_205 = QtWidgets.QLabel(parent=Cameras_Alignment) font = QtGui.QFont() font.setPointSize(12) font.setBold(True) self.label_205.setFont(font) self.label_205.setObjectName("label_205") self.gridLayout.addWidget(self.label_205, 4, 0, 1, 1) self.label_202 = QtWidgets.QLabel(parent=Cameras_Alignment) font = QtGui.QFont() font.setPointSize(12) font.setBold(True) self.label_202.setFont(font) self.label_202.setObjectName("label_202") self.gridLayout.addWidget(self.label_202, 1, 0, 1, 1) self.verticalLayout.addLayout(self.gridLayout) self.gridLayout_3 = QtWidgets.QGridLayout() self.gridLayout_3.setObjectName("gridLayout_3") self.label_211 = QtWidgets.QLabel(parent=Cameras_Alignment) font = QtGui.QFont() font.setBold(True) self.label_211.setFont(font) self.label_211.setObjectName("label_211") self.gridLayout_3.addWidget(self.label_211, 2, 0, 1, 1) ### # self.cam_angle_o = QtWidgets.QLabel(parent=Cameras_alignment) self.cam_angle_o = pg.ImageView(parent=Cameras_Alignment) self.cam_angle_o.adjustSize() self.cam_angle_o.ui.histogram.hide() self.cam_angle_o.ui.roiBtn.hide() self.cam_angle_o.ui.menuBtn.hide() self.cam_angle_o.setObjectName("cam_s_o") ### sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.cam_angle_o.sizePolicy().hasHeightForWidth()) self.cam_angle_o.setSizePolicy(sizePolicy) self.cam_angle_o.setMinimumSize(QtCore.QSize(250, 250)) self.cam_angle_o.setMaximumSize(QtCore.QSize(16777215, 16777215)) self.cam_angle_o.setStyleSheet( "QWidget{\n" " border: 2px solid gray;\n" " }\n" " " ) # self.cam_angle_o.setText("") self.cam_angle_o.setObjectName("cam_angle_o") self.gridLayout_3.addWidget(self.cam_angle_o, 3, 0, 1, 1) ### # self.cam_angle_d = QtWidgets.QLabel(parent=Cameras_alignment) self.cam_angle_d = pg.ImageView(parent=Cameras_Alignment) self.cam_angle_d.adjustSize() self.cam_angle_d.ui.histogram.hide() self.cam_angle_d.ui.roiBtn.hide() self.cam_angle_d.ui.menuBtn.hide() self.cam_angle_d.setObjectName("cam_s_o") ### sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(2) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.cam_angle_d.sizePolicy().hasHeightForWidth()) self.cam_angle_d.setSizePolicy(sizePolicy) self.cam_angle_d.setMinimumSize(QtCore.QSize(600, 250)) self.cam_angle_d.setMaximumSize(QtCore.QSize(16777215, 16777215)) self.cam_angle_d.setStyleSheet( "QWidget{\n" " border: 2px solid gray;\n" " }\n" " " ) # self.cam_angle_d.setText("") self.cam_angle_d.setObjectName("cam_angle_d") self.gridLayout_3.addWidget(self.cam_angle_d, 3, 1, 1, 1) self.label_210 = QtWidgets.QLabel(parent=Cameras_Alignment) font = QtGui.QFont() font.setBold(True) self.label_210.setFont(font) self.label_210.setObjectName("label_210") self.gridLayout_3.addWidget(self.label_210, 2, 1, 1, 1) self.label_206 = QtWidgets.QLabel(parent=Cameras_Alignment) font = QtGui.QFont() font.setPointSize(12) font.setBold(True) self.label_206.setFont(font) self.label_206.setObjectName("label_206") self.gridLayout_3.addWidget(self.label_206, 1, 0, 1, 1) self.verticalLayout.addLayout(self.gridLayout_3) self.gridLayout_4.addLayout(self.verticalLayout, 0, 0, 1, 1) self.verticalLayout_2 = QtWidgets.QVBoxLayout() self.verticalLayout_2.setObjectName("verticalLayout_2") self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") # LED indicator that sits directly to the left of the # auto-exposure button. Green = auto on, red = manual. Mirrors # the light button / led_light pair just to the right. self.led_auto_exposure = QtWidgets.QLabel(parent=Cameras_Alignment) self.led_auto_exposure.setMaximumSize(QtCore.QSize(50, 50)) self.led_auto_exposure.setObjectName("led_auto_exposure") self.horizontalLayout.addWidget(self.led_auto_exposure) self.auto_exposure_time = QtWidgets.QPushButton(parent=Cameras_Alignment) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.auto_exposure_time.sizePolicy().hasHeightForWidth()) self.auto_exposure_time.setSizePolicy(sizePolicy) self.auto_exposure_time.setMinimumSize(QtCore.QSize(0, 25)) self.auto_exposure_time.setStyleSheet( "QPushButton{\n" " background: rgb(193, 193, 193)\n" " }\n" " " ) self.auto_exposure_time.setObjectName("auto_exposure_time") self.horizontalLayout.addWidget(self.auto_exposure_time) spacerItem = QtWidgets.QSpacerItem( 40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum ) self.horizontalLayout.addItem(spacerItem) self.led_light = QtWidgets.QLabel(parent=Cameras_Alignment) self.led_light.setMaximumSize(QtCore.QSize(50, 50)) self.led_light.setObjectName("led_light") self.horizontalLayout.addWidget(self.led_light) self.light = QtWidgets.QPushButton(parent=Cameras_Alignment) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.light.sizePolicy().hasHeightForWidth()) self.light.setSizePolicy(sizePolicy) self.light.setMinimumSize(QtCore.QSize(0, 25)) self.light.setStyleSheet( "QPushButton{\n" " background: rgb(193, 193, 193)\n" " }\n" " " ) self.light.setObjectName("light") self.horizontalLayout.addWidget(self.light) spacerItem1 = QtWidgets.QSpacerItem( 40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum ) self.horizontalLayout.addItem(spacerItem1) self.verticalLayout_2.addLayout(self.horizontalLayout) self.gridLayout_2 = QtWidgets.QGridLayout() self.gridLayout_2.setObjectName("gridLayout_2") self.led_light_2 = QtWidgets.QLabel(parent=Cameras_Alignment) self.led_light_2.setMinimumSize(QtCore.QSize(130, 0)) self.led_light_2.setMaximumSize(QtCore.QSize(500, 50)) self.led_light_2.setObjectName("led_light_2") self.gridLayout_2.addWidget(self.led_light_2, 1, 0, 1, 1) self.exposure_time_cam_1 = QtWidgets.QLineEdit(parent=Cameras_Alignment) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.exposure_time_cam_1.sizePolicy().hasHeightForWidth()) self.exposure_time_cam_1.setSizePolicy(sizePolicy) self.exposure_time_cam_1.setMinimumSize(QtCore.QSize(0, 20)) self.exposure_time_cam_1.setStyleSheet( "QLineEdit{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.exposure_time_cam_1.setObjectName("exposure_time_cam_1") self.gridLayout_2.addWidget(self.exposure_time_cam_1, 1, 1, 1, 1) spacerItem2 = QtWidgets.QSpacerItem( 20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding ) self.gridLayout_2.addItem(spacerItem2, 4, 1, 1, 1) self.exposure_time_cam_2 = QtWidgets.QLineEdit(parent=Cameras_Alignment) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.exposure_time_cam_2.sizePolicy().hasHeightForWidth()) self.exposure_time_cam_2.setSizePolicy(sizePolicy) self.exposure_time_cam_2.setMinimumSize(QtCore.QSize(0, 20)) self.exposure_time_cam_2.setStyleSheet( "QLineEdit{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.exposure_time_cam_2.setObjectName("exposure_time_cam_2") self.gridLayout_2.addWidget(self.exposure_time_cam_2, 2, 1, 1, 1) self.exposure_time_cam_3 = QtWidgets.QLineEdit(parent=Cameras_Alignment) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.exposure_time_cam_3.sizePolicy().hasHeightForWidth()) self.exposure_time_cam_3.setSizePolicy(sizePolicy) self.exposure_time_cam_3.setMinimumSize(QtCore.QSize(0, 20)) self.exposure_time_cam_3.setStyleSheet( "QLineEdit{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.exposure_time_cam_3.setObjectName("exposure_time_cam_3") self.gridLayout_2.addWidget(self.exposure_time_cam_3, 3, 1, 1, 1) self.led_light_3 = QtWidgets.QLabel(parent=Cameras_Alignment) self.led_light_3.setMinimumSize(QtCore.QSize(130, 0)) self.led_light_3.setMaximumSize(QtCore.QSize(500, 50)) self.led_light_3.setObjectName("led_light_3") self.gridLayout_2.addWidget(self.led_light_3, 2, 0, 1, 1) self.default_exposure_time = QtWidgets.QPushButton(parent=Cameras_Alignment) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.default_exposure_time.sizePolicy().hasHeightForWidth()) self.default_exposure_time.setSizePolicy(sizePolicy) self.default_exposure_time.setMinimumSize(QtCore.QSize(0, 25)) self.default_exposure_time.setStyleSheet( "QPushButton{\n" " background: rgb(193, 193, 193)\n" " }\n" " " ) self.default_exposure_time.setObjectName("default_exposure_time") self.gridLayout_2.addWidget(self.default_exposure_time, 0, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter) self.led_light_4 = QtWidgets.QLabel(parent=Cameras_Alignment) self.led_light_4.setMinimumSize(QtCore.QSize(130, 0)) self.led_light_4.setMaximumSize(QtCore.QSize(500, 50)) self.led_light_4.setObjectName("led_light_4") self.gridLayout_2.addWidget(self.led_light_4, 3, 0, 1, 1) self.verticalLayout_2.addLayout(self.gridLayout_2) # ----- Camera list + connect/disconnect panel --------------------- # Compact box that lives right under the exposure-time controls # (inside verticalLayout_2) so it stacks under the "Exposure Time # Angle" row instead of widening the whole window. One row per # detected Basler camera; refreshed every 1.5 s. self.camera_list_box = QtWidgets.QGroupBox("Cameras detected", parent=Cameras_Alignment) self.camera_list_layout = QtWidgets.QVBoxLayout(self.camera_list_box) self.camera_list_layout.setContentsMargins(6, 6, 6, 6) self.camera_list_layout.setSpacing(2) self._camera_row_widgets = {} # serial -> dict(widget, label, connect_btn, disconnect_btn) self.camera_list_empty_label = QtWidgets.QLabel("(scanning …)", parent=self.camera_list_box) self.camera_list_empty_label.setStyleSheet("color: gray;") self.camera_list_layout.addWidget(self.camera_list_empty_label) self.camera_list_layout.addStretch(1) self.verticalLayout_2.addWidget(self.camera_list_box) self.gridLayout_4.addLayout(self.verticalLayout_2, 0, 1, 1, 1) self.gridLayout_5.addLayout(self.gridLayout_4, 0, 0, 1, 1) # ----- Bottom status banner -------------------------------------- # Mirrors the Error / status label used elsewhere in the GUI suite # so the user sees connection / grab failures without watching the # terminal. self.camera_status_label = QtWidgets.QLabel(parent=Cameras_Alignment) self.camera_status_label.setMinimumHeight(28) self.camera_status_label.setWordWrap(True) self.camera_status_label.setStyleSheet( "QLabel{ color: rgb(140,0,0); padding: 4px; border: 1px solid rgb(200,200,200); border-radius: 4px; }" ) self.camera_status_label.setText("") self.gridLayout_5.addWidget(self.camera_status_label, 1, 0, 1, 1) self.retranslateUi(Cameras_Alignment) QtCore.QMetaObject.connectSlotsByName(Cameras_Alignment) tooltips.apply_tooltips(self, tooltips.CAMERAS_TOOLTIPS) Cameras_Alignment.setTabOrder(self.auto_exposure_time, self.light) Cameras_Alignment.setTabOrder(self.light, self.default_exposure_time) Cameras_Alignment.setTabOrder(self.default_exposure_time, self.exposure_time_cam_1) Cameras_Alignment.setTabOrder(self.exposure_time_cam_1, self.exposure_time_cam_2) Cameras_Alignment.setTabOrder(self.exposure_time_cam_2, self.exposure_time_cam_3) ### # Create a ROI (Region of Interest) item self.roi_s = pg.ROI([1000, 1000], [500, 200], movable=True, resizable=True) self.roi_s.addScaleHandle([1, 1], [0, 0]) # Adding a scaling handle to the ROI self.roi_s.addScaleHandle([0, 0], [1, 1]) self.roi_s.addScaleHandle([1, 0], [0, 1]) self.roi_s.addScaleHandle([0, 1], [1, 0]) self.roi_s.setZValue(10) # Make sure ROI is drawn on top self.cam_s_o.addItem(self.roi_s) self.roi_b = pg.ROI([1000, 1000], [1000, 400], movable=True, resizable=True) self.roi_b.addScaleHandle([1, 1], [0, 0]) # Adding a scaling handle to the ROI self.roi_b.addScaleHandle([0, 0], [1, 1]) self.roi_b.addScaleHandle([1, 0], [0, 1]) self.roi_b.addScaleHandle([0, 1], [1, 0]) self.roi_b.setZValue(10) # Make sure ROI is drawn on top self.cam_b_o.addItem(self.roi_b) self.roi_angle = pg.ROI([1000, 1000], [1000, 400], movable=True, resizable=True) self.roi_angle.addScaleHandle([1, 1], [0, 0]) # Adding a scaling handle to the ROI self.roi_angle.addScaleHandle([0, 0], [1, 1]) self.roi_angle.addScaleHandle([1, 0], [0, 1]) self.roi_angle.addScaleHandle([0, 1], [1, 0]) self.roi_angle.setZValue(10) # Make sure ROI is drawn on top self.cam_angle_o.addItem(self.roi_angle) # Diagram and LEDs ############## self.led_red = QPixmap('./files/led-red-on.png') self.led_green = QPixmap('./files/green-led-on.png') self.led_light.setPixmap(self.led_red) # Auto-exposure starts in 'Continuous' (see __init__), so light # the LED green to match. self.led_auto_exposure.setPixmap(self.led_green) # bottom camera (x, y) # arrow1 = pg.ArrowItem(pos=(925, 770), angle=0) # self.cam_b_o.addItem(arrow1) # Side camera (x, y) main arrow for puck exchange (Blue arrow) arrow1 = pg.ArrowItem(pos=(920, 830), angle=-90, brush='r') arrow3 = pg.ArrowItem(pos=(940, 600), angle=0, brush='g') arrow2 = pg.ArrowItem(pos=(795, 850), angle=-90, brush='b') self.cam_s_o.addItem(arrow1) self.cam_s_o.addItem(arrow2) self.cam_s_o.addItem(arrow3) # side camera zoom (x, y) zoom arrow # arrow1 = pg.ArrowItem(pos=(380, 115), angle=90, brush='r') # self.cam_s_d.addItem(arrow1) # bottom camera zoom (x, y) zoom arrow # arrow1 = pg.ArrowItem(pos=(620, 265), angle=90, brush='r') # self.cam_b_d.addItem(arrow1) ### self.light.clicked.connect(self.light_switch) self.auto_exposure_time.clicked.connect(self.auto_exposure_time_switch) self.default_exposure_time.clicked.connect(self.default_exposure_time_switch) self.emitter.img0_orig.connect(self.update_cam_s_o) self.emitter.img1_orig.connect(self.update_cam_b_o) self.emitter.img2_orig.connect(self.update_cam_angle_o) self.initialize_camera_thread() if self.conf['usb_lamp_switch'] == 'on': self.usb_lamp_switch = usb_switch.USBSwitch("./control/usb_switch/USBaccessX64.dll") # 32 bit w/o X64 self.exposure_time_cam_1.editingFinished.connect(self.update_exposure_time) self.exposure_time_cam_2.editingFinished.connect(self.update_exposure_time) self.exposure_time_cam_3.editingFinished.connect(self.update_exposure_time) self.original_button_style = self.auto_exposure_time.styleSheet() # Manual exposure controls only make sense when the camera is # not running its own auto-exposure loop, so the "Default # Exposure Time" button and the three per-camera µs fields are # disabled while auto-exposure is on. Cameras start in auto, so # initialise these as disabled. self._set_manual_exposure_widgets_enabled(not self.auto_exposure_time_flag) self.emitter.cams_exposure_time_default.connect(self.set_default_exposure_time) # switch off the light if it is one before opening the window self.usb_lamp_switch.switch_off(16)
[docs] def retranslateUi(self, Cameras_Alignment): """ Args: Cameras_alignment: Returns: None """ _translate = QtCore.QCoreApplication.translate ### # Cameras_alignment.setWindowTitle(_translate("Cameras_alignment", "Form")) Cameras_Alignment.setWindowTitle(_translate("Cameras_alignment", "PyCCAPT Cameras")) Cameras_Alignment.setWindowIcon(QtGui.QIcon('./files/logo.png')) self.Cameras_Alignment = Cameras_Alignment ### self.label_208.setText(_translate("Cameras_Alignment", "Overview")) self.label_209.setText(_translate("Cameras_Alignment", "Detail")) self.label_204.setText(_translate("Cameras_Alignment", "Detail")) self.label_203.setText(_translate("Cameras_Alignment", "Overview")) self.label_205.setText(_translate("Cameras_Alignment", "Camera Top")) self.label_202.setText(_translate("Cameras_Alignment", "Camera Side")) self.label_211.setText(_translate("Cameras_Alignment", "Overview")) self.label_210.setText(_translate("Cameras_Alignment", "Detail")) self.label_206.setText(_translate("Cameras_Alignment", "Camera Angle")) self.auto_exposure_time.setText(_translate("Cameras_Alignment", "Auto Exposure Time")) self.led_light.setText(_translate("Cameras_Alignment", "Light")) self.light.setText(_translate("Cameras_Alignment", "Light")) self.led_light_2.setText(_translate("Cameras_Alignment", "Exposure Time Side (us)")) self.exposure_time_cam_1.setText(_translate("Cameras_Alignment", "2000000")) self.exposure_time_cam_2.setText(_translate("Cameras_Alignment", "2000000")) self.exposure_time_cam_3.setText(_translate("Cameras_Alignment", "2000000")) self.led_light_3.setText(_translate("Cameras_Alignment", "Exposure Time Top (us)")) self.default_exposure_time.setText(_translate("Cameras_Alignment", "Default Exposure Time")) self.led_light_4.setText(_translate("Cameras_Alignment", "Exposure Time Angle (us)")) ### self.timer = QtCore.QTimer() self.timer.timeout.connect(self.cameras_screenshot) self.timer.start(2000) # Check every 2000 milliseconds (1 second) # Periodic refresh of the camera list / status banner. self.camera_list_timer = QtCore.QTimer() self.camera_list_timer.timeout.connect(self._refresh_camera_panel) self.camera_list_timer.start(1500) # Run once immediately so the panel is populated before the # first 1.5s tick. QtCore.QTimer.singleShot(200, self._refresh_camera_panel)
[docs] def set_default_exposure_time(self, exposure_time_default): """ Set the default exposure time Args: exposure_time_default: Default exposure time Return: None """ self.exposure_time_cam_1.setText(str(exposure_time_default[0])) self.exposure_time_cam_2.setText(str(exposure_time_default[1])) self.exposure_time_cam_3.setText(str(exposure_time_default[2]))
[docs] def update_exposure_time(self): """ Update the exposure time of the cameras Args: None Return: None """ try: if self.exposure_time_cam_1.text() != '': self.emitter.cam_1_exposure_time.emit(int(self.exposure_time_cam_1.text())) if self.exposure_time_cam_2.text() != '': self.emitter.cam_2_exposure_time.emit(int(self.exposure_time_cam_2.text())) if self.exposure_time_cam_3.text() != '': self.emitter.cam_3_exposure_time.emit(int(self.exposure_time_cam_3.text())) except Exception as e: print(e) print('type the exposure time in microseconds')
[docs] def update_cam_s_o(self, img): # autoLevels=True stretches the per-frame intensity range to fill # the display, which is the most visible quality improvement for # alignment: dark "light off" frames stop looking nearly black # and bright "light on" frames stop saturating. self.cam_s_o.setImage(img, autoRange=False, autoLevels=True) roi_coords = self.roi_s.getArraySlice(img, self.cam_s_o.imageItem, axes=(0, 1)) # Extract the region and update the second image view if roi_coords is not None: region = img[roi_coords[0][0], roi_coords[0][1]] self.cam_s_d.setImage(region, autoRange=False, autoLevels=True)
[docs] def update_cam_b_o(self, img): self.cam_b_o.setImage(img, autoRange=False, autoLevels=True) roi_coords = self.roi_b.getArraySlice(img, self.cam_b_o.imageItem, axes=(0, 1)) # Extract the region and update the second image view if roi_coords is not None: region = img[roi_coords[0][0], roi_coords[0][1]] self.cam_b_d.setImage(region, autoRange=False, autoLevels=True)
[docs] def update_cam_angle_o(self, img): self.cam_angle_o.setImage(img, autoRange=False, autoLevels=True) roi_coords = self.roi_angle.getArraySlice(img, self.cam_angle_o.imageItem, axes=(0, 1)) # Extract the region and update the second image view if roi_coords is not None: region = img[roi_coords[0][0], roi_coords[0][1]] self.cam_angle_d.setImage(region, autoRange=False, autoLevels=True)
[docs] def light_switch(self): """ light switch function Args: None Return: None """ if not self.variables.light: self.led_light.setPixmap(self.led_green) if self.conf['usb_lamp_switch'] == 'on': self.usb_lamp_switch.switch_on(16) self.variables.light = True self.variables.light_switch = True elif self.variables.light: self.led_light.setPixmap(self.led_red) if self.conf['usb_lamp_switch'] == 'on': self.usb_lamp_switch.switch_off(16) self.variables.light = False self.variables.light_switch = True
[docs] def auto_exposure_time_switch(self): """ Auto exposure time switch function Args: None Return: None """ self.auto_exposure_time_flag = not self.auto_exposure_time_flag # LED mirrors the auto-exposure state: green = auto on, red = manual. if self.auto_exposure_time_flag: self.led_auto_exposure.setPixmap(self.led_green) else: self.led_auto_exposure.setPixmap(self.led_red) # Manual fields are only meaningful in manual mode. self._set_manual_exposure_widgets_enabled(not self.auto_exposure_time_flag) self.emitter.auto_exposure_time.emit(True)
def _set_manual_exposure_widgets_enabled(self, enabled): """Enable/disable the manual exposure inputs as a group. Disabled in auto mode because the camera firmware owns ExposureTime there — typing into the µs fields or hitting "Default Exposure Time" would have no effect and only confuse the user. """ for widget in ( self.default_exposure_time, self.exposure_time_cam_1, self.exposure_time_cam_2, self.exposure_time_cam_3, ): widget.setEnabled(enabled)
[docs] def default_exposure_time_switch(self): """ Default exposure time switch function Args: None Return: None """ self.emitter.default_exposure_time.emit(True)
[docs] def initialize_camera_thread(self): """ Initialize camera thread Args: None Return: None """ if self.conf['camera'] == "off": print('The cameras is off') return # Create a camera worker. The worker keeps running even when zero # cameras are currently connected so that hot-plugging a camera # later automatically populates the views — the GUI window itself # stays open in either case. self.camera_worker = camera.CameraWorker(variables=self.variables, emitter=self.emitter) if self.camera_worker.camera_status_message: print(self.camera_worker.camera_status_message) if not self.camera_worker.camera_available: # pypylon failed to load entirely — no point spinning up a thread. self.variables.flag_camera_grab = False return self.camera_thread = QThread() self.camera_worker.moveToThread(self.camera_thread) self.camera_thread.started.connect(self.camera_worker.start_capturing) self.camera_worker.finished.connect(self.camera_thread.quit) self.camera_worker.finished.connect(self.camera_worker.deleteLater) self.camera_thread.finished.connect(self.camera_thread.deleteLater) self.camera_thread.start() self.variables.flag_camera_grab = True
[docs] def stop(self): """ Stop the timer and any other background processes, timers, or threads here Args: None Return: None """ # Add any additional cleanup code here # with self.variables.lock_setup_parameters: self.variables.flag_camera_grab = False if hasattr(self, 'camera_thread'): self.camera_thread.wait()
[docs] def cameras_screenshot(self): if self.variables.flag_cameras_take_screenshot: screenshot = QtWidgets.QApplication.primaryScreen().grabWindow(self.Cameras_Alignment.winId()) screenshot.save(str(Path(self.variables.path_meta) / "cameras_screenshot.png"), 'png')
# -------------------------------------------------------------- list ui def _refresh_camera_panel(self): """Sync the camera-list rows and status banner with the worker.""" worker = getattr(self, 'camera_worker', None) if worker is None: return # Status banner status = getattr(worker, 'latest_status', '') or "" if status != self.camera_status_label.text(): self.camera_status_label.setText(status) # Camera list try: cams = worker.list_cameras() except Exception as e: cams = [] print(f"camera list refresh failed: {e}") current_serials = {c['serial'] for c in cams} # Remove rows for cameras that are no longer detected. for sn in list(self._camera_row_widgets): if sn not in current_serials: row = self._camera_row_widgets.pop(sn) row['widget'].deleteLater() if not cams: self.camera_list_empty_label.setText("(no Basler cameras detected)") self.camera_list_empty_label.show() return self.camera_list_empty_label.hide() for cam in cams: sn = cam['serial'] if sn in self._camera_row_widgets: self._update_camera_row(self._camera_row_widgets[sn], cam) else: self._camera_row_widgets[sn] = self._make_camera_row(cam) def _make_camera_row(self, cam): row = QtWidgets.QWidget(parent=self.camera_list_box) layout = QtWidgets.QHBoxLayout(row) layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(6) label = QtWidgets.QLabel(parent=row) label.setMinimumWidth(220) layout.addWidget(label, 1) connect_btn = QtWidgets.QPushButton("Connect", parent=row) disconnect_btn = QtWidgets.QPushButton("Disconnect", parent=row) layout.addWidget(connect_btn) layout.addWidget(disconnect_btn) # Insert above the trailing stretch. self.camera_list_layout.insertWidget(self.camera_list_layout.count() - 1, row) sn = cam['serial'] connect_btn.clicked.connect(lambda _checked=False, s=sn: self._on_connect_clicked(s)) disconnect_btn.clicked.connect(lambda _checked=False, s=sn: self._on_disconnect_clicked(s)) entry = { 'widget': row, 'label': label, 'connect_btn': connect_btn, 'disconnect_btn': disconnect_btn, } self._update_camera_row(entry, cam) return entry def _update_camera_row(self, entry, cam): sn = cam['serial'] model = cam['model'] or "Basler" if cam['user_disabled']: state = "disabled" color = "color: rgb(120,120,120);" elif cam['attached']: state = f"connected to slot {cam['slot']}" color = "color: rgb(0,120,0);" else: state = "detected (not connected)" color = "color: rgb(180,90,0);" entry['label'].setText(f"<b>{model}</b> &nbsp; {sn} &nbsp; — {state}") entry['label'].setStyleSheet(color) entry['connect_btn'].setEnabled(not cam['attached']) entry['disconnect_btn'].setEnabled(cam['attached'] or not cam['user_disabled']) def _on_connect_clicked(self, serial): worker = getattr(self, 'camera_worker', None) if worker is None: return try: worker.connect_serial(serial) except Exception as e: self.camera_status_label.setText(f"Connect failed: {e}") self._refresh_camera_panel() def _on_disconnect_clicked(self, serial): worker = getattr(self, 'camera_worker', None) if worker is None: return try: worker.disconnect_serial(serial) except Exception as e: self.camera_status_label.setText(f"Disconnect failed: {e}") self._refresh_camera_panel()
[docs] class SignalEmitter(QObject): img0_orig = pyqtSignal(np.ndarray) img1_orig = pyqtSignal(np.ndarray) img2_orig = pyqtSignal(np.ndarray) cam_1_exposure_time = pyqtSignal(int) cam_2_exposure_time = pyqtSignal(int) cam_3_exposure_time = pyqtSignal(int) cams_exposure_time_default = pyqtSignal(list) default_exposure_time = pyqtSignal(bool) auto_exposure_time = pyqtSignal(bool)
[docs] class CamerasAlignmentWindow(QtWidgets.QWidget): closed = QtCore.pyqtSignal() # Define a custom closed signal def __init__(self, variables, gui_cameras_alignment, close_event, command_queue, *args, **kwargs): """ Initialize the CamerasAlignmentWindow class. Args: gui_cameras_alignment: GUI cameras alignment instance. close_event: multiprocessing.Event signalled by this window when it is closed by the user. command_queue: multiprocessing.Queue of typed string commands sent from the main GUI ("show", "show_front", "hide"). """ super().__init__(*args, **kwargs) self.variables = variables self.gui_cameras_alignment = gui_cameras_alignment self.command_queue = command_queue self.close_event = close_event # Start hidden - check_if_should() below brings the window up the # first time a "show" command arrives on the queue. self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.check_if_should) self.timer.start(500)
[docs] def closeEvent(self, event): """ Don't actually close - hide the window so the subprocess stays alive and the next "open" from the main GUI is instant. Using hide() (not showMinimized) avoids leaving a leftover minimised stub in the taskbar / desktop. """ event.ignore() self.hide() self.close_event.set()
[docs] def check_if_should(self): """Drain the command queue and dispatch each message in order.""" raise_to_front = False make_visible = False hide = False while True: try: msg = self.command_queue.get_nowait() except Exception: break # queue empty if msg == "show": make_visible = True elif msg == "show_front": make_visible = True raise_to_front = True elif msg == "hide": hide = True if hide and not make_visible: self.hide() return if not make_visible: return # Always call show() + showNormal() unconditionally - after a # previous closeEvent->hide() a single show() call doesn't # always re-display the window on every platform, and # showNormal() additionally brings it out of a minimised state. # We deliberately do NOT toggle setWindowFlags() - that call # implicitly hides the widget (Qt docs). self.show() self.showNormal() self.raise_() if raise_to_front: self.activateWindow()
[docs] def setWindowStyleFusion(self): # Set the Fusion style QtWidgets.QApplication.setStyle("Fusion")
[docs] def run_camera_window(variables, conf, camera_closed_event, camera_command_queue): """ Run the Cameras window in a separate process. Args: camera_command_queue: multiprocessing.Queue of typed string commands from the main GUI ("show", "show_front", "hide"). """ # A startup crash in the subprocess otherwise dies silently - the # parent never sees it. Funnel any exception to a log file under # files/logs/ so the user can pick it up after the fact. import traceback try: app = QtWidgets.QApplication(sys.argv) app.setStyle('Fusion') # The window starts hidden and only appears when the main GUI signals - # don't let Qt quit the subprocess just because no window is visible. app.setQuitOnLastWindowClosed(False) SignalEmitter_Cameras = SignalEmitter() gui_cameras_alignment = Ui_Cameras_Alignment(variables, conf, SignalEmitter_Cameras) Cameras_alignment = CamerasAlignmentWindow( variables, gui_cameras_alignment, camera_closed_event, camera_command_queue, flags=QtCore.Qt.WindowType.Tool ) gui_cameras_alignment.setupUi(Cameras_alignment) sys.exit(app.exec()) except Exception: try: log_path = runtime.project_path("files", "logs", "camera_subprocess_crash.log") log_path.parent.mkdir(parents=True, exist_ok=True) with open(log_path, "a", encoding="utf-8") as fh: fh.write("=" * 60 + "\n") traceback.print_exc(file=fh) except Exception: pass traceback.print_exc() raise
if __name__ == "__main__": try: conf, _ = runtime.load_project_config() except Exception as exc: print('Can not load the configuration file') print(exc) sys.exit() shared = runtime.create_shared_context(conf) app = QtWidgets.QApplication(sys.argv) app.setStyle('Fusion') Cameras_Alignment = QtWidgets.QWidget() signal_emitter = SignalEmitter() ui = Ui_Cameras_Alignment(shared.variables, conf, signal_emitter) ui.setupUi(Cameras_Alignment) Cameras_Alignment.show() sys.exit(app.exec())