Avant de partir dans des calculs de caractéristiques à rallonge afin de labelliser correctement un objet dans une image, il peut être intéressant de détecter simplement ses formes. Par exemple trouver un carré pour représenter un dé ou bien un rond permettant de détecter une balle.

Vous l’aurez compris nous allons nous attaquer aujourd’hui à la reconnaissance de formes. Les images que nous allons utiliser aujourd’hui sont des images simple où le fond est noir. Pour une application réelle, il sera nécessaire d’effectuer un prétraitement comme une soustraction de fond ou un seuillage.

Comment allons-nous procéder pour détecter des formes ?

Voici les 3 étapes que nous allons effectuer :

  1. L’extraction des contours dans l’image, qui nous permettra d’extraire un à un les objets.
  2.  L’approximation de chaque contour grâce à la fonction approxPolyDP. Cette fonction, reposant sur l’algorithme de Douglas-Peucker, sert à diminuer le nombre de noeuds/points sur une polyligne / contour. En d’autres termes, nous allons essayer de décrire les contours par un ensemble de segments.
  3. La classification des formes en fonction de nombre de lignes formant leur contour.

Reconnaissance de formes

Commençons par déclarer les importations dans le programme detect_formes.py et à charger une image.

import numpy as np 
import cv2 
image = cv2.imread('image.bmp')

Voici donc l’image que nous souhaitons traiter (disponible ici) :

Reconnaissance de forme avec OpenCV: Image d'origine

Étape 1: Détection des contours

La première étape que nous avons soulignée précédemment est de détecter les contours de l’image. Pour ce faire, nous allons convertir l’image en niveau de gris (l. 6) avant de réaliser un seuillage (l. 7) pour améliorer la détection de contours (findContours – (l. 9)).

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(gray,250,255,cv2.THRESH_BINARY_INV)

img,contours,h=cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

Voilà, nos contours sont détectés. Passons maintenant à l’étape suivante : l’approximation de nos contours.

Edit: Si vous êtes passé à OpenCV 4, la fonction findContours ne renvoi plus l’image. Il faut donc remplacer la ligne 9 par:
contours,h = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

Étape 2: Approximation des contours

for cnt in contours:
    perimetre=cv2.arcLength(cnt,True)
    approx = cv2.approxPolyDP(cnt,0.01*perimetre,True)
    
    M = cv2.moments(cnt)
    cX = int(M["m10"] / M["m00"])
    cY = int(M["m01"] / M["m00"])
    cv2.drawContours(image,[cnt],-1,(0,255,0),2)

Pour chaque contour, nous commençons par calculer le périmètre du contour (l. 12). Afin de pouvoir calculer l’approximation du contour ligne 13. La fonction approxPolyDP prend en entrée les points du contour, ainsi qu’un pourcentage (1% dans notre cas) du périmètre du contour. Plus ce pourcentage est grand, moins l’approximation sera précise. Enfin le dernier paramètre spécifie que la courbe que nous cherchons est fermée.

Nous en profitons pour calculer les moments du contour (l. 15) afin de pouvoir calculer simplement le centre de chaque contour (l. 16-17).

Enfin, nous traçons chaque contour sur l’image d’origine (l. 18) avant de passer à l’étape suivante.

Étape 3: classification des formes

Cette étape va être relativement simple à comprendre, il suffira juste de se rappeler combien de sommets a chaque forme.

if len(approx)==3:
        shape = "triangle"
    elif len(approx)==4:
        (x, y, w, h) = cv2.boundingRect(approx)
        ratio = w / float(h)
        if ratio >= 0.95 and ratio <= 1.05:
            shape = "carre"
        else:
            shape = "rectangle"
    elif len(approx)==5:
        shape = "pentagone"
    elif len(approx)==6:
        shape = "hexagone"
    else:
        shape= "circle"
    cv2.putText(image, shape, (cX, cY), cv2.FONT_HERSHEY_SIMPLEX,0.5, (255, 255, 255), 2)

