OpenCV 4 et Python 3 : Premiers pas¶
Sur les machines de l'université, les Tps se feront en utilisant la plateforme Jupyter de Lyon1 Accessible ici
Connexion à la plateforme
- Se connecter sur la plateforme : Menu Connexion, choisir le seveur Serveur L3
- Saisir vos identifiants Lyon1
- Choisir l'option Notebook classique
- Cliquer sur le bouton Start
Création d'un dossier pour stocker les images
- créer un nouveau dossier : bouton Nouveau (à droite), choisir Répertoire
- un nouveau dossier est créé qui s'appelle Untitled Folder
- cocher la case à gauche de ce dossier
- Cliquer sur le bouton Renommer, saisir le nom de votre choix
Pour ajouter des images dans le dossier
- Cliquer sur le nom du dossier précédemment créé
- Une fois la page chargée, cliquer sur le bouton Téléverser
- Choisir les fichiers images, puis valider
Création d'un dossier pour stocker les notebooks jupyter
- revenir à la racine de votre arborescence
- répéter les opérations ci-dessus en choisissant un autre nom
Les codes que vous aller écrire seront stockés sous la forme de notebooks jupyter. Un notebook est un ensemble de bloc pouvant, dans le cas de la plateforme Lyon1, stocker :
- du texte formatté, en utilisant une description Makdown Documentation
- du code python
Les blocs de code python pourront être exécutés les uns à la suite des autres comme s'ils faisaient partie d'un même fichier. Ainsi, si vous déclarez une variable dans un bloc, une fois ce bloc exécuté, la variable sera accessible dans les blocs suivants. De plus, si vous modifiez un bloc, vous pouvez le ré-exécuter sans avoir besoin de ré-exécuter les blocs précédents.
Petit conseil : faites un notebook par exercice, cela évitera certains effets de bords.
Attention, le code python écrit dans un notebook s'exécute sur une machine distante. Ainsi, les fonctionnalités d'interface graphique d'OpenCV ne sont pas utilisables et font planter le noyau python. Si cela vous arrive, il sera nécessaire de redémarrer le noyau (menu Noyau)
Pour créer un nouveau notebook :
- Se placer dans le dossier dans lequel vous voulez créer le notebook
- Créer le notebook : menu Nouveau, choisir Python 3 (ipykernel)
- Renommer le fichier
- Cliquer sur le nom du fichier pour ouvrir l'interface d'étidion
Un notebook par défaut ne contiendra qu'un seul bloc de code.
Il est possible de changer le type de bloc en utilisant la liste déroulante dans la barre d'outils.
Un bloc est exécutable en utilisant le bouton ad-hoc dans cette même barre d'outil
- si le bloc est de type Markdown, le texte sera affiché après sa mise en forme
- si le bloc est de type Code, le code sera exécuté et le résultat affiché à la suite
Il est possible d'ajouter un bloc en utilisant le bouton +. Le nouveau bloc sera de type Code.
Le premier bloc de code devra contenir les commandes d'import des bibliothèques python.
import numpy as np
import matplotlib.pyplot as plt
import cv2
La bibliothèque numpy sera utilisée pour la représentation en mémoire des image. Le module pyplot de la bibliothèque matplotlib sera utilisé pour les affichage d'image. La bibliothèque cv2 sera utilisée pour tous la partie analyse et traitement d'images
Attention :
- une image couleur sera représentée en python par un tableau n-dimensions Numpy où n vaudra 3.
- une image en niveaux de gris sera aussi représentée en python par un tableau n-dimensions Numpy mais n vaudra 2.
Création d'une image¶
Créer une image BGR vide avec numpy
img = np.zeros((height,width,3), np.uint8)
Créer une image BGR vide aux mêmes dimensions qu'une autre image
img2 = np.zeros_like(img)
Dimensions d'une image¶
Les dimensions de la matrice nd représentant l'image sont accessible de la façon suivante :
img.shape
Cet attribut est un tuple python comprenant de 2 à 3 valeurs. Les deux premières valeurs représentent les dimensions de l'image : hauteur et largeur. La dernière valeur décrit le nombre de composantes permettant de représenter les couleurs.
Si l'image n'a qu'une seule composante par pixel alors le tuple ne comportera que 2 valeurs.
Accès aux données pixels¶
Pour accéder aux données couleur d'un pixel :
(b, g, r) = img[100, 100]
Pour modifier les données couleur d'un pixel :
img[100,100] = [255,255,255]
Pour accéder aux données d'une image en niveaux de gris (une seule composante) :
gray = img[100,100]
Pour modifier les données d'une image en niveaux de gris (une seule composante) :
img[100,100] = 196
Séparation/fusion des différentes composantes¶
Séparer les différentes composantes couleur d'une image :
(B, G, R) = cv2.split(img)
Après cet appel, B, G, R seront des images de même dimension que img mais ne comportant qu'une seule composante. B (resp. G et R) contient l'ensemble des valeurs de la composante bleu (resp. vert et rouge) de chaque pixel de l'image d'origine.
Combiner les images des différentes composantes pour former une image couleur :
img = cv2.merge((B,G,R))
Attention, cette fonction prend un tuple contenant les images des différentes composantes. D'où le double parenthésage.
Conversion entre espaces de couleur¶
Pour convertir une image entre deux espaces de couleur, il faut utiliser la fonction cv2.cvtColor(). Cette fonction prend 2 paramètres :
- image l'image à convertir
- code le code de conversion sous la forme cv2.COLOR_{ESPSRC}2{ESPDST}
par exemple, pour passer de l'espace BGR à un niveau de gris :
res = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
Les différents codes de conversion disponibles sont décrit ici.
Redimensionnement d'une image¶
Pour redimensionner une image, il faut utiliser la fonction OpenCV cv2.resize(s, size,fx,fy,interpolation). Cette fonction prend de 2 à 5 paramètres :
- s (obligatoire) l'image à redimensionner
- size (obligatoire) un tuple de 2 valeurs : les dimensions de l'image après redimensionnement
- fx (optionnel) le facteur d'échelle selon l'axe X
- fy (optionnel) le facteur d'échelle selon l'axe Y
- interpolation (optionnel) la méthode d'interpolation utilisée. Les différentes valeurs possibles sont disponibles ici
Affichage d'une image dans une fenêtre OpenCV¶
Pour afficher une image, il est nécessaire de passer par matplotlib
# création de l'image
img = np.zeros((480,640,3), np.uint8)
# affichage
fig = plt.figure()
plt.imshow(img)
plt.show()
Exercice 1¶
Ecrire un programme python permettant de créer et afficher une image couleur (3 composantes) avec les dimensions suivantes :
- hauteur : 480 pixels
- largeur : 640 pixels
Le programme devra successivement
- remplir intégralement l'image en noir et l'afficher
- remplir intégralement l'image avec une couleur choisie aléatoirement et l'afficher
- dessiner un rectangle de dimensions 50x100 centré dans l'image dans une couleur choisie aléatoirement et l'afficher
Interaction utilisateur¶
Attention: Il est normalement impossible, dans un notebook python, de gérer les interactions utilisateur (clavier, souris, ...).
Il est cependant possible d'utiliser une bibliothèque python pour gérer des sliders
from ipywidgets import interact
# création de l'image
img = np.zeros((480,640,3), np.uint8)
def setColor(f):
global img
img[...,0] = f
# affichage
fig = plt.figure()
plt.imshow(img)
plt.show()
interact(setColor,f=(0,255,1))
Dans l'exemple ci-dessus, la fonction setColor est la fonction à appeler si la valeur du slider change.
L'appel à interact permet de mettre en place l'interaction. On lui passe deux arguments :
- la fonction à appeler
- la définition de la valeur d'interation. Ici f sera un slider allant de 0 à 255 par pas de 1.
Attention le nom associé lors de l'appel à interact doit être le même que celui de l'argument de la fonction à appeler.
Attention : notez l'utilisation du mot-clé global dans la fonction fonctionTrackbar. Sans cette ligne de code, python considérera la variable valeur comme une variable locale de la fonction et ne modifiera donc pas la valeur de la variable globale. Cette ligne n'est cependant pas nécessaire dans la fonction traitement1 puisque valeur est utilisée en partie droite d'une affectation. Il en va de même pour la variable image. Comme elle ne fait pas l'objet d'une affectation directe (du type image=...) alors python comprend qu'il s'agit d'une variable globale.
Exercice 2¶
A partir du code de l'exercice précédent, modifier le programme afin que les dimensions du rectangle dessiné soit définies par deux sliders.
Le programme précédent permet de dessiner en blanc sur une image noire. L'appuie sur le bouton gauche de la souris active le dessin. Le relachement de ce même bouton désactive le dessin. Quand la souris bouge, on vérifie si le dessin est activée ou pas.
Entrées / sorties fichiers avec OpenCV¶
Pour ouvrir un fichier image et charger les données correspondantes, il faut utiliser la fonction cv2.imread(fichier,options). Cette fonction prend 2 paramètres :
- fichier le chemin d'accès au fichier, relativement à l'endroit d'où vous exécutez votre script python
- options un "drapeau" (optionnel) permettant de spécifier la façon dont seront chargées les données de l'image. Par défaut, OpenCV chargera les données couleur en 8 bits par composante avec suppression du canal alpha si celui-ci existe. Les différentes valeurs possibles sont disponibles sur la documentation OpenCV ici.
Cette fonction retourne les données image dans un tableau numpy (numpy.ndarray).
Si le fichier ne peut être ouvert ou si les données ne peuvent être chargées, la fonction renverra None.
import cv2
import numpy
img = cv2.imread('Pictures/Camera Roll/WIN_20220325_17_38_19_Pro.jpg')
if img is None:
print('impossible de charger les données du fichier')
sys.exit(-1)
#...
Par défaut, OpenCV charge les données en couleur au format BGR en 8bits par composante. Dans le cas d'une image en niveaux de gris, elle sera quand même considérée comme une image couleur BGR mais les 3 composantes seront identiques.
L'appel : img = cv2.imread('...',cv2.IMREAD_GRAYSCALE) permet de lire l'image directement comme une image en niveau de gris. De cette manière img sera une image à une seule composante.
Pour enregistrer une image sur le disque, il faut utiliser la fonction cv2.imwrite(fichier,image,options). Cette fonction prend 2 à 3 arguments :
- fichier le chemin d'accès au fichier dans lequel seront sauvegardées les données. Il est spécifié relativement à l'endroit d'où vous exécutez votre script python
- image l'image à sauvegarder
- options [optionnel] des options à passer à l'encodeur du fichier. Selon le format utilisé, il peut y avoir le taux de compression, ...
import cv2
import numpy
#...
cv2.imwrite('Pictures/Camera Roll/WIN_20220325_17_38_19_Pro.jpg', img)
sauvegarde d'un graphique pyplot dans une image¶
Afin de pouvoir utiliser cette fonction, il est nécessaire d'ajouter dans le préambule du fichier :
import io
def plotToImage(fig, dpi=180):
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=dpi)
buf.seek(0)
img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8)
buf.close()
img = cv2.imdecode(img_arr, 1)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
return img
def redimensionner(image,facteur):
width = int(image.shape[1] * facteur / 100)
height = int(image.shape[0] * facteur / 100)
dim = (width, height)
return cv2.resize(image, dim, interpolation = cv2.INTER_AREA)
Cette fonction redimensionne une image en gardant correct le ratio hauteur / largeur. Elle prend en argument :
- l'image à redimensionner
- le facteur d'échelle à appliquer
Elle renvoie en résultat l'image après redimensionnement.
def mse(img1, img2):
h, w = img1.shape
# on force le passage en double pour avoir les différences négatives
diff = cv2.subtract(img1, img2,dtype=np.double)
err = np.sum(diff**2)
mse = err/(float(h*w))
diff = np.clip(np.abs(diff),0.0,255.0)
return mse, diff.astype(np.uint8)
Cette fonction prend en argument les deux images à comparer. Ces images doivent être aux mêmes dimensions.
Elle renvoie deux résultats : l'erreur moyenne et l'image des différences
def display(image):
fig = plt.figure()
if len(image.shape)==2:
# histogramme
fig = plt.figure()
ax = fig.add_subplot(111)
hist = cv2.calcHist([image], [0], None, [256], [0, 256])
ax.plot(hist)
ax.axis("tight")
imgH = plotToImage(fig)
imgH = cv2.resize(imgH,(image.shape[1],int(image.shape[0]/3)), interpolation = cv2.INTER_AREA)
plt.imshow(np.vstack((cv2.merge((image,image,image)),imgH)))
else:
h = int(image.shape[0]/3)
w = int(image.shape[1]/3)
# composantes
a, b, c = cv2.split(image)
aa = cv2.resize(cv2.merge((a,a,a)),(w,h), interpolation = cv2.INTER_AREA)
bb = cv2.resize(cv2.merge((b,b,b)),(w,h), interpolation = cv2.INTER_AREA)
cc = cv2.resize(cv2.merge((c,c,c)),(w,image.shape[0]-2*h), interpolation = cv2.INTER_AREA)
# histogramme
fig = plt.figure()
ax = fig.add_subplot(111)
for (chan, color) in zip([a,b,c],("b", "g", "r")):
# create a histogram for the current channel and plot it
hist = cv2.calcHist([chan], [0], None, [256], [0, 256])
ax.plot(hist, color=color)
ax.axis("tight")
imgH = plotToImage(fig)
imgH = cv2.resize(imgH,(image.shape[1]+w,h), interpolation = cv2.INTER_AREA)
plt.imshow(np.vstack((np.hstack((image,np.vstack((aa,bb,cc)))),imgH)))
plt.show()
Cette fonction affiche un graphique matplotlib contenant une image, les vignettes des différentes composantes couleur de cette image ainsi que les histogrammes associés. Si c'est une image en niveaux de gris, il n'y aura pas de vignettes de composantes et un seul histogramme sera affiché.