5.16. Egyéni konvolúciós kernelek

Az eddig tárgyalt környezeti szűrők mindegyikéhez tartozott egy beépített statisztika, amelyet a szűrő minden pozícióban az ablakra alkalmazott – az átlag, a Gauss-súlyozott átlag, a medián. A morph() az egyetlen olyan szűrő, amely lehetővé teszi, hogy az alkalmazás maga adja meg a statisztikát egy kernel formájában: egy kis súlymátrix formájában, amely leírja, hogyan kell a szűrőnek a környezeti képpontokat egyetlen kimeneti értékké egyesítenie.

A mechanizmus a klasszikus konvolúció művelete. Minden kimeneti pozícióban minden környezeti képpontot megszorzunk a kernel megfelelő súlyával, a szorzatokat összeadjuk, az eredményt opcionálisan skálázzuk és eltoljuk, majd az értéket a kimeneti képpontba írjuk. A különböző kernelek ugyanabból a bemenetből különböző eredményeket állítanak elő. Egy csupa egyenlő pozitív súlyú kernel a mean() szűrőt reprodukálja; egy haranggörbe alakú a gaussian() szűrőt. Az ezeken túli mintázatok élválaszokat, domborításokat, gradienseket, élesítést, mozgási elmosódást és számos más effektust állítanak elő – mindent, amit a klasszikus képfeldolgozás valaha is el akart érni egyetlen lineáris menettel.

5.16.1. A morph metódus

A szignatúra a többi környezeti szűrőhöz hasonló, egyetlen plusz argumentummal:

img.morph(size, kernel, mul=1.0, add=0.0)

A size ugyanúgy a sugár, mint mindenhol máshol, így a kernelnek pontosan (2 * size + 1) sorból és (2 * size + 1) oszlopból kell állnia. Maga a kernel ennyi szám lapos Python listája, sorfolytonos (row-major) sorrendben – az első (2 * size + 1) bejegyzés a felső sor, a következő (2 * size + 1) a második sor, és így tovább, le egészen az alsó sorig. A mul skálázza a szorzatok összegét, mielőtt az a kimeneti képpontba kerül, az add pedig egy konstanst ad hozzá. Az alapértelmezett mul=1.0 és add=0.0 változatlanul hagyja a konvolúció kimenetét.

Egy részlet érdemes a kiemelésre: a metódus automatikusan elosztja a szorzatok összegét a kernel bejegyzéseinek összegével, mielőtt a kimenetet kiírná. Ez az automatikus osztás azt jelenti, hogy egy átlagoló kernel, amelynek bejegyzéseinek összege kilenc – például egy 3-szor-3-as box blur – külön erőfeszítés nélkül egykilenced skálán jön ki, egy tizenhatra összegződő Gauss-közelítő kernel pedig egytizenhatod skálán, mindkettő anélkül, hogy az alkalmazásnak magának kellene kiszámítania az osztást. Az alkalmazás csak akkor állítja be a mul értéket, ha további skálázást szeretne az automatikus normalizáláson felül – vagy, ami gyakoribb, amikor a kernel nullára összegződik (egy élválasz-kernel), és az automatikus osztás semmivel való osztás lenne. A keretrendszer ilyenkor az összeget egynek tekinti, és a mul lesz az egyetlen gomb, amellyel a skálázatlan szorzatösszeg a tartományon belül tartható.

Az adaptív küszöbölésről szóló szakaszból ismert threshold=True / offset=N páros a morph() metódussal is működik, így ugyanaz az egyéni kernel keretrendszer képes egy bináris küszöböt előállítani, amelynek vágási pontját egy egyéni statisztika számítja ki.

5.16.2. A kernel elrendezése

Egy 3-szor-3-as kernel (size=1) kilenc szám lapos listája, balról jobbra, fentről lefelé elrendezve. A konvenció természetesen olvasható, ha a listát három Python sorra törjük:

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

Ez a Sobel-x gradiens operátor – az első szabványos kernel, amelyet minden alkalmazás használni szeretne, és hasznos végigvezetni rajta. A mintázat egyértelmű: negatív súlyok a bal oszlopban, pozitív súlyok a jobb oszlopban, a középső oszlop nulla. A sorsúlyok -1, -2, -1 (vagy 1, 2, 1 a jobb oldalon) középen magasabbak, mint a sarkokban, ami a középső sornak nagyobb befolyást ad az eredményre, mint a sarokra eső soroknak.

Amikor a kernel egy függőleges él fölött halad át – egy képpontoszlop, amely balról sötétből jobbra világosba megy át – a negatív súlyok a sötét oldalt, a pozitív súlyok pedig a világos oldalt szedik fel. A szorzatok összege nagy pozitív szám, amelyet a szűrő világos kimeneti képpontként ír ki. Egy egyenletes fényerejű vízszintes folt nullát eredményez, mert minden pozitív súlynak egy ugyanakkora nagyságú negatív súly felel meg egy ugyanolyan értékű képponton.

