Einstieg in die Steuerung von Sensor-Hardware

Kinect und Linux – so nutzt man den Tiefensensor mit dem Pinguin
Kommentare

Microsofts Sensorhardware erfreut sich enormer Popularität. Aufgrund des – im Vergleich zur gebotenen Leistung – geringen Preises sind sogar eingefleischte Unix-Heads gewillt, das Produkt aus Redmond an ihre Workstation anzuschließen.

Unter Windows ist die Verwendung des Sensors beinahe primitiv: Microsoft bietet ein komplettes SDK an, das alle relevanten Elemente enthält und die sofortige Nutzung unter .NET und C++ erlaubt. Für Linux-Nutzer sieht die Sache aufgrund einer Designentscheidung von Microsoft etwas anders aus. Beginnen wir deshalb mit einem kleinen Exkurs in die faszinierende Geschichte des Sensorsystems.

PrimeSense und Konsorten

Die Ermittlung von Tiefendaten ist seit Jahren gelöst: Das Time-of-Flight-Verfahren liefert extrem genaue Informationen über den Abstand zwischen einem Sensor und den ihn umgebenden Objekten. Dabei kommt ein an Radar erinnerndes Verfahren zum Einsatz. Der Sensor emittiert ein Lichtsignal und misst die Zeit, bis der Lichtpunkt am Objekt aufscheint. Aus der „Verzögerung“ lässt sich der Abstand ermitteln. Leider sind sehr schnell arbeitende CCD-Sensoren alles andere als preiswert, was die Massenmarkttauglichkeit des Systems stark einschränkt.
PrimeSense entwickelte eine weitaus preiswertere Methode zur Abstandsentwicklung. Eine spezielle Infrarotquelle emittiert Licht, dessen Form durch eine spezielle Linse im Laufe der Bewegung verformt wird. Zur Ermittlung des Abstands der individuellen Elemente analysiert der Sensor die Form des auf dem Objekt befindlichen Infrarotpunkts – der Verformungsgrad liefert Informationen über die Distanz.
Anfangs nutzte PrimeSense einen im Sensor eingebauten DSP, der die skelettale Analyse durchführte. Im Laufe der Produktentwicklung beschloss Microsoft, diesen Prozessor einzusparen und die Berechnung der Skelettaldaten stattdessen auf der Xbox 360 durchzuführen. Die ausgelieferte Hardware ist in Abbildung 1 schematisch dargestellt

 

 Abb. 1: Die Hardware des Kinect als Flussdiagramm

Abb. 1: Die Hardware des Kinect als Flussdiagramm

Ein im Handel erhältlicher Kinect versorgt die an ihn angeschlossene Workstation mit zwei regelmäßig aktualisierten „Bitmaps“. Das eine zeigt die von der „Webcam“ gelieferten Farbdaten, das zweite zeigt die Abstände der einzelnen Elemente.
Die in der Werbung gezeigte skelettale Analyse erfolgt ausschließlich am Rechner. Microsoft nutzt dazu eine Datenbank mit Informationen über den menschlichen Körperbau – dass diese nicht an Unix-Entwickler herausgegeben wird, folgt aus der Logik.


Hardware nach Maß

