TP : Gestion d une image au format PGM Objectif : L objectif du sujet est de créer une classe de manipulation d images au format PGM (Portable GreyMap), et de programmer des opérations relativement simples telles que la lecture et l écriture d images dans ce format. Dans un second temps, il est proposé de compléter cette classe avec des méthodes de filtrage d images. Enfin, pour ceux qui sont très très très fort et qui vont très très très vite, on propose, dans la 3 ième partie, une extension du programme aux images couleurs (format PPM). Préliminaires : Dans un répertoire de votre choix, créez trois fichiers contenant le programme principal et la classe Image. Téléchargez dans ce même répertoire les images «blood.pgm», «flowers.pgm», et «Logo.pgm» qui se trouvent http://www.fresnel.fr/perso/derrode/tmp/. Vous pouvez observer les images en utilisant n importe quel logiciel de visualisation d images. Détails du format PGM : Le format PGM est un format d enregistrement d images non compressé relativement simple. Les images sont codées par des valeurs représentant les niveaux de gris de leurs pixels. Ces valeurs sont comprises entre 0 (noir) et 255 (blanc) et représentent différentes teintes de gris. D un point de vue programmation, le type de donnée le plus approprié pour stocker le niveau de gris d un pixel est «unsigned char». Le format d image PGM est composé d une en-tête de quelques lignes (contenant notamment les dimensions de l image et des commentaires), suivi de la liste des niveaux de gris, enregistrés par ligne par ligne. Comme vous pouvez le constater en ouvrant l image «logo.pgm» avec un éditeur de texte, un fichier au format PGM se présente sous la forme suivante : P5 Mot clé indiquant un fichier PGM en mode binaire # Ligne de commentaires # Ligne de commentaires 66 300 Dimensions de l image (66 colonnes, 300 lignes) 255 Valeur du niveau de gris max Ž ƒƒƒ... Niveau de gris des pixels octet par octet Le niveau de gris d un pixel de l image est, en fait, le code ASCII d un caractère (ce qui justifie la présence des caractères spéciaux). Pour simplifier le programme, on supposera qu il n y a qu une seule ligne de commentaire dans l en-tête. ère partie Création, lecture et écriture d une image au format PGM Pour manipuler les fichiers contenant les images (ouverture, lecture, écriture), il est nécessaire de connaître les classes C++ appropriées. Allez donc voir la synthèse sur les flots d entrée/sortie à la fin de l énoncé (en plus du cours, bien sûr)! - Création et enregistrement d une image Programmez une classe Image répondant au programme principal suivant : int main() { int i; // Création d'une image de 00 lignes, 50 colonnes, // initialisée à 0 (noir) Image im( 00, 50, true ); // Dessine une diagonale blanche dans l'image for (i = 0; i<im.ligne(); i++) im.setpixel(i,i, (unsigned char) 255); // Enregistrement de l'image de nom toto.pgm avec un commentaire
im.writepgm ( "toto.pgm", "Ceci est un commentaire"); return 0 ; } En mémoire, l image pourra être codée comme un vecteur de unsigned char de dimension = Nb Lignes * Nb Colonnes. Ainsi, le pixel de la ligne l et de la colonne c de l image aura pour coordonnée dans le vecteur : l * Nb Colonnes + c. 2- Constructeur par recopie et surcharge de l opérateur = Codez les méthodes nécessaires pour répondre aux instructions suivantes : // Constructeur par recopie de im dans im2 Image im2( im ); im2.writepgm ( "titi.pgm", "Bonjour!"); // Surcharge de l opérateur = entre im et im3 Image im3( 200, 50, false ); im3 = im; 3- Lecture d une image Codez la méthode de lecture nécessaire pour lire une image. Pour vous assurer que votre lecture est correcte, vous pouvez l enregistrer, juste après la lecture, sous un nouveau nom : // Constructeur avec lecture directe Image im4( "logo.pgm" ); Im4.WritePGM ( "copielogo.pgm", "Duplication de l image logo.pgm"); Vérifiez avec votre logiciel de visualisation d images que l image originale et la copie sont bien identiques. Vous pouvez également ouvrir votre copie avec un traitement de texte pour vérifier que l entête est correcte (notamment le commentaire). 2 ère partie Filtrage d images Dans cette seconde partie, complétez la classe avec une méthode de filtrage d image (filtre de Sobel). Celle-ci est brièvement présentée ci-dessous. Pour vos expériences, vous pouvez utiliser l image «Blood.pgm». Le programme principal aura l allure suivante : // Lecture de l'image originale Image Blood( "blood.pgm" ); // Filtrage de l'image selon Sobel Blood.Sobel(); // Enregistrement de l'image Blood.WritePGM("bSobel.pgm", "Filtre Sobel Im.Orig= blood.pgm"); 0 Le filtre de Sobel à l allure suivante : S = 0 4 0 Le principe du filtrage consiste à appliquer le filtre sur tous les pixels de l image, de la manière suivant : Si (l,c) désigne les coordonnées du pixel à filtrer, le niveau de gris filtré du pixel est obtenu en sommant les niveaux de gris de ses voisins pondérés par les coefficients de la matrice S, puis en divisant cette somme par 4. Si N et M désignent respectivement le niveau de gris du pixel original de coordonnées lc, lc, (l,c), et le niveau de gris du pixel filtré de coordonnées (l,c), nous avons :
M l, c 4 ( N N N + N + N N ) = l, c l, c l, c l, c+ l +, c + l +, c+ Remarques : - Ne traitez pas les pixels au bord de l image (ils posent certaines difficultés qu il n est pas nécessaire de résoudre dans le cadre de cet exercice). - Le niveau de gris filtré n est pas forcément un nombre entier positif De plus, la valeur obtenue n est pas forcément comprise entre 0 et 255 A vous de faire les traitements adéquats Examinez l image ainsi filtrée et interprétez le résultat à l aide des coefficients du filtre D autres filtres de même forme sont possibles, en voici quelques uns : Filtre de Prewitt : 2 P = 6 0 0 0 2, Filtre Laplacien : L = 9.96 Vous pouvez également combiner des filtres et en inventer vous-même! 0.45 0.45 4.958 3 ème partie Lecture / Ecriture d images couleur au format PPM 0.45 0.45 Il s agit de créer une nouvelle classe permettant de manipuler des images couleurs au format PPM. Créez un second projet et utilisez les noms de fichier suivants ImageCouleur.h et ImageCouleur.cpp. Le format PPM, qui permet d enregistrer des images couleurs, est presque identique au format PGM aux différences suivantes : - Dans l en-tête, nous avons P6, dans la première ligne, au lieu de P5. - Les données sont organisées de la manière suivante : la suite des valeurs est constituée des intensités de chaque pixel de l'image, dans les trois canaux (rouge), (vert) et (bleu). Chacune de ces intensités est codée sur un octet, donc a une valeur entière comprise entre et. Quels sont vos attributs? Outre les méthodes de la classe précédente qu il est possible d étendre à notre cas, proposez également une méthode qui permette de transformer une image couleur en une image à niveaux de gris. Dans ce cas, vous considèrerez que le niveau de gris est égale à la moyenne des valeurs sur les trois canaux R, V, B). Comment filtrer une image couleur? Pour vos tests, vous utiliserez l image «flowers.ppm».
Annexe Synthèse sur les flots associés aux fichiers (classes ofstream et ifstream) Jusqu à présent, nous avons parlé de flots prédéfinis (cout et cin) mais sans dire comment ce flot pourrait être associé à un fichier. Cette synthèse montre comment y parvenir et examine les possibilités d accès direct dont on peut alors bénéficier. Attention, les méthodes offertes par ces deux classes sont bien plus nombreuses que celles exposées. Néanmoins, les méthodes présentées devraient suffire pour réaliser le sujet. - Connexion d un flot de sortie à un fichier Pour associer un flot de sortie à un fichier, il suffit de créer un objet de type ofstream. L emploi de cette nouvelle classe nécessite d inclure un fichier d en-tête nommé fstream.h, en plus de iostream.h. Le constructeur de la classe nécessite deux arguments : - Le nom du fichier concerné (chaîne de caractères) - Un mode d ouverture défini par une constante (cf. table ) Voici un exemple de déclaration d un objet du type ofstream : ofstream sortie ("truc.dat", ios::out ios::binary); L objet sortie sera donc associé au fichier nommé truc.dat, après avoir été ouvert en écriture. Une fois construit, l écriture dans le fichier associé peut se faire comme n importe quel flot (en particulier cout). Pour tester si l ouverture s est bien passée, on pourra utiliser if (!sortie.is_open() ) { }; Pour fermer le fichier, on utilisera sortie.close(); Pour écrire dans un fichier, nous pourrons employer : sortie << << << endl; pour écrire en mode texte et int temp = 0 ; sortie.write( temp, sizeof(int) ); ou char buf[80]="bonjour!"; sortie.write( buffer, strlen(buf)*sizeof(char)); pour écrire en mode binaire 2- Connexion d un flot d entrée à un fichier Pour associer un flot d entrée à un fichier, on emploie un mécanisme analogue à celui utilisé pour un flot de sortie. Il faut toujours inclure le fichier d en-tête fstream.h en plus du fichier iostream.h. Le constructeur comporte les mêmes arguments que précédemment, c est à dire le nom du fichier et le mode d ouverture. Par exemple avec l instruction : ifstream entree ("truc.dat", ios::in ios::binary); l objet entree sera associé au fichier de nom truct.dat, après avoir été ouvert en lecture. Pour tester si l ouverture s est bien passée, on pourra utiliser if (!entree.is_open() ) { }; Pour fermer le fichier, on utilisera entree.close(); Pour lire dans un fichier, nous pourrons employer : entree >> >> ; pour lire en mode texte et int temp ; entree.read( temp, sizeof(int));
ou char buffer[80]; entree.getline(buffer, 80); pour lire en mode binaire. 3- Les possibilités d accès direct La méthode read( ) par exemple permet de lire séquentiellement dans un fichier, c est à dire depuis le début de ce fichier jusqu à sa fin. L incrémentation dans le fichier (pour se préparer à la prochaine lecture) est effectuée automatiquement par read( ). Ceci peut s avérer contraignant lorsque l on souhaite lire une information à une position connue dans le fichier. Des possibilités d accès directe sont donc offertes, en agissant sur un pointeur dans ce fichier, c est à dire en précisant le rang du prochain octet à lire ou à écrire. Les classes ifstream et ofstream offrent respectivement des méthodes de positionnement du pointeur, appelées seekg( ) et seekp( ). Ces méthodes comportent deux arguments : - Un entier représentant un déplacement du pointeur, par rapport à une origine précisée par le second argument - Une constante entière choisie parmi les trois possibilités présentées dans la table 2. Exemples : entree.seekg(0, ios::cur); sortie.seekp(0, ios::end); Par ailleurs, il existe dans chacune des classes ifstream et ofstream une fonction permettant de connaître la position courante du pointeur. Il s agit de tellg( ) pour ifstream et de tellp( ) pour ofstream. Exemples : int position = entree.tellg(); int position2 = sortie.tellp(); 4- Modes d ouverture et modes de déplacement Constante Action ios::in Ouverture en lecture (obligatoire pour la classe ifstream) ios::out Ouverture en écriture (obligatoire pour la classe ofstream) ios::app Ouverture en ajout de données (écriture en fin de fichier) ios::trunc Si le fichier existe, son contenu est perdu ios::binar Le fichier est ouvert en mode binaire. Ce mode est uniquement y utilisé pour les systèmes d exploitation (comme windows!) qui distinguent les fichiers texte des autres. Table : Modes d ouverture d un fichier Constante Action ios::beg Le déplacement est exprimé par rapport au début du fichier ios::cur Le déplacement est exprimé par rapport à la position actuelle ios::end Le déplacement est exprimé par rapport à la fin du fichier Table 2 : Modes de positionnement dans un fichier