5.17. A standard kernelek katalógusa

A klasszikus képfeldolgozás az évek során egy jókora katalógust gyűjtött össze a kernel-súlymintázatokból, amelyek újra meg újra előbukkannak – éldetektorok, élesítők, dombornyomások, simítók, mozgási elmosások –, és mindegyik a morph() metóduson keresztül fut. Mindegyik rövid, mindegyik egyetlen dolgot csinál, és a legtöbbjük könnyen olvasható, amint a súlyok alapvető logikája világossá válik.

Az alábbi kernelek mind 3-szor-3-asak, hacsak nincs másként jelölve, így mindegyik size=1 értéket használ a hívásban. Minden kernel súlyszerkezetét mellette ismertetjük, mert a súlyok olvasása alakítja ki azt az intuíciót, amely megmagyarázza, miért domborít az egyik kernel és miért élesít a másik.

5.17.1. Az identitás-kernel

A lehető legegyszerűbb kernel az identitás – a középen egy, mindenhol máshol nulla:

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

img.morph(1, identity)

Minden kimeneti képpont az értékét a környezet közepéből veszi, ami az ugyanazon a pozíción lévő bemeneti képpont. A kép változatlanul halad át. Az identitásnak szűrőként nincs gyakorlati haszna, de hasznos kiindulási alap minden más kernel megértéséhez: bármely nem-identitás kernel az identitás plusz valamilyen módosítás.

Az a kernel, amelynek a középső súlya nagy, körülötte pedig kis negatív súlyok vannak, kivonja a környezetet a középpontból. Az a kernel, amelynek a középső súlya nulla, figyelmen kívül hagyja magát a képpontot, és csak a szomszédai közti különbségekre reagál. Ha egy kernelt így olvasunk – mit tesz a középső súly a képponttal, mit ad hozzá vagy von el a környező súlyok –, az a leggyorsabb módja annak, hogy megjósoljuk a hatását.

5.17.2. Éldetektálás

Az éldetektáló kernelek erősen reagálnak azokon a pozíciókon, ahol a fényerő egy adott irányban gyorsan változik, és közel nulla kimenetet adnak ott, ahol a fényerő egyenletes. Ők az a család, amelynek súlyai nullára összegződnek: egy egyenletes folt (ahol minden képpont azonos értékű) nulla kimenetet ad, mert minden pozitív súlyt pontosan kiolt egy azonos nagyságú negatív súly.

A Sobel-x a kanonikus példa. Függőleges éleket detektál (bal/jobb fényerőátmeneteket):

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

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

A párját jelentő Sobel-y ugyanaz a mintázat 90 fokkal elforgatva; vízszintes éleket detektál (fel/le fényerőátmeneteket):

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

A Sobel-x középső sorában -2 és 2 súlyok vannak, nem pedig -1 és 1. A középső soron lévő extra súly egy kis beépített simítást ad a kernelnek az él mentén haladó irányban, ami zajjal szemben robusztusabbá teszi, mint az egyszerűbb Prewitt operátort, amely elhagyja ezeket az extra nagyságokat:

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

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

A Prewitt minden sort egyformán súlyoz, így a válasza egy árnyalattal élesebb a Sobelénél, azon az áron, hogy érzékenyebb az egyetlen képpontos zajra (a kernel futtatásának költsége azonos – a konvolúció ugyanazt a munkát végzi, bármilyenek is a súlyok). Tiszta, erős élekkel rendelkező képen tökéletesen használható helyettesítője a Sobelnek.

A Scharr a másik irányba megy. Súlyai nagyobbak, és arra vannak hangolva, hogy finomabb szögeknél is pontosan detektálják az él irányát:

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

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

A mul=0.0625 osztó (1/16) a kimenetet a nagyobb szorzatösszeg után visszahozza a 0255 tartományba. A Scharr a helyes válasz, ha az alkalmazásnak a leginkább geometriailag hű gradiensválaszra van szüksége, és hajlandó ezért egy kicsivel több aritmetikát fizetni.

5.17.3. A Laplace-operátor

A Laplace-kernel egyszerre reagál a bármely irányú élekre. Míg a Sobelek mindegyike egyetlen tengely menti fényerőváltozásokat detektál, a Laplace szimmetrikus súlymintázata ugyanúgy reagál, függetlenül attól, hogy az él milyen irányba halad:

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

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