Derzeit sind drei verschiedene Sensortypen erhältlich. Für den Einsatz in Applikationen bietet Microsoft den Kinect for Windows an. Er ist der teuerste der drei, bietet aber als einziger den für den sitzenden Einsatz sehr wichtigen Near Mode an.In ihm ist die Detektionsreichweite des Sensors wesentlich reduziert: Statt Objekte erst ab 80 cm Entfernung wahrzunehmen, versucht ein im Near Mode befindlicher Sensor, auch im Bereich von 40 bis 80 cm nach Tiefeninformationen zu suchen.
Leider ist die Genauigkeit dabei beschränkt: Microsoft spricht von „best effort“, die Hardware ist mit der der Xbox-Version identisch und wird lediglich im Rahmen der Fertigung selektiert.Für die Verwendung mit der Xbox gibt es zwei weitere Variationen. Microsoft erlaubt nicht, diese im Zusammenspiel mit dem SDK in Produktivapplikationen einzusetzen – für Entwicklungszwecke ist die Verwendung indes gestattet. Neben dem fehlenden Near Mode gibt es beim in den diversen Konsolen-Bundles enthaltenen Sensor das Problem der Stromversorgung und der Kommunikation. Der für die Xbox 360S vorgesehene Sensor kommuniziert über einen speziellen Port auf der Rückseite der Konsole, der auch Energie liefert. Für die Nutzung am PC ist ein spezielles Netzteil samt Adapterkabel erforderlich – dieses ist sowohl von Microsoft als auch von Drittanbietern erhältlich.
Besitzer einer klassischen Xbox 360 können ihre Konsole mit einem speziellen Sensor erweitern. Dieser kommuniziert per USB und bringt ein Netzteil mit – wenn Sie ein solches Paket erwerben, können Sie sofort losentwickeln.

Kommerzielle Überlegungen

Der Sensor war anfangs als reines Marketingwerkzeug für die Xbox 360 vorgesehen – im Rahmen der Einführung betonte Microsoft mehrmals, auf Reverse-Engineering-Versuche mit Klagen zu reagieren. Erst nach dem enormen öffentlichen Interesse kam man in Redmond auf die Idee, dass das Produkt auch im kommerziellen Einsatz brauchbar ist.
Leider entstand dadurch ein neues Problem für das Unternehmen. Spielkonsolen und die zu ihnen gehörende Hardware werden oft mit Verlust verkauft. Das eigentliche Einkommen entsteht durch den Verkauf von Spielen.Bei einem an Geschäftskunden verkauften Sensor fällt diese Möglichkeit der Querfinanzierung flach. Aus diesem Grund kostet der Kinect für Windows im Handel rund 50 Euro mehr – Microsoft-Mitarbeiter geben unter der Hand zu, dass es sich dabei um eine Art „Entschädigung“ für den Verdienst-Entgang handelt.

Bibliothekarisches

Linux-User dürfen den an ihren Computer angeschlossenen Sensor auf drei verschiedene Arten ansprechen. Die in aktuellen Kerneln enthaltene kspca_kinect erkennt angeschlossene Hardware automatisch und erlaubt die Nutzung der mit VGA-Auflösung arbeitenden Kamera als „Webcam“.Die Freenect-Bibliothek ermöglicht den Zugriff auf Tiefen- und Farbdaten. Das API ist vergleichsweise einfach; leider gibt es zum Zeitpunkt der Drucklegung zwei verschiedene und miteinander völlig inkompatible Versionen mit weiter Verbreitung.
Zu guter Letzt gibt es noch das OpenNI-Framework. Sein API ist extrem komplex, es ist allerdings der einzige Weg, unter Linux an Skelettaldaten zu kommen. Auch hier gibt es zwei verschiedene Versionen, die zueinander völlig inkompatibel sind. Außerdem ist die Installation ebenfalls alles andere als einfach.

Kinect hoch!

Der erste Schritt zum funktionierenden Sensor ist das Installieren einer aktuellen Version der libfreenect. In den folgenden Schritten arbeiten wir mit Ubuntu, die Nutzer anderer Distributionen müssen statt apt-get die jeweilige Paketverwaltung ihres Systems verwenden.
Da die meisten Distributionen nach wie vor keine aktuelle Bibliothek mitliefern (und auch in ihren Repositories nur veraltete Dateien anbieten), folgt die Ausführung der folgenden Befehle in einem Terminal:

Im nächsten Schritt muss der aktive User zur Plugdev-Gruppe hinzugefügt werden:

tamhan@ubuntu:~$ sudo adduser $USER plugdev
The user `tamhan' is already a member of `plugdev'.

Das Verhalten des Plugdev-Servers wird durch die Datei /etc/udev/rules.d/66-kinect.rules gesteuert. Sie lässt sich mit jedem beliebigen Texteditor editieren, der mit Root-Rechten gestartet wurde – falls die Datei noch nicht vorhanden ist, so erstellen Sie sie einfach neu. Der Inhalt sieht so aus:

