Processing/JS

Procesar imágenes

Processing permite acceder directamente a  los pixels en una imagen.

pixels[], loadPixels(), updatePixels(), createImage()

En Processing, cada imagen es almacenada en un array de colores.

Si una imagen es de 100 por 100 pixels, Ell array tiene 10,000 elementos. La primera posición en el array es el pixel de la esquina superior izquierda. Y la última posición del array es el pixel de la esquina inferior derecha. Para verlo más claro imaginemos una imagen de 10 * 6 pixels:

En processing la imagen se lee como un array unidimensional:

Pixels

El array pixels[] almacena los valores de color de cada pixel de la ventana de salida. La función loadPixels() carga los datos de los pixeles antes de utilizar el array pixels[]. Después de leer y cambiar los pixels debemos modificarlos usando la función updatePixels(). Como beginShape() y endShape(), loadPixels() y updatePixels() siempre van juntas.

En el siguiente ejemplo cambia el color de los pixels en la ventana cambiando un pixel en cada frame. Cuando el valor de los segundos con seconds() pasa de 59 a 0. El programa empieza de nuevo con el array.

size(200, 200);
int halfImage = width*height/2;
PImage myImage = loadImage("vg.jpg");
image(myImage, 0, 0);

loadPixels();
for (int i = 0; i < halfImage; i++) {
pixels[i+halfImage] = pixels[i];
}
updatePixels();

Cuando se necesita manipular muchos pixels a la vez, el array pixels[] es mucho más rápidoque usar get() y set(), en este ejemplo cogemos el color del pixel para dibujar un rectángulo.

PImage arch;
void setup() {
size(200, 200);
noStroke();
arch = loadImage("vg.jpg");
}
void draw() {
background(arch);
// la limitación debe ser menor que la ventana
int mx = constrain(mouseX, 0, 199);
int my = constrain(mouseY, 0, 199);
loadPixels();
color c = pixels[my*width + mx];
fill(c);
rect(10, 10, 60, 60);
}

En estos ejemplos usamos la estructura for con el array pixels[] para ver su potencial.

size(200, 200);
PImage arch = loadImage("vg.jpg");
int count = arch.width * arch.height;
arch.loadPixels();
loadPixels();
for (int i = 0; i < count; i += 2) {
pixels[i] = arch.pixels[i];
}
updatePixels();

size(200, 200);
PImage arch = loadImage("vg.jpg");
int count = arch.width * arch.height;
arch.loadPixels();
loadPixels();
for (int i = 0; i < count; i++) {
pixels[i] = arch.pixels[count - i - 1];
}
updatePixels();

size(200, 200);
PImage arch = loadImage("vg.jpg");
int count = arch.width * arch.height;
arch.loadPixels();
loadPixels();
for (int i = 0; i < count; i++) {
pixels[i] = arch.pixels[i/2];
}
updatePixels();

Componentes del pixel

Las funciones red(), green(), y blue() se emplean para leer los componentes individuales de color. Se modifican y regresan al array pixels[] modificando la imagen. Por ejemplo si multiplicamos por dos su valor la imagen será más clara y si los dividimos por dos más oscura.

Usando una estructura for es más fácil, por ser el array pixels[] un array unidimensional. En el ejemplo siguiente se muestra como podemos invertir el color de una imagen.

size(200, 200);
PImage arch = loadImage("vg.jpg");
background(arch);
loadPixels();
for (int i = 0; i < width*height; i++) {
color p = pixels[i]; // Guardamos el color del pixel
float r = 255 - red(p); // Modificamos el valor del rojo
float g = 255 - green(p); // Modificamos el valor del verde
float b = 255 - blue(p); // Modificamos el valor del azul
pixels[i] = color(r, g, b); // Asignamos el valor modificado
}
updatePixels();

En el siguiente ejemplo convertimos la imagen en escala de grises y usamos la posición del ratón en X para aclarar u oscurecer la imagen.

