5.16. Egendefinierade faltningskärnor¶
De grannskapsfilter som hittills har behandlats hade vart och ett en inbyggd statistik som filtret tillämpade på fönstret vid varje position – medelvärdet, det Gaussiskt viktade medelvärdet, medianen. morph() är det enda filtret som låter applikationen själv tillhandahålla statistiken, i form av en kärna: en liten matris av vikter som beskriver hur filtret ska kombinera grannskapets pixlar till ett enda utvärde.
Mekanismen är den klassiska faltningsoperationen. Vid varje utdataposition multipliceras varje granskapspixel med den matchande vikten i kärnan, produkterna summeras, resultatet skalas och förskjuts eventuellt, och värdet skrivs till utdatapixeln. Olika kärnor producerar olika resultat från samma indata. En kärna med enbart lika stora positiva vikter återskapar mean()-filtret; en klockformad återskapar gaussian(). Mönster utöver dessa producerar kantsvar, reliefer, gradienter, skärpning, rörelseoskärpa och en lång katalog av andra effekter – allt som klassisk bildbehandling någonsin har velat göra med ett enda linjärt svep.
5.16.1. morph-metoden¶
Signaturen ser ut som de andra grannskapsfiltren med ett extra argument:
img.morph(size, kernel, mul=1.0, add=0.0)
size är radien på samma sätt som överallt annars, så kärnan måste vara exakt (2 * size + 1) rader gånger (2 * size + 1) kolumner. Själva kärnan är en platt Python-lista med så många tal, i radvis ordning – de första (2 * size + 1) posterna är den översta raden, de nästa (2 * size + 1) är den andra raden, och så vidare ner till den nedersta raden. mul skalar produktsumman innan den skrivs in i utdatapixeln, och add adderar en konstant. Standardvärdena mul=1.0 och add=0.0 lämnar faltningens utdata oförändrad.
En detalj värd att vara tydlig med: metoden dividerar automatiskt produktsumman med summan av kärnans poster innan utdata skrivs. Den automatiska divisionen innebär att en medelvärdesbildande kärna vars poster summerar till nio – en 3-gånger-3 lådoskärpa, till exempel – kommer ut i en niondels skala utan något extra arbete, och en Gauss-approximerande kärna som summerar till sexton kommer ut i en sextondels skala, båda utan att applikationen behöver beräkna divisionen själv. Applikationen sätter mul endast när den vill ha en ytterligare skalning ovanpå den automatiska normaliseringen – eller, vanligare, när kärnan summerar till noll (en kantsvarskärna) och den automatiska divisionen skulle vara en division med ingenting. Ramverket behandlar summan som ett i det fallet, och mul blir den enda reglaget för att hålla den oskalade produktsumman inom intervallet.
Paret threshold=True / offset=N från avsnittet om adaptiv tröskling fungerar också på morph(), så samma ramverk för egendefinierade kärnor kan producera en binär tröskling vars gräns beräknas av en egendefinierad statistik.
5.16.2. Kärnans layout¶
En 3-gånger-3-kärna (size=1) är en platt lista med nio tal upplagda från vänster till höger, uppifrån och ner. Konventionen läses naturligt om listan bryts upp över tre Python-rader:
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
Detta är gradientoperatorn Sobel-x – den första standardkärnan som någon applikation kommer att vilja ha och en användbar att gå igenom från början till slut. Mönstret är okomplicerat: negativa vikter i vänsterkolumnen, positiva vikter i högerkolumnen, med mittkolumnen noll. Radvikterna -1, -2, -1 (eller 1, 2, 1 till höger) är högre i mitten än i hörnen, vilket ger mittraden mer inflytande över resultatet än hörnraderna.
När kärnan sveper över en vertikal kant – en kolumn av pixlar som går från mörk till vänster till ljus till höger – fångar de negativa vikterna upp den mörka sidan och de positiva vikterna fångar upp den ljusa sidan. Produktsumman är ett stort positivt tal, som filtret skriver som en ljus utdatapixel. En horisontell yta med enhetlig ljusstyrka producerar noll, eftersom varje positiv vikt matchas av en negativ vikt med samma storlek på en pixel med samma värde.
Köra kärnan:
img.morph(1, sobel_x, mul=0.25)
Sobel-kärnan summerar till noll – varje negativ vikt på vänster sida matchas av en lika stor positiv vikt på höger sida – så den automatiska divisionen dividerar inte med någonting, och mul är den enda skalningen av produktsumman. mul=0.25 håller svaret inom intervallet: den största absoluta summan som Sobel-x kan producera från en 3-gånger-3-yta är ungefär 4 * 255 = 1020 (åtta ljusa pixlar viktade upp till 2), och att dividera ner det med fyra placerar de extrema fallen vid 255, där formatet beskär dem rent.
Den matchande Sobel-y-kärnan detekterar horisontella kanter genom att rotera samma viktmönster 90 grader:
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
Applikationer som vill detektera vilken kant som helst, oavsett riktning, kör vanligtvis båda Sobel-kärnorna och kombinerar svaren.
5.16.3. Förskjuta utdata¶
add är den andra halvan av skalningshistorien. En nollsummekärnas svar är teckenförsett – positivt på ena sidan av en kant, negativt på den andra – och den negativa halvan beskärs till noll när den skrivs in i en pixel utan tecken. add=128 förskjuter svaret så att det centreras vid mellangrått, så att negativa svar överlever som värden under 128 och positiva hamnar över det: ett kantsvar eller en relief blir synlig i båda riktningarna, till priset av halva intervallet i vardera.
Vilken kombination av mul och add en kärna förväntar sig är en del av kärnans design; katalogen över standardkärnor listar rätt inställningar för varje vanlig kärna.
5.16.4. Större kärnor¶
Allt på denna sida har beskrivits med 3-gånger-3-kärnor (size=1), eftersom det är den storlek standardkatalogen använder och eftersom den radvisa layouten är lätt att skriva ut för hand i den storleken. Inget i mekanismen begränsar dock kärnan till 3-gånger-3. size=2 kör en 5-gånger-5-kärna, med tjugofem poster i den platta listan; size=3 kör en 7-gånger-7 med fyrtionio; och så vidare, upp till vilken radie applikationen är villig att betala för. Ramverket hanterar antingen platt-list- eller nästlade-rad-layouter i vilken udda storlek som helst.
Skälet att ta till en större kärna är detsamma som skälet att ta till ett större grannskap för något av de inbyggda filtren: mer medelvärdesbildning, bredare särdragsdetektering, mindre känslighet för brus i enstaka pixlar. Kostnaden växer som kvadraten på radien – en 5-gånger-5 gör ungefär 2,8 gånger så mycket arbete per pixel som en 3-gånger-3, en 7-gånger-7 omkring 5,4 gånger – och den faktorn kommer rakt ur bildfrekvensen.
Det praktiska mönstret är att stanna vid size=1 för standardkatalogen och ta till större storlekar endast när algoritmen behöver det större grannskapet. Kantdetektorer drar sällan nytta av mer än 3-gånger-3; utjämningsfilter gör det ibland; rätt storlek beror på skalan på de särdrag applikationen försöker framhäva eller dämpa.
5.16.5. När man bör ta till morph¶
För vardaglig utjämning är mean(), gaussian() och bilateral() snabbare och renare. För kantdetektering är laplacian() och find_edges() specialbyggda. Skälet att gripa direkt till morph() är när applikationen behöver en specifik faltning som de inbyggda filtren inte exponerar – en riktad Sobel, en egendefinierad kantmall, en kärna avstämd mot en viss textur som resten av pipelinen kommer att leta efter, eller någon av standardkatalogen av användbara kärnor som klassisk bildbehandling har byggt upp under decennierna. Den fulla flexibiliteten hos godtyckliga kärnor finns tillgänglig; priset är att applikationen ansvarar för att välja de kärnvärden som producerar det resultat den vill ha.