Files
scanengine-3/camera_test_app.py
T
2026-07-28 09:20:58 -05:00

253 lines
9.1 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
UC480 Camera Test Application
Simple PyQt6 GUI for testing and viewing the uC480/uEye camera.
"""
import sys
import logging
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QGroupBox, QLabel, QPushButton, QDoubleSpinBox, QSpinBox,
QStatusBar, QSizePolicy
)
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QPixmap, QImage
from hardware.uc480_camera import UC480Camera, CameraStreamThread
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
logger = logging.getLogger(__name__)
class CameraTestWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("UC480 Camera Test")
self.resize(900, 700)
self.camera: UC480Camera | None = None
self.stream_thread: CameraStreamThread | None = None
self._build_ui()
self._update_controls_enabled()
# ------------------------------------------------------------------
# UI construction
# ------------------------------------------------------------------
def _build_ui(self):
central = QWidget()
self.setCentralWidget(central)
root = QHBoxLayout(central)
root.setContentsMargins(8, 8, 8, 8)
# Left: video display
self.lbl_image = QLabel("No camera connected")
self.lbl_image.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.lbl_image.setMinimumSize(640, 480)
self.lbl_image.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.lbl_image.setStyleSheet("background: #111; color: #888; border: 1px solid #444;")
root.addWidget(self.lbl_image, stretch=1)
# Right: controls panel
panel = QWidget()
panel.setFixedWidth(230)
panel_layout = QVBoxLayout(panel)
panel_layout.setContentsMargins(0, 0, 0, 0)
root.addWidget(panel)
# Connection group
grp_conn = QGroupBox("Connection")
conn_layout = QVBoxLayout(grp_conn)
self.btn_connect = QPushButton("Connect")
self.btn_connect.clicked.connect(self._on_connect)
self.btn_disconnect = QPushButton("Disconnect")
self.btn_disconnect.clicked.connect(self._on_disconnect)
conn_layout.addWidget(self.btn_connect)
conn_layout.addWidget(self.btn_disconnect)
panel_layout.addWidget(grp_conn)
# Sensor info group
grp_info = QGroupBox("Sensor Info")
info_layout = QVBoxLayout(grp_info)
self.lbl_sensor_name = QLabel("Name: —")
self.lbl_resolution = QLabel("Resolution: —")
self.lbl_pixel_size = QLabel("Pixel size: —")
for lbl in (self.lbl_sensor_name, self.lbl_resolution, self.lbl_pixel_size):
lbl.setWordWrap(True)
info_layout.addWidget(lbl)
panel_layout.addWidget(grp_info)
# Exposure group
grp_exp = QGroupBox("Exposure (ms)")
exp_layout = QHBoxLayout(grp_exp)
self.spin_exposure = QDoubleSpinBox()
self.spin_exposure.setRange(0.01, 10000.0)
self.spin_exposure.setDecimals(2)
self.spin_exposure.setSingleStep(1.0)
self.spin_exposure.setValue(10.0)
self.btn_set_exposure = QPushButton("Set")
self.btn_set_exposure.setFixedWidth(40)
self.btn_set_exposure.clicked.connect(self._on_set_exposure)
exp_layout.addWidget(self.spin_exposure)
exp_layout.addWidget(self.btn_set_exposure)
panel_layout.addWidget(grp_exp)
# Gain group
grp_gain = QGroupBox("Master Gain (0–100)")
gain_layout = QHBoxLayout(grp_gain)
self.spin_gain = QSpinBox()
self.spin_gain.setRange(0, 100)
self.spin_gain.setValue(0)
self.btn_set_gain = QPushButton("Set")
self.btn_set_gain.setFixedWidth(40)
self.btn_set_gain.clicked.connect(self._on_set_gain)
gain_layout.addWidget(self.spin_gain)
gain_layout.addWidget(self.btn_set_gain)
panel_layout.addWidget(grp_gain)
# Stream control group
grp_stream = QGroupBox("Stream")
stream_layout = QVBoxLayout(grp_stream)
self.btn_start_stream = QPushButton("Start Stream")
self.btn_start_stream.clicked.connect(self._on_start_stream)
self.btn_stop_stream = QPushButton("Stop Stream")
self.btn_stop_stream.clicked.connect(self._on_stop_stream)
stream_layout.addWidget(self.btn_start_stream)
stream_layout.addWidget(self.btn_stop_stream)
panel_layout.addWidget(grp_stream)
panel_layout.addStretch()
# Status bar
self.statusBar().showMessage("Not connected")
# ------------------------------------------------------------------
# Button handlers
# ------------------------------------------------------------------
def _on_connect(self):
if self.camera is not None:
self.statusBar().showMessage("Already connected")
return
self.camera = UC480Camera(camera_id=1)
self.camera.error_occurred.connect(self._on_camera_error)
if not self.camera.initialize():
self.statusBar().showMessage("Failed to initialize camera")
self.camera = None
return
info = self.camera.get_sensor_info()
self.lbl_sensor_name.setText(f"Name: {info.get('sensor_name', '?')}")
self.lbl_resolution.setText(
f"Resolution: {info.get('max_width', '?')}×{info.get('max_height', '?')}"
)
self.lbl_pixel_size.setText(f"Pixel size: {info.get('pixel_size', '?')} µm")
self.statusBar().showMessage("Camera connected")
self._update_controls_enabled()
def _on_disconnect(self):
self._on_stop_stream()
if self.camera is not None:
self.camera.cleanup()
self.camera = None
self.lbl_image.setText("No camera connected")
self.lbl_sensor_name.setText("Name: —")
self.lbl_resolution.setText("Resolution: —")
self.lbl_pixel_size.setText("Pixel size: —")
self.statusBar().showMessage("Disconnected")
self._update_controls_enabled()
def _on_set_exposure(self):
if self.camera is None:
return
val = self.spin_exposure.value()
if self.camera.set_exposure(val):
actual = self.camera.get_exposure()
shown = f"{actual:.2f}" if actual is not None else f"{val:.2f}"
self.statusBar().showMessage(f"Exposure set to {shown} ms")
else:
self.statusBar().showMessage("Failed to set exposure")
def _on_set_gain(self):
if self.camera is None:
return
val = self.spin_gain.value()
if self.camera.set_gain(val):
self.statusBar().showMessage(f"Gain set to {val}")
else:
self.statusBar().showMessage("Failed to set gain")
def _on_start_stream(self):
if self.camera is None or self.stream_thread is not None:
return
self.stream_thread = CameraStreamThread(self.camera)
self.stream_thread.frame_ready.connect(self._on_frame)
self.stream_thread.error_occurred.connect(self._on_camera_error)
self.stream_thread.start()
self.statusBar().showMessage("Streaming…")
self._update_controls_enabled()
def _on_stop_stream(self):
if self.stream_thread is not None:
self.stream_thread.stop()
self.stream_thread = None
self.statusBar().showMessage("Stream stopped")
self._update_controls_enabled()
# ------------------------------------------------------------------
# Slots
# ------------------------------------------------------------------
def _on_frame(self, image: QImage):
scaled = image.scaled(
self.lbl_image.width(),
self.lbl_image.height(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.FastTransformation,
)
self.lbl_image.setPixmap(QPixmap.fromImage(scaled))
def _on_camera_error(self, msg: str):
self.statusBar().showMessage(f"Error: {msg}")
logger.error(msg)
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _update_controls_enabled(self):
connected = self.camera is not None
streaming = self.stream_thread is not None
self.btn_connect.setEnabled(not connected)
self.btn_disconnect.setEnabled(connected)
self.btn_set_exposure.setEnabled(connected)
self.spin_exposure.setEnabled(connected)
self.btn_set_gain.setEnabled(connected)
self.spin_gain.setEnabled(connected)
self.btn_start_stream.setEnabled(connected and not streaming)
self.btn_stop_stream.setEnabled(streaming)
def closeEvent(self, event):
self._on_disconnect()
super().closeEvent(event)
# ---------------------------------------------------------------------------
def main():
app = QApplication(sys.argv)
window = CameraTestWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()