Bewegungsdrang (Teil 4): Die tiefen Weiten des Kinect Universums

10. September 2012

Die Leser unter Ihnen, die, so wie ich, als Kind der 80er aufgewachsen sind, werden sich sicherlich erinnern. Damals rannte ein blaues pelziges Etwas namens Grobi hektisch durch das Programm der Sesamstraße und versuchte uns die Begriffe "Nah & Fern" zu erklären. Heute, also gute 30 Jahre später, habe ich das Vergnügen diese Begriffe noch mal neu für mich zu entdecken. Und wie damals sitze ich wieder gebannt vor dem Bildschirm und erfreue mich der Dinge. Kinect sei Dank!

Sie können sich sicherlich denken worum es geht. Während wir uns im letzten Artikel Über die Kinect im Bilde noch mit Oberflächlichkeiten begnügen mussten, gehen wir heute einen Schritt weiter. Wir verpassen einer Anwendung den nötigen Tiefgang indem wir uns dieses Mal den Stream des Tiefensensors zu nutze machen.

Eine Sache des Blickwinkels

Im ersten Teil dieser Serie hatte ich geschrieben, dass der maßgebliche Unterschied zwischen der Kinect für Windows verglichen mit der Xbox 360 Variante im Tiefensensor liegt. Heute möchte ich dies ein wenig besser spezifizieren, denn Microsoft hat hier nicht – wie man glauben könnte – einen verbesserten Sensor verbaut. Nein, stattdessen wurde eine neue Firmware entwickelt die besser mit den Lichtverhältnissen bezogen auf nahe Objekte umgehen kann.

Dies hat zur Folge dass die Kinect je nach Aufgabe im Default Mode und im Near Mode betrieben werden kann. Auf das Blickfeld (field of view), oder präziser gesagt auf die Distanz in der Objekte erkannt werden, hat das folgenden Einfluss:

Quelle: Kinect for Windows Blog, Near Mode – What t is (and isn’t)

Wie man also sieht gibt es, trotz der räumlichen Einschränkungen die durch die Kinect gegeben sind, genug Freiraum um ein wenig Kreativität in Sachen Interaktion ausleben zu können.

Alles grau in grau

So, nach all den einleitenden Worten raus aus der grauen Theorie und rein in die graue Praxis. Wir wollen wie auch im letzten Teil dieser Blogserie eine einfache WPF Applikation erstellen in der unser Antlitz erstrahlt. Dieses mal wird selbiges allerdings nicht ganz so farbgewaltig sein wie zuletzt.

