Catégories
Non classé UNIHIKER

Trouver une zone de couleur dans une image avec OpenCV

À 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

  1. Utilise MU Python sur ton poste de travail.
  1. Installe si besoin le package opencv-python
pip install opencv-python numpy
  1. 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)
  • imread charge l’image en mémoire.
  • resize la 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 for imbriqué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
LettreNomFourchette OpenCVCe qu’elle mesure
HHue (teinte)0 à 179 ⚠️Laquelle couleur c’est : rouge, jaune, vert…
SSaturation0 à 255La force : 0 = délavé/gris, 255 = très vif
VValue (luminosité)0 à 255La 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 :

ColonneConstanteSignification
0CC_STAT_LEFTCoordonnée x du côté gauche de la boîte englobante
1CC_STAT_TOPCoordonnée y du côté haut
2CC_STAT_WIDTHLargeur de la boîte englobante
3CC_STAT_HEIGHTHauteur de la boîte englobante
4CC_STAT_AREANombre 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])
  1. On garde uniquement les couleurs détectées (zones non None).
  2. max(..., key=lambda z: z[4]) choisit la zone dont la surface (5e valeur du tuple) est la plus grande.
  3. 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

ÉtapeOn a appris…
0Le programme tout-en-un et ses défauts (lent, fragile)
1Charger, redimensionner, structurer en fonctions avec des paramètres
2Le masque binaire HSV et le nettoyage morphologique
3Analyser plusieurs couleurs avec le même code
4Valider les entrées avec un message d’erreur clair
5Mesurer les zones : composantes connexes + filtre de surface
6Dessiner rectangles et étiquettes sur l’image
7La 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()






Catégories
MICRO:BIT UNIHIKER

Affichage écran OLED

Ton matériel

Un microcontrôleur la MICRO:BIT

Une carte d’extension

Cette carte d’extension modulable est conçue pour du prototypage et des projets robotiques. Elle combine un pilote de moteur, une alimentation avec support pour accu Li-Ion 18650 et de nombreuses E/S Gravity.

Alimentation :

  • Support pour batterie 18650 (3,7 Vcc) avec circuit de charge intégré et indicateur de niveau à LEDs.
  • Entrée USB Type-C (5 Vcc) : pour alimentation externe et recharge.
  • LEDs : état de charge, niveau de batterie, alimentation et sens des moteurs.

Moteurs et servomoteurs :

  • Driver pour 4 moteurs CC (1,5 A maxi par moteur) à connecter sur borniers à vis.
  • 6 sorties servomoteurs sur connecteurs 3 contacts

Interfaces :

  • 8 broches GPIO via le connecteur Edge des cartes micro:bit ou Unihiker.
  • 6 broches GPIO supplémentaires : digitale et analogique.
  • Bus I2C : trois interfaces à 3,3 V et une interface I2C en 5 V.
  • Interface pour capteurs à ultrasons 5 V (Trigger et Echo). 

Autres : 2 x LEDs RGB programmables (WS2812) et émetteur/récepteur infrarouge

Un écran OLED 12846

Afficheur OLED miniature 0,96″ de 128 x 64 pixels monochrome à caractères bleus et à fond noir. Ce module est compatible Gravity de DFRobot et il se raccorde sur un port I2C de la carte d’extension.


Extension du logiciel avec la carte micro:bit et la carte d’extension dans MIND+

Cliquer sur l’icone.

Puis choisis dans « Afficheur » l’écran OLED-12846 :

Clic sur retour en haut à gauche de l’écran :

Maintenant tu disposes de trois extensions : une pour la micro:bit, une autre pour la carte d’extension, puis pour l’écran OLED.


Instructions pour brancher l’écran OLED

  1. Branchement du servomoteur :
    • Insère le connecteur de l’écran OLED dans le port I2C de la carte d’extension, respecte les couleurs
    • Assure-toi que le câble vert est sur le connecteur vert pour garantir que l’information soit correctement transmise.
  2. Alimentation :
    • Assure-toi que l’interrupteur de la carte d’extension est bien en position ON.

Câble de l’écran OLED sur le connecteur I2C :

Bouton interrupteur sur ON :


Programmation de l’écran OLED

Choix des instructions par un clic sur display :

Au lancement initialiser l’écran OLED toujours sur cette adresse sur le bus I2C :

Possibilité d’inverser l’affichage par le choix 180° ou 0° :

Affichage d’un texte en colonne 1 sur 4 lignes :

Affichage d’un texte sur sur la colonne 0 à 127 et les lignes 0 à 3 :