Si la forme à trois sommets, il s’agit d’un triangle (l. 19-20). La démarche est identique pour le pentagone (l. 28-29) ou l’hexagone (l. 30-31). Pour 4 sommets, c’est légèrement différent. En effet, sur notre image, nous avons un carré, mais également un rectangle.

Nous savons que le carré à ses côtés de mêmes longueurs. Pour vérifier cela, nous utilisons la fonction boundingRect (l. 22) qui nous permet d’obtenir le rectangle encadrant de notre quadrilatère. Ce qui nous intéresse particulièrement, c’est que cette fonction nous renvoie la hauteur (h) et la largeur (w) de la forme. Ainsi le ratio largeur/hauteur devrait être égale à 1 pour un carré (l. 23) et différent de 1 pour un rectangle. Comme il est possible qu’il y ait des erreurs dans les calculs d’approximations ou lors du seuillage, nous acceptons une erreur de 5% autour de 1 afin de déterminer s’il s’agit bien d’un carré (l. 24).

Enfin, si le nombre de sommets est supérieur à 6, nous déterminons qu’il s’agit d’un cercle (l. 32-33). Bien évidemment, il serait possible de modifier notre code pour prendre en compte les heptagones, octogones et j’en passe 🙂

Enfin, nous rajoutons sur l’image, à proximité du centre de chaque contour, le type de forme dont il s’agit (l. 34).

Il nous reste plus qu’à afficher le résultat pour admirer notre travail.

