Tuto Python & NumPy : traitement d'images

Table des matières

Introduction

  1. Lire une image avec NumPy
  2. Créer une image
  3. Enregistrer un vecteur NumPy sous forme d’image
  4. Manipuler une image

4.1. Histogramme des pixels

4.2. Inverser les couleurs

4.3. Image monochrome

4.4. Image en nuance de gris

4.5. Segmentation

  1. Exercices

5.1. Exercice 1

5.2. Exercice 2

  1. Solution des exercices

6.1. Exercice 1

6.2. Exercice 2

Conclusion

Introduction

Dans ce Tutoriel, nous allons vous initier au traitement d’images en utilisant la bibliothèque NumPy. L'objectif n'est pas d'apprendre la bibliothèque en détail, mais d'en apprendre suffisamment pour que vous soyez capable de réaliser quelques manipulations de bases.

Le traitement d’images consiste à manipuler une image dans le but d’extraire des informations. Chaque image, sous forme numérique, est composée de pixels. Ce sont les plus petites unités d'information qui composent une image. Une façon simple de décrire chaque pixel est d'utiliser une combinaison de trois canaux (couleurs), à savoir: le rouge, le vert et le bleu. C'est ce que nous appelons une image RGB (mode RGB). Chaque canal est associé à une valeur entre 0 et 255. Si ces trois valeurs sont à leur pleine intensité (255), le pixel représente la couleur blanche, et si les trois valeurs sont nulles, la couleur est noire. En général, la combinaison de ces trois valeurs nous donnera une nuance spécifique de la couleur du pixel. Puisque chaque canal peut avoir 256 valeurs différentes d'intensité, cela fait un total de 16,8 millions de nuances. D’autre part, une image en nuance de gris (mode L) ne contient qu’un seul canal (le gris) et les pixels sont représentés par une seule valeur entre 0 et 255 qui représente l’intensité du gris.

Maintenant vous pouvez commencer! Allons-y. 

1. Lire une image avec NumPy

Commençons par importer les bibliothèques avec lesquelles nous allons travailler. Importez numpymatplotlib.pyplot (pour afficher les images) et le module Image de la bibliothèque Pillow (pour importer notre image de test). Vous pouvez consulter le tutoriel « Les bases de traitement d'images en Python : Bibliothèque Pillow », pour plus d’informations sur la bibliothèque Pillow.

Syntaxe:

import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

Avant de commencer, il faut choisir une image de test. 

La commande Image.open() charge une image à partir d'un fichier, et renvoie une instance de la classe Image PIL. Nous appellerons Image.open() et nous lui passerons le chemin vers le fichier image test.jpg :

Syntaxe:

Image = Image.open( "/path/test.jpg" ) 

Pour créer un vecteur numpy (un tableau qui contient les valeurs des pixels) de votre image vous pouvez utiliser np.array().

Syntaxe :


image_array = np.array( image )
print( 'classe :', type(image_array) )
print( 'type :', image_array.dtype )
print( 'taille :', image_array.shape )
print( 'modifiable :', image_array.flags.writeable )

Résultat d’exécution:

Remarquez que nous avons généré un vecteur de classe numpy.ndarray contenant des valeurs de type uint8 (entier) de 3 dimensions dont la forme est (hauteur, largeur, canal (Rouge, Vert, Bleu)). En plus, ce vecteur est modifiable (vous pouvez modifier les valeurs de chaque pixel). Si vous souhaitez créer un vecteur numpy non modifiable pour conserver par exemple votre image principale vous pouvez utiliser la commande np.asarray() : 

Syntaxe :

image_array = np.asarray( image )
print( 'classe :', type(image_array) )
print( 'type :', image_array.dtype )
print( 'taille :', image_array.shape )
print( 'modifiable :', image_array.flags.writeable )

Résultat d’exécution:

Si vous essayez de modifier une valeur par exemple image_array [0, 0, 0] = 0, cela va générer une erreur :

Finalement, vous pouvez afficher l’image à partir du vecteur numpy en utilisant la fonction plt.imshow() :

Syntaxe :

plt.imshow( image_array )
plt.show()

Résultat d’exécution:

2. Créer une image

Dans cette partie, nous allons générer des images en utilisant numpy. Pour créer une image en mode RGB, il faut créer un vecteur de 3 dimensions dont la première dimension est la hauteur, la seconde est la largeur et la troisième représente les canaux RGB (chaque canal est représenté par une valeur de type numpy uint8). Dans le script ci-dessous, nous allons créer un vecteur numpy de taille (100, 300, 3) dont les trois canaux ont tous la valeur 0 pour tous les pixels (ce qui représente une image en noir) :

