Author: macerobotics

Gestion de 2 cameras avec le robot MR25

Le robot MR25 est équipé d’une carte Raspberry Pi 5 disposant de deux connecteurs dédiés aux caméras. Il est donc possible d’utiliser simultanément deux caméras sur le robot.

Ce tutoriel présente les étapes nécessaires pour configurer et utiliser deux caméras avec le robot MR25, ainsi que la réalisation de captures d’images avec chacune d’elles.

Détecter les 2 cameras

Listez les cameras avec cette commande :

$ rpicam-hello --list-cameras

Vous devez voir la détection des 2 cameras :

Prendre une photo avec les 2 cameras

Voici un exemple de la prise d’une photo sur chaque cameras :

#!/usr/bin/env python3

# Import de la bibliothèque Picamera2 pour contrôler les caméras Raspberry Pi
from picamera2 import Picamera2

# Fonction permettant de mettre le programme en pause
from time import sleep

# Permet de récupérer la date et l'heure actuelles
from datetime import datetime

# Permet de gérer facilement les chemins et dossiers
from pathlib import Path


# ============================================================
# DOSSIER DE SAUVEGARDE
# ============================================================

# Création du dossier "photos" s'il n'existe pas déjà
output_dir = Path("photos")
output_dir.mkdir(exist_ok=True)


# ============================================================
# HORODATAGE
# ============================================================

# Création d'un horodatage commun aux deux photos
# Format : AAAAMMJJ_HHMMSS
# Exemple : 20260918_112530
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")


# ============================================================
# CAMÉRA 0
# ============================================================

# Création de l'objet correspondant à la caméra 0
cam0 = Picamera2(0)

# Création de la configuration pour une photo fixe
#
# Résolution utilisée :
# 4608 x 2592 pixels
#
# Cette résolution correspond à la résolution souhaitée
# pour la caméra 0.
config0 = cam0.create_still_configuration(
main={"size": (4608, 2592)}
)

# Application de la configuration à la caméra
cam0.configure(config0)

# Démarrage de la caméra
cam0.start()

# Petite attente afin de laisser la caméra s'initialiser
# et stabiliser l'image avant la capture.
sleep(2)

# Création du nom du fichier de sortie
#
# Exemple :
# photos/camera0_20260918_112530.jpg
file0 = output_dir / f"camera0_{timestamp}.jpg"

# Affichage dans le terminal du fichier qui va être créé
print(f"Capture caméra 0 : {file0}")

# Capture d'une photo et sauvegarde directement au format JPEG
cam0.capture_file(str(file0))

# Arrêt de la caméra 0
cam0.stop()

# Libération complète des ressources utilisées par la caméra 0
#
# Cette étape est particulièrement importante ici :
# elle permet de libérer les buffers mémoire DMA avant
# de configurer la deuxième caméra.
cam0.close()


# ============================================================
# CAMÉRA 1
# ============================================================

# Création de l'objet correspondant à la caméra 1
cam1 = Picamera2(1)

# Création de la configuration pour une photo fixe
#
# Résolution utilisée :
# 3280 x 2464 pixels
#
# Cette résolution correspond à la résolution souhaitée
# pour la caméra 1.
config1 = cam1.create_still_configuration(
main={"size": (3280, 2464)}
)

# Application de la configuration à la caméra
cam1.configure(config1)

# Démarrage de la caméra
cam1.start()

# Petite attente afin de laisser la caméra s'initialiser
# et stabiliser l'image avant la capture.
sleep(2)

# Création du nom du fichier de sortie
#
# Exemple :
# photos/camera1_20260918_112530.jpg
file1 = output_dir / f"camera1_{timestamp}.jpg"

# Affichage dans le terminal du fichier qui va être créé
print(f"Capture caméra 1 : {file1}")

# Capture d'une photo et sauvegarde directement au format JPEG
cam1.capture_file(str(file1))

# Arrêt de la caméra 1
cam1.stop()

# Libération complète des ressources utilisées par la caméra 1
cam1.close()


# ============================================================
# FIN DU PROGRAMME
# ============================================================

# Message indiquant que les deux captures sont terminées
print("Captures terminées.")

Fin du tuto !

Nicolas

 

 

 

Faire de l’électronique réparable !

Concevoir de l’électronique réparable consiste à intégrer la réparation dès la conception, plutôt que de chercher à réparer un produit une fois qu’il est cassé.

Démontabilité

  • Boîtier ouvrable avec des vis standard, sans colle ou clips destructifs.

Modularité

  • Séparer alimentation, commande, puissance, capteurs, interfaces, etc.,

    • Afin de remplacer un module plutôt que toute la carte électronique.

Composants remplaçables

  • Privilégier les composants standards et disponibles sur le long terme. Si possible utilisé des composants traversants.