Exemple de l’utilisation de l’écran :

Catégories
DFROBOT MICRO:BIT Robotique UNIHIKER

Active un bouton poussoir et une LED

Ton matériel

Un microcontrôleur la MICRO:BIT

Une carte d’extension

Cette carte d’extension modulable est conçue pour du prototypage et des projets robotiques. Elle combine un pilote de moteur, une alimentation avec support pour accu Li-Ion 18650 et de nombreuses E/S Gravity.

Alimentation :

  • Support pour batterie 18650 (3,7 Vcc) avec circuit de charge intégré et indicateur de niveau à LEDs.
  • Entrée USB Type-C (5 Vcc) : pour alimentation externe et recharge.
  • LEDs : état de charge, niveau de batterie, alimentation et sens des moteurs.

Moteurs et servomoteurs :

  • Driver pour 4 moteurs CC (1,5 A maxi par moteur) à connecter sur borniers à vis.
  • 6 sorties servomoteurs sur connecteurs 3 contacts

Interfaces :

  • 8 broches GPIO via le connecteur Edge des cartes micro:bit ou Unihiker.
  • 6 broches GPIO supplémentaires : digitale et analogique.
  • Bus I2C : trois interfaces à 3,3 V et une interface I2C en 5 V.
  • Interface pour capteurs à ultrasons 5 V (Trigger et Echo). 

Autres : 2 x LEDs RGB programmables (WS2812) et émetteur/récepteur infrarouge


Extension du logiciel avec la carte micro:bit et la carte d’extension dans MIND+

Cliquer sur l’icone.

Clic sur retour en haut à gauche de l’écran

Maintenant tu as deux extensions : une pour la micro:bit et l’autre pour la carte d’extension


Faire clignoter une LED

Instructions pour brancher la LED

  1. Branchement du câble de la LED :
    • Insère le câble de la LED dans le connecteur P15 de ta carte d’extension du microcontrôleur.
    • Assure-toi que le câble gris est positionné comme indiqué sur l’illustration sur le connecteur vert. Cela garantit que le signal de contrôle est envoyé correctement.
  2. Connexion du microcontrôleur :
    • Utilise un câble USB pour connecter ton microcontrôleur à ton ordinateur. Cela permet de transférer le programme et d’alimenter le microcontrôleur.

Une fois tout bien branché, tu peux programmer ton microcontrôleur pour envoyer des signaux à la LED et la faire clignoter.

Branche le câble de la LED sur le connecteur P15 de ta carte d’extension. Attention au sens du câble gris sur le connecteur vert.

Connecte ton microcontrôleur à ton ordinateur par le câble USB :

Pour en savoir plus sur une LED

Une LED (Light Emitting Diode) est un composant électronique qui émet de la lumière lorsqu’un courant électrique la traverse. Les LEDs sont utilisées dans une multitude d’applications, des indicateurs lumineux aux écrans de télévision. Dans ton cas, tu utilises une LED rouge qui s’allume ou s’éteint en fonction des signaux qu’elle reçoit. Le câble de connexion de ta LED a trois fils : deux noirs pour l’alimentation électrique et un gris pour le signal de contrôle qui dit à la LED quand s’allumer ou s’éteindre.

Pour en savoir plus sur un microcontrôleur

Un microcontrôleur est un petit ordinateur sur une seule puce. Il contient un processeur, de la mémoire et des entrées/sorties programmables. Les microcontrôleurs sont utilisés pour contrôler des dispositifs électroniques. Dans ton projet, le microcontrôleur envoie des signaux à la LED pour la faire clignoter.

Allume la LED :

Utilise les instructions « opération de la goupille » pour allumer ta LED :

Fais glisser le bloc set digital pin dans pour toujours puis choisis HIGH:

et le port P15 :

Puis connecte ton microcontrôleur Microbit V2:

Puis clic sur le menu en haut de l’écran et choisis Télécharger :

Tant que ce message est affiché ne touche pas au microcontrôleur ni au câble USB.

Il disparaîtra, une fois le transfert terminé.

Ta LED s’allume !!!


Allume ta LED avec un bouton poussoir :

Utilise un bouton poussoir mis à ta disposition :

Instructions pour brancher le bouton poussoir :

  1. Branchement du câble du bouton poussoir :
    • Insère le câble du bouton poussoir dans le connecteur entre P8 et P15 sur ta carte d’extension.
    • Assure-toi que le câble gris est positionné sur le connecteur vert pour garantir que le signal soit correctement transmis.

