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
- Opérations sur tous les canaux
- Toutes ces opérations en même temps !
- Exo : contraste automatique
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 :
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 ); |
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$.
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 ); |
- $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$) :
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 !
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); } |
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 ); |
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 ); |
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)$ !
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 ); |
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 ); |
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 ); } |
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.