Tutoriel OpenCV Python - Traitement d'images - Vision par ordinateur

OpenCV est la bibliothèque OpenSource de référence en ce qui concerne la vision par ordinateur (ou computer vision en anglais). En 22 ans d'existence, elle a accumulé plus de 2500 algorithmes optimisés issus de la littérature scientifique tels que SIFT, les cascades de Haar, ou encore la transformée de HoughElle permet entre autre : 

Bien qu'il existe des alternatives telles que MATLAB, Octave ou encore Pandore, OpenCV reste privilégiée par les ingénieurs et scientifiques du monde entier pour l'analyse et le traitement des images.

OpenCV, la bibliothèque favorite des ingénieurs  et des chercheurs en vision par ordinateur

L'adoption massive d'OpenCV par la communauté de l'analyse et du traitement d'images s'explique par les facteurs suivants :







Au cours de ce tutoriel, nous aborderons les notions suivantes : 

 


Comment installer OpenCV ?

Python est actuellement le "langage de référence" ou encore "la lingua franca" pour la pratique du machine learningC'est un langage simple et puissant qui dispose autour de lui d'une large communauté d'utilisateurs. De ce fait, une large collection d'algorithmes de vision par ordinateur tels que selective-search, les levels-set ,Graph-Cut ou encore les super-pixels sont déjà implémentés en Python. Malgré cela, mon avis sur ce langage demeure mitigé. Autant j'affectionne ce langage, autant je le désaffectionne.

Car Python est un excellent langage de par sa simplicité, sa clarté et le peu de ligne de code qu'il nécessite afin de résoudre un problème. L'utilisation de l'indentation afin de délimiter les conditions et les boucles est une excellente idéeCependant, en termes de performance, il demeure l'un des pires langages. En effet, l'usage du Python est 76 fois plus énergivore que l'usage du langage C et nécessite 70 plus de temps d'exécution qu'un algorithme implémenté en C.

C'est la raison pour laquelle, mon avis sur le Python reste mitigé.

Aussi, d'expérience, je vous recommande de privilégier Python de manière générale, que ce soit pour apprendre une notion, implémenter ou tester un algorithme. Car ce langage est nettement plus simple. De plus, 500 lignes écrites en python équivalent à 1500 en langage C.

Cependant, dès que le temps d'exécution en Python est trop élevé, pour le problème de vision par ordinateur que vous souhaitez résoudre (ex : Détection et suivi d'objet en temps réel sur du matériel embarqué), je vous recommande d'implémenter les parties les plus gourmandes de votre algorithme dans un second programme C++. Et de faire le programme python communiquer avec le programme C++. Cette communication peut s'effectuer à l'aide de tubes, des sockets ou tout simplement de fichiers écrits directement sur le disque dur.

Si Python n'est pas installé sur votre ordinateur, il vous faut le télécharger depuis la page officielle de téléchargement de python.org puis l'installer.

Page officielle de téléchargement de l'interpréteur Python

Une fois Python installé, le gestionnaire de paquet "pip" est automatiquement installé. Il vous faut donc ouvrir votre terminal de commande puis taper  la ligne suivante afin d'installer le package OpenCV de Python.


pip install opencv

Cependant, cette ligne de commande n'a jamais fonctionné pour moi, car je suis sur Windows.

Par conséquent, à chaque fois que je veux  installer un package depuis pip je suis obligé de taper "python -m" avant la commande pip officielle.  

python -m pip install opencv


Il existe une seconde version d'OpenCV nommée OpenCV Contrib. 

En plus de regrouper les algorithmes de la version standard,  elle y ajoute d'autres algorithmes relativement plus récents et  potentiellement plus performants même si ce n'est pas toujours le cas. 

Cependant, les algorithmes d'OpenCV COntrib ne sont pas toujours stables et pleinement testés/validés. Cette version est en quelque sorte un prototype évolutif des prochaines versions d'OpenCV. Ainsi, au fur et à mesure que les algorithmes d'OpenCV Contrib sont validés et gagnent en stabilité et en popularité, ils sont intégrés à la version standard.

 On y retrouve par exemple des algorithmes de détection et de reconnaissance de code-barre, de QR code ou encore de texte.

Entre autres, OpenCV Contrib contient les versions CUDA (utilisant la carte graphique)  des algorithmes de la version standard. Selon l'application que l'on fait d'OpenCV Contrib et la carte graphique NVIDIA de laquelle l'on dispose, nous pouvons aisément constater des gains en performance de l'ordre de 30 fois (voir plus) comparativement à la version CPU. 

La commande pip permettant d'installer OpenCV Contrib est la suivante :

pip install opencv-contrib-python


Pour la suite de ce tutoriel, vous pouvez installer la version d'OpenCV de votre choix. Cependant, nous n'aurons besoin que des fonctions incluses dans la version standard. 


Comment charger et enregistrer une image avec OpenCV?

Pour utiliser OpenCV, il est nécessaire de charger cette bibliothèque en mémoire en utilisant  le mot-clé "import".

import cv2

