En este capítulo nos prepararemos para la manipulación y el procesamiento de imágenes en NodeJS.
Comprenderemos como está compuesta una imagen y por qué la trabajaremos en su bajo nivel.
Más teoría, éste será un capítulo largo.
Lo básico
Antes que nada, es importante comprender un par de cositas, es solo un pequeño anclaje para lo que vendrá luego.
Al inicio de la serie dijimos que para que nuestro programa pueda “ver”, tomaríamos capturas de pantalla automáticas, y esto solo significa una cosa: Nuestra captura es una imagen de tipo bitmap, está compuesta por pixeles.
Siendo una imagen una sucesión de pixeles, sabemos que la unidad más pequeña es justamente el pixel.
Generalmente, cada uno de éstos está compuesto por cuatro valores:
RGBA (Red, Green, Blue y Alpha respectivamente).
Cada valor, es un canal de color.
Estos cuatro valores en conjunto forman los colores que vemos en nuestra pantalla, que percibimos con nuestros ojos.
Los pixeles que contienen RGBA son codificados en 32 bits, destinando 8 bits a cada canal (8*4=32), lo que se traduce a 256 variaciones de color para cada uno, excepto el Alpha que no representa un color sino la transparencia.
Sabemos que 8 bits es igual a 1 byte, y que cada byte es capaz de almacenar valores enteros de 0 a 256.
¿Ya te confundiste? Abramos Paint.
Como vemos en la imagen, el color Rojo es representado por tres canales (Rojo, Verde y Azul) con valores que van desde el 0 al 255 (256 variaciones), donde 0 es “nada de este color” y 255 es “mucho de este color”.
¿Qué es lo que nos interesa de todo esto?
Es importante saber que cada color de cada pixel se representa con 1 byte.
Y a su vez, cada byte puede ser representado en sistema hexadecimal, que es eso lo que nuestro buffer nos regresa. (En el siguiente capitulo veremos qué es un buffer).
Si cada byte trata valores que van de 0 a 255, también puede tratar sus equivalentes en hexadecimal, donde 0 es 00 y 255 es FF.
Nuestro color Rojo antes era representado como (255, 0, 0), y en hexadecimal es representado como FF0000
¡Ahora sí!
Ya hemos comprendido que la unidad más pequeña que podemos manipular de una imagen es el byte.
Pongamos un ejemplo:
No es necesario aclarar que una imagen tiene alto y ancho… Ups, ya lo hice.
-Tenemos una imagen de 3x1, los que nos da un total de 3 pixeles
(w*h = pixels
|3*1 = 3
)
-El primer pixel es rojo, el segundo pixel verde y el tercer pixel es azul.
-La imagen no tiene transparencia.
¿Cómo crees que será la representación de nuestra imagen en un vector/array de bytes?
//Tenemos que cada pixel tiene cuatro canales, R G B A
//El primer pixel es rojo, el segundo verde y el tercero azul, sin transparencia. Por lo tanto:PIXEL 1 = 255, 0, 0, 255
//R = 255
//G = 0
//B = 0
//A = 255PIXEL 2 = 0, 255, 0, 255
//R = 0
//G = 255
//B = 0
//A = 255PIXEL 3 = 0, 0, 255, 255
//R = 0
//G = 0
//B = 255
//A = 255//en enteros:
imagenINT = [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]
//O lo que sería lo mismo en hexadecimales:
imagenHEX = ["FF", 00, 00, "FF", 00, "FF", 00, "FF", 00, 00, "FF", "FF"]//Este array se obtiene de forma automática
Sencillo, ¿verdad?
Ahora, si utilizamos una imagen real, el largo de nuestro array de bytes será descomunal. Afortunadamente es posible calcular de antemano qué tan largo será.(w*h)*4 == imagen.length
Eh, dijimos que utilizaríamos matemática básica.
Perfecto, ahora tenemos nuestro array, pero este arreglo está desordenado y es caótico, de hecho, al ser capturas de pantalla damos por hecho que no habrá transparencia.
El Alpha es un dato que no nos interesa.
¿y si queremos recorrer este arreglo usando coordenadas?
Necesitaremos las medidas de la imagen, ¿no?
Usemos hexadecimal
Hasta ahora tenemos cada pixel representado en 4 posiciones subsecuentes de un arreglo.
Juntemos esos cuatro datos en uno.
Desafortunadamente no podemos representar el color Rojo como el numero 25500 (en realidad sí, no es lo correcto), pero sí podemos representarlo como string “FF0000”.
Transformaremos este array de bytes en uno de “pixeles”.
//Recordando el arreglo
imagenHEX = ["FF", 00, 00, "FF", 00, "FF", 00, "FF", 00, 00, "FF", "FF"]//podemos decir que
Pixel 1 = `${imagenHex[0]}${imagenHex[1]}${imagenHex[2]}`
//o
Pixel 1 = imagenHex[0] + imagenHex[1] + imagenHex[2]
// Pixel 1 >>> "FF0000"//...metemos cada pixel en el array
PixelsHex = ["FF0000","00FF00","0000FF"]
//La transparencia (el canal Alpha) no nos interesa, vamos a ignorarlo.
El algoritmo lo veremos en un próximo capitulo, pero no es más que un ciclo for.
Ya tenemos nuestro arreglo de pixeles.
Sin embargo sigue siendo una lista de pixeles sin orden.
Para recorrer adecuadamente este arreglo de colores necesitamos el alto y el ancho de la imagen para no usar coordenadas incorrectas.
De todas maneras, para el siguiente cálculo solo es necesario el ancho de la imagen.
Sabemos que las imágenes comienzan sus coordenadas en (0;0) en la parte superior izquierda, y se lee de izquierda a derecha y de arriba hacia abajo. Pero nuestro arreglo ordena toda la imagen en una única “tira”.
Utilizaremos una sencilla función a la que le pasaremos tres parámetros, las coordenadas que queremos explorar de la imagen y el ancho de la misma
(x, y, w){}
El valor que nos retornará la función será el resultado del ya mencionado cálculo:x + (y * w) = i
Donde i
es el equivalente en el array de las coordenadas (X;Y)
Por ejemplo, si queremos saber qué hay en el segundo pixel (1; 0) de nuestra supuesta imagen, realizaremos el cálculo de la siguiente manera:
PixelsHex = ["FF0000","00FF00","0000FF"](1, 0)
x + (y * w) = i
1 + (0 * 3) = i
1 + 0 = i
1 = i
PixelsHex[i]
>>> "00FF00"//Si nuestra imagen es de 500x600 y queremos saber qué hay en (169;242), hacemos lo mismo.(169;242)
x + (y * w) = i
169 + (242 * 500)= i
169 + 121000 = i
121169 = i
bigImage[i]
>>> //nos regresaría lo que hay en la posición 121169, justo en (169;242)
Cualquier duda, pueden dejarla en los comentarios.
¡Felicitaciones! Ya aprendiste cómo está compuesta una imagen y cómo podemos explorarla.
Ahora que somos unos casi expertos en la manipulación de imágenes en su más bajo nivel, podemos pasar al siguiente tema.
El buffer…
Para no alargar demasiado el capitulo, dejaremos este tema para el siguiente.
Aprende sobre el buffer de NodeJS en el siguiente capitulo haciendo click aquí