A szerkezet: középső súly 4, négy vízszintes/függőleges szomszéd -1 súllyal, a négy átló nulla súllyal. A kernel nullára összegződik, így az egyenletes foltok nulla kimenetet adnak. Ahol a fényerő változik, ott a középső érték eltér négy fő szomszédjának átlagától, és a kimenet ennek a különbségnek a nagysága.

A 8-szomszédos változat magában foglalja az átlós szomszédokat is:

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

Mindegyik kernel kissé más dolgokat detektál. A 4-szomszédos változat tisztább kimenetet ad a vízszintes és függőleges éleken; a 8-szomszédos izotrópabb – minden irányban egyformán jól reagál –, de kissé zajosabb kimenetet ad. A 8-szomszédos kernel outline (körvonal) néven is terjed, az élek vizualizálására való felhasználása nyomán.

5.17.4. Élesítés

Az élesítő kernel az identitás plusz egy élválasz-kernel. A kimenet az eredeti kép plusz az élek egy másolata, így a nagyfrekvenciás jellemzők felerősödnek a sima belső területekhez képest.

A standard 4-szomszédos élesítő kernel a 4-szomszédos Laplace-operátort adja az identitáshoz:

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

img.morph(1, sharpen)

A kernel olvasata: a középső súly identity (1) + Laplacian centre (4) = 5, a környezet pedig a Laplace-éhoz illeszkedik. Az egyenletes foltok 5 * 1 - 4 * 1 = 1-szeres középső értéket adnak – az identitást. Az élek az eredetit plusz a Laplace-választ adják. A súlyok összege 1, így a mul és az add az alapértelmezett értékükön marad.

Erősebb élesítéshez a 8-szomszédos változat tovább megy:

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

img.morph(1, sharpen_strong)

A 9 középső súly az identity (1) + Laplacian-8 centre (8). Ugyanaz a logika, több felerősítés, nagyobb kockázata annak, hogy az érzékelő zaját is felerősíti.

Az erős élesítő kernelek lényegében a gaussian() unsharp=True beállítással, csak közvetlenül kernelként kifejezve, nem pedig az unsharp-mask jelzőn keresztül. A képpontszintű viselkedés azonos; a választás a megnevezett metódus kényelme és a kézzel hangolt kernel finom vezérlése között van.

5.17.5. Dombornyomás (Emboss)

Az emboss (dombornyomás) kernel a klasszikus képszerkesztőkben megtalálható oldalról megvilágított hatást hozza létre. A kimenet úgy néz ki, mintha a képet domborművé alakították, majd egy sarokból megvilágították volna:

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

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

A trükk az átló menti aszimmetria. A bal felső saroknak van a legnegatívabb súlya, a jobb alsónak a legpozitívabb súlya, és a saroktól sarokig tartó átló a negatívtól az egyen át a pozitívig fut. Minden képpontnál a kernel lényegében azt számolja ki, hogy „a jobb alsó oldalamon lévő fényerő mínusz a bal felső oldalamon lévő fényerő”, ami pozitív ott, ahol a kép abba az irányba világosabb lesz, és negatív ott, ahol sötétebb. A 128 hozzáadása az előjeles kimenetet közepes szürkére centrálja vissza, hogy a hatás látható legyen.

Az aszimmetria másik átló menti elforgatása az ellenkező irányból domborít:

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

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

A két emboss-irány kombinációban hasznos – az egyiket a másikból kivonva, vagy mindkettőt ugyanazon a képen futtatva és a válaszokat összehasonlítva –, amikor egy alkalmazásnak az orientációt kell detektálnia.

5.17.6. Simítás

A simító kernelek az a család, amelynek súlyai egyre összegződnek (és mind nemnegatívak). Egy egyenletes folt egy ilyen kernelen keresztül ugyanazt az egyenletes fényerőt adja, mert a kernel a képpontértékeket átlagolja, ahelyett, hogy felerősítené a különbségeiket.

A legegyszerűbb a box blur (dobozos elmosás), ami pontosan az, amit a mean() számol ki:

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

img.morph(1, box_blur)

