Licence CC BY-SA

Filtres RVB

Nous allons voir ici comment créer des filtres d'images dans l'espace de couleurs RVB.

On cherche donc des fonctions mathématiques de $[0,1]^3$ dans $[0,1]^3$ (de RVB dans RVB) à insérer dans ce bout de code adapté de l'exo précédent :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Parametres utilisateurs
String AdresseImageEntree = "/piano.jpg";
String AdresseImageSortie = "/sortie2.jpg";

// Variables globales
PImage Image;
int    Largeur;
int    Hauteur;

// Fonction principale
void setup()
{
  // Chargement de l'image en entrée
  Image = loadImage( AdresseImageEntree );
  Largeur = Image.width;
  Hauteur = Image.height;
  
  // Paramètres de l'application
  size( Largeur , Hauteur );
  
  // Debut de modification de pixels[]
  loadPixels();
  
  RVB C_RVB = new RVB();
  CMJN C_CMJN = new CMJN();
  
  // Parcours de pixels en Hauteur
  for( int y = 0 ; y < Hauteur ; y++ )
  {
    // Parcours de pixels en Largeur
    for( int x = 0 ; x < Largeur ; x++ )
    {
      C_RVB.DefinirDepuisColor( Image.pixels[ y*Largeur + x ] );

      // Réaliser ici l'opération sur la couleur en RVB du pixel

      pixels[ y*Largeur + x ] = C_RVB.ConvertirColor();
    }
  }
  
  // Fin de modification de pixels[]
  updatePixels();
  
  // Sauvegarde de l'image modifiée en sortie
  save( AdresseImageSortie );
}

// Fonctions utiles

// Classes

// Classe pour gérer les couleurs
// en Rouge Vert Bleu
class RVB
{
  float r;
  float v;
  float b;
  
  RVB()
  {
    r = 0;
    v = 0;
    b = 0;
  }
  
  // Conversion color -> RVB
  RVB DefinirDepuisColor( color C )
  {
    r = red  ( C ) / 255.0;
    v = green( C ) / 255.0;
    b = blue ( C ) / 255.0;
    
    return this;
  }
  
  // Conversion RVB -> color
  color ConvertirColor()
  {
    return color(
        r * 255.0
      , v * 255.0
      , b * 255.0
    );
  }
}

Traiter les canaux séparément

Pour commencer on peut chercher des fonctions $f : [0,1] \to [0,1]$ à appliquer sur les canaux séparément : l'opération sur le pixel sera alors $\begin{pmatrix} R \\ V \\ B \end{pmatrix} \mapsto \begin{pmatrix} f(R) \\ f(V) \\ f(B) \end{pmatrix}$.

Fonction RentrerDans01

