Retour

Rudiments de traitement d'images avec Python

Qu'est-ce qu'une image

Quand on y réfléchit, définir une image n'est pas si facile ! Je dirais, de façon imparfaite, que c'est un ensemble de points qui représentent une forme au sens large du terme (paysage, personne, objet, dessin, etc.). La chose importante est que ce n'est pas un objet continu au sens mathématique du terme mais discret. Les points d'une image sont généralement appelés des pixels (du terme anglais "picture element") mais rigoureusement ce terme devrait être réservé aux images numérisées.

Une image peut être représentée sous la forme d'une matrice NxM, où chaque élément serait un pixel. N et M définissent la taille de la matrice et donc la taille de l'image numérisée encodée par la matrice. Son traitement va se résumer pour l'essentiel à du calcul matriciel et de la manipulation d'éléments d'une matrice.

Un pixel est un nombre, qui code son aspect dans l'image, sa couleur et parfois sa transparence. La nature informatique du nombre, short, integer, ou long, dépend donc du nombre de couleurs que l'on veut coder. Par exemple, si l'on veut coder une image en noir et blanc, un seul bit est suffisant pour exprimer un pixel. Si l'on désire coder une image en 256 couleurs, il faudra 8 bits et pour 64K couleurs il faudra 16 bits. Ainsi, une image de 64K couleurs sera codée par une matrice de pixels, chacun étant codé par un nombre de 16 bits (en Python un entier sera donc largement suffisant..).

Pour résumer, dans une image, un pixel est caractérisé par sa position qui est indiquée par les indices ligne et colonne de la matrice et par sa valeur, qui code la couleur du pixel.

Image et fichier image

Une erreur extrémement répandue jusque dans les cours de traitement d'images est de confondre l'image et le fichier qui contient, entre autre, l'image. On lit souvent des phrases du type "une image jpeg" ou "une image png": ce sont des abus de langage ! Ce n'est pas l'image, qui est je le rappelle une matrice de nombres, qui est jpeg ou png, c'est le fichier qui contient l'image !

Quelle est la différence ? Un fichier "image" contient plus qu'une image. Il contient dans son entête des données, plus exactement des métadonnées, qui décrivent la forme, la dimension, les traitements de compressions éventuellement subis par l'image, son auteur, la date de création et beaucoup d'autres données. D'ailleurs, si vous êtes curieux, vous pouvez calculer la taille d'une image donnée en octets (assez facile) puis la comparer à la taille en octets du fichier qui stocke l'image : c'est instructif !

Lorsqu'on parle d'une image au format jpeg, on désigne en fait un fichier avec une extension .jpeg ou .jpg, qui contient une image ayant subit une compression avec un algorithme jpeg. Les caractéristiques de la compression sont indiquées dans les métadonnées du fichier, mais pas dans la matrice qui code l'image ! C'est la même chose pour les autres formats de fichiers images, que je vais lister ci-dessous.

les principaux formats de fichiers images

Ils sont très nombreux, aussi, je ne vais citer que les formats les plus courants, ceux que vous voyez tous les jours:

En traitement d'images, il se peut que vous ayez besoin d'informations sur l'image stockée dans un fichier image. Vous les trouverez dans l'en-tête du fichier. Certaines librairies d'imagerie permettent d'extraire ces en-têtes et de les décomposer. Vous pouvez aussi chercher la description des en-têtes de fichier et lire vous-même ces informations.

Le traitement d'images en physique

Certains d'entre vous se demanderont peut-être pourquoi aborder ce sujet sur un site de physique numérique ? Il est vrai que les images interviennent peu en simulation, bien que l'on soit amené à en construire parfois. Mais le sujet n'est pas secondaire en physique expérimentale et en ingénierie.
Certaines disciplines de la physique sont grandes consommatrices de traitement d'images. L'astronomie par exemple, qui recueille dans ses télescopes des images brutes, qui subissent toujours des traitements complexes pour éliminer le bruit dû aux instruments, augmenter les contrastes, exhiber les contours ou analyser la colorimétrie. Même les astronomes amateurs disposent de logiciels de traitement d'images très puissants. La physique des particules utilise aussi ces traitements pour "dépatouiller" les images des traces provenant des détecteurs.

