5.16. Aangepaste convolutiekernels¶
Elk van de tot nu toe behandelde buurtfilters had een ingebouwde statistiek die het filter op elke positie op het venster toepaste – het gemiddelde, het Gaussiaans gewogen gemiddelde, de mediaan. morph() is het enige filter waarmee de applicatie de statistiek zelf kan aanleveren, in de vorm van een kernel: een kleine matrix van gewichten die beschrijft hoe het filter de buurtpixels tot een enkele uitvoerwaarde moet combineren.
Het mechanisme is de klassieke convolutie-bewerking. Op elke uitvoerpositie wordt elke buurtpixel vermenigvuldigd met het bijbehorende gewicht in de kernel, worden de producten opgeteld, wordt het resultaat optioneel geschaald en verschoven, en wordt de waarde naar de uitvoerpixel geschreven. Verschillende kernels leveren verschillende resultaten op uit dezelfde invoer. Een kernel met allemaal gelijke positieve gewichten reproduceert het mean()-filter; een klokvormige reproduceert gaussian(). Patronen daarbuiten leveren randresponsen, embossingen, gradiënten, verscherping, bewegingsonscherpte en een lange catalogus aan andere effecten op – alles wat klassieke beeldverwerking ooit met een enkele lineaire doorloop heeft willen doen.
5.16.1. De morph-methode¶
De signatuur lijkt op die van de andere buurtfilters, met één extra argument:
img.morph(size, kernel, mul=1.0, add=0.0)
size is de straal, op dezelfde manier als overal elders, dus de kernel moet precies (2 * size + 1) rijen bij (2 * size + 1) kolommen zijn. De kernel zelf is een platte Python-lijst van dat aantal getallen, in row-major-volgorde – de eerste (2 * size + 1) waarden vormen de bovenste rij, de volgende (2 * size + 1) de tweede rij, enzovoort, tot aan de onderste rij. mul schaalt de som-van-producten voordat deze in de uitvoerpixel wordt geschreven, en add telt er een constante bij op. De standaardwaarden mul=1.0 en add=0.0 laten de convolutie-uitvoer ongewijzigd.
Eén detail dat het waard is om expliciet te benoemen: de methode deelt de som-van-producten automatisch door de som van de kernelwaarden voordat de uitvoer wordt geschreven. Die automatische deling betekent dat een middelende kernel waarvan de waarden optellen tot negen – een 3-bij-3 box blur bijvoorbeeld – zonder extra moeite op een negende van de schaal uitkomt, en een Gaussiaanse benaderingskernel die optelt tot zestien op een zestiende van de schaal uitkomt, beide zonder dat de applicatie de deling zelf hoeft te berekenen. De applicatie stelt mul alleen in wanneer ze een verdere schaling bovenop de automatische normalisatie wil – of, vaker, wanneer de kernel optelt tot nul (een randresponskernel) en de automatische deling een deling door niets zou zijn. Het framework behandelt de som in dat geval als één, en mul wordt de enige knop om de ongeschaalde som-van-producten binnen bereik te houden.
Het paar threshold=True / offset=N uit het gedeelte over adaptieve drempelwaarden werkt ook op morph(), zodat hetzelfde framework voor aangepaste kernels een binaire drempelwaarde kan produceren waarvan de afkapwaarde door een aangepaste statistiek wordt berekend.
5.16.2. De kernelindeling¶
Een 3-bij-3-kernel (size=1) is een platte lijst van negen getallen, van links naar rechts en van boven naar beneden gerangschikt. De conventie leest natuurlijk als de lijst over drie Python-regels wordt verdeeld:
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
Dit is de Sobel-x-gradiëntoperator – de eerste standaardkernel die elke applicatie zal willen gebruiken en een nuttige om volledig door te lopen. Het patroon is eenvoudig: negatieve gewichten in de linkerkolom, positieve gewichten in de rechterkolom, met de middelste kolom op nul. De rijgewichten -1, -2, -1 (of 1, 2, 1 aan de rechterkant) zijn in het midden hoger dan aan de hoeken, wat de middelste rij meer invloed op het resultaat geeft dan de hoekrijen.
Wanneer de kernel over een verticale rand veegt – een kolom pixels die van donker aan de linkerkant naar helder aan de rechterkant overgaat – pikken de negatieve gewichten de donkere kant op en de positieve gewichten de heldere kant. De som van producten is een groot positief getal, dat het filter als een heldere uitvoerpixel schrijft. Een horizontaal vlak van uniforme helderheid levert nul op, omdat elk positief gewicht wordt gecompenseerd door een negatief gewicht van dezelfde grootte op een pixel met dezelfde waarde.
De kernel uitvoeren:
img.morph(1, sobel_x, mul=0.25)
De Sobel-kernel telt op tot nul – elk negatief gewicht aan de linkerkant wordt gecompenseerd door een gelijk positief gewicht aan de rechterkant – dus de automatische deling deelt niet door iets, en mul is de enige schaling op de som-van-producten. mul=0.25 houdt de respons binnen bereik: de grootste absolute som die de Sobel-x uit een 3-bij-3-vlak kan produceren is ongeveer 4 * 255 = 1020 (acht heldere pixels met een gewicht tot 2), en dat door vier delen brengt de extreme gevallen op 255, waar het formaat ze netjes afkapt.
De bijbehorende Sobel-y-kernel detecteert horizontale randen door hetzelfde gewichtspatroon 90 graden te roteren:
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
Applicaties die elke rand willen detecteren, ongeacht de richting, draaien doorgaans beide Sobels en combineren de responsen.
5.16.3. De uitvoer verschuiven¶
add is de andere helft van het schalingsverhaal. De respons van een kernel met som nul is getekend – positief aan de ene kant van een rand, negatief aan de andere – en de negatieve helft kapt af tot nul wanneer deze in een waardeloze (unsigned) pixel wordt geschreven. add=128 verschuift de respons zodat deze gecentreerd is op middengrijs, zodat negatieve responsen overleven als waarden onder 128 en positieve erboven uitkomen: een randrespons of een emboss wordt in beide richtingen zichtbaar, ten koste van de helft van het bereik in elke richting.
Welke combinatie van mul en add een kernel verwacht maakt deel uit van het ontwerp van de kernel; de standaardkernelcatalogus vermeldt de juiste instellingen voor elke veelvoorkomende kernel.
5.16.4. Grotere kernels¶
Alles op deze pagina is beschreven met 3-bij-3-kernels (size=1), omdat dat de grootte is die de standaardcatalogus gebruikt en omdat de row-major-indeling op die grootte gemakkelijk met de hand uit te schrijven is. Niets in het mechanisme beperkt de kernel echter tot 3-bij-3. size=2 draait een 5-bij-5-kernel, met vijfentwintig waarden in de platte lijst; size=3 draait een 7-bij-7 met negenenveertig; enzovoort, tot aan elke straal die de applicatie bereid is te betalen. Het framework verwerkt zowel platte-lijst- als geneste-rij-indelingen bij elke oneven grootte.
De reden om naar een grotere kernel te grijpen is dezelfde reden om naar een grotere buurt bij een van de ingebouwde filters te grijpen: meer middeling, bredere kenmerkdetectie, minder gevoeligheid voor ruis op enkele pixels. De kosten groeien met het kwadraat van de straal – een 5-bij-5 doet ongeveer 2,8 keer het werk per pixel van een 3-bij-3, een 7-bij-7 ongeveer 5,4 keer – en die vermenigvuldigingsfactor komt rechtstreeks uit de framerate.
Het praktische patroon is om bij size=1 te blijven voor de standaardcatalogus en alleen naar grotere groottes te grijpen wanneer het algoritme de grotere buurt nodig heeft. Randdetectoren profiteren zelden boven 3-bij-3; gladstrijkende filters soms wel; de juiste grootte hangt af van de schaal van de kenmerken die de applicatie probeert te benadrukken of te onderdrukken.
5.16.5. Wanneer naar morph te grijpen¶
Voor alledaags gladstrijken zijn mean(), gaussian() en bilateral() sneller en schoner. Voor randdetectie zijn laplacian() en find_edges() speciaal hiervoor ontworpen. Direct naar morph() grijpen is zinvol wanneer de applicatie een specifieke convolutie nodig heeft die de ingebouwde filters niet beschikbaar stellen – een directionele Sobel, een aangepaste randsjabloon, een kernel afgestemd op een bepaalde textuur waar de rest van de pijplijn naar gaat zoeken, of een van de standaardcatalogus van nuttige kernels die de klassieke beeldverwerking in de loop der decennia heeft opgebouwd. De volledige flexibiliteit van willekeurige kernels is beschikbaar; de prijs is dat de applicatie verantwoordelijk is voor het kiezen van de kernelwaarden die het gewenste resultaat opleveren.