Traitement d'images avec Python

Première partie : modifications des couleurs

1. Introduction

L'objectif de cette activité est d'illustrer le rôle du calcul matriciel (applications linéaires, transformations) en traitement numérique des images. Chaque exercice consiste à créer un filtre modifiant le contenu d'un fichier image.

Vous pouvez télécharger les deux fichiers PNG ci-dessous, utilisés dans les exemples, pour tester l'ensemble de vos filtres :

Configuration : import des modules maths, numpy pour la manipulation de tableaux et matplotlib pour l'affichage des images.

In [1]:
from math import *
import numpy as np
import matplotlib.pyplot as plt
In [2]:
%matplotlib inline
#permet l'affichage des images à l'intérieur du notebook Jupyter

1.1 Premiers pas avec les images

Chargement d'une image à partir de son url et affichage dans le notebook.

In [3]:
img = plt.imread("https://iut-info.univ-reims.fr/users/coutant/img/solo-256px.png")
In [4]:
plt.imshow(img)
Out[4]:
<matplotlib.image.AxesImage at 0x7f18c21b6be0>

La fonction imread initialise une variable de type array de taille hauteur × largeur × profondeur . Les hauteur et largeur correspondent aux dimensions, en pixels,de l'image chargée.
La profondeur, quant à elle, correspond au nombre de composantes du pixel :

  • 1 pour les images de luminance (niveaux de gris)
  • 3 pour les images couleur (rouge, vert, bleu)
  • 4 pour les images couleur avec transparence (rouge, vert, bleu, alpha)

Chaque composante est un nombre réel compris dans l'intervalle [0,1].

1.2 Dimensions de l'image

La propriété shape permet de connaître les dimensions de l'image sous la forme d'un n-tuple (hauteur, largeur, profondeur).

In [5]:
img.shape
Out[5]:
(256, 256, 3)
In [6]:
height = img.shape[0]
width = img.shape[1]
print(height,width)
256 256

1.3 Accès à un pixel de l'image

L'accès aux composantes RGB d'un pixel de coordonnées (x,y) s'effectue à l'aide de img[y,x]. Le premier indice correspond à la coordonnée y (hauteur, c-à-d le numéro de lignes) et le deuxième à la coordonnée x (largeur, c-à-d le numéro de colonnes).

Le pixel du coin supérieur gauche de l'image a pour coordonnées $(0,0)$, l'axe $x$ est horizontal dirigé vers la droite et l'axe $y$ est vertical dirigé vers le bas. Ainsi le pixel du coin inférieur droit de l'image a pour coordonnées $(width-1,height-1)$.

In [7]:
#le code ci-dessous affiche les composantes rouge, vert, bleu du pixel de coordonnées (15,96)

img[96,15]
Out[7]:
array([0.08627451, 0.09019608, 0.10196079], dtype=float32)
In [8]:
#le code ci-dessous affiche la composante bleu du pixel de coordonnées (15,96)

img[96,15,2]
Out[8]:
0.101960786

1.4 Affichage de l'image

Nous définissons ci-dessous la fonction display(imglist,size), qui affiche une série d'images stockées dans une liste imglist, la taille de chaque image étant de size pouces.

In [9]:
def display(imglist,size):
    cols = len(imglist)
    fig = plt.figure(figsize=(size*cols,size*cols))
    for i in range(0,cols):
        a = fig.add_subplot(1, cols, i+1)
        subfig = plt.imshow(imglist[i])
        subfig.axes.get_xaxis().set_visible(False)
        subfig.axes.get_yaxis().set_visible(False)
In [10]:
display([img,img],5)

2. Modification de couleurs

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

$$ \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) $$

Exercice 1

Écrire une fonction Python greyscale(src) qui retourne une image en niveaux de gris à partir de l’image RGB src passée en paramètre.

In [12]:
display([img, greyscale(img)], 5)

2.2 Sépia

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 dont la matrice figure ci-dessous :

$$ \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 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 ci-dessous.

In [13]:
def clamp(pixel):
    for i in range(0,len(pixel)):
        if pixel[i]<0:
            pixel[i] = 0
        else:
            if pixel[i]>1:
                pixel[i] = 1
  
    return(pixel)

Exercice 2
Écrire une fonction sepia(src) qui retourne une image au ton sépia à partir de l'image RGB src passée en paramètre.

In [15]:
display([img, sepia(img)], 5)

2.3 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 3
É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.

In [17]:
img2 = plt.imread('https://iut-info.univ-reims.fr/users/coutant/img/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 g_m \end{array} \right) $$

Exercice 4
Écrire une fonction multiply(src,mask) qui retourne une image issue du produit pixel par pixel d'une image src avec un masque mask.

In [19]:
display([img, multiply(img,img2), img2], 5)

2.4 Filtre 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.

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 5
Écrire une fonction blur(src,halfw) qui floute une image src passée en paramètre, en utilisant un filtre moyenneur, et un voisinage carré de demi-largeur halfw.

In [21]:
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 6
Écrire deux fonctions hblur(src,halfw) et vblur(src,halfw) qui floutent une image src passée en paramètre en parcourant un intervalle horizontal (resp. vertical) de demi-largeur halfw.

In [23]:
display([img,hblur(img,5),vblur(img,5)],5)

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

In [24]:
def smartblur(src, halfw):
    return(hblur(vblur(src,halfw),halfw))

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

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.

In [25]:
import time

def temps(f,img,p):
    t0 = time.clock()
    f(img,p)
    t1 = time.clock()
    return(t1-t0)

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

In [26]:
t1 = [temps(blur,img,p) for p in range(1,6)]
t2 = [temps(smartblur,img,p) for p in range(1,6)]
In [27]:
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()

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

In [28]:
#dans cet exemple, l'écart type sd vaut 0.1.
sd = 0.1
np.random.randn(1)*sd
Out[28]:
array([-0.11881193])

Exemple :

In [30]:
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.

In [31]:
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.

In [32]:
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 8
Écrire une fonction medianFilter(src,halfw) qui applique un filtre médian avec un voisinage carré centré de demi-largeur halfw à une image src passée en paramètre.

In [35]:
display([imgsnp,medianfilter(imgsnp,1)],5) 

3. Modèles de couleur : RGB, HSL

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

3.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, exprimée en pourcentage, permet de jouer sur le contraste, enfin la luminance (lightness) permet de définir une couleur plus ou moins claire.

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

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

In [36]:
#
# 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])

3.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 :

  • Conversion RGB vers HSL
  • Modification de la composante H correspondant à la teinte désirée
  • Conversion HSL vers RGB

Exercice 9
É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.

In [38]:
display([colorize(img,350),colorize(img,200)],5)

3.5 Saturation

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

In [40]:
display([img,saturation(img,0.5),saturation(img,2)],5)

3.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 11
É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.

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