Algèbre linéaire et traitement des images

DEUXIÈME PARTIE : TRANSFORMATION DES COULEURS

Étienne Coutant, Olivier Nocent, Frédéric Blanchard

Crayons de couleurs

Dans cette deuxième partie, nous allons appliquer des transformations aux composantes chromatiques des pixels. Plusieurs modèles de couleurs1 seront abordés.

1 Introduction

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)

{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 :

def clamp(pixel):
    """Fonction qui retourne une nouveau pixel dans lequel 
    les composantes du pixel passé en argument sont 
    replacées dans l'intervalle [0,1]"""
    pix = np.copy(pixel)
    for i in range(0,len(pix)):
        if pix[i]<0:
            pix[i] = 0
        else:
            if pix[i]>1:
                pix[i] = 1
    return(pix)

2 Conversion en niveaux de gris

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)

(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.

img = plt.imread("./images/solo-256px.png")
display([img, greyscale(img)], 5)

3 Conversion en sepia

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.

img = plt.imread("./images/solo-256px.png")
display([img, sepia(img)], 5)

4 Mélange d'images

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 :

img2 = plt.imread('./images/logo-starwars-256px.png')
display([img, mix(img,img2,0.5), img2], 5)

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.

display([img, multiply(img,img2), img2], 5)

5 Modèles de couleur : RGB, HSL, CMY, CMYB

5.1 Modèle RGB (Red Green Blue)

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.

Représentation graphique de l'espace du modèle RGB

5.2 Modèle HSL (Hue Saturation Lightness)

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.

Représentation graphique de l'espace du modèle HSL

5.3 Fonctions de conversion

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])

5.4 Colorisation

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 :

  1. Conversion RGB vers HSL
  2. Modification de la composante H correspondant à la teinte désirée
  3. Conversion HSL vers RGB

La teinte est un angle et s'exprime en degrés.

Le cercle suivant vous permettra de trouver l'angle associé à une couleur :

Cercle des teintes

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.

display([colorize(img,350),colorize(img,200)],5)

5.5 Saturation

Exercice

Écrire une fonction saturation(src,m) qui, pour une image src passée en paramètre, multiplie la saturation par m.

display([img,saturation(img,0.5),saturation(img,2)],5)

5.6 Gradient radial

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.

display([img,gradient(img,90,60,55)],5)

5.7 CMY et CMYB

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

p1 = [0.2,0.8,0.6]
p2 = rgb2cmyb(p1)
print(p2)
[0.6000000000000001, 0.0, 0.20000000000000007, 0.19999999999999996]
cmyb2rgb(p2)
[0.19999999999999996, 0.8, 0.6]

É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 :

On retourne enfin l'image obtenue.

L'exemple suivant simule une panne de la cartouche d'encre noire :

display([img, empty_cartridge(img,3)],5)

Avec l'image précédente, et en réalisant quelques essais, quelle panne aura visuellement le moins d'impact ?

6 Bonus : bruitage et débruitage

6.1 Ajout de flou

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.

Schéma d'un voisinage carré de largeur 2 du pixel p_{i,j}

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.

display([img,blur(img,5)],5)

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.

display([img,hblur(img,5),vblur(img,5)],5)

On définit la fonction smartblur(src, s) qui applique successivement un flou vertical et un flou horizontal.

6.2 Comparaison des temps d'exécution

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.

from time import perf_counter

def temps(f,img,p):
    t0 = perf_counter()
    f(img,p)
    t1 = perf_counter()
    return(t1-t0)

On définit maintenant la fonction smartblurqui utilise les fonctions hbluret vblur pour appliquer le flou sur l'image.

def smartblur(src, s):
    return(hblur(vblur(src,s),s))

display([blur(img,5),smartblur(img,5)],5)

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()

6.3 Application au débruitage d'images

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 :

#dans cet exemple, l'écart type sd vaut 0.1.
sd = 0.1
np.random.randn(1)*sd
array([0.12778698])

Exemple

display([img,gaussnoise(img,0.1)],5)

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.

imgbruit = gaussnoise(img,0.1)
display([imgbruit,smartblur(imgbruit,1),smartblur(imgbruit,2)],5)

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.

display([imgsnp,medianfilter(imgsnp,1)],5) 

7 Conclusion

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.


  1. L'excellent ouvrage en ligne « Programming design systems » présente notamment un chapitre sur l'histoire de la couleur très intéressant.↩︎

  2. 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↩︎