Nach dem erklärten Schema erstellen wir also zuerst eine WPF Applikation mit einem klangvollen Namen, platzieren ein Image Control, hängen uns an den Window_Loaded bzw. Window_Closed Event und fügen noch ein wenig Kinect Voodoo als letzte Zutat hinzu um zu folgendem Ergebnis zu kommen:

   1: <Window x:Class="DepthSensorFundamentals.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="540" Width="640" MinWidth="640" MinHeight="480" 

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

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

   7:         <Image Name="ImageDepth" Width="640" Height="480" MouseUp="ImageDepth_MouseUp" MouseMove="ImageDepth_MouseMove" />

   8:     </StackPanel>

   9: </Window>

   1: /// <summary>

   2: /// Interaction logic for MainWindow.xaml

   3: /// </summary>

   4: public partial class MainWindow : Window

   5: {

   6:     /// <summary>

   7:     /// Kinect Sensor

   8:     /// </summary>

   9:     public KinectSensor Kinect { get; set; }

  10:  

  11:     /// <summary>

  12:     /// Die Bitmap die in der Anwendung angezeigt wird

  13:     /// </summary>

  14:     public WriteableBitmap DepthBitmap { get; set; }

  15:  

  16:     /// <summary>

  17:     /// Pixel Array welches ein Image des Streams enthält

  18:     /// </summary>

  19:     public short[] PixelData { 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 an einem Rechner zu betreiben. Für

  29:         // unsere Zwecke reicht es deshalb aus den ersten verfügbaren Sensor mit Status Connected 

  30:         // zu ermitteln.

  31:         this.Kinect = KinectSensor.KinectSensors.FirstOrDefault(ks => ks.Status == KinectStatus.Connected);

  32:  

  33:         if (this.Kinect != null)

  34:         {

  35:             try

  36:             {

  37:                 // Initialisieren der DepthStreams

  38:                 InitializeDepthStream();

  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:     private void InitializeDepthStream()

  60:     {

  61:         // Fenstertitel dahingehend ändern das der aktuelle Modus angezeigt wird

  62:         this.Title = this.Kinect.DepthStream.Range.ToString();

  63:  

  64:         // Der jeweilige Stream muß für die Verarbeitung enabled werden. Wahlweise können auch

  65:         // die Formate Resolution320x240Fps30 sowie Resolution80x60Fps30 verarbeitet werden

  66:         // YuvResolution640x480Fps15 verarbeitet werden. Sie können Enable auch ohne ImageFormat

  67:         // aufrufen dann wird den das unten angegebene Format ist per defualt gesetzt.

  68:         this.Kinect.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);

  69:  

  70:         // Zwar könnte man auch eine normale Bitmap nehmen aber die WriteableBitmap eignet sich

  71:         // durch die Benutzun von internen Puffern besser zum rendern von Frames. Da wir den Depth

  72:         // Stream nutzen wir das PixelFormat Gray16. Dieses besagt dass 16 Pixel pro Tiefeninformation

  73:         // genutzt werden, was insgesamt 65536 Schattierungen von Grau bedeutet. Alternativ können 

  74:         // Sie natürlich auch andere Formate nutzen wie z.B: BGR32, allerdings sollten Sie vorher 

  75:         // darüber nachdenken wie sinnvoll das für Ihren Fall ist. Wenn Sie dies tun, dann sollten

  76:         // Sie allerdings daran denken der Pixel Array entsprechend zu dimensionieren. Bei BGR32 

  77:         // wäre dies z.B. byte[] pixelData = new byte[frame.PixelDataLength*sizeof(int)]  weil das

  78:         // BGR32 Format ja 32 Bit anstatt 16 Bit ist. Hinzu kommt dass man den Intensitätsgrad dann

  79:         // immer manuell für Rot, Grün und Blau setzen muss.

  80:         this.DepthBitmap = new WriteableBitmap(this.Kinect.DepthStream.FrameWidth, this.Kinect.DepthStream.FrameHeight,

  81:             96.0, 96.0, PixelFormats.Gray16, null);

  82:  

  83:         // Die Source des Images in der Anwendung ist natürlich die vorher definierte WriteableBitmap

  84:         this.ImageDepth.Source = this.DepthBitmap;

  85:  

  86:         // Last-but-not-least sorgen wir dafür dass die FrameReady Events in der Methode 

  87:         // Kinect_DepthFrameReady verarbeitet werden können

  88:         this.Kinect.DepthFrameReady += this.Kinect_DepthFrameReady;

  89:     }

  90:  

  91:     private void Kinect_DepthFrameReady(object sender, DepthImageFrameReadyEventArgs e)

  92:     {

  93:         // Der DepthImageFrame stellt den Container für die Daten des Sensors

  94:         using (DepthImageFrame frame = e.OpenDepthImageFrame())

  95:         {

  96:             if (frame != null)

  97:             {

  98:                 // Der Byte Array der vom Tiefensensor geliefert wird ist Array of short wobei

  99:                 // jeder Wert des Arrays zusammengesetzt ist aus Tiefeninformation und PlayerIndex

 100:                 PixelData = new short[frame.PixelDataLength];

 101:  

 102:                 // Übername der Daten aus dem Frame

 103:                 frame.CopyPixelDataTo(PixelData);

 104:  

 105:                 // Die WriteableBitmap benötigt beim rendern die Angabe welcher Bereich aktualisiert werden

 106:                 // soll. Hier benutzen wir die gesamte Fläche der Bitmap

 107:                 Int32Rect depthBitmapRect = new Int32Rect(0, 0, this.Kinect.DepthStream.FrameWidth, this.Kinect.DepthStream.FrameHeight);

 108:  

 109:                 // Die Angabe wieviele Pixel jeweils auf ein mal upgedated werden sollen. Mit der vorliegenden

 110:                 // Definition geschieht dies also Zeilenweise

 111:                 int depthBitmapStride = this.Kinect.DepthStream.FrameWidth * this.Kinect.DepthStream.FrameBytesPerPixel;

 112:  

 113:                 // Erstellen der neuen Bitmap

 114:                 this.DepthBitmap.WritePixels(depthBitmapRect, PixelData, depthBitmapStride, 0);

 115:             }

 116:         }

 117:     }

 118: }

So weit, so gut, was aber noch fehlt ist die Initialisierung sowie die Verarbeitung des Tiefensensor-Streams. Bevor ich jedoch dazu komme, möchte ich Ihnen noch den grundlegendsten Unterschied zum RGB Stream aufzeigen. Jedes DepthFrame wird nämlich durch ein short Array repräsentiert bei dem jedes Element eine einzelne Tiefeninformation enthält. Und da die 16 Bit mehr als genug Platz haben die Tiefe zu speichern, ist in jedem Element noch zusätzlich der sogenannte Playerindex zu finden (auf den ich ein anderes mal eingehe). Dies sieht dann in der Praxis wie folgt aus:

Mit diesen Informationen im Kopf und mit ein bisschen Kinect API Know-How können wir schließlich unsere Applikation vollenden. Und auch wenn wir damit sicherlich nicht den Olymp der Kinect Applikationen erklimmen werden, klar ist das wir mit recht einfachen Mitteln sehr schnell ans Ziel gelangen. Und weil dies der Fall ist fügen wir unserer Applikation noch ein Textfeld unterhalb des Image ein in der wir noch gerne den Abstand des Punktes in Millimetern zum Sensor ausgeben lassen wollen. Dazu fügen wir folgenden Code für den MouseMove-Event hinzu:

   1: private void ImageDepth_MouseMove(object sender, MouseEventArgs e)

   2: {

   3:     // Position innerhalb des Image ermitteln

   4:     Point point = e.GetPosition(ImageDepth);

   5:     

   6:     // Index des Pixels berechnen

   7:     int pixelIndex = (int)(point.X + ((int)point.Y * this.DepthBitmap.Width));

   8:  

   9:     // Tiefeninformation extrahieren (letzte drei Bits im short Wert sind ja der Player index

  10:     int depth = this.PixelData[pixelIndex] >> DepthImageFrame.PlayerIndexBitmaskWidth;

  11:  

  12:     // Player index extrahieren

  13:     int playerIndex = this.PixelData[pixelIndex] & DepthImageFrame.PlayerIndexBitmask;

  14:  

  15:     TextDepthInfo.Text = string.Format("Depth {0} mm - Player {1} ", depth, playerIndex);

  16: }

Aus der Nähe betrachtet

Haben Sie die Anwendung schon einmal gestartet? Nun … wenn Sie selbst stolzer Besitzer einer Kinect für Windows sind, dann könnte das Fenster das Ihren Bildschirm ziert in etwa so aussehen:

Da wir oben keine konkrete Initialisierung vorgenommen haben arbeitet die Kinect hier gerade im Default Mode. Stellt sich die Frage wie wir nun in den Near Mode wechseln und was für Auswirkungen das hat. Die Antwort ist denkbar einfach, aber bevor ich Sie Ihnen gebe sollten wir das WPF Fenster noch schnell erweitern. Der Einfachheit halber ergänzen wir die Anwendung einfach um ein Mouseup-Event in dem wir folgende Zeilen hinzufügen:

   1: private void ImageDepth_MouseUp(object sender, MouseButtonEventArgs e)

   2: {

   3:     if (this.Kinect != null)

   4:     {

   5:         // Zu beachten ist dass dieser Code eine InvalidOperationException werfen würde

   6:         // wenn er mit einer Xbox 360 Kinect ausgeführt werden würde 

   7:         if (this.Kinect.DepthStream.Range == DepthRange.Default)

   8:         {

   9:             this.Kinect.DepthStream.Range = DepthRange.Near;

  10:         }

  11:         else

  12:         {

  13:             this.Kinect.DepthStream.Range = DepthRange.Default;

  14:         }

  15:  

  16:         this.Title = this.Kinect.DepthStream.Range.ToString();

  17:     }

  18: }

Klicken wir nun zur Laufzeit auf das Image dann switchen wir zwischen Default- und Near-Mode hin und her. Optisch lässt sich die Veränderung hier sehr gut erkennen:.

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

Ausblick

Aus dem heutigen Artikel haben Sie wahrscheinlich zwei Dinge mitgenommen. Einerseits wie man den Zugriff auf die Daten vom Tiefensensor realisiert und andererseits, dass die guten alten Lehrstunden der Sesamstraße immer noch gut genug sind den Artikel eines Blogs einzuleiten. Ich hoffe Ihnen hat der Artikel genauso viel Spaß gemacht wie mir und ich würde mich freuen wenn Sie beim nächsten Mal wieder ein wenig Bewegungsdrang verspüren. Dann wird Ihnen die Kinect unter die Haut gehen.