Nous avons vu la semaine dernière comment détecter une forme dans une image. L’image que nous avons traitée comportait différentes formes de différentes couleurs. Il est alors normal de parler aujourd’hui de la reconnaissance de couleurs.

Comme vous le savez certainement, une image couleur est composée de pixels. Chaque pixel a un triplet de valeur représentant, dans la majorité des cas, les valeurs de teinte rouge vert et bleu (RGB). On notera que dans le cas d’OpenCV, ce triplet est inversé (BGR).
Cependant l’un des plus gros défausse ce repère, est que les informations de luminance et de chrominance sont complètement mélangées. Cela va poser un problème lorsque l’on cherchera à résoudre un problème indépendant des variations de luminosité, c’est-à-dire de ne s’intéresser qu’aux informations de chrominance. Dans notre cas, la luminance importe peu. En effet, nous travaillons avec une luminosité constante, c’est une image.

Cependant, l’intérêt de passer du repère RGB au repère HSV est qu’il est plus simple de classifier les couleurs. En effet, en RGB, il est nécessaire d’établir les conditions sur le triplet de valeurs alors que dans le cas du HSV, nous pourrons utiliser uniquement les informations de teinte (H – Hue).

Étape 1 : Définition des conditions sur les couleurs.

Afin de pouvoir définir les conditions sur la teinte, nous allons nous servir du cercle chromatique. En effet, la teinte en HSV est codée en fonction de l’angle sur le cercle chromatique. Ainsi le rouge sera proche de l’angle 0° ou 360°, le vert aux alentours des 120° et le bleu vers 240°.

Reconnaissance de couleurs: cercle chromatique
Cercle chromatique

Nous allons donc créer un dictionnaire comprenant l’association couleur – angle min, angle max. Il est à noter que nous définissons deux zones rouges : rouge1 pour la zone 0°- 20° et rouge2 pour 330°-360°.

colors={"Rouge1":[0,20],
        "Orange":[20,40],
        "Jaune":[40,80],
        "Vert":[80,160],
        "Cyan":[160,200],
        "Bleu":[200,260],
        "Magenta":[260,330],
        "Rouge2":[330,360]
        }

Étape 2 : Création d’une fonction de reconnaissance de couleurs

Maintenant que notre dictionnaire est prêt, nous allons l’utiliser dans une fonction. Cette fonction prendra en entrée l’image d’origine, les contours détectés, ainsi que le dictionnaire.

def detect_couleur(image,cnt,colors):
    image_hsv=cv2.cvtColor(image,cv2.COLOR_BGR2HSV)
    mask = np.zeros(image_hsv.shape[:2], dtype="uint8")
    cv2.drawContours(mask, [cnt], -1, 255, -1)
    mask = cv2.erode(mask, None, iterations=2)

La première étape (ligne 2) consiste à convertir l’image d’origine RGB en image HSV. Nous créons ensuite, ligne 3, une image noire, de taille identique à l’image HSV qui nous servira de masque. Nous dessinons les contours (l.4) de nos formes, tout en spécifiant un chiffre négatif pour l’épaisseur (dernière valeur) permettant ainsi de remplir la forme de la même couleur que le contour (255 – Blanc). Enfin, nous appliquons une érosion ligne 5 sur notre masque afin d’être sûr de ne pas prendre en compte le fond de l’image dans notre calcul de couleurs.

Detection de couleurs: différentes formes
Les différents masques obtenus

Il ne nous reste plus qu’à retrouver les couleurs:

def detect_couleur(image,cnt,colors):
    image_hsv=cv2.cvtColor(image,cv2.COLOR_BGR2HSV)
    mask = np.zeros(image_hsv.shape[:2], dtype="uint8")
    cv2.drawContours(mask, [cnt], -1, 255, -1)
    mask = cv2.erode(mask, None, iterations=2)
    mean = cv2.mean(image_hsv, mask=mask)
    if image.dtype=='uint8':
        for color in colors:
            if 2*mean[0] in range(colors[color][0],colors[color][1]):
                return(color)
    else:
        for color in colors:
            if mean[0] in range(colors[color][0],colors[color][1]):
                return(color)

Nous calculons ligne 6, la moyenne des valeurs des pixels de l’image HSV où les pixels de notre masque sont à 255, c’est-à-dire dans la forme étudiée. Enfin nous comparons aux lignes 9 ou 13, la valeur moyenne en H avec tous les seuils du dictionnaire.