Accessibilité

  • Rendre les connecteurs, fusibles, batteries et pièces d’usure facilement accessibles.

Diagnostic

  • Prévoir des points de test, des codes d’erreur, des interfaces de diagnostic et une documentation du circuit.

    • Un technicien doit pouvoir utiliser un multimètre ou un oscilloscope sans devoir deviner où mesurer.

Prévoir également :

  • LED d’état ;
  • connecteur de programmation/debug ;
  • fusibles ou protections accessibles ;
  • points de mesure sur les alimentations ;
  • indications sérigraphiées sur le PCB.
  • La sérigraphie devient une véritable documentation.

Réparabilité des PCB

  •  Éviter les composants inutilement difficiles à dessouder et laisser suffisamment d’espace autour des zones susceptibles d’être réparées.

Documentation

  • Fournir schémas, nomenclature (BOM), références des composants et procédures de dépannage.

    • Schéma électronique : licence open source adaptée au matériel, par exemple CERN-OHL.
    • Creative Commons, par exemple CC BY ou CC BY-SA.

Durabilité logicielle

  • Prévoir les mises à jour et, lorsque c’est possible, éviter qu’une panne logicielle rende le matériel inutilisable.

Gestion thermique

  • Concevoir le refroidissement pour limiter le vieillissement des composants sensibles.

Pièces détachées

  • Utiliser des références pérennes et prévoir leur remplacement sur la durée de vie du produit.

Robot MR25 – Détecter le frappement de main avec le microphone

On souhaite programmer le robot MR25 afin de contrôler ces mouvements via la détection des frappements de main :

👏 =>  Le robot MR25 avance

👏👏 =>  Le robot MR25 tourne à gauche

👏👏👏 =>  Le robot MR25 tourne à droite

👏👏👏👏 =>  Le robot MR25 s’arrête

Le microphone détecte les frappements en analysant le niveau sonore.

Algorithme

Voici un exemple de programme pour contrôler le robot avec le frappement de main :

import subprocess # permet de lancer le programme arecord pour récupérer le son du microphone.
import numpy as np # permet de traiter les données audio et de calculer le RMS.
import MR25
import time

DEVICE = "hw:0,0"
RATE = 48000
CHANNELS = 1
CHUNK = 1024

THRESHOLD = 0.01 # le seuil sonore.

# Temps minimum entre deux frappes
DEBOUNCE_TIME = 0.20

# Temps maximum pour regrouper les frappes
GROUP_TIMEOUT = 0.80
# Le programme attend au maximum 0,8 seconde après la dernière frappe.

cmd = [
"arecord",
"-D", DEVICE,
"-f", "S16_LE",
"-r", str(RATE),
"-c", str(CHANNELS),
"-t", "raw",
"-"
]

process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL
)

nombre_frappes = 0
derniere_frappe = 0
dernier_signal = 0

print("Écoute du microphone...")
print("1 frappe = LED rouge")
print("3 frappes = LED verte")
print("Ctrl+C pour arrêter.\n")

try:
while True:

data = process.stdout.read(CHUNK * 2)

if len(data) != CHUNK * 2:
break

audio = np.frombuffer(data, dtype=np.int16)
audio = audio.astype(np.float32) / 32768.0

volume = np.sqrt(np.mean(audio ** 2))

maintenant = time.time()

# Affichage du volume
bar = min(50, int(volume * 50))

print(
f"\rVolume : {volume:.3f} "
f"[{'#' * bar:<50}] "
f"Frappes : {nombre_frappes}",
end="",
flush=True
)

# Détection d'une frappe
if volume > THRESHOLD:

# Évite de compter plusieurs fois la même frappe
if maintenant - derniere_frappe > DEBOUNCE_TIME:

nombre_frappes += 1
derniere_frappe = maintenant

print(f"\n>>> Frappe {nombre_frappes}")

# Si le délai entre les frappes est dépassé,
# on analyse le nombre de frappes
if (
nombre_frappes > 0
and maintenant - derniere_frappe > GROUP_TIMEOUT
):

if nombre_frappes == 1:
print(">>> 1 FRAPPE : avancer")
MR25.ledRGB("100")
MR25.forward(30)
time.sleep(0.5)
MR25.ledRGB("000")

elif nombre_frappes == 2:
print(">>> 2 FRAPPES : tourner à gauche")
MR25.ledRGB("010")
MR25.turnLeft(30)
time.sleep(0.5)
MR25.ledRGB("000")

elif nombre_frappes == 3:
print(">>> 3 FRAPPES : tourner à droite")
MR25.ledRGB("001")
MR25.turnRight(30)
time.sleep(0.5)
MR25.ledRGB("000")

elif nombre_frappes == 4:
print(">>> 4 FRAPPES : STOP")
MR25.ledRGB("111")
MR25.stop()
time.sleep(0.5)
MR25.ledRGB("000")