Syntaxe :


hauteur = 100
largeur = 300
# mode RGB
canal = 3
new_array_RGB = np.zeros([hauteur, largeur, canal], dtype = np.uint8)
plt.imshow( new_array_RGB )
plt.show()

Résultat d’exécution :

Remplissons maintenant le vecteur numpy avec des pixels de couleur rouge (rouge = 255, vert = 0, bleu = 0), vert (rouge = 0, vert = 255, bleu = 0) et bleu (rouge = 0, vert = 0, bleu = 255). Pour réaliser cette tâche, nous décomposons notre vecteur en trois tranches. Les trois valeurs de chaque couleur sont diffusées sur toutes les lignes et colonnes de chaque tranche : la première tranche sera en rouge, la deuxième en vert et la troisième en bleu :

Syntaxe :

# rouge : première tranche 0, 100
new_array_RGB[:,:100,:] = (255, 0, 0)
# vert : deuxième tranche 100, 200
new_array_RGB[:,100:200,:] = (0, 255, 0)
# bleu : Troisième tranche 200, 300
new_array_RGB[:,200:,:] = (0, 0, 255)
plt.imshow(new_array_RGB)
plt.show()

Résultat d’exécution :

Pour créer une image en nuance de gris (mode L : un seul canal), il faut seulement créer un vecteur à deux dimensions représentant la hauteur et la largeur de l’image. Il faut également ajouter un paramètre à plt.imshow() pour préciser que nous voulons afficher une image en nuance de gris :

Syntaxe :

hauteur = 100
largeur = 300
# mode L
new_array_grey = np.zeros([hauteur, largeur], dtype=np.uint8)
# Noir
new_array_grey[:,:100] = 0
# Gris
new_array_grey[:,100:200] = 150
# Blanc
new_array_grey[:,200:] = 255
plt.imshow(new_array_grey, cmap = 'Greys_r')
plt.show()

Résultat d’exécution:

3. Enregistrer un vecteur NumPy sous forme d’image

Vous pouvez facilement faire le chemin inverse et créer une image PIL à partir d’un vecteur numpy en utilisant Image.fromarray() (Si le type de données du vecteur numpy n’est pas un entier une erreur se produira, il est donc nécessaire de le convertir en utilisant np.uint8()) :

Syntaxe :

PIL_image = Image.fromarray( np.uint8( image_array ) )
PIL_image

Résultat d’exécution:

Maintenant, vous pouvez l’enregistrer sous forme de fichier image avec la méthode save(). Le format du fichier sauvegardé est automatiquement déterminé à partir de l'extension :

Syntaxe :

PIL_image.save("/path/nom.png")

Remarque : Une image en nuance de gris (vecteur numpy à deux dimension) peut également être passée à Image.fromarray() (le mode devient automatiquement 'L').

4. Manipuler une image

Passons à la pratique ! L'objectif est d'apprendre à manipuler les images en utilisant la bibliothèque NumPy et de comprendre des notions basiques avec un exemple concret. Pour cela, nous allons utiliser notre image de test :

Syntaxe :

plt.imshow( image_array )
plt.show()

Résultat d’exécution:

4.1. Histogramme des pixels

Nous affichons l'histogramme de notre image qui représente la répartition des pixels selon leur intensité (la fonction flatten() transforme le vecteur numpy en un autre vecteur numpy avec une seule colonne) :

Syntaxe :

plt.hist(image_array.flatten(), bins = 20, density = True , alpha = .5 , edgecolor = 'black', color = 'red')
plt.show()

Résultat d’exécution:

Le filtrage constitue un volet important en traitement d'images. Dans cette partie, nous allons générer des filtres à partir de la bibliothèque PIL, créer des images filtrées et comparer les histogrammes des deux images (originale et filtrée). Vous devez tout d’abord importer le module ImageFilter :

Syntaxe:

from PIL import ImageFilter

Nous utilisons la fonction filter() qui prend en paramètre le nom du filtre que nous souhaitons utiliser. Pour générer une image avec le filtre BLUR, exécutez le script suivant :

Syntaxe:

img = Image.fromarray( np.uint8( image_array_copy ) )
img_BLUR = img.filter( ImageFilter.BLUR )
img_BLUR

Résultat d’exécution:

Pour savoir comment le filtre agit sur l’image, affichons les histogrammes des deux images :

Syntaxe:

plt.hist(image_array.flatten(), bins=20, density=True , alpha=.5 , edgecolor='black', color='red' , label = 'Image originale')
plt.hist( np.array(img_BLUR).flatten(), bins=20, density=True , alpha=.3 , edgecolor='black', color='blue', label = 'Filtre BLUR')
plt.legend()
plt.show()

