Pour commencer

  • Lancer Spyder avec Python 2.7 pour ce TP (pas Idle, pas Python 3), et vérifiez que ça fonctionne. On utilisera l'interpréteur actif et pas un interpréteur dédié (appuyer sur F6 : Configurer...). On peut aussi (a priori) utiliser Pyzo.
  • Récupérez les images utiles pour ce TP sur le lecteur réseau (lecteur S: (groupes), classe, données, info commune, TP Images) et copiez-les dans votre répertoire personnel.

Différences entre le type list de python, et array dans numpy

Dans la suite, on va utiliser un nouveau type de tableau : array disponible dans le module numpy

  • Un seul type pour tous les éléments
  • Un format (ligne et colonnes) comme un vrai tableau
  • l'opérateur plus : source de confusions (concaténation vs. opération terme à terme) !
  • range vs. arange
  • L'affichage est plus joli...
  • Sinon c'est très proche : on peut accéder aux éléments de la même façon et faire du slicing

Avec les listes python (essayez de prévoir les résultats obtenus dans python, tester, comprendre) :

# Dans l'interpréteur c'est aussi simple...
>>> a = [1, 2, 3]
>>> b = [4, 5, 6]

>>> a + b
>>> 2 * a
>>> 3 + a # Erreur, c'est normal

>>> range(0, 10, 2)
>>> range(0, 10, 0.5) # Erreur

>>> tab = [[i + j for i in range(10)] for j in range(15)]

Presque la même chose mais avec le type array de numpy : Notez bien les différences !

>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> b = np.array([4, 5, 6])
>>> a + b
>>> 2 * a
>>> 3 + a

>>> np.arange(0, 10, 2)
>>> np.arange(0, 10, 0.5)

>>> tab = [[i + j for i in range(10)] for j in range(15)]
>>> nptab = np.array(tab) # C'est pus joli
>>> nptab
>>> nptab.shape # Le format du tableau (lignes, colonnes, ...)

Une autre contrainte : les éléments considérés auront un type particulier (pas les entiers de python) :

>>> a = np.array([1, 2, 3], dtype = np.uint8) # On travaille avec des entiers (positifs)
                                              # sur 8 bits donc entre 0 et 255
>>> a + 250
>>> a + 255

Images : des pixels

On va utiliser plusieurs modules de python dont numpy pour les calculs et Python Image Library (PIL) pour manipuler les images :

import numpy as np
import matplotlib.pyplot as plt
import PIL.Image as im
import math

Lire / Afficher / Enregistrer une image dans un fichier

# Lecture du fichier
def lire_fichier(fichier):
    image = im.open(fichier)
    # On dit à python que chaque valeur est un entier entre 0 et 255
    pixels = np.asarray(image, np.uint8)
    return pixels

# Affichage
def affiche(pixels):
    plt.imshow(pixels) # plt.imshow(pixels, cmap = 'Greys_r') # niveaux de gris
    #plt.show()        # décommenter si l'image ne s'affiche pas

# Enregistrement dans un fichier
def enregistrer(pixels, fichier)
    image = im.fromarray(pixels)
    image.save(fichier)

# Utilisation
fichier_joconde = "images\joconde.jpg" # si le fichier est dans le répertoire 'images'
pixels_joconde = lire_fichier(fichier_joconde)

# C'est ici qu'on peut modifier l'image ou l'utiliser
print(pixels_joconde.shape) # Affichage du format
plt.imshow(pixels_joconde)  # Affichage du gros tableau de pixels

enregistrer(pixels_joconde, "joconde.bmp") # On peut utiliser l'extension .bmp, .png, .tif
# L'enregistrement en .jpg semble ne pas fonctionner au lycée

Remarque sur les format de fichiers

Les fichiers que vous avez à votre disposition sont en différents formats\footnote{Le format d'une image (bitmap, jpeg, png...) ne signifie pas la même chose que le format d'un tableau numpy (dimensions)} : certains sont compressés, d'autres non. Dans tous les cas (à la lecture ou à l'écriture), le module PIL fait la conversion. Le format BMP n'étant pas compressé c'est un des moins utilisés maitenant, mais on va l'utiliser pour parler de taille de fichier.