Pourquoi deux cas différents en fonction du nombre de bits sur lequel sont codés l’image ? Simplement car, dans le cas  où un pixel est codé sur 8 bits, OpenCV attribue une valeur entre 0 et 180 pour H et non entre 0 et 360. En effet, il est difficilement possible de coder 360 sur 8 bits. C’est pourquoi nous multiplions la moyenne par 2 dans le premier cas afin d’avoir des valeurs comprises entre 0 et 360.

Si la moyenne est comprise entre les bornes d’une couleur, nous renvoyons le nom de cette couleur lignes 10 ou 14.

Étape 3: Amélioration de la reconnaissance de formes

Reprenons le programme de la semaine dernière et modifions-le avec ce que nous avons vu aujourd’hui. Commencez par ajouter le dictionnaire et la fonction après les lignes d’import de bibliothèques. Ensuite, on appelle la fonction « detect_couleur » pour chaque contour détecté.

for cnt in contours:
    perimetre=cv2.arcLength(cnt,True)
    approx = cv2.approxPolyDP(cnt,0.01*perimetre,True)
    color = detect_couleur(image,cnt,colors)
    M = cv2.moments(cnt)

La dernière étape consistera à la modification du texte que l’on inscrit sur l’image:

    cv2.putText(image, shape + " " + color, (cX, cY), cv2.FONT_HERSHEY_SIMPLEX,0.5, (125, 125, 125), 2)

Nous pouvons alors admirer le résultat 🙂

Reconnaissance de formes et de couleurs

Nous avons réussi, nous avons une méthode de reconnaissance de formes et de couleurs ! Cette solution est simpliste et fonctionnelle uniquement dans le cas d’images virtuelles. Cependant, il ne sera pas difficile d’utiliser ces méthodes dans un environnement réel.

Le code complet:

import numpy as np
import cv2

colors={"Rouge1":[0,20],
        "Orange":[20,40],
        "Jaune":[40,80],
        "Vert":[80,160],
        "Cyan":[160,200],
        "Bleu":[200,260],
        "Magenta":[260,330],
        "Rouge2":[330,360]
        }

def detect_couleur(image,cnt,colors):
    image_hsv=cv2.cvtColor(image,cv2.COLOR_BGR2HSV)
    mask = np.zeros(image_hsv.shape[:2], dtype="uint8")
    cv2.drawContours(mask, [cnt], -1, 255, -1)
    mask = cv2.erode(mask, None, iterations=2)
    mean = cv2.mean(image_hsv, mask=mask)
    if image.dtype=='uint8':
        for color in colors:
            if 2*mean[0] in range(colors[color][0],colors[color][1]):
                return(color)
    else:
        for color in colors:
            if mean[0] in range(colors[color][0],colors[color][1]):
                return(color)


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)
    color = detect_couleur(image,cnt,colors)
    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= "cercle"
    cv2.putText(image, shape + " " + color, (cX, cY), cv2.FONT_HERSHEY_SIMPLEX,0.5, (125, 125, 125), 2)
cv2.imshow('image',image)
cv2.waitKey(5000)
cv2.imwrite('result.jpg',image)
cv2.destroyAllWindows()

19 commentaires

Antoine · 3 avril 2018 à 22 h 36 min

Serait-il possible de mettre le code final en entier car je n’arrive pas à l’intégrer dans le code de détection de formes.
Cordialement

    Florent · 3 avril 2018 à 22 h 40 min

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

Antoine · 17 mai 2018 à 16 h 24 min

Bonjour, je rencontre cette erreur :

cv2.putText(image, shape + color, (cX , cY), cv2.FONT_HERSHEY_SIMPLEX,0.5, (125, 125, 125), 2)
TypeError: cannot concatenate ‘str’ and ‘NoneType’ objects

Comment puis-je la régler?

