Archivo

Entradas Etiquetadas ‘rgb’

Leyendo archivos de imagen en formato BMP en C

Domingo, 26 de Junio de 2011 Gaspar Fernández Sin comentarios

n1214332197_30461081_3311160

Hoy vamos a practicar a leer una imagen desde un archivo BMP desde C. Aunque existen muchas APIs disponibles que son capaces de hacerlo, y mucho mejor que lo que voy a plantear (puesto que nos limitaremos a BMPs sin compresión y a 24bits por pixel), es un buen ejercicio para leer archivos con un formato especificado y documentado.

Para este tipo de archivos, tendremos dos cabeceras disponibles, la primera será la cabecera de fichero, y la segunda, la cabecera de información de imagen, que las definimos aquí:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct bmpFileHeader
{
  /* 2 bytes de identificación */
  uint32_t size;        /* Tamaño del archivo */
  uint16_t resv1;       /* Reservado */
  uint16_t resv2;       /* Reservado */
  uint32_t offset;      /* Offset hasta hasta los datos de imagen */
} bmpFileHeader;

typedef struct bmpInfoHeader
{
  uint32_t headersize;      /* Tamaño de la cabecera */
  uint32_t width;               /* Ancho */
  uint32_t height;          /* Alto */
  uint16_t planes;                  /* Planos de color (Siempre 1) */
  uint16_t bpp;             /* bits por pixel */
  uint32_t compress;        /* compresión */
  uint32_t imgsize;     /* tamaño de los datos de imagen */
  uint32_t bpmx;                /* Resolución X en bits por metro */
  uint32_t bpmy;                /* Resolución Y en bits por metro */
  uint32_t colors;              /* colors used en la paleta */
  uint32_t imxtcolors;      /* Colores importantes. 0 si son todos */
} bmpInfoHeader;

Todos los campos tienen un comentario que los explica, pero los detallaré algo más:

  • De primeras nos encontramos dos bytes de identificación que no he introducido en el registro, y es así por la alineación de variables. Es mejor leerlos a parte y luego leer el struct que os presento.
  • size (en FileHeader) es el tamaño del archivo completo, lo podremos usar para comprobar si hay más datos al final del archivo, o para ver si el archivo está cortado, es decir, como comprobación de errores
  • offset (en FileHeader), es la distancia en bytes desde que termina la cabecera de información hasta que empiezan los datos. No le vamos a hacer mucho caso a esto, sólo que puede ser que el archivo tenga más información entre la cabecera y los datos.
  • headersize (en InfoHeader), será el tamaño de la cabecera, dado que hay varias versiones del formato, ésta puede ser mucho más grande, aunque nosotros sólo leeremos siempre 40bytes.
  • planes (en InfoHeader), son los planos de color. Sólo está documentado el valor 1 de este campo, por tanto será el único que tratemos.
  • bpp (en InfoHeader), son los bits por pixel de la imagen, para el ejemplo, siempre serán 24, ya que no soportamos imágenes con otra profundidad de color.
  • compress (en InfoHeader), para este ejemplo será siempre 0, ya que no trataremos compresión.
  • imgsize (en InfoHeader), será el tamaño de los datos de imagen, al igual que size nos puede servir para control de errores, y para ver cuánta memoria vamos a reservar, aunque este tamaño, para el ejemplo será ancho*alto*3 (24bits por pixel)
  • bpmx, bpmy en (InfoHeader), será la resolución de la imagen en pixels por metro.
  • colors, imxtcolors en (InfoHeader), valdrán 0 en este ejemplo, ya que no usaremos imágenes con paleta de colores.

Ya estamos preparados para leer el archivo de imagen con la siguiente funció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
38
39
40
41
42
43
unsigned char *LoadBMP(char *filename, bmpInfoHeader *bInfoHeader)
{

  FILE *f;
  bmpFileHeader header;     /* cabecera */
  unsigned char *imgdata;   /* datos de imagen */
  uint16_t type;        /* 2 bytes identificativos */

  f=fopen (filename, "r");
  if (!f)
    return NULL;        /* Si no podemos leer, no hay imagen*/

  /* Leemos los dos primeros bytes */
  fread(&type, sizeof(uint16_t), 1, f);
  if (type !=0x4D42)        /* Comprobamos el formato */
    {
      fclose(f);
      return NULL;
    }

  /* Leemos la cabecera de fichero completa */
  fread(&header, sizeof(bmpFileHeader), 1, f);

  /* Leemos la cabecera de información completa */
  fread(bInfoHeader, sizeof(bmpInfoHeader), 1, f);

  /* Reservamos memoria para la imagen, ¿cuánta?
     Tanto como indique imgsize */

  imgdata=(unsigned char*)malloc(bInfoHeader->imgsize);

  /* Nos situamos en el sitio donde empiezan los datos de imagen,
   nos lo indica el offset de la cabecera de fichero*/

  fseek(f, header.offset, SEEK_SET);

  /* Leemos los datos de imagen, tantos bytes como imgsize */
  fread(imgdata, bInfoHeader->imgsize,1, f);

  /* Cerramos */
  fclose(f);

  /* Devolvemos la imagen */
  return imgdata;
}

