surfaces-unies/rapport/rapport.org

19 KiB
Raw Permalink Blame History

Compression dimages par surfaces unies

\newpage

\newpage

Le problème

Le but de ce projet a été de créer un logiciel permettant la compression dimages via la détection de surfaces dune même couleur, ainsi que de permettre la décompression du fichier ainsi généré à la compression en une image identique à limage dorigine. Il est à noter que cet algorithme est orienté vers les images de style comics ou avec peu de couleurs, doù limage test que vous trouverez en annexe qui fut mon image de test principale.

Le lien de lénoncé original se situe également en annexe. Une documentation du projet sera également disponible en suivant le lien donné également en annexe.

Résolution initiale du problème

Afin de résoudre ce problème, je me suis basé sur un algorithme de M. Jean-Jaques Bourdin et lai adapté au problème de la manière suivante : pour chaque pixel de limage, je teste si le pixel courant a une couleur identique à la couleur dune surface, ou zone, déjà existante. Si une telle zone nexiste pas déjà, alors je la créé puis pour chaque pixel de la ligne courante je me déplace à lextrême gauche et à lextrême droite du segment de la même couleur que le pixel courant. Ainsi on obtient un segment de couleur unie, et il est donc possible de maintenant stocker uniquement les limites de ce segment et à lajouter à la collection de segments de la zone, qui elle contient les informations de la couleur de la zone. Ensuite, pour chaque pixel en dessus puis en dessous de ce segment, on répète la même procédure. Cela permet de lister toutes les zones contigües et de les ajouter ainsi à la zone courante.

Actuellement, avec limage de test que lon peut trouver sur mon dépôt git (lien en annexes) dune taille de 3582016 octets, jobtiens un taux de compression de 67% environ avec une image compressée à 2403581 octets.

Pour ce qui est du temps dexécution, jexécute le script suivant avec la commande ./run-time.sh 10 (10 étant le nombre dexécutions que je souhaite effectuer) afin dobtenir le temps dexécution à répétition du programme :

Ainsi, jobtient la moyenne sur dix exécutions dun temps dexécution de 7.06 secondes sur un CPU Intel i7-6700HQ (3.500GHz).

Améliorations possibles

Algorithme

Théorie

Il est possible daméliorer lalgorithme par rapport à celui utilisé initialement en ignorant létape de test des pixels supérieurs et inférieurs à un segment testé. En effet, cette étape est un résidu de lalgorithem dorigine qui a pour but de ne détecter quune zone dont tous les segments sont contigus à au moint un autre segment de la même forme, le tout ne constituant quune seule et unique forme continue. Hors ici ce dernier point ne nous intéresse pas, et deux formes de la même couleur nont pas à être considérées comme étant deux entités différentes. Ainsi, en ignorant cette étape cela permet de passer de manière itérative sur tous les pixels, simplifiant ainsi lalgorithme.

Modifications apportées

La modification apportée au code source est relativement simple étant donné quil suffit de commenter les deux dernières boucles for de la fonction addPixelToSelectedZone du fichier compress.c. Cela bloque lexécution de la fonction créant les segments à partir des pixels au dessus et en dessous du segment courant.

     }
     darrayPushBack(t_zone->segments,
                    newSegment((uint32_t)right_limit, (uint32_t)left_limit));
  -  for (; left_limit <= right_limit; ++left_limit) {
  -    addPixelToSelectedZone(t_img, t_idx + t_img->sizeX, t_zone);
  -  }
  -  for (; left_limit <= right_limit; ++left_limit) {
  -    addPixelToSelectedZone(t_img, t_idx - t_img->sizeX, t_zone);
  -  }
  +  /* for (; left_limit <= right_limit; ++left_limit) { */
  +  /*   addPixelToSelectedZone(t_img, t_idx + t_img->sizeX, t_zone); */
  +  /* } */
  +  /* for (; left_limit <= right_limit; ++left_limit) { */
  +  /*   addPixelToSelectedZone(t_img, t_idx - t_img->sizeX, t_zone); */
  +  /* } */
   }

Résultats

