5.17. Katalog standardních jader

Klasické zpracování obrazu nashromáždilo poměrně rozsáhlý katalog vzorů vah jader, které se objevují stále dokola – detektory hran, zostřovače, embossy, vyhlazovače, pohybová rozmazání – a každý z nich běží přes morph(). Každé je krátké, každé dělá jednu věc a většina z nich je po pochopení základní logiky vah snadno čitelná.

Pokud není uvedeno jinak, jsou všechna níže uvedená jádra typu 3 krát 3, takže ve volání všechna používají size=1. Struktura vah každého jádra je popsána vedle něj, protože právě čtení vah buduje intuici, proč jedno jádro embossuje a jiné zostřuje.

5.17.1. Jednotkové jádro

Nejjednodušším možným jádrem je jednotkové (identity) – jednička uprostřed, nuly všude jinde:

identity = [0, 0, 0,
            0, 1, 0,
            0, 0, 0]

img.morph(1, identity)

Každý výstupní pixel přebírá svou hodnotu ze středu okolí, což je vstupní pixel na stejné pozici. Obraz projde beze změny. Jednotkové jádro nemá jako filtr žádné praktické využití, ale je užitečným základem pro pochopení každého dalšího jádra: jakékoli nejednotkové jádro je jednotkové jádro plus nějaká úprava.

Jádro, jehož středová váha je velká a kolem ní jsou malé záporné váhy, odečítá okolí od středu. Jádro s nulovou středovou váhou ignoruje samotný pixel a reaguje pouze na rozdíly mezi svými sousedy. Číst jádro tímto způsobem – co středová váha dělá s pixelem, co okolní váhy přidávají nebo odebírají – je nejrychlejší způsob, jak předpovědět jeho účinek.

5.17.2. Detekce hran

Jádra pro detekci hran silně reagují na pozice, kde se jas rychle mění v určitém směru, a produkují téměř nulový výstup tam, kde je jas rovnoměrný. Jsou to rodina, jejíž váhy se sčítají na nulu: rovná plocha (každý pixel se stejnou hodnotou) produkuje nulový výstup, protože každá kladná váha je přesně vyrušena zápornou váhou se stejnou velikostí.

Sobel-x je kanonickým příkladem. Detekuje svislé hrany (přechody jasu zleva/zprava):

sobel_x = [-1,  0,  1,
           -2,  0,  2,
           -1,  0,  1]

img.morph(1, sobel_x, mul=0.25, add=128)

Odpovídající Sobel-y je tentýž vzor otočený o 90 stupňů; detekuje vodorovné hrany (přechody jasu nahoru/dolů):

sobel_y = [-1, -2, -1,
            0,  0,  0,
            1,  2,  1]

Prostřední řádek Sobelu-x má váhy -2 a 2 namísto -1 a 1. Extra váha na středovém řádku dává jádru malé vestavěné vyhlazení ve směru podél hrany, díky čemuž je odolnější vůči šumu než jednodušší operátor Prewitt, který tyto extra velikosti vypouští:

prewitt_x = [-1, 0, 1,
             -1, 0, 1,
             -1, 0, 1]

prewitt_y = [-1, -1, -1,
              0,  0,  0,
              1,  1,  1]

Prewitt váží každý řádek stejně, takže jeho odezva je o něco ostřejší než u Sobelu, za cenu vyšší citlivosti na šum jednotlivých pixelů (náklady na běh jádra jsou shodné – konvoluce odvede stejnou práci, ať jsou váhy jakékoli). Na čistém obrazu s výraznými hranami je dokonale použitelnou náhradou za Sobel.

Scharr jde opačným směrem. Jeho váhy jsou větší a vyladěné pro přesnou detekci směru hrany při jemnějších úhlech:

scharr_x = [-3,   0,  3,
            -10,  0, 10,
            -3,   0,  3]

img.morph(1, scharr_x, mul=0.0625, add=128)

Dělitel mul=0.0625 (1/16) vrací výstup zpět do rozsahu 0255 po větším součtu součinů. Scharr je správnou odpovědí, když aplikace potřebuje geometricky nejvěrnější odezvu gradientu a je ochotna za to zaplatit o něco více aritmetiky.

5.17.3. Laplacián

Jádro Laplaciánu reaguje na hrany v libovolném směru naráz. Zatímco každý ze Sobelů detekuje změny jasu podél jedné osy, symetrický vzor vah Laplaciánu reaguje stejně bez ohledu na to, kterým směrem hrana vede:

laplacian_4 = [ 0, -1,  0,
               -1,  4, -1,
                0, -1,  0]

img.morph(1, laplacian_4, add=128)

Struktura: středová váha 4, čtyři vodorovní/svislí sousedé s váhou -1, čtyři diagonály s váhou nula. Jádro se sčítá na nulu, takže rovné plochy produkují nulový výstup. Tam, kde se jas mění, se hodnota středu liší od průměru jeho čtyř hlavních sousedů a výstupem je velikost tohoto rozdílu.

8-spojitá varianta zahrnuje i diagonální sousedy:

laplacian_8 = [-1, -1, -1,
               -1,  8, -1,
               -1, -1, -1]

Každé jádro detekuje mírně odlišné věci. 4-spojitá verze produkuje čistší výstup na vodorovných a svislých hranách; 8-spojitá je více izotropní – reaguje stejně dobře ve všech směrech – ale produkuje o něco šumnější výstup. 8-spojité jádro koluje také pod názvem outline, podle svého využití pro vizualizaci hran.

5.17.4. Zostřování

Jádro pro zostřování je jednotkové jádro plus jádro odezvy na hrany. Výstupem je původní obraz plus kopie hran, takže vysokofrekvenční příznaky jsou zesíleny vůči hladkým vnitřkům.

Standardní 4-spojité zostřovací jádro přidává 4-spojitý Laplacián k jednotkovému jádru:

sharpen = [ 0, -1,  0,
           -1,  5, -1,
            0, -1,  0]

img.morph(1, sharpen)

Čtení jádra: středová váha je identity (1) + Laplacian centre (4) = 5 a okolí odpovídá Laplaciánu. Rovné plochy produkují 5 * 1 - 4 * 1 = 1 násobek hodnoty středu – jednotkové jádro. Hrany produkují originál plus odezvu Laplaciánu. Součet vah je 1, takže mul a add zůstávají na svých výchozích hodnotách.

Pro silnější zostření jde 8-spojitá varianta dále:

sharpen_strong = [-1, -1, -1,
                  -1,  9, -1,
                  -1, -1, -1]

img.morph(1, sharpen_strong)

Středová váha 9 je identity (1) + Laplacian-8 centre (8). Stejná logika, větší zesílení, větší riziko, že se zesílí i šum senzoru.

Silná zostřovací jádra jsou v podstatě gaussian() s unsharp=True, jen vyjádřená přímo jako jádro místo přes příznak unsharp masky. Chování na úrovni pixelů je stejné; volba je mezi pohodlím pojmenované metody a jemným ovládáním ručně vyladěného jádra.

5.17.5. Emboss

Jádro emboss produkuje efekt nasvícení ze strany známý z klasických obrazových editorů. Výstup vypadá, jako by byl obraz vytlačen do reliéfu a poté nasvícen z jednoho rohu:

emboss = [-2, -1,  0,
          -1,  1,  1,
           0,  1,  2]

img.morph(1, emboss, add=128)

Trik spočívá v asymetrii napříč diagonálou. Levý horní roh má nejzápornější váhu, pravý dolní má nejkladnější váhu a diagonála od rohu k rohu vede od záporné přes jedničku ke kladné. U každého pixelu jádro v podstatě počítá „jas v mém pravém dolním rohu mínus jas v mém levém horním rohu“, což je kladné tam, kde se obraz v tomto směru rozjasňuje, a záporné tam, kde tmavne. Přičtení 128 posune znaménkový výstup zpět na střední šeď, aby byl efekt viditelný.

Otočení asymetrie přes druhou diagonálu embossuje z opačného směru:

emboss_alt = [ 0,  1,  2,
              -1,  1,  1,
              -2, -1,  0]

img.morph(1, emboss_alt, add=128)

Oba směry embossu jsou užitečné v kombinaci – odečtením jednoho od druhého nebo spuštěním každého na stejném obrazu a porovnáním odezev – když aplikace potřebuje detekovat orientaci.

5.17.6. Vyhlazování

Vyhlazovací jádra jsou rodina, jejíž váhy se sčítají na jedničku (a jsou všechny nezáporné). Rovná plocha procházející takovým jádrem produkuje stejný rovný jas, protože jádro hodnoty pixelů zprůměruje, místo aby zesilovalo jejich rozdíly.

