Traitement d’images#

Arcimboldo painting

Giuseppe Arcimboldo était un peintre du 16ème siècle qui a créé des portraits imaginaires à partir de fruits, légumes, fleurs, poissons et livres.

Objectif#

Dans cette feuille, vous allez créer une séquence d’images transformant pas à pas le smiley ci-dessous en une peinture à la Arcimboldo, puis les assembler pour réaliser une petite animation!

Smiley

Voici les étapes du projet:

  1. Extraire la partie droite de l’image pour avoir une image carrée

  2. Extraire le smiley de son fond

  3. Recadrer l’image sans changement de résolution, de sorte à obtenir une image de taille 256x256 centrée sur le smiley,

  4. Réduire la résolution à 128x128, avec une technique anti-crénelage, par lissage puis sous-échantillonage.

  5. Extraire les contours du smiley pour en faire une image noir et blanc.

  6. Mettre en toile de fond un papier-peint représentant des fruits

  7. Choisir trois images de fruits et les extraires de leur fond.

  8. Remplacer les yeux et la bouche du smiley par ces fruits

Lors de ces étapes, guidées pas-à-pas, vous apprendrez à:

  • Appliquer un filtre pixel-à-pixel pour extraire le premier plan d’une image

  • Appliquer un filtre de convolution, construire une pyramide Gaussienne et choisir le bon niveau de lissage avant de sous-échantillonner.

  • Appliquer un filtre de convolution par différence pour extraire un contour.

# Automatically reload code when changes are made
%load_ext autoreload
%autoreload 2
from PIL import Image, ImageDraw, ImageFont
import matplotlib.pyplot as plt
from scipy import signal
# Configuration intégration dans Jupyter
%matplotlib inline

from intro_science_donnees import *
from utilities import *

Lecture de l’image et extraction d’une sous image carrée#

Exercice

Chargez l’image smiley.jpg situé dans le dossier media et affectez-la à la variable img.

Indication: consultez la feuille image de la Semaine4 si besoin.

### BEGIN SOLUTION
img = Image.open("media/smiley.jpg")
### END SOLUTION
img
assert img.width == 640
assert img.height == 427

La bibliothèque PIL fournit une méthode crop pour recadrer des images. Elle prend en paramètre une liste (left, upper, right, lower) donnant successivement les coordonnées des points en haut à gauche et en bas à droite de la zone rectangulaire (box) à extraire. Voici, par exemple, l’image recadrée pour les points de coordonnées (0,0) et (200,100):

img.crop(box=(0,0,200,100) )

Exercice

Recadrer l’image pour ne garder que sa partie droite et de sorte à ce qu’elle soit carrée. Affectez le résultat à la variable img_carree.

Indication : Faites un dessin sur papier. Quel coin garder ? Comment trouver les autres coordonnées ? Pour cela, commencez par identifier la plus petite dimension entre la largeur (width) et la hauteur (height). Vous ferez en sorte que le carré soit de cette dimension là (on charche à garder le plus grand carré possible).

### BEGIN SOLUTION
img_carree = img.crop(box=(img.width-img.height, 0, img.width, img.height))
### END SOLUTION
img_carree
# Vérifications: on obtient une image de la bonne taille
assert isinstance(img_carree, Image.Image)
assert img_carree.size == (427, 427)

Ce sera la première image de votre animation!

Pour produire cette dernière, vous stockerez les images successives dans une liste images, puis les assemblerez à la fin. Commençons par celle-ci:

# Initialise une liste d'images vide
images = []
# Rajoute une image à la fin
images.append(img_carree)

La méthode append permet d’ajouter des élements à la fin d’une liste. Nous la réutiliserons pour mettre à jour images au fur et à mesure du TP.

image_grid(images)

Le générique de l’animation#

Lorsque l’on produit une œuvre, ici une animation, l’attribution de l’œuvre à ses auteurs est un élément indispensable. Pour cela, vous allez maintenant écrire votre nom et prénom sur l’image.

Exercice - question 1

