Universitat Oberta de Catalunya

Operaciones con imágenes en programación Processing

Una de las operaciones clásicas en la disciplina del Procesado de Imagen y del Retoque Fotográfico es la resta de imágenes sucesivas. Es muy útil como primer paso en la detección de, por ejemplo, movimiento en una sala. Imaginemos que tenemos una cámara de video gravando continuamente el mismo plano de la sala. Si nadie entra ni sale todas las imágenes serán exactamente iguales y por lo tanto una resta entre dos cualesquiera dará como resultado una imagen vacía (básicamente negra). Pero ahora imaginemos que, entre una y otra imagen, algo ha variado…debido a que por ejemplo entró alguien en la sala. Al efectuar la resta observaremos que las diferencias se contrastan y que no obtenemos tan sólo un fondo completamente negro…y en ese caso podríamos hacer saltar la alarma del banco, del museo o del local a proteger.

En nuestro caso realizaremos un sencillo programa Processing que a) Cargará dos imágenes de disco duro, b) Las restará píxel a píxel y c) El resultado lo almacenará como una tercera imagen que se mostrará también por pantalla. En la figura 1 podemos observarlo claramente.

Restamos las imágenes de arriba para obtener la de debajo. Imágenes cortesía de http://www.freeimageslive.com/.

Figura 1. Restamos las imágenes de arriba para obtener la de debajo. Imágenes cortesía de http://www.freeimageslive.com/.

Empecemos por aprender cómo se cargan nuestras imágenes en Processing, gracias al código 1.

// Let's load 2 images and generate
// a third one by substracting them both
// in a per-pixel basis

// We create 3 PImage objects for our 3 images
PImage myImage1;
PImage myImage2;
PImage myImage3;

// The Setup Function that initializes everything
void setup(){
// We setup the window with double dimensions than our images
// in order to fit them 3 inside the canvas
size(640, 480);
// We load the 2 original images into their PImage objects
myImage1 = loadImage("image1_to_test_with.jpg");
myImage2 = loadImage("image2_to_test_with.jpg");
// The third image is generated with the same dimensions than the 2nd
// that, by the way, has the same dimensions than the 1st
myImage3 = createImage(myImage2.width, myImage2.height, RGB);
}

Código 1. Variables, objetos e inicialización de nuestro programa con carga de imágenes incluida.

Empezamos creándonos tres objetos Pimage de Processing que serán los encargados de almacenar las imágenes en memoria, previa carga de éstas desde disco duro. Dentro de nuestra función de setup()creamos una ventana de 640 por 480 píxeles para poderlas mostrar todas a la vez. A continuación cargamos las dos imágenes a restar, es decir los ficheros image1_to_test_with.jpg e image2_to_test_with.jpg que previamente habremos introducido en un subdirectorio de nombre “data” dentro del directorio de nuestro sketch Processing. Finalmente tenemos las imágenes cargadas dentro de nuestros objetos myImage. El tercer objeto no lo creamos cargando una imagen sino reservando espacio para acomodar la que surgirá de la resta entre las dos anteriores. Se trata de una imagen RGB con la misma anchura y altura que cualquiera de las dos originales (obtenemos esos datos de la segunda imagen, por ejemplo).

El acceso a cada uno de los píxeles de las imágenes así como la aplicación de la resta en sí se observa dentro de la función draw(), en el código 2.