A kernel futtatása:

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

A Sobel-kernel nullára összegződik – minden bal oldali negatív súlynak egy egyenlő pozitív súly felel meg a jobb oldalon – így az automatikus osztás semmivel nem oszt, és a mul az egyetlen skála a szorzatösszegen. A mul=0.25 a tartományban tartja a választ: a legnagyobb abszolút összeg, amelyet a Sobel-x egy 3-szor-3-as foltból előállíthat, nagyjából 4 * 255 = 1020 (nyolc világos képpont, akár 2-vel súlyozva), és ezt néggyel leosztva a szélső esetek 255-nél kötnek ki, ahol a formátum tisztán levágja őket.

A megfelelő Sobel-y kernel a vízszintes éleket észleli azáltal, hogy ugyanazt a súlymintázatot 90 fokkal elforgatja:

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

Azok az alkalmazások, amelyek bármilyen élt szeretnének észlelni, az iránytól függetlenül, jellemzően mindkét Sobel-t lefuttatják, és kombinálják a válaszokat.

5.16.3. A kimenet eltolása

Az add a skálázás történetének másik fele. Egy nulla összegű kernel válasza előjeles – egy él egyik oldalán pozitív, a másikon negatív – és a negatív fél nullára vágódik, amikor egy előjel nélküli képpontba kerül. Az add=128 úgy tolja el a választ, hogy az a középszürkére legyen központosítva, így a negatív válaszok 128 alatti értékekként élnek tovább, a pozitívak pedig fölé kerülnek: egy élválasz vagy egy domborítás mindkét irányban láthatóvá válik, a tartomány felének elvesztése árán mindkét irányban.

Hogy egy kernel a mul és az add melyik kombinációját várja el, az a kernel tervezésének része; a szabványos kernelkatalógus minden gyakori kernelhez felsorolja a megfelelő beállításokat.

5.16.4. Nagyobb kernelek

Ezen az oldalon minden 3-szor-3-as kernelekkel (size=1) lett leírva, mert ezt a méretet használja a szabványos katalógus, és mert a sorfolytonos elrendezést ekkora méretben könnyű kézzel kiírni. A mechanizmusban azonban semmi sem korlátozza a kernelt 3-szor-3-asra. A size=2 egy 5-ször-5-ös kernelt futtat, huszonöt bejegyzéssel a lapos listában; a size=3 egy 7-szer-7-est futtat negyvenkilenccel; és így tovább, egészen addig a sugárig, amennyit az alkalmazás hajlandó megfizetni. A keretrendszer bármely páratlan méretnél kezeli mind a lapos listás, mind a beágyazott soros elrendezést.

A nagyobb kernel választásának oka ugyanaz, mint a nagyobb környezet választásáé bármelyik beépített szűrőnél: több átlagolás, szélesebb körű jellemzőészlelés, kisebb érzékenység az egyetlen képpontnyi zajra. A költség a sugár négyzetével arányosan nő – egy 5-ször-5-ös nagyjából 2,8-szor annyi képpontonkénti munkát végez, mint egy 3-szor-3-as, egy 7-szer-7-es körülbelül 5,4-szer annyit – és ez a szorzó egyenesen a képkockasebességből megy le.

A gyakorlati minta az, hogy size=1 méretnél maradunk a szabványos katalógushoz, és csak akkor nyúlunk nagyobb méretekhez, amikor az algoritmusnak szüksége van a nagyobb környezetre. Az éldetektorok ritkán profitálnak a 3-szor-3-ason túl; a simító szűrők néha igen; a megfelelő méret azon jellemzők léptékétől függ, amelyeket az alkalmazás ki akar emelni vagy el akar nyomni.

5.16.5. Mikor érdemes a morph-hoz nyúlni

A mindennapi simításhoz a mean(), a gaussian() és a bilateral() gyorsabb és tisztább. Az éldetektáláshoz a laplacian() és a find_edges() célzottan készültek. A morph() közvetlen használata akkor indokolt, amikor az alkalmazásnak egy konkrét konvolúcióra van szüksége, amelyet a beépített szűrők nem tesznek elérhetővé – egy irányfüggő Sobel, egy egyéni élsablon, egy adott textúrára hangolt kernel, amelyet a folyamat többi része keresni fog, vagy a hasznos kernelek bármelyike a szabványos katalógusból, amelyet a klasszikus képfeldolgozás az évtizedek során felépített. Tetszőleges kernelek teljes rugalmassága elérhető; az ára az, hogy az alkalmazás felelős a kívánt eredményt előállító kernelértékek kiválasztásáért.