R2-Bier2
Das Projekt R2-Bier2 ist der versuchte Aufbau eines Servierroboters mit der Implementierung von Funktionen zur Gesichts- und Abgrunderkennung. Dafür wurden die Softwarebibliotheken OpenCV und WiringPi verwendet. Das Projekt wurde im Rahmen des Moduls Eingebettete Systeme am Campus Velbert/Heiligenhaus der Hochschule Bochum von Peter Wiese bearbeitet.
Motivation
Das Projekt R2-Bier2 ist im Modul Eingebettete Systeme am Campus Velbert/Heiligenhaus entstanden. Ziel des Projekts war die Programmierung eines Servierroboters, welcher Gesichter erkennen und auf diese zufahren soll, bis eine Tischkante erkannt wird. Dadurch wird an das erkannte Gesicht eine Ware beliebiger Art -- im Idealfall Bier -- geliefert. Als Grundlage wird dafür eine Roboterplattform, welche im Rahmen des Projekts \textit{Hochschulsonde} im Modul \textit{Vertiefung Systemtechnik} im Wintersemester 2012/13 entstanden ist, verwendet. Die bereitgestellte Software des Projekts Hochschulsonde steht unter der GNU-GPL und erledigt vollständig die Motorsteuerung. Die Software läuft auf einem AVR-Mikrocontroller AtMega2560, welcher sich auf einem ITEADUINO MEGA 2560 Board (im Folgenden als Arduino bezeichnet) befindet. Die Gesichts- und Abgrunderkennung wird mithilfe eines Raspberry Pi 3 (im Folgenden als Raspberry bezeichnet) ausgeführt.
Konzept
Das Konzept des R2-Bier2 ist relativ einfach gehalten. Der Raspberry fungiert als Hauptrechner. Dabei gibt der Raspberry über seine GPIO-Pins Befehle an den Arduino und veranlasst den Roboter so zur Bewegung. Dabei gibt der Raspberry zuerst den Befehl, dass sich der Roboter langsam im Kreis drehen soll und aktiviert gleichzeitig die Gesichtserkennung. Sobald diese ein Gesicht erkannt hat, gibt der Raspberry den Befehl zum Stoppen der Drehung. Dadurch steht der Roboter so ausgerichtet, dass er durch eine einfache Vorwärtsbewegung direkt auf das erkannte Gesicht zufahren kann. Nun fährt der Roboter solange geradeaus, bis die Abgrundserkennung die Tischkannte erkannt hat. Ist dies geschehen verweilt der Roboter, an seiner Position, bis der Nutzer die Ware entsprechend abgeladen hat und über einen Taster den Befehl gibt, dass der Roboter zurück fahren soll. Ist dies geschehen, fährt der Roboter auf seine Ausgangsposition zurück. Die genaue Realisierung der Nutzung der GPIO-Pins, der Abstandserkennung, der Gesichtserkennung und der Kommunikation mit dem Arduino werden in den folgenden Abschnitten erläutert.
Programmierung
Ansteuerung der GPIO-Pins mittels WiringPi
Für dieses Projekt ist die Ansteuerung der GPIO-Pins des Raspberrys eines der wichtigsten Fähigkeiten des Programms, da ohne die GPIO-Pins weder die Kommunikation mit dem Arduino, noch die Abgrundserkennung funktionieren würden. Für die einfache Realisierung der GPIO Ansteuerung wurde auf die frei verfügbare Bibliothek WiringPi zurückgegriffen. WiringPi steht unter der GNU LGPLv3. Die Benutzung von WiringPi ähnelt absichtlich der GPIO Ansteuerung der Arduino IDE. WiringPi wird über GIT gepflegt. Dadurch kann der Quellcode einfach aus dem GIT geklont und compiliert werden. Für eine genaue Anleitung zur Installation von WiringPi sei auf [Dro16] verwiesen. Für die Nutzung von WiringPi muss die Bibliothek, wie jede andere genutzte Bibliothek auch, im Präprozessor definiert werden. Dafür muss im Präprozessor folgende Zeile eingefügt werden:
#include <wiringPi.h>
Ist dies geschehen, kann im weiteren Programm auf die Funktionen der Bibliothek zurück gegriffen werden. Bei der Verwendung von GPIO-Pins muss zuerst der Modus der Pins definiert werden. Im hier dokumentierten Programm erfolgt das über die Methode setUpPins()
. Diese ist wie folgt aufgebaut:
#include <wiringPi.h> #define TRIGGER 9 #define ECHO 7 #define STARTER 8 void setUpPins(){ wiringPiSetup(); pinMode(TRIGGER,OUTPUT); pinMode(ECHO,INPUT); pinMode(STARTER,OUTPUT); digitalWrite(TRIGGER,LOW); digitalWrite(STARTER,LOW); }
Es werden hierbei zuerst die einzelnen Pins im Präprozessor definiert um im gesamten Quelltext einfacher auf diese zugreifen zu können. In der Funktion setUpPins() wird dann als erstes die Funktion wiringPiSetup()
aufgerufen. Laut der WiringPi-Dokumentation muss diese Funktion einmalig bei Programmstart aufgerufen werden um WiringPi zu initialisieren. Nachfolgend werden dann die Modi der einzelnen Pins festgelegt. Dies erfolgt durch die Funktion pinMode(Pin,Mode)
. Die Funktion erwartet als Argumente die Pinnummer des zu initialisierenden Pins und der Modus in dem der Pin initialisiert werden soll. Verfügbare Modi sind INPUT und OUTPUT und die Pinnummerierung muss, wie in Abbildung 1 dargestellt, nach dem WiringPi-Schema erfolgen. Abschließend werden noch die initialen Zustände der Output-Pins gesetzt. Dies erfolgt durch den Aufruf der Funktion digitalWrite(Pin,Zustand)
, welche als Argumente die Pinnummer und den Zustand erwartet. Gültige Zustände sind hierbei HIGH und LOW. Der Aufruf dieser Funktion ist wichtig, da man sonst beim Starten des Programms nicht genau sagen kann, welchen Zustand die Pins aktuell haben.
Abgrunderkennung
Die Abgrundserkennung erfolgt über drei Ultraschallsensoren. Der erste dieser Sensoren ist dabei mittig an der Front des Roboters angebracht. Die anderen beiden Sensoren sind jeweils um ±45 ◦ versetzt vom ersten Sensor angebracht. Dadurch soll die Tischkante rechtzeitig und zuverlässig bei allen Fahrtrichtungen erkannt werden.
Die Funktionsweise eines Ultraschallsensors ist relativ simpel. Der Sensor sendet ein bestimmtes Ultraschallsignal aus, welches vom zu erfassenden Objekt reflektiert wird. Diese Reflexion wird vom Ultraschallsensor empfangen. Aus der Zeit die zwischen Aussendung und Empfang des Signals vergangen ist kann anschließend die Entfernung zwischen Sensor und Objekt berechnet werden. Die gesuchte Entfernung wird mit d bezeichnet, die vergangene Zeit wird mit t bezeichnet und die Schallgeschwindigkeit wird mit 34300 cm/s angenommen. Dadurch ergibt sich für den Berechnung des Abstands zwischen Objekt und Ultraschallsensor folgende Formel:
d = t/2 * 34300 cm/s
Gesichtserkennung
Für die Implementierung der Gesichtserkennung wird in diesem Projekt die Bibliothek OpenCV genutzt. OpenCV steht für „Open Source Computer Vision“ und ist unter einer BSD-Lizenz veröffentlicht und ermöglicht verschiedene Varianten der Gesichtserkennung [siehe IG16]. In diesem Projekt werden die Eigenfaceund die Fisherface-Methode verwendet. Da die Methoden bereits vollständig in OpenCV implementiert sind, wird in dieser Arbeit nicht weiter auf die zugrunde liegenden Algorithmen eingegangen. Dafür sei auf [TP91] und [BHK97] verwiesen. Die beiden vorgestellten Methoden sind jedoch nur für die Zuordnung der Gesichter verantwortlich und können eigenständig kein Gesicht im Bild finden. Dafür wird die sogenannte Haar-Cascade genutzt. Auch diese ist bereits vollständig in OpenCV implementiert. Für die zugrunde liegenden Algorithmen sei auf [VJ01] verwiesen.
Die Eigenface- und Fisherface-Methode müssen beide „trainiert“ werden um Gesichter erkennen zu können. Dafür benötigen sie verschiedene Fotos der zu erkennenden Gesichter. Wichtig ist dabei, dass die Trainingsfotos alle dieselbe Größe haben. Um dies zu gewährleisten wurde im Rahmen des Projekts ein kleines Programm entwickelt, mit dem Fotos in vordefinierten Maßen mittels einer Webcam aufgenommen werden können. Das Programm basiert auch auf OpenCV. Da damit grundlegende Funktionen von OpenCV erläutert werden können, wird es im folgenden vorgestellt.
#include "opencv2/opencv.hpp" #include <stdio.h> #include <stdexcept> using namespace cv; int main(int argc , char *argv[]) { int i = 0; char const *name = "peterWiese_"; Mat cameraFrame; VideoCapture cap(0); if(!cap.isOpened()) return -1; while(1){ cap.read(cameraFrame); imshow("cam", cameraFrame); uint32_t ch = waitKey(30); if ((ch & 0xff) =='c'){ char file[100]; sprintf(file,"%s%d.png",name,i); imwrite(file,cameraFrame); printf("Image saved: %s\n",file); i++; } if((ch & 0xff) =='a') break; } return 0; }
Dabei wird zuerst ein Objekt der Klasse VideoCapture
mit dem Namen cap
erstellt. Durch den Übergabeparameter 0 wird die Standard-Kamera geöffnet. Anschließend wird ein Objekt der Klasse Mat mit dem Namen cameraFrame erzeugt. Die Klasse Mat repräsentiert dabei ein n-dimensionales, numerisches Single- oder Multi-Channel-Array, welches genutzt wird um Bilder zu speichern [IG15]. Dies geschieht in der while-Schleife. Hier wird durch die Funktion cap.read(cameraFrame)
das aktuelle Bild der Kamera in cameraFrame
gespeichert und durch die Funktion imshow("cam",cameraFrame) auf dem Bildschirm ausgegeben. Im Anschluss wird auf 30ms auf eine Tastatureingabe gewartet und die in den nachfolgenden if-Abfragen ausgewertet. Wurde dabei ein Tastendruck der Taste „c“ erkannt, so speichert das Programm das aktuelle Bild in einer *.png-Datei unter einem im Vorfeld definierten Namen und der laufenden Nummer i. Wird ein Tastendruck der Taste „a“ erkannt, so wird die while-Schleife unterbrochen und das Programm beendet. Bei keinem oder einen anderem Tastendruck läuft die Schleife weiter und das bewegte Bild wird flüssig auf dem Bildschirm angezeigt.
Hat man dadurch einige Bilder von mindestens zwei Personen erzeugt, müssen diese dem Gesichtserkenner antrainiert werden. Die hierbei verwendeten Vorgehensweisen orientieren sich an dem Tutorial Face Recognition in Videos with OpenCV aus der OpenCV-Dokumentation. Für das Training der Gesichter wird eine CSV-Datei verwendet. Diese ist wie folgt aufgebaut:
/home/peter/FaceRec/data/PeterWiese/peterWiese_0.png;0 /home/peter/FaceRec/data/PeterWiese/peterWiese_1.png;0 [...] /home/peter/FaceRec/data/PeterGerwinski/peterGerwinski_0.png;1 /home/peter/FaceRec/data/PeterGerwinski/peterGerwinski_1.png;1 [...] /home/peter/FaceRec/data/LennartWoelki/lennartWoelki_0.png;2 /home/peter/FaceRec/data/LennartWoelki/lennartWoelki_1.png;2
Dementsprechend besteht eine Zeile der Datei aus dem Dateinamen des Bildes und einem Label. Als Trennzeichen wird ein Semikolon verwendet. Bilder die zur selben Person gehören haben dabei immer dasselbe Label. Die CSV-Datei wird dann über die Funktion read_csv()
eingelesen. Die Funktion ist im folgenden dargestellt.
static void read_csv(const string& filename, vector<Mat>& images, vector<int>& labels, char separator = ';') { std::ifstream file(filename.c_str(), ifstream::in); if (!file) { string error_message = "No valid input file was given, please check the given filename."; CV_Error(CV_StsBadArg, error_message); } string line, path, classlabel; while (getline(file, line)) { stringstream liness(line); getline(liness, path, separator); getline(liness, classlabel); if(!path.empty() && !classlabel.empty()) { images.push_back(imread(path, 0)); labels.push_back(atoi(classlabel.c_str())); } } }
Dabei werden die Bilder und Label jeweils in einem vector
-Objekt gespeichert.
Diese Vektoren können anschließend zum Training der Gesichtserkennung genutzt
werden. Dies ist wie folgt realisiert:
int main(void) { vector<Mat> images; vector<int> labels; try { read_csv(path_to_csv, images, labels); } catch (cv::Exception& e) { cerr << "Error opening file"; exit(1); } int im_width = images[0].cols; int im_height = images[0].rows; Ptr<cv::face::BasicFaceRecognizer> model = cv::face::createFisherFaceRecognizer(); model->train(images, labels); CascadeClassifier haar_cascade; haar_cascade.load(path_to_cascade); }
Das Training der Gesichtserkennung erfolgt, nachdem ein Objekt vom Typ BasicFaceRecognizer
über die Methode cv::face::creatFisherFaceRecognizer()
erzeugt wurde. Anschließend wird das Training mittels der Zeile model->train(images,labels)
gestartet und bekommt die Bilder, welche über die CSV-Datei eingelesen wurden, übergeben. Für die Haarcascade wird bereits ein fertiges Model von OpenCV zur Verfügung gestellt. Dieses muss nur übergeben werden.
Nachdem die Modelle zur Gesichtserkennung trainiert wurden, kann die eigentliche Erkennung starten. Dafür wird, wie bereits im Webcam-Beispiel, das Bild der Webcam Bild für Bild aufgenommen und ausgegeben. Dabei wird jedoch jedes Bild vorher untersucht, ob es Gesichter enthält. Dafür wird das Bild zuerst in Graustufen umgewandelt. Anschließend werden mittels der Haar-Cascade Gesichter gesucht. Dabei wird jedes gefundene Gesicht in einem Viereck gespeichert. Anschließend werden alle gefundenen Gesichter mittels der Fisher-Face-Methode untersucht. Anschließend wird das Bild ausgegeben, die Rechtecke um die gefundenen Gesichter gezeichnet und über den Gesichtern ausgegeben, um welche Person es sich handelt. Im Quellcode ist dieser Vorgang durch folgende Zeilen beschrieben:
while(1){ cap >> frame; Mat original = frame.clone(); Mat gray; cvtColor(original, gray, CV_BGR2GRAY); vector< Rect_<int> > faces; haar_cascade.detectMultiScale(gray, faces); for(int i = 0; i < faces.size(); i++) { Rect face_i = faces[i]; Mat face = gray(face_i); Mat face_resized; cv::resize(face, face_resized, Size(im_width, im_height), 1.0, 1.0, INTER_CUBIC); int prediction = model->predict(face_resized); rectangle(original, face_i, CV_RGB(0, 255,0), 1); string box_text = format("Prediction = %d", prediction); int pos_x = std::max(face_i.tl().x - 10, 0); int pos_y = std::max(face_i.tl().y - 10, 0); putText(original, box_text, Point(pos_x, pos_y), FONT_HERSHEY_PLAIN, 1.0, CV_RGB(0,255,0), 2.0); } imshow("face_recognizer", original); char key = (char) waitKey(20); if((key & 0xff) == 27) break; }
Dabei ist die Zuverlässigkeit der Gesichtserkennung stark von den verfügbaren Trainingsdaten ab. Dabei ist es von Vorteil, möglichst viele Bilder von den jeweiligen Gesichtern zu haben, die sich in Perspektive, Gesichtsausdruck und Belichtung unterscheiden. Es sollte auch darauf geachtet werden, dass alle Bilder die gleiche Größe haben und den gleichen Ausschnitt des Gesichts zeigen. Zum Zuschneiden und Skalieren der Bilder eignet sich dabei hervorragend ein Python-Script welches auf [Wag11] zur Verfügung gestellt wird.
Die Gesichtserkennung selber funktioniert dabei mit dem Raspberry nicht flüssig, da es sehr rechenintensiv ist und die Taktfrequenz vom Pi nicht ausreicht, um genug Bilder pro Sekunde untersuchen zu können, damit das ausgegebene Bild flüssig erscheint.