Licence CC BY-SA

Prérequis

Certaines connaissance en physique et en mathématiques sont nécessaires pour aborder la suite du tutoriel.

Nous allons aussi introduire l'utilisation de Processing pour le traitement d'images.

Lumière

Pour décrire les couleurs, nous devons tout d'abord nous pencher sur la lumière et ses propriétés physiques.

Qu'est-ce que la lumière ?

En première approximation, la lumière est composée de rayons lumineux se propageant en ligne droite. Un rayon lumineux est composé par des photons. Les photons sont les particules élémentaires de la lumière et ont une principale caractéristique à retenir : la longueur d'onde, qui est étroitement lié avec l'énergie emmagasinée par ce dernier.

L’œil est capable de voir certaines longueurs d'onde : celles qui correspondent entre le rouge et le violet. L'œil humain n'est pas capable de voir au dessus (infrarouge) ou au dessous (ultraviolet).

Spectre d'un rayon lumineux

A chaque fois que vous voyez du rouge à un endroit, il y a des milliards de milliards (!) de photons de longueur d'onde rouge qui sont partis de cet endroit, qui ont traversé l'espace entre cet endroit et vos yeux puis qui sont arrivés dans leurs rétines respectives et enfin qui ont "imprimé" des cellules sensibles au rouge dans vos rétines.

Il y a trois types de ces cellules appelées "cônes" : celles sensibles aux grandes longueurs d'ondes, celles sensibles aux moyennes et enfin celles sensibles aux courtes.

La couleur que l'on perçoit est reconstruite par le cerveau en réalisant des combinaisons linéaires de la quantité de lumière perçue par chacun de ces cônes (pour chaque direction de votre vision !).

En réalité ce que l'on voit est une impression de rouge (cf. explication après sur comment tromper l’œil).

Absorption et émission

Ce n'est pas parce que vous apercevez un objet que ce dernier émet de la lumière.

Il y a des corps qui émettent de la lumière comme le soleil par exemple. Tous les autres objets se contentent :

  • soit d'absorber la lumière reçue : le photon n'est pas réémis par le corps et son énergie est emmagasinée ;

  • soit de la refléter : le photon continue sa route dans une autre direction avec la même longueur d'onde.

Ce comportement est entièrement déterminé par la longueur d'onde du photon. Un faisceau de lumière qui contient du rouge et du bleu peut atterrir sur un corps qui absorbe le photon rouge et reflète le photon bleu. La couleur perçue de l'objet sera… bleue !

Autre exemple, un objet qui apparaît noir à l'humain absorbe tous les photons visibles (sous-entendu par l'humain). Il emmagasine plus d'énergie, c'est une des raisons qui font qu'il est plus chaud que d'autres objets.

Ainsi la couleur d'un objet qui n'émet pas de lumière que l'on perçoit dépend complètement de ses propriétés d'absorption !

C'est la raison pour laquelle une lumière presque blanche comme celle du Soleil illumine des objets que l'on perçoit après rouge, violet, bleu, vert, etc…

Spectres

La réalité est un poil plus complexe : pour une longueur d'onde donnée, la proportion de photons absorbés par l'objet (par rapport à tous les photons reçus) est fixe. On parle de taux d'absorption pour une longueur d'onde.

On peut alors tracer ce taux d'absorption quand la longueur d'onde varie. Ce graphique s'appelle un spectre d'absorption.

On peut aussi tracer le spectre d'émission d'une source de lumière.

Espaces de couleurs

Espaces de couleurs

Deux faisceaux lumineux avec des spectres différents peuvent être perçus de la même couleur. Il n'y a donc pas d'équivalence entre "voir une couleur" et "déterminer le spectre de la couleur reçu".

Pour déterminer le spectre d'un objet, d'une matière, d'un atome, d'une molécule ou d'étoiles dans le ciel on est donc obligés de faire des expériences ; on ne peut pas se fier à l'impression que nos yeux et notre cerveau nous donnent.

