Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

projektewise18:orr_s_public:start

ORR(s)

Der Projektname ORR(s) steht für Object Recognition Robot(s). Die Ursprungsidee, war die eines Roboter-Schwarms. Ein Hauptroboter sollte eine Aufgabe lösen – wir haben uns für die Objektsuche entschieden – und anschließend einen zweiten Roboter kontaktieren. Mithilfe einer Wegaufzeichnung des ersten Roboters findet auch der Zweite zum Zielobjekt. Mission erfolgreich.

Wir haben nur den Hauptroboter gebaut, die Problematik dabei war, dass er viele Aufgaben gleichzeitig zu erledigen hat. Der Bereich, in dem er „lebt“ umfasst einen Tisch, auf dem entsprechende Hindernisse (Holzklötze) aufgebaut sind. Während er fährt, muss gleichzeitig gewährleistet sein, dass er nicht vom Tisch fällt, keine Gegenstände umfährt, gegebenenfalls auf ein erkanntes Objekt zufährt und dann die Objekterkennung startet. Sobald er das Zielobjekt (vorher vorgegeben) gefunden hat, muss theoretisch der Weg, den er aufgezeichnet hat, an einen zweiten Roboter übermittelt werden.

Die Objekterkennung funktioniert in unserem Fall mit einem Farbsensor; ist aber auch mit einer Kamera und einem Raspberry Pi möglich (siehe unten).

Überblick über das Gesamtsystem

Die Grundlage vom ORR ist eine Holzplatte, an deren Unterseite zwei Steppermotoren mit Rädern und ein loses Rad befestigt sind. Auf der Oberseite sind die Schaltplatine mit dem Arduino und der Verkabelung, vier Ultraschall-Sensoren (teilweise mit Erweiterung der Grundfläche) und die Stützen für das Dach. Auf dem Dach ist ein Lipo mit Akkuwächter auf Klett befestigt und vorne in der Mitte auf einem Servo befindet sich ein weiterer Ultraschallsensor. Zwischen den Platten befindet sich der Farbsensor. Die fünf Holzstützen sind dafür da, dass der Roboter umgedreht werden und auf dem Dach stehen kann, ohne dass der Ultraschallsensor beschädigt wird.

Abbildung 1: ORR von Vorne (mit Dach)


Abbildung 2: ORR von Hinten (mit Dach)


Abbildung 3: ORR Vogelperspektive (ohne Dach)


Aufgaben, die zu lösen waren:

Fahren, Tischerkennung, Hinderniserkennung, Objekterkennung, Wegaufzeichnung

Aufgaben, die nach kurzer Zeit bewusst ausgeklammert wurden:

Vertiefung und visuelle Darstellung der Wegaufzeichnung, Bau eines zweiten Roboters

Details:

Grundsätzlich funktioniert der Roboter, sobald man ihn anschaltet und der Code hochgeladen ist, so:

  1. Der Servo bewegt sich von links nach rechts und sein Ultraschallsensor nimmt Werte auf. Dort wo der geringste Abstand ist, ist vermutlich ein Objekt und der Servo dreht sich dorthin.
  2. Der Roboter dreht sich auch in die entsprechende Richtung.
  3. Er fährt so lange geradeaus, bis der Abstand nach vorne zu gering ist (oder er bei einer Tischkante angekommen ist, weil dort gar kein Objekt war).
  4. Theoretisch findet jetzt die Objekterkennung statt.
  5. Falls es das falsche Objekt ist, fährt er für eine bestimmte Anzahl an Schritten rückwärts (außer ein Objekt oder der Tisch ist hinter ihm, dann hält er früher).
  6. Er dreht sich und das Programm startet von Vorne.

So sah auch der Code aus:

int grad = objektFinden();     //entspricht 1.
    zuObjektDrehen(grad);      //entspricht 2.
    fahreZuObjekt();           //entspricht 3.
    //theoretisch 4.
    vonObjektEntfernen();      //entspricht 5.
    drivebwdl(150);            //entspricht 6.