Afin de charger une image, il faut employer la fonction cv2.imread( nom_image) comme cela :

image =cv2.imread('fleur.png')

Pour sauvegarder une image, c'est la fonction imwrite (nom_image, image)  qu'il faut utiliser.

cv2.imwrite('fleur.png',gray)






Comment afficher une image avec OpenCV?

Afin d'afficher une image à l'écran, il est nécessaire d'appeler dans un premier temps la fonction namedWindow pour créer une fenêtre graphique.

Puis, il faut utiliser la fonction imshow qui prend pour paramètres le nom de la fenêtre et l'image à afficher.

La fonction  waitKey permettra ensuite de mettre la fenêtre en attente d'un événement (clic de souris, touche au clavier, ...).

Pour finir, la fonction destroyAllWindows libérera la mémoire occupée par  la fenêtre après que celle-ci ait été fermée.    


Code complet pour afficher une image :

import cv2
img=cv2.imread ("fleur.png")
cv2.namedWindow('image', cv2.WINDOW_NORMAL)
cv2.imshow('image',img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Résultat :



Ci-dessous, j'ai mis une petite documentation des fonctions qui ont été utilisées pour afficher l'image.

cv2.namedWindow('image',cv2.WINDOW_NORMAL)

Namewindow prend en argument le nom de la fenêtre et un flag qui indique si vous souhaitez que la fenêtre soit redimensionnable ou non. Par défauts, ce second paramètre vaut cv.WINDOW_AUTOSIZE. Cela signifie que la taille de la fenêtre s’adaptera à la taille de l’image et ne sera pas modifiable. Personnellement, je préfère mettre cv2.WINDOW_NORMAL, car si l’image est trop grande ou trop petite vous pourrez modifier manuellement la taille de la fenêtre.

cv2.imshow('image',img)

Imshow comme je l'ai dit précédemment prend en premier paramètre le nom de la fenêtre et en second paramètre l’image. 

cv2.waitKey(0)

Mets la fenêtre en attente pendant une certaine durée indiquée en milliseconde en paramètre de la fonction. Sinon, jusqu’à ce qu’il y ait un évènement généré par l’utilisateur.

cv2.destroyAllWindows()

Détruis toutes les fenêtres et libère la mémoire associée à celles-ci. La fonction n’a besoin d’aucun argument. Il est possible de détruire une fenêtre spécifique en passant son nom en argument de la fonction. 


Les espaces de couleurs

De manière générale, quand tu vas travailler sur une image, bien souvent,celle-ci sera au format RGBC'est le pire format qu'il puisse être pour travailler sur une image, car celui-ci est adapté au capteur CMOS que l'on retrouve dans les caméras.

En somme, pour ne pas trop m'attarder là-dessus, je vais t'expliquer comment fonctionne un appareil photo.  

Mais pour cela, je vais d'abord t'expliquer le fonctionnement de la rétine humaine, cette petite partie de l'oeil humain qui te permet de voir.

La rétine humaine, si tu l'as un peu étudié sinon je t'invite à lire ce tuto que j'ai écrit, est constitué de 150 millions de capteurs de lumière qui sont de deux types les « bâtonnets » et les  « cônes ».

Les bâtonnets sont chargés de la vision scotopique (en noir et blanc) tandis que les cônes sont chargés de la vision photopique (en couleurs).  les cônes sont au nombre de 7 millions sur notre rétine et sont divisibles en trois catégories : les cônes de faible, de moyenne et de forte longueur d’onde.Chacune des catégories étant de longueur d'ondes différentes permet de capter une lumière différente c'est pourquoi  les cônes de faible, de moyenne et de forte longueur d’onde permettront de capter respectivement la lumière bleue, la verte et le rouge.

Les autres couleurs que l'on peut voir telles que le jaune, le magenta ou encore la couleur du bois sont obtenus par combinaison de ses trois couleurs élémentaires.

Pour le capteur des appareils photo, c'est pareil que pour l'oeil humain il s’agit de milliers de photo-capteurs assemblés ensemble en forme de grille.

Ces photo-capteurs par des processus physiques que je ne peux expliquer puisque ce n'est pas mon domaine d'expertise transforment la lumière en signal électrique. Ce signal continu est par la suite converti en signal numérique qui sera traité par un ordinateur.


Ce qui a donné naissance à la représentation RGB (Red green blue)

La représentation RGB (Red green blue)

Le système RGB est celui utilisé par défaut dans les appareil photo car leur architecture matérielle nous y contraint.

Basiquement, un appareil photo consiste en un assemblage de nano photo-capteur en grille. Chaque capteur n’est capable de capter qu’une seule longueur d’onde parmi le rouge le vert et le bleu, d’où la représentation RGB.

Pour être manipulée informatiquement, il est nécessaire que la couleur soit convertie en valeur numérique. C’est pour cette raison que la notion d’espaces de couleurs a été inventée. Il existe énormément de manières de représenter une couleur.

Je ne vais aborder avec toi que les quatre des représentation représentations les plus populaires le RGB, le niveau de gris, le CMYB et le HSV. La CIELab est également populaire mais je ne suis pas habitué à l'utiliser.

L’espace CMYB (Cyan Magenta, Yellows, Black)

L’espace de couleurs CMYB (Cyan Magenta, Yellows, Black) est particulièrement utilisé en imprimerie, car l’encre des imprimantes est de couleur cyan, magenta et jaune, le noir parfait en mélangeant ces trois couleurs étant impossible à obtenir une cartouche d’encre noire est rajoutée d’où la composante B.



L’espace HSV (Hue, Saturation, Value)

Pour finir, l’espace HSV (teinte, saturation, luminosité) est proche de la perception visuelle humaine des couleurs. On se réfère à la teinte de l’objet  (le H) quand par exemple nous disons qu’il est rouge ou qu’il est vert.

La saturation (Le S) représente la pureté de la couleur de l’objet, ainsi un objet rouge ayant une forte pureté un nous paraîtra éclatant, tandis qu’avec une faible pureté il aura plutôt un aspect délavé.

Pour finir, l’intensité (Le V) représente la quantité de lumière émise sur un objet, plus elle est forte et plus la couleur est claire, tandis que plus celle-ci est faible, plus la couleur de l’objet est sombre.

La ligne de code suivante permet de passer de l’espace RGB à l’espace HSV :

Code :

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

Exemple :


import cv2
img=cv2.imread ("fleur.png")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
cv2.namedWindow('hsv', cv2.WINDOW_NORMAL)
cv2.imshow('hsv',hsv)
cv2.waitKey(0)
cv2.destroyAllWindows()


Résultat :


Transformer une image en niveau de gris

Il est possible de transformer une image couleur en niveau de gris avec la ligne de code suivant :

Code

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

Résultat :





Qu'est ce que c'est la segmentation d'images?

La segmentation permet d’isoler les différents objets présents dans une image.

Il existe différentes méthodes de segmentation : la classification, le clustering, les level-set, graph-cut, etc ....

Mais la plus simple est le seuillage, c’est pourquoi je vais te parler uniquement de celle-ci dans cette seconde partie.

Le seuillage est une opération qui permet de transformer une image en niveau de gris en image binaire (noir et blanc),

l'image obtenue est appelée masque binaire.

Le schéma ci-dessous illustre bien ce concept.


Illustration du seuillage optimal

Il existe 2 méthodes pour seuiller une image, le seuillage manuel et le seuillage automatique.

Seuillage manuel

Sur la l'illustration ci-dessus, trois formes sont présentes dans l’image originale.

Les pixels du rond sont représentés par des étoiles, ceux du carré par des rectangles jaunes, ceux du triangle par des triangles verts et les cercles bleus correspondent au fond.

Dans la figure, nous pouvons remarquer que tous les pixels correspondants du fond (rond bleu) ont une valeur supérieure à 175. Nous en déduisons que le seuil optimal est 175. En faisant cela, nous avons déterminé le seuil optimal manuellement.

Pour utiliser le seuil manuel avec OpenCV, il suffit d’appeler la fonction thresold

comme suit :

Code :
ret,th=cv2.threshold(img, seuil,couleur, option)

Elle prend en paramètre, img : l’image à traiter , seuil : la valeur du seuil , couleur : la couleur que l’on souhaite attribuer à la zone objet et elle retourne ret : la valeur du seuil, et th : l’image binaire résultat du seuillage. Afin d’illustrer cela, dans le code suivant, j’ai seuillé l’image fleur.png préalablement passée en niveau de gris avec 150 comme seuil.
Code :
import cv2
import numpy as np
img=cv2.imread("fleur.png");
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret,th=cv2.threshold(gray,150,255,cv2.THRESH_BINARY)
cv2.namedWindow('image',cv2.WINDOW_NORMAL)
cv2.imshow('image',th)
cv2.waitKey(0)
cv2.destroyAllWindows()
Résultat:

Nous remarquons que le seuillage n’a pas fonctionné.

Cela est dû au fait que les pixels du fond ont une valeur trop proche de celle des fleurs.

C’est pourquoi afin que ces valeurs soient bien distinctes, il est nécessaire de changer d’espace de couleur.
 Nous allons donc passer dans l’espace de couleur HSV et observer l’histogramme de l’image !

Construire l'histogramme d'une image

Pour calculer l’histogramme d’une image, il faut faire appel à la fonction calchist :

Code :

hist= cv2.calcHist([img],[i],[256],[0,256])
Elle prend en paramètre l’image, les indices des canaux que l’on souhaite extraire, la taille de l’histogramme, l’intervalle des valeurs de pixel à mesurer.Après avoir utilisé calchist pour obtenir le ou les histogrammes d’une image, il faut l’afficher avec la librairie Matplolib. Si vous ne l’avez pas installé, vous pouvez le faire en tapant dans le terminal :

pip install -U matplotlib

Une fois installée, il faudra appelé les fonction plot, xlim et show pour afficher l’histogramme. Plot servira à afficher le graphique à l’écran, xlim délimitera l’axe des abscisses à 256 niveau, et show créera la fenêtre qui affichera les histogrammes.

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img=cv.imread ("fleur.png");
#RGB -> HSV.
hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
#Déclaration des couleurs des courbes
color = ('r','g','b')
#Déclaration des noms des courbes.
labels = ('h','s','v')
#Pour col allant r à b et pour i allant de 0 au nombre de couleurs
for i,col in enumerate(color):
    #Hist prend la valeur de l'histogramme de hsv sur la canal i.
    hist = cv.calcHist([hsv],[i],None,[256],[0,256])
    # Plot de hist.
    plt.plot(hist,color = col,label=labels[i])
    plt.xlim([0,256])
#Affichage.
plt.show()
Dans le code ci-dessus, matplotlib est importé (ligne 3), l’image est mise en mémoire, puis convertis en hsv grâce à cvtColor (ligne 6). Ligne 12, une boucle qui va itérer sur chaque canal de l’image est créée. L’histogramme du canal courant est obtenu à la ligne 14 en utilsant calcHist puis est affiché à la ligne 18. 

Résultat :

Sur le résultat, nous remarquons, que le le canal hue représenté en rouge affiche 2 très grands pics de pixel. Ces pixels appartiennent probablement au fond, car ce dernier est majoritairement présent sur l’image.

Un bon seuil donc sur le canal hue serait un peu après les pics, au alentour de 35-45.

La saturation et la luminosité ne nous apprennent pas grand-chose sur l’image comme nous pouvons le constater. Afin de confirmer cela, nous pouvons seuiller pour voir.

Mais je préfère vous présenter dès à présent la méthode d’Otsu.

Seuillage automatique : Méthode d’Otsu

Il consiste à seuiller l’image courante pour tous les seuils possibles (de 0 à 255 pour une image en niveau de gris).

Un masque binaire pour chacun des seuils est alors créé.

Otsu va ensuite appliquer successivement chaque masque à l’image traitée.

Pour chaque masque, 2 images seront obtenues. Une ne contenant que le fond, et une ne contenant que les objets.

La variance interclasse, entre les pixels du fond et ceux des objets, est calculée et le seuil du masque pour lequel l'on obtient la plus grande variance est retenu comme seuil optimal.

La fonction python permettant d’utiliser cet algorithme est la fonction thresold.
Je ne vais pas la présenter, car je l’ai présenté précédemment et seuls le second (il passe à 0) et le dernier paramètre changent (+ cv.THRESH_OTSU a été rajouté) .
Code :

ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)