else:
print(f">>> {nombre_frappes} frappes : aucune action")

# Réinitialisation
nombre_frappes = 0

except KeyboardInterrupt:
print("\n\nArrêt du robot.")
MR25.stop()

finally:
process.terminate()

Fin du tuto !

Les condensateurs de sécurité : classes X et Y

Les condensateurs de sécurité sont des composants électroniques spécialement conçus pour être utilisés dans les circuits raccordés au secteur électrique. Contrairement aux condensateurs classiques, ils sont conçus pour limiter les risques d’incendie, de choc électrique et de détérioration dangereuse en cas de défaut. On distingue principalement deux grandes familles : les condensateurs de classe X et les condensateurs de classe Y.

Condensateurs de sécurité : classes X et Y

1. Pourquoi utiliser des condensateurs de sécurité ?

Dans un appareil électrique, un condensateur peut être placé directement sur le secteur afin de :

  • réduire les parasites électromagnétiques ;
  • filtrer les perturbations conduites ;
  • améliorer la compatibilité électromagnétique (CEM) ;
  • supprimer certains parasites générés par les moteurs ou les alimentations à découpage.

Cependant, un condensateur connecté au secteur constitue un élément soumis à des tensions potentiellement dangereuses. Son comportement en cas de défaillance est donc particulièrement important. Les condensateurs de sécurité sont conçus et testés pour présenter un comportement contrôlé lorsqu’ils vieillissent ou tombent en panne.

2. Les condensateurs de classe X

Les condensateurs X sont utilisés lorsqu’ils sont connectés entre les conducteurs actifs du secteur, par exemple entre phase et neutre. On les retrouve notamment dans les filtres antiparasites placés à l’entrée des appareils électriques. Le condensateur X est donc placé en parallèle sur l’alimentation.

Condensateur de classe X

3. Les condensateurs de classe Y

Les condensateurs Y ont une fonction de sécurité différente. Ils sont utilisés lorsqu’un condensateur est connecté entre une partie reliée au secteur et une partie accessible ou reliée à la terre.

Par exemple :

On peut également trouver des condensateurs Y entre les conducteurs actifs et la terre de protection.

Pourquoi les exigences sont-elles plus sévères ?

Une défaillance d’un condensateur Y pourrait, dans certaines configurations, créer une liaison dangereuse entre le secteur et une partie accessible de l’appareil. Le condensateur Y doit donc être conçu de manière à ce qu’une défaillance ne mette pas l’utilisateur en danger. C’est pourquoi les condensateurs Y sont soumis à des exigences de sécurité particulièrement strictes.

4. Différence fondamentale entre X et Y

La distinction peut être résumée ainsi :

Une règle pratique importante est donc :

X = entre conducteurs actifs
Y = vers la terre 

5. Les sous-classes X1, X2, X3

Les condensateurs X sont eux-mêmes divisés en plusieurs catégories, notamment X1, X2 et X3. Ces catégories correspondent à différents niveaux de tenue aux surtensions et à différentes conditions d’utilisation. Le X2 est très courant dans les équipements électroniques alimentés par le secteur et dans les filtres antiparasites. Il ne faut cependant pas choisir un condensateur uniquement en fonction de sa capacité. Sa classe de sécurité, sa tension nominale, les conditions du réseau et l’application doivent également être prises en compte.

6. Les sous-classes Y1, Y2, Y4

Les condensateurs Y sont également subdivisés en différentes catégories, notamment Y1, Y2 et Y4.

Les exigences dépendent notamment de la tension du réseau et du niveau d’isolation requis.

De manière générale, les condensateurs Y sont conçus avec des exigences d’isolation et de sécurité plus sévères que les condensateurs X.

7. Attention : un condensateur X ne remplace pas un condensateur Y

C’est une erreur importante à éviter.

Un condensateur X2 et un condensateur Y2 ne sont pas interchangeables simplement parce qu’ils ont la même capacité et une tension nominale similaire.

Par exemple, remplacer un condensateur Y par un condensateur X dans un circuit relié à une partie accessible peut compromettre la sécurité électrique de l’appareil.

De même, utiliser un condensateur ordinaire à la place d’un condensateur de sécurité est dangereux.

8. Marquage et identification

Les condensateurs de sécurité portent généralement un marquage indiquant leur classe, par exemple :

  • X2
  • Y2
  • X1
  • Y1

Ils peuvent également comporter des indications concernant :

  • la capacité, par exemple 100 nF ;
  • la tension nominale ;
  • la température maximale ;
  • les normes auxquelles ils répondent ;
  • le fabricant et la référence du composant.

Il est important de vérifier le marquage réel du composant plutôt que de déduire sa classe uniquement à partir de son aspect extérieur.

9. Exemples de condensateurs de sécurité

