Publi

Modificar brillo y contraste de cada componente de una imagen en C

windmill

Un poco do it yourself y friki a la vez es este articulo. En él, vamos a cargar una imagen jpg y vamos a cambiar el brillo y el contraste de la foto que hay en el encabezado de la página, todo desde nuestro programa, simplemente utilizando libjpeg ( $sudo apt-get install libjpeg8-dev ) para cargar y guardar de nuevo la foto. El efecto lo aplicaremos con una función que trabaje con los pixels de la imagen.

El código para cargar y salvar jpegs, lo he sacado de un ejemplo que hay en la red, ligeramente modificado, y el algoritmo para el brillo y contraste es parecido al que usa GIMP, sólo que los colores en GIMP van de 0 a 1 y aquí van de 0 a 255. Los valores del brillo y el contraste están comprendidos entre -1 y 1 por lo que 0 es el punto medio y debe dejar la imagen sin modificaciones.

Por otra parte, aunque lo lógico es modificar el brillo y el contraste de las tres componentes (rojo, verde y azul) al mismo tiempo, yo planteo modificarlas por separado para tener más libertad a la hora de crear efectos, resaltar colores, etc; por tanto la función byc() toma como parámetros la imagen a modificar, el valor de brillo para rojo, verde y azul y el de contraste rojo, verde y azul. El algoritmo es el mismo para cada componente, pero para esta primera versión he preferido dejarlo 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#include <stdio.h>
#include <jpeglib.h>
#include <stdlib.h>
#include <math.h>

typedef struct TImage
{
  int width;
  int height;
  int bytes_per_pixel;
  int color_space;
  int size;
  unsigned char *data;
} TImage;

int read_jpeg_file( char *filename, TImage *img )
{
    /* these are standard libjpeg structures for reading(decompression) */
    struct jpeg_decompress_struct cinfo;
    struct jpeg_error_mgr jerr;
    /* libjpeg data structure for storing one row, that is, scanline of an image */
    JSAMPROW row_pointer[1];
    FILE *infile = fopen( filename, "rb" );
    unsigned long location = 0;
    int i = 0;
   
    if ( !infile )
    {
      printf("Error opening jpeg file %s\n!", filename );
      return -1;
    }
    /* here we set up the standard libjpeg error handler */
    cinfo.err = jpeg_std_error( &jerr );
    /* setup decompression process and source, then read JPEG header */
    jpeg_create_decompress( &cinfo );
    /* this makes the library read from infile */
    jpeg_stdio_src( &cinfo, infile );
    /* reading the image header which contains image information */
    jpeg_read_header( &cinfo, TRUE );
    /* Uncomment the following to output image information, if needed. */
    img->width=cinfo.image_width;
    img->height=cinfo.image_height;
    img->bytes_per_pixel=cinfo.num_components;
    img->color_space=cinfo.jpeg_color_space;

    /* Start decompression jpeg here */
    jpeg_start_decompress( &cinfo );

    /* allocate memory to hold the uncompressed image */
    img->size=cinfo.output_width*cinfo.output_height*cinfo.num_components;
    img->data = (unsigned char*)malloc( img->size );
    /* now actually read the jpeg into the raw buffer */
    row_pointer[0] = (unsigned char *)malloc( cinfo.output_width*cinfo.num_components );
    /* read one scan line at a time */
    while( cinfo.output_scanline < cinfo.image_height )
    {
      jpeg_read_scanlines( &cinfo, row_pointer, 1 );
      for( i=0; i<cinfo.image_width*cinfo.num_components;i++)
        {
          img->data[location++] = row_pointer[0][i];
        }
    }
    /* wrap up decompression, destroy objects, free pointers and close open files */
    jpeg_finish_decompress( &cinfo );
    jpeg_destroy_decompress( &cinfo );
    free( row_pointer[0] );
    fclose( infile );
    /* yup, we succeeded! */
    return 1;
}

/**
 * write_jpeg_file Writes the raw image data stored in the raw_image buffer
 * to a jpeg image with default compression and smoothing options in the file
 * specified by *filename.
 *
 * \returns positive integer if successful, -1 otherwise
 * \param *filename char string specifying the file name to save to
 *
 */