Exemple : Application de la méthode d’Otsu sur chaque canal de l’image HSV

import cv2 as cv
import numpy as np
img=cv.imread ("fleur.png");
hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
h,s,v= cv.split(hsv)
ret_h, th_h = cv.threshold(h,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
ret_s, th_s = cv.threshold(s,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
ret_v, th_v = cv.threshold(s,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
cv.imwrite("th_h.png",th_h)
cv.imwrite("th_s.png",th_s)
cv.imwrite("th_v.png",th_v)
Résultat :
Seuillage d'Otsu sur le canal hue

Seuillage d'Otsu sur le canal Saturation

Seuillage d'Otsu sur le canal Luminosité

Les seuils optimaux trouvés par Otsu pour les canaux teinte, saturation et luminosité sont respectivement 75, 168 et 114.

Remarquons que, la segmentation obtenue sur le canal luminosité n’a rien donné d’intéressant. Celle du canal hue a permis d’extraire les fleurs blanches. Enfin celle de la saturation a extrait toutes les fleurs sauf les blanches.

Afin d’obtenir une segmentation parfaite qui sépare toutes les fleurs du fond, il nous est nécessaire de fusionner le résultat obtenu en hue et en saturation.

Cette opération se fera par l’intermédiaire d’opérateurs binaires d’image.






Les Opérateurs binaires d’image :

Les opérateurs binaires d’image permettent d’appliquer aux images des opérations binaire logique

telles que le NON, le OU, le ET, mais aussi, le OU exclusif.

L'opérateur NO 

L’opérateur NO passe en noir les pixels qui sont blancs et en blanc les pixels qui sont noirs. Sa fonction OpenCV est bitwise_not. Elle prend en paramètre une image binaire.

cv2.bitwise_not(img) 

L'opérateur AND

Le résultat du "ET" est une image dont les pixels sont blancs aux endravec p,q étant les ordres du moment, I(x,y) est la valeur du pixel de l'image à la position (x,y). $\bar{x}$ et $\bar{y}$ sont respectivement les moyennes des  abscisses et des ordonnées des pixels appartenant à la région.oits ou les pixels de l’image 1 et de l’image 2 était simultanément blancs. Sa fonction OpenCV est bitwise_and qui prend le paramètre deux images binaires.

cv2.bitwise_and(img1,img2) 

L'opérateur OR

Le "ou" passe les pixels en blanc si les pixels de l’image 1 ou de l’image 2 sont blancs. Sa fonction OpenCV est bitwise_or. Elle prend en paramètre deux images binaires.

cv2.bitwise_or(img1,img2) 


L'opérateur XOR

Le "ou exclusif" passe les pixels en blanc si les pixels de l’image 1 ou de l’image 2 sont blancs, mais pas si les deux sont blanc ensemble. Sa fonction OpenCV est bitwise_xor. Elle prend en paramètre deux images binaires.

cv2.bitwise_xor(img1,img2) 


Dans notre cas, nous appliquerons le "ou",car nous voulons une image dans laquelle les pixels des fleurs blanches et des autres fleurs sont allumés.

Autrement dit, nous souhaitons les fleurs présentes sur l’image binaire hue ou sur  l’image binaire saturation.  Nous utiliserons ensuite la fonction cv2.bitwise_and 

pour appliquer le masque binaire obtenu sur l’image fleur.png. Nous obtiendrons ainsi l’image fleur.png privée de son fond. Le code suivant permet de réaliser ces opérations. 

Exemple :

import cv2
import numpy as np
img=cv2.imread ("fleur.png");
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v= cv2.split(hsv)
ret_h, th_h = cv2.threshold(h,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
ret_s, th_s = cv2.threshold(s,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
#Fusion th_h et th_s
th=cv2.bitwise_or(th_h,th_s)
cv2.imwrite("th.png",th)

Aux lignes 6 et 7 les canaux h et s sont seuillés. Les images binaires résultantes sont th_h et th_s, à la ligne 9 l’image binaire finale th est obtenue en fusionnant et th_h et th_s.  

Résultat :




Remplissage des régions

Nous pouvons améliorer ce résultat en retirant  les trous à l’intérieur des fleurs blanches. Pour ce faire, nous allons remplir les trous présents dans le masque de l’image.



Cela se fera notamment grâce à la fonction floodfill qui permet de remplir des régions.

cv2.floodfill(im_floodfill,mask,seedPoint,newVal)

 Elle prend en paramètre une image source de 1 canal ou 3 canaux, un masque,   cet argument indique par des pixels non nuls les zones que la fonction ne doit pas traiter  il doit être deux pixels plus larges et deux pixels plus longs que l’image source. Le seedPoint qui est le pixel de départ de la fonction. NewVal est la valeur que l’on souhaite donner aux régions à remplir.

cv2.copyMakeBorder(th, top,bottom,left,right,borderType,value)

Nous nous aiderons également de la fonction copyMakeBorder qui nous permettra d’ajouter temporairement des bords vides à l’image afin que la fonction flood fill ne remplisse pas une mauvaise zone. Elle prend en paramètre une image source, la taille en nombre de pixels de chacun des bords  haut, bas, gauche, et droite. Un flag qui indique le type de bord que l’on souhaite. Dans notre cas, nous utiliserons BORDER_Constant qui signifie que nous voulons des bords d’une seule couleur. Et enfin la couleur des bords qui sera dans notre cas du noir.
Code :

import cv2
import numpy as np
img=cv2.imread ("fleur.png");
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v= cv2.split(hsv)
ret_h, th_h = cv2.threshold(h,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
ret_s, th_s = cv2.threshold(s,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
#Fusion th_h et th_s
th=cv2.bitwise_or(th_h,th_s)
#Ajouts de bord à l'image
bordersize=10
th=cv2.copyMakeBorder(th, top=bordersize, bottom=bordersize, left=bordersize, right=bordersize, borderType= cv2.BORDER_CONSTANT, value=[0,0,0] )
#Remplissage des contours
im_floodfill = th.copy()
h, w = th.shape[:2]
mask = np.zeros((h+2, w+2), np.uint8)
cv2.floodFill(im_floodfill, mask, (0,0), 255)
im_floodfill_inv = cv2.bitwise_not(im_floodfill)
th = th | im_floodfill_inv
#Enlèvement des bord de l'image
th=th[bordersize: len(th)-bordersize,bordersize: len(th[0])-bordersize]
resultat=cv2.bitwise_and(img,img,mask=th)
cv2.imwrite("im_floodfill.png",im_floodfill)
cv2.imwrite("th.png",th)
cv2.imwrite("resultat.png",resultat)


La première partie du code est la même que précédemment. Nous seuillons sur les canaux teinte et saturation, que par la suite nous fusionnons afin d’obtenir le masque binaire de l’image. 

Ensuite, nous commençons par ajouter des bords à l’image à la ligne 13,  ligne 16, nous créons une image imfloodfill qui contiendra les régions à remplir,le  masque est créé à la ligne 19  avec en noir les trous à remplir et en blanc le reste. Nous l’inversons donc à la ligne 20 avant de la fusionner avec le masque de l’image.  Nous enlevons les bords que nous avions précédemment mis avant d’appliquer le masque à l’image et d’enregistrer les résultats.

Nous obtenons alors ces trois images avec im_floodfill que j’ai volontairement laissées afin que vous voyiez ce qu’elles contiennent

.









Nous  constatons que les trous on bien disparu sauf pour un cas les bords n’étaient pas totalement fermés.  Enfin, nous obtenons l’image résultat finale.









Découpage des différents objets de l’image

Maintenant que nous avons vu comment retirer le fond d’une image, il serait bien qu’on apprenne à isoler les objets présents dans celle-ci. Cela se fait grâce aux fonctions findContours, drawContours et boundingRect
d’OpenCV. 

La fonction findContours nous retourne les contours des objets présents dans une image

binaire, elle prend en paramètre une image binaire, le mode, qui indique à la fonction les contours dont nous souhaitons obtenir cela peut être les contours extérieurs, intérieurs ou les deux. Enfin, l’argument method indique comment nous souhaitons que les contours soient représentés dans notre cas, ils seront représentés par une suite de points connectés.

contours, hierarchy = cv2.findContours(thresh,mode,method) 

DrawContours va permettre de dessiner un à un sur des images vierges  chacun des contours

extraits précédemment avec find contours. Elle prend en paramètre,  une image sur laquelle la fonction va dessiner les contours , l’indice du contour que l’on souhaite dessiner, la couleur que l’on souhaite donner à ce contour et enfin l’épaisseur que l’on souhaite, -1 si l’on souhaite que le contour soit rempli.

cv2.drawContours(image,contours,contourIdx,couleur, thickness) 

Pour finir boundingrect est une fonction qui retourne les coordonnées de la boundingbox d’un contour
c’est-à-dire les coordonnées du carré de taille minimum contenant le contour. Cette fonction prend en paramètre le contour et retourne les coordonnées de sa bounding box.

x,y,w,h = cv.boundingRect(contours[i]) 

Voici le code qui grâce à ces trois fonctions nous permet de découper les objets présents dans l’image.
Code :

import cv2
import numpy as np
img=cv2.imread ("fleur.png");
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v= cv2.split(hsv)
ret_h, th_h = cv2.threshold(h,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
ret_s, th_s = cv2.threshold(s,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
#Fusion th_h et th_s
th=cv2.bitwise_or(th_h,th_s)
#Ajouts de bord à l'image
bordersize=10
th=cv2.copyMakeBorder(th, top=bordersize, bottom=bordersize, left=bordersize, right=bordersize, borderType= cv2.BORDER_CONSTANT, value=[0,0,0] )
#Remplissage des contours
im_floodfill = th.copy()
h, w = th.shape[:2]
mask = np.zeros((h+2, w+2), np.uint8)
cv2.floodFill(im_floodfill, mask, (0,0), 255)
im_floodfill_inv = cv2.bitwise_not(im_floodfill)
th = th | im_floodfill_inv
#Enlèvement des bord de l'image
th=th[bordersize: len(th)-bordersize,bordersize: len(th[0])-bordersize]
resultat=cv2.bitwise_and(img,img,mask=th)
cv2.imwrite("im_floodfill.png",im_floodfill)
cv2.imwrite("th.png",th)
cv2.imwrite("resultat.png",resultat)
contours, hierarchy = cv2.findContours(th,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
for i in range (0, len(contours)) :
    mask_BB_i = np.zeros((len(th),len(th[0])), np.uint8)
    x,y,w,= cv2.boundingRect(contours[i])
    cv2.drawContours(mask_BB_i, contours, i, (255,255,255), -1)
    BB_i=cv2.bitwise_and(img,img,mask=mask_BB_i)
    if h >15 and w>15 :
        BB_i=BB_i[y:y+h,x:x+w]
        cv2.imwrite("BB_"+str(i)+".png",BB_i)


 Au début, c’est le même code que précédemment,  jusqu’à la ligne 26. À la ligne 26, on extrait de l’image binaire th les contours des fleurs, Ret_tree signifie que l’on veut tout les contours et chain_ approx_simple que les contours doivent être représentés par une suite de point. À la ligne 27,  il y a une boucle qui va itérer sur tous les contours, à la ligne 28 une image vierge est créée , la bounding box du contour courant est extraite a la ligne  29, la zone englobée par le contour courant est dessinée sur l’image vierge qui a été créée au début de la boucle ligne 30. Ligne 31, le masque nouvellement créé est appliqué sur  l’image fleur.png et l’imagette contenant uniquement la fleur  courante est créée grâce au coordonné de la bounding box à la ligne 33  enfin, l’imagette  est enregistré ligne 34. Vous remarquerez que j’ai mis une condition à la ligne 32 afin d’éviter que trop d’images qui ne sont pas des fleurs soient extraites.

Résultat :

Nous remarquons que toute les fleurs ont été extraites  de manières individuelle, mais pas que, beaucoup d’objet gênant ont aussi été extrait. Nous verrons prochainement comment automatiquement retirer ses objet.

Comment extraire des informations d’une image ?

La caractérisation d’objet est la deuxième étape primordiale d’un logiciel de reconnaissance d’images. Il s’agit de transformer l’imagette de chaque objet en valeurs quantitatives ou qualitatives afin d’obtenir une information traitable par les algorithmes de classification tels que les réseaux de neurones ou encore les arbres de décision. Cette information, se veut être une synthèse d’une ou plusieurs propriétés de l’objet, par exemple l’aire de l’objet est une synthèse de la taille de l’objet.

Il existe quatre grands types d’attributs permettant de décrire un objet à partir des pixels qui le compose  :les attributs de région (air, moments, etc ...), de contour (périmètre, les coefficients elliptiques de Fourier, etc.…), de couleur (moyenne,histogramme,etc...) , et de texture (GLCM, filtre de Gabor, etc ….). 

Pour des raisons de simplicité, dans ce chapitre, nous travaillerons exclusivement qu'avec les 4 fleurs suivantes :

Les attributs de régions

En analyse d’image, la région d’un objet est définie comme étant l’étendue de pixel qui la compose. La figure ci-dessous illustre cette définition. À côté de chaque fleur, nous pouvons visualiser sa région.



Pour l’analyse des régions, OpenCV propose les différents attributs suivants :

L’aire

Obtenir l’aire d’une région consiste à compter le nombre de pixels qui la compose. La fonction OpenCV permettant de l’extraire est contourArea(cnt), elle prend en paramètre le contour d’une région et retourne son aire.

Code :

area = cv2.contourArea(cnt)


Le périmètre

Tout comme il est possible d’extraire l’aire d’un objet, il est également possible d’en extraire le périmètre. Pour cela, il faut utiliser la fonction arclength, qui va compter le nombre de pixels entourant l’objet. Elle prend en paramètre le contour de l’objet, et un booléen indiquant si l’objet est une courbe où  non et retourne son périmètre.

Code :
perimeter = cv2.arcLength(cnt,True)


La circularité :

 La circularité est une mesure comprise entre 0 et 1 qui exprime si  l’objet est rond. Plus  l’objet est rond et plus cette mesure sera proche de 1 et inversement moins l’objet sera rond et moins cette mesure sera proche de 1. La circularité s’obtient via la formule suivante : 


Code :

circularity= 4*area/(perimeter*perimeter)


La longueur et la hauteur de la bounding box contenant la région.

Il existe d’autres caractéristiques de région basées sur la bounding box de l’objet par exemple la longueur et la hauteur de la bounding box. 


Code :

x,y,longueur,hauteur = cv.boundingRect(cnt)


L’aspect Ratio. 

C’est le quotient de la longueur divisé par la hauteur.

Code :

aspect_ratio=w/h


L'étendue

Il s’agit du quotient de l’aire de la région par l’aire de sa bounding box.


Code :

area = cv.contourArea(cnt)
x,y,w,= cv.boundingRect(cnt)
rect_area = w*h
extent = float(area)/rect_area


La solidité

La solidité est le quotient de l’aire d’un objet par l’aire de sa forme convexe.


Code :

area = cv.contourArea(cnt)
hull = cv.convexHull(cnt)
hull_area = cv.contourArea(hull)
solidity =float(area)/hull_area

Le diamètre équivalent

Le diamètre équivalent est le diamètre du cercle ayant la même aire que le contour.


Code :

area = cv.contourArea(cnt)
equi_diameter = np.sqrt(4*area/np.pi)


Exemple :

Dans cet exemple, nous allons calculer l’aire, le périmètre, la circularité, la longueur et la hauteur de la bounding box l’aspect ratio et l’étendue. 

Code :

import cv2
import math
fleurs=[] # chargement des images
fleurs.append(cv2.imread ("fleur1.png"));
fleurs.append(cv2.imread ("fleur2.png"));
fleurs.append(cv2.imread ("fleur3.png"));
fleurs.append(cv2.imread ("fleur4.png"));
gray_fleurs=[]
for i in range (0, len(fleurs)):  # transformation en niveau de gris
    gray_fleurs.append(cv2.cvtColor(fleurs[i], cv2.COLOR_BGR2GRAY))
th_fleurs=[]
for i in range (0, len(fleurs)):  # seuillage
    ret,th = cv2.threshold(gray_fleurs[i],5,255,cv2.THRESH_BINARY)
    th_fleurs.append(th)
contours_fleurs=[]  # extraction des contours
for i in range (0, len(fleurs)):
    contours, hierarchy = cv2.findContours(th_fleurs[i],cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    contours_fleurs.append(contours)avec p,q étant les ordres du moment, I(x,y) est la valeur du pixel de l'image à la position (x,y). $\bar{x}$ et $\bar{y}$ sont respectivement les moyennes des  abscisses et des ordonnées des pixels appartenant à la région.
airs_fleurs=[]
perimetres_fleurs=[]
circularités_fleurs=[]
extent_fleurs=[]
aspect_ratio_fleurs=[]
for i in range (0, len(fleurs)):  # extraction des caractéristiques
    airs_fleurs.append(cv2.contourArea(contours_fleurs[i][0]))
    perimetres_fleurs.append(cv2.arcLength(contours_fleurs[i][0],True))
    circularités_fleurs.append(4*math.pi*airs_fleurs[i]/(perimetres_fleurs[i]*perimetres_fleurs[i]))
    x,y,w,= cv2.boundingRect(contours_fleurs[i][0])
    extent_fleurs.append(w/h)
    aspect_ratio_fleurs.append(airs_fleurs[i]/(w*h))
    contours_fleurs.append(contours)
for i in range (0, len(fleurs)):  # affichage
    print ("fleur",i," : ", " air :", airs_fleurs[i], "perimetre :", perimetres_fleurs[i], "circularité :",
     circularités_fleurs[i],"extent :",extent_fleurs[i], "aspect ratio:",aspect_ratio_fleurs[i])

Nous chargeons les images de la ligne 10 à 12, nous passons celle-ci en niveau de gris afin de pouvoir de la ligne 14 à 17 les seuiller. De la ligne 19 à 22, nous extrayons les contours des objets et de la ligne 31 à 35, nous prélevons des objets leurs aires, leurs périmètres, leurs circularités, leurs longueurs, leurs largeurs, leur étendue et leurs aspect ratio.

Résultat :  

fleur 0 : aire : 4663.0 perimètre : 685.8376532793045 circularité : 0.12457549745517907 extent : aspect ratio: 0.26860599078341013
fleur 1 : aire : 97072.5 perimètre : 1532.5209821462631 circularité : 0.5193895650343685 extent : aspect ratio: 0.6075575027382256
fleur 2 : aire : 215627.0 perimètre : 2436.507910966873 circularité : 0.45643333292163246 extent : aspect ratio: 0.7069158265715925
fleur 3 : air : 29071.0 perimètre : 869.5777685642242 circularité : 0.4831177331746388 extent : aspect ratio: 0.4716255678131084




Moments centraux et invariants à l’échelle


 Les moments d'une image sont utilisés afin de caractériser la forme d'un objet. Les moments centraux ont la particularité d'être invariants à la translation. Ils sont définis comme suit :


avec p,q étant les ordres du moment, I(x,y) est la valeur du pixel de l'image à la position (x,y). $\bar{x}$ et $\bar{y}$ sont respectivement les moyennes des  abscisses et des ordonnées des pixels appartenant à la région.

Moments centraux

À partir des moments centraux, il est possible de construire des moments invariants en translation et en échelle en utilisant la formule qui suit : 



où i et j sont les ordres du moment et $\mu_{ij}$ est le moment central d'ordre i, j, $\mu_{00}$ est le moment central d'ordre zéro.

La fonction moments d'openCV 


Un tableau avec les moments centraux suivant: mu20, mu11, mu02, mu30, mu21, mu12, mu03 et les moments invariants à l'échelle suivant : nu20, nu11, nu02, nu30, nu21, nu12, nu03 est retourné par la fonction "moments" d'openCV. Elle prend en paramètre le contour de l'objet traité.


Code :

M=cv2.moments(cnt)


Les moments de Hue

Les moments de Hue se calculent en utilisant les moments invariants à l'échelle. Leurs formules sont les suivantes :









Les attributs de texture


Après l'analyse par région et par forme, la méthode d'analyse d'image la plus fréquemment utilisée est l'analyse texturale. La texture est une information très importante pour la caractérisation des objets, car elle permet de distinguer des objets de forme et de taille similaire. Dans ce chapitre, nous verrons 3 méthodes de caractérisation texturale fréquemment utilisées par les spécialistes du traitement et de l'analyse d'images:  les matrices de co-occurrences, les filtres de Gabor et les motifs binaires locaux.  Mais avant cela, nous allons d'abord installer Scikit-image.

 Scikit-image est une bibliothèque de traitement d'images Python que j'apprécie particulièrement pour deux raisons, la première est qu'elle implémente les algorithmes de traitement d'image les plus populaires au sein de la communauté, et la seconde est qu'elle est très bien documentée et  par conséquent simple à prendre en main. Je me servirais de cette bibliothèque tout au long de ce chapitre afin de vous présenter les attributs de texture.  La commande pip permettant de l'installer est la suivante 
python -m pip install -U scikit-image

Maintenant que Scikit-image est installé, nous pouvons utiliser ses fonctions et modules pour caractériser des textures. Afin de mieux illustrer les notions abordées au cours de ce chapitre, nous travaillerons exclusivement avec les 3 images suivantes: 



3 textures utilisées dans ce chapitre.


Matrice de co-occurrence (GLCM)


Les matrices de co-occurrences ont été proposées en 1979, par Robert Haralick. Elles sont construites en comptant, dans l'image traitée, le nombre de fois qu'une paire de pixels est présente selon une distance et une direction fixée en paramètre.  (équation \ref{eq:GLCM}).


où i et j sont une paire de valeurs de pixel. I(x,y) est la valeur du pixel de position x et y de l'image traitée. $\Delta x$, $\Delta y$ sont les valeurs du décalage spatial pour lesquelles la matrice est calculée. 




A l'issue de la construction des matrices de co-occurrence, le contraste, la dissemblance, l'homogénéité, l'ASM et la corrélation sont calculés pour chaque matrice afin de former un vecteur de caractéristiques qui sera utilisé pour l'identification des pollens.




























.