jardínBit

Arreglo (array) de pixeles

Processing nos permite acceder y modificar a los pixeles de una imagen a través de una estructura sistematizada llamada arreglo (array).

[Arreglos: Fundamentos]

1 Fundamentos

1.1 Atributo .pixels

Dada una variable de tipo PImage llamada imagen, su arreglo de pixeles lo obtenemos via imagen.pixels.

1.2 Tipo de dato

El arreglo de pixeles consiste en un arreglo de tipo color[], pues cada pixel lo podemos entender como una unidad fundamental de la imagen que solo tiene/es un color.

1.3 Dimensiones

El arreglo de pixeles tendrá imagen.width*imagen.height elementos, o bien imagen.pixels.length.

El primer pixel (imagen.pixels[0]) es el de la esquina superior izquierda, el segundo pixel es el que le sigue a la derecha, y así sucesivamente: de izquierda a derecha y de arriba a abajo.

El último pixel es el de la esquina inferior derecha.

1.4 Lectura y escritura

Antes de realizar operaciones sobre el arreglo de pixeles, necesitamos “cargarlos” de la siguiente forma:

imagen.loadPixels();

Después de modificarlos, actualizamos la imagen de la siguiente forma:

imagen.updatePixels();

Para leer y escribir pixeles usamos la notación de arreglos:

// almacena el color del primer pixel:
color pixel = imagen.pixels[0];

// colorea de blanco al pixel con índice 200
imagen.pixels[200] = color(255);

2 Ciclos

La principal ventaja de tener acceso a los pixeles a través de un arreglo, es que podemos utilizar un ciclo for (o while) para iterar sobre cada uno de ellos y entonces leerlos y modificarlos.

2.1 Una dimensión: índice

Esta es una plantilla de ciclo for que usa a una variable int i para iterar sobre cada uno de los pixeles.

imagen.loadPixels();
for( int i=0; i<imagen.pixels.length; i++){
  // lee el color del pixel
  color pixel = imagen.pixels[i];

  // escribe el color del pixel
  imagen.pixels[i] = pixel;
}
imagen.updatePixels();

2.1.1 Coordenadas a partir de índice

En ocasiones necesitaremos obtener las coordenadas x, y que corresponden al índice i.

Partiendo de que la imagen tiene imagen.height filas de imagen.width pixeles cada uno, podemos calcular el número de fila (y) con una división, y el número de pixel en esa fila (x) con el operador módulo, que es el residuo de la división:

int x = i % imagen.width;
int y = i / imagen.width;

[Operador módulo]

2.2 Dos dimensiones

Esta plantilla usa dos ciclos anidados, uno con la variable y para recorrer las filas de pixeles, y otro con la variable x para recorrer los pixeles de cada una.

2.2.1 Índice a partir de coordenadas

En este caso necesitamos calcular el valor del índice respecto a esas variables x,y

int i = x + y*imagen.width;

2.2.2 Plantilla