Esta función devolverá los datos de la imagen como resultado de la función, y a través de un parámetro de entrada/salida, devolveremos también el infoHeader.
Lo que hacemos es simplemente leer las cabeceras, aunque para la primera cabecera, como dijimos antes, leemos primero los 2 bytes identificativos del formato de archivo, éstos tienen que valer 0×4D42 (MB), aunque como los datos están en little-endian (el byte menos significativo primero), lo leeremos al revés (BM), que corresponde a uno de los formatos de imagen que podemos alojar dentro de un BMP.

Por último, vamos a probar todo esto con un ejemplo, para no complicar las cosas con APIs gráficas, o cálculos de histogramas, se me ocurrió hacer una pequeña representación de la imagen en la consola. Para este código, creé una imagen con ImageMagick de la siguiente manera:

$ convert -background red -fill blue -font /usr/share/fonts/truetype/ttf-liberation/LiberationMono-Bold.ttf -pointsize 72 “label:Poesía Binaria” +matte poesia.bmp

+matte nos servirá para eliminar el canal alfa por defecto de la imagen.

Ahora, en nuestro código incluimos los struct y la función anteriores con un par de cosas más:

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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

typedef struct bmpFileHeader
{
  /* 2 bytes de identificación */
  uint32_t size;        /* Tamaño del archivo */
  uint16_t resv1;       /* Reservado */
  uint16_t resv2;       /* Reservado */
  uint32_t offset;      /* Offset hasta hasta los datos de imagen */
} bmpFileHeader;

typedef struct bmpInfoHeader
{
  uint32_t headersize;      /* Tamaño de la cabecera */
  uint32_t width;       /* Ancho */
  uint32_t height;      /* Alto */
  uint16_t planes;          /* Planos de color (Siempre 1) */
  uint16_t bpp;             /* bits por pixel */
  uint32_t compress;        /* compresión */
  uint32_t imgsize;     /* tamaño de los datos de imagen */
  uint32_t bpmx;        /* Resolución X en bits por metro */
  uint32_t bpmy;        /* Resolución Y en bits por metro */
  uint32_t colors;      /* colors used en la paleta */
  uint32_t imxtcolors;      /* Colores importantes. 0 si son todos */
} bmpInfoHeader;

unsigned char *LoadBMP(char *filename, bmpInfoHeader *bInfoHeader);
void DisplayInfo(bmpInfoHeader *info);
void TextDisplay(bmpInfoHeader *info, unsigned char *img);

int main()
{
  bmpInfoHeader info;
  unsigned char *img;

  img=LoadBMP("poesia.bmp", &info);
  DisplayInfo(&info);
  TextDisplay(&info, img);

  return 0;
}

void TextDisplay(bmpInfoHeader *info, unsigned char *img)
{
  int x, y;
  /* Reducimos la resolución vertical y horizontal para que la imagen entre en pantalla */
  static const int reduccionX=6, reduccionY=4;
  /* Si la componente supera el umbral, el color se marcará como 1. */
  static const int umbral=90;
  /* Asignamos caracteres a los colores en pantalla */
  static unsigned char colores[9]=" bgfrRGB";
  int r,g,b;

  /* Dibujamos la imagen */
  for (y=info->height; y>0; y-=reduccionY)
    {
      for (x=0; x<info->width; x+=reduccionX)
    {
      b=(img[3*(x+y*info->width)]>umbral);
      g=(img[3*(x+y*info->width)+1]>umbral);
      r=(img[3*(x+y*info->width)+2]>umbral);

      printf("%c", colores[b+g*2+r*4]);
    }
      printf("\n");
    }
}

