# Raport wykonalności: Depth Estimation (DAv2-Small) + Guided Filter w fabric_recolor Data: 2026-09-20. Tryb: analiza + testy w `tmp/` (pliki produkcyjne nietknięte). Środowisko: GTX 1650 4 GB (driver 572.47, **WDDM**), torch 2.11.0+cu128, transformers 4.57.6, Python 3.14, host RAM 8 GB (~1 GB wolnego w trakcie testów). ## 1. Zasoby i budżet VRAM ### BiRefNet (`ZhengPeng7/BiRefNet`, fp32, 220.2M param.) | Pomiar | Wartość | |---|---| | wagi na GPU (po `.to(cuda)`) | 852 MB alloc / 898 MB reserved | | load z cache HF | ~14 s | | forward 1024×1024 | **2026–2120 ms** (5 runów) | | **peak alloc w forward** | **3198 MB** | | reserved (pool alokatora) | 4376 MB | Uwaga kluczowa: `_matte()` zawsze skaluje wejście do 1024×1024 (`birefnet.py:94`), więc **VRAM modelu jest identyczny dla obrazu 1024×1024 i 1600×1200** — rozmiar źródła wpływa tylko na ścieżkę negatywów (DINO/SAM przy `max_side=1600`) i post-processing CPU. ### Pełna `segment()` (BiRefNet + GroundingDINO-Tiny + SAM-Base rezydentne), obraz 1600×1200, realne zdjęcie hali | Pomiar | Wartość | |---|---| | czas | 93.3 s | | peak alloc | **3496 MB** | | rezydentne modele po przebiegu | 1884 MB | | reserved | 4516 MB | **Reserved > fizyczne 4096 MB** → driver WDDM już dziś przelewa część alokacji do shared system memory. Potwierdzenie kruchości: powtórzony przebieg tej samej ścieżki zakończył się twardym `CUDA error: out of memory` (host RAM był niemal pełny → brak miejsca na spill). Budżet fizyczny jest więc **na krawędzi**, nie komfortowy. ### Depth-Anything-V2-Small (`depth-anything/Depth-Anything-V2-Small-hf`, transformers `AutoModelForDepthEstimation`) a) Ładowanie na GPU — **działa** (pobranie ~100 MB, potem z cache): - fp32: 24.8M param., **94.6 MB** alloc, load 7.6 s (z downloadem) / 0.4 s (cache) - fp16: **57.8 MB** alloc b) Narzut VRAM forward (wejście procesora 518×686 dla zdjęcia 1024×768): - fp32: peak **246 MB** - fp16: peak **173 MB** c) Czas mapy głębi: - **fp32: ~115 ms** - fp16: ~441 ms — **wolniej niż fp32** (GTX 1650/Turing nie skaluje tej ścieżki na fp16; narzut konwersji). Rekomendacja: **fp32** — oszczędność ~37 MB wag nie rekompensuje 4× wolniejszego passu. Szacunek koegzystencji: rezydencja modeli ~1884 + ~95 = ~1980 MB; forward DAv2 (+~250 MB workspace) zaplanowany **po** szczycie transientnym BiRefNet nie podnosi peaku ponad ~3.5 GB — mieści się, ale z marginesem <500 MB przy zajętym desktopie. Bezpieczne warianty: sekwencyjne wywołanie depth w obrębie `segment()` (po `_matte`), `torch.cuda.empty_cache()` między etapami, albo proces osobny. Ryzyko: każdy dodatkowy konsument VRAM (DWM, przeglądarka) potrafi wywołać OOM jak w powtórce fazy C. ## 2. Integracja architektoniczna ### Punkt wpięcia mapy głębi `service.py` jest agnostyczny wobec backendu (konsumuje tylko `SegmentationResult.alpha`) — **depth nie należy wpiąć tam**, tylko w `segmentation/birefnet.py::segment()`, w bloku po `fill_small_holes` (linie ~139–161), obok `cut_cavity_volume` i `subtract_protected`. Depth liczone raz na tym samym zmniejszonym `image` (skala `max_side=1600`) → mapa w skali maski, zero dodatkowego resamplingu poza samym DAv2. Propozycja mechanizmu: nowa funkcja w `masks.py` (np. `recessed_plane_mask` / rozszerzenie `cut_cavity_volume`), która **dostarcza kotwicę komory zamiast/t uzupełniając DINO `CAVITY_PROMPTS`**. Głębia staje się negatywem fizycznym (Z-bufor sceny), podczas gdy obecny łańcuch komory jest czysto topologiczny + semantyczny (DINO). ### Wynik testu dyskryminatora (realne ujęcie skrzyni, matte `birefnet-matte-6413.png`) - **Próg globalny** `depth < mediana_maski − margin` → **odrzucić**: tnie 32–41% maski, łącznie z oparciem i tylnym rzędem siedzisk (głębia absolutna ≠ wglebienie). Overlay: `tmp/depth-cavity-overlay.png`. - **Deficyt lokalny** `dilate(depth·maska, R≈260px) − depth > 0.2` → działa: dominujący komponent 410 kpx dokładnie pokrywa wnętrze komory (dno, ścianki, zawias). Overlay: `tmp/depth-cavity-local.png`, mapa deficytu: `tmp/depth-deficit-map.png`. - Ograniczenie: siedzisko przed komorą też jest „poniżej obrysu podłokietnika" → drobne przecieki na siedzisko i fałszywe ślady w głębokich szczelinach (komponenty 5–9 kpx przy th=0.2). Wniosek: **głębia jako sygnał kotwiczący w istniejącej topologii bay/hull (`cut_cavity_volume`), nie samodzielny próg** — komora = region zamknięty w U=maska∪bay ORAZ o dużym deficycie względem obreczy. To też naturalnie eliminuje najsłabsze ogniwo obecnej ścieżki: fałszywy negatyw DINO (`nieznaleziony negatyw = drobny defekt`, ale dla komory to właśnie główny defekt jakości). ### Filtry krawędziowe (wycieki koloru bez rozmywania sztruksu) - `cv2.ximgproc.guidedFilter` — **niedostępne**: zainstalowane `opencv-python-headless 4.13.0` (non-contrib). Opcje: `opencv-contrib-python` (nowa zależność) albo własna implementacja. - Własny guided filter na `cv2.boxFilter` — **zweryfikowany**: ~15 linii, 33 ms @1024×768, edge-aware. `tmp/check_guided.py`. - `cv2.bilateralFilter` — dostępny, 53 ms @1024×768, słabszy na tej samej próbce (większy residual wycieku na tle). - Punkt wpięcia: `birefnet.py:164` — `alpha_small = matte * feather(mask, sigma=1.0)`; guided filter (guide=RGB, src=alfa) wyrównałby rampę alfy do krawędzi zdjęcia zamiast symetrycznego gaussowskiego rozmywania maski → mniejsze przebarwienie pikseli tuż za sztruksową krawędzią. Alternatywnie jako post-krok na `neg`/cięciach przed `feather`. ## 3. Historia i kontrakt API - **TS nie wywołuje dziś tego mikroserwisu.** Produkcja: `server/src/domain/recolor.ts` (`lab_match_v5`) — własny transfer na `sharp`; jedyny subprocess to `scripts/rembg_mask.py` (maska, sesje `birefnet-general`→fallback `u2net`). Endpointy `/api/products/:slug/variants/:variantId/recolor` i `/recolor-batch` (job `recolor_batch` w `worker.ts`) idą przez ten moduł. `fabric_recolor` to osobny pakiet (STAN.md 2026-09-20): decyzja integracji odroczona — ma wejść przez warstwę jobów (HTTP lub subprocess), nie zastępować `recolor.ts`. - Kontrakt wejścia serwisu: CLI `python -m fabric_recolor.cli [--anchor-swatch] [--texture]`; REST `POST /v1/recolor` multipart (`base_image`, `fabric_sample`, `anchor_swatch`, `transfer_texture`, `texture_strength`, `output_format`). - Kontrakt wyjścia: CLI = JSON `RecolorReport` (`ok, output_path, mask_coverage, backend, delta{l,a,b,map_a,map_t,anchor_mode}, texture, elapsed_s, error`); REST = bytes obrazu + nagłówki `X-Recolor-Backend/-Coverage/-Elapsed`. Błąd = `ok:false`/HTTP 422 (`RecolorError`) — `mask_coverage` poza `[0.02, 0.98]` odrzuca wynik. - Dodanie kroku depth **wewnątrz `segment()`** nie zmienia kontraktu: alfa wyjściowa jest tym samym artefaktem, `mask_coverage` liczone z finalnej alfy; metadane kroku mieszczą się w `SegmentationResult.notes` (już przewidziane — patrz `negatives_cut`, `cavity_volumes_cut`) i/lub `RecolorReport.texture`/`delta` bez zmian pól wymaganych. Warunek: depth opcjonalny/feature-flag — gdy model nieosiągalny, `segment()` musi nadal działać (asymetria błędów modułu: brak negatywu = drobny defekt, nie dziura). - Uwaga na testy: `test_service.py` wstrzykuje stub segmentera → krok depth w `BirefnetMatteSegmenter` nie psuje testów; `test_engine.py` testuje `masks.py` czysto numerycznie → nowa funkcja maskowa wymaga własnych testów jednostkowych syntetycznych. ## 4. Rekomendacja Wykonalne, w dwóch krokach o różnym ryzyku: 1. **Niskie ryzyko — guided filter krawędzi** (własna impl. boxFilter, zero deps, 33 ms): poprawia wycieki alfy bez ruszania budżetu VRAM. 2. **Średnie ryzyko — DAv2-Small fp32 jako kotwica głębi w `cut_cavity_volume`**: +95 MB rezydencji, +~115 ms GPU, ~100 MB wag do cache. Wymaga (a) planowania forward poza szczytem BiRefNet, (b) łączenia z topologią bay/hull — sam próg głębi jest niewystarczający (udowodnione nacięciem siedzisk), (c) flagi wyłączalnej i degradacji do obecnej ścieżki DINO przy braku modelu/OOM. Ryzyko sztywnego OOM na 4 GB jest realne już dziś (obserwowane) — wdrożenie depth powinno iść razem z polityką `empty_cache()`/sekwencjonowania albo oceną BiRefNet-fp16 jako osobny wątek. ### Artefakty testowe (tmp/) - `bench_birefnet.py` — pomiary A/B/C (VRAM, czasy) - `bench_depth.py` — DAv2 fp32/fp16 - `check_guided.py` — guided filter vs bilateral - `depth_viz.py` → `depth-storage.png`, `depth-front.png` - `depth_cavity_test.py` → `depth-cavity-overlay.png` (próg globalny — odrzucony) - `depth_cavity_local.py` → `depth-cavity-local.png`, `depth-deficit-map.png` (deficyt lokalny — OK)