Robot ROS et Neural Grid Beggar

Habituellement, deux de ces questions se posent pour ces métiers: «comment?» et pour quoi?" La publication elle-même est consacrée à la première question, et je répondrai immédiatement à la seconde:

j'ai commencé ce projet afin de maîtriser la robotique, à commencer par le Raspberry Pi et la caméra. Comme vous le savez, l'un des meilleurs moyens d'apprendre quelque chose est de proposer une tâche technique et d'essayer de l'accomplir, tout en acquérant les compétences nécessaires.

À cette époque, je n'avais toujours pas d'idées brillantes dans le domaine de la robotique, alors j'ai décidé de faire un projet exclusivement amusant - un robot mendiant. Le résultat est un robot autonome sur le Raspberry Pi et le ROS, utilisant le Movidius Neural Cumpute Stick pour détecter les visages. Il erre dans la pièce, cherche des gens, et secoue une canette devant eux. Voici à quoi ressemble ce robot:



Le robot se déplace au hasard dans la pièce, et s'il remarque une personne, il s'enroule et secoue un pot pour de petites choses. Pour le plaisir, je lui ai ajouté une petite expression faciale - il sait bouger ses sourcils:



après la première tentative, le robot essaie de retrouver son visage en vue, se tourne vers la personne et secoue à nouveau la banque. Mais que se passe-t-il si vous partez en ce moment:



Robot


J'ai pris l'idée d'un robot mendiant du magazine Popular Mechanics . La paternité du prototype de Chris Eckert appelé Gimme semble très esthétique.

image

Je voulais me concentrer davantage sur la fonctionnalité, donc le boîtier a été assemblé à partir de matériaux improvisés. En particulier, les coins en PVC se sont révélés être le matériau le plus polyvalent avec lequel vous pouvez connecter presque deux parties. Il semble qu'à l'heure actuelle, le robot soit composé à cinq pour cent de coins en PVC et de vis M3. Le boîtier lui-même se compose de trois plates-formes stratifiées sur lesquelles la tête et toute l'électronique sont montées.

La base du robot est Raspberry Pi 2B , et le code est écrit en C ++ et se trouve sur GitHub .

Vision


Pour percevoir la réalité, le robot utilise la caméra Paspberry Pi Camera Module v2 , qui peut être contrôlée à l'aide de la bibliothèque RaspiCam .

Pour la détection des visages, j'ai essayé plusieurs approches différentes. La qualité des détecteurs classiques d'OpenCV ne m'a pas satisfait, donc finalement je suis arrivé à une solution plutôt non standard. Détection de personnes engagées dans le réseau neuronal, fonctionnant sur l'appareil Movidius Neural Compute Stick (NCS) sous le cadre de contrôle OpenVINO .

NCS est un tel matériel pour le lancement efficace de réseaux de neurones, à l'intérieur desquels se trouvent plusieurs processeurs vectoriels spécialement conçus pour cela. L'appareil est connecté via USB et ne consomme que 1 Watt d'énergie. Ainsi, le NCS agit comme un co-processeur pour le Raspberry Pi, qui ne tire pas le réseau neuronal. Pendant que le NCS traite la trame suivante, le processeur Paspberry est libre pour d'autres opérations. Il convient de noter que pour un fonctionnement optimal de l'appareil, une interface USB 3.0 est requise, qui n'est pas disponible sur les anciennes versions de Raspberry; avec USB 2.0, cela fonctionne aussi, mais plus lentement. Aussi, afin de ne pas bloquer les connecteurs USB Raspberry, je lui connecte le NCS via un court câble USB. J'ai écrit en détail sur l'utilisation du Neural Compute Stick dans mon article précédent .

Au début, j'ai essayé de m'entraînerpropre détecteur de visage avec architecture MobileNet + SSD sur des ensembles de données ouverts. Le détecteur fonctionnait vraiment, mais pas très stable: avec la détérioration inévitable des conditions de prise de vue (exposition et clichés flous), la qualité du détecteur s'affaissait fortement. Cependant, après un certain temps, des détecteurs de visage prêts à l'emploi sont apparus dans OpenVINO, et je suis passé à un détecteur avec l' architecture SqueezeNet light + SSD , qui non seulement fonctionnait mieux dans diverses conditions de prise de vue, mais était également plus rapide.

