Catégories
MICRO PYTHON MICRO:BIT

Eclairage vélo

Objectif :

Réalisation d’un programme pour gérer un éclairage arrière personnalisable d’un vélo.

Tu apprendras l’utilisation des boucles For, l’importation des bibliothèques, l’utilisation des tableaux (liste) en python.

Mission :

  • Affichage d’une animation fixe.
  • Affichage d’une animation dynamique
  • Accélération de l’animation par le bouton A
  • Allumer ou éteindre l’animation par le bouton B

A découvrir avant de commencer : l’instruction For

En Python, la boucle for est utilisée pour itérer sur une séquence d’éléments tels qu’une liste, une chaîne de caractères, un tuple, un dictionnaire, etc. La boucle for exécute un bloc de code pour chaque élément de la séquence, jusqu’à ce que tous les éléments aient été parcourus.

for element in sequence:

    # Bloc de code à exécuter

Une explication pour chaque élément de la syntaxe :

  • Le mot-clé for est utilisé pour déclarer la boucle for.
  • element est une variable qui représente l’élément en cours d’itération dans la séquence. Tu peux choisir n’importe quel nom valide pour cette variable.
  • Le mot-clé in est utilisé pour spécifier la séquence sur laquelle itérer.
  • sequence est la séquence (liste, chaîne de caractères, tuple, etc.) sur laquelle la boucle for va itérer.
  • Le bloc de code qui suit le : est indenté et sera exécuté pour chaque élément de la séquence.

Voici un exemple simple pour illustrer son fonctionnement :

liste = [1, 2, 3, 4, 5]

for nombre in liste:

    print(nombre)  # Affiche chaque nombre de la liste

Utilise les indications suivantes pour réaliser ton premier programme. Reproduit le code en caractère GRAS avec l’icône « REPL »

Dans cet exemple, la boucle for itère sur chaque élément de la liste liste. À chaque itération, la variable nombre prend la valeur de l’élément en cours d’itération, puis le bloc de code indenté est exécuté, qui dans ce cas affiche le nombre. La boucle for se répète jusqu’à ce que tous les éléments de la liste aient été parcourus.

Il est également courant d’utiliser la fonction range() en conjonction avec la boucle for pour itérer sur une séquence de nombres. Par exemple :

for i in range(1, 6):

    print(i)  # Affiche les nombres de 1 à 5

Dans cet exemple, range(1, 6) génère une séquence de nombres de 1 à 5 (la borne supérieure spécifiée est exclusive). La boucle for itère sur chaque nombre de cette séquence, assigne la valeur à la variable i, puis exécute le bloc de code indenté.

QCM sur l’instruction FOR

Question 1 : Quelle est la principale utilité de l’instruction « for » en programmation ?

  1. Déclarer une fonction
  2. Répéter une séquence d’instructions un nombre fixe de fois
  3. Gérer les exceptions
  4. Établir une condition logique

Question 2 : Comment se structure généralement une boucle « for » en Python ?

  1. for (int i = 0; i < 5; i++)
  2. for i in range(5):
  3. loop (i = 0; i < 5; i++)
  4. repeat i in range(5):

Question 3 : Quelle est la fonction de la fonction intégrée range() utilisée fréquemment avec une boucle « for » en Python ?

  1. Générer une liste de nombres
  2. Calculer la moyenne d’une série de nombres
  3. Vérifier la présence d’un élément dans une liste
  4. Créer une chaîne de caractères

Question 4 : Comment peut-on sortir prématurément d’une boucle « for » en utilisant l’instruction « break » ?

  1. break for
  2. break loop
  3. exit for
  4. break

Question 5 : Quel mot-clé est utilisé pour passer à l’itération suivante dans une boucle « for » sans exécuter le reste du code à l’intérieur de la boucle ?

  1. pass
  2. skip
  3. continue
  4. next

Mu est un éditeur de code spécialement conçu pour les microcontrôleurs et plus particulièrement la carte micro :bit.

Utilise le menu « Mode » dans Mu pour choisir « BBC micro ».

Ainsi, tu pourras flasher directement ton programme sur la carte micro.

Choisis de créer un nouveau fichier pour ton programme en cliquant sur « + nouveau », puis demande à enregistrer ton fichier sous le nom de ton choix en utilisant l’option « Enregistrer ».

from microbit import display

Une bibliothèque en Python est comme une boîte à outils magique pleine de fonctions spéciales que tu peux utiliser pour rendre ton programme plus puissant. Plutôt que de tout créer depuis zéro, tu peux simplement emprunter des outils déjà créés par d’autres personnes.

Pour utiliser une bibliothèque, tu dois l’importer dans ton programme. C’est comme si tu vas à la bibliothèque emprunter un livre pour le lire. En Python, nous utilisons le mot clé import pour faire cela.

Tu vas utiliser deux bibliothèques une pour gérer ton robot et une autre pour gérer le temps.

#Double boucle pour afficher l’éclairage

for c in range(0, 5, 1):
for l in range(0, 5, 1):
      display.set_pixel(c, l, 9)

 Flasher puis bouton reset pour lancer puis vérifie que toutes les LEDs s’affichent

from microbit import display
import time
#Boucle sur la colonne pour afficher une LED
for c in range(0, 5, 1):
    #Boucle sur la ligne pour afficher une LED
    for l in range(0, 5, 1):
        #Attente de 300 millisecondes avant affichage de la LED
        time.sleep_ms(300)
        #Affichage de la LED
        display.set_pixel(c, l, 9)

Flasher puis bouton reset puis regarde comment fonctionne les LEDs

Amuse-toi en changeant les valeurs des boucles !!!

  • for c in range(2, 5, 1):
  • for c in range(0, 3, 1):
  • for c in range(0, 5, 2):
  • for l in range(2, 5, 1):
  • for l in range(0, 3, 1):
  • for l in range(0, 5, 2):

 Flasher puis bouton reset entre chaque essai

Question 1 : Comment peut-on importer une bibliothèque entière en Python ?

  1. import microbit
  2. include microbit
  3. import mocrobit as lib
  4. import microbit

Question 2 : Quelle est la méthode recommandée pour importer uniquement une fonction spécifique d’une bibliothèque en Python ?

  1. import display from microbit
  2. from microbit import display
  3. include display from microbit
  4. import microbit.display

Question 3 : Comment peut-on vérifier l’existence d’une bibliothèque dans l’éditeur MU ?

  1. Vérification manuelle dans le répertoire Python
  2. Import library, puis vérification avec la fonction library.check()
  3. Utiliser la commande pip show library dans le terminal
  4. Vérification avec le bouton du menu « REPL » puis saisir import microbit

Question 4 : Comment connaitre toutes les fonctions d’une bibliothèque dans l’éditeur MU ?

  1. List microbit.Image
  2. print microbit.Image
  3. dir(microbit.Image)
  4. show(microbit.Image )
from microbit import display, button_a, button_b

while not(button_a.is_pressed()) and not(button_b.is_pressed()):
            pass

while button_a.is_pressed() or button_b.is_pressed():
            pass

L’instruction while permet de répéter un ensemble d’instructions tant qu’une condition donnée est vraie. Elle est très utile lorsque tu as besoin d’exécuter le même code à plusieurs reprises sans avoir à le réécrire plusieurs fois.

while condition:
    # Code à exécuter tant que la condition est vraie

#Effacement de l’écran
display.clear()

Flasher puis bouton reset et vérifie que l’écran s’éteint après l’appui sur le bouton A ou B

display.set_pixel(…) et button_a.is_pressed() sont des objets de la bibliothèque microbit La première permet d’allumer ou éteindre une LED, la deuxième permet de contrôler les boutons A et B

L’instruction print est un outil précieux pour le débogage en programmation. Elle permet d’afficher des informations spécifiques à un moment donné de l’exécution du programme, ce qui permet de comprendre ce qui se passe dans ton code. Voici pourquoi elle est utile et comment l’utiliser pour le débogage :

Intérêt d’utiliser l’instruction print pour le débogage :

  1. Visualisation des données : La principale utilité de print est d’afficher des valeurs de variables, des résultats intermédiaires, ou des messages pour comprendre comment votre programme évolue pendant son exécution.
  2. Vérification des états : Vous pouvez utiliser print pour vérifier l’état des variables à différents points de votre code, ce qui te permet de confirmer que les valeurs sont correctes ou de repérer des erreurs potentielles.
  3. Traçage de l’exécution : En insérant des déclarations print aux différentes étapes de votre programme, tu peux « tracer » l’exécution du code et suivre son flux pour identifier les erreurs ou les incohérences.

Un exemple :

a = 10
print("La valeur de 'a' est :", a)

Tu veux rendre l’éclairage arrière de ton vélo super cool et dynamique !

Pour cela, tu veux créer un programme avec la carte micro:bit. Voici ce que ton éclairage puisse faire :

  1. Choisir ton Animation : Tu veux pouvoir appuyer sur le bouton A pour changer le style de l’éclairage. Chaque fois que tu appuies, l’éclairage passe à une nouvelle animation dynamique.
  2. Mettre en Veille : Si tu veux éteindre l’éclairage (peut-être quand tu es à l’arrêt ou que tu n’en as pas besoin), tu appuies sur le bouton B.
  3. Automatique : si la lumière du jour est trop forte ou si tu n’as pas bougé pendant un moment, l’éclairage doit se mette en veille tout seul pour économiser.

Maintenant, imagine les animations magiques que tu veux ! Ça pourrait être des clignotements, des pulsations ou même un effet de fondu. Quand tu as une idée, tu peux réaliser ton programme pour rendre ton vélo encore plus génial !

Objectif de la Mission : Programmer ton éclairage pour allumer toutes les LEDs en une seule fois.

#Bibliothèques à utiliser
from microbit import display, button_a, button_b
import time

Une fonction en programmation est une manière de regrouper un ensemble d’instructions pour effectuer une tâche spécifique.

def ma_fonction():
    # Corps de la fonction

C’est ainsi tu écris et utilises des fonctions en Python. Elles te permettent d’organiser ton code, de le rendre plus lisible et de le rendre plus réutilisable.

#Fonction pour l'affichage du tableau de LEDs
def affichage():
    for c in range(0, 5, 1):
        for l in range(0, 5, 1):
            time.sleep_ms(300)
            display.set_pixel(c, l, 9)

while True:
    #Appel de la fonction d'affichage
    affichage()
    #Attente de l'appui du bouton A ou B
    while not(button_a.is_pressed()) and not(button_b.is_pressed()):
        pass
    while button_a.is_pressed() or button_b.is_pressed():
        pass

    #Effacement de l'écran
    display.clear()

 Flasher puis bouton reset et constate l’animation.

En Python, les tableaux sont appelés des listes, et ils permettent de stocker une collection ordonnée d’éléments. Les listes peuvent contenir des valeurs de différents types tels que des entiers, des flottants, des chaînes de caractères, et même d’autres listes.

Création d’une liste : Pour créer une liste, tu utilises des crochets [] puis tu sépares les éléments de la liste par des virgules. Par exemple :

liste = [1, 2, 3, 4, 5]

Accès aux éléments d’une liste : Les éléments d’une liste sont accessibles par leur indice, qui commence à 0 pour le premier élément. Tu peux utiliser cet indice entre crochets [] pour accéder à un élément spécifique. Par exemple :

premier_element = liste[0]  # Accès au premier élément
deuxieme_element = liste[1]  # Accès au deuxième élément
print(premier_element)
print(deuxieme_element)

Modification d’un élément de liste : Les éléments d’une liste sont mutables, ce qui signifie qu’ils peuvent être modifiés. Tu peux assigner une nouvelle valeur à un élément spécifique en utilisant son indice. Par exemple :

liste[0] = 10  # Modification du premier élément
print(liste[0])

Longueur d’une liste : Tu peux obtenir la taille (le nombre d’éléments) d’une liste en utilisant la fonction len(). Par exemple :

taille = len(liste)  # Renvoie la taille de la liste
print(taille)

Opérations sur les listes : Les listes en Python offrent de nombreuses opérations utiles, telles que l’ajout d’éléments avec append(), l’insertion d’éléments avec insert(), la suppression d’éléments avec remove() ou del, la concaténation de listes avec +, etc. Tu peux également utiliser des boucles pour itérer sur les éléments d’une liste.

Voici un exemple illustrant certains de ces concepts :

liste = [1, 2, 3, 4, 5]  # Création d'une liste
print(liste[0])  # Affiche le premier élément (1)

liste[2] = 10  # Modification du troisième élément
print(len(liste))  # Affiche la taille de la liste (5)

liste.append(6)  # Ajout d'un nouvel élément (6) à la fin de la liste

for element in liste:
    print(element)  # Affiche chaque élément de la liste

print(len(liste))  # Affiche la taille de la liste (6)

Objectif de la Mission : Programmer ton éclairage pour allumer toutes les LEDs avec l’aide d’un tableau python !

Tu vass utiliser un tableau contenant des chiffres qui correspondent à l’état des LEDs. Ainsi, quand tu changes ces chiffres, tu contrôles la lumière de ton vélo !

from microbit import display, button_a, button_b
import time

# Tableau pour décrire le tableau des LEDs
affichageFixe_1 = [[9, 7, 5, 7, 9],
                   [7, 1, 1, 1, 7],
                   [5, 1, 9, 1, 5],
                   [7, 1, 1, 1, 7],
                   [9, 7, 5, 7, 9]]

