SchnickSchnackSchnuck

Aus toolbox_interaktion
Wechseln zu: Navigation, Suche

Ziel des Projektes

Ziel des Projetes war es, das namensgebende Spiel 'SchnickSchnackSchnuck' in digitaler Form zu erstellen. Um dem Feeling des Orginalspiels möglichst nahe zu kommen sollte eine kameragestütze Gestenerkennung verwendet werden und ein Mehrspielermodus über Netzwerk implementiert werden.

Implementierung

Verwendete Frameworks

Die Implementierung des Projektes erfolgte mithilfe folgender Frameworks in C++:

  • QT als C++ Framework
  • OpenCV für die Bildverarbeitung
  • FANN-Lib - ein neurales Netz - für die Gestenerkennung

Gearbeitet wurde ausschließlich unter Debian bzw. deren Derivaten. Die Verwendung von QT ermöglicht allerdings eine Platformunabhängige Entwicklung - eine Portierung auf ander Betriebssysteme sollte also jederzeit möglich sein.

Bildverarbeitung

Anfängliche Probleme

Der Anfänglich Versuch, die Binearisirung über Hintergrundsubstraktion zu realisieren scheiterte, weshalb zu einer Filterung nach Hauttönen im HSV-Farbbereich übergegnagen wurde. Das ergebniss hierbei war deutlich besser, wurde aber von diversen kleineren Artefakten gestört.

Umsetzung

Bei der Bildverarbeitung und Filtuerung wurde weitesgehend auf die Funktionen der OpenCV-Bibliothek zurückgegriffen. Zusätzlich wurde ein Flächenfilter entwickelt, der die Störungen im Bild weiter reduzierte. Hierzu werden dem Filter minimale und maximale Fläche von zusammenhängenden Objekten im binearisierten Bild übergeben und nur die dazwischen liegenden Flächengrößen zurückgegeben.

Verwendete Funktionalität von opencv und highgui

Es wurde, da die Qt-lib ein C++-Framework anbietet, auch bei der opencv das C++-Interface verwendet. Bei diesem ist eine der wichtigesten Klasse die cv::Mat. Das ist die algemeine Container-Klasse für Matrizen aller Art, also beispielsweise auch Bilder. Die Daten sind implicit-shared.

Einlesen eines Bildes von einer Kamera

Hierzu wird die cv::VideoCapture-Klasse verwendet. Diese wird mit der Device-Nummer (oder -1 == default) initialisiert. Anschließend kann mit dem operator>>() das Bild ausgelesen werden. Diese Funktion blockiert solange bis ein Bild gelesen werden konnte. Werden die Bilder nicht oft genug abgerufen, werden diese gepuffert. Beim Abrufen des nächsten Bildes wird das älteste Bild im Puffer zurückgegeben ==> Möchte man immer das neuste Bild musst man dafür sorgen, dass oft genug grab() oder operator>>() aufgerufen wird. Dies wird hier durch einen eigenen Capture-Thread bewerkstelligt.

Hier noch ein Anwendungsbeispiel:

#include <opencv/highgui.h>
 
int main()
{
   cv::VideoCapture cap(-1);
   assert(cap.isOpened());
   cv::Mat mat;
 
   cap >> mat;
   cv::imshow("Fenster-Titel", mat);
   cv::waitKey();
}

Wichtig: Damit VideoCapture ein Device öffnen kann, muss ein driver installiert sein (z.B. v4l(video4linux))

Median-Filter

/*
 * in (cv::Mat) mat ist das Bild gespeichert
 */
cv::medianBlur( /* src */ mat, /* dst */ mat, /* fenstergröße */ 3 );
/*
 * nun ist in mat das gefilterte Bild gespeichert
 */

Binearisieren nach Farbton

Zusätzlich zum Farbton wurde hier zur Binearisierung auch noch die Sättigung und die Helligkeit mit einbezogen.

/*
 * in (cv::Mat) mat ist das Bild in RGB gespeichert
 */
 
/*
 * zunächst wird das Bild in den HSV raum transformiert
 */
cv::Mat hsv;
cv::cvtColor(mat,hsv,CV_BGR2HSV);
 
/*
 * mit cv::split die 3 Kanäle aufgeteilt, wobei der h-kanal Index 0, der
 * s-kanal Index 1 und der v-kanal Index 2 sind.
 */
std::vector<Mat> hsv_ch;
cv::split(hsv,hsv_ch);
 