Même si le traitement d'images n'est pas de la simulation, il a toute sa place, comme plus généralement le traitement du signal, dans l'arsenal du physicien numéricien.

Les outils Python pour le traitement d'images

Notre image de test

Pour nos manipulations, nous travaillerons sur une image couleur au format RGB, c'est à dire que chaque pixel est codé par un 3-tuple (R,G,B) qui indique le poids, compris entre 0 et 255, de chaque canal de couleur : Red, Green et Blue. Il aurait été beaucoup plus simple de travailler sur une image en niveau de gris avec des pixels codés sur un seul entier, mais cela aurait été moins drôle !

Voici l'image en question :

Hawkeye

Vous aurez reconnu un Hawkeye E-2C de la flottille 4F, un AWACS embarqué sur porte-avion. Les connaisseurs auront reconnu un "chien jaune" du R91 "Charles de Gaulle".

La librairie Python PIL

La librairie PIL (Python Imaging Library) fournit les outils nécessaires pour les manipulations d'images que nous aborderons dans cette page. Ces manipulations sont simples et il existe des libraries plus complètes pour aborder les fonctions avancées de traitement des images. Vous trouverez une description des fonctions disponibles dans PIL (1.1.7) sur ce site et bien d'autres...

Pour charger la librairie PIL dans sa dernière version 1.1.7, rendez-vous sur le site officiel de PIL. Il suffit de choisir sa version de Python (2.7 pour moi) et de télécharger et d'exécuter le programme d'installation si vous êtes sous Windows. Sous Mac, il faut télécharger le source et le compiler sur votre Mac.

Pour information, PIL n'est plus maintenue depuis 2009 et un fork a été produit en 2010 qui s'appelle Pillow (pour supporter Python 3 surtout...). Mais beaucoup de monde continue d'utiliser PIL, qui fonctionne très bien en Python 2.7 et Pillow est trop proche, sans apporter grand chose...

A titre d'introduction , voyons le script Python TIProg1.py qui ouvre et affiche une image et ses principales caractéristiques (taille, compression, mode vidéo):

import sys

from PIL import Image

# ouverture du fichier image

ImageFile = 'e:\PhysNumWeb1\images\hawkeye.jpg'

try:

  img = Image.open(ImageFile)

except IOError:

  print 'Erreur sur ouverture du fichier ' + ImageFile

  sys.exit(1)

# affichage des caractéristiques de l'image

print img.format,img.size, img.mode

# affichage de l'image

img.show()

# fermeture du fichier image

img.close()

Les librairies utilisées sont PIL pour les fonctions de gestion des images et sys pour la gestion des erreurs à l'ouverture du fichier.

Puis vient la séquence d'ouverture du fichier contenant l'image. Attention au chemin d'accès du fichier, vous devrez sans doute le modifier pour l'adapter à votre machine ! Notez l'usage des intructions try et except : elles permettent de détecter et d'avertir en cas de problème à l'ouverture du fichier, par exemple lorsque le programme ne trouve pas ou ne peut pas ouvrir le fichier. Il est vivement recommandé lorsque vous procédez à des accès fichiers de détecter et traiter les exceptions (erreurs) !

La méthode open() retourne un pointeur sur l'image, que je stocke dans img. Il s'agit d'un pointeur sur une instance de classe Image, qui nous permettra d'accéder à toutes les données de l'image.

Par exemple, pour accéder et afficher dans la console Python le format, la taille et le mode de l'image, je les désignerai simplement par img.format, img.size et ing.mode ! Attention, img.size est un tuple Python, il contient deux données : la taille en lignes et en colonnes. On verra plus loin comment récupérer individuellement ces deux données.

L'affichage de l'image est des plus simples : j'utilise la méthode img.show(). Celle-ci se contente d'appeller le programme d'affichage d'images par défaut de votre OS, Windows ou OSX, ce qui implique qu'il existe. Si ce n'est pas le cas, il faut en installer un !

Enfin, pensez à fermer le fichier avec img.close(), afin de libérer les ressources système !

Voilà, c'est simple et de bon goût ! Je vais utiliser cette trame de programme dans tous les scripts qui suivent.