# Fonction pour l'affichage du tableau de LEDs
def affichage(animation,vitesse=300):
    l = 0
    for ligne in animation:
        c = 0
        for led in ligne:
            time.sleep_ms(vitesse)
            display.set_pixel(c, l, led)
            c+= 1
        l+= 1

while True:
    # Appel de la fonction d'affichage
    affichage(affichageFixe_1,50)
    # Attente de l'appui du bouton A ou B
    while not(button_a.is_pressed()) and not(button_b.is_pressed()):
        pass

    while button_a.is_pressed() or button_b.is_pressed():
        pass

    # Effacement de l'écran
    display.clear()

 Flasher puis bouton reset et change les valeurs dans le tableau : affichageFixe_1 !!

Objectif de la Mission : Programmer plusieurs tableaux pour créer une animation dynamique !

from microbit import display, button_a, button_b
import time

# Tableau pour décrire le tableau des LEDs
affichageFixe_1 = [[9, 7, 5, 7, 9],
                   [7, 1, 1, 1, 7],
                   [5, 1, 9, 1, 5],
                   [7, 1, 1, 1, 7],
                   [9, 7, 5, 7, 9]]

affichageFixe_2 = [[0, 0, 9, 0, 0],
                   [0, 0, 9, 0, 0],
                   [9, 9, 9, 9, 9],
                   [0, 0, 9, 0, 0],
                   [0, 0, 9, 0, 0]]

affichageFixe_3 = [[9, 0, 0, 0, 9],
                   [0, 9, 0, 9, 0],
                   [0, 0, 9, 0, 0],
                   [0, 9, 0, 9, 0],
                   [9, 0, 0, 0, 9]]

affichageDynamique = [affichageFixe_1,
                      affichageFixe_2,
                      affichageFixe_3]

# Fonction pour l'affichage du tableau de LEDs
def affichage(animation, vitesse=300):
    l = 0
    for ligne in animation:
        c = 0
        for led in ligne:
            time.sleep_ms(vitesse)
            display.set_pixel(c, l, led)
            c += 1
        l += 1
animation = 0

while True:
    # Appel de la fonction d'affichage
    affichage(affichageDynamique[animation], 10)
    # Attente de l'appui du bouton A ou B
    while not(button_a.is_pressed()) and not(button_b.is_pressed()):
        pass

    while button_a.is_pressed() or button_b.is_pressed():
        pass

    # Effacement de l'écran
    display.clear()
    animation += 1

    if animation >= (len(affichageDynamique)):
        animation = 0

 Flasher puis bouton reset et créer ton affichage dynamique !! 

Objectif de la Mission : Gère dans ton tableau d’animation des temps pour l’affichage des LEDs pour des animations spectaculaires !

affichageDynamique = [[affichageFixe_1, 10],
                      [affichageFixe_2, 200],
                      [affichageFixe_3, 500]]

def affichage(animation, vitesse=300):
    l = 0
    for ligne in animation:
        c = 0
        for led in ligne:
            if led > 0 :
                time.sleep_ms(vitesse)
            display.set_pixel(c, l, led)
            c += 1
        l += 1

et dans le corps principal du programme :

    affichage(affichageDynamique[animation][0],affichageDynamique[animation][1]) 

Flasher puis bouton reset

Question 1 : Quel est le terme utilisé en Python pour désigner une structure de données ordonnée qui peut contenir différents types d’éléments ?

  1. Ensemble
  2. Liste
  3. Dictionnaire
  4. Tuple

Question 2 : Comment créer une liste vide en Python ?

  1. list()
  2. empty_list()
  3. []
  4. new List()

Question 3 : Comment accéder au deuxième élément d’une liste en Python (en supposant que l’index commence à 0) ?

  1. list[1]
  2. list(2)
  3. list[2]
  4. list.second()

Question 4 : Comment déclarer une liste de listes ?

  1. [[],[],[]…]
  2. {(),(),()…}
  3. new List(new List, new List …)
  4. ((),(),()…)

Question 5 : Comment accéder au deuxième élément d’une liste en Python qui est elle-même dans la troisième occurrence d’une liste (en supposant que les index des listes commencent à 0) ?

  1. List[2][1]
  2. list(2,1)
  3. list[3][2]
  4. list[3,1]

Objectif de la Mission : Programme l’enchainement dynamique des tableaux lumineux en utilisant la bibliothèque time et en créant un chronomètre de temps pour gérer la durée d’affichage d’un écran.

# Départ du chronomètre       
def start_chronometre(temps):
    global start
    global chrono
    start = time.ticks_ms()
    chrono = temps

# Si temps du chronomètre écoulé   
def chronometre():
    if time.ticks_ms() - start > chrono :
        return True
    else:
        return False

animation = 0
while True:
    # Appel de la fonction d'affichage
    affichage(affichageDynamique[animation][0], affichageDynamique[animation][1])
    start_chronometre(1000)

    # Attente de l'appui du bouton A ou B ou le chronometre
    while (not (button_a.is_pressed()) and
           not (button_b.is_pressed()) and
           not (chronometre())):
        pass

    while button_a.is_pressed() or button_b.is_pressed():
        pass

    # Effacement de l'écran
    display.clear()
    animation += 1
    if animation >= (len(affichageDynamique)):
        animation = 0

 Flasher puis bouton reset

Les variables globales en programmation avec Python sont des variables qui sont définies en dehors de toute fonction et qui peuvent être utilisées à n’importe quel endroit du programme. Elles sont accessibles de manière globale, d’où leur nom.

Tu peux utiliser des variables globales lorsque tu as besoin de partager des données entre plusieurs parties de ton programme et que ces données ont une signification globale. Cependant, évite de les utiliser pour des données temporaires ou des valeurs qui ne sont pertinentes que dans des parties spécifiques de ton code. Cela aidera à rendre ton code plus lisible et à éviter des erreurs potentielles.

Si la même information est utilisée dans plusieurs fonctions, une variable globale peut être pratique. Cela évite de passer la valeur en paramètre à chaque appel de fonction.

Si tu as une valeur constante qui reste la même tout au long du programme, tu peux la définir comme une variable globale plutôt que de la répéter à chaque fois dans différentes fonctions.

Objectif de la Mission : Programme pour que le temps d’affichage d’un écran soit variable et que l’on puisse le programmer dans le tableau d’affichage dynamique.

affichageDynamique = [[affichageFixe_1, 10,1000],
                      [affichageFixe_2, 200,5000],
                      [affichageFixe_3, 500,10000]]

 start_chronometre(affichageDynamique[animation][2])

 Flasher puis bouton reset

Objectif de la Mission : Donne la possibilité de créer plusieurs animations et que l’utilisateur puisse changer d’animation avec le bouton A.

"""
Enchainement de plusieurs affichages fixes :
 - affichage fixe
 - temps de l'affichage des LEDs de l'affichage fixe
 - temps entre chaque affichage fixe
"""

affichageDynamique_1 = [[affichageFixe_1, 10, 1000],
   [affichageFixe_2, 200, 5000],
   [affichageFixe_3, 500, 10000]]

affichageDynamique_2 = [[affichageFixe_3, 2, 100],
   [affichageFixe_2, 2, 100]]

affichageDynamique_3 = [[affichageFixe_1, 2, 1000],
   [affichageFixe_2, 2, 1000]]

# Liste des affichages dynamiques
affichageDynamique = [affichageDynamique_1, affichageDynamique_2, affichageDynamique_3]

animation = 0
choix = 0
while True:
    # Appel de la fonction d'affichage
    affichage(affichageDynamique[choix][animation][0],
              affichageDynamique[choix][animation][1])
    start_chronometre(affichageDynamique[choix][animation][2])

    # Attente de l'appui du bouton A ou B ou le chronometre
    while (not (button_a.is_pressed()) and
           not (button_b.is_pressed()) and
           not (chronometre())):
        pass

    # Changement du choix de l'animation dynamique
    if button_a.is_pressed():
        choix += 1
        animation = 0

    if choix >= (len(affichageDynamique)):
        choix = 0

    # Attente du bouton relâché
    while button_a.is_pressed() or button_b.is_pressed():
        pass

    # Effacement de l'écran
    display.clear()

    # Progression de l'animation de l'affichage dynamique
    animation += 1
    if animation >= (len(affichageDynamique[choix])):
        animation = 0

 Flasher puis bouton reset

Objectif de la Mission : Par le bouton B, l’utilisateur met le dispositif en veille, puis par l’appui du bouton  A ou B l’utilisateur réveille le dispositif.

# Mise en veille du dispositif attendre un appui sur les touches A ou B
def mettre_en_veille():
    display.clear()
    # Attentre que les boutons A et B soient relachés
    while button_a.is_pressed() or button_b.is_pressed():
        pass

    # Attentre l’appui d’un bouton A ou A pour le réveil du dispositif
    while (not (button_a.is_pressed()) and
           not (button_b.is_pressed())):
        pass

animation = 0
choix = 0
mettre_en_veille()

while True:
    # Appel de la fonction d'affichage
    affichage(affichageDynamique[choix][animation][0],
              affichageDynamique[choix][animation][1])

    start_chronometre(affichageDynamique[choix][animation][2])

    # Attente de l'appui du bouton A ou B ou le chronometre
    while (not (button_a.is_pressed()) and
           not (button_b.is_pressed()) and
           not (chronometre())):
        pass

    # Changement du choix de l'animation dynamique
    if button_a.is_pressed():
        choix += 1
        animation = 0

    if choix >= (len(affichageDynamique)):
        choix = 0

    # Fermeture des animations par l'appui du bouton B
    if button_b.is_pressed():
        mettre_en_veille()

    # Attente du bouton relaché
    while button_a.is_pressed() or button_b.is_pressed():
        pass

    # Effacement de l'écran
    display.clear()

    # Progression de l'animation de l'affichage dynamique
    animation += 1
    if animation >= (len(affichageDynamique[choix])):
        animation = 0

Objectif de la Mission : Le dispositif se met automatiquement en veille lorsque la luminosité dépasse un seuil de la luminosité du jour.

La carte MICRO :BIT est équipée de plusieurs capteurs, dont un capteur de luminosité. Le capteur de luminosité sur la carte micro:bit est constitué d’une photorésistance (capteur de lumière) qui mesure l’intensité de la lumière réfléchie.

Voici comment vous pouvez utiliser le capteur de luminosité sur la carte micro:bit en programmation avec MicroPython :

from microbit import *

while True:
    # Lire la valeur de luminosité du capteur
    luminosite = display.read_light_level()

    # Afficher la valeur de luminosité sur l'écran LED
    display.scroll("Lum: " + str(luminosite))

    # Pause avant la prochaine mesure
    sleep(1000)

# Initialisation varaible pour la luminosité
def init_luminosite():
    global chrono_luminosite
    chrono_luminosite = time.ticks_ms()       

# Détection si la luminosité actuelle est supérieure à celle de la luminosité du jour
def lec_luminosite():
    global chrono_luminosite
    if display.read_light_level() < LUMINOSITE_JOUR:
        chrono_luminosite = time.ticks_ms()

# Si depuis un certain temps dans la luminosité du jour
def si_luminosite():
    if (chrono_luminosite and
       (time.ticks_ms() - chrono_luminosite > (DELAI_LUMINOSITE*1000))):
        return True
    else:
        return False

# Mise en veille du dispositif attendre un appui sur les touches A ou B
def mettre_en_veille():
    display.clear()
    # Attentre que les boutons A et B soient relachés
    while button_a.is_pressed() or button_b.is_pressed():
        pass

    # Attentre l’appui d’un bouton A ou A pour le réveil du dispositif
    while (not (button_a.is_pressed()) and
           not (button_b.is_pressed())):
        pass

    init_luminosite()

LUMINOSITE_JOUR = 100
DELAI_LUMINOSITE = 2
animation = 0
choix = 0
mettre_en_veille()

while True:
    # Appel de la fonction d'affichage
    affichage(affichageDynamique[choix][animation][0],
              affichageDynamique[choix][animation][1])
    start_chronometre(affichageDynamique[choix][animation][2])

    # Test de la luminosité
    lec_luminosite()

    # Attente de l'appui du bouton A ou B ou le chronometre
    while (not (button_a.is_pressed()) and
           not (button_b.is_pressed()) and
           not (chronometre())):
        pass

    # Changement du choix de l'animation dynamique
    if button_a.is_pressed():
        choix += 1
        animation = 0

    if choix >= (len(affichageDynamique)):
        choix = 0

    # Fermeture des animations par l'appui du bouton B
    if button_b.is_pressed():
        mettre_en_veille()

    # Si luminosité du jour depuis un certain temps alors mise en veille

    if si_luminosite():
        mettre_en_veille()

    # Attente du bouton relaché
    while button_a.is_pressed() or button_b.is_pressed():
        pass

    # Effacement de l'écran
    display.clear()

    # Progression de l'animation de l'affichage dynamique
    animation += 1
    if animation >= (len(affichageDynamique[choix])):
        animation = 0
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
Jeu vidéo ROBLOX ROBLOX-NIVEAU01

Générer un escalier

avec une Boucle et des Clones