#Rules for Kinect #################################################### 
SYSFS{idVendor}=="045e", SYSFS{idProduct}=="02ae", MODE="0660",GROUP="video" 
SYSFS{idVendor}=="045e", SYSFS{idProduct}=="02ad", MODE="0660",GROUP="video" 
SYSFS{idVendor}=="045e", SYSFS{idProduct}=="02b0", MODE="0660",GROUP="video" 
### END #############################################################

Zu guter Letzt müssen Sie Ihren User nur noch zur Videogruppe hinzufügen, um die Beispiele nicht zwangsweise als Superuser ausführen zu müssen:

tamhan@ubuntu:~$ sudo usermod -a -G video tamhan

Nach einem Neustart der Workstation verbinden Sie den Sensor mit der Stromversorgung und danach mit einem freien USB-Port. Führen Sie danach den Befehl freenect-glview aus – er wird mit einer Fehlermeldung scheitern, aber das Finden des Kinect-Sensors anzeigen:

tamhan@tamhan-X360:~$ freenect-glview
Kinect camera test
Number of devices found: 1
Could not claim interface on camera: -6
Could not open device

 

Libgspca im Sommerurlaub

Wenn das Beispiel ein Gerät findet, ist alles in bester Ordnung – der Start schlägt fehl, weil das weiter oben erwähnte Kernel-Modul den Sensor bereits in Beschlag genommen hat. Erfreulicherweise ist es kein Problem, das Modul in den Sommerurlaub zu schicken:

tamhan@tamhan-X360:~$ sudo modprobe -r gspca_kinect

Nach dem Abschluss dieses Kommandos haben Sie bis zum nächsten Neustart der Maschine freie Bahn. Starten Sie beispielsweise freenect-glview, um sich vom einwandfreien Funktionieren Ihres Sensors zu überzeugen.

Threading mit K

Freenect lässt sich mit jedem beliebigen Framework nutzen. Für die folgenden Schritte setzen wir auf Qt – der Stack ist weit verbreitet und den meisten Informatikern bekannt, die Entwicklungsumgebung ist sehr leistungsfähig, die resultierenden Applikationen sehen brauchbar aus. Aus Platzgründen wird in diesem Artikel nicht näher auf Qt eingegangen; Interessierte finden weitere Informationen im Internet oder im Buch des Autors zum Thema.
Nach dem Erstellen des Projektskeletts müssen Sie die .pro-Datei um die folgenden Passagen erweitern. Sie weisen den Compiler an, die Freenect-Bibliothek zu linken und immer im x86-Modus zu kompilieren – libfreenect arbeitet im x64-Modus nur sehr unzuverlässig:

CONFIG += i386
DEFINES += USE_FREENECT
LIBS += -lfreenect

Alle auf libfreenect zurückgreifenden Dateien benötigen zusätzlich die folgende Inklusion. Diese Header-Datei enthält neben den Deklarationen auch Dokumentation – es lohnt sich also immer, einen Blick zu riskieren:

#include <libfreenect.h>

Die Bibliothek ist nicht in der Lage, sich selbst mit Rechenleistung zu versorgen. Die meisten Implementierungen lösen dieses Problem durch einen Worker Thread – sein Header sieht in unserem Beispiel so aus:

class QFreenectThread : public QThread
{
  Q_OBJECT
public:
  explicit QFreenectThread(QObject *parent = 0);
  void run();
public:
  bool myActive;
  freenect_context *myContext;
};
Der eigentliche Code ist primitiv, wie Listing 1 zeigt:

QFreenectThread::QFreenectThread(QObject *parent) :
 QThread(parent)
{
} 
void QFreenectThread::run() 
{
 while(myActive) 
 { 
   if(freenect_process_events(myContext) < 0) 
   { qDebug("Cannot process events!"); 
     QApplication::exit(1); 
   } 
 } 
}

