Images et contours

Objectifs

Utilisation de la binarisation pour détecter les contours d'une image en niveaux de gris. Toutes les images dont vous aurez besoin sont dans le dossier : commun/travail/infoPCSI

Notions de base

Les instructions de cette première partie sont à tester dans un shell Python. Il est fortement conseillé de prendre un peu de temps pour réaliser les tests et faire éventuellement les réglages nécessaires sur votre distribution Python (Anaconda) pour que l'image s'affiche avec l'instruction show() et comprendre la notion de chemin relatif et absolu.

Ouvrir une image

In [14]:
from PIL import Image
chemin_absolu = "/chemin_depuis_la_racine/"
src = Image.open(chemin_absolu + "chaplin.png")
dest = Image.new("L",src.size)
src.show()

Dans cet exemple nous avons créé l'objet image dest, une image en niveau de gris (mode='L'), de dimension src.size (tuple indiquant la largeur et la hauteur de l'image). L'objet dest est une image vide

Lecture d'une image sous forme de liste

In [6]:
src_data = list(src.getdata())
print src_data[20:30]#slicing on affiche les valeurs dans l'intervalle [20; 30[
[2, 2, 2, 2, 3, 4, 4, 4, 5, 5]

La liste contient toutes les intensités de niveaux de gris des pixels de l'image. Pratique il n'y a plus qu'à les modifier pour effectuer un traitement sur l'image.

Informations sur l'image

In [50]:
width, height = src.size
print width, height
217 232

Modifier une image

Dans cet exemple nous allons modifier le contraste de l'image

In [15]:
dest_data = [] #création d'une liste vide destinée à recevoir les intensités de niveaux de gris de l'image finale
for i in src_data:#on parcourt la liste des niveaux de gris de l'image source
    dest_data.append(i * 2 ) #on stocke les modifications
dest.putdata(dest_data)#De la liste vers l'image
dest.show()#On affiche

Enregistrer l'image modifiée

In [86]:
dest.save(chemin_absolu + "chaplin_nb.png")

Recherche de contours

D'après Wikipédia

Le but de la détection de contours est de repérer les points d'une image numérique qui correspondent à un changement brutal de l'intensité lumineuse. Ces changements de propriétés de l'image traduisent en général des événements importants ou des changements dans les propriétés du monde. Ils incluent des discontinuités dans la profondeur, dans l'orientation d'une surface, dans les propriétés d'un matériau et dans l'éclairage d'une scène. La détection de contours est un champ de la recherche qui appartient au traitement d'image et à la vision par ordinateur, particulièrement dans le domaine de l'extraction de caractéristiques. La détection des contours d'une image réduit de manière significative la quantité de données et élimine les informations qu'on peut juger moins pertinentes, tout en préservant les propriétés structurelles importantes de l'image.

Il existe un grand nombre de méthodes de détection de contours, nous allons nous intéresser à la recherche de contours après binarisation d'une image en niveau de gris. Rien n'empêche d'étendre cette méthode à des images couleurs.

Approche naïve

Commençons avec une image binaire comme ci-dessous : contoursV.png. Elle présente une seule discontinuité de l'intensité. Les bords noirs ne sont pas présents sur le fichier image, ils ont été ajoutés uniquement pour délimiter les bords de l'image sur le fond blanc de la page. Les contours cherchés se réduisent dans ce cas à une ligne verticale, frontière entre le noir et le blanc. Notre objectif est d'obtenir une nouvelle image dans laquelle seule cette frontière sera conservée.

  1. Écrire une fonction imgData(img_file) qui prend en paramètre le nom de fichier d'une image et renvoyant les valeurs d'intensité de chaque pixel dans une liste à 1 dimension. L'image de départ (à gauche ci-dessous) peut-être représentée comme une matrice des intensités de gris des pixels.

    \[\begin{array}{|c|c|c|c|}\hline 0 & 0 & 255 & 255 \\\hline \bf{0} & 0 & 255 & 255 \\\hline 0 & 0 & 255 & 255 \\\hline 0 & 0 & 255 & 255 \\\hline \end{array}\qquad \longrightarrow \qquad \begin{array}{|c|c|c|c|c|c|c|c|}\hline 0 & 0 & 255 & 255 & \bf{0} & 0 & \dots & 255\\\hline \end{array} \]
  2. Compter les pixels
    • Une image possède une largeur WIDTH et une hauteur HEIGHT exprimée en pixels.
    • On peut déterminer le nombre total de pixels contenu dans l'image avec : WIDTH * HEIGHT
    • Les coordonnées (i,j) dans l'image 2-dimensions permettent d'obtenir l'index d'un pixel dans une liste à 1 dimension à l'aide de la relation suivante :\[index = i + j* WIDTH\]

      Dans l'exemple suivant le pixel de coordonnées (0, 1) a pour valeur d'intensité 0 (noir), dans notre liste l'index de ce pixel est \[index = 0 + 1 * 4 = 4\]

    • L'opérateur modulo (%) permet d'obtenir dans la liste 1-Dimension le pixel correspondant à la fin d'une ligne sur l'image 2-Dimensions.
        Par exemple avec \(WIDTH=4\)
      • si \[index=5 \qquad \Longrightarrow \qquad index\%WIDTH=1\] donc pas en fin de ligne
      • alors que si \[index=8 \qquad \Longrightarrow \qquad index\%WIDTH=0\] donc fin de ligne
    • Écrire une fonction imgInfo(img_file) qui prend en argument le nom de fichier de l'image et qui renvoie la largeur, la hauteur et le nombre de pixels de l'image
  3. Écrire une fonction contoursV(img_data, size) qui prend en paramètre une image sous la forme d'une liste de pixels, un tuple avec la largeur et la hauteur de l'image et renvoyant une nouvelle liste de même dimension mais dont chaque pixel a pour valeur la différence entre la valeur du pixel (i,j) et celle du pixel (i-1, j). Pour commencer vous pourrez tester votre fonction avec la liste de l'exemple ci-dessus L = [0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255] qui doit fournir le résultat suivant:

    \[\begin{array}{|c|c|c|c|}\hline 0 & 255 & 0 & 0 \\\hline 0 & 255 & 0 & 0 \\\hline 0 & 255 & 0 & 0 \\\hline 0 & 255 & 0 & 0 \\\hline \end{array}\qquad \longrightarrow \qquad \begin{array}{|c|c|c|c|c|c|c|c|}\hline 0 & 255 & 0 & 0 & 0 & 255 & \dots & 0\\\hline \end{array} \]

  4. Écrire une fonction newImage(img_data, size) qui prend en argument une liste d'intensité de pixels, un tuple (largeur, hauteur) de l'image et qui renvoie une image prête à visualiser.

  5. Pour finir nous allons écrire la première version de la fonction contoursImage(img_file) suivante qui renvoie une image ne conservant que les contours verticaux de l'image source:

In [5]:
def contoursImage(img_file):
    data = imgData(img_file) #liste des intensités de pixels
    size = imgInfo(img_file)[0] # informations sur l'image
    dest_data = contoursV(data, size) # Détection contours verticaux
    dest = newImage(dest_data, size) # création de la nouvelle image
    return dest
  1. Appliquer la fonction contoursImage avec l'image : contoursV.png

  2. Appliquer la fonction contourV(img_data) avec l'image suivante (contoursH.png). Que se passe-t-il ? Expliquez pourquoi

  3. Écrire la fonction contourH(img_data). Modifier la fonction contoursImage pour que l'utilisateur puisse choisir entre les contours verticaux ou horizontaux de l'image source. Tester cette nouvelle fonction avec l'image : contoursH.png
    Tester également la fonction contoursImage sur l'image : formes.png

  4. À l'aide des fonctions contoursV et contoursH écrire une fonction contoursNaifImageBin(img_file) renvoyant une image des contours de toutes les formes. Évaluer la complexité de la fonction contoursNaifImageBin

Image en intensité de gris

Les changements d'intensité entre deux pixels voisins en niveaux de gris ne sont pas forcément significatifs d'un contour, il est donc nécessaire d'effectuer un pré-traitement avant la détection des contours

La binarisation ou comment transformer une image en niveaux de gris en image binaire. Pour cela nous allons utiliser une technique très simple appelée seuillage. Le seuillage d'image est une méthode permettant de rassembler entre eux les pixels d'une région à partir d'un critère précis. Dans notre exemple à partir d'une image en niveaux de gris, le seuillage d'image peut être utilisé pour créer une image comportant uniquement deux couleurs, noir et blanc, d'où la notion d'image binaire. Ces deux couleurs sont définies par les niveaux d'intensité de gris 0 pour le noir et 255 pour le blanc. Le seuillage d'image permet donc de remplacer un à un les pixels d'une image en comparant avec une valeur seuil, les pixels de la nouvelle image prennent la valeur 0 en dessous du seuil et la valeur 255 sinon.

In [82]:
#exemple de code pour seuillage
dest_data=[] #liste contenant les valeurs d'intensité finales
seuil = 126
for i in range(len(src_data)): #src_data : liste des valeurs d'intensité initiales
    if src_data[i] < seuil:
        dest_data.append(0)
    else: 
        dest_data.append(255)
dest.putdata(dest_data) #construction de l'image finale dest
dest.show() #Affichage de l'image à l'écran
  1. Écrire une fonction dont le prototype est le suivant : seuillage(img_data, seuil) avec comme arguments respectifs la liste des intensités de gris, la valeur seuil et renvoyant l'image modifiée de même dimension sans pour autant l'afficher à l'écran
  2. Tester votre fonction seuillage avec l'image : chaplin.png pour différentes valeurs seuils. Une bonne valeur est seuil = 60
  3. Comment trouver une bonne valeur seuil ? Le problème est complexe et nous allons nous contenter d'un seuillage global c'est à dire que la valeur seuil sera la même pour tous les points de l'image. Cette technique pose beaucoup de problèmes en particulier avec les images comportant un fort bruit de fond. Pour déterminer une valeur seuil nous allons procéder en deux étapes:
    • Construction de l'histogramme
      L’histogramme d’une image en niveau de gris associe à chaque valeur d’intensité le nombre de pixels prenant cette valeur. Pour créer l’histogramme d’une image en niveau de gris, il suffit de parcourir l’image pour compter le nombre de pixels de chaque intensité de gris.
    • Binarisation
      Nous pouvons pour cela utiliser l’histogramme. Une fois l’histogramme de l’image en notre possession, il faut établir une somme cumulée des intensités, c’est à dire faire la somme du nombre de pixels pondéré par leurs intensités. Pour établir le seuil, il suffit alors de diviser cette somme par le nombre de pixels de notre image.
  4. Écrire les fonctions:
    • histo(img_data) : qui prend en arguments une liste des intensités des pixels de l'image et qui renvoie une liste correspondant à l'histogramme. Cette liste est construite de telle manière que les index correspondent aux différents niveaux d'intensité de gris. Par exemple si on appelle H cette liste alors H[20] renvoie le nombre de pixels d'intensité de gris égale à 20
    • seuil(H) : qui prend en argument une liste de valeurs d'un histogramme et renvoyant la valeur seuil
  5. Écrire une fonction contoursNaifImageGris afin d'obtenir l'image de droite en partant de l'image chaplin.png