Apprendre à faire construire un escalier géant de 200 marches par le script lui-même ! Au lieu de placer les marches une par une à la main dans Roblox Studio (bon courage pour 200 marches ! :

  1. Utiliser une Part modèle (template) qui sert de moule.
  2. Cloner cette Part dans une boucle for (rappel du tutoriel de la pyramide !).
  3. Positionner chaque marche avec un calcul basé sur le numéro de la marche (i).
  4. Ranger proprement les marches dans un dossier créé par le script.

Ce tutoriel réunit des briques déjà connues : Clone(), Parent, Instance.new, les boucles for, les Vector3 et les paramètres en variables !

Introduction : Pourquoi générer un escalier par script ?

Imagine que tu veuilles construire un escalier de 200 marches dans Studio :

  • Chaque marche doit être positionnée un peu plus haut et un peu plus loin que la précédente.
  • À la main, c’est 200 manipulations… et si tu veux changer la hauteur des marches, il faut tout recommencer !

Avec un script :

  • Les 200 marches sont créées en moins d’une seconde.
  • Changer la hauteur, la largeur, le nombre de marches = changer 1 nombre et relancer !
  • Le niveau reste parfaitement régulier (aucune marche de travers !).

C’est toute la puissance de la programmation : l’ordinateur exécute les tâches répétitives à ta place.

Préparation dans Roblox Studio

  1. Dans Workspace, crée un dossier nommé Folder.
  2. Ajoute une Part nommée Part dans ce dossier : c’est le moule.
    • Place-la à l’endroit où l’escalier doit commencer.
    • Sa taille n’a pas grande importance (le script la règle à chaque clone !), mais tu peux lui donner une couleur sympa : toutes les marches hériteront de cette couleur (et du matériau !).
  3. Crée un Script dans ServerScriptService et colle le code.
  4. Lance le jeu :
    • L’escalier de 200 marches se construit instantanément !
    • Dans l’Explorer, un dossier EscalierDroit contient Step0, Step1, … Step199.
  5. Teste la montée : ton joueur peut-il grimper ? Si les marches sont trop hautes, réduis rise !

⚠️ Conseil de test : mets stepCount = 10 pendant tes essais (escalier court, facile à inspecter), et seulement à la fin remets 200 !

Générer une échelle

Voici le script simple commenté en français :

-- Récupérer le dossier et la Part modèle (le "moule" des marches)
local stairsFolder = workspace:WaitForChild("Folder")
local template = stairsFolder:WaitForChild("Part")

-- Ancrer le modèle pour qu'il ne tombe pas pendant la construction
template.Anchored = true
template.Size = Vector3.new(8, 0.5, 4)

-- Boucle : créer les marches une par une
-- i va de 1 à 10 
for i = 1, 10 do
	-- 1) Cloner le modèle
	local step = template:Clone()

	-- 3) LE CŒUR DU SCRIPT : positionner la marche
	-- La marche n° i se trouve à :
	--   - X : interval * i  → chaque marche est décalée de "interval" studs vers l'avant
	--   - Y : (step.Size.Y + hauteur) * i → chaque marche est décalée vers le HAUT
	--   - Z : 0 → pas de décalage sur l'axe Z (escalier droit !)
	step.Position += Vector3.new(
		0,                          -- décalage horizontal
		(step.Size.Y + 1) * i,               -- décalage vertical
		0                                      -- pas de décalage Z
	)

	-- 4) Attacher la marche au dossier et la nommer
	step.Parent = stairsFolder
	step.Name = "Step" .. i     -- "Step0", "Step1", "Step2"...
end

Générer un escalier droit

Voici le script simple commenté en français :

-- Récupérer le dossier et la Part modèle (le "moule" des marches)
local stairsFolder = workspace:WaitForChild("Folder")
local template = stairsFolder:WaitForChild("Part")

-- Ancrer le modèle pour qu'il ne tombe pas pendant la construction
template.Anchored = true
template.Size = Vector3.new(4, 0.5, 8)

-- Boucle : créer les marches une par une
-- i va de 1 à 10 
for i = 1, 10 do
	-- 1) Cloner le modèle
	local step = template:Clone()

	-- 3) LE CŒUR DU SCRIPT : positionner la marche
	-- La marche n° i se trouve à :
	--   - X : interval * i  → chaque marche est décalée de "interval" studs vers l'avant
	--   - Y : (step.Size.Y + hauteur) * i → chaque marche est décalée vers le HAUT
	--   - Z : 0 → pas de décalage sur l'axe Z (escalier droit !)
	step.Position += Vector3.new(
		3 * i,                          -- décalage horizontal
		(step.Size.Y + 1) * i,               -- décalage vertical
		0                                      -- pas de décalage Z
	)

	-- 4) Attacher la marche au dossier et la nommer
	step.Parent = stairsFolder
	step.Name = "Step" .. i     -- "Step0", "Step1", "Step2"...
end

Et le script modifié pour le même résultat mais avec des constantes pour mieux modifier le code :

-- Récupérer le dossier et la Part modèle (le "moule" des marches)
local stairsFolder = workspace:WaitForChild("Folder")
local template = stairsFolder:WaitForChild("Part")

-- ============================================================
-- PARAMÈTRES : c'est ICI que tout se règle ! Modifie-les !
-- ============================================================
local stepCount = 200          -- nombre de marches
local rise = 2                 -- hauteur gagnée par marche (studs)
local stepWidth = 4            -- largeur de la marche (axe X)
local stepDepth = 8            -- profondeur de la marche (axe Z) — espace pour poser le pied
local stepThickness = 0.5      -- épaisseur de la planche de la marche
local interval = 5            -- distance entre deux marches (axe X)

-- Ancrer le modèle pour qu'il ne tombe pas pendant la construction
template.Anchored = true
template.Size = Vector3.new(stepWidth, stepThickness, stepDepth)

-- Boucle : créer les marches une par une
-- i va de 0 à stepCount - 1 (donc 200 tours pour stepCount = 200)
for i = 1, stepCount do
	-- 1) Cloner le modèle
	local step = template:Clone()

	-- 3) LE CŒUR DU SCRIPT : positionner la marche
	-- La marche n° i se trouve à :
	--   - X : interval * i  → chaque marche est décalée de "interval" studs vers l'avant
	--   - Y : (step.Size.Y + rise) * i → chaque marche est décalée vers le HAUT
	--   - Z : 0 → pas de décalage sur l'axe Z (escalier droit !)
	step.Position += Vector3.new(
		interval * i,                          -- décalage horizontal
		(step.Size.Y + rise) * i,               -- décalage vertical
		0                                      -- pas de décalage Z
	)

	-- 4) Attacher la marche au dossier et la nommer
	step.Parent = stairsFolder
	step.Name = "Step" .. i     -- "Step0", "Step1", "Step2"...
end


Explications Détaillées

1. La Part modèle (template) : le moule

local template = folder:WaitForChild("Part")
  • La Part nommée Part dans le dossier Folder ne sera jamais visible dans le jeu : elle sert uniquement de moule.
  • template:Clone() (rappel du tutoriel de la pyramide inversée !) crée une copie exacte à chaque tour de boucle.
  • À la fin, template:Destroy() supprime le moule : il a fait son travail ! (La ligne est facultative : sans elle, le moule resterait visible au sol.)

2. Les paramètres : le tableau de bord de l’escalier

local stepCount = 200
local rise = 2
local stepWidth = 4
local stepDepth = 8
local stepThickness = 0.5
local interval = 5

Toutes les dimensions sont des variables en haut du script : c’est le tableau de bord ! Expérimente :

ParamètreRôleQue se passe-t-il si tu le changes ?
stepCountNombre de marches10 → un petit escalier de test ! 500 → une échelle vers le ciel !
riseHauteur gagnée par marche1 → escalier doux. 5 → escalier raide qu’on ne peut pas grimper !
stepWidthLargeur (axe X)20 → autoroute géante !
stepDepthProfondeur (axe Z)2 → marches minuscules, impossible de monter.
stepThicknessÉpaisseur de la planche2 → marches massives.
intervalDistance entre marches (axe X)2 → marches serrées, plus facile à monter !

⚠️ Rappel important : pour qu’un joueur puisse monter l’escalier, la hauteur gagnée par marche (rise + stepThickness) doit rester inférieure à la hauteur de marche maximale que Roblox permet de franchir (environ 2.2 studs par défaut). Ici : stepThickness (0.5) + rise (2) = 2.5… c’est juste au-dessus ! Si ton joueur bloque sur une marche, réduis rise à 1.5 ! C’est le genre de chose qu’on découvre… en testant !

3. LA LIGNE MAGIQUE : le calcul de la position

step.Position += Vector3.new(interval * i, (step.Size.Y + rise) * i, 0)

C’est le cœur du tutoriel ! Décomposons-la :

L’axe X : interval * i

  • Marche n° 0 : décalage = 5 × 0 = 0 → position du moule.
  • Marche n° 1 : décalage = 5 × 1 = 5 studs.
  • Marche n° 2 : décalage = 5 × 2 = 10 studs.
  • Marche n° 199 : décalage = 5 × 199 = 995 studs !
  • Le numéro de la marche i pilote son décalage : plus i est grand, plus la marche est loin. C’est ça, une progression arithmétique (des maths de collège ! ).

L’axe Y : (step.Size.Y + rise) * i

  • Chaque marche est posée au-dessus de la précédente.
  • Pourquoi step.Size.Y + rise et pas juste rise ? L’épaisseur de la planche (stepThickness) doit être ajoutée : sans elle, chaque marche serait enfoncée dans la précédente au lieu d’être posée dessus. Le saut vertical entre deux marches = l’épaisseur de la planche + la hauteur gagnée.
  • Marche n° 199 : hauteur ≈ 2.5 × 199 = 497.5 studs… un escalier qui monte jusqu’au ciel !

L’axe Z : 0

  • Aucun décalage : les marches s’alignent droit. C’est pour ça que le dossier s’appelle « EscalierDroit » !

La position de départ : +=

  • step.Position += ... : on part de la position du moule et on ajoute le décalage. Avantage : l’escalier se construit où que tu aies placé la Part modèle ! Déplace le moule dans Studio → tout l’escalier suit !

4. La boucle et le compteur i

for i = 0, stepCount - 1 do
  • Pourquoi commencer à 0 et finir à stepCount - 1 ? Avec i = 0, la première marche est positionnée exactement à l’emplacement du moule (décalage × 0 = aucun décalage). C’est plus intuitif pour placer l’escalier !
  • On pourrait aussi écrire for i = 1, stepCount — mais alors le calcul des positions devrait devenir (i - 1) partout. Choisir 0, c’est éviter les -1 partout !
  • step.Name = "Step" .. i (rappel : concaténation avec un nombre !) : chaque marche a un nom unique → on pourra la retrouver plus tard avec FindFirstChild("Step" .. i).

QCM : Teste ta compréhension !

Sur le moule (template) :

  1. À quoi sert la Part nommée Part dans le dossier ?
    • a) C’est la première marche de l’escalier.
    • b) Elle sert à décorer le niveau.
    • c) C’est un moule : le script la clone pour créer chaque marche, puis la détruit.
  1. Pourquoi appelle-t-on template:Destroy() à la fin ?
    • a) Le moule a servi et n’a plus d’utilité : on nettoie le niveau.
    • b) Pour supprimer tout l’escalier.
    • c) C’est obligatoire sinon le jeu plante.

Sur les paramètres :

  1. Que se passe-t-il si on change stepCount = 200 en stepCount = 50 ?
    • a) Les marches sont 4 fois plus petites.
    • b) L’escalier ne fait plus que 50 marches (il est 4 fois plus court).
    • c) Rien ne change.
  1. Pourquoi les dimensions sont-elles mises en variables en haut du script ?
    • a) Parce que Lua oblige.
    • b) Pour décorer le script.
    • c) Pour pouvoir ajuster l’escalier facilement en changeant un seul nombre, sans toucher au reste du code.

Sur le calcul de position :

  1. Pour la marche n° 3 (i = 3), quel est son décalage horizontal (axe X) avec interval = 5 ?
    • a) 15 studs (interval * i = 5 × 3).
    • b) 5 studs.
    • c) 3 studs.
  1. Pourquoi ajoute-t-on step.Size.Y dans le calcul vertical (step.Size.Y + rise) * i ?
    • a) Pour changer la couleur des marches.
    • b) Pour compenser l’épaisseur de la planche : sinon chaque marche serait enfoncée dans la précédente au lieu d’être posée dessus.
    • c) Pour aller plus vite.
  1. Pourquoi le décalage Z est-il à 0 ?
    • a) Parce que l’escalier est DROIT : les marches avancent sur un seul axe, sans tourner.
    • b) Parce que l’axe Z n’existe pas.
    • c) Parce que les marches sont invisibles.
  1. Que signifie step.Position += Vector3.new(...) ?
    • a) On remplace la position par celle du moule.
    • b) On AJOUTE le décalage à la position actuelle du clone (celle du moule) — l’escalier se construit là où est placé le moule.
    • c) On supprime la position.
      Réponse : a)

Sur la boucle :

  1. Pourquoi la boucle va-t-elle de 0 à stepCount - 1 ?
    • a) Pour que la première marche (i = 0) soit positionnée exactement à l’emplacement du moule, sans décalage.
    • b) Parce que Lua interdit de commencer à 1.
    • c) Pour économiser une marche.
  1. Pourquoi chaque marche reçoit-elle un nom "Step" .. i ?
    • a) Pour avoir un nom unique et pouvoir retrouver chaque marche plus tard (FindFirstChild).
    • b) Pour la détruire immédiatement.
    • c) Pour changer sa couleur.
  1. Pourquoi range-t-on toutes les marches dans le dossier EscalierDroit ?
    • a) Pour garder l’Explorer propre et pouvoir manipuler tout l’escalier d’un seul coup.
    • b) Parce que les Parts ne peuvent pas être ailleurs.
    • c) Pour rien.

