diff --git a/python_pkg/praca_magisterska_video/__init__.py b/python_pkg/praca_magisterska_video/__init__.py new file mode 100644 index 0000000..f3dd9bc --- /dev/null +++ b/python_pkg/praca_magisterska_video/__init__.py @@ -0,0 +1 @@ +"""Thesis video visualization scripts.""" diff --git a/python_pkg/praca_magisterska_video/answers/2/pytanie_02.md b/python_pkg/praca_magisterska_video/answers/pytanie_02.md similarity index 100% rename from python_pkg/praca_magisterska_video/answers/2/pytanie_02.md rename to python_pkg/praca_magisterska_video/answers/pytanie_02.md diff --git a/python_pkg/praca_magisterska_video/answers/pytanie_23.md b/python_pkg/praca_magisterska_video/answers/pytanie_23.md new file mode 100644 index 0000000..5fe1bcf --- /dev/null +++ b/python_pkg/praca_magisterska_video/answers/pytanie_23.md @@ -0,0 +1,563 @@ +## PYTANIE 23: Segmentacja obrazu + +**Problem, strategie klasyczne i sieci neuronowe.** + +--- + +### Tło pojęciowe — słowniczek + +**Obraz cyfrowy (digital image)** — macierz pikseli. Obraz 1920×1080 = ~2 mln pikseli. Każdy piksel ma wartość (grayscale: 0-255) lub kanały RGB (3 × 0-255). Segmentacja operuje na tej macierzy. + +**Piksel (pixel)** — najmniejsza jednostka obrazu. „Picture element." Segmentacja = przypisanie etykiety KAŻDEMU pikselowi. + +**Segmentacja obrazu (image segmentation)** — podział obrazu na regiony, gdzie każdy piksel dostaje etykietę klasy (np. „samochód", „droga", „niebo"). Różni się od klasyfikacji (cały obraz → 1 etykieta) i detekcji (bounding box + etykieta). + +**Czy naprawdę KAŻDY piksel?** Tak, w semantic segmentation wynik to mapa o IDENTYCZNYM rozmiarze jak obraz wejściowy. Obraz 640×480 → mapa 640×480, w której KAŻDY z 307 200 pikseli ma etykietę klasy. Żaden piksel nie jest pominięty. Nawet piksele „tła" dostają etykietę (np. klasa „background" lub „void"). W instance segmentation dodatkowo piksele tego samego obiektu dostają ten sam ID instancji. + + Obraz wejściowy: 640 × 480 pikseli (RGB, 3 kanały) + Mapa segmentacji: 640 × 480 pikseli (1 kanał — numer klasy) + Piksel (100, 200): RGB=(134, 178, 210) → klasa 3 ("niebo") + Piksel (320, 400): RGB=(82, 79, 73) → klasa 7 ("droga") + KAŻDY piksel ma etykietę — nawet ten "nudny" fragment tła. + +--- + +**Over-segmentation (nad-segmentacja)** — sytuacja, gdy algorytm segmentacji generuje ZBYT WIELE regionów — więcej niż jest obiektów/klas na obrazie. Jeden obiekt zostaje podzielony na kilka-kilkadziesiąt fragmentów. Problem typowy dla metod klasycznych (watershed, region growing). + + Obraz: jeden kubek na stole + Idealna segmentacja: 2 regiony (kubek, tło) + Over-segmentation: 47 regionów! (kubek podzielony na 12 kawałków, + stół na 20, tło na 15) + + Dlaczego to się dzieje? + - Watershed: każde lokalne minimum jasności → osobny region → setki regionów + - Region Growing: drobne różnice w intensywności → osobne regiony + - Szum (noise) w obrazie → fałszywe granice + + Jak sobie z tym radzić? + - **Markers/seeds:** zamiast automatycznych minimów → podaj ręczne punkty startowe + - **Superpixels:** celowa nad-segmentacja na ~100-500 jednorodnych "superpikseli" + (np. SLIC), potem GRUPOWANIE superpikseli w klasy → szybsze i stabilniejsze + - **Hierarchiczne:** wielopoziomowa segmentacja → scalanie regionów bottom-up + - **Deep learning:** sieci neuronowe uczą się "co jest obiektem" z danych → nie mają + problemu z over-segmentation (bo wiedzą, że kubek to jeden obiekt) + +**Under-segmentation (pod-segmentacja)** — przeciwieństwo: zbyt mało regionów, różne obiekty zlane w jeden region. Mniej typowy problem. + +--- + +**Typy segmentacji:** + +**Semantic segmentation** — każdy piksel → klasa, ale NIE rozróżnia instancji. Wszystkie samochody = jedna klasa „samochód". + + [samochód][samochód][droga][droga][pieszo][niebo] + Dwa samochody = ta sama etykieta "samochód" + +**Instance segmentation** — rozróżnia instancje tego samego obiektu. Samochód#1 i Samochód#2 mają różne etykiety. + +**Panoptic segmentation** — łączy semantic + instance. Obiekty „things" (samochody, ludzie) mają instancje; „stuff" (niebo, droga) — tylko klasy. + +--- + +#### Pojęcia kluczowe dla progowania i Otsu + +**Wariancja (variance, σ²)** — miara tego, jak bardzo wartości RÓŻNIĄ SIĘ od swojej średniej. Im większa wariancja, tym bardziej „rozrzucone" są dane. Wzór: σ² = Σ(xᵢ - μ)² / n, gdzie μ to średnia. + + Przykład 1 — MAŁA wariancja (dane skupione): + wartości: [48, 50, 52, 49, 51] średnia μ = 50 + σ² = ((48-50)² + (50-50)² + (52-50)² + (49-50)² + (51-50)²) / 5 + = (4 + 0 + 4 + 1 + 1) / 5 = 2.0 + + Przykład 2 — DUŻA wariancja (dane rozrzucone): + wartości: [10, 90, 30, 80, 50] średnia μ = 52 + σ² = ((10-52)² + (90-52)² + (30-52)² + (80-52)² + (50-52)²) / 5 + = (1764 + 1444 + 484 + 784 + 4) / 5 = 896.0 + + Mała σ² = punkty blisko średniej = dane JEDNORODNE + Duża σ² = punkty daleko od średniej = dane RÓŻNORODNE + +**Wewnątrzklasowa (within-class)** — „wewnątrz klasy" oznacza, że mierzymy wariancję OSOBNO dla każdej grupy (klasy), a potem ważymy wynik proporcją pikseli w grupie. Jeśli klasa 0 ma piksele [30, 50, 45] a klasa 1 ma piksele [180, 200, 190], to σ²_wewnątrz = (udział_kl0 × σ²_kl0) + (udział_kl1 × σ²_kl1). + +**Wariancja wewnątrzklasowa (within-class variance)** — obliczasz wariancję KAŻDEJ klasy osobno, ważysz przez udział pikseli w tej klasie, sumujesz. Jeśli σ²_wewnątrz jest MAŁA → klasy są „jednorodne" (piksele w klasie 0 mają podobne jasności, piksele w klasie 1 też). + +**Co to znaczy „klasy jednorodne"?** — jednorodna klasa to taka, w której WSZYSTKIE piksele mają podobne wartości. Np. klasa „tło" ma jasności [195, 200, 198, 205] → jednorodna (σ² mała). Klasa mieszająca tło i obiekt [30, 200, 50, 190] → niejednorodna (σ² duża). Otsu szuka progu T, który daje NAJBARDZIEJ jednorodne klasy. + +**Histogram bimodalny (bimodal histogram)** — histogram z DWOMA wyraźnymi „garbami" (pikami). „Bi" = dwa, „modal" = moda (najczęstsza wartość). Typowy dla obrazów z jednym obiektem na tle — garb 1 odpowiada ciemnym pikselom (obiekt), garb 2 jasnym (tło). Otsu działa TYLKO gdy histogram jest bimodalny — bo szuka progu MIĘDZY garbami. + + Garb 1 (ciemne~60): piksele obiektu + Garb 2 (jasne~190): piksele tła + Dolina między garbami → tu Otsu stawia próg T! + + Gdyby histogram miał JEDEN garb (unimodalny) → brak naturalnego + podziału → Otsu wybierze losowy próg → słaby wynik. + +![Histogram bimodalny, wariancja wewnątrzklasowa i jednorodność klas — Otsu](img/q23_otsu_bimodal.png) + +--- + +**Thresholding (progowanie)** — najprostsza metoda segmentacji. Pomysł: każdy piksel ma wartość jasności (0=czarny, 255=biały). Wybierz PRÓG T: piksel > T → klasa 1 (obiekt), piksel ≤ T → klasa 0 (tło). Działa lepiej niż się wydaje na prostych obrazach (tekst na kartce, RTG, dokumenty). + + Obraz (jasność pikseli): [50][200][180][30][220][190] + Próg T=128: + 50 ≤ 128 → 0 (tło) + 200 > 128 → 1 (obiekt) + 180 > 128 → 1 + 30 ≤ 128 → 0 + Wynik: [ 0 ][ 1 ][ 1 ][ 0][ 1 ][ 1 ] + + Problem: JAK wybrać T? Ręcznie → subiektywne. Rozwiązanie → Otsu. + + Mnemonik: „PRÓG na bramce" — jak bramkarz, przepuszcza piksele jaśniejsze od T, + blokuje ciemniejsze. + +**Otsu** — automatyczny dobór progu. Algorytm: przetestuj WSZYSTKIE progi T=0..255, dla każdego oblicz wariancję wewnątrzklasową (jak „różnorodne" są piksele w klasie 0 i klasie 1). Wybierz T minimalizujące tę wariancję = klasy jak najbardziej jednorodne. Złożoność: O(n·L) gdzie n=piksele, L=poziomy jasności (256). Ograniczenie: działa TYLKO dla 2 klas i zakłada bimodalny histogram jasności (dwa „garby"). Patrz diagram powyżej. + + Pseudokod Otsu: + best_T = 0 + min_var = ∞ + for T in 0..255: + c0 = piksele z jasność ≤ T + c1 = piksele z jasność > T + w0 = len(c0) / len(all_pixels) + w1 = len(c1) / len(all_pixels) + var = w0 * variance(c0) + w1 * variance(c1) + if var < min_var: + min_var = var + best_T = T + return best_T + + Mnemonik: „AUTO-bramkarz Otsu" — sam sprawdza 256 progów i wybiera najlepszy. + +--- + +#### Pojęcia kluczowe dla Region Growing + +**Region Growing (rozrastanie regionu)** — zaczynasz od jednego piksela „ziarna" (seed) wybranego ręcznie lub automatycznie. Sprawdzasz sąsiadów: jeśli sąsiad jest PODOBNY (np. |jasność_sąsiada - jasność_regionu| < próg), dodaj go do regionu. Powtarzaj aż nie ma więcej podobnych sąsiadów. Następnie nowy seed → nowy region. + +**Dlaczego seed „ręcznie LUB automatycznie"?** — to dwa różne scenariusze użycia: + + RĘCZNY seed: + - Użytkownik klika myszką na obraz: „tu jest obiekt" + - Użycie: segmentacja interaktywna (Photoshop „magic wand", + narzędzia medyczne do zaznaczania guzów na RTG) + - Zaleta: precyzyjny, użytkownik wie co chce segmentować + - Wada: wymaga człowieka → nie skaluje się do 10 000 obrazów + + AUTOMATYCZNY seed — metody: + 1. Siatka (grid): seed co N pikseli (np. co 50 px na obrazie 500×500 → 100 seedów) + 2. Lokalne ekstrema histogramu: znajdź najczęstszą jasność → seed tam + 3. Losowanie: wylosuj K punktów jako seedy + 4. Analiza gradientu: piksele w „płaskich" regionach (brak krawędzi) → dobre seedy + + Dlaczego OR a nie AND? + Bo to ALTERNATYWNE podejścia — albo człowiek wybiera (mało i precyzyjnie), + albo algorytm wybiera (dużo i szybko, ale mniej precyzyjnie). + +![Region Growing: seed ręczny vs automatyczny, krok po kroku, fale BFS](img/q23_region_growing.png) + + Pseudokod Region Growing: + region = {seed} + queue = [seed] + while queue not empty: + pixel = queue.pop() + for neighbor in pixel.neighbors(): # 4 lub 8 sąsiadów + if neighbor not visited AND similar(neighbor, region): + region.add(neighbor) + queue.append(neighbor) + + Mnemonik: „PLAMA atramentu" — seed to kropla atramentu na papierze, + rozlewa się na podobne (jasne) miejsca, zatrzymuje się na granicach. + +--- + +#### Pojęcia kluczowe dla Watershed + +**Watershed (metoda zlewiska)** — traktuje obraz jak mapę topograficzną: wartość jasności piksela = wysokość terenu. Ciemne piksele = doliny, jasne = szczyty. Algorytm „zalewa" mapę wodą od najniższych punktów (minimów). Gdy woda z dwóch dolin się spotyka — tam jest GRANICA segmentu (grań). + +![Watershed: obraz jako mapa topograficzna, zalewanie, over-segmentation i marker-controlled watershed](img/q23_watershed.png) + + Algorytm: + 1. Zamień obraz na „mapę wysokości" (jasność = wysokość) + 2. Znajdź wszystkie lokalne minima (najciemniejsze punkty) + 3. „Zalewaj" od minimów — woda rośnie równomiernie + 4. Gdy woda z dwóch dolin się spotyka → postaw TAMĘ (granicę segmentu) + 5. Kontynuuj aż cały obraz zalany + + Problem: MASYWNA over-segmentation — każde lokalne minimum (nawet szum!) → osobna dolina + Rozwiązanie: marker-controlled watershed — użytkownik podaje markery (seedy), + zalewamy TYLKO od tych markerów + + Mnemonik: „ZALEWANIE terenu" — wyobraź sobie model terenu z plasteliny w wannie. + Powoli nalewasz wodę → doliny się wypełniają → granie gór = granice segmentów. + +--- + +#### Pojęcia kluczowe dla Mean Shift + +**Okno (window) / jądro (kernel)** — w kontekście Mean Shift to koło (lub kula w wielowymiarowej przestrzeni) o ustalonej szerokości (bandwidth = promień h) wokół aktualnego punktu. Wewnątrz okna algorytm oblicza „średnią ważoną" pozycji pikseli. Okno = jądro — to synonim. Nazwa „jądro" pochodzi od estymacji jądrowej gęstości (kernel density estimation, KDE). + + Okno o promieniu h = 30 wokół punktu (100, 150): + Bierze WSZYSTKIE piksele, których cechy (jasność, x, y) + są w odległości ≤ 30 od (100, 150). + Oblicza ich średnią → przesuwa okno NA TĘ ŚREDNIĄ. + Powtarza aż okno się „zatrzyma" (przesunięcie < ε). + +**Najwyższa gęstość (density peak)** — punkt w przestrzeni cech, gdzie jest NAJWIĘKSZE skupisko pikseli. Jak najwyższy szczyt góry w 3D. Mean Shift = „przesuń w kierunku średniej" → iteracyjnie zbliża się do szczytu gęstości. + +**Przestrzeń cech (feature space)** — każdy piksel jest opisany nie tylko pozycją (x, y) ale też cechami koloru (jasność, R, G, B). Przestrzeń cech to przestrzeń wielowymiarowa, np. (R, G, B, x, y) = 5 wymiarów. Piksele o podobnych kolorach i blisko siebie będą blisko w przestrzeni cech → tworzą klastry (skupiska). + + Piksel A: (x=100, y=200, R=30, G=25, B=35) → punkt w 5D + Piksel B: (x=102, y=201, R=32, G=27, B=33) → BLISKO A w 5D + Piksel C: (x=105, y=198, R=200, G=210, B=220) → DALEKO od A w 5D (inny kolor!) + → A i B w jednym segmencie, C w innym + +**Dlaczego Mean Shift NIE wymaga podania liczby segmentów?** — W K-means musisz podać K=3 (trzy klastry) ZANIM uruchomisz algorytm. Mean Shift działa inaczej: każdy piksel startuje i „toczy się" do najbliższego szczytu gęstości. Ile jest szczytów = tyle segmentów. Algorytm sam ODKRYWA liczbę klastrów. Parametrem jest tylko bandwidth (szerokość okna h): duże h → mało szczytów → mało segmentów; małe h → dużo szczytów → dużo segmentów. + +![Mean Shift: przestrzeń cech, jądro przesuwane do max gęstości, dlaczego bez K](img/q23_mean_shift.png) + + Pseudokod Mean Shift: + for each pixel p: + x = p.features # np. (R, G, B, pos_x, pos_y) + repeat: + window = all pixels within distance h from x + x_new = weighted_mean(window) + if |x_new - x| < epsilon: + break + x = x_new + p.cluster = x # zbieżny punkt = ID klastra + + Mnemonik: „KULKI toczą się do dołków" — rozsyp kulki na nierównym stole, + każda toczy się do najbliższego zagłębienia. Ile dołków = tyle segmentów. + +--- + +#### Pojęcia kluczowe dla Normalized Cuts + +**Cięcie grafu (graph cut)** — graf to zbiór węzłów (pikseli) połączonych krawędziami (z wagami = podobieństwo). „Ciąć graf" to znaleźć LINIĘ dzielącą węzły na grupy, tak aby krawędzie „przecięte" tą linią miały niską wagę (= łączyły niepodobne piksele), a krawędzie wewnątrz grup miały wysoką wagę (= łączyły podobne piksele). + +**Jak szukamy cięcia?** — Naiwnie: sprawdź WSZYSTKIE możliwe podziały → wykładnicza złożoność. Normalized Cuts zamienia problem na rozwiązanie „problemu wartości własnych" (eigenvalue problem) macierzy Laplacianu grafu. Drugi najmniejszy wektor własny wskazuje, które piksele należą do grupy A (wartości dodatnie) a które do B (wartości ujemne). + +**Dlaczego „znormalizowane" (normalized)?** — Zwykłe cięcie (min-cut) ma wadę: preferuje odcinanie MALUTKICH grup (1 piksel odcięty = małe cięcie). Normalizowanie dzieli koszt cięcia przez rozmiar grup → duże, zrównoważone segmenty. + +![Normalized Cuts: obraz jako graf, cięcie, algorytm krok po kroku](img/q23_normalized_cuts.png) + + Pseudokod Normalized Cuts (uproszczony): + # 1. Zbuduj macierz podobieństwa W + for each pair of pixels (i, j): + W[i,j] = exp(-|color_i - color_j|^2 / sigma^2) # jeśli sąsiedzi + W[i,j] = 0 # jeśli odlegli + + # 2. Macierz stopni D + D = diag(sum(W, axis=1)) # D[i,i] = suma wiersza i + + # 3. Rozwiąż problem wartości własnych + (D - W) * y = lambda * D * y + # Weź DRUGI najm. wektor własny y (pierwszy = trywialny) + + # 4. Podziel piksele + segment_A = {i : y[i] > 0} + segment_B = {i : y[i] <= 0} + + Mnemonik: „CIĘCIE sznurków" — piksele połączone sznurkami (mocne = podobne). + Tnij SŁABE sznurki → dwie grupy. Normalizacja = nie odcinaj samotnych pikseli. + +--- + +#### Pojęcia kluczowe dla sieci neuronowych + +**ReLU (Rectified Linear Unit)** — najpopularniejsza funkcja aktywacji w sieciach neuronowych. Wzór: ReLU(x) = max(0, x). Jeśli wejście jest ujemne → wynik = 0 (neuron „milczy"). Jeśli wejście jest dodatnie → wynik = x (neuron „przepuszcza" sygnał bez zmiany). Prosta, ale bardzo skuteczna — szybsza od starszych funkcji (sigmoid, tanh), bo nie wymaga obliczania exp(). + + ReLU(-3) = max(0, -3) = 0 ← neuron „wyłączony" + ReLU(0) = max(0, 0) = 0 ← na granicy + ReLU(2.5) = max(0, 2.5) = 2.5 ← neuron „włączony", przekazuje 2.5 + + Dlaczego nie po prostu f(x) = x (bez progu)? + Bo liniowość → cała sieć = jedna warstwa liniowa (tracisz głębokość). + ReLU jest NIELINIOWA (ma „zakręt" w 0) → pozwala sieci uczyć się + skomplikowanych wzorców. + +![ReLU: wykres funkcji, dlaczego ReLU, przykład numeryczny](img/q23_relu.png) + +**Iloczyn skalarny (dot product)** — operacja na dwóch wektorach (listach liczb) dająca JEDNĄ liczbę. Mnożysz odpowiednie elementy parami i sumujesz wyniki. W CNN konwolucja = iloczyn skalarny filtra × fragment obrazu. Duży wynik = wektory „podobne" (filtr pasuje do fragmentu). + + a = [1, 3, -2] b = [4, -1, 5] + a · b = 1·4 + 3·(-1) + (-2)·5 = 4 - 3 - 10 = -9 + + W konwolucji: + filtr = [-1, 0, 1, -1, 0, 1, -1, 0, 1] (spłaszczony 3×3) + fragment = [50, 50, 200, 50, 50, 200, 50, 50, 200] + dot = (-1)·50 + 0·50 + 1·200 + ... = 450 → duży = krawędź! + +![Iloczyn skalarny: definicja, geometryczna interpretacja, użycie w konwolucji](img/q23_dot_product.png) + +--- + +**Warstwa Fully Connected (FC, gęsta, dense)** — warstwa, w której KAŻDY neuron jest połączony z KAŻDYM wejściem. Obraz 7×7×512 (po konwolucjach) = 25 088 wartości. FC z 4096 neuronami = 25 088 × 4 096 = **~103 miliony wag**. Wady: (1) wymaga STAŁEGO rozmiaru wejścia (zawsze 7×7×512), (2) traci informację GDZIE coś jest (spłaszcza przestrzeń na wektor 1D). + +**Konwolucja (convolution)** — operacja przesuwania małego filtra (np. 3×3) po obrazie. W każdej pozycji oblicza iloczyn skalarny filtra × fragment obrazu → jedną liczbę. TE SAME wagi filtra użyte w KAŻDEJ pozycji → dzielenie parametrów. Zachowuje informację przestrzenną (GDZIE coś jest). + +**Conv 1×1 (konwolucja punktowa)** — filtr o rozmiarze 1×1 pikseli. „Patrzy" na JEDEN piksel, ale WSZYSTKIE kanały (np. 512). Działa jak FC, ale OSOBNO dla KAŻDEGO piksela → zachowuje mapę H×W. FCN zamienia FC na Conv 1×1: zamiast spłaszczyć 7×7×512 → 25 088 → FC, robi Conv1×1 na KAŻDYM z 7×7 pikseli × 512 kanałów → mapa 7×7×C (C = liczba klas). + +**Jak FCN zamienia FC na Conv 1×1?** — Klasyczny CNN: ostatnia mapa cech 7×7×512 → FLATTEN → wektor 25 088 → FC → 1000 klas → „to jest kot". FCN: ostatnia mapa cech H×W×512 → Conv1×1(512→C) → mapa H×W×C → upsample do pełnej rozdzielczości. Kluczowa różnica: NIE spłaszczamy → możemy przetwarzać obraz o DOWOLNYM rozmiarze. + +**Skip connections z encodera** — w encoder-decoder encoder zmniejsza obraz (pooling): 224→112→56→28→14. W tym procesie traci DETALE przestrzenne (dokładne krawędzie). Skip connections = „drogi na skróty" — cechy z wczesnych warstw encodera (pełne detali) są przekazywane WPROST do odpowiednich warstw decodera. Decoder wie CO i GDZIE. + +![FCN: warstwa FC vs Conv 1×1, konwolucja, skip connections](img/q23_fc_vs_conv1x1.png) + +--- + +**U-Net — dlaczego kształt „U"?** — Narysuj architekturę: encoder zmniejsza rozdzielczość (bloki idą w DÓŁ po lewej stronie), bottleneck jest na dole, decoder zwiększa rozdzielczość (bloki idą W GÓRĘ po prawej stronie). Wizualnie tworzy literę „U". „Encoder schodzi w dół" = każda warstwa encodera ma MNIEJSZĄ rozdzielczość (224→112→56→28), wizualizowane jako bloki o malejącym rozmiarze ułożone jeden pod drugim. + +**Concatenation (konkatenacja, złączenie)** — operacja „sklejania" dwóch tensorów wzdłuż osi kanałów. Jeśli encoder na poziomie 2 daje mapę 128×128×64 kanałów, a decoder na poziomie 2 daje mapę 128×128×64 kanałów, to concatenation = 128×128×**128** kanałów (64+64). Różni się od DODAWANIA (addition), które daje 128×128×64 (element-wise sum). Concatenation zachowuje WIĘCEJ informacji — sieć sama wybiera, które kanały wykorzystać. + + Dodawanie (ResNet-style): + encoder [a, b, c] + decoder [x, y, z] = [a+x, b+y, c+z] → 3 kanały + + Concatenation (U-Net-style): + encoder [a, b, c] ++ decoder [x, y, z] = [a, b, c, x, y, z] → 6 kanałów! + → więcej informacji, sieć sama zdecyduje co ważne + +![U-Net: architektura w kształcie U, skip connections z concatenation, encoder ↓ decoder ↑](img/q23_unet_arch.png) + + Mnemonik U-Net: „Litera U — w dół i w górę" — encoder schodzi ↓ (zmniejsza), + decoder wraca ↑ (zwiększa), między nimi mosty (skip = concat). + +--- + +**Receptive field (pole widzenia, pole recepcyjne)** — ile pikseli WEJŚCIOWYCH wpływa na JEDEN piksel wyjściowy. Konwolucja 3×3 → RF = 3×3. Dwie konwolucje 3×3 pod rząd → RF = 5×5 (druga widzi 3×3 fragmenty, z których każdy widział 3×3 → efektywnie 5×5). Większe RF = neuron widzi większy kontekst = lepiej rozumie co to za piksel. + +**Dlaczego większe RF jest lepsze?** — Pojedynczy piksel o jasności 150 może być fragmentem nieba LUB samochodu. Patrząc na otoczenie 3×3 → nadal nie wiesz. Patrząc na otoczenie 50×50 → widzisz budynki obok → „to droga!". Segmentacja wymaga KONTEKSTU globalnego. + +**Rate (współczynnik dylatacji)** — parametr atrous (dilated) convolution. Rate=1 = zwykła konwolucja (filtr dotyka sąsiadów). Rate=2 = filtr próbkuje co DRUGI piksel → RF rośnie z 3×3 do 5×5 przy TYCH SAMYCH 9 wagach. Rate=3 → RF = 7×7. Większy kontekst za darmo (bez dodatkowych parametrów). + +**Global Average Pooling (GAP)** — operacja redukcji: mapa cech H×W×C → 1×1×C. Dla KAŻDEGO kanału oblicza ŚREDNIĄ ze wszystkich H×W pikseli. Wynik: jeden wektor o wymiarze C, reprezentujący „średnią informację" z całego obrazu. RF = nieskończone (cały obraz). Używane w ASPP DeepLab jako jedna z równoległych gałęzi. + + Mapa cech 7×7×512: + Kanał 0: macierz 7×7 wartości → średnia → jedna liczba + Kanał 1: macierz 7×7 wartości → średnia → jedna liczba + ... + Kanał 511: macierz 7×7 wartości → średnia → jedna liczba + Wynik: wektor [avg₀, avg₁, ..., avg₅₁₁] → 1×1×512 + +![Receptive field: zwykła vs dilated konwolucja, rate, global average pooling](img/q23_receptive_field.png) + +--- + +**Transformer** — architektura sieci neuronowej zaproponowana w 2017 (Vaswani et al., „Attention Is All You Need"). Oryginalnie dla NLP (tłumaczenie), od 2020 (ViT — Vision Transformer) stosowana w wizji komputerowej. Kluczowy mechanizm: **self-attention** — każdy element (piksel/token) „pyta" WSZYSTKIE inne elementy: „jak bardzo jesteś ze mną powiązany?". Każdy element tworzy trzy wektory: Q (Query — czego szukam?), K (Key — co oferuję), V (Value — moja wartość). Attention = softmax(Q·Kᵀ / √d) · V. Koszt: O(n²) pamięci (n = liczba elementów). + +**SOTA (State Of The Art)** — najlepszy znany wynik na danym benchmarku (zbiorze testowym) w danym momencie. Np. „Mask2Former osiąga mIoU 57.8% na ADE20K — to aktualny SOTA". SOTA ciągle się zmienia — każdy nowy paper może pobić poprzedni rekord. + +![Transformer: CNN lokalny vs Transformer globalny, self-attention Q/K/V, SOTA](img/q23_transformer_attention.png) + +--- + +**mIoU (mean Intersection over Union)** — standardowa metryka segmentacji. Dla każdej klasy: IoU = (piksele poprawne ∩ ground truth) / (piksele poprawne ∪ ground truth). Potem średnia z klas. + + Klasa "samochód": predykcja=100 pikseli, GT=120, wspólne=80 + IoU = 80 / (100+120-80) = 80/140 = 0.571 = 57.1% + +**Dice Loss** — funkcja kosztu powiązana z IoU: 2·|A∩B| / (|A|+|B|). Popularna w segmentacji medycznej (dobrze radzi sobie z class imbalance). + +**Focal Loss** — modyfikacja cross-entropy redukująca wpływ łatwych przykładów, skupiająca uczenie na trudnych. Kluczowa przy class imbalance (np. 99% tła, 1% obiekt). + +--- + +### Problem: czym jest segmentacja obrazu? + +Segmentacja obrazu to **przypisanie etykiety klasy KAŻDEMU pikselowi** obrazu. Wynik: mapa segmentacji o tym samym rozmiarze co obraz wejściowy, gdzie każdy piksel ma etykietę (np. „samochód", „droga", „niebo"). + + Wejście: obraz 640×480 (RGB) = 307 200 pikseli + Wynik: mapa 640×480, każdy piksel → etykieta = 307 200 etykiet + + Obraz: [niebo niebo niebo niebo] + [niebo drzewo drzewo niebo] + [droga droga samochód droga] + [droga droga droga droga] + +**Czym segmentacja NIE jest:** + + Zadanie Wynik Granulacja + ────────────────────────────────────────────────────────────── + Klasyfikacja 1 etykieta na cały obraz obraz + Detekcja bounding box + klasa prostokąt + Segmentacja etykieta per piksel piksel + +**3 warianty segmentacji:** + +![Typy segmentacji obrazu](img/segmentation_types.png) + +| Wariant | Co robi | Przykład | +| ------------ | -------------------------------------------- | ------------------------------------------- | +| **Semantic** | klasa per piksel, bez rozróżniania instancji | wszystkie samochody = „samochód" | +| **Instance** | rozróżnia instancje tego samego obiektu | samochód#1, samochód#2 | +| **Panoptic** | semantic + instance razem | „stuff" (niebo) + „things" (samochód#1, #2) | + +--- + +### Strategie klasyczne + +Metody niewymagające uczenia maszynowego — oparte na ręcznie zdefiniowanych regułach (próg, podobieństwo, struktura grafu). + +| Metoda | Idea | Wada | Złożoność | Mnemonik | +| ------------------- | -------------------------------------------------------- | ---------------------------------- | ---------- | ------------------ | +| **Thresholding** | piksel > T → klasa 1, else → klasa 0 | tylko 2 klasy, proste sceny | O(n) | „PRÓG na bramce" | +| **Otsu** | automatyczny próg (min wariancja wewnątrzklasowa) | j.w. ale dobiera T sam | O(n·L) | „AUTO-bramkarz" | +| **Region Growing** | dodawaj sąsiednie piksele o podobnej wartości | over-segmentation, zależy od seeda | O(n) | „PLAMA atramentu" | +| **Watershed** | obraz = mapa wysokości, granice = granie gór | over-segmentation | O(n log n) | „ZALEWANIE terenu" | +| **Mean Shift** | iteracyjnie przesuwaj jądro do max gęstości | wolny | O(n²) | „KULKI toczą się" | +| **Normalized Cuts** | piksele = węzły grafu, minimalizuj znormalizowane cięcie | bardzo wolny | O(n³) | „CIĘCIE sznurków" | + +#### DIY Przykład — Thresholding (Otsu) krok po kroku + +Poniższy diagram pokazuje CAŁY pipeline progowania Otsu od obrazu wejściowego do wyniku. Obraz syntetyczny 64×64 z ciemnym kołem na jasnym tle — typowy przypadek bimodalny. + +![DIY Thresholding + Otsu: obraz → histogram bimodalny → progowanie → szukanie min σ² → pseudokod → wynik](img/q23_diy_thresholding.png) + + Pseudokod Otsu (Python-style): + best_T, min_var = 0, float('inf') + for T in range(256): + c0 = pixels[pixels <= T] # piksele ciemne + c1 = pixels[pixels > T] # piksele jasne + if len(c0) == 0 or len(c1) == 0: + continue + w0 = len(c0) / len(pixels) # udział klasy 0 + w1 = len(c1) / len(pixels) # udział klasy 1 + var = w0 * variance(c0) + w1 * variance(c1) # σ² wewnątrzklasowa + if var < min_var: + min_var = var + best_T = T + # best_T = optymalny próg (np. 128) + result = (pixels > best_T).astype(int) # binaryzacja + +**Wspólna wada klasycznych metod:** wymagają ręcznego doboru parametrów (próg, seed, kernel), nie uczą się cech z danych, słabe na złożonych obrazach naturalnych. + +--- + +### Sieci neuronowe (deep learning) + +Metody uczące się automatycznie rozpoznawać cechy z danych treningowych. Wszystkie oparte na architekturze **encoder-decoder** z wariacjami. + +**Wspólna idea encoder-decoder:** + + Encoder: obraz [224×224] → [112] → [56] → [28] → [14] (wyciąga CECHY) + Decoder: cechy [14] → [28] → [56] → [112] → [224×224] (odtwarza MAPĘ) + bottleneck + +| Sieć | Rok | Kluczowa innowacja | Use case | Mnemonik | +| --------------- | ---- | ----------------------------------------- | -------------------- | ----------------------- | +| **FCN** | 2015 | w pełni konwolucyjna + skip connections | pierwsza end-to-end | „FC → Conv 1×1" | +| **U-Net** | 2015 | U-shape + skip concat + data augmentation | segmentacja medyczna | „Litera U + mosty" | +| **DeepLab v3+** | 2018 | atrous (dilated) conv + ASPP | general-purpose | „DZIURY w filtrze" | +| **SegFormer** | 2021 | transformer encoder (self-attention) | SOTA lightweight | „WSZYSCY ze WSZYSTKIMI" | +| **Mask2Former** | 2022 | masked attention + unified architecture | SOTA universal | „WSZYSCY ze WSZYSTKIMI" | + +**FCN (Fully Convolutional Network):** + + Mnemonik: „FC → Conv 1×1 = otwieramy bramkę dla DOWOLNEGO rozmiaru" + Zwykły CNN: Conv → Conv → Pool → ... → FC → FC → "kot" + FCN: Conv → Conv → Pool → ... → Conv1×1 → Upsample → mapa pikseli + Innowacja: zamiana FC na Conv1×1 → wejście dowolnego rozmiaru + Skip connections: łączą cechy z encodera → zachowują detale przestrzenne + +**U-Net:** + + Mnemonik: „Litera U + mosty" — schodzisz w dół, wracasz w górę, + po drodze mosty (skip connections z concat) przenoszą detale. + Encoder (↓) Decoder (↑) + [64]────skip────→[64] ← skip connections = concatenation + [128]───skip───→[128] (przenosi detale z encodera do decodera) + [256]──skip──→[256] + [512]─skip─→[512] + [1024] ← bottleneck + Dlaczego medycyna? Działa dobrze z MAŁYMI zbiorami danych (data augmentation) + +**DeepLab v3+:** + + Mnemonik: „DZIURY w filtrze" — filtr dosłownie ma dziury (à trous), + przez co widzi dalej bez dodatkowych parametrów. + Zwykła konwolucja 3×3: [x][x][x] receptive field = 3 + Dilated (rate=2): [x][ ][x][ ][x] receptive field = 5, te same parametry! + ASPP: równolegle rate=6,12,18 → multi-scale features → łączenie + Efekt: widzi kontekst globalny BEZ zwiększania parametrów + +**Transformery (SegFormer, Mask2Former):** + + Mnemonik: „WSZYSCY ze WSZYSTKIMI" — każdy piksel rozmawia z KAŻDYM innym. + CNN: filtr 3×3 widzi LOKALNY kontekst (sąsiadów) + Transformer: self-attention widzi CAŁY obraz naraz + Cena: O(n²) pamięci (n = piksele), ale lepsze wyniki + +#### DIY Przykład — U-Net krok po kroku + +Poniższy diagram pokazuje CAŁY pipeline U-Net od obrazu wejściowego do mapy segmentacji. Obraz syntetyczny 64×64 z dwoma obiektami (koła) na jasnym tle. + +![DIY U-Net: obraz → encoder zmniejsza → bottleneck → decoder zwiększa + skip → mapa segmentacji → pseudokod](img/q23_diy_unet.png) + + Pseudokod U-Net (PyTorch-style): + # ENCODER — zmniejsza rozdzielczość, wyciąga cechy + e1 = conv_block(input, filters=64) # [64×64×64] + e2 = conv_block(maxpool(e1), filters=128) # [32×32×128] + e3 = conv_block(maxpool(e2), filters=256) # [16×16×256] + + # BOTTLENECK — najgłębsza warstwa + b = conv_block(maxpool(e3), filters=512) # [8×8×512] + + # DECODER — zwiększa rozdzielczość + skip connections (concat!) + d3 = conv_block(concat(upconv(b), e3), filters=256) # [16×16×256] + d2 = conv_block(concat(upconv(d3), e2), filters=128) # [32×32×128] + d1 = conv_block(concat(upconv(d2), e1), filters=64) # [64×64×64] + + # WYNIK — Conv 1×1 → mapa klas + output = conv_1x1(d1, n_classes=3) # [64×64×3] → argmax → [64×64] etykiety + +--- + +### Metryki i funkcje kosztu + +| Metryka/Loss | Wzór | Kiedy użyć | +| ------------------ | ----------------------------- | ------------------------------------ | +| **mIoU** | mean(IoU per klasa) | standardowy benchmark | +| **Pixel Accuracy** | poprawne / wszystkie | prosta, ale zła przy class imbalance | +| **Dice Loss** | 1 - 2·\|A∩B\| / (\|A\|+\|B\|) | segmentacja medyczna | +| **Focal Loss** | -α(1-p)^γ · log(p) | class imbalance (99% tła) | + +### Etymologia + +**Segmentacja** — łac. „segmentum" = odcięty kawałek; podział obrazu na regiony. **Otsu** — Nobuyuki Otsu (1979); automatyczny dobór progu. **Watershed** — metafora: woda spływająca z grani do dolin (z geografii). **U-Net** — Ronneberger et al. (Freiburg, 2015); „U" od kształtu architektury. **FCN** — Fully Convolutional Network (Long, Shelhamer, Darrell, 2015). **DeepLab** — Google (2015–2018); „Atrous" z fr. „à trous" = „z dziurami" (dilated convolutions). **mIoU** — mean Intersection over Union. + +### Jak zapamiętać + +**Super-mnemonik na kolejność algorytmów:** + + „Turyści Oglądają Rzekę, Wodospad, Morze, Nurt — Fotografują Uroczy Dwór Tajemnic" + + Klasyczne: Thresholding → Otsu → Region growing → Watershed → Mean shift → Normalized cuts + Neuronowe: FCN → U-Net → DeepLab → Transformer + +![Mnemoniki: karty z algorytmami segmentacji i ich skojarzeniami](img/q23_mnemonics.png) + +**Mnemoniki per algorytm — STRATEGIE KLASYCZNE:** + +| Algorytm | Mnemonik | Skojarzenie | +| ------------------- | --------------------------- | ------------------------------------------------------- | +| **Thresholding** | „PRÓG na bramce" | Bramkarz przepuszcza piksele > T, blokuje ≤ T | +| **Otsu** | „AUTO-bramkarz" | Sam sprawdza 256 progów, wybiera najlepszy (min σ²) | +| **Region Growing** | „PLAMA atramentu" | Kropla atramentu rozlewa się na podobne piksele (BFS) | +| **Watershed** | „ZALEWANIE terenu" | Woda zalewa doliny, granie gór = granice segmentów | +| **Mean Shift** | „KULKI toczą się do dołków" | Każda kulka → max gęstości, ile dołków = tyle segmentów | +| **Normalized Cuts** | „CIĘCIE sznurków" | Tnij słabe sznurki (krawędzie grafu), zachowaj silne | + +**Mnemoniki per algorytm — SIECI NEURONOWE:** + +| Sieć | Mnemonik | Skojarzenie | +| --------------- | ----------------------- | ------------------------------------------------------------ | +| **FCN** | „FC → Conv 1×1" | Otwiera bramkę dla dowolnego rozmiaru wejścia | +| **U-Net** | „Litera U + mosty" | Schodzisz ↓, wracasz ↑, mosty (skip concat) przenoszą detale | +| **DeepLab** | „DZIURY w filtrze" | Filtr ma dziury (à trous) → widzi dalej bez dodatkowych wag | +| **Transformer** | „WSZYSCY ze WSZYSTKIMI" | Każdy piksel pyta każdy inny (self-attention, O(n²)) | + +**Mnemoniki per metrykę:** + +- **mIoU** = „Nakładka / Suma" → intersection / union, uśrednione per klasa +- **Dice** = „Dwie nakładki / Razem" → 2·|A∩B| / (|A|+|B|) +- **Focal** = „Fokus na TRUDNYCH" → trudne piksele ważą więcej diff --git a/python_pkg/praca_magisterska_video/answers/pytanie_24.md b/python_pkg/praca_magisterska_video/answers/pytanie_24.md new file mode 100644 index 0000000..48af933 --- /dev/null +++ b/python_pkg/praca_magisterska_video/answers/pytanie_24.md @@ -0,0 +1,1045 @@ +## PYTANIE 24: Detekcja obiektów + +**Problem, metody klasyczne, deep learning. Jak zbudować detektor z klasyfikatora?** + +--- + +### Tło pojęciowe — słowniczek + +**Detekcja obiektów (object detection)** — zadanie widzenia komputerowego: zlokalizuj obiekty na obrazie (bounding box) i przypisz im klasy (samochód, pieszo, kot...). Wynik: lista (klasa, prostokąt, pewność). Trudniejsze niż klasyfikacja (→ cały obraz, 1 label), ale łatwiejsze niż segmentacja (→ per piksel). + + Klasyfikacja: "To zdjęcie zawiera kota" + Detekcja: "Kot w prostokącie (50,30)-(200,180), pewność 95%" + Segmentacja: Maska pikseli kota + +**Bounding box (prostokąt ograniczający, bbox)** — prostokąt opisujący położenie obiektu. Zwykle: (x_min, y_min, x_max, y_max) lub (x_center, y_center, width, height). Przybliżenie — obiekty rzadko są prostokątne. + +**Confidence (pewność)** — wynik 0-1 mówiący jak pewny jest detektor, że wykrył obiekt danej klasy. Zwykle próg np. 0.5: detekcje poniżej odrzucane. + +--- + +**CNN (Convolutional Neural Network, konwolucyjna sieć neuronowa)** — typ sieci neuronowej zaprojektowany specjalnie do przetwarzania OBRAZÓW. Używany w KAŻDYM nowoczesnym detektorze (R-CNN, YOLO, SSD, DETR). Kluczowa idea: zamiast łączyć KAŻDY piksel z KAŻDYM neuronem (→ miliardy parametrów), CNN używa MAŁYCH filtrów (np. 3×3 piksele) przesuwanych po obrazie. Dzięki temu: + +1. Mało parametrów (filtr 3×3 = 9 wag, niezależnie od rozmiaru obrazu) +2. Wykrywa lokalne wzorce (krawędzie, rogi, tekstury) +3. Inwariantność na przesunięcie (kot w lewym rogu = kot w prawym rogu) + + Dlaczego CNN a nie zwykła sieć neuronowa? + Obraz 224×224×3 = 150 528 pikseli. + Zwykła sieć (FC): 150 528 × 4096 neuronów = 616 MILIONÓW wag w 1 warstwie! + CNN: filtr 3×3×3 = 27 wag, przesuwany po CAŁYM obrazie → 27 wag zamiast 616M! + + Mnemonik: CNN = „Czytaj Nie Naraz" — nie bierzesz całego obrazu naraz, + tylko małe fragmenty (filtry 3×3), krok po kroku. + +**Konwolucja (convolution)** — podstawowa operacja CNN: mały filtr (macierz np. 3×3) przesuwa się po obrazie, w każdej pozycji mnoży element-po-elemencie z fragmentem obrazu i sumuje → jedna liczba na wyjściu. Wynik = „feature mapa" — mapa pokazująca GDZIE na obrazie dany wzorzec jest obecny. + + Przykład liczbowy: + Fragment obrazu 3×3: Filtr 3×3: Wynik (1 piksel feature mapy): + [1 2 3] [-1 0 1] + [4 5 6] × [-1 0 1] = 1(-1)+2(0)+3(1)+4(-1)+5(0)+6(1)+7(-1)+8(0)+9(1) + [7 8 9] [-1 0 1] = (-1+0+3) + (-4+0+6) + (-7+0+9) = 6 + + Ten filtr wykrywa PIONOWE KRAWĘDZIE (liczy różnicę prawa-lewa strona). + Duży wynik (6) = silna krawędź. Wynik ≈ 0 = brak krawędzi. + Filtr przesuwa się po CAŁYM obrazie → cała mapa cech. + + Pseudokod konwolucji: + def convolve(image, filter_3x3): + output = zeros(image.height - 2, image.width - 2) + for y in range(1, image.height - 1): + for x in range(1, image.width - 1): + patch = image[y-1:y+2, x-1:x+2] # wycinek 3×3 + output[y-1][x-1] = sum(patch * filter) # iloczyn + suma + return output + +**Filtr / Kernel** — mała macierz wag (np. 3×3, 5×5) uczona AUTOMATYCZNIE podczas treningu. CNN ma WIELE filtrów — każdy uczy się wykrywać INNY wzorzec. 64 filtry w jednej warstwie → 64 map cech. + + KLUCZOWA RÓŻNICA: w HOG cechy projektuje CZŁOWIEK. + W CNN filtry uczy się SIEĆ SAMA — to główna przewaga deep learning! + + Warstwa conv z 64 filtrami 3×3: + Filtr 1: nauczył się wykrywać pionowe krawędzie + Filtr 2: nauczył się wykrywać poziome krawędzie + Filtr 3: nauczył się wykrywać rogi + ... + Filtr 64: jakiś inny wzorzec pomocny w rozpoznawaniu + +**Feature map (mapa cech)** — wynik zastosowania JEDNEGO filtra do obrazu. Jasne piksele = „tu jest ten wzorzec". 64 filtry → 64 map cech → tensor [H × W × 64]. Feature mapy to WEWNĘTRZNA REPREZENTACJA tego, co sieć „widzi" na obrazie. + + Hierarchia cech w CNN (każda warstwa coraz bardziej abstrakcyjna): + Warstwa 1: krawędzie, gradienty (jak HOG!) + Warstwa 2: rogi, proste tekstury + Warstwa 3: fragmenty obiektów (oko, koło, ucho) + Warstwa 4+: całe obiekty (twarz = oczy+nos+usta, samochód = koła+okna+dach) + + Mnemonik: „K-R-F-O" = „Każdy Rycerz Znajduje Obiekt" + (Krawędzie → Rogi → Fragmenty → Obiekty) + +**Pooling (łączenie / podpróbkowanie)** — warstwa ZMNIEJSZAJĄCA rozmiar feature mapy. Najczęstsza: **max pooling 2×2** — z każdego bloku 2×2 pikseli zachowaj MAKSIMUM. Wynik: mapa 2× mniejsza w każdym wymiarze (= 4× mniej pikseli), ale zachowuje najsilniejsze cechy. + + Feature map 4×4: Po Max Pool 2×2: + [1 3 | 2 1] [3 2] ← max(1,3,0,3)=3 max(2,1,1,2)=2 + [0 3 | 1 2] [4 3] ← max(0,4,1,2)=4 max(1,0,3,1)=3 + ───────────── + [0 4 | 1 0] Rozmiar: 4×4 → 2×2 (4× mniej danych!) + [1 2 | 3 1] Zachowane: najsilniejsze cechy z każdego bloku + + Dlaczego max pooling? + 1. Mniej pikseli = mniej obliczeń w następnych warstwach + 2. Większe „pole widzenia" (receptive field) — warstwa „widzi" większy fragment + 3. Odporność na małe przesunięcia: obiekt ±1px → ten sam max + +**Stride (krok)** — o ile pikseli filtr przesuwa się za jednym krokiem. Stride=1: co 1 piksel (wyjście duże). Stride=2: co 2 piksele (wyjście 2× mniejsze). Max pool 2×2 ze stride 2 = typowy pooling. + +**FC (Fully Connected layer, warstwa w pełni połączona)** — warstwa, w której KAŻDY neuron jest połączony z KAŻDYM wyjściem poprzedniej warstwy. W CNN zwykle na KOŃCU sieci: feature mapy (3D) → spłaszczone do wektora 1D → FC klasyfikuje. + + CNN: Conv → Pool → Conv → Pool → [Flatten] → FC(4096) → FC(1000) → "kot" + ↑ ↑ + spłaszcz 3D→1D 1000 klas (ImageNet) + + FC = „warstwa decyzyjna" — łączy cechy z CAŁEGO obrazu w jedną decyzję. + Mnemonik: FC = „Full Connection" — każdy z każdym, jak klasa każdy-z-każdym. + Problem FC: DUŻO parametrów (np. 25088 × 4096 = 102M wag w VGG-16!) + +**Forward pass (przejście w przód)** — JEDNO przetworzenie danych przez sieć od wejścia do wyjścia. Obraz wchodzi → przechodzi przez Conv, Pool, FC → wychodzi predykcja. Nie aktualizuje wag (to backward pass / backpropagation = uczenie). + + Forward pass CNN (czasy na GPU): + Jeden obraz przez ResNet-50: ~5ms + R-CNN: 2000 regionów × 5ms = 10 SEKUND (dlatego był wolny!) + Fast R-CNN: 1 forward pass cały obraz + ROI Pool = ~200ms (50× szybciej!) + +**ReLU (Rectified Linear Unit)** — funkcja aktywacji: f(x) = max(0, x). Przepuszcza wartości dodatnie, zeruje ujemne. Standard w CNN — stosowana PO KAŻDEJ warstwie konwolucyjnej. + + Wejście: [-3, 5, -1, 2, 0, -7, 4] + ReLU: [ 0, 5, 0, 2, 0, 0, 4] + + Dlaczego potrzebna? Bez ReLU sieć = seria mnożeń macierzy = JEDNA liniowa + transformacja → nie potrafi uchwycić złożonych wzorców. + ReLU dodaje NIELINIOWOŚĆ → sieć aproksymuje DOWOLNĄ funkcję. + +**Softmax** — funkcja na WYJŚCIU klasyfikatora: zamienia surowe wyniki (logits) na prawdopodobieństwa sumujące się do 1. + + Logits: [2.0, 1.0, 0.1] + Softmax: [0.66, 0.24, 0.10] ← e^2.0 / (e^2.0 + e^1.0 + e^0.1) ≈ 0.66 + Klasy: ["kot", "pies", "ryba"] + → „66% szans, że to kot" + +**Tensor** — wielowymiarowa tablica liczb. Uogólnienie wektora i macierzy. + + Skalar = 0D tensor: 5 + Wektor = 1D: [1, 2, 3] + Macierz = 2D: [[1,2],[3,4]] + Obraz RGB = 3D: [224 × 224 × 3] ← wysokość × szerokość × kanały + Batch obrazów = 4D: [32 × 224 × 224 × 3] ← 32 obrazy naraz + Wyjście YOLO = 3D: [7 × 7 × 30] ← siatka × predykcje + +**Architektura CNN — pełny przykład (AlexNet, wygrał ImageNet 2012):** + +![CNN — od obrazu do predykcji](img/q24_cnn_architecture.png) + + ROZMIARY MALEJĄ: 224 → 55 → 27 → 13 → 6 (kompresja przestrzenna) + KANAŁY ROSNĄ: 3 → 96 → 256 → 384 → 256 (coraz więcej wyuczonych cech) + +--- + +**Backbone (kręgosłup / sieć bazowa)** — duża, pretrenowana sieć CNN (np. ResNet-50, VGG-16) używana jako „ekstraktor cech". Backbone przetwarza obraz → feature mapa. Na wierzch dodaje się GŁOWICĘ (head) specyficzną dla zadania. + + Analogia: backbone = SILNIK samochodu, head = KAROSERIA. + Ten sam silnik (ResNet) w różnych karoseriach: + Sedan → klasyfikacja: FC head → "kot" + SUV → detekcja: RPN + ROI Pool head → bbox + klasa + Pickup → segmentacja: dekoder head → maska pikseli + + Backbone PRETRENOWANY na ImageNet (miliony obrazów). + Head TRENOWANY od zera na konkretnym zadaniu (detekcja, segmentacja). + +**Detection head (głowa detekcyjna)** — warstwy dodane NA WIERZCH backbone'u. Predykują klasy obiektów + pozycje bbox. W Faster R-CNN: RPN + ROI Pool + FC. W YOLO: warstwy conv + wyjście S×S×(B×5+C). + +**ResNet, VGG, AlexNet — popularne backbone'y:** + + Sieć Rok Warstw Parametrów Top-5 ImageNet Innowacja + ───────────────────────────────────────────────────────────────────── + AlexNet 2012 8 60M 84.7% Pierwsza głęboka CNN + VGG-16 2014 16 138M 92.7% Małe filtry 3×3 + ResNet-50 2015 50 25M 96.4% Skip connections + + Mnemonik: A → V → R = „Architektura Bardzo Rezylientna" (2012 → 2014 → 2015) + + Skip connection (ResNet): y = F(x) + x + Wejście bloku DODAWANE do wyjścia → gradient nie zanika + → można trenować 50-152 warstw (bez skip: >20 warstw = DEGRADACJA!) + +**ImageNet** — ogromny zbiór danych: 14M obrazów, 1000 klas (pies, samolot, gitara...). Standard pretrenowania w computer vision. ILSVRC (coroczne zawody) — AlexNet wygrał 2012 → rewolucja deep learning. + +**Transfer learning (uczenie transferowe)** — weź sieć pretrenowaną na dużym zbiorze (ImageNet), użyj do INNEGO zadania (detekcja, segmentacja). Backbone „wie" jak wyglądają krawędzie i kształty — trzeba tylko nauczyć nowej głowicy. + + Krok po kroku: + 1. ResNet-50 pretrenowany na ImageNet (1000 klas, miliony obrazów) + 2. Odtnij warstwę FC (klasyfikujse 1000 klas ImageNet) ← WYRZUĆ + 3. Dodaj nową głowicę detekcji (bbox + 80 klas COCO) ← NOWA + 4. Trenuj głowicę na danych detekcyjnych (COCO/VOC) + 5. Opcjonalnie: fine-tune = odmroź backbone, ucz z MAŁYM learning rate + + Dlaczego działa? Cechy niskiego poziomu (krawędzie, tekstury) SĄ UNIWERSALNE. + Kot, samochód, twarz — wszystko ma krawędzie i tekstury! + +**Fine-tuning (dostrajanie)** — forma transfer learning: odmrażasz backbone i uczysz CAŁĄ sieć z MAŁYM learning rate, żeby subtelnie dopasować cechy do nowego zadania. + +**COCO (Common Objects in Context)** — benchmark detekcji: 330K obrazów, 80 klas (samochód, osoba, pies...), 1.5M bboxów. Standard oceny detektorów. + +**Pascal VOC (Visual Object Classes)** — starszy benchmark: 20 klas. Używany w oryginalnym YOLO i R-CNN. + +**mAP (mean Average Precision)** — główna metryka jakości detekcji. Łączy trafność klasy z trafnością lokalizacji. + + mAP@0.5: detekcja „trafna" jeśli IoU ≥ 0.5 (≥50% pokrycia z prawdą) + mAP@0.5:0.95: średnia po progach 0.5, 0.55, ..., 0.95 (dużo surowsza) + + Faster R-CNN (COCO): mAP ≈ 42% + YOLOv8-X (COCO): mAP ≈ 53% + +**End-to-end (od końca do końca)** — cała sieć trenowana jako JEDNOŚĆ, jeden loss, jeden trening. Przeciwieństwo: R-CNN miał ODDZIELNIE Selective Search + CNN + SVM = 3 osobne kroki. Faster R-CNN = end-to-end → komponenty uczą się WSPÓŁPRACOWAĆ → lepsze wyniki. + +**FPN (Feature Pyramid Network)** — technika łączenia feature map z RÓŻNYCH warstw backbone'u. Wczesne warstwy (wysoka rozdzielczość) → małe obiekty. Późne warstwy (niska rozdzielczość) → duże obiekty. FPN łączy obie → wykrywa obiekty WSZYSTKICH rozmiarów. + +![FPN (Feature Pyramid Network)](img/q24_fpn.png) + +--- + +**Klasyfikator (classifier)** — model przypisujący etykietę do wejścia. Np. CNN trenowany na ImageNet: obraz → „kot" (+ prawdopodobieństwo). Klasyfikator nie mówi GDZIE jest obiekt — mówi tylko CO jest na obrazie. Pytanie brzmi: jak z takiego modelu zbudować detektor? + +**Sliding window (okno przesuwane)** — najprostsza metoda budowy detektora z klasyfikatora: wytnij prostokątny fragment obrazu (wiele rozmiarów, wiele pozycji), każdy fragment sklasyfikuj. Jeśli „pozytywny" → detekcja. Ekstremalnie wolne: tysiące fragmentów × klasyfikacja per fragment. + + [okno 64×64] przesuwa się po obrazie 640×480: + (640-64)×(480-64) ≈ ~240 000 pozycji × wiele skal = MILIONY klasyfikacji! + +--- + +**HOG (Histogram of Oriented Gradients)** — klasyczny deskryptor cech wizualnych. Rozbijmy nazwę: + +- **Gradient** — w kontekście obrazu to „kierunek i siła zmiany jasności" w danym pikselu. Oblicza się go jako różnicę jasności sąsiednich pikseli. Gradient wskazuje KRAWĘDZIE — tam, gdzie jasność zmienia się szybko. + + Piksel: [50] [50] [200] ← nagły skok jasności + Gradient w x: 0 150 ← duży gradient = KRAWĘDŹ! + Gradient w y: obliczany analogicznie (góra/dół) + Kierunek krawędzi: arctan(gy/gx) ← np. 0° = pionowa, 90° = pozioma + +- **Orientacja (Oriented)** — kierunek gradientu. Gradient ma KĄTP (0°–180°): krawędź pionowa = ~0°, pozioma = ~90°, ukośna = ~45°. + +- **Histogram** — zliczenie „ile pikseli ma gradient w danym kierunku". Dla komórki 8×8 pikseli liczymy histogram 9 binów (co 20°: 0°, 20°, 40°, ..., 160°). + +- **HOG pipeline krok po kroku:** + + Krok 1: Oblicz gradient KAŻDEGO piksela (Gx, Gy → magnitude + direction) + Gx = pixel[x+1] - pixel[x-1] + Gy = pixel[y+1] - pixel[y-1] + magnitude = √(Gx² + Gy²) + direction = arctan(Gy / Gx) + + Krok 2: Podziel obraz na komórki (cells) 8×8 pikseli + Okno 64×128 → 8×16 komórek + + Krok 3: Dla każdej komórki stwórz histogram 9 binów (0°-180°, co 20°) + Każdy piksel w komórce „głosuje" na bin odpowiadający jego kierunkowi + z wagą = magnitude (silniejsze krawędzie głosują mocniej) + + Krok 4: Normalizuj histogramy w blokach 2×2 komórek (16×16 px) + → odporność na zmiany oświetlenia + + Krok 5: Połącz wszystkie histogramy w jeden wektor cech + Okno 64×128: (8-1)×(16-1) = 7×15 = 105 bloków × 4 komórki × 9 binów = 3780 cech + + Wynik: wektor 3780 liczb = „odcisk palca" kształtu w oknie + Sylwetka człowieka → charakterystyczny wzorzec kierunków krawędzi + +**Pseudokod HOG:** + + def compute_hog(window_64x128): + gradients = compute_gradients(window) # Gx, Gy per pixel + magnitudes = sqrt(Gx**2 + Gy**2) + directions = arctan2(Gy, Gx) * 180 / pi # kąt w stopniach + + hog_vector = [] + for block in sliding_blocks_2x2(cells_8x8): + block_hist = [] + for cell in block.four_cells(): + hist = zeros(9) # 9 binów + for pixel in cell.pixels(): + bin_idx = int(directions[pixel] / 20) + hist[bin_idx] += magnitudes[pixel] + block_hist.append(hist) + block_hist = normalize(concatenate(block_hist)) # L2-norm + hog_vector.extend(block_hist) + + return hog_vector # 3780-dim vector + +**SVM (Support Vector Machine)** — klasyczny klasyfikator binarny (2 klasy: „tak/nie", „pieszy/nie-pieszy"). Pomysł: + +- Dane treningowe to punkty w przestrzeni wielowymiarowej (np. wektory HOG 3780-dim) +- Każdy punkt ma etykietę: +1 (pozytywna klasa) lub -1 (negatywna) +- SVM szuka **hiperpłaszczyzny** (w 2D to linia, w 3D to płaszczyzna) najlepiej SEPARUJĄCEJ dwie klasy + +**Czym jest hiperpłaszczyzna?** W 2D: linia dzieląca punkty na dwie grupy. W 3D: płaszczyzna. W N wymiarach: (N-1)-wymiarowa „ściana". + +**Margines (margin)** — odległość od hiperpłaszczyzny do najbliższego punktu danych. SVM MAKSYMALIZUJE margines → najlepsza generalizacja. + +**Support Vectors** — punkty danych NAJBLIŻSZE hiperpłaszczyźnie. To one „podpierają" (support) margines i definiują pozycję hiperpłaszczyzny. Reszta punktów jest nieistotna! Nazwa: „wektory nośne" — bo to wektory cech, które „niosą" decyzję. + +![SVM — hiperpłaszczyzna i margines](img/q24_svm_hyperplane.png) + +**HOG+SVM — klasyczny pipeline detekcji pieszych:** + +![HOG + SVM pipeline detekcji pieszych](img/hog_svm_pipeline.png) + + 1. Sliding window (okno 64×128) przesuwa się po obrazie + 2. Dla każdej pozycji okna: + a) Oblicz HOG → wektor 3780 cech + b) SVM klasyfikuje: „pieszy" (+1) lub „nie-pieszy" (-1) + 3. NMS (Non-Maximum Suppression) → usuń duplikaty + 4. Wynik: lista bounding boxów z detekcjami pieszych + +**Viola-Jones (2001)** — przełomowy detektor twarzy w CZASIE RZECZYWISTYM. Trzy kluczowe innowacje wyjasnione szczegółowo: + +**Haar features (cechy Haarowe)** — najprostsze cechy obrazowe: prostokąty podzielone na jasną i ciemną część. Wartość cechy = (suma pikseli jasnych) − (suma pikseli ciemnych). Proste, ale skuteczne — wykrywają kontrasty typowe dla twarzy. + +![Cechy Haar — typy i zastosowanie na twarzy](img/q24_haar_features.png) + + Dlaczego działa na TWARZACH? + - Oczy CIEMNIEJSZE niż czoło → cecha "krawędź pozioma" daje dużą wartość + - Nos JAŚNIEJSZY niż policzki → cecha "linia pionowa" daje dużą wartość + - Twarz = charakterystyczna KOMBINACJA takich kontrastów! + + Ile cech? W oknie 24×24 pikseli: ponad 160 000 możliwych cech Haar + (różne rozmiary × różne pozycje). AdaBoost wybiera ~200 NAJLEPSZYCH. + +**Integral Image (obraz całkowy)** — precomputed tabela pozwalająca obliczyć sumę pikseli w DOWOLNYM prostokącie w O(1) — stały czas, niezależnie od rozmiaru! To dlatego Haar features liczą się tak szybko. + + Jak? Integral Image[x,y] = suma WSZYSTKICH pikseli od (0,0) do (x,y). + +![Integral Image — suma prostokąta w O(1)](img/q24_integral_image.png) + + Zawsze 4 odczyty z tabeli → O(1)! + Czy prostokąt ma 4 piksele czy 4 MILIONY — czas TEN SAM! + Bez Integral Image: O(w×h) — suma 1000×1000 = milion operacji. + Z Integral Image: O(1) — 4 operacje. ZAWSZE. + + Pseudokod: + def integral_image(img): + II = zeros_like(img) + for y in range(H): + for x in range(W): + II[y][x] = img[y][x] + II[y-1][x] + II[y][x-1] - II[y-1][x-1] + return II + + def rect_sum(II, x1, y1, x2, y2): # O(1) zawsze! + return II[y2][x2] - II[y1-1][x2] - II[y2][x1-1] + II[y1-1][x1-1] + +**AdaBoost (Adaptive Boosting)** — algorytm uczenia maszynowego łączący wiele SŁABYCH klasyfikatorów w jeden SILNY. Słaby = niewiele lepszy od losowego (>50% trafień). AdaBoost iteracyjnie: + +1. Trenuj słaby klasyfikator (np. 1 cecha Haar + próg: "czy wartość > 1200?") +2. Sprawdź, które przykłady ŹLE sklasyfikował +3. Nadaj źle sklasyfikowanym WIĘKSZĄ wagę → następny klasyfikator SKUPI się na nich +4. Powtórz 200× → suma ważona 200 słabych klasyfikatorów ≈ silny klasyfikator + + Intuicja: jak PANEL EKSPERTÓW, z których każdy zna się na JEDNEJ rzeczy. + Ekspert 1: "czy okolice oczu ciemne?" (trafność 55%) + Ekspert 2: "czy nos jaśniejszy niż policzki?" (trafność 60%) + Ekspert 3: "czy brwi ciemne?" (trafność 53%) + ... + 200 ekspertów razem → trafność >95%! + Mnemonik: AdaBoost = "ADAptacyjnie BOOSTuj" słabe modele do silnego. + +**Cascade (kaskada klasyfikatorów)** — genialna optymalizacja szybkości: zamiast sprawdzać WSZYSTKIE 200 cech na każdym oknie, użyj KASKADY etapów. Każdy etap = prosty klasyfikator, który szybko ODRZUCA "na pewno nie-twarz". + +![Viola-Jones — kaskada klasyfikatorów (SITO)](img/q24_viola_jones_cascade.png) + + Mnemonik: kaskada = "SITO" — coraz drobniejsze oczka, + na początku odpada piach, na końcu zostaje ZŁOTO (twarz). + + Pseudokod kaskady: + def cascade_classify(window): + for stage in cascade_stages: # etap 1, 2, ..., 25 + score = stage.evaluate(window) # oblicz kilka cech Haar + if score < stage.threshold: # za niski wynik + return "NIE-TWARZ" # SZYBKIE odrzucenie! + return "TWARZ" # przeszło WSZYSTKIE etapy + +--- + +![Ewolucja detektorów: R-CNN → Faster R-CNN → YOLO](img/q24_rcnn_evolution.png) + +**R-CNN family (two-stage detectors)** — dwuetapowe: najpierw generuj propozycje regionów, potem klasyfikuj każdy region. Nazwa: Region-based CNN. + +**Selective Search (wyszukiwanie selektywne)** — klasyczny algorytm (NIE sieć neuronowa!) generowania propozycji regionów. Zamiast MILIONÓW pozycji okna (sliding window), inteligentnie łączy podobne fragmenty obrazu i proponuje ~2000 prostokątów, w których MOGĄ być obiekty. + + Algorytm krok po kroku: + 1. Over-segmentation: podziel obraz na ~1000 małych regionów (superpixele) + (na podstawie koloru i tekstury — algorytm Felzenszwalb) + 2. Powtarzaj aż zostanie 1 region: + a) Znajdź 2 najbardziej PODOBNE sąsiednie regiony: + - podobny kolor? (histogram kolorów) + - podobna tekstura? (histogram gradientów) + - pasujący rozmiar? (preferuj łączenie MAŁYCH regionów) + b) Połącz je w jeden → zapamiętaj bounding box nowego regionu + 3. Zebrane bbox-y ze WSZYSTKICH kroków → ~2000 propozycji + + Sliding window: ~500 000 okien → 99.9% to "tło" → marnujesz czas + Selective Search: ~2 000 regionów → ~50% zawiera coś → 250× wydajniej + RPN (Faster R-CNN): ~300 propozycji → sieć neuronowa (najszybciej!) + +**Czym jest "region proposal" (propozycja regionu)?** — prostokąt, w którym MOŻE być obiekt. Dużo mniej niż sliding window (2000 zamiast milionów), ale każda propozycja ma WYSOKIE prawdopodobieństwo trafienia obiektu. + +**R-CNN (2014, Ross Girshick)** — pierwszy detektor oparty na CNN. Pipeline: + + Krok 1: Selective Search → ~2000 regionów-kandydatów (prostokątów) + Krok 2: Dla KAŻDEGO z 2000 regionów: + a) Wytnij prostokąt z obrazu, przeskaluj do 224×224 + b) Przepuść przez CNN (np. AlexNet) → wektor cech 4096-dim + c) SVM klasyfikuje: „samochód? kot? tło?" + Krok 3: Bbox regression — doprecyzuj pozycję prostokąta + Krok 4: NMS — usuń duplikaty + + Problem: 2000 × CNN forward pass = 50 SEKUND na obraz! (2000 razy odpalasz CNN) + Dlaczego tak wolno? Bo CNN liczy cechy na KAŻDYM wyciętym regionie OSOBNO, + choć regiony się częściowo nakładają → redundantne obliczenia + +**Fast R-CNN (2015)** — kluczowa optymalizacja: przepuść cały obraz przez CNN RAZ, uzyskaj "mapę cech" (feature map). Potem wytnij cechy regionów z tej mapy (ROI Pooling), zamiast odpalać CNN 2000 razy. + +**ROI (Region of Interest, region zainteresowania)** — prostokątny fragment feature mapy odpowiadający propozycji regionu na oryginalnym obrazie. Np. Selective Search zaproponował bbox (100,50)-(200,150) na obrazie 800×600 → odpowiadający ROI na feature mapie (po redukcji 16× przez pooling) to mniej więcej (6,3)-(12,9). + +**ROI Pooling (pooling regionu zainteresowania)** — operacja zamieniająca ROI o DOWOLNYM rozmiarze na tensor o STAŁYM rozmiarze (np. 7×7). Konieczne, bo warstwa FC wymaga stałego rozmiaru wejścia! + + Problem: region 1 = 14×10 na feature mapie, region 2 = 8×6 → RÓŻNE! + Warstwa FC wymaga np. 7×7 → STAŁY rozmiar. + + Rozwiązanie — ROI Pooling: + 1. Weź ROI (np. 14×10) z feature mapy + 2. Podziel go na siatkę 7×7 (= 7 wierszy × 7 kolumn) + Każda komórka obejmuje ok. 2×1.4 pikseli feature mapy + 3. W każdej komórce weź MAX (jak max pooling) + 4. Wynik: tensor 7×7 — STAŁY rozmiar niezależnie od oryginalnego ROI! + +![ROI Pooling](img/q24_roi_pooling.png) + + Kluczowa sztuczka Fast R-CNN: + CNN raz na CAŁY obraz → JEDNA feature mapa → ROI Pool 2000 regionów z TEJ SAMEJ mapy + (zamiast 2000× odpalać CNN jak w R-CNN!) + Przyspieszenie: ~2 sec/obraz (vs 50 sec) → 25× szybciej! + + Pseudokod ROI Pooling: + def roi_pool(feature_map, roi_bbox, output_size=7): + roi = feature_map[roi_bbox] # wycinek z feature mapy + h, w = roi.shape + cell_h, cell_w = h // output_size, w // output_size + output = zeros(output_size, output_size) + for i in range(output_size): + for j in range(output_size): + cell = roi[i*cell_h:(i+1)*cell_h, j*cell_w:(j+1)*cell_w] + output[i][j] = max(cell) # max pooling w komórce + return output # stały rozmiar 7×7! + + CNN raz → feature map → ROI Pool 2000 regionów → FC → klasy + bbox + +**Bbox regression (regresja prostokąta ograniczającego)** — sieć predykuje nie bezpośrednie współrzędne bbox, ale PRZESUNIĘCIA (offsets) od propozycji: Δx, Δy (przesunięcie środka), Δw, Δh (zmiana szerokości/wysokości). + + Propozycja (z RPN/Selective Search): (x=100, y=80, w=60, h=90) ← przybliżone + Predykcja regresji: (Δx=+5, Δy=-3, Δw=+10, Δh=+5) + Ostateczny bbox: (x=105, y=77, w=70, h=95) ← dokładniejsze! + + Dlaczego offsets a nie współrzędne bezpośrednio? + Łatwiejsze zadanie! Sieć poprawia przybliżony prostokąt O TROCHĘ, + zamiast zgadywać lokalizację od zera. + Mnemonik: bbox regression = "GPS korekta" — masz przybliżoną pozycję, + poprawiasz o parę metrów w prawo i w górę. + +**Faster R-CNN (2015)** — ostatni krok ewolucji: zastąp Selective Search (osobny algorytm) siecią neuronową! **RPN (Region Proposal Network)** — mała sieć przesuwana po feature mapie, która w KAŻDEJ pozycji predykuje: "czy tu jest obiekt?" + proponuje bbox. Wszystko w jednej sieci, end-to-end. + + Pipeline Faster R-CNN: + Obraz → CNN backbone (np. ResNet) → Feature Map → RPN (proposals) → ROI Pool → FC → klasy + bbox + + RPN krok po kroku: + Feature mapa [40×60×256] ← z backbone + ↓ Filtr 3×3 przesuwa się po feature mapie + ↓ W KAŻDEJ pozycji (x,y) rozważ k=9 "anchor boxes": + + 9 anchorów = 3 rozmiary × 3 proporcje: + ┌───┐ ┌─────┐ ┌───────┐ ← 128×128, 256×256, 512×512 + │ │ │ │ │ │ × proporcje 1:1, 1:2, 2:1 + └───┘ └─────┘ └───────┘ + + ↓ Dla KAŻDEGO z 9 anchorów sieć predykuje: + - P(obiekt) = prawdopodobieństwo, że tu jest obiekt + - (Δx, Δy, Δw, Δh) = przesunięcie bbox względem anchora + + 40×60 = 2400 pozycji × 9 anchorów = 21 600 potencjalnych propozycji! + → Weź ~300 z najwyższym P(obiekt) → ROI Pool → FC → klasy + bbox + + Faster R-CNN: ~5 fps (~0.2 sec/obraz) — 250× szybciej niż R-CNN! + + Mnemonik ewolucji R-CNN: "CORAZ MNIEJ MARNOWANIA" + R-CNN: Selective Search + 2000×CNN = 50s → WOLNE + Fast R-CNN: Selective Search + 1×CNN + ROI Pool = 2s → lepiej + Faster R-CNN: RPN (w sieci!) + 1×CNN + ROI Pool = 0.2s → 250× szybciej! + +--- + +**One-stage detectors** — klasyfikacja i lokalizacja w jednym przejściu (bez osobnego etapu propozycji). Szybsze, ale historycznie mniej precyzyjne. + +**YOLO (You Only Look Once, 2016)** — rewolucyjny pomysł: „po co robić 2 etapy, skoro można w JEDNYM?" Obraz dzielony jest na siatkę S×S (np. 13×13 = 169 komórek). Każda komórka odpowiada za wykrycie obiektu, którego ŚRODEK wpada w tę komórkę. Każda komórka predykuje: + +- B bounding boxów × (x, y, w, h, confidence) = lokalizacja + „pewność, że tu jest obiekt" +- C prawdopodobieństw klas = „jaki to obiekt?" + Jedno przejście przez sieć → WSZYSTKIE detekcje naraz. 45-155 fps! + +![YOLO — detekcja jednoetapowa (siatka S×S)](img/q24_yolo_grid.png) + +**SSD (Single Shot MultiBox Detector, 2016)** — ulepsza YOLO przez multi-scale feature maps: predykcje z WIELU warstw CNN, każda o innej rozdzielczości. Wczesne warstwy (wysoka rozdzielczość) wykrywają MAŁE obiekty; późne warstwy (niska rozdzielczość) wykrywają DUŻE. Anchor boxes predefiniowane na każdej skali. + +**Anchor box (kotwica)** — predefiniowany prostokąt o określonym kształcie/proporcji (np. 1:1, 1:2, 2:1). Sieć NIE predykuje bbox od zera — predykuje PRZESUNIĘCIE (offset) od najbliższego anchora. Łatwiejsze zadanie! Wiele anchorów → pokrycie różnych kształtów obiektów (osoby = wysoki prostokąt, samochód = szeroki). + +![Anchor boxes — predefiniowane kształty](img/q24_anchor_boxes.png) + +**Anchor-free** — nowoczesne podejście (FCOS, YOLOv8): bezpośrednia predykcja środka i wymiarów, bez predefiniowanych anchorów. Prostsza architektura, mniej hyperparametrów. + +**Transformer** — architektura sieci neuronowej pierwotnie z NLP (2017, "Attention is All You Need"), ale skutecznie zaadaptowana do wizji komputerowej (ViT, DETR). Kluczowy mechanizm: **self-attention** — każdy element wejścia "patrzy" na WSZYSTKIE inne elementy i decyduje, które są dla niego ważne. + + W tekście: słowo "bank" patrzy na "rzeka" i "pieniądze" → + attention decyduje: "w tym zdaniu chodzi o brzeg RZEKI, nie bank pieniędzy" + + W obrazie (DETR): fragment obrazu "patrzy" na inne fragmenty → + attention: "ta łapa jest częścią TEGO kota, a nie tamtego psa" + +**Self-attention (samo-uwaga)** — mechanizm: dla każdego elementu oblicz "uwagę" do KAŻDEGO innego elementu. Matematycznie: Query × Key → wagi attention → ważona suma Values. + + Uproszczony pseudokod: + def self_attention(features): # features = N elementów + Q = features × W_query # Query: "czego szukam?" + K = features × W_key # Key: "co oferuję?" + V = features × W_value # Value: "jaką informację niosę?" + + attention = softmax(Q × K^T / sqrt(d)) # macierz N×N: "kto ważny dla kogo" + output = attention × V # ważona kombinacja wartości + return output + + Złożoność: O(n^2) — każdy element z każdym → wolne dla dużych obrazów. + Dlatego DETR wolniej się TRENUJE niż YOLO (ale architektura jest PROSTSZA). + +**DETR (DEtection TRansformer, 2020)** — model Facebooka stosujący Transformer do detekcji. Radykalnie prostszy pipeline: BRAK anchorów, BRAK NMS! Sieć predykuje bezpośrednio ZESTAW N obiektów (np. N=100). + +![DETR — Transformer do detekcji](img/q24_detr_pipeline.png) + + "Object queries" = 100 wyuczonych wektorów, każdy "szuka" jednego obiektu. + Obraz z 5 obiektami → 5 queries dopasuje się do obiektów, + 95 queries zwróci klasę "brak obiektu" (empty set). + + Pseudokod DETR: + def detr_forward(image): + features = backbone(image) # ResNet → feature mapa + encoded = transformer_encoder(features) # self-attention na feat. mapie + queries = learnable_queries(100) # 100 wyuczonych zapytań + decoded = transformer_decoder(queries, encoded) # cross-attention + predictions = [] + for q in decoded: + cls = classify(q) # "samochód" / "pies" / "brak" + box = regress(q) # (x, y, w, h) + predictions.append((cls, box)) + return predictions # 100 predykcji (większość = brak) + + Mnemonik DETR: "Detekcja Eliminująca Trikowe Redundancje" + → bez NMS, bez anchorów, prosty pipeline. + +**Hungarian matching (dopasowanie węgierskie)** — algorytm używany podczas TRENINGU DETR. Problem: sieć daje 100 predykcji, na obrazie jest 5 obiektów — która predykcja odpowiada któremu obiektowi? Algorytm węgierski znajduje OPTYMALNE dopasowanie 1:1 minimalizując łączny koszt (błąd klasy + błąd bbox). + + Predykcje DETR: Ground truth: + pred_1: "samochód" gt_1: "samochód" (bbox A) + pred_2: "pies" gt_2: "pies" (bbox B) + pred_3: "brak" + ... Hungarian matching: + pred_100: "brak" pred_1 ↔ gt_1 (najlepsze dopasowanie!) + pred_2 ↔ gt_2 + reszta ↔ "brak obiektu" + + Efekt: BRAK DUPLIKATÓW → BRAK NMS! + (Każdy obiekt dopasowany do DOKŁADNIE jednej predykcji) + +--- + +**NMS (Non-Maximum Suppression, tłumienie nie-maksymalnych)** — algorytm post-processingu usuwający ZDUPLIKOWANE detekcje. Problem: detektor generuje WIELE nakładających się bbox dla tego samego obiektu. NMS zachowuje NAJLEPSZĄ i usuwa resztę. Jedyny detektor BEZ NMS = DETR. + + Algorytm NMS krok po kroku: + Wejście: detekcje posortowane malejąco po confidence + [bbox_1 conf=0.95], [bbox_2 conf=0.90], [bbox_3 conf=0.85], [bbox_4 conf=0.40] + + Pseudokod NMS: + def nms(detections, iou_threshold=0.5): + detections.sort(by=confidence, descending=True) + keep = [] + while detections: + best = detections.pop(0) # weź najlepszą + keep.append(best) # ZACHOWAJ ją + detections = [d for d in detections + if iou(best, d) < iou_threshold] # usuń nakładające + return keep + + Krok 1: Weź bbox_1 (0.95) → ZACHOWAJ + Krok 2: IoU(bbox_1, bbox_2) = 0.82 > 0.5 → USUŃ (duplikat tego samego kota!) + IoU(bbox_1, bbox_3) = 0.75 > 0.5 → USUŃ (duplikat!) + IoU(bbox_1, bbox_4) = 0.10 < 0.5 → ZACHOWAJ (INNY obiekt!) + Krok 3: Wynik: [bbox_1, bbox_4] — 2 unikalne obiekty + + Mnemonik: NMS = "Najlepszy Ma Się dobrze" — zachowaj najlepszą, usuń resztę. + +**IoU (Intersection over Union)** — miara nakładania dwóch prostokątów. IoU = pole przecięcia / pole sumy. Wartości: 0.0 (nie nakładają się) do 1.0 (identyczne). + +![IoU (Intersection over Union)](img/q24_iou_diagram.png) + + IoU = pole(∩) / pole(A ∪ B) + = pole(∩) / (pole(A) + pole(B) − pole(∩)) + + Przykład liczbowy: + A = [0, 0, 100, 100] → pole = 10 000 + B = [50, 50, 150, 150] → pole = 10 000 + ∩ = [50, 50, 100, 100] → pole = 2 500 + IoU = 2500 / (10000 + 10000 − 2500) = 2500 / 17500 ≈ 0.14 + + IoU > 0.5 w NMS → "to TEN SAM obiekt" → usuń słabszą detekcję + IoU > 0.5 w mAP → "detekcja TRAFNA" → poprawna lokalizacja + +--- + +**Jak zbudować detektor z klasyfikatora? Trzy podejścia (+ bonus):** + +1. **Sliding window** — wytnij, sklasyfikuj, NMS. Bardzo wolne (miliony klasyfikacji). +2. **Region proposals + klasyfikator** — Selective Search → ~2000 regionów → klasyfikuj + NMS. Wolne ale działa (= R-CNN). +3. **Fine-tune backbone** — weź pretrained classifier (ResNet z ImageNet), dodaj detection head (bbox regression + cls), dotrenuj na danych detekcyjnych. **Najlepsza jakość** (= Faster R-CNN, YOLO, SSD). +4. **Transformer (DETR)** — bez anchorów, bez NMS, predykcja zestawu obiektów end-to-end. + +--- + +### Problem: czym jest detekcja obiektów? + +Detekcja obiektów to **lokalizacja** (gdzie?) i **klasyfikacja** (co?) obiektów na obrazie. Wynik: lista krotek **(klasa, bounding box, confidence)**. + + Wejście: zdjęcie ulicy + Wynik: [("samochód", [50,30,200,180], 0.95), + ("pieszy", [300,100,350,250], 0.88), + ("rower", [400,150,480,300], 0.72)] + +**Porównanie z innymi zadaniami:** + +![Klasyfikacja vs Detekcja vs Segmentacja](img/q24_detection_tasks.png) + +--- + +### Metody klasyczne + +Metody sprzed deep learningu — ręcznie projektowane cechy (features) + klasyczny klasyfikator. + +| Metoda | Rok | Cechy | Klasyfikator | Szybkość | Use case | +| --------------- | ---- | ------------------------------- | ---------------- | ------------------- | ----------------- | +| **HOG + SVM** | 2005 | Histogram of Oriented Gradients | SVM | wolna (~1 fps) | detekcja pieszych | +| **Viola-Jones** | 2001 | Haar features + Integral Image | AdaBoost cascade | real-time (30+ fps) | detekcja twarzy | + +#### HOG + SVM (Dalal & Triggs, 2005) — krok po kroku + +**Mnemonik kroków HOG: „GÓRA KOCHA BOGATYCH NARCIARZY" → Gradienty → Orientacja → Komórki → Bloki → Normalizacja** + +![HOG + SVM pipeline detekcji pieszych](img/q24_hog_svm_pipeline.png) + +**Krok 1 — Gradienty (G jak GÓRA):** Oblicz gradient KAŻDEGO piksela. Gradient = „siła i kierunek zmiany jasności". Tam, gdzie jasność skacze (np. 50→200), jest krawędź. + + Przykład liczbowy: + Piksele w wierszu: [50, 50, 200] + Gx = pixel[x+1] − pixel[x−1] = 200 − 50 = 150 ← silna krawędź pionowa! + Gy = analogicznie w pionie + Siła: magnitude = √(Gx² + Gy²) = √(150² + 0²) = 150 + Kierunek: direction = arctan(Gy/Gx) = arctan(0/150) = 0° (krawędź pionowa) + +**Krok 2 — Orientacja (O jak KOCHA):** Każdy piksel głosuje na kierunek swojej krawędzi. 9 „koszyków" (binów) co 20°: 0°, 20°, 40°, …, 160°. Głos ważony SIŁĄ gradientu (silniejsza krawędź = mocniejszy głos). + + Piksel z magnitude=150, direction=10°: + Głosuje na bin 0° (z wagą proporcjonalną do bliskości) i bin 20° + Piksel z magnitude=30, direction=85°: + Głosuje na bin 80° i bin 100° (słabsza krawędź = słabszy głos) + +**Krok 3 — Komórki (K jak BOGATYCH):** Podziel okno (64×128 px) na komórki 8×8 pikseli = 8×16 = 128 komórek. Dla KAŻDEJ komórki stwórz histogram 9 binów — to jej „odcisk palca kierunkowości krawędzi". + +![HOG — kroki obliczania cech](img/q24_hog_gradient_steps.png) + +**Krok 4 — Bloki (B jak NARCIARZY):** Grupuj komórki w bloki 2×2 (= 16×16 px). Przesuwaj blok z krokiem 1 komórki. Okno 64×128 → (8−1)×(16−1) = 7×15 = 105 bloków. + +**Krok 5 — Normalizacja (N):** Dla KAŻDEGO bloku (4 komórki × 9 binów = 36 wartości) wykonaj normalizację L2 → odporność na zmiany oświetlenia. 105 bloków × 36 = **3780 cech** → wektor HOG. + + Pseudokod: + def compute_hog(window_64x128): + Gx = pixel[x+1] - pixel[x-1] # gradient poziomy + Gy = pixel[y+1] - pixel[y-1] # gradient pionowy + mag = sqrt(Gx**2 + Gy**2) # siła + dir = arctan2(Gy, Gx) * 180 / pi # kierunek 0°-180° + + hog = [] + for block_2x2 in sliding_blocks(cells_8x8): + block_hist = [] + for cell in block_2x2: # 4 komórki + hist = [0]*9 # 9 binów + for px in cell.pixels: # 64 piksele + bin = int(dir[px] / 20) # który bin? + hist[bin] += mag[px] # ważone głosowanie + block_hist += hist + block_hist = L2_normalize(block_hist) # normalizacja! + hog += block_hist + return hog # wektor 3780 cech → do SVM + +**Krok 6 — SVM klasyfikuje:** Wektor 3780 cech → SVM odpowiada: „pieszy" (+1) lub „tło" (−1). + +![SVM — hiperpłaszczyzna i margines](img/q24_svm_hyperplane.png) + + Mnemonik SVM: „LINIA MAKSYMALNEGO ODDECHU" + SVM = linia (hiperpłaszczyzna) z MAKSYMALNYM marginesem. + Jak MOST nad rzeką — im szerszy, tym bezpieczniejszy (lepiej generalizuje). + +**Krok 7 — NMS:** Usuń duplikaty (wiele okien wykryło tego samego pieszego → zachowaj najlepsze). + + Mnemonik PEŁNEGO pipeline'u HOG+SVM: „GOKBN-SN" + → Gradienty → Orientacja → Komórki → Bloki → Normalizacja → SVM → NMS + = „Grasz Ostro, Kumplu? Bądź Naturalny, Szybko Nabierz (wprawy)!" + +--- + +#### Viola-Jones (2001) — krok po kroku + +**Mnemonik 3 innowacji: „HIC" → Haar + Integral Image + Cascade** + +**Innowacja 1 — Haar features (H):** Prostokąty dzielone na jasną i ciemną część. Wartość = Σ(jasna) − Σ(ciemna). Proste, ale wykrywają kontrasty typowe dla twarzy. + +![Cechy Haar — typy i zastosowanie na twarzy](img/q24_haar_features.png) + + Pseudokod cechy Haar: + def haar_edge_vertical(img, x, y, w, h): + left_sum = sum_pixels(img, x, y, x+w//2, y+h) # jasna połówka + right_sum = sum_pixels(img, x+w//2, y, x+w, y+h) # ciemna połówka + return left_sum - right_sum # duża wartość = silna krawędź + + Mnemonik: Haar = „Hej, A tu jest Różnica?" + Cechy Haar pytają: „Czy lewa strona JAŚNIEJSZA niż prawa?" + +**Innowacja 2 — Integral Image (I):** Precomputed tabela: suma DOWOLNEGO prostokąta w O(1) — 4 odczyty z tabeli, niezależnie od rozmiaru! + +![Integral Image — suma prostokąta w O(1)](img/q24_integral_image.png) + + Pseudokod: + def build_integral_image(img): + II = zeros(H, W) + for y in range(H): + for x in range(W): + II[y][x] = img[y][x] + II[y-1][x] + II[y][x-1] - II[y-1][x-1] + return II + + def rect_sum(II, x1, y1, x2, y2): # ZAWSZE O(1)! + return II[y2][x2] - II[y1-1][x2] - II[y2][x1-1] + II[y1-1][x1-1] + + Mnemonik: Integral Image = „4 Odczyty I Gotowe!" = 4OIG + Jak czytanie z gotowej tabeli: nie liczymy, tylko odczytujemy! + +**Innowacja 3 — Cascade (C):** Kaskada etapów — szybkie odrzucanie „na pewno nie-twarz". + +![Viola-Jones — kaskada klasyfikatorów (SITO)](img/q24_viola_jones_cascade.png) + + Pseudokod: + def cascade_classify(window): + for stage in [stage_1, stage_2, ..., stage_25]: + score = sum(stage.weights[i] * haar_feature[i](window) + for i in stage.features) + if score < stage.threshold: + return "NIE-TWARZ" # szybkie odrzucenie! + return "TWARZ" # przeszło WSZYSTKIE etapy + + Mnemonik: Cascade = „SITO z coraz drobniejszymi oczkami" + Etap 1: sito o dużych oczkach → odpada piach (oczywiste nie-twarze) + Etap 25: sito najdrobniejsze → zostaje ZŁOTO (twarz) + 99% okien odpada w pierwszych 3 etapach → REAL-TIME! + +**Pełny pipeline Viola-Jones:** + + 1. Sliding window (24×24) po obrazie w wielu skalach + 2. Integral Image (preprocessing, O(n) — raz) + 3. Dla każdego okna: kaskada (Haar + AdaBoost, najczęściej odrzuci w 1-3 etapie) + 4. NMS na detekcjach → wynik + + Mnemonik pipeline'u: „SIKN" = Sliding → Integral → Kaskada → NMS + = „Szybko Identyfikuj Kształty Niezwykłe!" + +--- + +### Deep learning + +**Two-stage detectors (dwuetapowe)** — najpierw generuj propozycje regionów, potem klasyfikuj. + +| Model | Rok | Propozycje | Szybkość | Innowacja | +| ---------------- | ---- | ------------------------ | -------------- | ----------------------- | +| **R-CNN** | 2014 | Selective Search (~2000) | 50 sec/img (!) | CNN per region | +| **Fast R-CNN** | 2015 | Selective Search | ~2 sec/img | CNN raz + ROI Pooling | +| **Faster R-CNN** | 2015 | RPN (w sieci!) | ~5 fps | Region Proposal Network | + + Ewolucja R-CNN: + R-CNN: [Selective Search] → 2000 × [CNN] → 2000 × [SVM] = 50s WOLNE! + Fast R-CNN: [CNN raz] → [ROI Pool 2000 regionów] → [FC] = 2s lepiej + Faster R-CNN:[CNN] → [RPN generuje propozycje] → [ROI Pool] → [FC] = 0.2s! + +![Ewolucja detektorów: R-CNN → Faster R-CNN](img/q24_rcnn_evolution.png) + +**One-stage detectors (jednoetapowe)** — klasyfikacja i lokalizacja w JEDNYM przejściu. + +| Model | Rok | Szybkość | Innowacja | +| ---------- | ---- | ---------- | --------------------------- | +| **YOLO** | 2016 | 45-155 fps | siatka S×S, jedno przejście | +| **SSD** | 2016 | 46-59 fps | multi-scale feature maps | +| **YOLOv8** | 2023 | 100+ fps | anchor-free, SOTA | +| **DETR** | 2020 | ~40 fps | transformer, bez NMS | + + YOLO: + Obraz [416×416] → siatka 13×13 → każda komórka predykuje: + - B bounding boxów (pozycja + rozmiar + confidence) + - C klas (prawdopodobieństwa) + Jedno forward pass → WSZYSTKIE detekcje naraz → NMS → wynik + +**Two-stage vs One-stage:** + +![Two-stage vs One-stage — porównanie](img/q24_two_vs_one_stage.png) + +--- + +### Jak zbudować detektor z klasyfikatora? + +Masz wytrenowany klasyfikator (np. ResNet na ImageNet: obraz → „kot"). Jak go użyć do **lokalizacji** obiektów? + +**Mnemonik 3 podejść: „SRF" = „Sliding → Region → Fine-tune" = „Szukaj Ręcznie, Finalnie optymalizuj!"** + +![Jak zbudować detektor z klasyfikatora? — 3 podejścia](img/q24_detector_from_classifier.png) + +--- + +#### Podejście 1 — Sliding Window (najprostsze, NAJWOLNIEJSZE) + +**Idea:** Wycinaj prostokątne fragmenty obrazu, KAŻDY pokaż klasyfikatorowi, zbierz pozytywne. + +**Mnemonik: „WYCINAJ i PYTAJ" — jak wycinanie ciasteczek: koło po kole, aż cały obraz pokryty.** + +![Sliding Window — najprostsze podejście](img/q24_sliding_window.png) + + Pseudokod: + def sliding_window_detect(image, classifier, window_size=64, step=8): + detections = [] + for scale in [0.5, 0.75, 1.0, 1.5, 2.0]: # 5 skal + resized = resize(image, scale) + for y in range(0, resized.height - window_size, step): + for x in range(0, resized.width - window_size, step): + window = resized[y:y+window_size, x:x+window_size] + label, confidence = classifier.predict(window) + if label != "tło" and confidence > 0.5: + # przelicz współrzędne na oryginał + bbox = (x/scale, y/scale, + (x+window_size)/scale, (y+window_size)/scale) + detections.append((label, bbox, confidence)) + return nms(detections) # usuń duplikaty + +**Dlaczego wiele skal?** Obiekty mają różne rozmiary — kot blisko = duży, kot daleko = mały. Okno 64×64 nie złapie kota 200×200. + + Obliczenia dla obrazu 640×480: + Pozycje na skali 1.0: (640-64)/8 × (480-64)/8 = 72 × 52 = 3 744 + × 5 skal = 18 720 okien + × klasyfikacja ResNet (~10ms/obraz na GPU) = ~3 minuty + × na CPU (~100ms/obraz) = ~30 minut na 1 obraz! + ⚠ NIEPRAKTYCZNE dla zastosowań real-time + +**Wady:** (1) Ekstremalnie wolne. (2) Stały kształt okna — obiekty nie są kwadratowe. (3) ~99.9% okien to „tło" → marnowanie czasu. + +--- + +#### Podejście 2 — Region Proposals + Klasyfikator (= R-CNN) + +**Idea:** Zamiast milionów okien, inteligentnie zaproponuj ~2000 regionów, w których MOGĄ być obiekty, i tylko te sklasyfikuj. + +**Mnemonik: „INTELIGENTNE CIĘCIE" — zamiast kroić cały tort na milion kawałków, wytnij tylko tam, gdzie widzisz wiśnie (obiekty).** + + Pseudokod (= R-CNN): + def region_proposal_detect(image, classifier): + # Krok 1: Selective Search — inteligentnie generuj regiony + proposals = selective_search(image) # ~2000 prostokątów + detections = [] + + # Krok 2: Dla KAŻDEGO regionu — clasificuj + for bbox in proposals: # ~2000 iteracji (nie milion!) + crop = image[bbox] # wytnij region + crop = resize(crop, 224, 224) # rozmiar wymagany przez CNN + features = cnn_backbone(crop) # ResNet → wektor 2048 cech + label, conf = svm_classify(features) # SVM: "samochód? kot? tło?" + if label != "tło" and conf > 0.5: + detections.append((label, bbox, conf)) + + # Krok 3: bbox regression — doprecyzuj pozycje + for det in detections: + det.bbox += bbox_regressor(det.features) # Δx, Δy, Δw, Δh + + return nms(detections) # Krok 4: usuń duplikaty + +**Dlaczego 2000 a nie milion?** Selective Search łączy podobne fragmenty obrazu (kolor, tekstura) bottom-up. Wynik: ~2000 „mądrych" propozycji, z których ~50% zawiera coś (vs 0.1% w sliding window). + + Porównanie z sliding window: + Sliding Window: ~18 000 okien × 10ms = ~3 min + Proposals: ~2 000 regionów × 10ms = ~20 sec ← 9× szybciej + ALE wciąż 2000 × forward pass CNN → dlatego powstał Fast R-CNN! + +**Wady:** (1) Selective Search jest osobnym algorytmem (nie end-to-end). (2) 2000 × forward pass CNN = wciąż wolno. (3) SVM trenowany OSOBNO od CNN. + +--- + +#### Podejście 3 — Fine-tune backbone + detection head (NAJLEPSZE) + +**Idea:** Weź pretrenowany klasyfikator, ODETNIJ głowicę klasyfikacyjną (FC 1000 klas), zastąp ją DWOMA nowymi głowicami: (1) głowica klasyfikacji → klasa obiektu, (2) głowica regresji → pozycja bbox. + +**Mnemonik: „PRZESZCZEP GŁOWY" — ten sam silnik (backbone), nowa głowa (detection head).** + + Pseudokod (= Faster R-CNN / YOLO w uproszczeniu): + # KROK 1: Weź pretrenowany klasyfikator + resnet = load_pretrained("resnet50_imagenet") # 1000 klas ImageNet + + # KROK 2: Odetnij starą głowicę klasyfikacji + backbone = resnet.layers[:-2] # ZACHOWAJ: Conv1...Conv5 (ekstraktor cech) + # WYRZUĆ: FC(1000) + Softmax + + # KROK 3: Dodaj nowe głowice detekcji + class DetectionHead: + def __init__(self): + self.cls_head = Linear(2048, num_classes) # "samochód? kot? tło?" + self.bbox_head = Linear(2048, 4) # Δx, Δy, Δw, Δh + + def forward(self, features): + cls = softmax(self.cls_head(features)) # P(klasa) + bbox = self.bbox_head(features) # przesunięcie bbox + return cls, bbox + + # KROK 4: Zamroź backbone, trenuj głowice na danych detekcyjnych + for image, gt_boxes, gt_labels in coco_dataset: + features = backbone(image) # pretrenowane cechy (zamrożone) + cls, bbox = detection_head(features) + loss = cls_loss(cls, gt_labels) + bbox_loss(bbox, gt_boxes) + loss.backward() # aktualizuj TYLKO detection_head + + # KROK 5 (opcja): Fine-tune — odmroź backbone z MAŁYM learning rate + backbone.unfreeze() + optimizer = SGD(lr=0.0001) # 10× mniejszy niż dla głowicy! + # trenuj jak w kroku 4, ale teraz backbone też się uczy + +**Dlaczego to działa?** Pretrenowany backbone na ImageNet „wie", jak wyglądają krawędzie, tekstury, kształty. Te cechy są UNIWERSALNE — przydają się zarówno do klasyfikacji „złota rybka vs samolot" jak i do detekcji „samochód na zdjęciu z drona". + + Transfer learning w liczbach: + Trenowanie od zera na COCO (330K obrazów): ~12h na 8×V100 GPU + Fine-tune pretrained ResNet-50: ~4h na 8×V100 GPU ← 3× szybciej! + Fine-tune osiąga mAP ~42%, od zera ~38% ← lepsze wyniki! + +**Pełny przykład w PyTorch (Faster R-CNN z pretrained backbone):** + + import torchvision + from torchvision.models.detection import fasterrcnn_resnet50_fpn + + # Gotowy detektor z pretrained backbone! + model = fasterrcnn_resnet50_fpn(pretrained=True) + + # Custom: zmiana na 5 klas (zamiast 91 COCO) + num_classes = 5 # 4 obiekty + tło + in_features = model.roi_heads.box_predictor.cls_score.in_features + model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes) + + # Trening: + model.train() + for images, targets in dataloader: + loss_dict = model(images, targets) # cls_loss + bbox_loss + total_loss = sum(loss_dict.values()) + total_loss.backward() + optimizer.step() + + # Inferencja: + model.eval() + predictions = model([test_image]) + # predictions = [{'boxes': tensor, 'labels': tensor, 'scores': tensor}] + # boxes = [[x1,y1,x2,y2], ...], labels = [1, 3, ...], scores = [0.95, 0.88, ...] + +--- + +#### Podsumowanie — porządek od NAJGORSZEGO do NAJLEPSZEGO: + + Podejście Okien Czas/obraz Jakość Rok Przykład + ────────────────────────────────────────────────────────────────────────── + Sliding Window ~milion ~30 min niska - (teoria) + Region Proposals ~2000 ~20-50 sec średnia 2014 R-CNN + Fine-tune + RPN ~300 ~0.2 sec wysoka 2015 Faster R-CNN + One-stage 1×siatka ~7-22 ms wysoka 2016+ YOLO, SSD + Transformer N queries ~25 ms wysoka 2020 DETR + + Mnemonik porządku: „SRFTD" = „Sliding → Region → Fine-tune → Transformer → (Done!)" + = „Szukaj Ręcznie, Finalnie Transformer (Detekuje!)" + +--- + +### NMS (Non-Maximum Suppression) — post-processing + +![NMS — usuwanie duplikatów](img/q24_nms_steps.png) + + Detektor generuje WIELE nakładających się bbox dla jednego obiektu: + [bbox1, 0.95], [bbox2, 0.90], [bbox3, 0.85] — wszystkie na tym samym kocie + + Pseudokod NMS: + def nms(detections, iou_threshold=0.5): + detections.sort(by=confidence, descending=True) + keep = [] + while detections: + best = detections.pop(0) # weź najlepszą + keep.append(best) # ZACHOWAJ + detections = [d for d in detections + if iou(best, d) < iou_threshold] # usuń nakładające + return keep + + Krok po kroku (przykład): + 1. Sortuj: [0.95, 0.90, 0.85, 0.40] + 2. Weź bbox₁ (0.95) → ZACHOWAJ + 3. IoU(bbox₁, bbox₂) = 0.82 > 0.5 → USUŃ (duplikat!) + IoU(bbox₁, bbox₃) = 0.75 > 0.5 → USUŃ (duplikat!) + IoU(bbox₁, bbox₄) = 0.10 < 0.5 → ZACHOWAJ (INNY obiekt!) + 4. Wynik: [bbox₁, bbox₄] — 2 unikalne obiekty + +![IoU (Intersection over Union)](img/q24_iou_diagram.png) + + Mnemonik NMS: „Najlepszy Ma Się dobrze" — zachowaj najlepszą, resztę wyrzuć + Mnemonik IoU: „Ile pokrycia Ustalono?" — pole(∩) / pole(A∪B) + +### Etymologia + +**CNN** — Convolutional Neural Network (sieć z konwolucjami). **YOLO** — You Only Look Once (Joseph Redmon et al., 2016). **R-CNN** — Region-based CNN (Ross Girshick, 2014). **HOG** — Histogram of Oriented Gradients (Dalal & Triggs, 2005). **SVM** — Support Vector Machine (Vapnik, 1995). **Viola-Jones** — Paul Viola + Michael Jones (2001). **DETR** — DEtection TRansformer (Facebook AI, 2020). **SSD** — Single Shot MultiBox Detector (Liu et al., 2016). **NMS** — Non-Maximum Suppression; tłumienie nie-maksymalnych detekcji. **ROI** — Region of Interest (region zainteresowania). **RPN** — Region Proposal Network (sieć propozycji regionów). **FPN** — Feature Pyramid Network (piramida cech). **IoU** — Intersection over Union (przecięcie przez sumę). **FC** — Fully Connected (w pełni połączona). **ReLU** — Rectified Linear Unit (wyprostowana jednostka liniowa). **mAP** — mean Average Precision (średnia precyzja). + +### Jak zapamiętać + +- **CNN = „Czytaj Nie Naraz"** — małe filtry 3×3 przesuwane po obrazie, nie cały obraz naraz +- **Hierarchia CNN: „K-R-F-O" = „Każdy Rycerz Znajduje Obiekt"** — Krawędzie → Rogi → Fragmenty → Obiekty +- **FC = „Full Connection"** — każdy z każdym, warstwa decyzyjna na końcu CNN +- **Backbone = SILNIK samochodu** — ten sam silnik (ResNet), różne karoserie (klasyfikacja/detekcja/segmentacja) +- **Backbone'y: A→V→R = „Architektura Bardzo Rezylientna"** — AlexNet (2012) → VGG (2014) → ResNet (2015) +- **Transfer learning = „PRZESZCZEP GŁOWY"** — nie ucz się od zera, przenieś wiedzę z ImageNet, zmień głowicę +- **HOG kroki: „GOKBN" = „Grasz Ostro, Kumplu? Bądź Naturalny"** — Gradienty → Orientacja → Komórki → Bloki → Normalizacja +- **SVM = „LINIA MAKSYMALNEGO ODDECHU"** — margines jak most: im szerszy, tym bezpieczniej +- **Viola-Jones: „HIC" = Haar + Integral Image + Cascade** +- **Haar = „Hej, A tu jest Różnica?"** — porównuje jasne i ciemne prostokąty +- **Integral Image = „4 Odczyty I Gotowe" (4OIG)** — suma dowolnego prostokąta O(1) +- **Kaskada = „SITO"** — piach odpada wcześnie, złoto (twarz) zostaje na końcu +- **Viola-Jones pipeline: „SIKN" = „Szybko Identyfikuj Kształty Niezwykłe"** — Sliding → Integral → Kaskada → NMS +- **AdaBoost = „ADAptacyjnie BOOSTuj"** — słabe modele razem = silny +- **Selective Search** — inteligentne łączenie regionów zamiast milionów okien +- **ROI Pooling** — dowolny rozmiar → stały rozmiar (siatkowanie + max) +- **Bbox regression = „GPS korekta"** — popraw przybliżoną pozycję o Δx, Δy, Δw, Δh +- **Ewolucja R-CNN: „CORAZ MNIEJ MARNOWANIA"** — R-CNN (50s) → Fast (2s) → Faster (0.2s) +- **YOLO = „You Only Look Once"** — jednoetapowy, szybki, siatka S×S +- **Faster R-CNN = CNN + RPN + ROI Pool** — dwuetapowy, dokładny +- **NMS = „Najlepszy Ma Się dobrze"** — zachowaj najlepszą detekcję, usuń duplikaty +- **IoU = „Ile pokrycia Ustalono?"** — pole(∩) / pole(A∪B) +- **DETR = „Detekcja Eliminująca Trikowe Redundancje"** — bez NMS, bez anchorów, transformer +- **Detektor z klasyfikatora: „SRF" = „Szukaj Ręcznie, Finalnie optymalizuj!"** — Sliding Window (wolno) → Region Proposals (lepiej) → Fine-tune backbone (najlepiej) diff --git a/python_pkg/praca_magisterska_video/visualize_q02.py b/python_pkg/praca_magisterska_video/visualize_q02.py new file mode 100644 index 0000000..fd5e859 --- /dev/null +++ b/python_pkg/praca_magisterska_video/visualize_q02.py @@ -0,0 +1,530 @@ +"""MoviePy visualization for PYTANIE 2: Shortest path algorithms. + +Creates an animated video walking through Dijkstra, Bellman-Ford, and A* +on a small example graph, rendering each algorithm step by step. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +import numpy as np + +os.environ["FFMPEG_BINARY"] = "/usr/bin/ffmpeg" + +from moviepy import ( + ColorClip, + CompositeVideoClip, + TextClip, + VideoClip, + concatenate_videoclips, +) +from moviepy.video.fx import FadeIn, FadeOut + +# ── Constants ───────────────────────────────────────────────────── +W, H = 1280, 720 +FPS = 24 +STEP_DUR = 8.0 +HEADER_DUR = 5.0 +FONT_B = "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf" +FONT_R = "/usr/share/fonts/TTF/DejaVuSans.ttf" +OUTPUT_DIR = Path(__file__).resolve().parent / "videos" +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) +OUTPUT = str(OUTPUT_DIR / "q02_shortest_path.mp4") + +# Graph definition +NODE_POS = {"S": (250, 280), "A": (550, 180), "B": (550, 450), "C": (850, 320)} +EDGES_DIJKSTRA = [ + ("S", "A", 2), + ("S", "B", 5), + ("A", "C", 3), + ("B", "A", 1), + ("B", "C", 6), +] +EDGES_BF = [("S", "A", 2), ("A", "C", 3), ("S", "B", 5), ("B", "A", -4)] + +# Colors +BG = (20, 20, 40) +COL_DEFAULT = (70, 130, 200) +COL_CURRENT = (255, 200, 50) +COL_VISITED = (80, 200, 100) +COL_EDGE = (100, 100, 130) +COL_EDGE_ACT = (255, 100, 80) +INF = "inf" + + +def _tc(**kwargs: object) -> TextClip: + """TextClip wrapper that adds enough bottom margin to prevent clipping.""" + fs = kwargs.get("font_size", 24) + m = int(fs) // 3 + 2 + kwargs["margin"] = (0, m) + return TextClip(**kwargs) + + +def _make_header( + title: str, subtitle: str, duration: float = HEADER_DUR +) -> CompositeVideoClip: + bg = ColorClip(size=(W, H), color=BG).with_duration(duration) + t = ( + _tc( + text=title, + font_size=52, + color="white", + font=FONT_B, + ) + .with_duration(duration) + .with_position(("center", 250)) + ) + s = ( + _tc( + text=subtitle, + font_size=28, + color="#AABBCC", + font=FONT_R, + ) + .with_duration(duration) + .with_position(("center", 340)) + ) + return CompositeVideoClip([bg, t, s], size=(W, H)).with_effects( + [FadeIn(0.5), FadeOut(0.5)] + ) + + +def _draw_circle( + frame: np.ndarray, cx: int, cy: int, r: int, color: tuple[int, ...] +) -> None: + yy, xx = np.ogrid[:H, :W] + mask = ((xx - cx) ** 2 + (yy - cy) ** 2) <= r**2 + frame[mask] = color + + +def _draw_line( + frame: np.ndarray, + x1: int, + y1: int, + x2: int, + y2: int, + color: tuple[int, ...], + thickness: int = 2, +) -> None: + length = max(int(np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)), 1) + for i in range(length): + frac = i / length + px = int(x1 + frac * (x2 - x1)) + py = int(y1 + frac * (y2 - y1)) + for dx in range(-thickness, thickness + 1): + for dy in range(-thickness, thickness + 1): + nx, ny = px + dx, py + dy + if 0 <= nx < W and 0 <= ny < H: + frame[ny, nx] = color + + +def _draw_arrow( + frame: np.ndarray, + x1: int, + y1: int, + x2: int, + y2: int, + color: tuple[int, ...], + thickness: int = 2, +) -> None: + r = 32 + length = max(np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2), 1) + ddx = (x2 - x1) / length + ddy = (y2 - y1) / length + sx = int(x1 + ddx * r) + sy = int(y1 + ddy * r) + ex = int(x2 - ddx * r) + ey = int(y2 - ddy * r) + _draw_line(frame, sx, sy, ex, ey, color, thickness) + angle = np.arctan2(ey - sy, ex - sx) + arrow_len = 12 + for side in [-1, 1]: + a = angle + np.pi + side * 0.4 + ax = int(ex + arrow_len * np.cos(a)) + ay = int(ey + arrow_len * np.sin(a)) + _draw_line(frame, ex, ey, ax, ay, color, thickness) + + +def _render_graph( + nodes: dict[str, tuple[int, int]], + edges: list[tuple[str, str, int]], + _distances: dict[str, str], + current: str | None = None, + visited: set[str] | None = None, + active_edge: tuple[str, str] | None = None, +) -> np.ndarray: + if visited is None: + visited = set() + frame = np.full((H, W, 3), BG, dtype=np.uint8) + + for src, dst, _w in edges: + sx, sy = nodes[src] + dx, dy = nodes[dst] + ec = COL_EDGE_ACT if active_edge == (src, dst) else COL_EDGE + _draw_arrow(frame, sx, sy, dx, dy, ec, thickness=2) + + for name, (x, y) in nodes.items(): + if name == current: + nc = COL_CURRENT + elif name in visited: + nc = COL_VISITED + else: + nc = COL_DEFAULT + _draw_circle(frame, x, y, 30, nc) + # Border ring + border = tuple(max(c - 40, 0) for c in nc) + yy, xx = np.ogrid[:H, :W] + ring = (((xx - x) ** 2 + (yy - y) ** 2) <= 30**2) & ( + ((xx - x) ** 2 + (yy - y) ** 2) > 27**2 + ) + frame[ring] = border + + return frame + + +def _make_step( + nodes: dict[str, tuple[int, int]], + edges: list[tuple[str, str, int]], + distances: dict[str, str], + current: str | None = None, + visited: set[str] | None = None, + active_edge: tuple[str, str] | None = None, + step_text: str = "", + algo_name: str = "", + duration: float = STEP_DUR, +) -> CompositeVideoClip: + if visited is None: + visited = set() + + graph_frame = _render_graph(nodes, edges, distances, current, visited, active_edge) + + def make_frame(_t: float) -> np.ndarray: + return graph_frame.copy() + + bg_clip = VideoClip(make_frame, duration=duration).with_fps(FPS) + overlays: list[VideoClip] = [bg_clip] + + if algo_name: + overlays.append( + _tc( + text=algo_name, + font_size=28, + color="#64B5F6", + font=FONT_B, + ) + .with_duration(duration) + .with_position((40, 20)) + ) + + dist_items = [f"{k}: {v}" for k, v in distances.items()] + table_text = "dist = { " + ", ".join(dist_items) + " }" + overlays.append( + _tc( + text=table_text, + font_size=18, + color="#B0BEC5", + font=FONT_R, + ) + .with_duration(duration) + .with_position((40, 60)) + ) + + visited_text = f"visited = {{ {', '.join(sorted(visited))} }}" + overlays.append( + _tc( + text=visited_text, + font_size=18, + color="#A5D6A7", + font=FONT_R, + ) + .with_duration(duration) + .with_position((40, 90)) + ) + + for src, dst, w in edges: + sx, sy = nodes[src] + dx, dy = nodes[dst] + mx = (sx + dx) // 2 - 6 + my = (sy + dy) // 2 - 12 + wcol = "#FF8A65" if active_edge == (src, dst) else "#90A4AE" + overlays.append( + _tc( + text=str(w), + font_size=16, + color=wcol, + font=FONT_B, + ) + .with_duration(duration) + .with_position((mx, my)) + ) + + for name, (x, y) in nodes.items(): + overlays.append( + _tc( + text=name, + font_size=20, + color="white", + font=FONT_B, + ) + .with_duration(duration) + .with_position((x - 7, y - 12)) + ) + d = distances.get(name, INF) + overlays.append( + _tc( + text=f"d={d}", + font_size=14, + color="#FFE082", + font=FONT_R, + ) + .with_duration(duration) + .with_position((x - 16, y + 35)) + ) + + if step_text: + overlays.append( + _tc( + text=step_text, + font_size=18, + color="#E0E0E0", + font=FONT_R, + ) + .with_duration(duration) + .with_position((40, 600)) + ) + + return CompositeVideoClip(overlays, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + + +def _dijkstra_steps() -> list[CompositeVideoClip]: + n = NODE_POS + e = EDGES_DIJKSTRA + return [ + _make_step( + n, + e, + {"S": "0", "A": INF, "B": INF, "C": INF}, + current="S", + step_text="Inicjalizacja: d[S]=0, reszta=∞. Wybierz S (min d).", + algo_name="Algorytm Dijkstry", + ), + _make_step( + n, + e, + {"S": "0", "A": "2", "B": "5", "C": INF}, + current="S", + active_edge=("S", "A"), + step_text="Relaksacja S→A: d[A]=0+2=2. S→B: d[B]=0+5=5.", + algo_name="Algorytm Dijkstry", + ), + _make_step( + n, + e, + {"S": "0", "A": "2", "B": "5", "C": "5"}, + current="A", + visited={"S"}, + active_edge=("A", "C"), + step_text="Zamknij S. Min=A(2). Relaksacja A→C: d[C]=2+3=5.", + algo_name="Algorytm Dijkstry", + ), + _make_step( + n, + e, + {"S": "0", "A": "2", "B": "5", "C": "5"}, + current="B", + visited={"S", "A"}, + active_edge=("B", "A"), + step_text="Zamknij A. Min=B(5). B→A: 5+1=6>2, nie zmieniaj. B→C: 5+6=11>5.", + algo_name="Algorytm Dijkstry", + ), + _make_step( + n, + e, + {"S": "0", "A": "2", "B": "5", "C": "5"}, + current="C", + visited={"S", "A", "B"}, + step_text="Zamknij B. Min=C(5). Koniec! Wynik: d={S:0, A:2, B:5, C:5}.", + algo_name="Dijkstra -- WYNIK", + ), + ] + + +def _bellman_ford_steps() -> list[CompositeVideoClip]: + n = NODE_POS + e = EDGES_BF + return [ + _make_step( + n, + e, + {"S": "0", "A": INF, "B": INF, "C": INF}, + step_text="Bellman-Ford: relaksuj WSZYSTKIE krawędzie V-1=3 razy. Ujemne wagi OK!", + algo_name="Algorytm Bellmana-Forda", + ), + _make_step( + n, + e, + {"S": "0", "A": "2", "B": "5", "C": "5"}, + active_edge=("S", "A"), + step_text="Iteracja 1: S→A:2, A→C:5, S→B:5. Potem B→A: 5+(-4)=1 < 2 → A=1!", + algo_name="Bellman-Ford -- iteracja 1", + ), + _make_step( + n, + e, + {"S": "0", "A": "1", "B": "5", "C": "5"}, + active_edge=("B", "A"), + step_text="B→A z ujemną wagą -4: d[A] poprawione z 2 na 1! (Dijkstra by to pominął!)", + algo_name="Bellman-Ford -- ujemna waga", + ), + _make_step( + n, + e, + {"S": "0", "A": "1", "B": "5", "C": "4"}, + active_edge=("A", "C"), + step_text="Iteracja 2: A→C: 1+3=4 < 5 → C=4. Propagacja poprawionego A.", + algo_name="Bellman-Ford -- iteracja 2", + ), + _make_step( + n, + e, + {"S": "0", "A": "1", "B": "5", "C": "4"}, + step_text="Iteracja 3: brak zmian. V-ta iteracja: brak popraw → brak cyklu ujemnego.", + algo_name="Bellman-Ford -- WYNIK, O(V*E)", + ), + ] + + +def _astar_steps() -> list[CompositeVideoClip]: + n = NODE_POS + e = EDGES_DIJKSTRA + return [ + _make_step( + n, + e, + {"S": "0", "A": INF, "B": INF, "C": INF}, + current="S", + step_text="A*: f(n)=g(n)+h(n). Cel=C. h(S)=5, h(A)=3, h(B)=4, h(C)=0. f(S)=0+5=5.", + algo_name="Algorytm A*", + ), + _make_step( + n, + e, + {"S": "0", "A": "2", "B": "5", "C": INF}, + current="S", + active_edge=("S", "A"), + step_text="Relaksuj S: A(g=2,f=2+3=5), B(g=5,f=5+4=9). Min f → A(5).", + algo_name="A* -- rozwijanie S", + ), + _make_step( + n, + e, + {"S": "0", "A": "2", "B": "5", "C": "5"}, + current="A", + visited={"S"}, + active_edge=("A", "C"), + step_text="Rozwiń A(f=5): A→C: g=2+3=5, f=5+0=5. Min f → C(5) = CEL!", + algo_name="A* -- rozwijanie A", + ), + _make_step( + n, + e, + {"S": "0", "A": "2", "B": "5", "C": "5"}, + current="C", + visited={"S", "A"}, + step_text="Dotarliśmy do C! Koszt=5. A* NIE przetwarza B (3 vs 4 w Dijkstrze).", + algo_name="A* -- cel osiągnięty!", + ), + ] + + +def _comparison_slide() -> CompositeVideoClip: + bg = ColorClip(size=(W, H), color=BG).with_duration(12.0) + title = ( + _tc( + text="Porównanie algorytmów", + font_size=40, + color="white", + font=FONT_B, + ) + .with_duration(12.0) + .with_position(("center", 40)) + ) + rows = [ + ("Cecha", "Dijkstra", "Bellman-Ford", "A*"), + ("Typ", "Zachłanny", "Prog. dynamiczne", "Heurystyczny"), + ("Problem", "SSSP", "SSSP", "Single-pair"), + ("Ujemne wagi", "NIE", "TAK", "NIE"), + ("Cykl ujemny", "NIE wykrywa", "TAK wykrywa", "NIE"), + ("Złożoność", "O((V+E)log V)", "O(V*E)", "Zależy od h(n)"), + ] + clips: list[VideoClip] = [bg, title] + for i, row in enumerate(rows): + y_pos = 120 + i * 85 + for j, cell in enumerate(row): + x_pos = 60 + j * 300 + fs = 18 if i > 0 else 22 + color = "#64B5F6" if i == 0 else "#CFD8DC" + tc = ( + _tc( + text=cell, + font_size=fs, + color=color, + font=FONT_B if i == 0 else FONT_R, + ) + .with_duration(12.0) + .with_position((x_pos, y_pos)) + ) + clips.append(tc) + return CompositeVideoClip(clips, size=(W, H)).with_effects( + [FadeIn(0.5), FadeOut(0.5)] + ) + + +def main() -> None: + """Generate the Q02 shortest path visualization video.""" + sections: list[VideoClip] = [] + + sections.append( + _make_header( + "Pytanie 2: Algorytmy najkrótszej ścieżki", + "Dijkstra * Bellman-Ford * A*", + duration=8.0, + ) + ) + + sections.append(_make_header("Algorytm Dijkstry", "Zachłanny, SSSP, wagi ≥ 0")) + sections.extend(_dijkstra_steps()) + + sections.append( + _make_header("Algorytm Bellmana-Forda", "Prog. dynamiczne, ujemne wagi, O(V·E)") + ) + sections.extend(_bellman_ford_steps()) + + sections.append( + _make_header("Algorytm A*", "Heurystyczny, f(n)=g(n)+h(n), Single-pair") + ) + sections.extend(_astar_steps()) + + sections.append(_comparison_slide()) + + sections.append( + _make_header( + "Podsumowanie", + "Dijkstra=chciwy | Bellman-Ford=brute force x(V-1) | A*=Dijkstra+GPS", + duration=8.0, + ) + ) + + final = concatenate_videoclips(sections, method="compose") + final.write_videofile( + OUTPUT, fps=FPS, codec="libx264", audio=False, preset="medium", threads=4 + ) + print(f"Video saved to: {OUTPUT}") + + +if __name__ == "__main__": + main() diff --git a/python_pkg/praca_magisterska_video/visualize_q23.py b/python_pkg/praca_magisterska_video/visualize_q23.py new file mode 100644 index 0000000..9981934 --- /dev/null +++ b/python_pkg/praca_magisterska_video/visualize_q23.py @@ -0,0 +1,1627 @@ +"""MoviePy visualization for PYTANIE 23: Image Segmentation. + +Creates animated video demonstrating: +- What segmentation is (pixel-level classification) +- Thresholding / Otsu (bimodal histogram) +- Region Growing (BFS flood fill) +- Watershed (topographic flooding) +- U-Net encoder-decoder architecture +""" + +from __future__ import annotations + +import os +from pathlib import Path + +import numpy as np + +os.environ["FFMPEG_BINARY"] = "/usr/bin/ffmpeg" + +from moviepy import ( + ColorClip, + CompositeVideoClip, + TextClip, + VideoClip, + concatenate_videoclips, +) +from moviepy.video.fx import FadeIn, FadeOut + +# ── Constants ───────────────────────────────────────────────────── +W, H = 1280, 720 +FPS = 24 +STEP_DUR = 7.0 +HEADER_DUR = 4.0 +FONT_B = "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf" +FONT_R = "/usr/share/fonts/TTF/DejaVuSans.ttf" +OUTPUT_DIR = Path(__file__).resolve().parent / "videos" +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) +OUTPUT = str(OUTPUT_DIR / "q23_segmentation.mp4") + +BG_COLOR = (15, 20, 35) +rng = np.random.default_rng(42) + + +def _tc(**kwargs: object) -> TextClip: + """TextClip wrapper that adds enough bottom margin to prevent clipping.""" + fs = kwargs.get("font_size", 24) + m = int(fs) // 3 + 2 + kwargs["margin"] = (0, m) + return TextClip(**kwargs) + + +def _make_header( + title: str, subtitle: str, duration: float = HEADER_DUR +) -> CompositeVideoClip: + bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(duration) + t = ( + _tc( + text=title, + font_size=48, + color="white", + font=FONT_B, + ) + .with_duration(duration) + .with_position(("center", 260)) + ) + s = ( + _tc( + text=subtitle, + font_size=24, + color="#90CAF9", + font=FONT_R, + ) + .with_duration(duration) + .with_position(("center", 340)) + ) + return CompositeVideoClip([bg, t, s], size=(W, H)).with_effects( + [FadeIn(0.5), FadeOut(0.5)] + ) + + +def _text_slide( + lines: list[tuple[str, int, str, str, tuple[str | int, str | int]]], + duration: float = STEP_DUR, +) -> CompositeVideoClip: + """Create a slide with multiple text elements.""" + bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(duration) + clips: list[VideoClip] = [bg] + for text, font_size, color, font, pos in lines: + tc = ( + _tc( + text=text, + font_size=font_size, + color=color, + font=font, + ) + .with_duration(duration) + .with_position(pos) + ) + clips.append(tc) + return CompositeVideoClip(clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + + +# ── Segmentation concept ───────────────────────────────────────── +def _segmentation_concept() -> list[CompositeVideoClip]: + """Show what segmentation is: pixel-level labeling.""" + slides = [] + + # Synthetic image: grid of colored pixels + def make_image_frame(_t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + + # Draw a small "image" grid + grid_x, grid_y = 100, 150 + cell = 40 + # Sky (top rows) + colors_map = [ + [(135, 206, 235)] * 8, # sky + [(135, 206, 235)] * 5 + [(34, 139, 34)] * 3, # sky + tree + [(34, 139, 34)] * 3 + + [(128, 128, 128)] * 3 + + [(34, 139, 34)] * 2, # tree+road+tree + [(128, 128, 128)] * 3 + + [(200, 50, 50)] * 2 + + [(128, 128, 128)] * 3, # road+car+road + ] + labels_map = [ + ["niebo"] * 8, + ["niebo"] * 5 + ["drzewo"] * 3, + ["drzewo"] * 3 + ["droga"] * 3 + ["drzewo"] * 2, + ["droga"] * 3 + ["samochód"] * 2 + ["droga"] * 3, + ] + label_colors = { + "niebo": (100, 180, 255), + "drzewo": (50, 200, 50), + "droga": (180, 180, 180), + "samochód": (255, 80, 80), + } + + for r, row in enumerate(colors_map): + for c, col in enumerate(row): + y = grid_y + r * cell + x = grid_x + c * cell + frame[y : y + cell - 2, x : x + cell - 2] = col + + # Draw segmentation map on the right + seg_x = 600 + for r, row in enumerate(labels_map): + for c, lab in enumerate(row): + y = grid_y + r * cell + x = seg_x + c * cell + frame[y : y + cell - 2, x : x + cell - 2] = label_colors[lab] + + return frame + + image_clip = VideoClip(make_image_frame, duration=STEP_DUR).with_fps(FPS) + labels_text = [ + ("Obraz wejściowy", 22, "white", FONT_B, (170, 100)), + ("Mapa segmentacji", 22, "white", FONT_B, (660, 100)), + ("→", 50, "#FFE082", FONT_B, (450, 250)), + ("Każdy piksel → etykieta klasy", 20, "#B0BEC5", FONT_R, (100, 420)), + ("niebo | drzewo | droga | samochód", 18, "#90CAF9", FONT_R, (600, 420)), + ("Segmentacja = klasyfikacja per-piksel", 24, "#FFE082", FONT_B, (100, 500)), + ( + "Semantic: klasy bez instancji | Instance: rozróżnia obiekty | Panoptic: oba", + 16, + "#78909C", + FONT_R, + (100, 560), + ), + ] + clips: list[VideoClip] = [image_clip] + for text, fs, color, font, pos in labels_text: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR) + .with_position(pos) + ) + clips.append(tc) + + slides.append( + CompositeVideoClip(clips, size=(W, H)).with_effects([FadeIn(0.3), FadeOut(0.3)]) + ) + return slides + + +# ── Thresholding / Otsu ─────────────────────────────────────────── +def _thresholding_demo() -> list[CompositeVideoClip]: + """Animate thresholding and Otsu concept.""" + slides = [] + + # Show histogram & threshold + def make_threshold_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + + # Draw bimodal histogram bars + bar_start_x = 80 + bar_y = 500 + bar_w = 4 + + for i in range(256): + # Bimodal: peaks at 60 and 190 + h1 = 200 * np.exp(-((i - 60) ** 2) / (2 * 20**2)) + h2 = 150 * np.exp(-((i - 190) ** 2) / (2 * 25**2)) + bar_h = int(h1 + h2) + x = bar_start_x + i * bar_w + if x + bar_w < W: + frame[bar_y - bar_h : bar_y, x : x + bar_w - 1] = (150, 150, 170) + + # Animated threshold line + threshold = int(60 + (190 - 60) * min(t / (STEP_DUR * 0.7), 1.0)) + tx = bar_start_x + threshold * bar_w + if tx < W: + frame[bar_y - 250 : bar_y + 10, tx : tx + 3] = (255, 80, 80) + + # Color the two sides + for i in range(threshold): + x = bar_start_x + i * bar_w + h1 = 200 * np.exp(-((i - 60) ** 2) / (2 * 20**2)) + h2 = 150 * np.exp(-((i - 190) ** 2) / (2 * 25**2)) + bar_h = int(h1 + h2) + if x + bar_w < W and bar_h > 0: + frame[bar_y - bar_h : bar_y, x : x + bar_w - 1] = (70, 130, 200) + + for i in range(threshold, 256): + x = bar_start_x + i * bar_w + h1 = 200 * np.exp(-((i - 60) ** 2) / (2 * 20**2)) + h2 = 150 * np.exp(-((i - 190) ** 2) / (2 * 25**2)) + bar_h = int(h1 + h2) + if x + bar_w < W and bar_h > 0: + frame[bar_y - bar_h : bar_y, x : x + bar_w - 1] = (200, 100, 80) + + return frame + + hist_clip = VideoClip(make_threshold_frame, duration=STEP_DUR).with_fps(FPS) + text_clips: list[VideoClip] = [hist_clip] + labels = [ + ("Progowanie (Thresholding) z metodą Otsu", 28, "#FFE082", FONT_B, (80, 30)), + ( + "Histogram jasności pikseli — dwumodalny (bimodal)", + 20, + "#B0BEC5", + FONT_R, + (80, 80), + ), + ("Garb 1: piksele obiektu (ciemne ~60)", 16, "#64B5F6", FONT_R, (80, 120)), + ("Garb 2: piksele tła (jasne ~190)", 16, "#EF9A9A", FONT_R, (80, 150)), + ( + "Próg T (czerwona linia) dzieli piksele na 2 klasy", + 18, + "white", + FONT_R, + (80, 540), + ), + ( + "Otsu: automatycznie testuje T=0..255, minimalizuje σ² wewnątrzklasową", + 16, + "#A5D6A7", + FONT_R, + (80, 580), + ), + ( + "Piksel ≤ T → klasa 0 (tło) | Piksel > T → klasa 1 (obiekt)", + 16, + "#78909C", + FONT_R, + (80, 620), + ), + ] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR) + .with_position(pos) + ) + text_clips.append(tc) + + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── Region Growing ──────────────────────────────────────────────── +def _region_growing_demo() -> list[CompositeVideoClip]: + """Animate region growing BFS from a seed pixel.""" + slides = [] + + grid_size = 10 + cell_size = 40 + rng = np.random.default_rng(42) + # Create a simple grid: dark region (30-80) and bright region (160-220) + grid = np.zeros((grid_size, grid_size), dtype=np.uint8) + grid[:] = 60 # dark background + grid[2:7, 3:8] = 180 # bright rectangle + + # Add some noise + noise = rng.integers(-15, 15, (grid_size, grid_size)) + grid = np.clip(grid.astype(int) + noise, 0, 255).astype(np.uint8) + + # BFS steps from seed (4, 5) + seed = (4, 5) + threshold_val = 50 + visited_order: list[tuple[int, int]] = [] + queue = [seed] + visited_set = {seed} + while queue: + r, c = queue.pop(0) + visited_order.append((r, c)) + for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + nr, nc = r + dr, c + dc + if ( + 0 <= nr < grid_size + and 0 <= nc < grid_size + and (nr, nc) not in visited_set + ) and abs(int(grid[nr, nc]) - int(grid[seed])) < threshold_val: + visited_set.add((nr, nc)) + queue.append((nr, nc)) + + def make_region_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + ox, oy = 100, 180 + + # How many cells to show as visited + progress = min(t / (STEP_DUR * 0.8), 1.0) + n_visited = int(progress * len(visited_order)) + + for r in range(grid_size): + for c in range(grid_size): + x = ox + c * cell_size + y = oy + r * cell_size + val = grid[r, c] + color = (val, val, val) + + # Highlight visited + if (r, c) in visited_order[:n_visited]: + color = (80, 200, 120) # green for region + elif (r, c) == seed: + color = (255, 200, 50) # yellow seed + + frame[y : y + cell_size - 2, x : x + cell_size - 2] = color + + # Show value + # (drawn as a simple marker since we can't render text in numpy easily) + + # Mark the seed with a bright border + sx = ox + seed[1] * cell_size + sy = ox + seed[0] * cell_size + 80 + frame[sy : sy + cell_size, sx : sx + 2] = (255, 200, 50) + frame[sy : sy + cell_size, sx + cell_size - 2 : sx + cell_size] = (255, 200, 50) + frame[sy : sy + 2, sx : sx + cell_size] = (255, 200, 50) + frame[sy + cell_size - 2 : sy + cell_size, sx : sx + cell_size] = (255, 200, 50) + + return frame + + region_clip = VideoClip(make_region_frame, duration=STEP_DUR).with_fps(FPS) + text_clips: list[VideoClip] = [region_clip] + labels = [ + ("Region Growing — rozrastanie regionu", 28, "#FFE082", FONT_B, (100, 30)), + ("Seed (ziarno) → BFS do podobnych sąsiadów", 20, "#B0BEC5", FONT_R, (100, 80)), + ( + "Żółty = seed | Zielony = region | Szary = nieodwiedzone", + 16, + "#78909C", + FONT_R, + (100, 120), + ), + ( + "Sąsiad PODOBNY (|jasność - jasność_regionu| < próg) → dodaj do regionu", + 16, + "#A5D6A7", + FONT_R, + (100, 600), + ), + ( + "Algorytm zatrzymuje się gdy brak podobnych sąsiadów", + 16, + "#90CAF9", + FONT_R, + (100, 640), + ), + ( + "Mnemonik: PLAMA atramentu — rozlewa się na podobne piksele", + 18, + "#EF9A9A", + FONT_R, + (100, 670), + ), + ] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR) + .with_position(pos) + ) + text_clips.append(tc) + + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── Watershed ───────────────────────────────────────────────────── +def _watershed_demo() -> list[CompositeVideoClip]: + """Animate watershed flooding concept.""" + slides = [] + + def make_watershed_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + + # Draw terrain profile (1D cross-section) + ox, oy = 100, 450 + terrain_w = 900 + terrain_points = 100 + + xs = np.linspace(0, 1, terrain_points) + # Two valleys with a ridge + terrain = ( + 120 * np.exp(-((xs - 0.25) ** 2) / 0.005) + + 80 * np.exp(-((xs - 0.75) ** 2) / 0.008) + + 30 + ) + terrain = 250 - terrain # invert for visual (valleys at bottom) + + # Water level rises over time + water_level = int(160 + 80 * min(t / (STEP_DUR * 0.7), 1.0)) + + for i in range(terrain_points - 1): + x1 = ox + int(xs[i] * terrain_w) + x2 = ox + int(xs[i + 1] * terrain_w) + y1 = oy - int(terrain[i]) + y2 = oy - int(terrain[i + 1]) + + # Fill terrain + for x in range(x1, min(x2 + 1, W)): + top = min(y1, y2) - 5 + frame[top:oy, x : x + 1] = (100, 80, 60) + + # Fill water + water_y = oy - water_level + for x in range(x1, min(x2 + 1, W)): + t_y = oy - int(terrain[i]) + if water_y < t_y: + # Water fills below terrain surface + fill_top = max(water_y, 0) + fill_bot = min(t_y, oy) + if fill_top < fill_bot: + frame[fill_top:fill_bot, x : x + 1] = (70, 130, 220) + + # Dam marker at ridge + ridge_x = ox + int(0.5 * terrain_w) + if water_level > 160: + frame[oy - water_level : oy - 140, ridge_x - 2 : ridge_x + 2] = ( + 255, + 80, + 80, + ) + + return frame + + ws_clip = VideoClip(make_watershed_frame, duration=STEP_DUR).with_fps(FPS) + text_clips: list[VideoClip] = [ws_clip] + labels = [ + ("Watershed — metoda zlewiska", 28, "#FFE082", FONT_B, (100, 20)), + ( + "Obraz = mapa topograficzna (jasność = wysokość)", + 20, + "#B0BEC5", + FONT_R, + (100, 65), + ), + ( + "Brązowy = teren (ciemne=doliny, jasne=szczyty)", + 16, + "#8D6E63", + FONT_R, + (100, 100), + ), + ("Niebieski = woda zalewająca od minimów", 16, "#64B5F6", FONT_R, (100, 130)), + ( + "Czerwony = TAMA (granica segmentu) — gdy woda z 2 dolin się spotka", + 16, + "#EF9A9A", + FONT_R, + (100, 160), + ), + ( + "Problem: over-segmentation (za dużo regionów). Rozwiązanie: marker-controlled.", + 16, + "#A5D6A7", + FONT_R, + (100, 560), + ), + ( + "Mnemonik: ZALEWANIE terenu — granie gór = granice segmentów", + 18, + "#FFE082", + FONT_R, + (100, 600), + ), + ] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR) + .with_position(pos) + ) + text_clips.append(tc) + + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── U-Net Architecture ─────────────────────────────────────────── +def _unet_demo() -> list[CompositeVideoClip]: + """Animate U-Net encoder-decoder architecture.""" + slides = [] + + def make_unet_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + + # Draw U-shape: encoder blocks going down, decoder going up + # Encoder: 4 blocks getting smaller + enc_sizes = [(80, 120), (60, 100), (45, 80), (30, 60)] + dec_sizes = list(reversed(enc_sizes)) + enc_x = 150 + dec_x = 850 + + progress = min(t / (STEP_DUR * 0.6), 1.0) + n_blocks = int(progress * 8) + 1 # 1 to 8 + + enc_positions = [] + y_offset = 120 + for i, (bw, bh) in enumerate(enc_sizes): + x = enc_x + y = y_offset + i * 130 + enc_positions.append((x, y, bw, bh)) + if i < n_blocks: + # Draw encoder block + frame[y : y + bh, x : x + bw] = (70, 130, 200) + # Border + frame[y : y + 2, x : x + bw] = (100, 180, 255) + frame[y + bh - 2 : y + bh, x : x + bw] = (100, 180, 255) + frame[y : y + bh, x : x + 2] = (100, 180, 255) + frame[y : y + bh, x + bw - 2 : x + bw] = (100, 180, 255) + + # Down arrow + if i < len(enc_sizes) - 1: + ax = x + bw // 2 + ay = y + bh + 10 + frame[ay : ay + 20, ax - 1 : ax + 2] = (150, 150, 170) + + # Bottleneck + bx, by = 500, y_offset + 3 * 130 + 30 + if n_blocks > 4: + frame[by : by + 50, bx : bx + 25] = (200, 100, 80) + frame[by : by + 2, bx : bx + 25] = (255, 140, 100) + frame[by + 48 : by + 50, bx : bx + 25] = (255, 140, 100) + + # Decoder + dec_positions = [] + for i, (bw, bh) in enumerate(dec_sizes): + x = dec_x + y = y_offset + (3 - i) * 130 + dec_positions.append((x, y, bw, bh)) + if n_blocks > 4 + i + 1: + frame[y : y + bh, x : x + bw] = (80, 200, 120) + frame[y : y + 2, x : x + bw] = (120, 230, 150) + frame[y + bh - 2 : y + bh, x : x + bw] = (120, 230, 150) + frame[y : y + bh, x : x + 2] = (120, 230, 150) + frame[y : y + bh, x + bw - 2 : x + bw] = (120, 230, 150) + + # Up arrow + if i < len(dec_sizes) - 1: + ax = x + bw // 2 + ay = y - 30 + frame[ay : ay + 20, ax - 1 : ax + 2] = (150, 150, 170) + + # Skip connections (horizontal dashed lines) + if n_blocks > 5: + for i in range(min(n_blocks - 5, 4)): + ey = enc_positions[i][1] + enc_positions[i][3] // 2 + ex_end = enc_positions[i][0] + enc_positions[i][2] + dx_start = dec_x + for dash_x in range(ex_end + 10, dx_start - 10, 15): + frame[ey : ey + 2, dash_x : dash_x + 8] = (255, 200, 50) + + return frame + + unet_clip = VideoClip(make_unet_frame, duration=STEP_DUR + 1).with_fps(FPS) + text_clips: list[VideoClip] = [unet_clip] + labels = [ + ("U-Net: Encoder-Decoder + Skip Connections", 28, "#FFE082", FONT_B, (80, 20)), + ( + "Niebieski = Encoder (↓ zmniejsza rozdzielczość, wyciąga cechy)", + 16, + "#64B5F6", + FONT_R, + (80, 65), + ), + ( + "Zielony = Decoder (↑ zwiększa rozdzielczość, odtwarza mapę)", + 16, + "#A5D6A7", + FONT_R, + (80, 90), + ), + ( + "Żółte przerywane = Skip connections (przenoszą detale z encodera)", + 16, + "#FFE082", + FONT_R, + (80, 115), + ), + ( + "Czerwony = Bottleneck (najgłębsza warstwa, max abstrakcja)", + 16, + "#EF9A9A", + FONT_R, + (450, 570), + ), + ( + "Kształt U: encoder ↓ decoder ↑, mosty pośrodku", + 18, + "white", + FONT_R, + (80, 640), + ), + ( + "Concatenation: skip łączy kanały (więcej informacji niż dodawanie)", + 16, + "#78909C", + FONT_R, + (80, 670), + ), + ] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR + 1) + .with_position(pos) + ) + text_clips.append(tc) + + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── FCN Architecture ───────────────────────────────────────────── +def _fcn_demo() -> list[CompositeVideoClip]: + """Animate FCN step-by-step: FC → Conv 1x1 transformation.""" + slides = [] + + # Slide 1: Classic CNN vs FCN pipeline comparison + def make_fcn_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + progress = min(t / (STEP_DUR * 0.8), 1.0) + + # TOP: Classic CNN → FC → 1 label + top_y = 140 + blocks_classic = [ + ((80, top_y), (70, 50), (70, 130, 200)), + ((170, top_y), (50, 40), (50, 100, 160)), + ((240, top_y), (60, 50), (70, 130, 200)), + ((320, top_y), (40, 35), (50, 100, 160)), + ((385, top_y), (55, 50), (160, 80, 60)), + ((465, top_y), (55, 50), (180, 60, 60)), + ((545, top_y), (80, 50), (200, 80, 80)), + ] + n_top = min(int(progress * 7) + 1, 7) + for i, ((bx, by), (bw, bh), color) in enumerate(blocks_classic): + if i < n_top: + frame[by : by + bh, bx : bx + bw] = color + frame[by : by + 2, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + if i < 6: + ax = bx + bw + 3 + ay = by + bh // 2 + frame[ay - 1 : ay + 2, ax : ax + 12] = (150, 150, 170) + + # Red X over Flatten+FC when FCN appears + if progress > 0.6: + for d in range(-2, 3): + for step in range(50): + x1 = 385 + int(step * 135 / 50) + y1 = top_y + step + d + if 0 <= y1 < H and 0 <= x1 < W: + frame[y1, x1] = (255, 80, 80) + y2 = top_y + 50 - step + d + if 0 <= y2 < H and 0 <= x1 < W: + frame[y2, x1] = (255, 80, 80) + + # BOTTOM: FCN pipeline + bot_y = 380 + blocks_fcn = [ + ((80, bot_y), (70, 50), (70, 130, 200)), + ((170, bot_y), (50, 40), (50, 100, 160)), + ((240, bot_y), (60, 50), (70, 130, 200)), + ((320, bot_y), (40, 35), (50, 100, 160)), + ((385, bot_y), (70, 50), (80, 200, 120)), + ((480, bot_y), (75, 50), (200, 160, 80)), + ((580, bot_y), (80, 50), (100, 200, 100)), + ] + if progress > 0.4: + n_bot = min(int((progress - 0.4) / 0.6 * 7) + 1, 7) + for i, ((bx, by), (bw, bh), color) in enumerate(blocks_fcn): + if i < n_bot: + frame[by : by + bh, bx : bx + bw] = color + frame[by : by + 2, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + if i < 6: + ax = bx + bw + 3 + ay = by + bh // 2 + frame[ay - 1 : ay + 2, ax : ax + 12] = (150, 150, 170) + + return frame + + fcn_clip = VideoClip(make_fcn_frame, duration=STEP_DUR + 1).with_fps(FPS) + dur = STEP_DUR + 1 + labels = [ + ("FCN: Fully Convolutional Network (2015)", 26, "#FFE082", FONT_B, (80, 20)), + ("KROK 1: Zamień FC → Conv 1x1", 18, "#A5D6A7", FONT_R, (80, 60)), + ("Klasyczny CNN:", 16, "#EF9A9A", FONT_B, (80, 105)), + ("Conv", 11, "white", FONT_R, (92, 148)), + ("Pool", 11, "white", FONT_R, (178, 148)), + ("Conv", 11, "white", FONT_R, (250, 148)), + ("Pool", 11, "white", FONT_R, (325, 148)), + ("Flatten", 11, "#EF9A9A", FONT_R, (390, 148)), + ("FC", 11, "#EF9A9A", FONT_R, (480, 148)), + ("1 label", 11, "#EF9A9A", FONT_R, (555, 148)), + ("FCN:", 16, "#A5D6A7", FONT_B, (80, 350)), + ("Conv", 11, "white", FONT_R, (92, 388)), + ("Pool", 11, "white", FONT_R, (178, 388)), + ("Conv", 11, "white", FONT_R, (250, 388)), + ("Pool", 11, "white", FONT_R, (325, 388)), + ("Conv1x1", 11, "#A5D6A7", FONT_R, (390, 388)), + ("Upsample", 11, "#FFE082", FONT_R, (486, 388)), + ("Mapa", 11, "#A5D6A7", FONT_R, (595, 388)), + ( + "FC: spłaszcza 3D→1D, wymusza stały rozmiar → 1 etykieta", + 16, + "#EF9A9A", + FONT_R, + (80, 250), + ), + ( + "Conv1x1: działa per piksel x kanały → DOWOLNY rozmiar → mapa klasy", + 16, + "#A5D6A7", + FONT_R, + (80, 460), + ), + ( + "KROK 2: Skip connections — łączą wczesne detale z późną abstrakcją", + 17, + "#64B5F6", + FONT_R, + (80, 510), + ), + ( + "Wczesne warstwy = krawędzie, tekstury | Późne = koncepty obiektów", + 15, + "#78909C", + FONT_R, + (80, 545), + ), + ( + "FCN = PIERWSZA sieć end-to-end do segmentacji per-piksel!", + 18, + "white", + FONT_R, + (80, 590), + ), + ( + "Mnemonik: FC → Conv 1x1 = otwieramy bramkę dla DOWOLNEGO rozmiaru", + 16, + "#FFE082", + FONT_R, + (80, 640), + ), + ] + text_clips: list[VideoClip] = [fcn_clip] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(dur) + .with_position(pos) + ) + text_clips.append(tc) + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + + # Slide 2: FCN skip connections step by step + skip_lines = [ + ("FCN: Skip Connections — krok po kroku", 26, "#FFE082", FONT_B, (80, 30)), + ( + "1. Encoder zmniejsza: 224→112→56→28→14 (pooling)", + 18, + "#64B5F6", + FONT_R, + (100, 100), + ), + ( + " Każdy pooling traci detale przestrzenne (dokładne krawędzie)", + 15, + "#78909C", + FONT_R, + (100, 135), + ), + ( + "2. Decoder powiększa: 14→28→56→112→224 (upsample/deconv)", + 18, + "#A5D6A7", + FONT_R, + (100, 190), + ), + ( + " Upsample ODGADUJE piksele — rozmyty wynik!", + 15, + "#78909C", + FONT_R, + (100, 225), + ), + ( + "3. Skip connections: dodaj cechy z encodera do decodera", + 18, + "#FFE082", + FONT_R, + (100, 280), + ), + ( + " Wczesne cechy = GDZIE (precyzyjne krawędzie)", + 15, + "#64B5F6", + FONT_R, + (100, 315), + ), + ( + " Późne cechy = CO (abstrakcyjne koncepty)", + 15, + "#A5D6A7", + FONT_R, + (100, 345), + ), + ( + " Skip = daje decoderowi OBA → ostry wynik!", + 15, + "#FFE082", + FONT_R, + (100, 375), + ), + ( + "Warianty: FCN-32s (brak skip, rozmyty) → FCN-16s → FCN-8s (najlepszy)", + 16, + "#B0BEC5", + FONT_R, + (80, 440), + ), + ( + "FCN-32s: upsample 32x naraz → ROZMYTE granice", + 15, + "#EF9A9A", + FONT_R, + (100, 485), + ), + ( + "FCN-16s: skip z pool4 + upsample 16x → lepiej", + 15, + "#FFE082", + FONT_R, + (100, 520), + ), + ( + "FCN-8s: skip z pool3+pool4 + upsample 8x → OSTRE granice!", + 15, + "#A5D6A7", + FONT_R, + (100, 555), + ), + ( + "Im więcej skip connections → tym więcej detali z encodera → ostrzejszy wynik", + 17, + "white", + FONT_R, + (80, 620), + ), + ] + slides.append(_text_slide(skip_lines, duration=STEP_DUR + 1)) + + return slides + + +# ── DeepLab Architecture ───────────────────────────────────────── +def _deeplab_demo() -> list[CompositeVideoClip]: + """Animate DeepLab: dilated convolution + ASPP step by step.""" + slides = [] + + # Slide 1: Regular vs Dilated convolution + def make_dilated_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + progress = min(t / (STEP_DUR * 0.7), 1.0) + + cell = 36 + # Draw three grids side by side for rate=1, rate=2, rate=3 + grids = [ + ( + "rate=1", + 60, + [ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 1), + (1, 2), + (2, 0), + (2, 1), + (2, 2), + ], + ), + ( + "rate=2", + 420, + [ + (0, 0), + (0, 2), + (0, 4), + (2, 0), + (2, 2), + (2, 4), + (4, 0), + (4, 2), + (4, 4), + ], + ), + ( + "rate=3", + 820, + [ + (0, 0), + (0, 3), + (0, 6), + (3, 0), + (3, 3), + (3, 6), + (6, 0), + (6, 3), + (6, 6), + ], + ), + ] + + for gi, (_label, gx, positions) in enumerate(grids): + if progress < gi * 0.3: + break + gy = 180 + grid_size = 7 + # Draw background grid + for r in range(grid_size): + for c in range(grid_size): + x = gx + c * cell + y = gy + r * cell + frame[y : y + cell - 2, x : x + cell - 2] = (35, 40, 55) + + # Highlight filter positions + for r, c in positions: + x = gx + c * cell + y = gy + r * cell + frame[y : y + cell - 2, x : x + cell - 2] = (70, 130, 200) + frame[y : y + 2, x : x + cell - 2] = (120, 180, 255) + frame[y + cell - 4 : y + cell - 2, x : x + cell - 2] = (120, 180, 255) + + return frame + + dil_clip = VideoClip(make_dilated_frame, duration=STEP_DUR + 1).with_fps(FPS) + dur = STEP_DUR + 1 + labels = [ + ("DeepLab: Atrous (Dilated) Convolution", 26, "#FFE082", FONT_B, (80, 20)), + ( + "KROK 1: Zrozum dilated convolution — filtr z DZIURAMI", + 18, + "#A5D6A7", + FONT_R, + (80, 60), + ), + ("rate=1 (zwykła)", 14, "#64B5F6", FONT_B, (60, 160)), + ("RF = 3x3", 14, "#64B5F6", FONT_R, (60, 440)), + ("9 wag, kontekst 3px", 12, "#78909C", FONT_R, (60, 470)), + ("rate=2 (dilated)", 14, "#FFE082", FONT_B, (420, 160)), + ("RF = 5x5", 14, "#FFE082", FONT_R, (420, 440)), + ("9 wag, kontekst 5px!", 12, "#78909C", FONT_R, (420, 470)), + ("rate=3 (dilated)", 14, "#A5D6A7", FONT_B, (820, 160)), + ("RF = 7x7", 14, "#A5D6A7", FONT_R, (820, 440)), + ("9 wag, kontekst 7px!", 12, "#78909C", FONT_R, (820, 470)), + ( + "Niebieski = pozycja wag filtra 3x3 | Szary = pominięte (dziury)", + 15, + "#B0BEC5", + FONT_R, + (80, 510), + ), + ( + "TE SAME 9 wag → WIĘKSZE pole widzenia → lepszy kontekst BEZ dodatkowych parametrów!", + 16, + "white", + FONT_R, + (80, 550), + ), + ( + "Mnemonik: DZIURY w filtrze — à trous = z dziurami (fr.)", + 16, + "#FFE082", + FONT_R, + (80, 600), + ), + ] + text_clips: list[VideoClip] = [dil_clip] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(dur) + .with_position(pos) + ) + text_clips.append(tc) + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + + # Slide 2: ASPP module step by step + def make_aspp_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + progress = min(t / (STEP_DUR * 0.7), 1.0) + + # Input feature map on left + frame[250:330, 50:130] = (70, 130, 200) + frame[250:252, 50:130] = (120, 180, 255) + frame[328:330, 50:130] = (120, 180, 255) + + # ASPP parallel branches + branches = [ + ("1x1 conv", 250, (200, 170), (100, 40), (80, 200, 120)), + ("rate=6", 310, (200, 250), (100, 40), (200, 160, 80)), + ("rate=12", 370, (200, 330), (100, 40), (200, 120, 60)), + ("rate=18", 430, (200, 410), (100, 40), (180, 100, 80)), + ("GAP", 490, (200, 490), (100, 40), (160, 80, 160)), + ] + n_branches = min(int(progress * 5) + 1, 5) + for i, (_lbl, _h, (bx, by), (bw, bh), color) in enumerate(branches): + if i < n_branches: + frame[by : by + bh, bx : bx + bw] = color + frame[by : by + 2, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + # Arrow from input + ay = by + bh // 2 + frame[ay - 1 : ay + 2, 133:197] = (150, 150, 170) + + # Concatenation box + if progress > 0.6: + frame[250:530, 380:420] = (50, 60, 80) + frame[250:252, 380:420] = (200, 200, 100) + frame[528:530, 380:420] = (200, 200, 100) + # Arrows from branches to concat + for i, (_lbl, _h, (bx, by), (bw, bh), _c) in enumerate(branches): + if i < n_branches: + ay = by + bh // 2 + frame[ay - 1 : ay + 2, bx + bw + 3 : 378] = (150, 150, 170) + + # Final conv after concat + if progress > 0.8: + frame[350:420, 450:550] = (100, 200, 100) + frame[350:352, 450:550] = (150, 230, 150) + frame[418:420, 450:550] = (150, 230, 150) + # Arrow from concat + frame[388:391, 423:448] = (150, 150, 170) + + return frame + + aspp_clip = VideoClip(make_aspp_frame, duration=STEP_DUR + 1).with_fps(FPS) + labels2 = [ + ( + "DeepLab: ASPP (Atrous Spatial Pyramid Pooling)", + 24, + "#FFE082", + FONT_B, + (80, 20), + ), + ( + "KROK 2: Multi-scale — analizuj obraz na WIELU skalach naraz", + 17, + "#A5D6A7", + FONT_R, + (80, 60), + ), + ("Wejście", 13, "#64B5F6", FONT_B, (55, 235)), + ("Conv 1x1", 12, "white", FONT_R, (210, 178)), + ("Dilated r=6", 12, "white", FONT_R, (205, 258)), + ("Dilated r=12", 12, "white", FONT_R, (203, 338)), + ("Dilated r=18", 12, "white", FONT_R, (203, 418)), + ("GAP (global)", 12, "white", FONT_R, (205, 498)), + ("Concat", 13, "#FFE082", FONT_B, (381, 537)), + ("Conv", 13, "#A5D6A7", FONT_B, (470, 425)), + ( + "5 gałęzi RÓWNOLEGŁYCH → różne skale kontekstu:", + 16, + "#B0BEC5", + FONT_R, + (550, 170), + ), + (" 1x1: kontekst punktowy (piksel)", 14, "#A5D6A7", FONT_R, (560, 210)), + (" r=6: kontekst lokalny (~13px)", 14, "#FFE082", FONT_R, (560, 245)), + (" r=12: kontekst średni (~25px)", 14, "#FFE082", FONT_R, (560, 280)), + (" r=18: kontekst szeroki (~37px)", 14, "#FFE082", FONT_R, (560, 315)), + (" GAP: kontekst GLOBALNY (cały obraz)", 14, "#CE93D8", FONT_R, (560, 350)), + ("Concat → 1x1 conv → mapa segmentacji", 16, "#A5D6A7", FONT_R, (550, 400)), + ( + "Efekt: sieć widzi OD piksela DO całego obrazu naraz!", + 17, + "white", + FONT_R, + (80, 600), + ), + ( + "Mnemonik: ASPP = Piramida z DZIURAMI, patrzy na 5 skal jednocześnie", + 15, + "#FFE082", + FONT_R, + (80, 645), + ), + ] + text_clips2: list[VideoClip] = [aspp_clip] + for text, fs, color, font, pos in labels2: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(dur) + .with_position(pos) + ) + text_clips2.append(tc) + slides.append( + CompositeVideoClip(text_clips2, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + + return slides + + +# ── Transformer Segmentation ──────────────────────────────────── +def _transformer_seg_demo() -> list[CompositeVideoClip]: + """Animate transformer-based segmentation: self-attention concept.""" + slides = [] + + # Slide 1: CNN local vs Transformer global + def make_attention_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + progress = min(t / (STEP_DUR * 0.7), 1.0) + + cell = 40 + grid_n = 6 + + # LEFT: CNN — local receptive field + lx, ly = 60, 200 + for r in range(grid_n): + for c in range(grid_n): + x = lx + c * cell + y = ly + r * cell + frame[y : y + cell - 2, x : x + cell - 2] = (35, 40, 55) + + # Highlight 3x3 kernel in CNN + if progress > 0.2: + cx, cy = 2, 2 # center cell + for dr in range(-1, 2): + for dc in range(-1, 2): + r, c = cy + dr, cx + dc + x = lx + c * cell + y = ly + r * cell + frame[y : y + cell - 2, x : x + cell - 2] = (70, 130, 200) + # Center highlighted more + x = lx + cx * cell + y = ly + cy * cell + frame[y : y + cell - 2, x : x + cell - 2] = (120, 180, 255) + + # RIGHT: Transformer — global attention + rx, ry = 680, 200 + for r in range(grid_n): + for c in range(grid_n): + x = rx + c * cell + y = ry + r * cell + frame[y : y + cell - 2, x : x + cell - 2] = (35, 40, 55) + + # All cells connected to center + if progress > 0.4: + cx_t, cy_t = 2, 2 + # Center cell + x0 = rx + cx_t * cell + cell // 2 + y0 = ry + cy_t * cell + cell // 2 + n_connections = int(progress * 36) + conn_idx = 0 + for r in range(grid_n): + for c in range(grid_n): + conn_idx += 1 + if conn_idx > n_connections: + break + x = rx + c * cell + y = ry + r * cell + # Color by "attention strength" — closer = stronger + dist = abs(r - cy_t) + abs(c - cx_t) + strength = max(30, 200 - dist * 30) + frame[y : y + cell - 2, x : x + cell - 2] = ( + strength // 3, + strength // 2, + strength, + ) + # Draw connection line + x1 = x + cell // 2 + y1 = y + cell // 2 + steps = max(abs(x1 - x0), abs(y1 - y0)) + if steps > 0: + for s in range(0, steps, 3): + px = x0 + int((x1 - x0) * s / steps) + py = y0 + int((y1 - y0) * s / steps) + if 0 <= px < W - 1 and 0 <= py < H - 1: + frame[py : py + 1, px : px + 1] = (200, 180, 50) + else: + continue + break + # Center highlighted strongly + x = rx + cx_t * cell + y = ry + cy_t * cell + frame[y : y + cell - 2, x : x + cell - 2] = (255, 200, 50) + + return frame + + att_clip = VideoClip(make_attention_frame, duration=STEP_DUR + 1).with_fps(FPS) + dur = STEP_DUR + 1 + labels = [ + ("Transformer: Self-Attention w segmentacji", 26, "#FFE082", FONT_B, (80, 20)), + ("CNN = LOKALNY kontekst", 18, "#64B5F6", FONT_B, (60, 160)), + ("Transformer = GLOBALNY kontekst", 18, "#FFE082", FONT_B, (680, 160)), + ("Filtr 3x3 widzi", 14, "#64B5F6", FONT_R, (60, 460)), + ("TYLKO 9 sąsiadów", 14, "#64B5F6", FONT_R, (60, 485)), + ("Self-attention: każdy", 14, "#FFE082", FONT_R, (680, 460)), + ("piksel widzi WSZYSTKIE!", 14, "#FFE082", FONT_R, (680, 485)), + ("vs", 28, "#B0BEC5", FONT_B, (450, 300)), + ] + text_clips: list[VideoClip] = [att_clip] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(dur) + .with_position(pos) + ) + text_clips.append(tc) + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + + # Slide 2: Self-attention Q/K/V step by step + qkv_lines = [ + ("Self-Attention: Q / K / V krok po kroku", 26, "#FFE082", FONT_B, (80, 30)), + ("Każdy piksel (token) tworzy 3 wektory:", 18, "#B0BEC5", FONT_R, (100, 100)), + ( + " Q (Query) = 'czego szukam?' - pytanie piksela", + 17, + "#64B5F6", + FONT_R, + (120, 145), + ), + ( + " K (Key) = 'co oferuj\u0119?' - odpowied\u017a piksela", + 17, + "#A5D6A7", + FONT_R, + (120, 185), + ), + ( + " V (Value) = 'moja warto\u015b\u0107' - informacja do przekazania", + 17, + "#FFE082", + FONT_R, + (120, 225), + ), + ("Algorytm attention:", 18, "#B0BEC5", FONT_R, (100, 285)), + ( + " 1. Mnożenie Q x K\u1d40 → macierz NxN (kto ważny dla kogo)", + 16, + "white", + FONT_R, + (120, 320), + ), + ( + " 2. Skalowanie: / √d (stabilność gradientów)", + 16, + "white", + FONT_R, + (120, 355), + ), + ( + " 3. Softmax → wagi attention (sumują się do 1)", + 16, + "white", + FONT_R, + (120, 390), + ), + ( + " 4. Mnożenie wag x V → ważona suma wartości", + 16, + "white", + FONT_R, + (120, 425), + ), + ( + "Attention(Q,K,V) = softmax(Q · K\u1d40 / √d) · V", + 20, + "#FFE082", + FONT_B, + (100, 480), + ), + ( + "Złożoność: O(n²) pamięci — n = liczba pikseli/tokenów", + 16, + "#EF9A9A", + FONT_R, + (100, 535), + ), + ( + "Dlatego SegFormer używa efficient attention (liniowa złożoność)", + 15, + "#78909C", + FONT_R, + (100, 570), + ), + ( + "SegFormer (2021): lightweight + hierarchiczny encoder", + 16, + "#A5D6A7", + FONT_R, + (100, 610), + ), + ( + "Mask2Former (2022): masked attention + unified (semantic+instance+panoptic)", + 16, + "#CE93D8", + FONT_R, + (100, 645), + ), + ] + slides.append(_text_slide(qkv_lines, duration=STEP_DUR + 1)) + + # Slide 3: Encoder-Decoder in DL summary + summary_lines = [ + ( + "Podsumowanie: Encoder-Decoder w segmentacji DL", + 24, + "#FFE082", + FONT_B, + (80, 30), + ), + ("Wspólna idea WSZYSTKICH sieci segmentacji:", 18, "#B0BEC5", FONT_R, (80, 90)), + ( + "Encoder: obraz → cechy (zmniejsza rozdzielczość, wyciąga CO)", + 16, + "#64B5F6", + FONT_R, + (100, 140), + ), + ( + "Decoder: cechy → mapa (zwiększa rozdzielczość, odtwarza GDZIE)", + 16, + "#A5D6A7", + FONT_R, + (100, 175), + ), + ( + "Skip: przenosi detale z encodera do decodera", + 16, + "#FFE082", + FONT_R, + (100, 210), + ), + ("", 10, "white", FONT_R, (100, 240)), + ( + "FCN (2015): Conv1x1 + skip → pierwsza end-to-end", + 16, + "#64B5F6", + FONT_R, + (100, 275), + ), + ( + "U-Net (2015): U-shape + skip concat → segmentacja medyczna", + 16, + "#A5D6A7", + FONT_R, + (100, 310), + ), + ( + "DeepLab (2018): dilated conv + ASPP → multi-scale kontekst", + 16, + "#FFE082", + FONT_R, + (100, 345), + ), + ( + "SegFormer: transformer encoder (globalny kontekst)", + 16, + "#CE93D8", + FONT_R, + (100, 380), + ), + ( + "Mask2Former: masked attention (unified, SOTA)", + 16, + "#CE93D8", + FONT_R, + (100, 415), + ), + ("", 10, "white", FONT_R, (100, 440)), + ( + "Ewolucja: więcej kontekstu + lepsze skip connections:", + 17, + "white", + FONT_R, + (80, 465), + ), + ( + " CNN lokal. → dilated (szersze RF) → transformer (global) → masked att.", + 16, + "#B0BEC5", + FONT_R, + (80, 505), + ), + ( + " addition skip → concat skip → cross-attention skip", + 16, + "#B0BEC5", + FONT_R, + (80, 540), + ), + ( + "Metryki: mIoU (standard), Dice (medycyna), Focal Loss (imbalance)", + 16, + "#90CAF9", + FONT_R, + (80, 590), + ), + ( + "Loss: Cross-Entropy per piksel + opcjonalnie Dice/Focal", + 15, + "#78909C", + FONT_R, + (80, 625), + ), + ] + slides.append(_text_slide(summary_lines, duration=STEP_DUR + 1)) + + return slides + + +# ── Methods comparison ──────────────────────────────────────────── +def _methods_comparison() -> CompositeVideoClip: + bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(10.0) + title = ( + _tc( + text="Porównanie metod segmentacji", + font_size=36, + color="white", + font=FONT_B, + ) + .with_duration(10.0) + .with_position(("center", 20)) + ) + + rows = [ + ("Metoda", "Typ", "Idea", "Mnemonik"), + ("Thresholding", "Klasyczna", "piksel > T → klasa 1", "PRÓG na bramce"), + ("Otsu", "Klasyczna", "auto-próg, min σ²", "AUTO-bramkarz"), + ("Region Growing", "Klasyczna", "BFS od seeda", "PLAMA atramentu"), + ("Watershed", "Klasyczna", "zalewanie minimów", "ZALEWANIE terenu"), + ("Mean Shift", "Klasyczna", "jądro → max gęstości", "KULKI do dołków"), + ("U-Net", "Deep Learning", "encoder-decoder + skip", "Litera U + mosty"), + ("DeepLab", "Deep Learning", "dilated conv + ASPP", "DZIURY w filtrze"), + ] + + clips: list[VideoClip] = [bg, title] + for i, row in enumerate(rows): + y_pos = 75 + i * 72 + col_x = [40, 210, 340, 660] + for j, cell in enumerate(row): + fs = 16 if i > 0 else 18 + color = "#64B5F6" if i == 0 else ("#E0E0E0" if j < 3 else "#FFE082") + tc = ( + _tc( + text=cell, + font_size=fs, + color=color, + font=FONT_B if i == 0 else FONT_R, + ) + .with_duration(10.0) + .with_position((col_x[j], y_pos)) + ) + clips.append(tc) + + return CompositeVideoClip(clips, size=(W, H)).with_effects( + [FadeIn(0.5), FadeOut(0.5)] + ) + + +# ── Main ────────────────────────────────────────────────────────── +def main() -> None: + """Generate the Q23 segmentation visualization video.""" + sections: list[VideoClip] = [] + + sections.append( + _make_header( + "Pytanie 23: Segmentacja obrazu", + "Problem, strategie klasyczne i sieci neuronowe", + duration=4.0, + ) + ) + + # Concept + sections.append(_make_header("Co to segmentacja?", "Etykieta klasy per piksel")) + sections.extend(_segmentation_concept()) + + # Thresholding + sections.append( + _make_header("Progowanie + Otsu", "Najprostsza metoda — automatyczny próg") + ) + sections.extend(_thresholding_demo()) + + # Region Growing + sections.append(_make_header("Region Growing", "Seed → BFS do podobnych sąsiadów")) + sections.extend(_region_growing_demo()) + + # Watershed + sections.append(_make_header("Watershed", "Obraz jako mapa topograficzna")) + sections.extend(_watershed_demo()) + + # FCN + sections.append( + _make_header("FCN (Deep Learning)", "Fully Convolutional Network — Conv 1x1") + ) + sections.extend(_fcn_demo()) + + # U-Net + sections.append( + _make_header( + "U-Net (Deep Learning)", "Architektura encoder-decoder + skip concat" + ) + ) + sections.extend(_unet_demo()) + + # DeepLab + sections.append( + _make_header( + "DeepLab v3+ (Deep Learning)", "Dilated convolution + ASPP — multi-scale" + ) + ) + sections.extend(_deeplab_demo()) + + # Transformer segmentation + sections.append( + _make_header( + "Transformer (SegFormer, Mask2Former)", "Self-attention — globalny kontekst" + ) + ) + sections.extend(_transformer_seg_demo()) + + # Comparison + sections.append(_methods_comparison()) + + # Summary + sections.append( + _make_header( + "Podsumowanie", + "Klasyczne: próg/region/watershed | DL: FCN/U-Net/DeepLab/Transformer", + duration=4.0, + ) + ) + + final = concatenate_videoclips(sections, method="compose") + final.write_videofile( + OUTPUT, fps=FPS, codec="libx264", audio=False, preset="medium", threads=4 + ) + print(f"Video saved to: {OUTPUT}") + + +if __name__ == "__main__": + main() diff --git a/python_pkg/praca_magisterska_video/visualize_q24.py b/python_pkg/praca_magisterska_video/visualize_q24.py new file mode 100644 index 0000000..d5c380a --- /dev/null +++ b/python_pkg/praca_magisterska_video/visualize_q24.py @@ -0,0 +1,1891 @@ +"""MoviePy visualization for PYTANIE 24: Object Detection. + +Creates animated video demonstrating: +- What detection is (bounding box + class + confidence) +- HOG + SVM pipeline (gradient → histogram → classify) +- Viola-Jones (Haar features, integral image, cascade) +- R-CNN evolution (R-CNN → Fast → Faster) +- YOLO one-stage detection +- Building a detector from a classifier +""" + +from __future__ import annotations + +import os +from pathlib import Path + +import numpy as np + +os.environ["FFMPEG_BINARY"] = "/usr/bin/ffmpeg" + +from moviepy import ( + ColorClip, + CompositeVideoClip, + TextClip, + VideoClip, + concatenate_videoclips, +) +from moviepy.video.fx import FadeIn, FadeOut + +# ── Constants ───────────────────────────────────────────────────── +W, H = 1280, 720 +FPS = 24 +STEP_DUR = 7.0 +HEADER_DUR = 4.0 +FONT_B = "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf" +FONT_R = "/usr/share/fonts/TTF/DejaVuSans.ttf" +OUTPUT_DIR = Path(__file__).resolve().parent / "videos" +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) +OUTPUT = str(OUTPUT_DIR / "q24_object_detection.mp4") + +BG_COLOR = (15, 20, 35) + + +def _tc(**kwargs: object) -> TextClip: + """TextClip wrapper that adds enough bottom margin to prevent clipping.""" + fs = kwargs.get("font_size", 24) + m = int(fs) // 3 + 2 + kwargs["margin"] = (0, m) + return TextClip(**kwargs) + + +def _make_header( + title: str, subtitle: str, duration: float = HEADER_DUR +) -> CompositeVideoClip: + bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(duration) + t = ( + _tc( + text=title, + font_size=48, + color="white", + font=FONT_B, + ) + .with_duration(duration) + .with_position(("center", 260)) + ) + s = ( + _tc( + text=subtitle, + font_size=24, + color="#90CAF9", + font=FONT_R, + ) + .with_duration(duration) + .with_position(("center", 340)) + ) + return CompositeVideoClip([bg, t, s], size=(W, H)).with_effects( + [FadeIn(0.5), FadeOut(0.5)] + ) + + +# ── Detection concept ──────────────────────────────────────────── +def _detection_concept() -> list[CompositeVideoClip]: + """Show what detection is: bounding box + class + confidence.""" + slides = [] + + def make_det_frame(_t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + + # Draw a "scene" with colored rectangles representing objects + # Sky background area + frame[140:500, 100:700] = (40, 50, 70) + + # "Car" object + frame[350:430, 150:320] = (180, 60, 60) + # "Person" object + frame[280:440, 450:520] = (60, 120, 180) + # "Tree" object + frame[200:400, 580:650] = (40, 130, 50) + + # Bounding boxes (with labels drawn as colored borders) + # Car bbox + for thickness in range(3): + t = thickness + frame[348 - t : 432 + t, 148 - t : 148 - t + 2] = (255, 80, 80) + frame[348 - t : 432 + t, 322 + t - 2 : 322 + t] = (255, 80, 80) + frame[348 - t : 348 - t + 2, 148 - t : 322 + t] = (255, 80, 80) + frame[432 + t - 2 : 432 + t, 148 - t : 322 + t] = (255, 80, 80) + + # Person bbox + for thickness in range(3): + t = thickness + frame[278 - t : 442 + t, 448 - t : 448 - t + 2] = (80, 180, 255) + frame[278 - t : 442 + t, 522 + t - 2 : 522 + t] = (80, 180, 255) + frame[278 - t : 278 - t + 2, 448 - t : 522 + t] = (80, 180, 255) + frame[442 + t - 2 : 442 + t, 448 - t : 522 + t] = (80, 180, 255) + + # Tree bbox + for thickness in range(3): + t = thickness + frame[198 - t : 402 + t, 578 - t : 578 - t + 2] = (80, 220, 100) + frame[198 - t : 402 + t, 652 + t - 2 : 652 + t] = (80, 220, 100) + frame[198 - t : 198 - t + 2, 578 - t : 652 + t] = (80, 220, 100) + frame[402 + t - 2 : 402 + t, 578 - t : 652 + t] = (80, 220, 100) + + # Comparison boxes on right side + # Classification + frame[180:260, 800:1150] = (35, 45, 65) + # Detection + frame[290:370, 800:1150] = (35, 45, 65) + # Segmentation + frame[400:480, 800:1150] = (35, 45, 65) + + return frame + + det_clip = VideoClip(make_det_frame, duration=STEP_DUR).with_fps(FPS) + text_clips: list[VideoClip] = [det_clip] + labels = [ + ("Detekcja obiektów — co to jest?", 28, "#FFE082", FONT_B, (100, 20)), + ("Wynik: (klasa, bounding box, pewność)", 20, "#B0BEC5", FONT_R, (100, 65)), + ("samochód 95%", 14, "#EF9A9A", FONT_B, (150, 340)), + ("osoba 88%", 14, "#64B5F6", FONT_B, (450, 268)), + ("drzewo 72%", 14, "#A5D6A7", FONT_B, (580, 188)), + ("Klasyfikacja: cały obraz → 1 etykieta", 15, "#78909C", FONT_R, (810, 210)), + ("Detekcja: bbox + klasa + pewność", 15, "#FFE082", FONT_R, (810, 320)), + ("Segmentacja: maska per piksel", 15, "#78909C", FONT_R, (810, 430)), + ("← granulacja rośnie →", 14, "#90CAF9", FONT_R, (810, 520)), + ] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR) + .with_position(pos) + ) + text_clips.append(tc) + + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── HOG + SVM pipeline ─────────────────────────────────────────── +def _hog_svm_demo() -> list[CompositeVideoClip]: + """Animate HOG feature computation and SVM classification.""" + slides = [] + + def make_hog_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + + progress = min(t / (STEP_DUR * 0.8), 1.0) + + # Pipeline stages as boxes with arrows + stages = [ + ("Gradient", (80, 250), (130, 80), (100, 160, 220)), + ("Orientacja", (260, 250), (130, 80), (80, 180, 140)), + ("Komórki 8x8", (440, 250), (130, 80), (200, 160, 80)), + ("Bloki 2x2", (620, 250), (130, 80), (200, 120, 60)), + ("Normalizacja", (800, 250), (130, 80), (180, 100, 80)), + ("SVM", (980, 250), (130, 80), (220, 80, 80)), + ] + + n_active = int(progress * len(stages)) + 1 + + for i, (_label, (sx, sy), (sw, sh), color) in enumerate(stages): + if i < n_active: + frame[sy : sy + sh, sx : sx + sw] = color + # Border + frame[sy : sy + 2, sx : sx + sw] = tuple( + min(c + 60, 255) for c in color + ) + frame[sy + sh - 2 : sy + sh, sx : sx + sw] = tuple( + min(c + 60, 255) for c in color + ) + + # Arrow to next + if i < len(stages) - 1: + ax = sx + sw + 5 + ay = sy + sh // 2 + frame[ay - 1 : ay + 2, ax : ax + 20] = (150, 150, 170) + + # Show gradient computation example at bottom + if progress > 0.2: + # Mini pixel grid showing gradient computation + gx, gy = 100, 430 + pixels = [50, 50, 200] + for idx, val in enumerate(pixels): + x = gx + idx * 50 + frame[gy : gy + 40, x : x + 40] = (val, val, val) + + return frame + + hog_clip = VideoClip(make_hog_frame, duration=STEP_DUR).with_fps(FPS) + text_clips: list[VideoClip] = [hog_clip] + labels = [ + ("HOG + SVM — pipeline detekcji pieszych", 28, "#FFE082", FONT_B, (80, 20)), + ( + "Mnemonik: GOKBN = Gradienty→Orientacja→Komórki→Bloki→Normalizacja", + 16, + "#A5D6A7", + FONT_R, + (80, 65), + ), + ("Gradient: siła i kierunek zmiany jasności", 14, "#64B5F6", FONT_R, (80, 95)), + ( + "Histogram: 9 binów (0°-180°, co 20°) per komórka 8x8", + 14, + "#78909C", + FONT_R, + (80, 120), + ), + ( + "[50][50][200] → Gx = 200-50 = 150 = silna krawędź!", + 16, + "#EF9A9A", + FONT_R, + (80, 490), + ), + ( + "Wektor HOG (3780 cech) → SVM: pieszy (+1) / tło (-1)", + 16, + "white", + FONT_R, + (80, 540), + ), + ( + "Sliding window 64x128 przesuwa się po obrazie → NMS → wynik", + 16, + "#90CAF9", + FONT_R, + (80, 580), + ), + ( + "SVM = LINIA MAKSYMALNEGO ODDECHU (max margines, support vectors)", + 16, + "#FFE082", + FONT_R, + (80, 620), + ), + ] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR) + .with_position(pos) + ) + text_clips.append(tc) + + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── Viola-Jones ─────────────────────────────────────────────────── +def _viola_jones_demo() -> list[CompositeVideoClip]: + """Animate Viola-Jones cascade concept.""" + slides = [] + + def make_cascade_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + + progress = min(t / (STEP_DUR * 0.8), 1.0) + + # Draw cascade "funnel" — stages filtering out non-faces + stages = 5 + start_width = 1000 + start_count = 10000 + x_center = W // 2 + + for i in range(stages): + stage_progress = min(progress * stages - i, 1.0) + if stage_progress <= 0: + break + + width = int(start_width * (1 - i * 0.18)) + int(start_count * (0.3**i)) + y = 150 + i * 100 + h_box = 60 + + # Stage box + x1 = x_center - width // 2 + frame[y : y + h_box, x1 : x1 + width] = ( + 50 + i * 10, + 60 + i * 10, + 80 + i * 10, + ) + # Border + frame[y : y + 2, x1 : x1 + width] = (100 + i * 20, 130 + i * 15, 200) + frame[y + h_box - 2 : y + h_box, x1 : x1 + width] = ( + 100 + i * 20, + 130 + i * 15, + 200, + ) + + # Arrow down to next + if i < stages - 1: + frame[y + h_box + 5 : y + h_box + 25, x_center - 1 : x_center + 2] = ( + 150, + 150, + 170, + ) + + # Red "rejected" arrows on sides + if i > 0: + # Left reject arrow + rx = x1 - 30 + ry = y + h_box // 2 + frame[ry - 1 : ry + 2, rx : rx + 25] = (200, 80, 80) + + return frame + + cascade_clip = VideoClip(make_cascade_frame, duration=STEP_DUR).with_fps(FPS) + text_clips: list[VideoClip] = [cascade_clip] + labels = [ + ( + "Viola-Jones — kaskada klasyfikatorów (2001)", + 28, + "#FFE082", + FONT_B, + (80, 20), + ), + ( + "3 innowacje: HIC = Haar + Integral Image + Cascade", + 20, + "#B0BEC5", + FONT_R, + (80, 65), + ), + ("Etap 1: 2 cechy Haar", 14, "#64B5F6", FONT_R, (170, 170)), + ("Etap 2: 10 cech", 14, "#64B5F6", FONT_R, (210, 270)), + ("Etap 3: 25 cech", 14, "#64B5F6", FONT_R, (240, 370)), + ("Etap 4: 50 cech", 14, "#64B5F6", FONT_R, (260, 470)), + ("→ TWARZ!", 16, "#A5D6A7", FONT_B, (590, 560)), + ( + "SITO: 99% okien odpada w pierwszych 3 etapach → REAL-TIME!", + 16, + "#EF9A9A", + FONT_R, + (80, 620), + ), + ( + "Haar: kontrast jasna/ciemna | Integral Image: suma prostokąta O(1) = 4 odczyty", + 14, + "#78909C", + FONT_R, + (80, 655), + ), + ("odrzucone →", 12, "#EF9A9A", FONT_R, (60, 275)), + ("odrzucone →", 12, "#EF9A9A", FONT_R, (60, 375)), + ] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR) + .with_position(pos) + ) + text_clips.append(tc) + + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── R-CNN Evolution ─────────────────────────────────────────────── +def _rcnn_evolution() -> list[CompositeVideoClip]: + """Animate R-CNN → Fast R-CNN → Faster R-CNN evolution.""" + slides = [] + + def make_evolution_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + + progress = min(t / (STEP_DUR * 0.8), 1.0) + + # Three rows: R-CNN, Fast R-CNN, Faster R-CNN + models = [ + ( + "R-CNN (2014)", + 50, + [ + ("Selective\nSearch", (200, 150), (100, 50), (120, 100, 60)), + ("2000x\nCNN", (350, 150), (80, 50), (180, 60, 60)), + ("2000x\nSVM", (480, 150), (80, 50), (180, 60, 60)), + ("NMS", (610, 150), (60, 50), (100, 140, 100)), + ], + "50 sec/obraz!", + ), + ( + "Fast R-CNN (2015)", + 300, + [ + ("Selective\nSearch", (200, 150), (100, 50), (120, 100, 60)), + ("1x CNN\n(cały obraz)", (350, 150), (100, 50), (80, 140, 200)), + ("ROI Pool\n(2000)", (500, 150), (90, 50), (200, 160, 80)), + ("FC", (640, 150), (50, 50), (100, 140, 100)), + ], + "2 sec/obraz", + ), + ( + "Faster R-CNN (2015)", + 300, + [ + ("CNN\nbackbone", (200, 150), (90, 50), (80, 140, 200)), + ("RPN\n(~300)", (340, 150), (80, 50), (200, 120, 60)), + ("ROI Pool", (470, 150), (80, 50), (200, 160, 80)), + ("FC", (600, 150), (50, 50), (100, 140, 100)), + ], + "0.2 sec → 5 fps!", + ), + ] + + n_models = int(progress * 3) + 1 + + for mi, (_name, base_y, stages, _speed) in enumerate(models): + if mi >= n_models: + break + for _label, (bx, by_off), (bw, bh), color in stages: + by = base_y + by_off - 150 + frame[by : by + bh, bx : bx + bw] = color + frame[by : by + 2, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + + # Arrows between stages + for si in range(len(stages) - 1): + sx = stages[si][1][0] + stages[si][2][0] + ex = stages[si + 1][1][0] + ay = base_y + 25 + frame[ay - 1 : ay + 2, sx + 3 : ex - 3] = (150, 150, 170) + + return frame + + evo_clip = VideoClip(make_evolution_frame, duration=STEP_DUR + 1).with_fps(FPS) + text_clips: list[VideoClip] = [evo_clip] + labels = [ + ("Ewolucja R-CNN — CORAZ MNIEJ MARNOWANIA", 28, "#FFE082", FONT_B, (80, 20)), + ("R-CNN (2014)", 20, "#EF9A9A", FONT_B, (50, 80)), + ("50 sec/obraz (2000x forward pass!)", 14, "#EF9A9A", FONT_R, (720, 100)), + ("Fast R-CNN (2015)", 20, "#64B5F6", FONT_B, (50, 330)), + ("2 sec/obraz (CNN raz + ROI Pool)", 14, "#64B5F6", FONT_R, (720, 350)), + ("Faster R-CNN (2015)", 20, "#A5D6A7", FONT_B, (50, 580)), + ("0.2 sec → 5 fps (RPN w sieci!)", 14, "#A5D6A7", FONT_R, (720, 600)), + ( + "Kluczowe innowacje: ROI Pooling → stały rozmiar | RPN → propozycje w sieci", + 14, + "#78909C", + FONT_R, + (80, 660), + ), + ] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR + 1) + .with_position(pos) + ) + text_clips.append(tc) + + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── R-CNN Detailed Pipeline ────────────────────────────────────── +def _rcnn_detailed() -> list[CompositeVideoClip]: + """Animate R-CNN step-by-step pipeline in detail.""" + slides = [] + + # Slide 1: R-CNN pipeline step by step + def make_rcnn_pipeline(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + progress = min(t / (STEP_DUR * 0.8), 1.0) + + # Step boxes arranged vertically with arrows + steps = [ + ((80, 130), (200, 55), (120, 100, 60), "1. Selective Search"), + ((80, 230), (200, 55), (180, 60, 60), "2. Wytnij 2000 regionów"), + ((80, 330), (200, 55), (70, 130, 200), "3. CNN per region"), + ((80, 430), (200, 55), (200, 100, 80), "4. SVM klasyfikuje"), + ((80, 530), (200, 55), (100, 180, 100), "5. Bbox regresja + NMS"), + ] + n_steps = min(int(progress * 5) + 1, 5) + for i, ((bx, by), (bw, bh), color, _lbl) in enumerate(steps): + if i < n_steps: + frame[by : by + bh, bx : bx + bw] = color + frame[by : by + 2, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + # Arrow down + if i < 4: + ax = bx + bw // 2 + ay = by + bh + 5 + frame[ay : ay + 20, ax - 1 : ax + 2] = (150, 150, 170) + + # Illustration: many overlapping regions from Selective Search + if progress > 0.2: + rng_local = np.random.default_rng(42) + n_boxes = min(int((progress - 0.2) * 15), 8) + for i in range(n_boxes): + rx = 500 + rng_local.integers(-30, 100) + ry = 200 + rng_local.integers(-20, 120) + rw = 60 + rng_local.integers(0, 80) + rh = 50 + rng_local.integers(0, 70) + c = (80 + i * 15, 100 + i * 10, 60 + i * 20) + for tt in range(2): + frame[ry - tt : ry + rh + tt, rx - tt : rx - tt + 2] = c + frame[ry - tt : ry + rh + tt, rx + rw + tt - 2 : rx + rw + tt] = c + frame[ry - tt : ry - tt + 2, rx - tt : rx + rw + tt] = c + frame[ry + rh + tt - 2 : ry + rh + tt, rx - tt : rx + rw + tt] = c + + return frame + + rcnn_clip = VideoClip(make_rcnn_pipeline, duration=STEP_DUR + 1).with_fps(FPS) + dur = STEP_DUR + 1 + labels = [ + ("R-CNN: krok po kroku (2014, Girshick)", 26, "#FFE082", FONT_B, (80, 20)), + ("Pipeline detekcji two-stage", 16, "#B0BEC5", FONT_R, (80, 60)), + ("Selective Search", 11, "white", FONT_R, (105, 145)), + ("2000 regionów", 11, "white", FONT_R, (105, 245)), + ("CNN per region", 11, "white", FONT_R, (105, 345)), + ("SVM klasyfikuje", 11, "white", FONT_R, (105, 445)), + ("Regresja + NMS", 11, "white", FONT_R, (105, 545)), + ("~2000 propozycji regionów", 14, "#78909C", FONT_R, (500, 155)), + ("(inteligentne łączenie", 13, "#78909C", FONT_R, (500, 180)), + ("podobnych fragmentów)", 13, "#78909C", FONT_R, (500, 200)), + ("Problem: 2000 x CNN forward pass", 16, "#EF9A9A", FONT_R, (400, 400)), + ("= 50 SEKUND na obraz!", 18, "#EF9A9A", FONT_B, (400, 430)), + ("CNN liczy cechy per region OSOBNO", 14, "#EF9A9A", FONT_R, (400, 470)), + ( + "→ regiony się nakładają → obliczenia się powtarzają!", + 14, + "#EF9A9A", + FONT_R, + (400, 495), + ), + ( + "Rozwiązanie: CNN raz na cały obraz → Fast R-CNN →", + 16, + "#A5D6A7", + FONT_R, + (80, 620), + ), + ] + text_clips: list[VideoClip] = [rcnn_clip] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(dur) + .with_position(pos) + ) + text_clips.append(tc) + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + + return slides + + +# ── ROI Pooling ────────────────────────────────────────────────── +def _roi_pooling_demo() -> list[CompositeVideoClip]: + """Animate ROI Pooling: key Fast R-CNN innovation.""" + slides = [] + + def make_roi_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + progress = min(t / (STEP_DUR * 0.7), 1.0) + + # Left: feature map with ROI highlighted + fm_x, fm_y = 60, 180 + fm_cell = 30 + fm_grid = 8 + for r in range(fm_grid): + for c in range(fm_grid): + x = fm_x + c * fm_cell + y = fm_y + r * fm_cell + # Random-looking feature values + val = 30 + ((r * 7 + c * 13 + 42) % 40) + frame[y : y + fm_cell - 1, x : x + fm_cell - 1] = ( + val, + val + 10, + val + 20, + ) + + # ROI region highlighted + roi_r1, roi_c1 = 2, 1 + roi_r2, roi_c2 = 6, 5 + for tt in range(3): + ry1 = fm_y + roi_r1 * fm_cell - tt + ry2 = fm_y + roi_r2 * fm_cell + tt + rx1 = fm_x + roi_c1 * fm_cell - tt + rx2 = fm_x + roi_c2 * fm_cell + tt + frame[ry1:ry2, rx1 : rx1 + 2] = (255, 200, 50) + frame[ry1:ry2, rx2 - 2 : rx2] = (255, 200, 50) + frame[ry1 : ry1 + 2, rx1:rx2] = (255, 200, 50) + frame[ry2 - 2 : ry2, rx1:rx2] = (255, 200, 50) + + # Arrow + if progress > 0.3: + frame[300:303, 310:380] = (150, 150, 170) + + # Middle: ROI divided into 3x3 grid (output_size) + if progress > 0.3: + out_x, out_y = 400, 220 + out_cell = 50 + out_n = 3 + roi_h = roi_r2 - roi_r1 + roi_w = roi_c2 - roi_c1 + for r in range(out_n): + for c in range(out_n): + x = out_x + c * out_cell + y = out_y + r * out_cell + + # Compute the max from corresponding region + src_r1 = roi_r1 + r * roi_h // out_n + src_r2 = roi_r1 + (r + 1) * roi_h // out_n + src_c1 = roi_c1 + c * roi_w // out_n + src_c2 = roi_c1 + (c + 1) * roi_w // out_n + max_val = 0 + for sr in range(src_r1, src_r2): + for sc in range(src_c1, src_c2): + v = 30 + ((sr * 7 + sc * 13 + 42) % 40) + max_val = max(max_val, v) + + frame[y : y + out_cell - 2, x : x + out_cell - 2] = ( + max_val, + max_val + 20, + max_val + 40, + ) + frame[y : y + 2, x : x + out_cell - 2] = (80, 200, 120) + frame[y + out_cell - 4 : y + out_cell - 2, x : x + out_cell - 2] = ( + 80, + 200, + 120, + ) + + # Arrow to FC + if progress > 0.6: + frame[300:303, 560:630] = (150, 150, 170) + # FC box + frame[270:340, 650:730] = (200, 100, 80) + frame[270:272, 650:730] = (240, 140, 120) + frame[338:340, 650:730] = (240, 140, 120) + + return frame + + roi_clip = VideoClip(make_roi_frame, duration=STEP_DUR + 1).with_fps(FPS) + dur = STEP_DUR + 1 + labels = [ + ("ROI Pooling: kluczowa innowacja Fast R-CNN", 26, "#FFE082", FONT_B, (80, 20)), + ( + "KROK 1: CNN raz na CAŁY obraz → feature mapa", + 17, + "#64B5F6", + FONT_R, + (80, 60), + ), + ( + "KROK 2: Wytnij ROI z feature mapy (nie z obrazu!)", + 17, + "#FFE082", + FONT_R, + (80, 90), + ), + ( + "KROK 3: Siatkuj ROI na 3x3 → max pool per komórka → stały rozmiar", + 17, + "#A5D6A7", + FONT_R, + (80, 120), + ), + ("Feature mapa", 14, "#64B5F6", FONT_B, (60, 160)), + ("ROI (żółta ramka)", 13, "#FFE082", FONT_R, (60, 440)), + ("ROI Pool 3x3", 14, "#A5D6A7", FONT_B, (400, 195)), + ("(max z komórki)", 13, "#78909C", FONT_R, (400, 380)), + ("FC", 14, "white", FONT_B, (670, 280)), + ( + "Problem: ROI mają RÓŻNE rozmiary, FC wymaga STAŁEGO", + 15, + "#B0BEC5", + FONT_R, + (80, 500), + ), + ( + "ROI Pooling: dzieli ROI na siatkę, max pool → STAŁY rozmiar!", + 16, + "white", + FONT_R, + (80, 535), + ), + ( + "Fast R-CNN: CNN raz → 1 feature mapa → ROI Pool 2000 regionów → 25x szybciej!", + 16, + "#A5D6A7", + FONT_R, + (80, 580), + ), + ( + "(R-CNN: 2000x CNN = 50s | Fast R-CNN: 1xCNN + ROI Pool = 2s)", + 15, + "#EF9A9A", + FONT_R, + (80, 620), + ), + ] + text_clips: list[VideoClip] = [roi_clip] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(dur) + .with_position(pos) + ) + text_clips.append(tc) + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── RPN + Anchor Boxes ─────────────────────────────────────────── +def _rpn_anchors_demo() -> list[CompositeVideoClip]: + """Animate RPN and anchor boxes: Faster R-CNN innovation.""" + slides = [] + + # Slide 1: Anchor boxes concept + def make_anchors_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + progress = min(t / (STEP_DUR * 0.7), 1.0) + + # Draw feature map grid point with multiple anchors + cx, cy = 350, 360 # center point on feature map + + # Draw a "feature map" grid background + cell = 60 + for r in range(-3, 4): + for c in range(-3, 4): + x = cx + c * cell - cell // 2 + y = cy + r * cell - cell // 2 + frame[y : y + cell - 1, x : x + cell - 1] = (30, 35, 48) + + # Center point highlighted + frame[cy - 5 : cy + 5, cx - 5 : cx + 5] = (255, 200, 50) + + # Draw anchors around center: 3 sizes x 3 ratios = 9 + anchor_specs = [ + # (half_w, half_h, color) + (30, 30, (200, 80, 80)), # small 1:1 + (20, 40, (200, 60, 60)), # small 1:2 + (40, 20, (180, 60, 60)), # small 2:1 + (60, 60, (80, 200, 80)), # medium 1:1 + (40, 80, (60, 180, 60)), # medium 1:2 + (80, 40, (60, 160, 60)), # medium 2:1 + (90, 90, (80, 80, 200)), # large 1:1 + (60, 120, (60, 60, 180)), # large 1:2 + (120, 60, (60, 60, 160)), # large 2:1 + ] + n_anchors = min(int(progress * 9) + 1, 9) + for i in range(n_anchors): + hw, hh, color = anchor_specs[i] + x1 = max(0, cx - hw) + y1 = max(0, cy - hh) + x2 = min(W - 1, cx + hw) + y2 = min(H - 1, cy + hh) + for tt in range(2): + frame[y1 - tt : y2 + tt, x1 - tt : x1 - tt + 2] = color + frame[y1 - tt : y2 + tt, x2 + tt - 2 : x2 + tt] = color + frame[y1 - tt : y1 - tt + 2, x1 - tt : x2 + tt] = color + frame[y2 + tt - 2 : y2 + tt, x1 - tt : x2 + tt] = color + + return frame + + anch_clip = VideoClip(make_anchors_frame, duration=STEP_DUR + 1).with_fps(FPS) + dur = STEP_DUR + 1 + labels = [ + ("Anchor Boxes + RPN (Faster R-CNN)", 26, "#FFE082", FONT_B, (80, 20)), + ( + "KROK 1: Anchory = predefiniowane kształty w każdej pozycji", + 17, + "#A5D6A7", + FONT_R, + (80, 60), + ), + ( + "3 rozmiary x 3 proporcje = 9 anchorów per punkt", + 16, + "#B0BEC5", + FONT_R, + (80, 90), + ), + ("Małe (1:1, 1:2, 2:1)", 14, "#EF9A9A", FONT_R, (750, 170)), + ("Średnie (1:1, 1:2, 2:1)", 14, "#A5D6A7", FONT_R, (750, 210)), + ("Duże (1:1, 1:2, 2:1)", 14, "#64B5F6", FONT_R, (750, 250)), + ("Żółty punkt = pozycja", 14, "#FFE082", FONT_R, (750, 310)), + ("na feature mapie", 14, "#FFE082", FONT_R, (750, 335)), + ("Sieć NIE predykuje bbox od zera!", 16, "white", FONT_R, (80, 530)), + ( + "Predykuje OFFSET od najbliższego anchora: (Δx, Δy, Δw, Δh)", + 16, + "#FFE082", + FONT_R, + (80, 565), + ), + ( + "+ P(obiekt) = 'czy w tym anchorze jest coś?'", + 16, + "#A5D6A7", + FONT_R, + (80, 600), + ), + ( + "Mnemonik: Anchor = KOTWICA — sieć dopasowuje bbox do kotwicy", + 15, + "#78909C", + FONT_R, + (80, 645), + ), + ] + text_clips: list[VideoClip] = [anch_clip] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(dur) + .with_position(pos) + ) + text_clips.append(tc) + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + + # Slide 2: RPN step by step + rpn_lines = [ + ( + "RPN: Region Proposal Network — krok po kroku", + 24, + "#FFE082", + FONT_B, + (80, 30), + ), + ( + "Zastępuje Selective Search SIECIĄ NEURONOWĄ (end-to-end!)", + 17, + "#B0BEC5", + FONT_R, + (80, 85), + ), + ("", 10, "white", FONT_R, (80, 110)), + ( + "1. Backbone (ResNet) przetwarza obraz → feature mapa [40x60x256]", + 16, + "#64B5F6", + FONT_R, + (100, 140), + ), + ( + "2. Filtr 3x3 przesuwa się po feature mapie", + 16, + "#A5D6A7", + FONT_R, + (100, 180), + ), + ( + "3. W KAŻDEJ pozycji (x,y) rozważ k=9 anchorów:", + 16, + "#FFE082", + FONT_R, + (100, 220), + ), + (" → P(obiekt) — 'czy tu jest coś?'", 15, "white", FONT_R, (120, 255)), + (" → (Δx, Δy, Δw, Δh) — poprawka pozycji", 15, "white", FONT_R, (120, 285)), + ( + "4. 40x60 pozycji x 9 anchorów = 21 600 kandydatów!", + 16, + "#EF9A9A", + FONT_R, + (100, 325), + ), + ( + "5. Weź ~300 z najwyższym P(obiekt) → ROI Pool → FC", + 16, + "#A5D6A7", + FONT_R, + (100, 365), + ), + ("", 10, "white", FONT_R, (100, 395)), + ("Porównanie generowania propozycji:", 17, "white", FONT_B, (80, 420)), + ( + " Selective Search: ~2000 regionów, osobny algorytm, ~2 sec", + 15, + "#EF9A9A", + FONT_R, + (100, 460), + ), + ( + " RPN: ~300 regionów, W SIECI, ~10 ms → 200x szybciej!", + 15, + "#A5D6A7", + FONT_R, + (100, 495), + ), + ("", 10, "white", FONT_R, (100, 520)), + ( + "Faster R-CNN = Backbone + RPN + ROI Pool + FC — WSZYSTKO end-to-end", + 17, + "#FFE082", + FONT_R, + (80, 545), + ), + ( + "→ 5 fps (0.2 sec/obraz) vs R-CNN 50 sec = 250x szybciej!", + 17, + "#A5D6A7", + FONT_R, + (80, 585), + ), + ( + "Wciąż two-stage: (1) RPN generuje propozycje, (2) FC klasyfikuje", + 15, + "#78909C", + FONT_R, + (80, 630), + ), + ] + slides.append(_text_slide(rpn_lines, duration=STEP_DUR + 1)) + + return slides + + +# ── YOLO ────────────────────────────────────────────────────────── +def _yolo_demo() -> list[CompositeVideoClip]: + """Animate YOLO grid detection concept.""" + slides = [] + + def make_yolo_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + + progress = min(t / (STEP_DUR * 0.7), 1.0) + + # Draw image with grid overlay + img_x, img_y = 100, 140 + img_size = 420 + grid_n = 7 + + # Background "image" + frame[img_y : img_y + img_size, img_x : img_x + img_size] = (50, 55, 70) + + # Objects in the image + frame[img_y + 80 : img_y + 200, img_x + 50 : img_x + 180] = ( + 180, + 60, + 60, + ) # "car" + frame[img_y + 150 : img_y + 350, img_x + 250 : img_x + 330] = ( + 60, + 120, + 180, + ) # "person" + + # Grid lines + cell = img_size // grid_n + for i in range(grid_n + 1): + # Vertical + x = img_x + i * cell + frame[img_y : img_y + img_size, x : x + 1] = (100, 100, 120) + # Horizontal + y = img_y + i * cell + frame[y : y + 1, img_x : img_x + img_size] = (100, 100, 120) + + # Highlight cells containing object centers + if progress > 0.3: + # Car center ~ cell (1, 1) + cx, cy = 1, 2 + hx = img_x + cx * cell + hy = img_y + cy * cell + frame[hy : hy + cell, hx : hx + cell] = np.clip( + frame[hy : hy + cell, hx : hx + cell].astype(int) + 40, 0, 255 + ).astype(np.uint8) + + if progress > 0.5: + # Person center ~ cell (4, 4) + cx, cy = 4, 4 + hx = img_x + cx * cell + hy = img_y + cy * cell + frame[hy : hy + cell, hx : hx + cell] = np.clip( + frame[hy : hy + cell, hx : hx + cell].astype(int) + 40, 0, 255 + ).astype(np.uint8) + + # Bounding boxes predictions from cells + if progress > 0.6: + # Car bbox + for tt in range(2): + frame[ + img_y + 78 - tt : img_y + 202 + tt, + img_x + 48 - tt : img_x + 48 - tt + 2, + ] = (255, 80, 80) + frame[ + img_y + 78 - tt : img_y + 202 + tt, + img_x + 182 + tt - 2 : img_x + 182 + tt, + ] = (255, 80, 80) + frame[ + img_y + 78 - tt : img_y + 78 - tt + 2, + img_x + 48 - tt : img_x + 182 + tt, + ] = (255, 80, 80) + frame[ + img_y + 202 + tt - 2 : img_y + 202 + tt, + img_x + 48 - tt : img_x + 182 + tt, + ] = (255, 80, 80) + + # Person bbox + for tt in range(2): + frame[ + img_y + 148 - tt : img_y + 352 + tt, + img_x + 248 - tt : img_x + 248 - tt + 2, + ] = (80, 180, 255) + frame[ + img_y + 148 - tt : img_y + 352 + tt, + img_x + 332 + tt - 2 : img_x + 332 + tt, + ] = (80, 180, 255) + frame[ + img_y + 148 - tt : img_y + 148 - tt + 2, + img_x + 248 - tt : img_x + 332 + tt, + ] = (80, 180, 255) + frame[ + img_y + 352 + tt - 2 : img_y + 352 + tt, + img_x + 248 - tt : img_x + 332 + tt, + ] = (80, 180, 255) + + return frame + + yolo_clip = VideoClip(make_yolo_frame, duration=STEP_DUR).with_fps(FPS) + text_clips: list[VideoClip] = [yolo_clip] + labels = [ + ("YOLO — You Only Look Once", 28, "#FFE082", FONT_B, (80, 20)), + ( + "Jednoetapowy detektor: siatka SxS → wszystkie detekcje naraz!", + 18, + "#B0BEC5", + FONT_R, + (80, 65), + ), + ("Siatka 7x7 = 49 komórek", 16, "#64B5F6", FONT_R, (600, 180)), + ("Każda komórka predykuje:", 16, "white", FONT_R, (600, 220)), + (" • B bbox (x, y, w, h, conf)", 14, "#B0BEC5", FONT_R, (600, 255)), + (" • C klas (prawdopodobieństwa)", 14, "#B0BEC5", FONT_R, (600, 285)), + ("Komórka odpowiada za obiekt", 14, "#A5D6A7", FONT_R, (600, 325)), + ("którego ŚRODEK w niej wpada", 14, "#A5D6A7", FONT_R, (600, 350)), + ("45-155 fps! (vs 5 fps Faster R-CNN)", 18, "#EF9A9A", FONT_B, (600, 400)), + ( + "Jedno przejście przez sieć → WSZYSTKIE detekcje naraz → NMS → wynik", + 14, + "#78909C", + FONT_R, + (80, 620), + ), + ( + "Two-stage (R-CNN): propozycje+klasyfikacja | One-stage (YOLO): bez propozycji!", + 14, + "#90CAF9", + FONT_R, + (80, 655), + ), + ] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR) + .with_position(pos) + ) + text_clips.append(tc) + + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── YOLO Architecture Detail ────────────────────────────────────── +def _yolo_architecture() -> list[CompositeVideoClip]: + """Show YOLO architecture: backbone → head, output tensor.""" + slides = [] + + # Slide 1: YOLO architecture breakdown + def make_yolo_arch(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + progress = min(t / (STEP_DUR * 0.7), 1.0) + + # Pipeline: Image → Backbone → Neck → Head → SxSx(B*5+C) tensor + blocks = [ + ((60, 280), (100, 80), (50, 70, 90), "Obraz"), + ((200, 280), (100, 80), (70, 130, 200), "Backbone"), + ((340, 280), (100, 80), (200, 160, 80), "Neck"), + ((480, 280), (100, 80), (200, 100, 60), "Head"), + ((620, 280), (160, 80), (80, 200, 120), "SxSx(B*5+C)"), + ] + n_blocks = min(int(progress * 5) + 1, 5) + for i, ((bx, by), (bw, bh), color, _lbl) in enumerate(blocks): + if i < n_blocks: + frame[by : by + bh, bx : bx + bw] = color + frame[by : by + 2, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + if i < 4: + ax = bx + bw + 5 + ay = by + bh // 2 + frame[ay - 1 : ay + 2, ax : ax + 25] = (150, 150, 170) + + # Output tensor breakdown (right side) + if progress > 0.6: + # Show SxS grid + gx, gy = 850, 180 + gs = 120 + gn = 4 # simplified from 7 + gc = gs // gn + for r in range(gn): + for c in range(gn): + x = gx + c * gc + y = gy + r * gc + frame[y : y + gc - 1, x : x + gc - 1] = (40, 50, 65) + # Highlight one cell + frame[gy + gc : gy + 2 * gc - 1, gx + gc : gx + 2 * gc - 1] = (80, 200, 120) + + return frame + + arch_clip = VideoClip(make_yolo_arch, duration=STEP_DUR + 1).with_fps(FPS) + dur = STEP_DUR + 1 + labels = [ + ("YOLO: Architektura — krok po kroku", 26, "#FFE082", FONT_B, (80, 20)), + ( + "One-stage: JEDEN forward pass → WSZYSTKIE detekcje naraz", + 17, + "#B0BEC5", + FONT_R, + (80, 60), + ), + ("Obraz", 13, "white", FONT_R, (85, 295)), + ("Backbone", 13, "white", FONT_R, (215, 295)), + ("(ResNet/", 11, "#78909C", FONT_R, (210, 370)), + ("Darknet)", 11, "#78909C", FONT_R, (210, 390)), + ("Neck", 13, "white", FONT_R, (365, 295)), + ("(FPN/", 11, "#78909C", FONT_R, (360, 370)), + ("PANet)", 11, "#78909C", FONT_R, (360, 390)), + ("Head", 13, "white", FONT_R, (505, 295)), + ("(conv)", 11, "#78909C", FONT_R, (500, 370)), + ("Tensor wyjścia", 13, "#A5D6A7", FONT_R, (640, 295)), + ("Każda komórka SxS predykuje:", 15, "#FFE082", FONT_R, (830, 320)), + (" B bbox x (x,y,w,h,conf)", 13, "#B0BEC5", FONT_R, (830, 350)), + (" + C klas (prob.)", 13, "#B0BEC5", FONT_R, (830, 375)), + ("= SxSx(Bx5+C) tensor", 13, "#A5D6A7", FONT_R, (830, 400)), + ("Np. 7x7x(2x5+20) = 7x7x30", 13, "#78909C", FONT_R, (830, 430)), + ( + "Two-stage (R-CNN): (1) propozycje → (2) klasyfikacja = 2 przejścia", + 15, + "#EF9A9A", + FONT_R, + (80, 470), + ), + ( + "One-stage (YOLO): siatka → predykcja all-in-one = 1 przejście!", + 15, + "#A5D6A7", + FONT_R, + (80, 505), + ), + ( + "Ewolucja YOLO: v1(2016)→v3→v5→v8(2023, anchor-free, SOTA)", + 16, + "#FFE082", + FONT_R, + (80, 555), + ), + ( + "SSD (2016): multi-scale feature maps → lepsza detekcja małych obiektów", + 15, + "#64B5F6", + FONT_R, + (80, 595), + ), + ( + "FPN: łączy wczesne warstwy (małe obiekty) + późne (duże obiekty)", + 15, + "#78909C", + FONT_R, + (80, 630), + ), + ] + text_clips: list[VideoClip] = [arch_clip] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(dur) + .with_position(pos) + ) + text_clips.append(tc) + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + + return slides + + +# ── DETR ────────────────────────────────────────────────────────── +def _detr_demo() -> list[CompositeVideoClip]: + """Animate DETR: transformer detection, object queries, no NMS.""" + slides = [] + + # Slide 1: DETR pipeline + def make_detr_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + progress = min(t / (STEP_DUR * 0.7), 1.0) + + # DETR pipeline: Image → Backbone → Encoder → Decoder → N predictions + blocks = [ + ((50, 260), (80, 60), (50, 70, 90)), + ((170, 260), (90, 60), (70, 130, 200)), + ((300, 260), (110, 60), (200, 120, 60)), + ((450, 260), (110, 60), (200, 80, 160)), + ((600, 260), (120, 60), (80, 200, 120)), + ] + n_blocks = min(int(progress * 5) + 1, 5) + for i, ((bx, by), (bw, bh), color) in enumerate(blocks): + if i < n_blocks: + frame[by : by + bh, bx : bx + bw] = color + frame[by : by + 2, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple( + min(c + 50, 255) for c in color + ) + if i < 4: + ax = bx + bw + 5 + ay = by + bh // 2 + frame[ay - 1 : ay + 2, ax : ax + 25] = (150, 150, 170) + + # Object queries illustration (right side) + if progress > 0.5: + qx, qy = 800, 140 + for i in range(6): + y = qy + i * 50 + w = 130 + active = i < 3 + color = (80, 180, 120) if active else (60, 50, 50) + frame[y : y + 35, qx : qx + w] = color + frame[y : y + 1, qx : qx + w] = tuple(min(c + 40, 255) for c in color) + + # Arrow from decoder to queries + frame[285:288, 723:798] = (150, 150, 170) + + return frame + + detr_clip = VideoClip(make_detr_frame, duration=STEP_DUR + 1).with_fps(FPS) + dur = STEP_DUR + 1 + labels = [ + ("DETR: DEtection TRansformer (2020)", 26, "#FFE082", FONT_B, (80, 20)), + ( + "Radykalnie prostszy pipeline: BEZ anchorów, BEZ NMS!", + 17, + "#B0BEC5", + FONT_R, + (80, 60), + ), + ("Obraz", 12, "white", FONT_R, (65, 275)), + ("Backbone", 12, "white", FONT_R, (185, 275)), + ("Transformer", 12, "white", FONT_R, (310, 275)), + ("Encoder", 12, "white", FONT_R, (325, 295)), + ("Transformer", 12, "white", FONT_R, (460, 275)), + ("Decoder", 12, "white", FONT_R, (478, 295)), + ("N predykcji", 12, "white", FONT_R, (615, 275)), + ("Object Queries:", 14, "#FFE082", FONT_B, (800, 115)), + ("samochód 95%", 11, "white", FONT_R, (810, 148)), + ("pies 88%", 11, "white", FONT_R, (810, 198)), + ("rower 72%", 11, "white", FONT_R, (810, 248)), + ("brak", 11, "#78909C", FONT_R, (810, 298)), + ("brak", 11, "#78909C", FONT_R, (810, 348)), + ("brak", 11, "#78909C", FONT_R, (810, 398)), + ("100 wyuczonych queries", 13, "#FFE082", FONT_R, (800, 440)), + ("→ każdy 'szuka' obiektu", 13, "#FFE082", FONT_R, (800, 465)), + ] + text_clips: list[VideoClip] = [detr_clip] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(dur) + .with_position(pos) + ) + text_clips.append(tc) + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + + # Slide 2: Why no NMS + Hungarian matching + detr_details = [ + ("DETR: Dlaczego bez NMS? — krok po kroku", 24, "#FFE082", FONT_B, (80, 30)), + ( + "Problem NMS: duplikaty detekcji → ręcznie usuwaj post-hoc", + 16, + "#EF9A9A", + FONT_R, + (80, 90), + ), + ( + "DETR rozwiązanie: Hungarian matching (dopasowanie węgierskie)", + 17, + "#A5D6A7", + FONT_R, + (80, 130), + ), + ("", 10, "white", FONT_R, (80, 155)), + ("Jak to działa podczas TRENINGU:", 17, "white", FONT_B, (80, 180)), + (" 1. Sieć daje N=100 predykcji (queries)", 15, "#64B5F6", FONT_R, (100, 220)), + ( + " 2. Na obrazie jest np. 5 obiektów (ground truth)", + 15, + "#64B5F6", + FONT_R, + (100, 255), + ), + ( + " 3. Hungarian matching: optymalne dopasowanie 1:1", + 15, + "#FFE082", + FONT_R, + (100, 290), + ), + ( + " → query_1 ↔ gt_samochód (najlepsze dopasowanie)", + 14, + "#A5D6A7", + FONT_R, + (120, 325), + ), + (" → query_7 ↔ gt_pies", 14, "#A5D6A7", FONT_R, (120, 355)), + (" → query_3 ↔ gt_rower", 14, "#A5D6A7", FONT_R, (120, 385)), + ( + " → pozostałe 97 queries ↔ klasa 'brak obiektu'", + 14, + "#78909C", + FONT_R, + (120, 415), + ), + ( + " 4. Każdy obiekt ma DOKŁADNIE 1 predykcję → BRAK duplikatów!", + 15, + "#A5D6A7", + FONT_R, + (100, 455), + ), + ("", 10, "white", FONT_R, (100, 475)), + ( + "Self-attention w encoderze: cechy obrazu 'rozmawiają' ze sobą", + 15, + "#64B5F6", + FONT_R, + (80, 500), + ), + ( + "Cross-attention w decoderze: queries 'pytają' cechy obrazu", + 15, + "#CE93D8", + FONT_R, + (80, 535), + ), + ( + "→ query 'rozumie' który fragment obrazu to 'jego' obiekt", + 15, + "#FFE082", + FONT_R, + (80, 570), + ), + ( + "DETR = Detekcja Eliminująca Trikowe Redundancje (NMS, anchory)", + 16, + "#FFE082", + FONT_R, + (80, 620), + ), + ( + "Wada: wolniejszy trening (O(n²) attention) | Zaleta: prostszy pipeline!", + 15, + "#78909C", + FONT_R, + (80, 660), + ), + ] + slides.append(_text_slide(detr_details, duration=STEP_DUR + 1)) + + # Slide 3: Two-stage vs One-stage vs Transformer summary + summary_lines = [ + ( + "Podsumowanie: Two-stage vs One-stage vs Transformer", + 22, + "#FFE082", + FONT_B, + (80, 30), + ), + ("", 10, "white", FONT_R, (80, 55)), + ("TWO-STAGE (R-CNN family):", 18, "#EF9A9A", FONT_B, (80, 90)), + ( + " (1) Generuj propozycje → (2) Klasyfikuj per region", + 15, + "white", + FONT_R, + (100, 125), + ), + ( + " + Wysoka precyzja | - Wolniejsze (2 przejścia)", + 15, + "#78909C", + FONT_R, + (100, 155), + ), + ( + " R-CNN → Fast R-CNN → Faster R-CNN (0.2s)", + 15, + "#B0BEC5", + FONT_R, + (100, 185), + ), + ("", 10, "white", FONT_R, (80, 210)), + ("ONE-STAGE (YOLO, SSD):", 18, "#A5D6A7", FONT_B, (80, 240)), + ( + " Siatka → predykcja all-in-one (1 przejście)", + 15, + "white", + FONT_R, + (100, 275), + ), + ( + " + Bardzo szybkie (45-155 fps) | - Historycznie mniej precyzyjne", + 15, + "#78909C", + FONT_R, + (100, 305), + ), + ( + " YOLOv8 (2023): anchor-free, dorównuje two-stage!", + 15, + "#B0BEC5", + FONT_R, + (100, 335), + ), + ("", 10, "white", FONT_R, (80, 360)), + ("TRANSFORMER (DETR):", 18, "#CE93D8", FONT_B, (80, 390)), + ( + " Object queries + self-attention (globalny kontekst)", + 15, + "white", + FONT_R, + (100, 425), + ), + ( + " + Brak NMS/anchorów | - Wolniejszy trening (O(n²))", + 15, + "#78909C", + FONT_R, + (100, 455), + ), + ( + " Hungarian matching → 1:1 obiekt↔predykcja → brak duplikatów", + 15, + "#B0BEC5", + FONT_R, + (100, 485), + ), + ("", 10, "white", FONT_R, (80, 510)), + ( + "Trend: coraz prostsze pipeline, mniej ręcznych komponentów", + 17, + "white", + FONT_R, + (80, 540), + ), + ( + " R-CNN (SS+CNN+SVM+NMS) → YOLO (backbone+head+NMS) → DETR (backbone+transformer)", + 14, + "#90CAF9", + FONT_R, + (80, 580), + ), + ( + "Metryki: mAP@0.5 (standard), mAP@0.5:0.95 (surowsza), IoU do dopasowania", + 15, + "#78909C", + FONT_R, + (80, 630), + ), + ] + slides.append(_text_slide(summary_lines, duration=STEP_DUR + 1)) + + return slides + + +# ── NMS + IoU ───────────────────────────────────────────────────── +def _nms_iou_demo() -> list[CompositeVideoClip]: + """Animate NMS and IoU concepts.""" + slides = [] + + def make_nms_frame(t: float) -> np.ndarray: + frame = np.zeros((H, W, 3), dtype=np.uint8) + frame[:] = BG_COLOR + + progress = min(t / (STEP_DUR * 0.7), 1.0) + + # Draw overlapping bounding boxes + ox, oy = 100, 200 + obj_w, obj_h = 150, 120 + + # Multiple overlapping detections for same object + boxes = [ + (ox, oy, obj_w, obj_h, 0.95, (255, 80, 80)), # best + (ox + 15, oy - 10, obj_w + 10, obj_h + 5, 0.90, (200, 60, 60)), + (ox - 10, oy + 5, obj_w - 5, obj_h + 10, 0.85, (160, 50, 50)), + ] + # Different object far away + boxes.append((ox + 350, oy + 50, 100, 100, 0.40, (80, 180, 255))) + + for i, (bx, by, bw, bh, _conf, color) in enumerate(boxes): + if progress > 0.4 and i > 0 and i < 3: + # After NMS, these get removed (shown as faded/crossed) + color = (60, 40, 40) + + for tt in range(2): + frame[by - tt : by + bh + tt, bx - tt : bx - tt + 2] = color + frame[by - tt : by + bh + tt, bx + bw + tt - 2 : bx + bw + tt] = color + frame[by - tt : by - tt + 2, bx - tt : bx + bw + tt] = color + frame[by + bh + tt - 2 : by + bh + tt, bx - tt : bx + bw + tt] = color + + # IoU visualization on right side + iou_x, iou_y = 700, 200 + # Box A + frame[iou_y : iou_y + 100, iou_x : iou_x + 100] = (80, 80, 200) + # Box B (overlapping) + frame[iou_y + 40 : iou_y + 140, iou_x + 40 : iou_x + 140] = (200, 80, 80) + # Intersection highlighted + frame[iou_y + 40 : iou_y + 100, iou_x + 40 : iou_x + 100] = (200, 150, 200) + + return frame + + nms_clip = VideoClip(make_nms_frame, duration=STEP_DUR).with_fps(FPS) + text_clips: list[VideoClip] = [nms_clip] + labels = [ + ("NMS (Non-Maximum Suppression) + IoU", 28, "#FFE082", FONT_B, (80, 20)), + ( + "NMS = Najlepszy Ma Się dobrze — zachowaj najlepszą, usuń duplikaty", + 18, + "#B0BEC5", + FONT_R, + (80, 65), + ), + ("conf=0.95 ✓", 14, "#A5D6A7", FONT_B, (100, 340)), + ("0.90 ✗ IoU>0.5", 13, "#EF9A9A", FONT_R, (100, 365)), + ("0.85 ✗ IoU>0.5", 13, "#EF9A9A", FONT_R, (100, 390)), + ("0.40 ✓ INNY obiekt", 13, "#64B5F6", FONT_R, (100, 420)), + ("IoU = Intersection over Union", 18, "#FFE082", FONT_B, (700, 160)), + ("IoU = pole(∩) / pole(AUB)", 16, "white", FONT_R, (700, 380)), + ("Fioletowy = intersection", 14, "#CE93D8", FONT_R, (700, 410)), + ("IoU > 0.5 → TEN SAM obiekt → usuń", 14, "#EF9A9A", FONT_R, (700, 440)), + ("IoU < 0.5 → INNY obiekt → zachowaj", 14, "#A5D6A7", FONT_R, (700, 470)), + ( + "DETR: jedyny detektor BEZ NMS (Hungarian matching zamiast tego)", + 14, + "#78909C", + FONT_R, + (80, 620), + ), + ] + for text, fs, color, font, pos in labels: + tc = ( + _tc(text=text, font_size=fs, color=color, font=font) + .with_duration(STEP_DUR) + .with_position(pos) + ) + text_clips.append(tc) + + slides.append( + CompositeVideoClip(text_clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + ) + return slides + + +# ── Detector from Classifier ───────────────────────────────────── +def _detector_from_classifier() -> list[CompositeVideoClip]: + """Show 3 approaches to building a detector from a classifier.""" + slides = [] + + approaches = [ + ( + "Podejście 1: Sliding Window (NAJWOLNIEJSZE)", + [ + ("Okno przesuwa się po obrazie w wielu skalach", "#B0BEC5"), + ("Każde okno → klasyfikator (np. ResNet) → klasa + pewność", "#B0BEC5"), + ("~18 000 okien x 10ms = ~3 minuty na obraz!", "#EF9A9A"), + ("Mnemonik: WYCINAJ i PYTAJ — jak wycinanie ciasteczek", "#FFE082"), + ], + "SRF", + ), + ( + "Podejście 2: Region Proposals (= R-CNN)", + [ + ("Selective Search → ~2000 inteligentnych regionów", "#B0BEC5"), + ("Każdy region → CNN → wektor cech → SVM klasyfikuje", "#B0BEC5"), + ("~2000 x 10ms = ~20 sec — 9x szybciej!", "#64B5F6"), + ( + "Mnemonik: INTELIGENTNE CIĘCIE — wytnij tylko tam gdzie wiśnie", + "#FFE082", + ), + ], + "SRF", + ), + ( + "Podejście 3: Fine-tune backbone (NAJLEPSZE)", + [ + ( + "Pretrained backbone (ResNet) → odetnij FC → dodaj detection head", + "#B0BEC5", + ), + ( + "Detection head = głowica klasyfikacji + głowica regresji bbox", + "#B0BEC5", + ), + ("~0.2 sec/obraz, najlepsza jakość (mAP ~42%)", "#A5D6A7"), + ("Mnemonik: PRZESZCZEP GŁOWY — ten sam silnik, nowa głowa", "#FFE082"), + ], + "SRF", + ), + ] + + for title, points, _mnem in approaches: + lines = [ + (title, 24, "#FFE082", FONT_B, (80, 140)), + ] + for i, (text, color) in enumerate(points): + lines.append((f"• {text}", 18, color, FONT_R, (100, 220 + i * 50))) + + lines.append( + ( + "Detektor z klasyfikatora: SRF = Sliding → Region → Fine-tune", + 16, + "#78909C", + FONT_R, + (80, 520), + ) + ) + lines.append( + ( + "= Szukaj Ręcznie, Finalnie optymalizuj!", + 16, + "#90CAF9", + FONT_R, + (80, 550), + ) + ) + + slides.append(_text_slide(lines, duration=STEP_DUR)) + + return slides + + +def _text_slide( + lines: list[tuple[str, int, str, str, tuple[str | int, str | int]]], + duration: float = STEP_DUR, +) -> CompositeVideoClip: + bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(duration) + clips: list[VideoClip] = [bg] + for text, font_size, color, font, pos in lines: + tc = ( + _tc( + text=text, + font_size=font_size, + color=color, + font=font, + ) + .with_duration(duration) + .with_position(pos) + ) + clips.append(tc) + return CompositeVideoClip(clips, size=(W, H)).with_effects( + [FadeIn(0.3), FadeOut(0.3)] + ) + + +# ── Methods comparison ──────────────────────────────────────────── +def _methods_comparison() -> CompositeVideoClip: + bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(10.0) + title = ( + _tc( + text="Porównanie detektorów", + font_size=36, + color="white", + font=FONT_B, + ) + .with_duration(10.0) + .with_position(("center", 20)) + ) + + rows = [ + ("Model", "Rok", "Typ", "Szybkość", "Kluczowe"), + ("HOG+SVM", "2005", "Klasyczny", "~1 fps", "Gradient histogramy"), + ("Viola-Jones", "2001", "Klasyczny", "30+ fps", "Haar+Cascade"), + ("R-CNN", "2014", "Two-stage", "50 sec!", "CNN per region"), + ("Fast R-CNN", "2015", "Two-stage", "2 sec", "ROI Pooling"), + ("Faster R-CNN", "2015", "Two-stage", "5 fps", "RPN w sieci"), + ("YOLO", "2016", "One-stage", "45+ fps", "Siatka SxS"), + ("DETR", "2020", "Transformer", "~40 fps", "Bez NMS!"), + ] + + clips: list[VideoClip] = [bg, title] + for i, row in enumerate(rows): + y_pos = 75 + i * 72 + col_x = [40, 200, 280, 400, 530] + for j, cell in enumerate(row): + fs = 16 if i > 0 else 18 + color = "#64B5F6" if i == 0 else "#E0E0E0" + tc = ( + _tc( + text=cell, + font_size=fs, + color=color, + font=FONT_B if i == 0 else FONT_R, + ) + .with_duration(10.0) + .with_position((col_x[j], y_pos)) + ) + clips.append(tc) + + return CompositeVideoClip(clips, size=(W, H)).with_effects( + [FadeIn(0.5), FadeOut(0.5)] + ) + + +# ── Main ────────────────────────────────────────────────────────── +def main() -> None: + """Generate the Q24 object detection visualization video.""" + sections: list[VideoClip] = [] + + sections.append( + _make_header( + "Pytanie 24: Detekcja obiektów", + "Problem, metody klasyczne, deep learning", + duration=4.0, + ) + ) + + # What is detection + sections.append( + _make_header("Co to detekcja?", "Lokalizacja (bbox) + klasyfikacja (klasa)") + ) + sections.extend(_detection_concept()) + + # HOG + SVM + sections.append( + _make_header("HOG + SVM (2005)", "Klasyczny pipeline — gradient histogramy") + ) + sections.extend(_hog_svm_demo()) + + # Viola-Jones + sections.append( + _make_header("Viola-Jones (2001)", "Haar features + Integral Image + Cascade") + ) + sections.extend(_viola_jones_demo()) + + # R-CNN evolution (overview) + sections.append(_make_header("Ewolucja R-CNN", "R-CNN → Fast R-CNN → Faster R-CNN")) + sections.extend(_rcnn_evolution()) + + # R-CNN detailed pipeline + sections.append( + _make_header("R-CNN: krok po kroku", "Selective Search → 2000xCNN → SVM → NMS") + ) + sections.extend(_rcnn_detailed()) + + # ROI Pooling + sections.append( + _make_header("ROI Pooling (Fast R-CNN)", "CNN raz + ROI Pool → 25x szybciej") + ) + sections.extend(_roi_pooling_demo()) + + # RPN + Anchors + sections.append( + _make_header("RPN + Anchor Boxes", "Faster R-CNN: propozycje W SIECI") + ) + sections.extend(_rpn_anchors_demo()) + + # YOLO + sections.append( + _make_header("YOLO (2016)", "You Only Look Once — jednoetapowy detektor") + ) + sections.extend(_yolo_demo()) + + # YOLO architecture detail + sections.append( + _make_header("YOLO: Architektura", "Backbone → Neck → Head → tensor SxS") + ) + sections.extend(_yolo_architecture()) + + # DETR + sections.append(_make_header("DETR (2020)", "Transformer: bez NMS, bez anchorów!")) + sections.extend(_detr_demo()) + + # NMS + IoU + sections.append(_make_header("NMS + IoU", "Post-processing — usuwanie duplikatów")) + sections.extend(_nms_iou_demo()) + + # Detector from classifier + sections.append( + _make_header( + "Detektor z klasyfikatora", "3 podejścia: Sliding → Region → Fine-tune" + ) + ) + sections.extend(_detector_from_classifier()) + + # Comparison table + sections.append(_methods_comparison()) + + # Summary + sections.append( + _make_header( + "Podsumowanie", + "Klasyczne: HOG+SVM, Viola-Jones | DL: R-CNN, YOLO, DETR", + duration=4.0, + ) + ) + + final = concatenate_videoclips(sections, method="compose") + final.write_videofile( + OUTPUT, fps=FPS, codec="libx264", audio=False, preset="medium", threads=4 + ) + print(f"Video saved to: {OUTPUT}") + + +if __name__ == "__main__": + main()