Place le câble du bouton poussoir sur P14 :

Programmation du bouton poussoir :

Utilise l’instruction read digital pin P14 puisque tu as installé le bouton poussoir sur P14 :

Puis une instruction serial string pour envoyer la mesure vers ton PC dans la console de Min+ :

Assemble les deux instructions pour les positionner dans ta boucle pour toujours :

Avant de télécharger ton programme allume la carte d’extension :

Puis demande à télécharger ton programme :

Clic sur le bouton Code en haut à droite pour ouvrir les deux onglets dont celui « Génération automatique » :

Tu constate que ton téléversement est réussi ou pas, puis clic sur cette icone :

Tu constateras l’affichage dans la console du résultat de l’appui sur le bouton poussoir :

A 1 quand le bouton n’est pas appuyé.

A 0 quand le bouton est appuyé.

Programme le bouton poussoir

Programme ton bouton poussoir pour allumer la LED.

Ton objectif : allumer la LED quand le bouton poussoir est appuyé donc a une valeur retournée de 0.

Utilise les instruction de contrôle pour tester la valeur de retour du photoréflecteur.

Utilise l’instruction de condition SI SINON

Maintenant pour comparer la valeur retournée par le bouton poussoir utilise l’opérateur mathématique = :

Puis compare avec la valeur retournée par le bouton poussoir :

Décide d’allumer la LED quand le bouton poussoir est appuyé :

Puis clic sur le menu en haut de l’écran et choisis Télécharger :

Tant que le message de transfert est affiché ne touche pas au microcontrôleur ni au câble USB.

Il disparaîtra, une fois le transfert terminé.

Teste en appuyant sur le bouton poussoir

Catégories
DFROBOT MICRO:BIT Robotique UNIHIKER

Active un photo réflecteur IR et une LED

Ton matériel

Un microcontrôleur la MICRO:BIT

Une carte d’extension

Cette carte d’extension modulable est conçue pour du prototypage et des projets robotiques. Elle combine un pilote de moteur, une alimentation avec support pour accu Li-Ion 18650 et de nombreuses E/S Gravity.

Alimentation :

  • Support pour batterie 18650 (3,7 Vcc) avec circuit de charge intégré et indicateur de niveau à LEDs.
  • Entrée USB Type-C (5 Vcc) : pour alimentation externe et recharge.
  • LEDs : état de charge, niveau de batterie, alimentation et sens des moteurs.

Moteurs et servomoteurs :

  • Driver pour 4 moteurs CC (1,5 A maxi par moteur) à connecter sur borniers à vis.
  • 6 sorties servomoteurs sur connecteurs 3 contacts

Interfaces :

  • 8 broches GPIO via le connecteur Edge des cartes micro:bit ou Unihiker.
  • 6 broches GPIO supplémentaires : digitale et analogique.
  • Bus I2C : trois interfaces à 3,3 V et une interface I2C en 5 V.
  • Interface pour capteurs à ultrasons 5 V (Trigger et Echo). 

Autres : 2 x LEDs RGB programmables (WS2812) et émetteur/récepteur infrarouge


Extension du logiciel avec la carte micro:bit et la carte d’extension dans MIND+

Cliquer sur l’icone.

Clic sur retour en haut à gauche de l’écran

Maintenant tu as deux extensions : une pour la micro:bit et l’autre pour la carte d’extension


Faire clignoter une LED

Instructions pour brancher la LED

  1. Branchement du câble de la LED :
    • Insère le câble de la LED dans le connecteur P15 de ta carte d’extension du microcontrôleur.
    • Assure-toi que le câble gris est positionné comme indiqué sur l’illustration sur le connecteur vert. Cela garantit que le signal de contrôle est envoyé correctement.
  2. Connexion du microcontrôleur :
    • Utilise un câble USB pour connecter ton microcontrôleur à ton ordinateur. Cela permet de transférer le programme et d’alimenter le microcontrôleur.

Une fois tout bien branché, tu peux programmer ton microcontrôleur pour envoyer des signaux à la LED et la faire clignoter.

Branche le câble de la LED sur le connecteur P15 de ta carte d’extension. Attention au sens du câble gris sur le connecteur vert.

Connecte ton microcontrôleur à ton ordinateur par le câble USB :

Pour en savoir plus sur une LED

