DEUXIÈME PARTIE : TRANSFORMATION DES COULEURS
Dans cette deuxième partie, nous allons appliquer des transformations aux composantes chromatiques des pixels. Plusieurs modèles de couleurs1 seront abordés.
Nous avons vu dans la première partie qu'un pixel (couleurs) p pouvait être représenté par un triplet (r, g, b).
En considérant cette représentation du pixel, on peut le voir comme un vecteur de \mathbb{R}^3. On peut alors appliquer à p des transformations (linéaires notamment), qui changeront ses composantes chromatiques et donc sa couleur.
Si on appelle f l'application associée à une transformation et M, la matrice 3\times 3 correspondante, on peut formaliser la transformation de la manière suivante :
{p'} = f(p) \Longleftrightarrow {p'} = M\times p \Longleftrightarrow \left(\begin{array}{c} {r'} \\ {g'} \\ {b'} \end{array}\right) = \left(\begin{array}{ccc} {m}_{1,1} & {m}_{1,2} & {m}_{1,3} \\ {m}_{2,1} & {m}_{2,2} & {m}_{2,3} \\ {m}_{3,1} & {m}_{3,2} & {m}_{3,3} \end{array}\right) . \left(\begin{array}{c} {r} \\ {g} \\ {b} \end{array}\right)
où {r'}, {g'} et {b'} sont les composantes chromatiques du pixel transformé {p'}.
Attention : il arrivera que les valeurs obtenues {r'}, {g'}, {b'} sortent de l'intervalle [0,1]. Il sera nécessaire de les y replacer. La solution la plus simple consiste à fixer à 1 toute valeur dépassant 1 et à 0 toute valeur inférieure à 0.
Le fonction clamp(pixel)
ci-dessous permet d'effectuer cette opération :
Pour convertir une image couleur RGB en une image en niveaux de gris, il faut calculer la luminance de chaque pixel. Cette luminance est une combinaison linéaire des composantes rouge, vert et bleu.
À un pixel p, de composantes (r,g,b) dans l'image en couleurs, on associe le niveau de gris : 0.2126 \times r + 0.7152 \times g + 0.0722 \times b.
Le rendu en niveaux de gris d'une image en couleurs est donc obtenue par la transformation linéaire suivante :
\left( \begin{array}{c} {r'} \\ {g'} \\ {b'} \end{array} \right)= \begin{pmatrix} 0.2126 & 0.7152 & 0.0722 \\ 0.2126 & 0.7152 & 0.0722 \\ 0.2126 & 0.7152 & 0.0722 \end{pmatrix} \left( \begin{array}{c} r \\ g \\ b \end{array} \right)
où (r^{\prime}, g^{\prime}, b^{\prime}) est l'encodage du niveau de gris (sur trois composantes) de la couleur (r,g,b) transformée.
Exercice
Écrire une fonction Python greyscale(img)
qui retourne une image en niveaux de gris à partir de l’image couleurs img
.
Le ton sépia rappelle l'aspect visuel des photographies du début du XXe siècle. Cet effet peut être obtenu grâce à l'application linéaire suivante :
\left( \begin{array}{c} {r'} \\ {g'} \\ {b'} \end{array} \right)= \begin{pmatrix} 0.393 & 0.769 & 0.189 \\ 0.349 & 0.686 & 0.168 \\ 0.272 & 0.534 & 0.131 \end{pmatrix} \left( \begin{array}{c} r \\ g \\ b \end{array} \right)
Dans la mesure où la somme des coefficients d'une même ligne de la matrice est supérieure à 1, le résultat de la combinaison linéaire doit être limité à 1 si celui-ci est hors de l'intervalle [0,1]. Pour cela, vous pouvez utiliser la fonction clamp(pixel)
disponible au début de cette partie.
Exercice
Écrire une fonction Python sepia(img)
qui retourne l'image sepia obtenue à partir de l’image couleurs img
.
Cette dernière utilisation des combinaisons linéaires consiste à mélanger deux images de même taille en calculant une moyenne pondérée des pixels des deux images source. La formule ci-dessous illustre le calcul à réaliser :
\left( \begin{array}{c} r' \\ g' \\ b' \end{array} \right)= \alpha \left( \begin{array}{c} r_1 \\ g_1 \\ b_1 \end{array} \right)+(1-\alpha) \left( \begin{array}{c} r_2 \\ g_2 \\ b_2 \end{array} \right) \quad \text{où} \quad \alpha \in [0,1]
Exercice
Écrire une fonction mix(src1,src2,factor)
qui retourne une image issue du mélange des images src1
et src2
passées en paramètres avec un poids égal à factor
.
Exemple :
Une autre manière de mélanger des images consiste à multiplier les composantes des pixels d'une image avec celles d'un masque (i.e. une image noir et blanc). La formule ci-dessous illustre le calcul à réaliser :
\left( \begin{array}{c} {r'} \\ {g'} \\ {b'} \end{array} \right) = \left( \begin{array}{c} r \times r_{m} \\ {g} \times g_{m} \\ {b} \times b_{m} \end{array} \right)
Exercice
Écrire une fonction multiply(src,mask)
qui retourne une image issue du produit pixel par pixel d'une image src
avec un masque mask
.
Ce modèle repose sur le principe de la synthèse additive qui permet de générer une grande variété de couleurs à partir des trois couleurs rouge, vert et bleu. En utilisant 256 nuances pour chaque couleur, on peut ainsi produire jusqu'à 256^3 = 16 777 216 couleurs différentes.
HSL est un modèle de couleur alternatif à RGB. La teinte (hue) est représentée par un angle exprimé en degrés couvrant tout le spectre colorimétrique. La saturation (saturation), exprimée en pourcentage, permet de jouer sur le contraste, enfin la luminance2 (lightness) permet de définir une couleur plus ou moins claire.
Afin de faciliter le passage d'un modèle de couleur à l'autre, vous pourrez utiliser les fonctions utilitaires rgb2hsl(pixel)
et hsl2rgb(pixel)
dont le code est disponible ci-dessous.
#
# RGB to HSL conversion function
# http://www.rapidtables.com/convert/color/rgb-to-hsl.htm
#
# Parameter:
# - pixel: array containing red, green and blue values
#
# Returns:
# array containing hue, saturation and lightness values
#
def rgb2hsl(pixel):
r=pixel[0]
g=pixel[1]
b=pixel[2]
cmax=max(r,g,b)
cmin=min(r,g,b)
delta=cmax-cmin
# Lightness calculation
l=(cmax+cmin)/2
# Saturation calculation
s=0
if delta!=0:
s=delta/(1-abs(2*l-1))
# Hue calculation
h=0
if delta!=0:
if cmax==r:
h=(((g-b)/delta)%6)*60
if cmax==g:
h=(2+(b-r)/delta)*60
if cmax==b:
h=(4+(r-g)/delta)*60
return([h,s,l])
#
# HSL to RGB conversion function
# http://www.rapidtables.com/convert/color/hsl-to-rgb.htm
#
# Parameter:
# - pixel: array containing hue, saturation and lightness values
#
# Returns:
# array containing red, green and blue values
#
def hsl2rgb(pixel):
h=pixel[0]
s=pixel[1]
l=pixel[2]
c=(1-abs(2*l-1))*s
x=c*(1-abs((h/60)%2-1))
r=0
g=0
b=0
if h>=0 and h<60:
r=c
g=x
if h>=60 and h<120:
r=x
g=c
if h>=120 and h<180:
g=c
b=x
if h>=180 and h<240:
g=x
b=c
if h>=240 and h<300:
r=x
b=c
if h>=300 and h<360:
r=c
b=x
m=l-c/2
return clamp([r+m,g+m,b+m])
Afin de coloriser une image, il suffit d'affecter à chaque pixel une teinte constante. Cette opération peut être réalisée en trois étapes :
La teinte est un angle et s'exprime en degrés.
Le cercle suivant vous permettra de trouver l'angle associé à une couleur :
Exercice
Écrire une fonction colorize(src,hue)
qui colorise une image src
passée en paramètre avec la teinte hue
, correspondant à un angle exprimé en degrés.
Exercice
Écrire une fonction saturation(src,m)
qui, pour une image src
passée en paramètre, multiplie la saturation par m
.
Le principe du gradient radial consiste à diminuer la luminosité (lightness) d'un pixel en fonction de sa distance d au centre (x_C,y_C) d'un cercle de rayon r : la nouvelle luminosité L' correspond au minimum entre l'ancienne lumonisité L du pixel et la valeur f(d).
L' = min(L,f(d)) \text{avec} \quad f(d) = max(0;1-d/r) \quad \text{et} \quad d = \sqrt{ (x - x_C)^2 + (y - y_C)^2 }
Exercice
Écrire une fonction gradient(src,centerx,centery,radius)
qui applique le principe du gradient radial à une image src
passée en paramètre pour un cercle de centre (centerx
,centery
) et de rayon radius
.
Le modèle CMYB (Cyan, Magenta, Yellow, Black)$ est le modèle de couleur utilisé par les imprimeurs. En effet, s'il est bien adapté à un affichage à l'écran, le modèle RGB ne l'est pas pour l'impression papier.
Pour passer de l'espace RGB à l'espace CMYB, on passe d'abord par l'espace CMY, à l'aide de la transformation suivante :
\left(\begin{array}{c}C\\M\\Y\end{array}\right) = \left(\begin{array}{c}1\\1\\1\end{array}\right) - \left(\begin{array}{c}r\\g\\b\end{array}\right)
On passe ensuite à l'espace CMYB en ajoutant un calcul du noir :
b = min(C,M,Y)
puis :
\left(\begin{array}{c}c\\m\\y\end{array}\right) = \left(\begin{array}{c}C\\M\\Y\end{array}\right) - \left(\begin{array}{c}b\\b\\b\end{array}\right)
Exercice
Écrire les fonctions rgb2cmyb(pixel)
et cymb(pixel)
qui permettent de passer d'un pixel en RVB à un pixel en CYMB et réciproquement.
Exemple
[0.6000000000000001, 0.0, 0.20000000000000007, 0.19999999999999996]
Écrire ensuite une fonction empty_cartridge(img, col)
qui simule le résultat visuel obtenu avec une imprimante jet d'encre dont une cartouche d'encre est vide. Elle prend en argument une image img
et un entier col
désignant la cartouche vide (0 : cartouche Cyan ; 1 : cartouche Magenta ; 2 : cartouche Yellow ; 3 : cartouche Black).
La transformation opérée sur l'image initiale est la suivante suivante :
Pour chaque pixel p :
col
On retourne enfin l'image obtenue.
L'exemple suivant simule une panne de la cartouche d'encre noire :
Avec l'image précédente, et en réalisant quelques essais, quelle panne aura visuellement le moins d'impact ?
Le principe des filtres de flous est de calculer la couleur d’un pixel de l’image destination à partir de la couleur du pixel de l’image source et de ses voisins. En général, le voisinage est carré, centré sur le pixel courant. Plus le voisinage choisi est grand, plus l'image est floue.
Pour le filtre moyenneur, la couleur d'un pixel de l'image de destination est la moyenne des couleurs du pixel source et de ses voisins.
Exercice
Écrire une fonction blur(src,s)
qui floute une image src
passée en paramètre, en utilisant un filtre moyenneur, et un voisinage carré de demi-largeur s
.
Le filtre flou peut devenir lent avec des tailles de voisinage importantes. Ce filtre étant séparable, il peut se décomposer en deux filtres consécutifs : un flou horizontal suivi d'un flou vertical.
Exercice
Écrire deux fonctions hblur(src,s)
et vblur(src,s)
qui floutent une image src passée en paramètre en parcourant un intervalle horizontal (resp. vertical) de demi-largeur s
.
On définit la fonction smartblur(src, s)
qui applique successivement un flou vertical et un flou horizontal.
La fonction temps(f,img,p)
, définie ci-après, permet de calculer le temps d'exécution d'une fonction f
appliquée à une image img
avec un paramètre p
.
On définit maintenant la fonction smartblur
qui utilise les fonctions hblur
et vblur
pour appliquer le flou sur l'image.
Le graphique ci-dessous représente l'évolution des temps de calcul du filtre flou via blur
(courbe rouge) et de la combinaison des filtres flous horizontal et vertical via smartblur
(courbe verte).
t1 = [temps(blur,img,p) for p in range(1,6)]
t2 = [temps(smartblur,img,p) for p in range(1,6)]
abscisse = range(1,6)
plt.plot(abscisse,t1,'r',label="blur")
plt.plot(abscisse,t2,'g',label="smartblur")
plt.legend()
plt.xlabel("half-width (pixels)")
plt.ylabel("time (s)")
plt.show()
Le bruit numérique est une dégradation d'une image numérique. En photographie numérique par exemple, un bruit peut apparaître en augmentant la sensibilité ISO de son appareil.
Pour simuler un tel bruit, on peut créer un bruit additif gaussien qui, pour chaque pixel, va ajouter à chaque composante RGB un nombre issu d'une loi gaussienne (=loi normale).
Exercice
Écrire une fonction gaussnoise(src,sd)
qui ajoute un bruit gaussien, de moyenne 0 et d'écart type sd
, à une image src
passée en paramètre.
La commande suivante permet de générer un nombre issue d'une loi normale de moyenne 0 et d'écart type sd
:
Exemple
Le filtre flou peut être utilisé pour atténuer ce bruit au sein d'une image en remplaçant la valeur d'un pixel par la moyenne de ses voisins.
Le bruit est un peu atténué, mais au détriment de la netteté.
Cette technique s'avère encore moins efficace avec un bruit ponctuel sel et poivre.
def saltnpepper(src,density):
h = src.shape[0]
w = src.shape[1]
n = floor(w*h*density/2)
dst = np.copy(src)
x = np.random.randint(0,w,size=n)
y = np.random.randint(0,h,size=n)
for i in range(0,n):
dst[y[i],x[i]] = [0,0,0]
x = np.random.randint(0,w,size=n)
y = np.random.randint(0,h,size=n)
for i in range(0,n):
dst[y[i],x[i]] = [1,1,1]
return(dst)
imgsnp = saltnpepper(img,0.05)
display([imgsnp,smartblur(imgsnp,1),smartblur(imgsnp,2)],5)
Le filtre médian consiste à rassembler dans un tableau les valeurs des pixels voisins du pixel courant. La valeur du pixel résultat correspond alors à la médiane de ce tableau.
Pour des images RGB, le filtre médian est appliqué indépendamment sur chacune des 3 composantes à l'aide de la fonction np.median()
.
Exercice
Écrire une fonction medianFilter(src,s)
qui applique un filtre médian avec un voisinage carré centré de demi-largeur s
à une image src
passée en paramètre.
C'est la fin de cette deuxième partie dans laquelle nous avons vu comment on pouvait utiliser l'algèbre linéaire pour transformer les couleurs d'une image.
Dans la partie suivante, consacrée aux transformations géométriques, nous allons nous intéresser aux autres caractéristiques du pixel : ses coordonnées dans l'image.
L'excellent ouvrage en ligne « Programming design systems » présente notamment un chapitre sur l'histoire de la couleur très intéressant.↩︎
La traduction rigoureuse serait plutôt « luminosité », toutefois, pour éviter les confusions, nous avons choisi de préféré le terme de « luminance » voir article Wikipedia sur TSL↩︎