Affectez votre nom à la variable name.

### BEGIN SOLUTION
name = "Correction"
### END SOLUTION
assert isinstance(name, str)

Exercice - question 2

Étudiez en détail les quatre commandes suivantes qui respectivement: - font une copie de l’image - construisent un canevas pour dessiner sur cette image - choisissent une fonte (si vous voulez changer essayez “DejaVuSans.ttf” qui devrait etre disponible ou lisez les indications) - dessinent un texte

Indication: la liste des fonts disponibles sur MyDocker-VD peut être obtenue en tapant dans une cellule de code ! ls /opt/conda/fonts (le point d’exclammation indique que ce n’est pas du python mais du bash).

img_title = img_carree.copy()
canvas = ImageDraw.Draw(img_title)
try: 
    font_name = os.path.join(os.environ["CONDA_PREFIX"], "fonts", "arial.ttf") #en 336, en local
except:
    font_name = os.path.join(os.environ["CONDA_DIR"], "fonts", "arial.ttf") # sur le hub
font = ImageFont.truetype(font_name, size=40)
canvas.text(xy=(100,10), text=name, font=font)

Exercice -question 3

Affichez l’image avec le titre qui vient d’etre créée

### BEGIN SOLUTION

img_title

### END SOLUTION

Exercice - question 4

Quel paramètre permet de définir où le texte sera écrit ?

BEGIN SOLUTION

C’est le paramètre xy de la méthode .text() Ici il vaut (100,10), soit x=100 et y=10.

END SOLUTION

Exercice - question 5

Ajoutez l’image produite à la liste images

### BEGIN SOLUTION
images.append(img_title)
### END SOLUTION
image_grid(images)
assert len(images) >= 2
assert images[-1] is img_title

Exercice ♣ - question 6

Modifiez ce qui précède pour personnaliser votre générique. Voici quelques idées: - explorez les options de canvas.text pour dessiner le texte selon vos préférences (couleur, fonte, position, …); - ajoutez non pas une seule image à images, mais une séquence d’images de sorte à réaliser un générique qui défile; - explorez les fonctions de canvas pour réaliser tout dessin de votre goût sur l’image.

Indications

- rajoutez autant de cellules ci-dessus que vous en aurez besoin;
- rassemblez les commandes pour rajouter du texte à une image en une fonction réutilisable dans `utilities.py`

Détection de l’avant-plan#

L’objectif est maintenant de déterminer la position du smiley sur l’image. Pour cela nous allons commencer par détecter quels pixels sont dans l’avant-plan (le smiley), et non dans l’arrière plan.

Un premier essai#

Reprenons la fonction foreground_filter du Projet1, qui est fournie dans utilities.py. Prenez le temps d’en consulter le code pour vous remémorer son fonctionnement.

show_source(foreground_filter)

Exercice

Appliquez cette fonction à img_carree, affectez le résultat à foreground0, et visualisez le résultat

### BEGIN SOLUTION
foreground0 = foreground_filter(img_carree, theta=100)
### END SOLUTION
plt.imshow(foreground0, cmap='gray');
# Vérifications:
assert isinstance(foreground0, np.ndarray)    # foreground est un tableau ...
assert foreground0.dtype == np.dtype('bool')  # de booléens ...
assert foreground0.shape == img_carree.size   # de même taille que l'image

Exercice

Essayez le calcul ci-dessus avec différentes valeurs de seuil theta. Laquelle donne le meilleur résultat selon vous?

BEGIN SOLUTION

  • Avec la valeur par défaut, theta=150, presque toute l’image est dans l’avant-plan (en blanc donc).

  • Pour theta=20, tout est dans l’arrière plan.

  • theta=100 donne un compromis où le smiley est en bonne partie distingué de ses alentours. Cependant une grande partie du fond reste dans l’avant-plan.

END SOLUTION

Exercice

Cette méthode de détection d’avant plan est elle satisfaisante? Si non, quel est le problème?

BEGIN SOLUTION

