5.2 ROBOTS INTELLIGENTS

 

 

INTRODUCTION

 

 

Les robots capables de trouver leur chemin dans un environnement dynamique présentent un potentiel d’applications important, notamment dans le domaine d’engins volants, dans l’exploration sous-marine ou dans l’examen des systèmes de canalisation d’égouts. Dans cette section, nous allons voir pas à pas comment construire un robot capable de s’orienter dans un environnement dynamique.


  CONCEPTS DE PROGRAMMATION: Robot auto-apprenant en mode autonome ou contrôlé à distance, mode apprentissage, mode exécution, boucle d’événements.

 

 

LE ROBOT CONNAÎT LE CHEMIN

 

Dans le cas le plus simple, un robot devrait être capable de trouver son chemin à l’intérieur d’un canal très spécial constitué d’éléments orthogonaux de même longueur.

L’information concernant la longueur constante des éléments du canal et si les virages tournent à gauche ou à droite est pour le moment codée en dur dans le programme.
 

from simrobot import *
#from nxtrobot import *
#from ev3robot import *

RobotContext.useObstacle("sprites/bg.gif", 250, 250)
RobotContext.setStartPosition(310, 470)

moveTime = 3200
turnTime = 545

robot = LegoRobot()
gear = Gear()
robot.addPart(gear)
gear.forward(moveTime)
gear.left(turnTime)
gear.forward(moveTime)
gear.right(turnTime)
gear.forward(moveTime)
gear.right(turnTime)
gear.forward(moveTime)
gear.left(turnTime)
gear.forward(moveTime)
robot.exit()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Il faut déterminer expérimentalement les bonnes valeurs pour moveTime et turnTime en réalisant plusieurs expériences successives et en ajustant les valeurs de manière appropriée. Il est clair que ces valeurs dépendent de la vitesse du robot. En situation réelle, on préfèrera spécifier le trajet à parcourir et les angles de rotation des roues plutôt que des durées pour les mouvements.  

 

 

ROBOT CONTRÔLÉ PAR UN HUMAIN

 

Dans le programme suivant, le robot connaît la longueur constante des éléments du canal mais ses mouvements de rotation sont contrôlés par un humain. Il n’est pas encore capable d’effectuer un apprentissage pour se souvenir du chemin emprunté. Il demeure donc pour le moment « stupide ».

Afin de contrôler le robot en mode simulation ou en mode de contrôle à distance, il faut utiliser les touches directionnelles gauche et droite du clavier. Pour faire de même en mode autonome, il faudra utiliser les boutons gauche et droite de la brique EV3. Les méthodes isLeftHit() et isRightHit() permettent de déterminer si les touches directionnelles ou les boutons de la brique ont été pressés et relâchés. Pour terminer le programme, il faut utiliser la touche Escape du clavier.
from simrobot import *
#from nxtrobot import *
#from ev3robot import *

RobotContext.useObstacle("sprites/bg.gif", 250, 250)
RobotContext.setStartPosition(310, 470)

moveTime = 3200
turnTime = 545

robot = LegoRobot()
gear = Gear()
robot.addPart(gear)
gear.forward(moveTime)

while not robot.isEscapeHit():
    if robot.isLeftHit():
        gear.left(turnTime)
        gear.forward(moveTime)
    if robot.isRightHit():
        gear.right(turnTime)
        gear.forward(moveTime)
robot.exit()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Dans ce cas, il est moins intéressant d’utiliser le robot en mode autonome puisque l’on veut pouvoir le contrôler à distance à l’aide des touches du clavier. Il est également possible d’utiliser le capteur infrarouge et la télécommande au lieu du clavier pour contrôler le robot dans les virages (cf. le matériel supplémentaire disponible à la fin de cette section).

 

 

MODE APPRENTISSAGE

 

Des systèmes assistés par ordinateur dont le comportement n’est pas codé en dur dans le programme mais qui sont capables d’adapter leur comportement à leur environnement sont appelés systèmes adaptatifs. Ils sont donc plus ou moins capables d’effectuer des apprentissages. Les robots industriels sont « entrainés » par des spécialistes de la tâche à accomplir en « mode apprentissage ». Cela consiste par exemple à enseigner au robot les mouvements précis qu’il doit effectuer avec son bras robotisé. Dans la plupart des cas, l’opérateur du robot utilise un système d’entrée similaire à une télécommande. Le robot est ainsi déplacé successivement à la position désirée qui est immédiatement enregistrée.

