ROS und Neural Grid Beggar Robot

Normalerweise stellen sich für solche Handwerke zwei solche Fragen: "Wie?" und wofür?" Die Veröffentlichung selbst ist der ersten Frage gewidmet, und ich werde sofort auf die zweite antworten:

Ich habe dieses Projekt gestartet, um die Robotik zu beherrschen, beginnend mit dem Raspberry Pi und der Kamera. Wie Sie wissen, besteht eine der besten Möglichkeiten, etwas zu lernen, darin, eine technische Aufgabe zu entwickeln und zu versuchen, diese zu erfüllen, während Sie die erforderlichen Fähigkeiten erwerben.

Zu dieser Zeit hatte ich noch keine guten Ideen auf dem Gebiet der Robotik, also beschloss ich, ein ausschließlich lustiges Projekt zu machen - einen Bettlerroboter. Das Ergebnis ist ein eigenständiger Roboter auf dem Raspberry Pi und ROS, der mit dem Movidius Neural Cumpute Stick Gesichter erkennt. Er wandert durch den Raum, sucht nach Menschen und schüttelt eine Dose vor ihnen. So sieht dieser Roboter aus:



Der Roboter bewegt sich zufällig durch den Raum, und wenn er eine Person bemerkt, rollt er sich auf ihn zu und schüttelt ein Glas für kleine Dinge. Zum Spaß habe ich ihm einen kleinen Gesichtsausdruck hinzugefügt - er weiß, wie man seine Augenbrauen bewegt:



Nach dem ersten Versuch versucht der Roboter, sein Gesicht wieder in Sichtweite zu finden, dreht sich zu der Person um und schüttelt die Bank erneut. Aber was passiert, wenn Sie in diesem Moment gehen:



Roboter


Ich habe die Idee eines Bettelroboters aus der Zeitschrift Popular Mechanics übernommen . Die Prototyp-Autorschaft von Chris Eckert namens Gimme sieht sehr ästhetisch aus.

Bild

Ich wollte mich mehr auf die Funktionalität konzentrieren, deshalb wurde das Gehäuse aus improvisierten Materialien zusammengesetzt. Insbesondere PVC-Ecken erwiesen sich als das vielseitigste Material, mit dem Sie nahezu zwei beliebige Teile verbinden können. Es scheint, dass der Roboter derzeit zu fünf Prozent aus PVC-Ecken und M3-Schrauben besteht. Das Gehäuse selbst besteht aus drei Laminatplattformen, auf denen der Kopf und die gesamte Elektronik montiert sind.

Die Basis des Roboters ist Raspberry Pi 2B , und der Code ist in C ++ geschrieben und liegt auf GitHub .

Vision


Um die Realität wahrzunehmen, verwendet der Roboter die Kamera Paspberry Pi Camera Module v2 , die über die RaspiCam- Bibliothek gesteuert werden kann .

Zur Gesichtserkennung habe ich verschiedene Ansätze ausprobiert. Die Qualität der klassischen Detektoren von OpenCV hat mich nicht zufrieden gestellt, so dass ich am Ende zu einer eher nicht standardmäßigen Lösung kam. Erkennung von Personen, die am neuronalen Netzwerk beteiligt sind und auf dem Gerät Movidius Neural Compute Stick (NCS) unter dem Steuerungsrahmen OpenVINO ausgeführt werden .

NCS ist eine solche Hardware für den effektiven Start neuronaler Netze, in denen sich mehrere speziell darauf zugeschnittene Vektorprozessoren befinden. Das Gerät ist über USB angeschlossen und verbraucht nur 1 Watt Strom. Somit fungiert das NCS als Co-Prozessor für den Raspberry Pi, der das neuronale Netzwerk nicht zieht. Während das NCS den nächsten Frame verarbeitet, ist der Paspberry-Prozessor für andere Vorgänge frei. Es ist zu beachten, dass für einen optimalen Betrieb des Geräts eine USB 3.0-Schnittstelle erforderlich ist, die bei älteren Versionen von Raspberry nicht verfügbar ist. mit USB 2.0 funktioniert es auch, nur langsamer. Um die Raspberry-USB-Anschlüsse nicht zu blockieren, schließe ich das NCS über ein kurzes USB-Kabel an. Ich habe in meinem vorherigen Artikel ausführlich über die Arbeit mit dem Neural Compute Stick geschrieben .

