Université de Savoie Module ETRS711 Travaux pratiques Compression en codage de Huffman 1. Organisation du projet 1.1. Objectifs Le but de ce projet est d'écrire un programme permettant de compresser des fichiers textes, sans perte d'information, afin qu'ils occupent moins d'espace sur un disque. On demande également d'écrire un décompresseur qui devra restaurer le fichier original. Les algorithmes de compression sont nombreux. Celui proposé ici est l'algorithme de Huffman. Nous nous proposons de le coder en langage C. 1.2. Notation du projet La notation sera faite au terme d’une présentation de votre travail. La forme de cette présentation est laissée totalement libre (Power Point, film, exposé oral en français, en anglais, animation, documents écrits…). Un vidéo-projecteur est à votre disposition si besoin. Le temps de cette présentation est de 5 minutes (démonstrations comprises). Vous devrez être donc extrêmement concis et préparer toutes vos démonstrations au préalable afin de limiter le temps de manipulation. 1.3. Organisation d’un projet de programmation L’ensemble du projet devra obligatoirement comporter : · Toutes les versions de votre logiciel en commençant par huff_v0.c. Chaque amélioration de votre logiciel vous fera passer à la version supérieure et vous conserverez une sauvegarde de la dernière version qui fonctionne. · Un fichier « version.txt » dans lequel vous écrirez la description de chacune des versions de votre logiciel que vous avez écrit. Il est préférable que vous respectiez les propositions de prototypage des fonctions faites dans l’énoncé afin que le debugage soit plus aisé. 2. Principe de l'algorithme de Huffman Une idée apparue très tôt en informatique pour compresser les données a été exploitée indépendamment dans les années 1950 par Shannon et Fano. Elle est basée sur la remarque suivante : les caractères d'un fichier sont habituellement codés sur un octet, donc tous sur le même nombre de bits. Il serait plus économique en terme d'espace disque, pour un fichier donné, de coder ses caractères sur un nombre variable de bits, en utilisant peu de bits pour les caractères fréquents et plus de bits pour les caractères rares. Le codage choisi dépend donc du fichier à compresser. Les propriétés d'un tel codage sont les suivantes : a. Les caractères sont codés sur un nombre différent de bits (pas nécessairement un multiple de 8) ; b. Les codes des caractères fréquents sont courts ; ceux des caractères rares sont longs ; c. Bien que les codes soient de longueur variable, on peut décoder le fichier compressé de façon unique. d. La dernière de ces trois propriétés est automatiquement assurée si l'on à la propriété suivante : Si c1 et c2 codent deux caractères différents, c1 ne commence pas par c2 et c2 ne commence pas par c1. En effet, si la propriété d est assurée, lorsqu'on décode le fichier compressé en le lisant linéairement, dès que l'on reconnaît le code d'un caractère, on sait que l'on ne pourra pas le compléter en un autre code. L'algorithme de Huffman, qui garantit ces propriétés, fonctionne de la façon suivante : 2.1. Calcul des occurrences des caractères On prend l’exemple d’un fichier texte dont le contenu est « Une banane ». · On calcule tout d'abord le nombre d’occurrence de chaque caractère dans le fichier à compresser. Dans l’exemple ci-dessous, on a le nombre d’occurrence du caractère ‘espace’, ‘U’, ‘b’, ‘n’, ‘a’ et ‘e’. Figure 1 : Fréquence des caractères 2.2. Création de l’arbre de Huffman On prend ensuite les caractères qui possèdent la plus faible occurrence et on ajoute leur nombre d’occurrence pour réaliser un nouveau nœud. Figure 2 : Création d’un nouveau nœud On fait exactement la même chose en reprenant en compte le nouveau nœud (et non plus les caractères espace et U). Dans l’exemple précédent l’occurrence la plus faible est celle de b et celle du nouveau nœud (on aurait pu prendre aussi ‘a’ ou ‘e’ puisque leur valeur est 2 aussi, cela revient au même à la fin). Figure 3 : Création d’un autre nouveau nœud IMPORTANT : Dans toute la suite du TP, on appellera « une feuille » de l’arbre les éléments qui sont en bout de l’arbre (les caractères). On appellera un nœud les éléments regroupant deux feuilles (ou deux nœuds). Figure 4 : Création complète de l'arbre de Huffman Avec d'autres choix, on peut obtenir des arbres différents, mais l'algorithme fonctionne avec tout choix respectant la construction ci-dessus. À partir de l'arbre, on construit le code d'un caractère en lisant le chemin qui va de la racine au caractère. Un pas à gauche étant lu comme 0 et un pas à droite comme 1. Figure 5 : Arbre de Huffman avec le codage des caractères On lit le code des caractères en descendant depuis le haut jusqu’au caractère. Figure 6 : Code binaire de chaque caractère Dans l'exemple précédent, on obtient la suite de bits 0001011100000011001100111 pour le codage du mot « Une banane ». On peut voir que ce codage vérifie toujours les propriétés a) à d), et permet donc la compression et la décompression. Avec cet algorithme, le décompresseur doit connaître les codes construits par le compresseur. Lors de la compression, le compresseur écrira donc ces codes en début de fichier compressé, sous un format à définir, connu du compresseur et du décompresseur. Le fichier compressé aura donc deux parties disjointes : · Une première partie permettant au décompresseur de retrouver le code de chaque caractère; · Une seconde partie contenant la suite des codes des caractères du fichier à compresser. Attention, le décompresseur doit toujours pouvoir trouver la séparation entre ces deux parties. Aussi, vous pouvez toujours vérifier le code générer par votre algorithme en vérifiant que le code générer pour coder un caractère ne doit pas ressembler au début d’un code d’un autre caractère. 3. Travail demandé 3.1. Première partie : Occurrences des caractères L’objectif est de travailler avec des fichiers (qui par la suite seront les fichiers à compresser). Il faut donc que nous maitrisions les fonctions de manipulation de fichier. Q1. Réaliser un programme qui ouvre un fichier texte et qui affiche une partie du contenu à l’écran. (Utiliser la fonction fread()). Q2. Réaliser un programme qui ouvre un fichier texte et qui affiche la totalité du contenu du fichier à l’écran. Vous ferez une lecture grâce à la fonction fgetc( ) jusqu'à ce que vous rencontriez le caractères de fin de fichier (EOF=End Of File). Nous nous intéressons au comptage des occurrences des caractères présents dans le fichier texte. Pour cela nous utiliserons un tableau de caractère appelé tab_caractere[256]. Ce tableau comporte 256 éléments. Dans chaque case de ce tableau, nous rentrerons le nombre d’occurrence du caractère dont le code ASCII est donné par l’index de tab_caractère. Exemple : Le code ASCII du ‘a’ est 97. tab_caractere[97] comportera donc le nombre d’occurrence du caractère ‘a’. Q3. Coder une fonction dont le prototype est [ void occurence(FILE* file, int tab[256]) ]. Cette fonction comptera les occurrences des caractères du fichier ‘file’, et stockera les occurrences dans le tableau ‘tab’ passé en paramètre. Afficher à l’écran une partie du tableau pour vérifier le fonctionnement. 3.2. Seconde partie : Réalisation de l’arbre de Huffman Pour chaque caractère (ou nœud de l’arbre), nous allons créer une structure commune qui nous permettra de créer l’arbre et de créer le code. Nous avons appelé cette structure « nœud » mais en réalité elle sera aussi utilisée pour les feuilles de l’arbre. La structure est donc définie comme suit : 1. struct noeud{ 2. unsigned char c; /* Caractère initial*/ 3. unsigned int occurence; /* Nombre d'occurrences dans le fichier */ 4. int code; /* Codage dans l'arbre */ 5. int bits; /* Nombre de bits sur lesquels est codée le caractère */ 6. struct noeud *gauche, *droite; /* Lien vers les nœuds suivants */ 7. }; La seule différence entre un nœud et une feuille, est que pour une feuille, les pointeurs vers les nœuds suivant sont NULL. C’est de cette façon que nous les distinguerons. Q4. Réaliser une boucle dans votre programme afin de créer dynamiquement une structure ‘nœud’ pour chacun des caractères contenu dans le fichier. Vous testerez votre programme en affichant le contenu du champ ‘c’ caractère et ‘occurrence’ de chacune des structures créées. Chacune de ces structures seront réservées en mémoire par la fonction malloc(). De plus, afin de conserver l’ensemble des structures créées, nous sauvegarderons les pointeurs sur ces structures nœud (struct nœud*) dans le tableau suivant : [struct noeud* arbre_huffman[256] ] Explications complémentaires : On doit donc créer pour chacun des caractères de notre tableau tab_caractere[256] une structure nœud comme suit : struct nœud c=’ ‘ Occurrence=1 gauche=NULL droite=NULL bits=0 code=0 struct nœud c=’U‘ Occurrence=1 gauche=NULL droite=NULL bits=0 code=0 struct nœud c=’b‘ Occurrence=1 gauche=NULL droite=NULL bits=0 code=0 struct nœud c=’n‘ Occurrence=3 gauche=NULL droite=NULL bits=0 code=0 struct nœud c=’a‘ Occurrence=2 gauche=NULL droite=NULL bits=0 code=0 struct nœud c=’e‘ Occurrence=2 gauche=NULL droite=NULL bits=0 code=0 Afin de conserver les références sur chacune des structures créées, il faut que nous conservions l’adresse de ces structures. Nous allons donc les conserver dans un tableau appelé : [ struct nœud* arbre_huffman[256] ] Ce tableau Struct nœud* arbre_huffman[256] aura donc le contenu suivant : [0] [1] [2] […] […] [5] Adresse de la structure « nœud » du caractère ‘ ‘ Adresse de la structure« nœud » du caractère ‘U‘ Adresse de la structure « nœud »du caractère ‘b‘ … … Adresse de la structure « nœud »du caractère ‘e‘ Q5. Réalisez exactement la même chose mais cette fois ci en faisant appel à une fonction dont le prototype est le suivant : [ struct noeud* creer_feuille(int* tab, int index )] Nous allons maintenant faire apparaître des nouveaux nœuds dans l’arbre comme préciser dans la Figure 2. Q6. En dehors du contexte de cet algorithme de compression, réaliser une fonction pouvant chercher dans un tableau les deux entiers les plus petits. Exemple : int tab[5]={6,0,4,2,1} >>> Le premier entier le plus petit est 0 à l’index 1. >>> Le second entier le plus petit est 1 à l’index 4. Note : Une façon simple de rechercher les deux entiers les plus petits est de trier le tableau et de récupérer les valeurs à l’index 0 et 1. Une fois que cette fonction est validée, vous pouvez parcourir votre tableau de Huffman [struct noeud* arbre_huffman[256] ] afin de rechercher les nœuds comportant la plus petite occurrence. Dès lors que vous avez trouvé ces deux nœuds, vous aller créer un nouveau nœud dont la valeur d’occurrence vaut la somme des deux nœuds d’origine comme préciser dans la Figure 2. Q7. Réaliser la fonction [ void creer_noeud(struct noeud* tab[], int taille) ] qui réalisera la recherche des plus petites occurrences, fera la création du nouveau nœud et le sauvegardera dans le même tableau de Huffman. Note : il y a deux emplacements possibles pour le nouveau nœud. « ! » est le nouveau nœud. Figure 7 : Création du tableau de Huffman avec un nouveau nœud A la fin de la création de l’arbre, vous devez obtenir une seule référence (pointeur sur structure) qui est celle du haut de l’arbre binaire, et qui possède dans les champs « droite » et « gauche » les références des deux nœuds suivant (qui eux même possèdent deux références des deux nœuds suivants, etc, etc…) 3.3. Troisième partie : Création du code En parcourant l’arbre binaire depuis le haut de l’arbre, nous allons créer les codes comme nous l’avons exposé à la Figure 5. La méthode pour le parcours de l’arbre de la racine jusqu’au feuille est donné par le code C suivant. Il s’agit d’une fonction récursive, c'est-à-dire qu’elle s’appelle elle-même : · niveau : nombre de bit du code de chaque caractère · element : un nœud · code : code du caractère 1. void creer_code(struct noeud* element,int code,int niveau){ 2. //On est sur une feuille 3. if(element->droite==NULL && element->gauche==NULL){ 4. element->bits=niveau; 5. element->code=code; 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. } Q8. else{ printf("\n %c\t %d\t",element->c,element->occurence); } //On va a droite (on injecte un 0 à droite) creer_code(element->droite,code<<1,niveau+1); // On va a gauche (On injecte un 1 à droite) creer_code(element->gauche,(code<<1)+1,niveau+1); } Réaliser la création du code de votre arbre binaire Q9. Dans la fonction créer code, lorsque vous êtes dans une feuille et que vous avez trouvé le code correspondant, faites appel à une fonction [ void affichage_code(int nbr_bits, int codage) ] qui permettra d’afficher le code du caractère sous forme binaire. int nbr_bits : Correspond au nombre de bits du code ( champ bit de la structure) int codage : Correspond au code (champ code de la structure) Dans notre exemple nous aurons l’affichage suivant : Caractere ‘ ‘ Code : 0000 Caractere U Code : 0001 Caractere b Code : 001 Caractere n Code : 01 etc… La compression va maintenant pouvoir avoir lieu. Pour cela, nous allons modifié un peu la fonction créer_code() afin de sauvegarder toutes les liens (pointeur) vers les structures « noeud » des caractères existants dans le fichier. Ceci sera très similaire au tableau tab_caractere du début (int tab_caractere[256]). Nous conservons donc le pointeur sur la structure de chacun des caractères du fichier dans le tableau : Struct nœud * alphabet[256] Le positionnement dans ce tableau sera fait de la même façon que pour tab_caractere, c'est-àdire : Le code ASCII du ‘a’ est 97. alphabet[97] comportera donc le pointeur vers la structure nœud du caractère « a ». On aura donc directement accès au code de « a » et au nombre de bit du code par : alphabet[97]->code ; alphabet[97]->bits ; 3.4. Quatrième partie : Compression Le fichier final devra posséder 2 parties : · · Une partie « entête » possédant le nombre de feuilles différentes (nombre de caractères différents) de votre fichier, et la structure « alphabet » correspondant à chacun de ces caractères. Une partie « contenu » possédant le code compressé des caractères du fichier. Q10. Réaliser une entête dans votre fichier et faite en la relecture immédiate. Grâce à cette entête le logiciel de décompression pourra prendre connaissance des caractères présent dans le fichier, et pour chacun d’entre eux connaître son code. Note : Entre la création de l’entête, et sa relecture, vous pouvez effacer le tableau struct nœud * alphabet[256]. En effet, l’objectif même de la relecture est justement de recréer ce tableau sans en avoir la connaissance au préalable. Q11. Ecrire l’ensemble des codes correspondant à chaque caractère du fichier d’origine les uns à la suite des autres. 3.5. Cinquième partie : Décompression L’objectif est d’une part de recréer le tableau struct nœud * alphabet[256] (déjà fait). D’autre part il nous faut recréer le tableau de Huffman afin de décoder les bits du fichier compresser. Le travail que nous avons effectué au début de l’algorithme était : Il nous faut maintenant réaliser le travail inverse. Grâce à l’arbre reconstitué, lorsque nous allons lire chacun des bits du fichier compressé, nous descendrons dans les nœuds de l’arbre, jusqu'à ce que nous trouvions une feuille (un nœud avec des valeurs NULL pour les pointeurs droite et gauche). Q12. Réaliser la création « inverse » de l’arbre de Huffman depuis le tableau struct nœud * alphabet[256]. Q13. Parcourir votre arbre pour chacun des bits lus et afficher la valeur du caractère décompressé. BON COURAGE