Wie genau es zu diesem Code gekommen ist, viele weitere Details und welche Probleme wir beim Entwickeln hatten, ist in den folgenden Abschnitten erklärt. Wie es letztendlich aussah, könnt ihr euch hier kurz in Zeitraffer (Tipp: stellt den Ton an) ansehen (es findet keine Objekterkennung statt). Weiter unten (hier: 9) ist das Video nochmal in Normalgeschwindigkeit.

Abbildung 4: Ablauf in Zeitraffer


Programmierung der Stepper-Motoren und Klassen:

Die Stepper-Motoren brauchten wir für jegliche Fortbewegung des Roboters. Das Anschließen und die grundsätzliche Programmierung der Stepper-Motoren war kein Problem. Hier ein beispielhaftes Bild, einen der Motoren anzuschließen; man braucht zusätzlich zum Arduino noch einen Kondensator und einen Motortreiber.

Abbildung 5: Schaltplan von einem Stepper

Anmerkung: Anders als im Schaltplan zu sehen ist, benötigt man einen Lipo zwischen 7 und 8 Volt.

Um einen der Motoren anzusteuern, muss man die Richtung vorgeben (vorwärts oder rückwärts entsprechen HIGH oder LOW beim direction-Pin) und den stepper-Pin abwechselnd HIGH und LOW stellen. Je nachdem, wie groß das delay milliSekDelay) dazwischen ist, dreht der Motor sich schneller oder langsamer.

//Funktion zum Vorwärtsfahren eines Motors
void drivefwd(int milliSekDelay) {
  digitalWrite(directionPin, HIGH);
 
  digitalWrite(stepperPin, HIGH);
  delay(milliSekDelay);
  digitalWrite(stepperPin, LOW);
  delay(milliSekDelay);
}

Da so nur ein Motor angesteuert wird und wir zwei zum Fahren brauchten, wäre der Code sehr unübersichtlich geworden. Wir schrieben also eine Klasse, mit der wir später beide Motoren einfacher ansteuern konnten, ohne jedes Mal den Code kopieren zu müssen (siehe MotorKlasse.h im zip-Ordner). Mithilfe der in der Klasse definierten Funktionen ist es möglich einen Motor vorwärts oder rückwärts fahren zu lassen. Im Hauptcode ist die richtige fahren-Funktionen definiert.

Linker Motor Rechter Motor Roboter
vorwärts vorwärts vorwärts
rückwärts rückwärts rückwärts
vorwärts rückwärts rechts
rückwärts vorwärts links

Beispiel – nach links fahren für eine bestimmte Schrittanzahl (val):

//Funktion zum nach links Drehen (rechter Motor vorwärts, linker rückwärts)
void drivebwdr(int val) {
   //fahre nach hinten rechts
   for (int i = 0; i < val; i++) {
     linkerMotor.drivebwd(motorenDelay);     //linker Motor fährt rückwärts
     rechterMotor.drivefwd(motorenDelay);    //rechter Motor fährt vorwärts
 
     //fügt der Liste der Wegaufzeichnung ein Element hinzu, l steht für "drivebwdr" also links, siehe Abschnitt Wegaufzeichnung
     weg->add('l');
   }
}


Problem des Geradeausfahrens:

Unser Hauptproblem war, dass der Roboter nicht geradeaus fahren wollte und immer nach links zog, da der rechte Motor stärker war. Wir drehten an den Schrauben der Motortreiber, damit beide Motoren exakt gleich viel Strom bekamen; das veränderte nichts. Also gaben wir dem schwächeren Motor mehr Strom, das führte auch zu nichts, außer dass der Motor extrem heiß wurde und irgendwann nur noch stockend fuhr.

Wir bauten die Motoren wieder aus und verglichen insgesamt vier Motoren, um zwei zu finden, die ungefähr gleich schnell fuhren. Diese bauten wir ein; der Linksdrang war höchstens ein bisschen besser geworden (später fanden wir durch eine andere Gruppe heraus, dass Motoren vorwärts schneller fahren als rückwärts und der eine war gedreht zum anderen). Am Ende veränderten wir den Code so, dass der schwächere Motor alle paar Schritte einen Schritt mehr machte. Nicht optimal, aber besser als vorher.