Zuerst habe ich versucht zu trainiereneigener Gesichtsdetektor mit MobileNet + SSD-Architektur für offene Datensätze. Der Detektor funktionierte wirklich, aber nicht sehr stabil: Mit der unvermeidlichen Verschlechterung der Aufnahmebedingungen (Belichtung und verschwommene Aufnahmen) sank die Qualität des Detektors stark. Nach einiger Zeit erschienen jedoch in OpenVINO vorgefertigte Gesichtsdetektoren, und ich wechselte zu einem Detektor mit der SqueezeNet light + SSD- Architektur , der nicht nur unter verschiedenen Aufnahmebedingungen besser funktionierte, sondern auch schneller war.

Vor dem Hochladen des Bildes in das NCS, um die Vorhersagen des Detektors zu erhalten, muss das Bild vorverarbeitet werden. Der Detektor meiner Wahl arbeitet mit Farbbildern300×300Daher muss das Bild zuerst komprimiert werden. Dazu verwende ich den leichtesten Skalierungsalgorithmus - die Methode des nächsten Nachbarn (INTER_NEAREST in der OpenCV-Bibliothek). Es funktioniert etwas schneller als Interpolationsmethoden und hat fast keinen Einfluss auf das Ergebnis. Beachten Sie auch die Reihenfolge der Bildkanäle: Der Detektor erwartet die BGR-Reihenfolge, daher müssen Sie diese für die Kamera einstellen.

Ich habe auch versucht, die Videoverarbeitung in zwei Streams zu unterteilen, von denen einer das nächste Bild von der Kamera empfangen und verarbeitet hat, und der andere hat zu diesem Zeitpunkt das vorherige Bild in NCS hochgeladen und auf die Detektorergebnisse gewartet. Mit diesem Schema erhöht sich technisch die Verarbeitungsgeschwindigkeit, aber auch die Verzögerung zwischen dem Empfangen des Rahmens und dem Empfangen von Erkennungen für ihn nimmt zu. Aufgrund dieser Verzögerung hinter der Realität wird die Überwachung des Gesichts nur noch schwieriger, so dass ich dieses Schema am Ende abgelehnt habe.

Sie müssen nicht nur Gesichter erkennen, sondern auch verfolgt werden, um Detektorfehler zu vermeiden. Dazu verwende ich den Lightweight Tracker Simple Online Realtime Tracker (SORT) . Dieser einfache Tracker besteht aus zwei Teilen: Der ungarische Algorithmus wird verwendet, um Objekte auf benachbarten Frames abzugleichenund um die Flugbahn des Objekts vorherzusagen, wenn es plötzlich verschwindet - Kalman-Filter . Während ich mit Face Tracking spielte, stellte ich fest, dass die vom Kalman-Filter vorhergesagten Trajektorien bei plötzlichen Bewegungen sehr unplausibel sein können, was wiederum den Prozess nur erschwert.

Aus diesem Grund habe ich den Kalman-Filter ausgeschaltet und nur den Gesichtsanpassungsalgorithmus und den Zähler der fortlaufenden Anzahl von Frames belassen, auf denen das Gesicht erkannt wurde. Auf diese Weise werden die falsch positiven Ergebnisse des Detektors entfernt.

Obere Plattform von links nach rechts: Kamera, Servos zur Steuerung von Kopf und Augenbrauen, Schalter, Stromanschlüsse, Big Red Button.


Der Verkehr


Für die Bewegung verfügt der Roboter über fünf Servos: Zwei Servos mit kontinuierlicher Rotation FS5103R drehen die Räder; Es gibt zwei weitere gewöhnliche FS5109M, von denen einer den Kopf dreht und der andere die Dose schüttelt. Schließlich zieht der kleine SG90 die Augenbrauen hoch.

Um ehrlich zu sein, schienen mir die SG90-Miniservos wie Müll zu sein - eines meiner Servos hatte die falsche Steuerimpulsbreite, und nur eines überlebte unter den anderen vier. Fairerweise habe ich versehentlich einen der Diener mit meinem Ellbogen genommen, aber die anderen beiden konnten die Last einfach nicht tragen (ich habe sie früher für den Kopf und die Dose verwendet). Sogar das letzte Servo, das die einfachste Aufgabe hatte - das Bewegen der Augenbrauen - muss von Zeit zu Zeit einen Stock stechen, damit es nicht verkeilt. Bei anderen Servos habe ich keine Probleme bemerkt. Zwar müssen Servos mit kontinuierlicher Rotation manchmal so kalibriert werden, dass sie sich im inaktiven Zustand nicht drehen. Dazu befindet sich ein kleiner Regler, der mit einem Schlitzschraubendreher gedreht werden kann.

Es stellt sich heraus, dass das Verwalten von Servos mit Raspberry nicht so einfach ist. Erstens werden sie von gesteuertPulsweitenmodulation (PWM / PWM) , und bei Raspberry gibt es nur zwei Pins, an denen PWM von Hardware unterstützt wird . Zweitens wird Raspberry natürlich nicht in der Lage sein, die Servos anzutreiben, es wird dies nicht ertragen. Glücklicherweise werden diese Probleme mit einem externen PWM-Controller gelöst.