Avant de télécharger l'image sur le NCS pour obtenir les prévisions du détecteur, l'image doit être prétraitée. Le détecteur de mon choix fonctionne avec des images en couleur300×300, donc l'image doit d'abord être compressée. Pour ce faire, j'utilise l'algorithme de mise à l'échelle le plus léger - la méthode du plus proche voisin (INTER_NEAREST dans la bibliothèque OpenCV). Cela fonctionne un peu plus rapidement que les méthodes d'interpolation et n'affecte presque pas le résultat. Il convient également de prêter attention à l'ordre des canaux d'image: le détecteur attend l'ordre BGR, vous devez donc le définir pour la caméra.

J'ai également essayé de séparer le traitement vidéo en deux flux, dont l'un a reçu l'image suivante de la caméra et l'a traitée, et l'autre à ce moment-là a téléchargé l'image précédente sur NCS et a attendu les résultats du détecteur. Avec ce schéma, techniquement, la vitesse de traitement augmente, mais le délai entre la réception de la trame et la réception des détections pour elle augmente également. En raison de ce retard par rapport à la réalité, la surveillance du visage devient seulement plus difficile, donc j'ai finalement refusé ce schéma.

En plus de détecter réellement les visages, ils doivent également être suivis pour éviter les erreurs de détection. Pour ce faire, j'utilise le tracker léger Simple Online Realtime Tracker (SORT) . Ce tracker simple se compose de deux parties: l' algorithme hongrois est utilisé pour faire correspondre les objets sur les images adjacentes, et de prédire la trajectoire de l'objet, s'il disparaît subitement - filtre de Kalman . Pendant que je jouais avec le suivi du visage, j'ai trouvé que les trajectoires prédites par le filtre de Kalman peuvent être très invraisemblables avec des mouvements brusques, ce qui ne fait que compliquer le processus.

Par conséquent, j'ai désactivé le filtre de Kalman, ne laissant que l'algorithme de correspondance de visage et le compteur du nombre séquentiel d'images sur lesquelles le visage a été détecté - de cette façon, je me débarrasse des faux positifs du détecteur.

Plateforme supérieure, de gauche à droite: caméra, servos pour contrôler la tête et les sourcils, interrupteur, bornes d'alimentation, Big Red Button.


Trafic


Pour le mouvement, le robot dispose de cinq servos: deux servos à rotation continue FS5103R font tourner les roues; Il y a deux autres FS5109M ordinaires, dont l'un fait tourner la tête et l'autre secoue la boîte; enfin, le petit SG90 remue les sourcils.

Pour être honnête, les mini-servos SG90 me semblaient être une poubelle - l'un de mes servos avait la mauvaise largeur d'impulsion de contrôle, et un seul a survécu parmi les quatre autres. En toute honnêteté, j'ai accidentellement pris l'un des domestiques avec mon coude, mais les deux autres ne pouvaient tout simplement pas supporter la charge (je les utilisais pour la tête et la boîte). Même le dernier servo, qui a obtenu le travail le plus simple - déplacer les sourcils, doit pousser un bâton de temps en temps pour qu'il ne se coince pas. Avec d'autres servos, je n'ai remarqué aucun problème. Il est vrai que les servos à rotation continue doivent parfois être calibrés afin qu'ils ne tournent pas à l'état inactif - pour cela, il y a un petit régulateur sur eux qui peut être tourné avec un tournevis à tête d'horloge.

Il s'avère que la gestion des servos avec Raspberry n'est pas si simple. Premièrement, ils sont contrôlés parmodulation de largeur d'impulsion (PWM / PWM) , et sur Raspberry il n'y a que deux broches sur lesquelles PWM est pris en charge par le matériel . Deuxièmement, bien sûr, Raspberry ne pourra pas alimenter les servos, il ne le supportera pas. Heureusement, ces problèmes sont résolus à l'aide d'un contrôleur PWM externe.

