À quoi ça sert ?
Sur les parcours de nos compétitons ROBOCUP à identifier des zones de couleurs et les lignes noires !
Pourquoi ne pas inventer un robot qui va réagir à des feux de circulation ?
Préparer ton atelier
- Utilise MU Python sur ton poste de travail.

- Installe si besoin le package opencv-python

pip install opencv-python numpy
- Crée un dossier avec les images suivantes pour test (par exemple
image08.png) qui contient des zones rouges.






Les concepts à connaître avant de commencer
Un pixel, c’est quoi ?
Une image, c’est une grille de minuscules carrés de couleur : les pixels. Comme un jeu de bataille navale, chaque pixel a une ligne (position verticale) et une colonne (position horizontale).
BGR : la recette de la couleur
Chez OpenCV, chaque pixel est mélangé comme un pot de peinture avec 3 doses : Bleu, Vert (Green), Rouge — attention, c’est BGR chez OpenCV, pas RGB ! Chaque dose va de 0 (rien) à 255 (le maximum).
- Rouge pur =
(0, 0, 255) - Blanc =
(255, 255, 255) - Gris moyen =
(127, 127, 127)
HSV : la couleur vue par un peintre
Le BGR, c’est la recette chimique. Le HSV, c’est comme un humain décrit une couleur :
- H = Hue (la teinte : « c’est du rouge ? du vert ? »)
- S = Saturation (la force : « flashy ou délavé ? »)
- V = Value (la luminosité : « clair ou sombre ? »)
Pourquoi HSV ? Parce qu’un « rouge » peut être clair, sombre, vif ou pâle. Avec HSV, on peut dire « toutes les teintes entre 0 et 10 » et ça attrape TOUS les rouges, quel que soit leur éclairage !
Piège OpenCV : la teinte H va de 0 à 179 (pas 360°), et S et V de 0 à 255.
Le cercle des teintes (rouges coupés en deux !)
La teinte est un cercle : le rouge est à cheval sur le début (H≈0) et la fin (H≈179). C’est pour ça qu’on cherche le rouge avec deux plages : 0-10 et 170-179.
Rouge: 0→10 et 170→179
Jaune: 20→35
Vert: 40→85
Étape 0 : La version « brute de décoffrage » (tout dans un seul bloc)
Le premier programme fait tout d’un coup : il charge une image, la réduit, parcourt tous les pixels un par un, encadre la zone rouge et dessine une flèche. Juste pour comprendre comment fonctionne une image !
Chargement d’une image avec opencv :
import cv2
# Chargement de l'image
img = cv2.imread("C:/Users/patri/Pictures/image01.png",cv2.IMREAD_COLOR)
# Réduire la taille de l'image
img = cv2.resize(img,(0,0),fx=0.3,fy=0.3)
# Eventuellement touner l'image
#img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
cv2.imshow("image",img)
# attente de l'appui d'une touche
cv2.waitKey(0)
# destruction des fenêtres
cv2.destroyAllWindows()
Cloner l’image initial pour visualiser une image cible :
import cv2
# Chargement de l'image
img = cv2.imread("C:/Users/patri/Pictures/image01.png",cv2.IMREAD_COLOR)
# Réduire la taille de l'image
img = cv2.resize(img,(0,0),fx=0.3,fy=0.3)
# Eventuellement touner l'image
#img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
# Copier l'image dans une image cible pour voir le résultat
img_cible =img.copy()
# Dimansion de l'image
print(img.shape)
# affichage de l'image initiale et du résultat de la couleur rouge
cv2.imshow("image",img)
cv2.imshow("image cible",img_cible)
# attente de l'appui d'une touche
cv2.waitKey(0)
# destruction des fenêtres
cv2.destroyAllWindows()
Détection d’une zone rouge et contour pour visualiser :
import cv2
# Chargement de l'image
img = cv2.imread("C:/Users/patri/Pictures/image01.png",cv2.IMREAD_COLOR)
# Réduire la taille de l'image
img = cv2.resize(img,(0,0),fx=0.3,fy=0.3)
# Eventuellement touner l'image
#img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
# Copier l'image dans une image cible pour voir le résultat
img_cible =img.copy()
# Dimansion de l'image
print(img.shape)
lignes, colonnes, colors = img.shape
l1=0
c1=0
l2=0
c2=0
# seuil pour retrouver la couleur
seuil = 80
# analyse de l'image pour retrouver la couleur rouge
for ligne in range(lignes):
for colonne in range(colonnes) :
if img[ligne][colonne][2] > img[ligne][colonne][0]+seuil and img[ligne][colonne][2] > img[ligne][colonne][1]+seuil:
if l1 == 0 :
l1 = ligne
c1 = colonne
l2 = ligne
c2 = colonne
# dessin d'un rectangle autour de la couleur rouge
cv2.rectangle(img_cible,(c1,l1),(c2,l2),(0,255,0),4)
# affichage de l'image initiale et du résultat de la couleur rouge
cv2.imshow("image",img)
cv2.imshow("image cible",img_cible)
# attente de l'appui d'une touche
cv2.waitKey(0)
# destruction des fenêtres
cv2.destroyAllWindows()
Elimination des zones trop petites :
import cv2
# Chargement de l'image
img = cv2.imread("C:/Users/patri/Pictures/image01.png",cv2.IMREAD_COLOR)
# Réduire la taille de l'image
img = cv2.resize(img,(0,0),fx=0.3,fy=0.3)
# Eventuellement touner l'image
#img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
# Copier l'image dans une image cible pour voir le résultat
img_cible =img.copy()
# Dimansion de l'image
print(img.shape)
lignes, colonnes, colors = img.shape
l1=0
c1=0
l2=0
c2=0
# seuil pour retrouver la couleur
seuil = 80
# analyse de l'image pour retrouver la couleur rouge
for ligne in range(lignes):
for colonne in range(colonnes) :
if img[ligne][colonne][2] > img[ligne][colonne][0]+seuil and img[ligne][colonne][2] > img[ligne][colonne][1]+seuil:
if l1 == 0 :
l1 = ligne
c1 = colonne
l2 = ligne
c2 = colonne
else:
# élimination des petites zones rouges
if l1 > 0 and ligne == l2 + 1 and colonne == c2:
if (l2-l1) * (c2-c1) < 2000 :
l1=0
# dessin d'un rectangle autour de la couleur rouge
cv2.rectangle(img_cible,(c1,l1),(c2,l2),(0,255,0),4)
# affichage de l'image initiale et du résultat de la couleur rouge
cv2.imshow("image",img)
cv2.imshow("image cible",img_cible)
# attente de l'appui d'une touche
cv2.waitKey(0)
# destruction des fenêtres
cv2.destroyAllWindows()
Affichage d’une flèche pour indiquer la direction vers la zone :
import cv2
# Chargement de l'image
img = cv2.imread("C:/Users/patri/Pictures/image01.png",cv2.IMREAD_COLOR)
# Réduire la taille de l'image
img = cv2.resize(img,(0,0),fx=0.3,fy=0.3)
# Eventuellement touner l'image
#img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
# Copier l'image dans une image cible pour voir le résultat
img_cible =img.copy()
# Dimansion de l'image
print(img.shape)
lignes, colonnes, colors = img.shape
l1=0
c1=0
l2=0
c2=0
# seuil pour retrouver la couleur
seuil = 80
# analyse de l'image pour retrouver la couleur rouge
for ligne in range(lignes):
for colonne in range(colonnes) :
if img[ligne][colonne][2] > img[ligne][colonne][0]+seuil and img[ligne][colonne][2] > img[ligne][colonne][1]+seuil:
if l1 == 0 :
l1 = ligne
c1 = colonne
l2 = ligne
c2 = colonne
else:
# élimination des petites zones rouges
if l1 > 0 and ligne == l2 + 1 and colonne == c2:
if (l2-l1) * (c2-c1) < 2000 :
l1=0
# dessin d'un rectangle autour de la couleur rouge
cv2.rectangle(img_cible,(c1,l1),(c2,l2),(0,255,0),4)
# affichage d'une flêche vers le zone de couleur rouge
m = (c1 + c2) // 2
if m < colonnes//2 :
m = (colonnes // 2) - m
cv2.arrowedLine(img_cible, (colonnes // 2,lignes-10), ((colonnes // 2)-m,lignes-10), (0,255,0),4)
else:
m = m -(colonnes // 2)
cv2.arrowedLine(img_cible, (colonnes // 2,lignes-10), ((colonnes // 2)+m,lignes-10), (0,255,0),4)
# affichage de l'image initiale et du résultat de la couleur rouge
cv2.imshow("image",img)
cv2.imshow("image cible",img_cible)
# attente de l'appui d'une touche
cv2.waitKey(0)
# destruction des fenêtres
cv2.destroyAllWindows()
Ce que fait le code
img = cv2.imread(".../image08.png", cv2.IMREAD_COLOR)
img = cv2.resize(img, (0,0), fx=0.7, fy=0.7)
imreadcharge l’image en mémoire.resizela réduit à 70 % (fx=0.7, fy=0.7) : moins de pixels = programme plus rapide !
lignes, colonnes, colors = img.shape
img.shape donne les dimensions : combien de lignes, de colonnes, et combien de couches de couleur (3 pour BGR).
La chasse au rouge pixel par pixel
for ligne in range(lignes):
for colonne in range(colonnes):
if img[ligne][colonne][2] > img[ligne][colonne][0] + seuil \
and img[ligne][colonne][2] > img[ligne][colonne][1] + seuil:
Pour chaque pixel, on compare les 3 doses BGR : si le canal Rouge (index [2]) dépasse largement le Bleu ([0]) et le Vert ([1]) (au moins de seuil = 80), alors c’est un pixel rouge !
On mémorise les coordonnées extrêmes de la zone : l1, c1 (le premier pixel rouge trouvé = coin en haut à gauche) et l2, c2 (le dernier = coin en bas à droite). On peut alors tracer un rectangle avec cv2.rectangle, et une flèche (cv2.arrowedLine) qui indique si la zone est à gauche ou à droite du milieu de l’image.
Les défauts de cette version
- Deux boucles
forimbriquées sur des milliers de pixels = très lent. - La comparaison « R > B + 80 » est fragile : ça se dérègle dès que la lumière change.
- Le rectangle englobe tous les pixels rouges : si tu as 2 zones rouges séparées, il les relie par un seul grand cadre.
- Tout est mélangé : impossible de réutiliser une partie du code ailleurs.
L’objectif des étapes suivantes : réécrire ce programme proprement, comme un pro ! On va ranger le code dans des fonctions — des petites machines, chacune avec un seul rôle.
Étape 1 : Charger et afficher l’image (le squelette du programme)
Ici on construit la structure du programme : les paramètres en haut (comme le sommaire d’une recette), et une fonction load_image() qui a un seul job : charger l’image.
import cv2
import numpy as np
# ============================================================
# PARAMÈTRES (constantes)
# ============================================================
IMAGE_PATH = "C:/Users/patri/Pictures/image08.png"
# IMAGE_PATH = "C:/Users/patri/Pictures/Screenshots/163159.png"
RESIZE_FACTOR = 0.5 # Facteur de redimensionnement de l'image
ROTATE_IMAGE = False # Mettre True pour tourner l'image de 90° (sens horaire)
def load_image(path, resize_factor, rotate):
"""Charge l'image, la redimensionne et la tourne si nécessaire."""
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
raise FileNotFoundError(f"Impossible de charger l'image : {path}")
image = cv2.resize(image, (0, 0), fx=resize_factor, fy=resize_factor)
if rotate:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
print("Dimension : ",image.shape)
return image
def main():
image = load_image(IMAGE_PATH, RESIZE_FACTOR, ROTATE_IMAGE)
# Affichage
cv2.imshow("image", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
Les paramètres au sommet
IMAGE_PATH = "C:/Users/patri/Pictures/image08.png"
RESIZE_FACTOR = 0.5
ROTATE_IMAGE = False
Tout ce qui peut changer est regroupé en haut du fichier, en MAJUSCULES. Tu veux tester une autre image ? Tu changes une seule ligne, et tu ne risques pas de casser le reste.
La fonction load_image()
def load_image(path, resize_factor, rotate):
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
raise FileNotFoundError(f"Impossible de charger l'image : {path}")
image = cv2.resize(image, (0, 0), fx=resize_factor, fy=resize_factor)
if rotate:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
return image
Une fonction, c’est comme une machine dans une usine : elle reçoit des matières premières (le chemin, le facteur, la rotation) et renvoie un produit fini (l’image prête à analyser).
L’astuce du pro : if image is None attrape l’erreur classique « faute de frappe dans le chemin » : au lieu d’un crash mystérieux plus loin, on affiche un message clair.
Le chef d’orchestre : main()
def main():
image = load_image(IMAGE_PATH, RESIZE_FACTOR, ROTATE_IMAGE)
cv2.imshow("image", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
main() ne fait presque rien lui-même : il appelle les fonctions dans le bon ordre. Et if __name__ == "__main__": signifie « c’est ce fichier qui est lancé » — c’est le drapeau de départ du programme !
Test : tu dois voir ta photo apparaître dans une fenêtre. Appuie sur une touche pour fermer (waitKey(0) attend un appui, destroyAllWindows ferme les fenêtres).
Étape 2 : Le masque binaire — » rouge ou pas rouge ? «
On passe à la recherche moderne : plus de double boucle ! On convertit l’image en HSV, puis on fabrique un masque : une image en noir et blanc où les pixels de la couleur cherchée sont blancs et tout le reste noir.
import cv2
import numpy as np
# ============================================================
# PARAMÈTRES (constantes)
# ============================================================
IMAGE_PATH = "C:/Users/patri/Pictures/image08.png"
# IMAGE_PATH = "C:/Users/patri/Pictures/Screenshots/163159.png"
RESIZE_FACTOR = 0.5 # Facteur de redimensionnement de l'image
ROTATE_IMAGE = False # Mettre True pour tourner l'image de 90° (sens horaire)
SHOW_MASKS = True # Afficher une fenêtre de masque par couleur analysée
# --- Choix des couleurs à analyser : "red", "yellow", "green" ---
# Exemples : ["red"] | ["red", "green"] | ["red", "yellow", "green"]
COLORS_TO_ANALYZE = ["red"]
MORPH_KERNEL_SIZE = 5 # Taille du noyau pour nettoyer les masques
# --- Plages HSV par couleur (OpenCV : H de 0 à 179, S et V de 0 à 255) ---
# Chaque couleur a une liste de plages (min, max) car le rouge est à cheval
# sur le début et la fin du cercle des teintes.
SATURATION_MIN = 100 # Saturation minimale (exclut les couleurs délavées)
VALUE_MIN = 70 # Luminosité minimale (exclut les zones trop sombres)
HSV_RANGES = {
"red": [
((0, SATURATION_MIN, VALUE_MIN), (10, 255, 255)), # Rouge proche de 0°
((170, SATURATION_MIN, VALUE_MIN), (179, 255, 255)), # Rouge proche de 180°
]
}
def load_image(path, resize_factor, rotate):
"""Charge l'image, la redimensionne et la tourne si nécessaire."""
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
raise FileNotFoundError(f"Impossible de charger l'image : {path}")
image = cv2.resize(image, (0, 0), fx=resize_factor, fy=resize_factor)
if rotate:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
print("Dimension : ",image.shape)
return image
def build_color_mask(hsv, color_name, kernel_size):
"""Crée le masque binaire d'une couleur à partir de l'image HSV."""
mask = np.zeros(hsv.shape[:2], np.uint8)
# On fusionne toutes les plages de la couleur (ex : 2 plages pour le rouge)
for lower, upper in HSV_RANGES[color_name]:
mask |= cv2.inRange(hsv, np.array(lower), np.array(upper))
kernel = np.ones((kernel_size, kernel_size), np.uint8)
# Ouverture : supprime les petits pixels isolés (bruit)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# Fermeture : comble les petits trous à l'intérieur des zones
return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
def analyze_colors(image, colors):
"""Analyse chaque couleur demandée. Retourne (résultats, masques)."""
# La conversion HSV n'est faite qu'une seule fois pour toutes les couleurs
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
masks = {}
for color_name in colors:
masks[color_name] = build_color_mask(hsv, color_name, MORPH_KERNEL_SIZE)
return masks
def main():
image = load_image(IMAGE_PATH, RESIZE_FACTOR, ROTATE_IMAGE)
# Analyse des couleurs choisies
masks = analyze_colors(image, COLORS_TO_ANALYZE)
# Affichage
cv2.imshow("image", image)
if SHOW_MASKS:
for color_name, mask in masks.items():
cv2.imshow(f"mask {color_name}", mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
Fabriquer le masque avec inRange
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, np.array(lower), np.array(upper))
cv2.inRange est un détecteur de métaux géant : il passe sur chaque pixel et demande « ta teinte est-elle entre 0 et 10, ta saturation au-dessus de 100, ta luminosité au-dessus de 70 ? » Si oui → pixel blanc (255), sinon → noir (0). Et il fait ça pour toute l’image d’un coup, des centaines de fois plus vite que nos boucles !
Le rouge, en deux plages
HSV_RANGES = {
"red": [
((0, 100, 70), (10, 255, 255)), # rouge proche de H=0
((170, 100, 70), (179, 255, 255)), # rouge proche de H=179
],
}
Souviens-toi : le rouge est à cheval sur le début et la fin du cercle des teintes, donc il faut deux plages. Le symbole |= fusionne les deux résultats : « blanc si trouvé dans l’une OU l’autre plage ».
Décryptage des valeurs HSV
Chaque couleur a deux bornes : (min) et (max). Chaque borne contient 3 nombres : H, S, V. cv2.inRange garde tous les pixels dont les 3 valeurs sont entre la borne min et la borne max.
Les 3 nombres
| Lettre | Nom | Fourchette OpenCV | Ce qu’elle mesure |
|---|---|---|---|
| H | Hue (teinte) | 0 à 179 ⚠️ | Laquelle couleur c’est : rouge, jaune, vert… |
| S | Saturation | 0 à 255 | La force : 0 = délavé/gris, 255 = très vif |
| V | Value (luminosité) | 0 à 255 | La lumière : 0 = noir, 255 = très clair |
Piège classique : dans le monde réel la teinte va de 0 à 360°, mais OpenCV la divise par 2 → 0 à 179.
Lecture ligne par ligne
pythonCopier
((0, 100, 70), (10, 255, 255))
Borne min (0, 100, 70) : teinte ≥ 0, saturation ≥ 100 (on ignore les couleurs délavées/grises), luminosité ≥ 70 (on ignore les zones presque noires).
Borne max (10, 255, 255) : teinte ≤ 10, saturation et luminosité au maximum.
Traduction : « toutes les teintes de 0 à 10, à condition d’être assez vives et assez éclairées ».
((170, 100, 70), (179, 255, 255))
Même chose, mais pour les teintes de 170 à 179.
Pourquoi le rouge a besoin de 2 plages ?
La teinte est un cercle (comme l’arc-en-ciel refermé sur lui-même). Le rouge pur est à H = 0… mais aussi juste à côté, à H = 179 (la fin du cercle touche le début !). Un rouge légèrement orangé ou rosé peut donc tomber d’un côté ou de l’autre. Solution : deux plages, fusionnées avec |= (« OU »).
0 rouge
179 10
168 20 jaune
magenta 35
90 cyan 85 40
vert
À retenir
S ≥ 100: élimine le gris et les couleurs « pastel » — pratique pour ignorer le fond d’une photo.V ≥ 70: élimine les ombres et le noir.- S et V max à
255= on accepte toutes les intensités au-dessus du minimum.
Le nettoyage : la morphologie
Un vrai masque a du bruit : des pixels blancs isolés (grain de poussière) et des trous dans les zones. On nettoie avec un petit carré (le noyau, kernel) qu’on promène sur le masque :
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # OUVERTURE : gomme les points isolés
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # FERMETURE : bouche les petits trous
- Ouverture = on enlève les tâches plus petites que le noyau (érosion puis dilatation).
- Fermeture = on comble les fissures plus petites que le noyau (dilatation puis érosion).
Test : une fenêtre mask red s’affiche : ta zone rouge apparaît en blanc sur fond noir. Si rien n’est blanc, baisse SATURATION_MIN ou VALUE_MIN !
Étape 3 : Plusieurs couleurs à la fois (rouge, jaune, vert)
On agrandit simplement le dictionnaire HSV_RANGES — pas besoin de toucher au reste du code, c’est la magie de la structure en fonctions !
import cv2
import numpy as np
# ============================================================
# PARAMÈTRES (constantes)
# ============================================================
IMAGE_PATH = "C:/Users/patri/Pictures/image08.png"
# IMAGE_PATH = "C:/Users/patri/Pictures/Screenshots/163159.png"
RESIZE_FACTOR = 0.5 # Facteur de redimensionnement de l'image
ROTATE_IMAGE = False # Mettre True pour tourner l'image de 90° (sens horaire)
SHOW_MASKS = True # Afficher une fenêtre de masque par couleur analysée
# --- Choix des couleurs à analyser : "red", "yellow", "green" ---
# Exemples : ["red"] | ["red", "green"] | ["red", "yellow", "green"]
COLORS_TO_ANALYZE = ["red", "yellow", "green"]
MORPH_KERNEL_SIZE = 5 # Taille du noyau pour nettoyer les masques
# --- Plages HSV par couleur (OpenCV : H de 0 à 179, S et V de 0 à 255) ---
# Chaque couleur a une liste de plages (min, max) car le rouge est à cheval
# sur le début et la fin du cercle des teintes.
SATURATION_MIN = 100 # Saturation minimale (exclut les couleurs délavées)
VALUE_MIN = 70 # Luminosité minimale (exclut les zones trop sombres)
HSV_RANGES = {
"red": [
((0, SATURATION_MIN, VALUE_MIN), (10, 255, 255)), # Rouge proche de 0°
((170, SATURATION_MIN, VALUE_MIN), (179, 255, 255)), # Rouge proche de 180°
],
"yellow": [
((20, SATURATION_MIN, VALUE_MIN), (35, 255, 255)),
],
"green": [
((40, SATURATION_MIN, VALUE_MIN), (85, 255, 255)),
],
}
def load_image(path, resize_factor, rotate):
"""Charge l'image, la redimensionne et la tourne si nécessaire."""
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
raise FileNotFoundError(f"Impossible de charger l'image : {path}")
image = cv2.resize(image, (0, 0), fx=resize_factor, fy=resize_factor)
if rotate:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
print("Dimension : ",image.shape)
return image
def build_color_mask(hsv, color_name, kernel_size):
"""Crée le masque binaire d'une couleur à partir de l'image HSV."""
mask = np.zeros(hsv.shape[:2], np.uint8)
# On fusionne toutes les plages de la couleur (ex : 2 plages pour le rouge)
for lower, upper in HSV_RANGES[color_name]:
mask |= cv2.inRange(hsv, np.array(lower), np.array(upper))
kernel = np.ones((kernel_size, kernel_size), np.uint8)
# Ouverture : supprime les petits pixels isolés (bruit)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# Fermeture : comble les petits trous à l'intérieur des zones
return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
def analyze_colors(image, colors):
"""Analyse chaque couleur demandée. Retourne (résultats, masques)."""
# La conversion HSV n'est faite qu'une seule fois pour toutes les couleurs
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
masks = {}
for color_name in colors:
masks[color_name] = build_color_mask(hsv, color_name, MORPH_KERNEL_SIZE)
return masks
def main():
image = load_image(IMAGE_PATH, RESIZE_FACTOR, ROTATE_IMAGE)
# Analyse des couleurs choisies
masks = analyze_colors(image, COLORS_TO_ANALYZE)
# Affichage
cv2.imshow("image", image)
if SHOW_MASKS:
for color_name, mask in masks.items():
cv2.imshow(f"mask {color_name}", mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
HSV_RANGES = {
"red": [((0, 100, 70), (10, 255, 255)), ((170, 100, 70), (179, 255, 255))],
"yellow": [((20, 100, 70), (35, 255, 255))],
"green": [((40, 100, 70), (85, 255, 255))],
}
COLORS_TO_ANALYZE = ["red", "yellow", "green"]
La fonction analyze_colors() fait une boucle sur la liste des couleurs et fabrique un masque pour chacune. La conversion HSV n’est faite qu’une seule fois et servie à toutes les couleurs : pas de travail inutile !
Métaphore : l’image HSV est le buffet, chaque couleur prend ce qui l’intéresse dans les plats, sans recuire le buffet à chaque fois.
Test : trois fenêtres de masques. Change COLORS_TO_ANALYZE pour n’analyser que ["red"] et vérifie que seules les zones rouges apparaissent.
Étape 4 : Protéger le code avec check_colors()
Que se passe-t-il si quelqu’un écrit COLORS_TO_ANALYZE = ["blue"] alors que "blue" n’existe pas dans HSV_RANGES ? Erreur mystérieuse plus tard ! On ajoute un contrôleur d’entrée :
import cv2
import numpy as np
# ============================================================
# PARAMÈTRES (constantes)
# ============================================================
IMAGE_PATH = "C:/Users/patri/Pictures/image08.png"
# IMAGE_PATH = "C:/Users/patri/Pictures/Screenshots/163159.png"
RESIZE_FACTOR = 0.5 # Facteur de redimensionnement de l'image
ROTATE_IMAGE = False # Mettre True pour tourner l'image de 90° (sens horaire)
SHOW_MASKS = True # Afficher une fenêtre de masque par couleur analysée
# --- Choix des couleurs à analyser : "red", "yellow", "green" ---
# Exemples : ["red"] | ["red", "green"] | ["red", "yellow", "green"]
COLORS_TO_ANALYZE = ["red", "yellow", "green"]
MORPH_KERNEL_SIZE = 5 # Taille du noyau pour nettoyer les masques
# --- Plages HSV par couleur (OpenCV : H de 0 à 179, S et V de 0 à 255) ---
# Chaque couleur a une liste de plages (min, max) car le rouge est à cheval
# sur le début et la fin du cercle des teintes.
SATURATION_MIN = 100 # Saturation minimale (exclut les couleurs délavées)
VALUE_MIN = 70 # Luminosité minimale (exclut les zones trop sombres)
HSV_RANGES = {
"red": [
((0, SATURATION_MIN, VALUE_MIN), (10, 255, 255)), # Rouge proche de 0°
((170, SATURATION_MIN, VALUE_MIN), (179, 255, 255)), # Rouge proche de 180°
],
"yellow": [
((20, SATURATION_MIN, VALUE_MIN), (35, 255, 255)),
],
"green": [
((40, SATURATION_MIN, VALUE_MIN), (85, 255, 255)),
],
}
def load_image(path, resize_factor, rotate):
"""Charge l'image, la redimensionne et la tourne si nécessaire."""
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
raise FileNotFoundError(f"Impossible de charger l'image : {path}")
image = cv2.resize(image, (0, 0), fx=resize_factor, fy=resize_factor)
if rotate:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
print("Dimension : ",image.shape)
return image
def check_colors(colors):
"""Vérifie que les couleurs demandées sont connues."""
unknown = [c for c in colors if c not in HSV_RANGES]
if unknown:
raise ValueError(f"Couleur(s) inconnue(s) : {unknown}. Disponibles : {list(HSV_RANGES)}")
def build_color_mask(hsv, color_name, kernel_size):
"""Crée le masque binaire d'une couleur à partir de l'image HSV."""
mask = np.zeros(hsv.shape[:2], np.uint8)
# On fusionne toutes les plages de la couleur (ex : 2 plages pour le rouge)
for lower, upper in HSV_RANGES[color_name]:
mask |= cv2.inRange(hsv, np.array(lower), np.array(upper))
kernel = np.ones((kernel_size, kernel_size), np.uint8)
# Ouverture : supprime les petits pixels isolés (bruit)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# Fermeture : comble les petits trous à l'intérieur des zones
return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
def analyze_colors(image, colors):
"""Analyse chaque couleur demandée. Retourne (résultats, masques)."""
# La conversion HSV n'est faite qu'une seule fois pour toutes les couleurs
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
masks = {}
for color_name in colors:
masks[color_name] = build_color_mask(hsv, color_name, MORPH_KERNEL_SIZE)
return masks
def main():
check_colors(COLORS_TO_ANALYZE)
image = load_image(IMAGE_PATH, RESIZE_FACTOR, ROTATE_IMAGE)
# Analyse des couleurs choisies
masks = analyze_colors(image, COLORS_TO_ANALYZE)
# Affichage
cv2.imshow("image", image)
if SHOW_MASKS:
for color_name, mask in masks.items():
cv2.imshow(f"mask {color_name}", mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
def check_colors(colors):
unknown = [c for c in colors if c not in HSV_RANGES]
if unknown:
raise ValueError(f"Couleur(s) inconnue(s) : {unknown}. Disponibles : {list(HSV_RANGES)}")
C’est le videur du club : il vérifie les couleurs à l’entrée, et refuse poliment celles qui ne sont pas dans la liste, avec un message qui dit quelles couleurs sont acceptées.
C’est une habitude de pro : détecter l’erreur au début avec un message clair plutôt que laisser le programme planter au milieu sans explication.
Test : écris volontairement "blue" dans COLORS_TO_ANALYZE → tu reçois un message d’erreur limpide. Remets une vraie couleur !
Étape 5 : Mesurer les zones avec find_largest_zone()
Le masque nous dit où est la couleur, en pixels blancs. Mais on veut une boîte (position + taille) et la plus grande zone seulement.
import cv2
import numpy as np
# ============================================================
# PARAMÈTRES (constantes)
# ============================================================
IMAGE_PATH = "C:/Users/patri/Pictures/image08.png"
# IMAGE_PATH = "C:/Users/patri/Pictures/Screenshots/163159.png"
RESIZE_FACTOR = 0.5 # Facteur de redimensionnement de l'image
ROTATE_IMAGE = False # Mettre True pour tourner l'image de 90° (sens horaire)
SHOW_MASKS = True # Afficher une fenêtre de masque par couleur analysée
# --- Choix des couleurs à analyser : "red", "yellow", "green" ---
# Exemples : ["red"] | ["red", "green"] | ["red", "yellow", "green"]
COLORS_TO_ANALYZE = ["red", "yellow", "green"]
MORPH_KERNEL_SIZE = 5 # Taille du noyau pour nettoyer les masques
# --- Plages HSV par couleur (OpenCV : H de 0 à 179, S et V de 0 à 255) ---
# Chaque couleur a une liste de plages (min, max) car le rouge est à cheval
# sur le début et la fin du cercle des teintes.
SATURATION_MIN = 100 # Saturation minimale (exclut les couleurs délavées)
VALUE_MIN = 70 # Luminosité minimale (exclut les zones trop sombres)
HSV_RANGES = {
"red": [
((0, SATURATION_MIN, VALUE_MIN), (10, 255, 255)), # Rouge proche de 0°
((170, SATURATION_MIN, VALUE_MIN), (179, 255, 255)), # Rouge proche de 180°
],
"yellow": [
((20, SATURATION_MIN, VALUE_MIN), (35, 255, 255)),
],
"green": [
((40, SATURATION_MIN, VALUE_MIN), (85, 255, 255)),
],
}
MIN_AREA = 4000 # Surface minimale (en pixels) pour considérer une zone comme "grande"
def load_image(path, resize_factor, rotate):
"""Charge l'image, la redimensionne et la tourne si nécessaire."""
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
raise FileNotFoundError(f"Impossible de charger l'image : {path}")
image = cv2.resize(image, (0, 0), fx=resize_factor, fy=resize_factor)
if rotate:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
print("Dimension : ",image.shape)
return image
def check_colors(colors):
"""Vérifie que les couleurs demandées sont connues."""
unknown = [c for c in colors if c not in HSV_RANGES]
if unknown:
raise ValueError(f"Couleur(s) inconnue(s) : {unknown}. Disponibles : {list(HSV_RANGES)}")
def build_color_mask(hsv, color_name, kernel_size):
"""Crée le masque binaire d'une couleur à partir de l'image HSV."""
mask = np.zeros(hsv.shape[:2], np.uint8)
# On fusionne toutes les plages de la couleur (ex : 2 plages pour le rouge)
for lower, upper in HSV_RANGES[color_name]:
mask |= cv2.inRange(hsv, np.array(lower), np.array(upper))
kernel = np.ones((kernel_size, kernel_size), np.uint8)
# Ouverture : supprime les petits pixels isolés (bruit)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# Fermeture : comble les petits trous à l'intérieur des zones
return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
def find_largest_zone(mask, min_area):
"""Retourne (x, y, w, h, area) de la plus grande zone du masque, ou None."""
# Composantes connexes : chaque zone séparée reçoit un label
count, _, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
if count <= 1: # Seul le fond a été trouvé
return None
# On ignore le label 0 (fond) et on cherche la plus grande surface
best = np.argmax(stats[1:, cv2.CC_STAT_AREA]) + 1
area = stats[best, cv2.CC_STAT_AREA]
if area < min_area:
return None
return (
stats[best, cv2.CC_STAT_LEFT],
stats[best, cv2.CC_STAT_TOP],
stats[best, cv2.CC_STAT_WIDTH],
stats[best, cv2.CC_STAT_HEIGHT],
area,
)
def analyze_colors(image, colors):
"""Analyse chaque couleur demandée. Retourne (résultats, masques)."""
# La conversion HSV n'est faite qu'une seule fois pour toutes les couleurs
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
results = {}
masks = {}
for color_name in colors:
masks[color_name] = build_color_mask(hsv, color_name, MORPH_KERNEL_SIZE)
results[color_name] = find_largest_zone(masks[color_name], MIN_AREA)
return results, masks
def print_report(results):
"""Affiche un résumé de l'analyse dans la console."""
for color_name, zone in results.items():
if zone is None:
print(f"{color_name:>7} : aucune grande zone détectée")
else:
x, y, w, h, area = zone
print(f"{color_name:>7} : x={x}, y={y}, largeur={w}, hauteur={h}, surface={area} px")
def main():
check_colors(COLORS_TO_ANALYZE)
image = load_image(IMAGE_PATH, RESIZE_FACTOR, ROTATE_IMAGE)
# Analyse des couleurs choisies
results, masks = analyze_colors(image, COLORS_TO_ANALYZE)
print_report(results)
# Affichage
cv2.imshow("image", image)
if SHOW_MASKS:
for color_name, mask in masks.items():
cv2.imshow(f"mask {color_name}", mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
Les composantes connexes : regrouper les pixels voisins
count, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8)
Imagine que chaque groupe de pixels blancs qui se touchent forme une île. connectedComponentsWithStats compte les îles et donne pour chacune sa boîte englobante (x, y, largeur, hauteur) et sa surface.
Sorties (4 tableaux)
labels : image de même taille que l’entrée. Chaque pixel reçoit un étiquette :
0= fond,1, 2, 3…= identifiant de la région connexe.
stats : matrice de N lignes (une par région, y compris le fond en ligne 0) et 5 colonnes :
| Colonne | Constante | Signification |
|---|---|---|
| 0 | CC_STAT_LEFT | Coordonnée x du côté gauche de la boîte englobante |
| 1 | CC_STAT_TOP | Coordonnée y du côté haut |
| 2 | CC_STAT_WIDTH | Largeur de la boîte englobante |
| 3 | CC_STAT_HEIGHT | Hauteur de la boîte englobante |
| 4 | CC_STAT_AREA | Nombre de pixels de la région |
centroids : coordonnées (x, y) du centre de gravité de chaque région.
best = np.argmax(stats[1:, cv2.CC_STAT_AREA]) + 1
np.argmax trouve l’île à la plus grande surface. Le +1 décale car on a ignoré le label 0.
Le filtre anti-poussière
if area < min_area:
return None
Avec MIN_AREA = 4000, les toutes petites taches (moins de 4000 pixels) sont ignorées : elles sont trop petites pour être la vraie cible. Si aucune zone ne passe le filtre, la fonction renvoie None (= « rien trouvé »).
Le rapport dans la console
print_report(results)
Affiche pour chaque couleur : position, dimensions et surface en pixels. Très utile pour vérifier ce que « voit » le programme !
Test : regarde la console :
red : x=210, y=95, largeur=180, hauteur=160, surface=18400 px
yellow : aucune grande zone détectée
Varie MIN_AREA : trop grand → plus rien n’est détecté ; trop petit → le bruit est détecté !
Étape 6 : Dessiner les résultats sur l’image
Les chiffres dans la console, c’est bien ; voir les cadres sur l’image, c’est mieux !
import cv2
import numpy as np
# ============================================================
# PARAMÈTRES (constantes)
# ============================================================
IMAGE_PATH = "C:/Users/patri/Pictures/image08.png"
# IMAGE_PATH = "C:/Users/patri/Pictures/Screenshots/163159.png"
RESIZE_FACTOR = 0.5 # Facteur de redimensionnement de l'image
ROTATE_IMAGE = False # Mettre True pour tourner l'image de 90° (sens horaire)
SHOW_MASKS = True # Afficher une fenêtre de masque par couleur analysée
# --- Choix des couleurs à analyser : "red", "yellow", "green" ---
# Exemples : ["red"] | ["red", "green"] | ["red", "yellow", "green"]
COLORS_TO_ANALYZE = ["red", "yellow", "green"]
MORPH_KERNEL_SIZE = 5 # Taille du noyau pour nettoyer les masques
# --- Plages HSV par couleur (OpenCV : H de 0 à 179, S et V de 0 à 255) ---
# Chaque couleur a une liste de plages (min, max) car le rouge est à cheval
# sur le début et la fin du cercle des teintes.
SATURATION_MIN = 100 # Saturation minimale (exclut les couleurs délavées)
VALUE_MIN = 70 # Luminosité minimale (exclut les zones trop sombres)
HSV_RANGES = {
"red": [
((0, SATURATION_MIN, VALUE_MIN), (10, 255, 255)), # Rouge proche de 0°
((170, SATURATION_MIN, VALUE_MIN), (179, 255, 255)), # Rouge proche de 180°
],
"yellow": [
((20, SATURATION_MIN, VALUE_MIN), (35, 255, 255)),
],
"green": [
((40, SATURATION_MIN, VALUE_MIN), (85, 255, 255)),
],
}
MIN_AREA = 4000 # Surface minimale (en pixels) pour considérer une zone comme "grande"
# --- Couleurs d'affichage des contours (BGR) pour chaque couleur détectée ---
DISPLAY_COLORS = {
"red": (255, 0, 0),
"yellow": (0, 255, 255),
"green": (0, 255, 0),
}
RECT_THICKNESS = 4 # Épaisseur des rectangles
FONT_SCALE = 0.8 # Taille du texte des étiquettes
FONT_THICKNESS = 2 # Épaisseur du texte des étiquettes
def load_image(path, resize_factor, rotate):
"""Charge l'image, la redimensionne et la tourne si nécessaire."""
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
raise FileNotFoundError(f"Impossible de charger l'image : {path}")
image = cv2.resize(image, (0, 0), fx=resize_factor, fy=resize_factor)
if rotate:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
print("Dimension : ",image.shape)
return image
def check_colors(colors):
"""Vérifie que les couleurs demandées sont connues."""
unknown = [c for c in colors if c not in HSV_RANGES]
if unknown:
raise ValueError(f"Couleur(s) inconnue(s) : {unknown}. Disponibles : {list(HSV_RANGES)}")
def build_color_mask(hsv, color_name, kernel_size):
"""Crée le masque binaire d'une couleur à partir de l'image HSV."""
mask = np.zeros(hsv.shape[:2], np.uint8)
# On fusionne toutes les plages de la couleur (ex : 2 plages pour le rouge)
for lower, upper in HSV_RANGES[color_name]:
mask |= cv2.inRange(hsv, np.array(lower), np.array(upper))
kernel = np.ones((kernel_size, kernel_size), np.uint8)
# Ouverture : supprime les petits pixels isolés (bruit)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# Fermeture : comble les petits trous à l'intérieur des zones
return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
def find_largest_zone(mask, min_area):
"""Retourne (x, y, w, h, area) de la plus grande zone du masque, ou None."""
# Composantes connexes : chaque zone séparée reçoit un label
count, _, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
if count <= 1: # Seul le fond a été trouvé
return None
# On ignore le label 0 (fond) et on cherche la plus grande surface
best = np.argmax(stats[1:, cv2.CC_STAT_AREA]) + 1
area = stats[best, cv2.CC_STAT_AREA]
if area < min_area:
return None
return (
stats[best, cv2.CC_STAT_LEFT],
stats[best, cv2.CC_STAT_TOP],
stats[best, cv2.CC_STAT_WIDTH],
stats[best, cv2.CC_STAT_HEIGHT],
area,
)
def analyze_colors(image, colors):
"""Analyse chaque couleur demandée. Retourne (résultats, masques)."""
# La conversion HSV n'est faite qu'une seule fois pour toutes les couleurs
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
results = {}
masks = {}
for color_name in colors:
masks[color_name] = build_color_mask(hsv, color_name, MORPH_KERNEL_SIZE)
results[color_name] = find_largest_zone(masks[color_name], MIN_AREA)
return results, masks
def print_report(results):
"""Affiche un résumé de l'analyse dans la console."""
for color_name, zone in results.items():
if zone is None:
print(f"{color_name:>7} : aucune grande zone détectée")
else:
x, y, w, h, area = zone
print(f"{color_name:>7} : x={x}, y={y}, largeur={w}, hauteur={h}, surface={area} px")
def draw_results(image, results):
"""Dessine un rectangle étiqueté par couleur et la flèche vers la plus grande zone."""
output = image.copy()
rows, cols = output.shape[:2]
image_center_x = cols // 2
for color_name, zone in results.items():
if zone is None:
continue
x, y, w, h, _ = zone
color = DISPLAY_COLORS[color_name]
cv2.rectangle(output, (x, y), (x + w, y + h), color, RECT_THICKNESS)
# Étiquette placée au-dessus du rectangle (ou dedans si trop près du bord)
label_y = y - 8 if y > 25 else y + 25
cv2.putText(output, color_name, (x, label_y),
cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, color, FONT_THICKNESS)
return output
def main():
check_colors(COLORS_TO_ANALYZE)
image = load_image(IMAGE_PATH, RESIZE_FACTOR, ROTATE_IMAGE)
# Analyse des couleurs choisies
results, masks = analyze_colors(image, COLORS_TO_ANALYZE)
print_report(results)
output = draw_results(image, results)
# Affichage
cv2.imshow("image", image)
cv2.imshow("target image", output)
if SHOW_MASKS:
for color_name, mask in masks.items():
cv2.imshow(f"mask {color_name}", mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
La fonction draw_results() copie l’image (image.copy() — on ne dessine jamais sur l’original, comme on ne griffonne pas sur la photo de famille !) et dessine dessus :
cv2.rectangle(output, (x, y), (x + w, y + h), color, RECT_THICKNESS)
cv2.rectangle: un cadre autour de la zone, dans la couleur choisie (DISPLAY_COLORS).cv2.putText: le nom de la couleur écrit au-dessus du cadre.
label_y = y - 8 if y > 25 else y + 25
Petite sécurité : si le cadre touche le haut de l’image, on écrit le texte dedans pour qu’il ne sorte pas de l’écran.
Test : la fenêtre target image montre les cadres colorés, chaque fenêtre de masque reste disponible pour vérifier. Compare avec le rapport console !
Étape 7 : La flèche du capitaine (le programme final !)
Dernière touche : une flèche blanche au bas de l’image qui pointe vers la zone la plus grande, toutes couleurs confondues. Comme un guide qui dit « par ici la cible ! ».
import cv2
import numpy as np
# ============================================================
# PARAMÈTRES (constantes)
# ============================================================
IMAGE_PATH = "C:/Users/patri/Pictures/image08.png"
# IMAGE_PATH = "C:/Users/patri/Pictures/Screenshots/163159.png"
RESIZE_FACTOR = 0.5 # Facteur de redimensionnement de l'image
ROTATE_IMAGE = False # Mettre True pour tourner l'image de 90° (sens horaire)
SHOW_MASKS = True # Afficher une fenêtre de masque par couleur analysée
# --- Choix des couleurs à analyser : "red", "yellow", "green" ---
# Exemples : ["red"] | ["red", "green"] | ["red", "yellow", "green"]
COLORS_TO_ANALYZE = ["red", "yellow", "green"]
MORPH_KERNEL_SIZE = 5 # Taille du noyau pour nettoyer les masques
# --- Plages HSV par couleur (OpenCV : H de 0 à 179, S et V de 0 à 255) ---
# Chaque couleur a une liste de plages (min, max) car le rouge est à cheval
# sur le début et la fin du cercle des teintes.
SATURATION_MIN = 100 # Saturation minimale (exclut les couleurs délavées)
VALUE_MIN = 70 # Luminosité minimale (exclut les zones trop sombres)
HSV_RANGES = {
"red": [
((0, SATURATION_MIN, VALUE_MIN), (10, 255, 255)), # Rouge proche de 0°
((170, SATURATION_MIN, VALUE_MIN), (179, 255, 255)), # Rouge proche de 180°
],
"yellow": [
((20, SATURATION_MIN, VALUE_MIN), (35, 255, 255)),
],
"green": [
((40, SATURATION_MIN, VALUE_MIN), (85, 255, 255)),
],
}
MIN_AREA = 4000 # Surface minimale (en pixels) pour considérer une zone comme "grande"
# --- Couleurs d'affichage (BGR) pour chaque couleur détectée ---
DISPLAY_COLORS = {
"red": (255, 0, 0),
"yellow": (0, 255, 255),
"green": (0, 255, 0),
}
RECT_THICKNESS = 4 # Épaisseur des rectangles
FONT_SCALE = 0.8 # Taille du texte des étiquettes
FONT_THICKNESS = 2 # Épaisseur du texte des étiquettes
DRAW_ARROW = True # Dessiner la flèche vers la zone la plus grande (toutes couleurs confondues)
ARROW_COLOR = (255, 255, 255) # Couleur de la flèche (BGR) : blanc
ARROW_THICKNESS = 4 # Épaisseur de la flèche
ARROW_MARGIN = 10 # Distance entre la flèche et le bas de l'image
def load_image(path, resize_factor, rotate):
"""Charge l'image, la redimensionne et la tourne si nécessaire."""
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
raise FileNotFoundError(f"Impossible de charger l'image : {path}")
image = cv2.resize(image, (0, 0), fx=resize_factor, fy=resize_factor)
if rotate:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
print("Dimension : ",image.shape)
return image
def check_colors(colors):
"""Vérifie que les couleurs demandées sont connues."""
unknown = [c for c in colors if c not in HSV_RANGES]
if unknown:
raise ValueError(f"Couleur(s) inconnue(s) : {unknown}. Disponibles : {list(HSV_RANGES)}")
def build_color_mask(hsv, color_name, kernel_size):
"""Crée le masque binaire d'une couleur à partir de l'image HSV."""
mask = np.zeros(hsv.shape[:2], np.uint8)
# On fusionne toutes les plages de la couleur (ex : 2 plages pour le rouge)
for lower, upper in HSV_RANGES[color_name]:
mask |= cv2.inRange(hsv, np.array(lower), np.array(upper))
kernel = np.ones((kernel_size, kernel_size), np.uint8)
# Ouverture : supprime les petits pixels isolés (bruit)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# Fermeture : comble les petits trous à l'intérieur des zones
return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
def find_largest_zone(mask, min_area):
"""Retourne (x, y, w, h, area) de la plus grande zone du masque, ou None."""
# Composantes connexes : chaque zone séparée reçoit un label
count, _, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
if count <= 1: # Seul le fond a été trouvé
return None
# On ignore le label 0 (fond) et on cherche la plus grande surface
best = np.argmax(stats[1:, cv2.CC_STAT_AREA]) + 1
area = stats[best, cv2.CC_STAT_AREA]
if area < min_area:
return None
return (
stats[best, cv2.CC_STAT_LEFT],
stats[best, cv2.CC_STAT_TOP],
stats[best, cv2.CC_STAT_WIDTH],
stats[best, cv2.CC_STAT_HEIGHT],
area,
)
def analyze_colors(image, colors):
"""Analyse chaque couleur demandée. Retourne (résultats, masques)."""
# La conversion HSV n'est faite qu'une seule fois pour toutes les couleurs
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
results = {}
masks = {}
for color_name in colors:
masks[color_name] = build_color_mask(hsv, color_name, MORPH_KERNEL_SIZE)
results[color_name] = find_largest_zone(masks[color_name], MIN_AREA)
return results, masks
def print_report(results):
"""Affiche un résumé de l'analyse dans la console."""
for color_name, zone in results.items():
if zone is None:
print(f"{color_name:>7} : aucune grande zone détectée")
else:
x, y, w, h, area = zone
print(f"{color_name:>7} : x={x}, y={y}, largeur={w}, hauteur={h}, surface={area} px")
def draw_results(image, results):
"""Dessine un rectangle étiqueté par couleur et la flèche vers la plus grande zone."""
output = image.copy()
rows, cols = output.shape[:2]
image_center_x = cols // 2
for color_name, zone in results.items():
if zone is None:
continue
x, y, w, h, _ = zone
color = DISPLAY_COLORS[color_name]
cv2.rectangle(output, (x, y), (x + w, y + h), color, RECT_THICKNESS)
# Étiquette placée au-dessus du rectangle (ou dedans si trop près du bord)
label_y = y - 8 if y > 25 else y + 25
cv2.putText(output, color_name, (x, label_y),
cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, color, FONT_THICKNESS)
# Flèche vers la zone la plus grande, toutes couleurs confondues
detected = {name: zone for name, zone in results.items() if zone is not None}
if DRAW_ARROW and detected:
biggest = max(detected.values(), key=lambda z: z[4])
x, _, w, _, _ = biggest
offset = (x + w // 2) - image_center_x
arrow_y = rows - ARROW_MARGIN
cv2.arrowedLine(output, (image_center_x, arrow_y),
(image_center_x + offset, arrow_y),
ARROW_COLOR, ARROW_THICKNESS)
return output
def main():
check_colors(COLORS_TO_ANALYZE)
image = load_image(IMAGE_PATH, RESIZE_FACTOR, ROTATE_IMAGE)
# Analyse des couleurs choisies
results, masks = analyze_colors(image, COLORS_TO_ANALYZE)
print_report(results)
output = draw_results(image, results)
# Affichage
cv2.imshow("image", image)
cv2.imshow("target image", output)
if SHOW_MASKS:
for color_name, mask in masks.items():
cv2.imshow(f"mask {color_name}", mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
detected = {name: zone for name, zone in results.items() if zone is not None}
if DRAW_ARROW and detected:
biggest = max(detected.values(), key=lambda z: z[4])
- On garde uniquement les couleurs détectées (zones non
None). max(..., key=lambda z: z[4])choisit la zone dont la surface (5e valeur du tuple) est la plus grande.- On calcule l’écart entre le milieu de la zone et le milieu de l’image :
offset = (x + w // 2) - image_center_x
cv2.arrowedLine(output, (image_center_x, arrow_y),
(image_center_x + offset, arrow_y),
ARROW_COLOR, ARROW_THICKNESS)
La flèche part du centre et s’allonge vers la cible : offset négatif = cible à gauche, offset positif = à droite, offset nul = pile au centre !
Ce genre de flèche, c’est exactement ce qui permettrait à un robot ou un jeu de « viser » une cible automatiquement. Tu viens de coder un mini système de vision !
Récapitulatif du voyage
| Étape | On a appris… |
|---|---|
| 0 | Le programme tout-en-un et ses défauts (lent, fragile) |
| 1 | Charger, redimensionner, structurer en fonctions avec des paramètres |
| 2 | Le masque binaire HSV et le nettoyage morphologique |
| 3 | Analyser plusieurs couleurs avec le même code |
| 4 | Valider les entrées avec un message d’erreur clair |
| 5 | Mesurer les zones : composantes connexes + filtre de surface |
| 6 | Dessiner rectangles et étiquettes sur l’image |
| 7 | La flèche vers la plus grande cible : un mini système de vision ! |
Analyse d’une zone de couleur noire avec un contrôle sur la largeur minimum sur le coté le plus petit :
import cv2
import numpy as np
# ============================================================
# PARAMÈTRES (constantes)
# ============================================================
IMAGE_PATH = "C:/Users/patri/Pictures/image10.png"
# IMAGE_PATH = "C:/Users/patri/Pictures/Screenshots/163159.png"
RESIZE_FACTOR = 0.5 # Facteur de redimensionnement de l'image
ROTATE_IMAGE = False # Mettre True pour tourner l'image de 90° (sens horaire)
SHOW_MASKS = True # Afficher une fenêtre de masque par couleur analysée
# --- Choix des couleurs à analyser : "red", "yellow", "green" ---
# Exemples : ["red"] | ["red", "green"] | ["red", "yellow", "green"]
COLORS_TO_ANALYZE = ["red", "yellow", "green", "black"]
MORPH_KERNEL_SIZE = 5 # Taille du noyau pour nettoyer les masques
# --- Plages HSV par couleur (OpenCV : H de 0 à 179, S et V de 0 à 255) ---
# Chaque couleur a une liste de plages (min, max) car le rouge est à cheval
# sur le début et la fin du cercle des teintes.
SATURATION_MIN = 100 # Saturation minimale (exclut les couleurs délavées)
VALUE_MIN = 70 # Luminosité minimale (exclut les zones trop sombres)
# --- Plages spécifiques au noir ---
BLACK_V_MAX = 60 # Valeur maximale : plus c'est élevé, plus on accepte de gris foncé
BLACK_S_MAX = 80 # Saturation maximale : exclut les couleurs sombres mais teintées
HSV_RANGES = {
"red": [
((0, SATURATION_MIN, VALUE_MIN), (10, 255, 255)), # Rouge proche de 0°
((170, SATURATION_MIN, VALUE_MIN), (179, 255, 255)), # Rouge proche de 180°
],
"yellow": [
((20, SATURATION_MIN, VALUE_MIN), (35, 255, 255)),
],
"green": [
((40, SATURATION_MIN, VALUE_MIN), (85, 255, 255)),
],
# Noir : S et V faibles, teinte quelconque
"black": [
((0, 0, 0), (179, BLACK_S_MAX, BLACK_V_MAX)),
],
}
# --- Filtre sur le plus petit côté, par couleur ---
# None = pas de filtre ; pour "black" : garde uniquement les lignes
# assez épaisses (en pixels) et élimine le bruit fin
MIN_SMALL_SIDE = {
"red": None,
"yellow": None,
"green": None,
"black": 3, # <<< une ligne noire doit faire au moins 3 px d'épaisseur
}
MIN_AREA = 4000 # Surface minimale (en pixels) pour considérer une zone comme "grande"
# Les lignes fines ont une petite surface : seuil réduit pour le noir
MIN_AREA_BY_COLOR = {
"red": MIN_AREA,
"yellow": MIN_AREA,
"green": 5,
"black": 500, # <<< surface minimale réduite pour les lignes
}
# --- Couleurs d'affichage (BGR) pour chaque couleur détectée ---
DISPLAY_COLORS = {
"red": (255, 0, 0),
"yellow": (0, 255, 255),
"green": (255, 0, 0),
"black": (255, 0, 0), # blanc pour rester visible sur du noir
}
RECT_THICKNESS = 4 # Épaisseur des rectangles
FONT_SCALE = 0.8 # Taille du texte des étiquettes
FONT_THICKNESS = 2 # Épaisseur du texte des étiquettes
DRAW_ARROW = True # Dessiner la flèche vers la zone la plus grande (toutes couleurs confondues)
ARROW_COLOR = (255, 255, 255) # Couleur de la flèche (BGR) : blanc
ARROW_THICKNESS = 4 # Épaisseur de la flèche
ARROW_MARGIN = 10 # Distance entre la flèche et le bas de l'image
def load_image(path, resize_factor, rotate):
"""Charge l'image, la redimensionne et la tourne si nécessaire."""
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
raise FileNotFoundError(f"Impossible de charger l'image : {path}")
image = cv2.resize(image, (0, 0), fx=resize_factor, fy=resize_factor)
if rotate:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
print("Dimension : ",image.shape)
return image
def check_colors(colors):
"""Vérifie que les couleurs demandées sont connues."""
unknown = [c for c in colors if c not in HSV_RANGES]
if unknown:
raise ValueError(f"Couleur(s) inconnue(s) : {unknown}. Disponibles : {list(HSV_RANGES)}")
def build_color_mask(hsv, color_name, kernel_size):
"""Crée le masque binaire d'une couleur à partir de l'image HSV."""
mask = np.zeros(hsv.shape[:2], np.uint8)
# On fusionne toutes les plages de la couleur (ex : 2 plages pour le rouge)
for lower, upper in HSV_RANGES[color_name]:
mask |= cv2.inRange(hsv, np.array(lower), np.array(upper))
kernel = np.ones((kernel_size, kernel_size), np.uint8)
# Ouverture : supprime les petits pixels isolés (bruit)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# Fermeture : comble les petits trous à l'intérieur des zones
return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
def find_largest_zone(mask, min_area, min_small_side=None):
"""Retourne (x, y, w, h, area) de la plus grande zone du masque, ou None.
Si min_small_side est défini, ignore les zones dont le plus petit côté
(largeur ou hauteur) est inférieur à cette valeur."""
count, _, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
if count <= 1:
return None
best = None
best_area = 0
for label in range(1, count): # 0 = fond, on le saute
x = stats[label, cv2.CC_STAT_LEFT]
y = stats[label, cv2.CC_STAT_TOP]
w = stats[label, cv2.CC_STAT_WIDTH]
h = stats[label, cv2.CC_STAT_HEIGHT]
area = stats[label, cv2.CC_STAT_AREA]
if area < min_area:
continue
if min_small_side is not None and min(w, h) < min_small_side:
continue
if area > best_area:
best_area = area
best = (x, y, w, h, area)
return best
def analyze_colors(image, colors):
"""Analyse chaque couleur demandée. Retourne (résultats, masques)."""
# La conversion HSV n'est faite qu'une seule fois pour toutes les couleurs
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
results = {}
masks = {}
for color_name in colors:
masks[color_name] = build_color_mask(hsv, color_name, MORPH_KERNEL_SIZE)
results[color_name] = find_largest_zone(
masks[color_name],
MIN_AREA_BY_COLOR[color_name], # seuil de surface adapté
MIN_SMALL_SIDE[color_name], # filtre sur le plus petit côté
)
return results, masks
def print_report(results):
"""Affiche un résumé de l'analyse dans la console."""
for color_name, zone in results.items():
if zone is None:
print(f"{color_name:>7} : aucune grande zone détectée")
else:
x, y, w, h, area = zone
print(f"{color_name:>7} : x={x}, y={y}, largeur={w}, hauteur={h}, surface={area} px")
def draw_results(image, results):
"""Dessine un rectangle étiqueté par couleur et la flèche vers la plus grande zone."""
output = image.copy()
rows, cols = output.shape[:2]
image_center_x = cols // 2
for color_name, zone in results.items():
if zone is None:
continue
x, y, w, h, _ = zone
color = DISPLAY_COLORS[color_name]
cv2.rectangle(output, (x, y), (x + w, y + h), color, RECT_THICKNESS)
# Étiquette placée au-dessus du rectangle (ou dedans si trop près du bord)
label_y = y - 8 if y > 25 else y + 25
cv2.putText(output, color_name, (x, label_y),
cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, color, FONT_THICKNESS)
# Flèche vers la zone la plus grande, toutes couleurs confondues
detected = {name: zone for name, zone in results.items() if zone is not None}
if DRAW_ARROW and detected:
biggest = max(detected.values(), key=lambda z: z[4])
x, _, w, _, _ = biggest
offset = (x + w // 2) - image_center_x
arrow_y = rows - ARROW_MARGIN
cv2.arrowedLine(output, (image_center_x, arrow_y),
(image_center_x + offset, arrow_y),
ARROW_COLOR, ARROW_THICKNESS)
return output
def main():
check_colors(COLORS_TO_ANALYZE)
image = load_image(IMAGE_PATH, RESIZE_FACTOR, ROTATE_IMAGE)
# Analyse des couleurs choisies
results, masks = analyze_colors(image, COLORS_TO_ANALYZE)
print_report(results)
output = draw_results(image, results)
# Affichage
cv2.imshow("image", image)
cv2.imshow("target image", output)
if SHOW_MASKS:
for color_name, mask in masks.items():
cv2.imshow(f"mask {color_name}", mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
Traitement de plusieurs zones en leur donnant une priorité :
import cv2
import numpy as np
# ============================================================
# PARAMÈTRES (constantes)
# ============================================================
IMAGE_PATH = "C:/Users/patri/Pictures/image11.png"
# IMAGE_PATH = "C:/Users/patri/Pictures/Screenshots/163159.png"
RESIZE_FACTOR = 0.5 # Facteur de redimensionnement de l'image
ROTATE_IMAGE = False # Mettre True pour tourner l'image de 90° (sens horaire)
SHOW_MASKS = True # Afficher une fenêtre de masque par couleur analysée
# --- Choix des couleurs à analyser : "red", "yellow", "green" ---
# Exemples : ["red"] | ["red", "green"] | ["red", "yellow", "green"]
COLORS_TO_ANALYZE = ["red", "yellow", "green", "black"]
MORPH_KERNEL_SIZE = 5 # Taille du noyau pour nettoyer les masques
# --- Plages HSV par couleur (OpenCV : H de 0 à 179, S et V de 0 à 255) ---
# Chaque couleur a une liste de plages (min, max) car le rouge est à cheval
# sur le début et la fin du cercle des teintes.
SATURATION_MIN = 100 # Saturation minimale (exclut les couleurs délavées)
VALUE_MIN = 70 # Luminosité minimale (exclut les zones trop sombres)
# --- Plages spécifiques au noir ---
BLACK_V_MAX = 60 # Valeur maximale : plus c'est élevé, plus on accepte de gris foncé
BLACK_S_MAX = 80 # Saturation maximale : exclut les couleurs sombres mais teintées
HSV_RANGES = {
"red": [
((0, SATURATION_MIN, VALUE_MIN), (10, 255, 255)), # Rouge proche de 0°
((170, SATURATION_MIN, VALUE_MIN), (179, 255, 255)), # Rouge proche de 180°
],
"yellow": [
((20, SATURATION_MIN, VALUE_MIN), (35, 255, 255)),
],
"green": [
((40, SATURATION_MIN, VALUE_MIN), (85, 255, 255)),
],
# Noir : S et V faibles, teinte quelconque
"black": [
((0, 0, 0), (179, BLACK_S_MAX, BLACK_V_MAX)),
],
}
# --- Filtre sur le plus petit côté, par couleur ---
# None = pas de filtre ; pour "black" : garde uniquement les lignes
# assez épaisses (en pixels) et élimine le bruit fin
MIN_SMALL_SIDE = {
"red": None,
"yellow": None,
"green": None,
"black": 3, # <<< une ligne noire doit faire au moins 3 px d'épaisseur
}
MIN_AREA = 4000 # Surface minimale (en pixels) pour considérer une zone comme "grande"
# Les lignes fines ont une petite surface : seuil réduit pour le noir
MIN_AREA_BY_COLOR = {
"red": 50,
"yellow": MIN_AREA,
"green": 5,
"black": 500, # <<< surface minimale réduite pour les lignes
}
# --- Couleurs d'affichage (BGR) pour chaque couleur détectée ---
DISPLAY_COLORS = {
"red": (255, 0, 0),
"yellow": (0, 255, 255),
"green": (255, 0, 0),
"black": (255, 0, 0), # blanc pour rester visible sur du noir
}
RECT_THICKNESS = 4 # Épaisseur des rectangles
FONT_SCALE = 0.8 # Taille du texte des étiquettes
FONT_THICKNESS = 2 # Épaisseur du texte des étiquettes
DRAW_ARROW = True # Dessiner la flèche vers la zone la plus grande (toutes couleurs confondues)
ARROW_COLOR = (255, 255, 255) # Couleur de la flèche (BGR) : blanc
ARROW_THICKNESS = 4 # Épaisseur de la flèche
ARROW_MARGIN = 10 # Distance entre la flèche et le bas de l'image
MAX_ZONES_PER_COLOR = 5 # <<< Nombre maximum de zones affichées par couleur
# Mettre None pour tout afficher
def load_image(path, resize_factor, rotate):
"""Charge l'image, la redimensionne et la tourne si nécessaire."""
image = cv2.imread(path, cv2.IMREAD_COLOR)
if image is None:
raise FileNotFoundError(f"Impossible de charger l'image : {path}")
image = cv2.resize(image, (0, 0), fx=resize_factor, fy=resize_factor)
if rotate:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
print("Dimension : ",image.shape)
return image
def check_colors(colors):
"""Vérifie que les couleurs demandées sont connues."""
unknown = [c for c in colors if c not in HSV_RANGES]
if unknown:
raise ValueError(f"Couleur(s) inconnue(s) : {unknown}. Disponibles : {list(HSV_RANGES)}")
def build_color_mask(hsv, color_name, kernel_size):
"""Crée le masque binaire d'une couleur à partir de l'image HSV."""
mask = np.zeros(hsv.shape[:2], np.uint8)
# On fusionne toutes les plages de la couleur (ex : 2 plages pour le rouge)
for lower, upper in HSV_RANGES[color_name]:
mask |= cv2.inRange(hsv, np.array(lower), np.array(upper))
kernel = np.ones((kernel_size, kernel_size), np.uint8)
# Ouverture : supprime les petits pixels isolés (bruit)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# Fermeture : comble les petits trous à l'intérieur des zones
return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
def find_largest_zone(mask, min_area, min_small_side=None):
"""Retourne (x, y, w, h, area) de la plus grande zone du masque, ou None.
Si min_small_side est défini, ignore les zones dont le plus petit côté
(largeur ou hauteur) est inférieur à cette valeur."""
count, _, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
if count <= 1:
return None
best = None
best_area = 0
for label in range(1, count): # 0 = fond, on le saute
x = stats[label, cv2.CC_STAT_LEFT]
y = stats[label, cv2.CC_STAT_TOP]
w = stats[label, cv2.CC_STAT_WIDTH]
h = stats[label, cv2.CC_STAT_HEIGHT]
area = stats[label, cv2.CC_STAT_AREA]
if area < min_area:
continue
if min_small_side is not None and min(w, h) < min_small_side:
continue
if area > best_area:
best_area = area
best = (x, y, w, h, area)
return best
def find_all_zones(mask, min_area, min_small_side=None, max_zones=None):
"""Retourne la liste [(x, y, w, h, area), ...] des zones du masque,
triées de la plus grande à la plus petite, ou [] si aucune.
- min_area : surface minimale d'une zone
- min_small_side : si défini, plus petit côté (w ou h) minimal
- max_zones : nombre maximum de zones retournées (None = toutes)"""
count, _, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
if count <= 1:
return []
zones = []
for label in range(1, count): # 0 = fond, on le saute
x = stats[label, cv2.CC_STAT_LEFT]
y = stats[label, cv2.CC_STAT_TOP]
w = stats[label, cv2.CC_STAT_WIDTH]
h = stats[label, cv2.CC_STAT_HEIGHT]
area = stats[label, cv2.CC_STAT_AREA]
if area < min_area:
continue
if min_small_side is not None and min(w, h) < min_small_side:
continue
zones.append((x, y, w, h, area))
# Tri de la plus grande à la plus petite
zones.sort(key=lambda z: z[4], reverse=True)
if max_zones is not None:
zones = zones[:max_zones]
return zones
def analyze_colors(image, colors):
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
results = {} # {couleur: [(x, y, w, h, area), ...]}
masks = {}
for color_name in colors:
masks[color_name] = build_color_mask(hsv, color_name, MORPH_KERNEL_SIZE)
results[color_name] = find_all_zones(
masks[color_name],
MIN_AREA_BY_COLOR[color_name],
MIN_SMALL_SIDE[color_name],
MAX_ZONES_PER_COLOR,
)
return results, masks
def assign_priorities(results):
"""Attribue un rang de priorité À L'INTÉRIEUR DE CHAQUE COULEUR.
Pour chaque couleur, la zone dont le bas est le plus proche du BAS
de l'image reçoit la priorité 1 (la plus forte). Les suivantes sont
numérotées en remontant vers le haut de l'image.
Retourne (by_color, all_ranked) :
by_color : {couleur: {zone_tuple: rang}}
all_ranked : liste [(couleur, zone, rang), ...] de toutes les zones
prioritaires, utile pour la flèche
"""
by_color = {}
all_ranked = []
for color_name, zones in results.items():
if not zones or color_name not in DISPLAY_COLORS:
continue
# Tri du bas vers le haut : bas de zone (y + h) décroissant
zones_sorted = sorted(
zones,
key=lambda z: (z[1] + z[3]), # y + h
reverse=True, # la plus basse d'abord
)
by_color[color_name] = {}
for rank, zone in enumerate(zones_sorted, start=1):
by_color[color_name][zone] = rank
all_ranked.append((color_name, zone, rank))
return by_color, all_ranked
def print_report(results, by_color=None):
for color_name, zones in results.items():
if not zones:
print(f"{color_name:>7} : aucune zone détectée")
continue
print(f"{color_name:>7} : {len(zones)} zone(s)")
for i, zone in enumerate(zones, start=1):
x, y, w, h, area = zone
rank = by_color.get(color_name, {}).get(zone, "-")
print(f" zone {i} : x={x}, y={y}, largeur={w}, "
f"hauteur={h}, surface={area} px, priorite={rank}")
def draw_results(image, results, all_ranked=None, by_zone=None):
output = image.copy()
rows, cols = output.shape[:2]
image_center_x = cols // 2
for color_name, zones in results.items():
color = DISPLAY_COLORS[color_name]
for i, (x, y, w, h, area) in enumerate(zones, start=1):
cv2.rectangle(output, (x, y), (x + w, y + h), color, RECT_THICKNESS)
label_y = y - 8 if y > 25 else y + 25
rank = by_zone.get(color_name, {}).get((x, y, w, h, area))
label = f"{color_name} {i}" if rank is None else f"{color_name} {i} P{rank}"
cv2.putText(output, label, (x, label_y),
cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, color, FONT_THICKNESS)
# Flèche vers la zone la plus grande, toutes couleurs confondues
detected = {name: zone for name, zone in results.items() if zone is not None}
if DRAW_ARROW and all_ranked:
firsts = [(c, z, r) for c, z, r in all_ranked if r == 1] # une par couleur
# Si plusieurs couleurs ont une priorité 1, choisir par ex. la plus basse :
color_name, (x, y, w, h, _), rank = max(firsts, key=lambda e: e[1][1] + e[1][3])
offset = (x + w // 2) - image_center_x
arrow_y = rows - ARROW_MARGIN
cv2.arrowedLine(output, (image_center_x, arrow_y),
(image_center_x + offset, arrow_y),
ARROW_COLOR, ARROW_THICKNESS)
return output
def main():
check_colors(COLORS_TO_ANALYZE)
image = load_image(IMAGE_PATH, RESIZE_FACTOR, ROTATE_IMAGE)
# Analyse des couleurs choisies
results, masks = analyze_colors(image, COLORS_TO_ANALYZE)
by_color, all_ranked = assign_priorities(results)
print_report(results, by_color)
output = draw_results(image, results, all_ranked, by_color)
# Affichage
cv2.imshow("image", image)
cv2.imshow("target image", output)
if SHOW_MASKS:
for color_name, mask in masks.items():
cv2.imshow(f"mask {color_name}", mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