int write_jpeg_file( char *filename, TImage *img )
{
    struct jpeg_compress_struct cinfo;
    struct jpeg_error_mgr jerr;
   
    /* this is a pointer to one row of image data */
    JSAMPROW row_pointer[1];
    FILE *outfile = fopen( filename, "wb" );
   
    if ( !outfile )
    {
        printf("Error opening output jpeg file %s\n!", filename );
        return -1;
    }
    cinfo.err = jpeg_std_error( &jerr );
    jpeg_create_compress(&cinfo);
    jpeg_stdio_dest(&cinfo, outfile);

    /* Setting the parameters of the output file here */
    cinfo.image_width = img->width;
    cinfo.image_height = img->height;
    cinfo.input_components = img->bytes_per_pixel;
    cinfo.in_color_space = JCS_RGB; //img->color_space;
    /* default compression parameters, we shouldn't be worried about these */
    jpeg_set_defaults( &cinfo );
    /* Now do the compression .. */
    jpeg_start_compress( &cinfo, TRUE );
    /* like reading a file, this time write one row at a time */
    while( cinfo.next_scanline < cinfo.image_height )
    {
        row_pointer[0] = &img->data[ cinfo.next_scanline * cinfo.image_width *  cinfo.input_components];
        jpeg_write_scanlines( &cinfo, row_pointer, 1 );
    }
    /* similar to read file, clean up after we're done compressing */
    jpeg_finish_compress( &cinfo );
    jpeg_destroy_compress( &cinfo );
    fclose( outfile );
    /* success code is 1! */
    return 1;
}

float byc(TImage *img, double bred, double bgreen, double bblue, double cred, double cgreen, double cblue)
{
  int i;
  double tanred, tangreen, tanblue;
  int v;
  tanred=tan ((cred + 1.0) * 0.78539816);
  tangreen=tan ((cgreen + 1.0) * 0.78539816);
  tanblue=tan ((cblue + 1.0) * 0.78539816);
  for (i=0; i<img->size; i+=3)
    {
      if (bred<0)
        img->data[i]=round(img->data[i]*(1+bred));
      else
        img->data[i]+=round((255-img->data[i])*bred);

      if (bgreen<0)
        img->data[i+1]=round(img->data[i+1]*(1+bgreen));
      else
        img->data[i+1]+=round((255-img->data[i+1])*bgreen);

      if (bblue<0)
        img->data[i+2]=round(img->data[i+2]*(1+bblue));
      else
        img->data[i+2]+=round((255-img->data[i+2])*bblue);

      v=round( (img->data[i] - 128) * tanred) + 128;
      if (v>0 && v<255)
    img->data[i]=v;
      else if (v<0)
    img->data[i]=0;
      else
    img->data[i]=255;

      v=round( (img->data[i+1] - 128) * tangreen) + 128;
      if (v>0 && v<255)
    img->data[i+1]=v;
      else if (v<0)
    img->data[i+1]=0;
      else
    img->data[i+1]=255;

      v=round( (img->data[i+2] - 128) * tanblue) + 128;
      if (v>0 && v<255)
    img->data[i+2]=v;
      else if (v<0)
    img->data[i+2]=0;
      else
    img->data[i+2]=255;

    }
}

int main()
{
    char *infilename = "windmill_flickr.jpg";
    TImage img;
    /* Try opening a jpeg*/
    if( read_jpeg_file( infilename, &img ) > 0 )
      {
        byc(&img, 0.1, 0.1, 0.2,0.1, -0.1, 0.8);
        write_jpeg_file("windmill.jpg", &img);
      }
    return 0;
}

Hay que prestar atención a que tan ( (contraste+1) * pi/4) lo calculamos antes del bucle, ya que será un valor constante para toda la imagen, no merece la pena calcularlo a cada iteración, perderíamos mucho tiempo, ya que es el cálculo más pesado del algoritmo. Además, para el contraste, hacemos las operaciones en un entero y luego lo pasamos a unsigned char (tipo de dato de nuestras componentes), eso es debido a que cuando calculamos el contraste la variable puede desbordarse por arriba o por abajo, y puede producir efectos curiosos (podemos eliminar las condiciones de comprobación para ver si es mayor que 255 o menor que 0 y ver efectos curiosos).

