Bewegungsdrang (Teil 3): Über die Kinect im Bilde

6. August 2012

Ein verregneter Sonntag und im Fernsehen laufen zum x-ten mal Filme die ich schon zu Jugendzeiten in die Kategorie alte Schinken eingeordnet habe. Was also spricht dagegen einen eigenen kleinen Film zu machen? Schließlich steht da ein neues Spielzeug auf dem Tisch, welches mich im Stile von "Nummer 5 lebt!" mitleidig anschaut und darauf wartet in Betrieb genommen zu werden.

Gesagt, getan! Und so gibt ein kleines grünes Licht schließlich den Startschuss für den nächsten Flurfunk-Artikel, der sich wie in meinem letzten Beitrag "Hello (Kinect) World!" angekündigt ganz und gar um die bewegten Bilder dreht. Selbiges scheint mir wesentlich spannender als der Film der im Hintergrund läuft und dessen Dialoge mein Unterbewusstsein wahrscheinlich schon auswendig kennt. Ich hoffe Sie verspüren ähnlichen Bewegungsdrang!

Stromaufwärts

Als ich Ihnen vor ein paar Wochen den Kinect Controller vorgestellt hatte erwähnte ich schon, dass sowohl RGB-Kamera als auch der Tiefensensor mit 30fps arbeiten. Die Frage aber ist, wie gelangen diese Daten bzw. Bilder nun in die eigene Anwendung? Und wie Sie sich sicher vorstellen können ist die Antwort denkbar einfach und lautet Datenströme oder zu neudeutsch Streams.

Die NUI Library (Natural User Interface) ist an dieser Stelle vereinfacht gesagt nichts anderes als die Runtime der Kinect. Sie umfasst somit alles was für die Nutzung notwendig ist, angefangen vom Treiber, über den Kernel, bis hin zu Bibliotheken die in Visual Studio referenziert werden können. Anschaulich wird das Ganze in dem folgenden SDK Architektur Überblick:

Wie wir sehen sind die Daten also bereits durch einige Schichten "geströmt" bevor Sie in einer Anwendung über das NUI API das Licht der Welt erblicken. Selbiges möchte ich Ihnen in den nächsten Abschnitten für die RGB-Kamera veranschaulichen.

Lauter bunte Bilder

Schluss mit der Theorie, rein in die Praxis! Nachdem wir das SDK ja bereits installiert haben, starten wir Visual Studio und erstellen uns einfach eine neue WPF Applikation, dessen Name ich einfach mal Ihrer eigenen Kreativität überlasse. Ist dies vollbracht brauchen wir als erstes Zugriff auf das NUI API und das lösen wir durch einfaches einbinden der Kinect Library, die im Verzeichnis “C:Program FilesMicrosoft SDKsKinectv1.5Assemblies” zu finden ist.

Mit dem hochgesteckten Ziel ein bewegtes Bild zu sehen und mit dem Wissen dass die Kinect uns Frames durch Streams zur Verfügung stellt, platzieren wir in unserem WPF Hauptfenster wohlweislich schon mal ein Image Control. Ihr XAML sollte dann in etwa so aussehen:

   1: <Window x:Class="RGBCameraFundamentals.MainWindow"

   2:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   3:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   4:         Title="MainWindow" Height="480" Width="640" MinWidth="640" 

   5:         MinHeight="480" Loaded="Window_Loaded" Closed="Window_Closed">

   6:     <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">

   7:         <Image Name="ImageRGB" Width="640" Height="480"/>

   8:     </StackPanel>

   9: </Window>