Hier beispielsweise unsere vorwärts-fahren-Funktion:

//Funktion zum Vorwärtsfahren (beide Motoren fahren vorwärts)
void drivefwd(int zaehler) {
  rechterMotor.drivefwd(motorenDelay);
  linkerMotor.drivefwd(motorenDelay);
  zaehler++;
  //zum Ausgleich des Linksdrangs muss der rechte Motor regelmäßig mehr Schritte machen
  if (zaehler == 3) {
    rechterMotor.drivefwd(motorenDelay);
    rechterMotor.drivefwd(motorenDelay);
    zaehler = 0;
  }
  //fügt der Liste der Wegaufzeichnung ein Element hinzu, f steht für "drivefwd" also vorwärtsfahren, siehe Abschnitt Wegaufzeichnung
  weg->add('f');
}

Außerdem funktioniert das ganze auch nur, wenn der Lipo eine Stärke von unter 8 Volt hat. Wir wissen nicht, warum. Ansonsten spinnt der Roboter rum und macht Dinge, die nicht im Code stehen.

Programmierung der Ultraschallsensoren und Klassen:

Insgesamt haben wir 5 Ultraschallsensoren verwendet: vorne unten zwei und hinten unten einen, damit er den Tischrand erkennen kann; hinten einen, damit er Objekte erkennt, die im Weg stehen; einen vorne auf dem Servo für die Objektsuche.
Genauso wie bei den Steppern war das Anschließen und programmieren nicht schwierig, aber unübersichtlich. Hier die Klasse: (siehe SensorKlasse.h in der zip-Datei).

Abbildung 6: Schaltplan eines Ultraschallsensors

In regelmäßigen Abständen werden Signale versendet, wieder empfangen und dabei die Dauer gemessen. Diese Werte mussten umgerechnet werden:

int abstand = dauer * 0.034 / 2;

