1

(Dé-)saturer une image en passant en couleurs HSV

27 mars 2006
par ARNO*

[SPIP 1.9 et GD2] Cet article va vous présenter la création d’un filtre qui convertira les informations de couleurs RVB (Rouge Vert Bleu) en HSV (Hue Saturation Value), avant d’appliquer l’effet désiré. Technique plus fréquente dans les filtres Photoshop que dans un CMS...

Commençons par expliquer notre réalisation graphique : notre filtre va saturer ou désaturer les couleurs d’une image. La luminosité de l’image et le contraste sont inchangés, mais la couleur est « affadie » ; l’effet, utilisé subtilement, donnera des tonalités de photographie vieillie (les couleurs sont « passées », délavées), ou encore de Soldat Ryan...

À l’inverse, le même filtre pourra « doper » les couleurs.

Trois version de l’image
Au centre, l’image d’origine. À gauche, la version désaturée. À droite, les couleurs sont « dopées ».

Commençons par signaler que l’on peut déjà simuler l’effet de désaturation avec les filtres fournis en standard avec SPIP. Il suffit d’afficher l’image d’origine et, par dessus (en superposition), la même image passée en noir et blanc et rendue semi-transparente. Selon le niveau de transparence de l’image en noir et blanc placée au-dessus, le résultat à l’écran sera plus ou moins désaturé.

  1. <div style="position: relative;">
  2. [<div style="position: absolute">(#FICHIER|reduire_image{400})</div>]
  3. [<div style="position: absolute">(#FICHIER|reduire_image{400}|image_nb|image_alpha{40})</div>]
  4. </div>

Télécharger

En revanche, il n’est pas possible de saturer avec une telle méthode. De toute façon, le résultat graphique n’est pas exactement le but de cet article ; l’idée ici est d’expliquer le passage d’un codage de couleurs à un autre, afin de pouvoir par la suite appliquer des effets quasiment impossibles en RVB.

Le codage HSV

Il est nécessaire ici d’expliquer ce qu’est le codage HSV.

Vous utilisez quotidiennement, sur le Web, le codage RVB : il s’agit, pour chaque pixel, de connaître les valeurs de ses composantes rouge, vert et bleu. Il s’agit de valeurs comprises entre 0 et 255 (ou, en hexadécimal, FF)1. Dans ce codage, on n’a pas accès directement aux informations de saturation, par exemple (on ne sait pas si le « bleu » présent dans l’image est « très bleu » ou « un peu bleuté », il faut pour cela comparer sa composante « Bleu » aux deux autres et faire des calculs compliqués).

Il existe d’autres méthodes pour décrire une couleur, et parmi elles : HSV (en français : TSV). La couleur est décrite par sa teinte (hue), l’intensité de cette couleur (saturation) et la valeur luminosité (value, appelée brightness dans Photoshop). On peut d’ailleurs visualiser ces trois valeurs dans l’interface de choix de couleurs de Photoshop et de The Gimp.

À droite, le curseur vertical permet de faire varier le choix de la teinte (hue). Cette teinte s’applique alors au grand carré de sélection à gauche, où l’on peut choisir des variantes de cette teinte :
— de gauche à droite, on augmente la saturation ; on voit que sur le bord gauche, on part du gris, et sur le bord droit on arrive à la couleur « pure » ;
— de haut en bas, on diminue la luminosité : on se déplace vers le bas en augmentant la « value » (ce qui diminue la luminosité, désolé pour la logique), ce qui rend la couleur plus sombre.

La saturation et la valeur de luminosité sont codées en pourcentages, c’est-à-dire entre 0 et 1 (puisque 1 = 100%). La valeur de teinte est bouclée sur elle-même : arrivé à une extrêmité, on arrive à l’autre bout (sur la copie d’écran ci-dessus, on constate que le haut et le bas de la barre verticale sont exactement le même rouge) ; Photoshop code assez logiquement cette valeur en degrés, de 0° à 360°. Notre script n’a pas ce besoin de lisibilité, aussi il se contentera de coder la valeur de teinte selon la même logique que les deux autre valeurs, de 0 à 1 (de 0% à 100%).

L’intérêt de ce codage est de donner immédiatement des informations extrêmement « visuelles » sur la couleur. On peut ainsi, en faisant varier la saturation et la valeur de luminosité, modifier la couleur en restant exactement dans sa « teinte » (la notion n’est pas évidente à exprimer...).

Parmi les effets très faciles à réaliser en travaillant sur les valeurs HSV (et quasiment impossible en RVB) :
— on peut, en modifier la teinte (hue), changer les couleurs de l’image, mais sans changer ni son contraste ni l’intensité des couleurs ;
— on peut « inverser » l’image (le noir devient blanc ; le blanc devient noir) mais sans changer les couleurs (le rouge reste rouge, simplement un rouge foncé deviendra rouge clair) ;
— on peut affadir ou « doper » les couleurs sans changer le contraste de l’image.

Dans ce qui suit, nous nous contenterons de réaliser le dernier effet.

Du codage RVB au codage HSV et inversement

Il n’est pas question de réinventer la roue. On trouvera sur le Web une excellente page qui récapitule tous les algorithmes pour passer d’un codage à un autre. Il suffit de les adapter à PHP :

Voici la fonction qui transforme des variables RVB en un tableau de variables HSV :

  1. function image_rgb2hsv ($R,$G,$B) {
  2.         $var_R = ( $R / 255 ) ;                    //Where RGB values = 0 ÷ 255
  3.         $var_G = ( $G / 255 );
  4.         $var_B = ( $B / 255 );
  5.  
  6.         $var_Min = min( $var_R, $var_G, $var_B ) ;   //Min. value of RGB
  7.         $var_Max = max( $var_R, $var_G, $var_B ) ;   //Max. value of RGB
  8.         $del_Max = $var_Max - $var_Min  ;           //Delta RGB value
  9.  
  10.         $V = $var_Max;
  11.         $L = ( $var_Max + $var_Min ) / 2;
  12.        
  13.         if ( $del_Max == 0 )                     //This is a gray, no chroma...
  14.         {
  15.            $H = 0 ;                            //HSL results = 0 ÷ 1
  16.            $S = 0 ;
  17.         }
  18.         else                                    //Chromatic data...
  19.         {
  20.            $S = $del_Max / $var_Max;
  21.        
  22.            $del_R = ( ( ( $var_Max - $var_R ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
  23.            $del_G = ( ( ( $var_Max - $var_G ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
  24.            $del_B = ( ( ( $var_Max - $var_B ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
  25.        
  26.            if      ( $var_R == $var_Max ) $H = $del_B - $del_G;
  27.            else if ( $var_G == $var_Max ) $H = ( 1 / 3 ) + $del_R - $del_B;
  28.            else if ( $var_B == $var_Max ) $H = ( 2 / 3 ) + $del_G - $del_R;
  29.        
  30.            if ( $H < 0 )  $H =  $H + 1;
  31.            if ( $H > 1 )  $H = $H - 1;
  32.         }
  33.                                
  34.         $ret["h"] = $H;
  35.         $ret["s"] = $S;
  36.         $ret["v"] = $V;
  37.        
  38.         return $ret;
  39. }

Télécharger

Et la fonction inverse :

  1. function image_hsv2rgb ($H,$S,$V) {
  2.        
  3.         if ( $S == 0 )                       //HSV values = 0 ÷ 1
  4.         {
  5.            $R = $V * 255;
  6.            $G = $V * 255;
  7.            $B = $V * 255;
  8.         }
  9.         else
  10.         {
  11.            $var_h = $H * 6;
  12.            if ( $var_h == 6 ) $var_h = 0 ;     //H must be < 1
  13.            $var_i = floor( $var_h )  ;           //Or ... var_i = floor( var_h )
  14.            $var_1 = $V * ( 1 - $S );
  15.            $var_2 = $V * ( 1 - $S * ( $var_h - $var_i ) );
  16.            $var_3 = $V * ( 1 - $S * ( 1 - ( $var_h - $var_i ) ) );
  17.        
  18.        
  19.            if      ( $var_i == 0 ) { $var_r = $V     ; $var_g = $var_3 ; $var_b = $var_1 ; }
  20.            else if ( $var_i == 1 ) { $var_r = $var_2 ; $var_g = $V     ; $var_b = $var_1 ; }
  21.            else if ( $var_i == 2 ) { $var_r = $var_1 ; $var_g = $V     ; $var_b = $var_3 ; }
  22.            else if ( $var_i == 3 ) { $var_r = $var_1 ; $var_g = $var_2 ; $var_b = $V ;     }
  23.            else if ( $var_i == 4 ) { $var_r = $var_3 ; $var_g = $var_1 ; $var_b = $V ; }
  24.            else                   { $var_r = $V     ; $var_g = $var_1 ; $var_b = $var_2; }
  25.        
  26.            $R = $var_r * 255;                  //RGB results = 0 ÷ 255
  27.            $G = $var_g * 255;
  28.            $B = $var_b * 255;
  29.         }
  30.         $ret["r"] = floor($R);
  31.         $ret["g"] = floor($G);
  32.         $ret["b"] = floor($B);
  33.        
  34.         return $ret;
  35. }

Télécharger

Pour l’instant, je peux vous dire que je n’ai même pas cherché à comprendre...

Voici la partie qui m’intéresse réellement : notre filtre destiné à saturer ou désaturer une image :

  1. function image_saturer($im, $sat=1)
  2. {
  3.         $image = valeurs_image_trans($im, "saturer-$sat");
  4.         if (!$image) return("");
  5.        
  6.         $x_i = $image["largeur"];
  7.         $y_i = $image["hauteur"];
  8.        
  9.         $im = $image["fichier"];
  10.         $dest = $image["fichier_dest"];
  11.         $creer = $image["creer"];
  12.        
  13.         if ($creer) {
  14.                 $im = $image["fonction_imagecreatefrom"]($im);
  15.  
  16.                 $im_ = imagecreatetruecolor($x_i, $y_i);
  17.                 @imagealphablending($im_, false);
  18.                 @imagesavealpha($im_,true);
  19.                 $color_t = ImageColorAllocateAlpha( $im_, 255, 255, 255 , 127 );
  20.                 imagefill ($im_, 0, 0, $color_t);
  21.  
  22.                 for ($x = 0; $x < $x_i; $x++) {
  23.                         for ($y=0; $y < $y_i; $y++) {
  24.                        
  25.                                 $rgb = ImageColorAt($im, $x, $y);
  26.                                 $a = ($rgb >> 24) & 0xFF;
  27.                                 $r = ($rgb >> 16) & 0xFF;
  28.                                 $g = ($rgb >> 8) & 0xFF;
  29.                                 $b = $rgb & 0xFF;
  30.                                
  31.                                 if ($a < 127) {
  32.                                         $hsv = image_rgb2hsv($r,$g,$b);
  33.                                         $h = $hsv["h"];
  34.                                         $s = $hsv["s"];
  35.                                         $v = $hsv["v"];
  36.                                        
  37.                                         $s = $s * $sat;                                                                 $s = min($s,1);
  38.                                        
  39.                                         $rgb = image_hsv2rgb($h,$s,$v);
  40.                                         $r = $rgb["r"];
  41.                                         $g = $rgb["g"];
  42.                                         $b = $rgb["b"];
  43.  
  44.                                 }
  45.                                 $color = ImageColorAllocateAlpha( $im_, $r, $g, $b , $a );
  46.                                 imagesetpixel ($im_, $x, $y, $color);                           }
  47.                 }              
  48.                 $image["fonction_image"]($im_, "$dest");                }
  49.  
  50.         $class = $image["class"];
  51.         if (strlen($class) > 1) $tags=" class='$class'";
  52.         $tags = "$tags alt='".$image["alt"]."'";
  53.         $style = $image["style"];
  54.        
  55.         return "<img src='$dest'$tags />";
  56. }

Télécharger

On relira l’article consacré au filtre image_podpod pour comprendre le début et la fin du filtre. La partie qui inédite est ici :

  1. if ($a < 127) {
  2.         $hsv = image_rgb2hsv($r,$g,$b);
  3.         $h = $hsv["h"];
  4.         $s = $hsv["s"];
  5.         $v = $hsv["v"];
  6.                                        
  7.         $s = $s * $sat;                                                
  8.         $s = min($s,1);
  9.                                        
  10.         $rgb = image_hsv2rgb($h,$s,$v);
  11.         $r = $rgb["r"];
  12.         $g = $rgb["g"];
  13.         $b = $rgb["b"];
  14.  
  15. }
  16. $color = ImageColorAllocateAlpha( $im_, $r, $g, $b , $a );
  17. imagesetpixel ($im_, $x, $y, $color);  

Télécharger

On extrait les valeurs de chaque pixel, et on obtient :
— $a la transparence du pixel,
— les composantes $r, $g et $b.

On convertit ces composantes RVB dans leurs équivalenrs HSV, et on obtient $h (hue), $s (saturation) et $v (value).

On modifie la valeur de saturation (qu’on multiplie par la valeur passée en variable), et on limite à 1 (la saturation ne peut pas être supérieure à 100%). Comme vous le constatez, notre effet repose uniquement sur deux lignes de code particulièrement simples ! (Mon conseil à Steven : pour Le soldat Ryan 2, utilise SPIP pour la post-production, ça coûtera moins cher.)

Ensuite on réencode nos valeurs HSV en RVB afin de les appliquer au pixel.

Et c’est tout !

Remarquez le test $a<127, qui permet de laisser complètement inchangés les pixels transparents, ce qui économise du temps de calcul.

(Notez qu’auparavant, j’avais validé mes deux fonctions de conversion — de RVB à HSV puis de HSV vers RVB — en commentant simplement les deux lignes de modification de $s. Je vérifiais ainsi que, après la double conversion, j’obtenais bien une image rigoureusement identique à l’originale.)

Le filtre peut s’utiliser ainsi :

  1. <BOUCLE_doc(DOCUMENTS){id_article}{par hasard}{0,1}>
  2. [(#FICHIER|reduire_image{300}|image_saturer{0.5})]
  3. [(#FICHIER|reduire_image{300})]
  4. [(#FICHIER|reduire_image{300}|image_saturer{1.5})]
  5. </BOUCLE_doc>

Télécharger

qui donne les images affichées au début de cet article :

Trois version de l’image
Au centre, l’image d’origine. À gauche, la version désaturée. À droite, les couleurs sont « dopées ».

Il est évidemment recommandé de jouer un peu avec ces filtres, par exemple voici ce que cela donne en « bouchant » complètement les zones sombres (filtre image_podpod en affichage au-dessus de la même image, désaturée à 50% :

Enfin, si à l’endroit où l’on joue avec $s :

  1. $s = $s * $sat;
  2. $s = min($s,1);

Télécharger

vous décidez de jouer avec les valeurs $h ou $v, vous pouvez obtenir d’autres effets intéressants, comme par exemple :

1Nous parlons ici d’images codées en 24 bits, c’est-à-dire 3 couleurs codées en 8 bits, ce qui est le cas des images utilisées sur le Web.

Qui êtes-vous ?
Votre message

Ce formulaire accepte les raccourcis SPIP [->url] {{gras}} {italique} <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.