Im Konstruktor der Klasse reichen wir den Parent an den Konstruktor von QThread weiter, um die Initialisierung des Threads zu ermöglichen. Die run()-Methode wird nebenläufig ausgeführt – sie ruft die Methode freenect_process_events auf, um die Verarbeitung der eingehenden Daten zu ermöglichen. Um das Beenden des Threads zu erleichtern, prüfen wir in der Schleife den Wert von myActive. Sobald diese auf false steht, beendet sich der Thread selbst.

[ header = Initialisierung, Datenfluß und Datennutzung ]

Initialisiere mich

Nach dem Fertigstellen des Worker Threads müssen wir uns mit dem eigentlichen Aufbau der Arbeitsumgebung befassen. Aus Gründen der Bequemlichkeit binden wir die Kinect-Engine in das Signal-Slot-System ein. Der Konstruktor der Klasse sieht wie in Listing 2 aus:

QFreenect::QFreenect(QObject *parent) :
  QObject(parent)
{
  myMutex=NULL;
  myRGBBuffer=NULL;
  myMutex=new QMutex();
  myWantDataFlag=false;
  myFlagFrameTaken=true;
  mySelf=this;

  if (freenect_init(&myContext, NULL) < 0)
  {
    qDebug("init failed");
    QApplication::exit(1);
  }

Im ersten Schritt setzen wir einige Flags und andere Werte auf definierte Werte. Danach folgt ein Aufruf von freenect_init – er hat die Aufgabe, ein Context-Objekt zu erstellen. Dieses dient als „Zugriffspunkt“ in die Bibliothek und wird während der gesamten weiteren Programmausführung an die Bibliotheksmethoden übergeben.

Listing 3:

  freenect_set_log_level(myContext, FREENECT_LOG_FATAL);
  int nr_devices = freenect_num_devices (myContext);
  if (nr_devices < 1)
  {
    freenect_shutdown(myContext);
    qDebug("No Kinect found!");
    QApplication::exit(1);
  }
  if (freenect_open_device(myContext, &myDevice, 0) < 0)
  {
    qDebug("Open Device Failed!");
    freenect_shutdown(myContext);
    QApplication::exit(1);
  }

Die Methode setLogLevel legt fest, wie „kommunikativ“ die Bibliothek arbeiten soll – je höher die Einstellung, desto mehr Statusmeldungen landen während der Programmausführung in der Debugger-Konsole.

Der darauf folgende Code sucht einen Sensor und erstellt danach ein Device-Objekt. Dieses dient als Verweis auf den vom Programm zu verwendenden Sensor – libfreenect erlaubt das gleichzeitige Verwenden von mehreren an den Computer angeschlossenen Sensoren:

myRGBBuffer = (uint8_t*)malloc(640*480*3);
freenect_set_video_callback(myDevice, videoCallback);
freenect_set_video_buffer(myDevice, myRGBBuffer);
freenect_frame_mode vFrame = freenect_find_video_mode(FREENECT_RESOLUTION_MEDIUM,FREENECT_VIDEO_RGB);
freenect_set_video_mode(myDevice,vFrame);
freenect_start_video(myDevice);

Die Macher der libfreenect planen, die Bibliothek längerfristig auch für andere Tiefen- und Farbsensorkombinationsgeräte anzubieten. Aus diesem Grund wurde im „neuen“ API das klassische Anfordern eines Farbstroms durch ein etwas komplexeres Verfahren ersetzt. Statt wie bisher direkt die Auflösung des gewünschten Stroms anzugeben, spezifiziert Ihr Programm nun „Wünsche“.

Nach dem Aufruf der find-Methode liefert libfreenect ein Objekt, das Informationen über den mit der vorliegenden Hardware realisierbaren Datenstrom enthält. So Ihnen dieses zusagt, aktivieren Sie es durch Aufruf von set_video_mode – start_video beginnt die eigentliche Datenanlieferung.

myDepthBuffer= (uint16_t*)malloc(640*480*2);
freenect_set_depth_callback(myDevice, depthCallback);
freenect_set_depth_buffer(myDevice, myDepthBuffer);
freenect_frame_mode aFrame = freenect_find_depth_mode(FREENECT_RESOLUTION_MEDIUM,FREENECT_DEPTH_REGISTERED);
freenect_set_depth_mode(myDevice,aFrame);
freenect_start_depth(myDevice);

Der Code für den Tiefenstrom unterscheidet sich nur minimal vom soeben besprochenen. Erstens ist der Datenpuffer kleiner, da die Tiefendaten nur 16 Bit pro Pixel ausmachen. Zweitens übergeben wir als Parameter FREENECT_DEPTH_REGISTERED, was das Programm zum Korrelieren der Tiefen- und der Bilddaten animiert.

Microsoft platziert den Tiefen- und den Bildsensor nicht direkt übereinander. Aus diesem Grund entsteht ein „Versatz“ zwischen Farb- und Tiefeninformationen, die allerdings durch softwareseitige Korrelation korrigierbar sind. Im hier verwendeten Betriebsmodus liegen zueinander gehörende Farb- und Tiefendaten an derselben Position im Array.
Zu guter Letzt erstellt unser Programm den vorher besprochenen Worker Thread und setzt einige weitere Flags:

myWorker=new QFreenectThread(this);
myWorker->myActive=true;
myWorker->myContext=myContext;
myWorker->start();
}

