doc: add note about last changes
This commit is contained in:
368
notes/04-mvp-uvc.md
Normal file
368
notes/04-mvp-uvc.md
Normal file
@@ -0,0 +1,368 @@
|
||||
# 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)` — zamiast `populate_resolutions` + `populate_fps`
|
||||
- `set_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`.
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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ść:**
|
||||
|
||||
```python
|
||||
@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
|
||||
|
||||
```python
|
||||
class UvcParam(Enum):
|
||||
BRIGHTNESS
|
||||
CONTRAST
|
||||
SATURATION
|
||||
HUE
|
||||
SHARPNESS
|
||||
GAMMA
|
||||
WHITE_BALANCE
|
||||
BACKLIGHT_COMPENSATION
|
||||
EXPOSURE
|
||||
```
|
||||
|
||||
#### `UvcParamInfo` dataclass
|
||||
|
||||
```python
|
||||
@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
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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/CamProp` string:
|
||||
- `BRIGHTNESS → "brightness"`, `CONTRAST → "contrast"`, ..., `EXPOSURE → "exposure"`
|
||||
- `set_auto()` używa `duvc.CamMode.AUTO/MANUAL`
|
||||
- `_refresh_supported()` odpytuje `cam.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_name` string:
|
||||
- `WHITE_BALANCE → "White Balance temperature"`, `EXPOSURE → "Absolute Exposure Time"`, itd.
|
||||
- `auto_mode` via `ctrl.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` + `QSpinBox` zsynchronizowane dwukierunkowo
|
||||
- Jeśli `auto_supported=True`: dodatkowy `QCheckBox "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:**
|
||||
|
||||
```python
|
||||
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()` → tworzy `CameraSettingsDialog(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śli `is_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)
|
||||
|
||||
```bash
|
||||
brew install libusb jpeg-turbo
|
||||
pip install pyuvc
|
||||
```
|
||||
|
||||
### Windows (dev)
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
1. Przetestować na Mac Mini z kamerą ELP:
|
||||
- czy `MacUvcController` otwiera urządzenie po `pip install pyuvc`
|
||||
- czy slidery zmieniają rzeczywiste parametry kamery
|
||||
- czy `Camera Settings…` poprawnie pokazuje zakresy dla ELP (np. brightness 0–255)
|
||||
|
||||
2. Sprawdzić czy AVFoundation na macOS honoruje `setCameraFormat` bez stop+start (Qt docs mówią że na macOS inny proces może nadpisać format — `_log_actual_format` oznaczy to jako warning)
|
||||
|
||||
3. Dodać `set_active_format()` call po `_log_actual_format()` żeby menu zaznaczało faktycznie działający format (nie żądany)
|
||||
|
||||
4. Rozważyć `QSlider` z etykietą wartości min/max/default w dialogu — aktualnie są tylko slider + spinbox
|
||||
|
||||
5. (Opcjonalnie) Preset systemu: zapisz/wczytaj ustawienia do JSON per kamera
|
||||
|
||||
---
|
||||
|
||||
## Critical Context
|
||||
|
||||
- `CameraService.qt_camera` → `QCamera | None` — potrzebny do `CameraSettingsDialog`, jest `None` gdy kamera zatrzymana
|
||||
- `make_uvc_controller(name)` zwraca **już otwarty lub próbujący otworzyć** kontroler; `MainWindow._open_uvc()` nie wywołuje `.open()` ponownie jeśli `is_open()` zwróci True
|
||||
- `MacUvcController` używa `uvc.device_list()` (pyuvc API) — wymaga że kamera **nie jest** aktywnie używana przez inny proces (ograniczenie libusb na macOS)
|
||||
- `WindowsUvcController` działa **równolegle** z Qt — DirectShow pozwala na jednoczesny dostęp do controls i streamu
|
||||
- Testy UVC nie wymagają sprzętu ani bibliotek natywnych — `NullUvcController` jest czystym Pythonem
|
||||
|
||||
## Uruchamianie
|
||||
|
||||
```bash
|
||||
# Windows dev
|
||||
.venv-win\Scripts\python.exe -m app.main
|
||||
|
||||
# Mac Mini (po instalacji pyuvc)
|
||||
.venv/bin/python -m app.main
|
||||
```
|
||||
Reference in New Issue
Block a user