Las máscaras de Convolución son una manera de aplicar efecto a las representaciones gráficas y obtener nuevos efectos o asignaciones de color a partir de operaciones matemáticas. Usualmente se hace uso de la llamada matriz de convolución que incluye los valores de color de la imagen/elemento original y el kernel el cual es una matriz cuadrada que contiene pesos definidos que alteran los valores de los pixeles según su magnitud. La operación de convolución se expresa matemáticamente de la siguiente manera:
Sea I la matriz de los valores de color de una imagen, y K el kernel de un filtro determinado, entonces la matriz de la imagen filtrada G se calculará como:
donde los elementos de K cumplen: y . Por tanto el proceso de convolución es la accion de sumar cada elemento de la imagen a sus vecinos próximos ponderados por el kernel. Sin embargo la convolución no consiste en la multiplicacián de matrices habitual. Es usual usar kernels de tamaño 3x3 (a pesar de que existen de mayor tamaño), la operación de convolución corresponderá a situar el elemento del centro del kernel en el pixel a analizar. El kernel se solapará con los valores vecinos alrededor del centro, por lo que cada elemento del kernel debe multiplicarse con el valor que está solapando y finalmente todos los valores deben sumarse para dar como resultado el nuevo valor del pixel analizado (que se solapa por el centro del kernel). La imagen a continuación muestra gráficamente este proceso:
En caso de que el kernel tenga mayores dimensiones, la operación de convolución requiere que las filas y columnas del kernel se intercambien horizontal y verticalmente antes de realizar calculos. Adicionalmente se debe manejar una estrategia para los bordes de una imagen, ya que en las operaciones, una parte del kernel se situará por fuera de la imagen; para esto se puede adoptar una de las siguientes convenciones:
Dependiendo de los valores, un kernel puede generar diferentes efectos, entre los más comunes se encuentran la detección de bordes, difuminación, emboss (identificación de sombras o brillos). En el presente taller se detallará en la implementación de convoluciones, tanto de las que hacen uso de un kernel como algunas especiales que no (ver pestaña Máscaras video). Para esto a continuación en la pestaña Máscaras Imagen se podrá visualizar la implementación de algunas convoluciones mencionadas anteriormente aplicadas a una imagen por medio de shadders en WEBGL, lo cual permite que se ejecute el código directamente en la unidad de procesamiento gráfica GPU. Se crea una textura con la imagen y a esa textura se le aplica la matriz de convolución correspondiente según el usuario especifique con las siguientes teclas:
Tecla | Máscara |
---|---|
0 | Identidad (Original) |
1 | Emboss |
2 | Outline (Edge Detection) |
3 | Sharpen |
4 | Top Sobel |
5 | Gaussian Blur |
6 | Left Sobel |
La representación de la imagen convolucionada se visualizará aplicada a una elipse y a un cubo que rota, con el ánimo de explorar una de las capacidades de WEBGL que es el manejo de elementos en 3D. De igual manera veremos como la luz puede afectar principalmente a el mencionado objeto 3D al pasar el cursor sobre el mismo y ver como la luz de distorsiona sobre el objeto (se observa mejor al dejar el cursor estático en un lugar cerca al cubo).
En la pestaña Máscaras Video se visualizan otras máscaras pero esta vez aplicadas a la captura de video por cámara en tiempo real. El video responde a los comandos por teclas mostrados en la siguiente tabla, igualmente aplicando las texturas a un shader de WEBGL.
Tecla | Máscara |
---|---|
0 | Identidad (Original) |
1 | RGB Negative ? |
2 | Inverse |
3 | Single Pass Blur |
4 | Outline (Edge Detection) |
5 | Emboss |
Para este caso, las máscaras denominadas como RGB Negative e Inverse no hacen uso de un kernel como tal sino que efectuan operaciones determinadas sobre cada color de la imagen, están incluidas a causa de su aplicación en la transformación de imagenes.
Por su parte, la pestaña Instrucciones describe un paso a paso del proceso de convolución de los elementos mencionados (imagen y video) ya que son pasos en su mayoría similares; Iniciando con la creación de los shaders respectivos a partir de los Fragment Shader y Vertex Shader correspondientes, una vez creado el canvas tipo WEBGL se pasan los datos de imagen o video al fragment shader para que identifique según la tecla presionada por el usuario que tipo de máscara de convolución desea. Por último el Fragment Shader realiza las operaciones de la máscara deseada y devuelve el renderizado de la imagen/video para ser mostrada.
Finalmente, las pestañas Código Imagen y Código Video muestran el código de implementación para los fragment shaders usados en la imagen y el video respectivamente, ambos tienen comentarios de su funcionamiento para mayor comprensión.
No. | Descripción |
---|---|
1 | Precargar Shader para imagen/ video con el vertex y fragment shader. |
2 | Dividir el área a lo largo de una línea horizontal o vertical. |
3 | Seleccionar una de las dos nuevas celdas de partición. |
4 | Realizar nuevamente el paso numero 2. |
5 | A partir de la tecla presionada por el usuario se definen los valores de la matriz kernel de convolución (si aplica). |
6 | En el caso de Imagen (y algunas máscaras de Video) se obtienen los pixeles vecinos por cada pixel. |
7 | Se realiza la operación de convolución con el kernel o valores correspondientes a cada pixel. |
8 | Con el vector del nuevo color renderiza este valor producto de la convolución y se muestra en pantalla. |
9 | En el caso de Imagen, aplicar la textura renderizada en 2D(elipse) y 3D (cubo). |
1linkvarying vec2 vTexCoord;
2link// Valores que se pasan desde p5
3linkuniform sampler2D u_img;
4linkuniform int u_key;
5linkuniform vec2 stepSize; //Tamaño del texel a usar para cada paso ( 1.0 / width)
6link
7link//Arreglo de 9 valores, cada uno representa un valor alrederor de un pixel (vecinos)
8link// y el pixel mismo
9linkvec2 offset[9];
10link
11link// Arreglo con los valores de la matriz de convolucion a usar
12linkfloat kernel[9];
13link
14link// valor de convolucion que sera renderizado en la pantalla
15linkvec4 conv = vec4(0.0);
16link
17linkvoid main() {
18link vec2 uv = vTexCoord;
19link
20link //Invierte la posicion de la cordenada para que la imagen no quede alrreves
21link uv.y = 1.0 - uv.y;
22link
23link // identity kernel
24link kernel[0] = 0.0; kernel[1] = 0.0; kernel[2] = 0.0;
25link kernel[3] = 0.0; kernel[4] = 1.0; kernel[5] = 0.0;
26link kernel[6] = 0.0; kernel[7] = 0.0; kernel[8] = 0.0;
27link
28link //Segun la tecla presionada por el usuario se define el kernel de la mascara respectiva
29link
30link if(u_key==1){
31link
32link // emboss kernel
33link kernel[0] = -2.0; kernel[1] = -1.0; kernel[2] = 0.0;
34link kernel[3] = -1.0; kernel[4] = 1.0; kernel[5] = 1.0;
35link kernel[6] = 0.0; kernel[7] = 1.0; kernel[8] = 2.0;
36link
37link }else if (u_key==2){
38link
39link // edge detection
40link kernel[0] = -1.0; kernel[1] = -1.0; kernel[2] = -1.0;
41link kernel[3] = -1.0; kernel[4] = 8.0; kernel[5] = -1.0;
42link kernel[6] = -1.0; kernel[7] = -1.0; kernel[8] = -1.0;
43link
44link }else if (u_key==3){
45link
46link // sharpen kernel
47link kernel[0] = 0.0; kernel[1] = -1.0; kernel[2] = 0.0;
48link kernel[3] = -1.0; kernel[4] = 5.0; kernel[5] = -1.0;
49link kernel[6] = 0.0; kernel[7] = -1.0; kernel[8] = 0.0;
50link
51link }else if (u_key==4){
52link
53link // top sobel
54link kernel[0] = 1.0; kernel[1] = 2.0; kernel[2] = 1.0;
55link kernel[3] = 0.0; kernel[4] = 0.0; kernel[5] = 0.0;
56link kernel[6] = -1.0; kernel[7] = -2.0; kernel[8] = -1.0;
57link
58link }else if (u_key==5){
59link
60link // blur kernel
61link kernel[0] = 0.0625; kernel[1] = 0.125; kernel[2] = 0.0625;
62link kernel[3] = 0.125; kernel[4] = 0.25; kernel[5] = 0.125;
63link kernel[6] = 0.0625; kernel[7] = 0.125; kernel[8] = 0.0625;
64link
65link }else if (u_key==6){
66link
67link // left sobel kernel
68link kernel[0] = 1.0; kernel[1] = 0.0; kernel[2] = -1.0;
69link kernel[3] = 2.0; kernel[4] = 0.0; kernel[5] = -2.0;
70link kernel[6] = 1.0; kernel[7] = 0.0; kernel[8] = -1.0;
71link
72link } else if (u_key==0){
73link
74link // identity kernel values
75link kernel[0] = 0.0; kernel[1] = 0.0; kernel[2] = 0.0;
76link kernel[3] = 0.0; kernel[4] = 1.0; kernel[5] = 0.0;
77link kernel[6] = 0.0; kernel[7] = 0.0; kernel[8] = 0.0;
78link
79link }
80link
81link //Guardar la ubicaci�n de los pixeles vecinos
82link offset[0] = vec2(-stepSize.x, -stepSize.y); // top left
83link offset[1] = vec2(0.0, -stepSize.y); // top middle
84link offset[2] = vec2(stepSize.x, -stepSize.y); // top right
85link offset[3] = vec2(-stepSize.x, 0.0); // middle left
86link offset[4] = vec2(0.0, 0.0); //middle
87link offset[5] = vec2(stepSize.x, 0.0); //middle right
88link offset[6] = vec2(-stepSize.x, stepSize.y); //bottom left
89link offset[7] = vec2(0.0, stepSize.y); //bottom middle
90link offset[8] = vec2(stepSize.x, stepSize.y); //bottom right
91link
92link //Por cada pixel vecino
93link for(int i = 0; i<9; i++){
94link //sample a 3x3 grid of pixels
95link vec4 color = texture2D(u_img, uv + offset[i]);
96link
97link //multiplicar el color del pixel por el valor correspondiente del kernel y
98link // añadirlo al valor total de la convolucion
99link conv += color * kernel[i];
100link
101link }//for end
102link
103link // Se renderiza la salida con el valor de la convolucion
104link gl_FragColor = vec4(conv.rgb, 1.0);
105link}
106link}
107link
1link// textura de p5
2linkuniform sampler2D tex0;
3link//valor de la tecla presionada por el usuario
4linkuniform int u_key;
5link//Tamaño de pixel en la pantalla
6linkuniform vec2 texelSize;
7link
8link//Arreglo de 9 valores, cada uno representa un valor alrederor de un pixel (vecinos)
9link// y el pixel mismo
10linkvec2 offset[9];
11link
12link// Arreglo con los valores de la matriz de convolucion a usar
13linkfloat kernel[9];
14link
15link// valor de convolucion que sera renderizado en la pantalla
16linkvec4 conv = vec4(0.0);
17link
18linkvoid main() {
19link
20link vec2 uv = vTexCoord;
21link // voltear la textura para mostrarse al derecho
22link uv = 1.0 - uv;
23link
24link vec4 tex = texture2D(tex0, uv);
25link
26link //Dejar valores de color originales desde el inicio
27link
28link float threshR = tex.r ;
29link float threshG = tex.g ;
30link float threshB = tex.b ;
31link
32link vec4 thresh = vec4(threshR, threshG, threshB, 1.0);
33link
34link //Si la tecla es 0 se muestra el video original
35link if (u_key==0){
36link
37link float threshR = tex.r ;
38link float threshG = tex.g ;
39link float threshB = tex.b ;
40link
41link vec4 thresh = vec4(threshR, threshG, threshB, 1.0);
42link
43link }else if (u_key==1){
44link
45link //Si la tecla es 1 se muestra la mascara correspondiente
46link
47link float gray = (tex.r + tex.g + tex.b) / 3.0;
48link
49link float res = 20.0;
50link float scl = res / (10.0);
51link
52link float threshR = (fract(floor(tex.r*res)/scl)*scl) * gray ;
53link float threshG = (fract(floor(tex.g*res)/scl)*scl) * gray ;
54link float threshB = (fract(floor(tex.b*res)/scl)*scl) * gray ;
55link
56link thresh = vec4(threshR, threshG, threshB, 1.0);
57link }else if (u_key==2){
58link
59link//Si la tecla es 2 se muestra la mascara inversa
60link
61link tex.rgb = 1.0 - tex.rgb;
62link thresh = tex;
63link
64link }else if (u_key==3){
65link
66link //Si la tecla es 3 se muestra la mascara single pass blur
67link //consiste en promediar todos los pixeles vecinos del mismo, incluyendo el propio pixel
68link
69link // creacion de valor del paso a dar para calcular vecinos de un pixel
70link vec2 step = texelSize * 4.0;
71link
72link // obtener los pixeles vecinos y sumarlos en el vector tex
73link
74link vec4 tex = texture2D(tex0, uv); // middle middle -- el pixel actual
75link
76link tex += texture2D(tex0, uv + vec2(-step.x, -step.y)); // top left
77link tex += texture2D(tex0, uv + vec2(0.0, -step.y)); // top middle
78link tex += texture2D(tex0, uv + vec2(step.x, -step.y)); // top right
79link
80link tex += texture2D(tex0, uv + vec2(-step.x, 0.0)); //middle left
81link tex += texture2D(tex0, uv + vec2(step.x, 0.0)); //middle right
82link
83link tex += texture2D(tex0, uv + vec2(-step.x, step.y)); // bottom left
84link tex += texture2D(tex0, uv + vec2(0.0, step.y)); // bottom middle
85link tex += texture2D(tex0, uv + vec2(step.x, step.y)); // bottom right
86link
87link // se toma el promedio de los valores sumados
88link tex /= 9.0;
89link
90link thresh = tex;
91link
92link }else if (u_key==4 || u_key==5){
93link
94link //Si la tecla es 4 o 5 se aplica la mascara edge detection (outline) o emboss
95link vec4 conv = vec4(0.0);
96link if(u_key==4){
97link // edge detection kernel
98link kernel[0] = -1.0; kernel[1] = -1.0; kernel[2] = -1.0;
99link kernel[3] = -1.0; kernel[4] = 8.0; kernel[5] = -1.0;
100link kernel[6] = -1.0; kernel[7] = -1.0; kernel[8] = -1.0;
101link
102link } if (u_key==5){
103link
104link // emboss kernel values
105link kernel[0] = -2.0; kernel[1] = -1.0; kernel[2] = 0.0;
106link kernel[3] = -1.0; kernel[4] = 1.0; kernel[5] = 1.0;
107link kernel[6] = 0.0; kernel[7] = 1.0; kernel[8] = 2.0;
108link }
109link
110link //Guardar la ubicaci�n de los pixeles vecinos
111link offset[0] = vec2(-texelSize.x, -texelSize.y); // top left
112link offset[1] = vec2(0.0, -texelSize.y); // top middle
113link offset[2] = vec2(texelSize.x, -texelSize.y); // top right
114link offset[3] = vec2(-texelSize.x, 0.0); // middle left
115link offset[4] = vec2(0.0, 0.0); //middle
116link offset[5] = vec2(texelSize.x, 0.0); //middle right
117link offset[6] = vec2(-texelSize.x, texelSize.y); //bottom left
118link offset[7] = vec2(0.0, texelSize.y); //bottom middle
119link offset[8] = vec2(texelSize.x, texelSize.y); //bottom right
120link
121link //Por cada pixel vecino
122link for(int i = 0; i<9; i++){
123link //sample a 3x3 grid of pixels
124link vec4 color = texture2D(tex0, uv + offset[i]);
125link
126link //multiplicar el color del pixel por el valor correspondiente del kernel y
127link // añadirlo al valor total de la convolucion
128link conv += color * kernel[i];
129link
130link }//for end
131link thresh = vec4(conv.rgb, 1.0);
132link }else{
133link
134link //Dejar valores de color originales
135link
136link float threshR = tex.r ;
137link float threshG = tex.g ;
138link float threshB = tex.b ;
139link
140link vec4 thresh = vec4(threshR, threshG, threshB, 1.0);
141link }
142link
143link // Se renderiza la salida con el valor de la convolucion
144link gl_FragColor = thresh;
145link}
Con base en la implementación mostrada se puede evidenciar como la convolución afecta en la representación de una imagen y permite a su vez analizar propiedades que no son evidentes a la vista humana a priori. La principal aplicación de las máscaras de convolución es evidentemente en el campo de la fotografía y las aplicaciones relacionadas, en donde la lógica de los diferentes estilos y filtros que se le pueden aplicar a las imagenes está fundamentado en una operación de convolución.
Se resalta que, en el presente taller se observaron operaciones relativamente sencillas y sin mayor grado de complejidad, sin embargo, para filtros más específicos es necesario efectuar operaciones de mayor grado como por ejemplo los filtros sepia para imagenes.
Por otra parte se concluye que las máscaras de convolución tienen un alto potencial en aplicarse en el diagnóstico de trastornos visuales e incluso en la terapia del manejo de estos mismos al diseñar e implementar filtros definidos que posean las propiedades médicas necesarias para la función deseada. De igual manera en la industria del entretenimiento como los videojuegos y la producción audiovisual se abre la posibilidad de representar con mas certeza algunas características del sentido de la vista como condiciones de enfoque y perspectiva de manera más realista.
Convolution Process and Kernel Fundamentals
Image Convolution Fragment Shader Base