imagen.loadPixels();
for( int y=0; y<imagen.height; y++){
  for( int x=0; x<imagen.width; x++){
    int i = x + y*imagen.width;

    // lee el color del pixel
    color pixel = imagen.pixels[i];

    // escribe el color del pixel
    imagen.pixels[i] = pixel;
}
imagen.updatePixels();

Nota: también podríamos utilizar imagen.get(x,y) para obtener el color del pixel, pero es una operación un poco más lenta que en ocasiones se va a notar

3 En el ciclo de animación

Si solo queremos realizar un proceso una vez, conviene hacerlo dentro de setup(). Si ese proceso requiere cambiar en el tiempo, habrá que hacerlo dentro de draw().

La siguiente es una plantilla que carga una imagen y recorre sus pixeles sin cambiarlos. Más adelante se mostrarán posibilidades con las cuales reemplazar el ciclo for mostrado.

PImage imagen;

void setup(){
  size(640, 480);

  imagen = loadImage("trajinera.jpg");
  imagen.resize(640, 480);

  // recorre pixeles:
  imagen.loadPixels();
  for( int i=0; i<imagen.pixels.length; i++){
    // lee el color del pixel
    color pixel = imagen.pixels[i];
  
    // escribe el color del pixel
    imagen.pixels[i] = pixel;
  }
  imagen.updatePixels();
}

void draw(){
  image(imagen, 0, 0);
}

4 Información de color

Las siguientes funciones nos permiten obtener información específica sobre el color que leímos, como un número de tipo float. Sus nombres indican qué valor calculan a partir del color que utilicemos como argumento.

Por ejemplo, las siguientes líneas obtener el valor de brillo del primer pixel en el arreglo:

color pixel = imagen.pixeles[ 0 ];
float brillo = brightness( pixel );

5 Ejemplos de procesamientos por pixel

Las siguientes son ejemplos de procesamientos que buscan ser modificados y explorados.

Todos estos actúan sobre un pixel a la vez, y lo modifican respecto a sus propias características de color.

5.1 Cambio lineal de niveles

5.1.1 Eliminación de canal RGB

Este ejemplo lee los tres canales RGB y crea un nuevo color sin uno de ellos:

imagen.loadPixels();
for( int i=0; i<imagen.pixels.length; i++){
  // lee el color del pixel
  color pixel = imagen.pixels[i];

  // obtén componentes RGB
  float rojo = red( pixel );
  float verde = green( pixel );
  float azul = blue( pixel );

  // asigna un nuevo color
  imagen.pixels[i] = color( 0, verde, azul);
}
imagen.updatePixels();

5.1.2 Cambio de brillo

Este ejemplo lee los tres canales HSB y escala (multiplica) uno de ellos.

Nota el uso de colorMode( ) para que el color creado funcione en HSB

colorMode(HSB, 360, 100, 100);
imagen.loadPixels();
for( int i=0; i<imagen.pixels.length; i++){
  // lee el color del pixel
  color pixel = imagen.pixels[i];

  // obtén componentes HSB
  float hue = hue( pixel );
  float sat = saturation( pixel );
  float bri = brightness( pixel );

  // asigna un nuevo color con brillo al 80%
  imagen.pixels[i] = color( hue, sat, bri*0.8);
}
imagen.updatePixels();

5.1.3 Más posibilidades

5.2 Umbral (Threshold)

Podemos utilizar expresiones condicionales para modificar los pixeles de manera no lineal.

5.2.1 Brillo

Este ejemplo usa el valor de brillo para decidir entre dos posibilidades: si el valor de brillo es mayor a cierta cantidad, deja el color como está, y si no lo reemplaza por negro.

imagen.loadPixels();
for( int i=0; i<imagen.pixels.length; i++){
  // lee el color del pixel
  color pixel = imagen.pixels[i];

  // obtén componente del brillo 
  float bri = brightness( pixel );

  if ( bri > 50 ){ // si es brillo es mayor a 50%
    // asigna el color original
    imagen.pixels[i] = pixel;
  } else {
    imagen.pixels[i] = color(0);
  }
}
imagen.updatePixels();

5.2.2 Chroma key

En este caso utilizamos un condicional más complejo para encontrar cierto rango de colores con valores determinados de hue, saturación y brillo. Los que cumplen la condición se vuelven transparentes, y los que no se dejan igual.

imagen.loadPixels();
for( int i=0; i<imagen.pixels.length; i++){
  // lee el color del pixel
  color pixel = imagen.pixels[i];

  // obtén componentes HSB
  float hue = hue( pixel );
  float sat = saturation( pixel );
  float bri = brightness( pixel );

  // encuentra rango de colores via HSB
  if( hue > 85 && hue < 140 && sat > 20 && bri > 50){
    // asigna tono con transparencia total
    foto.pixels[i] = color( 0, 0 );
  } else {
    // deja el pixel como estaba:
    foto.pixels[i] = pixel;
  }
}
imagen.updatePixels();

5.2.3 Más posibilidades

6 Ejemplos de procesamientos con pixeles aledaños

6.1 Intercambio de pixeles

Este ejemplo intercambia el color de dos pixeles que son aledaños en el arreglo.

imagen.loadPixels();
for( int i=0; i<imagen.pixels.length; i++){
  // lee el color del pixel
  color pixel = imagen.pixels[i];

  // calcula el índice de un pixel entre 0 y 10 lugares
  // más adelante en el arreglo
  int nuevoindice = (i + int(random(10)) ) % foto.pixels.length;

  // lee el color de ese pixel
  color pixel2 = imagen.pixels[nuevoindice];

  // asigna los valores intercambiados:
  imagen.pixels[i] = pixel2;
  imagen.pixels[nuevoindice] = pixel;

}
imagen.updatePixels();

Nota el uso del operador módulo al calcular el nuevo índice, para asegurarnos que el resultado siempre esté entre 0 y foto.pixels.length - 1.

[Operador módulo]