Lire et écrire des fichiers image BMP

Lire et écrire nativement des fichiers image BMP

Voir version :

Pas de dépendances

Télécharger :

Plusieurs fichiers :
01_09_02_testbmp.c
01_09_02_bmp.h
01_09_02_bmp.c


01_09_02_testbmp.c

#include "01_09_02_bmp.h" int main() { int i,j; Image* I = NouvelleImage(256,256); for(i=0;i<256;i++) { for(j=0;j<256;j++) { Pixel p; p.r = i; p.g = j; p.b = 0; SetPixel(I,i,j,p); } } //Image* I = Charger("test.bmp"); Sauver(I,"test.bmp"); DelImage(I); return 0; }


01_09_02_bmp.h

#ifndef _BMP_H #define _BMP_H typedef struct Pixel { unsigned char r,g,b; } Pixel; typedef struct Image { int w,h; Pixel* dat; } Image; Image* Charger(const char* fichier); int Sauver(Image*,const char* fichier); Image* NouvelleImage(int w,int h); Image* CopieImage(Image*); void SetPixel(Image*,int i,int j,Pixel p); Pixel GetPixel(Image*,int i,int j); void DelImage(Image*); #endif


01_09_02_bmp.c

#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include "01_09_02_bmp.h" Image* NouvelleImage(int w,int h) { Image* I = malloc(sizeof(Image)); I->w = w; I->h = h; I->dat = calloc(1,w*h*sizeof(Pixel*)); return I; } Image* CopieImage(Image* I) { Image* res; if (!I) return NULL; res = NouvelleImage(I->w,I->h); memcpy(res->dat,I->dat,I->w*I->h*sizeof(Pixel)); return res; } void DelImage(Image* I) { if (I) { free(I->dat); free(I); } } void SetPixel(Image* I,int i,int j,Pixel p) { assert(I && i>=0 && i<I->w && j>=0 && j<I->h); I->dat[I->w*j+i] = p; } Pixel GetPixel(Image* I,int i,int j) { assert(I && i>=0 && i<I->w && j>=0 && j<I->h); return I->dat[I->w*j+i]; } // ------------------------------------------- #pragma pack(1) // desative l'alignement mémoire typedef int int32; typedef short int16; struct BMPImHead { int32 size_imhead; int32 width; int32 height; int16 nbplans; // toujours 1 int16 bpp; int32 compression; int32 sizeim; int32 hres; int32 vres; int32 cpalette; int32 cIpalette; }; struct BMPHead { char signature[2]; int32 taille; int32 rsv; int32 offsetim; struct BMPImHead imhead; }; Image* Charger(const char* fichier) { struct BMPHead head; Image* I; int i,j,pitch; unsigned char bgrpix[3]; char corrpitch[4] = {0,3,2,1}; Pixel p; FILE* F = fopen(fichier,"rb"); if (!F) return NULL; fread(&head,sizeof(struct BMPHead),1,F); if (head.signature[0]!='B' || head.signature[1]!='M') return NULL; // mauvaise signature, ou BMP non supporté. if (head.imhead.bpp!=24) return NULL; // supporte que le 24 bits pour l'instant if (head.imhead.compression!=0) return NULL; // rarrissime, je ne sais même pas si un logiciel écrit/lit des BMP compressés. if (head.imhead.cpalette!=0 || head.imhead.cIpalette!=0) return NULL; // pas de palette supportée, cependant, a bpp 24, il n'y a pas de palette. I = NouvelleImage(head.imhead.width,head.imhead.height); pitch = corrpitch[(3*head.imhead.width)%4]; for(j=0;j<I->h;j++) { for(i=0;i<I->w;i++) { fread(&bgrpix,1,3,F); p.r = bgrpix[2]; p.g = bgrpix[1]; p.b = bgrpix[0]; SetPixel(I,i,I->h-j-1,p); } fread(&bgrpix,1,pitch,F); } fclose(F); return I; } int Sauver(Image* I,const char* fichier) { struct BMPHead head; Pixel p; int i,j,tailledata,pitch; unsigned char bgrpix[3]; char corrpitch[4] = {0,3,2,1}; FILE* F = fopen(fichier,"wb"); if (!F) return -1; memset(&head,0,sizeof(struct BMPHead)); head.signature[0] = 'B'; head.signature[1] = 'M'; head.offsetim = sizeof(struct BMPHead); // je vais toujours écrire sur le même moule. head.imhead.size_imhead = sizeof(struct BMPImHead); head.imhead.width = I->w; head.imhead.height = I->h; head.imhead.nbplans = 1; head.imhead.bpp = 24; pitch = corrpitch[(3*head.imhead.width)%4]; tailledata = 3*head.imhead.height*head.imhead.width + head.imhead.height*pitch; head.imhead.sizeim = tailledata; head.taille = head.offsetim + head.imhead.sizeim; fwrite(&head,sizeof(struct BMPHead),1,F); for(j=0;j<I->h;j++) { for(i=0;i<I->w;i++) { p = GetPixel(I,i,I->h-j-1); bgrpix[0] = p.b; bgrpix[1] = p.g; bgrpix[2] = p.r; fwrite(&bgrpix,1,3,F); } bgrpix[0] = bgrpix[1] = bgrpix[2] = 0; fwrite(&bgrpix,1,pitch,F); } fclose(F); return 0; }