Une LED (Light Emitting Diode) est un composant électronique qui émet de la lumière lorsqu’un courant électrique la traverse. Les LEDs sont utilisées dans une multitude d’applications, des indicateurs lumineux aux écrans de télévision. Dans ton cas, tu utilises une LED rouge qui s’allume ou s’éteint en fonction des signaux qu’elle reçoit. Le câble de connexion de ta LED a trois fils : deux noirs pour l’alimentation électrique et un gris pour le signal de contrôle qui dit à la LED quand s’allumer ou s’éteindre.

Pour en savoir plus sur un microcontrôleur

Un microcontrôleur est un petit ordinateur sur une seule puce. Il contient un processeur, de la mémoire et des entrées/sorties programmables. Les microcontrôleurs sont utilisés pour contrôler des dispositifs électroniques. Dans ton projet, le microcontrôleur envoie des signaux à la LED pour la faire clignoter.

Allume la LED :

Utilise les instructions « opération de la goupille » pour allumer ta LED :

Fais glisser le bloc set digital pin dans pour toujours puis choisis HIGH:

et le port P15 :

Puis connecte ton microcontrôleur Microbit V2:

Puis clic sur le menu en haut de l’écran et choisis Télécharger :

Tant que ce message est affiché ne touche pas au microcontrôleur ni au câble USB.

Il disparaîtra, une fois le transfert terminé.

Ta LED s’allume !!!


Allume ta LED avec ton doigt

Utilise un photo réflecteur IR mis à ta disposition :

Instructions pour brancher le photo réflecteur IR

  1. Branchement du câble du photoréflecteur :
    • Insère le câble du photoréflecteur dans le connecteur P0 de ton microcontrôleur.
    • Assure-toi que le câble gris est positionné sur le connecteur vert pour garantir que le signal soit correctement transmis.

Place le photo réflecteur IR en face du bloc noir :

Programmation du photo réflecteur IR :

Tu rencontreras deux types de dispositifs un analogique et un autre digital :

Le monde analogique
C’est comme un robinet d’eau : tu peux l’ouvrir un petit peu, moyennement, beaucoup… il y a une infinité de positions possibles. La température peut être de 20°C, 20,1°C, 20,15°C…

Un photoréflecteur IR (le petit capteur avec la boule transparente) fonctionne comme ça : il mesure la quantité de lumière qui rebondit sur le sol. Il peut dire « il y a un tout petit peu de reflet », « il y en a moyen », « il y en a beaucoup »… comme un robinet qui donne toutes les valeurs entre fermé et ouvert.

Le monde digital
C’est comme une lumière ON/OFF : allumée ou éteinte. Point. Pas de « un peu allumée ».

Une LED, c’est ça : soit elle est allumée (1), soit elle est éteinte (0). Comme un interrupteur de chambre.

Utilise les blocs d’instructions de la micro:bit dont ceux « opération de la goupille« . Le dispositif photo réflecteur IR est un dispositif analogique. Tu vas mesurer la quantité de lumière réfléchi sur le bloc noir puis sur ton doigt.

Utilise l’instruction read analog pin P0 puisque tu as installé le photo réflecteur sur P0 :

Puis une instruction serial string pour envoyer la mesure vers ton PC dans la console de Min+ :

Assemble les deux instructions pour les positionner dans ta boucle pour toujours :

Avant de télécharger ton programme allume la carte d’extension :

Puis demande à télécharger ton programme :

Clic sur le bouton Code en haut à droite pour ouvrir les deux onglets dont celui « Génération automatique » :

Tu constate que ton téléversement est réussi ou pas, puis clic sur cette icone :

Tu constateras l’affichage dans la console du nombre de quantité de lumière réfléchi par le bloc noir :

Puis pose ton doigt entre le bloc noir et le photo réflecteur IR, tu constates un changement dans les valeurs retournées : autour de 120 et maintenant autour de 400 :

Pose un bloc blanc à la place de ton doigt, tu constates encore un changement des valeurs retournées :

En savoir plus sur le photoréflecteur

Un photoréflecteur est un capteur qui détecte la présence d’un objet en utilisant la lumière. Il émet une lumière (généralement infrarouge que tu ne vois pas) et mesure la quantité de lumière réfléchie. Lorsque tu places ton doigt devant le photoréflecteur, la lumière est réfléchie différemment, ce qui permet au capteur de détecter la présence de ton doigt.

Programme le photoréflecteur

Programme ton photoréflecteur pour allumer la LED sur la présence de ton doigt.

Ton objectif : allumer la LED quand le photoréflecteur retourne une valeur détectant la présence du doigt sinon éteindre la LED

Utilise les instruction de contrôle pour tester la valeur de retour du photoréflecteur.

Utilise l’instruction de condition SI SINON

