Table des matières Python

Espaces de couleur RGB

1. Définition d'un espace RGB

Le repérage d'une couleur se fait dans l'espace CIE XYZ par ses composantes trichromatiques X,Y,Z, associées à des couleurs primaires virtuelles. Pour l'affichage des couleurs sur un moniteur, il est nécessaire d'utiliser des couleurs primaires correspondant aux possibilités d'affichage de l'écran (par exemple écran LCD). De même, les opérations comme l'impression, la numérisation de films, la photographie numérique, etc, nécessitent la définition de primaires adaptées au matériel utilisé.

Dans l'espace CIE XYZ une lumière [M] est une combinaison linéaire des trois primaires virtuelles :

Lorsqu'on mutliplie les composantes trichromatiques (X,Y,Z) par une constante, on modifie la luminance sans modifier la couleur. Pour cette raison, on définit les coordonnées trichromatiques par :

Les coordonnées (x,y) représentent la couleur et Y la luminance visuelle.

Les coordonnées trichromatiques des primaires de l'espace RGB sont définies par la matrice :

Il faut définir également le point blanc, c'est-à-dire la couleur qui sera considérée comme neutre (composantes égales) dans l'espace RGB. Le point blanc correspond généralement à la couleur d'un illuminant standard, par exemple D65. Le point blanc est défini par ses coordonnées trichromatiques

Les composantes trichromatiques du point blanc de luminance maximale (1) sont :

Soient (R,G,B) les composantes de la lumière considérée dans l'espace RGB. Le point blanc de luminance maximale a par définition ses trois composantes égales à 1.

2. Transformation CIEXYZ-RGB

La transformation entre les espaces XYZ et RGB s'écrit :

Les valeurs de u,v,w sont choisies de manière à satisfaire le point blanc défini ci-dessus, c'est-à-dire :

On a donc :

La matrice de transformation s'écrit aussi :

Pour obtenir les composantes RGB en fonction des composantes XYZ, on écrit :

La matrice de transformation permettant d'effectuer la conversion de X,Y,Z vers R,G,B est donc T = Q-1.

Le module XYZ2RGB.py effectue ces calculs. Le constructeur prend en argument les primaires et le points blanc, sous forme de listes.

from numpy import *
from numpy.linalg import *
from math import *
class XYZ2RGB:
    def __init__(self,red,green,blue,white):
        self.red = red
        self.green = green
        self.blue = blue
        self.white = white
        P=mat([[red[0],green[0],blue[0]],[red[1],green[1],blue[1]],[1-red[0]-red[1],1-green[0]-green[1],1-blue[0]-blue[1]]])
        U=inv(P)*mat([[white[0]],[white[1]],[1-white[0]-white[1]]])/white[1]
        self.Q=P*mat([[U[0,0],0,0],[0,U[1,0],0],[0,0,U[2,0]]])
        self.T=inv(self.Q)
        self.gamma = 2.2
    def rgb(self,XYZ):
        RGB = self.T*mat([[XYZ[0]],[XYZ[1]],[XYZ[2]]])
        return [RGB[0,0],RGB[1,0],RGB[2,0]]
    def xyY2RGB(self,x,y,Y):
        X=x/y*Y
        Z=(1-x-y)/y*Y
        return self.rgb([X,Y,Z])
    def XYZ(self,RGB):
        XYZ0=self.Q*mat([[RGB[0]],[RGB[1]],[RGB[2]]])
        return [XYZ0[0,0],XYZ0[1,0],XYZ0[2,0]]
            

3. Espace sRGB

3.a. Définition

L'espace de couleur sRGB est un espace standard similaire au standard de la télevision. Il a été défini à l'origine pour l'internet (http://www.w3.org/Graphics/Color/sRGB). Il est aujourd'hui utilisé comme espace de couleur par défaut sur le matériel grand public (moniteurs, imprimantes, scanners, appareils photo numériques).

Une image dont les couleurs sont codées dans l'espace sRGB s'affiche convenablement sur la plupart des écrans LCD. Néanmoins, une restitution parfaite des couleurs (pour la photographie) nécessite un écran de bonne qualité, calibré à l'aide d'un colorimètre et l'emploi d'un logiciel lisant les profils de couleurs intégrés aux images.

