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

@@ -1,4 +1,4 @@
"""Menu bar — camera, video format, FPS and debug controls."""
"""Menu bar — camera, video format and debug controls."""
from __future__ import annotations
@@ -8,7 +8,7 @@ from PySide6.QtCore import Signal
from PySide6.QtGui import QAction, QActionGroup
from PySide6.QtWidgets import QMenuBar, QWidget
from app.camera.camera_enumerator import CameraInfo
from app.camera.camera_enumerator import CameraFormat, CameraInfo
from app.logging_setup import set_console_level
logger = logging.getLogger(__name__)
@@ -19,27 +19,26 @@ class AppMenuBar(QMenuBar):
Application menu bar.
Signals:
camera_selected(CameraInfo) — user picked a camera
resolution_selected(int, int) — user picked (width, height)
fps_selected(float) — user picked a target FPS
reconnect_requested() — user hit Reconnect
overlay_toggled(bool) — overlay show/hide
log_toggled(bool) — console logging on/off
camera_selected(CameraInfo) — user picked a camera
format_selected(CameraFormat) — user picked a full format (res+fps+pixel)
reconnect_requested() — user hit Reconnect
overlay_toggled(bool) — overlay show/hide
log_toggled(bool) — console logging on/off
camera_settings_requested() — user opened Image Settings dialog
"""
camera_selected = Signal(object) # CameraInfo
resolution_selected = Signal(int, int)
fps_selected = Signal(float)
format_selected = Signal(object) # CameraFormat
reconnect_requested = Signal()
overlay_toggled = Signal(bool)
log_toggled = Signal(bool)
camera_settings_requested = Signal()
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self._camera_group: QActionGroup | None = None
self._resolution_group: QActionGroup | None = None
self._fps_group: QActionGroup | None = None
self._format_group: QActionGroup | None = None
self._cameras: list[CameraInfo] = []
self._build_menus()
@@ -72,9 +71,8 @@ class AppMenuBar(QMenuBar):
self._camera_group.actions()[0].setChecked(True)
def populate_formats(self, camera_info: CameraInfo) -> None:
"""Populate Resolution and FPS menus based on a camera's supported formats."""
self._populate_resolutions(camera_info)
self._populate_fps(camera_info)
"""Populate the Resolution submenu with full format entries."""
self._populate_format_menu(camera_info)
def set_active_camera(self, camera_info: CameraInfo) -> None:
if self._camera_group is None:
@@ -84,10 +82,24 @@ class AppMenuBar(QMenuBar):
action.setChecked(True)
return
def set_active_format(self, fmt: CameraFormat) -> None:
"""Mark the given format as checked in the Resolution menu."""
if self._format_group is None:
return
for action in self._format_group.actions():
f: CameraFormat = action.data()
if (
f.width == fmt.width
and f.height == fmt.height
and abs(f.max_fps - fmt.max_fps) < 0.5
and f.pixel_format == fmt.pixel_format
):
action.setChecked(True)
return
def set_log_file_path(self, path: str) -> None:
"""Display the log file path as a disabled menu item in Debug menu."""
# Truncate long paths for display
display = path if len(path) <= 60 else "" + path[-57:]
display = path if len(path) <= 60 else "\u2026" + path[-57:]
self._log_file_action.setText(f"Log: {display}")
self._log_file_action.setToolTip(path)
@@ -106,7 +118,12 @@ class AppMenuBar(QMenuBar):
# Video menu
self._video_menu = self.addMenu("Video")
self._res_menu = self._video_menu.addMenu("Resolution")
self._fps_menu = self._video_menu.addMenu("FPS")
# Image menu (camera controls)
self._image_menu = self.addMenu("Image")
self._settings_action = QAction("Camera Settings\u2026", self)
self._settings_action.triggered.connect(self.camera_settings_requested)
self._image_menu.addAction(self._settings_action)
# Debug menu
debug_menu = self.addMenu("Debug")
@@ -129,47 +146,26 @@ class AppMenuBar(QMenuBar):
self._log_file_action.setEnabled(False)
debug_menu.addAction(self._log_file_action)
def _populate_resolutions(self, camera_info: CameraInfo) -> None:
def _populate_format_menu(self, camera_info: CameraInfo) -> None:
"""Build Resolution submenu: one action per unique (W, H, FPS, pixel_format)."""
self._res_menu.clear()
self._resolution_group = QActionGroup(self)
self._resolution_group.setExclusive(True)
self._format_group = QActionGroup(self)
self._format_group.setExclusive(True)
seen: set[tuple[int, int]] = set()
for fmt in camera_info.formats:
key = (fmt.width, fmt.height)
if key in seen:
continue
seen.add(key)
action = QAction(f"{fmt.width} × {fmt.height}", self)
label = (
f"{fmt.width}\u00d7{fmt.height}"
f" @ {fmt.max_fps:.4g}fps"
f" ({fmt.pixel_format})"
)
action = QAction(label, self)
action.setCheckable(True)
action.setData((fmt.width, fmt.height))
self._resolution_group.addAction(action)
action.setData(fmt)
self._format_group.addAction(action)
self._res_menu.addAction(action)
action.triggered.connect(self._on_resolution_action)
action.triggered.connect(self._on_format_action)
actions = self._resolution_group.actions()
if actions:
actions[0].setChecked(True)
def _populate_fps(self, camera_info: CameraInfo) -> None:
self._fps_menu.clear()
self._fps_group = QActionGroup(self)
self._fps_group.setExclusive(True)
seen: set[int] = set()
for fmt in camera_info.formats:
key = round(fmt.max_fps)
if key in seen:
continue
seen.add(key)
action = QAction(f"{key} fps", self)
action.setCheckable(True)
action.setData(float(fmt.max_fps))
self._fps_group.addAction(action)
self._fps_menu.addAction(action)
action.triggered.connect(self._on_fps_action)
actions = self._fps_group.actions()
actions = self._format_group.actions()
if actions:
actions[0].setChecked(True)
@@ -184,24 +180,18 @@ class AppMenuBar(QMenuBar):
cam: CameraInfo = action.data()
logger.debug("Camera selected: %s", cam.name)
self.camera_selected.emit(cam)
self._populate_resolutions(cam)
self._populate_fps(cam)
self._populate_format_menu(cam)
def _on_resolution_action(self) -> None:
def _on_format_action(self) -> None:
action = self.sender()
if action is None:
return
w, h = action.data()
logger.debug("Resolution selected: %dx%d", w, h)
self.resolution_selected.emit(w, h)
def _on_fps_action(self) -> None:
action = self.sender()
if action is None:
return
fps: float = action.data()
logger.debug("FPS selected: %.1f", fps)
self.fps_selected.emit(fps)
fmt: CameraFormat = action.data()
logger.debug(
"Format selected: %dx%d @ %.4g fps (%s)",
fmt.width, fmt.height, fmt.max_fps, fmt.pixel_format,
)
self.format_selected.emit(fmt)
def _on_log_toggled(self, enabled: bool) -> None:
set_console_level(enabled)