Certaines images sont en couleur : on stocke chaque pixel (picture element) avec 3 octets (rouge, vert, bleu).

  • Utilisez python pour ouvrir l'image 'joconde.tif' et afficher son format (avec pixels.shape). On obtient (1600, 1058, 3) : c'est la hauteur, la largeur, et le nombre d'octet pour chaque pixel.

D'autres images sont en niveaux de gris : ouvrir 'hepburn.tif' et afficher son format. On obtient (621, 451) : la hauteur est de 621 pixels, la largeur de 451 pixels, et il n'y a qu'un seul octet par pixel (pour la composante de gris)

Modifications

Composantes

On va travailler sur la joconde :

def rouge(pixels):
    hauteur, largeur, _ = pixels.shape
    vide = np.zeros(pixels.shape, np.uint8) # Crée une nouvelle image du même format
    for y in range(hauteur):
        for x in range(largeur):
            (r, v, b) = pixels[y][x]  # On récupère les composantes rouge, vert, bleu
            vide[y][x] = (r, 0, 0)    # Ici on ne garde que le rouge
    return vide

pix = lire_fichier('images\joconde.jpg')
pix_rouge = rouge(pix)                  # C'est très long, c'est normal (4 secondes chez moi)
enregistrer(pix_rouge, 'rouge.bmp')
  • Que faut-il faire pour récupérer la composante de vert ? de bleu ?

  • Comment obtenir le négatif de l'image ? (Pour chaque composante $c$ dans $r, v, b$, on calcule $255 - c$)

  • On peu rendre l'image plus claire en augmentant la valeur de chaque composante. Que se passe-t-il pour les valeurs trop grandes ? Si vous bloquez, faites l'exercice suivant et revenez ici... Régler le problème avec branchements conditionnels (if). Ou l'utilisation judicieuse de la fonction min.

Niveaux de gris

  • On souhaite transformer cette image en niveaux de gris : on peut affecter à chaque couleur la moyenne des composantes (r, v, b) Par exemple : (100, 12, 49) -> moyenne de 54 environ -> (54, 54, 54)
# Le code ci-dessous ne fait pas ce que l'on veut, pourquoi ?
            r, v, b = pixels[y][x]  # On récupère les composantes rouge, vert, bleu
            moyenne = (r+v+b)/3
            vide[y][x] = moyenne, moyenne, moyenne

On peut s'en sortir avec\footnote{Dans ce cas précis, on peut aussi faire les divisions avant les additions...} :

def niveau_gris(pixels):
    hauteur, largeur, _ = pixels.shape
    vide = np.zeros(pixels.shape, np.uint8) # Crée une nouvelle image du même format
    for y in range(hauteur):
        for x in range(largeur):
            (r, v, b) = np.int16(pixels[y][x])  # On récupère les composantes rouge, vert, bleu
            moyenne = (r + v + b) / 3
            vide[y][x] = np.uint8((moyenne, moyenne, moyenne))
    return vide
  • Seuiller un image consiste à transformer chaque pixel en blanc (255, 255, 255) ou noir (0, 0, 0) selon que son niveau de gris est plus petit ou plus grand qu'un seuil donné.

  • On peut aussi seuiller couleur par couleur

  • La couleur sepia est $(94, 38, 18)$. On décide que si le niveau de gris est plus petite qu'un seuil, on remplace le pixel par une moyenne pondére (barycentre) entre un pixel noir et la couleur sepia. Si le niveau de gris est plus grand que le seuil, on calcule une moyenne pondérée entre le sepia et le blanc.

  • Pour augmenter le contraste : on augmente les valeurs associées aux pixels clairs, et on diminue celles des pixels foncés.

Flou, lissage, accentuation

  • On peut créer un effet de flou (lissage) en remplaçant chaque pixel par la moyenne des 9 pixels qui l'entoure. On pourra utiliser du slicing... et on fera surtout attention à ne pas accéder à des cases hors du tableau !!! [[1/9, 1/9, 1/9], [1/9, 1/9, 1/9], [1/9, 1/9, 1/9]]

  • Accentuation : presque comme le flou, mais en pondérant les cases adjacentes différement. [[0, -0.5, 0], [-0.5, 3, -0.5], [0, -0.5, 0]]