PImage arch;
void setup() {
size(200, 200);
arch = loadImage("vg.jpg");
}
void draw() {
background(arch);
loadPixels();
for (int i = 0; i < width*height; i++) {
color p = pixels[i]; // Guardamos el color del pixel
float r = red(p); // Modificamos el valor del rojo
float g = green(p); // Modificamos el valor del verde
float b = blue(p); // Modificamos el valor del azul
float bw = (r + g + b) / 3.0;
bw = constrain(bw + mouseX, 0, 255);
pixels[i] = color(bw); // Asignamos el valor modificado por mouseX
}
updatePixels();
line(mouseX, 0, mouseX, height);
}

Convolución

Otra manera de modificar una imagen es modificar los pixels en relación a los pixels vecinos.

una convolución es un operador matemático que transforma dos funciones f y g en una tercera función que en cierto sentido representa la magnitud en la que se superponen f y una versión trasladada e invertida de g.

En la siguiente imagen vemos una imagen de 6x6 pixels, el resultado calculando la media es un nuevo valor.

El valor resultante es la media de los nueve valores, 129.426, que convertido a un número entero es 129.

Con este sistema encontramos problema en los laterales de la imagen, dónde los pixels que no existen deben ser ignorados.

En el siguiente ejemplo demuestra como usar una caja de 3x3 como matriz para transformer una imagen, modificando los valores el resultado obtenido será diferente.

La function createImage() crea una imagen vacia. La function require tres parámetros que asignan el ancho, el alto y el formato de la imagen: RGB o ARGB (con alpha).

size(200, 200);
float[][] kernel = { { -1, 0, 1 },
{ -2, 0, 2 },
{ -1, 0, 1 } };
PImage img = loadImage("vg.jpg");
img.loadPixels();
// Crea una imagen del mismo tamaño que la orioginal
PImage edgeImg = createImage(img.width, img.height, RGB);
for (int y = 1; y < img.height-1; y++) {
for (int x = 1; x < img.width-1; x++) {
float sum = 0;
for (int ky = -1; ky <= 1; ky++) {
for (int kx = -1; kx <= 1; kx++) {
// Calcula el pixel adyacente
int pos = (y + ky)*width + (x + kx);
// Image en escala de grises con los valores iguales de rgb
float val = red(img.pixels[pos]);
sum += kernel[ky+1][kx+1] * val;
}
}
edgeImg.pixels[y*img.width + x] = color(sum);
}
}
edgeImg.updatePixels();
image(edgeImg, 0, 0); // Dibuja la nueva imagen

La imagen como datos

Una imagen puede ser una secuencia de números que define los colores, pero que puede ser vista de otra manera. En el siguiente ejemplo genera movimiento al definir el radio de un círculo con el valor del rojo de la imagen o la longitud de las líneas.

// Convierte el valor de los pixeles en el diametro de un círculo
PImage arch;
int index;
void setup() {
size(200, 200);
smooth();
fill(0);
arch = loadImage("vg.jpg");
arch.loadPixels();
}
void draw() {
background(204);
color c = arch.pixels[index]; // Get a pixel
float r = red(c) / 3.0; // Get the red value
ellipse(width/2, height/2, r, r);
index++;
if (index == width*height) {
index = 0; // Vuelve al inicio, el primer pixel
}
}

En este ejemplo convierte el valor rojo de los pixel del la línea de la imagen

PImage arch;
void setup() {
size(200, 200);
arch = loadImage("vg.jpg");
arch.loadPixels();
}
void draw() {
background(204);
int my = constrain(mouseY, 0, 199);
for (int i = 0; i < arch.height; i++) {
color c = arch.pixels[my*width + i]; // coge el valor del color del pixel
float r = red(c); // coge el valor rojo
line(i, 0, i, height/2 + r/6);
}
}

PImage arch;
void setup() {
size(200, 200);
smooth();
arch = loadImage("vg.jpg");
arch.loadPixels();
image(arch, 0, 0);
}
void draw() {
//background(204);
image(arch, 0, 0);
int mx = constrain(mouseX, 0, arch.width-1);
int offset = mx * arch.width;
beginShape(LINES);
for (int i = 0; i < arch.width; i += 2) {
float r1 = blue(arch.pixels[offset + i]);
float r2 = blue(arch.pixels[offset + i + 1]);
float vx = map(r1, 0, 255, 0, height);
float vy = map(r2, 0, 255, 0, height);
vertex(vx, vy);
}
endShape();
}