// The infinite loop
void draw(){
// To determine where each pixel is inside the image's array
int pixelLocation;
// The new color that we are calculating from the substraction
color newColor;
// Accessing pixels in the window implies loading them
loadPixels();
// We also access our test image's pixels
myImage1.loadPixels();
myImage2.loadPixels();
// We loop through all the pixels (#pixels = width x height)
// We use myImage1 or myImage2 interchangeably because of being equal
for (int x = 0; x < myImage1.width; x++) {
for (int y = 0; y < myImage1.height; y++ ) {
// Pixels are located sequentially inside a very long array
pixelLocation = x + y*myImage1.width;
// The R,G,B values from our 2 images
float r1 = red(myImage1.pixels[pixelLocation]);
float g1 = green(myImage1.pixels[pixelLocation]);
float b1 = blue(myImage1.pixels[pixelLocation]);
float r2 = red(myImage2.pixels[pixelLocation]);
float g2 = green(myImage2.pixels[pixelLocation]);
float b2 = blue(myImage2.pixels[pixelLocation]);
// We substract pixel by pixel (RGB values): 3rd image = 2nd - 1st
float r3 = r2 - r1;
float g3 = g2 - g1;
float b3 = b2 - b1;
// We need our components within the 0-255 window
r3 = constrain(r3,0,255);
g3 = constrain(g3,0,255);
b3 = constrain(b3,0,255);
// We create a new color with our 3 new components
newColor = color(r3,g3,b3);
// We load the new color into the 3rd image
myImage3.pixels[pixelLocation] = newColor;
}
}
// Everything needs to be updated with the new values
updatePixels();
// We are drawing some yellow text to show captions onto the 3 images
fill(255,255,0);
// First image and its text
image(myImage1,0,0);
String s = "Original First Image";
text(s, 0, 0, 70, 80); // Text wraps within text box
// Second image and its text
image(myImage2,320,0);
s = "Original Second Image";
text(s, 320, 0, 70, 80); // Text wraps within text box
// Third image and its text
image(myImage3,160,240);
s = "A third image generated by substracting them both!";
text(s, 160, 240, 70, 100); // Text wraps within text box
}

Código 2. Aplicación de la resta entre imágenes.

Cuando Processing carga una imagen dentro de un objeto PImage no lo almacena bidimensionalmente, es decir teniendo en cuenta el ancho por el alto, sino que lo hace unidimensionalmente. Se trata en definitiva de un array muy largo donde cada posición almacena el color de un píxel. Lee las sucesivas filas de la imagen y las va concatenando, tal y como podéis observar en el apartado “Pixels, pixels, and more pixels” de este tutorial. Eso implica que para acceder a un píxel en concreto de nuestra imagen, tenemos que calcular su localización dentro del objeto PImage. Para un píxel que tenga coordenadas XY en la imagen, su ubicación dentro del objeto se calculará como X + Y x Ancho de la Imagen. Y ese valor lo almacenaremos en la variable entera pixelLocation que definimos a tal efecto.

Definimos una variable newColor para guardar el nuevo color que obtendremos de restar los colores de nuestros pares de píxeles y después llamamos a la función de acceso a los píxeles de las imágenes (loadPixels()). A continuación accedemos en concreto a los píxeles de cada una de nuestras dos imágenes de partida.

Hay que recorrer todos los píxeles de la imagen y hay tantos como la resolución de ésta (ancho x alto). Lo haremos con dos estructuras de control de tipo bucle FOR, una dentro de otra, de manera que gracias a la primera iteraremos por columnas (width) y gracias a la segunda por filas (height). Dentro del bucle ya podemos suponer que nos encontramos apuntando a un píxel concreto con coordenadas XY. Calculamos su ubicación dentro del objeto (pixelLocation) y obtenemos los valores de sus tres componentes de color RGB (funciones red, green y blue) para cada imagen. Las asociamos a seis variables de nueva creación (r1, g1 y b1 / r2, g2 y b2). Nótese que éstas son de tipo real y por tanto con decimales (float) dado que las componentes de color se retornan en ese formato.

Llegados a este punto procedemos a obtener el color de la tercera imagen a partir de la resta de colores entre las dos originales. Éste, para cada píxel, se almacena en las variables r3, g3 y b3. Además limitamos las componentes de color que acabamos de calcular a un valor máximo de 255 (si en el cálculo hemos obtenido un número por encima quedará automáticamente recortado) para después crear un nuevo color (newColor) a partir de las variables r3, g3 y b3 que introduciremos en el objeto Pimage de la tercera imagen. Además actualizaremos los píxeles de la imagen a continuación.