Adafruit PCA9685 ist ein 16-Kanal-PWM-Controller, der über die I2C-Schnittstelle gesteuert werden kann . Es ist auch sehr praktisch, dass es Anschlüsse für die Stromversorgung von Servos hat. Darüber hinaus ist es [theoretisch] möglich, bis zu 62 Controller zu verketten und dabei bis zu 992 Steuerpins zu empfangen. Dazu müssen Sie jedem Controller mit speziellen Jumpern eine eindeutige Adresse zuweisen. Wenn Sie also plötzlich eine Armee von Servos brauchen, wissen Sie, was zu tun ist.

Zur Steuerung des PCA9685 gibt es eine übergeordnete Bibliothek , die als WiringPi- Erweiterung fungiert. Die Arbeit mit diesem Ding ist sehr praktisch - während der Initialisierung werden 16 virtuelle Pins erstellt, in die Sie ein PWM-Signal schreiben können, aber zuerst müssen Sie die Anzahl der Ticks berechnen. Um den Servohebel auf einen bestimmten Winkel im Bereich [0, 180] zu drehen, müssen Sie diesen Winkel zunächst in den Bereich der Steuerimpulslängen in Millisekunden [SERVO_MS_MIN, SERVO_MS_MAX] übersetzen. Für alle meine Servos betragen diese Werte ungefähr 0,6 ms bzw. 2,4 ms. Im Allgemeinen finden Sie diese Werte im Servodatenblatt. Die Praxis hat jedoch gezeigt, dass sie sich unterscheiden können, sodass sie möglicherweise ausgewählt werden müssen. Teilen Sie dann den resultierenden Wert durch 20 ms (den Standardwert der Länge des Regelzyklus) und multiplizieren Sie ihn mit der maximalen Anzahl von Ticks 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);
}

In ähnlicher Weise geschieht dies mit Servos mit kontinuierlicher Rotation - anstelle eines Winkels stellen wir die Geschwindigkeit im Bereich [-1,1] ein.

Ich habe das Roboterchassis sowie die Karosserie aus improvisierten Mitteln zusammengebaut: Ich habe Möbelräder auf die Servoantriebe mit kontinuierlicher Rotation gesetzt, und eine Möbelkugelstütze fungiert als drittes Rad. Früher stand stattdessen ein Rad auf einer rotierenden Stütze, aber mit einem solchen Chassis war es schwierig, präzise Kurven zu fahren, deshalb musste ich es ersetzen. Unter der Dose befindet sich auch ein kleines Rad, mit dem ein Teil des Gewichts vom Servo auf das Gehäuse übertragen werden kann. Eine einfache Sache, die mir anfangs nicht klar war, war, dass Servohebel mit einer Schraube befestigt werden müssen, insbesondere für Räder, damit sie auf dem Weg nicht herunterfallen. Wegen dieser Dummheit musste ich das Chassis einmal wiederholen. Ich habe den Roboter auch zu einem breiten Stoßfänger aus PVC-Ecken gemacht, damit er nicht so oft hängen bleibt.

Nun darüber, was Sie dagegen tun können. Zunächst können Sie das Glas schütteln und die Augenbrauen bewegen - dazu müssen Sie nur den Servohebel in einen vorgewählten Winkel drehen.

Zweitens können Sie Ihren Kopf drehen. Ich wollte nicht, dass sich der Kopf mit der maximalen Geschwindigkeit des Servos dreht, da eine Kamera darauf ist. Deshalb habe ich mich entschlossen, die Geschwindigkeit programmgesteuert zu reduzieren: Ich muss den Hebel um einen kleinen Winkel drehen und dann einige Millisekunden warten - und so weiter, bis der gewünschte Winkel erreicht ist. In diesem Fall muss die aktuelle absolute Position des Kopfes gespeichert und jedes Mal überprüft werden, ob er die zulässigen Grenzwerte überschritten hat (bei meinem Roboter liegt er im Bereich von [10, 90] Grad).

Drittens können Sie die Bewegungsrichtung ändern, indem Sie die Drehzahl der Räder ändern. Auf die gleiche Weise können Sie beispielsweise die Plattform drehen, um dem Gesicht zu folgen. Die Winkeldrehzahl hängt sowohl von den Servos selbst als auch von ihrer Position auf dem Chassis ab. Daher ist es einfacher, sie einmal zu messen und sie dann bei Kurvenfahrten zu berücksichtigen. Um die erforderliche Verzögerung zwischen dem Einschalten der Motoren zum Drehen und dem Ausschalten zu ermitteln, müssen Sie das Winkelmodul durch die Winkelgeschwindigkeit dividieren.

