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 0x4D42 (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 para una imagen como esta:
nos queda algo como esto:
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.
Actualizado: 2016/09/18 : Se ha añadido la imagen original con la que se ejecuta el programa.