Cette méthode de détection d’avant plan n’est pas vraiment satisfaisante. Le problème est que foreground_filter fait l’hypothèse que l’arrière plan de l’image est blanc ou très clair, ce qui n’est pas du tout le cas sur cette image. Il va falloir jouer sur les différences de couleurs, donc les trois canaux.

END SOLUTION

Le smiley a la jaunisse#

Pour améliorer cela, nous allons utiliser un filtre pixel-à-pixel mesurant non pas le niveau de gris mais le niveau de jaune de chaque pixel.

Exercice - question 1

Implémentez la fonction yellowness_filter dans le fichier utilities.py en utilisant la formule \(y = r + g - b\)\(y\) est l’intensité du jaune (yellow). Puis affichez ici la fonction avec show_source.

Indication:

si vous n’avez plus en tête comment extraire différentes couches de couleur de l’image et faire des calculs dessus, consulter la feuille d’extraction d’attributs.

### BEGIN SOLUTION 
show_source(yellowness_filter)
### END SOLUTION

Exercice - question 2

Appliquez ce filtre à img_carree et gardez le résultat dans l’objet img_yellowness

### BEGIN SOLUTION 
img_yellowness = yellowness_filter(img_carree)
### END SOLUTION

Affichons le résultat:

plt.imshow(img_yellowness, cmap='gray')
plt.colorbar();
# Vérifications: 
assert isinstance(img_yellowness, np.ndarray)      # img_yellowness est un tableau numpy ...
assert img_yellowness.dtype == np.dtype('float64') # de nombre flottants ...
assert img_yellowness.shape == img_carree.size     # de même taille que l'image
assert img_yellowness[250, 250]- 351.0 <=2.0
assert img_yellowness[210, 260] - 196.0 <= 2.0
assert img_yellowness[212, 265] - 191 <= 1

Exercice - question 3

Quelle est la valeur maximale théorique que peut prendre un pixel d”img_yellowness ? Et minimale ?

BEGIN SOLUTION

Le max est atteint quand il y a beaucoup de rouge et de vert et pas de bleu donc 510. Le min est atteint quand il y a que du bleu donc -255.

END SOLUTION

Exercice - question 4

Ajoutez img_yellowness à la liste d’images

### BEGIN SOLUTION
images.append(img_yellowness)
### END SOLUTION
image_grid(images)
assert len(images) >= 3
assert images[-1] is img_yellowness

Généralisons!#

Notez que la formule \(r+g-b\) est un produit scalaire! Nommément, c’est le produit scalaire \(v.w\) de la couleur du pixel – représentée par le vecteur \(v = (r,g,b)\) – avec le vecteur \(w=(1,1,-1)\). Le rôle du coefficient \(-1\) est de discriminer les couleurs du blanc – représentée par le vecteur \((255,255,255)\) – qui contiennent du bleu en plus du jaune.

Une autre façon de discriminer ces couleurs, est de diviser le produit scalaire par la norme de \(v\). Cela nous donne alors \(\frac{v . w}{|v|}\), où \(w=(1,1,0)\). À un coefficient multiplicateur et centrage près, on retrouve la formule \(\frac{\overline v.\overline c}{|\overline v|,|\overline c|}\) de la corrélation de \(v\) avec la couleur jaune \(c=(255,255,0)\). La corrélation est maximale lorsque \(v\) est proportionnel à \(c\), c’est-à-dire que \(v\) représente une couleur jaune quelconque, qu’elle soit sombre ou claire.

Exercice ♣ - question 1

Implémenter dans utilities.py la fonction color_correlation_filter calculant, pour chaque pixel d’une image, sa corrélation avec une couleur donnée (paramètre color). Affichez ici le code de cette fonction à l’aide de show_source

Indication: Transformez l’image en array. Chaque pixel et color sont alors des vecteurs de longueur 3. Pour chaque pixel, calculez la corrélation avec la couleur à l’aide de la fonction np.corrcoef(). Usage : la fonction de Numpy np.corrcoef(u, v)[0, 1] calcule la corrélation entre deux vecteurs u et v. Vous pouvez utiliser une compréhension avec deux boucles for imbriquées.