Datenfluß im Blick

Nachdem der Worker Thread die Bibliothek mit Rechenzeit versorgt, beginnt diese mit der Bearbeitung der per USB angelieferten Datenpakete. Fertige Frames werden an die Callbacks übergeben – da libfreenect eine C++-Bibliothek ist, definieren wir die beiden nach dem in Listing 4 abgebildeten Schema.

QFreenect* QFreenect::mySelf;
static inline void videoCallback(freenect_device *myDevice, void *myVideo, uint32_t myTimestamp=0)
{
  QFreenect::mySelf->processVideo(myVideo, myTimestamp);
}
static inline void depthCallback(freenect_device *myDevice, void *myVideo, uint32_t myTimestamp=0)
{
  QFreenect::mySelf->processDepth(myVideo, myTimestamp);
}

Die beiden Funktionen haben die Aufgabe, die Methoden processVideo und processDepth aufzurufen. Diese sind beide als Teile des Engine-Objekts realisiert – der „globale Pointer“ erlaubt uns den schnellen und unbürokratischen Zugriff auf die Objektinstanz (unter Umständen ist die Realisierung eines Singletons empfehlenswerter).
Beim Verarbeiten der eingehenden Daten bekommen wir zwei Probleme: erstens eine Race Condition, zweitens ein potenziell höchstgefährliches Speicherleck. Beide lassen sich am einfachsten am Beispiel von processVideo illustrieren – die Funktion verarbeitet die vom Sensor angelieferten Farbdatenframes.
libfreenect kommuniziert mit unserem Modul über einen geteilten Speicherbereich. Die Kommunikation zwischen Applikation und Engine erfolgt indes über ein Signal, das als Parameter einen Pointer auf einen mit malloc() allozierten Stackframe enthält. Dieser geht in das Eigentum des Empfängers über und wird von diesem abgetragen.
Leider hat die Routine keine Möglichkeit, Informationen über den Empfänger zu erhalten. Ist das Signal mit keinem Slot verbunden, so geht der Zeiger verloren – das Resultat ist ein Speicherleck, das aufgrund der rasenden Geschwindigkeit (30 Frames/Sekunde) selbst Hochleistungsworkstations binnen Sekunden in die Knie zwingt. Nicht weniger lästig ist es, wenn das Signal-Slot-System einen Stapel von nicht verarbeiteten Frames verwalten muss.
Die Lösung dafür sind die beiden Flags. Die eine zeigt an, dass ein Empfänger verfügbar ist. Die andere zeigt an, dass der letzte Frame bereits verarbeitet wurde – nur wenn beide korrekt gesetzt sind, gibt die Engine den nächsten Speicherbereich frei.
Das zweite Problem – die Nebenläufigkeit – wird über den QMutexLocker gelöst. Er sperrt den Zugriff in die kritische Sektion, während der Applikationscode die Daten verarbeitet (Listing 5).

