232 lines
7.6 KiB
Python
232 lines
7.6 KiB
Python
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
|