Utiliser les librairies classiques scipy et matplotlib

Il est aussi possible d'utiliser nos librairies standards SciPy et MatPlotLib pour lire et afficher une image. Le script Python TIProg2.py en donne un exemple. Vous noterez qu'ici, l'image s'affiche sur la console Python et non dans une fenêtre particulière gérée par le programme par défaut d'affichage de votre OS. Cela peut dans certains cas, présenter un intérêt.

# importation des librairies

from scipy import misc

import matplotlib.pyplot as plt

# ouverture du fichier image

ImageFile = 'e:\PhysNumWeb1\images\hawkeye.jpg'

try:

  img = misc.imread(ImageFile)

except IOError:

  print 'Erreur sur ouverture du fichier ' + ImageFile

  sys.exit(1)

# affichage des caractéristiques de l'image

print img.shape

# affichage de l'image

plt.imshow(img)

plt.axis('off')

plt.show()

Le principe de ce script est exactement le même que le précédent, hormis les librairies utilisées.

Dans la suite de la page, j'utiliserai plutôt la librairie PIL, sachant que vous pourrez porter très facilement les exemples pour utiliser SciPy si l'envie vous en prenait.

Quelques opérations sur les images avec Python

Manipulations simples sur une image

Inversion d'une image

Il existe une méthode de la classe ImageChops qui permet de produire le négatif d'une image. Il s'agit de la méthode ImageChops.invert(). Cependant, on peut essayer de créer un petit script qui assure la même transformation.

Techniquement, cette transformation est simple : il suffit de calculer le complément à 255 (au blanc) pour chaque composante R,G et B d'un pixel. Il faut donc parcourir tous les pixels de l'image et leur appliquer cette transformation.

Le script TIProgNégatif.py se présente comme suit, dans sa partie traitement, sachant que la partie ouverture du fichier image est inchangée. Je commence par récupérer le nombre de lignes et de colonnes de l'image. Une précision : le pixel en haut à gauche est d'indice (0,0) et celui en bas à droite (colonne-1,ligne-1).

colonne,ligne = img.size

Je créé une image de mêmes caractéristiques que l'image source pour stocker l'image issue du traitement:

imgF = Image.new(img.mode,img.size)

Puis vient la boucle de traitement, en fait deux boucles imbriquées qui parcourent l'image d'abord selon les lignes, puis selon les colonnes. Ce sens est conventionnel. Pour chaque pixel, je lis son 3-tuple avec la méthode getpixel(). Je calcule la valeur du nouveau pixel en appliquant la transformation, dans ce cas le complément à 255 pour chaque composante du pixel. Enfin, je créé le pixel transformé dans l'image finale imgF avec la méthode putpixel(). Ce qui nous donne le code :

for i in range(ligne):

  for j in range(colonne):

    pixel = img.getpixel((j,i)) # récupération du pixel

    # on calcule le complement à MAX pour chaque composante - effet négatif

    p = (255 - pixel[0], 255 - pixel[1], 255 - pixel[2])

    # composition de la nouvelle image

    imgF.putpixel((j,i), p)

Le script s'achève par l'affichage de l'image en négatif puis la fermeture de l'image originale par les instructions :

imgF.show()

img.close()

J'ai laissé en commentaire la ligne d'appel à la méthode PIL : imgF = ImageChops.invert(img), afin que vous puissiez l'utiliser et comparer les résultats.

Retenez bien ce schéma de code, car nous le retrouverons dans tous les scripts qui suivent. Seul l'algorithme de la transformation appliquée aux pixels sera différent.

Voilà les résultats obtenus par le script TIProgNégatif.py :

Négatif Négatif PIL

L'image de gauche est obtenue avec mon algorithme, celle de droite avec la méthode ImageChops.invert(). Vous constatez qu'il y a vraiment peu de différence ! Par contre, vous aurez aussi constaté que le temps d'exécution de la méthode PIL est au moins dix fois plus court ! Cela est du à mon utilisation des méthodes getpixel() et putpixel() qui ne sont pas des plus efficaces ! Nous reviendrons sur cet aspect des choses plus loin.

Symétrie d'une image