Cordialement

    Florent · 17 mai 2018 à 16 h 49 min

    Bonjour,

    L’erreur renvoyé montre un problème dans la concatenation d’une chaine de caractères avec une variable vide.
    Il semble donc que la variable color soit mal définie.

      Antoine · 23 mai 2018 à 11 h 15 min

      Bonjour, merci de votre réponse.
      Pourtant, j’utilise le même code que vous:

      ////////////////////////////////////////////////////////////////////////////////

      import numpy as np
      import cv2

      colors={« Rouge1 »:[0,20],
      « Orange »:[20,40],
      « Jaune »:[40,80],
      « Vert »:[80,160],
      « Cyan »:[160,200],
      « Bleu »:[200,260],
      « Magenta »:[260,330],
      « Rouge2″:[330,360]
      }

      def detect_couleur(image,cnt,colors):
      image_hsv=cv2.cvtColor(image,cv2.COLOR_BGR2HSV)
      mask = np.zeros(image_hsv.shape[:2], dtype= »uint8 »)
      cv2.drawContours(mask, [cnt], -1, 255, -1)
      mask = cv2.erode(mask, None, iterations=2)
      mean = cv2.mean(image_hsv, mask=mask)
      if image.dtype==’uint8′:
      for color in colors:
      if 2*mean[0] in range(colors[color][0],colors[color][1]):
      return(color)
      else:
      for color in colors:
      if mean[0] in range(colors[color][0],colors[color][1]):
      return(color)

      image = cv2.imread(‘/home/pi/toubab/image.jpg’)
      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)
      color = detect_couleur(image,cnt,colors)
      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= "cercle"
      cv2.putText(image, shape + " " + color, (cX, cY), cv2.FONT_HERSHEY_SIMPLEX,0.5, (125, 125, 125), 2)
      cv2.imshow('image',image)
      cv2.waitKey(5000)
      cv2.imwrite('result.jpg',image)
      cv2.destroyAllWindows()

      ////////////////////////////////////////////////////////////////////////////////
      celà viendrait peut-être de la fonction?

Antoine · 23 mai 2018 à 11 h 55 min

Je viens de remarquer que la structure a été cassée en copiant le code. Du coup je vous envoie le code via ce lien google doc:
https://docs.google.com/document/d/1VDiIHmYSyg1DN7EtmXerWlDxs_b8vIjARR4_S2Yiw50/edit?usp=sharing

    Florent · 23 mai 2018 à 16 h 01 min

    L’erreur provient bien de la fonction detect-couleur, au niveau de la comparaison de la moyenne.
    La moyenne est un float alors que la fonction range renvoie des entiers:

    7.5 in range(0,10) -> False | 7 in range (0,10) -> True

    Vu que j’utilise une image générée numériquement, les valeurs des moyennes sont des entiers.
    Cependant cela ne marche effectivement pas avec des photos ou des images présentant du bruit.

    Pour palier ce problème, il faut soit convertir la moyenne en entier:

    if int(2*mean[0]) in range(colors[color][0],colors[color][1]):

    ou bien utiliser des inégalités:

    if colors[color][0] < 2*mean[0] < colors[color][1]:

    Encore merci pour le signalement de l'erreur

Mat0s · 7 avril 2019 à 14 h 27 min

Bonjour, j’essaye de faire fonctionner cette reconnaissance avec une caméra raspberry. La détéction de couleur fonctionne nickel, mais au niveau de la forme, c’est toujours rectangle qui est détecté. Apparemment c’est parce que c’est tout le cadre de l’image qui est pris. En effet, le contour vert pour désigner la forme détectée prend tout le tour de l’image. D’où vient le porblème ? Voici le code utilisé :

[code/]
import numpy as np
import cv2
import time
import os
import picamera

colors={« Rouge1 »:[0,20],
« Orange »:[20,40],
« Jaune »:[40,80],
« Vert »:[80,160],
« Cyan »:[160,200],
« Bleu »:[200,260],
« Magenta »:[260,330],
« Rouge2″:[330,360]
}

def detect_couleur(image,cnt,colors):
image_hsv=cv2.cvtColor(image,cv2.COLOR_BGR2HSV)
mask = np.zeros(image_hsv.shape[:2], dtype= »uint8 »)
cv2.drawContours(mask, [cnt], -1, 255, -1)
mask = cv2.erode(mask, None, iterations=2)
mean = cv2.mean(image_hsv, mask=mask)
if image.dtype==’uint8′:
for color in colors:
if int(2*mean[0]) in range(colors[color][0],colors[color][1]):
return(color)
else:
for color in colors:
if mean[0] in range(colors[color][0],colors[color][1]):
return(color)

# Supression de l’image précédente dans le fichier
file = os.listdir(‘/home/pi/Camera/PhotoReconnaissanceColor’)
os.remove(‘/home/pi/Camera/PhotoReconnaissanceColor’+’/’+file[0])
camera = picamera.PiCamera()
camera.start_preview(fullscreen = False, window=(50,50,640,480))
time.sleep(5)
# Photo prise
camera.capture(‘/home/pi/Camera/PhotoReconnaissanceColor/image.jpg’)
camera.stop_preview()

