5.16. Núcleos de convolución personalizados¶
Cada uno de los filtros de vecindad vistos hasta ahora tenía una estadística integrada que el filtro aplicaba a la ventana en cada posición: la media, el promedio ponderado gaussiano, la mediana. morph() es el único filtro que permite que la aplicación proporcione la estadística por sí misma, en forma de un núcleo: una pequeña matriz de pesos que describe cómo el filtro debe combinar los píxeles de la vecindad en un único valor de salida.
El mecanismo es la clásica operación de convolución. En cada posición de salida, cada píxel de la vecindad se multiplica por el peso correspondiente del núcleo, los productos se suman, el resultado se escala y desplaza opcionalmente, y el valor se escribe en el píxel de salida. Distintos núcleos producen resultados distintos a partir de la misma entrada. Un núcleo con todos los pesos positivos e iguales reproduce el filtro mean(); uno con forma de campana reproduce gaussian(). Los patrones más allá de esos producen respuestas de borde, relieves, gradientes, realce, desenfoque de movimiento y un largo catálogo de otros efectos: todo lo que el procesamiento de imágenes clásico ha querido hacer alguna vez con una única pasada lineal.
5.16.1. El método morph¶
La firma se parece a la de los otros filtros de vecindad con un argumento adicional:
img.morph(size, kernel, mul=1.0, add=0.0)
size es el radio igual que en todas partes, por lo que el núcleo debe tener exactamente (2 * size + 1) filas por (2 * size + 1) columnas. El núcleo en sí es una lista plana de Python con esa cantidad de números, en orden por filas (row-major): las primeras (2 * size + 1) entradas son la fila superior, las siguientes (2 * size + 1) son la segunda fila, y así sucesivamente, hasta la fila inferior. mul escala la suma de productos antes de escribirla en el píxel de salida, y add suma una constante. Los valores por defecto mul=1.0 y add=0.0 dejan la salida de la convolución sin cambios.
Un detalle que vale la pena dejar explícito: el método divide automáticamente la suma de productos por la suma de las entradas del núcleo antes de escribir la salida. Esa división automática significa que un núcleo de promediado cuyas entradas suman nueve – un desenfoque de caja de 3 por 3, por ejemplo – sale a un noveno de escala sin esfuerzo adicional, y un núcleo de aproximación gaussiana que suma dieciséis sale a un dieciseisavo de escala, ambos sin que la aplicación tenga que calcular la división por sí misma. La aplicación establece mul solo cuando quiere una escala adicional sobre la auto-normalización – o, más comúnmente, cuando el núcleo suma cero (un núcleo de respuesta de borde) y la división automática sería una división por nada. En ese caso el framework trata la suma como uno, y mul se convierte en el único control para mantener la suma de productos sin escalar dentro de rango.
El par threshold=True / offset=N de la sección de umbral adaptativo también funciona con morph(), por lo que el mismo framework de núcleos personalizados puede producir un umbral binario cuyo corte se calcula mediante una estadística personalizada.
5.16.2. La disposición del núcleo¶
Un núcleo de 3 por 3 (size=1) es una lista plana de nueve números dispuestos de izquierda a derecha, de arriba abajo. La convención se lee de forma natural si la lista se divide en tres líneas de Python:
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
Este es el operador de gradiente Sobel-x – el primer núcleo estándar que cualquier aplicación va a querer y uno útil para recorrer de principio a fin. El patrón es directo: pesos negativos en la columna izquierda, pesos positivos en la columna derecha, con la columna central en cero. Los pesos de fila -1, -2, -1 (o 1, 2, 1 a la derecha) son mayores en el medio que en las esquinas, lo que da a la fila central más influencia sobre el resultado que a las filas de las esquinas.
Cuando el núcleo barre un borde vertical – una columna de píxeles que pasa de oscuro a la izquierda a brillante a la derecha – los pesos negativos captan el lado oscuro y los pesos positivos captan el lado brillante. La suma de productos es un número positivo grande, que el filtro escribe como un píxel de salida brillante. Una zona horizontal de brillo uniforme produce cero, porque cada peso positivo se compensa con un peso negativo de igual magnitud sobre un píxel del mismo valor.
Ejecutar el núcleo:
img.morph(1, sobel_x, mul=0.25)
El núcleo Sobel suma cero – cada peso negativo del lado izquierdo se compensa con un peso positivo igual a la derecha – por lo que la división automática no divide por nada, y mul es la única escala sobre la suma de productos. mul=0.25 mantiene la respuesta dentro de rango: la mayor suma absoluta que Sobel-x puede producir a partir de una zona de 3 por 3 es de aproximadamente 4 * 255 = 1020 (ocho píxeles brillantes ponderados hasta 2), y dividir eso entre cuatro deja los casos extremos en 255, donde el formato los recorta limpiamente.
El núcleo Sobel-y correspondiente detecta bordes horizontales rotando el mismo patrón de pesos 90 grados:
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
Las aplicaciones que quieren detectar cualquier borde, sin importar la dirección, normalmente ejecutan ambos Sobel y combinan las respuestas.
5.16.3. Desplazar la salida¶
add es la otra mitad de la historia del escalado. La respuesta de un núcleo de suma cero tiene signo – positiva en un lado de un borde, negativa en el otro – y la mitad negativa se recorta a cero al escribirse en un píxel sin signo. add=128 desplaza la respuesta para que quede centrada en gris medio, de modo que las respuestas negativas sobreviven como valores por debajo de 128 y las positivas quedan por encima: una respuesta de borde o un relieve se vuelve visible en ambas direcciones, a costa de la mitad del rango en cada una.
Qué combinación de mul y add espera un núcleo forma parte del diseño del núcleo; el catálogo de núcleos estándar enumera los ajustes correctos para cada núcleo común.
5.16.4. Núcleos más grandes¶
Todo en esta página se ha descrito con núcleos de 3 por 3 (size=1), porque ese es el tamaño que usa el catálogo estándar y porque la disposición por filas es fácil de escribir a mano a ese tamaño. Sin embargo, nada en el mecanismo restringe el núcleo a 3 por 3. size=2 ejecuta un núcleo de 5 por 5, con veinticinco entradas en la lista plana; size=3 ejecuta uno de 7 por 7 con cuarenta y nueve; y así sucesivamente, hasta el radio que la aplicación esté dispuesta a pagar. El framework gestiona disposiciones de lista plana o de filas anidadas en cualquier tamaño impar.
La razón para recurrir a un núcleo más grande es la misma razón para recurrir a una vecindad más grande en cualquiera de los filtros integrados: más promediado, detección de características más amplia, menos sensibilidad al ruido de un solo píxel. El coste crece con el cuadrado del radio – un núcleo de 5 por 5 hace aproximadamente 2,8 veces el trabajo por píxel de uno de 3 por 3, uno de 7 por 7 unas 5,4 veces – y ese multiplicador sale directamente de la tasa de fotogramas.
El patrón práctico es quedarse en size=1 para el catálogo estándar y recurrir a tamaños mayores solo cuando el algoritmo necesite la vecindad más grande. Los detectores de bordes rara vez se benefician más allá de 3 por 3; los filtros de suavizado a veces sí; el tamaño adecuado depende de la escala de las características que la aplicación intenta resaltar o suprimir.
5.16.5. Cuándo recurrir a morph¶
Para el suavizado cotidiano, mean(), gaussian() y bilateral() son más rápidos y limpios. Para la detección de bordes, laplacian() y find_edges() están diseñados para ese propósito. El motivo para recurrir directamente a morph() es cuando la aplicación necesita una convolución específica que los filtros integrados no exponen – un Sobel direccional, una plantilla de borde personalizada, un núcleo ajustado a una textura particular que el resto de la cadena va a buscar, o cualquiera de los núcleos útiles del catálogo estándar que el procesamiento de imágenes clásico ha acumulado a lo largo de las décadas. Está disponible toda la flexibilidad de los núcleos arbitrarios; el precio es que la aplicación es responsable de elegir los valores del núcleo que producen el resultado que desea.