Remarque : sur certains écrans dit à large gamut, l'espace des couleurs de l'écran est plus large que l'espace sRGB. En conséquence, l'affichage correct des photographies sRGB doit se faire avec un logiciel prenant en charge les profils de couleurs (comme Photoshop).

Le point blanc de l'espace sRGB correspond à la couleur de l'illuminant D65. Le tableau suivant définit les coordonnées trichromatiques des primaires et du point blanc :

Espace sRGB
RedGreenBlueD65
x0.640.30.150.3127
y0.330.60.060.329
z0.030.10.790.3583

On place ces points sur le diagramme de chromaticité. On utilise pour cela le module défini dans : repérage des couleurs CIE XYZ.

from CieXYZ import *
from pylab import *
cie = CIEXYZ("../ciexyz/ciexyz31.txt")
figure(1)
xlabel('x')
ylabel('y')
xy=cie.xyColor()
plot(xy[0],xy[1],c='k',label='Spectrum locus')
plot([0.64,0.3,0.15,0.64],[0.33,0.6,0.06,0.33],c='r',marker='o',label='sRGB')
plot([0.3127],[0.329],c='k',marker='o',label='D65')
legend()
grid(True)
            
plotAplotA.pdf

Par synthèse additive (comme c'est le cas sur un écran CRT ou LCD), seules les couleurs situées à l'intérieur du triangle (dont les sommets sont les primaires) peuvent être obtenues. Il y a donc des couleurs inaccessibles à l'espace sRGB, particulièrement dans le vert et bleu-vert. Il faut toutefois remarquer que le diagramme de chromaticité ne représente par les différences de couleurs de manière uniforme. L'espace sRGB permet de reproduire de manière satisfaisante la majorité des couleurs même s'il ne peut accéder aux verts très saturés. Pour les applications exigeantes, on préfère généralement l'espace AdobeRGB.

3.b. Matrice de transformation :

from XYZ2RGB import *
xw=0.3127
yw=0.329
sRGB=XYZ2RGB([0.64,0.33],[0.3,0.6],[0.15,0.06],[xw,yw])
            
print(sRGB.T)
--> matrix([[ 3.24096994, -1.53738318, -0.49861076],
        [-0.96924364,  1.8759675 ,  0.04155506],
        [ 0.05563008, -0.20397696,  1.05697151]])

La matrice Q représente les composantes X,Y,Z des primaires (en colonne) :

print(sRGB.Q)
--> matrix([[ 0.4123908 ,  0.35758434,  0.18048079],
        [ 0.21263901,  0.71516868,  0.07219232],
        [ 0.01933082,  0.11919478,  0.95053215]])

On remarque que la luminance des primaires (deuxième ligne) est très variable, celle du bleu étant particulièrement faible. Cela est dû aux variations de la sensibilité visuelle avec la longueur d'onde.

Exemple : point blanc , de luminance 1 :

RGB=sRGB.xyY2RGB(xw,yw,1)
print(RGB)
--> [0.99999999999999967, 1.0, 1.0]

Comme prévu, le blanc possède trois composantes égales à 1. La transformation étant linéaire, les composantes RGB sont proportionnelles à la luminance, ce que l'on peut vérifier pour une luminance de 0.5 :

RGB=sRGB.xyY2RGB(xw,yw,0.5)
print(RGB)
--> [0.49999999999999983, 0.5, 0.5]

Considérons à présent la primaire verte, de luminance 0.71 :

RGB=sRGB.xyY2RGB(0.3,0.6,0.7151)

La luminance maximale Y d'une couleur dépend de sa position dans le triangle RGB. Seul le blanc peut se voir affecter une luminance maximale de 1.

print(RGB)
--> [1.8510719216116578e-16, 0.99990396843458751, 6.4916605077569578e-18]

3.c. Normalisation des niveaux

En pratique, la luminance Y est égale à 1 seulement lorsque le facteur de luminance spectrale est R(λ)=1 pour toute longueur d'onde. Dans ce cas on obtient une lumière blanche si l'illuminant utilisé est D65.

Lorsque la lumière est colorée, la luminance Y est nécessairement inférieure à 1. Comme exemple, considérons une lumière obtenue par élimination des longueurs d'ondes supérieures à 600 nm.

cie.readD65("../ciexyz/IlluminantD65.txt")
cie.setIlluminant("D65",0)
def Rf(L):
    if L>600:
        return 0
    else:
        return 1
XYZ=cie.spectralF2XYZ(Rf)
xy=cie.XYZ2xy(XYZ[0],XYZ[1],XYZ[2])
                
print(XYZ)
--> [0.6206142625960358, 0.8501977135819314, 1.0887495666214424]
print(xy)
--> [0.24246897455619132, 0.3321653726098981]

Il s'agit d'une lumière bleu-verte (cyan) située à l'intérieur du triangle sRGB mais proche du segment BG.

Voyons les composantes RGB correspondantes :

RGB=sRGB.rgb(XYZ)
print(RGB)
--> [0.16145025893977871, 1.0386599066725055, 1.0118813548816488]

Cet exemple montre que l'on peut obtenir des composantes RGB supérieures à 1. Il faut donc prévoir une transformation permettant d'obtenir des composantes inférieures ou égales à 1. La plus simple consiste à diviser toutes les composantes par la plus grande des trois, lorsque celle-ci est supérieure à 1. La fonction suivante effectue cette transformation :

    def rgbN(self,XYZ):
        RGB = self.rgb(XYZ)
        m=max(RGB)
        if m>1.0:
            return [RGB[0]/m,RGB[1]/m,RGB[2]/m]
        else:
            return RGB
                

3.d. Couleurs hors gamut

Théoriquement, l'espace sRGB ne peut représenter des couleurs situées en dehors de son triangle RGB. Dans ce cas, au moins une des composantes est négative. Bien entendu, les écrans ne peuvent effectuer de synthèse soustractive.

Ce cas doit survenir très fréquemment dans la modélisation des phénomènes physiques, en particulier lorsque la lumière est monochromatique. Comme exemple, considérons un filtre éliminant toutes les longeurs d'ondes supérieures à 530 nm et inférieures à 490 nm :

def Rf(L):
    if (L>530)|(L<490):
        return 0.0
    else:
        return 1.0
XYZ=cie.spectralF2XYZ(Rf) 
xy=cie.XYZ2xy(XYZ[0],XYZ[1],XYZ[2])
RGB=sRGB.rgb(XYZ)
                
print(xy)
--> [0.056023391927449435, 0.6907376361733416]
print(RGB)
--> [-0.31337102291718133, 0.3897655926604191, 0.040435338393942713]

La composante R est négative. Plaçons le point C sur le diagramme de chromaticité :

figure(1)
plot([xy[0]],[xy[1]],c='g',marker='o',label='C')
plot([xy[0],xw],[xy[1],yw],c='g',label='CW')
text(0.26,0.42,'M')
text(0.06,0.71,'C')
text(0.32,0.32,"W")
legend()
                
plotDplotD.pdf

Pour ramener cette couleur dans le domaine sRGB, on peut considérer la couleur la plus saturée de sRGB s'approchant le plus de la couleur C : il s'agit du point M obtenu par intersection du triangle avec le segment reliant le point C au point blanc W.

L'opération consiste à désaturer la couleur C en ajoutant une quantité égale sur chaque composante (ajout de blanc) pour qu'elles soient toutes positives ou nulles. Cette méthode est certainement très approximative, en particulier pour les rouges et les violets (extrémités du spectre) qui sont approchés par des pourpres. Elle a néanmoins l'avantage de la simplicité.

Pour étudier le problème plus en détail, considérons les courbes des composantes RGB des lumières monochromatiques, obtenues avec l'illuminant E (densité spectrique énergétique uniforme) :

cie.setIlluminant("E",0)
r=[]
g=[]
b=[]
Lambda=[]
for k in range(300):
    L=400.0+float(k)
    Lambda.append(L)
    XYZ=cie.XYZMonochrome(L)
    lu=40
    XYZ[0] *= lu
    XYZ[1] *= lu
    XYZ[2] *= lu
    RGB=sRGB.rgb(XYZ)
    r.append(RGB[0])
    g.append(RGB[1])
    b.append(RGB[2])
clf()
xlabel('lambda (nm)')
plot(Lambda,r,c='r',label='r')
plot(Lambda,g,c='g',label='g')
plot(Lambda,b,c='b',label='b')
legend()
grid(True)
                
plotHplotH.pdf

Considérons par exemple la longueur d'onde de 520 nm, pour laquelle la composante rouge est fortement négative. La technique d'ajout de blanc conduit à un niveau de vert de 0.7 et un niveau de bleu de 0.3. On obtient donc un bleu-vert (cyan), assez différent du vert pur de cette longueur d'onde. Pour éviter cet effet, une méthode préférable consiste à simplement annuler les composantes négatives.

Les deux fonctions suivantes réalisent ces opérations puis normalisent comme indiqué plus haut :

Méthode d'approximation par ajout de blanc :

    def rgbN1(self,XYZ):
        RGB = self.rgb(XYZ)
        m=-min([0,RGB[0],RGB[1],RGB[2]])
        RGB[0] += m
        RGB[1] += m
        RGB[2] += m
        m=max(RGB)
        if m>1.0:
            return [RGB[0]/m,RGB[1]/m,RGB[2]/m]
        else:
            return RGB
            

Méthode d'approximation par annulation des composantes négatives :

    def rgbN2(self,XYZ):
        RGB = self.rgb(XYZ)
        for k in range(3):
            if RGB[k]<0:
                RGB[k]=0;
        m=max(RGB)
        if m>1.0:
            return [RGB[0]/m,RGB[1]/m,RGB[2]/m]
        else:
            return RGB
                

L'exemple précédent donne :

RGB=sRGB.rgbN2(XYZ)
                
print(RGB)
--> [0.012194444639277617, 0, 0]

3.e. Correction gamma

La figure suivante est une image comportant un dégradé de blanc, la luminance variant de 0 à 1 :

im=[]
for k in range(400):
    l=float(k)/400.0;
    im.append([l,l,l])
figure(figsize=(9,2.5))
imshow([im],aspect=0.2,extent=[0,1,0,1])
xlabel('Y')
                
plotBplotB.pdf

On obtient un dégradé de gris qui visuellement ne semble pas uniforme. En particulier, le blanc occupe beaucoup moins de place que le noir. Ce résultat est dû à la non linéarité des luminophores des écrans LCD ou CRT.

La luminance délivrée par un luminiphore en fonction du niveau qu'il lui est appliqué peut être modélisée par une loi en exposant. Par exemple pour le canal rouge :

En principe, la non linéarité aurait pu être corrigée de manière matériel dans les écrans, mais cette solution n'a pas été retenue par l'industrie de la télévision. L'effet doit donc être corrigé de manière logiciel, sans quoi les images apparaissent trop sombres. Pour obtenir un dégradé uniforme, on transforme donc les composantes RGB selon la relation :

Cette transformation est appelée la correction gamma. En principe, elle dépend du moniteur, dont le gamma varie entre 1.8 et 2.4. Pour la correction, une valeur 2.2 donne un résultat satisfaisant sur la majorité des moniteurs actuels (parfait sur les moniteurs calibrés sRGB). À noter que le système d'exploitation n'effectue pas de correction gamma lorsqu'on lui demande d'afficher une image RGB (sans profil ICC); c'est pourquoi la correction doit être faite à la source, au moment de coder l'image.

from math import pow
im=[]
gamma=2.2
for k in range(400):
    l=math.pow(float(k)/400.0,1/gamma);
    im.append([l,l,l])
figure(figsize=(9,2.5))
imshow([im],aspect=0.2,extent=[0,1,0,1])
xlabel('Y')
                
plotCplotC.pdf

La norme sRGB est prévue pour l'affichage brute des données RGB, par des logiciels (comme les navigateurs web) n'effectuant aucune correction des luminances. C'est pourquoi la correction gamma doit être intégrée dans la conversion de XYZ vers sRGB. La gamma retenu pour la norme sRGB est 2.2.

Sur certains écrans LCD, la gamma peut dépendre du canal de couleur (R,G ou B). Dans ce cas, il faut effectuer un réglage matériel ou utiliser un profil de couleur adapté pour afficher correctement les images. Le standard sRGB ne prend pas en charge ce cas.

La fonction suivante réalise la conversion complète de XYZ vers RGB, avec normalisation des niveaux, correction des couleurs hors gamut et correction gamma. La valeur par défaut de γ (2.2) est définie dans le constructeur.

    def rgbN2G(self,XYZ):
        RGB = self.rgbN2(XYZ)
        a = 1.0/self.gamma
        RGB[0] = math.pow(RGB[0],a)
        RGB[1] = math.pow(RGB[1],a)
        RGB[2] = math.pow(RGB[2],a) 
        return RGB
    def rgbN1G(self,XYZ):
        RGB = self.rgbN1(XYZ)
        a = 1.0/self.gamma
        RGB[0] = math.pow(RGB[0],a)
        RGB[1] = math.pow(RGB[1],a)
        RGB[2] = math.pow(RGB[2],a) 
        return RGB
        

La fonction suivante effectue la conversion inverse, de l'espace RGB vers CIE XYZ. Cette fonction est utile pour effectuer des conversions entre espaces RGB, l'espace CIE XYZ servant alors d'intermédiaire.

    def XYZG(self,RGB):
        RGB[0] = math.pow(RGB[0],self.gamma)
        RGB[1] = math.pow(RGB[1],self.gamma)
        RGB[2] = math.pow(RGB[2],self.gamma)
        return self.XYZ(RGB)
        

La fonction suivante effectue la conversion de CIE XYZ vers RGB en utilisant la correction gamma préconisée par la norme sRGB :

    def rgbN2G2(self,XYZ):
        RGB = self.rgbN2(XYZ)
        a=1.0/2.4
        for k in range(3):
            if RGB[k]<=0.00304:
                RGB[k] *= 12.92
            else:
                RGB[k] = 1.055*math.pow(RGB[k],a)-0.055
        return RGB
                

3.f. Conversion de xyY ver RGB

Les coordonnées chromatiques (x,y) représentent la couleur; elles sont reportées sur le diagramme de chromaticité. En principe, Y représente la luminance, comprise entre 0 et 1 par convention.

Commme mentionné plus haut, la variable Y ne peut se voir affecter une valeur arbitraire (entre 0 et 1) car la luminance maximale varie. Par exemple, elle devient très faible à proximité de la primaire bleue.

Voyons ce qui se passe si on augmente Y de 0 à 1 :

im=[]
x=0.25
y=0.25
for k in range(100):
    Y=float(k)*0.01
    XYZ=cie.xyY2XYZ(x,y,Y)
    RGB=sRGB.rgbN2G(XYZ)
    im.append(RGB)
figure(figsize=(9,2.5))
imshow([im],aspect=0.2,extent=[0,1,0,1])
xlabel('Y')
                
plotIplotI.pdf

Pour cette couleur, la luminance maximale est atteinte pour Y = 0.5 environ. Cela signifie que cette couleur est deux fois moins lumineuse que le blanc. Voyons les composantes RGB pour cette valeur :

print(sRGB.rgbN2G(cie.xyY2XYZ(x,y,0.5)))
--> [0.6230819182815516, 0.7263585852617359, 0.9921439317613417]

Si on augmente Y au delà de 0.5, il se produit une normalisation par la composante B qui devient égale à 1. Par exemple :

print(sRGB.rgbN2G(cie.xyY2XYZ(x,y,0.7)))
--> [0.6280156520993898, 0.7321100921035115, 1.0]

Il est clair que la procédure de normalisation choisie n'affecte pas la couleur lorsque le maximum est atteint. En revanche, il n'est pas possible d'utiliser la variable Y pour attribuer un niveau relatif de luminance (relatif au maximum), puisque le maximum de luminance n'est pas connu à priori. Pour cela, la solution la plus simple consiste à calculer les composantes RGB avec une luminance de 1, puis les multiplier par la luminance relative choisie. La fonction suivante réalise cette opération :

    def xyL2rgb(self,x,y,Lu):
        XYZ=[x/y,1,(1-x-y)/y]
        RGB=self.rgbN2G(XYZ)
        return [RGB[0]*Lu,RGB[1]*Lu,RGB[2]*Lu]
                

Exemple avec une couleur primaire :

print(sRGB.xyL2rgb(0.15,0.06,0.5))
--> [0.0, 0.0, 0.5]

Effectuons à présent un dégradé de luminance sur une couleur hors gamut :

im=[]
x=0.15
y=0.7
for k in range(100):
    Lu=float(k)*0.01
    RGB=sRGB.xyL2rgb(x,y,Lu)
    im.append(RGB)
figure(figsize=(9,2.5))
imshow([im],aspect=0.2,extent=[0,1,0,1])
xlabel('Y/Ymax')
                
plotJplotJ.pdf

3.g. Exemple : couleurs spectrales

On choisit comme illuminant le corps noir à 6500 K plutôt que D65 (même température de couleur). Un facteur de luminance est appliqué sinon le spectre est trop sombre : en effet, il s'agit d'un filtre qui transmet seulement 1/470 du flux total de l'illuminant. Les deux méthodes de traitement des couleurs hors gamut sont testées.

cie.setIlluminant("CN",6500)
im1=[]
im2=[]
for k in range(300):
    L=400.0+float(k)
    XYZ=cie.XYZMonochrome(L)
    lu=80
    XYZ[0] *= lu
    XYZ[1] *= lu
    XYZ[2] *= lu
    RGB=sRGB.rgbN2G(XYZ)
    im1.append(RGB)
    RGB=sRGB.rgbN1G(XYZ)
    im2.append(RGB)
figure(figsize=(9,2))
imshow([im1],aspect=50,extent=[400,700,0,1])
xlabel("lambda (nm)")
                
plotEplotE.pdf
figure(figsize=(9,2))
imshow([im2],aspect=50,extent=[400,700,0,1])
xlabel("lambda (nm)")
                
plotKplotK.pdf

Le rendu des couleurs spectrales est meilleur avec la méthode d'annulation des composantes négatives (1ere image). Avec la méthode d'ajout de blanc, les rouges sont légèrement pourpres et les verts beaucoup trop bleutés. Le spectre devra être comparé à un spectre réel (observé directement au spectroscope) afin d'optimiser la méthode de traitement des couleurs hors gamut.

4. Espace Adobe RGB(98)

4.a. Définition

Cette espace a été conçu pour les cas où le gamut de l'espace sRGB est insuffisant, par exemple en photographie. Sa définition est donnée par le tableau suivant :

Espace sRGB
RedGreenBlueD65
x0.640.210.150.3127
y0.330.710.060.329
z0.030.080.790.3583

Il diffère de l'espace sRGB par la saturation du vert. Voyons sa position sur le diagramme de chromaticité :

figure(figsize=(8,6))
xlabel('x')
ylabel('y')
xy=cie.xyColor()
plot(xy[0],xy[1],c='k',label='Spectrum locus')
plot([0.64,0.3,0.15,0.64],[0.33,0.6,0.06,0.33],c='r',marker='o',label='sRGB')
plot([0.64,0.21,0.15,0.64],[0.33,0.71,0.06,0.33],c='b',marker='o',label='Adobe RGB')
plot([0.3127],[0.329],c='k',marker='o',label='D65')
legend()
grid(True)
            
plotFplotF.pdf

Matrices de transformation

adobeRGB=XYZ2RGB([0.64,0.33],[0.21,0.71],[0.15,0.06],[xw,yw])
print(adobeRGB.T)
--> matrix([[ 2.0415879 , -0.56500697, -0.34473135],
        [-0.96924364,  1.8759675 ,  0.04155506],
        [ 0.01344428, -0.11836239,  1.01517499]])
print(adobeRGB.Q)
--> matrix([[ 0.57666904,  0.18555824,  0.18822865],
        [ 0.29734498,  0.62736357,  0.07529146],
        [ 0.02703136,  0.07068885,  0.99133754]])

4.b. Exemple : couleurs spectrales

cie.setIlluminant("CN",6500)
im=[]
for k in range(300):
    L=400.0+float(k)
    XYZ=cie.XYZMonochrome(L)
    lu=80
    XYZ[0] *= lu
    XYZ[1] *= lu
    XYZ[2] *= lu
    RGB=adobeRGB.rgbN2G(XYZ)
    im.append(RGB)
figure(figsize=(9,2))
imshow([im],aspect=50,extent=[400,700,0,1])
xlabel("lambda (nm)")
                
plotGplotG.pdf
Creative Commons LicenseTextes et figures sont mis à disposition sous contrat Creative Commons.