Il est ainsi possible de "tromper" l'œil et le cerveau humain. C'est grâce à ça que vous pouvez lire ce cours en ce moment-même.

Le but d'un modèle de couleurs est à la fois d'être plus ou moins bon pour tromper l'humain (afin de lui faire percevoir la couleur que l'on veut) et de pouvoir stocker et transmettre ces couleurs :

  1. physiquement : les mélanges de peinture depuis des siècles, les pellicules pour la photographie et le cinéma, l'impression…

  2. analogiquement : les anciennes télévisions, les écrans cathodiques…

  3. numériquement : les écrans de nos appareils électroniques, les appareils photos numériques…

L'approximation RVB pour l'émission

Le modèle de couleurs le plus répandu en informatique est le RVB (pour Rouge Vert Bleu), en anglais RGB (pour Red Green Blue).

Ce modèle consiste à faire semblant que toutes les couleurs que l'on perçoit sont un mélange des trois longueurs d'ondes dites primaires : celles respectivement du rouge, du vert et du bleu. Ces trois couleurs ne sont pas prises aux hasard : elles correspondent aux maximums d'absorption des trois types de cônes dans les rétines !

Ainsi on a trois coefficients entre 0 et 1 qui indiquent la proportion de chacune des couleurs primaires dans le résultat final.

L'espace de couleurs CMJ

Cyan Magenta Jaune est un espace de couleurs dans lequel on décrit les quantités de Cyan, de Magenta et de Jaune qui doivent être absorbées (et non émis comme pour le RVB) pour obtenir la couleur finale à partir d'une couleur blanche.

  • Si les trois quantités sont nulles, rien n'est absorbé et on obtient la couleur blanche.
  • Si toutes les quantités sont maximales, tout est absorbé et on obtient la couleur noire.
  • L'absorption de Cyan et de Magenta donne du Bleu.
  • L'absorption de Cyan et de Jaune donne du Vert.
  • L'absorption de Magenta et de Jaune donne du Rouge.

Cet espace de couleurs est beaucoup utilisé en impression couleur d'imprimantes grand public (chaque canal représentant la proportion d'encre à mélanger pour chaque endroit de l'image à imprimer).

CMJN : ajout de noir en absorption

Une idée assez simple permet d'économiser de l'encre : on ajoute une quatrième encre noire (moins chère que les autres). Étant donné que le Noir est l'ajout de Cyan, de Magenta et de Jaune, tout mélange de CMJ contient une certaine "dose" de Noir.

On peut alors tout simplement rajouter la quantité d'encre noire nécessaire pour créer la couleur, puis soustraire cette quantité de Noir des trois quantités de CMJ initiales.

Comment calculer la quantité de Noir à partir de CMJ ? Solution :

Mathématiquement, la quantité de Noir est tout simplement le minimum des trois autres quantités.

Remarquez que si les trois quantités de CMJ sont égales (pour imprimer un gris), il n'y a plus besoin de rajouter de l'encre Cyan, Magenta ou Jaune (les quantités deviennent égales à 0 après soustraction de la quantité de Noir).

RGBW : ajout de blanc en émission

On peut faire le même raisonnement "en émission" avec le modèle RGB pour créer le modèle RGBW (Red Green Blue White).

Cette idée est utilisée dans certaines télévision : les LED bleues ont une durée de vie plus courte que les autres (rouge et verte). On ajoute alors une quatrième LED de couleur blanche afin de minimiser l'utilisation de la LED bleue.

Les transformations mathématiques

Pour suivre le reste du tutoriel vous avez besoin de comprendre comment on peut exprimer de différentes façons le même jeu de variables. Par exemple les trois quantités de CMJ contiennent la même information que les trois quantités de RVB ou les quatre quantités de CMJN, mais exprimées différemment.

Chaque modèle a son utilité : il faut alors savoir écrire les formules pour passer d'un modèle à un autre.

L'exemple du robinet d'eau chaude

Mélangeur et mitigeur