En mode exécution, le robot parcourt les états mémorisés de manière indépendante en adoptant toutefois une vitesse bien supérieure.

Comme précédemment, notre robot de canalisation connaît la longueur exacte des divers éléments droits mais les virages sont encore contrôlés par un humain. Dans le programme suivant, le robot est cependant en mesure de retenir les virages que lui fait faire l’humain en mode apprentissage pour ensuite les reproduire à l’identique en mode exécution aussi souvent que nécessaire.

Il est souvent fort utile de s’imaginer que le robot se trouve à chaque instant dans un état d’exécution particulier. Ces états sont typiquement représentés par des mots significatifs et stockés sous forme de chaînes de caractères. Dans notre cas, on considère les états suivants du robot : être arrêté, avancer en ligne droite, tourner à gauche ou à droite. On peut nommer ces états STOPPED, FORWARD, LEFT, RIGHT [plus... Il serait préférable de stocker les états dans un type énuméré (enum) mais un tel type n'est pas présent dans la version de Python disponible sur le EV3]

Au lieu de vérifier constamment l’état des touches directionnelles du clavier ou des boutons de la brique (polling), il faut adopter une solution plus élégante utilisant le modèle de programmation événementielle. On y parvient en ayant recours à des fonctions de rappel qui sont d’abord enregistrées puis appelées automatiquement indépendamment du programme lorsque survient l’événement correspondant.

Le programme principal utilise une boucle infinie pour effectuer l’action correspondant à l’état courant du robot. Les changements d’état ont lieu dans la fonction de rappel onButtonHit().
from simrobot import *
#from nxtrobot import *
#from ev3robot import *

RobotContext.useObstacle("sprites/bg.gif", 250, 250)
RobotContext.setStartPosition(310, 470)
RobotContext.showStatusBar(30)

def onButtonHit(buttonID):
    global state
    if buttonID == BrickButton.ID_LEFT:
        state = "LEFT"
    elif buttonID == BrickButton.ID_RIGHT:
        state = "RIGHT"
    elif buttonID == BrickButton.ID_ENTER:
        state = "RUN"

moveTime = 3200
turnTime = 545
memory = []        
robot = LegoRobot(buttonHit = onButtonHit)
gear = Gear()
robot.addPart(gear)
state = "FORWARD"

while not robot.isEscapeHit():
    if state == "FORWARD":
        robot.drawString("Moving forward", 0, 3)
        gear.forward(moveTime)
        state = "STOPPED"
        robot.drawString("Teach me!", 0, 3)
    elif state == "LEFT":
        memory.append(0)
        robot.drawString("Saved: LEFT-TURN", 0, 3)
        gear.left(turnTime)
        state = "FORWARD"
    elif state == "RIGHT":
        memory.append(1)
        robot.drawString("Saved: RIGHT-TURN", 0, 3)
        gear.right(turnTime)
        state = "FORWARD"
    elif state == "RUN":
        robot.drawString("Executing memory", 0, 1)
        robot.drawString(str(memory), 0, 2)
        robot.reset()
        robot.drawString("Moving forward", 0, 3)
        gear.forward(moveTime)
        for k in memory:
            if k == 0:
                robot.drawString("Turning left", 0, 3)
                gear.left(turnTime)
            else:          
                robot.drawString("Turning right", 0, 3)
                gear.right(turnTime)
            robot.drawString("Moving forward", 0, 3)
            gear.forward(moveTime)
        gear.stop()  
        robot.drawString("All done", 0, 3)
        state = "STOPPED"
robot.exit()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

La lecture de l’état courant et sa traduction en une action sont effectuées dans une boucle while infinie au sein du programme principal et non dans les fonctions de rappel. En informatique, on appelle ce genre de boucles infinies une boucle d’événements. Les fonctions de rappel ne servent qu’à changer l’état du robot (state switch). Cette technique de programmation permet d’obtenir une synchronisation chronologique très claire entre les tâches de longue durée et les appels des fonctions de rappel en réponse aux événements qui peuvent survenir d’une seconde à l’autre. On utilise une liste pour mémoriser la succession des états appris en mode apprentissage en y stockant les nombres 0 ou 1 selon que le canal tourne à gauche ou à droite.

 

 

ROBOT CAPABLE D’APPRENTISSAGE NON SUPERVISÉ

 

Dans certaines situations, le robot doit être capable de s’adapter à son environnement et d’effectuer des apprentissages sans être supervisé par un opérateur humain. Le robot pourrait par exemple se trouver hors de portée de tout moyen de communication sur la planète Mars.

Pour retrouver son chemin, le robot doit pouvoir percevoir son environnement grâce à ses capteurs intégrés et réagir de manière appropriée. Les humains perçoivent essentiellement l’environnement par la vue. Il est facile pour les robots de capturer des images de leur environnement mais il leur est très difficile d’analyser et d’interpréter ces images [plus... La reconnaissance d’images appartient au domaine de la reconnaissance de formes (Pattern recognition en anglais)].

Pour s’orienter dans la canalisation, notre robot n’utilisera qu’un capteur tactile qui déclenchera un événement lorsqu’il sera pressé. On suppose que les canalisations sont composées de segments rectilignes de longueur identique. Lorsque le robot détecte une pression sur le capteur tactile après avoir parcouru toute la longueur d’un segment, il sait qu’il se trouve face à un mur et qu’il doit tourner à gauche ou à droite. Il va donc reculer un peu pour se dégager du mur qui lui fait face et tente d’effectuer un virage à gauche. Si, presque immédiatement, il détecte à nouveau une interaction avec le capteur tactile, il sait qu’il a pris la mauvaise décision en tournant à gauche. Il va donc à nouveau reculer un petit peu et faire un demi-tour afin de partir vers la droite. Le robot va mémoriser le sens du virage à cet endroit du parcours de sorte qu’il pourra ensuite refaire le parcours sans se tromper dans les virages et en évitant, en théorie du moins, tous les murs.

Le programme suivant fait en sorte que le robot traverse le parcours en mode d’apprentissage non supervisé. Pour passer de la phase d’apprentissage au mode d’exécution, il faut presser la touche ENTER du clavier ou le bouton ENTER de la brique EV3.

from simrobot import *
#from nxtrobot import *
#from ev3robot import *

import time

RobotContext.useObstacle("sprites/bg.gif", 250, 250)  
RobotContext.setStartPosition(310, 470)
RobotContext.showStatusBar(30)

def onPressed(port):
    global startTime
    global backTime
    robot.drawString("Press event!", 0, 1)
    dt = time.clock() - startTime # time since last hit in s
    gear.backward(backTime)
    if dt > 2:
        memory.append(0) 
        gear.left(turnTime) # turning left   
    else:
        memory.pop()
        memory.append(1) 
        gear.right(2 * turnTime) # turning right
    robot.drawString("Mem: " + str(memory), 0, 1)
    gear.forward()
    startTime = time.clock()      

def run():
    for k in memory:
        robot.drawString("Moving forward", 0, 1)
        gear.forward(moveTime)
        if k == 0:
            robot.drawString("Turning left", 0, 1)
            gear.left(turnTime)            
        elif k == 1:
            robot.drawString("Turning right", 0, 1)
            gear.right(turnTime)            
    gear.forward(moveTime) 
    robot.drawString("All done", 0, 1)          

    
moveTime = 3200
turnTime = 545
backTime = 700
memory = []      
robot = LegoRobot()
gear = Gear()

robot.addPart(gear)
ts = TouchSensor(SensorPort.S3, pressed = onPressed)      
robot.addPart(ts)
startTime = time.clock()
gear.forward()
robot.drawString("Moving forward", 0, 1)

while not robot.isEscapeHit():
    if robot.isEnterHit():
        robot.reset()    
        run()      
robot.exit()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Le capteur tactile est connecté au port S3, ce qui est interprété en mode simulation comme un positionnement du capteur en position centrale sur l’avant du robot. Le capteur signale les événements tactiles par l’intermédiaire de la fonction de rappel onPressed() qui est enregistrée avec le paramètre pressed du constructeur TouchSensor() . Le capteur tactile est un composant standard qui est rajouté au robot avec addPart(). Pour déterminersi le robot a foncé dans une paroi en ligne droite ou, au contraire, après avoir fait une tentative infructueuse en tournant à gauche, on peut se baser sur le temps écoulé depuis le dernier événement généré par le capteur tactile. On utilise pour cela la fonction clock() du module time intégré à Python. Si ce temps est supérieur à deux secondes, c’est que le robot a simplement foncé dans le mur en ligne droite alors que, dans le cas contraire, il a fait une tentative de virage infructueuse et s’est retrouvé coincé. Du fait que le robot tourne toujours à gauche dans un premier temps en stockant un 0 dans sa mémoire, il doit remplacer ce 0 par un 1 s’il se retrouve bloqué suite à un faux virage.

 

 

UN ENVIRONNEMENT PLUS COMPLEXE

 

Vous avez probablement compris que le robot pourrait facilement déterminer par lui-même de combien il doit avancer en ligne droite sur chaque segment avant de tourner puisqu’il peut mesurer le temps qui s’écoule jusqu’à ce qu’il fonce dans un mur. De cette manière, le robot pourrait apprendre à se mouvoir dans un dédale de canalisation dont les segments sont de longueur variable. Cela nécessite cependant de mémoriser non seulement s’il doit tourner à gauche ou à droite, mais encore le temps de parcours de chacun des segments droits. Le plus simple est de stocker, pour chaque segment, ces deux informations dans une liste à deux éléments node = [moveTime, k], où moveTime est le temps de parcours (en ms), k = 0 correspond à un virage à gauche et k = 1 à un virage à droite.

On obtiendra bien ainsi le temps moveTime nécessaire pour parcourir le segment mais il ne faudra pas oublier de le corriger en déduisant le temps pendant lequel le robot a foncé dans la paroi de la canalisation. Une fois ce temps corrigé, il faut le stocker dans une variable globale réutilisable dans la situation où le robot s’est retrouvé bloqué suite à un faux virage.

 


# Robo2e.py

from simrobot import *
#from nxtrobot import *
#from ev3robot import *

import time

RobotContext.useObstacle("sprites/bg2.gif", 250, 250)  
RobotContext.setStartPosition(410, 460)
RobotContext.showStatusBar(30)

def pressCallback(port):
    global startTime
    global backTime
    global turnTime
    global moveTime
    dt = time.clock() - startTime # time since last hit in s
    gear.backward(backTime)
    if dt > 2: 
        moveTime = int(dt * 1000) - backTime  # save long-track time
        node = [moveTime, 0] 
        memory.append(node) # save long-track time 
        gear.left(turnTime) # turning left   
    else:
        memory.pop() # discard node
        node = [moveTime, 1] 
        memory.append(node) 
        gear.right(2 * turnTime) # turning right
    robot.drawString("Memory: " + str(memory), 0, 1)
    gear.forward()
    startTime = time.clock()      

def run():
    for node in memory:
        moveTime = node[0]
        k = node[1]
        robot.drawString("Moving forward",0, 1)
        gear.forward(moveTime)
        if k == 0:
            robot.drawString("Turning left",0, 1)
            gear.left(turnTime)            
        elif k == 1:
            robot.drawString("Turning right",0, 1)
            gear.right(turnTime)            
    gear.forward() # must stop manually
    robot.drawString("All done, press DOWN to stop", 0, 1)          
    isExecuting = False
    
turnTime = 545
backTime = 700
     
robot = LegoRobot()
gear = Gear()

robot.addPart(gear)
ts = TouchSensor(SensorPort.S3, pressed = pressCallback)      
robot.addPart(ts)
startTime = time.clock()
moveTime = 0
memory = [] 
gear.forward()

while not robot.isEscapeHit():
    if robot.isDownHit():  
        gear.stop()
    elif robot.isEnterHit():
        robot.reset()    
        run()      
robot.exit()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

À première vue, la structure de données représentant la mémoire du robot en tant que liste de nœuds peut paraître compliquée. Il faut cependant toujours stocker des valeurs intrinsèquement liées dans une structure de données commune. En l’occurrence, il est indispensable d’associer la durée de parcours du prochain segment de canalisation et le sens dans lequel tourner à la fin dans une structure de données commune regroupant les deux informations. Admirez la manière dont le robot est capable, avec un seul et même programme, de retrouver son chemin dans un canal complètement différent (par exemple bg3.gif).

 

 

EXERCICES

 

1.


Programmer le robot pour qu’il puisse trouver son chemin dans un canal comme l'image à droite. Le programme utilisera le capteur tactile pour détecter les parois sans pour autant effectuer d’apprentissage. Pour la phase de simulation, utiliser les options de RobotContext suivantes:

RobotContext.useObstacle("sprites/bg2gif", 250, 250)
RobotContext.setStartPosition(400, 470)

 

 
2.

Programmer une tondeuse robotisée équipée d’un capteur tactile pour qu’elle soit capable de tondre le gazon par bandes verticales successives. La tondeuse fonce dans les murs supérieurs et inférieurs avant de tourner de manière appropriée.

Pour la phase de simulation, utiliser les options de RobotContext suivantes:

RobotContext.useBackground("sprites/fieldbg.gif")
RobotContext.useObstacle("sprites/field1.gif", 250, 250)
RobotContext.setStartPosition(350, 300)
 

3.

Dans ce cas, la bordure supérieure du gazon est percée d’un trou par lequel la tondeuse robotisée peut s’échapper. Améliorer le programme de l’exercice précédent en rajoutant une composante d’apprentissage automatique permettant au robot de mémoriser la longueur des bandes après le premier passage afin d’éviter de devoir utiliser le capteur tactile pour détecter la bande supérieure.

Pour la phase de simulation, utiliser les options de RobotContext suivantes :

RobotContext.useBackground("sprites/fieldbg.gif")
RobotContext.useObstacle("sprites/field2.gif", 250, 250)
RobotContext.setStartPosition(350, 300)
 
 

 

   

MATÉRIEL SUPPLÉMENTAIRE


 

APPRENTISSAGE SUPERVISÉ À L’AIDE DE LA TÉLÉCOMMANDE IR

 

(uniquement en mode autonome sur le EV3)

Dans le dernier chapitre, vous avez déjà vu comment utiliser la télécommande IR pour commander le robot. Dans cette section, nous allons voir comment l’utiliser pour superviser l’apprentissage du robot. Le programme s’exécute en mode autonome directement sur le EV3, que ce soit en mode apprentissage ou en mode exécution.

Cette approche imite de manière réaliste la manière dont les robots industriels sont entraînés par une commande à distance pour mémoriser les actions à entreprendre qui sont simplement rejouées par la suite en mode exécution.

En mode apprentissage, l’opérateur utilise les deux boutons du haut de la télécommande pour faire tourner le robot à gauche ou à droite. Une pression sur le bouton inférieur gauche démarre le mode exécution. Une fois encore, il est élégant de travailler avec la notion d’états.


from ev3robot import *

def onActionPerformed(port, command):
    global state
    if command == 1:
        state = "LEFT"
    elif command == 3:
        state = "RIGHT"
    elif command == 2:
        state = "RUN"

moveTime = 3200
turnTime = 545
memory = []       
robot = LegoRobot()
gear = Gear()
gear.setSpeed(50)
robot.addPart(gear)
irs = IRRemoteSensor(SensorPort.S1, actionPerformed = onActionPerformed)
robot.addPart(irs)
state = "FORWARD"
robot.drawString("Learning...", 0, 3)

while not robot.isEscapeHit():
    if state == "FORWARD":
        gear.forward(moveTime)
        state = "STOPPED"
    elif state == "LEFT":
        memory.append(0)
        gear.left(turnTime)
        gear.forward(moveTime)
        state = "STOPPED"
    elif state == "RIGHT":
        memory.append(1)
        gear.right(turnTime)
        gear.forward(moveTime)
        state = "STOPPED"
    elif state == "RUN":
        robot.drawString("Executing...", 0, 3)
        robot.reset()
        gear.forward(moveTime)
        for k in memory:
            if k == 0:
                gear.left(turnTime)
            else:         
                gear.right(turnTime)
            gear.forward(moveTime)
        gear.stop() 
        robot.drawString("All done", 0, 3)
        state = "STOPPED"
robot.exit()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)