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).
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:
loadPixels(); imagen.
Después de modificarlos, actualizamos la imagen de la siguiente forma:
updatePixels(); imagen.
Para leer y escribir pixeles usamos la notación de arreglos:
// almacena el color del primer pixel:
pixels[0];
color pixel = imagen.
// colorea de blanco al pixel con índice 200
pixels[200] = color(255); imagen.
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.
loadPixels();
imagen.for( int i=0; i<imagen.pixels.length; i++){
// lee el color del pixel
pixels[i];
color pixel = imagen.
// escribe el color del pixel
pixels[i] = pixel;
imagen.
}updatePixels(); imagen.
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;
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
loadPixels();
imagen.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
pixels[i];
color pixel = imagen.
// escribe el color del pixel
pixels[i] = pixel;
imagen.
}updatePixels(); imagen.
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);
loadImage("trajinera.jpg");
imagen = resize(640, 480);
imagen.
// recorre pixeles:
loadPixels();
imagen.for( int i=0; i<imagen.pixels.length; i++){
// lee el color del pixel
pixels[i];
color pixel = imagen.
// escribe el color del pixel
pixels[i] = pixel;
imagen.
}updatePixels();
imagen.
}
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.
alpha( c )
red( c )
green( c )
blue( c )
hue( c )
saturation( c )
brightness( c )
Por ejemplo, las siguientes líneas obtener el valor de brillo del primer pixel en el arreglo:
pixeles[ 0 ];
color pixel = imagen.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:
loadPixels();
imagen.for( int i=0; i<imagen.pixels.length; i++){
// lee el color del pixel
pixels[i];
color pixel = imagen.
// obtén componentes RGB
float rojo = red( pixel );
float verde = green( pixel );
float azul = blue( pixel );
// asigna un nuevo color
pixels[i] = color( 0, verde, azul);
imagen.
}updatePixels(); imagen.
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);
loadPixels();
imagen.for( int i=0; i<imagen.pixels.length; i++){
// lee el color del pixel
pixels[i];
color pixel = imagen.
// obtén componentes HSB
float hue = hue( pixel );
float sat = saturation( pixel );
float bri = brightness( pixel );
// asigna un nuevo color con brillo al 80%
pixels[i] = color( hue, sat, bri*0.8);
imagen.
}updatePixels(); imagen.
5.1.3 Más posibilidades
- ¿Qué pasa si intercambias la posición de los canales de color?
- ¿O si usas solo uno de los canales como nuevo color/tono del pixel?
- ¿Y si sumas o restas alguna cantidad a los canales? ¿Y si esa cantidad es aleatoria?
- ¿Cómo podrías invertir los colores a través de arimética?
- ¿Y si usas
map()
para redimensionar el rango de alguno de los canales?
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.
loadPixels();
imagen.for( int i=0; i<imagen.pixels.length; i++){
// lee el color del pixel
pixels[i];
color pixel = imagen.
// obtén componente del brillo
float bri = brightness( pixel );
if ( bri > 50 ){ // si es brillo es mayor a 50%
// asigna el color original
pixels[i] = pixel;
imagen.else {
} pixels[i] = color(0);
imagen.
}
}updatePixels(); imagen.
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.
loadPixels();
imagen.for( int i=0; i<imagen.pixels.length; i++){
// lee el color del pixel
pixels[i];
color pixel = imagen.
// 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
pixels[i] = color( 0, 0 );
foto.else {
} // deja el pixel como estaba:
pixels[i] = pixel;
foto.
}
}updatePixels(); imagen.
5.2.3 Más posibilidades
- ¿Y si dejas igual a los pixeles en cierto rango de colores, y modificas a los que no?
- ¿Y si a cada canal de color le aplicas un umbral para asignalo en uno u otro valor?
- ¿Y si tienes más de un condicional para asignar diferentes posibles niveles a cada canal?
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.
loadPixels();
imagen.for( int i=0; i<imagen.pixels.length; i++){
// lee el color del pixel
pixels[i];
color pixel = imagen.
// 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
pixels[nuevoindice];
color pixel2 = imagen.
// asigna los valores intercambiados:
pixels[i] = pixel2;
imagen.pixels[nuevoindice] = pixel;
imagen.
}updatePixels(); imagen.
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
.