Idées pour aller plus loin

1. Un escalier aux couleurs dégradées

  • Objectif : dégradé du bleu au rouge du bas en haut (rappel des boucles de couleurs du tutoriel de découverte !).
  • Comment faire ? Calcule la couleur avec i :
-- Dans la boucle : 
local ratio = i / stepCount -- de 0 à 1 
step.Color = Color3.fromRGB(255 * ratio, 0, 255 * (1 - ratio))

2. Une marche sur deux avec un effet spécial

  • Objectif : les marches paires sont normales, les marches impaires personnalisées !
  • Comment faire ? Utilise l’opérateur modulo (i % 2) qui donne le reste de la division :
if i % 2 == 1 then -- marches impaires           step.Color = Color3.fromRGB(0, 255, 0) 
        step.BrickColor = BrickColor.new("Lime green") 
end

3. Un escalier construit marche par marche (effet visuel)

  • Objectif : voir l’escalier se construire sous tes yeux.
  • Comment faire ? Ajoute task.wait(0.05) à la fin de la boucle : chaque marche apparaît l’une après l’autre ! Pour 200 marches, ça donne un bel effet de construction.

4. Démontage progressif de l’escalier

  • Objectif : comme pour la pyramide, détruire les marches une par une (mode « escalier qui s’effondre » !).
  • Comment faire ? (rappel du tutoriel de découverte !)
  • task.wait(3) for i = stepCount - 1, 0, -1 do stairsFolder:FindFirstChild("Step" .. i):Destroy() task.wait(0.1) end
for i = stepCount - 1, 0, -1 do 
  stairsFolder:FindFirstChild("Step" .. i):Destroy()
  task.wait(0.1)
 
end

Résumé des concepts clés

ConceptDescriptionExemple
Part modèle (template)Un moule cloné autant de fois que nécessaire, puis détruit.template:Clone()
Paramètres en variablesToutes les dimensions réglables en haut du script.stepCount, rise, interval
Boucle for génératriceLe compteur i pilote à la fois le nombre ET la position.for i = 0, stepCount - 1 do
Progression arithmétiqueDécalage = valeur × numéro de la marche.interval * i
Vector3.new de positionDécalage X (avant), Y (haut), Z (côté).‘+= Vector3.new(interval*i, hauteur*i, 0)
Instance.new("Folder")Créer un dossier par le script pour ranger les clones..Parent = workspace
Noms uniquesRetrouver chaque clone plus tard."Step" .. i
Ne pas masquer les variablesDeux variables du même nom = confusion !folder vs stairsFolder

Astuce : teste toujours avec stepCount = 10 avant de passer à 200 — un escalier court, c’est plus rapide à inspecter, et tu gagneras un temps fou pendant tes réglages


Génère ton escalier par une fonction paramètrable

-- Récupérer le dossier et la Part modèle (le "moule" des marches)
local stairsFolder = workspace:WaitForChild("Folder")
local template = stairsFolder:WaitForChild("Part")

-- ============================================================
-- PARAMÈTRES : c'est ICI que tout se règle ! Modifie-les !
-- ============================================================

local function createStairs(base, stepCount, rise, interval, stepWidth, stepDepth, stepThickness)
	-- 2) Vérifier que le modèle a une base pour se coller

	if not base:IsA("BasePart") then
		warn("❌ Le modèle doit être une Part pour être cloné !")
		return
	end
	
	if not stepCount then stepCount = 12 end
	if not rise then rise = 2 end -- hauteur entre chaque marche
	if not interval then interval = 5 end -- décalage entre chaque marche
	if not stepWidth then stepWidth = base.Size.X end
	if not stepDepth then stepDepth = base.Size.Z end
	if not stepThickness then stepThickness = base.Size.Y end

	
	-- Ancrer le modèle pour qu'il ne tombe pas pendant la construction
	template.Anchored = true
	template.Size = Vector3.new(stepWidth, stepThickness, stepDepth)

	-- Boucle : créer les marches une par une
	-- i va de 0 à stepCount - 1 (donc 200 tours pour stepCount = 200)
	for i = 1, stepCount do
		-- 1) Cloner le modèle
		local step = template:Clone()

		-- 3) LE CŒUR DU SCRIPT : positionner la marche
		-- La marche n° i se trouve à :
		--   - X : interval * i  → chaque marche est décalée de "interval" studs vers l'avant
		--   - Y : (step.Size.Y + rise) * i → chaque marche est décalée vers le HAUT
		--   - Z : 0 → pas de décalage sur l'axe Z (escalier droit !)
		step.Position += Vector3.new(
			interval * i,                          -- décalage horizontal
			(step.Size.Y + rise) * i,               -- décalage vertical
			0                                      -- pas de décalage Z
		)

		-- 4) Attacher la marche au dossier et la nommer
		step.Parent = stairsFolder
		step.Name = "Step" .. i     -- "Step0", "Step1", "Step2"...
	end
end

createStairs(template)

Dans tous les sens :

-- Récupérer le dossier et la Part modèle (le "moule" des marches)
local stairsFolder = workspace:WaitForChild("Escalier")
local template = stairsFolder:WaitForChild("Marche")

-- ============================================================
-- PARAMÈTRES : c'est ICI que tout se règle ! Modifie-les !
-- ============================================================

local function createStairs(base, stepCount, rise, name, intervalX, intervalZ, stepWidth, stepDepth, stepThickness)
	-- 2) Vérifier que le modèle a une base pour se coller

	if not base:IsA("BasePart") then
		warn("❌ Le modèle doit être une Part pour être cloné !")
		return
	end
	
	if not stepCount then stepCount = 12 end
	if not name then name = "Step" end -- Par defaut "Step"
	if not rise then rise = 2 end -- hauteur entre chaque marche
	if not intervalX then intervalX = 5 end -- décalage entre chaque marche
	if not intervalZ then intervalZ = 0 end -- décalage entre chaque marche
	if not stepWidth then stepWidth = base.Size.X end
	if not stepDepth then stepDepth = base.Size.Z end
	if not stepThickness then stepThickness = base.Size.Y end

	
	-- Ancrer le modèle pour qu'il ne tombe pas pendant la construction
	template.Anchored = true
	template.Size = Vector3.new(stepWidth, stepThickness, stepDepth)

	-- Boucle : créer les marches une par une
	-- i va de 0 à stepCount - 1 (donc 200 tours pour stepCount = 200)
	for i = 1, stepCount do
		-- 1) Cloner le modèle
		local step = template:Clone()

		-- 3) LE CŒUR DU SCRIPT : positionner la marche
		-- La marche n° i se trouve à :
		--   - X : interval * i  → chaque marche est décalée de "interval" studs vers l'avant
		--   - Y : (step.Size.Y + rise) * i → chaque marche est décalée vers le HAUT
		--   - Z : 0 → pas de décalage sur l'axe Z (escalier droit !)
		step.Position += Vector3.new(
			intervalX,                          -- décalage horizontal
			(step.Size.Y + rise),               -- décalage vertical
			intervalZ                                      -- pas de décalage Z
		) * i

		-- 4) Attacher la marche au dossier et la nommer
		step.Parent = stairsFolder
		step.Name = name .. i     -- "Step0", "Step1", "Step2"...
	end
end

createStairs(template, 20, 1 , "StairA", 5, 0)
createStairs(template, 20, 1 , "StairB", -5, 0)
createStairs(template, 20, 1 , "StairC", 0, -5)
createStairs(template, 20, 1 , "StairD", 0, 5)
createStairs(template, 20, 1 , "StairE", 5, 5)
createStairs(template, 20, 1 , "StairF", -5, 5)
createStairs(template, 20, 1 , "StairG", 5, -5)
createStairs(template, 20, 1 , "StairH", -5, -5)

Générer un escalier hélicoïdal ou en colimaçon

local folder = workspace:WaitForChild("Folder")
local template = folder:WaitForChild("Part")

-- Paramètres
local stepCount = 400        -- nombre de marches
local angleStep = 15        -- degrés entre deux marches (360 / 15 = 24 marches par tour)
local rise = 1              -- hauteur gagnée par marche (studs)
local radius = 12           -- distance entre l'axe de l'escalier et le centre d'une marche
local stepWidth = 8         -- largeur radiale de la marche
local stepThickness = 0.5   -- épaisseur de la marche
local center = template.Position

-- Profondeur de la marche : corde de l'arc extérieur, pour éviter les trous entre deux marches
local outerRadius = radius + stepWidth / 2
local stepDepth = 2 * outerRadius * math.sin(math.rad(angleStep) / 2)

template.Anchored = true
local stairsFolder = Instance.new("Folder")
stairsFolder.Name = "EscalierHelicoidal"
stairsFolder.Parent = workspace


for i = 0, stepCount - 1 do
	local step = template:Clone()
	
	step.Size = Vector3.new(stepWidth, stepThickness, stepDepth)
	step.Anchored = true
	
	local ratio = i / stepCount -- de 0 à 1 
	step.Color = Color3.fromRGB(255 * ratio, 0, 255 * (1 - ratio))	
	
	-- 1) on se place à la bonne hauteur, 2) on tourne autour de l'axe Y,
	-- 3) on s'écarte du centre : la marche est ainsi orientée radialement
	step.CFrame = CFrame.new(center + Vector3.new(0, i * rise, 0))
		* CFrame.Angles(0, math.rad(i * angleStep), 0)
		* CFrame.new(radius, 0, 0)

	step.Parent = stairsFolder
end

template:Destroy() -- retire la part modèle (facultatif)
Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU01

Comprendre l’apparition des joueurs

et les changements d’états

Découvrir la vie d’un joueur dans Roblox avec un script « d’observation » :

  1. Quand un joueur se connecte (PlayerAdded).
  2. Quand son personnage apparaît (CharacterAdded).
  3. Quand son personnage change d’état (StateChanged : saut, chute, atterrissage…).

Ce script n’est pas un jeu : c’est un script espion ! Il ne fait qu’afficher dans la console (Output) tout ce qui arrive aux joueurs. C’est l’outil parfait pour comprendre ces trois événements avant de les utiliser dans de vrais jeux (comme la zone anti-gravité ou les checkpoints !).

Introduction : La vie d’un joueur Roblox

Quand tu joues à un jeu Roblox, voici ce qui se passe dans l’ordre :

1. Le JOUEUR se connecte au jeu        → événement PlayerAdded
2. Son PERSONNAGE apparaît dans le monde → événement CharacterAdded
3. Le personnage agit : court, saute, tombe, atterrit...
                                          → événements StateChanged (très fréquents !)
4. Le personnage meurt → il est détruit
5. Un NOUVEAU personnage apparaît        → de nouveau CharacterAdded !
6. Le joueur quitte le jeu               → événement PlayerRemoving

Les trois personnages-clés de ce tuto :

  • player : le compte du joueur (son nom, son UserId…). Il ne meurt jamais.
  • character : le bonhomme 3D dans le monde (le Model). Il est détruit à chaque mort et recréé à chaque respawn !
  • humanoid : l’ordinateur de bord du personnage : il gère la santé, la marche, les sauts… et ses états.

Les états d’un Humanoid (Enum.HumanoidStateType)

ÉtatSignification
RunningLe personnage court au sol.
JumpingIl vient de sauter.
FreefallIl tombe dans les airs.
LandedIl vient d’atterrir.
ClimbingIl grimpe à une échelle.
SwimmingIl nage.
SeatedIl est assis (dans un siège).
DeadIl est mort !
PlatformStanding / PhysicsÉtats spéciaux (plateformes, physique).

C’est la même liste que celle utilisée dans le tutoriel de la zone anti-gravité (Jumping, Freefall, Landed) !

Voici le script commenté en français :

-- Récupérer le service des joueurs
local Players = game:GetService("Players")

-- Fonction appelée à chaque changement d'état du Humanoid
-- Paramètres : le joueur, l'ancien état, le nouvel état
local function onStateChanged(player, oldState, newState)
    -- Affiche dans la console : nom du joueur, ancien état -> nouvel état
    print(player.Name, "État changé :", oldState, "->", newState)
end

-- Fonction appelée à chaque apparition du personnage
local function onCharacterAdded(player, character)
    -- Attendre que le Humanoid existe (le personnage se construit progressivement !)
    local humanoid = character:WaitForChild("Humanoid")

    -- Connecter le suivi des changements d'états de CE personnage
    humanoid.StateChanged:Connect(function(oldState, newState)
        onStateChanged(player, oldState, newState)
    end)
end

-- Fonction appelée à chaque connexion d'un joueur
local function onPlayerAdded(player)
    print(player.Name .. " est connecté")

    -- Cas 1 : le personnage existe DÉJÀ (joueur connecté avant le script)
    if player.Character then
        task.spawn(onCharacterAdded, player, player.Character)
    end

    -- Cas 2 : chaque apparition/réapparition FUTURE (mort, respawn...)
    player.CharacterAdded:Connect(function(character)
        onCharacterAdded(player, player.Character and character)
    end)
end

-- Nouveaux joueurs : événement pour chaque joueur qui se connecte
Players.PlayerAdded:Connect(onPlayerAdded)

-- Joueurs déjà connectés AVANT l'exécution du script
for _, player in ipairs(Players:GetPlayers()) do
    task.spawn(onPlayerAdded, player)