A kernel 9-re összegződik, így a kernelösszeggel való automatikus osztás a szorzatösszeget valódi átlaggá alakítja a kilenc szomszédos képpont felett. A gyakorlatban a mean() a jobb módja ennek a kernelnek a futtatására – ugyanazt a kimenetet gyorsabban állítja elő, egy kizárólag az átlag kiszámítására optimalizált útvonalon keresztül, míg a morph az általános konvolúciós gépezetet futtatja. A box blur azért van a katalógusban, mert ez a helyes kiindulási alap minden más simító kernel megértéséhez.

A Gaussian 3-szor-3-as közelítése a középpontot és a fő szomszédokat jobban súlyozza, mint a sarkokat:

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

img.morph(1, gaussian)

A súlyok a Pascal-háromszög 1, 2, 1 sora önmagával képzett külső szorzata. A 4 középső súly a legnagyobb, mert a középső képpont járul hozzá leginkább a saját kimenetéhez; a sarkok 1 értékűek, mert a legtávolabb vannak a középponttól. A kernel 16-ra összegződik, és a kernelösszeggel való automatikus osztás elvégzi a normalizálást – nincs szükség mul argumentumra. A 3-szor-3-as forma egy valódi Gaussian durva közelítése, és size=1 esetén megkülönböztethetetlen a gaussian() metódustól; a morph-forma többnyire akkor hasznos, ha egy alkalmazás a simítást egy másik művelettel akarja összevonni ugyanabban a menetben.

5.17.7. Mozgási elmosás

A motion-blur (mozgási elmosás) kernel a képpontokat egy irány mentén átlagolja, a merőleges irányt elmosatlanul hagyva. A legegyszerűbb eset a vízszintes:

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

img.morph(1, motion_h)

A középső sor három képpontot átlagol a vízszintes tengely mentén; a felső és alsó sor nulla. A kernel 3-ra összegződik, így a kernelösszeggel való automatikus osztás valódi háromképpontos átlagot ad anélkül, hogy bármilyen mul szükséges lenne. A kimenet a bemenet vízszintesen elkent másolata – az a hatás, amelyet egy kamera rögzít, amikor a téma oldalirányban mozog az expozíció során. A függőleges mozgási elmosás ugyanaz a mintázat elforgatva:

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

Az átlós mozgási elmosás a fő átlót használja:

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

img.morph(1, motion_diag)

A mozgási elmosás kernelek hasznosak mind effektusként (egy képkocka szándékos elmosása vizuális célokból), mind tesztmintázatként olyan algoritmusokhoz, amelyeknek robusztusnak kell lenniük a mozgási műtermékekkel szemben (futtasd az algoritmust egy mozgásilag elmosott bemeneten, és ellenőrizd, hogy még mindig a helyes választ adja-e).

5.17.8. Kernelek ránézésre olvasása

Néhány ökölszabály megkönnyíti az új kernelek ránézésre olvasását:

  • Egyre összegződő nemnegatív súlyokkal ⇒ simítás (megőrzi az átlagos fényerőt).

  • Nullára összegződő pozitív és negatív súlyokkal egyaránt ⇒ élválasz (egyenletes foltokon nulla).

  • Egyre összegződő nagy pozitív középponttal és kis negatív környezettel ⇒ élesítés (identitás plusz élválasz).

  • Átló mentén aszimmetrikus egyre összegződéssel ⇒ dombornyomás (minden fényerőátmenet egyik oldalát kiemeli).

  • Egy tengely mentén koncentrált egyre összegződéssel ⇒ irányított elmosás.

E szabályok közül az első, amelyikre a kernel illeszkedik, általában a helyes tipp arra nézve, hogy mit csinál. A legtöbb hasznos kernel pusztán a súlymintázat elrendezéséből felismerhető.

Amikor a standard kernelek egyike sem azt teszi, amit az alkalmazás szeretne, a következő lépés egy kézi hangolása. A fenti szabályok és a mul / add vezérlők kombinációja szinte minden lineáris menetet lefed, amelyet egy klasszikus gépi látás csővezeték valaha is akart; onnantól már csak a súlyok kipróbálásáról, a kimenet megfigyeléséről és az iterálásról van szó.