### BEGIN SOLUTION 
show_source(color_correlation_filter)
### END SOLUTION

Exercice - question 2

Essayez votre fonction sur l’exemple suivant et interprétez chacun des coefficients obtenus. Un avertissement peut apparaître; quel en est la cause?

BEGIN SOLUTION

END SOLUTION
img = np.array([[[255,255,0], [255,  0,255], [0,255,255]],
                [[255,0,  0], [  0,255,  0], [0,  0,255]],
                [[1,  1,  0], [  2,  0,  1], [0,  0,  0]]])
resultat = color_correlation_filter(img, np.array([255,255,0]))
resultat
expected = np.array([[1, -.5, -.5],
                     [.5, .5,  -1],
                     [1., 0., np.nan]])
assert np.allclose(resultat, expected, equal_nan=True)

Exercice - question 3

Appliquer cette fonction à img_carree avec la couleur jaune \((255,255,0)\) et affecter le résultat à img_yellowness.

yellow = (255,255,0)
### BEGIN SOLUTION
img_yellowness = color_correlation_filter(img_carree, yellow)
### END SOLUTION

Exercice - question 4

Affichez le résultat

### BEGIN SOLUTION
plt.imshow(img_yellowness, cmap="gray")
plt.colorbar();
### END SOLUTION

Exercice - question 5

Le smiley n’est en fait pas en jaune pur (la corrélation n’est pas exactement de 1). Recommencons le calcul en calculant la corrélation avec un jaune moyen de notre smiley.

  • Commencer par extraire une zone du smiley. Transformer cette zone en np.array() et garder la en mémoire dans la variable img_cropped

  • Calculer la couleur moyenne de cet objet dans la variable average color. Suivre les indications pour calculer cette couleur

  • Ensuite, appliquer le filtre avec cette couleur moyenne. Mettre le resultat dans un nouvel img_yellowness.

Indication: Une couleur moyenne aura trois valeurs selon les canaux R, G et B. Calculer la moyenne de chaque couche de couleur en utilisant l’option axis=(0,1) de np.mean() sur la variable dont vous voulez calculer la couleur moyenne.

### BEGIN SOLUTION
img_cropped = np.array(img_carree)[250:255, 205:250]
average_color = np.mean(img_cropped, axis = (0,1))
### END SOLUTION
img_yellowness = color_correlation_filter(img_carree, average_color)
assert len(average_color) == 3
assert img_cropped.shape[0] < np.array(img_carree).shape[0]
assert img_cropped.shape[1] < np.array(img_carree).shape[1]
assert img_cropped.shape[2] == np.array(img_carree).shape[2]
plt.imshow(img_yellowness, cmap="gray")
plt.colorbar();

Exercice - question 6

Quels sont les avantages et inconvénients de cette méthode?

BEGIN SOLUTION

Avantage: facile à implémenter.

Inconvénient: ne s’applique que lorsque l’avant plan est de couleur homogène et n’apparaissant pas dans le fond.

END SOLUTION

Exercice - question 7

Ajoutez img_yellowness à la liste d’images

### BEGIN SOLUTION
images.append(img_yellowness)
### END SOLUTION
image_grid(images)

Avant-plan par jaunisse#

Exercice

Utilisez un seuil sur img_yellowness pour calculer les pixels du smiley; affectez le résultat à la variable foreground.

### BEGIN SOLUTION
foreground = img_yellowness > 250  # Avec la première façon de calculer img_yellowness
foreground = img_yellowness > 0.99 # Avec la deuxième façon de calculer img_yellowness
### END SOLUTION
plt.imshow(foreground, cmap='gray');
# Vérifications:
assert isinstance(foreground, np.ndarray)    # foreground est un tableau ...
assert foreground.dtype == np.dtype('bool')  # de booléens ...
assert foreground.shape == img_carree.size   # de même taille que l'image

Exercice

Ajoutez foreground à la liste d’images