Les opérations de transformation géométrique sont relativement simples, ne consistant qu'en un déplacement de pixels, sans modification. Ici, nous voulons obtenir un effet miroir, c'est à dire une symétrie par rapport à un axe vertical, le bord droit de l'image en l'occurence.

Le script TIProgSymétrie.py, qui réalise cette opération s'écrit, pour sa partie traitement :

for i in range(ligne):

  for j in range(colonne):

    pixel = img.getpixel((j,i))

    imgF.putpixel((colonne-j-1,i), pixel)

Le reste du script est conforme au schéma de code que nous avons déjà vu.

Vous pouvez également utiliser la méthode transpose() de PIL que j'ai laissé en commentaire dans le script :

imgF = img.transpose(Image.FLIP_LEFT_RIGHT)

Notre image de HawkEye, en reflet miroir :

HawkEye Miroir
Rotation d'une image

La transformation par rotation d'une image est plus complexe, dans la mesure où il existe plusieurs options dans le choix du centre de rotation, généralement le centre de l'image, et de la conservation ou non de la taille de l'image. La transformation est toujours basée sur l'application de la matrice de rotation standard sur l'ensemble de l'image. Dans les méthodes évoluées, comme celle de PIL, on applique ensuite des filtres pour atténuer le crénelage provoqué par la rotation.

Nous pouvons aussi utilisé la méthode PIL rotate() avec ou sans argument autre que l'angle (en degrés) pour opérer une rotation. Voyons ce que cela donne sans argument et avec un angle de rotation de 90° dans le sens trigonométrique (image de gauche). L'appel de la méthode est :

imgF = img.rotate(90)

Puis avec un filtrage bicubique et une adaptation de la taille de l'image (image de droite) :

imgF = img.rotate(90, Image.BICUBIC, True)

Rotation 90° sans filtrage Rotation 90° avec filtrage

Il est certain que l'image de droite est un peu plus convaincante...

Le script TIProgRotate.py réalise la même transformation : rotation de PI/2 en sens trigonométrique avec redimensionnement de l'image. Il est basé sur le schéma de code habituel, sauf pour la définition de l'image finale, qui est une transposée de l'image initiale :

imgF = Image.new(img.mode,(ligne,colonne))

La boucle de traitement revient à calculer la transposée d'une matrice :

for i in range(ligne):

  for j in range(colonne):

    pixel = img.getpixel((j,i))

    imgF.putpixel((i,j), pixel)

Evidemment, me direz-vous, j'ai choisi un cas très simple : une rotation de PI/2... Vous pouvez toujours, pour l'exercice, écrire un algorithme qui effectue une rotation quelconque. Et vous vous apercevrez dans ce cas de l'utilité d'un filtre anti-crénelage !

Fusionner deux images

Il s'agit ici de fusionner deux images couleur qui peuvent être de dimensions différentes. Pour chaque pixel de l'image fusionnée, chacune de ses composantes est égale à la composante de valeur la plus élevée des deux pixels des images à fusionner.

Dans le script TIProgFusion.py, nous commencerons par ouvrir les deux images et à récupérer leurs dimensions, comme d'habitude. Puis nous calculerons les dimensions de l'image fusionnée en posant qu'elles seront égales aux plus petites dimensions des deux images à fusionner :

colonne = min(colonne1, colonne2)

ligne = min(ligne1,ligne2)

Puis après avoir créé l'image fusionnée, nous déroulerons la boucle de traitement de chaque pixel :

for i in range(ligne):

  for j in range(colonne):

    p1 = img1.getpixel((j,i))

    p2 = img2.getpixel((j,i))

    p = (max(p1[0],p2[0]),max(p1[1],p2[1]),max(p1[2],p2[2]) )

    imgF.putpixel((j,i), p)

Rien de bien particulier à signaler... Et voici le résultat obtenu :

Fusion d'un Hawkeye et d'un F14

Le mariage de deux des plus beaux appareils d'aviation embarquée...

Transformer une image couleur en niveaux de gris

Il existe une routine PIL, ImageOps.grayscale(), qui transforme une image en couleurs en une image en niveaux de gris. Mais pourquoi ne pas tenter d'écrire un script Python qui fasse cela pour nous ?