end

Explications Détaillées : les 4 fonctions du script

Le script est construit comme des poupées russes : chaque fonction appelle la suivante !

onPlayerAdded ──► onCharacterAdded ──► onStateChanged
     (1)                (2)                  (3)

1. onStateChanged : l’espion des états

local function onStateChanged(player, oldState, newState)
    print(player.Name, "État changé :", oldState, "->", newState)
end
  • oldState : l’état avant le changement (ex: Enum.HumanoidStateType.Freefall).
  • newState : le nouvel état (ex: Enum.HumanoidStateType.Landed).
  • Rappel des tutoriels précédents : print(...) avec plusieurs valeurs séparées par des virgules affiche tout sur une ligne (pas besoin de concaténer avec .. !).

2. onCharacterAdded : l’espion des personnages

local function onCharacterAdded(player, character)
    local humanoid = character:WaitForChild("Humanoid")
    humanoid.StateChanged:Connect(function(oldState, newState)
        onStateChanged(player, oldState, newState)
    end)
end
  • WaitForChild("Humanoid") : quand le personnage apparaît, il se construit pièce par pièce (jambes, torse, tête, Humanoid…). Si on cherche le Humanoid trop tôt, il n’existe pas encore → erreur ! WaitForChild attend qu’il soit prêt. (Rappel : c’est pareil que WaitForChild("HumanoidRootPart") dans le tutoriel des checkpoints !)
  • On connecte StateChanged pour ce personnage précis. Quand il mourra, ce personnage sera détruit — et sa connexion avec lui ! C’est pour ça qu’il faut reconnecter StateChanged à chaque nouveau personnage (d’où la fonction !).

3. onPlayerAdded : l’espion des joueurs (avec ses DEUX pièges !)

local function onPlayerAdded(player)
    print(player.Name .. " est connecté")

    -- Cas 1 : le personnage existe DÉJÀ
    if player.Character then
        task.spawn(onCharacterAdded, player, player.Character)
    end

    -- Cas 2 : apparitions futures
    player.CharacterAdded:Connect(function(character)
        onCharacterAdded(player, character)
    end)
end

Piège n°1 : le joueur est arrivé AVANT le script !

  • Le problème : PlayerAdded ne se déclenche que pour les joueurs qui arrivent après la connexion de l’événement. Si tu es déjà dans le jeu quand le script démarre (par exemple au lancement d’un test Studio), l’événement t’a raté !
  • La solution en deux temps :
    • Cas 1 : if player.Character then → le personnage existe déjà → on appelle directement onCharacterAdded.
    • Cas 2 : player.CharacterAdded:Connect(...) → on guette toutes les apparitions futures (respawn après mort !).

Piège n°2 : CharacterAdded ne se déclenche pas à la connexion… mais un peu après !

  • Quand un joueur se connecte, PlayerAdded se déclenche, puis Roblox crée son personnage (quelques millisecondes plus tard) → c’est ensuite que CharacterAdded se déclenche. C’est pour ça que le Cas 2 attrape aussi la toute première apparition !

Pourquoi task.spawn ?

  • task.spawn(fonction, arguments...) lance la fonction dans un thread séparé : le script n’attend pas qu’elle soit finie pour continuer.
  • Indispensable ici : onCharacterAdded commence par un WaitForChild… qui peut bloquer quelques instants. Sans task.spawn, un joueur « lent à charger » bloquerait le traitement des autres joueurs !

4. La boucle finale : rattraper tout le monde

Players.PlayerAdded:Connect(onPlayerAdded)

for _, player in ipairs(Players:GetPlayers()) do
    task.spawn(onPlayerAdded, player)
end
  • Players.PlayerAdded:Connect(onPlayerAdded) : tous les joueurs qui arriveront à partir de maintenant.
  • Players:GetPlayers() : la liste des joueurs déjà présents (rappel : c’est une liste numérotée, donc on utilise ipairs, comme pour les checkpoints et les blocs de la pyramide !).
  • La boucle appelle onPlayerAdded pour chacun : personne n’est oublié, même si le script a démarré en retard !

Le schéma complet :


Préparation et Test dans Roblox Studio

  1. Crée un Script dans ServerScriptService et colle le code corrigé.
  2. Ouvre la fenêtre Output (onglet View → Output) : c’est là que les print s’affichent.
  3. Lance le jeu en mode multi-joueurs (onglet Test → 2 Players) pour observer plusieurs joueurs !
  4. Expérimente et observe la console :
ActionCe que tu dois voir dans l’Output
Le jeu démarrepatrick est connecté (et celui du 2ème joueur !)
Tu marchesRien (rester en Running ne change pas l’état !)
Tu sautesJumping puis Freefall
Tu atterrisLanded puis Running
Tu montes une échelleClimbing
Tu tombes dans l’eauSwimming
Tu t’assois sur un siègeSeated
Tu te reset (menu Echap → Reset)Dead… puis re-connexion automatique de CharacterAdded !

Contrôle important : après ta mort, observe bien : onCharacterAdded est rappelé automatiquement pour ton nouveau personnage (le script continue d’afficher tes états, sans rien refaire !). C’est la preuve que le Cas 2 de onPlayerAdded fonctionne.


QCM : Teste ta compréhension !

Sur la vie du joueur :

  1. Dans quel ordre se passent les événements quand un joueur rejoint le jeu ?
    • a) PlayerAdded → CharacterAdded → puis les StateChanged.
    • b) CharacterAdded → PlayerAdded → StateChanged.
    • c) Tout en même temps.
  1. Quelle est la différence entre player et character ?
    • a) player est le compte (ne meurt jamais) ; character est le bonhomme 3D, détruit à chaque mort et recréé à chaque respawn.
    • b) Ce sont deux noms pour la même chose.
    • c) character est le compte du joueur.
  1. Que se passe-t-il pour la connexion StateChanged quand le personnage meurt et respawn ?
    • a) Elle est détruite avec l’ancien personnage ; onCharacterAdded reconnecte StateChanged sur le NOUVEAU Humanoid.
    • b) Elle reste active sur l’ancien personnage.
    • c) Roblox la reconnecte automatiquement tout seul.

Sur les états du Humanoid :

  1. Dans quel ordre les états s’enchaînent-ils quand tu sautes d’une plateforme ?
    • a) Jumping → Freefall → Landed → Running.
    • b) Running → Landed → Jumping.
    • c) Freefall → Jumping → Running.
  1. Pourquoi ne voit-on rien s’afficher quand on marche normalement pendant longtemps ?
    • a) Parce qu’on reste dans l’état Running : sans changement d’état, StateChanged ne se déclenche pas.
    • b) Parce que print est cassé.
    • c) Parce que la marche est trop rapide.
  1. Quels sont les paramètres reçus par la fonction connectée à StateChanged ?
    • a) L’ancien état et le nouvel état (oldState, newState).
    • b) Le joueur et le personnage.
    • c) Rien.

Sur les deux pièges :

  1. Pourquoi le script a-t-il besoin de la boucle for ... ipairs(Players:GetPlayers()) à la fin ?
    • a) Pour rattraper les joueurs déjà connectés AVANT le démarrage du script (l’événement PlayerAdded ne se déclenche pas pour eux).
    • b) Pour déconnecter les joueurs.
    • c) Pour afficher la liste des règles du jeu.
  1. Pourquoi utilise-t-on task.spawn(onCharacterAdded, player, player.Character) ?
    • a) Pour lancer la fonction dans un thread séparé et ne pas bloquer le script, car elle contient un WaitForChild qui peut attendre.
    • b) Pour aller plus vite que WaitForChild.
    • c) Parce que onCharacterAdded est interdite ailleurs.
  1. Pourquoi onCharacterAdded utilise-t-elle WaitForChild("Humanoid") ?
    • a) Parce que le personnage se construit pièce par pièce : au moment de l’événement, le Humanoid n’existe peut-être pas encore.
    • b) Pour faire joli.
    • c) Parce que le Humanoid n’existe jamais.
  1. Dans onPlayerAdded, à quoi sert le if player.Character then ?
    • a) À vérifier si le personnage existe DÉJÀ (le joueur était là avant le script) pour ne pas rater son suivi.
    • b) À créer un personnage.
    • c) À détruire le personnage.

Résumé des concepts clés

ConceptDescriptionExemple
PlayerAddedUn joueur se connecte au jeu.Players.PlayerAdded:Connect(onPlayerAdded)
CharacterAddedUn personnage apparaît (1ère fois OU respawn !).player.CharacterAdded:Connect(...)
StateChangedLe Humanoid change d’état (saut, chute, atterrissage…).humanoid.StateChanged:Connect(...)
Enum.HumanoidStateTypeLa liste des états possibles.Landed, Jumping, Freefall, Dead…
GetPlayers()Liste des joueurs déjà présents (rattrapage !).for _, player in ipairs(...) do
task.spawnLance une fonction sans bloquer le script.task.spawn(onPlayerAdded, player)
WaitForChild("Humanoid")Attend que le personnage soit prêt.Anti-erreur !
GetState()Lit l’état ACTUEL (sans attendre un changement).Utilisé dans la zone anti-gravité

Astuce : Garde ce script espion dans un coin de tes projets : dès qu’un comportement te semble mystérieux (pourquoi mon joueur ne saute plus ?), les print des états te diront exactement ce qui se passe dans la tête du Humanoid ! C’est l’outil de débogage parfait

Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU02

Créer une Zone Antigravité (Zone d’Apesanteur)

Construire en trois phases une zone magique où le joueur flotte comme dans l’espace :

  1. Phase 1 : donner au joueur une force vers le haut pour annuler la gravité quand il touche une Part.
  2. Phase 2 : supprimer cette force quand le joueur sort de la Part (et qu’il est retombé au sol).
  3. Phase 3 : supprimer cette force quand le joueur touche le sol (atterrissage).

Ce tutoriel introduit des concepts physiques réels (masse, gravité, poids) et des outils Roblox puissants : VectorForce, Attachment, les états de l’Humanoid (StateChanged) et GetTouchingParts().

Introduction : La physique de la gravité dans Roblox

Pourquoi le joueur tombe-t-il ?

Dans Roblox (comme dans la vraie vie !), chaque objet subit son poids : une force qui le tire vers le bas.

Poids = Masse × Gravité
  • La gravité de Roblox vaut par défaut 196.2 studs/seconde² (accessible via workspace.Gravity).
  • La masse du personnage est accessible via rootPart.AssemblyMass.
  • Donc le poids d’un personnage = 196.2 × masse, dirigé vers le bas (Vector3.new(0, -poids, 0)).

L’astuce du script : annuler le poids !

Si on applique sur le joueur une force exactement égale à son poids, mais vers le HAUT, les deux forces s’annulent :

Gravité (vers le bas) + Notre force (vers le haut) = 0  → le joueur FLOTE !

C’est exactement ce que ressentent les astronautes en orbite : ils sont en apesanteur !

L’outil : VectorForce

Un VectorForce est un objet qui applique une force en continue sur une Part. Il a besoin :

  • d’un Attachment (un point d’ancrage invisible sur la Part),
  • d’une direction relative au monde (Enum.ActuatorRelativeTo.World),
  • d’une force exprimée en Vector3.

Préparation dans Roblox Studio

  1. Dans Workspace, crée un dossier GravityFolder.
  2. Ajoute une Part nommée Part : c’est la zone d’apesanteur.
    • Forme libre : une grande zone transparente (Transparency = 0.8, Anchored = true, CanCollide = false) donne le meilleur effet « champ de force » !
    • Important : laisse CanTouch = true (sinon Touched, TouchEnded et GetTouchingParts ne fonctionnent pas !).
  3. Crée un Script dans ServerScriptService et colle le code complet (utilitaires + Phase 1 + Phase 2 + Phase 3).

Le script complet, corrigé et commenté en français

Voici le script commenté en français :

-- Récupérer le service des joueurs
local Players = game:GetService("Players")

-- Récupérer la zone anti-gravité
local folder = workspace:WaitForChild("GravityFolder")
local antiGravityPart = folder:WaitForChild("Part")

-- Constantes du script
local GRAVITY = workspace.Gravity * 0.99   -- 99% de la gravité (petite marge de confort)
local DEFAULT_JUMP_HEIGHT = 7.2            -- Hauteur de saut normale de Roblox

-- Fonction utilitaire : retrouve le personnage, son rootPart et son humanoid
-- à partir de l'objet qui a touché la zone (hit)
local function getCharacterFromHit(hit)
    local character = hit:FindFirstAncestorOfClass("Model")
    if not character then return nil end

    local rootPart = character:FindFirstChild("HumanoidRootPart")
    if not rootPart then return nil end

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid then return nil end

    return character, rootPart, humanoid   -- On retourne les trois !
end

-- Fonction utilitaire : crée (ou retrouve) la force anti-gravité du joueur
local function getOrCreateAntiGravityForce(rootPart)
    -- Chercher une force déjà existante (pour ne pas en créer deux !)
    local force = rootPart:FindFirstChildOfClass("VectorForce")
    if not force then
        -- Créer le point d'ancrage invisible sur le joueur
        local attachment = Instance.new("Attachment")
        attachment.Parent = rootPart

        -- Créer la force vectorielle
        force = Instance.new("VectorForce")
        force.Attachment0 = attachment                          -- où la force s'applique
        force.RelativeTo = Enum.ActuatorRelativeTo.World        -- relative au MONDE
        force.ApplyAtCenterOfMass = true                         -- au centre de masse
        force.Parent = rootPart
    end
    return force