Adafruit PCA9685 est un contrôleur PWM à 16 canaux qui peut être contrôlé via l' interface I2C . Il est également très pratique de disposer de bornes pour l'alimentation des servos. De plus, [théoriquement] il est possible de chaîner jusqu'à 62 contrôleurs, tout en recevant jusqu'à 992 broches de contrôle - pour cela, vous devez attribuer une adresse unique à chaque contrôleur à l'aide de cavaliers spéciaux. Donc, si vous avez soudainement besoin d'une armée de servos - vous savez quoi faire.

Pour contrôler le PCA9685, il existe une bibliothèque de haut niveau qui agit comme une extension WiringPi. Travailler avec cette chose est assez pratique - lors de l'initialisation, il crée 16 broches virtuelles dans lesquelles vous pouvez écrire un signal PWM, mais vous devez d'abord calculer le nombre de ticks. Pour tourner le servo-levier à un certain angle dans la plage [0, 180], vous devez d'abord traduire cet angle dans la plage de longueurs d'impulsion de contrôle en millisecondes [SERVO_MS_MIN, SERVO_MS_MAX]. Pour tous mes servos, ces valeurs sont respectivement d'environ 0,6 ms et 2,4 ms. En général, ces valeurs peuvent être trouvées dans la fiche technique du servo, mais la pratique a montré qu'elles peuvent différer, il peut donc être nécessaire de les sélectionner. Divisez ensuite la valeur résultante par 20 ms (la valeur standard de la durée du cycle de contrôle) et multipliez par le nombre maximal de graduations PCA9685 (4096):

void driveDegs(float angle, int pin) {
    int ticks = (int) (PCA_MAX_PWM * (angle/180.0f*(SERVO_MS_MAX-SERVO_MS_MIN) + SERVO_MS_MIN) / 20.0f); 
    pwmWrite(pin, ticks);
}

De même, cela se fait avec des servos à rotation continue - au lieu d'un angle, nous définissons la vitesse dans la plage [-1,1].

J'ai assemblé le châssis du robot, ainsi que le corps, à partir de moyens improvisés: je mets des roues de meuble sur les servo-entraînements à rotation continue, et un support de boule de meuble fait office de troisième roue. Auparavant, au lieu de cela, une roue se tenait sur un support rotatif, mais avec un tel châssis, il était difficile de faire des virages précis, j'ai donc dû le remplacer. Il y a aussi une petite roue sous la boîte pour transférer une partie du poids du servo au boîtier. Une chose simple qui n'était pas évidente pour moi au départ était que les servo-leviers doivent être fixés avec une vis, en particulier pour les roues, afin qu'ils ne tombent pas en cours de route. À cause d'une telle stupidité, j'ai dû refaire le châssis une fois. J'ai également fait du robot un large pare-chocs fait de coins en PVC pour qu'il ne se coince pas si souvent.

Maintenant, ce que vous pouvez faire à ce sujet. Tout d'abord, vous pouvez secouer le pot et bouger les sourcils - pour cela, il vous suffit de tourner le servo-levier à des angles présélectionnés.

Deuxièmement, vous pouvez tourner la tête. Je ne voulais pas que la tête tourne à la vitesse maximale du servo, car il y a un appareil photo dessus. Par conséquent, j'ai décidé de réduire la vitesse par programme: je dois tourner le levier d'un petit angle, puis attendre quelques millisecondes - et ainsi de suite jusqu'à ce que l'angle souhaité soit atteint. Dans ce cas, il est nécessaire de se souvenir de la position absolue actuelle de la tête et de vérifier à chaque fois si elle a dépassé les limites autorisées (sur mon robot, elle est dans la plage de [10, 90] degrés).

Troisièmement, vous pouvez changer la direction du mouvement en modifiant la vitesse de rotation des roues. De la même manière, vous pouvez faire pivoter la plate-forme, par exemple, pour suivre le visage. La vitesse angulaire de rotation dépend à la fois des servos eux-mêmes et de leur emplacement sur le châssis, il est donc plus facile de la mesurer une fois puis de la prendre en compte dans les virages. Pour trouver le délai nécessaire entre l'activation des moteurs en rotation et leur désactivation, vous devez diviser le module d'angle par la vitesse angulaire.

Enfin, vous pouvez faire pivoter la tête et le châssis simultanément et de manière asynchrone afin de ne pas perdre de temps. Je le fais comme ça:

auto waitRotation = std::async(std::launch::async, rotatePlatform, platformAngle);
success = driveHead(headAngle);
waitRotation.wait();

Plateforme centrale, de gauche à droite: PCA9685, bus d'alimentation, Raspberry Pi, MCP3008 ADC


La navigation


Ensuite, je n'ai rien compliqué, donc le robot utilise uniquement deux télémètres infrarouges Sharp GP2Y0A02YK pour la navigation. Ce n'est pas aussi simple, car les capteurs sont analogiques, mais Raspberry, contrairement à Arduino, n'a pas d'entrées analogiques. Ce problème est résolu par le convertisseur analogique-numérique (ADC / ADC) - j'utilise le MCP3008 10 bits et 8 canaux. Il est vendu comme un microcircuit séparé, il a donc dû être soudé à une carte de circuit imprimé et des broches y ont également été soudées pour le rendre plus pratique à connecter. De plus, sur les conseils de mon bati, qui tâtonne davantage dans les circuits, j'ai soudé deux condensateurs (céramique et électrolytique) entre les jambes de l'alimentation et le sol pour absorber le bruit de la partie numérique de l'ensemble du circuit. Les capteurs ne produisent pas plus de trois volts à la sortie, donc 3,3 V avec Raspberry peuvent être connectés en tant que tension ADC de référence (VREF) - la même que pour l'alimentation MCP3008 (VDD).

Le MCP3008 peut être contrôlé via l'interface SPI , et pour cela, il est encore plus facile de trouver du code prêt à l' emploi sur GitHub .

Malgré cela, pour un travail pratique avec l'ADC, vous aurez besoin de quelques danses avec un tambourin.
unsigned int analogRead(mcp3008Spi &adc, unsigned char channel)
{
    unsigned char spi_data[3];
    unsigned int val = 0;

    spi_data[0] = 1;  // start bit
    spi_data[1] = 0b10000000 | ( channel << 4); // mode and channel
    spi_data[2] = 0; // anything
    adc.spiWriteRead(spi_data, sizeof(spi_data));
  
    // read value, combine last two bits of second byte with whole third byte
    val = (spi_data[1]<< 8) & 0b1100000000; 
    val |= (spi_data[2] & 0xff);
    return val;
}


Trois octets doivent être envoyés au MCP3008, où le bit de début est écrit dans le premier octet, et le numéro de mode et de canal (0-7) dans le second. Nous récupérons également trois octets, après quoi nous devons coller les deux bits les moins significatifs du deuxième octet avec tous les bits du troisième.

Maintenant que nous pouvons obtenir les valeurs des capteurs, nous devons les calibrer, car les deux capteurs peuvent différer légèrement l'un de l'autre. En général, l'affichage à distance en raison de la puissance du signal de ces capteurs est non linéaire et pas très simple ( pour plus de détails voir fiche technique, pdf ). Par conséquent, il suffit de prendre deux coefficients, multipliés par lesquels les capteurs donneront une valeur de 1,0 à une distance égale et significative.

Les lectures des capteurs peuvent être assez bruyantes, en particulier sur les obstacles difficiles, donc j'utilise une moyenne mobile à pondération exponentielle (EWMA) pour lisser le signal de chaque capteur. J'ai sélectionné les paramètres de lissage à l'œil, afin que le signal ne fasse pas de bruit et ne soit pas loin derrière la réalité.

Vue de face: banque, télémètres et pare-chocs.


Nutrition