/*
 * da bei Rot der Hue-Wert an den beiden Rändern liegt, wird der h-Kanal um 127
 * verschoben ( dies muss natürlich beim angeben der Schwellwerte
 * berücksichtigt werden ). An dieser Stelle könnte man auch noch Glättungsfilter
 * oder Ähnliches einbauen
 */
cv::Mat &h = hsv_ch[0];
for (int i = 0; i < h.rows; i++) {
   for (int j = 0; j < h.cols; j++) {
      h.at<uint8_t>(i,j) += 127 ;
   }
}
 
/*
 * Anschließend werden werden die 3 Kanäle wieder zusammen geführt
 */
cv::merge(hsv_ch, hsv);
 
/*
 * mit cv::inRange wird nach mat_1C das binearisierte Bild geschrieben
 * Die beiden cv::Scalar geben den obenen und unteren Schwellwerte für
 * die 3 Kanäle an
 */
cv::Mat mat_1C;
cv::Scalar hsv_min(h_min, s_min, v_min);
cv::Scalar hsv_max(h_max, s_max, v_max);
cv::inRange(hsv, hsv_min, hsv_max, mat_1C);

Nur größere Flächen anzeigen

Um alle größeren zusammenhänden Flächen zu extrahieren werden diese zuerste gesucht und anschließend ein neues Bild gemalt.

/*
 * Das Eingangs Bild (mat_1C) ist binär
 */
 
/*
 * Es wird ein leeres binäres Bild erzeugt in das die Flächen gemalt werden
 * können. Dieses hat die gleiche Größe und Farbtiefe wie das Eingangsbild.
 */
cv::Mat output(cv::Mat::zeros(mat_1C.size(),mat_1C.type()));
 
/*
 * In contours werden nun alle Eckpunkte von allen Flächen, die von findContours
 * gefunden werden konnten, geschrieben.
 */
std::vector<std::vector<cv::Point> > contours;
cv::findContours(mat_1C,contours,CV_RETR_LIST,CV_CHAIN_APPROX_SIMPLE,cv::Point(0,0));
 
/*
 * Nun iteriert man über alle gefundenen Flächen
 */
std::vector<std::vector<cv::Point> >::iterator it;
for(it=contours.begin() ; it < contours.end(); it++)
{
   std::vector<cv::Point> *shape(&(*it));
   /*
    * Mit cv::contourArea wird die Größe der Fläche berechnet. Ist diese größer
    * als der Schwellwert area_min, wird anschließend diese Fläche zu der
    * output-Matrix hinzugefügt.
    */
   double area = cv::contourArea(*shape);
   if (area < area_min || area > area_max) continue;
 
   /*
    * Da cv::fillPoly ein Array statt einem Vector braucht muss hier
    * konvertiert werden
    */
   const cv::Point* elementPoints[1] = &shape->at(0);
   int numberOfPoints = (int)shape->size();
 
   /*
    * Wie beim Erstellen der output-Matrix auch wird hier eine leere Matrix mit
    * der Farbtiefe und Größe des Ursprungsbild erzeugt. Auf diese leere Matrix
    * wird nun mit cv::fillPoly ein ausgefülltes Polygon in Form der vorher mit
    * cv::findContours gefundenen Fläche gemalt.
    */
   cv::Mat tmp(cv::Mat::zeros(mat_1C.size(),mat_1C.type()));
   cv::fillPoly(tmp,&elementPoints,&numberOfPoints,1,cv::Scalar(255,255,255));
 
   /*
    * Das Zusammenführen kann, da tmp und output binäre Bilder sind einfach
    * mit der cv::max verzogen werden.
    */
   cv::max(tmp,output,output);
}

Berechnen der Hu-Momente

/*
 * Das Eingangs Bild (mat_1C) ist binär
 */
/*
 * Zunächst müssen aus dem Eingangbild die Momente berechnet werden, wobei der
 * 2. Parameter angibt ob es sich um ein Binärbild handelt.
 */
cv::Moments moms = cv::moments(mat_1C, true);
 
/*
 * Mit den Momenten können mit cv::HuMoments die HuMomente berechnet werden.
 */
double huMoms[NUMBEROFHUS];
cv::HuMoments(moms, huMoms);

Berechnen des Formfaktors

Zunächst müssen hier die zusammenhängenden Flächen gesucht werden. Wenn man mehere Flächen findet, wird in diesem Beispiel, mit dem größten Umfang ausgefählt. Mit diesem Umfang und Fläche (hier aus dem Moment m00 genommen) errechnet sich der Formfaktor.

