Publi

Generando imágenes en C, sólo por diversión, empezando desde cero (Parte I)

colores_y_formasHace tiempo que no pongo nada de imagen digital, y ya tenía ganas. No haremos nada complicado, pero muchas veces, cuando empezamos a programar, pensamos en representar el contenido de un array en una imagen, o para esas veces en que pensamos que un simple algoritmo nos puede ayudar a crear la imagen que queremos.

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.

rejilla_numeradaAl final, en memoria tendremos algo así. Nosotros sabemos que es una matriz (con dos dimensiones), pero en realidad para acceder a cada casilla tendremos que utilizar un número consecutivo a la anterior. Y aunque nosotros, para acceder a una posición digamos fila 3, columna 4, en realidad le estamos pasando un 14, sólo que el compilador aplica de forma transparente una transformación.
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

gradientePara el gradiente más sencillo, lo que hacemos es jugar con la variable con la que recorremos el array, haciendo su módulo con 256, con lo que conseguiremos valores entre 0 y 255 consecutivos (y cuando lleguemos a 255 volvemos de nuevo al 0):

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);
}

gradiente2Aunque observamos que si ANCHO lo hacemos más grande el gradiente se repetirá, si queremos que éste se adapte al ancho que queramos:

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);
}

gradiente3También podemos hacer el gradiente en vertical (y que sea flexible en alto y ancho):

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:
gradiente4

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

cuadriculaTambién podemos intentar dibujar una cuadrícula en la imagen, además, con la posibilidad de modificar las divisiones horizontales y verticales, así como el grosor de la línea, de la siguiente forma:

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

randOtro tipo de imagen muy común es la imagen ruido, o imagen con puntos aleatorios. Es decir, el color de cada punto es obtenido mediante la generación de un número aleatorio y la función rand(). Lo podemos hacer así:

1
2
3
4
5
6
7
8
9
10
int main()
{
  unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO);
  int i;

  for (i=0; i<ANCHO*ALTO; ++i)
    pixels[i] = rand()%255;

  salva_imagen("rand.png", "I", ANCHO, ALTO, pixels);
}

Fondo de estrellas

estrellasAhora sólo generaremos unos pocos puntos. Obtenemos de forma aleatoria las coordenadas de varios puntos, y establecemos un color brillante (blanco). En el ejemplo podremos seleccionar el número de estrellas que queremos pintar, así como la variación de color (varBrillo) que queremos darle a cada punto.

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
  unsigned char* pixels = malloc(sizeof(char)*ANCHO*ALTO);
  int i;
  int Nestrellas=300;
  int varBrillo=30;

  for (i=0; i<Nestrellas; ++i)
    pixels[rand()%(ANCHO*ALTO)] = 255-varBrillo+rand()%varBrillo;

  salva_imagen("estrellas.png", "I", ANCHO, ALTO, pixels);
}

Estrellas de diferentes tamaños

estrellas2Otra posibilidad que tenemos es dibujar estrellas de diferentes tamaños, con diferentes brillos, en realidad, antes, una estrella era un punto, ahora será un punto rodeado de más puntos, en los que veremos que el brillo se va degradando hasta llegar a negro.
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

lineas
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

lineasbresenhamTambién tenemos el algoritmo de Bresenham para dibujar líneas, y es otra forma de aproximar los puntos con el fin de terminar nuestro dibujo. Para verlo en acción, aquí lo tenéis:

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…

También podría interesarte....

Leave a Reply