Tout d'abord, évaluons le courant que le robot va consommer (à propos de la consommation actuelle de framboise et de périphériques ):

  • Raspberry Pi 2B: pas moins de 350 mA, mais plus sous charge (jusqu'à 750-820 mA (?));
  • Appareil photo: environ 250 mA;
  • Neural Compute Stick: consommation d'énergie déclarée de 1 watt, à une tension de 5 volts sur USB, elle est de 200 mA;
  • Capteurs IR: 33 mA chacun ( fiche technique, pdf );
  • MCP3008: , 0.5 (, pdf);
  • PCA9685: , 6 (, pdf);
  • : ~150-200 1500-2000 (stall current), ( FS5109M, pdf)
  • HDMI ( ): 50 ;
  • + ( ): ~200 .

Au total, on peut estimer que 1,5 à 2,5 ampères devraient être suffisants, à condition que tous les servos ne se déplacent pas simultanément sous une lourde charge. Dans le même temps, Raspberry a besoin d'une tension conditionnelle de 5 volts, et pour les servos - de 4,8 à 6 volts. Reste à trouver une source d'alimentation répondant à ces exigences.

En conséquence, j'ai décidé d'alimenter le robot à partir de batteries au format 18650. Si vous prenez deux batteries ROBITON 3.4 / Li18650 (3,6 volts, 3400 mAh, courant de décharge maximum 4875 mA) et les connectez en série, elles peuvent produire jusqu'à 4,8 ampères à une tension de 7,2 volts. Avec un courant de consommation de 1,5 à 2,5 ampères, ils devraient être suffisants pour une heure ou deux.

Les batteries, en passant, ont un hic: malgré le facteur de forme indiqué 18650, leurs tailles sont loin d'être18×650mm - ils sont plus longs de plusieurs millimètres grâce au circuit de contrôle de charge intégré. Pour cette raison, j'ai dû poignarder le compartiment de la batterie avec un couteau pour qu'ils y tiennent.

Il ne reste plus qu'à abaisser la tension à 5 volts. Pour cela, j'utilise deux convertisseurs DC-DC abaisseur séparés DFRobot Power Module. Ce morceau de fer vous permet d'abaisser la tension à une tension d'entrée de 3,6-25 volts et une différence de tension d'au moins 0,6 volts. Pour plus de commodité, il dispose d'un interrupteur qui vous permet de sélectionner exactement 5 volts à la sortie, ou vous pouvez configurer une tension de sortie arbitraire à l'aide d'un régulateur spécial. J'ai réglé les deux convertisseurs à 5 volts; l'un d'eux alimente la framboise via un connecteur micro-USB, et le second alimente les servos via les terminaux PCA9685. Ceci est nécessaire afin de maximiser l'alimentation électrique des parties logiques et électriques du robot afin qu'elles n'interfèrent pas entre elles.

Au stade du débogage, j'ai utilisé une alimentation chinoise de 9 volts, 2 ampères au lieu des batteries, et c'était suffisant pour que le robot fonctionne - je l'ai connecté, comme les batteries, à deux convertisseurs DC-DC. Par conséquent, pour plus de commodité, j'ai créé des bornes sur le robot, auxquelles vous pouvez connecter une alimentation ou un compartiment à piles au choix. Cela a beaucoup aidé lorsque j'ai complètement réécrit tout le code sur ROS, et j'ai dû déboguer le robot pendant longtemps, y compris les servos.

Pour plus de commodité, j'ai également dû faire un "bus d'alimentation" - en fait, juste un morceau de la carte avec trois rangées de broches connectées pour la masse, 3,3v et 5v, respectivement. Le bus se connecte aux broches Raspberry correspondantes. Seuls les télémètres infrarouges sont alimentés par le bus 5v et les MCP3008 et PCA9685 par le bus 3,3v.

Et bien sûr, selon la bonne vieille tradition, j'ai mis le gros bouton rouge sur le robot - quand il est pressé, il interrompt simplement tout le circuit d'alimentation. Il n'était pas nécessaire de l'utiliser pour un arrêt d'urgence, mais allumer le robot à l'aide d'un bouton est vraiment plus pratique.

Plateforme inférieure, de gauche à droite: compartiment batterie, NCS, convertisseurs DC-DC, servo variateurs avec roues, télémètres.


Contrôle du robot


Il n'y a pas de Wi-Fi sur le Raspberry Pi 2B, je dois donc me connecter via ssh via un câble Ethernet (au fait, cela peut être fait directement depuis l'ordinateur portable, sans utiliser de routeur ). Il s'avère que ce schéma: nous nous connectons via ssh via le câble, démarrons le robot et retirons le câble. Ensuite, il peut être remis à sa place pour accéder à nouveau à la framboise. Il existe des solutions plus élégantes, mais j'ai décidé de ne pas compliquer les choses.

