feat: Add Camera Settings dialog for UVC controls and Qt parameters

- Implemented CameraSettingsDialog to manage UVC and Qt camera controls.
- Integrated UVC parameter sliders and auto controls for brightness, contrast, saturation, hue, sharpness, gamma, white balance, backlight compensation, and exposure.
- Added functionality to change white balance and exposure settings via Qt controls.
- Updated MainWindow to open CameraSettingsDialog and manage UVC controller lifecycle.
- Enhanced AppMenuBar to include a Camera Settings option.
- Created tests for UVC controller abstraction layer and parameter info.
- Documented camera specifications and supported features in new markdown files.
This commit is contained in:
2026-05-13 19:19:39 +02:00
parent d62416db4e
commit cdeac53555
12 changed files with 1756 additions and 118 deletions

View File

@@ -8,13 +8,17 @@ from pathlib import Path
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QLabel, QMainWindow, QSizePolicy, QStatusBar
from app.camera.camera_enumerator import CameraEnumerator, CameraInfo
from app.camera.camera_enumerator import CameraEnumerator, CameraFormat, CameraInfo
from app.camera.camera_service import CameraService
from app.camera.uvc import make_uvc_controller
from app.camera.uvc.base import UvcControllerBase
from app.camera.uvc.stub import NullUvcController
from app.config import APP_NAME, APP_VERSION
from app.overlay.telemetry_overlay import TelemetryOverlay
from app.pipeline.frame_dispatcher import FrameDispatcher
from app.telemetry.csv_logger import CsvTelemetryLogger
from app.telemetry.telemetry_collector import TelemetryCollector
from app.ui.camera_settings_dialog import CameraSettingsDialog
from app.ui.camera_view import CameraView
from app.ui.menu_bar import AppMenuBar
@@ -52,6 +56,9 @@ class MainWindow(QMainWindow):
self._dispatcher = FrameDispatcher(self)
self._telemetry = TelemetryCollector(parent=self)
# --- UVC controller (platform-specific, lazy-opened per camera) ---
self._uvc: UvcControllerBase = NullUvcController()
# --- CSV telemetry logger ---
self._csv_logger: CsvTelemetryLogger | None = None
if log_path is not None:
@@ -79,7 +86,7 @@ class MainWindow(QMainWindow):
# --- Status bar ---
self._status_bar = QStatusBar(self)
self.setStatusBar(self._status_bar)
self._status_label = QLabel("Initialising")
self._status_label = QLabel("Initialising\u2026")
self._status_bar.addWidget(self._status_label)
# --- Wire signals ---
@@ -113,6 +120,17 @@ class MainWindow(QMainWindow):
self._camera_service.start(cam)
self._menu.set_active_camera(cam)
self._status_label.setText(f"Opening: {cam.name}")
self._open_uvc(cam)
def _open_uvc(self, cam: CameraInfo) -> None:
"""Open or reopen the UVC controller for the given camera."""
if self._uvc.is_open():
self._uvc.close()
ctrl = make_uvc_controller(cam.name)
if not ctrl.is_open():
# factory may return a pre-opened controller or a NullUvcController
ctrl.open(cam.name)
self._uvc = ctrl
# ------------------------------------------------------------------
# Signal wiring
@@ -125,11 +143,13 @@ class MainWindow(QMainWindow):
# FrameDispatcher → CameraView (render) — drop if busy
self._dispatcher.subscribe(self._camera_view.on_frame, drop_if_busy=True)
# FrameDispatcher → TelemetryCollector — never drop, count every frame
# FrameDispatcher → TelemetryCollector — never drop
self._dispatcher.subscribe(self._telemetry.on_frame, drop_if_busy=False)
# TelemetryCollector → overlay
self._telemetry.metrics_updated.connect(self._telemetry_overlay.on_metrics_updated)
self._telemetry.metrics_updated.connect(
self._telemetry_overlay.on_metrics_updated
)
# TelemetryCollector → CSV logger (throttled internally)
if self._csv_logger is not None:
@@ -145,10 +165,10 @@ class MainWindow(QMainWindow):
# Menu signals
self._menu.camera_selected.connect(self._on_camera_selected)
self._menu.resolution_selected.connect(self._on_resolution_selected)
self._menu.fps_selected.connect(self._on_fps_selected)
self._menu.format_selected.connect(self._on_format_selected)
self._menu.reconnect_requested.connect(self._camera_service.reconnect)
self._menu.overlay_toggled.connect(self._camera_view.set_all_overlays_visible)
self._menu.camera_settings_requested.connect(self._on_settings_requested)
# ------------------------------------------------------------------
# Camera status slots
@@ -174,11 +194,20 @@ class MainWindow(QMainWindow):
def _on_camera_selected(self, cam: CameraInfo) -> None:
self._start_camera(cam)
def _on_resolution_selected(self, width: int, height: int) -> None:
self._camera_service.set_resolution(width, height)
def _on_format_selected(self, fmt: CameraFormat) -> None:
logger.info(
"Format selected via menu: %dx%d @ %.4g fps (%s)",
fmt.width, fmt.height, fmt.max_fps, fmt.pixel_format,
)
self._camera_service.set_format(fmt)
def _on_fps_selected(self, fps: float) -> None:
self._camera_service.set_fps(fps)
def _on_settings_requested(self) -> None:
qt_cam = self._camera_service.qt_camera
if qt_cam is None:
logger.warning("Settings requested but no camera is active")
return
dlg = CameraSettingsDialog(qt_cam, self._uvc, parent=self)
dlg.exec()
# ------------------------------------------------------------------
# Qt overrides
@@ -186,6 +215,8 @@ class MainWindow(QMainWindow):
def closeEvent(self, event) -> None: # noqa: N802
self._camera_service.stop()
if self._uvc.is_open():
self._uvc.close()
if self._csv_logger is not None:
logger.info(
"CSV telemetry: %d rows written", self._csv_logger.rows_written