import cv2, numpy as np import os from Tbd.helper import CameraObject from UI.UILogger import UILogger from PySide6.QtCore import Qt, QTimer, QThread, Signal from PySide6.QtGui import QImage, QPixmap from PySide6.QtWidgets import QLineEdit from PySide6.QtGui import QIntValidator from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QPushButton, QHBoxLayout from typing import List class LoadCamerasWorker(QThread): """ Opens/verifies/closes each camera off the UI thread - camera.open() does MSMF format negotiation with retries+sleeps, which used to block the Qt event loop (window shows as "Not Responding") for the whole duration. """ cameraOpened = Signal(object) logMessage = Signal(str) def __init__(self, indexToAdd: List[int]): super().__init__() self.indexToAdd = indexToAdd def run(self): for index in self.indexToAdd: camera = CameraObject(capture=None, index=index) if not camera.open(): self.logMessage.emit(f"cam{index}: failed to open, skipping.") continue ret, frame = camera.capture.read() if ret: self.logMessage.emit(f"cam{index}: verified, frame shape={frame.shape}") else: self.logMessage.emit(f"cam{index}: opened but failed to read a frame.") camera.close() self.cameraOpened.emit(camera) class SetupPage(QWidget): """ Page that handles camera preview & cycling through cameras. """ def __init__(self, cameraList: List[CameraObject], on_cameraList_changed, logger : UILogger): super().__init__() self.on_cameraList_changed = on_cameraList_changed self.cameraList = cameraList self.logger = logger self.cameraObject = CameraObject( capture=None, index=None ) # --- UI --- self.setUpLayout = QVBoxLayout(self) self.setUpLayout.addLayout(self._make_Setup_Controls()) self.setUpLayout.addLayout(self._make_Setup_Options()) def _make_Setup_Options(self): setupOptions = QHBoxLayout() setupOptions2 = QVBoxLayout() self.cameraIndexLable = QLabel("Camera Index:") self.cameraIndex = QLineEdit("0") self.cameraIndex.setValidator(QIntValidator(0, 9999, self)) self.addCameraBTN = QPushButton("Add Camera") self.nextCameraBTN = QPushButton("Next Camera") self.previousCameraBTN = QPushButton("Previous Camera") setupOptions.addWidget(self.cameraIndexLable) setupOptions.addWidget(self.cameraIndex, 1) setupOptions.addWidget(self.addCameraBTN) setupOptions.addWidget(self.nextCameraBTN) setupOptions.addWidget(self.previousCameraBTN) self.nextCameraBTN.clicked.connect(self._nextIndex) self.previousCameraBTN.clicked.connect(self._previousIndex) #self.cameraIndex.editingFinished.connect(self._updateCamera) self.addCameraBTN.clicked.connect(self._addCamera) # --- self.camera_preview = QLabel("No camera") self.camera_preview.setAlignment(Qt.AlignCenter) self.camera_preview.setMinimumSize(640, 360) self.camera_preview.setStyleSheet("background: #222; color: #aaa;") self.timer = QTimer(self) self.timer.timeout.connect(self._updateCamera) setupOptions2.addLayout(setupOptions) setupOptions2.addWidget(self.camera_preview) return setupOptions2 def _addCamera(self): if any(camera.index == int(self.cameraIndex.text()) for camera in self.cameraList): print("Camera already added.") return self.cameraList.append(self.cameraObject) self._cameraList_changed() def load_setup(self): if (os.path.exists(self.setupFilePath.text())): self.logger.log("Path found") else: self.logger.log("Path not found") indexToAdd = [1, 2, 3, 4, 5, 6] #indexToAdd = [0, 3] # Cameras are verified one at a time and left closed afterwards - this # hardware can't sustain many concurrent capture graphs (confirmed via # both CAP_DSHOW and a raw DirectShow graph), so callers open a camera # right before they need it (CameraSetupWidget.Start, calibration capture) # and close it right after, instead of keeping all 6 open simultaneously. self.loadBTN.setEnabled(False) self._loadWorker = LoadCamerasWorker(indexToAdd) self._loadWorker.logMessage.connect(self.logger.log) self._loadWorker.cameraOpened.connect(self._onCameraLoaded) self._loadWorker.finished.connect(lambda: self.loadBTN.setEnabled(True)) self._loadWorker.start() def _onCameraLoaded(self, camera: CameraObject): self.cameraList.append(camera) self._cameraList_changed() def save_setup(self): print("ToDo") return True def _cameraList_changed(self): if self.on_cameraList_changed: self.on_cameraList_changed() def _updateCamera(self): if self.cameraObject.capture is None: return ret, frame = self.cameraObject.capture.read() if not ret: return frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = frame_rgb.shape bytes_per_line = ch * w qimg = QImage(frame_rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) self.camera_preview.setPixmap(QPixmap.fromImage(qimg)) def _nextIndex(self): cameraIndex = int(self.cameraIndex.text()) self.cameraIndex.setText(str(cameraIndex + 1)) if self.open_camera(): if not self.timer.isActive(): self.timer.start(30) self._updateCamera() def _previousIndex(self): cameraIndex = int(self.cameraIndex.text()) if((cameraIndex - 1) < 0): self.cameraIndex.setText("0") self.cameraIndex.setText(str(cameraIndex - 1)) if self.open_camera(): if not self.timer.isActive(): self.timer.start(30) self._updateCamera() def open_camera(self): index = int(self.cameraIndex.text()) self.release_camera() # Reuse the CameraObject from cameraList (if this index is already known) # instead of opening a second handle to the same physical device. existing = next((cam for cam in self.cameraList if cam.index == index), None) self.cameraObject = existing if existing is not None else CameraObject(capture=None, index=index) if not self.cameraObject.open(): self.camera_preview.setText(f"Failed to open camera {index}") return False self.camera_preview.setText("") return True def release_camera(self): if self.cameraObject.capture is not None: self.timer.stop() self.cameraObject.close() def closeEvent(self, event): self.release_camera() event.accept() def _make_Setup_Controls(self): # Create Setup Input setupFileLayout = QHBoxLayout() self.setupFilePathLable = QLabel("Setupfile path:") self.setupFilePath = QLineEdit() self.loadBTN = QPushButton("Load") self.saveBTN = QPushButton("Save") setupFileLayout.addWidget(self.setupFilePathLable) setupFileLayout.addWidget(self.setupFilePath, 1) setupFileLayout.addWidget(self.loadBTN) setupFileLayout.addWidget(self.saveBTN) self.loadBTN.clicked.connect(self.load_setup) self.saveBTN.clicked.connect(self.save_setup) return setupFileLayout