Files
duck-preview/app/camera/uvc/windows.py
bartool cdeac53555 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.
2026-05-13 19:19:39 +02:00

176 lines
6.4 KiB
Python

"""Windows UVC controller — backed by duvc-ctl (DirectShow)."""
from __future__ import annotations
import logging
import duvc_ctl as duvc # type: ignore[import-untyped] # noqa: PLC0415 (lazy import)
from app.camera.uvc.base import UvcControllerBase, UvcParam, UvcParamInfo
logger = logging.getLogger(__name__)
# Map our UvcParam → duvc_ctl VidProp string name
_VIDPROP: 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",
UvcParam.BACKLIGHT_COMPENSATION: "backlight_compensation",
}
# Map UvcParam → duvc_ctl CamProp string for camera-side controls
_CAMPROP: dict[UvcParam, str] = {
UvcParam.EXPOSURE: "exposure",
}
class WindowsUvcController(UvcControllerBase):
"""
UVC camera controls on Windows via duvc-ctl (DirectShow).
duvc-ctl docs: https://allanhanan.github.io/duvc-ctl/
"""
def __init__(self, device_name: str) -> None:
self._device_name = device_name
self._cam: duvc.CameraController | None = None
self._supported: dict[UvcParam, bool] = {}
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
def open(self, device_name: str) -> bool:
self.close()
try:
devices = duvc.list_devices()
target = None
for d in devices:
if device_name.lower() in d.name.lower() or d.name.lower() in device_name.lower():
target = d
break
if target is None and devices:
target = devices[0] # fallback to first device
logger.warning(
"UVC: camera '%s' not found by name, using '%s'",
device_name, target.name,
)
if target is None:
logger.warning("UVC: no devices found on Windows")
return False
self._cam = duvc.CameraController(target)
self._cam.__enter__()
self._refresh_supported()
logger.info("UVC: opened device '%s'", target.name)
return True
except Exception as exc:
logger.warning("UVC: failed to open device: %s", exc)
self._cam = None
return False
def close(self) -> None:
if self._cam is not None:
try:
self._cam.__exit__(None, None, None)
except Exception:
pass
self._cam = None
self._supported.clear()
def is_open(self) -> bool:
return self._cam is not None
# ------------------------------------------------------------------
# Query
# ------------------------------------------------------------------
def get_param_info(self, param: UvcParam) -> UvcParamInfo:
if not self.is_open() or not self._supported.get(param, False):
return UvcParamInfo(param=param, supported=False)
try:
prop_name = _VIDPROP.get(param) or _CAMPROP.get(param)
if prop_name is None:
return UvcParamInfo(param=param, supported=False)
rng = self._cam.get_property_range(prop_name)
current = getattr(self._cam, prop_name, rng.get("default", 0))
auto_supported = param in (UvcParam.WHITE_BALANCE, UvcParam.EXPOSURE)
return UvcParamInfo(
param=param,
supported=True,
minimum=int(rng.get("min", 0)),
maximum=int(rng.get("max", 100)),
default=int(rng.get("default", 50)),
current=int(current),
step=int(rng.get("step", 1)),
auto_supported=auto_supported,
)
except Exception as exc:
logger.debug("UVC get_param_info(%s): %s", param.name, exc)
return UvcParamInfo(param=param, supported=False)
def get_all_params(self) -> list[UvcParamInfo]:
return [self.get_param_info(p) for p in UvcParam]
# ------------------------------------------------------------------
# Set
# ------------------------------------------------------------------
def set_value(self, param: UvcParam, value: int) -> bool:
if not self.is_open():
return False
try:
prop_name = _VIDPROP.get(param) or _CAMPROP.get(param)
if prop_name is None:
return False
setattr(self._cam, prop_name, value)
logger.debug("UVC set %s = %d", param.name, value)
return True
except Exception as exc:
logger.warning("UVC set_value(%s, %d): %s", param.name, value, exc)
return False
def set_auto(self, param: UvcParam, enabled: bool) -> bool:
if not self.is_open():
return False
try:
if param == UvcParam.WHITE_BALANCE:
prop = "white_balance"
elif param == UvcParam.EXPOSURE:
prop = "exposure"
else:
return False
mode = duvc.CamMode.AUTO if enabled else duvc.CamMode.MANUAL
self._cam.set_camera_property(prop, mode=mode)
logger.debug("UVC auto %s = %s", param.name, enabled)
return True
except Exception as exc:
logger.warning("UVC set_auto(%s, %s): %s", param.name, enabled, exc)
return False
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _refresh_supported(self) -> None:
if self._cam is None:
return
try:
info = self._cam.get_supported_properties()
vid = set(info.get("video", []))
cam = set(info.get("camera", []))
for param in UvcParam:
prop = _VIDPROP.get(param) or _CAMPROP.get(param, "")
self._supported[param] = prop in vid or prop in cam
except Exception as exc:
logger.debug("UVC _refresh_supported: %s", exc)
for param in UvcParam:
self._supported[param] = False