Bevor jedoch unser Anlitz das soeben erstellte Control ziert, gilt es erst noch die ein oder andere Zeile Code zu implementieren. In unserem Fall wäre es also durchaus sinnvoll sich an das Window_Loaded bzw. Window_Closed Event zu hängen um die Frame Capture Engine der Kinect parallel zur Anwendung zu starten bzw. zu beenden.

   1: using System;

   2: using System.Linq;

   3: using System.Windows;

   4: using Microsoft.Kinect;

   5: using System.IO;

   6: using System.Windows.Media.Imaging;

   7: using System.Windows.Media;

   8:  

   9: namespace RGBCameraFundamentals

  10: {

  11:     /// <summary>

  12:     /// Interaction logic for MainWindow.xaml

  13:     /// </summary>

  14:     public partial class MainWindow : Window

  15:     {

  16:         /// <summary>

  17:         /// Kinect Sensor

  18:         /// </summary>

  19:         public KinectSensor Kinect { get; set; }

  20:  

  21:         public MainWindow()

  22:         {

  23:             InitializeComponent();

  24:         }

  25:  

  26:         private void Window_Loaded(object sender, RoutedEventArgs e)

  27:         {

  28:             // Generell ist es möglich mehr als einen Kinect Sensor 

  29:             // an einem Rechner zu betreiben. Für unsere Zwecke reicht 

  30:             // es deshalb aus den ersten verfügbaren Sensor mit Status 

  31:             // Connected zu ermitteln.

  32:             this.Kinect = KinectSensor.KinectSensors

  33:                 .FirstOrDefault(ks => ks.Status 

  34:                     == KinectStatus.Connected);

  35:  

  36:             if (this.Kinect != null)

  37:             {

  38:                 try

  39:                 {

  40:                     // Starten des Sensors

  41:                     this.Kinect.Start();

  42:                 }

  43:                 catch (IOException)

  44:                 {

  45:                     this.Kinect = null;

  46:                 }

  47:             }

  48:         }

  49:  

  50:         private void Window_Closed(object sender, EventArgs e)

  51:         {

  52:             if (this.Kinect != null)

  53:             {

  54:                 // Stoppen des Sensors

  55:                 this.Kinect.Stop();

  56:             }

  57:         }

  58:     }

  59: }

So weit, so gut, zwar haben wir die Kinect jetzt eingebunden, aber unserem Ziel sind wir nicht sonderlich viel näher gekommen. Zeit für ein bisschen Initialisierung:

   1: private void Window_Loaded(object sender, RoutedEventArgs e)

   2: {

   3:     ....

   4:  

   5:             try

   6:             {

   7:                 // Initialisieren der ColorStreams

   8:                 InitializeColorStream();

   9:  

  10:                 // Starten des Sensors

  11:                 this.Kinect.Start();

  12:             }

  13:             catch (IOException)

  14:             {

  15:                 this.Kinect = null;

  16:             }

  17:  

  18:     ....

  19: }

  20:  

  21: private void InitializeColorStream()

  22: {

  23:     // Der jeweilige Stream muß für die Verarbeitung enabled werden. 

  24:     // Wahlweise können auch die Formate RgbResolution1280x960Fps12, 

  25:     // RawYuvResolution640x480Fps15 und YuvResolution640x480Fps15 

  26:     // verarbeitet werden

  27:     this.Kinect.ColorStream.Enable

  28:         (ColorImageFormat.RgbResolution640x480Fps30);

  29:  

  30:     // Zwar könnte man auch eine normale Bitmap nehmen aber die 

  31:     // WriteableBitmap eignet sich durch die Benutzun von internen 

  32:     // Puffern besser zum rendern von Frames. Da wir einen RGB Stream

  33:     // benutzen ist das PixelFormat Bgr32 was einem Standard RGB Format 

  34:     // mit 32 Bits pro Pixel entspricht. Falls Sie eine eigene Palette 

  35:     // benutzen können Sie desweiteren den null Parameter ersetzen, in 

  36:     // der Regel ist dies aber nicht nötig. Und um es nicht zu vergessen 

  37:     // Windows samt WPF nutzen per Default 96dpi, so gesehen sollten 

  38:     // Ihnen die zwei bisher nicht genannten Parameter durchaus klar 

  39:     // sein.

  40:     this.ColorBitmap = new WriteableBitmap(

  41:         this.Kinect.ColorStream.FrameWidth, 

  42:         this.Kinect.ColorStream.FrameHeight,

  43:         96.0, 96.0, PixelFormats.Bgr32, null);

  44:  

  45:     // Die Source des Images in der Anwendung ist natürlich die vorher 

  46:     // definierte WriteableBitmap

  47:     this.ImageRGB.Source = this.ColorBitmap;

  48: }

Die Frage die sich jetzt noch stellt ist die, wie den nun der Zugriff auf die von der Kinect erstellten Frames aus dem ColorStream realisiert wird? Hierfür gibt es generell zwei Möglichkeiten. Einerseits lassen sich die Daten abrufen (Poll-Model) und andererseits können Sie die FrameReady-Events nutzen die seitens der Frame Capture Engine gefeuert werden (Event-Model).

