OpenCV 4 et Python 3 : Premiers pas¶

Table of contents

  • Premiers pas avec OpenCV et Numpy
    • Mise en place
    • Manipulations de base
    • Affichage d'une image dans une fenêtre OpenCV
  • Exercice 1
  • Interactions utilisateur
  • Exercice 2
  • Interactions souris
  • Exercice 3
  • Entrées / sorties fichiers avec OpenCV
  • Petites fonctions utiles
    • sauvegarde d'un graphique pyplot dans une image
    • redimensionnment "propre" d'une image
    • Différence entre deux images
    • Affichage complexe d'une image

Premiers pas avec OpenCV et Numpy¶

Mise en place¶

La première chose à faire est d'inclure les packages python permettant par la suite d'utiliser :

  • OpenCV via le package cv2
  • numpy
  • pyplot le module d'affichage du package matplot lib
In [ ]:
import cv2
import numpy as np
import matplotlib.pyplot as plt

#...

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.

Manipulations de base¶


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 possible d'utiliser la fonction native d'OpenCV cv2.imshow(name,image).

Cette fonction prend 2 paramètres :

  • name le nom de la fenêtre (texte qui s'affichera dans la barre de titre de la fenêtre)
  • image le tableau de données de l'image

Attention : par défaut, OpenCV encode les images en Bleu-Vert-Rouge. L'affichage de l'image via OpenCV prend en compte cela et donc les images s'affichent correctement.

In [ ]:
import cv2
import numpy as np
import sys

# création de l'image

img = np.zeros((480,640,3), np.uint8)

cv2.namedWindow('image originale')
cv2.imshow('image originale',img)

cv2.destroyWindow('image originale')

La fonction cv2.destroyWindow permet de fermer la fenêtre OpenCV dont le nom est passé en argument. Pour fermer l'ensemble des fenêtres en un seul appel, il faut utiliser la fonction OpenCV cv2.destroyAllWindows qui ne prend aucun paramètre.

La fonction cv2.imshow n'est pas bloquante, elle rend la main à l'appelant une fois l'image affichée. Ainsi l'exécution de ce programme se termine dès l'affichage de la fenêtre.

Il est donc nécessaire de "bloquer" manuellement l'exécution du programme après l'affichage en attendant l'appuie sur une touche du clavier. Ceci peut être fait en utilisant la fonction cv2.waitKey(delai). Cette fonction prend un seul paramètre : le temps (en milliseconde) d'attente avant de rendre la main au programme. Une valeur de 0 fera que la fonction attendra indéfiniement.

Si une touche est pressée avant la fin du délai d'attente, la fonction va rendre la main au programme en renvoyant le code de la touche pressée.

Attention : la fonction cv2.waitKey ne fonctionnera que si une fenêtre OpenCV est affichée.

In [ ]:
import cv2
import numpy as np
import sys

# création de l'image
img = np.zeros((480,640,3), np.uint8)

cv2.namedWindow('image originale')
cv2.imshow('image originale',img)

key = cv2.waitKey(0) & 0x0FF
if key == 27:
    print('arrêt du programme par l\'utilisateur')
    cv2.destroyWindow('image originale')
    sys.exit(0)

Dans l'exemple ci-dessus, l'attente se poursuit indéfiniement tant que l'utilisateur n'appuie pas sur une touche.

Une fois que l'utilisateur appuie sur une touche, le programme récupère le code ASCII correspondant (via le ET binaire avec la valeur 0x0FF). Si la touche appuyée est la touche échap (code 27) alors le programme se termine sinon il passe à la suite.

Il est possible de prendre en compte la touche appuyée afin de réaliser différents traitements. Pour cela, il est nécessaire de placer la gestion des touches dans une boucle infinie :

In [ ]:
import cv2
import numpy as np
import sys

def traitement1(image):
    # resultat = ...
    cv2.imshow('image originale',image)

# création de l'image
img = np.zeros((480,640,3), np.uint8)

cv2.namedWindow('image originale')
cv2.imshow('image originale',img)

while True:
    key = cv2.waitKey(30) & 0x0FF
    if key == 27 or key==ord('q'):
        print('arrêt du programme par l\'utilisateur')
        break;
    if key ==ord('t'):
        traitement1(img)

cv2.destroyWindow('image originale')

Exercice 1¶

Corrigé

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 proposer les interactions suivantes :

  • touche 'n' : remplir intégralement l'image en noir
  • touche 'c' : remplir intégralement l'image avec une couleur choisie aléatoirement
  • touche 'r' : dessiner un rectangle de dimensions 50x100 centré dans l'image dans une couleur choisie aléatoirement

Interactions utilisateur¶

Il est possible d'intégrer, dans une fenêtre OpenCV, des sliders afin de définir la valeur de certains paramètres.

Pour créer un slider, il faut utiliser la fonction cv2.createTrackbar : cv2.createTrackbar(slidername,windowName,currentValue,maxValue,callbackFunction,userData)

Dans cette fonction :

  • sliderName est le nom affiché à coté du slider dans la fenêtre
  • windowName est le nom de la fenêtre dans laquelle le slider sera intégré (la fenêtre doit avoir été créée préalablement)
  • currentValue est la valeur initiale donnée au paramètre
  • maxValue est la valeur maximale que peut prendre le paramètre
  • callbackFunction est une fonction qui sera appelée lorsque le slider change de valeur. Cette fonction prend un argument : la valeur du slider.
  • userData [optionnel] permet de passer des données à la fonction de callbackFunction

Attention : la valeur gérée par le slider est forcément une valeur entière comprise dans l'intervalle [0,maxValue].

La fonction de réponse callbackFunction prend 2 arguments si userData a été défini :

  • la valeur du slider
  • les données utilisateur userData

Si userData n'a pas été défini alors la fonction callbackFunction prend un seul argument qui correspond à la valeur du slider.

In [ ]:
import cv2
import numpy as np

def traitement1():
    image[...] = valeur
    cv2.imshow('image originale',image)
    
def fonctionTrackbar(v):
    global valeur
    valeur = v
    traitement1()

image = np.zeros((480,640,3), np.uint8)
valeur = 100

cv2.namedWindow('image originale')
cv2.createTrackbar('Valeur','image originale',valeur,255,fonctionTrackbar)
traitement1()

while True:
    key = cv2.waitKey(30) & 0x0FF
    if key == 27 or key==ord('q'):
        print('arrêt du programme par l\'utilisateur')
        break;

cv2.destroyWindow('image originale')

Le programme précédent va remplir intégralement l'image avec la valeur définie par le slider.

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¶

Corrigé

A partir du code de l'exercice précédent, modifier le programme afin que :

  • la touche 'r' permette d'active/désactiver le dessin du rectangle
  • les dimensions du rectangle dessiné soit définies par deux sliders.

Le rectangle devra être redessiné à chaque modification d'un slider si le dessin est activé.


Interactions souris¶

Il est possible d'interagir avec la souris dans une image. Ceci se fait en définissant une fonction de callback pour gérer les évènements souris en utilisant la fonction cv2.setMouseCallback : cv2.setMouseCallback(windowName,callbackFunction)

Dans cette fonction :

  • windowName est le nom de la fenêtre dans laquelle il sera possible d'interagir avec la souris (la fenêtre doit avoir été créée préalablement)
  • callbackFunction est une fonction qui sera appelée lorsqu'un évènement souris survient.
  • userData [optionnel] permet de passer des données à la fonction de callbackFunction

La fonction de callback prend 5 paramètres :

  • action : la définition de l'évènement. les différentes valeurs possibles sont disponibles ici
  • x : la position en X dans l'image de la souris lors de l'évènement
  • y : la position en Y dans l'image de la souris lors de l'évènement
  • flags : des indicateurs sur l'état des touches spéciales. les différentes valeurs possibles sont disponibles ici
  • data : les données utilisateur définies lors de l'appel à cv2.setMouseCallback
In [ ]:
import cv2
import numpy as np

# création de l'image
image = np.zeros((480,640,3), np.uint8)
draw = False

def drawCallback(event,x,y,flags,data):
    global draw
    if event==cv2.EVENT_LBUTTONDOWN:
        draw = True
        image[y,x] = [255,255,255]
    elif event==cv2.EVENT_LBUTTONUP:
        draw=False
    elif event==cv2.EVENT_MOUSEMOVE:
        if draw:
            image[y,x] = [255,255,255]
    cv2.imshow('image originale',image)

cv2.namedWindow('image originale')
cv2.setMouseCallback('image originale',drawCallback)
cv2.imshow('image originale',image)

while True:
    key = cv2.waitKey(30) & 0x0FF
    if key == 27 or key==ord('q'):
        break;

print('arrêt du programme par l\'utilisateur')
cv2.destroyWindow('image originale')

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.


Exercice 3¶

Corrigé

A partir du code de l'exercice précédent, modifier le programme afin que :

  • le dessin du rectangle se fasse lors d'un click souris
  • les dimensions du rectangle dessiné soit définies par deux sliders.
  • le rectangle devra être centré sur la position de la souris

Le rectangle devra être redessiné à chaque modification d'un slider si le dessin est activé.


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.

In [ ]:
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, ...
In [ ]:
import cv2
import numpy

#...

cv2.imwrite('Pictures/Camera Roll/WIN_20220325_17_38_19_Pro.jpg', img)

Petites fonctions utiles¶

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
In [ ]:
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

redimensionnment "propre" d'une image¶

In [ ]:
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.

Différence entre deux images¶

In [ ]:
def mse(img1, img2):
   h, w = img1.shape
   diff = cv2.subtract(img1, img2)
   err = np.sum(diff**2)
   mse = err/(float(h*w))
   return mse, diff

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

Affichage complexe d'une image¶

In [ ]:
def display(image,winName):
    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)
        cv2.imshow(winName,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)
        cv2.imshow(winName,np.vstack((np.hstack((image,np.vstack((aa,bb,cc)))),imgH)))

Cette fonction affiche une fenêtre OpenCV 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é.