Résultat d’exécution:

Générez une image avec le filtre CONTOUR :

Syntaxe:

img_CONTOUR = img.filter( ImageFilter.CONTOUR )
img_CONTOUR

Résultat d’exécution:

Affichez les histogrammes des deux images :

Syntaxe:

plt.hist(image_array.flatten(), bins=20, density=True , alpha=.5 , edgecolor='black', color='red' , label = 'Image originale')
plt.hist(np.array(img_CONTOUR).flatten(), bins=20, density=True , alpha=.3 , edgecolor='black', color='blue', label = 'Filtre CONTOUR')
plt.legend()
plt.show()

Résultat d’exécution:

Générez une image avec le filtre FIND_EDGES :

Syntaxe:

img_FINDEDGES = img.filter( FIND_EDGES )
img_FINDEDGES

Résultat d’exécution:

Affichez les histogrammes des deux images :

Syntaxe:

plt.hist(image_array.flatten(), bins=20, density=True , alpha=.5 , edgecolor='black', color='red' , label = 'Image originale')
plt.hist(np.array(img_FINDEDGES).flatten(), bins=20, density=True , alpha=.3 , edgecolor='black', color='blue', label = 'Filtre FIND_EDGES')
plt.legend()
plt.show()

Résultat d’exécution:

Il y a plusieurs filtres que vous pouvez essayer et visualiser leur effet sur l’image. Vous pouvez afficher tous les filtres avec la commande dir( ImageFilter ).

4.2. Inverser les couleurs

Les couleurs dans une image inversée sont les couleurs négatives des originales. Par exemple, un cercle noir sur fond blanc s'affiche en blanc sur fond noir. Il est facile de calculer et de manipuler les valeurs des pixels avec numpy, une image inversée peut être générée en soustrayant la valeur du pixel de la valeur maximale (255) :

Syntaxe:

array_inver = image_array.copy()
array_inver = 255 - array_inver
plt.imshow(array_inver)
plt.show()

Résultat d’exécution:

Remarquez que nous avons créé une copie de notre vecteur numpy image_array pour pouvoir modifier les valeurs des pixels.

4.3. Image monochrome

En mode RGB, Chaque pixel de l'image est représenté par trois entiers. Pour diviser l'image en composantes de couleur distinctes et générer des images monochromes (un seul canal), il suffit de garder un seul canal (Rouge, Vert ou bleu) et mettre les deux autres valeurs à 0. Si vous ne voulez garder que le canal rouge, le script est le suivant :

Syntaxe:

array_R = image_array.copy()
array_R[:,:,(1,2)] = 0
plt.imshow(array_R)
plt.show()

Résultat d’exécution:

Vous pouvez aussi générer des images qui ne contiennent que le canal vert ou bleu :

Syntaxe:

# vert
array_G = image_array.copy()
array_G[:,:,(0,2)] = 0
# Bleu
array_B = image_array.copy()
array_B[:,:,(0,1)] = 0

Finalement, vous pouvez concaténer les images horizontalement avec np.concatenate() :

Syntaxe:

array_RGB = np.concatenate((array_R, array_G, array_B), axis=1)
plt.imshow(array_RGB)
plt.show()

Résultat d’exécution:

4.4. Image en nuance de gris

Nous pouvons aussi facilement transformer l'image en nuance de gris. Il existe plusieurs façons de le faire, mais une façon simple est de prendre la moyenne pondérée des valeurs RGB de l'image originale. Dans le script suivant, nous utilisons les poids suivant : 0.3 pour le canal rouge, 0.5 pour le canal vert et 0.2 pour le bleu. Vous pouvez changer les poids et voir leur effet sur l’image en nuance de gris générée.

Syntaxe:

L = image_array.shape[0]
H = image_array.shape[1]
# Créer un vecteur numpy de taille (L, H)
array_grey = np.zeros((L,H))
# Moyenne pondérée
for i in range(L):
for j in range(H):
array_grey[i,j] = ( 0.3*image_array[i,j,0] + 0.5*image_array[i,j,1] + 0.2*image_array[i,j,2] )
plt.imshow(array_grey, cmap = 'Greys_r')
plt.show()

Résultat d’exécution:

4.5. Segmentation