Intuitivement, on "sent" que pour obtenir le niveau de gris d'un pixel couleur, il faudrait mélanger les composantes du 3-tupel(R,G,B). De vieux souvenirs de cours de colorimétrie en optique. Soit, mais dans quelles proportions ? A parts égales ? Si vous faites l'expérience, en modifiant le script ci-dessous, vous verrez qu'on obtient bien une image en niveaux de gris, mais avec un je ne sais quoi de pas convaincant !

En cherchant, j'ai trouvé que les proportions recherchées étaient normalisées : c'est la CIE (Commission Internationale de l'Eclairage) qui normalise ce genre de chose, histoire de savoir de quoi l'on parle. Et dans sa norme 709, elle dit que pour les images naturelles les poids respectifs doivent être 0.2125 * R + 0.7154 * G + 0.0721 * B. Nous allons donc utiliser cette répartition dans notre code.

Autre chose, qui semble évident : dans un pixel en niveaux de gris, les composantes du 3-tuples ont la même valeur, celle calculée ci-dessus.

Le partie traitement du script TIProgNiveauGris.py reprend le schéma de code que nous avons vu plus haut. Pour chaque pixel, je lis son 3-tuple et calcule à partir des composantes la valeur du gris en appliquant simplement la formule de la CIE 709. Enfin, je recompose un 3-tuple de gris, avec 3 composantes identiques et égales à gris et je créé le pixel gris dans l'image finale imgF. Ce qui nous donne le code :

for i in range(ligne):

  for j in range(colonne):

    pixel = img.getpixel((j,i)) # récupération du pixel

    # calcul du poids de chaque composante du gris dans le pixel (CIE709)

    gris = int(0.2125 * pixel[0] + 0.7154 * pixel[1] + 0.0721 * pixel[2])

# gris = int(0.33 * pixel[0] + 0.33 * pixel[1] + 0.33 * pixel[2])

    p = (gris,gris,gris)

    # composition de la nouvelle image

    imgF.putpixel((j,i), p)

J'ai laissé en commentaire le calcul du gris par pondération égale afin que vous puissiez faire l'essai. J'ai aussi laissé en commentaire la ligne d'appel à la routine PIL : imgF = ImageOps.grayscale(img), afin que vous puissiez l'utiliser et comparer les résultats.

Voilà les résultats obtenus :

Niveaux de gris CIE709 Niveaux de gris PIL

L'image de gauche est obtenue avec mon algorithme de traitement des gris (celui de la CIE709 !) et l'image de droite est produite par la routine grayscale() de PIL. Elles sont très similaires, mon image étant un peu plus sombre que celle de PIL. Je ne sais pas quelle est la pondération utilisée par PIL. Vous constaterez aussi que le temps d'exécution de la routine PIL est beaucoup plus court (0,02 contre 3,38 s), ici aussi !

Essayons de faire mieux ! Dans le script TIProgNiveauGrisSpeed.py, j'utilise la méthode load() pour charger l'image dans le tableau TabPixel. Puis j'accède aux pixels de la matrice, en utilisant l'affectation classique au lieu de la méthode getpixel(). Le code devient:

TabPixel = img.load()

for i in range(ligne):

for i in range(ligne):

  for j in range(colonne):

    pixel = TabPixel[j,i] # récupération du pixel

    # calcul du poids de chaque composante du gris dans le pixel (CIE709)

    gris = int(0.2125 * pixel[0] + 0.7154 * pixel[1] + 0.0721 * pixel[2])

# gris = int(0.33 * pixel[0] + 0.33 * pixel[1] + 0.33 * pixel[2])

    p = (gris,gris,gris)

    # composition de la nouvelle image

    TabPixel[j,i] = p

Ici, je n'ai pas crée de nouvelle image, j'ai seulement modifié l'image existant pointée par img. J'affiche donc img par :

img.show()

Comme vous pourrez le constater, j'obtiens le même résultat, mais en 0,71 s ! C'est mieux que 3,38 s, mais pas aussi bien que PIL. Sans doute que leurs méthodes sont écrites en C compilé !

Filtrage

Le filtrage est une opération fondamentale en traitement du signal et donc en traitement des images. Il existe deux types de filtrage d'un signal: le filtrage spatial et le filtrage fréquentiel. Concernant une image, et dans les limites du scope de TangenteX.Com, nous nous limiterons au filtrage spatial, c'est à dire sur les pixels, d'une image.

On peut assimiler un filtre à un opérateur qui réaliserait une opération sur un pixel en fonction de la valeur de ce pixel et des pixels de son voisinage. En ce sens, les transformations que nous avons abordé plus haut sont des filtrages, par exemple le passage au négatif ou le passage en niveau de gris. L'opérateur peut être linéaire ou non linéaire. On obtiendra donc des filtres linéaires ou non linéaires en fonction de la nature de l'opérateur.

Le filtrage des couleurs

C'est un exemple simple de filtrage, couramment pratiqué en photographie : on ne garde qu'une seule couleur dans une image. Il peut s'agir par exemple de ne conserver que le canal rouge, vert ou bleu dans une image. Cette opération est généralement destinée à faire ressortir des détails dans une image.

Techniquement ce filtrage est très simple. Le script TIProgFiltre1.py filtre l'image de notre Hawkeye pour n'en conserver que la composante verte. La boucle de traitement qui permet cette opération est des plus simples :

for i in range(ligne):

  for j in range(colonne):

    pixel = img.getpixel((j,i))

    # filtrage couleur - filtre vert

    p = (0,pixel[1],0)

    imgF.putpixel((j,i), p)

On se contente de mettre à zéro les composantes R et B du pixel ! Voyons ce que cela donne :

HawkEye HawkEye vert

Vous pouvez bien sur adapter ce filtre pour d'autres couleurs, simples (R, G et B) ou plus complexes.

Les filtres par convolution

En filtrage d'images, le plus classique est d'appliquer sur un pixel un opérateur qui dépend du voisinage de ce pixel. La valeur du pixel sera modifiée en fonction de la valeur de ses voisins immédiats. Il s'agit de filtrage par convolution, car il utilise un outil du calcul matriciel qui s'appelle le produit de convolution.

Cette technique de filtrage consiste à considérer notre image comme une matrice de pixels que l'on va convoluer avec une autre matrice, plus petite, qui est le masque de convolution. C'est cette matrice qui va décider de la nature du filtre. En traitement du signal, on dit que cette matrice est la réponse impulsionnelle du filtre. Ici, nous allons utiliser une matrice 3x3, dont on fera varier la valeur des éléments pour obtenir différents types de filtre.

Analysons le script TIProgFiltreConvolution.py qui est un exemple de filtre par convolution. Tout d'abord, commençons par définir le masque de convolution de notre filtre. Techniqument, cela consiste à initialiser une matrice que j'appelerai Filtre :

Filtre = [[-1,-2,-1],[-2,16,-2],[-1,-2,-1]]

Les coefficients du masque correspondent à un filtre de constraste, mais nous en verrons d'autres plus loin. En fait, définir un filtre, c'est modifier les coefficients de cette matrice, le reste du programme reste inchangé, ce qui est plutôt pratique.

Le coeur du programme est la fonction Convolution2D dont voici le code :

def Convolution2D(Filtre,TPix,x,y):

  p0 = p1 = p2 = 0

  for i in range(-1,1):

   for j in range(-1,1):

    p0 += Filtre[i+1][j+1]*TPix[y+i,x+j][0]

    p1 += Filtre[i+1][j+1]*TPix[y+i,x+j][1]

    p2 += Filtre[i+1][j+1]*TPix[y+i,x+j][2]

    # normalisation des composantes

    p0 = int(p0/9.0)

    p1 = int(p1/9.0)

    p2 = int(p2/9.0)

  # retourne le pixel convolué

  return (p0,p1,p2)

Nous passons à la fonction Convolution2D plusieurs paramètres : le masque de convolution (la matrice Filtre); la matrice image contenue dans TPix et les coordonnées (x,y) du pixel à convoluer.

La fonction contient essentiellement l'implémentation de l'algorithme de convolution. Mathématiquement, pour un filtre de dimensions 3x3, la convolution du pixel TPix(x,y) s'écrit:
\( TPix(x,y) = \frac{1}{9} \sum_{i=-1}^1 \sum_{j=-1}^1 Filtre(i+1,j+1).TPix(x+i, y+j) \).
Pour chaque canal de couleur, il s'agit de calculer la composante du pixel TPix(x,y) en fonction des composantes de ses voisins pondérées par les coefficients du filtre. Puis vient la normalisation où l'on divise par 9, le nombre d'éléments de la matrice, afin que la valeur de chaque composante reste comprise entre 0 et 255. Enfin, je créé le pixel convolué avec les trois composantes calculées et la fonction le retourne au programme appelant.

La boucle de traitement reste très simple :

TabPixel = img.load()

for x in range(1,ligne-1):

  for y in range(1,colonne-1):

   p = Convolution2D(Filtre,TabPixel,x,y)

   imgF.putpixel((y,x),p)

Je charge l'image dans la matrice TabPixel en utilisant la méthode load(). Puis je parcours la matrice pixel par pixel. Pour chaque pixel, je procéde à sa convolution en appelant la fonction Convolution2D puis je stocke le pixel convolué dans imgF, l'image filtrée.

Voici le résultat obtenu avec le filtre de convolution défini ci-dessus :

HawkEye HawkEye filtre contraste
Un filtre passe-haut

Un filtre passe-haut est un filtre qui accentue les fréquences hautes par rapport aux fréquences basses. Dans une image, cela se traduit pas l'accentuation des détails.

Pour obtenir un filtre passe-haut, nous utiliserons la matrice Filtre :

Filtre = [[0,-4,0],[-4,18,-4],[0,-4,-0]]

Voici le résultat obtenu avec ce filtre :

HawkEye HawkEye filtre passe-haut

Je vous invite à faire varier les valeurs des coefficients de la matrice de convolution et de vérifier l'effet sur l'image. Par exemple, quel est l'effet d'une modification de l'élément Filtre[2][2] ?

Un filtre passe-bas

Un filtre passe-haut est un filtre qui accentue les fréquences basses par rapport aux fréquences hautes. Dans une image, cela se traduit pas l'adoucissement des détails et la réduction du bruit.

Pour obtenir un filtre passe-bas, nous utiliserons la matrice Filtre :

Filtre = [[1,1,1],[1,6,1],[1,1,1]]

Voici le résultat obtenu avec ce filtre :

HawkEye HawkEye filtre passe-bas

Là aussi, faites varier les composantes du filtre, expérimentez !

Détection de contours

La reconnaissance de formes dans une image est une composante importante de l'analyse d'images. Elle se décompose en plusieurs étapes qui consistent à extraire les contours des objets dans l'image afin de les reconnaitre ou d'en détecter le mouvement. La première de ces étapes est la mise en évidence des contours des objets dans l'image. C'est cette étape que nous allons aborder très succintement.

Un contour définit la limite d'un objet dans une image. Cette limite est caractérisée par un changement dans l'image : un changement de couleur ou de contraste. Ce changement se traduit dans la valeur des pixels qui sont localisés de part et d'autre de la limite. Nous sommes donc à la recherche d'un moyen de détecter et de localiser un changement. Les mathématiques nous donnent ce moyen sous la forme de la différentiation. On utilise habituellement des outils comme le gradient et le laplacien, bien connus des élèves de prépa, pour détecter ce changement. Mais ici, nous allons faire un peu plus simple en gardant le même principe.

Considérons un pixel p(i,j) dans une image couleur. Ce pixel est-il semblable, de même couleur, que ses voisins ? Si non, quelle est la différence de couleur entre lui et ses voisins ? Est-elle grande, ce qui signifierait qu'il est situé à la limite d'un objet ? Que signifie une "grande" ou une "petite" différence ? Comment la mesurer pratiquement ? C'est ce que nous allons essayer de traduire en algorithme.

Ce problème a fait l'objet de très nombreuses recherches. Il existe des algorithmes très efficaces et compliqués pour résoudre ce problème, surtout s'agissant d'images en couleurs. Mais il existe aussi des moyens simples, pas très performants mais utiles pour comprendre. Voyons la solution rudimentaire que je vous propose et ses résultats.

Le principe est de récupérer la valeur de chaque pixel avoisinant pour chaque pixel de l'image, ce que je fait dans cette boucle :

for i in range(1,ligne-1):

  for j in range(1,colonne-1):

    p1 = img.getpixel((j-1,i))

    p2 = img.getpixel((j,i-1))

    p3 = img.getpixel((j+1,i))

    p4 = img.getpixel((j,i+1))

Puis de mesurer la différence, la "distance", entre notre pixel de référence et ses voisins en utilisant une fonction de norme standard, que vous reconnaissez surement ! Il existe bien d'autres normes, mais je vais au plus simple :

def Norme(p1,p2,p3,p4):

  n = sqrt((p1[0]-p3[0])*(p1[0]-p3[0]) + (p2[0]-p4[0])*(p2[0]-p4[0]))

return n

Et c'est là que nous avons un petit problème ! Je passe à mon fonction Norme() les 4 pixels dont je veux évaluer la distance. Ce serait très simple si les pixels étaient codés sur un entier, comme dans une image en gris. Mais dans une image couleur, chaque pixel est un 3-tuple ! Il faudrait donc coder une norme avec trois variables par pixel. c'est faisable, mais assez compliqué, en tous les cas trop pour le scope de notre site. Donc, il va falloir ruser ! Vous remarquez que dans ma fonction, je ne travaille que sur le premier élément du 3-tuple, ce qui me permet d'écrire une norme pas trop compliquée. Mais qu'est-ce qui m'autorise à faire ça ?

C'est que j'ai un peu triché ! J'ai transformé notre image de référence en image à niveaux de gris avec l'algorithme vu précédement. Et vous savez maintenant que les composantes d'un 3-tuple d'une image en niveaux de gris sont identiques !

Après avoir calculé la distance entre mon pixel courant et ses voisins, je décide si ce pixel est sur un contour ou pas à l'aide d'un seuillage. S'il est inférieur au seuil, c'est à dire pas très "distant" de ses voisins, je décide qu'il n'est pas élément d'un contour et je trace le en blanc, sinon, je le trace en noir, comme un contour. C'est le bout de code suivant qui fait ça :

if n < seuil:

  p = (255,255,255)

else:

  p = (0,0,0)

imgC.putpixel((j-1,i-1),p)

Voilà pour le principe. Lançons maintenant le script TIExtractionContours.py correspondant et voyons les résultats:

Extraction contours algo simplifié Extraction contours PIL

L'image de gauche a été obtenue avec mon script, seuillé à 30. L'image de droite a été obtenue avec la méthode PIL qui procède à l'extraction de contours par filtrage avec amincissement et fermeture des contours, ce que mon algo ne fait pas... Je ne sais pas ce que vous en pensez, mais mon algo trivial n'a pas trop à rougir ! Bien sur, il est bien plus lent et inefficace. En bref, il est à usage pédagogique et certainement pas pratique ! Mais il peut être amélioré, en particulier en amincissant les contours. On peut aussi améliorer son temps d'exécution en utilisant la méthode load() puis des affectations, plutôt que getpixel() et putpixel().

Une dernière chose à propos de la détection de contours : on peut très bien la faire avec un filtrage convolutif, vu plus haut, en utilisant les filtres convenables: un filtre laplacien, de Sobel, Prewitt, Freeman, Kirsch et autres. Google vous renseignera sur le sujet.

Les scripts Python

Les scripts Python étudiés dans cette page sont disponibles dans le package TIPython.zip :

Pour conclure

Les scripts proposés ici sont uniquement à usage pédagogique. Cependant, la librairie PIL fournit elle des outils de traitement d'images assez performants. Je vous invite à les explorer. Vous pouvez produire assez simplement, même avec mes petits scripts, des images avec des effets visuels amusants, par exemple en superposant une image et le négatif d'une autre, ou en forçant les contrastes selon les canaux pour obtenir un effet "Andy Warhol".


Contenu et design par Dominique Lefebvre - www.tangenteX.com janvier 2016 -- Vous pouvez me joindre par mail ou sur PhysiqueX

Licence Creative Commons

Cette œuvre est mise à disposition selon les termes de la Licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Pas de Modification 3.0 France.