Commentaires

	Ah ! Le format BMP est intéressant. 
	Voila un bon exercice pour débuter dans l'écriture/lecture de fichiers binaires.

	Ce format est relativement facile, mais va quand même faire peur aux novices du domaine.

	Alors regardons le main, ainsi que le .h : ce sont les mêmes que l'exemple précédent sur les PPM.
	Je vous laisse regarder l'exemple précédent.

	De même toutes les fonctions, sauf chager et sauver sont les mêmes.

	Je ne parlerai donc ici que des fonctions charger et sauver.

Tout d'abord, une description du format BMP.
Une documentation ici :

http://www.commentcamarche.net/contents/video/format-bmp.php3

	Un BMP, c'est un header (des données), et ensuite le pixels les uns à la suite des autres, non compressés.

	Voyons le header.

	Le Header se compose de plusieurs choses : 
    - la signature BM : il existe d'autres signatures, mais on ne considerera que celle la, regardez le premier
      test de sortie de la fonction charger.
    - la taille du fichier
    - 4 octets à 0
    - l'offset de l'image : ou est ce que les données commencent.

	Dans notre cas, ce sera toujours à 54, après la taille du header.

	Vous voyez que j'ai fait des structures qui correspondent à cela dans bmp.c

	Le #pragma pack(1) permet de désactiver l'alignement mémoire : cherchez sur ce recueil ce qu'est l'alignement mémoire
	Vous comprendrez qu'il faut la désactiver, car sur le disque, il n'y a pas de "trous" entre les données.
	Il faut donc que notre structure soit contigue en mémoire, car on va l'écrire d'un seul coup.

	Nous avons ensuite une sous structure Entête de l'image (bien expliquée dans le lien ci dessus). Nous avons :
    - sa taille (40)
    - la largeur et la hauteur de l'image (important !) regardez dans charger et sauver, je les associe à I->w et I->h
    - le nombre de plans (toujours à 1 : des extentions du format BMP qui n'ont jamais été exploitées)
      j'imagine qu'il était prévu de faire des fichiers BMP a plusieurs plans, plusieurs images dans le même fichier ? 
      des layers ? ça n'a jamais été exploité.
    - Méthode de compression : pareil, rares sont les BMP compressés. On preferera d'autres formats pour compresser.
      Donc on ne lira, et écrira que des BMP non compressés.
    - résolution horizontale ou verticale. ça c'est utile pour imprimer par exemple. Si vous avez une image, et que vous 
      voulez l'imprimer, quelle taille fera-t-elle sur le papier ? En fonction de son nombre de pixels ? On peut renseigner
      ça ou pas. Moi je mets des 0 (et je lis des 0 sur les BMP que j'ai créé avec Paint), en gros, on s'en fout : le gars
      qui veut imprimer l'image se débrouillera pour lui donner la taille qu'il veu manuellement.
    - la palette (et les couleurs importantes de la palette)

Alors la palette, dans notre cas, on s'en fout : il n'y en a pas. 
En effet, en 24 bpp (ce que je supporte uniquement), chaque pixel embarque ses couleurs.

Dans les formats plus pauvres (256 couleurs), chaque pixel est codé sous un seul octet. Cet octet, ce n'est pas une couleur, mais
un index vers une palette existante. On définit 256 couleurs dans une palette, juste en dessous du header,et chaque pixel,
avec un seul octet (entre 0 et 255) est un index vers une couleur de la palette.
L'image est bien plus compacte, mais n'a que 256 couleurs maximum. Et si vous changez une couleur, tous les pixels utilisant
la même couleur seront donc changés en conséquence !

Ici donc, pas de palette : chaque pixel est indépendant et coute 3 octet (24 bits).

Voila pour le header.
On lit et on écrit le header d'un seul coup avec un fread ou un fwrite bien placé.

Passons maintenant à l'image elle même !

Les pixels sont codés les uns à la suite des autres.
On pourrait donc se dire "un grand fread et c'est plié !"

Cependant, hélas, nous ne stockons pas, ma structure et les BMP, les choses de la même façon.

Ma structure est ainsi faite :
1) Je code les pixels de gauche à droite, puis de haut en bas (sens de lecture occidental)
2) mes pixels sont tous de 3 octets, codés en rouge, vert, bleu (RGB) dans cet ordre.
3) tout se suit en mémoire (mon tableau dat n'a pas de trous)

Donc c'est simple !

Mais le format BMP est différent :
1) stockage des pixels de gauche à droite, mais de ---> BAS EN HAUT <---