void QFreenect::processVideo(void *myVideo, uint32_t myTimestamp)
{
  QMutexLocker locker(myMutex);
  if(myWantDataFlag && myFlagFrameTaken)
  {
    uint8_t* mySecondBuffer=(uint8_t*)malloc(640*480*3);
    memcpy(mySecondBuffer,myVideo,640*480*3);
    myFlagFrameTaken=false;
    emit videoDataReady(mySecondBuffer);
  }
}

processDepth folgt 1:1 dem gleichen Schema – die Unterschiede liegen lediglich in den anderen Parametern und dem Abfeuern des Tiefendatensignals (Listing 6).

void QFreenect::processDepth(void *myDepth, uint32_t myTimestamp)
{
  QMutexLocker locker(myMutex);
  if(myWantDataFlag && myFlagDFrameTaken)
  {
    uint16_t* mySecondBuffer=(uint16_t*)malloc(640*480*2);
    memcpy(mySecondBuffer,myDepth,640*480*2);
    myFlagDFrameTaken=false;
    emit depthDataReady(mySecondBuffer);
  }
}

Datennutzung

Für versierte Qt-Programmierer stellt die Nutzung unserer Engine kein allzu großes Problem dar: Instanz erstellen, Signale mit Slots im Anwendungsprogramm verbinden, losrechnen. Dabei sind allerdings einige Besonderheiten der Daten zu beachten.
In der vorliegenden Version liefert der Kinect sowohl Farb- als auch Tiefendaten mit einer Auflösung von 640 x 480 Pixeln an; die Aktualisierung erfolgt bei ausreichender USB-Bandbreite 30 Mal pro Sekunde.
Bei den Farbdaten setzt die libfreenect auf das klassische 24-Bit-Format. Der zurückgegebene Frame besteht aus einer Abfolge von achtbittigen Werten, die die einzelnen Farbkomponenten des Bilds beschreiben. Zur Ermittlung des genauen Speicherbereichs verwenden Sie die folgenden Formeln:

r=(myRGBBuffer[3*(x+y*640)+0]);
g=(myRGBBuffer[3*(x+y*640)+1]);
b=(myRGBBuffer[3*(x+y*640)+2]);

Angemerkt sei, dass die Ermittlung der Koordinaten in einer engen Schleife viel Rechenleistung erfordert. Es ist in höchstem Maße sinnvoll, das Resultat der Multiplikation zwischenzuspeichern – in Tests des Autors bringt diese primitive Optimierung eine enorme Performancesteigerung.

Der Tiefenframe besteht aus 16 Bit langen Wörtern, die den Abstand des jeweiligen Objekts in Millimetern angeben. Wichtig ist die Konstante FREENECT_DEPTH_MM_NO_VALUE. Sie drückt aus, dass der Sensor zu diesem Pixel keinerlei Tiefeninformationen besitzt.
Zu guter Letzt sollten Sie nicht vergessen, die angelieferten Speicherbereiche durch den Aufruf von free() abzutragen und die jeweiligen FrameTaken-Flaggen auf true zu setzen.

Fazit

Nach diesen Betrachtungen sind wir vorerst am Ziel angekommen – unsere Engine nutzt sowohl den Farb- als auch den Tiefenstrom des Sensors aus. Damit eröffnet sich eine Vielzahl faszinierender Möglichkeiten – die Bandbreite reicht vom Erstellen von Tiefenhistogrammen bis zum Kreieren eines 3-D-Scanners.
Skelettales Tracking wird durch die in der Einleitung erwähnte OpenNI-Bibliothek ermöglicht. Ihre Verwendung besprechen wir im Detail im nächsten Heft. Ungeduldige dürfen auch auf das Buch des Autors zurückgreifen – das hier verwendete Codebeispiel ist ebenfalls aus diesem Werk entnommen.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -