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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user