Le but de la segmentation des images est d'étiqueter chaque pixel d'une image avec une classe correspondante. Il y a plusieurs façons de faire cela, la plus simple consiste à segmenter l’image en deux régions (premier plan et arrière-plan). Cela revient à convertir l'image en image monochrome ou en niveaux de gris, et fixer un seuil. Les pixels dont la valeur est supérieure au seuil sont traités comme appartenant à la première région, et les autres pixels (inférieur au seuil) à la deuxième région. La manière dont nous choisirons le seuil sera spécifique à l'application. Commençons d’abord par afficher le vecteur numpy de notre image en nuance de gris :

Syntaxe:

print(array_grey)

Résultat d’exécution:

Fixons le seuil à 180, le script suivant génère un vecteur numpy de type boolien : True si la valeur est supérieur au seuil et False si le contraire :

Syntaxe:

array_seg = array_grey > 180
print(array_seg)

Résultat d’exécution:

Vous pouvez afficher l’image segmentée directement à partir du vecteur précédant (True prend automatiquement la valeur maximale 255 ce qui correspond à la couleur blanche et False prend la valeur minimal 0 ce qui correspond à la couleur noire) :

Syntaxe:

plt.imshow(array_seg, cmap='Greys_r')
plt.show()

Résultat d’exécution:

5. Exercices

5.1. Exercice 1

Question : Générer une image d’échiquier en noir et blanc de taille (200,200) avec numpy.

5.2. Exercice 2

Pour cet exercice, nous allons utiliser une image (PIL_image) générée à partir de la bibliothèque Scikit Image. Le script est le suivant:

Syntaxe :

from skimage import data
import matplotlib.pyplot as plt
import numpy as np
image = data.astronaut()
plt.imshow(image)
plt.show()

Résultat d’exécution:

Question 1: Générer trois images monochromes (Rouge, Vert et Bleu).

Question 2: Pour chaque image monochrome, effectuer une segmentation avec trois seuils : [100, 150, 200]. Concaténer les images et afficher le résultat.

6. Solution des exercices

6.1. Exercice 1

Syntaxe :

import numpy as np
import matplotlib.pyplot as plt
echiquier_array = np.zeros([200, 200], dtype = np.uint8)
for x in range(200):
for y in range(200):
if (x % 50) // 25 == (y % 50) // 25:
echiquier_array[x, y] = 0
else:
echiquier_array[x, y] = 255
plt.imshow(echiquier_array, cmap='Greys_r')
plt.show()

Résultat d’exécution:

6.2. Exercice 2

Question 1:

Syntaxe :

fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(15,5))
array_R = image.copy()
array_R[:,:,(1,2)] = 0
array_G = image.copy()
array_G[:,:,(0,2)] = 0
array_B = image.copy()
array_B[:,:,(0,1)] = 0
axs[0].imshow(array_R)
axs[1].imshow(array_G)
axs[2].imshow(array_B)
plt.show()

Résultat d’exécution:

Question 2:

Syntaxe :

fig, axs = plt.subplots(nrows=3, ncols=1, figsize=(15,8))
# rouge
array_R = image.copy()[:,:,0]
array_R_100 = array_R > 100
array_R_150 = array_R > 150
array_R_200 = array_R > 200
array_R_conca = np.concatenate((array_R_100, array_R_150, array_R_200), axis = 1)
# Vert
array_G = image.copy()[:,:,1]
array_G_100 = array_G > 100
array_G_150 = array_G > 150
array_G_200 = array_G > 200
array_G_conca = np.concatenate((array_G_100, array_G_150, array_G_200), axis = 1)
# Bleu
array_B = image.copy()[:,:,2]
array_B_100 = array_B > 100
array_B_150 = array_B > 150
array_B_200 = array_B > 200
array_B_conca = np.concatenate((array_B_100, array_B_150, array_B_200), axis = 1)
# afficher les images
axs[0].imshow(array_R_conca, cmap = 'Greys_r')
axs[0].set_title('Rouge')
axs[0].set_axis_off()
axs[1].imshow(array_G_conca, cmap = 'Greys_r')
axs[1].set_title('Vert')
axs[1].set_axis_off()
axs[2].imshow(array_B_conca, cmap = 'Greys_r')
axs[2].set_title('Bleu')
axs[2].set_axis_off()
plt.show()

Résultat d’exécution:

Conclusion

Nous sommes arrivés à la fin de ce tutoriel. Nous espérons qu’il vous sera utile pour vous initier au traitement d’images avec la bibliothèque NumPy. Mais ne vous arrêtez pas là, ce tutoriel n’a qu'effleuré la surface de ce qu’on peut faire avec le traitement d'images en utilisant numpy. Cependant, il existe encore trop de notions à apprendre.

Merci d’avoir lu et bon courage pour la suite.

Article publié le 21 Novembre 2020par Sami Nadif