end

-- Fonction utilitaire : supprime la force anti-gravité du joueur
local function removeAntiGravityForce(rootPart, humanoid)
    local force = rootPart:FindFirstChildOfClass("VectorForce")
    if force then
        force.Attachment0:Destroy()   -- supprimer le point d'ancrage
        force:Destroy()               -- supprimer la force
        humanoid.JumpHeight = DEFAULT_JUMP_HEIGHT  -- restaurer le saut normal
    end

end

-- Fonction utilitaire : vérifie si le personnage touche encore la zone
local function isStillTouching(character)
    for _, touchingPart in pairs(antiGravityPart:GetTouchingParts()) do
        if touchingPart.Parent == character then
            return true    -- une partie du corps touche encore la zone !
        end
    end
    return false           -- plus aucun contact
end

PHASE 1 : Donner une force vers le haut quand le joueur touche la Part

But : dès que le joueur entre dans la zone, il flotte.

Explication :

  • L’événement Touched se déclenche à chaque contact (rappel : des dizaines de fois par seconde !).
  • On cherche si une VectorForce existe déjà sur le joueur (sinon on en crée une). C’est notre réflexe « cooldown » des tutoriels précédents : ne jamais créer deux fois le même objet !
  • On applique la formule physique : Force = Masse × Gravité, vers le haut.
  • On réduit JumpHeight à 1 : en apesanteur, un grand saut propulserait le joueur au plafond !
-- PHASE 1 : le joueur touche la zone → il flotte
antiGravityPart.Touched:Connect(function(hit)
    -- Retrouver personnage, rootPart et humanoid (fonction utilitaire)
    local character, rootPart, humanoid = getCharacterFromHit(hit)
    if not character then return end   -- pas un joueur : on ignore

    -- Créer la force (ou retrouver celle qui existe déjà)
    local force = getOrCreateAntiGravityForce(rootPart)

    -- LA PHYSIQUE : force qui annule exactement le poids, vers le haut
    -- Force = gravité × masse, dirigée vers +Y
    force.Force = Vector3.new(0, GRAVITY * rootPart.AssemblyMass, 0)

    -- En apesanteur, on limite le saut pour ne pas partir dans le décor !
    humanoid.JumpHeight = 1
end)

Points clés :

  1. rootPart.AssemblyMass : la masse totale du personnage (corps, habits, accessoires…). C’est pour ça qu’on utilise AssemblyMass et pas Mass (qui ne donnerait que la masse d’une seule Part !).
  2. GRAVITY = workspace.Gravity * 0.99 : pourquoi 99 % et pas 100 % ? Avec exactement 100 %, le joueur resterait figé en l’air comme une statue. Avec 99 %, il descend très lentement : il peut quitter la zone en marchant, c’est plus jouable ! (Tu peux essayer 1.0 pour voir la différence.)
  3. force.Force = Vector3.new(0, GRAVITY * rootPart.AssemblyMass, 0) : la force est recalculée à chaque Touched, donc si le joueur met un chapeau (masse change), la force s’adapte !

Teste ! Place une Part dans un dossier GravityFolder, lance le jeu et marche dedans.
Contrôle :

  • Ton personnage ** monte lentement / flotte ** dans la zone.
  • Dans l’Explorer, tu vois apparaître un VectorForce et un Attachment dans le HumanoidRootPart.
  • Si tu sautes, tu ne montes presque plus (JumpHeight = 1).

QCM :

  1. Que vaut la force appliquée au joueur ?
    • a) La masse du personnage multipliée par la gravité, dirigée vers le haut.
    • b) Toujours 100, vers le bas.
    • c) La taille du personnage.
  1. Pourquoi utilise-t-on rootPart.AssemblyMass plutôt que rootPart.Mass ?
    • a) Pour avoir la masse totale du personnage entier, pas seulement celle du HumanoidRootPart.
    • b) C’est plus rapide.
    • c) Mass n’existe pas.
  1. Que se passe-t-il si on ne vérifie pas si la force existe déjà (if not force then)?
    • a) Des dizaines de VectorForce seraient créées (un par contact Touched !) et le joueur partirait en fusée.
    • b) Rien.
    • c) Le jeu plante immédiatement.
  1. Pourquoi mettre humanoid.JumpHeight = 1 ?
    • a) Pour empêcher le joueur de sauter très haut dans la zone et s’échapper au plafond.
    • b) Pour l’empêcher de marcher.
    • c) Pour changer sa couleur.

PHASE 2 : Supprimer la force quand le joueur SORT de la Part

But : quand le joueur n’a plus aucun contact avec la zone, il retrouve une gravité normale.

Explication — le problème est plus subtil qu’il n’y paraît :

  • L’événement TouchEnded se déclenche quand une Part précise (ex: le pied gauche) quitte le contact. Mais le joueur a encore peut-être un autre contact (le pied droit !).
  • Il faut donc vérifier avec GetTouchingParts() : la liste de toutes les Parts qui touchent actuellement la zone. Si une seule appartient au personnage → il est encore dedans → on ne fait rien !
  • Autre piège : si le joueur saute ou est en chute libre au moment du TouchEnded, il est en l’air et va retoucher la zone dans un instant. On regarde donc son état avec humanoid:GetState() :
    • Enum.HumanoidStateType.Jumping : il saute → on attend.
    • Enum.HumanoidStateType.Freefall : il tombe → on attend.
  • Dernier piège : GetTouchingParts() est appelé après un task.wait(1) pour laisser le temps au moteur physique de « réaliser » le nouveau contact (ex: le joueur qui retombe dans la zone juste après le saut).
-- PHASE 2 : le joueur sort de la zone → on enlève la force
antiGravityPart.TouchEnded:Connect(function(hit)
    -- Retrouver le personnage
    local character, rootPart, humanoid = getCharacterFromHit(hit)
    if not character then return end

    -- S'il saute ou tombe, il va peut-être retoucher la zone : on ne fait rien !
    local state = humanoid:GetState()
    if state == Enum.HumanoidStateType.Jumping then return end
    if state == Enum.HumanoidStateType.Freefall then return end

    -- Attendre 1 seconde que le contact se stabilise
    task.wait(1)

    -- Le joueur touche-t-il ENCORE la zone (avec une autre partie du corps) ?
    if isStillTouching(character) then return end   -- oui : il est toujours dedans !

    -- Non : il est bien sorti → supprimer la force
    removeAntiGravityForce(rootPart, humanoid)
end)

Teste ! Entre dans la zone, flotte, puis sors complètement.
Contrôle :

  • Tant qu’une seule partie de ton corps touche la zone, tu continues de flotter (demande à un camarade de regarder depuis l’extérieur !).
  • Dès que tu es totalement sorti, tu retombes : gravité normale, saut restauré (JumpHeight = 7.2).
  • Le VectorForce et l’Attachment ont disparu de l’Explorer.

QCM :

  1. Pourquoi vérifier isStillTouching(character) avant de supprimer la force ?
    • a) Parce que TouchEnded se déclenche pour UNE Part (ex: un pied), mais le joueur peut encore toucher la zone avec une autre (ex: l’autre pied).
    • b) Pour faire durer le jeu.
    • c) Parce que TouchEnded ne fonctionne pas.
  1. Que retourne part:GetTouchingParts() ?
    • a) La liste de toutes les Parts actuellement en contact avec la zone.
    • b) Le nom de la zone.
    • c) La liste des joueurs du serveur.
  1. Pourquoi ignorer l’événement si l’état du joueur est Jumping ou Freefall ?
    • a) Parce qu’en sautant ou en tombant, il va probablement retoucher la zone juste après : ce serait bête de couper la force pour rien.
    • b) Parce que les sauts sont interdits.
    • c) Parce que l’état Jumping détruit la force tout seul.
  1. Que fait removeAntiGravityForce(rootPart, humanoid) ?
    • a) Détruit l’Attachment et le VectorForce, et restaure JumpHeight à 7.2.
    • b) Détruit le personnage.
    • c) Téléporte le joueur.

PHASE 3 : Supprimer la force quand le joueur TOUCCHE LE SOL

But : si le joueur saute hors de la zone et atterrit au sol (sans jamais déclencher le TouchEnded complet de la Phase 2), il faut quand même couper la force — sinon il flotterait en permanence !

Explication — un nouvel outil puissant : StateChanged !

  • Un Humanoid passe par des états : Running (il court), Jumping (il saute), Freefall (il tombe), Landed (il vient d’atterrir !)…
  • L’événement humanoid.StateChanged se déclenche à chaque changement d’état, avec l’ancien et le nouvel état en paramètres.
  • Notre règle : si le nouvel état est Landed ET que le joueur ne touche plus la zone → on supprime la force.

Le problème du respawn : le VectorForce est attaché au personnage. Si on connectait StateChanged n’importe quand, on raterait les joueurs déjà connectés ou ceux qui respawnent. Solution en 3 étages (déjà vue dans le tutoriel des checkpoints !) :

  1. Players.PlayerAdded → nouveau joueur.
  2. player.CharacterAdded → chaque apparition/réapparition du personnage.
  3. humanoid.StateChanged → chaque changement d’état.
-- PHASE 3 : le joueur atterrit au sol → on enlève la force

-- Fonction appelée à chaque changement d'état du Humanoid
local function onStateChanged(character, humanoid, oldState, newState)
    -- On ne s'intéresse qu'à l'atterrissage
    if newState ~= Enum.HumanoidStateType.Landed then return end

    local rootPart = character:FindFirstChild("HumanoidRootPart")
    if not rootPart then return end

    -- S'il touche encore la zone, il est dedans : on garde la force !
    if isStillTouching(character) then return end

    -- Il a atterri ailleurs : supprimer la force
    removeAntiGravityForce(rootPart, humanoid)
end

-- Fonction appelée à chaque apparition du personnage
local function onCharacterAdded(player, character)
    -- Attendre que le Humanoid existe
    local humanoid = character:WaitForChild("Humanoid")

    -- Connecter le suivi des changements d'état
    humanoid.StateChanged:Connect(function(oldState, newState)
        onStateChanged(character, humanoid, oldState, newState)
    end)
end

-- Fonction appelée à chaque connexion d'un joueur
local function onPlayerAdded(player)
    -- Cas 1 : le personnage existe DÉJÀ (joueur connecté avant le script)
    if player.Character then
        task.spawn(onCharacterAdded, player, player.Character)
    end

    -- Cas 2 : chaque apparition/réapparition future (mort, respawn...)
    player.CharacterAdded:Connect(function(character)
        onCharacterAdded(player, character)
    end)
end

-- Nouveaux joueurs
Players.PlayerAdded:Connect(onPlayerAdded)

-- Joueurs déjà connectés AVANT l'exécution du script
for _, player in ipairs(Players:GetPlayers()) do
    task.spawn(onPlayerAdded, player)
end

Points clés :

  1. newState ~= Enum.HumanoidStateType.Landed : on filtre tout ce qui n’est pas un atterrissage (courir, sauter, nager…). Seul Landed nous intéresse.
  2. Le duo PlayerAdded + boucle sur GetPlayers() : rappelle-toi le tutoriel des checkpoints ! Si le script démarre après qu’un joueur soit déjà là, PlayerAdded ne se déclenchera pas pour lui → la boucle finale rattrape tout le monde.
  3. task.spawn(...) : lance la fonction dans un thread séparé pour ne pas bloquer le script pendant les WaitForChild.

Teste ! Entre dans la zone, saute au-dessus des bords et atterris loin de la zone.
Contrôle :

  • À l’atterrissage hors zone, tu retombes immédiatement sous la gravité normale : le VectorForce a été détruit par la Phase 3 (et non la Phase 2 !).
  • Tue ton personnage (Reset Character dans le menu) : il respawn… sans force résiduelle (le VectorForce était attaché à l’ancien personnage, détruit avec lui — et le nouveau personnage est suivi par CharacterAdded !).

QCM :

  1. À quoi sert humanoid.StateChanged ?
    • a) À exécuter une fonction à chaque changement d’état du personnage (saut, chute, atterrissage…).
    • b) À détecter les collisions.
    • c) À changer la couleur du personnage.
  1. Quel état nous intéresse dans la Phase 3 ?
    • a) Enum.HumanoidStateType.Landed (le joueur vient d’atterrir).
    • b) Enum.HumanoidStateType.Running.
    • c) Enum.HumanoidStateType.Dead.
  1. Pourquoi le script se connecte-t-il aussi à player.CharacterAdded ?
    • a) Pour suivre chaque nouveau personnage (mort, respawn) et reconnecter StateChanged à chaque fois.
    • b) Pour créer un nouveau joueur.
    • c) Pour rien.
  1. Pourquoi une boucle for sur Players:GetPlayers() à la fin du script ?
    • a) Pour rattraper les joueurs déjà connectés avant le démarrage du script (le PlayerAdded ne se déclenche pas pour eux).
    • b) Pour les déconnecter.
    • c) Pour afficher leurs noms.
  1. Pourquoi vérifier isStillTouching(character) même dans la Phase 3 ?
    • a) Parce qu’un joueur peut atterrir SUR la zone elle-même (ou juste à côté, en la touchant encore) : il doit continuer à flotter !
    • b) Pour compter les joueurs.
    • c) C’est inutile.

Résumé des concepts clés