Donc vous remarquerez dans ma fonction charger et sauver, les pixels sont lus/écrits non pas à position i,j
par rapport à ma structure, mais i,I->h-j-1, ce qui permet de bien mettre les pixels ou il faut.

2) stockage en bleu, vert, rouge (BRG), je suis donc obligé de passer par un triplet d'octets temporaire (bgrpix)
que je croise pour le remplissage avec ma structure Pixel (le r devient le bgrmix[2] et non [0])

3) et non des moindres, il existe ce qu'on appelle le picth.
Le pitch, c'est le nombre d'octets que prend une ligne, et le format BMP IMPOSE qu'une ligne soit multiple de 4 octets.
Cela permet, dans certains anciens buffer graphiques, de charger le BMP d'un seul coup, pour un blit rapide :
on perd quelques octets, mais on gagne énormément en ne gérant pas de cas particulier en arrivant en bout de ligne avec
un pross 32 bits, bref.

Donc chaque pixel fait 3 octets, j'ai X pixels par ligne. et 3*X n'est pas nécessairement un multiple de 4.
Le reste de la division de 3*X par 4 me dit combien d'octets sont en plus. Et en conséquence, je complète par des 0
J'appelle pitch le nombre d'octets à rajouter ici.

Si (3*X)%4 == 0, je ne rajoute rien : pitch = 0
Si (3*X)%4 == 1, je dois rajouter 3 octets (pour compléter)
Si (3*X)%4 == 2, je dois rajouter 2 octets (pour compléter)
Si (3*X)%4 == 3, je dois rajouter 1 octets (pour compléter)

Mon tableau corrpitch permet de considérer rapidement chacun de ces 4 cas en évitant 4 if.

Voila, il ne me reste plus qu'à écrire ou lire l'image...

Ensuite, je manipule de la même manière que dans l'exemple précédent.

Et c'est du BMP officiel, vous pouvez l'ouvrir avec Paint ou autre !