Voici quelques exemples courants de condensateurs de sécurité utilisés dans les équipements électriques et électroniques :

  • Condensateur X2 – 100 nF / 275 VAC : utilisé entre la phase et le neutre pour supprimer les parasites haute fréquence dans les alimentations secteur.
  • Condensateur X2 – 470 nF / 275 VAC : utilisé dans les filtres antiparasites lorsque le filtrage capacitif nécessaire est plus important.
  • Condensateur Y2 – 2,2 nF / 300 VAC : utilisé entre une ligne secteur et la terre ou une partie accessible reliée à la terre, notamment pour réduire les perturbations électromagnétiques.
  • Condensateur Y1 – 4,7 nF / 500 VAC : utilisé dans certaines applications nécessitant un niveau d’isolation plus élevé.

 

10. Conclusion

Les condensateurs X et Y remplissent tous deux des fonctions de filtrage antiparasite, mais leur rôle de sécurité est différent.

Condensateur X :

  • connecté entre les conducteurs actifs ;
  • généralement entre phase et neutre ;
  • principalement concerné par le risque associé à un court-circuit entre les lignes ;
  • utilisé notamment dans les filtres secteur.

Condensateur Y :

  • connecté entre une partie reliée au secteur et la terre ou une partie accessible selon l’architecture ;
  • soumis à des exigences de sécurité particulièrement strictes ;
  • destiné à limiter le risque de choc électrique en cas de défaillance.

Le choix d’un condensateur de sécurité ne doit donc pas se limiter à la capacité et à la tension inscrites sur le composant. La classe de sécurité X ou Y fait partie intégrante de sa fonction dans le circuit.

Réparation d’un robot mobile SCOUT 2.0

Aujourd’hui, Macé Robotics à réparer un robot mobile SCOUT 2.0. Ce robot, dédié à la recherche et à l’enseignement, présentait un dysfonctionnement au niveau d’un moteur : celui-ci ne tournait pas correctement, avec une perte de couple et une vitesse de rotation insuffisante. Les trois autres moteurs fonctionnent, quant à eux, correctement.

Robot SCOUT 2.0

Ce robot est équipé de quatre moteurs brushless, chacun piloté par un driver dédié. Les quatre drivers sont reliés à la carte mère du robot via un bus de communication CAN.

Le driver d’un moteur

Après démontage du robot et inspection de la carte driver, j’en déduis que le problème provient de la carte capteur du moteur BLDC. Un moteur Brushless DC (BLDC) doit connaître en permanence la position de son rotor afin que le driver puisse déterminer quelles phases alimenter et à quel moment. Cette information permet de synchroniser le champ magnétique généré par le stator avec la position du rotor et ainsi d’assurer une rotation correcte du moteur. En cas de défaut de détection de la position du rotor, cette synchronisation n’est plus assurée. Le moteur peut alors présenter différents symptômes : vibrations, perte de couple, vitesse de rotation incorrecte, rotation dans le mauvais sens ou encore impossibilité de démarrer. Ces symptômes correspondent précisément au comportement observé sur le moteur concerné du robot, ce qui oriente le diagnostic vers un dysfonctionnement de la carte capteur du moteur BLDC.

Démontage de la carte capteur du moteur

Ensuite, la carte capteur du moteur a été démontée afin d’effectuer une inspection plus approfondie. Cette carte est équipée d’un encodeur magnétique MT6825 ainsi que de deux drivers RS422 TPT4031. L’encodeur magnétique utilise les propriétés du champ magnétique pour mesurer avec précision la position angulaire du rotor. Pour permettre cette mesure, un aimant est fixé directement sur l’axe du moteur (voir photo). Lorsque le rotor tourne, l’aimant entraîne une variation du champ magnétique perçue par l’encodeur, qui peut ainsi déterminer la position angulaire du rotor et transmettre cette information à la carte driver.

Carte capteur du moteur BLDC

Après réparation de la carte capteur, la roue du robot fonctionne à nouveau correctement. Les performances du moteur sont revenues à la normale et le robot est désormais pleinement opérationnel. Le SCOUT 2.0 est donc prêt à reprendre du service et à partir pour de nouvelles aventures !

Bonne journée,

Nicolas

Réseau de neurones artificiel

Présentation

Dans cet article nous allons apprendre au robot MR25 à éviter les obstacles en utilisant un réseau de neurones. Nous allons coder un réseau à 2 couches, une seule couche (le perceptron) suffit quand le problème est linéairement séparable, c’est-à-dire qu’on peut tracer une ligne droite entre les cas « oui » et les cas « non ».

Éviter un obstacle unique devant soi, c’est ce cas. Simple, rapide, mais très limité. Deux couches de neurones permet de résoudre des problèmes avec des frontières courbes ou des zones multiples. C’est exactement notre cas : il faut distinguer 4 situations (libre, gauche, droite, obstacle) qui ne sont pas séparables par une seule ligne dans l’espace des 5 capteurs.