Nejjednodušší je box blur, což je přesně to, co počítá mean():

box_blur = [1, 1, 1,
            1, 1, 1,
            1, 1, 1]

img.morph(1, box_blur)

Jádro se sčítá na 9, takže automatické dělení součtem jádra promění součet součinů ve skutečný průměr přes devět pixelů okolí. V praxi je mean() lepším způsobem, jak toto jádro spustit – produkuje stejný výstup rychleji, cestou optimalizovanou pro výpočet průměru a nic jiného, zatímco morph spouští obecnou konvoluční mašinerii. Box blur je v katalogu proto, že je správným základem pro pochopení každého dalšího vyhlazovacího jádra.

Aproximace Gaussiánu typu 3 krát 3 váží střed a hlavní sousedy více než rohy:

gaussian = [1, 2, 1,
            2, 4, 2,
            1, 2, 1]

img.morph(1, gaussian)

Váhy jsou řádek Pascalova trojúhelníku 1, 2, 1 vnějškově vynásobený sám se sebou. Středová váha 4 je největší, protože středový pixel přispívá nejvíce ke svému vlastnímu výstupu; rohy jsou 1, protože jsou od středu nejvzdálenější. Jádro se sčítá na 16 a automatické dělení součtem jádra zajistí normalizaci – žádný argument mul není potřeba. Forma 3 krát 3 je hrubou aproximací skutečného Gaussiánu a nerozlišitelná od gaussian() při size=1; forma morph je užitečná hlavně tehdy, když chce aplikace složit vyhlazení s jinou operací v rámci jednoho průchodu.

5.17.7. Pohybové rozmazání

Jádro pohybového rozmazání (motion blur) průměruje pixely podél jednoho směru a kolmý směr ponechává nerozmazaný. Nejjednodušším případem je vodorovné:

motion_h = [0, 0, 0,
            1, 1, 1,
            0, 0, 0]

img.morph(1, motion_h)

Prostřední řádek průměruje tři pixely podél vodorovné osy; horní a dolní řádek jsou nulové. Jádro se sčítá na 3, takže automatické dělení součtem jádra produkuje skutečný třípixelový průměr bez jakéhokoli potřebného mul. Výstupem je vodorovně rozmazaná kopie vstupu – efekt, který kamera zachytí, když se objekt během expozice pohybuje do strany. Svislé pohybové rozmazání je tentýž vzor otočený:

motion_v = [0, 1, 0,
            0, 1, 0,
            0, 1, 0]

Diagonální pohybové rozmazání používá hlavní diagonálu:

motion_diag = [1, 0, 0,
               0, 1, 0,
               0, 0, 1]

img.morph(1, motion_diag)

Jádra pohybového rozmazání jsou užitečná jak jako efekt (záměrné rozmazání snímku z vizuálních důvodů), tak jako testovací vzor pro algoritmy, které musí být odolné vůči pohybovým artefaktům (spusťte algoritmus na pohybově rozmazaném vstupu a ověřte, že stále produkuje správnou odpověď).

5.17.8. Čtení jader na první pohled

Několik praktických pravidel usnadňuje čtení nových jader na první pohled:

  • Součet na jedničku s nezápornými váhami ⇒ vyhlazování (zachovává průměrný jas).

  • Součet na nulu s kladnými i zápornými váhami ⇒ odezva na hrany (nula na rovných plochách).

  • Součet na jedničku s velkým kladným středem a malým záporným okolím ⇒ zostřování (jednotkové jádro plus odezva na hrany).

  • Asymetrie napříč diagonálou se součtem na jedničku ⇒ embossování (zvýrazňuje jednu stranu každého přechodu jasu).

  • Soustředění podél jedné osy se součtem na jedničku ⇒ směrové rozmazání.

První z těchto pravidel, kterému jádro odpovídá, je obvykle správným odhadem toho, co jádro dělá. Většinu užitečných jader lze rozpoznat již ze samotného rozložení jejich vzoru vah.

Když žádné ze standardních jader nedělá to, co aplikace chce, dalším krokem je vyladit si jedno ručně. Kombinace výše uvedených pravidel a ovládacích prvků mul / add pokrývá téměř každý lineární průchod, jaký kdy klasický pipeline strojového vidění chtěl; odtud už jde jen o zkoušení vah, sledování výstupu a iterování.