Ahora estaría bien simplificarlo un poco, hacerlo más legible y más flexible, primero vamos a crear una función para el brillo y otra para el contraste, que llamaremos una vez por cada componente:

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
void brillo(unsigned char *comp, double nivel)
{
  if (nivel<0)
    *comp=round(*comp*(1+nivel));
  else
    *comp=*comp+round((255-*comp)*nivel);
}

void contraste(unsigned char *comp, double tang)
{
  int v;

  v=round( (*comp - 128) * tang) + 128;
  if (v>0 && v<255)
    *comp=v;
  else if (v<0)
    *comp=0;
  else
    *comp=255;  
}

float byc(TImage *img, double bred, double bgreen, double bblue, double cred, double cgreen, double cblue)
{
  int i;
  double tanred, tangreen, tanblue;
  int v;
  tanred=tan ((cred + 1.0) * 0.78539816);
  tangreen=tan ((cgreen + 1.0) * 0.78539816);
  tanblue=tan ((cblue + 1.0) * 0.78539816);
  for (i=0; i<img->size; i+=3)
    {
      brillo(&img->data[i], bred);
      brillo(&img->data[i+1], bgreen);
      brillo(&img->data[i+2], bblue);

      contraste(&img->data[i], tanred);
      contraste(&img->data[i+1], tangreen);
      contraste(&img->data[i+2], tanblue);
    }
}

Este método está bien, el código del brillo y el contraste está sólo una vez, aunque estamos haciéndolo con funciones, y esto implica saltos en el programa y paso de parámetros que no serían problema si no es porque se hace muchas veces, si mi foto es de 500×375 (como la del ejemplo) estaríamos haciendo 500x375x6 = 1125000 llamadas a funciones que aunque no aumenta mucho la carga cuando lo hacemos, si aumentamos las dimensiones de la foto a 4288×3216 (como foto de una cámara digital) serían 4288x3216x6=82741248 lo cual empieza a ser significativo y si cronometramos el código puede que lo notemos. Si queremos optimizar un poco más en ese sentido podemos utilizar macros, como muestro a continuación:

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
#define BRILLO(comp, nivel) {           \
  if (nivel<0)                  \
    comp=round(comp*(1+nivel));         \
  else                      \
    comp=comp+round((255-comp)*nivel);      \
  }


#define CONTRASTE(comp, tang) {         \
  v=round( (comp - 128) * tang) + 128;      \
  if (v>0 && v<255)             \
    comp=v;                 \
  else if (v<0)                 \
    comp=0;                 \
  else                      \
    comp=255;                   \
  }


float byc(TImage *img, double bred, double bgreen, double bblue, double cred, double cgreen, double cblue)
{
  int i;
  double tanred, tangreen, tanblue;
  int v;
  tanred=tan ((cred + 1.0) * 0.78539816);
  tangreen=tan ((cgreen + 1.0) * 0.78539816);
  tanblue=tan ((cblue + 1.0) * 0.78539816);
  for (i=0; i<img->size; i+=3)
    {
      BRILLO(img->data[i], bred);
      BRILLO(img->data[i+1], bgreen);
      BRILLO(img->data[i+2], bblue);

      CONTRASTE(img->data[i], tanred);
      CONTRASTE(img->data[i+1], tangreen);
      CONTRASTE(img->data[i+2], tanblue);

    }
}

Con esto el tiempo sería igual que en el primer código publicado en el post.

Para compilar el código, tenemos que enlazar la biblioteca libjpeg y la biblioteca matemática:

$ gcc -o jpbc jpeg_bright_contrast.c -ljpeg -lm

Foto original:
windmill_flickr
Foto: Alter Wolf (Flickr) Creative Commons a día 30/05/2012

También podría interesarte...

Only 1 comment left Ir a comentario

  1. Pingback: Bitacoras.com /

Leave a Reply