Manipuler des images#

Dans cette feuille, vous allez apprendre à effectuer quelques manipulations et traitements simples sur les images. Nous allons commencer par nous entraîner sur une image riche en couleurs (source: wikimedia):

media/apple.png

Pour cela, nous la chargeons avec la bibliothèque PIL (Python Imaging Library) en précisant le nom du fichier la contenant, puis l’affectons à une variable img pour pouvoir la manipuler par la suite:

from PIL import Image
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 1
----> 1 from PIL import Image

ModuleNotFoundError: No module named 'PIL'
img = Image.open("media/apple.png")

Voici cette image:

img

Pour l’afficher avec des axes et – lorsque l’image a une basse résolution – mieux repérer les pixels individuels, on peut utiliser matplotlib:

import matplotlib.pyplot as plt
plt.imshow(img);

Indication

Interfaces traditionnelle et objet de matplotlib

Pourquoi un ; à la fin de la commande précédente? Parce que plt.imshow ne renvoie pas une image, mais l’affiche par effet de bord. Le ; évite l’affichage de ce que renvoie vraiment plt.imshow (un objet de type figure).

Cette approche quelque peu datée est traditionnelle dans des systèmes comme Matlab. La bibliothèque matplotlib.pyplot l’a reproduit pour faciliter la migration d’utilisateurs de ces systèmes. Par habitude beaucoup d’exemples sur internet utilisent encore cette approche; cela peut rester pratique comme raccourci dans des exemples en une ligne comme ci-dessus.

Mais on sait depuis – et c’est ce que nous vous enseignons depuis le début de l’année – que l’on obtient du code beaucoup plus modulaire si l’on sépare proprement les traitements et calculs (par exemple construire une figure) des entrées et sorties (par exemple afficher la figure).

De ce fait, pour tout usage non trivial, il est préférable d’utiliser l’interface objet de matplotlib, comme dans l’exemple suivant:

from matplotlib.figure import Figure
fig = Figure()              # Construction d'une nouvelle figure
subplot = fig.add_subplot() # Ajout d'une zone de dessin (appelée «axes» dans matplotlib) à la figure
subplot.imshow(img)         # Ajout d'une image à la zone de dessin
fig                         # Affichage de la figure

Consultez la documentation de PIL Image sur internet, pour trouver comment obtenir la largeur et la hauteur de cette image. Stockez le résultat dans des variables width et height et vérifiez la cohérence avec la figure ci-dessus.

### BEGIN SOLUTION
width, height = img.size 
### END SOLUTION
assert width == 256
assert height == 256

Images comme tableaux#

On souhaite maintenant pouvoir accéder au contenu de l’image pour pouvoir calculer avec. Pour cela, nous allons convertir l’image en un tableau de nombres NumPy, tels ceux que nous avons manipulés dans la fiche précédente.

Voici le tableau associé à l’image:

import numpy as np
M = np.array(img)

En vous référant éventuellement au cours, combien de lignes, de colonnes et de couches devrait avoir ce tableau?

BEGIN SOLUTION

Il devrait y avoir 256 lignes, 256 colonnes et quatre couches.

END SOLUTION

Vérifier avec l’attribut shape:

### BEGIN SOLUTION
M.shape
### END SOLUTION

Pourquoi quatre couches? Rouge, Vert, Bleu, … et transparence!

Comprendre les couches de couleurs#

Comme toujours, pour mieux comprendre des données, il faut les visualiser ! Voici une figure représentant notre image et ses trois couches rouge, vert, bleu. Observez comment les couleurs de l’image de départ (blanc, vert, noir, rouge) se décomposent dans les différentes couches.

# Échelles de couleur (colormap) allant du noir à la couleur primaire correspondante
from matplotlib.colors import LinearSegmentedColormap
black_red_cmap   = LinearSegmentedColormap.from_list('black_red_cmap',   ["black", "red"])
black_green_cmap = LinearSegmentedColormap.from_list('black_green_cmap', ["black", "green"])
black_blue_cmap  = LinearSegmentedColormap.from_list('black_blue_cmap',  ["black", "blue"])

fig = Figure(figsize=(30, 5));
(subplot, subplotr, subplotg, subplotb) = fig.subplots(1, 4)  # Quatre zones de dessin
# Dessin de l'image et de ses trois couches
subplot.imshow(M)
imgr = subplotr.imshow(M[:,:,0], cmap=black_red_cmap,   vmin=0, vmax=255)
imgg = subplotg.imshow(M[:,:,1], cmap=black_green_cmap, vmin=0, vmax=255)
imgb = subplotb.imshow(M[:,:,2], cmap=black_blue_cmap,  vmin=0, vmax=255)
# Ajout des barres d'échelle de couleur aux images
fig.colorbar(imgr, ax=subplotr);
fig.colorbar(imgg, ax=subplotg);
fig.colorbar(imgb, ax=subplotb);
fig

Par la suite, nous visualiserons de même de nombreuses images. Il est donc temps d’automatiser la construction de la figure ci-dessus.

Exercice