Schließlich können Sie den Kopf und das Chassis gleichzeitig und asynchron drehen, um keine Zeit zu verschwenden. So mach ich es:

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

Zentrale Plattform von links nach rechts: PCA9685, Power Bus, Raspberry Pi, MCP3008 ADC


Navigation


Dann habe ich nichts kompliziert, so dass der Roboter nur zwei Sharp GP2Y0A02YK Infrarot-Entfernungsmesser für die Navigation verwendet. Dies ist auch nicht so einfach, da die Sensoren analog sind, aber Raspberry hat im Gegensatz zu Arduino keine analogen Eingänge. Dieses Problem wird durch den Analog-Digital-Wandler (ADC / ADC) gelöst. Ich verwende den 10-Bit-8-Kanal-MCP3008. Es wird als separate Mikroschaltung verkauft, daher musste es auf eine Leiterplatte gelötet werden, und dort wurden auch Stifte verlötet, um das Anschließen zu vereinfachen. Auf Anraten meines Bati, der mehr an Schaltkreisen herumfummelt, habe ich zwei Kondensatoren (Keramik und Elektrolyt) zwischen die Schenkel des Netzteils und den Boden gelötet, um das Rauschen des digitalen Teils des gesamten Schaltkreises zu absorbieren. Die Sensoren geben am Ausgang nicht mehr als drei Volt aus, sodass 3,3 V mit Raspberry als Referenz-ADC-Spannung (VREF) angeschlossen werden können - genau wie beim Netzteil MCP3008 (VDD).

Der MCP3008 kann über die SPI- Schnittstelle gesteuert werden , und dafür ist es sogar einfach, vorgefertigten Code auf GitHub zu finden .

Trotzdem benötigen Sie für eine bequeme Arbeit mit dem ADC einige Tänze mit einem Tamburin.
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;
}


Es müssen drei Bytes an den MCP3008 gesendet werden, wo das Startbit im ersten Byte und der Modus und die Kanalnummer (0-7) im zweiten Byte geschrieben werden. Wir erhalten auch drei Bytes zurück, wonach wir die zwei niedrigstwertigen Bits des zweiten Bytes mit allen Bits des dritten verbinden müssen.

Nachdem wir die Werte von den Sensoren erhalten können, müssen wir sie kalibrieren, da sich die beiden Sensoren geringfügig voneinander unterscheiden können. Im Allgemeinen ist die Anzeige aus der Ferne aufgrund der Signalstärke dieser Sensoren nicht linear und nicht sehr einfach ( weitere Einzelheiten siehe Datenblatt, pdf ). Daher reicht es aus, zwei Koeffizienten zu erfassen, wenn sie multipliziert werden, wodurch die Sensoren in einem sinnvollen, gleichen Abstand einen Wert von 1,0 ergeben.

Die Sensorwerte können sehr verrauscht sein, insbesondere bei schwierigen Hindernissen. Daher verwende ich einen exponentiell gewichteten gleitenden Durchschnitt (EWMA), um das Signal von jedem Sensor zu glätten. Ich habe die Glättungsparameter mit dem Auge ausgewählt, damit das Signal kein Rauschen verursacht und nicht weit hinter der Realität zurückbleibt.

Vorderansicht: Bank, Entfernungsmesser und Stoßstange.


Ernährung


Lassen Sie uns zunächst bewerten, welchen Strom der Roboter verbraucht ( über den aktuellen Stromverbrauch von Himbeeren und Peripheriegeräten ):

  • Raspberry Pi 2B: nicht weniger als 350 mA, aber mehr unter Last (bis zu 750-820 mA (?));
  • Kamera: ca. 250 mA;
  • Neural Compute Stick: angegebener Stromverbrauch von 1 Watt, bei einer Spannung von 5 Volt an USB sind es 200 mA;
  • IR-Sensoren: jeweils 33 mA ( Datenblatt, pdf );
  • MCP3008: , 0.5 (, pdf);
  • PCA9685: , 6 (, pdf);
  • : ~150-200 1500-2000 (stall current), ( FS5109M, pdf)
  • HDMI ( ): 50 ;
  • + ( ): ~200 .

Insgesamt kann geschätzt werden, dass 1,5 bis 2,5 Ampere ausreichen sollten, sofern sich nicht alle Servos unter starker Last gleichzeitig bewegen. Gleichzeitig benötigt Raspberry eine bedingte Spannung von 5 Volt und für Servos 4,8-6 Volt. Es bleibt eine Stromquelle zu finden, die diese Anforderungen erfüllt.