„Nun berechnet man die Entfernung in Zentimetern. Man teilt zunächst die Zeit durch zwei (Weil man ja nur eine Strecke berechnen möchte und nicht die Strecke hin- und zurück). Den Wert multipliziert man mit der Schallgeschwindigkeit in der Einheit Zentimeter/Mikrosekunde und erhält dann den Wert in Zentimetern.“ (Quelle: https://funduino.de/nr-10-entfernung-messen)

Mit der Klasse konnten wir unsere fünf Ultraschall-Sensoren im Code unterbringen, deren Werte speichern und verwenden.

Montage der Ultraschallsensoren:

Ein weiteres Problem war die Montage der Sensoren an den optimalen Stellen, sowie ein öfter auftretender Sensorenausfall,bzw. die Ausgabe sinnloser oder gar keiner Werte. Als der Roboter nicht richtig funktioniert hat, wussten wir, einer der Sensoren spinnt. Daher musste jeder einzelne getestet und neu angeschlossen werden. Des Weiteren festigten wir die Kabel und überprüften den Code. Insgesamt waren zweimal Sensoren kaputt, die ausgetauscht werden mussten.

Trotz funktionierender Sensoren fiel der Roboter vom Tisch. Dies lag daran, dass die Sensoren an ungünstigen Stellen angebracht waren, sodass der Tisch noch erkannt worden ist, die Räder aber schon über dem Abgrund waren. Um jenes zu beheben nahmen wir einen zweiten Sensor für vorne unten hinzu und anschließend versetzten wir die beiden weiter nach außen (wie oben Abb. 1 zu sehen). Aufgrund dessen kann der Roboter auch schräg auf eine Tischkante zufahren, bleibt rechtzeitig stehen und springt im Code zur entsprechenden Stelle.

Hinderniserkennung/Hindernisfindung:

Der 5. Ultraschallsensor:

Oben in der Mitte zeigt ein 5. Ultraschallsensor nach vorne, um Hindernisse rechtzeitig zu erkennen. Wenn der Roboter nicht direkt auf ein Objekt zufuhr, es also weiter rechts oder links stand, war er zu breit und das Objekt nicht mehr in seinem „Blickfeld“; er fuhr dagegen und schob es vor sich her. Schmaler machen kam nicht infrage, da sonst für alles andere zu wenig Platz gewesen wäre.

Wir versuchten den Sensor auf einem Servo zu montieren, der sich mit jedem Schritt drehen sollte, wie ein Suchscheinwerfer. Der Roboter machte nicht mehr das, was er sollte, egal was wir versuchten. Wir vermuten, dass es am Code lag, der mit der Zeit immer unübersichtlicher wurde. Hier das Schaltbild des Servos:

Abbildung 7: Schaltplan des Servos

Im zweiten Versuch ließen wir den Servo einmal von rechts nach links schwenken und dabei den Sensor Werte aufnehmen; das vereinfachte den Code. Der geringste Abstand sollte ein Objekt sein, zu dem er sich dreht und gezielt hinfährt, bis er davor steht. Dazu mussten wir messen, in welchem Winkelbereich der Servo sich bewegen kann, ohne dass der Sensor durch die Blöcke auf dem Dach (siehe Abb. 1) verwirrende Werte misst: zwischen 40 und 115 Grad. Er sucht also ein Objekt, dreht sich dorthin und fährt geradeaus, bis er davor steht.
Ursprünglich sollte er ziellos durch die Gegend fahren und wenn er zufällig ein Objekt wahrnimmt, sich davor positionieren und überprüfen, ob es das gesuchte ist. Jetzt fährt er gezielt auf Objekte zu.

Benötigte Funktionen: Objekt finden, zum Objekt drehen, zum Objekt fahren, Abgrund erkennen und darauf reagieren, nach der Erkennung vom Objekt entfernen, falls es das falsche ist (all diese Funktionen findet ihr am Ende des Hauptcodes in der zip-Datei).

Farberkennung:

Abbildung 8: Schaltplan Farbsensor

Für die Farberkennung brauchten wir neben dem Farbsensor noch bestimmte Funktionen, mit denen der Farbsensor angesteuert und die Werte umgerechnet werden können. Diese haben wir aus dem Wiki übernommen; nur die Werte mussten kalibriert werden. Dazu hält man sowohl etwas sehr Dunkles (schwarz) und etwas sehr Helles (weiß) vor den Sensor und schreibt die Werte an die entsprechenden Stellen im Code (im Wiki nachzulesen).
Als wir den Farbsensor dann bei Farben testen wollten, fiel uns auf, dass er leider so ungenau ist, dass komplett unterschiedliche Werte entstehen, wenn der Sensor aus einem anderen Winkel auf eine Farbe gerichtet ist. Die unterschiedlichen Lichtverhältnisse haben die Werte nur noch weiter verfälscht.
Nach mehreren Testversuchen, um sinnvolle Grenzwerte zu bestimmen, haben wir entschieden, dass nur zwischen zwei Farben unterscheiden werden soll.

Anmerkung: An dieser Stelle haben wir aufgehört, da uns die Zeit fehlte. Deswegen ist die Farberkennung auch nicht vollständig im Code integriert.

Objekterkennung:

Zuerst hatten wir die Idee, dass der Roboter mithilfe einer Kamera verschiedenste Objekte erkennen und identifizieren soll. Wenn das richtige Objekt erkannt wird, wird das gesamte Programm beendet.
Damit wir verschiedene Objekte anhand des Kamerabildes erkennen können, brauchten wir eine künstliche Intelligenz (KI), diese musste auf einem Raspberry PI (RPI) laufen. Um die KI und zugehörigen Programme auf dem RPI zu installieren, folgten wir der Anleitung in diesem YouTube-Video: https://www.youtube.com/watch?v=npZ-8Nj1YwY. Wie im Video beschrieben kann man die Objekterkennung mithilfe des Programms Object_detection_picamera.py starten. Hierbei kam uns die Idee, dass ein Nutzer selbständig angeben könnte, welches Objekt der Roboter suchen soll und das Programm dann startet. Dazu haben wir in der erwähnten Datei (und ein paar weiteren – die Dateien sind unten angehängt) ein paar Änderungen vorgenommen.

Damit der Nutzer ein eigenes Objekt festlegen kann, haben wir auf dem RPI einen Apache Webserver installiert, auf dem eine Webseite läuft (dazu wird die unten angehängte index.php bei laufendem Webserver in den Ordner /var/www/html kopiert); hier wählt der Nutzer zuerst ein Objekt aus und klickt dann auf OK, mit diesem Klick wird das Objekt in eine Datei geschrieben und diese Datei wird an denselben Speicherort verschoben, an dem auch die Datei Object_detection_picamera.py (Pfad: /home/pi/tensorflow1/models/research/opbject_detection) liegt.

Das Programm soll daraufhin von der Webseite aus gestartet werden (zumindest ist das geplant, bisher aber noch nicht umgesetzt). Das Programm Object_detection_picamera.py ruft die Funktion vis_util.visualize_boxes_and_labels_on_image_array auf, diese Funktion findet man in der Datei object_detection/utils/visualizations_utils ab Zeile 621; diese Funktion haben wir stellenweise verändert, sodass sie prüft, ob das Objekt, das in der Datei steht, mit dem Objekt, das in der Kamera erkannt wird, übereinstimmt. Die Funktion gibt zwei Werte zurück, der zweite davon (recog) ist ein binärer Wert, der von dem Skript Object_detection_picamera.py ausgewertet wird. Stimmen beide Objekte nicht überein, teilt der RPI dem Arduino (der den Roboter steuert) mit, dass der Roboter weiter fahren soll. Dieser fährt so lange, bis der Roboter wieder vor einem Hindernis steht. In diesem Fall schickt der Arduino dem RPI ein Signal, dass dieser mit der Objekterkennung fortfahren soll.

Das gesamte Skript läuft so lange, bis das korrekte Objekt erkannt wurde. Wenn dies geschehen ist, wird dem Nutzer auf der Webseite eine Erfolgsmeldung ausgegeben, damit dieser erfährt, dass das Objekt gefunden wurde. Für den genauen Programmablaufplan siehe PAP.png weiter unten. Man kann noch überlegen, das Skript zu verbessern, indem der Nutzer das Kamerabild live sehen kann.

Anmerkung: Leider sind wir nicht so weit gekommen, die Objekterkennung zu vervollständigen und mit dem restlichen Teil des Roboters und Codes zu vereinen.

Genannte Anhänge: objekterkennung.zip

Wegaufzeichnung mit einer dynamischen Liste:

Die Idee hinter einer dynamischen Liste war es, zu speichern, in welcher Reihenfolge der Roboter wie viele Schritte in welche Richtung gemacht hat. Das Problem ist nur, dass Arduino keine dynamischen Listen vorsieht; ein array mit festgelegter Größe ist aber unpraktisch. Wir holten uns eine selbstgeschriebene Bibliothek aus dem Internet, mit dem das funktionieren sollte. Allerdings waren dafür Standard-Befehle von C++ nötig, die Arduino nicht unterstützte.
Eine andere Bibliothek sollte Abhilfe schaffen; damit diese funktionierte, musste noch ein C++-Compiler installiert werden (die Bibliothek findet ihr am Ende der Seite im zip-Ordner des Codes). Da der Programmteil der Liste im Code integriert ist, hier nochmal, wie das ganze beispielhaft funktioniert (wir lassen uns die Liste nicht ausgeben, da erst der zweite Roboter das braucht, aber hier wird es ausgegeben).
Beispiel einer dynamischen Liste mit SimpleList.h (hier das Ganze nochmal als zip: liste_beispiel.zip):

#include <SimpleList.h>     //einbinden der Bibliothek
 
SimpleList<char>* weg = new SimpleList<char>();       //erstellen der Liste weg
int theSize = weg->size();
 
 
void setup() {
  Serial.begin(9600);
 
  weg->add('r');            //hinzufügen eines neuen Elements
 
  char beispiel = weg->get(0);    //speichern dieses Elements in einer Variable
 
  Serial.println(beispiel);       //Ausgabe
}

Dazu braucht man die Bibliothek SimpleList.h (siehe zip-Ordner). In unserem Code fügt jede Funktion, die den Roboter fahren lässt, einen bestimmten, dieser Funktion zugeordneten Buchstaben hinzu. Zum Beispiel ‚r‘ für nach rechts fahren oder ‚f‘ für vorwärts fahren (‚forward‘) (siehe oben bei der vorwärts-fahren-Funktion).
Hier der Link zur github-Seite von SimpleList: https://github.com/spacehuhn/SimpleList

Weitere Errungenschaften:

Montage eines An-/Ausknopfs, Bau eines Dachs (siehe Abb. 1 und 2)

Zusammenspiel des gesamten Programms

Das Video, das am Anfang erwähnt wurde, leider immer noch ohne vollständige Objekterkennung:

Abbildung 9: Ablauf in normaler Geschwindigkeit


Technische Daten, Bauteile, Pins

Liste verwendeter Bauteile:

  • zwei Holzplatten, weitere zugeschnittene Holzteile
  • zwei Stepper-Motoren
  • zwei Räder, ein drittes loses Rad
  • 5 Ultraschallsensor
  • ein Farbsensor
  • ein Servo (für den Ultraschallsensor vorne in der Mitte)
  • Schaltplatine
  • Arduino Mega
  • 2 Motortreiber und Kondensatoren (für die Stepper-Motoren)
  • Kabel, Schrauben, Klebeband, Draht, Winkel
  • (Kamera, Raspberry Pi)

Pinbelegungstabelle

Pin angeschlossen
D2 Farbsensor S2
D3 Rechter Motor, stepper-Pin
D4 Rechter Motor, direction-Pin
D5 Linker Motor, stepper-Pin
D6 Linker Motor, direction-Pin
D7 sensorVorneUntenRechts, trig-Pin
D8 sensorVorneUntenRechts, echo-Pin
D9 sensorHintenUnten, trig-Pin
D10 sensorHintenUnten, echo-Pin
D11 sensorHinten, trig-Pin
D12 sensorHinten, echo-Pin
D31 Farbsensor sensorOUT
D33 Farbsensor S3
A1 Servo
A2 sensorVorne, trig-Pin
A3 sensorVorne, echo-Pin
A4 sensorVorneUntenLinks, trig-Pin
A5 sensorVorneUntenLinks, echo-Pin


Endstand des Projekts:

Der Roboter kann sowohl fahren, als auch währenddessen darauf achten nicht vom Tisch zu fallen oder gegen Objekte zu fahren. Allerdings hat er tote Winkel, da die Sensoren nicht alles abdecken. Das Problem mit dem Tisch haben wir gelöst, aber manchmal fährt er noch gegen Objekte, die in einem ungünstigen Winkel zu ihm stehen. Außerdem haben wir es nur weitestgehend geschafft, dass er geradeaus fährt, aber allein durch den Code ließ sich der Linksdrang nicht entfernen.

Bei der Objekterkennung haben wir uns sehr viel mit der Kamera beschäftigt, was bisher noch nicht so funktioniert, wie wir uns das vorgestellt haben. Deshalb mussten wir uns auf die Farberkennung beschränken, die immer noch sehr fehleranfällig ist, da leichte Schwankungen der Lichtverhältnisse, oder der Winkel in dem der Roboter auf ein Objekt zufährt, die Messergebnisse stark verändern. Im Endeffekt mussten wir uns auf zwei gut unterscheidbare Farben beschränken, jedoch ist jenes bisher nicht vollständig im Code integriert.

Verbesserungswürdig und ausbaufähig ist die Wegaufzeichnung, wobei man sich zum Beispiel an einem Projekt aus dem letzten Sommersemester orientieren könnte, dem Kartographen, dessen Hauptaufgabe genau darin bestand. Zudem fehlt noch der zweite Roboter, der durch Berechnung des kürzesten Weges direkt zum Zielobjekt fahren würde. Zur Objekterkennung kann man sich entweder dazu entscheiden, die Farberkennung noch vollständig zu integrieren oder das Programm mit der Kamera zu vollenden und dann den Code zu erweitern.

Code: code.zip

Vollständiger Schaltplan: schaltplan.png

projektewise18/orr_s_public/start.txt · Zuletzt geändert: 2019/04/02 23:12 von d.golovko