- 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.
311 lines
11 KiB
Python
311 lines
11 KiB
Python
"""Camera Settings dialog — sliders for UVC controls + Qt WhiteBalance/Exposure."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtMultimedia import QCamera
|
|
from PySide6.QtWidgets import (
|
|
QCheckBox,
|
|
QComboBox,
|
|
QDialog,
|
|
QDialogButtonBox,
|
|
QDoubleSpinBox,
|
|
QFormLayout,
|
|
QGroupBox,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QScrollArea,
|
|
QSlider,
|
|
QSpinBox,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from app.camera.uvc.base import UvcControllerBase, UvcParam
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Human-readable labels for each UVC parameter
|
|
_PARAM_LABELS: dict[UvcParam, str] = {
|
|
UvcParam.BRIGHTNESS: "Brightness",
|
|
UvcParam.CONTRAST: "Contrast",
|
|
UvcParam.SATURATION: "Saturation",
|
|
UvcParam.HUE: "Hue",
|
|
UvcParam.SHARPNESS: "Sharpness",
|
|
UvcParam.GAMMA: "Gamma",
|
|
UvcParam.WHITE_BALANCE: "White Balance (K)",
|
|
UvcParam.BACKLIGHT_COMPENSATION: "Backlight Compensation",
|
|
UvcParam.EXPOSURE: "Exposure Time",
|
|
}
|
|
|
|
# Qt WhiteBalance modes shown in the combo
|
|
_WB_MODES: list[tuple[str, QCamera.WhiteBalanceMode]] = [
|
|
("Auto", QCamera.WhiteBalanceMode.WhiteBalanceAuto),
|
|
("Sunlight", QCamera.WhiteBalanceMode.WhiteBalanceSunlight),
|
|
("Cloudy", QCamera.WhiteBalanceMode.WhiteBalanceCloudy),
|
|
("Shade", QCamera.WhiteBalanceMode.WhiteBalanceShade),
|
|
("Tungsten", QCamera.WhiteBalanceMode.WhiteBalanceTungsten),
|
|
("Fluorescent", QCamera.WhiteBalanceMode.WhiteBalanceFluorescent),
|
|
("Flash", QCamera.WhiteBalanceMode.WhiteBalanceFlash),
|
|
("Sunset", QCamera.WhiteBalanceMode.WhiteBalanceSunset),
|
|
("Manual (K)", QCamera.WhiteBalanceMode.WhiteBalanceManual),
|
|
]
|
|
|
|
_EXPOSURE_MODES: list[tuple[str, QCamera.ExposureMode]] = [
|
|
("Auto", QCamera.ExposureMode.ExposureAuto),
|
|
("Manual", QCamera.ExposureMode.ExposureManual),
|
|
]
|
|
|
|
|
|
class CameraSettingsDialog(QDialog):
|
|
"""
|
|
Modal dialog for camera image controls.
|
|
|
|
Sections:
|
|
• Qt controls — WhiteBalance mode + colour temperature, Exposure mode + time
|
|
• UVC controls — sliders for Brightness, Contrast, Saturation, Hue,
|
|
Sharpness, Gamma, White Balance (manual K), Backlight,
|
|
Exposure (if UVC controller is open)
|
|
|
|
Changes are applied live to the camera.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
camera: QCamera,
|
|
uvc: UvcControllerBase,
|
|
parent: QWidget | None = None,
|
|
) -> None:
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Camera Settings")
|
|
self.setMinimumWidth(440)
|
|
|
|
self._camera = camera
|
|
self._uvc = uvc
|
|
|
|
outer = QVBoxLayout(self)
|
|
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
outer.addWidget(scroll)
|
|
|
|
content = QWidget()
|
|
scroll.setWidget(content)
|
|
layout = QVBoxLayout(content)
|
|
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
|
|
layout.addWidget(self._build_qt_group())
|
|
|
|
uvc_group = self._build_uvc_group()
|
|
if uvc_group is not None:
|
|
layout.addWidget(uvc_group)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
|
buttons.rejected.connect(self.accept)
|
|
outer.addWidget(buttons)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Qt controls section
|
|
# ------------------------------------------------------------------
|
|
|
|
def _build_qt_group(self) -> QGroupBox:
|
|
group = QGroupBox("Qt Camera Controls")
|
|
form = QFormLayout(group)
|
|
|
|
# White Balance mode
|
|
self._wb_combo = QComboBox()
|
|
for label, mode in _WB_MODES:
|
|
self._wb_combo.addItem(label, mode)
|
|
|
|
current_wb = self._camera.whiteBalanceMode()
|
|
for i, (_, mode) in enumerate(_WB_MODES):
|
|
if mode == current_wb:
|
|
self._wb_combo.setCurrentIndex(i)
|
|
break
|
|
|
|
self._wb_combo.currentIndexChanged.connect(self._on_wb_mode_changed)
|
|
form.addRow("White Balance:", self._wb_combo)
|
|
|
|
# Colour temperature (manual only)
|
|
self._temp_label = QLabel("Colour Temp (K):")
|
|
self._temp_spin = QSpinBox()
|
|
self._temp_spin.setRange(2000, 10000)
|
|
self._temp_spin.setSingleStep(100)
|
|
current_temp = self._camera.colorTemperature()
|
|
self._temp_spin.setValue(current_temp if current_temp >= 2000 else 5500)
|
|
self._temp_spin.valueChanged.connect(self._on_color_temp_changed)
|
|
form.addRow(self._temp_label, self._temp_spin)
|
|
self._update_temp_visibility()
|
|
|
|
form.addRow(QLabel("")) # spacer
|
|
|
|
# Exposure mode
|
|
self._exp_combo = QComboBox()
|
|
for label, mode in _EXPOSURE_MODES:
|
|
self._exp_combo.addItem(label, mode)
|
|
|
|
current_exp = self._camera.exposureMode()
|
|
for i, (_, mode) in enumerate(_EXPOSURE_MODES):
|
|
if mode == current_exp:
|
|
self._exp_combo.setCurrentIndex(i)
|
|
break
|
|
|
|
self._exp_combo.currentIndexChanged.connect(self._on_exp_mode_changed)
|
|
form.addRow("Exposure Mode:", self._exp_combo)
|
|
|
|
# Manual exposure time
|
|
self._exp_label = QLabel("Exposure Time (s):")
|
|
self._exp_spin = QDoubleSpinBox()
|
|
exp_min = self._camera.minimumExposureTime()
|
|
exp_max = self._camera.maximumExposureTime()
|
|
self._exp_spin.setRange(
|
|
exp_min if exp_min > 0 else 1e-6,
|
|
exp_max if exp_max > 0 else 1.0,
|
|
)
|
|
self._exp_spin.setDecimals(6)
|
|
self._exp_spin.setSingleStep(0.001)
|
|
manual_t = self._camera.manualExposureTime()
|
|
self._exp_spin.setValue(manual_t if manual_t > 0 else 0.033)
|
|
self._exp_spin.valueChanged.connect(self._on_exp_time_changed)
|
|
form.addRow(self._exp_label, self._exp_spin)
|
|
self._update_exp_visibility()
|
|
|
|
return group
|
|
|
|
# ------------------------------------------------------------------
|
|
# UVC controls section
|
|
# ------------------------------------------------------------------
|
|
|
|
def _build_uvc_group(self) -> QGroupBox | None:
|
|
params = self._uvc.get_all_params()
|
|
supported = [p for p in params if p.supported]
|
|
|
|
group = QGroupBox("UVC Camera Controls")
|
|
form = QFormLayout(group)
|
|
|
|
if not supported:
|
|
note = QLabel(
|
|
"UVC controls not available.\n"
|
|
"Install duvc-ctl (Windows) or pyuvc (macOS) to enable."
|
|
)
|
|
note.setEnabled(False)
|
|
form.addRow(note)
|
|
return group
|
|
|
|
self._uvc_sliders: dict[UvcParam, QSlider] = {}
|
|
self._uvc_spins: dict[UvcParam, QSpinBox] = {}
|
|
self._uvc_auto_boxes: dict[UvcParam, QCheckBox] = {}
|
|
|
|
for info in params:
|
|
label = _PARAM_LABELS.get(info.param, info.param.name.title())
|
|
|
|
if not info.supported:
|
|
lbl = QLabel("Not supported")
|
|
lbl.setEnabled(False)
|
|
form.addRow(f"{label}:", lbl)
|
|
continue
|
|
|
|
row_widget = QWidget()
|
|
row_layout = QHBoxLayout(row_widget)
|
|
row_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
slider = QSlider(Qt.Orientation.Horizontal)
|
|
slider.setRange(info.minimum, info.maximum)
|
|
slider.setValue(info.current)
|
|
slider.setSingleStep(max(1, info.step))
|
|
|
|
spin = QSpinBox()
|
|
spin.setRange(info.minimum, info.maximum)
|
|
spin.setValue(info.current)
|
|
spin.setFixedWidth(75)
|
|
|
|
# Keep slider ↔ spin in sync
|
|
slider.valueChanged.connect(spin.setValue)
|
|
spin.valueChanged.connect(slider.setValue)
|
|
slider.valueChanged.connect(
|
|
lambda v, p=info.param: self._on_uvc_value(p, v)
|
|
)
|
|
|
|
row_layout.addWidget(slider)
|
|
row_layout.addWidget(spin)
|
|
|
|
if info.auto_supported:
|
|
auto_box = QCheckBox("Auto")
|
|
auto_box.setChecked(info.auto_enabled)
|
|
auto_box.toggled.connect(
|
|
lambda checked, p=info.param: self._on_uvc_auto(p, checked)
|
|
)
|
|
row_layout.addWidget(auto_box)
|
|
self._uvc_auto_boxes[info.param] = auto_box
|
|
# Init enabled state
|
|
slider.setEnabled(not info.auto_enabled)
|
|
spin.setEnabled(not info.auto_enabled)
|
|
|
|
self._uvc_sliders[info.param] = slider
|
|
self._uvc_spins[info.param] = spin
|
|
form.addRow(f"{label}:", row_widget)
|
|
|
|
return group
|
|
|
|
# ------------------------------------------------------------------
|
|
# Qt control slots
|
|
# ------------------------------------------------------------------
|
|
|
|
def _on_wb_mode_changed(self, index: int) -> None:
|
|
mode: QCamera.WhiteBalanceMode = self._wb_combo.itemData(index)
|
|
if self._camera.isWhiteBalanceModeSupported(mode):
|
|
self._camera.setWhiteBalanceMode(mode)
|
|
logger.debug("WB mode: %s", mode)
|
|
else:
|
|
logger.debug("WB mode %s not supported by this camera", mode)
|
|
self._update_temp_visibility()
|
|
|
|
def _on_color_temp_changed(self, value: int) -> None:
|
|
self._camera.setColorTemperature(value)
|
|
logger.debug("Colour temp: %d K", value)
|
|
|
|
def _on_exp_mode_changed(self, index: int) -> None:
|
|
mode: QCamera.ExposureMode = self._exp_combo.itemData(index)
|
|
if self._camera.isExposureModeSupported(mode):
|
|
self._camera.setExposureMode(mode)
|
|
logger.debug("Exposure mode: %s", mode)
|
|
self._update_exp_visibility()
|
|
|
|
def _on_exp_time_changed(self, value: float) -> None:
|
|
self._camera.setManualExposureTime(value)
|
|
logger.debug("Exposure time: %.6f s", value)
|
|
|
|
def _update_temp_visibility(self) -> None:
|
|
manual = (
|
|
self._wb_combo.currentData()
|
|
== QCamera.WhiteBalanceMode.WhiteBalanceManual
|
|
)
|
|
self._temp_label.setVisible(manual)
|
|
self._temp_spin.setVisible(manual)
|
|
|
|
def _update_exp_visibility(self) -> None:
|
|
manual = (
|
|
self._exp_combo.currentData()
|
|
== QCamera.ExposureMode.ExposureManual
|
|
)
|
|
self._exp_label.setVisible(manual)
|
|
self._exp_spin.setVisible(manual)
|
|
|
|
# ------------------------------------------------------------------
|
|
# UVC control slots
|
|
# ------------------------------------------------------------------
|
|
|
|
def _on_uvc_value(self, param: UvcParam, value: int) -> None:
|
|
self._uvc.set_value(param, value)
|
|
|
|
def _on_uvc_auto(self, param: UvcParam, enabled: bool) -> None:
|
|
self._uvc.set_auto(param, enabled)
|
|
slider = self._uvc_sliders.get(param)
|
|
spin = self._uvc_spins.get(param)
|
|
if slider:
|
|
slider.setEnabled(not enabled)
|
|
if spin:
|
|
spin.setEnabled(not enabled)
|