Les robinets ont accès à deux sources d'eau : un tuyau d'eau froide à une température $T_{f}$ et un tuyau d'eau chaude à température $T_{c}$. La fonction du robinet est de servir de l'eau plus ou moins chaude en débit variable.

Il y a deux types de robinet d'eau chaude. Lorsque vous avez à faire à un mélangeur, vous pouvez choisir la quantité d'eau froide ainsi que la quantité d'eau chaude. Ce système est proche de ce qui se passe réellement dans la tuyauterie (mélanger les deux sources d'eau). Sauf que ce système apporte un problème du point de vue de l'utilisateur : ce dernier est sensible aux paramètres "température de l'eau" ainsi que "débit de l'eau". En effet, lorsque vous trempez votre main dans la sortie du robinet, vous ressentez plus ou moins de chaleur au niveau de la main ainsi qu'une pression plus ou moins forte. Mais jamais vous ne vous direz "tiens il y a 60 % d'eau froide à 10°C ainsi que 40% d'eau chaude à 25°C" ;) .

Lorsque vous avez à faire à un mitigeur, par contre, vous pouvez directement décider de la température (horizontalement) ainsi que du débit (verticalement).

Modélisation mathématique

Nous avons donc deux systèmes pour définir nos variables. Il y a le système "interne" avec deux variables $f$ et $c$ représentant respectivement la quantité d'eau froide ainsi que la quantité d'eau chaude. Puis il y a le système "utilisateur" avec deux variables $T$ et $Q$ représentant respectivement la température $T$ ainsi que $Q$ la quantité d'eau en sortie.

Maintenant nous allons devoir trouver les formules pour passer d'un système à un autre. Les formules pour passer du système interne au système utilisateur (c'est à dire calculer $T$ et $Q$ en fonction de $f$ et $c$) proviennent de calculs physiques simple. Puisque l'eau se mélange de façon homogène, la température est simplement un barycentre entre les deux température $T_{f}$ et $T_{c}$ avec des coefficients respectifs $f$ et $c$ : $\dfrac{ f T_{f} + c T_{c} }{ f + c }$. Encore par un raisonnement physique, les quantités d'eau s'ajoutent entre elles : $Q = f + c$. Du coup on peut même écrire $T = \dfrac{f T_{f} + c T_{c}}{Q}$ !

Il est important de comprendre cette notion de barycentre pour la suite du tutoriel.

A chaque fois que vous établissez une formule, il est important de vérifier qu'elle est cohérente avec quelques cas pratiques. Ici on vérifie bien que :

  • Si il y a autant d'eau chaude que d'eau froide ($f=c$) alors $T = \dfrac{ T_{f} + T_{c}}{2}$

  • Si il n'y a pas d'eau froide ($f=0$) alors $T = T_{c}$ peu importe la valeur de $c$ !

  • Et inversement si il n'y a pas d'eau chaude ($c=0$) alors $T = T_{f}$ peu importe la valeur de $f$ !

  • La température n'est pas définie quand il n'y a pas de débit d'eau ($Q=0$).

Transformation inverse

Maintenant pour vous entraîner votre tâche va être de trouver la transformation inverse, c'est-à-dire comment passer de $(T,Q)$ à $(f,c)$ !

Bijection entre les deux systèmes