Aus diesem Grund habe ich beschlossen, den Roboter mit 18650 Batterien zu betreiben. Wenn Sie zwei ROBITON 3.4 / Li18650-Batterien (3,6 Volt, 3400 mAh, maximaler Entladestrom 4875 mA) in Reihe schalten , können sie bei einer Spannung von 7,2 Volt bis zu 4,8 Ampere erzeugen. Bei einem Verbrauchsstrom von 1,5 bis 2,5 Ampere sollten sie für ein oder zwei Stunden ausreichen.

Batterien haben übrigens einen Haken: Trotz des angegebenen Formfaktors 18650 sind ihre Größen weit davon entfernt18×650mm - sie sind aufgrund des eingebauten Ladesteuerkreises einige Millimeter länger. Aus diesem Grund musste ich das Batteriefach mit einem Messer erstechen, damit sie dort passen.

Es bleibt nur die Spannung auf 5 Volt zu senken. Dafür verwende ich zwei separate Abwärts-DC-DC-Wandler DFRobot Power Module. Mit diesem Stück Eisen können Sie die Spannung bei einer Eingangsspannung von 3,6 bis 25 Volt und einer Spannungsdifferenz von mindestens 0,6 Volt senken. Der Einfachheit halber verfügt es über einen Schalter, mit dem Sie genau 5 Volt am Ausgang auswählen oder eine beliebige Ausgangsspannung mit einem speziellen Regler einstellen können. Ich habe beide Wandler auf 5 Volt eingestellt; Einer von ihnen speist Himbeere über einen Micro-USB-Anschluss und der zweite Servos über PCA9685-Terminals. Dies ist erforderlich, um die Stromversorgung der logischen und der Leistungsteile des Roboters zu maximieren, damit sie sich nicht gegenseitig stören.

Beim Debuggen habe ich anstelle der Batterien ein chinesisches 9-Volt-2-Ampere-Netzteil verwendet, und es hat gereicht, damit der Roboter funktioniert. Ich habe es wie die Batterien an zwei DC-DC-Wandler angeschlossen. Aus praktischen Gründen habe ich am Roboter Terminals hergestellt, an die Sie ein Netzteil oder ein Batteriefach anschließen können, aus denen Sie auswählen können. Dies hat mir sehr geholfen, als ich den gesamten Code auf ROS komplett neu geschrieben habe und den Roboter, einschließlich Servos, lange Zeit debuggen musste.

Der Einfachheit halber musste ich auch einen "Leistungsbus" bauen - tatsächlich nur ein Stück der Platine mit drei Reihen verbundener Stifte für Masse, 3,3 V bzw. 5 V. Der Bus wird mit den entsprechenden Himbeerstiften verbunden. Nur IR-Entfernungsmesser werden vom 5-V-Bus und MCP3008 und PCA9685 vom 3,3-V-Bus mit Strom versorgt.

Und natürlich habe ich nach der guten alten Tradition den Big Red Button auf den Roboter gelegt - wenn er gedrückt wird, unterbricht er einfach den gesamten Stromkreis. Es war nicht notwendig, es für einen Notstopp zu verwenden, aber das Einschalten des Roboters mit Hilfe eines Knopfes ist wirklich bequemer.

Untere Plattform von links nach rechts: Batteriefach, NCS, DC-DC-Wandler, Servoantriebe mit Rädern, Entfernungsmesser.


Robotersteuerung


Auf dem Raspberry Pi 2B gibt es kein WLAN, daher muss ich eine Verbindung über ssh über ein Ethernet-Kabel herstellen (dies kann übrigens direkt vom Laptop aus erfolgen, ohne einen Router zu verwenden ). Es stellt sich dieses Schema heraus: Wir verbinden uns über ssh über das Kabel, starten den Roboter und ziehen das Kabel ab. Dann kann es an seinen Platz zurückgebracht werden, um wieder auf die Himbeere zuzugreifen. Es gibt elegantere Lösungen, aber ich habe beschlossen, nicht zu komplizieren.

Damit der Roboter leicht gestoppt werden kann, ohne ihn auszuschalten, habe ich einen massiven sowjetischen Schalter (von einem U-Boot?) Hinzugefügt. Wenn Sie ihn ausschalten, endet das Programm und der Roboter stoppt.

Der Switch ist mit Masse und einem der Raspberry GPIO-Pins verbunden, und Sie können mithilfe der WiringPi-Bibliothek daraus lesen :

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

Es ist anzumerken, dass bei dieser Verbindung die Spannung am Pin auf 3,3 V erhöht werden muss und gleichzeitig ein hohes Signal im offenen Zustand und ein niedriges Signal im geschlossenen Zustand erzeugt wird.

Alles zusammenfügen


Threads

