Lib yarp APP » Historique » Révision 40
Révision 39 (Frederic Elisei, 05/05/2025 16:42) → Révision 40/43 (Frederic Elisei, 05/05/2025 16:50)
h1. Objectif Pouvoir réaliser rapidement une maquette, en python, d'une interaction *avec le robot Furhat,* en faisant intervenir : * synthèse et reconnaissance de parole via Furhat (et le cloud) * détection des interlocuteurs avec la caméra de Furhat * détection visuelle *avec une caméra externe et YOLO* (cartes et main vues de dessus par exemple) * gestion du regard/orientation de la tête du robot vers les interlocuteurs ou les cartes détectées par YOLO On découpera l'application sous forme d'un automate réactif, composé d'états simples (des objets python). L'environnement d'exécution s'appuie aussi sur le middleware YARP [https://yarp.it/latest/], pour permettre la répartition des tâches entre plusieurs machines (Windows, Linux...) qui communiqueront par messages. !{width:300px}VideoCapture_20250126-104416.jpg!:https://redmine.gipsa-lab.grenoble-inp.fr/attachments/1046 { <<< Cliquez sur l'image pour voir la vidéo résultat de l'après-midi Kaleidoscope avec 4 étudiants} Crédits "vidéo :":https://redmine.gipsa-lab.grenoble-inp.fr/attachments/1046 Frédéric ELISEI (GIPSA-lab), avec Benjamin POIREAULT (Ense3), Alexis LE MEUR (Ensimag), Kinjy BIALADE (Ense3) et Ivan PETERSCHMITT (Ensimag) h1. Squelette minimal : On va s'appuyer sur lib_yarp_APP.py (et indirectement sur my_yarp.py) pour créer nos états, programmer des actions réactives et gérer les transitions entre états. <pre><code class="python"> #! /usr/bin/env python3 # import app, furhat, State, AnyKeywords, yolo_center, yolo_target from lib_yarp_APP import * # create and run initial state, through its name State("main") app.run("main") </code></pre> h1. Gestion des évènements et perception : Les états -- comme celui nommé "main" dans l'exemple précédent -- sont des objets dérivés de la classe State (ou d'une sous-classe). *Les états doivent avoir des noms différents.* Pour implémenter des réactions à des évènements, il faut qu'ils fournissent une implémentation de tout ou partie des prototypes suivants : | <code class="python">do_in(self)</code> | appelée lorsqu'on rentre nouvellement dans l'état. Par exemple pour dire un message de bienvenue.| # | <code class="python">do_out(self)</code> | appelée lorsqu'on quitte l'état.| # | <code class="python">do_reco(self, key, msg)</code> | *msg* contient la chaîne de parole reconnue par le robot. *key* peut faciliter la détection de certaines classes d'intention, indépendamment de leur formulation exacte. Il n'y en a pas beaucoup par défaut : "Oui{}" ou "Non{}" ou "Bonjour{}" "Fini{}" ... "Oui{}" "je suis d'accord" "Oui{}" "oui" "Oui{}" "ok" "unknown{}" "C'est complètement l'idée que je me faisais de la chose." | # | <code class="python">do_user_in(self, user)</code> | un nouvel utilisateur a été détecté par le robot en face de lui. ... l'objet reçu a plusieurs champs: *user.id* qui peut être utilisé avec *app.track()* ou *app.glance()* *user.location* une chaine (str), directement utilisable avec *app.look3D()* *user.visible* vaudra *True* *user.total* rappelle combien d'utilisateurs sont détectés, celui-là compris *user.mouthOpen* peut aider à détecter qui parle (peu fiable...) *user.attending* vaut *"system"* si le robot pense qu'il est regardé par cet utilisateur. Il peut correspondre à un autre *user-id* si le robot croit détecter un regard dirigé vers un autre des interlocuteurs. | # | <code class="python">do_user_out(self, user)</code> | l'utilisateur précédemment détecté comme *user.id* n'est plus visible (et *user.visible* vaudra donc *False*) On ne peut bien sûr plus localiser ou regarder cet utilisateur ! *user.total* vous informe s'il reste d'autres utilisateurs, et combien. ... Si c'était le dernier utilisateur vers qui vous aviez fait un *app.attend()* votre cible est perdue. C'est peut-être le moment de faire un *app.attend("All")* pour ne pas donner l'impression aux autres que vous les ignorez... | # | <code class="python">do_detect(self,yins,ydel,observations)</code> | Cette méthode est appelée lorsque YOLO détecte des apparitions ou disparitions (de cartes, de mains...), ou régulièrement pour mettre à jour les observations et leurs positions. *yins* et *ydel* sont des sets python (itérables mais non indexables comme une liste ou un tableau !), possiblement vides. Par exemple *{}* ou *{0,4}* ou <code class="python">yins.pop()</code> si *yins* n'est pas vide. ... Les labels (str) sont accessibles en indexant *app.yolo_classes[]* : 0 correspond à "HAND" (une main), 1 à "CARD" (une carte inconnue), les suivants sont des cartes identifiées ("Bouilloire verte"...). Si deux mains deviennent visibles/invisibles, un set ne contiendra 0 qu'une fois au maximum (au moment de l'apparition ou de la disparition), mais *observations* contiendra les références multiples, ainsi que les coordonnées. ... *observations* est un tableau vide ou un tableau de chaînes, dont chacune contient les informations et coordonnées de chaque objet vu, *sous forme de chaîne* : avec le format *"id1 confidence x y w h"* ... <code class="python">len(observations)</code> vous donne le nombre d'objets observés (peut-être 0...) ... pour retrouver s'il y a une/des mains : <code class="python">(d for d in observations if d.startswith("0 ") )</code> | # | <code class="python">do_aruco(self,ains,adel, data)</code> | Cette méthode est appelée lorsque des marqueurs ARUCO ont été détectés sur le flux vidéo fourni au service (même flux que pour le détecteur fourni par YOLO). ... *data* est un tableau vide ou un tableau de chaînes, dont chacune contient les informations et coordonnées de chaque objet vu, *sous forme de chaîne* : <code class="python">obs=set(int(d.split()[0]) for d in data)</code> | On peut par exemple attacher une telle méthode directement à un ou plusieurs objets état : <pre><code class="python"> State("main").set_behaviour(State.do_reco,catch_default_msg) State("waiting").set_behaviour(State.do_reco,catch_default_msg) </code></pre> Plus classiquement, on peut sous-classer State (ou une de ses sous-classes), comme ici: <pre><code class="python"> class StateParrot(State): def do_reco(self,key,msg): app.say(msg) StateParrot("repeat_as_parrot") app.run("repeat_as_parrot") </code></pre> Si on a besoin de plusieurs méthodes dans le même état, ou beaucoup d'états différents, il est probablement plus simple/lisible d'utiliser des sous-classes. Si une méthode est utilisée dans plusieurs états, l'attacher aux instances est peut-être plus lisible, ou permet des modifications dynamiques. Commencer par sous-classer State avec tous les comportements par défaut est aussi une possibilité. h1. Génération d'actions Pour générer des actions/comportements, on s'appuie sur les états, à réception d'un événement ainsi que lorsqu'on y entre ou en sort. On ne peut *pas* générer d'action régulière (idle), ou au bout d'un certain temps sans passer par ces évènements ou transitions. C'est un choix lié à la vitesse d'exécution sous Python et pour éviter de se retrouver avec beaucoup d'évènements en retard non traités. Voici les actions possibles : | <code class="python">app.switch("etat2"</code>) | prépare la transition vers un autre état, via *son nom* (pas un objet). Elle ne sera pas instantanée, laissant le temps aux évènements déjà en attente d'être dépilés. En clair, c'est la même file d'attente pour les évènements et le changement d'état. | # | <code class="python">app.sayNB("texte à prononcer")</code> | fait prononcer au robot le texte voulu. La suite du traitement des évènements va reprendre dès que la phrase *commencera* à être prononcée _(NB = non blocking)_ | # | <code class="python">app.say("texte à prononcer")</code> | fait prononcer au robot le texte voulu. *Attention, l'appel est bloquant*, jusqu'à ce que la phrase soit prononcée en entier : plus la phrase est longue, plus des évènements en retard vont s'empiler dans la pile de traitement... ... La syntaxe suivante permet de synchroniser le regard sur certains mots, éventuellement pour certaines durées (en millisecondes) : *"Regarde cette personne|glance user_0| ou là|glance (1,2,3) 200|"* ... Pour éviter les erreurs sur les espacements obligatoires ou non autorisés (dans le triplet de coordonnées), il est conseillée d'utiliser la syntaxe avec la fonction auxiliaire suivante : <code class="python">app.sayNB("regarde ici|%s| ou |%|là", glancing("user_0")+glancing((1.0,2.0,3.0),200))</code> ... | # | <code class="python">app.track(target)</code> | active le suivi automatique par le regard de la cible *target* Les valeurs possible sont *"All",* ou le *user-id* reçu lors d'un évènement USERIN. Avec *"Nobody"* on désactive ce suivi automatique. | # | <code class="python">app.glance(target)</code> <code class="python">app.glance(target, duration=sec)</code> | comme précédemment, mais seulement en jetant un coup d’œil | # | <code class="python">app.look3D([x y z],duration=Tsec)</code> <code class="python">app.gaze3D([x y z],duration=Tsec)</code> | demande au robot de regarder aux coordonnées correspondant au triplet *[ x, y, z ]* en paramètre. Le repère est centré sur les yeux au repos, les coordonnées sont en mètres (x-axis to the robot's left, the y-axis up, and z-axis to the front). Si *duration* est spécifiée (en secondes), le regard reviendra sur sa cible initiale une fois ce délai écoulé. Sans ce paramètre, le regard et l'orientation de la tête du robot seront changés de façon plus définitive. ... Dans la version *gaze3D* seuls les yeux sont recrutés. Avec *look3D*, le cou peut être recruté aussi. | # | <code class="python">app.program(when, (msg_typ, msg) )</code> | permet de programmer un évènement dans le futur, pour réaliser un *timeout* par exemple un *timeout* et sortir de l'état courant. Pour que ces timers fonctionnent, *app.run()* doit avoir été invoqué avec *with_timer=True*. Attention, les types de messages, système ou utilisateurs, ne sont pas encore documentés/figés... ... L'exemple qui suit permet de Par exemple, pour retourner dans l'état "main" après dans 700 millisecondes environ (au prochain message dépilé quand le délai sera écoulé) : <code class="python">app.program(now()+.7, ("ST_in","main"))</code> ... Ici, "ST_in" provoque une transition vers un autre état (qui peut être le même... dans tous les cas, do_out() puis do_in() seront appelés). | h1. Méthodes auxiliaires | <code class="python">AnyKeyword(**strings).isin(message)</code> | pour tester la présence de l'un des mots clefs (synonymes) dans un message reçu par le robot. ... par exemple : if AnyKeyword('rouge','vert','bleu').isin(msg): | # | <code class="python">x,y = yolo_center(yolo_data)</code> | à partir d'une des lignes détectées par YOLO, retourne la position détectée *(x,y)* Utile pour comparer des positions de cartes (droite, gauche, proches d'une autre ou de la main...). | # | <code class="python">id = yolo_target(yolo_data,duration=Tsec)</code> <code class="python">id = yolo_target(yolo_data,cmd="Glance3D")</code> | à partir d'une des lignes détectées par YOLO, oriente tête et regard du robot vers la carte (ou main) correspondante, soit définitivement, soit temporairement si *duration* est passé en paramètre (durée en secondes). La valeur retournée *id* permet d'avoir le label correspondant via *app.yolo_classes[id]* |