Maintenant pour comparer la valeur retournée par le photo réflecteur IR utilise l’opérateur mathématique > :

Puis récupère les valeurs retourbées par le photoréflecteur :

Décide d’allumer la LED quand les valeurs retournées par le photo réflecteur IR sont supérieures à la valeur de 400 :

Puis clic sur le menu en haut de l’écran et choisis Télécharger :

Tant que le message de transfert est affiché ne touche pas au microcontrôleur ni au câble USB.

Il disparaîtra, une fois le transfert terminé.

Teste avec ton doigt :

Catégories
DFROBOT MICRO:BIT Robotique UNIHIKER

Activer un moteur CC

Ton matériel

Un microcontrôleur la MICRO:BIT

Une carte d’extension

Cette carte d’extension modulable est conçue pour du prototypage et des projets robotiques. Elle combine un pilote de moteur, une alimentation avec support pour accu Li-Ion 18650 et de nombreuses E/S Gravity.

Alimentation :

  • Support pour batterie 18650 (3,7 Vcc) avec circuit de charge intégré et indicateur de niveau à LEDs.
  • Entrée USB Type-C (5 Vcc) : pour alimentation externe et recharge.
  • LEDs : état de charge, niveau de batterie, alimentation et sens des moteurs.

Moteurs et servomoteurs :

  • Driver pour 4 moteurs CC (1,5 A maxi par moteur) à connecter sur borniers à vis.
  • 6 sorties servomoteurs sur connecteurs 3 contacts

Interfaces :

  • 8 broches GPIO via le connecteur Edge des cartes micro:bit ou Unihiker.
  • 6 broches GPIO supplémentaires : digitale et analogique.
  • Bus I2C : trois interfaces à 3,3 V et une interface I2C en 5 V.
  • Interface pour capteurs à ultrasons 5 V (Trigger et Echo). 

Autres : 2 x LEDs RGB programmables (WS2812) et émetteur/récepteur infrarouge


Extension du logiciel avec la carte micro:bit et la carte d’extension dans MIND+

Cliquer sur l’icone.

Clic sur retour en haut à gauche de l’écran

Maintenant tu as deux extensions : une pour la micro:bit et l’autre pour la carte d’extension


Branche ton moteur CC

Instructions pour brancher le moteur CC

Insère le câble rouge dans la broche blanche correspondant au câble rouge du moteur puis le câble noir dans celui correspondant au câble du moteur :

Puis le câble rouge dans le bornier M1+, puis le câble noir dans le bornier M1- :

Utilise un tourne vis pour fixer les câbles :

  1. Branchement du moteur :
    • Fil rouge dans le bornier M1+.
    • Fil noir dans le bornier M1-.
    • Assure-toi de ne pas inverser les couleurs sur les deux bornier + et –
  2. Alimentation :
    • Assure-toi que l’interrupteur de la carte d’extension est bien en position ON. Le moteur nécessite plus d’énergie que celle fournie par la liaison USB, donc une source d’alimentation externe est nécessaire.

Programme le moteur

Pour programmer le moteur, tu disposes de trois instructions principales :

  1. Déclaration de la puissance du moteur :
    • Tu peux définir la puissance du moteur, sous la forme d’un chiffre de 0 à 255.
  2. Sens de la marche :
    • Forward pour faire tourner le moteur dans le sens des aiguilles d’une montre -> avancer.
    • Backward pour faire tourner le moteur dans le sens inverse des aiguilles d’une montre -> reculer.
  3. Arrêter le moteur :
    • Définis la puissance du moteur à zéro -> 0.
  • Avancer -> forward
  • Reculer – backward

Puissance du moteur : 0 arrêt

Quand tu utilises les moteurs, commence toujours sous l’étiquette micro:bit starts par cette instruction qui remet tous les moteurs à l’arrêt:

Programme pour tester ton moteur avec le bouton A pour lancer ton moteur et le bouton B pour l’arrêter :

Teste le bon fonctionnement du moteur

Puis clic sur le menu en haut de l’écran et choisis Télécharger :

Tant que le message de transfert est affiché ne touche pas au microcontrôleur ni au câble USB.

Il disparaîtra, une fois le transfert terminé.

En savoir plus sur le moteur à courant continu CC

Un moteur à courant continu (CC) est un dispositif qui convertit l’énergie électrique en énergie mécanique. Il est alimenté par un courant continu et est utilisé dans une variété d’applications, des jouets aux robots. Le sens de rotation du moteur peut être inversé en changeant la polarité de l’alimentation.