cv::Mat forContour(mat_1C);
std::vector<std::vector<cv::Point> > contours;
cv::findContours(forContour,contours,CV_RETR_LIST,CV_CHAIN_APPROX_SIMPLE);
 
double maxLen = 0;
std::vector<std::vector<cv::Point> >::iterator it;
for (it = contours.begin(); it != contours.end(); ++it)
{
   double currLen = cv::arcLength(cv::Mat(*it), true);
   if (currLen > maxLen)  maxLen = currLen;
}
m_formFactor  = moms.m00 / (maxLen * maxLen);

Gestenerkennung

Aus den gefilterten Bildern werden die Hu-Momente und der Formfaktor berechnet. Diese Parameter werden in ein Neuronales-Netz gegeben, dass aus 8 Eingangsneuronen, 2 * 12 Neuronen und 3 Ausgangsneuronen. Die Ausgangsneuronen representieren die einzelnen Gesten. So entspricht das Erste Ausgangsneuron die Schere, das Zweite das Papier und das Dritte den Stein.

Das Programm verfügt über einen Trainingsdialog. Mit diesem wird das Neuronale-Netz trainiert und die einzelnen Geste danach in einer Datei abgespeichert.

Mehrspielermodus

Die für die Netzwerkfunktionalität verantwortlichen Klassen sind nach dem Muster Network*.[cpp|h|ui] aufgebaut, wobei es sich bei der Klasse 'Network' um eine Handler für die komplette Netzwerkfunktionalität handelt.

Protokolle

Typische Client-Server-Kommunikation

Zur Umsetzung der Mehrspielerfunktionalität wurden zwei Protkolle verwendet:

  • SSS-Protokoll auf Basis von UDP, zum finden eines Servers im lokalem Netzwerk
  • SSS-Protokoll auf Basis von TCP, für die Abwicklung der Kommunikation zwischen Server und Clients.

Generell wird über beide Protokolle reiner Text übertragen (was das debuggen deutlich erleichtert). Die Pakte sind wie folgt aufgebaut:

+---------------+--------------+------------------+
| SSS-Header | Payload[0] | Payload[1...n] |
+---------------+--------------+------------------+

Wobei der Header einem QString mit dem Inhalt "SchnickSchnackSchnuck" entspricht und die Payload eine beliebig lange QStringList ist. Länge und Bedeutung der Payload werden durch ihr erstes Element (einem der definierten Befehle) definiert. Überflüssige Elemente werden verworfen.

Zur Sicherung der Datenübertragung sind die TCP-Sicherungsmechanismen zuständig

Client

Aufbau der Statemachine eines Clients

Der Client wird in der Klasse NetworkClient implementiert und enthält eine einfache StateMachine die für die Abwicklung der Empfangen Packete zuständig ist und die komplette Kommunikation mit dem Server implementiert. Beim Starten des Programmes wird nach einem Server im Netzwerk gesucht und daruaf verbunden. Wird kein Server gefunden wechselt die Anwendung automatisch in den Server Modus.

Server

Der Server wird in den beiden Klassen NetworkServer und NetworkSocket implementiert, wobei die Sockets den einzelnen Clientverbindungen entsprechen. Jeder Socket implementiert eine eigene StateMachine und kümmert ich um den Datenaustausch mit dem zugeteiltem Client. Zum besseren Handling wurden dem Server einige Funktionen hinzugefügt, die die Signale der Clients Bündeln bzw. ein Ansprechen ALLER Clients erlauben.

Hardware

Als Kameras kamen orginal PS3 Kameras zum Einsatz. Die in den Laptops integrierten Kameras lieferten zu schlechte Bilder und machten durch ihr permanentes Nachregeln eine automatische Weiterverarbeitung im Rechner nahezu unmöglich!

Sourcen

Download

Den kompletten Programmcode gibts hier. Außerdem werden benötigt:

Build

Neben qmake (von der Qt-Lib), make und g++ muss pkg-config mit pc-files für die libfann und libopencv installiert werden. Die pc-files werden bei Installation der Bibliotheken normalerweise in das lib/pkgconfig Verzeichnis gelegt. Liegen fann.pc und opencv.pc an einer anderen Stelle muss die PKG_CONFIG_PATH Variable entsprechend gesetzt werden. Ist alles richtig installiert, kann wie folgt das Projekt gebaut werden:

qmake && make

Anschließend wird im gleichen Verzeichniss die ausführbare Datei 'sss' erzeugt.