A tout couple de variables $(f,c) \in (\mathbb{R}^{+2})^*$ (c'est à dire que $f$ et $c$ sont des réels positifs non tous les deux nuls en même temps) correspond un couple de variables $(Q,T) \in \mathbb{R}^{+*} \times [T_{f};T_{c}]$ et réciproquement.

On dit que les ensembles $(\mathbb{R}^{+2})^*$ et $\mathbb{R}^{+*} \times [T_{f};T_{c}]$ sont en bijections ! On démontre avec les formules pour passer d'un système à un autre qu'ils sont équivalents.

On est obligé d'enlever $Q=0$ (et du coup $f=c=0$) puisque la température n'est pas définie dans ce cas !

Intérêt

L'intérêt de disposer d'un autre système pour exprimer nos variables est de pouvoir appliquer une opération dans cet autre système. Par exemple si on a trouvé la bonne température, on veut pouvoir la fixer puis augmenter le débit sans se pré-occuper des valeurs de $f$ et $c$.

L'algorithme est en trois étapes :

  1. Transformer nos variables $f,c$ en $T,Q$ ;

  2. Modifier $T,Q$ en $T',Q'$. Par exemple $Q'=2Q$ pour avoir un débit deux fois plus élevé et $T'=T$ garder la même température.

  3. Transformer les nouvelles variables $T',Q'$ en $f',c'$ .

L'exemple du segment

Un autre exemple simple pour bien comprendre cette notion. Il y a deux façons de définir un segment fermé en une dimension. Soit vous spécifiez la valeur minimale et la valeur maximale, soit vous spécifiez le centre du segment ainsi que le "rayon" du segment (un segment est un cercle en une dimension).

Ainsi on a les formules suivantes :

$$ \left\{\begin{aligned} \text{centre} = \dfrac{\text{min} + \text{max}}{2} \\ \text{rayon} = \dfrac{|\text{max} - \text{min}|}{2} \end{aligned}\right. $$

Nouvel exercice : trouver la transformation inverse !

$$ \left\{\begin{aligned} \text{min} = \text{centre} - \text{rayon} \\ \text{max} = \text{centre} + \text{rayon} \end{aligned}\right. $$
Il y a bijection entre $(\text{min},\text{max})$ dans $\mathbb{R}^2$ sachant que $\text{min} <= \text{max}$ et $(\text{centre},\text{rayon}) \in \mathbb{R} \times \mathbb{R}^+$.

Intérêts

Encore une fois ici il peut être pratique de vouloir changer le centre du segment ou son rayon sans se soucier des valeurs min et max. Si on veut réaliser une translation du segment, il suffit d'ajouter une valeur au centre ! Si on veut que le segment soit deux fois plus large, il suffit du multiplier le rayon par deux. Ce modèle de segment est aussi plus pratique pour réaliser des homothéties. L'algorithme en 3 étapes décrit pour le robinet est valable ici avec nos deux nouveaux espaces de variables.

Stockage des variables

Représentation des variables continues en mémoire

Il faut bien stocker des valeurs en mémoire pour les variables que l'on va manipuler.

Les formules que nous écrivons font appels à des réels mathématiques, qui peuvent être aussi précis que l'on veut. Elles sont indépendantes de la précision des représentations en mémoire de ces variables.

Cette représentation est importante, puisque c'est elle qui décide combien de valeurs différentes la variable peut prendre ainsi que quand deux variables sont identiques (elles sont trop proches pour être différentes en mémoire).

Pourtant la façon dont sont codées ces variables intervient forcément quand on les manipule !

Pour éviter de faire intervenir tout le temps ces paramètres, on utilise encore l'algorithme en trois étapes décrit ci-dessus où les deux espaces colorimétriques sont Couleur_Signal (qui est proche de la façon dont est stocké la couleur) et Couleur_Information (qui est apte à être manipulé par nos formules de maths).

En Processing Couleur_Signal sera la classe color et dans la suite du tutoriel Couleur_Information correspondra à notre classe RVB.

Stockage de RVB

Les couleurs RVB par exemple sont généralement codées de sorte que chaque canal (Rouge, Vert ou Bleu) soit stocké sur 8 bits (= 256 valeurs). Comme chaque canal est un nombre entre 0 et 255 inclus, il est courant de diviser chaque canal par 255 en calcul flottant (normalisation vers $[0,1]$), d'appliquer nos formules mathématiques sur la couleur et enfin de remultiplier chaque canal par 255 (puis converti en entier) afin de pouvoir stocker la couleur.

Pourquoi l'intervalle $[0;1]$ ?

Je rappelle que les valeurs de 0 et 1 signifient respectivement le minimum et le maximum des plages des quantités de chaque canal. Ils sont un peu arbitraires dans le sens où on aurait pu dire que chaque canal "vit" dans $[4;8.5]$ par exemple (au lieu de $[0;1]$).

Mais 0 et 1 sont pratiques puisque :

  1. 0 est l'élément absorbant de l'addition;
  2. 0 est l'élément nul de la multiplication;
  3. 1 est l'élément absorbant de la multiplication.

On a les conséquences suivantes :

  1. Le noir est l'unique couleur telle que tout ajout (composante par composante) avec une autre couleur ne change pas cette dernière ;
  2. Le noir est l'unique couleur telle que toute multiplication (composante par composante) avec une autre couleur la rend noir ;
  3. Le blanc est l'unique couleur telle que toute multiplication avec une autre couleur ne change pas cette dernière.

Nous verrons par la suite que c'est pratique pour les formules de maths utilisées.

Zeste de Processing

Qu'est-ce que Processing et pourquoi ?

Ce tutoriel est accompagné d'exemples pour pouvoir appliquer les formules vues dans des cas concrets. Processing est un langage de programmation proche du Java accompagné d'un IDE libre disponible sur beaucoup de plateformes. Processing est en partie pensé pour réaliser du traitement d'images.

Processing est donc très pratique pour ce tutoriel : sur n'importe quel OS vous pouvez coder avec l'IDE et suivre le tutoriel.

A savoir sur Processing

  1. Lors du lancement de l'application, la fonction setup() est appelée ;
  2. L'application dispose d'une zone graphique où l'on peut modifier les pixels et afficher ce que l'on veut ;
  3. On peut changer les dimensions de cette zone avec un appel à size( LargeurEnPixels , HauteurEnPixels ) ;
  4. Pour modifier les pixels de cette zone graphique, on modifie le tableau unidimensionnel pixels où les pixels sont rangés d'abord par ligne (axe x), puis par colonnes (axe y) ;
  5. Avant de modifier pixels, on les charge en faisant loadPixels()
  6. Après avoir modifié pixels, on les met à jour en faisant updatePixels()
  7. save( adresse ) permet de sauvegarder cette zone dans une image dont l'adresse est l'argument ;
  8. Les objets PImage permettent de charger des images externes, les modifier, et les sauvegarder (cf. exemple)

Exemple de code

Voici un exemple de code Processing, qui effectue simplement une symétrie horizontale d'une image externe :

 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
// Parametres utilisateurs
String AdresseImageEntree = "/piano.jpg";
String AdresseImageSortie = "/sortie.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();
  
  // Parcours de pixels en Hauteur
  for( int y = 0 ; y < Hauteur ; y++ )
  {
    // Parcours de pixels en Largeur
    for( int x = 0 ; x < Largeur ; x++ )
    {
      // Operation sur le pixel
      pixels[ y*Largeur + x ] = Image.pixels[ (Hauteur - 1 - y)*Largeur + x ];
    }
  }
  
  // Fin de modification de pixels[]
  updatePixels();
  
  // Sauvegarde de l'image modifiée en sortie
  save( AdresseImageSortie );
}

Avant Apres

Certaines images dans ce tutoriel sont stockés dans un format destructifs (par exemple certaines images en JPG ou les animations en GIF) dans le sens où l'image stockée et l'image en sortie du programme Processing (ce qu'il y a dans le tableau pixels) sont légèrement différentes, de sorte à pouvoir réduire la taille du fichier. Si on veut pouvoir réellement étudier l'image de sortie une bonne pratique à prendre en traitement d'images est de ne pas comprimer l'image de sortie du programme.

Les images en PNG de ce tutoriel par contre sont fidèles à la sortie du programme.

Exo : il n'y a plus de cyan !

Cahier des charges

Cet exercice va mettre en pratique tout ce que l'on a vu dans cette partie. Vous allez devoir écrire des formules de maths et les transcrire en Processing. Voici le cahier des charges du programme Processing :

  1. Le programme prend en paramètre l'adresse d'une image "source".
  2. Cette image sera modifiée et stockée dans une adresse dite "cible"
  3. Chaque pixel de l'image cible est de couleur du même pixel de l'image source mais dont la composante Cyan (du modèle de couleur CMJN) est nulle.
  4. Bonus : le programme calcule les proportions nécessaire de chaque encre pour imprimer l'image avant modification (la quantité d'encre divisée par la surface de l'image, en supposant que l'ajout d'encre est proportionnel à la quantité du canal).

Comme vous l'avez compris, ce programme permet de simuler le fait que vous soyez à court d'encre Cyan lorsque vous imprimez l'image source.

Indices

Voici quelques indices si vous n'y arrivez pas :

Indice n°1 :

Vous pouvez créer une classe CMJN et une méthode pour obtenir une couleur en CMJN depuis une couleur en RVB.

Indice n°2 :

Pour la conversion RVB vers CMJN, pré-occupez-vous d'abord de convertir en CMJ puis occupez-vous enfin de la composante Noir.

Une solution

Voici une solution possible :

  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
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Paramètres utilisateurs
String AdresseImageEntree = "/piano.jpg";
String AdresseImageSortie = "/sortie.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();
  
  float proportionCyan = 0.0;
  float proportionMagenta = 0.0;
  float proportionJaune = 0.0;
  float proportionNoir = 0.0;
  float Surface = Hauteur * Largeur;
  
  // 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 ] );
      // Operation sur la couleur en RVB du pixel
      
      C_CMJN.DefinirDepuisRVB( C_RVB ); // Etape 1 : on convertit la couleur en CMJN
      
      proportionCyan    += C_CMJN.c;
      proportionMagenta += C_CMJN.m;
      proportionJaune   += C_CMJN.j;
      proportionNoir    += C_CMJN.n;
      
      C_CMJN.c = 0.0;                   // Etape 2 : on enlève le canal cyan
      C_RVB.DefinirDepuisCMJN( C_CMJN );// Etape 3 : on reconvertit la couleur en RVB
      
      // Fin de l'opération
      pixels[ y*Largeur + x ] = C_RVB.ConvertirColor();
    }
  }
  
  // Fin de modification de pixels[]
  updatePixels();
      
  // Sauvegarde de l'image modifiée en sortie
  save( AdresseImageSortie );
  
  proportionCyan    /= Surface;
  proportionMagenta /= Surface;
  proportionJaune   /= Surface;
  proportionNoir    /= Surface;
  
  println( "proportion d'encre Cyan \t: "    + proportionCyan    * 100.0 + " %" );
  println( "proportion d'encre Magenta \t: " + proportionMagenta * 100.0 + " %" );
  println( "proportion d'encre Jaune \t: "   + proportionJaune   * 100.0 + " %" );
  println( "proportion d'encre Noir \t: "    + proportionNoir    * 100.0 + " %" );
}