Catégories
DFROBOT MICRO:BIT Robotique UNIHIKER

Activer un servomoteur

Ton matériel

Un microcontrôleur la MICRO:BIT

Une carte d’extension

Cette carte d’extension modulable est conçue pour du prototypage et des projets robotiques. Elle combine un pilote de moteur, une alimentation avec support pour accu Li-Ion 18650 et de nombreuses E/S Gravity.

Alimentation :

  • Support pour batterie 18650 (3,7 Vcc) avec circuit de charge intégré et indicateur de niveau à LEDs.
  • Entrée USB Type-C (5 Vcc) : pour alimentation externe et recharge.
  • LEDs : état de charge, niveau de batterie, alimentation et sens des moteurs.

Moteurs et servomoteurs :

  • Driver pour 4 moteurs CC (1,5 A maxi par moteur) à connecter sur borniers à vis.
  • 6 sorties servomoteurs sur connecteurs 3 contacts

Interfaces :

  • 8 broches GPIO via le connecteur Edge des cartes micro:bit ou Unihiker.
  • 6 broches GPIO supplémentaires : digitale et analogique.
  • Bus I2C : trois interfaces à 3,3 V et une interface I2C en 5 V.
  • Interface pour capteurs à ultrasons 5 V (Trigger et Echo). 

Autres : 2 x LEDs RGB programmables (WS2812) et émetteur/récepteur infrarouge


Extension du logiciel avec la carte micro:bit et la carte d’extension dans MIND+

Cliquer sur l’icone.

Clic sur retour en haut à gauche de l’écran

Maintenant tu as deux extensions : une pour la micro:bit et l’autre pour la carte d’extension


Instructions pour brancher le servomoteur

  1. Branchement du servomoteur :
    • Insère le connecteur du servomoteur dans le port S0 de la carte d’extension, câble gris sur le connecteur jaune orangé..
    • Assure-toi que le câble gris est sur le connecteur jaune orangé pour garantir que le signal est correctement transmis.
  2. Alimentation :
    • Assure-toi que l’interrupteur de la carte d’extension est bien en position ON. Le servomoteur nécessite plus d’énergie que celle fournie par la liaison USB, donc une source d’alimentation externe est nécessaire.

Câble du servomoteur sur le connecteur S0 :

Câble gris sur connecteur jaune orangé :

Bouton interrupteur sur ON :

Programmation du servomoteur

Pour programmer le servomoteur, tu disposes d’une instruction principale pour définir l’angle de rotation :

Tu peux définir l’angle de rotation du servomoteur, généralement entre 0° et 180°. Par exemple, Régler le servomoteur D9 à 90 pour positionner le servomoteur à 90°.

Définir l’angle de rotation :

Le choix du connecteur :

Pour faire bouger ton servomoteur de 0 à 180 degrés, crée une variable angle :

Puis une boucle pour faire bouger ton servomoteur :

En faisant évoluer la variable angle de 0° à 180° :

Puis teste la fin de la boucle :

Et également pour de 180° à ° :

Programme le servomoteur lorsque tu appuies sur le bouton A angle à 180° ou sur le bouton B angle à 0° :

Programme ton servomoteur avec « Mes blocs » pour appeler un seul bloc afin de donner l’angle que tu souhaites à ton servomoteur;

Ton objectif :

Crée un bloc servomoteur :

Crée un paramètre, un nombre float, pour définir l’angle cible à atteindre :

Définis le nom de ton paramètre : angle_cible :

Change ton programme pour une cible avec le bouton A à 180 et une cible avec le bouton B à 0 :

Maintenant tu peux changer les valeurs de tes cibles à atteindre :

Catégories
DFROBOT MICRO:BIT Robotique UNIHIKER

Programmer le Joystick

Module joystick Gravity DFRobot basé sur 2 potentiomètres (axes X et Y) et d’un bouton-poussoir pour des applications spécifiques. Il délivre deux sorties analogiques en fonction de la position des deux potentiomètres et une sortie logique en fonction du bouton-poussoir.

Il se raccorde sur deux entrées analogiques et une entrée logique directement sur le Board pour UNIHIKER K10, M10 et micro:bit. Cette carte d’extension modulable est conçue pour du prototypage et des projets robotiques. Elle combine un pilote de moteur, une alimentation avec support pour accu Li-Ion 18650 et de nombreuses E/S Gravity.

Alimentation :

  • Support pour batterie 18650 (3,7 Vcc) avec circuit de charge intégré et indicateur de niveau à LEDs.
  • Entrée USB Type-C (5 Vcc) : pour alimentation externe et recharge.
  • LEDs : état de charge, niveau de batterie, alimentation et sens des moteurs.