cv2.imshow('image',image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Résultat reconnaissance de formes

Maintenant que nous arrivons à détecter les contours, il peut être intéressant d’en étudier la couleur. Les caractéristiques de forme et de couleur seront utiles lorsque l’on voudra suivre un objet dans une vidéo.

Le code complet:

import numpy as np
import cv2

image = cv2.imread('image.bmp')

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(gray,250,255,cv2.THRESH_BINARY_INV)

img,contours,h =cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

for cnt in contours:
    perimetre=cv2.arcLength(cnt,True)
    approx = cv2.approxPolyDP(cnt,0.01*perimetre,True)
    
    M = cv2.moments(cnt)
    cX = int(M["m10"] / M["m00"])
    cY = int(M["m01"] / M["m00"])
    cv2.drawContours(image,[cnt],-1,(0,255,0),2)
    if len(approx)==3:
        shape = "triangle"
    elif len(approx)==4:
        (x, y, w, h) = cv2.boundingRect(approx)
        ratio = w / float(h)
        if ratio >= 0.95 and ratio <= 1.05:
            shape = "carre"
        else:
            shape = "rectangle"
    elif len(approx)==5:
        shape = "pentagone"
    elif len(approx)==6:
        shape = "hexagone"
    else:
        shape= "circle"
    cv2.putText(image, shape, (cX, cY), cv2.FONT_HERSHEY_SIMPLEX,0.5, (255, 255, 255), 2)


cv2.imshow('image',image)
cv2.waitKey(0)
cv2.destroyAllWindows()

42 commentaires

Antoine · 3 avril 2018 à 22 h 34 min

Bonjour,
Serait-il possible de mettre le code final en entier car, j’ai compris toutes les parties mais impossible de les assembler.
Cordialement

    Florent · 3 avril 2018 à 22 h 40 min

    Le code a été ajouté.
    Bonne soirée 🙂

      MELBOUCI · 31 mars 2019 à 19 h 49 min

      y a t il un autre moyen de faire cette détection de contour sans passer par opencv ..??!!

        Florent · 1 avril 2019 à 20 h 13 min

        Bonjour,
        Oui il est possible de le faire avec d’autres outils.
        Que souhaitez-vous faire précisément ?

opencv · 1 juillet 2018 à 10 h 02 min

Bonjour, j’ai une division par zéro sur le calcul des moments ligne 16 une idée? Merci

    Florent · 2 juillet 2018 à 16 h 49 min

    Bonjour,
    Le problème semble venir du fait qu’aucun contour n’a été trouvé dans l’image.
    Il est possible que le seuillage soit trop important.
    Une visualisation de l’image seuillée permettrait de voir si le problème vient de là.

      Snaga · 27 novembre 2018 à 11 h 28 min

      Bonjour,
      j’ai récupéré l’image que vous utilisez dans votre exemple mais j’ai toujours une division par 0 ligne 16. Une idée ?? merci

        Florent · 27 novembre 2018 à 14 h 31 min

        Bonjour,
        Il y a un problème avec l’image que vous avez récupéré: il s’agit d’une image fortement compressé (avec perte).
        L’image seuillée que vous obtenez est donc fortement bruité, ce qui empêche la reconnaissance des contours.
        En effet, certains contours extraits correspondent à un seul pixel.

        J’ai mis à jour l’article afin de pouvoir télécharger l’image d’origine (non compressée).
        Elle est disponible à cette adresse.

HAMDI · 30 juillet 2018 à 14 h 24 min

Bonjour
Antoine à déjà posé la question.
Serait-il possible de mettre le code ?

Merci

    Florent · 1 août 2018 à 13 h 31 min

    Bonjour,
    Il semble il y avoir un problème sur le site sur lequel je travaille actuellement.
    En attendant voici le code.
    Désolé du désagrément !
    import numpy as np
    import cv2

    image = cv2.imread(‘image.bmp’)

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    ret,thresh = cv2.threshold(gray,250,255,cv2.THRESH_BINARY_INV)

    img,contours,h = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

    for cnt in contours:
    perimetre=cv2.arcLength(cnt,True)
    approx = cv2.approxPolyDP(cnt,0.01*perimetre,True)

    M = cv2.moments(cnt)
    cX = int(M[« m10 »] / M[« m00 »])
    cY = int(M[« m01 »] / M[« m00 »])
    cv2.drawContours(image,[cnt],-1,(0,255,0),2)
    if len(approx)==3:
    shape = « triangle »
    elif len(approx)==4:
    (x, y, w, h) = cv2.boundingRect(approx)
    ratio = w / float(h)
    if ratio >= 0.95 and ratio <= 1.05:
    shape = « carre »
    else:
    shape = « rectangle »
    elif len(approx)==5:
    shape = « pentagone »
    elif len(approx)==6:
    shape = « hexagone »
    else:
    shape= « circle »
    cv2.putText(image, shape, (cX, cY), cv2.FONT_HERSHEY_SIMPLEX,0.5, (255, 255, 255), 2)

    cv2.imshow(‘image’,image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

TIPE · 21 septembre 2018 à 15 h 11 min

Bonjour,
Nous nous aidons de votre programme dans le cadre d’un projet.
Nous travaillons sur Spyder (Python 3.4) mais nous n’arrivons pas à installer le module cv2 : pourriez-vous nous indiquer la démarche à suivre?
Merci

    Florent · 21 septembre 2018 à 15 h 29 min

    Bonjour,
    Je n’ai jamais utilisé Spyder, mais il est possible qu’une de ces deux solutions puisse vous aider:

    Si vous avez installé Spyder en utilisant Anaconda, vous pouvez utiliser Anaconda pour l’installer: https://pymotion.com/installation-opencv3-anaconda/

    Sinon, vous pouvez essayer de l’installer en utilisant Pip: https://pymotion.com/howto-opencv-windows/

    Tenez-moi au courant si cela marche, ou pas.

      TIPE · 28 septembre 2018 à 15 h 04 min

      Bonjour,
      Nous avons réessayé avec votre méthode, mais cela ne fonctionne pas… Auriez-vous une autre idée?
      Merci de votre aide

        Florent · 28 septembre 2018 à 16 h 24 min

        Bonjour,

        Je n’ai malheureusement pas de windows sous la main, donc il m’est difficile de vous donner une réponse précise …

        Tout d’abord quelle méthode avez-vous utilisez pour installer Spyder et OpenCV ?

        Pouvez-vous essayer dans un invite de commande:
        – de lancer python (juste taper python)
        – puis d’importer OpenCV (import cv2)

        Si tout se passe correctement, c’est que OpenCV est bien installé mais que le chemin n’est pas spécifié dans Spyder.
        Vous pouvez utiliser la fonction path de la bibliothèque sys (« import sys » puis « sys.path »)afin de connaitre le chemin d’accès aux librairies.
        Il faudra ensuite ajouter ce chemin à Spyder.

        J’espère que cela pourra vous aider à débloquer la situation.

        Florent

Dorian · 2 novembre 2018 à 18 h 15 min

Bonjour,
Dans le cadre d’un projet, j’ai besoin de crée un programme python qui reconnais les formes. Or je n’arrive pas a faire fonctionner le programme proposer ci-dessous. L’erreur ce trouve l’orsque je veux convertir l’image en niveau de gris. Pourriez vous m’aider?? Merci d’avance de votre réponse!
Pour information j’utilise spyder 3.6 et c’est la première fois que j’utilise open CV
voici ce que cela m’affiche

File « C:/Users/Jeune/Desktop/tipe/Imformatique/Logiciel/Spyder/settings/.spyder-py3/temp.py », line 9, in
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

error: OpenCV(3.4.3) C:\projects\opencv-python\opencv\modules\imgproc\src\color.cpp:181: error: (-215:Assertion failed) !_src.empty() in function ‘cv::cvtColor’

    Florent · 5 novembre 2018 à 8 h 19 min

    Bonjour Dorian,
    Je pense que votre problème provient du chargement de l’image.
    Ou plutôt du non chargement de l’image que vous voulez traiter.

    Regardez si le chemin utilisé pour importer l’image est correct (cv2.imread()).
    normalement, lorsque vous tapez la commande type(img), juste après le chargement, vous devriez obtenir: class ‘numpy.ndarray’ signifiant que votre image est bien ouverte. (class ‘NoneType’ dans le cas contraire)

      Dorian · 9 novembre 2018 à 22 h 22 min

      Re,
      Je vous remercie de votre réponse , en effet il y avais un problème sur le chemin utiliser. J’ai trouver mon erreur.
      Cordialement
      Dorian

Rémy · 16 décembre 2018 à 23 h 27 min

Bonjour, j’ai essayé la technique et copié le code à la lettre près mais ça m’affiche le message d’erreur :
File « +1 », line 5
image = cv2.imread(‘image.bmp’)
^
SyntaxError: invalid character in identifier
Cordialement,
Rémy

    Florent · 17 décembre 2018 à 9 h 12 min

    Bonjour Rémy,
    Le problème semble venir d’un caractère invalide dans la ligne.

    A première vue, cela semble provenir des guillemets.
    Essayez de remplacer ‘image.bmp’ par « image.bmp »

    Si cela ne marche pas, essayer de réécrire toute la ligne, cela doit venir d’un problème lors de la copie.

    Bonne journée,
    Florent

      Rémy · 3 janvier 2019 à 14 h 53 min

      Bonjour et merci de votre réponse, j’ai maintenant plus ce message d’erreur. Sauf qu’en lançant le programme il ne se passe rien. Avez-vous une idée de pourquoi et comment arranger cela ?

      Bonne journée,
      Rémy

        Florent · 7 janvier 2019 à 16 h 36 min

        Bonjour,

        Le programme devrait au minimum afficher l’image, ou renvoyer un message d’erreur.
        Êtes-vous sûr que l’image est bien correcte et est bien chargé ?

        Bonne journée,
        Florent

Alexandre Mayeux · 11 janvier 2019 à 16 h 06 min

Bonjour j’aimerai me servir de ce programme pour un projet très important pour le dit projet.
Vous dîtes qu’il est applicable en vrai mais comment ?
Merci de me repondre.

    Florent · 12 janvier 2019 à 10 h 32 min

    Bonjour Alexandre,
    Il faut dans un premier temps que vous arriviez à extraire les objets que vous voulez reconnaître.

    Dans cet article, nous avons seulement eu à effectuer un seuillage sur les niveaux de gris.
    En fonction de votre projet, il peut être nécessaire d’effectuer le seuillage sur un espace colorimétrique, ou bien d’effectuer une soustraction d’arrière-plan.

    Ensuite, en fonction de vos formes à reconnaître, il peut être nécessaire de modifier la classification des formes.

    Cordialement,
    Florent

      Alexandre Mayeux · 13 janvier 2019 à 20 h 50 min

      Tout d’abord merci beaucoup pour cette réponse,
      Selon vous il faudrait donc par exemple isoler l’arrière plan pour pouvoir voir ce qu’il en ressort et ensuite identifier les formes?
      Mais comment identifier des objets en 3 dimensions qui ne présenterons pas forcément la même face ?
      Merci beaucoup
      Cordialement
      Alexandre.

Thibault · 16 février 2019 à 20 h 45 min

Bonsoir, et tout d’abord merci de partager votre travail !
J’essaye d’utiliser votre programme mais j’ai un petit souci à la lecture de l’image (j’ai repris l’image que vous utilisez, celle que vous avez partagé dans les commentaires). La console m’affiche cela :
OpenCV(3.4.1) C:\Miniconda3\conda-bld\opencv-suite_1533128839831\work\modules\highgui\src\window.cpp:356: error: (-215) size.width>0 && size.height>0 in function cv::imshow
… c’est un problème avec l’image que j’utilise?
Sauriez-vous me débloquer ?
Merci beaucoup, bonne soirée

    Florent · 19 février 2019 à 9 h 42 min

    Bonjour Thibault,
    Je ne pense pas que ce soit un problème de lecture d’image, sinon vous auriez une erreur dès la conversion en niveaux de gris ligne 6.
    L’erreur que vous rencontrez semble être au niveau de l’affichage de l’image et notamment un problème avec l’image que vous affichez.
    Essayez d’afficher l’image à différents endroits du code: juste après la lecture, après le traçage des formes (l.18),… N’hésitez pas à afficher les autres images également (gray, thresh) qui vous donneront des idées sur d’éventuels problèmes.

Mat0s · 3 avril 2019 à 15 h 30 min

Bonjour,
J’ai un problème avec la lecture de l’image. J’ai vu plus haut de tester son type et il s’agit d’un NoneType et non numpy.ndarray. J’ai essayé divers choses tel que le noms de l’image directement ‘formes.bmp’ ou bien encore de le chemin ‘/home/pi/Camera/formes.bmp’. J’ai également tenté de directement prendre en photo l’image (mon objectif final) avec la bibliothèque picamera et datetime en tentant la commande : image = cv2.imread(camera.capture(str(datetime.datetime.now())+’.bmp’))
Mais rien à faire, toujours NoneType.
Merci de votre aide

    Florent · 3 avril 2019 à 21 h 26 min

    Bonjour,
    Le fait que vous obteniez un type NoneType montre que l’image n’est pas chargé.
    Vérifier que le nom de votre image est bien correct.
    Pour la caméra, vous pouvez utiliser la fonction cv2.VideoCapture(0).
    Vous trouverez les explications sur cette fonction ici: https://pymotion.com/capturer-video-camera-raspberry

Mat0s · 3 avril 2019 à 18 h 47 min

De ce fait, savez vous comment faire pour prendre directement une photo avec une caméra ? Le but est d’en pouvoir vérifier en permanence si il y a l’une de c’est formes avec l’an camera. Et est-il possible de détecter sur un fond blanc ?
Merci

    Florent · 3 avril 2019 à 21 h 22 min

    Bonjour,
    Pour récupérer les images provenant de la caméra d’une Raspberry ou d’une webcam, je vous invite à lire cet article : https://pymotion.com/capturer-video-camera-raspberry/
    Il n’y a aucun problème pour détecter des formes à partir de la caméra, il faut juste que vous prenez en compte la couleur du fond dans le seuillage effectué dans l’étape 1 de cet article.

Mat0s · 4 avril 2019 à 10 h 10 min

Du coup, est ce qu’il y a une dimension précise pour la photo à avoir pour cette reconnaissance ? J’ai cru comprendre que trop petite ou trop grande posait problème non ?
Merci

    Florent · 4 avril 2019 à 10 h 23 min

    Il n’a pas de dimension minimale ou maximale pour l’image.
    Il est cependant nécessaire d’adapter le seuillage à vos conditions d’acquisitions afin de permettre de détecter correctement les formes.

Mat0s · 4 avril 2019 à 20 h 28 min

Re-bonjour,
J’ai effectué des tests avec ma caméra, mais ils ne sont pas très concluants. En effet, le programme reconnaît tout le temps un triangle, peu importe la forme. Je précise que j’ai réalisé les tests avec l’image des formes ci-dessus. Cependant, je les ai filmés sur écran, est-il gênant pour effectuer les tests et fausse le résultat ? Ou bien est-ce un problème de programme ?

Jaco51 modelisme · 6 mai 2019 à 19 h 29 min

Bonjour,
J’ai refait votre programme, avec la reproduction de votre image. Et pour moi ça fonctionne.
Seul bémol, sur le triangle, il m’affiche « circle triangle ». Toutes les autres formes sont bien définies…
Est-ce mon triangle qui n’est pas net ?

    Florent · 6 mai 2019 à 20 h 40 min

    Bonjour,
    Normalement, l’algorithme devrait trouver un triangle ou un cercle, mais pas les deux.
    Est-il possible que deux formes soient présentes au niveau du triangle ?
    Vous pouvez essayer de déplacer la ligne pour dessiner les contours au niveau de la détection du cercle (ligne 33) afin d’observer si un cercle est dans le triangle.

Gxst · 17 mai 2019 à 16 h 18 min

Bonjours, j’ai utilisé la fonction mais celle-ci change les couleurs de l’image ? Avez-vous une idée d’ou pourrait venir le problème?

    Florent · 18 mai 2019 à 14 h 38 min

    Bonjour,
    De quelle fonction parlez-vous ?
    Le repère colorimétrique de la variable image n’est pas changé au cours du programme.
    Cependant, vous pouvez essayer de le changer avec la fonction cv2.cvtColor(image, cv2.COLOR_BGR2RGB).

      Gxst · 21 mai 2019 à 12 h 40 min

      Merci de votre réponse, j’ai réussi à régler le problème. J’ai une autre question, est-ce que c’est normal que dans certaines images (assez Simone) les formes ne soient pas détectés (J’ai essayé de modifier le seuillage mais ça ne change rien), une idée pour remédier à cela?

        Florent · 22 mai 2019 à 9 h 43 min

        Lorsque vous réalisez le seuillage, est ce que les formes sont facilement identifiables ?
        Essayez de visualiser les contours extraits pour voir s’ils sont nets.
        Enfin, vous pouvez modifier les paramètres de la fonction cv2.approxPolyDP afin d’affiner l’approximation du contour.

          Gxst · 22 mai 2019 à 20 h 01 min

          ça fonctionne assez bien , merci de vos conseils!

Pauli · 21 juin 2019 à 17 h 35 min

Bonjour, je voudrais savoir à quoi sert le passage en niveau de gris ?

    Florent · 21 juin 2019 à 18 h 29 min

    Bonjour,
    Le passage en niveau de gris permet d’extraire facilement les objets du fond lors du seuillage.
    Il est cependant possible de ne pas l’effectuer et de réaliser le seuillage sur les trois composantes.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.