Traitement d’images#
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!
Voici les étapes du projet:
Extraire la partie droite de l’image pour avoir une image carrée
Extraire le smiley de son fond
Recadrer l’image sans changement de résolution, de sorte à obtenir une image de taille 256x256 centrée sur le smiley,
Réduire la résolution à 128x128, avec une technique anti-crénelage, par lissage puis sous-échantillonage.
Extraire les contours du smiley pour en faire une image noir et blanc.
Mettre en toile de fond un papier-peint représentant des fruits
Choisir trois images de fruits et les extraires de leur fond.
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\) où \(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 variableimg_cropped
Calculer la couleur moyenne de cet objet dans la variable
average color
. Suivre les indications pour calculer cette couleurEnsuite, 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\)
Dessinez un rond au centre de l’image pour indiquer le barycentre à l’aide d”
ImageDraw.Draw()
et de sa méthode .ellipse().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.