13 KiB
Stan projektu po sesji: UVC controls + przebudowa menu formatu
Kontekst
Aplikacja uruchomiona na docelowym Mac Mini. Kamera ELP dostarcza klatki w formacie NV12 (przez backend AVFoundation). Stare menu z osobnymi podmenu Resolution i FPS nie nadawało się do użycia — kamera eksponuje formaty jako kombinacje (rozdzielczość + fps + pixel format), a ich niezależne ustawianie dawało nieskoordynowane wyniki.
Dodano też interfejs do kontroli parametrów obrazu kamery.
Zmiany w tej sesji
1. Menu formatu — przebudowane (app/ui/menu_bar.py)
Przed: dwa osobne podmenu — Video → Resolution i Video → FPS.
Po: jedno podmenu Video → Resolution z pozycjami reprezentującymi pełny format:
1920×1080 @ 30fps (NV12)
1920×1080 @ 30fps (MJPG)
1280×720 @ 30fps (NV12)
1280×720 @ 5fps (YUY2)
640×480 @ 30fps (MJPG)
...
Każda pozycja odpowiada dokładnie jednemu CameraFormat z CameraInfo.formats. Kolejność odzwierciedla sortowanie z CameraEnumerator (największa powierzchnia → najwyższe fps).
Sygnały:
- Usunięto:
resolution_selected(int, int),fps_selected(float) - Dodano:
format_selected(CameraFormat)— emituje pełny obiekt formatu - Dodano:
camera_settings_requested()— otwiera dialog ustawień
Metody publiczne:
populate_formats(camera_info)— zamiastpopulate_resolutions+populate_fpsset_active_format(fmt)— zaznacza aktywny format po zmianie (matching po w/h/fps/pixel_format)- Usunięto:
set_console_level()(przeniesione do wewnętrznego_on_log_toggled)
Nowe menu Image:
Image
└── Camera Settings… → otwiera CameraSettingsDialog
2. CameraService — set_format() (app/camera/camera_service.py)
Przed: dwie metody set_resolution(w, h) i set_fps(fps) przechowujące _desired_width, _desired_height, _desired_fps osobno.
Po: jedna metoda set_format(fmt: CameraFormat) przechowująca _desired_fmt: CameraFormat | None.
def set_format(self, fmt: CameraFormat) -> None:
self._desired_fmt = fmt
if self._current_info is not None:
self.start(self._current_info) # stop+start jak poprzednio
Scoring przy wyborze formatu Qt (_apply_format):
Poprzednio scoringował tylko rozdzielczość i fps. Teraz uwzględnia też pixel format:
exact_res = int(w == desired.width and h == desired.height) * 1000
exact_pf = int(pf == desired.pixel_format) * 500
exact_fps = int(abs(f - desired.max_fps) < 0.5) * 100
area_diff = abs(w * h - desired.width * desired.height)
score = exact_res + exact_pf + exact_fps - area_diff
Pixel format jest drugim co do ważności kryterium (po rozdzielczości, przed fps).
Nowa właściwość:
@property
def qt_camera(self) -> QCamera | None:
...
Udostępnia wewnętrzny QCamera dla CameraSettingsDialog (potrzebny do ustawiania WB i exposure przez Qt API).
Poprawione logowanie:
_log_actual_format() loguje teraz ostrzeżenia zarówno dla FPS jak i rozdzielczości jeśli kamera nie dostarczyła tego co było żądane.
3. Warstwa UVC (app/camera/uvc/)
Nowy pakiet z abstrakcją dla sterowania parametrami obrazu przez UVC (USB Video Class).
Dlaczego osobna warstwa, nie Qt?
QCamera.Feature w Qt6 obejmuje: ColorTemperature, ExposureCompensation, IsoSensitivity, ManualExposureTime, CustomFocusPoint, FocusDistance. Nie obejmuje brightness, contrast, saturation, hue, sharpness, gamma, backlight compensation — są to niskopoziomowe UVC/V4L2 controls poza zakresem Qt Multimedia.
Sterowanie nimi wymaga osobnych bibliotek natywnych, innych na każdej platformie.
Dostępne biblioteki
| Biblioteka | Platforma | Instalacja | Uwagi |
|---|---|---|---|
duvc-ctl |
Windows only | pip install duvc-ctl |
DirectShow, stabilna, PyPI, MIT |
pyuvc |
macOS + Linux | brew install libusb jpeg-turbo && pip install pyuvc |
libuvc bindings, wymaga kompilacji |
uvc-py |
macOS + Linux (Win w planie) | pip install uvc-py |
dev/alpha, brak UVC controls |
Struktura
app/camera/uvc/
├── __init__.py # make_uvc_controller(name) — factory z detekcją platformy
├── base.py # UvcControllerBase ABC, UvcParam enum, UvcParamInfo dataclass
├── stub.py # NullUvcController — no-op fallback
├── windows.py # WindowsUvcController — duvc-ctl (DirectShow)
└── macos.py # MacUvcController — pyuvc (libuvc)
UvcParam enum
class UvcParam(Enum):
BRIGHTNESS
CONTRAST
SATURATION
HUE
SHARPNESS
GAMMA
WHITE_BALANCE
BACKLIGHT_COMPENSATION
EXPOSURE
UvcParamInfo dataclass
@dataclass
class UvcParamInfo:
param: UvcParam
supported: bool
minimum: int = 0
maximum: int = 100
default: int = 50
current: int = 50
step: int = 1
auto_supported: bool = False
auto_enabled: bool = False
UvcControllerBase ABC — interfejs
def open(self, device_name: str) -> bool # otwiera uchwyt do urządzenia
def close(self) -> None
def is_open(self) -> bool
def get_param_info(self, param: UvcParam) -> UvcParamInfo
def get_all_params(self) -> list[UvcParamInfo]
def set_value(self, param: UvcParam, value: int) -> bool
def set_auto(self, param: UvcParam, enabled: bool) -> bool
make_uvc_controller(device_name) — factory
Detekcja platformy w runtime:
if system == "Windows":
→ WindowsUvcController (duvc-ctl)
elif system == "Darwin":
→ MacUvcController (pyuvc)
else:
→ NullUvcController
Jeśli biblioteka nie jest zainstalowana — ImportError przechwycony, fallback na NullUvcController. Loguje WARNING z instrukcją instalacji.
WindowsUvcController — szczegóły
- Łączy się z pierwszym urządzeniem którego nazwa zawiera
device_name(case-insensitive) - Mapowanie
UvcParam → duvc-ctl VidProp/CamPropstring:BRIGHTNESS → "brightness",CONTRAST → "contrast", ...,EXPOSURE → "exposure"
set_auto()używaduvc.CamMode.AUTO/MANUAL_refresh_supported()odpytujecam.get_supported_properties()po otwarciu
MacUvcController — szczegóły
- Używa
uvc.device_list()do wyszukania urządzenia po nazwie - Otwiera przez
uvc.Capture(uid) - Mapowanie
UvcParam → pyuvc display_namestring:WHITE_BALANCE → "White Balance temperature",EXPOSURE → "Absolute Exposure Time", itd.
auto_modeviactrl.auto_mode(atrybut pyuvc control)
4. CameraSettingsDialog (app/ui/camera_settings_dialog.py)
QDialog ze scrollowaną zawartością, dwoma sekcjami. Zmiany aplikowane na żywo (brak OK/Apply — tylko Close).
Sekcja Qt Camera Controls
| Kontrolka | Widget | Akcja |
|---|---|---|
| White Balance | QComboBox (8 trybów + Manual) |
camera.setWhiteBalanceMode() |
| Colour Temp (K) | QSpinBox 2000–10000, krok 100 |
camera.setColorTemperature() — widoczny tylko w trybie Manual |
| Exposure Mode | QComboBox (Auto / Manual) |
camera.setExposureMode() |
| Exposure Time (s) | QDoubleSpinBox, 6 miejsc po przecinku |
camera.setManualExposureTime() — widoczny tylko w trybie Manual |
Dynamiczna widoczność: pola Colour Temp i Exposure Time pojawiają się tylko gdy wybrano tryb Manual.
Tryby WB sprawdzane przez camera.isWhiteBalanceModeSupported() przed aplikacją — jeśli kamera nie obsługuje trybu, zmiana jest ignorowana (logowane DEBUG).
Sekcja UVC Camera Controls
Dla każdego UvcParam:
- Jeśli
supported=False: wyświetla "Not supported" (disabled) - Jeśli
supported=True:QSlider+QSpinBoxzsynchronizowane dwukierunkowo - Jeśli
auto_supported=True: dodatkowyQCheckBox "Auto"— po zaznaczeniu blokuje slider i spinbox
Jeśli NullUvcController (żadne kontrolki nieobsługiwane): sekcja wyświetla notatkę:
UVC controls not available.
Install duvc-ctl (Windows) or pyuvc (macOS) to enable.
5. MainWindow — aktualizacje (app/ui/main_window.py)
Nowe pola:
self._uvc: UvcControllerBase = NullUvcController()
Nowe metody:
_open_uvc(cam)— zamyka poprzedni UVC controller i otwiera nowy dla wybranej kamery_on_format_selected(fmt)→camera_service.set_format(fmt)_on_settings_requested()→ tworzyCameraSettingsDialog(qt_camera, uvc)i wywołuje.exec()
Zmiany w _wire_signals():
- Usunięto:
menu.resolution_selected,menu.fps_selected - Dodano:
menu.format_selected → _on_format_selected - Dodano:
menu.camera_settings_requested → _on_settings_requested
closeEvent:
- Dodano:
self._uvc.close()jeśliis_open()
6. Testy (tests/test_uvc.py) — nowy plik, 15 testów
| Klasa testowa | Co testuje |
|---|---|
TestNullUvcController |
open → False, is_open → False, close bez wyjątku, get_param_info → unsupported, get_all_params zwraca wszystkie UvcParam, set_value/set_auto → False bez wyjątku |
TestUvcParamInfo |
wartości domyślne dataclass, flaga supported=False, custom range |
TestMakeUvcControllerFallback |
zwraca instancję UvcControllerBase, fallback na stub gdy ImportError |
Łącznie testów: 35 (20 poprzednich + 15 nowych), wszystkie zielone.
Decyzje architektoniczne
Dlaczego set_format() zamiast osobnych set_resolution/set_fps?
Kamera ELP przez AVFoundation eksponuje formaty jako niepodzielne kombinacje (W × H, fps, pixel_format). Próba niezależnej zmiany rozdzielczości bez zmiany fps (i odwrotnie) prowadziła do niespójnych stanów — kamera ignorowała jedną z wartości albo wybierała domyślny format. Jeden CameraFormat jako atomowa jednostka wyboru eliminuje problem.
Dlaczego warstwowa architektura UVC a nie jedna biblioteka?
Brak cross-platform biblioteki Python dla UVC controls. duvc-ctl — Windows only (DirectShow), pyuvc — macOS/Linux (libuvc). Warstwa abstrakcji z platformową detekcją w runtime pozwala:
- Działać bez żadnej biblioteki (NullUvcController — grayed-out w UI)
- Dodać obsługę nowej platformy bez zmiany warstwy wyżej
- Testować logikę UI niezależnie od dostępności sprzętu
Dlaczego Qt controls i UVC controls w jednym dialogu?
WB i exposure są dostępne przez oba API (Qt i UVC). Qt API jest wyżej poziomowe (tryby, EV), UVC daje surowe wartości rejestrów. Użytkownik widzi jedno spójne miejsce — nie musi wiedzieć przez które API co działa.
Pliki zmienione / dodane
Zmienione
| Plik | Co zmieniono |
|---|---|
app/ui/menu_bar.py |
Przebudowa menu: format_selected zamiast resolution_selected+fps_selected, nowe menu Image |
app/camera/camera_service.py |
set_format() zamiast set_resolution/set_fps, qt_camera property, lepsza walidacja w _log_actual_format |
app/ui/main_window.py |
UVC lifecycle, nowe sygnały menu, _on_settings_requested |
Dodane
| Plik | Co zawiera |
|---|---|
app/camera/uvc/__init__.py |
make_uvc_controller() factory |
app/camera/uvc/base.py |
UvcControllerBase, UvcParam, UvcParamInfo |
app/camera/uvc/stub.py |
NullUvcController |
app/camera/uvc/windows.py |
WindowsUvcController (duvc-ctl) |
app/camera/uvc/macos.py |
MacUvcController (pyuvc) |
app/ui/camera_settings_dialog.py |
CameraSettingsDialog |
tests/test_uvc.py |
15 testów UVC layer |
Instalacja bibliotek UVC
Mac Mini (cel docelowy)
brew install libusb jpeg-turbo
pip install pyuvc
Windows (dev)
pip install duvc-ctl
Bez bibliotek
Aplikacja działa normalnie — sekcja UVC w CameraSettingsDialog wyświetla komunikat o braku biblioteki. Qt controls (WB, Exposure) działają niezależnie od UVC.
Next Steps
-
Przetestować na Mac Mini z kamerą ELP:
- czy
MacUvcControllerotwiera urządzenie popip install pyuvc - czy slidery zmieniają rzeczywiste parametry kamery
- czy
Camera Settings…poprawnie pokazuje zakresy dla ELP (np. brightness 0–255)
- czy
-
Sprawdzić czy AVFoundation na macOS honoruje
setCameraFormatbez stop+start (Qt docs mówią że na macOS inny proces może nadpisać format —_log_actual_formatoznaczy to jako warning) -
Dodać
set_active_format()call po_log_actual_format()żeby menu zaznaczało faktycznie działający format (nie żądany) -
Rozważyć
QSliderz etykietą wartości min/max/default w dialogu — aktualnie są tylko slider + spinbox -
(Opcjonalnie) Preset systemu: zapisz/wczytaj ustawienia do JSON per kamera
Critical Context
CameraService.qt_camera→QCamera | None— potrzebny doCameraSettingsDialog, jestNonegdy kamera zatrzymanamake_uvc_controller(name)zwraca już otwarty lub próbujący otworzyć kontroler;MainWindow._open_uvc()nie wywołuje.open()ponownie jeśliis_open()zwróci TrueMacUvcControllerużywauvc.device_list()(pyuvc API) — wymaga że kamera nie jest aktywnie używana przez inny proces (ograniczenie libusb na macOS)WindowsUvcControllerdziała równolegle z Qt — DirectShow pozwala na jednoczesny dostęp do controls i streamu- Testy UVC nie wymagają sprzętu ani bibliotek natywnych —
NullUvcControllerjest czystym Pythonem
Uruchamianie
# Windows dev
.venv-win\Scripts\python.exe -m app.main
# Mac Mini (po instalacji pyuvc)
.venv/bin/python -m app.main