unsigned char *LoadBMP(char *filename, bmpInfoHeader *bInfoHeader)
{

  FILE *f;
  bmpFileHeader header;     /* cabecera */
  unsigned char *imgdata;   /* datos de imagen */
  uint16_t type;        /* 2 bytes identificativos */

  f=fopen (filename, "r");
  if (!f)
    return NULL;        /* Si no podemos leer, no hay imagen*/

  /* Leemos los dos primeros bytes */
  fread(&type, sizeof(uint16_t), 1, f);
  if (type !=0x4D42)        /* Comprobamos el formato */
    {
      fclose(f);
      return NULL;
    }

  /* Leemos la cabecera de fichero completa */
  fread(&header, sizeof(bmpFileHeader), 1, f);

  /* Leemos la cabecera de información completa */
  fread(bInfoHeader, sizeof(bmpInfoHeader), 1, f);

  /* Reservamos memoria para la imagen, ¿cuánta?
     Tanto como indique imgsize */

  imgdata=(unsigned char*)malloc(bInfoHeader->imgsize);

  /* Nos situamos en el sitio donde empiezan los datos de imagen,
   nos lo indica el offset de la cabecera de fichero*/

  fseek(f, header.offset, SEEK_SET);

  /* Leemos los datos de imagen, tantos bytes como imgsize */
  fread(imgdata, bInfoHeader->imgsize,1, f);

  /* Cerramos */
  fclose(f);

  /* Devolvemos la imagen */
  return imgdata;
}

void DisplayInfo(bmpInfoHeader *info)
{
  printf("Tamaño de la cabecera: %u\n", info->headersize);
  printf("Anchura: %d\n", info->width);
  printf("Altura: %d\n", info->height);
  printf("Planos (1): %d\n", info->planes);
  printf("Bits por pixel: %d\n", info->bpp);
  printf("Compresión: %d\n", info->compress);
  printf("Tamaño de datos de imagen: %u\n", info->imgsize);
  printf("Resolucón horizontal: %u\n", info->bpmx);
  printf("Resolucón vertical: %u\n", info->bpmy);
  printf("Colores en paleta: %d\n", info->colors);
  printf("Colores importantes: %d\n", info->imxtcolors);
}

Compilamos el código anterior y nos queda algo como esto:
pbinaria

Lo más raro es la forma de acceder a cada pixel (podremos simplificar más adelante), y es que la imagen está almacenada boca abajo, por lo que la primera línea que leamos será la última que se debe mostrar, por otra parte, los colores están en formato BGR, y, generalmente para acceder a cada pixel lo podremos hacer con:

img[3*(x+y*info->width)+comp]

Teniendo en cuenta, como ya dijimos que la coordenada y va desde la última línea a la primera, y que:

  • Si queremos leer la componente azul, comp=0
  • Si queremos leer la componente verde, comp=1
  • Si queremos leer la componente roja, comp=2

Más adelante, iré publicando algunas aplicaciones prácticas de todo esto, y pronto, una función parecida a LoadBMP que se encargue de salvar los archivos y así empezar a probar filtros gráficos.

Actualizado: 2011/06/26 13:00 : Añadida la información de las componentes de color al final

Jugando con ImageMagick (I): Dimensiones, captura, color y efectos

Jueves, 26 de Agosto de 2010 Gaspar Fernández Sin comentarios

A menudo es necesario hacer una manipulación básica de imágenes y da mucha pereza ejecutar GIMP u otro editor de imágenes para una tontería; e incluso a veces necesitamos modificar gran cantidad de imágenes y necesitamos automatizar el proceso:

Ejecución

Aunque ImageMagick, que seguro que lo encontráis en los repositorios de vuestra distribución favorita, da mucho más de sí, aquí veremos ejemplos con convert, import y mogrify

Redimensionado de imágenes

hamburgueson_2$ convert -resize [dimensión/porcentaje] origen destino

Por ejemplo para redimensionar con un ancho de 320 pixels:

$ convert -resize 320 hamburgueson.jpg hamburgueson_mini.jpg

De esta forma, si especificamos la altura, se ignorará, ya que convert intentará mantener el aspecto de la imagen.

También podemos imponer la altura y que se calcule automáticamente el ancho:

$ convert -resize x500 hamburgueson.jpg hamburgueson_mini.jpg

O si queremos, podemos también redimensionar con un porcentaje del tamaño original:

$ convert -resize 40% hamburgueson.jpg hamburgueson_mini.jpg

hamburgueson_21También es posible la redimensión sin mantener el aspecto (relación ancho/alto) de la siguiente forma:

$ convert -resize \!600×300 hamburgueson.jpg hamburgueson_deforme.jpg

Y esto mismo, también por porcentaje:

$ convert -resize \!100×30% hamburgueson.jpg hamburgueson_deforme.jpg

Capturando el escritorio

Puede que nuestro entorno preferido de escritorio no venga con esta funcionalidad, o incluso que estemos desde una máquina remota y queramos ver lo que pasa en el escritorio, una captura de pantalla no viene mal. Para ello podemos hacer lo siguiente:

screenshot-26-08-2010-100842

$ import -window root captura.jpg

O si queremos capturar una ventana en especial:

$ import ventana.jpg

Nos aparecerá un puntero especial que nos permitirá seleccionar la ventana a capturar.

O si queremos capturar una ventana en especial sin necesidad de hacer click (muy útil si estamos en un equipo remoto):

$ import -window “Titulo de la ventana” ventana.jpg

También podemos sacar el ID de la ventana con:

$ wmctrl -l

Y poner en lugar del título, el ID. Es necesario que nos encontremos en el mismo escritorio de la ventana para hacer la captura. Cómo podemos hacer eso remotamente ?

$ wmctrl -l # Nos dirá en la segunda columna el escritorio donde está la ventana

$ wmctrl -s [numero] # Saltaremos al escritorio [numero], el primero es el cero.

Espacios de color

Podemos cambiar el formato del color de forma rápida usando colorspaces, dependiendo del formato de salida y la forma en la que la imagen será utilizada, para vídeo, por ejemplo. Aunque también tenemos algunos espacios interesantes como:hamburgueson_3

$ convert -colorspace gray hamburgueson.jpg hamburgueson_gris.jpg

El tono de gris estará calculado para adecuarse al ojo humano: Gris = 0.299*Rojo+0.587*Verde+0.114*Azul

Aunque también podremos transformar a YUV, XYZ, LAB, HSL, etc; también necesitamos que el formato de salida sea compatible con esos espacios de color. Para ver una lista de todos los colorspaces disponibles:

$ covert -list colorspace

hamburgueson_31También podemos ecualizar la imagen con respecto a los diferentes espacios de color de la siguiente forma:

$ convert -colorspace [espacio] -equalize origen destino

En el ejemplo:

$ convert -colorspace hsl -equalice hamburgueson.jpg hamburgueson_hsl.jpg

Y si no queremos decir el colorspace, usaremos el actual de la imagen:

$ convert -equalize hamburgueson.jpg hamburgueson_e.jpg

Recortar imágenes

Es más cómodo hacerlo con el ratón, pero en ocasiones querremos hamburgueson_4automatizar el proceso, y podemos hacerlo de la siguiente forma:

$ convert -crop [ancho]x[alto]+[x]+[y]

Por ejemplo lo siguiente:

$ convert -crop 300×300+100+100

Creará una imagen de 300×300 y empezará a partir del punto 100×100 (desde la izquierda/arriba).

Bordes

hamburgueson_41Para añadir un borde podemos hacer lo siguiente:

$ convert -bordercolor [color] -border [ancho]x[alto] origen destino

En el ejemplo tenemos:

$ convert -bordercolor black -border 10×10 hamburgueson.jpg hamburgueson_borde.jpg

Para especificar el color podemos hacerlo de varias formas:

$ convert -bordercolor black/red/green/blue… # Por su nombre. Si queremos saber los nombres de color disponibles:

$ convert -list color

$ convert -bordercolor “#aacc11″ # Por su equivalente hexadecimal

$ convert -bordercolor “rgb(100,150,200)” # En función de sus valores RGB

$ convert -bordercolor “hsl(100,150,200)” # En función de sus valores HSL

$convert -bordercolor “cmyk(100,200,150,230)” # En función de sus valores CMYK

… etcétera… tantos formatos como espacios de color.

Ahora bien, queremos quitar el reborde añadido:

$ convert -trim hamburgueson_borde.jpg hamburgueson_nuevo.jpg

Pero es posible que por la compresión jpeg no se detecte bien el borde (es una compresión con pérdida de calidad, y se modifica la imagen; para nosotros el borde es negro, pero para el ordenador hay muchos tonos de negro), entonces podemos hacer lo siguiente:

$ convert -fuzz 10% -trim hamburgueson_borde.jpg hamburgueson_nuevo.jpg

Con esto damos una tolerancia de un 10% al color, es decir, si se parecen los tonos de negro en menos de un 10% serán considerados iguales.

hamburgueson_42También podemos añadir un biselado como el del ejemplo de la siguiente manera:

$ convert -mattecolor red -frame 20×20+10+10 hamburgueson.jpg hamburgueson_rojo.jpg

Filtros y efectos

hamburgueson_32Por si fuera poco, podemos aplicar los filtros típicos a las imágenes como blur, motion-blur, blur gaussiano (gaussian-blur), ruido (noise), carboncillo (charcoal).

En el ejemplo, se ha añadido motion-blur de la siguiente forma:

$ convert -motion-blur 10×40+10+0 hamburgueson.jpg hamburgueson_movido.jpg

Visita otras webs de la red