La couche 1 construit des « concepts intermédiaires » (obstacle gauche, obstacle devant, obstacle droite), et la couche 2 combine ces concepts pour décider (action).

Voici le réseau de neurone utiliser avec le robot MR25 :

Le Programme

Ce programme implémente un petit réseau de neurones à deux couches (apprentissage supervisé ) qui apprend à piloter un robot MR25 à partir de ses 5 capteurs de proximité. Il comporte deux phases :

  1. Phase d’apprentissage  : le réseau apprend à associer des situations à des actions.
  2. Phase de pilotage en temps réel  : le robot utilise le réseau entraîné pour décider de ses mouvements.

Les 5 capteurs sont regroupés en 3 zones :

  • Neurone gauche : il mesure l’état de la zone gauche.
  • Neurone devant : il représente l’état de la zone centrale.
  • Neurone droit : il représente l’état de la zone droite.

Le réseau apprend grâce à la rétropropagation du gradient, puis pilote le robot en temps réel en transformant les mesures des capteurs en actions de navigation et d’évitement d’obstacles. La rétropropagation du gradient (backpropagation en anglais) est l’algorithme qui permet à un réseau de neurones d’apprendre de ses erreur, on commence par corriger la couche de sortie, puis on remonte vers la couche 1.

L’idée est simple :

  1. Le réseau fait une prédiction.
  2. On compare cette prédiction à la réponse attendue.
  3. On mesure l’erreur.
  4. On remonte cette erreur de la sortie vers les couches précédentes.
  5. On modifie les poids pour réduire l’erreur lors du prochain essai.

Le gradient indique dans quelle direction modifier les poids pour diminuer l’erreur le plus rapidement. Le gradient indique la pente. Le réseau suit la pente descendante pour atteindre le minimum d’erreur.

#!/usr/bin/python3
import MR25
import time
import math

# =============================================================================
# MR25 — Réseau de neurones à deux couches
# =============================================================================
#
# Couche 1 : 3 neurones de perception (5 entrées → 3 sorties continues)
#
# Neurone 1 (gauche) : capteurs 1 et 2
# Neurone 2 (devant) : capteurs 2, 3 et 4
# Neurone 3 (droite) : capteurs 4 et 5
#
# Couche 2 : 4 neurones de décision (3 entrées → 4 sorties)
#
# Sortie 0 : AVANCE
# Sortie 1 : TOURNE GAUCHE
# Sortie 2 : TOURNE DROITE
# Sortie 3 : STOP
#
# =============================================================================

# -----------------------------
# Fonctions d'activation
# -----------------------------

def sigmoid(x):
"""
Fonction sigmoïde — sortie continue dans ]0, 1[.
Utilisée dans la couche 1 pour produire un 'niveau d'obstacle' graduel.
"""
return 1.0 / (1.0 + math.exp(-x))

def softmax(vec):
"""
Softmax — transforme un vecteur en distribution de probabilités.
Utilisée dans la couche 2 : la somme des 4 sorties vaut 1.
L'action choisie est celle dont la probabilité est la plus haute.
"""
e = [math.exp(v) for v in vec]
s = sum(e)
return [v / s for v in e]

# -----------------------------
# Architecture du réseau
# -----------------------------
#
# Couche 1 — poids W1[neurone][capteur] et biais B1[neurone]
#
# Chaque neurone de perception combine les capteurs de son côté.
# Valeurs initiales : chaque capteur pèse pareil, biais nul.
#
# neurone 0 (gauche) → capteurs x1, x2
# neurone 1 (devant) → capteurs x2, x3, x4 (x2 et x4 = flancs proches centre)
# neurone 2 (droite) → capteurs x4, x5

W1 = [
# x1 x2 x3 x4 x5
[0.5, 0.5, 0.0, 0.0, 0.0], # neurone 0 : gauche
[0.0, 0.33, 0.33, 0.33, 0.0], # neurone 1 : devant
[0.0, 0.0, 0.0, 0.5, 0.5], # neurone 2 : droite
]
B1 = [0.0, 0.0, 0.0]

# Couche 2 — poids W2[action][neurone] et biais B2[action]
#
# action 0 (AVANCE) → récompenser l'ABSENCE d'obstacle
# action 1 (TOURNE GAUCHE) → obstacle à droite, pas à gauche
# action 2 (TOURNE DROITE) → obstacle à gauche, pas à droite
# action 3 (STOP) → obstacle des deux côtés