Jetzt müssen alle oben genannten Punkte in einem Programm kombiniert werden, das den Roboter steuert. In der ersten Version des Roboters habe ich dies mit Threads ( pthread ) gemacht. Diese Version ist in dem Master - Zweig , aber der Code ist ziemlich beängstigend.

Das Programm arbeitet in vier Threads: Ein Thread nimmt Bilder von der Kamera und startet den Detektor am NCS; der zweite Strom liest Daten von Entfernungsmessern; Der dritte Thread überwacht den Switch und setzt die globale Variable is_runningauffalsewenn es aus ist; Der Hauptfaden ist für das Roboterverhalten und die Servosteuerung verantwortlich. Threads haben Zeiger gemeinsam mit dem Haupt-Thread, mit dem sie die Ergebnisse ihrer Arbeit schreiben. Ich habe die Vektoren, die Informationen über die vom Detektor gefundenen Gesichter speichern, auf den Mutex beschränkt und die anderen, einfacheren allgemeinen Variablen als atomar deklariert. Um den Fluss des Gesichtsdetektors mit dem Hauptfaden zu koordinieren, gibt es ein Flag face_processed, das zurückgesetzt wird, wenn ein neues Ergebnis vom Detektor kommt, und ausgelöst wird, wenn der Hauptfaden dieses Ergebnis zur Auswahl eines Verhaltens verwendet. Dies ist erforderlich, um alte Daten, die möglicherweise nicht relevant sind, nicht zu verarbeiten nach dem Umzug.

Die ROS-

Version mit Streams hat gut funktioniert, aber ich habe das alles gestartet, um etwas zu lernen. Warum also nicht gleichzeitig Master?Ros ? Ich habe dieses Framework schon lange gehört und musste sogar ein bisschen damit an einem Hackathon arbeiten, also habe ich mich am Ende entschlossen, den gesamten Code auf ROS neu zu schreiben. Diese Version des Codes befindet sich im Standardzweig von ros und sieht viel anständiger aus. Es ist klar, dass die Implementierung in ROS aufgrund des Overheads beim Senden von Nachrichten und allem anderen mit ziemlicher Sicherheit langsamer sein wird als die Implementierung in den Flows - die einzige Frage ist, wie viel?

ROS-Konzept
ROS (Robot Operating System) — , , , .

, , , (node), , , .

(topic) (message) , - .

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

.msg .srv . .

ROS .

Für meinen Roboter habe ich keine vorgefertigten Pakete mit ROS-Algorithmen verwendet. Ich habe den Robotercode nur in einem separaten Paket entworfen, das aus fünf Knoten besteht, die über Nachrichten und ROS-Dienste miteinander kommunizieren.

Der einfachste Knoten switch_nodeüberwacht den Status des Switches. Sobald sich der Schalter ausschaltet, beginnt der Knoten, nicht informative Nachrichten des Typs boolim Thema zu spammen terminator. Dies ist ein Signal an den Hauptknoten, dass es Zeit ist, die Arbeit abzuschließen.

Der zweite Knoten sensor_nodeliest regelmäßig die Messwerte beider IR-Entfernungsmesser und sendet sie in sensor_stateeiner Nachricht an das Thema . Dieser Knoten ist auch für die Signalverarbeitung verantwortlich: Skalierung durch Kalibrierungsfaktoren und gleitenden Durchschnitt.

Dritter Knotencamera_nodeEr ist für alles verantwortlich, was mit Gesichtern zu tun hat: Er nimmt Bilder von der Kamera auf, verarbeitet sie, empfängt die Ergebnisse des Detektors, leitet sie durch den Tracker und findet dann das Gesicht, das der Bildmitte am nächsten liegt - der Roboter verwendet den Rest sowieso nicht, aber Sie möchten kleinere Nachrichten erstellen. Die Nachrichten, die der Knoten an das Thema sendet, camera_stateenthalten die Rahmennummer, die Tatsache, dass ein Gesicht vorhanden ist (da Sie auch über das Fehlen eines Gesichts Bescheid wissen müssen), die relativen Koordinaten der oberen linken Ecke, die Breite und Höhe des Gesichts. So sieht die Beschreibung des Nachrichtentyps in der Datei aus DetectionBox.msg:

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