Avec dix exécutions successives, sur le même processeur avec les mêmes arguments et le programme également compilé avec les options doptimisation `-O3` également, jobtiens une vitesse dexécution moyenne à la compression/décompression successives de 7.23s, soit une perte de 2.35% du temps dexécution dorigine. Jexplique cela par le fait que pour chaque nouveau pixel non-traité, il est nécessaire de trouver la zone correspondant à la couleur du nouveau pixel, ce qui en soit prend du temps alors quavec la méthode dorigine, on connaît déjà la zone et on na quà explorer les pixels de couleur unie, sachant que là où on a le plus de chance de trouver des pixels de couleur identique sur des images de type comics est au niveau des pixels adjascents au pixel actuel. Ainsi, cette piste sest révélée non concluante, et je ne continuerai pas dans cette direction.

Sortir du concept dimage avec des lignes et colonnes

Théorie

Lalgorithme actuel considère le fichier dentrée comme étant une image en deux dimensions, limitant ainsi les segments au début dune ligne de pixels même si le pixel précédent dans le vecteur dans lequel ces derniers sont stockés est de la même couleur, divisant ainsi des segments en plusieurs segments inutilement. En ignorant ces fins de lignes pour ne procéder quà lanalyse des pixels comme ne faisant partie que dune ligne unique permettrait déviter ces ajouts inutiles de segments, et ainsi économiser un peu despace avec le fichier compressé. Cependant, je ne pense pas que cette solution soit réellement efficace du fait du peu de cas où ce problème se pose.

Modifications apportées

Les deux boucles de la fonction addPixelToSelectedZone cherchant les segments ont été modifiées ainsi :

  for (right_limit = t_idx; right_limit < img_size; ++right_limit) {
    current_pixel = darrayGet(t_img->pixels, (size_t)right_limit);
    if (!sameColor(current_pixel, t_zone)) {
      break;
    }
    (*current_pixel).visited = 1;
   }

  for (left_limit = t_idx; left_limit > 0; --left_limit) {
    current_pixel = darrayGet(t_img->pixels, (size_t)left_limit);
    if (current_pixel->visited || !sameColor(current_pixel, t_zone)) {
      break;
    }
    (*current_pixel).visited = 1;
   }

Cette modification permet dignorer les retours à la ligne et ainsi potentiellement économiser de lespace lors de lécriture de fichier compressés.

Résultats

Hélas le gain despace nest pas flagrant, et cela sexplique par le faible nombre de lignes de pixels de limage, 1500 dans ce cas, et du fait quéconomiser deux uint32_t par ligne est une amélioration mineure, permettant dans le cas de limage asterix.ppm déconomiser uniquement 12 kilo octets. La différence sur le fichier compressé de 2.3 méga octets dorigine est donc négligeable. Je ne continuerai donc pas de travailler sur cette piste.

Passer à de la compression à pertes

Théorie

Il existe toujours la possibilité de ne plus faire que de la compression sans pertes mais de faire également de la compression avec pertes, en acceptant en argument un taux de tolérance quant à la similarité des couleurs entre elles. Ainsi, cela éliminera certaines couleurs de limage et économisera ainsi des zones disposant dun petit nombre de zones ou de segments en les assimilant aux couleurs qui leur sont proches. Linconvénient est quavec ce paramètre, il sera impossible de restituer limage dorigine à lidentique.

Modifications apportées