Por último tenemos que mostrar las tres imágenes por pantalla y asociarles un texto explicativo en color amarillo (fill(255,255,0)). La primera imagen se carga en la esquina superior izquierda de la ventana (image(myImage1,0,0)) mientras que la segunda se muestra desplazada horizontalmente a partir de la mitad de la ventana (image(myImage2,320,0)). La tercera se mostrará debajo de ambas, centrada (image(myImage3,160,240)). En los tres casos modificaremos el texto a introducir dentro de la variable de tipo string s para que se adapte a cada situación. Todo gracias a la función text() de Processing que recibe como parámetros la cadena de texto a mostrar por pantalla así como las dimensiones de la caja de texto que la contiene.

El programa completo podéis observarlo en el código 4 y os invito a que lo modifiquéis a conveniencia para obtener resultados de todo tipo.

// Let's load 2 images and generate
// a third one by substracting them both
// in a per-pixel basis

// We create 3 PImage objects for our 3 images
PImage myImage1;
PImage myImage2;
PImage myImage3;

// The Setup Function that initializes everything
void setup(){
// We setup the window with double dimensions than our images
// in order to fit them 3 inside the canvas
size(640, 480);
// We load the 2 original images into their PImage objects
myImage1 = loadImage("image1_to_test_with.jpg");
myImage2 = loadImage("image2_to_test_with.jpg");
// The third image is generated with the same dimensions than the 2nd
// that, by the way, has the same dimensions than the 1st
myImage3 = createImage(myImage2.width, myImage2.height, RGB);
}

// The infinite loop
void draw(){
// To determine where each pixel is inside the image's array
int pixelLocation;
// The new color that we are calculating from the substraction
color newColor;
// Accessing pixels in the window implies loading them
loadPixels();
// We also access our test image's pixels
myImage1.loadPixels();
myImage2.loadPixels();
// We loop through all the pixels (#pixels = width x height)
// We use myImage1 or myImage2 interchangeably because of being equal
for (int x = 0; x < myImage1.width; x++) {
for (int y = 0; y < myImage1.height; y++ ) {
// Pixels are located sequentially inside a very long array
pixelLocation = x + y*myImage1.width;
// The R,G,B values from our 2 images
float r1 = red(myImage1.pixels[pixelLocation]);
float g1 = green(myImage1.pixels[pixelLocation]);
float b1 = blue(myImage1.pixels[pixelLocation]);
float r2 = red(myImage2.pixels[pixelLocation]);
float g2 = green(myImage2.pixels[pixelLocation]);
float b2 = blue(myImage2.pixels[pixelLocation]);
// We substract pixel by pixel (RGB values): 3rd image = 2nd - 1st
float r3 = r2 - r1;
float g3 = g2 - g1;
float b3 = b2 - b1;
// We need our components within the 0-255 window
r3 = constrain(r3,0,255);
g3 = constrain(g3,0,255);
b3 = constrain(b3,0,255);
// We create a new color with our 3 new components
newColor = color(r3,g3,b3);
// We load the new color into the 3rd image
myImage3.pixels[pixelLocation] = newColor;
}
}
// Everything needs to be updated with the new values
updatePixels();
// We are drawing some yellow text to show captions onto the 3 images
fill(255,255,0);
// First image and its text
image(myImage1,0,0);
String s = "Original First Image";
text(s, 0, 0, 70, 80); // Text wraps within text box
// Second image and its text
image(myImage2,320,0);
s = "Original Second Image";
text(s, 320, 0, 70, 80); // Text wraps within text box
// Third image and its text
image(myImage3,160,240);
s = "A third image generated by substracting them both!";
text(s, 160, 240, 70, 100); // Text wraps within text box
}

Código 4. Programa completo de resta de imágenes.

Deja un comentario