### BEGIN SOLUTION
images.append(foreground)
### END SOLUTION
image_grid(images)

Recadrage (crop)#

Calcul du centre du smiley#

Exercice

Calculez les coordonnées du centre du smiley et affectez le résultat à la variable center.

Indications: Vous pouvez calculer le barycentre des coordonnées \((i,j)\) des pixels dans le smiley.
Consultez le code de la fonction elongation dans le utilities du Projet 1.

### BEGIN SOLUTION
center = np.mean(np.argwhere(foreground), axis=0)
### END SOLUTION
assert np.linalg.norm(center - np.array([275, 230])) < 5

Exercice \(\clubsuit\)

  1. Dessinez un rond au centre de l’image pour indiquer le barycentre à l’aide d”ImageDraw.Draw() et de sa méthode .ellipse().

  2. Affichez le résultat et ajoutez le résultat à la liste d’images.

Indication: Pour dessiner sur l’image, il faut au préalable la reconvertir depuis notre tableau de booléen vers une image couleur. C’est ce que fait la première ligne ci-dessous.

img_barycentre = Image.fromarray(foreground).convert("RGB")

### BEGIN SOLUTION
# Dessin du cercle de couleur
canvas = ImageDraw.Draw(img_barycentre)
canvas.ellipse(xy=(center[1]-3,center[0]-3,center[1]+3, center[0]+3), fill="red")
images.append(img_barycentre)
plt.imshow(img_barycentre, cmap='gray');
### END SOLUTION
assert isinstance(img_barycentre, Image.Image)
assert img_barycentre.size == img_carree.size
assert list(np.array(img_barycentre)[int(center[0]), int(center[1]), :]) == [255,0,0], "Le centre du smiley n'est pas rouge"

Recadrage du smiley#

Exercice

Recadrez votre image pour qu’elle soit centrée sur le smiley et de taille 256 x 256. Vous la nommerez img_cropped.

Indication: Utilisez la méthode crop vue en début de TP et le centre de l’image défini juste avant.

### BEGIN SOLUTION
i, j = center
img_cropped = img_carree.crop(box=(j-128, i-128, j+128, i+128))
### END SOLUTION
plt.imshow(img_cropped);
assert img_cropped.size == (256,256)
assert np.array(img_cropped)[0:50,0:50,2].mean() >= 75  # Le coin en haut à gauche n'est pas jaune
jaune = np.array([250,205,75])
center = np.array(img_cropped)[120:130, 120:130]
assert (center - jaune).mean() < 5 # le centre est en moyenne jaune
assert (center - jaune).std() < 10 # le centre est homogène

Exercice

Procédez de même pour foreground en affectant le résultat à foreground_cropped.

Indication: Utilisez Image.fromarray pour reconvertir foreground en Image.

### BEGIN SOLUTION
foreground_cropped = Image.fromarray(foreground).crop(box=(j-128, i-128, j+128, i+128))
### END SOLUTION
plt.imshow(foreground_cropped);

Exercice

Ajoutez foreground_cropped à la liste d’image! Vous pouvez aussi ajouter l’image recadrée maintenant, ou bien plus tard selon votre goût.

### BEGIN SOLUTION
images.append(foreground_cropped)
### END SOLUTION
image_grid(images)

Réduction de la résolution#

Nous allons voir comment réduire la résolution d’une image. Cela peut être typiquement utile pour réduire le volume des données et accélérer les calculs.

Essayons de réduire la résolution de notre image à 128x128 pixels.

img_downsampled = foreground_cropped.resize((128, 128))
plt.imshow(img_downsampled, interpolation="none"); # imshow applique un anti-crénelage par défaut

Comme on le voit ci-dessus, l’opération de sous-échantillonnage permettant de réduire la résolution d’une image est sujette au problème de crénelage (aliasing en anglais).

Pour mitiger cela, il existe plusieurs techniques d”anti-crénelage; elles consistent à lisser l’image obtenue, en donnant à chaque pixel de l’image obtenue une valeur interpolée à partir des pixels qu’il «représente» dans l’image d’origine.