Der vierte Knoten servo_nodeist für die Servos verantwortlich. Erstens unterstützt es einen Dienst servo_action, der es ermöglicht, eine der Aktionen von Servos anhand ihrer Nummer auszuführen: den gesamten Knoten in seinen Ausgangszustand zu versetzen (Augenbrauen, Bank, Kopf, Chassis stoppen); den Kopf in seinen Ausgangszustand versetzen; schüttle das Glas; Stellen Sie mit einer Augenbraue einen von drei Ausdrücken dar (gut, neutral, böse). Zweitens können Sie mithilfe des Dienstes servo_speedneue Geschwindigkeiten für beide Räder festlegen, indem Sie sie in der Anforderung senden. Beide Dienste geben nichts zurück. Schließlich gibt es einen Service servo_head_platform, mit dem Sie den Kopf und / oder das Chassis um einen bestimmten Winkel relativ zur aktuellen Position drehen können. Dieser Service kehrt zurück, truewenn es möglich war, den Kopf zumindest teilweise zu drehen, undfalseAndernfalls, wenn sich der Kopf bereits am Rand des zulässigen Winkels befindet und wir versuchen, ihn noch weiter zu drehen. Wenn beide Winkel in der Anforderung ungleich Null sind, dreht sich der Dienst wie oben angegeben asynchron. In der Hauptschleife tut der Servoknoten nichts.

Hier ist zum Beispiel eine Beschreibung des Dienstes servo_head_platform:

float32 head_delta
float32 platform_delta
---
bool head_success

Jeder der aufgelisteten Knoten unterstützt einen Dienst terminate_{switch, camera, sensor, servo}mit einer leeren Antwortanforderung, wodurch der Betrieb des Knotens gestoppt wird. Es wird folgendermaßen implementiert:

Etwas 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
    }
    ...
}


Der Knoten hat eine globale Variable is_running, deren Wert den Hauptzyklus des Knotens bestimmt. Der Dienst setzt diese Variable einfach zurück und die Hauptschleife wird unterbrochen.

Es gibt auch einen Hauptknoten, beggar_botin dem die Grundlogik des Roboters implementiert ist. Vor dem Start der Hauptschleife werden Themen abonniert sensor_stateund camera_stateder Inhalt von Nachrichten in globalen Variablen in Rückruffunktionen gespeichert. Er hat auch das Thema abonniert terminator, dessen Rückruf das Flag zurücksetzt is_runningund die Hauptschleife unterbricht. Bevor der Zyklus beginnt, kündigt der Knoten die Schnittstellen für die Dienste des Servoknotens an und wartet einige Sekunden, bis die anderen Knoten gestartet sind. Nach dem Ende der Hauptschleife ruft dieser Knoten die Dienste aufterminate_{switch, camera, sensor, servo}Dadurch werden alle anderen Knoten ausgeschaltet und dann selbst ausgeschaltet. Das heißt, wenn der Schalter ausgeschaltet ist, schließen alle fünf Knoten den Vorgang ab.

Der Wechsel zu ROS hat mich gezwungen, die Struktur des Programms ziemlich zu ändern. Früher war es beispielsweise möglich, die Radgeschwindigkeit mit einer hohen Frequenz zu ändern, und dies funktionierte einwandfrei, aber der ROS-Dienst arbeitet um eine Größenordnung langsamer, sodass ich den Code neu schreiben musste, damit der Dienst nur aufgerufen wurde, wenn sich die Geschwindigkeit wirklich ändert (im "Lazy-Modus").

Mit ROS können Sie auch ganz bequem alle Knoten des Roboters ausführen. Dazu müssen Sie eine .launch-Startdatei schreiben, in der alle Knoten und andere Attribute des Roboters im XML-Format aufgelistet sind , und dann den folgenden Befehl ausführen:

roslaunch beggar_bot robot.launch

ROS vs. pthread

Nun können Sie endlich die Geschwindigkeit der ROS-Version und der pthread-Version vergleichen. Ich mache es so: Der Thread / Knoten, der für die Arbeit mit der Kamera verantwortlich ist, betrachtet seine FPS (als das langsamste Element), vorausgesetzt, alles andere funktioniert auch. Für die Pthread-Version habe ich durchweg FPS 9.99 oder so bekommen, für die ROS-Version stellte sich heraus, dass es ungefähr 8.3 war. In der Tat ist dies für ein solches Spielzeug völlig ausreichend, aber der Overhead ist durchaus spürbar.

Roboterverhalten


Die Idee ist ganz einfach: Wenn der Roboter eine Person sieht, muss er zu ihr fahren und die Dose schütteln. Das Glas zu schütteln ist ganz einfach und macht Spaß, aber zuerst müssen Sie zu der Person gelangen.

Es gibt eine Funktion follow_face, die bei einer Fläche im Rahmen das Chassis und den Kopf des Roboters in seine Richtung dreht (nur die Fläche, die der Mitte am nächsten liegt, wird berücksichtigt). Dies ist notwendig, damit der Roboter immer seinen Kurs auf einer Person behält, wenn er sich im Rahmen befindet, und auch direkt in das Gesicht schaut, wenn er ein Glas schüttelt. Dieselbe Variable wird verwendet