W2 = [
# n_gauche n_devant n_droite
[-0.5, -1.0, -0.5], # AVANCE : mauvais score si obstacle
[ 0.0, -0.5, 1.0], # TOURNE GAUCHE: obstacle à droite
[ 1.0, -0.5, 0.0], # TOURNE DROITE: obstacle à gauche
[ 0.5, 1.0, 0.5], # STOP : obstacles partout
]
B2 = [0.0, 0.0, 0.0, 0.0]

# Taux d'apprentissage
eta = 0.1

# -----------------------------
# Jeu d'entraînement
# -----------------------------
#
# Entrées : [x1, x2, x3, x4, x5] normalisées dans [0, 1]
# 0 = obstacle très proche 1 = voie libre
#
# Cibles : indice de l'action correcte
# 0 = AVANCE 1 = TOURNE GAUCHE 2 = TOURNE DROITE 3 = STOP
#
# Répartition des capteurs sur le MR25 (vue de dessus) :
#
# [1] [2] [3] [4] [5]
# \ | (avant) | /
# gauche droite

training_data = [
# Voie complètement libre → AVANCE
([1.0, 1.0, 1.0, 1.0, 1.0], 0),

# Obstacle devant uniquement → STOP (pas d'info sur quel côté éviter)
([0.9, 0.9, 0.1, 0.9, 0.9], 3),

# Obstacle à gauche (capteurs 1-2 faibles) → TOURNE DROITE
([0.1, 0.2, 0.8, 0.9, 0.9], 2),
([0.2, 0.1, 1.0, 1.0, 1.0], 2),

# Obstacle à droite (capteurs 4-5 faibles) → TOURNE GAUCHE
([0.9, 0.9, 0.8, 0.2, 0.1], 1),
([1.0, 1.0, 1.0, 0.1, 0.2], 1),

# Obstacle sur toute la largeur → STOP
([0.1, 0.1, 0.1, 0.1, 0.1], 3),
([0.2, 0.3, 0.2, 0.3, 0.2], 3),

# Obstacle léger devant-gauche, droite libre → TOURNE DROITE
([0.3, 0.4, 0.4, 0.8, 0.9], 2),

# Obstacle léger devant-droite, gauche libre → TOURNE GAUCHE
([0.9, 0.8, 0.4, 0.4, 0.3], 1),
]

# Noms des actions (pour les affichages)
ACTIONS = ["AVANCE", "TOURNE GAUCHE", "TOURNE DROITE", "STOP"]

# -----------------------------
# Propagation avant (forward pass)
# -----------------------------

def forward(inputs):
"""
Calcule la sortie complète du réseau pour un vecteur d'entrées.

Retourne :
h — sorties de la couche 1 (3 valeurs sigmoïdes)
probs — distribution softmax de la couche 2 (4 probabilités)
action — indice de l'action ayant la probabilité la plus haute
"""
# --- Couche 1 : neurones de perception ---
h = []
for n in range(3):
z = sum(W1[n][i] * inputs[i] for i in range(5)) + B1[n]
h.append(sigmoid(z))

# --- Couche 2 : neurones de décision ---
logits = []
for a in range(4):
z = sum(W2[a][n] * h[n] for n in range(3)) + B2[a]
logits.append(z)

probs = softmax(logits)
action = probs.index(max(probs))
return h, probs, action

# -----------------------------
# Mise à jour des poids (rétropropagation simplifiée)
# -----------------------------
#
# On utilise une règle delta adaptée au softmax + entropie croisée :
# delta_couche2[a] = probs[a] - (1 si a == cible, sinon 0)
#
# Pour la couche 1, on rétropropage le gradient à travers la sigmoïde :
# delta_couche1[n] = sigmoid'(h[n]) * somme_a(W2[a][n] * delta_c2[a])
# avec sigmoid'(h) = h * (1 - h)

def train_step(inputs, target, h, probs):
"""
Effectue une mise à jour des poids W1, B1, W2, B2
à partir d'un exemple (inputs, target).
"""
# --- Gradient couche 2 ---
delta2 = [probs[a] - (1.0 if a == target else 0.0) for a in range(4)]

# --- Mise à jour W2, B2 ---
for a in range(4):
for n in range(3):
W2[a][n] -= eta * delta2[a] * h[n]
B2[a] -= eta * delta2[a]

# --- Gradient couche 1 (rétropropagation) ---
delta1 = []
for n in range(3):
grad = sum(W2[a][n] * delta2[a] for a in range(4))
delta1.append(h[n] * (1.0 - h[n]) * grad)

# --- Mise à jour W1, B1 ---
for n in range(3):
for i in range(5):
W1[n][i] -= eta * delta1[n] * inputs[i]
B1[n] -= eta * delta1[n]

# -----------------------------
# Phase d'apprentissage
# -----------------------------

print("=" * 50)
print(" APPRENTISSAGE DU RÉSEAU DE NEURONES")
print("=" * 50)

for epoch in range(500):
nb_erreurs = 0