Ich für meinen Teil bevorzuge letztere Variante, denn warum soll ich mich in diesem Kontext mit Threads, Timern oder gar Endlosschleifen herumschlagen wenn ich mich mit einfachsten Mitteln an einen bereits vorhandenen Eventhandler hängen kann? Sollten Sie das anders sehen oder gar zur seltenen Spezies der XNA Entwickler gehören (die Events pollen müssen), dann dürfen Sie gerne einen Blick auf die Methode OpenNextFrame der Klasse ColorImageStream werfen.

   1: private void InitializeColorStream()

   2: {

   3:     ....

   4:  

   5:     // Last-but-not-least sorgen wir dafür dass die FrameReady Events 

   6:     // in der Methode Kinect_ColorFrameReady verarbeitet werden können

   7:     this.Kinect.ColorFrameReady += this.Kinect_ColorFrameReady;

   8: }

   9:  

  10: private void Kinect_ColorFrameReady(object sender, 

  11:     ColorImageFrameReadyEventArgs e)

  12: {

  13:     // Der ColorImageFrame stellt den Container für die Daten des Sensors

  14:     using (ColorImageFrame frame = e.OpenColorImageFrame())

  15:     {

  16:         if (frame != null)

  17:         {

  18:             // Ersten des für die Erstellung des Image nötigen Byte Arrays. 

  19:             // Die Größe des Arrays von 1228800 Byte errechnet sich durch 

  20:             // 640x480x4 (letzteres siehe BGR32)

  21:             byte[] pixelData = new byte[frame.PixelDataLength];

  22:  

  23:             // Übername der Daten aus dem Frame

  24:             frame.CopyPixelDataTo(pixelData);

  25:  

  26:             // Die WriteableBitmap benötigt beim rendern die Angabe welcher 

  27:             // Bereich aktualisiert werden soll. Hier benutzen wir die gesamte 

  28:             //Fläche der Bitmap

  29:             Int32Rect colorBitmapRect = 

  30:                 new Int32Rect(0, 0, this.Kinect.ColorStream.FrameWidth, 

  31:                 this.Kinect.ColorStream.FrameHeight);

  32:  

  33:             // Die Angabe wieviele Pixel jeweils auf ein mal upgedated werden 

  34:             // sollen. Mit der vorliegenden Definition geschieht dies also 

  35:             //Zeilenweise

  36:             int colorBitmapStride = this.Kinect.ColorStream.FrameWidth * 

  37:                 this.Kinect.ColorStream.FrameBytesPerPixel;

  38:  

  39:             // Erstellen der neuen Bitmap

  40:             this.ColorBitmap.WritePixels(colorBitmapRect, pixelData, colorBitmapStride, 0);

  41:         }

  42:     }

  43: }

So, nun ist der große Moment gekommen in dem Sie Ihrer ersten Kinect Applikation eine Chance geben dürfen. Starten Sie die Anwendung und empfinden Sie Freude beim Anblick Ihres Gesichts.

Das kleine grüne Männchen in mir

Bevor ich das Projekt RGB Kamera vorerst schließe, will ich noch einen kurzen und einfachen Exkurs Richtung Imagemanipulation machen. Sicherlich können Sie mit GDI+ die Images vor der Anzeige manipulieren. Worauf ich aber in meinem Beispiel hinaus will, ist die einfache Manipulation des Farbschemas durch Anpassung der Frames bzw. des damit verbundenen Byte-Arrays.

Passen wir unsere Methode Kinect_ColorFrameReady doch einfach mal wie folgt an:

   1: private void Kinect_ColorFrameReady(

   2:     object sender, ColorImageFrameReadyEventArgs e)

   3: {

   4:             ....

   5:  

   6:             // Übername der Daten aus dem Frame

   7:             frame.CopyPixelDataTo(pixelData);

   8:  

   9:             // Wie BGR schon sagt sind die vier Byte pro Pixel Blau, Grün 

  10:             // und Rot gefolgt von einer null die in anderen Formaten wie

  11:             // Bgra32, Pbgra32 den Alpha Wert darstellt. In unserem Fall 

  12:             // reicht es also die Blau und Rot Werte auf 0 zu setzen

  13:             for (int i = 0; i < pixelData.Length; i += frame.BytesPerPixel)

  14:             {

  15:                 pixelData[i] = 0x00;

  16:                 pixelData[i + 2] = 0x00;

  17:             }

  18:  

  19:             ....

  20: }

Wenn man die Anwendung nun startet sieht man sich, sofern man das so ausdrücken kann, plötzlich in einem ganz neuen Licht.

Der Vollständigkeit halber finden sie den kompletten Source-Code hier.

Ausblick

Der aufmerksame Leser mag gemerkt haben, dass ich ursprünglich angekündigt hatte auch auf den Tiefensensor einzugehen. Asche auf mein Haupt, aber letztlich sind die Themen einfach zu umfangreich (und zu spannend). Somit vertröste ich Sie an dieser Stelle auf den nächsten Artikel in dem ich der Entwicklung den nötigen Tiefgang verpasse. Ich würde mich freuen Sie auch dann wieder die Lust auf ein wenig Bewegungsdrang verspüren.