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.
from math import *
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
#permet l'affichage des images à l'intérieur du notebook Jupyter
Chargement d'une image à partir de son url et affichage dans le notebook.
img = plt.imread("https://iut-info.univ-reims.fr/users/coutant/img/solo-256px.png")
plt.imshow(img)
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 :
Chaque composante est un nombre réel compris dans l'intervalle [0,1].
La propriété shape
permet de connaître les dimensions de l'image sous la forme d'un n-tuple (hauteur, largeur, profondeur).
img.shape
height = img.shape[0]
width = img.shape[1]
print(height,width)
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)$.
#le code ci-dessous affiche les composantes rouge, vert, bleu du pixel de coordonnées (15,96)
img[96,15]
#le code ci-dessous affiche la composante bleu du pixel de coordonnées (15,96)
img[96,15,2]
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.
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)
display([img,img],5)
$$ \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.
display([img, greyscale(img)], 5)
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.
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.
display([img, sepia(img)], 5)
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
.
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
.
display([img, multiply(img,img2), img2], 5)
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
.
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
.
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.
def smartblur(src, halfw):
return(hblur(vblur(src,halfw),halfw))
display([blur(img,5),smartblur(img,5)],5)
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
.
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).
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 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
:
#dans cet exemple, l'écart type sd vaut 0.1.
sd = 0.1
np.random.randn(1)*sd
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 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.
display([imgsnp,medianfilter(imgsnp,1)],5)
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, exprimée en pourcentage, permet de jouer sur le contraste, enfin la luminance (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 :
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.
display([colorize(img,350),colorize(img,200)],5)
Exercice 10
É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)
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
.
display([img,gradient(img,90,60,55)],5)