for inputs, target in training_data:
h, probs, action = forward(inputs)
if action != target:
nb_erreurs += 1
train_step(inputs, target, h, probs)

# Affichage toutes les 50 époques
if (epoch + 1) % 50 == 0:
print(f"Époque {epoch + 1:4d} — erreurs : {nb_erreurs}/{len(training_data)}")

if nb_erreurs == 0:
print(f"\nConvergence atteinte à l'époque {epoch + 1}")
break

print("\nPoids finaux — couche 1 (perception) :")
for n, nom in enumerate(["Gauche", "Devant", "Droite"]):
print(f" Neurone {nom} : w={[f'{v:.3f}' for v in W1[n]]} b={B1[n]:.3f}")

print("\nPoids finaux — couche 2 (décision) :")
for a, nom in enumerate(ACTIONS):
print(f" {nom:15s} : w={[f'{v:.3f}' for v in W2[a]]} b={B2[a]:.3f}")

# Vérification finale sur le jeu d'entraînement
print("\nVérification sur les données d'entraînement :")
for inputs, target in training_data:
_, probs, action = forward(inputs)
statut = "✓" if action == target else "✗"
print(f" {statut} capteurs={inputs} attendu={ACTIONS[target]:15s} obtenu={ACTIONS[action]}")

print("\nDémarrage du robot dans 5 secondes…")
time.sleep(5)

# -----------------------------
# Pilotage en temps réel
# -----------------------------

SEUIL_MAX = 80.0 # mm — distance au-delà de laquelle la voie est considérée libre

print("\n" + "=" * 50)
print(" PILOTAGE EN COURS (Ctrl+C pour stopper)")
print("=" * 50)

try:
while True:
# --- Lecture et normalisation des 5 capteurs ---
raw = [MR25.proxSensor(i) for i in range(1, 6)]
inputs = [min(v, SEUIL_MAX) / SEUIL_MAX for v in raw]

# --- Inférence ---
h, probs, action = forward(inputs)

# --- Affichage diagnostique ---
obstacle = ["⬛" if v < 0.5 else "⬜" for v in inputs]
print(
f"Capteurs {''.join(obstacle)} "
f"G={h[0]:.2f} D={h[1]:.2f} Dr={h[2]:.2f} "
f"→ {ACTIONS[action]}"
)

# --- Commande moteur ---
if action == 0:
MR25.forward(40)
elif action == 1:
MR25.turnLeft(30)
elif action == 2:
MR25.turnRight(30)
else:
MR25.stop()

time.sleep(0.1) # 10 Hz

except KeyboardInterrupt:
MR25.stop()
print("\nArrêt propre.")

# end of file

IA – Neurone artificiel avec le robot MR25 (Partie 2)

Présentation

Dans cet article nous allons apprendre au robot MR25 à éviter les obstacles en utilisant un neurone perception avec 5 entrées. Avec les 5 capteurs du robot, il est préférable de ne plus simplement décider avance / stop comme dans la partie n°1, mais aussi de choisir une direction lorsque l’on détecte un obstacle.

Voici l’idée :

  • Capteurs 1 et 2 → surveillent la gauche.
  • Capteur 3 → surveille le centre.
  • Capteurs 4 et 5 → surveillent la droite.

  • Distance proche de 0 mm → obstacle très proche.
  • Distance proche de 250 mm → espace libre.

On peut entraîner un perceptron à 5 entrées qui décide si le robot peut avancer, puis utiliser les capteurs pour déterminer le sens de rotation lorsqu’il ne peut pas avancer.

#!/usr/bin/python3

import MR25 # Bibliothèque de contrôle du robot MR25
import time # Pour les pauses dans la boucle principale

# ============================================================
# PERCEPTRON — Neurone artificiel à seuil
# ============================================================
# Un perceptron calcule une somme pondérée de ses entrées,
# puis applique une fonction d'activation pour décider : 0 ou 1.

def activation(x):
"""Fonction d'activation en échelon (step function).
Retourne 1 si x >= 0 (voie libre), 0 sinon (obstacle)."""
return 1 if x >= 0 else 0

# 5 poids — un par capteur de proximité (c1 à c5)
w = [0.0, 0.0, 0.0, 0.0, 0.0]
b = 0.0 # Biais : ajuste le seuil de décision
eta = 0.1 # Taux d'apprentissage (learning rate) : amplitude des corrections

# ============================================================
# DONNÉES D'ENTRAÎNEMENT
# ============================================================
# Chaque exemple est un couple ([c1,c2,c3,c4,c5], étiquette)
# Les valeurs des capteurs sont normalisées entre 0.0 et 1.0 :
# - Proche de 0.0 → obstacle détecté (capteur saturé)
# - Proche de 1.0 → voie dégagée (capteur à portée max)
# Étiquette : 0 = obstacle → s'arrêter/tourner, 1 = avancer