ConceptDescriptionExemple
Poids = masse × gravitéLa formule physique pour annuler la gravité.GRAVITY * rootPart.AssemblyMass
VectorForceApplique une force continue sur une Part.force.Force = Vector3.new(0, p, 0)
AttachmentPoint d’ancrage invisible pour la force.force.Attachment0 = attachment
AssemblyMassMasse totale du personnage.rootPart.AssemblyMass
TouchEndedDéclenché quand UN contact se termine (attention aux autres contacts !).Phase 2
GetTouchingParts()Liste toutes les Parts en contact actuel.isStillTouching(character)
humanoid:GetState()État actuel du personnage (Jumping, Freefall, Landed…).Phase 2
StateChangedÉvénement à chaque changement d’état.Phase 3
PlayerAdded / CharacterAddedSuivre les joueurs et leurs respawns.Phase 3
FactorisationDes fonctions utilitaires au lieu de dupliquer le code.removeAntiGravityForce(...)

Idées pour aller plus loin

1. Une zone qui donne une force supérieure au poids

  • Objectif : le joueur monte lentement comme dans un ascenseur de geysers.
  • Comment faire ? Multiplie la force par un coefficient : Vector3.new(0, GRAVITY * rootPart.AssemblyMass * 1.5, 0) — le joueur s’élève à 50 % de la gravité nette !

2. Zone de lune (gravité réduite)

  • Objectif : le joueur fait des bonds géants comme sur la Lune.
  • Comment faire ? Force à 50 % du poids : Vector3.new(0, GRAVITY * rootPart.AssemblyMass * 0.5, 0) et augmente JumpHeight à 25 !

3. Ajouter des particules de lévitation

  • Objectif : rendre la zone visuellement magique.
  • Comment faire ? Ajoute un ParticleEmitter à la zone (rappel du tutoriel des particules !) avec EmissionDirection = Enum.NormalId.Top.

4. Un effet de rotation pendant l’apesanteur

  • Objectif : le joueur tourne lentement sur lui-même comme un astronaute.
  • Comment faire ? Ajoute un BodyAngularVelocity ou applique rootPart.AssemblyAngularVelocity = Vector3.new(0, 2, 0) à l’entrée dans la zone.

Défi final :

Crée un niveau spatial complet :

  1. Trois zones : une d’apesanteur (le script du jour), une « lune » (gravité à 50 %), une « geyser » (force à 150 %).
  2. Des plateformes flottantes entre les zones (réutilise le tutoriel TweenService !).
  3. Un checkpoint au sommet (réutilise le tutoriel des checkpoints !) avec une bulle de chat « Bien joué ! » verte à l’arrivée.
Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU01

Mes premiers pas de la programmation Lua avec Roblox

Objectif : Découvrir pas à pas les bases de la programmation Lua avec Roblox :

  • Les variables (texte et nombres)
  • Les propriétés d’objets (écran, texte, Part)
  • Les boucles (for et while)
  • Les tableaux (simples et doubles)
  • Le clonage et la destruction d’objets

Le fil du jeu : tu possèdes un écran géant (TableauPart) qui affiche des messages, et un cube magique (Part) qui va grandir, changer de couleur, devenir invisible… et même se multiplier en pyramide inversée !

Règle du jeu : à chaque étape, essaye d’écrire le code, teste, contrôle ce qui est affiché, puis réponds au QCM avant de passer à la suite !

Étape 0 : Préparer le terrain dans Roblox Studio

Avant de coder, construis la scène :

  1. Dans Workspace, crée un dossier nommé Folder.
  2. Dans ce dossier :
    • Une Part nommée TableauPart : aplatie comme un écran (ex: Size = Vector3.new(12, 8, 0.5)), avec Anchored = true.
    • Une Part nommée Part : un cube visible (Size = Vector3.new(4, 4, 4)), avec Anchored = true.
  3. Sur TableauPart, ajoute (clic droit → Insert Object) :
    • Un SurfaceGui (nomme-le SurfaceGui).
    • Dans le SurfaceGui, un TextBox (nomme-le TextBox) : étire-le sur tout l’écran avec Size = {1,0},{1,0}.
  4. Crée un Script dans ServerScriptService : c’est là que tu colleras le code, étape par étape.

En haut du script, ces lignes (elles ne changent plus jamais) :

-- Récupérer les objets de la scène
local folder = workspace:WaitForChild("Folder")
local tableauPart = folder:WaitForChild("TableauPart")
local surfaceGui = tableauPart:WaitForChild("SurfaceGui")
local screen = surfaceGui:WaitForChild("TextBox")  -- notre écran d'affichage
local part = folder:WaitForChild("Part")           -- notre cube magique

Convention : comme dans tous nos tutoriels, les noms de variables sont en anglais (screen, firstName…) et les commentaires en français.

Étape 1 : Afficher un message (les propriétés d’un objet)

But : écrire « Bonjour » sur l’écran géant.

Explication : un objet Roblox (comme notre TextBox) possède des propriétés : sa couleur, sa taille… et son texte (Text). Pour changer une propriété, on écrit objet.propriete = valeur.

-- ÉTAPE 1 : Affiche un Bonjour à l'écran
screen.Text = "Bonjour"

Teste ! Lance le jeu.
Contrôle : tu dois voir « Bonjour » s’afficher sur l’écran géant. Sinon, vérifie que ton TextBox s’appelle bien TextBox et que le script pointe vers le bon objet.

QCM :

  1. Comment change-t-on le texte affiché dans le TextBox ?
    • a) screen.Bonjour = Text
    • b) print("Bonjour")
    • c) screen.Text = "Bonjour"
  2. Text est… ?
    • a) Une propriété du TextBox.
    • b) Une boucle.
    • c) Un joueur.

Étape 2 : Les variables texte et la concaténation

But : afficher « Bonjour patrick ! » (avec ton prénom !).

Explication : une variable, c’est une boîte avec une étiquette qui contient une valeur. Pour assembler des morceaux de texte, on utilise .. (deux points) : c’est la concaténation.

-- ÉTAPE 2 : Renseigne une variable pour afficher ton prénom
task.wait(2)                       -- petite pause de 2 secondes
local firstName = "patrick"        -- remplace par TON prénom !
screen.Text = "Bonjour " .. firstName .. " !"

Teste ! Contrôle : l’écran affiche d’abord « Bonjour » (2 secondes), puis « Bonjour tonprénom ! ». Attention : il y a un espace dans "Bonjour "; essaie sans espace pour voir la différence !

QCM :

  1. Que fait "Bonjour " .. firstName ?
    • a) Il colle les deux textes bout à bout (concaténation).
    • b) Il les additionne comme des nombres.
    • c) Il supprime le premier texte.
  2. Que fait task.wait(2) ?
    • a) Il supprime 2 objets.
    • b) Il met le script en pause pendant 2 secondes.
    • c) Il écrit 2 fois le texte.

Étape 3 : Modifier les propriétés de l’écran et du texte

But : rendre l’écran coloré et le texte stylé.

Explication : on peut modifier plein de propriétés d’un objet : couleur de fond (BackgroundColor3), couleur du texte (TextColor3), bordure, taille du texte… Color3.fromRGB(rouge, vert, bleu) fabrique une couleur avec 3 nombres de 0 à 255.

-- ÉTAPE 3 : Modifie les propriétés de l'écran et du texte
screen.BackgroundColor3 = Color3.fromRGB(85, 255, 255)   -- fond cyan
sreen.TextColor3 = Color3.fromRGB(50, 50, 100)           -- gris bleu
screen.BorderMode = Enum.BorderMode.Inset               -- style de bordure
screen.BorderColor3 = Color3.fromRGB(255, 0, 0)         -- bordure rouge
screen.BorderSizePixel = 20                              -- épaisseur de la bordure
screen.TextScaled = true                                 -- texte agrandi automatiquement
screen.TextWrapped = true                               -- texte qui passe à la ligne
screen.TextXAlignment = Enum.TextXAlignment.Center      -- texte centré

Teste ! Contrôle : fond cyan, texte jaune, bordure rouge épaisse, texte centré et plus grand. Amuse-toi : change les 3 nombres de fromRGB et observe ! Que donne Color3.fromRGB(255, 0, 0) ? Et Color3.fromRGB(0, 0, 255) ?

QCM :

  1. Que représente les 3 nombres de Color3.fromRGB(...) ?
    • a) Les quantités de rouge, de vert et de bleu (de 0 à 255).
    • b) La taille de l’écran.
    • c) L’heure.
  2. Que fait screen.TextScaled = true ?
    • a) L’écran disparaît.
    • b) Le texte s’agrandit automatiquement pour remplir l’espace.
    • c) Le texte devient bleu.

Étape 4 : Les variables nombre

But : afficher ton âge à l’écran.

Explication : une variable peut contenir du texte ("patrick") ou un nombre (12). Attention au piège : pour coller un nombre à du texte, on utilise quand même .. !

-- ÉTAPE 4 : Renseigne ton âge dans une variable et affiche-le
task.wait(2)
local age = 12                          -- remplace par TON âge !
screen.Text = screen.Text .. "\nJ'ai " .. age .. " ans"

Nouveauté : \n = un saut de ligne dans le texte (le message s’écrit en dessous du précédent).

Teste ! Contrôle : le message « J’ai X ans » apparaît en dessous du message précédent. Si tout s’affiche sur une seule ligne, vérifie ton \n.

QCM :

  1. Que fait "\n" dans un texte ?
    • a) Il affiche un « n ».
    • b) Il efface le texte.
    • c) Il saute à la ligne suivante.
      Réponse : a)
  2. Peut-on coller un nombre à du texte avec .. ?
    • a) Oui, .. " ans" fonctionne avec une variable nombre.
    • b) Non, jamais.
    • c) Seulement le lundi.

Étape 5 : À toi de jouer — une variable de ton choix

But : afficher une information de ton choix (ex: « Fille » ou « Garçon », ou ta classe).

Explication : c’est le même principe qu’avant. À toi d’écrire le code sans copier ! Indice : crée une variable texte, puis concatène-la avec \n à la suite.

-- ÉTAPE 5 : Renseigne une information et affiche-la
task.wait(2)
local gender = "Garçon"                -- ou "Fille"... ou invente ta variable !
screen.Text = screen.Text .. "\n" .. gender

Teste ! Contrôle : ta nouvelle ligne s’affiche sous les précédentes, sans effacer le reste.

QCM :

  1. Que se passe-t-il si tu écris screen.Text = gender (sans concaténation) ?
    • a) Tout le texte précédent est remplacé par ta variable.
    • b) Ta variable s’ajoute à la fin.
    • c) Rien ne s’affiche.

Étape 6 : Les nombres et l’addition (ton inventaire)

But : imagine que tu récupères une épée, un laser et une lampe, et calcule le total des points de ton inventaire.

Explication : avec des variables nombre, on peut calculer : +, -, * (multiplié), / (divisé).

-- ÉTAPE 6 : Calcule le total des points de l'inventaire
task.wait(2)
local sword = 10        -- points de l'épée
local laser = 50        -- points du laser
local lamp = 2          -- points de la lampe
local total = sword + laser + lamp   -- le calcul !
screen.Text = screen.Text .. "\nTotal inventaire : " .. total

Teste ! Contrôle : l’écran affiche « Total inventaire : 62 ». Vérifie le calcul de tête : 10 + 50 + 2 = 62 ?

QCM :

  1. Que vaut total si sword = 10, laser = 50, lamp = 2 ?
    • a) 62
    • b) 10502
    • c) 60
  2. Que fait local total = sword + laser + lamp ?
    • a) Il colle les trois textes.
    • b) Il additionne les trois variables et range le résultat dans total.
    • c) Il supprime les variables.

Étape 7 : Un vrai calcul (le voyage en vaisseau)

But : ton vaisseau part avec un réservoir vide, tu achètes 100 litres, puis tu visites 3 villes qui consomment chacune 30 litres. Combien reste-t-il ?

Explication : on peut combiner plusieurs opérations, et même réutiliser une variable avec =. Les parenthèses ( ) forcent le calcul à se faire en priorité.

-- ÉTAPE 7 : Calcule le carburant restant
task.wait(2)
local fuelTank = 0                          -- réservoir vide au départ
local fuelBought = 100                      -- litres achetés
local trips = 3                             -- nombre de déplacements
local fuelPerTrip = 30                      -- litres consommés par déplacement

fuelTank = fuelTank + fuelBought - (trips * fuelPerTrip)   -- le calcul !

screen.Text = screen.Text .. "\nReste dans mon réservoir : " .. fuelTank

Teste ! Contrôle : l’écran affiche 10 (100 – 90 = 10). Enlève les parenthèses autour de trips * fuelPerTrip : le résultat est-il le même ? Pourquoi ? (Indice : sans parenthèses, le calcul se fait de gauche à droite !)

QCM :

  1. Pourquoi met-on des parenthèses autour de trips * fuelPerTrip ?
    • a) Pour que la multiplication se fasse avant la soustraction.
    • b) Pour décorer le code.
    • c) Pour afficher le texte.

Étape 8 : La boucle for (compteur et décompte)

But : afficher un compteur de 1 à 10, puis un décompte de 10 à 1.

Explication : une boucle for répète un bloc de code. for i = 1, 10 do signifie : « pour i allant de 1 à 10 ». La variable i change à chaque tour ! Et for i = 10, 1, -1 compte à l’envers grâce au pas de -1.

