R2-Bier2

Aus Das Projektwiki
Version vom 23. Februar 2017, 09:56 Uhr von Pwiese (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Zur Navigation springen Zur Suche springen
Projekt R2-Bier2
R2bier2.jpg
Entwickler Peter Wiese
Hochschule Campus Velbert/Heiligenhaus
Modul Vertiefung Eingebettete Systeme
Semester Sommersemester 2016, Wintersemester 2016/17
Professor Prof. Dr. rer. nat. Peter Gerwinski
Programmiersprache C,C++
genutzte Bibliotheken OpenCV, WiringPi
Lizenz GNU General Public Licence, BSD Simplified Licence

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

Raspberry Pi 3

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 Hochschulsonde im Modul 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

Iteaduino mit angeschlossenem Motorcontroller

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[1]. 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

In diesem Projekt werden Ultraschallsensoren vom Typ HC-SR04 verwendet. Diese haben vier Pins. Zwei dieser Pins werden dabei für die 5V Spannungsversorgung und die gemeinsame Masse mit dem Raspberry genutzt. Die anderen beiden Pins – Trigger und Echo – sind für die eigentliche Funktion des Sensors verantwortlich. Bei dem Pin Trigger handelt es sich um einen Input-Pin, welcher ein Signal vom Raspberry erwartet. Sobald er dieses Signal bekommen hat beginnt die Aussendung des Ultraschallsignals. Bei dem Pin Echo handelt es sich um einen Output-Pin. Dieser wird auf High gesetzt, sobald das reflektierte Ultraschallsignal vom Sensor empfangen wird. Da das Signal vom Sensor eine Spannung von 5V hat, der Raspberry jedoch mit 3,3V arbeitet, muss unbedingt darauf geachtet werden die Spannung über einen Vorwiderstand herunter zu regeln. Anschließend wird der Pin wieder auf Low gesetzt. Im Programmcode wird die Ansteuerung der Ultraschallsensoren durch die Funktion getDistance(int trigger, int echo) übernommen. Diese ist wie folgt aufgebaut:

#include<time.h>

double getDistance(int trigger, int echo){
	double time = 0;
	double distance = 0;
	
	digitalWrite(trigger, LOW);
	delay(50);
	digitalWrite(trigger,HIGH);
	delay(10);
	digitalWrite(trigger, LOW);
	while(!digitalRead(echo));
	clock_t start = clock();
	while(digitalRead(echo));
	clock_t end = clock();
	time = ((double)(end-start))/CLOCKS_PER_SEC;
	distance = (time/2)*34300;	
	printf("Zeit: %f \n",time);
	printf("Distance: %f \n",distance);
	
	return distance;
}

Da diese Funktion für alle Ultraschallsensoren gültig ist, benötigt sie als Argumente die Pins vom Raspberry, an dem der Trigger- und Echo-Pin des jeweiligen Sensors angeschlossen sind. In den Zeilen Neun bis Elf der Funktion wird der Trigger für 10ms auf High gesetzt. Dadurch beginnt die Aussendung des Signals. Sobald der Echo-Pin ein High-Signal empfängt, wird die Startzeit gespeichert. Sobald der Echo-Pin wieder auf Low gesetzt wurde, wird die Endzeit gespeichert. Die Funktion clock() gibt dabei die Anzahl der Clock-Ticks seit Programmstart wieder. Um diese Funktion nutzen zu können muss die Bibliothek time.h verwendet werden. In Zeile 16 des dargestellten Codes wird anschließend die Zeit berechnet. Dafür wird die Differenz der End- und Startzeit bestimmt und anschließend durch die Systemkonstante „CLOCKS_ PER_ SECOND“ geteilt. Dadurch wird die vergangene Zeit in Sekunden ermittelt, welche dann im nächsten Schritt in die Entfernung umgerechnet werden kann. Dafür wird die schon erläuterte Formel 1 genutzt. Zum Abschluss der Funktion werden die Zeit und die Distanz noch ausgegeben und die Distanz zur weiteren Verarbeitung zurückgegeben.

In der Main-Methode des Programms ist die Abgrundserkennung wie folgt implementiert:

#define TRIGGER 9
#define ECHO 7
#define DRIVER 8
#define DIST_TO_GROUND 10

int main(void){
	[...]
	digitalWrite(DRIVER,HIGH);
	while(getDistance(TRIGGER,ECHO) < DIST_TO_GROUND);
	digitalWrite(DRIVER,LOW);
	[...]
	return 0;
}

Dabei werden im Präprozessor die Pinnummern für Trigger, Echo und dem Kommunikationspin zwischen Raspberry und Arduino und der Schwellwert für die Abgrundserkennung festgelegt. Der Schwellwert beträgt dabei 10cm und ergibt sich aus der Höhe des Roboters (ca. 6cm) zuzüglich 4cm Toleranz. Sobald im Programm die Stelle erreicht wurde, an dem der Roboter losfahren soll, wird der Pin Driver auf High gesetzt. Dadurch startet der Roboter seine Bewegung. Auf die genaue Funktionsweise dieser Kommunikation wird im Abschnitt Kommunikation zwischen Raspberry Pi und Arduino eingegangen. Der Pin Driver bleibt nun so lange auf High bis einer der Sensoren einen Abstand ausgibt, der größer als der Schwellwert ist. Dann wird der Pin Driver auf Low gesetzt und die Bewegung des Roboters wird gestoppt.

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[2]. 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 "Eigenfaces for Recognition"[3] und "Eigenfaces vs. Fisherfaces: recognition using class specific linear projection"[4] 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 "Rapid Object Detection using a Boosted Cascade of Simple Features"[5] 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[6]. 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 in der OpenCV-Dokumentation[7] 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.

Fazit und offene Punkte

Abschließend kann gesagt werden, dass das Projekt nicht alle Punkte bearbeiten konnte. Es wurden ein Programm zur Abgrunderkennung geschrieben und, aufbauend auf "Face Recognition in Videos with OpenCV"[7], auch ein Programm zur Gesichtserkennung. Außerdem wurde zur Aufnahme der Trainingsdaten ein Programm zur Ansteuerung der Webcam entwickelt und ein Datensatz an Trainingsdaten für das Fisher-Face-Model bereitgestellt. Leider konnten diese Einzelprogramme noch nicht zu einem Programm zusammengeführt werden, da es bei der Kompilierung von WiringPi und OpenCV in einer Datei zu Problemen kam, welche im Rahmen des Projektes nicht behoben werden konnten. Auch die Verknüpfung zwischen Raspberry und Arduino wurde noch nicht implementiert, sodass auch der Roboter noch nicht zum Fahren gebracht werden konnte.

Im Projekt wurden jedoch auch wichtige Dinge, wie die Ansteuerung der GPIOs eines Raspberrys, die konkrete Funktionsweise von Ultraschallsensoren und die Schwierigkeiten und Verfahrensweisen in der Bild- bzw. Gesichtserkennung erlernt. Auch nichttechnische Dinge, wie Projektplanung, Zeitmanagement und Ressourcenmanagement wurden verbessert und erlernt, sodass in zukünftigen Projekten des Autors gewisse Fehler die in diesem Projekt unterlaufen sind, nicht mehr vorkommen sollten. Somit war das Projekt, obwohl kein fertiges Produkt entstanden ist, für den Lernerfolg während des Studiums trotzdem erfolgreich und konnte einen guten Überblick über die Schwierigkeiten bei der Entwicklung von eingebetteten System geben.

Einzelnachweise

  1. WiringPi Download and Install. Abgerufen am 02. Dezember 2016
  2. Website des OpenCV-Projekts. Abgerufen am 05. Dezember 2016.
  3. Mathew Turk und Alex Pentland: Eigenfaces for Recognition. In: Journal of Cognitive Neuroscience Volume 3, Number 1 (1991). Abgerufen am 02. Dezember 2016
  4. Peter N. Belhumeur, Joao P. Hespanha und David J. Kriegman: Eigenfaces vs. Fisherfaces: recognition using class specific linear projection. In: IEEE Transactions on Pattern Analysis and Machine Intelligence 19 (1997)
  5. Paul Viola und Micheal Jones: Rapid Object Detection using a Boosted Cascade of Simple Features (2001)
  6. cv::Mat Class Reference. Abgerufen am 05. Dezember 2016
  7. 7,0 7,1 Philipp Wagner: Face Recognition in Videos with OpenCV. Abgerufen am 05. Dezember 2016