Cependant, pour interpoler entre du noir et du blanc, il faut avoir du gris à disposition! C’est pourquoi la méthode resize n’a pas pu la lisser notre image foreground_cropped qui est en noir et blanc.

Dans tous les autres cas, resize mets automatiquement en œuvre de l’anti-crénelage. Il suffit donc de convertir au préalable notre image en niveaux de gris pour en bénéficier:

img_downsampled = foreground_cropped.convert("L").resize((128, 128))
plt.imshow(img_downsampled, interpolation="none", cmap="gray");

Exercice

Consultez la documentation de convert! Cette méthode vous resservira.

Suppression du bruit par lissage#

À ce stade, notre avant-plan contient de nombreux points isolés qui viennent du bruit dans l’image (des pixels du fond qui sont par hasard de la même couleur que l’objet, et réciproquement):

plt.imshow(foreground_cropped, cmap='gray');

Pour réduire le bruit, une technique classique est de lisser (ou flouter) l’image, en remplaçant chaque pixel par une moyenne avec ses pixels voisins. Ainsi, des pixels blancs isolés au milieu de pixels noirs deviendront gris sombre. Réciproquement pour des pixels noirs isolés au milieu de pixels blancs deviendront gris clair. Il restera alors à appliquer un nouveau seuillage pour les recataloguer en pixels blancs ou noirs.

Comme beaucoup de traitements d’images, lisser est un traitement local: un pixel est transformé en fonction de sa valeur et des valeurs de ses voisins plus ou moins proche. Cela peut s’exprimer par un produit de convolution.

Lissage par filtre de Gauß#

Une première façon de lisser une image est d’utiliser un filtre de Gauss. On convolue avec une fonction Gaussienne (courbe en cloche). Nous allons utiliser celui implanté dans la bibliothèque SciPy.

sigma=1

from scipy.ndimage import gaussian_filter
img_filtered = gaussian_filter(foreground_cropped.convert("L"), sigma=sigma)
plt.imshow(img_filtered, cmap='gray');

Après seuillage, on obtient:

seuil = 100

foreground_cropped_clean = Image.fromarray(img_filtered > seuil)
plt.imshow(foreground_cropped_clean, cmap='gray');

Exercice

Faites varier ci-dessous la valeur du paramètre sigma et celle du seuil et observez le résultat. Que se passe-t-il si sigma est trop faible ou trop élevé?

BEGIN SOLUTION

Si sigma est trop faible, il reste du bruit. S'il est trop élevé, l'image est trop floue et déformée par le seuillage.

END SOLUTION

Exercice

Proposez ci-dessous des valeurs de sigma et de seuil pour extraire au mieux l’avant-plan.

### BEGIN SOLUTION
sigma = 1
seuil = 100
### END SOLUTION
img_filtered = gaussian_filter(foreground_cropped.convert("L"), sigma=sigma)
foreground_cropped_clean = Image.fromarray(img_filtered > seuil)

fig = plt.figure(figsize=(15,6))
ax = fig.add_subplot(1,3,1)
ax.set_title("avant-plan original")
ax.imshow(foreground_cropped, cmap="gray")
ax = fig.add_subplot(1,3,2)
ax.imshow(img_filtered, cmap="gray")
ax.set_title("lissé")
ax = fig.add_subplot(1,3,3)
ax.imshow(foreground_cropped_clean, cmap="gray")
ax.set_title("avant-plan nettoyé");

Exercice ♣

À quelle propriété de la fonction Gaussienne correspond le paramètre sigma (noté 𝜎). Expliquer avec vos mots pourquoi augmenter 𝜎 floute davantage l’image.

BEGIN SOLUTION

Le paramètre 𝜎 est l'écart type de la fonction Gaussienne. Plus celui-ci
est élevé, plus la fonction Gaussienne est étalée horizontalement,
et plus la convolution fait intervenir les pixels plus lointains.

END SOLUTION