Moteurs et servomoteurs :

  • Driver pour 4 moteurs CC (1,5 A maxi par moteur) à connecter sur borniers à vis.
  • 6 sorties servomoteurs sur connecteurs 3 contacts

Interfaces :

  • 8 broches GPIO via le connecteur Edge des cartes micro:bit ou Unihiker.
  • 6 broches GPIO supplémentaires : digitale et analogique.
  • Bus I2C : trois interfaces à 3,3 V et une interface I2C en 5 V.
  • Interface pour capteurs à ultrasons 5 V (Trigger et Echo). 

Autres : 2 x LEDs RGB programmables (WS2812) et émetteur/récepteur infrarouge

installation du logiciel de la carte d’extension dans MIND+

Cliquer sur l’icone.

Installe les trois câbles du joystick sur les 3 premières broches GPIO supplémentaires C0, C1, C2 : digitale et analogique.

  • C0 déplacement en X (0 à 4095, autour de 2000 en repos)
  • C1 déplacement en Y (0 à 4095, autour de 2000 en repos)
  • C2 appui sur le Joystick (1 repos, 0 appuié)

Exemple de programme pour récupérer les évènements du Joystick :

Catégories
DFROBOT MAQUEENPLUS MICRO:BIT Robotique UNIHIKER

Programmer le Gamepad

Passez en mode téléchargement : tous les projets de cet atelier utilisent ce mode.

Ajouter la carte de contrôle principale : « Extensions » > « Cartes » > « MICRO:BIT »

Ajoutez la carte d’extension : « Extensions » > «User Ext » > «gamepad »

Le Gamepad pourra envoyer des commandes en fonction des boutons et du joystick.

Pour cela active la radio et définit le canal 1 à 7 (à adapter selon ton groupe).

Pour détecter les boutons pressés du gamepad, tu vas utiliser les instructions suivantes :

Pour programmer le joystick

Utilise cette instruction pour capter les mouvements du Joystick :

  • Si X a une valeur comprise de 550 à 900, le joystick à droite
  • Si X a une valeur comprise de 0 à 450, le joystick à gauche
  • Si Y a une valeur comprise de 550 à 900, le joystick vers le haut
  • Si Y a une valeur comprise de 0 à 450, le joystick vers le bas

Si cette instruction est vraie alors un appui sur le joystick :

Capter les boutons de couleur

Utilise cette instruction pour capter les quatre boutons :

Capter les boutons A et B de la micro:bit

Utilise cette instruction pour capter les boutons A ou B :

Pour faire vibrer le gamepad

Catégories
DFROBOT MICRO:BIT Robotique UNIHIKER

HUSKYLENS 2

Ce guide complet explique comment intégrer HUSKYLENS 2 à micro:bit ou avec la carte UNIHIKER K10 à l’aide du logiciel MindPlus. Il aborde la configuration matérielle, le chargement des bibliothèques et propose des exemples de programmes pour la reconnaissance faciale, le suivi d’objets, etc.

Des instructions détaillées et des illustrations permettent aux utilisateurs de créer des projets innovants de reconnaissance visuelle.

1. Introduction

1.1 Introduction à Mind+

Mind+ est un logiciel de programmation destiné aux jeunes et basé sur Scratch 3.0. Il prend en charge la programmation graphique et la programmation par code pour diverses plateformes matérielles open source telles qu’Arduino, UNIHIKER K10 et UNIHIKER M10. Les utilisateurs peuvent réaliser des programmes simplement en glissant-déposant des blocs, et peuvent également utiliser des langages de programmation avancés tels que Python, C et C++, permettant ainsi à chacun de découvrir facilement le plaisir de la création.

Site officiel de Mind+ : https://mindplus.cc

1.2 Introduction au micro:bit

Le micro:bit est un micro-ordinateur conçu spécifiquement pour l’apprentissage de la programmation chez les enfants et pour l’application des technologies à la résolution de problèmes concrets. HUSKYLENS 2 est compatible avec le micro:bit V2, permettant ainsi le développement de divers projets de reconnaissance visuelle intéressants grâce à la programmation graphique.

Lien d’achat du micro:bit V2 : https://www.dfrobot.com/product-2125.html

2. Préparation du matériel

Matériel de préparation

2.1 Schéma de câblage

Insérez la carte micro:bit dans la carte d’extension compatible FR1616 :

