Una LUT o “Look Up Table” (ver apartado “Lookup tables in image processing” aquí) es un procedimiento por el cual podemos variar las características (pseudocolorear) de una imagen al retocar sus píxeles, uno a uno. Se trata de recorrerlos todos, obtener su valor y, precisamente en base a la LUT que diseñemos, retocarlo de manera que pase a ser diferente. Las LUTs se utilizan en todos los programas de edición y retoque fotográfico e incluso en algunas cámaras digitales para aplicar filtros a nuestras imágenes: pasarla de color a blanco y negro, pasarla a tono sepia, variar su brillo, etc. También se han empleado para “colorear” películas que originalmente se filmaron en blanco y negro o para aplicar tonalidades de colores distintas en mapas del tiempo y en función de la temperatura (rojo para el calor, verde para zonas neutras y azul para el frío).
En nuestro caso realizaremos un sencillo programa Processing que a) Cargará una imagen de disco duro y b) Aplicará una LUT sobre ella que además variará en función del movimiento de nuestro ratón. En la figura 1 podemos entenderlo mejor.
Puede observarse que la imagen de la izquierda es la original. A medida que nos desplazamos hacia la derecha, ésta se “quema” por momentos tendiendo cada vez más al blanco (partiremos de negro para el caso del ratón situado en el borde izquierdo de la imagen y ésta tenderá a blanco según se acerque al borde derecho). Por tanto, ¿qué hace nuestra LUT? Lo cierto es que aplica un factor de brillo que se multiplica por cada una de las 3 componentes RGB de los píxeles. Y este factor aumentará o disminuirá dependiendo de la posición del ratón.
Además incluiremos un curioso efecto que se aplicará alternativamente o no al hacer clic con el ratón. Podemos observarlo en la figura 2 y es claramente una segunda LUT que diseñaremos.
Empecemos por aprender cómo se carga una imagen en Processing, gracias al código 1.
// Our LookUP Table (LUT) example
// is built on the top of this official
// tutorial in http://processing.org/learning/pixels/
// We set the mode to 1 which means that
// initially we vary the contrast of the image with the mouse
// LUTmode = 2 will deliver a different behavior (pseudocoloring the image)
int LUTmode = 1;
// We create a PImage object
PImage myImage;
// The Setup Function that initializes everything
void setup(){
// We setup the window with the same dimensions than our test image
size(320, 240);
// We load the image into the PImage object
myImage = loadImage("image_to_test_with.jpg");
}
Código 1. Variables e inicialización de nuestro programa.
Empezamos creándonos una variable para controlar cual de las dos LUTs queremos aplicar (LUTmode). A continuación definimos un objeto Pimage de Processing que será el encargado de almacenar la imagen en memoria, previa carga de ésta desde disco duro. Dentro de nuestra función de setup()creamos una ventana de 320 por 240 píxeles, dado que ésta es la resolución de nuestra imagen originalmente. A continuación cargamos la imagen, el fichero image_to_test_with.jpg que previamente habremos introducido en un subdirectorio de nombre “data” dentro del directorio de nuestro sketch Processing. Finalmente tenemos la imagen cargada dentro de nuestro objeto myImage.
El acceso a cada uno de los píxeles de la imagen así como la aplicación de las LUTs lo podemos observar 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;
// More or less brightness for our image's pixels
float brightnessFactor;
// Depending on the LUTmode something different happens to the image
// The new color that we are calculating
color newColor;
// LUTmode == 1 implies varying brightness with the horizontal movement of the mouse
if (LUTmode == 1){
// Accessing pixels in the window implies loading them
loadPixels();
// We also access our test image's pixels
myImage.loadPixels();
// We loop through all the pixels (#pixels = width x height)
for (int x = 0; x < myImage.width; x++) {
for (int y = 0; y < myImage.height; y++ ) {
// Pixels are located sequentially inside a very long array
pixelLocation = x + y*myImage.width;
// The R,G,B values from our test image
float r = red(myImage.pixels[pixelLocation]);
float g = green(myImage.pixels[pixelLocation]);
float b = blue(myImage.pixels[pixelLocation]);
// As in the official tutorial, we change brightness here
// We use casting here, to convert the mouseX coords from int to float
brightnessFactor = ((float) mouseX / width) * 10.0;
r *= brightnessFactor;
g *= brightnessFactor;
b *= brightnessFactor;
// We need our components within the 0-255 window
r = constrain(r,0,255);
g = constrain(g,0,255);
b = constrain(b,0,255);
// We create a new color with our 3 new components
newColor = color(r,g,b);
pixels[pixelLocation] = newColor;
}
}
// Everything needs to be updated with the new values
updatePixels();
}
// LUTmode == 2 implies pseudocoloring the image...
// From green to blue depending on the mouse movement
else{
// From green (left) to blue (right) by the mouse movement
tint(0,width-mouseX,mouseX);
// We re-draw the test image with the new "tinting"
image(myImage, 0, 0);
}
}
Código 2. Aplicación de las LUTs a nuestra imagen.
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.
Dependiendo del valor de la variable LUTmode aplicaremos una LUT u otra. Si ésta vale 1 aplicaremos el factor de brillo. Si vale 2 retocaremos los canales de imagen eliminando el rojo y alternando entre verdes y azules. Todo según el desplazamiento del ratón de izquierda a derecha. Después de llamar a la función de acceso a los píxeles de la imagen (loadPixels()) tenemos ambos bucles.
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 asociarlas a tres variables de nueva creación (r, g y b). 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 y dependiendo del valor de LUTmode tendremos que aplicar una u otra LUT. Concretamente:
- Si LUTmode vale 1, multiplicamos a las tres componentes por el factor de brillo brightnessFactor. Éste se ha calculado teniendo en cuenta la posición del cursor del ratón de manera que debería ser menor si se encuentra cercano al borde izquierdo y mayor si lo está del derecho. Por eso “normalizamos” el valor de la coordenada X del ratón (mouseX) en relación a la anchura de ventana (width). En el caso de que nos encontremos en el borde izquierdo esta división será próxima a 0. En el borde derecho lo será a 1. Y por último aplicamos un factor de 10 de forma que nuestro brillo oscile entre 0 y 10. Esto último puede variarse a voluntad lógicamente.
- Si LUTmode vale 2, aplicamos una metodología distinta y “atacamos” a la imagen por entero sin necesidad de acceder a ella píxel a píxel. Empleamos la función tint() de Processing para aplicarle un filtro de color que, en nuestro caso, consiste en eliminar la componente roja (la hacemos igual a 0) y aplicar una cuantía de verde y de azul que depende de la coordenada X del ratón (mouseX). El verde “decrece” con el movimiento horizontal del ratón mientras que el azul “crece”.
En todo caso terminamos por limitar nuestras componentes de color 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 r, g y b que introduciremos de nuevo en nuestro objeto PImage. Además actualizaremos los píxeles de la imagen a continuación. Todo esto para el caso de LUTmode igual a 1. Si LUTmode vale 2 tendremos suficiente con utilizar la función image(myImage, 0, 0)para pintar el contenido de nuestro objeto myImage en el origen de nuestra ventana.
Por último y como observamos en el código 3, alternamos entre los modos 1 y 2 de nuestras LUTs gracias al evento/callback de clic de ratón. 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.
// We toggle between LUTmode 1 and 2 by clicking a mouse button
void mousePressed(){
if (LUTmode == 1) LUTmode = 2;
else LUTmode = 1;
}
Código 3. Alternando modos de LUT gracias a 1 clic de ratón.
// Our LookUP Table (LUT) example
// is built on the top of this official
// tutorial in http://processing.org/learning/pixels/
// We set the mode to 1 which means that
// initially we vary the contrast of the image with the mouse
// LUTmode = 2 will deliver a different behavior (pseudocoloring the image)
int LUTmode = 1;
// We create a PImage object
PImage myImage;
// The Setup Function that initializes everything
void setup(){
// We setup the window with the same dimensions than our test image
size(320, 240);
// We load the image into the PImage object
myImage = loadImage("image_to_test_with.jpg");
}
// The infinite loop
void draw(){
// To determine where each pixel is inside the image's array
int pixelLocation;
// More or less brightness for our image's pixels
float brightnessFactor;
// Depending on the LUTmode something different happens to the image
// The new color that we are calculating
color newColor;
// LUTmode == 1 implies varying brightness with the horizontal movement of the mouse
if (LUTmode == 1){
// Accessing pixels in the window implies loading them
loadPixels();
// We also access our test image's pixels
myImage.loadPixels();
// We loop through all the pixels (#pixels = width x height)
for (int x = 0; x < myImage.width; x++) {
for (int y = 0; y < myImage.height; y++ ) {
// Pixels are located sequentially inside a very long array
pixelLocation = x + y*myImage.width;
// The R,G,B values from our test image
float r = red(myImage.pixels[pixelLocation]);
float g = green(myImage.pixels[pixelLocation]);
float b = blue(myImage.pixels[pixelLocation]);
// As in the official tutorial, we change brightness here
// We use casting here, to convert the mouseX coords from int to float
brightnessFactor = ((float) mouseX / width) * 10.0;
r *= brightnessFactor;
g *= brightnessFactor;
b *= brightnessFactor;
// We need our components within the 0-255 window
r = constrain(r,0,255);
g = constrain(g,0,255);
b = constrain(b,0,255);
// We create a new color with our 3 new components
newColor = color(r,g,b);
pixels[pixelLocation] = newColor;
}
}
// Everything needs to be updated with the new values
updatePixels();
}
// LUTmode == 2 implies pseudocoloring the image...
// From green to blue depending on the mouse movement
else{
// From green (left) to blue (right) by the mouse movement
tint(0,width-mouseX,mouseX);
// We re-draw the test image with the new "tinting"
image(myImage, 0, 0);
}
}
// We toggle between LUTmode 1 and 2 by clicking a mouse button
void mousePressed(){
if (LUTmode == 1) LUTmode = 2;
else LUTmode = 1;
}
Código 4. Programa completo de aplicación de LUTs en nuestra imagen.