Tabla de contenidos
Cómo generar las imágenes
Para generar las imágenes, vamos a pensar en un buffer sencillo, lineal de tipo unsigned char, o uint8_t, para imágenes en blanco y negro… vamos a empezar con imágenes sencillas, ya meteremos color… y más cosas.
Utilizaremos un array unidimensional, en lugar de uno bidimensional porque nos va a hacer la vida más fácil a la hora de recorrer todos los pixels de la imagen y manipularlos. Así, en muchas ocasiones nos ahorraremos tener dos variables, una para indicar coordenada x (o columna) y otra para indicar coordenada y (o filas). Aunque el ahorro de memoria no es significativo: da igual tener un array bidimensional de 8×8 que uno unidimensional de 64, sólo que tendremos una variable menos en el bucle principal; y el tiempo de procesamiento tampoco: actualmente los compiladores son capaces de optimizar muchísimo el ejecutable y los procesadores son cada vez mejores, capaces de realizar los dos recorridos prácticamente en el mismo tiempo; hay ocasiones en las que esto nos beneficia, por ejemplo, cuando utilizamos un código complicado para generar una imagen, el código con el array unidimensional puede tardar menos (el compilador intenta utilizar registros de CPU para las variables que más usamos (los contadores del bucle, y los registros son limitados y puede que tengamos que escribir memoria->registro->memoria muchas veces, si son dos contadores, habrá más operaciones del estilo), también tendremos un bucle menos, un indentado menos en el código que siempre se agradece, y, bueno, muchas bibliotecas gráficas utilizan este formato, así que de esta forma nos será fácil, por ejemplo crear plugins de frei0r, por ejemplo.
Todo esto nos sirve para poder calcular la posición del array en función de las coordenadas y viceversa, porque en algunos casos suele ser útil (digamos que la fila es y, y la columna es x, el ancho de la imagen será el número de columnas por fila):
casilla = y * ancho + x
y = casilla / ancho
x = casilla % ancho
Vemos un pequeño ejemplo de todo esto en C, con un array bidimensional (al que podremos acceder como si fuera un array unidimensional, viendo que en memoria está almacenado así):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include <stdio.h> int main() { char imagen[640][480]; char *imagenp=(char*)imagen; int x,y, i, n; for (x=0; x<640; ++x) for (y=0; y<480; ++y) imagen[x][y] = (x+1)*(y+1); printf ("%p\n", imagen); printf ("%p\n", &imagen[0][0]); // Es la misma que la dirección de imagen printf ("%p\n", &imagen[5][2]); // La última, debe ser la dirección de imagen + 5 * 8 + 2 printf ("----------- RECORRIDO BIDIMENSIONAL -----------\n"); for (x=0; x<640; ++x) { for (y=0; y<8; ++y) { printf ("%02d (%02d,%02d)\t", imagen[x][y],x,y); } printf("\n"); } printf ("----------- RECORRIDO UNIDIMENSIONAL -----------\n"); for (i=0; i<307200; ++i) { printf ("%02d (%02d,%02d)\t", imagenp[i],i/8, i%8); if ((i+1)%8==0) printf("\n"); } return 0; } |
Cómo guardar las imágenes
Almacenar una imagen en disco es complicado, muy complicado, se utilizan complejos algoritmos y fórmulas matemáticas para realizar la compresión de la información, y esto puede terminar en varios miles de líneas de código. En el pasado, he escrito sobre varios formatos de imagen, como BMP, JPEG (con libjpeg) y más, en este último utilizamos MagickCore, que nos ayudará a pelearnos con los formatos gráficos de forma indolora, y es que, nos proporciona una API muy sencilla para interactuar con efectos gráficos, lectura y salvado de múltiples formatos de imagen.
Es la que vamos a utilizar aquí, para salvar archivos PNG, por detrás utilizará libpng, aunque con MagickCore, podremos utilizar cualquier formato gráfico de la misma manera, sin necesidad de cambiar mucho nuestro código, y eso es bueno para experimentar).
Por ejemplo, tendremos este sencillo código para salvar nuestras imágenes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #include <magick/MagickCore.h> #include <string.h> #include <stdio.h> #include <stdint.h> #define ANCHO 256 #define ALTO 256 void salva_imagen(const char* fichero, const char* tipoImagen, uint32_t ancho, uint32_t alto, unsigned char* pixels) { Image* im; ExceptionInfo* error; ImageInfo* imgInfo; error = AcquireExceptionInfo(); imgInfo=CloneImageInfo((ImageInfo *) NULL); im = ConstituteImage(ancho, alto, tipoImagen, CharPixel, pixels, error); strcpy(im->filename, fichero); WriteImage(imgInfo, im); } int main() { unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO); /* Aquí generamos nuestra imagen */ salva_imagen("test.png", "I", ANCHO, ALTO, pixels); } |
Imágenes en blanco y negro
En principio, vamos a crear algunos patrones (no pondré el código por completo, puesto que la función salva_imagen(), ANCHO y ALTO, ya los tenemos arriba, pondré sólo la función main() o alguna función extra que vea necesaria).
Un color gris
Empezamos desde el principio, en estos ejemplos, para una imagen gris, supongo que tenemos 256 niveles de gris y, por eso la variable donde almaceno los pixels es de tipo unsigned char (con rango desde 0 a 255). Por lo tanto si queremos un color intermedio (nivel 128, debemos rellenar nuestro array de pixels de este valor):
1 2 3 4 5 6 7 8 9 10 11 12 | int main() { unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO); unsigned i; for (i=0; i<ANCHO*ALTO; ++i) { pixels[i] = 128; } salva_imagen("plano.png", "I", ANCHO, ALTO, pixels); } |
Gradiente lineal de grises
1 2 3 4 5 6 7 8 9 10 11 12 | int main() { unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO); unsigned i; for (i=0; i<ANCHO*ALTO; ++i) { pixels[i] = i%256; } salva_imagen("gradiente.png", "I", ANCHO, ALTO, pixels); } |
1 2 3 4 5 6 7 8 9 10 11 12 | int main() { unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO); unsigned i; for (i=0; i<ANCHO*ALTO; ++i) { pixels[i] = (i%ANCHO)*255/ANCHO; } salva_imagen("gradiente2.png", "I", ANCHO, ALTO, pixels); } |
1 2 3 4 5 6 7 8 9 10 11 12 | int main() { unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO); unsigned i; for (i=0; i<ANCHO*ALTO; ++i) { pixels[i] = (i/ANCHO)*255/ALTO; } salva_imagen("gradiente3.png", "I", ANCHO, ALTO, pixels); } |
Y, por supuesto un gradiente circular:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | int main() { unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO); int i; double d; double max=sqrt(ANCHO*ANCHO+ALTO*ALTO)/2; int focox = ANCHO/2; int focoy = ALTO/2; double min=-100; for (i=0; i<ANCHO*ALTO; ++i) { d = sqrt(pow( focox-(i%ANCHO), 2) + pow( focoy-(i/ANCHO), 2)); if (d<min) pixels[i] = 0; else if (d<max) pixels[i] = (d-min)*255/(max-min); else pixels[i] = 255; } salva_imagen("gradiente4.png", "I", ANCHO, ALTO, pixels); } |
Donde podemos cambiar el valor de min y max para que haya más o menos negro o más o menos blanco en la imagen. Así como modificar focox y focoy en función de dónde queremos que esté el centro.
Cuadrícula
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | int main() { unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO); int i; int div_horiz = 4; int div_vert = 4; int cuadro_ancho = (ANCHO-1)/div_horiz; int cuadro_alto = (ALTO-1)/div_horiz; int grosor = 2; int c; for (i=0; i<ANCHO*ALTO; ++i) { if ( ( (c = (i/ANCHO)%cuadro_alto)>cuadro_alto-grosor) || (c<grosor)) pixels[i] = 255; else if ( ( (c= (i%ANCHO)%cuadro_ancho)>cuadro_ancho-grosor) || (c<grosor) ) pixels[i] = 255; else pixels[i] = 0; } salva_imagen("cuadricula.png", "I", ANCHO, ALTO, pixels); } |
Puntos aleatorios, o ruido
1 2 3 4 5 6 7 8 9 10 |
Fondo de estrellas
1 2 3 4 5 6 7 8 9 10 11 12 |
Estrellas de diferentes tamaños
En este caso puede haber transparencias, en este caso, se ha cogido siempre el pixel de mayor valor (ya hablaremos del blending un día de estos). Este será uno de los más simples que podamos hacer. En este ejemplo, podremos variar el tamaño de las estrellas, el número y el brillo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | void pixel_brillante(unsigned char*pixels, int ancho, int alto, int x, int y, unsigned char color, int tam) { tam=tam*2-1; /* Los tamaños son impares */ if (tam<1) return; unsigned char* bigpix = malloc(tam*tam); if (tam==1) { pixels[y*ancho+x] = color; return; } int c=(tam-1)/2, i; double d; for (i=0; i<tam*tam; ++i) { d = sqrt ( pow(c - (i%tam), 2) + pow(c - (i/tam), 2)); bigpix[i] = 255-(d*255/(sqrt(2)*tam/2) ); } int basepix = (y-c)*ancho+x-c; for (i=0; i<tam*tam; ++i) { if ( (basepix>=0) && (basepix<ancho*alto) ) { int color = (pixels[basepix]>bigpix[i])?pixels[basepix]+bigpix[i]:bigpix[i]+pixels[basepix]; pixels[basepix]=(color>255)?255:color; } basepix++; if ((i+1)%tam==0) { basepix+=ancho-tam; } } free(bigpix); } int main() { unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO); int i; int Nestrellas=300; int varBrillo=30; int varTam=10; for (i=0; i<ANCHO*ALTO; ++i) pixels[0] = 0; for (i=0; i<Nestrellas; ++i) pixel_brillante(pixels, ANCHO, ALTO, rand()%ANCHO, rand()%ALTO, 255-varBrillo+rand()%varBrillo, rand()%varTam+1); salva_imagen("estrellas2.png", "I", ANCHO, ALTO, pixels); |
Dibujar líneas
Se pueden dibujar líneas con varios algoritmos diferentes. Emepzaremos por el más sencillo, el DDA (Digital Differential Analyzer). El problema de dibujar líneas es que las coordenadas de cada pixel son números enteros, y en ocasiones, una línea, no pasa exactamente por un punto. Como vemos, una línea recta sí tiene un trazo puro, una línea con inclinación de 45º también, pero casi todas las demás, si aumentamos la imagen serán una sucesión de pequeñas líneas rectas puesto que no tenemos nada intermedio.
Aquí tenemos un ejemplo,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | void linea_dda(unsigned char* pixels, int ancho, int alto, double x0,double y0,double x1,double y1,int col) // DDA subpixel -> thick { int i,n; double x, y; x1-=x0; y1-=y0; i=ceil(fabs(x1)); n=ceil(fabs(y1)); if (n<i) n=i; if (!n) n=1; x1/=(double)n; y1/=(double)n; for (x=x0,y=y0,i=0;i<n;i++,x+=x1,y+=y1) { if (y<alto && x<ancho) pixels[(int)y*ancho +(int)x] = col; } } int main() { unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO); int i; // ANCHO Y ALTO minimo 512 linea_dda(pixels, ANCHO, ALTO, 30, 0, 30, ALTO-1, 255); linea_dda(pixels, ANCHO, ALTO, 35, 0, 37, ALTO-1, 255); linea_dda(pixels, ANCHO, ALTO, 40, 0, 80, ALTO-1, 255); linea_dda(pixels, ANCHO, ALTO, 50, 0, 200, ALTO-1, 255); linea_dda(pixels, ANCHO, ALTO, 60, 0, 300, ALTO-1, 255); linea_dda(pixels, ANCHO, ALTO, 70, 0, 400, ALTO-1, 255); linea_dda(pixels, ANCHO, ALTO, 80, 0, 500, ALTO-1, 255); linea_dda(pixels, ANCHO, ALTO, 0, 20, ANCHO-1, 300, 255); linea_dda(pixels, ANCHO, ALTO, 0, 30, ANCHO-1, 400, 255); linea_dda(pixels, ANCHO, ALTO, 0, 40, ANCHO-1, 500, 255); linea_dda(pixels, ANCHO, ALTO, 500, 300, 400, 100, 255); salva_imagen("lineas.png", "I", ANCHO, ALTO, pixels); } |
Algoritmo de Bresenham
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | void linea_bresenham(unsigned char* pixels, int ancho, int alto, int x0, int y0, int x1, int y1, int col) { int dx = abs(x1-x0); int sx = x0<x1 ? 1 : -1; int dy = abs(y1-y0); int sy = y0<y1 ? 1 : -1; int err = (dx>dy ? dx : -dy)/2; int e2; for(;;) { if (y0<alto && x0<ancho) pixels[y0*ancho +x0] = col; if (x0==x1 && y0==y1) break; e2 = err; if (e2 >-dx) { err -= dy; x0 += sx; } if (e2 < dy) { err += dx; y0 += sy; } } } int main() { unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO); int i; // ANCHO Y ALTO minimo 512 linea_bresenham(pixels, ANCHO, ALTO, 30, 0, 30, ALTO-1, 255); linea_bresenham(pixels, ANCHO, ALTO, 35, 0, 37, ALTO-1, 255); linea_bresenham(pixels, ANCHO, ALTO, 40, 0, 80, ALTO-1, 255); linea_bresenham(pixels, ANCHO, ALTO, 50, 0, 200, ALTO-1, 255); linea_bresenham(pixels, ANCHO, ALTO, 60, 0, 300, ALTO-1, 255); linea_bresenham(pixels, ANCHO, ALTO, 70, 0, 400, ALTO-1, 255); linea_bresenham(pixels, ANCHO, ALTO, 80, 0, 500, ALTO-1, 255); linea_bresenham(pixels, ANCHO, ALTO, 0, 20, ANCHO-1, 300, 255); linea_bresenham(pixels, ANCHO, ALTO, 0, 30, ANCHO-1, 400, 255); linea_bresenham(pixels, ANCHO, ALTO, 0, 40, ANCHO-1, 500, 255); linea_bresenham(pixels, ANCHO, ALTO, 500, 300, 400, 100, 255); salva_imagen("lineasbresenham.png", "I", ANCHO, ALTO, pixels); } |
Lo que nos queda…
Aunque con algoritmos de Bresenham podemos dibujar líneas de varias formas (reservadas para la próxima entrega). Veremos muchos algoritmos para dibujar en C, con ejemplos y mucho más… formas simples y complejas, superposición, colores, antialias…