Ouvrez le fichier utilities.py et complétez-y la fonction show_color_channels à partir du code ci-dessus.

# Automatically reload code when changes are made
%load_ext autoreload
%autoreload 2
from intro_science_donnees import *
from utilities import *
show_source(show_color_channels)
show_color_channels(img)

Vérification: show_color_channels renvoie bien une figure

assert isinstance(show_color_channels(img), Figure)

Étudions maintenant les images du jeu de données de la semaine dernière:

import os.path
dataset_dir = os.path.join(data.dir, 'ApplesAndBananasSimple')
images = load_images(dataset_dir, "*.png")
image_grid(images, titles=images.index)

Exercice

Observez l’image suivante et ses couches. Expliquez ce que vous voyez. Essayez d’autres exemples.

BEGIN SOLUTION

On remarque que les pixels noirs sont à zéro dans les trois couches et les pixels blancs à 255 dans les trois couches. Le jaune étant la couleur complémentaire du bleu est à 255 en rouge et en vert et 0 en bleu.

END SOLUTION

img = images[10]
show_color_channels(img)

Nous allons maintenant observer l”histogramme des couleurs apparaissant dans une image, en utilisant l’utilitaire color_histogram (vous pouvez comme d’habitude en consulter la documentation et le code par introspection avec color_histogram? et color_histogram??):

color_histogram(img)

Exercice

Observez les histogrammes ci-dessous de la dixième et la troisième image, et interprétez-les.

BEGIN SOLUTION

Image 10 : on remarque que de les pixels verts et rouges se situent très majoritairement dans des valeurs supérieures à 150, tandis que de nombreux pixels bleus se situent dans des valeurs inférieures. C’est logique étant donné la couleur jaune de la pomme (jaune étant la couleur complémentaire du bleu).

Image 3 : on remarque qu’il y a davantage de pixels rouges ayant la valeur 256 ; bleus et verts ayant dans l’ensemble des valeurs plus faibles que les pixels rouges. Cohérent, puisque la pomme de l’image est rouge.

END SOLUTION

img = images[9]
show_color_channels(img)
color_histogram(img)
img = images[2]
show_color_channels(img)
color_histogram(img)

Séparation des couleurs#

Nous allons maintenant extraire les trois canaux, rouge, vert, bleu. Pour le canal des rouges, on extrait le sous-tableau à deux dimensions de toutes les cases d’indice \((i,j,k)\) avec \(k=0\). Le * 1.0 sert à convertir les valeurs en nombres à virgule.

R = M[:,:,0] * 1.0

Regarder le résultat directement n’est pas très informatif :

R

Comme d’habitude, il vaut mieux le visualiser :

fig = Figure(figsize=(5,5))
ax, axr = fig.subplots(1,2)
ax.imshow(M)
axr.imshow(R, cmap='Greys_r', vmin=0, vmax=255)
fig

Exercice

Extrayez de même le canal des verts et des bleus de la première image dans les variables G et B. N’hésitez pas à les visualiser !

### BEGIN SOLUTION
G = M[:,:,1] * 1.0

fig = Figure(figsize=(5,5))
ax, axr = fig.subplots(1,2)
ax.imshow(M)
axr.imshow(G, cmap='Greys_r', vmin=0, vmax=255)
fig
### END SOLUTION
assert G.shape == (256, 256)
assert abs(G.mean() - 158.27) < 0.1
### BEGIN SOLUTION
B = M[:,:,2] * 1.0

fig = Figure(figsize=(5,5))
ax, axr = fig.subplots(1,2)
ax.imshow(M)
axr.imshow(B, cmap='Greys_r', vmin=0, vmax=255)
fig
### END SOLUTION
assert B.shape == (256, 256)
assert abs(B.mean() - 148.39) < 0.1

Il est maintenant facile de faire de l’arithmétique sur tous les pixels. Par exemple la somme des intensités en vert et rouge s’écrit:

G + R

Exercice

Calculez et visualisez la luminosité de tous les pixels de l’image, la luminosité d’un pixel \((r,g,b)\) étant définie comme la moyenne \(v=\frac{r+g+b}{3}\):

### BEGIN SOLUTION
V = (R+G+B)/3

fig = Figure(figsize=(5,5))
ax, axr = fig.subplots(1,2)
ax.imshow(M)
axr.imshow(V, cmap='Greys_r', vmin=0, vmax=255)
fig
### END SOLUTION
assert V.shape == (256, 256)
assert abs(V.mean() - 172.44) < 0.1

Vous venez de transformer l’image en niveaux de gris! Pour que cela colle au mieux avec notre perception visuelle, il faudrait en fait utiliser une moyenne légèrement pondérée; voir par exemple la Wikipedia.

Conclusion#

Vous avons vu dans cette feuille comment charger une image dans Python et effectuer quelques manipulations, visualisations et calculs simples dessus. Cela a été l’occasion de mieux comprendre la décomposition d’une image en couches de couleur.

Exercice

Mettez à jour votre rapport, et notamment la section « revue de code » pour vérifier vos utilitaires dans utilities.py.

Vous pouvez maintenant passer à l”extraction d’attributs