Charger des images TGA
Date de publication : 27/12/2004 , Date de mise à jour : 29/12/2004
Par
David Henry (Autres articles)
Cet article a pour objectif d'expliquer comment créer une texture OpenGL à partir d'un
fichier image, et en particulier ici, une image TGA (TARGA). Le code est en C.
1. Introduction
2. Texture OpenGL
3. Lecture de l'en-tête TGA
4. Type de texture OpenGL
5. La palette de couleurs
6. Lecture des données
6.1. Les images en couleurs vraies
6.2. Les images 8 bits avec palette couleurs
6.3. Les images en dégradé de gris
6.4. Les images avec compression RLE
7. Création de la texture
8. Conclusion
1. Introduction
Cet article a pour objectif d'expliquer comment créer une texture OpenGL à partir d'un
fichier image, et en particulier ici, une image TGA (TARGA). Le code est en C.
Un fichier image est un fichier binaire contenant (le plus souvent) une en-tête
(header) de fichier puis les données. Les données peuvent être les données de
l'image et la palette de couleur, ou seulement les données si l'image ne nécessite pas de
palette. L'en-tête fournis des informations quant à la nature des données : les
dimensions de l'image, le type de l'image (avec palette ou non), le type de compression,
etc. Un format d'image peut contenir plusieurs types d'images (c'est le cas du format TGA
par exemple) comme une image 24 bits non compressées, une image 32 bits, une image 8 bits
avec palette, etc. Les informations que l'on trouve sont spécifiques au format de
fichier.
Le format TGA peut contenir des images 8, 16, 24 et 32 bits avec ou sans compression.
La méthode de compression est l'algorithme RLE, qui permet une compression non destructrice
de l'image. La compression JPEG est dite « destructrice », puisque les données
compressées ne seront pas identiques à celles d'origine (il y a perte d'information). Les
images 8 bits peuvent utiliser une palette de couleur.
Certains formats de fichier ont aussi un « pied » (footer) contenant
des informations additionnelles. C'est le cas du format TGA, mais nous ne le verront pas ici
ces données ne nous seront pas utiles.
Nous allons voir dans cet article comment lire un fichier d'image TGA ; c'est-à-dire
lire son en-tête afin de déterminer comment extraire les données de l'image, puis lire ces
données et les stocker en mémoire pour créer à partir de cette image, une texture OpenGL.
Les types utilisés ont les tailles suivantes :
- char : 1 octet ;
- short : 2 octets ;
- int : 4 octets ;
- ubyte est un octet non signé (unsigned char) d'un octet.
2. Texture OpenGL
Avant de commencer à lire le fichier, il est nécessaire de connaître les données dont nous
avons besoin pour créer une texture OpenGL.
Il nous faut bien évidemment un tableau de données pour stocker les pixels de l'image.
OpenGL peut prendre plusieurs types de données pour créer une texture (octets, entiers, etc.).
Nous nous restreindront ici au type « octet non signé » pour stocker nos données.
Nous avons également besoin de connaître les dimensions de l'image, le nombre de composantes
R, V, B, A ou Luminance et Alpha de l'image et le format (RVB, RVBA, Luminance ou Luminance
Alpha).
Nous allons regrouper ces données dans une type structure gl_texture_t :
typedef struct
{
GLuint width;
GLuint height;
GLenum format;
GLint internalFormat;
GLubyte *texels;
} gl_texture_t; |
Pour la lecture d'une image TGA, nous créeront une fonction qui prendra en paramètre un
nom de fichier et un pointeur vers un objet de type gl_texture_t. Une fois
l'image chargée, nous pourront utiliser les fonctions OpenGL pour créer une texture à partir
de notre objet gl_texture_t.
3. Lecture de l'en-tête TGA
L'en-tête d'un fichier TGA possède divers champs et est invariable d'un fichier à un
autre (on retrouve toujours les mêmes champs dans le même ordre et le même nombre). On
peut donc regrouper toutes ces informations à l'intérieure d'une structure de données
que l'on lira d'un bloc :
#pragma pack(push, 1)
typedef struct
{
ubyte id_lenght;
ubyte colormap_type;
ubyte image_type;
short cm_first_entry;
short cm_length;
ubyte cm_size;
short x_origin;
short y_origin;
short width;
short height;
ubyte pixel_depth;
ubyte image_descriptor;
} tga_header_t;
#pragma pack(pop) |
Le type tga_header_t doit avoir une taille de 18 octets. Certains compilateurs
utilisent par défaut un alignement différent (sur 4 octets par exemple) et qui font qu'une
structure de 18 octets occupe 20 octets (multiple de 4). C'est pour cela qu'on utilise les
directives préprocesseur #pragma pack pour aligner le type tga_header_t
sur un octet.
Étudions les champs de cette structure : id_lenght est la taille
d'un champ de données situé juste après l'en-tête du fichier. Ce champ ne nous est pas
utile et n'est pas toujours présent. id_lenght nous permet de sauter cette
zone mémoire pour atteindre les données de l'image.
colormap_type indique si l'image utilise une palette de couleur ou non. Si
colormap_type vaut 1, alors on doit trouver la palette au début du champ de
données, juste avant les données des pixels.
image_type indique le type d'image contenu dans le fichier. Les valeurs
qu'il peut prendre sont les suivantes :
| Valeur |
Type d'image |
| 0 |
pas de données images |
| 1 |
image 8 bits non compressée en mode index couleur (avec palette) |
| 2 |
image 16, 24 ou 32 bits non compressée en mode BVR |
| 3 |
image 8 ou 16 bits non compressée en mode dégradé de gris |
| 9 |
image 8 bits compressée en mode index couleur (avec palette) |
| 10 |
image 16, 24 ou 32 bits compressée en mode BVR |
| 11 |
image 8 ou 16 bits compressée en mode dégradé de gris |
Les trois variables commençant par cm sont spécifiques à la palette de
couleur. cm_first_entry indique l'index de la première couleur dans la
palette. Il est possible en effet que la palette soit d'une taille plus grande que
nécessaire et les données de la palette peuvent ne pas se trouver au début de ce champs
(au milieu par exemple). cm_length est le nombre de couleurs de la palette.
Enfin, cm_size est le nombre de bits par couleur de la palette. Il peut
être de 15, 16, 24 ou 32.
x_origin et y_origin sont les coordonnées absolues du
coin bas gauche de l'image, et elles ne nous serviront pas.
width et height sont les dimensions de l'image (largeur et
hauteur). pixel_depth indique le nombre de bits utilisés pour stocker un
pixel de l'image. Typiquement 8, 16, 24 ou 32. Le champ image_descriptor
n'est pas utile dans notre cas.
À partir de cette structure, nous pouvons déjà obenir un certain nombre d'informations
sur l'image et initialiser tous les champs de notre objet gl_texture_t à
l'exception de texels. Nous pouvons néanmoins réserver une zone mémoire
pour stocker les données pixels de l'image. Voyons la lecture de l'en-tête :
int ReadTGAFile( char *filename, gl_texture_t *texinfo )
{
FILE *fp;
tga_header_t header;
ubyte *colormap = NULL;
fp = fopen( filename, "rb" );
if( !fp ) {
return 0;
}
fread( &header, sizeof( tga_header_t ), 1, fp );
GetTextureInfo( &header, texinfo );
fseek( fp, header.id_lenght, SEEK_CUR );
fclose( fp );
return 1;
} |
La fonction ReadTGAFile() a pour but de lire un fichier image TGA et de
stocker ses données dans texinfo. En cas d'échec elle retourne 0 (faux),
sinon 1 (vrai).
Pour le moment notre fonction ne fait pas grand chose : ouverture du fichier
en mode binaire ("rb"), lecture de l'en-tête dans header,
initialisation des variables de texinfo et déplacement au début des données
de l'image (rappelez-vous, il peut y avoir des informations additionnelles entre l'en-tête
et les données de l'image). La fonction GetTextureInfo() est détaillée
dans la section suivante.
4. Type de texture OpenGL
À partir de l'en-tête TGA, nous pouvons en tirer toutes les informations nécessaire
quant au type de texture OpenGL que l'on va utiliser. C'est le travail de la fonction
GetTextureInfo() :
void GetTextureInfo( tga_header_t *header, gl_texture_t *texinfo )
{
texinfo->width = header->width;
texinfo->height = header->height;
switch( header->image_type )
{
case 3:
case 11:
{
if( (int)header->pixel_depth == 8 ) {
texinfo->format = GL_LUMINANCE;
texinfo->internalFormat = 1;
}
else {
texinfo->format = GL_LUMINANCE_ALPHA;
texinfo->internalFormat = 2;
}
break;
}
case 1:
case 2:
case 9:
case 10:
{
if( (int)header->pixel_depth <= 24 ) {
texinfo->format = GL_RGB;
texinfo->internalFormat = 3;
}
else {
texinfo->format = GL_RGBA;
texinfo->internalFormat = 4;
}
break;
}
}
} |
Les dimensions de l'image (width et height) sont récupérées
directement à partir de l'en-tête. En revanche il faut déterminer le nombre de composantes
et le format de l'image à partir de son type TGA et du nombre de bits par pixel.
Les images en 8 et 16 bits seront converties à la lecture en 24 bits sauf pour les
images en dégradé de gris, où l'on utilise le format GL_LUMINANCE. Les
autres sont au format GL_RGB ou GL_RGBA. Une image 16 bits
en dégradé de gris est composé de 8 bits pour l'intensité de gris et d'un canal alpha
de 8 bits.
5. La palette de couleurs
Les images 8 bits (sauf dégradé de gris) nécessitent une palette de couleur. Chaque
octet représentant un pixel dans les données de l'image, est en fait un index pour aller
chercher sa couleur dans la palette. La palette est un tableau de couleurs.
Dans cet article on supposera que la palette a toujours une profondeur de 24 bits
par couleur. C'est-à-dire, chaque couleur est composé de 8 bits pour le bleu, 8 bits
pour le vert et 8 bits pour le rouge. La palette est en BVR (et non RVB !).
Dans notre fonction ReadTGAFile(), nous nous sommes arrêté au début
des données de l'image. Nous devons maintenant tester s'il y a une palette de couleur
et, le cas échéant, la stocker. Nous la stockeront dans le tableau colormap
déclaré en début de fonction :
if( header.colormap_type )
{
colormap = (ubyte *)malloc( sizeof( ubyte ) * header.cm_length * (header.cm_size >> 3));
fread( colormap, sizeof( ubyte ), header.cm_length * (header.cm_size >> 3), fp );
} |
L'expression (header.cm_size >> 3) nous donne la taille en octet
d'une couleur à partir d'une taille en bits (division par 23 ou 8). On peut
ainsi connaître la taille en octets de la palette et la lire.
Attention : ici nous allouons dynamiquement de la mémoire pour
stocker la palette ; il ne faudra pas oublier de la libérer à la fin de la fonction,
lorsque nous auront lus les données !
6. Lecture des données
Il nous faut maintenant lire les données pixels de l'image. Elle se trouvent juste
après la palette couleur si celle-ci existe, sinon après le petit champ d'informations
additionnelles suivant l'en-tête.
Il nous faut allouer un espace mémoire pour stocker les données pixels :
texinfo->texels = (GLubyte *)malloc( sizeof( GLubyte ) * texinfo->width * texinfo->height * texinfo->internalFormat );
if( !texinfo->texels ) {
return 0;
} |
À l'aide des variables image_type et pixel_depth de l'en-tête,
nous allons devoir déterminer quelle méthode utiliser pour lire ces données. Le plus
pratique je pense est de faire un choix conditionnel (switch) suivant
image_type puis pour chaque cas, voir avec pixel_depth si
c'est nécessaire.
Il y a quatre grand type d'images : les images en 16-24-32 bits, appelées également
images en couleurs vraies, les images 8 bits avec palette, les images en dégradé
de gris 8 ou 16 bits, et les images compressées (où l'on retrouve les trois premiers types
à chaque fois). On va commencer par le plus simple et finir par les images compressées.
6.1. Les images en couleurs vraies
Les images en couleurs vraies sont des images en 16, 24 ou 32 bits où les données
« complètes » de chaque pixel se suivent. Pour les images TGA, les pixels
sont en Bleu-Vert-Rouge (BVR). Pour OpenGL, nous avons besoin de données stockées en
Rouge-Vert-Bleu (RVB) (à moins d'utiliser une extension permettant d'utiliser directement
des données au format BVR). Il suffit donc de lire le nombre d'octets approprié pour
autant de pixels qu'il y'a dans l'image (pour connaître le nombre de pixels, il faut
multiplier la hauteur par la largeur) :
void ReadTGA24bits( FILE *fp, gl_texture_t *texinfo )
{
unsigned int i;
for( i = 0; i < texinfo->width * texinfo->height; i++ )
{
texinfo->texels[ (i * 3) + 2 ] = (GLubyte)fgetc( fp );
texinfo->texels[ (i * 3) + 1 ] = (GLubyte)fgetc( fp );
texinfo->texels[ (i * 3) + 0 ] = (GLubyte)fgetc( fp );
}
} |
Pour chaque pixel, on lit bien 3 octets (24 bits). Pour connaître la position des
données d'un pixel, il faut multiplier par 3 sa position i dans l'image,
car on utilise 3 octet par pixel. Ensuite, on peut accéder aux composantes rouge,
vert et bleu en ajoutant 0, 1 ou 2 à la position du début des données du pixel.
Pour une image 32 bits, c'est pareil sauf que l'on utilise 4 octets par pixel. Le
dernier octet est la composante alpha (transparance) :
void ReadTGA32bits( FILE *fp, gl_texture_t *texinfo )
{
unsigned int i;
for( i = 0; i < texinfo->width * texinfo->height; i++ )
{
texinfo->texels[ (i * 4) + 2 ] = (GLubyte)fgetc( fp );
texinfo->texels[ (i * 4) + 1 ] = (GLubyte)fgetc( fp );
texinfo->texels[ (i * 4) + 0 ] = (GLubyte)fgetc( fp );
texinfo->texels[ (i * 4) + 3 ] = (GLubyte)fgetc( fp );
}
} |
Les images en 16 bits sont un peu plus délicates, car il faut extraire les
3 composantes d'une zone mémoire de 2 octets. Chaque composante est codée
sur 5 bits ; le bit de poids fort (le plus à gauche) de ces deux octets
n'est pas utilisé (c'est une image 15 bits en fait).
Pour extraire chaque composante, on applique un masque binaire :
| Composante |
Masque |
Masque en Binaire |
| rouge |
couleur & 7C00 |
01111100 00000000 |
| vert |
couleur & 03E0 |
00000011 11100000 |
| bleu |
couleur & 001F |
00000000 00011111 |
Une fois la composante isolée, il faut faire un décalage de 10 bits vers la
droite pour le rouge et 5 bits pour le vert afin d'avoir une valeur comprise entre
0 et 31. Il faut ramener cette valeur sur un intervalle de 0 à 255. Pour cela,
on multiplie chaque composante obtenue par 8 (ou on fait un décalage à gauche de
3 rangs).
Pour chaque pixel, on lit 2 octets. On peut les lire d'un bloc ou les lire
octet par octet. Dans de dernier cas, il faut décaller le premier octet de 8
rangs pour reconstituer la valeur sur 16 bits.
void ReadTGA16bits( FILE *fp, gl_texture_t *texinfo )
{
unsigned int i;
short color;
for( i = 0; i < texinfo->width * texinfo->height; i++ )
{
color = fgetc( fp ) + (fgetc( fp ) << 8);
texinfo->texels[ (i * 3) + 0 ] = (GLubyte)(((color & 0x7C00) >> 10) << 3);
texinfo->texels[ (i * 3) + 1 ] = (GLubyte)(((color & 0x03E0) >> 5) << 3);
texinfo->texels[ (i * 3) + 2 ] = (GLubyte)(((color & 0x001F) >> 0) << 3);
}
} |
6.2. Les images 8 bits avec palette couleurs
La lecture d'images 8 bits avec palette n'est pas difficile non plus. Chaque
« donnée pixel » est codée sur un octet. Cet octet est l'index de la
couleur 24 bits (dans le cadre de cet article) dans la palette.
Pour créer une texture OpenGL à partir d'une image 8 bits, on va d'abord la
convertir en 24 bits.
void ReadTGA8bits( FILE *fp, ubyte *colormap, gl_texture_t *texinfo )
{
unsigned int i;
ubyte color;
for( i = 0; i < texinfo->width * texinfo->height; i++ )
{
color = fgetc( fp );
texinfo->texels[ (i * 3) + 2 ] = (GLubyte)colormap[ (color * 3) + 0 ];
texinfo->texels[ (i * 3) + 1 ] = (GLubyte)colormap[ (color * 3) + 1 ];
texinfo->texels[ (i * 3) + 0 ] = (GLubyte)colormap[ (color * 3) + 2 ];
}
} |
L'accès aux composantes des couleurs de la palette se fait de la même façon
que pour une image 24 bits. On en profite ici pour échanger les composantes
bleu et rouges dans notre tableau de pixels.
6.3. Les images en dégradé de gris
Les images en dégradé de gris sont simples à traiter. Pour les images 8 bits,
chaque octet représente la valeur de l'intensité lumineuse. Pour les images 16
bits, le second octet est la valeur du cannal alpha.
void ReadTGAgray8bits( FILE *fp, gl_texture_t *texinfo )
{
unsigned int i;
for( i = 0; i < texinfo->width * texinfo->height; i++ )
{
texinfo->texels[i] = (GLubyte)fgetc( fp );
}
} |
Pour ce type d'image, on peut même lire les données en une instruction :
fread( texinfo->texels, sizeof( GLubyte ), texinfo->width * texinfo->height, fp ); |
Pour une image 16 bits, seule une ligne change :
void ReadTGAgray16bits( FILE *fp, gl_texture_t *texinfo )
{
unsigned int i;
for( i = 0; i < texinfo->width * texinfo->height; i++ )
{
texinfo->texels[ (i * 2) + 0 ] = (GLubyte)fgetc( fp );
texinfo->texels[ (i * 2) + 1 ] = (GLubyte)fgetc( fp );
}
} |
6.4. Les images avec compression RLE
Les images compressées utilisent un algorithme de compression appelé
Run Lenght Encoding (RLE). Il est est simple et non destructeur
(les données de l'image ne sont pas altérée on retrouve au décodage
la même chose qu'à l'encodage).
Le principe est le suivant : les champs de pixels consécutifs
égaux sont remplacés par deux valeurs, un pour leur nombre, l'autre
pour leur valeur. Ainsi on on gagne à partir de trois pixels consécutifs
égaux pour une image 8 bits (la valeur et le pixel sont codés sur un
octet chacun). Dans le cas où il y a une large zone de pixels complètement
différents les un des autres, on l'indique sur un octet ainsi que leur
nombre (les deux sur le même octet). La première valeur est toujours
stockée sur un octet. La seconde dépend du type d'image (1, 2, 3 ou 4
octets).
Concrètement, ce que l'on fait : on lit un octet. Le bit de poids
fort (le plus à gauche) indique s'il s'agit un champs d'octets consécutifs
égaux (1) ou pas (0). Les 7 bits restant sont le nombre d'octets égaux ou
non égaux du champs que l'on va traiter. Dans le cas d'un champs d'octets
égaux, on lit un pixel, qui est la valeur de tous les pixels de ce champs,
et on initialise ces dernier avec. Si le champs n'est pas constitué
de pixels égaux, on lit chaque pixel à partir du fichier.
Remarque : comme le nombre de pixels par champs est codé sur 7 bits,
on ne peut avoir au plus que 128 pixels traités par packet.
Voici comment faire pour une image 24 bits. Les autres types d'image
se lisent pareil, avec les méthodes de décodage des octets vues plus haut
sans compression. Voyez les sources complètes pour les autres types
d'image (voir fin de l'article) :
void ReadTGA24bitsRLE( FILE *fp, gl_texture_t *texinfo )
{
int i, size;
ubyte rgb[3];
ubyte packet_header;
GLubyte *ptr = texinfo->texels;
while( ptr < texinfo->texels + (texinfo->width * texinfo->height) * 3 )
{
packet_header = fgetc( fp );
size = 1 + (packet_header & 0x7f);
if( packet_header & 0x80 )
{
fread( rgb, sizeof( ubyte ), 3, fp );
for( i = 0; i < size; i++, ptr += 3 )
{
ptr[0] = rgb[2];
ptr[1] = rgb[1];
ptr[2] = rgb[0];
}
}
else
{
for( i = 0; i < size; i++, ptr += 3 )
{
ptr[2] = (GLubyte)fgetc( fp );
ptr[1] = (GLubyte)fgetc( fp );
ptr[0] = (GLubyte)fgetc( fp );
}
}
}
} |
Comme il peut y avoir d'autres données après les données de l'image, on doit
veiller à ne pas dépasser la taille de l'image. On utilise un masque binaire
pour extraire le premier bit et les sept autres.
7. Création de la texture
Maintenant que l'on a lu et stocké les données de l'image sous un format
exploitable par OpenGL, on peut aisément créer notre texture à partir de
l'objet gl_texture_t :
if( !ReadTGAFile( filename, &gltex ) ) {
printf( "error: couldn't load %s!\n", filename );
exit(1);
}
glGenTextures( 1, &texid );
glBindTexture( GL_TEXTURE_2D, texid );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexImage2D( GL_TEXTURE_2D, 0, gltex.internalFormat, gltex.width, gltex.height,
0, gltex.format, GL_UNSIGNED_BYTE, gltex.texels );
free( gltex.texels ); |
D'abord on utilise notre fonction ReadTGAFile() pour initialiser
gltex, un objet de type gl_texture_t. En cas de succès
de la lecture de l'image, on crée la texture OpenGL avec les fonctions faites
pour. Vérifiez bien que les dimensions de votre image sont multiple de 2 ou alors
il vous faudra utiliser gluScaleImage() ou bien utiliser
gluBuild2DMipmaps() à la place de glTexImage2D() (dans
ce cas il faut aussi modifier le paramètre de filtrage associé à
GL_TEXTURE_MAG_FILTER).
Une fois la texture créée, on libère la mémoire allouée pour stocker les
données des pixels de l'image car OpenGL possède sa propre copie de ces données,
il est donc inutile d'encombrer la mémoire avec ça.
8. Conclusion
Cet article vous a permis de voir comment on crée une texture à partir d'une
image stockée dans un fichier, ici un fichier TGA. Pour d'autres formats, ça se
passe un peu de la même manière : lecture de l'en-tête, choix de la méthode
appropriée pour lire les données suivant le type de l'image, etc.
Il y a deux façons de lire un fichier : lire les éléments un par un,
comme ici, ou lire le fichier tout entier dans une mémoire tampon puis extraire
les données à partir de cette zone mémoire. Cette dernière est peut-être plus
rapide car demande moins d'accès fichiers. Elle est d'ailleurs facilement
adaptable ici.
Pour certains formats, des bibliothèques toutes prêtes existent pour manipuler
les fichiers images comme libpng pour le format PNG,
libjpeg pour le format JPEG ou libtiff pour
les images TIFF. Il existe églament des bibliothèques permettant de gérer plusieurs
formats de fichiers image avec une seule fonction, comme
SDL_image.
Vous pouvez télécharger les sources complètes ( tga.c)
avec un exemple simple et les réutiliser à votre guise ; elles sont
entièrement libres. Je me suis inspiré d'une documentation
plus complète trouvée sur le net il y'a longtemps, je ne sais plus où... Vous
pouvez tester le programme avec des images de test
TGA de différents types. Contact : tfc_duke, (CHEZ) club-internet, (POINT) fr.
 

Cet article est mis à disposition sous un contrat
Creative Commons (licence CC-BY-ND).
|