training_data = [
# --- Cas OBSTACLE : capteurs faibles (obstacles proches) ---
([0.1, 0.1, 0.1, 0.1, 0.1], 0), # Obstacles partout
([0.2, 0.2, 0.2, 0.2, 0.2], 0), # Obstacles proches
([0.3, 0.2, 0.1, 0.2, 0.3], 0), # Obstacle central fort
([0.2, 0.2, 0.2, 0.8, 0.8], 0), # Obstacles proches
([0.8, 0.8, 0.2, 0.2, 0.2], 0), # Obstacles proches

# --- Cas AVANCER : capteurs forts (voie libre) ---
([0.8, 0.8, 0.8, 0.8, 0.8], 1), # Voie totalement libre
([0.9, 0.8, 0.9, 0.8, 0.9], 1), # Voie très dégagée
([0.7, 0.9, 0.8, 0.9, 0.7], 1), # Voie dégagée (légère variation)
]

# ============================================================
# PHASE D'APPRENTISSAGE — Règle de Hebb / Perceptron
# ============================================================
# Pour chaque exemple, on calcule la sortie et on corrige les
# poids proportionnellement à l'erreur : Δw = η × erreur × entrée

for epoch in range(100): # Maximum 100 passes sur les données

erreur_totale = 0

for inputs, target in training_data:

# Calcul de la somme pondérée : s = Σ(wi × xi) + b
s = sum(wi * xi for wi, xi in zip(w, inputs)) + b

# Application de la fonction d'activation
y = activation(s)

# Erreur = différence entre valeur attendue et valeur calculée
erreur = target - y # 0, +1 ou -1

# Mise à jour des poids : correction proportionnelle à l'erreur
for i in range(5):
w[i] += eta * erreur * inputs[i]

# Mise à jour du biais (entrée fictive toujours égale à 1)
b += eta * erreur

erreur_totale += abs(erreur)

# Arrêt anticipé si le perceptron classe tout correctement
if erreur_totale == 0:
break

# Affichage des paramètres appris
print("Poids :", w)
print("Biais :", b)

# ============================================================
# PILOTAGE EN TEMPS RÉEL — Boucle principale
# ============================================================

SEUIL_MAX = 250.0 # Distance maximale de détection (en cm ou unité capteur)
# Au-delà, on considère la voie comme libre

try:
while True:

# --- Lecture et normalisation des 5 capteurs de proximité ---
# proxSensor(n) retourne une valeur brute ; on la plafonne à SEUIL_MAX,
# puis on divise pour obtenir un ratio entre 0.0 et 1.0
p1 = MR25.proxSensor(1)
p2 = MR25.proxSensor(2)
p3 = MR25.proxSensor(3)
p4 = MR25.proxSensor(4)
p5 = MR25.proxSensor(5)

capteurs = [p1, p2, p3, p4, p5]
print(capteurs)

c1 = min(p1, SEUIL_MAX) / SEUIL_MAX # Capteur avant-gauche extrême
c2 = min(p2, SEUIL_MAX) / SEUIL_MAX # Capteur avant-gauche
c3 = min(p3, SEUIL_MAX) / SEUIL_MAX # Capteur avant-centre
c4 = min(p4, SEUIL_MAX) / SEUIL_MAX # Capteur avant-droite
c5 = min(p5, SEUIL_MAX) / SEUIL_MAX # Capteur avant-droite extrême

entree = [c1, c2, c3, c4, c5]
print(entree)

# --- Inférence : le perceptron décide d'avancer ou non ---
s = sum(wi * xi for wi, xi in zip(w, entree)) + b
print("s = ", s)
avance = activation(s) # 1 = avancer, 0 = éviter l'obstacle

print("avance = ", avance)

if avance:
# Voie libre → avancer à vitesse modérée
MR25.forward(25)
print("AVANCE")

else:
# Obstacle détecté → choisir le côté le plus dégagé pour tourner

gauche = c1 + c2 # Score d'espace à gauche (somme des 2 capteurs gauches)
droite = c4 + c5 # Score d'espace à droite (somme des 2 capteurs droits)

# Le côté avec le score le plus élevé est le plus dégagé
if gauche > droite:
MR25.turnLeft(40) # Plus d'espace à gauche → tourner à gauche
print("TOURNE GAUCHE")
else:
MR25.turnRight(40) # Plus d'espace à droite (ou égalité) → tourner à droite
print("TOURNE DROITE")

time.sleep(0.2) # Pause

except KeyboardInterrupt:
# Arrêt propre du robot lors d'une interruption clavier (Ctrl+C)
MR25.stop()
# en of file

Le neurone perceptron simple est un modèle de prédiction linéaire. Il est nécessaire d’utiliser plusieurs couches (réseau) pour résoudre des problèmes plus complexes.

Fin !