Files
Table-Python/source/UI/SetupPageWidget.py
T

212 lines
6.7 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
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 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]
# 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.
for index in indexToAdd:
camera = CameraObject(capture=None, index=index)
if not camera.open():
self.logger.log(f"cam{index}: failed to open, skipping.")
continue
ret, frame = camera.capture.read()
if ret:
self.logger.log(f"cam{index}: verified, frame shape={frame.shape}")
else:
self.logger.log(f"cam{index}: opened but failed to read a frame.")
camera.close()
self.cameraList.append(camera)
self._cameraList_changed()
return
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