Connectez la carte d’extension à l’ordinateur à l’aide du câble USB-C. Insérez le câble capteur I2C/UART à 4 broches dans l’interface I2C/UART du HUSKYLENS 2, puis connectez l’autre extrémité au port « 5V Huskylens » de la carte d’extension.

Le HUSKYLENS 2 nécessitant une alimentation spécifique, utilisez un câble USB-C supplémentaire pour le connecter à son port d’alimentation, l’autre extrémité étant connectée à un ordinateur ou à une alimentation compatible.

Veuillez vous référer à l’image suivante pour le schéma de câblage :

Diagramme d'interface

3. Préparation du logiciel

3.1 Charger la bibliothèque HUSKYLENS 2 dans MindPlus 2.0

Double-cliquez pour ouvrir Mind+. Une fois ouvert, repérez « Extensions » en bas à gauche et  cliquez sur « Extensions ».

Lors de la première utilisation, vous devrez télécharger la bibliothèque d’extension du contrôleur principal correspondant.

Recherchez « micro:bit » et cliquez sur l’image de la micro:bit :

Puis cliquez sur User-ext pour charger l’extension logiciel de la carte d’extension :

Charger la bibliothèque drobot_yeezb-unihikerioexpansion-thirdex-V0.0.6.mpext sous 123codage\10 PROJETS ROBOTIQUES\CARTE EXTENSION DF ROBOT :

Cliquez sur l’image de l’extension puis vous validez :

Cliquez sur « Capteur », recherchez « HUSKYLENS 2 » dans la zone de recherche, appuyez sur Enter, trouvez la bibliothèque d’extension « HUSKYLENS 2 », puis cliquez pour charger la bibliothèque d’extension.

Puis demandez le retour :

Vous obtenez les extensions suivantes :

Exemple de projet de reconnaissance faciale

Détection des visages et exportation des données associées

Grâce à la fonction de reconnaissance faciale , lorsqu’un visage apparaît sur l’écran du HUSKYLENS 2, il est détecté et encadré. Vous pouvez ainsi obtenir des données telles que le nombre total de visages détectés. Ces données comprennent l’identifiant du visage, les coordonnées X/Y de son centre, ainsi que sa largeur et sa hauteur.

Le programme d’exemple est présenté ci-dessous.

Puis chaque visage :

Catégories
MICRO:BIT Robotique UNIHIKER

Carte expansion M10 FR1216-1

Cette platine Expansion Board pour UNIHIKER K10, M10 et micro:bit est une carte d’extension modulable conçue pour du prototypage et des projets robotiques. Elle combine un pilote de moteur, une alimentation avec support pour accu Li-Ion 18650 et de nombreuses E/S Gravity.

Carte d’extension chez DFROBOT

Dépôt pour l’extension logicielle pour Mind+

Alimentation :

  • Support pour batterie 18650 (3,7 Vcc) avec circuit de charge intégré et indicateur de niveau à LEDs.
  • Entrée USB Type-C (5 Vcc) : pour alimentation externe et recharge.
  • LEDs : état de charge, niveau de batterie, alimentation et sens des moteurs.

Moteurs et servomoteurs :

  • Driver pour 4 moteurs CC (1,5 A maxi par moteur) à connecter sur borniers à vis.
  • 6 sorties servomoteurs sur connecteurs 3 contacts

Interfaces :

  • 8 broches GPIO via le connecteur Edge des cartes micro:bit ou Unihiker.
  • 6 broches GPIO supplémentaires : digitale et analogique.
  • Bus I2C : trois interfaces à 3,3 V et une interface I2C en 5 V.
  • Interface pour capteurs à ultrasons 5 V (Trigger et Echo). 

Autres : 2 x LEDs RGB programmables (WS2812) et émetteur/récepteur infrarouge

installation du logiciel de la carte d’extension dans MIND+

Cliquer sur l’icone.

Broches analogiques et numériques GPIO

Contrôle des 8 broches GPIO via le connecteur Edge des cartes :

Exemple pour un bouton poussoir sur pin14 et une led sur pin13.

Bouton poussoir sur le pin14 :

LED sur le pin13 :

Utiliser ces instructions pour contrôler ces deux dispositifs digitaux :

La LED s’allume lorsque l’on appuie sur le bouton poussoir.

Exemple pour les 6 broches GPIO supplémentaires : digitale et analogique.

Utiliser les instructions :

Exemple de programme pour allumer un dispositif digital comme une LED :

Exemple pour lire un dispositif analogique, par exemple un phototoreflector :

Piloter un servomoteur

Piloter 4 moteurs