♣ Lissage itératif#

Dans cette section, nous allons explorer un autre procédé de lissage. Au lieu d’effectuer une seule convolution avec une fonction compliquée, nous allons itérer plusieurs fois une convolution très simple.

À chaque itération chaque pixel de la nouvelle image est obtenu à partir du pixel d’origine et de ses huit voisins immédiats en y appliquant des coefficients. Ces coefficients sont donnés par une matrice \(3\times3\) appelée le noyau (kernel en anglais). Nous utiliserons le noyau suivant. Le \(.25\) au centre spécifie par exemple que le pixel d’origine contribuera avec un coefficient de \(1/4\), tandis que celui du dessus contribuera avec un coefficient de \(1/8\):

ker = np.outer([1, 2, 1], [1, 2, 1])
ker = ker/np.sum(ker)
ker

Nous appliquons seize fois de suite la convolution à l’image foreground_cropped en affichant les résultats intermédiaire pour visualiser l’évolution. La convolution est calculée en utilisant signal.convolve2d; les options précisent le comportement au bord.

import scipy.signal

M = foreground_cropped

fig = plt.figure(figsize=(20,20))
for k in range(16):
    M = scipy.signal.convolve2d(M, ker, boundary='wrap', mode='same')

    fig.add_subplot(4,4,k+1)
    plt.imshow(M, cmap='gray')

On note que, au fur et à mesure, les pixels blancs isolés deviennent de plus en plus sombres: ils sont en effet influencés par leurs voisins noirs, puis par les voisins des voisins et ainsi de suite. On note aussi que l’image devient de plus en plus floue; il ne faudrait donc pas aller trop loin.

Il ne reste plus qu’à appliquer à nouveau un seuil, pour obtenir une image propre:

foreground_cropped_clean = M > 0.7
plt.imshow(foreground_cropped_clean, cmap='gray');
smiley = transparent_background_filter(img_cropped, foreground_cropped_clean)
smiley

Exercice

Rajoutez les images que vous souhaitez à votre liste d’images

### BEGIN SOLUTION
images.append(foreground_cropped_clean)
images.append(img_cropped)
images.append(smiley)
### END SOLUTION
image_grid(images)

Exercice

Rappelez ci-dessous les étapes effectuées jusque là

BEGIN SOLUTION

  • Recadrage

  • Ajout du nom, d’un titre

  • Filtration du jaune et identifiaction de l’image

  • Recadrage et centrage

  • Diminution de la résolution et lissage

  • Retour à l’image

  • Extraction de l’objet

END SOLUTION

Extraction des contours#

Nous souhaitons maintenant extraire les contours de l’image. Remettons l’image en réels:

M = foreground_cropped_clean * 1.0

Le principe est de calculer la valeur absolue de la différence entre chaque pixel et son voisin de gauche. S’ils sont identiques, on obtient zéro. S’ils sont différents – on est sur un bord – on obtient une valeur positive. De manière équivalente, on calcule la différence entre l’image et l’image décallée de un pixel.

Note: lors de cette opération de différence, on perd une bande de 1 pixel en largeur; pour conserver l’image carrée, on a aussi supprimé une bande de 1 pixel en hauteur.

contour_horizontal = np.abs(M[:-1, 1:] - M[:-1, 0:-1])
plt.imshow(contour_horizontal, cmap='gray');

Exercice

Notez que c’est déjà presque parfait, sauf lorsque le contour est horizontal. Pourquoi ?

BEGIN SOLUTION

Lorsque le contour est horizontal, chaque pixel est du même côté
du contour que son voisin de gauche. Il faut considérer les autres
voisins.

END SOLUTION

Exercice

Pour améliorer cela, procédez de même verticalement et affectez le résultat dans contour_vertical

### BEGIN SOLUTION
contour_vertical = np.abs(M[1:, :-1] - M[0:-1, :-1])
### END SOLUTION
assert contour_vertical.shape == contour_horizontal.shape
assert contour_vertical.max() == 1.0
assert contour_vertical.min() == 0.0
assert (contour_horizontal != contour_vertical).any() == True
plt.imshow(contour_vertical, cmap='gray');