, um diese Funktion mit dem Thema zu synchronisieren camera_state.face_processed, wie in der Version mit Streams. Die Idee ist dieselbe - wir wollen Daten nur einmal verarbeiten, weil sich der Roboter ständig bewegt. Die Funktion wartet zunächst, bis der Rückruf des Themas mit den Erkennungen das Flag senkt, dass der letzte Frame verarbeitet wurde. Während sie wartet, ruft sie ständig ros::spinOnce()an, um neue Nachrichten zu erhalten (im Allgemeinen sollte dies überall dort erfolgen, wo das Programm neue Daten erwartet). Befindet sich ein Gesicht im Rahmen, werden die Winkel berechnet, die die Plattform und den Kopf drehen müssen. Dies kann erreicht werden, indem die relativen Koordinaten der Gesichtsmitte und des Sichtfelds der Kamera horizontal und vertikal bekannt sind. Danach können Sie den Service anrufen servo_head_platformund den Roboter bewegen.

Es gibt einen subtilen Punkt: Informationen über die Position des Gesichts bleiben hinter der tatsächlichen Bewegung des Gesichts zurück und können hinter den Bewegungen des Roboters selbst zurückbleiben. Daher kann der Roboter den Drehwinkel überschätzen, wodurch sich der Kopf mit zunehmender Amplitude hin und her zu bewegen beginnt. Um dies zu verhindern, mache ich Verzögerungen nach dem Umzug (300 ms) und überspringe auch einen Frame nach dem Umzug. Aus dem gleichen Grund werden die Drehwinkel von Chassis und Kopf mit dem Faktor 0,8 multipliziert (die P-Komponenten des PID-Reglers sind sinnvoll ).

Funktionfollow_faceGibt den Status einer Person zurück. Eine Person kann: abwesend sein, nah genug am Zentrum sein, zu weit vom Roboter entfernt sein; eine andere Option - als wir den Roboter drehten und nicht wussten, was mit dem Gesicht passiert ist (im Suchprozess); Es gibt immer noch einen seltenen Fall, in dem sich der Kopf an der Grenze befindet, weshalb es unmöglich ist, sich dem Gesicht zuzuwenden.

In der Hauptschleife passiert etwas ziemlich Einfaches:

  1. Rufen Sie an, follow_facebis die Person einen bestimmten Status hat (beliebig, außer "im Suchvorgang"). Am Ende dieses Schritts schaut der Roboter direkt ins Gesicht.
  2. Wenn das Gesicht gefunden wird und es nahe ist:
    1. Schütteln Sie die Dose;
    2. Finde das Gesicht wieder;
    3. Wenn das Gesicht vorhanden ist, machen Sie einen guten Ausdruck mit den Augenbrauen und schütteln Sie das Glas erneut.
    4. Wenn das Gesicht verschwunden ist, machen Sie einen wütenden Ausdruck mit Augenbrauen;
    5. Drehen Sie sich um und gehen Sie zum Anfang des Zyklus.

  3. Wenn es keine Person gibt (oder es weit weg ist) - Navigation durch den Raum:
    1. Wenn auf beiden Seiten keine Hindernisse vorhanden sind, fahren Sie vorwärts (wenn das Gesicht gefunden wurde, sich aber als zu weit herausgestellt hat, geht der Roboter zu der Person).
    2. Wenn sich auf beiden Seiten Hindernisse befinden, drehen Sie den Bereich nach dem Zufallsprinzip [90,180][180,90];;
    3. Wenn sich das Hindernis nur auf einer Seite befindet, drehen Sie es in einem zufälligen Winkel in die entgegengesetzte Richtung [0,90];;
    4. Wenn die Vorwärtsbewegung zu lange andauert (möglicherweise stecken bleibt), ziehen Sie sich ein wenig zurück und drehen Sie den Bereich nach dem Zufallsprinzip [90,180][180,90];;


Dieser Algorithmus behauptet nicht, eine starke künstliche Intelligenz zu sein. Zufälliges Verhalten und ein breiter Stoßfänger ermöglichen es dem Roboter jedoch, früher oder später aus fast jeder Position herauszukommen.

Fazit


Trotz seiner offensichtlichen Einfachheit deckt dieses Projekt viele nicht triviale Themen ab: Arbeiten mit analogen Sensoren, Arbeiten mit PWM, Computer Vision, Koordination asynchroner Aufgaben. Außerdem macht es einfach wahnsinnig Spaß. Wahrscheinlich werde ich weiter etwas Bedeutenderes tun, aber mehr mit einer Tendenz zum tiefen Lernen.

Als Bonus - die Galerie:








All Articles