-- ÉTAPE 8a : Compteur de 1 à 10
task.wait(2)
local baseText = screen.Text .. "\n"
for i = 1, 10 do
    screen.Text = baseText .. "Attente : " .. i .. " seconde(s)"
    task.wait(0.3)
end

-- ÉTAPE 8b : Décompte de 10 à 1
for i = 10, 1, -1 do                       -- pas de -1 : on compte à l'envers !
    screen.Text = baseText .. "Attente : " .. i .. " seconde(s)"
    task.wait(0.3)
end

Teste ! Contrôle : le chiffre affiché monte 1, 2, 3… 10 puis redescend 10, 9… 1. Remarque : on a préparé baseText avant la boucle pour ne pas coller 10 fois la phrase !

QCM :

  1. Combien de fois le code dans for i = 1, 10 do s’exécute-t-il ?
    • a) 10 fois.
    • b) 1 fois.
    • c) 11 fois.
  2. Que fait le -1 dans for i = 10, 1, -1 do ?
    • a) Il fait diminuer i de 1 à chaque tour (décompte).
    • b) Il répète la boucle à l’infini.
    • c) Il affiche -1 à l’écran.

Étape 9 : Les tableaux (ton inventaire en liste)

But : ranger les valeurs de l’inventaire dans un tableau et les afficher une par une.

Explication : un tableau ({ }) range plusieurs valeurs dans une seule variable. pairs parcourt le tableau et donne l’index (i, la position) et la valeur (item).

-- ÉTAPE 9 : Affiche l'inventaire avec une boucle
task.wait(2)
screen.Text = baseText .. "\n"
local inventory = {sword, laser, lamp}      -- tableau avec les 3 valeurs

for i, item in pairs(inventory) do
    screen.Text = screen.Text .. "Objet " .. i .. " : " .. item .. "\n"
end

Teste ! Contrôle : l’écran affiche 3 lignes : « Objet 1 : 10 », « Objet 2 : 50 », « Objet 3 : 2 ».

QCM :

  1. Que vaut inventory[1] ?
    • a) Le nombre de cases.
    • b) La première valeur du tableau (10, la valeur de sword).
    • c) Une erreur.
  2. Dans for i, item in pairs(inventory) do, que vaut item ?
    • a) La valeur de la case courante du tableau.
    • b) Le nom du tableau.
    • c) Toujours 1.

Étape 10 : Le tableau double (objet ET valeur)

But : afficher le nom de chaque objet et sa valeur. Un tableau dans un tableau !

Explication : chaque case d’un tableau peut contenir… un autre tableau ! {{"sword", 10}, {"laser", 50}}. Pour lire la case 1 d’un sous-tableau : item[1].

-- ÉTAPE 10 : Affiche les noms et les valeurs de l'inventaire
task.wait(2)
screen.Text = baseText .. "\n"
inventory = {{"sword", sword}, {"laser", laser}, {"lamp", lamp}}   -- tableau double

for i, item in pairs(inventory) do
    screen.Text = screen.Text .. i .. ") " .. item[1] .. " : " .. item[2] .. "\n"
end
baseText = screen.Text     -- on mémorise pour la suite

Teste ! Contrôle : l’écran affiche : « 1) sword : 10 », « 2) laser : 50 », « 3) lamp : 2 ». Le nom ET la valeur !

QCM :

  1. Que vaut item[2] dans la boucle ?
    • a) Le nom de l’objet.
    • b) La position dans le tableau.
    • c) La valeur (le nombre de points) de l’objet courant.
  2. Comment s’appelle un tableau qui contient d’autres tableaux ?
    • a) Un tableau double (imbriqué).
    • b) Une boucle.
    • c) Une propriété.

Étape 11 : La boucle while (la jauge d’énergie)

But : faire monter une jauge d’énergie jusqu’à 100, barre par barre.

Explication : la boucle while (« tant que ») répète un code tant qu’une condition est vraie. Ici : tant que l’énergie est inférieure au maximum, on ajoute 10.

-- ÉTAPE 11 : Charge la jauge d'énergie
task.wait(2)
local energy = 10                 -- énergie de départ
local maxEnergy = 100             -- énergie maximale
screen.Text = screen.Text .. "Energie : "
while energy < maxEnergy do       -- TANT QUE energy < 100
    energy = energy + 10          -- ajouter 10
    screen.Text = screen.Text .. "▄"    -- une barre de plus (Alt+220)
    task.wait(0.3)
end

Teste ! Contrôle : les barres « ▄ » s’ajoutent une par une, puis s’arrêtent d’elles-mêmes quand l’énergie atteint 100. La boucle s’est toute seule arrêtée : c’est la condition qui l’a décidée !

QCM :

  1. Quand la boucle while s’arrête-t-elle ?
    • a) Quand la condition devient fausse (ici, quand energy atteint 100).
    • b) Après 10 secondes.
    • c) Jamais.
  2. Quelle est la différence avec la boucle for ?
    • a) Aucune.
    • b) for répète un nombre de fois connu ; while répète jusqu’à une condition.
    • c) while ne marche que sur les écrans.

Étape 12 : Transformer le cube (taille et couleur)

But : faire grandir, rétrécir, changer la couleur du cube magique… en boucle !

Explication : la Size d’une Part est un Vector3 (largeur, hauteur, profondeur) : on peut l’additionner ou la soustraire avec += et -=. La couleur est faite avec Color3.fromRGB : si les 3 nombres sont égaux, on obtient un gris (0 = noir, 255 = blanc) !

-- ÉTAPE 12a : Fais grandir le cube, puis rétrécir
task.wait(2)
for i = 1, 10 do
    part.Size += Vector3.new(i, i, i)    -- le cube grossit de plus en plus fort !
    task.wait(0.3)
end
for i = 1, 10 do
    part.Size -= Vector3.new(i, i, i)   -- et il rétrécit
    task.wait(0.3)
end

-- ÉTAPE 12b : Du noir au blanc (le gris)
for i = 0, 255 do
    part.Color = Color3.fromRGB(i, i, i)    -- gris qui s'éclaircit
    task.wait(0.01)
end

-- ÉTAPE 12c : Une couleur vivante (du noir au rouge)
for i = 0, 255 do
    part.Color = Color3.fromRGB(i, 160, 160)   -- rouge qui monte, vert/bleu fixes
    task.wait(0.01)
end

Teste ! Contrôle : le cube grossit de plus en plus vite (car i augmente à chaque tour !), rétrécit, passe du noir au gris clair, puis prend une teinte rouge. Modifie le 160 : que se passe-t-il avec Color3.fromRGB(i, 0, 0) ?

QCM :

  1. Pourquoi le cube grandit-il de plus en plus vite dans la première boucle ?
    • a) Parce que task.wait raccourcit.
    • b) Parce que i augmente à chaque tour, et on ajoute Vector3.new(i, i, i).
    • c) Parce qu’il est magique.
  2. Que donne Color3.fromRGB(100, 100, 100) ?
    • a) Un gris moyen.
    • b) Du rouge pur.
    • c) Du blanc pur.

Étape 13 : Invisible puis visible (la transparence)

But : faire disparaître le cube puis le faire réapparaître.

Explication : la propriété Transparency va de 0 (opaque) à 1 (invisible). Deux boucles while parfaites pour ça !

-- ÉTAPE 13a : Rend le cube transparent (disparition)
while part.Transparency < 1 do
    part.Transparency += 0.1    -- un peu plus transparent...
    task.wait(0.1)
end

-- ÉTAPE 13b : Rend-le opaque (réapparition)
task.wait(2)
while part.Transparency > 0 do
    part.Transparency -= 0.1    -- un peu plus opaque...
    task.wait(0.1)
end

Teste ! Contrôle : le cube s’efface progressivement puis réapparaît tout doucement, comme un fantôme !

QCM :

  1. Quand une Part est-elle complètement invisible ?
    • a) Quand Transparency = 0.
    • b) Quand Transparency = 1.
    • c) Quand Transparency = 255.
  2. Que fait part.Transparency += 0.1 ?
    • a) Il ajoute 0.1 à la transparence actuelle (avec +=).
    • b) Il remplace la transparence par 0.1.
    • c) Il change la taille.

Étape 14 : Le clonage : construis une pyramide inversée !

But : cloner le cube 10 fois pour construire une pyramide inversée : chaque nouvel étage est plus large que le précédent, posé au-dessus.

Explication : part:Clone() crée une copie exacte de la Part. On la modifie ensuite (position, taille) puis on l’attache au dossier avec .Parent. Sans .Parent, l’objet reste invisible ! Attention aussi : chaque copie doit avoir un nom différent (« Part1 », « Part2″…) pour pouvoir la retrouver ensuite.

-- ÉTAPE 14 : Construis la pyramide inversée
task.wait(3)
screen.Text = baseText                     -- on nettoie l'écran
for i = 1, 10 do
    local newPart = part:Clone()           -- une copie du cube
    newPart.Position += Vector3.new(0, newPart.Size.Y * i, 0)   -- posée plus haut à chaque fois
    newPart.Size += Vector3.new(i * 3, 0, i * 3)                -- de plus en plus large !
    newPart.Parent = folder               -- ATTACHE la copie au monde
    newPart.Name = "Part" .. i             -- nom unique : "Part1", "Part2"...
    task.wait(1)
end

Teste ! Contrôle : un nouveau bloc apparaît chaque seconde, posé au-dessus du précédent, et de plus en plus large : la pyramide pointe vers le bas ! Regarde dans l’Explorer : les Part1 à Part10 sont bien dans Folder.

QCM :

  1. Que fait part:Clone() ?
    • a) Il crée une copie identique de la Part.
    • b) Il supprime la Part.
    • c) Il change la couleur.
  2. Que se passe-t-il si on oublie newPart.Parent = folder ?
    • a) Le jeu plante.
    • b) La copie existe en mémoire mais n’apparaît pas dans le jeu.
    • c) Rien, c’est la même chose.
  3. Pourquoi newPart.Name = "Part" .. i ?
    • a) Pour donner un nom unique à chaque copie et la retrouver plus tard.
    • b) Pour la détruire.
    • c) Pour changer sa couleur.

Étape 15 : La destruction : démolis la pyramide à l’envers !

But : détruire les blocs du haut vers le bas (ordre inverse).

Explication : :Destroy() supprime définitivement un objet. Pour retrouver chaque bloc par son nom, on utilise FindFirstChild : "Part" .. i fabrique le nom à chercher !

-- ÉTAPE 15 : Détruis les blocs dans l'ordre inverse
task.wait(3)
for i = 10, 1, -1 do                                   -- du dernier au premier !
    local block = folder:FindFirstChild("Part" .. i)   -- retrouve le bloc par son nom
    if block then
        block:Destroy()                                -- ... et détruis-le !
    end
    task.wait(1)
end
task.wait(3)

Teste ! Contrôle : les blocs disparaissent un par un, du haut vers le bas. À la fin, l’Explorer ne contient plus que TableauPart et Part !

  1. Que fait folder:FindFirstChild("Part" .. i) ?
    • a) Il détruit la Part.
    • b) Il cherche dans le dossier l’objet nommé « Part1 », « Part2″… selon la valeur de i.
    • c) Il crée une Part.
  2. Pourquoi la boucle va de 10 à 1 avec un pas de -1 ?
    • a) Pour aller plus vite.
    • b) Par hasard.
    • c) Pour détruire les blocs dans l’ordre inverse de leur création (du haut vers le bas).
  3. Pourquoi vérifie-t-on if block then avant de détruire ?
    • a) Pour éviter une erreur si le bloc n’existe pas (il retourne nil).
    • b) Pour changer sa couleur.
    • c) Ce n’est pas utile.

Résumé des concepts découverts

ConceptDescriptionExemple
PropriétésLes réglages d’un objet.screen.Text, part.Color
Variable texte / nombreUne boîte étiquetée contenant une valeur.local firstName = "patrick"
Concaténation ..Coller des textes (et nombres) ensemble."J'ai " .. age
\nSaut de ligne dans un texte.screen.Text .. "\n"
Color3.fromRGBFabriquer une couleur (rouge, vert, bleu : 0-255).Color3.fromRGB(85, 255, 255)
Calculs‘+, -, *, / et les parenthèses.fuelTank + fuelBought - (trips * fuelPerTrip)
Boucle forRépéter un nombre de fois connu (avec pas négatif pour décompter).for i = 10, 1, -1 do
Boucle whileRépéter tant qu’une condition est vraie.while energy < maxEnergy do
TableauUne liste de valeurs.{sword, laser, lamp}
Tableau doubleUn tableau de tableaux (nom + valeur).{{"sword", 10}}
pairsParcourir un tableau (index + valeur).for i, item in pairs(inventory) do
‘+= et -=Ajouter / retirer à une valeur.part.Size += Vector3.new(i, i, i)
Clone()Copier un objet.part:Clone()
Destroy()Supprimer un objet.block:Destroy()
FindFirstChildRetrouver un objet par son nom.folder:FindFirstChild("Part" .. i)

Défi final :

  1. Personnalise l’écran : affiche ton prénom, ton âge, ton inventaire imaginé (5 objets dans un tableau double !).
  2. Crée un escalier : au lieu d’une pyramide inversée, fais des marches qui montent en décalant aussi sur l’axe X (Vector3.new(i * 5, i * 2, 0)).

Félicitations ! Tu viens de découvrir presque toutes les bases de la programmation Lua : variables, conditions, boucles, tableaux, objets et propriétés. Tous les grands jeux Roblox sont construits avec ces mêmes briques !

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.