// 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
    );
  }
  
  
  // Conversion CMJN -> RVB
  RVB DefinirDepuisCMJN( CMJN C )
  {
    r = ( C.c - C.m - C.j - C.n + 1.0 )/2.0;
    v = ( C.m - C.c - C.j - C.n + 1.0 )/2.0;
    b = ( C.j - C.m - C.c - C.n + 1.0 )/2.0;
    
    return this;
  }
}

// Classe pour gérer les couleurs
// en Cyan Magenta Jaune Noire
class CMJN
{
  float c;
  float m;
  float j;
  float n;
  
  CMJN()
  {
    c = 0;
    m = 0;
    j = 0;
    n = 0;
  }
  
  // Conversion RVB -> CMJN
  CMJN DefinirDepuisRVB( RVB C )
  {
    c = 1.0 - ( C.v + C.b );
    m = 1.0 - ( C.r + C.b );
    j = 1.0 - ( C.r + C.v );
    
    n = min( c , min( m , j ) );
    
    c -= n;
    m -= n;
    j -= n;
    
    return this;
  }
}

Avant Apres

1
2
3
4
proportion d'encre Cyan     : 13.722335 %
proportion d'encre Magenta  : 8.488182 %
proportion d'encre Jaune    : 0.018533962 %
proportion d'encre Noir     : 41.635376 %