image = cv2.imread(‘/home/pi/Camera/PhotoReconnaissanceColor/image.jpg’)
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)
color = detect_couleur(image,cnt,colors)
M = cv2.moments(cnt)
if M[« m00 »] != 0:
cX = int(M[« m10 »] / M[« m00 »])
cY = int(M[« m01 »] / M[« m00 »])
else:
cX, cY = 0, 0
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 + "" + color, (cX, cY), cv2.FONT_HERSHEY_SIMPLEX,0.5, (125, 125, 125), 2)
cv2.imshow('image',image)
cv2.waitKey(5000)
cv2.imwrite('result.jpg',image)
cv2.destroyAllWindows()
[/code]

Merci.

    Florent · 9 avril 2019 à 8 h 55 min

    Bonjour,
    Pourriez-vous m’envoyer une ou plusieurs images capturées avec la caméra ?
    Merci d’avance

Alex · 12 avril 2019 à 15 h 18 min

Bonjour, comment prendre en compte seulement une lumière blanche, et non colorée ?

    Florent · 12 avril 2019 à 15 h 32 min

    Bonjour Alex,
    Pour détecter une source lumineuse, je vous conseille de réaliser un seuillage dans le domaine colorimétrique HSV ou LAB.
    L’avantage de ces domaines est que l’information de luminosité est représentée par dans une composante (V pour HSV et L pour LAB).
    Un seuillage sur la composante V ou L permettra d’identifier les sources lumineuses.

Jaco51 modelisme · 6 mai 2019 à 19 h 41 min

Bonjour,
Merci pour ce bon exemple. J’ai copié le programme et chargé une image similaire à la votre, et ça fonctionne. Un petit bémol : pour l’affichage des formes sur l’image ; pour le triangle il m’affiche « cercle triangle », alors que c’est ok pour toutes les autres formes ! Une idée ?

GX · 31 mai 2019 à 14 h 49 min

Bonjour, je ne comprend pas très bien ce que l’on réalise des lignes 7 à 13, pourriez-vous m’expliquer s’il vous plait?

    Florent · 3 juin 2019 à 9 h 10 min

    Bonjour,
    Nous cherchons dans ces lignes à trouver dans quelle plage de valeur se trouve la valeur moyenne des pixels d’une forme.

    La ligne 8/12 sert à parcourir chaque valeur du dictionnaire colors.
    Dans les lignes 9/13, nous regardons si la valeur moyenne (mean) est située (in) entre les bornes d’une couleur (range(min,max)). Si c’est le cas, nous retournons la couleur concernée. Sinon, nous passons à la couleur suivante.

    Nous avons deux cas d’application : les lignes 7 à 10 sont utilisées si l’image est de type ‘uint8’, les lignes 11 à 14 dans les autres cas.
    Cette séparation vient du fait que les valeurs de la composante H sont codée, dans le cas d’une image ‘uint8’ sur 180 valeur et non 360. Il est alors nécessaire de multiplier par deux pour avoir des valeurs correspondantes aux valeurs définies dans le dictionnaire.

      GX · 3 juin 2019 à 20 h 24 min

      Ok merci de l’explication, je comprend mieux. Est-il alors possible d’ajouter le blanc dans la bibliothèque de couleur ?

        Florent · 4 juin 2019 à 9 h 09 min

        Pour pouvoir détecter du blanc, il faut chercher des pixels avec une saturation faible et une valeur/luminance forte.
        Représentation conique du repère HSV
        Il faut donc prendre toutes les composantes en comptes (H, S et V) pour intégrer le blanc au dictionnaire.

Jamel Jean Jacque · 4 juillet 2019 à 19 h 51 min

Bonjour, je voulais vous demander quel différence y a t-il entre une image et l’écran en temps réel (une forme de couleur qui bouge), que doit on changer ou à quoi s’intéresser ? Merci de votre réponse

    Jamel Jean Jacque · 4 juillet 2019 à 19 h 56 min

    Est-il possible aussi de capter l’écran en définissant toutes les x secondes que l’écran est une image ? (Donc toutes les 2 secondes, le programme ne prend en compte que la forme présente et pas les déplacements suivant. Avec des captures écran ? Mais cela rajouterai pas mal de chose non ?)

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.