Aller plus loin

Morceau d'une image

  • Utiliser le slicing (sur les deux première composantes) pour récupérer une partie d'une image. On commencera par remarquer que la syntaxe n'est pas la même que d'habitude, y réfléchir, et comprendre pourquoi la syntaxe classique ne renvoie pas ce que l'on veut...
pixels[:2, :5]
  • Blanchir (mettre à 255) la zone $[60, 180[ \times [120, 200[$ de l'image Mona Lisa

  • Echangez deux parties de l'image de Mona Lisa : $[90, 190[ \times [120, 220[$ et $[400, 500[ \times [80, 180[$. Quelle est la difficulté classique quand on veut échanger le contenu de deux variables ? C'est pareil ici...

Modifier la forme de l'image

  • On voit l'image comme un grand tableau : recopier l'image en 4 petits exemplaires comme dans un photomaton... ou en 9, 16, ...

  • On peut imaginer à peu près n'importe quelle transformation géométrique dans le plan (translation, rotation, homothétie) mais aussi appliquer n'importe quelle fonction du plan dans le plan qui 'déplace' des pixels. Amusez-vous ! Attention : que se passe-t-il quand on sort de l'image ?

Répartition des couleurs : histogram

  • Que renvoie la ligne suivante ?
im.open('joconde.jpg').histogram()
  • Écrire votre propre fonction python qui calcule l'histogramme de répartition des niveaux de gris / couleurs d'une image (plus lent oui...)

Détection de bords

On considère une image en niveaux de gris : Pour chaque pixel (sauf ceux du bord), on considère ses 4 pixels adjacents : $haut, bas, gauche, droite$. On calcule la valeur du changement de ce pixel $\sqrt{(droite - gauche)^2 + (haut - bas)^2)}$. Si ce changement est plus petit qu'un certain seuil, on lui associe un pixel blanc. Noir sinon.

Miscéllanées

  • De nombreux exemples de traitement d'image utilisent l'image appelée "Lena". On pourra découvrir son histoire et la photo originale (NSFW) sur \url{lenna.org} (avec deux 'n')
from scipy import misc
l = misc.lena()
misc.imsave('lena.png', l) # nécessite PIL
  • Réaliser une interface graphique (simplement des boutons) pour faciliter l'utilisation par l'utilisateur des fonctions précédentes. Voici un mini exemple. Appelez les fonctions précédentes à l'aide de boutons.
# coding: utf8

from Tkinter import Tk, Label, Button

def bonjour():
    print("Bonjour !")

master = Tk()
master.title("Ma première fenêtre")
master.label = Label(master, text="Du texte")
master.label.pack()

# Remarque : on nomme la fonction 'bonjour' sans parenthèses.
master.bonjour_button = Button(master, text="Bonjour", command=bonjour)
master.bonjour_button.pack()

master.close_button = Button(master, text="Close", command=master.quit)
master.close_button.pack()

master.mainloop()
  • Seam carving : on pourra lire http://en.wikipedia.org/wiki/Seam_carving (en anglais). On pourra s'inspirer de l'exercice sur la détection de bords : on souhaite trouver des chemins (de haut en bas de l'image) qui traversent "peu de bords"... on commencera pas regarder une video sur internet.

  • (plus dur) Peut-on utiliser le même principe pour rajouter une colonne de pixels ? plusieurs colonnes ?

  • Tout ce qu'on vient de faire est très lent : pour aller plus vite dans certains cas (typiquement toutes les transformations qui modifient les couleurs point par point), on peut utiliser les fonctions "vectorisées" dans numpy.

  • Sinon il existe d'autres bibliothèques (modules) que PIL / numpy qui sont encore plus adaptées à la manipulation d'image en python : OpenCV / SimpleCV\footnote{\url{http://tutorial.simplecv.org/}. CV : Computer Vision}.