La manipulación de archivos de imágenes en diferentes formatos y su exitoso posicionamiento son las metas iniciales del trabajo con imágenes digitales. Lo interesante es que Processing no solo tiene la capacidad de realizar estas acciones, sino que, en realidad, se pueden manipular todos los píxeles de una imagen de forma colectiva o individual. En este apartado se revisarán las opciones disponibles en este lenguaje de programación. Además, se indicarán las opciones para la utilización de filtros digitales preconfigurados que ofrece Processing.
En Processing las imágenes son manipuladas como arrays. En primera instancia ya se cuenta con un tipo exclusivo de
datos para las imágenes denominado “PImage”. Este tipo de datos guardará la información que se cargue desde un
archivo de imagen con extensión .gif, .jpg, .png y .tga. Para guardar una imagen dentro del tipo de datos PImage se
utiliza la función loadimage, la cual recibe la ruta de la imagen que se quiere cargar:
PImage imagen = loadimage(“imagen.jpg”);
En este ejemplo se crea una variable con el tipo de datos PImage, llamada “imagen”, a la cual se le carga un archivo de
imagen llamado “imagen.jpg”.
De esta manera, ya se tiene disponible la información de la imagen requerida dentro de una variable, de manera tal
que, a partir de ese momento, se pueden manipular los datos de forma directa.
Algo importante dentro de Processing es que si se colocan las imágenes que se utilizan en el programa dentro de la
carpeta Data, solo con indicar el nombre de la imagen a cargar es suficiente, de lo contrario hay que indicar la ruta
completa de la misma.
Una vez que se tiene cargada la imagen en la variable, se debe utilizar otra función para lograr posicionarla y visualizarla.
Esta función es la denominada image(), que recibe como parámetros la variable donde está guardada la imagen
y la posición x e y que se requiere.
image(PImage imagen, x, y);
Muchas veces es necesario que la ventana tenga el mismo tamaño que la imagen. Para esto es útil saber que dentro
de la variable del tipo PImage se tiene también los datos del tamaño de la imagen cargada.
A continuación, un ejemplo de carga, ubicación y visualización:
//Se define una variable del tipo PImage
PImage imagen;
//Se carga un archivo de imagen en la variable
imagen = loadimagen(“imagen.jpg”);
//Se define el tamaño de la ventana, configurando el mismo tamaño que //la imagen cargada
size(imagen.width, imagen.height);
//Se posiciona y visualiza la imagen
image(imagen, 0, 0);
//Una opción puede ser guardar una copia de la imagen mediante la función //save
save(“copia_de_imagen.jpg”);
Actividad 1
A partir del ejemplo anterior, generar un sketch donde se posicionen y visualicen dos imágenes diferentes. Recordar que cada imagen debe ser guardada en un objeto PImage diferente.
Cualquier píxel de una imagen puede ser manipulado dentro de Processing. Se puede querer obtener información de
un píxel determinado o se puede querer configurar un píxel de alguna manera en particular. Para ello, se utilizan las
funciones get() y set(), respectivamente.
get() puede recibir hasta cuatro argumentos (x, y, w, h). Los dos primeros, x e y, son los que indican qué o desde qué
píxel queremos obtener información; los dos siguientes, w y h son para determinar una zona de la imagen de la cual
se quiere obtener información, serían el ancho y el alto de un rectángulo cuyo vértice superior izquierdo está en las
coordenadas x e y. Por supuesto, si no se indica ancho y alto, se tiene la información de un solo píxel.
get(x, y, w, h);
set() recibe tres argumentos, las coordenadas x e y de un píxel y el color que tendrá este píxel, y configura el color de
ese píxel con la variable de color que se pasó a la función.
set(x, y, color);
Otras características interesantes que se pueden extraer de un píxel son su valor alfa, y las componentes rojo, verde y
azul. Para esto existen cuatro funciones alpha(), red(), green() y blue(); cada una de las cuales obtiene la información
específica de cada píxel. Estas son las llamadas “componentes de una imagen digital”.
Un ejemplo de cómo combinar estas funciones en un solo programa es el siguiente:
//Se declara la variable PImage
PImage imagen;
//Se carga el archivo de imagen en la variable
imagen = loadimagen(imagen.jpg);
//Se establece el tamaño de la ventana
size(imagen.width, imagen.height);
//Se posiciona y visualiza la imagen
image(imagen, 0, 0);
//Ahora mediante dos loops for anidados se recorre cada píxel de la imagen.
//Coordenada Y del píxel
for(int y = 0; y >imagen.height; y++)
{
//Coordenada X del píxel
for(int x = 0; y >imagen.width; x++)
{
//Se recoge la información del píxel //correspondiente
color miPixel = get(x, y);
//Se extraen las características del píxel
int a = alpha(miPixel);
int r = red(miPixel);
int g = green(miPixel);
int b = blue(miPixel);
//Se calcula la inversa de color y alfa del píxel actual
//Prestar atención a cómo se calcula la inversa de cada //componente
color inversa = color(255 - a, 255 – r, 255 – g, 255 - b);
//Se aplica esta inversa al píxel
set(x, y, inversa);
}
}
//Se guarda una copia de la imagen pero con sus características de color y alfa
//invertidas
save(“imagen_invertida.jpg”);
Actividad 2
A partir del ejemplo anterior, tomar una imagen y alterar sus componentes de color por separado obteniendo tres imágenes diferentes, una con una componente roja más pronunciada, otra con una componente verde más pronunciada y otra con una componente azul más pronunciada. Guardar las tres opciones en archivos de imagen diferentes.
Processing ofrece una serie de filtros preconfigurados que pueden ser aplicados a una imagen. Para esto se utiliza la
función filter(), que aplica un filtro a una imagen utilizando la siguiente sintaxis:
filter(MODE);
filter(MODE, level);
Donde MODE puede tener los siguientes valores:
- THRESHOLD: convierte los valores de color de los píxeles de la imagen en negro o blanco, dependiendo del valor
definido por el argumento level. Si el valor del argumento es 0.0, la imagen será totalmente negra, si es 1.0 la imagen
será totalmente blanca, en valores intermedios hay un balance entre estos dos colores. Si no se indica el argumento
level, se ajusta 0.5 por default.
- GRAY: convierte cualquier color de la imagen en su equivalente en escala de grises.
- INVERT: configura cada píxel de la imagen con su valor inverso.
- POSTERIZE: limita cada canal de la imagen a el número especificado (un nivel de color) por el argumento level.
- BLUR: ejecuta un desenfoque del tipo gausiano que depende del parámetro level. Si este parámetro no es indicado,
el desenfoque equivale a un desenfoque gausiano de radio 1.
- OPAQUE: configura el canal de alfa de la imagen en totalmente opaco.
Se puede observar cómo funciona cada uno de estos filtros corriendo el siguiente ejemplo complementario.
CAP3EjemploComplementario1.pde
Dentro de esta categoría de filtros se puede añadir el uso de la función tint(). Esta función es el equivalente de la función
fill() que se estudió en unidades anteriores, pero para el uso en imágenes. Gracias a tint() se puede configurar el
color y la transparencia alfa de cualquier imagen que se encuentre visualizada en el sketch. Debido a que una imagen
no tiene un solo color, tint() recibe como argumento la cantidad de color y transparencia que se configura a cada
píxel. Por lo tanto, esta función puede recibir entre uno y cuatro argumentos:
Color en escala de grises
tint(a1);
Color en escala de grises más transparencia
tint(a1, a2);
Color en RGB
tint(a1, a2, a3);
Color en RGB más transparencia
tint(a1, a2, a3, a4) ;
El siguiente ejemplo complementario muestra las diferentes versiones de tint() aplicadas en una sola imagen.
CAP3EjemploComplementario2.pde
Es interesante pensar que mientras los filtros que se proveen en Processing de forma preconfigurada son adecuados
para procesamientos básicos de la imagen, estos no permiten experimentar más allá de un nivel simplificado. En contraste
con estos filtros, utilizando las funciones get() y set() pueden ser creados filtros a medida.
Los ejemplos que se revisaron anteriormente, donde se utilizaban las funciones get() y set(), en general trabajan con
todos los píxeles de una imagen de forma secuencial, y no es necesario saber exactamente en qué lugar del array se
encuentra cada píxel. Pero en muchas de las aplicaciones de procesamiento de imagen esta información espacial
es crucial, como por ejemplo cuando solo se pretende manipular los píxeles pares e impares por separado. Una característica
que tiene Processing es que una imagen al ser cargada es guardada en un array de una sola dimensión,
conteniendo la información de las coordenadas XY de cada píxel.
Lo importante es entender cómo un array lineal unidimensional como este:
puede ser interpretado como una matriz (la propia imagen) como esta:
Si se asume que se sabe qué tamaño tiene la imagen (lo hacemos mediante las características width height de cada
imagen), se calcula la cantidad de píxeles como el largo por el ancho de la imagen y, para saber la ubicación en este
array de cualquier píxel, solo se debe respetar la siguiente ecuación:
Ubicación: X + (Y*WIDTH)
Una vez que ya se sabe cómo acceder a cada uno de los píxeles de la imagen cargada, se debe saber también que,
para acceder al array de la imagen de forma directa, se utiliza el array especial pixels[].
pixels[] contiene los valores de todos los píxeles de la ventana de Processing. Estos valores son del tipo de dato color.
El índice del array define la posición de cada elemento. Por ejemplo, si se realiza la siguiente operación, buscando un
píxel por su ubicación en el array:
color b = pixels[230];
La variable b tomará los valores de color de esa ubicación específica.
Lo interesante de este array especial es la idea de que en realidad uno puede acceder a todos los píxeles de una ventana
de visualización de Processing, donde puede haber más de una imagen. Para poder trabajar con este array especial
se deben utilizar dos funciones, una antes de utilizarlo y otra luego de hacerlo. Estas funciones son loadpixels() y
updatepixels(). La primera, loadpixels(), se utiliza antes del array especial pixels[], y tiene la tarea de avisarle a Processing
que se está por usar esta información. La segunda, updatepixels(), se utiliza cuando ya se terminó de usar el
array y le indica a Processing que puede actualizar los píxeles en la pantalla.
Esta forma de trabajar con los píxeles de una imagen tiene la ventaja de ser más veloz. En el caso de set() y get(), lo que
sucede es que se actualiza cada píxel en cada momento, tomando un tiempo en cada caso. De esta nueva forma se procesan
todos los píxeles y luego se los muestra todos juntos, realizando la visualización una sola vez en todo el proceso.
El siguiente ejemplo sirve para observar cómo funcionan estos tres nuevos elementos dentro de un sketch:
//En este ejemplo no se carga una imagen sino que se utilizan los píxeles de la
//ventana del sketch.
//Se configura el tamaño de la ventana
size(200, 200);
//Se avisa a Processing que se utilizará el array especial pixels
loadPixels();
//Se genera un loop for anidado para recorrer el array
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
//Se utiliza la fórmula para establecer la
//ubicación de cada píxel en el array
int ubicacion = x + y * width;
//El condicional busca las columnas pares
if (x % 2 == 0)
{
//Si la columna es par se pinta de
//blanco el píxel
pixels[ubicacion] = color(255);
}
else
{
//Si es impar, se pinta de negro el píxel
pixels[ubicacion] = color(0) ;
}
}
}
//Se actualizan todos los píxeles.
updatePixels( ) ;
De esta manera se torna sencillo procesar los píxeles correspondientes a una ventana de visualización de Processing.
A continuación, se muestra cómo se realiza el procedimiento anterior, pero en una imagen determinada y utilizando
los nuevos elementos que se presentaron para el manejo de píxeles Prestar especial atención en cómo se accede al
array especial pixels[] de la propia imagen.
//Tamaño de la ventana
size(200, 200);
//Se define una variable PImage
PImage img;
//Opciones de configuración
void setup()
{
//Tamaño de la ventana
size(200, 200);
//Se carga una imagen en la variable
img = loadImage(“imagen.jpg”);
}
void draw()
{
//Se avisa a Processing que se utilizará el array pixels
loadPixels();
//Y de esta manera se accede al array pixels de la imagen en sí.
//Es necesario entender que la información de esta imagen en particular será a
//que llenará el array pixels[]
img.loadPixels();
//Loops for anidados para recorrer píxel por píxel
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
//Se calcula el índice de cada píxel según la posición XY
int ubicacion = x + y*width;
//Se obtiene la información RGB de cada píxel indexado de la imagen
float r = red(img.pixels[ubicacion]);
float g = green(img.pixels[ubicacion]);
float b = blue(img.pixels[ubicacion]);
//Se realiza el procesamiento de los píxeles de la imagen a continuación
//Se copia la información de la imagen en la ventana de Processing.
pixels[ubicacion] = color(r,g,b);
}
}
//Se actualizan los píxeles de la ventana
updatePixels();
}
Tras esta explicación sobre el tratamiento de píxeles de una imagen se tiene la información necesaria para estudiar algunos algoritmos básicos de procesamiento que permiten efectos interesantes, los cuales son revisados en el siguiente apartado.
Este apartado mostrará algunos algoritmos de procesamiento básicos utilizando la manipulación de píxeles.
En el primer caso, a partir del filtro existente GRAY, provisto por Processing, se puede generar un efecto donde se
remarca la diferencia de los píxeles que presentan los bordes de la imagen.
El algoritmo utilizado es muy sencillo. Partiendo de la idea de que el filtro GRAY genera una escala de grises, y que en
general los bordes de una imagen se caracterizan por tener valores más cercanos al negro (así se observa la marca
del borde justamente), se van comparando los píxeles hasta que alguno presenta una diferencia en el componente
de color más grande que otro (un color más oscuro tendrá cualquiera de sus componentes de color más altos, en este
caso se comparan los componentes rojos de cada píxel) y si la diferencia supera un umbral, se pinta el píxel con el
componente de rojo más alto, de negro, marcándolo como un borde. El secreto de la eficiencia de este sencillo algoritmo
es establecer un umbral correcto, el cual seguramente se plasmará en imágenes de salida diferentes.
La aplicación del algoritmo se observa en el ejemplo siguiente:
//Se define una variable PImage
PImage Imagen;
//Se carga un archivo de imagen en la variable PImage
Imagen = loadImage(“imagen.jpg”);
//Se define el tamaño de la ventana según el tamaño de la imagen
size(Imagen.width, Imagen.height);
//Se ubica y visualiza la imagen
image(Imagen, 0, 0);
//Se convierte la imagen en escala de grises. Nótese cómo el filtro puede ser //aplicado solo a
esa imagen
Imagen.filter(GRAY);
//Se define y guarda el color blanco
color blanco = color(255, 255, 255);
//Se define y guarda el color negro
color negro = color(0, 0, 0);
//Se extraen los bordes. Aquí se aplica el algoritmo
//Se utiliza un loop for anidado el cual recorre los píxeles en XY
for (int y=0; y<Imagen.height-1; y++)
for (int x=0; x<Imagen.width-1; x++)
{
//Se extraen las características de color del píxel actual y el siguiente en //el eje X
color PixelActual = get(x, y);
color PixelSiguiente = get(x+1, y);
//Se obtienen los valores de alguna componente de color de los píxeles
int ComponenteActual = int(red(PixelActual));
int ComponenteSiguiente = int(red(PixelSiguiente));
//Si la diferencia supera el umbral establecido, se pinta de negro el píxel, de lo //contrario se
pinta de blanco
if (abs(ComponenteActual - ComponenteSiguiente)>6)
{
save(“imagen_bordes.jpg”);
set(x, y, negro);
}
else
{
set(x, y, blanco);
}
}
//Se guarda una copia de la imagen procesada
save(“imagen_bordes.jpg”);
Actividad 3
A partir del ejemplo anterior, utilizar diferentes tipos de umbral y de extracción de componentes para obtener el mejor resultado de una imagen diferente a la que allí se presenta. Los diferentes umbrales deben poder ser seleccionados mediante dos teclas del teclado.
Otro algoritmo simple pero interesante y visualmente efectivo es el utilizado para generar un corrimiento en los píxeles
de una imagen, logrando un efecto similar al de una pintura puntillista.
El procesamiento es sumamente sencillo, pero el efecto es contundente. Se toma la posición original de cada píxel y
se lo reubica de forma aleatoria una cierta cantidad de lugares, luego se dibuja un cuadrado del mismo color que el
píxel pero que cubre varios píxeles, en este caso 4x4, lo que hace que la imagen se altere.
El código que ejecuta este algoritmo se presenta en el siguiente ejemplo.
//Se define una variable PImage y se carga un archivo de imagen en ella
PImage Imagen = loadImage(“imagen.jpg”);
//Se establece el tamaño de la ventana a partir de las dimensiones de la imagen
size(Imagen.width, Imagen.height);
//Se posiciona y visualiza la imagen
image(Imagen, 0, 0);
//Se recorren todos los píxeles a partir de un loops for anidados
for (int x=0; x<width; x++)
for (int y=0; y<height; y++)
{
//Se extraen las características de cada píxel
color c = get(x, y);
//Se crean dos variables que contendrá la nueva posición XY de cada
//píxel
int xx = x+int(random(-4, 4));
int yy = y+int(random(-4, 4));
//Se configura el color de los píxeles en la nueva ubicación, corriendo así
//la imagen de forma aleatoria
set(xx, yy, c);
//Luego se dibujan los cuadrados de 4x4 píxeles con el color
//correspondiente y sin bordes
fill(c);
noStroke();
rect(xx-5, yy-5, 4, 4);
}
Actividad 4
Crear un algoritmo que permita espejar una imagen en el eje X, mediante la reubicación de sus píxeles. Por ejemplo, el primer píxel ocupará el último lugar y el último el primero y así sucesivamente. Se requiere que el código esté correctamente comentado.
Una forma más directa de aplicar algún tipo de procesamiento sobre una imagen es tomando una imagen y realizar determinadas operaciones con matrices.
Una operación interesante es utilizar las denominadas
“matrices de convolución”. Una matriz de convolución
no es otra cosa que una matriz cuadrada que tiene una
dimensión determinada; por ejemplo, tres elementos
por tres elementos (3 filas X 3 columnas), los cuales
contienen valores numéricos. Aplicar un filtro de convolución
no es otra cosa que superponer la matriz de
convolución sobre una parte (o toda la imagen, dependiendo
del tamaño de la matriz) de una imagen.
La operación se realiza multiplicando el valor de cada cuadrícula de la matriz por el correspondiente a la misma ubicación
de la parte de la imagen seleccionada, luego se suman todos los valores y se obtiene un promedio del resultado
que luego será asignado al píxel central. De esta manera, se obtendrán nuevos valores de color y textura en esa
posición de la imagen.
Este tipo de procesamiento en realidad propone un promedio ponderado de un píxel de entrada y los píxeles vecinos.
En otras palabras, el nuevo píxel que se obtiene después del procesamiento es una función de los píxeles vecinos. De
esta manera se pueden tener diferentes tipos de matrices que tengan un píxel central como 3x3 o 5x5. Los valores de
los píxeles limítrofes serán los que imponga el tipo de procesamiento.
De esta manera, mediante diferentes tipos de combinaciones de píxeles vecinos resultan en efectos diferentes. Por
ejemplo, si se desea solamente las frecuencias altas de una imagen (colores más claros) se debe tener una matriz que
incremente el píxel central y bajar los píxeles vecinos. Una matriz como la siguiente sería de mucha utilidad:
-1 -1 -1
-1 9 -1
-1 -1 -1
Y justamente, este es un ejemplo de una matriz para aplicar un filtro pasa alto a un conjunto de datos. Este tipo de procedimiento es también conocido como filtrado espacial.
Daniel Shiffman presenta un ejemplo de aplicación de este tipo
de procesamiento dentro de los ejemplos de Processing. Debido
a la extensión del mismo, se presenta como ejemplo complementario,
debidamente comentado en español.
CAP3EjemploComplementario3.pde
Actividad 5
A partir del EjemploComplementario3 utilizar tres matrices de filtrado diferente
para obtener tres tipos de efectos diferenciados.
Como ejercicio complementario hacer que estas tres matrices convivan en el mismo
programa.
Para terminar, se presentará un último algoritmo de procesamiento, el cual genera un efecto de iluminación, utilizando solo un cálculo simple de distancia entre píxeles. Mediante el movimiento del mouse por la ventana, se pueden ir iluminando las diferentes partes de la misma. Para esto, se calcula la distancia entre el cursor del mouse y los píxeles circundantes. A partir de esta distancia se toma la decisión de la cantidad de componente de color que se le da a los píxeles que conforman el foco de la iluminación. El tamaño de este foco, así como el nivel de iluminación, es variable y totalmente configurable.
Mediante un ejemplo, adaptado también a partir de un ejemplo
de Daniel Shiffman, se observa cómo realizar los cálculos de distancia
y aplicar el filtrado de brillo. Este algoritmo también será
tratado como un ejemplo complementario, debido a la extensión
del propio código.
CAP3EjemploComplementario4.pde
De esta manera, se han revisado cuatro posibles algoritmos básicos de procesamiento, los cuales pueden ser adaptados a las necesidades de cada caso. Es necesario entender que estos son solo casos sencillos de algoritmos de proce samiento, pero que permiten dar una idea de las cualidades y herramientas que presenta el lenguaje Processing en la tarea de operar sobre imágenes. También se debe entender que estos algoritmos son válidos para otros lenguajes de programación que operen con imágenes. Existe una gran cantidad de algoritmos para procesar imágenes, pero una extensión más exhaustiva de su explicación excede los límites de este material.
Lectura Obligatoria
Ordóñez, S. (2005), “Formatos de Imagen Digital” en: Revista Digital
Universitaria, 5(7). UNAM, México.
Aldalur B y Santamaria, M. (2002), “Realce de imágenes: filtrado
espacial” en: Revista de Teledetección, 17. Asociación Española de
Teledetección, España.
Steegma nn Pascual, C,, Rodríguez Velázquez, J. Y Pérez, A. (2002), “Algebra
de matrices” en: Proyecto e-Math. Universidad Abierta de
Cataluña, España.