Maintenant, c’est au tour des contours verticaux d’être peu détectés. Qu’à cela ne tienne, il suffit d’additionner les deux résultats:

contour = contour_horizontal + contour_vertical

Et voilà le travail!

plt.imshow(contour, cmap='gray');

Superposition d’images#

Nous allons maintenant vous montrer comment superposer deux images. Récupérons nos images de fruits de la semaine 3:

from intro_science_donnees import data
fruit_dir = os.path.join(data.dir, 'ApplesAndBananasSimple')
fruits = load_images(fruit_dir, "*.png")
image_grid(fruits)

Et choisissons une banane:

banana = fruits.iloc[18]

On convertit les deux images en tableaux NumPy :

M = np.array(smiley)
B = np.array(banana)

On choisit les coordonnés où l’on veut superposer la banane :

i = 100
j = 130

On extrait dans P la zone de l’image qui contiendra l’image, on calcule l’avant-plan de la banane et, pour tous ces pixels de l’avant-plan, on affecte les couleurs de F à P :

P = M[i:i+32, j:j+32]
F = foreground_filter(banana)
P[F] = B[F]

Enfin on réinsère P dans l’image d’origine :

M[i:i+32, j:j+32] = P

Et voilà :

plt.imshow(M);
images.append(M)

Exercice

Superposer de même un autre fruit là où vous le souhaitez!

### BEGIN SOLUTION
apple = fruits.iloc[6]
A = np.array(apple)
i = 106
j = 100
P = M[i:i+32, j:j+32]
F = foreground_filter(apple)
P[F] = A[F]
M[i:i+32, j:j+32] = P

plt.imshow(M);

### END SOLUTION

Exercice

Rajoutez l’image à la liste.

### BEGIN SOLUTION
images.append(M)
### END SOLUTION

Bilan#

Voici toutes les images que vous avez produites au cours du traitement :

image_grid(images)
assert len(images) >= 7

Production de l’animation#

Nous allons maintenant assembler toutes les images pour produire l’animation illustrant toutes les étapes du traitement d’images ci-dessus.

La cellule suivante utilise les widgets de Jupyter pour construire une mini application interactive:

import ipywidgets

# Vue
output = ipywidgets.Output()
play = ipywidgets.Play(min=0, max=len(images)-1, value=0, interval=500)
slider = ipywidgets.IntSlider(min=0, max=len(images)-1, value=0)
controls = ipywidgets.HBox([play, slider])
application = ipywidgets.VBox([controls, output])

# Controleur
def update(event):
    with output:
        output.clear_output(wait=True)
        fig = Figure()
        ax = fig.add_subplot()
        ax.imshow(images[slider.value], cmap='gray')
        display(fig)

slider.observe(update)
ipywidgets.jslink((play, "value"), (slider, "value"))

application

Comme l’animation est courte et les images petites, nous allons nous contenter de la produire comme une image gif animée directement avec PIL. Consultez le code de l’utilitaire animation fourni pour en savoir plus. Une alternative pour des animations plus longues serait d’utiliser la bibliothèque OpenCV que l’on peut appeler directement depuis Python. Voir l’utilitaire video. Encore une alternative serait d’utiliser moviepy qui utilise ffmpeg sous le capot.

animation(images, "video.gif")

Selon votre navigateur, vous pourrez visualiser la vidéo directement ici:

import IPython.display
IPython.display.Image('video.gif')

Sinon, téléchargez la depuis le navigateur de fichier.

Conclusion#

Vous êtes arrivé à la fin de ce TP où vous avez appris à créer des diaporamas avec Jupyter et à effectuer de petits traitements d’images tels que ceux que vous mettrez en œuvre pour prétraiter vos données lors de votre projet final.

Exercice

Il ne vous reste plus qu’à revenir à la feuille d”index, mettre à jour votre rapport de TP et vérifier la qualité de votre code.