Pour que le robot puisse être facilement arrêté sans s'éteindre, j'ai ajouté un interrupteur soviétique massif (à partir d'un sous-marin?). Lorsque vous l'éteignez, le programme se termine et le robot s'arrête.

Le commutateur se connecte à la terre et à l'une des broches Raspberry GPIO, et vous pouvez y lire à l'aide de la bibliothèque WiringPi :

wiringPiSetup();
pinMode(PIN_SWITCH, INPUT);
pullUpDnControl(PIN_SWITCH, PUD_UP);
bool value = digitalRead(BB_PIN_SWITCH);

Il convient de noter qu'avec cette connexion, la tension sur la broche doit être augmentée jusqu'à 3,3 V, et en même temps, elle produira un signal haut à l'état ouvert et un signal bas à l'état fermé.

Mettre tous ensemble


Threads

Maintenant, tout ce qui précède doit être combiné en un seul programme qui contrôle le robot. Dans la première version du robot, je l'ai fait en utilisant des threads ( pthread ). Cette version est dans la branche master , mais le code y est assez effrayant.

Le programme fonctionne en quatre fils: un fil prend des images de la caméra et démarre le détecteur sur le NCS; le deuxième flux lit les données des télémètres; le troisième thread surveille le commutateur et définit la variable globale is_runningsurfalses'il est éteint; Le thread principal est responsable du comportement du robot et de l'asservissement. Les threads ont des pointeurs en commun avec le thread principal, par lesquels ils écrivent les résultats de leur travail. J'ai limité les vecteurs qui stockent les informations sur les faces trouvées par le détecteur au mutex, et déclaré les autres variables communes plus simples comme atomiques. Pour coordonner le flux du détecteur de visage avec le fil principal, un indicateur face_processedest réinitialisé lorsqu'un nouveau résultat provient du détecteur et augmente lorsque le fil principal utilise ce résultat pour sélectionner un comportement - cela est nécessaire afin de ne pas traiter les anciennes données qui peuvent ne pas être pertinentes après avoir déménagé.

La

version ROS avec les streams fonctionnait bien, mais j'ai commencé tout cela pour apprendre quelque chose, alors pourquoi pas en même temps masterRos ? J'entends ce framework depuis longtemps, et j'ai même dû travailler un peu avec lui sur un hackathon, donc à la fin j'ai décidé de réécrire tout le code sur ROS. Cette version du code se trouve dans la branche par défaut de ros et semble beaucoup plus décente. Il est clair que la mise en œuvre sur ROS sera presque certainement plus lente que la mise en œuvre sur les flux en raison des frais d'envoi de messages et de tout le reste - la seule question est de savoir combien?

Concept ROS
ROS (Robot Operating System) — , , , .

, , , (node), , , .

(topic) (message) , - .

— (service). , , . « », .

.msg .srv . .

ROS .

Pour mon robot, je n'ai utilisé aucun package prêt à l'emploi avec des algorithmes de ROS, j'ai uniquement conçu le code du robot dans un package séparé composé de cinq nœuds communiquant entre eux à l'aide de messages et de services ROS.

Le nœud le plus simple switch_node,, surveille l'état du commutateur. Dès que le commutateur est désactivé, le nœud commence à spammer des messages non informatifs du type boolde la rubrique terminator. C'est un signal au nœud principal qu'il est temps de terminer le travail.

Le deuxième nœud ,, sensor_nodelit périodiquement les lectures des deux télémètres IR et les envoie au sujet dans sensor_stateun message. De plus, ce nœud est responsable du traitement du signal: mise à l'échelle par des facteurs d'étalonnage et moyenne mobile.

Troisième nœudcamera_nodeIl est responsable de tout ce qui concerne les visages: il prend des images de la caméra, les traite, reçoit les résultats du détecteur, les passe à travers le tracker, puis trouve le visage le plus proche du centre du cadre - le robot n'utilise pas le reste de toute façon, mais vous voulez faire des messages plus petits. Les messages que le nœud envoie au sujet camera_statecontiennent le numéro de la trame, le fait d'avoir un visage (car il faut aussi savoir l'absence d'un visage), les coordonnées relatives du coin supérieur gauche, la largeur et la hauteur du visage. Voici à quoi ressemble la description du type de message dans le fichier DetectionBox.msg:

int64 count
bool present
float32 x
float32 y
float32 width
float32 height

Le quatrième noeud ,, servo_nodeest responsable des servos. Premièrement, il prend en charge un service servo_actionqui permet à l'une des actions d'être exécutée par les servos par son numéro: mettre le nœud entier dans son état initial (sourcils, inclinaison, tête, arrêt du châssis); remettre la tête dans son état initial; secouez le pot; dépeindre avec un sourcil l'une des trois expressions (bon, neutre, mal). Deuxièmement, en utilisant le service, servo_speedvous pouvez définir de nouvelles vitesses pour les deux roues en les envoyant dans la demande. Les deux services ne retournent rien. Enfin, il existe un service servo_head_platformqui vous permet de faire pivoter la tête et / ou le châssis d'un certain angle par rapport à la position actuelle. Ce service revient trues'il était possible de tourner la tête au moins partiellement, etfalsesinon, dans le cas où la tête est déjà au bord de l'angle admissible, et nous essayons de la tourner encore plus loin. Si les deux angles de la demande sont différents de zéro, le service tourne de manière asynchrone, comme indiqué ci-dessus. Dans la boucle principale, le nœud d'asservissement ne fait rien.

Voici, par exemple, une description du service servo_head_platform:

float32 head_delta
float32 platform_delta
---
bool head_success

Chacun des nœuds répertoriés prend en charge un service terminate_{switch, camera, sensor, servo}avec une demande de réponse vide, ce qui arrête le fonctionnement du nœud. Il est mis en œuvre de cette manière:

Du code
...
std::atomic_bool is_running; // global

bool terminate_node(std_srvs::Empty::Request &req, std_srvs::Empty::Response &ignored) {
    is_running = false;
    return true;
}

int main(int argc, char **argv) {
    is_running = true;
    ...
    while (is_running && ros::ok()) {
        // do stuff
    }
    ...
}


Le nœud a une variable globale is_running, dont la valeur détermine le cycle principal du nœud. Le service réinitialise simplement cette variable et la boucle principale est interrompue.

Il existe également un nœud principal beggar_botdans lequel la logique de base du robot est implémentée. Avant le début de la boucle principale, il s'abonne aux rubriques sensor_stateet camera_stateenregistre le contenu des messages dans des variables globales dans les fonctions de rappel. Il est également abonné au sujet terminator, dont le rappel réinitialise l'indicateur is_running, interrompant la boucle principale. De plus, avant le début du cycle, le nœud annonce les interfaces pour les services du nœud d'asservissement et attend quelques secondes que les autres nœuds démarrent. Une fois la boucle principale terminée, ce nœud appelle les servicesterminate_{switch, camera, sensor, servo}, désactivant ainsi tous les autres nœuds, puis le désactivant lui-même. Autrement dit, lorsque le commutateur est désactivé, les cinq nœuds terminent l'opération.

Le passage à ROS m'a obligé à changer beaucoup la structure du programme. Par exemple, auparavant, il était possible de modifier la vitesse de la roue avec une fréquence élevée, et cela fonctionnait bien, mais le service ROS fonctionne un ordre de grandeur plus lentement, j'ai donc dû réécrire le code afin que le service ne soit appelé que lorsque la vitesse change vraiment (en "mode paresseux").

ROS vous permet également d'exécuter assez commodément tous les nœuds du robot. Pour ce faire, vous devez écrire un fichier de lancement .launch répertoriant tous les nœuds et autres attributs du robot au format xml, puis exécuter la commande:

roslaunch beggar_bot robot.launch

ROS vs pthread

Maintenant, enfin, vous pouvez comparer la vitesse de la version ROS et de la version pthread. Je le fais de cette façon: le thread / nœud responsable de travailler avec la caméra considère son FPS (comme l'élément le plus lent), à condition que tout le reste fonctionne également. Pour la version pthread, j'ai toujours obtenu FPS 9,99 ou plus, pour la version ROS, il s'est avéré environ 8,3. En fait, cela suffit pour un tel jouet, mais les frais généraux sont assez visibles.

Comportement du robot


L'idée est assez simple: si le robot voit une personne, il doit se diriger vers lui et secouer la boîte. Secouer le pot est assez simple et amusant, mais vous devez d'abord vous rendre à la personne.

Il existe une fonction follow_facequi, s'il y a un visage dans le cadre, fait tourner le châssis et la tête du robot dans sa direction (seule la face la plus proche du centre est prise en compte). Cela est nécessaire pour que le robot garde toujours son cap sur une personne, s'il est dans le cadre, et regarde également directement en face lorsqu'il secoue un pot.

La camera_statemême variable est utilisée pour synchroniser cette fonction avec le sujet .face_processed, comme dans la version avec flux. L'idée est la même: nous voulons traiter les données une seule fois, car le robot est en mouvement constant. La fonction attend d'abord que le rappel du sujet avec les détections abaisse l'indicateur que la dernière trame a été traitée. Pendant qu'elle attend, elle appelle constamment ros::spinOnce()pour recevoir de nouveaux messages (en général, cela doit être fait partout où le programme attend de nouvelles données). S'il y a un visage dans le cadre, les angles sont calculés, ce qui nécessite de faire pivoter la plate-forme et la tête - cela peut être fait en connaissant les coordonnées relatives du centre du visage et le champ de vision de la caméra horizontalement et verticalement. Après cela, vous pouvez appeler le service servo_head_platformet déplacer le robot.

Il y a un point subtil: les informations sur la position du visage sont en retard sur le mouvement réel du visage et peuvent être en retard sur les mouvements du robot lui-même. Par conséquent, le robot peut surestimer l'angle de rotation, à cause duquel la tête commence à se déplacer d'avant en arrière avec une amplitude croissante. Pour éviter cela, je fais des retards après le déplacement (300 ms) et saute également une image après le déplacement. Dans le même but, les angles de rotation du châssis et de la tête sont multipliés par un facteur de 0,8 (les composants P du contrôleur PID ont du sens ).

Une fonctionfollow_facerenvoie le statut d'une personne. Une personne peut: être absente, être suffisamment proche du centre, trop éloignée du robot; une autre option - lorsque nous avons tourné le robot et ne savons pas ce qui est arrivé au visage (dans le processus de recherche); il y a encore un cas rare où la tête est à la frontière, c'est pourquoi il est impossible de se tourner vers le visage.

Une chose assez simple se produit dans la boucle principale:

  1. Appelez follow_facejusqu'à ce que la personne ait un certain statut (n'importe lequel, sauf «dans le processus de recherche»). À la fin de cette étape, le robot regardera directement le visage.
  2. Si le visage est trouvé et qu'il est proche:
    1. Secouez la boîte;
    2. Retrouver le visage;
    3. Si le visage est en place, faites une bonne expression avec les sourcils et secouez à nouveau le pot;
    4. Si le visage a disparu, faites une expression de colère avec les sourcils;
    5. Retournez-vous, allez au début du cycle.

  3. S'il n'y a personne (ou c'est loin) - navigation dans la pièce:
    1. S'il n'y a pas d'obstacles des deux côtés, avancez (si le visage a été trouvé, mais s'est avéré trop éloigné, le robot ira vers la personne);
    2. Si les obstacles sont proches des deux côtés, faites un tour aléatoire dans la plage [90,180][180,90];
    3. Si l'obstacle n'est que d'un côté, tournez dans la direction opposée à un angle aléatoire [0,90];
    4. Si le mouvement vers l'avant continue trop longtemps (peut-être bloqué), reculez un peu et faites un tour aléatoire dans la plage [90,180][180,90];


Cet algorithme ne prétend pas être une forte intelligence artificielle, cependant, un comportement aléatoire et un large pare-chocs permettent au robot de se lever de presque n'importe quelle position tôt ou tard.

Conclusion


Malgré son apparente simplicité, ce projet couvre de nombreux sujets non triviaux: travail avec capteurs analogiques, travail avec PWM, vision par ordinateur, coordination des tâches asynchrones. De plus, c'est juste incroyablement amusant. Probablement, plus loin, je ferai quelque chose de plus significatif, mais plus avec un parti pris dans l'apprentissage profond.

En bonus - la galerie:








All Articles