Seuls trois fichiers ont eu à être modifiés : main.c, compress.h et compress.c. Voici ci-dessous les modifications apportées à chacun de ces fichiers (sortie de la commande git diff) :

  diff --git a/src/compress.c b/src/compress.c
  index 191265a..25e43aa 100644
  --- a/src/compress.c
  +++ b/src/compress.c
  @@ -5,6 +5,8 @@

   #include "compress.h"

  +int tolerance;
  +
   /**
    ,*  Cette fonction permet dévaluer si le pixel passé en argument est éligible à
    ,*  la zone passée également en argument.
  @@ -14,10 +16,17 @@
    ,*  \return Valeur booléenne, `1` si le pixel est éligible, `0` sinon
    ,*/
   int32_t sameColor(Pixel *t_pixel, Zone *t_zone) {
  -  return (t_pixel->red == t_zone->red && t_pixel->green == t_zone->green &&
  -          t_pixel->blue == t_zone->blue)
  -             ? 1
  -             : 0;
  +  int diff_red, diff_green, diff_blue;
  +  if(tolerance == 0) {
  +    return (t_pixel->red == t_zone->red && t_pixel->green == t_zone->green &&
  +            t_pixel->blue == t_zone->blue)
  +               ? 1
  +               : 0;
  +  }
  +  diff_red = (abs((int32_t)t_zone->red - (int32_t)t_pixel->red) * 100) / 255;
  +  diff_green = (abs((int32_t)t_zone->green - (int32_t)t_pixel->green) * 100) / 255;
  +  diff_blue = (abs((int32_t)t_zone->blue - (int32_t)t_pixel->blue) * 100) / 255;
  +  return ((diff_red + diff_green + diff_blue) / 3) <= tolerance;
   }

   /**
  @@ -173,13 +182,15 @@ void write_compressed_file(Image *t_img, FILE *t_output, darray *t_zones) {
    ,*  \param[in] t_input_file Nom/chemin du fichier `.ppm` d'entrée
    ,*  \param[in] t_output_file Nom/chemin du fichier `.su` de sortie
    ,*/
  -void compress(const char *t_input_file, const char *t_output_file) {
  +void compress(const char *t_input_file, const char *t_output_file,
  +              int32_t t_tolerance) {
     Image *img;
     darray *zones;
     FILE *output_file;
     if (!t_output_file) {
       t_output_file = DEFAULT_COMPRESSED_NAME;
     }
  +  tolerance = t_tolerance;
     img = newImage();
     imageLoadPPM(t_input_file, img);
     output_file = get_file(t_output_file, "wb");
  diff --git a/src/compress.h b/src/compress.h
  index 9b2f832..50f7c25 100644
  --- a/src/compress.h
  +++ b/src/compress.h
  @@ -23,6 +23,7 @@ void write_segments(FILE *t_output, darray *t_segments);
   /// Écrit les données compressées dans le fichier de sortie
   void write_compressed_file(Image *t_img, FILE *t_output, darray *t_zones);
   /// Compresse l'image d'entrée
  -void compress(const char *t_input_file, const char *t_output_file);
  +void compress(const char *t_input_file, const char *t_output_file,
  +              int32_t tolerance);

   #endif /* SRC_COMPRESS_H_ */
  diff --git a/src/main.c b/src/main.c
  index 8ba5bee..4676ca7 100644
  --- a/src/main.c
  +++ b/src/main.c
  @@ -24,18 +24,21 @@
    ,*/
   void help(int t_exit_code) {
     puts("Usage:\n"
  -       "surfaces-unies -i path [-o path] [-options]\n\n"
  +       "surfaces-unies -i path [-o path] [--options] [-t 0-100]\n\n"
          "The default action is to compress the mandatory input image to a .su\n"
          "file saved in the current directory.\n"
          "The input image MUST be saved in the ppm format.\n"
          "Options available:\n"
  -       "-h --help\n\tdisplay the current message\n"
  -       "-i --input\n\tpath to the input file (MANDATORY)\n"
  +       "-h --help\n\tDisplay the current message\n"
  +       "-i --input\n\tPath to the input file (MANDATORY)\n"
          "-o --output\n"
  -       "\tpath to the output file (if the file already exists, it will be\n"
  +       "\tPath to the output file (if the file already exists, it will be\n"
          "\toverwritten)\n"
  -       "-c --compress\n\tcompress the input file\n"
  -       "-u --uncompress\n\tuncompresses the input file to the output file.");
  +       "-t --tolerance\n"
  +       "\tColor tolerance for lossy compression. By default at 0 for lossless\n"
  +       "\tcompression, at 100 will consider every color to be the same.\n"
  +       "-c --compress\n\tCompress the input file\n"
  +       "-u --uncompress\n\tUncompresses the input file to the output file.");
     exit(t_exit_code);
   }

  @@ -49,9 +52,10 @@ void help(int t_exit_code) {
    ,*  supplémentaires aux fonctions.
    ,*/
   struct Argres {
  -  char *input;   /*!< Nom du fichier d'entrée */
  -  char *output;  /*!< Nom du fichier de sortie */
  -  char compress; /*!< Le fichier d'entrée doit-il être compressé ? */
  +  char *input;       /*!< Nom du fichier d'entrée */
  +  char *output;      /*!< Nom du fichier de sortie */
  +  char compress;     /*!< Le fichier d'entrée doit-il être compressé ? */
  +  int32_t tolerance; /*!< Tolérance en pourcentage des différences de couleur */
   };
   typedef struct Argres Argres;

  @@ -72,6 +76,12 @@ void get_args(Argres *t_args, const int *const t_c) {
     case 'o': (*t_args).output = optarg;  break;
     case 'c': (*t_args).compress = 1;  break;
     case 'u': (*t_args).compress = 0; break;
  +  case 't':
  +    (*t_args).tolerance = atoi(optarg);
  +    if(t_args->tolerance < 0 || t_args->tolerance > 100) {
  +      help(ARGERROR);
  +    }
  +    break;
     case '?':
     default: help(ARGERROR);
     }
  @@ -95,16 +105,18 @@ Argres process_args(const int t_argc, char *t_argv[]) {
     res.input = NULL;
     res.compress = 1;
     res.output = NULL;
  +  res.tolerance = 0;
     while (1) {
       int option_index = 0;
       static struct option long_options[] = {
           {"help", no_argument, NULL, 'h'},
           {"input", required_argument, NULL, 'i'},
           {"output", required_argument, NULL, 'o'},
  +        {"tolerance", required_argument, NULL, 't'},
           {"compress", no_argument, NULL, 'c'},
           {"uncompress", no_argument, NULL, 'u'},
           {NULL, 0, NULL, 0}};
  -    int c = getopt_long(t_argc, t_argv, "hi:o:cu", long_options, &option_index);
  +    int c = getopt_long(t_argc, t_argv, "hi:o:t:cu", long_options, &option_index);
       if (c == -1)
         break;
       get_args(&res, &c);
  @@ -129,8 +141,8 @@ int main(int argc, char **argv) {
       fprintf(stderr, "ERROR: no input file.");
       help(ARGERROR);
     }
  -  if(argresults.compress) {
  -    compress(argresults.input, argresults.output);
  +  if (argresults.compress) {
  +    compress(argresults.input, argresults.output, argresults.tolerance);
     } else {
       uncompress(argresults.input, argresults.output);
     }

Résultats

La nouvelle option -t est une option très sensible qui peut profondément changer la qualité de limage dès des valeurs basses, aux alentours de 15 à 25. Cependant, une nette réduction de la taille de limage compressée peut être constatée, ainsi quune baisse notable du temps de compression de limage dû aux recherches des zones correspondant aux pixels moins fréquentes, et lorsquelles se produisent, la recherche se fait parmi moins de zones. Ainsi, pour différentes valeurs de tolérance, on obtient ces résultats :

/ < <
tolérance vitesse dexécution taille de limage compressée
0 7.37s 2.3Mo
5 0.85s 2.0Mo
10 0.82s 2.0Mo
15 0.86s 2.0Mo
20 0.84s 1.9Mo
25 0.85s 1.9Mo
30 0.92s 1.8Mo
40 1.18s 2.1Mo
50 0.90s 1.5Mo
60 1.41s 1.9Mo
70 1.34s 1.8Mo
80 1.40s 1.9Mo
90 1.42s 1.9Mo
100 0.17s 12Ko

On remarque cependant quaprès environ 30% de tolérance de couleur, les temps ont tendance à remonter et les fichiers ont tendance à devenir plus lours, excepté pour la tolérance de 50%. Je suppose quil sagit du fait dun grand nombre de segments contigus de couleur différente salternant rapidement. En tous cas, bien quon aie en effet quelques améliorations concernant la taille de limage compressée, je ne considère personellement pas cette technique comme en vallant la peine du fait dartéfacts facilement visibles dès les premières valeurs proches de 0, comme on peut le voir sur limage ci-dessous, compressée avec une tolérance de 5% :

\newpage

/phundrak/surfaces-unies/media/branch/master/img/asterix5p.png

Dans le cas où cela ne serait pas bien visible en version papier, une version digitale de ce document est disponible en ligne, voir le lien donnée en annexes.

\newpage

Annexes

Image de test

/phundrak/surfaces-unies/media/branch/master/img/asterix.png

\newpage

Code source

main.c

compress.h

compress.c

uncompress.h

uncompress.c

ppm.h

ppm.c

utilities.h

utilities.c

darray.h

darray.c