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:
172
app/camera/uvc/macos.py
Normal file
172
app/camera/uvc/macos.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""macOS UVC controller — backed by pyuvc (libuvc bindings)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from app.camera.uvc.base import UvcControllerBase, UvcParam, UvcParamInfo
|
||||
from app.camera.uvc.stub import NullUvcController
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# pyuvc provides access to UVC controls via libuvc.
|
||||
# Install: pip install pyuvc (requires libusb + libjpeg-turbo via brew)
|
||||
#
|
||||
# If pyuvc is not installed, fall back silently to NullUvcController.
|
||||
try:
|
||||
import uvc # type: ignore[import-untyped]
|
||||
_PYUVC_AVAILABLE = True
|
||||
except ImportError:
|
||||
_PYUVC_AVAILABLE = False
|
||||
logger.debug("pyuvc not available — macOS UVC controls disabled")
|
||||
|
||||
|
||||
# Map UvcParam → pyuvc control name string
|
||||
_CONTROL_NAME: 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 temperature",
|
||||
UvcParam.BACKLIGHT_COMPENSATION: "Backlight Compensation",
|
||||
UvcParam.EXPOSURE: "Absolute Exposure Time",
|
||||
}
|
||||
|
||||
|
||||
class MacUvcController(UvcControllerBase):
|
||||
"""
|
||||
UVC camera controls on macOS via pyuvc / libuvc.
|
||||
|
||||
pyuvc: https://github.com/pupil-labs/pyuvc
|
||||
Install: pip install pyuvc (requires: brew install libusb jpeg-turbo)
|
||||
"""
|
||||
|
||||
def __init__(self, device_name: str) -> None:
|
||||
if not _PYUVC_AVAILABLE:
|
||||
raise ImportError("pyuvc not installed")
|
||||
self._device_name = device_name
|
||||
self._cap: object | None = None
|
||||
self._controls: dict[str, object] = {}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def open(self, device_name: str) -> bool:
|
||||
self.close()
|
||||
if not _PYUVC_AVAILABLE:
|
||||
return False
|
||||
try:
|
||||
device_list = uvc.device_list()
|
||||
target = None
|
||||
for d in device_list:
|
||||
if device_name.lower() in d.get("name", "").lower():
|
||||
target = d
|
||||
break
|
||||
if target is None and device_list:
|
||||
target = device_list[0]
|
||||
logger.warning(
|
||||
"UVC: camera '%s' not found by name, using '%s'",
|
||||
device_name, target.get("name"),
|
||||
)
|
||||
if target is None:
|
||||
logger.warning("UVC: no UVC devices found on macOS")
|
||||
return False
|
||||
|
||||
self._cap = uvc.Capture(target["uid"])
|
||||
# Index controls by name for fast lookup
|
||||
self._controls = {c.display_name: c for c in self._cap.controls}
|
||||
logger.info(
|
||||
"UVC: opened '%s', controls: %s",
|
||||
target.get("name"), list(self._controls.keys()),
|
||||
)
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.warning("UVC macOS open failed: %s", exc)
|
||||
self._cap = None
|
||||
return False
|
||||
|
||||
def close(self) -> None:
|
||||
if self._cap is not None:
|
||||
try:
|
||||
self._cap.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._cap = None
|
||||
self._controls.clear()
|
||||
|
||||
def is_open(self) -> bool:
|
||||
return self._cap is not None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Query
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_param_info(self, param: UvcParam) -> UvcParamInfo:
|
||||
ctrl_name = _CONTROL_NAME.get(param)
|
||||
if ctrl_name is None or ctrl_name not in self._controls:
|
||||
return UvcParamInfo(param=param, supported=False)
|
||||
try:
|
||||
ctrl = self._controls[ctrl_name]
|
||||
auto_supported = hasattr(ctrl, "auto_mode")
|
||||
auto_enabled = bool(ctrl.auto_mode) if auto_supported else False
|
||||
return UvcParamInfo(
|
||||
param=param,
|
||||
supported=True,
|
||||
minimum=int(ctrl.min_val),
|
||||
maximum=int(ctrl.max_val),
|
||||
default=int(ctrl.def_val),
|
||||
current=int(ctrl.value),
|
||||
step=int(getattr(ctrl, "step", 1)),
|
||||
auto_supported=auto_supported,
|
||||
auto_enabled=auto_enabled,
|
||||
)
|
||||
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:
|
||||
ctrl_name = _CONTROL_NAME.get(param)
|
||||
if ctrl_name is None or ctrl_name not in self._controls:
|
||||
return False
|
||||
try:
|
||||
self._controls[ctrl_name].value = 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:
|
||||
ctrl_name = _CONTROL_NAME.get(param)
|
||||
if ctrl_name is None or ctrl_name not in self._controls:
|
||||
return False
|
||||
try:
|
||||
ctrl = self._controls[ctrl_name]
|
||||
if hasattr(ctrl, "auto_mode"):
|
||||
ctrl.auto_mode = enabled
|
||||
logger.debug("UVC auto %s = %s", param.name, enabled)
|
||||
return True
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.warning("UVC set_auto(%s, %s): %s", param.name, enabled, exc)
|
||||
return False
|
||||
|
||||
|
||||
def make_mac_uvc_controller(device_name: str) -> UvcControllerBase:
|
||||
"""Factory: returns MacUvcController or NullUvcController if pyuvc absent."""
|
||||
if not _PYUVC_AVAILABLE:
|
||||
return NullUvcController()
|
||||
ctrl = MacUvcController(device_name)
|
||||
if ctrl.open(device_name):
|
||||
return ctrl
|
||||
return NullUvcController()
|
||||
Reference in New Issue
Block a user