La première préoccupation consiste à toujours ramener les valeurs dans $[0,1]$ (au cas où on a fait n'importe quoi) grâce à la fonction dont voici le graphe :

Graphique de la fonction RentrerDans01

Je vous laisse coder cette fonction en trois parties ! Vous devrez bien évidemment insérer cette fonction dans le code dans la partie "Fonctions utiles".

Fonction affine

On peut par exemple appliquer une fonction affine à chaque canal : $f(x)=a x + b$ avec $(a,b) \in \mathbb{R}^2$. $a$ est le coefficient directeur de la droite et $b$ sa valeur à l'origine.

En particulier si on applique $f(x)=1 - x$ sur chaque canal on réalise le négatif de l'image.

1
2
3
4
// Operation sur la couleur en RVB du pixel
C_RVB.r = RentrerDans01( a * C_RVB.r + b );
C_RVB.v = RentrerDans01( a * C_RVB.v + b );
C_RVB.b = RentrerDans01( a * C_RVB.b + b );

Avant Apres Apres Apres

Pour un $a$ positif :

  • $a>1$ : éclaircit l'image et plus $a$ augmente, plus l'image sera clair ;
  • $a<1$ : assombrit l'image et plus $a$ diminue, plus l'image sera sombre ;
  • $b>0$ : éclaircit l'image puisque le noir deviendra gris ;
  • $b<0$ : assombrit l'image puisqu'un certain seuil de gris (qui varie selon $a$) deviendra noir ;

Cette fonction peut provoquer la perte d'informations dans l'image i.e. certains pixels de différentes couleurs deviennent identiques et ne peuvent donc plus être différenciés.

Fonction puissance (ou gamma)

Cette famille de fonctions permet de donner plus d'importance au "1" (c'est-à-dire que les valeurs proches de 0 sont plus éloignées de 0 qu'avant, tandis que les valeurs proches de 1 se rapprochent plus et se confondent), ou au contraire de donner plus d'importance au 0, selon un paramètre $g$.

Graphique animé de la famille de fonctions puissance

Elle s'exprime comme cela : $f_{g}(x)=x^g$ (lire $x$ à la puissance $g$), avec $x \in [0;1]$ et $g \in \mathbb{R}^{+*}$ En Processing avec la fonction pow : pow(x,g).

Cette fonction est une généralisation des fonctions $x \mapsto x$ ($g=1$), $x \mapsto x^2$ ($g=2$), $x \mapsto x^3$ ($g=3$), $x \mapsto \sqrt{x}$ ($g=1/2$) à un paramètre continu $g$.

1
2
3
4
// Operation sur la couleur en RVB du pixel
C_RVB.r = pow( C_RVB.r, g );
C_RVB.v = pow( C_RVB.v, g );
C_RVB.b = pow( C_RVB.b, g );

Apres Apres Apres Apres Apres

  • $g<1$ éclaircit l'image, plus $g$ diminue, plus l'image sera clair ;
  • $g>1$ obscurcit l'image, plus $g$ augmente, plus l'image sera sombre ;
  • La particularité de ces fonctions sont que $f(0)=0$ et $f(1)=1$, contrairement aux fonctions affines vues précédemment. Ainsi le blanc reste blanc et le noir reste noir une fois la fonction appliquée.

Postérisation

On peut aussi utiliser une fonction en escalier, de paramètre $n$, qui discrétise les valeurs que peut prendre un canal : les valeurs du canal peuvent prendre seulement $n$ valeurs différentes équitablement espacées.

Voici le graphe d'une fonction en escalier à 8 "marches" ($n=8$) :

Graphique de la fonction en escalier à 8 marches

Encore une fois à vous de comprendre comment coder cette fonction !

Indice n°1 :

En Processing int(x) permet de récupérer la partie entière de x, c'est-à-dire le plus grand entier inférieur ou égale à x.

Indice n°2 :

Vous pouvez appliquer "l'algorithme en 3 étapes" vu au sous-chapitre précédent à ce problème. A vous de trouver les espaces en question..

Il est fortement déconseillé de lire les indications secrètes. Vous êtes sensé trouver les formules et les implémenter correctement. Le tutoriel a pour but de vous guider à trouver tout le code vous-même. Comprendre ce qu'on code est le plus important !

Graphique animé de la famille des fonctions en escalier en utilisant round pour que $f(1)=1$

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Operation sur la couleur en RVB du pixel
C_RVB.r = Marches( C_RVB.r , n );
C_RVB.v = Marches( C_RVB.v, n );
C_RVB.b = Marches( C_RVB.b, n );
...
// Fonctions utiles
float Marches( float x , int n )
{
  return round( x * (n - 1.0) ) / (n - 1.0);
}

Apres Apres Apres Apres Apres Apres Apres

Valeurs différentes selon les canaux

Enfin toutes les opérations que l'on a vu là peuvent être faites sur les canaux pris séparément : ainsi on peut attribuer des valeurs différentes aux paramètres (les nombres $a,b,g,n$) par canal. On aura donc par exemple les paramètres $a_{R},a_{V},a_{B}$ pour respectivement le paramètre $a$ du canal Rouge, le paramètre $a$ du canal Vert et le paramètre $a$ du canal Bleu.

Par exemple avec une fonction gamma par canal :

1
2
3
4
5
6
7
8
float ga = 1.5;
float gb = 0.8;
float gc = 0.4;
...
// Operation sur la couleur en RVB du pixel
C_RVB.r = pow( C_RVB.r , ga );
C_RVB.v = pow( C_RVB.v , gv );
C_RVB.b = pow( C_RVB.b , gb );

Avant Apres

De façon encore plus générale on peut appliquer différentes fonctions aux canaux. Si on a les fonctions $f,g,h : [0,1] \to [0,1]$ l'opération sur le pixel sera alors : $\begin{pmatrix} R \\ V \\ B \end{pmatrix} \mapsto \begin{pmatrix} f(R) \\ g(V) \\ h(B) \end{pmatrix}$

Opérations sur tous les canaux

Nous allons voir ici des opérations sur les pixels qui s'écrivent comme ceci : $\begin{pmatrix} R \\ V \\ B \end{pmatrix} \mapsto \begin{pmatrix} f(R,V,B) \\ g(R,V,B) \\ h(R,V,B) \end{pmatrix}$.

N&B : Moyenne, min et max

Ici on va chercher à rendre une image monochromatique i.e. toutes les couleurs ont des canaux égaux entre eux (ce qui donne un niveau de gris). Puisqu'on a trois nombre ($R,V,B$), on peut choisir la moyenne des trois, le minimum des trois ou bien le maximum des trois. Voici les résultats :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Operation sur la couleur en RVB du pixel

if( action == 0 )
{
  z = ( C_RVB.r + C_RVB.v + C_RVB.b ) / 3.0;
}
else if( action == 1 )
{
  z = min( C_RVB.r , min( C_RVB.v , C_RVB.b ) );
}
else
{
  z = max( C_RVB.r , max( C_RVB.v , C_RVB.b ) );
}

C_RVB.Definir( z , z , z );

Apres Apres Apres

Et on peut aussi prendre la moitié de la couleur d'avant et la moitié de la couleur moyenne pour rendre l'image un peu plus N&B sans l'être totalement. Je vous laisse imaginer d'autres opérations ^^ .

Échange de canaux

Une idée toute simple consiste à échanger les valeurs des canaux. Par exemple transformer $(R,V,B)$ en $(V,B,R)$ !

Avant Apres Apres Apres Apres Apres

L'opération $(R,V,B) \to (V,B,R)$ par exemple transforme le rouge en bleu, le vert en rouge et le bleu en rouge. Répétée trois fois elle revient à la couleur d'origine.

Combinaisons linéaires

Pour généraliser les fonctions affines, on exprime le canal Rouge par une combinaison linéaire des trois canaux et on ajoute une certaine quantité : $f(R,V,B)=a R + b V + c B + d$. On fait de même pour les deux autres canaux avec des valeurs différents, ce qui nous donne 12 coefficients à déterminer pour appliquer le filtre.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
float[][] M = new float[][]{
    new float[]{ 1.0 , 0.2 , 1.4 }
  , new float[]{ 0.2 , -0.8 , 0.2 }
  , new float[]{ 1.1 , 1.0 , -1.3 }
};

RVB CouleurNoir = new RVB( 0.3 , 0.8 , 1.6 );
...
// Operation sur la couleur en RVB du pixel
r = C_RVB.r;
v = C_RVB.v;
b = C_RVB.b;

C_RVB.r = RentrerDans01( M[0][0]*r + M[0][1]*v + M[0][2]*b + CouleurNoir.r );
C_RVB.v = RentrerDans01( M[1][0]*r + M[1][1]*v + M[1][2]*b + CouleurNoir.v );
C_RVB.b = RentrerDans01( M[2][0]*r + M[2][1]*v + M[2][2]*b + CouleurNoir.b );

Avant Apres

Cette formule permet de généraliser les fonctions affines vues précédemment, la moyenne N&B, l'échange de canaux et même la conversion RVB-CMJ. Je vous laisse en exercice trouver les bons coefficients pour retomber sur ces filtres !

Toutes ces opérations en même temps !

Et enfin vous pouvez vous amuser à appliquer toutes ces opérations à la suite :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Operation sur la couleur en RVB du pixel

C_RVB.r = Marches( C_RVB.r , 4 );
C_RVB.v = Marches( C_RVB.v , 5 );
C_RVB.b = Marches( C_RVB.b , 6 );

C_RVB.v += C_RVB.r;
C_RVB.v /= 2.0;

C_RVB.r = pow( C_RVB.r , 1.6 );
C_RVB.v = pow( C_RVB.v , 0.8 );
C_RVB.b = pow( C_RVB.b , 1.9 );

C_RVB.r = RentrerDans01( 3.0*C_RVB.r + 0.1 );
C_RVB.v = RentrerDans01( C_RVB.v );
C_RVB.b = RentrerDans01( 1.5*C_RVB.b );

Avant Apres

Exo : contraste automatique

Enoncé

L'exercice de ce sous-chapitre sera simple. Vous devez remettre tous les canaux de toutes les couleur dans l'intervalle $[0;1]$. Par exemple si tous les canaux Rouge des couleurs de l'image sont entre 0.2 et 0.9, l'image de sortie aura tous les canaux Rouge entre 0 et 1 : ceux qui étaient égaux à 0.2 seront égaux à 0 et ceux qui étaient égaux à 0.9 seront égaux à 1 après l'application du filtre.

Ceci vaut pour les deux autres canaux indépendamment. Au travail !

Une solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Parametres utilisateurs
String AdresseImageEntree = "/piano2.jpg";
String AdresseImageSortie = "/piano2_contraste_automatique.jpg";

float min = 0.0;
float max = 1.0;

// Variables globales
PImage Image;
int    Largeur;
int    Hauteur;

// Fonction principale
void setup()
{
  // Chargement de l'image en entrée
  Image = loadImage( AdresseImageEntree );
  Largeur = Image.width;
  Hauteur = Image.height;
  
  // Paramètres de l'application
  size( Largeur , Hauteur );
  
  // Debut de modification de pixels[]
  loadPixels();
  
  RVB C_RVB = new RVB();
  
  float minR = 1.0;
  float maxR = 0.0;
  
  float minV = 1.0;
  float maxV = 0.0;
  
  float minB = 1.0;
  float maxB = 0.0;
  
  // Premier parcours des pixels
  for( int y = 0 ; y < Hauteur ; y++ )
  {
    for( int x = 0 ; x < Largeur ; x++ )
    {
      C_RVB.DefinirDepuisColor( Image.pixels[ y*Largeur + x ] );
      
      minR = min( C_RVB.r , minR );
      maxR = max( C_RVB.r , maxR );
      
      minV = min( C_RVB.v , minV );
      maxV = max( C_RVB.v , maxV );
      
      minB = min( C_RVB.b , minB );
      maxB = max( C_RVB.b , maxB );
      
    }
  }
  
  println( "R : " + minR + " , " + maxR );
  println( "V : " + minV + " , " + maxV );
  println( "B : " + minB + " , " + maxB );
  
  // Second parcours des pixels
  for( int y = 0 ; y < Hauteur ; y++ )
  {
    for( int x = 0 ; x < Largeur ; x++ )
    {
      C_RVB.DefinirDepuisColor( Image.pixels[ y*Largeur + x ] );
      C_RVB.r = IntervalABdansCD( C_RVB.r , minR , maxR , min , max );
      C_RVB.v = IntervalABdansCD( C_RVB.v , minV , maxV , min , max );
      C_RVB.b = IntervalABdansCD( C_RVB.b , minB , maxB , min , max );
      pixels[ y*Largeur + x ] = C_RVB.ConvertirColor();
    }
  }
  
  // Fin de modification de pixels[]
  updatePixels();
  
  // Sauvegarde de l'image modifiée en sortie
  save( AdresseImageSortie );
}

Avant Apres

1
2
3
R : 0.36078432 , 1.0
V : 0.3529412 , 1.0
B : 0.32941177 , 1.0

Quand vous codez des fonctions ou des mini programmes, il est utile de faire des vérifications simples pour s'assurer de leur bon fonctionnement. Par exemple ici la vérification à faire est : si les canaux sont déjà entre 0 et 1, l'image de sortie est la même que celle en entrée.