Algorithme de compression de Huffman en C++
Date de publication : 8/03/2007 , Date de mise à jour : 8/03/2007
Par
gRRosminet
Nous allons voir dans ce tutoriel une implémentation dans le langage C++ de l'algorithme de Huffman pour la compression de données sans perte.
I. Introduction et remerciements
II. Outils nécessaires
II-A. Statistiques
II-B. Arbre de Huffman
II-C. Compression/Décompression
III. Implémentation de l'objet
III-A. Définition de la classe
III-B. Définition des outils connexes
III-C. Codage des méthodes
IV. Remarques et conclusion
IV-A. Compilateur
IV-B. Optimisation
I. Introduction et remerciements
Si le principe de la compression de Huffman peut paraître simple, sa mise en oeuvre n'en n'est pas moins une aventure périlleuse. C'est ce que nous allons voir dans cet article. Je tiens à préciser que mon objectif premier à été de réaliser une solution fonctionnelle et qu'elle n'est absolument pas optimisée. Avant de vous plonger dans cet exemple, je vous conseille fortement de
jeter un oeil à l'explication de l'algorithme.
Vous pouvez télécharger le code source complet
ici
II. Outils nécessaires
Tout d'abord, essayons d'envisager tout ce dont nous aurons besoin pour ce développement.
II-A. Statistiques
Nous aurons besoin d'une liste de fréquences d'apparition des fragments. Dans notre cas, le fragment est un octet et on peut donc faire un tableau de 256 statistiques sans avoir à se soucier de l'espace mémoire que cela représente puisque c'est plutot restreint.
Le format d'une statistique :
- On a besoin de savoir quel est l'élément : son indice dans le tableau l'indiquera, il n'est donc pas nécessaire de le stocker ;
- On a besoin de connaitre la fréquence d'apparition du fragment : les fichiers pouvant faire jusqu'à 4Go, il faut dont utiliser un usigned long int pour compter le nombre d'apparition. La fréquence d'apparition n'est pas nécessaire, on ne la stockera donc pas ;
- Finalement, on aura besoin de stocker le code de Huffman associé au fragment. J'ai fait le choix d'une chaîne de caractères qui a l'avantage de simplifier le débuggage. Mais le désavantage d'être plutot lent.
Pour calculer les statistiques, nous aurons besoin d'une fonction qui parcourt tout le fichier d'entrée et incrémente les compteurs des fragments.
II-B. Arbre de Huffman
Nous aurons besoin d'une structue d'arbre binaire dont les noeux prennent en charge la comparaison. nous aurons également besoin d'une liste triée pour ordonner les noeuds et les morceaux d'arbre je propose d'utiliser la file de priorité de la bibliothèque de templates standards du C++. Le problème est qu'en général, on utilise des listes de pointeurs pour éviter d'avoir des temps de recopie des éléments trop importants. Je propose donc de faire un objet template qui propose les opérateurs de comparaison et un pointeur vers un type d'objet.
Nous aurons également besoin d'une fonction qui associe les éléments les plus légers 2 à 2 jusqu'a ce que la liste n'en contienne plus qu'un et une qui détermine le code de Huffman de chaque élément.
II-C. Compression/Décompression
Nous aurons besoin d'une fonction qui permette d'écrire l'entête de compression associée au fichier et qui effectue la compression. Cette dernière devra s'assurer que les étapes d'analyse et d'écriture de l'entête ont été réalisées correctement avant de réaliser la compression.
Finalement nous aurons également besoin de fonctions réalisant l'opération inverse : lecture de l'entête de compression, reconstruction de l'arbre puis décompression.
III. Implémentation de l'objet
Pour des raisons de clarté du point de vue du compilateur et afin d'éviter tout enchevêtrement possible, tous les développements seront fait dans le namespace Huffman qui contiendra donc tous les outils nécessaires à ce développement et donc l'utilisation dans d'autres projets n'est pas envisageable.
III-A. Définition de la classe
La classe contient des informations sur les fichiers mis en jeux, ainsi que des pointeurs vers les éléments de compression qui seront eux alloués dynamiquement. J'ai également inclu des pointeurs de fonctions permettant de faire communiquer l'objet avec l'application principale et que celle-ci puisse afficher des informations à l'utilisateur final. Toutes les fonctions nécessaires dont nous avons énoncé les propriétés ci-avant ont été prévues et elles sont acompagnées des fonctions nécessaires à la gestion des messages utilisateurs.
J'ajouterai que si vous prêtez un peu attention au code, vous remarquerez que le type choisi pour le stockage du nombre d'éléments est un unsigned short int et non pas un unsigned char alors qu'on va compter des éléments de type char. Cela est dû au fait qu'on peut avoir un nombre quelconque d'élément (entre 0 et 255).
| Classe THuffman |
class THuffman
{
private :
bool bCompresse;
std::string sSource;
std::string sDestination;
TNoeudHuffman * ArbreHuffman;
unsigned short int NombreElements;
unsigned long int TailleReelle;
unsigned long long int Finentête;
TStatistiqueHuffman * Statistiques;
TDegreDialogue DegreVerbalite;
TFoncMessage FoncAffichageProgression;
TFoncMessage FoncAffichageEtape;
unsigned int ErrCode;
std::string sErrString;
protected :
virtual bool Ecrireentête();
virtual bool Lireentête();
bool ConstruireArbreFreq();
bool ConstruireArbreCode();
public :
explicit THuffman(const std::string &sSrc = std::string(""),
const std::string &sDst = std::string(""),
TDegreDialogue Dialogue = ddBavard,
const TFoncMessage Progres = NULL,
const TFoncMessage Etape = NULL);
virtual ~THuffman();
virtual bool Analyser();
bool Compresser();
bool Decompresser();
unsigned long int TailleInitiale();
unsigned long int TailleCompresse();
unsigned long int TailleFinale();
void SetSource(const std::string &sSrc);
void SetDestination(const std::string &sDst);
void SetDegreDialogue(const TDegreDialogue v);
void SetFonctionProgression(TFoncMessage Progres);
void SetFonctionEtape(TFoncMessage Etape);
std::string GetSource() const;
std::string GetDestination() const;
TDegreDialogue GetDegreDialogue() const;
unsigned int GetDerniereErreur(std::string &sMessage) const;
static void AfficherMessage(const std::string &sMessage);
};
|
III-B. Définition des outils connexes
Comme vous avez pu le voir, j'utilise des types qui ne sont pas standards, je les ai définis un peu plus haut dans mon entête.
Commencons par TDegreDialogue : c'est un type énuméré utilisé pour la gestion des message d'information de l'utilisateur.
typedef enum {ddMuet = 0, ddProgression = 1, ddEtape = 2, ddBavard = 3}
TDegreDialogue;
|
TFoncMessage : C'est le type des fonctions auxquelles mon objet enverra les messages sur la progression des opérations.
|
typedef void (*TFoncMessage)(const std::string &) ;
|
TStatistiqueHuffman : c'est la structure que j'ai utilisée pour compter le nombre d'apparitions de chaque élément. La méthode CreerNoeudHuffman() permet de générer un noeud (feuille) à utiliser dans l'arbre de Huffman. J'ai fais le choix de stocker le code de Huffman dans la statistique pour des raisons de facilité d'accès lors de la compression.
|
struct TStatistiqueHuffman
{
unsigned long int Frequence;
std::string sCode;
TStatistiqueHuffman() : Frequence(0), sCode("") {}
TNoeudHuffman * CreerNoeudHuffman(unsigned char Valeur);
};
|
TNoeudHuffman : cette structure permet de construire l'arbre et contient un pointeur vers la string du code de Huffman dans le cas d'une feuille. J'aurrai pu utiliser le principe de l'héritage et faire deux structures : noeud et feuille, mais je suis trop fainéant pour ca ;-) Ceci dit, vous pouvez toujours le faire pour votre propre dévelopement. Cette structure propose une méthode pour déterminer le code de Huffman des feuilles ainsi qu'une autre pour recréer une feuille dans l'arbre a partir d'un code de Huffman. J'ai bien entendu intégré quelques opérateurs nécessaires au fonctionnement de la file de priorité que nous utiliserons pour la construction de l'arbre. J'ai également déclaré un type pointeur sur ce type de structure.
|
struct TNoeudHuffman
{
unsigned char Valeur;
unsigned int Frequence;
std::string * lpsCode;
TNoeudHuffman * FilsG;
TNoeudHuffman * FilsD;
inline TNoeudHuffman(unsigned char Val = 0, unsigned int Freq = 0,
std::string * lpsCodeStr = NULL,
TNoeudHuffman * Fg = NULL, TNoeudHuffman * Fd = NULL)
: Valeur(Val), Frequence(Freq), lpsCode(lpsCodeStr), FilsG(Fg),
FilsD(Fd){}
inline ~TNoeudHuffman()
{
if (FilsG) delete FilsG;
if (FilsD) delete FilsD;
}
void DeterminerCodeHuffman(const std::string &sCode) const;
void CreerFeuille(const std::string &sCode, unsigned char valeur);
friend bool operator<(const TNoeudHuffman &A, const TNoeudHuffman &B);
friend bool operator>(const TNoeudHuffman &A, const TNoeudHuffman &B);
friend bool operator==(const TNoeudHuffman &A, const TNoeudHuffman &B);
};
typedef TNoeudHuffman * PNoeudHuffman;
|
Finalement, je vais vous présenter l'outil dont je vous parlais pour pouvoir utiliser les objets de types divers avec la file de priorité. Il est extrement simple et propose divers opérateurs qui sont mappés sur ceux de l'objet sur lequel il pointe.
|
template<class T>
class PComparateur
{
private :
T * pointeur;
public :
inline explicit PComparateur(T *p = NULL) : pointeur(p) {}
inline explicit PComparateur(T &p) : pointeur(&p) {}
inline T * get() const { return pointeur; }
inline void set(T *p) { pointeur = p; }
inline void set(T &p) { pointeur = &p; }
friend bool operator<(const PComparateur<T> &A, const PComparateur<T> &B)
{
return *(A.pointeur) < *(B.pointeur);
}
friend bool operator>(const PComparateur<T> &A, const PComparateur<T> &B)
{
return *(A.pointeur) > *(B.pointeur);
}
friend bool operator<=(const PComparateur<T> &A, const PComparateur<T> &B)
{
return *(A.pointeur) <= *(B.pointeur);
}
friend bool operator>=(const PComparateur<T> &A, const PComparateur<T> &B)
{
return *(A.pointeur) >= *(B.pointeur);
}
friend bool operator==(const PComparateur<T> &A, const PComparateur<T> &B)
{
return *(A.pointeur) == *(B.pointeur);
}
friend std::ostream &operator<<(std::ostream &out, const PComparateur<T> &B)
{
return out << *(B.pointeur);
}
friend std::istream &operator>>(std::istream &in, const PComparateur<T> &B)
{
return in >> *(B.pointeur);
}
};
|
III-C. Codage des méthodes
Dans cette partie, je vais simplement parcourir le fichier de code et vous expliquer les méthodes une à une.
DeterminerCodeHuffman : L'utilisation d'une chaîne de caractères rend la détermination du code de Huffman très simple par l'utilisation d'une fonction récursive : tout ce qu'il y a à faire est un parcours en profondeur de l'arbre en concaténant un caractère '0' ou '1' au code du noeud actuel suivant si on prend le fils gauche ou le fils droite.
|
void TNoeudHuffman::DeterminerCodeHuffman(const std::string &sCode) const
{
if (lpsCode)
*lpsCode = sCode;
else
{
if (FilsG)
FilsG->DeterminerCodeHuffman(sCode + "0");
if (FilsD)
FilsD->DeterminerCodeHuffman(sCode + "1");
}
}
|
CreerFeuille : C'est exactement l'inverse de la fonction précédente : on va "manger" un caractère a chaque étape et créer les noeuds du chemin au fur et à mesure si nécessaire, jusqu'a ce qu'on arrive à la feuille.
|
void TNoeudHuffman::CreerFeuille(const std::string &sCode,
unsigned char valeur)
{
TNoeudHuffman * * p;
if (sCode[0] == '0')
p = &FilsG;
else
p = &FilsD;
if (sCode.length() > 1)
{
if (!(*p))
*p = new TNoeudHuffman();
(*p)->CreerFeuille(sCode.substr(1,sCode.length()),valeur);
}
else
{
*p = new TNoeudHuffman(valeur);
p = p;
}
}
|
Je fais l'impasse sur les opérateurs, le constructeur et le destructeur qui ne présentent pas d'intérêt particulier.
Analyser : Cette fonction est extrêmement simple : elle alloue dynamiquement la mémoire nécessaire aux statistiques puis compte le nombre d'apparitions de chaque caractère du fichier.
| Classe THuffman |
bool THuffman::Analyser()
{
std::ifstream fSrc;
char Valeur;
char BufferProgression[10];
unsigned long int Taille = TailleInitiale();
unsigned long int Position = 0;
if (DegreVerbalite & ddEtape)
FoncAffichageEtape("Analyse du fichier source");
if (sSource.length())
fSrc.open(sSource.c_str(), std::ios::in | std::ios::binary);
else
{
ErrCode = ERRCODE_FICHIERINDEFINI;
sErrString = "Veuillez préciser le nom du fichier source";
return false;
}
if (fSrc.is_open())
{
if (DegreVerbalite & ddProgression)
FoncAffichageProgression("0%");
if (Statistiques)
delete [] Statistiques;
Statistiques = new TStatistiqueHuffman[256];
while (!fSrc.eof())
{
fSrc.read((char *)(&Valeur),sizeof(char));
Statistiques[(unsigned char)(Valeur)].Frequence++;
sprintf(BufferProgression,"%d%%",int(float(Position)/Taille*100));
if (DegreVerbalite & ddProgression)
FoncAffichageProgression(BufferProgression);
Position++;
}
fSrc.close();
return true;
}
else
{
ErrCode = ERRCODE_OUVERTURELECTURE;
sErrString = "Impossible d'ouvrir le fichier d'entrée en lecture";
return false;
}
}
|
Compresser : Cette fonction s'arrange pour que toutes les étapes nécessaires à la compression soient effectuées (analyse, arbre, entête) puis lit le fichier source et concatène le code correspondant à chacun des caractères dans une chaîne de buffer. Dès que la chaîne de buffer est suffisament remplie (32 bits), on utilise alors un bitset de 32 bits pour convertir la sous-chaîne des 32 premiers caractères en un entier long non-signé qui est alors écrit dans le fichier de destination. Lorsque tout le fichier source a été lu, le reste du buffer est rempli de 0 pour arriver à 4 octets puis écrit sur le disque. La compression est finie
|
bool THuffman::Compresser()
{
typedef std::bitset<32> TDataBuffer;
std::ifstream fSrc;
std::ofstream fDst;
char Valeur;
char BufferProg[10];
unsigned long int Position = 0;
std::string BufferOut;
TDataBuffer Data;
unsigned long int uData;
TailleReelle = TailleInitiale();
if (!Statistiques)
if (!Analyser())
return false;
if (!ArbreHuffman)
if (!ConstruireArbreFreq())
return false;
if (!Ecrireentête())
return false;
if (DegreVerbalite & ddEtape)
FoncAffichageEtape("Compression en cours");
if (sSource.length())
{
fSrc.open(sSource.c_str(), std::ios::in | std::ios::binary);
if (fSrc.is_open())
{
if (sDestination.length())
fDst.open(sDestination.c_str(), std::ios::app | std::ios::binary);
else
{
ErrCode = ERRCODE_FICHIERINDEFINI;
sErrString = "Veuillez préciser le nom du fichier de destination";
return false;
}
}
else
{
ErrCode = ERRCODE_OUVERTURELECTURE;
sErrString = "Impossible d'ouvrir le fichier d'entrée en lecture";
return false;
}
}
else
{
ErrCode = ERRCODE_FICHIERINDEFINI;
sErrString = "Veuillez préciser le nom du fichier source";
return false;
}
if (fDst.is_open())
{
if (DegreVerbalite & ddProgression)
FoncAffichageProgression("0%");
fSrc.read((char *)(&Valeur),sizeof(char));
while (!fSrc.eof())
{
BufferOut += Statistiques[(unsigned char)(Valeur)].sCode;
if (BufferOut.length() > 31)
{
Data = static_cast<TDataBuffer>(BufferOut.substr(0,32));
uData = Data.to_ulong();
fDst.write((char *)(&uData),sizeof(unsigned long int));
if (!fDst.good())
{
ErrCode = ERRCODE_ERREURECRITURE;
sErrString = "Erreur lors de l'écriture des données";
return false;
}
BufferOut.erase(0,32);
}
sprintf(BufferProg,"%d%%",int(float(Position) / TailleReelle * 100));
if (DegreVerbalite & ddProgression)
FoncAffichageProgression(BufferProg);
Position++;
fSrc.read((char *)(&Valeur),sizeof(char));
}
Data.reset();
BufferOut.resize(32,'0');
Data = static_cast<TDataBuffer>(BufferOut);
uData = Data.to_ulong();
fDst.write((char *)(&uData),sizeof(unsigned long int));
fSrc.close();
fDst.close();
}
else
{
ErrCode = ERRCODE_OUVERTUREECRITURE;
sErrString = "Impossible d'ouvrir le fichier de sortie en écriture";
return false;
}
ErrCode = ERRCODE_PASDERREUR;
return true;
}
|
Decompresser : Cette méthode s'arrange pour que toutes les étapes nécessaires à la décompression soient effectuées (lecture de l'entête, arbre) puis lit le fichier source 4 octets par 4 octets. Chaque groupe de 4 octets est alors transformé en bitset qu'on utilise alors pour parcourir l'arbre de Huffman.
|
bool THuffman::Decompresser()
{
typedef std::bitset<32> TDataBuffer;
std::ifstream fSrc;
std::ofstream fDst;
char Valeur;
char BufferProg[10];
unsigned long int Position = 0;
TDataBuffer Data;
unsigned long int uData;
TNoeudHuffman * Pos;
int i;
if (!Statistiques)
if (!Lireentête())
return false;
if (!ArbreHuffman)
if (!ConstruireArbreCode())
return false;
if (DegreVerbalite & ddEtape)
FoncAffichageEtape("Décompression en cours");
if (sSource.length())
{
fSrc.open(sSource.c_str(), std::ios::in | std::ios::binary);
if (fSrc.is_open())
{
if (sDestination.length())
fDst.open(sDestination.c_str(), std::ios::out | std::ios::binary);
else
{
ErrCode = ERRCODE_FICHIERINDEFINI;
sErrString = "Veuillez préciser le nom du fichier de destination";
return false;
}
}
else
{
ErrCode = ERRCODE_OUVERTURELECTURE;
sErrString = "Impossible d'ouvrir le fichier d'entrée en lecture";
return false;
}
}
else
{
ErrCode = ERRCODE_FICHIERINDEFINI;
sErrString = "Veuillez préciser le nom du fichier source";
return false;
}
if (fDst.is_open())
{
fSrc.seekg(Finentête, std::ios::beg);
if (DegreVerbalite & ddProgression)
FoncAffichageProgression("0%");
Pos = ArbreHuffman;
fSrc.read((char *)(&uData),sizeof(unsigned long int));
while (!fSrc.eof())
{
Data = static_cast<TDataBuffer>(uData);
for (i = 31 ; (i >= 0) && (Position < TailleReelle) ; i--)
{
if (Data.test(i))
Pos = Pos->FilsD;
else
Pos = Pos->FilsG;
if (!Pos)
{
ErrCode = ERRCODE_INATTENDUE;
sErrString = "Impossible de lire dans l'arbre de Huffman";
return false;
}
if (!(Pos->FilsD || Pos->FilsG))
{
fDst.write((char *)(&(Pos->Valeur)),1);
if (!fDst.good())
{
ErrCode = ERRCODE_ERREURECRITURE;
sErrString = "Erreur lors de l'écriture des données";
return false;
}
Pos = ArbreHuffman;
Position++;
}
}
sprintf(BufferProg,"%d%%",int(float(Position) / TailleReelle * 100));
if (DegreVerbalite & ddProgression)
FoncAffichageProgression(BufferProg);
fSrc.read((char *)(&uData),sizeof(unsigned long int));
}
fSrc.close();
fDst.close();
}
else
{
ErrCode = ERRCODE_OUVERTUREECRITURE;
sErrString = "Impossible d'ouvrir le fichier de sortie en écriture";
return false;
}
ErrCode = ERRCODE_PASDERREUR;
return true;
}
|
Je fais encore l'impasse sur les fonctions de calcul de taille des fichiers qui ne sont que de simples opérations mathématiques à la portée de tous. Je saute également les fonctions d'accès aux aux membres de la classe.
Ecrireentête : Ici, on fournit les informations sur le fichier original et la table de décompresion. pour la table de décompression, je fournis : 1 octet pour désigner l'élément, 1 octet pour le nobre de bits et 1 octet ou 1 short pour le code en lui même. Bien entendu, je ne donne que les caractères utiles.
|
bool THuffman::Ecrireentête()
{
std::ofstream fDst;
unsigned long int ULongValue;
unsigned short int UShortValue;
unsigned char UCharValue;
std::string sNomFichier;
unsigned long int DernierSeparateur;
char BufferProgression[10];
if (DegreVerbalite & ddEtape)
FoncAffichageEtape("Ecriture de l'entête");
if (sSource.length())
{
if (sDestination.length())
fDst.open(sDestination.c_str(), std::ios::out | std::ios::binary);
else
{
ErrCode = ERRCODE_FICHIERINDEFINI;
sErrString = "Veuillez préciser le nom du fichier de destination";
return false;
}
}
else
{
ErrCode = ERRCODE_FICHIERINDEFINI;
sErrString = "Veuillez préciser le nom du fichier source";
return false;
}
if (fDst.is_open())
{
if (DegreVerbalite & ddProgression)
FoncAffichageProgression("0%");
ULongValue = TailleInitiale();
fDst.write((char *)(&ULongValue),sizeof(unsigned long int));
if (!fDst.good())
{
ErrCode = ERRCODE_ERREURECRITURE;
sErrString = "Erreur lors de l'écriture de l'entête";
return false;
}
DernierSeparateur = sSource.rfind(SEPARATEUR_REPERTOIRE);
if (DernierSeparateur > sSource.length())
sNomFichier = sSource;
else
sNomFichier = sSource.substr(DernierSeparateur + 1);
if (sSource.empty())
{
ErrCode = ERRCODE_FICHIERINDEFINI;
sErrString = "Veuillez préciser le fichier d'entrée";
return false;
}
|