5.16. Vlastní konvoluční jádra¶
Filtry pracující s okolím, které jsme dosud probrali, měly každý vestavěnou statistiku, kterou filtr aplikoval na okno v každé pozici – průměr, Gaussovsky vážený průměr, medián. morph() je jediný filtr, který aplikaci umožňuje dodat statistiku samotnou, a to ve formě jádra: malé matice vah, která popisuje, jak má filtr zkombinovat pixely okolí do jediné výstupní hodnoty.
Mechanismem je klasická operace konvoluce. V každé výstupní pozici se každý pixel okolí vynásobí odpovídající vahou v jádře, součiny se sečtou, výsledek se volitelně škáluje a posune a hodnota se zapíše do výstupního pixelu. Různá jádra produkují ze stejného vstupu různé výsledky. Jádro se samými stejnými kladnými vahami reprodukuje filtr mean(); zvonovité jádro reprodukuje gaussian(). Vzory mimo tyto produkují odezvy na hrany, reliéfy, gradienty, doostření, pohybové rozmazání a dlouhý katalog dalších efektů – vše, co kdy klasické zpracování obrazu chtělo udělat jediným lineárním průchodem.
5.16.1. Metoda morph¶
Signatura vypadá jako u ostatních filtrů pracujících s okolím, jen s jedním argumentem navíc:
img.morph(size, kernel, mul=1.0, add=0.0)
size je poloměr stejně jako všude jinde, takže jádro musí mít přesně (2 * size + 1) řádků krát (2 * size + 1) sloupců. Samotné jádro je plochý Python seznam tohoto počtu čísel v pořadí po řádcích – prvních (2 * size + 1) položek je horní řádek, dalších (2 * size + 1) je druhý řádek a tak dále, až po spodní řádek. mul škáluje součet součinů předtím, než se zapíše do výstupního pixelu, a add přičte konstantu. Výchozí hodnoty mul=1.0 a add=0.0 ponechávají výstup konvoluce beze změny.
Jeden detail, který stojí za to explicitně zmínit: metoda automaticky dělí součet součinů součtem položek jádra předtím, než zapíše výstup. Toto automatické dělení znamená, že průměrovací jádro, jehož položky se sečtou na devět – například box blur 3 krát 3 – vyjde v měřítku jedné devítiny bez jakékoli další práce, a jádro aproximující Gauss, které se sečte na šestnáct, vyjde v měřítku jedné šestnáctiny, obojí bez toho, aby aplikace musela dělení počítat sama. Aplikace nastavuje mul pouze tehdy, když chce další škálování nad rámec automatické normalizace – nebo, častěji, když se jádro sečte na nulu (jádro pro odezvu na hrany) a automatické dělení by bylo dělením ničím. Framework v takovém případě považuje součet za jedničku a mul se stává jediným ovladačem pro udržení neškálovaného součtu součinů v rozsahu.
Dvojice threshold=True / offset=N z části o adaptivním prahování funguje rovněž na morph(), takže stejný framework vlastních jader může produkovat binární práh, jehož mez se počítá vlastní statistikou.
5.16.2. Rozložení jádra¶
Jádro 3 krát 3 (size=1) je plochý seznam devíti čísel rozložených zleva doprava, shora dolů. Konvence se čte přirozeně, pokud je seznam rozdělen na tři Python řádky:
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
Toto je gradientní operátor Sobel-x – první standardní jádro, které bude každá aplikace chtít, a užitečné jádro k podrobnému projití od začátku do konce. Vzor je přímočarý: záporné váhy v levém sloupci, kladné váhy v pravém sloupci, se středovým sloupcem nulovým. Váhy řádků -1, -2, -1 (nebo 1, 2, 1 vpravo) jsou uprostřed vyšší než v rozích, což dává prostřednímu řádku větší vliv na výsledek než řádkům rohovým.
Když jádro přejíždí přes svislou hranu – sloupec pixelů, který přechází z tmavého vlevo na světlý vpravo – záporné váhy zachytí tmavou stranu a kladné váhy zachytí světlou stranu. Součet součinů je velké kladné číslo, které filtr zapíše jako světlý výstupní pixel. Vodorovná oblast rovnoměrného jasu produkuje nulu, protože každé kladné váze odpovídá záporná váha stejné velikosti na pixelu se stejnou hodnotou.
Spuštění jádra:
img.morph(1, sobel_x, mul=0.25)
Sobelovo jádro se sečte na nulu – každé záporné váze na levé straně odpovídá stejná kladná váha vpravo – takže automatické dělení nedělí ničím a mul je jediným měřítkem součtu součinů. mul=0.25 udržuje odezvu v rozsahu: největší absolutní součet, který může Sobel-x z oblasti 3 krát 3 vyprodukovat, je zhruba 4 * 255 = 1020 (osm světlých pixelů vážených až 2), a vydělení tohoto čtyřmi umístí krajní případy na 255, kde je formát čistě ořízne.
Odpovídající jádro Sobel-y detekuje vodorovné hrany otočením stejného vzoru vah o 90 stupňů:
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
Aplikace, které chtějí detekovat libovolnou hranu bez ohledu na směr, obvykle spustí oba Sobely a odezvy zkombinují.
5.16.3. Posunutí výstupu¶
add je druhou polovinou příběhu o škálování. Odezva jádra s nulovým součtem je znaménková – kladná na jedné straně hrany, záporná na druhé – a záporná polovina se při zápisu do bezznaménkového pixelu ořízne na nulu. add=128 posune odezvu tak, aby byla vystředěna na střední šeď, takže záporné odezvy přežijí jako hodnoty pod 128 a kladné se umístí nad ní: odezva na hranu nebo reliéf se stane viditelným v obou směrech, za cenu poloviny rozsahu v každém z nich.
To, jakou kombinaci mul a add jádro očekává, je součástí návrhu jádra; katalog standardních jader uvádí správná nastavení pro každé běžné jádro.
5.16.4. Větší jádra¶
Vše na této stránce bylo popsáno s jádry 3 krát 3 (size=1), protože to je velikost, kterou používá standardní katalog, a protože rozložení po řádcích se v této velikosti snadno vypíše ručně. Nic v mechanismu však jádro neomezuje na 3 krát 3. size=2 spustí jádro 5 krát 5 s dvaceti pěti položkami v plochém seznamu; size=3 spustí 7 krát 7 se čtyřiceti devíti; a tak dále, až po jakýkoli poloměr, který je aplikace ochotna zaplatit. Framework zvládá při libovolné liché velikosti jak rozložení v plochém seznamu, tak vnořené po řádcích.
Důvod sáhnout po větším jádře je stejný jako důvod sáhnout po větším okolí u kteréhokoli z vestavěných filtrů: více průměrování, širší detekce příznaků, menší citlivost na šum jednotlivých pixelů. Náklady rostou s druhou mocninou poloměru – 5 krát 5 vykoná zhruba 2,8krát více práce na pixel než 3 krát 3, 7 krát 7 asi 5,4krát – a tento násobitel jde přímo na úkor snímkové frekvence.
Praktický postup je zůstat u size=1 pro standardní katalog a sahat po větších velikostech jen tehdy, když algoritmus potřebuje větší okolí. Detektory hran zřídka těží z něčeho většího než 3 krát 3; vyhlazovací filtry někdy ano; správná velikost závisí na měřítku příznaků, které se aplikace snaží zdůraznit nebo potlačit.
5.16.5. Kdy sáhnout po morph¶
Pro běžné vyhlazování jsou mean(), gaussian() a bilateral() rychlejší a čistší. Pro detekci hran jsou laplacian() a find_edges() určeny přímo k tomuto účelu. Sáhnout přímo po morph() má smysl tehdy, když aplikace potřebuje konkrétní konvoluci, kterou vestavěné filtry nezpřístupňují – směrový Sobel, vlastní šablonu hrany, jádro vyladěné na konkrétní texturu, kterou bude zbytek řetězce hledat, nebo kterékoli ze standardního katalogu užitečných jader, který klasické zpracování obrazu nashromáždilo během desetiletí. Plná flexibilita libovolných jader je k dispozici; cenou je, že aplikace je zodpovědná za volbu hodnot jádra, které produkují požadovaný výsledek.