Teil 1: Erste Schritte mit Kinect

Kinectologie (Teil 3)
Kommentare

Tiefeninformationen herbei
Als nächstes Beispiel wollen wir uns zusätzlich zum Bildstream noch einen Strom mit Tiefeninformationen beschaffen. Dazu erstellen wir ein neues Projekt namens SuSKinect3,

Tiefeninformationen herbei

Als nächstes Beispiel wollen wir uns zusätzlich zum Bildstream noch einen Strom mit Tiefeninformationen beschaffen. Dazu erstellen wir ein neues Projekt namens SuSKinect3, das im Prinzip SuSKinect2 entspricht. Der wesentliche Unterschied liegt darin, dass wir anstatt des einzelnen Images nun insgesamt vier Images mit einer Größe von je 320 x 240 benötigen. Zusätzlich brauchen wir zwei vertikale Slider und eine Checkbox, wir wollen mit den Tiefendaten nämlich auch rechnen. Die Initialisierungsroutine ist diesmal ein wenig erweitert, da sie sowohl einen Videostream als auch einen Tiefenstream anfordert (Listing 7).

Listing 7

public MainWindow()
{
  myRuntime = Microsoft.Research.Kinect.Nui.Runtime.Kinects[0];
  myRuntime.Initialize(RuntimeOptions.UseColor | RuntimeOptions.UseDepth);
  myRuntime.DepthStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution320x240, ImageType.Depth);
  myRuntime.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color);
  myRuntime.DepthFrameReady += new EventHandler(myRuntime_DepthFrameReady);
  myRuntime.VideoFrameReady += new EventHandler(myRuntime_VideoFrameReady);
  InitializeComponent();
}  

Die Routine unterscheidet sich von ihren Vorgängern durch zwei wichtige Details: Erstens bekommt Initialize nun zwei „zusammen-geORte“ Flags übergeben, um sowohl Tiefendaten als auch Videodaten anzufordern. Logischerweise brauchen wir nun auch zwei Event Handler, die Video- und Tiefen-Frames müssen ja nicht zwangsläufig gleichzeitig eintreffen. Während der Handler für VideoFrameReady unverändert vom zweiten Beispiel übernommen wird, müssen wir den Aufbau der BitmapSource für den Tiefenframe ein wenig adjustieren:

bs = BitmapSource.Create(myImg.Width, myImg.Height, 96, 96, PixelFormats.Gray16, null, myImg.Bits, myImg.Width * myImg.BytesPerPixel);  

Fürs Erste wären wir damit fertig, das Resultat der Ausführung der Anwendung zeigt Abbildung 3.

Abb. 3: Im Vergleich sieht man den Tiefenstream
Abb. 3: Im Vergleich sieht man den Tiefenstream
Tiefenströme verstehen

Die Scanauflösung von Kinect ist beschränkt: Der Sensor arbeitet nur mit einer Auflösung von 13 Bit, die er – oh Freude – in die minderwertigen Bits des invertierten, 16-bittigen Datenworts schreibt. Aus diesem Grund sieht man beim direkten Verwenden nur Grau in Grau, denn die 16 Bit der Bitmap werden ja kaum ausgenutzt. Zur Kompensation müssten wir in zwei Schritten vorgehen: Nach dem Ermitteln des realen Werts müssten wir diesen skalieren, danach abermals splitten und ins RawImage zurückschreiben. Die Beispielimplementierung erledigt das mit einer zugegebenermaßen langen Routine, die zusätzlich den Wert der Checkbox abfragt, wie in Listing 8 zu sehen ist.

Listing 8

void myRuntime_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)
{
  PlanarImage myImg = e.ImageFrame.Image;
  BitmapSource bs;
  if (checkBox1.IsChecked==true)
  {
    int limit = myImg.Bits.Count() / myImg.BytesPerPixel;
    for (int i = 0; i < limit; i++)
     {
       byte lower = myImg.Bits[i * myImg.BytesPerPixel];
       byte higher = myImg.Bits[i * myImg.BytesPerPixel + 1];
       int value = higher;
       value = value << 8;
       value += lower;
       value = value * 8;

       myImg.Bits[i * myImg.BytesPerPixel] = (byte)(value & 0x00FF);
       myImg.Bits[i * myImg.BytesPerPixel + 1]=(byte)(value  >> 8);
     }
  }
  bs = BitmapSource.Create(myImg.Width, myImg.Height, 96, 96, PixelFormats.Gray16, null, myImg.Bits, myImg.Width * myImg.BytesPerPixel);
  ImgDepth.Source = bs;  

Unsere Routine ist zwar lang, aber nicht sonderlich kompliziert. Die äußere For-Schleife durchläuft die Zeilen, während die innere Schleife die einzelnen Pixel beackert. Die einzelnen, jeweils durch ein 16-Bit-Wort repräsentierten Pixel befinden sich dabei in einem Byte Array, das aus acht-bittigen Elementen besteht. Dabei ist wichtig, dass das minderwertigere Byte im Array „vor“ dem höherwertigen steht und somit einen niedrigeren Index hat. Nachdem die Pixelbytes in den Variablen lower und higher liegen, beginnt ihre eigentliche Verarbeitung. Durch einen Shift von acht Bits nach links kommt der höhere Wert an seine finale Position im int, der niederere Wert wird danach einfach „dazuaddiert“ und kommt automatisch „hinten“ dran. Die Multiplikation mit dem Faktor acht sorgt dafür, dass aus dem Wertebereich 0-2^13 der Wertebereich 0-2^16 wird. Zu guter Letzt teilen wir das int wieder in zwei Byte-Worte. Dabei kommt einmal eine UND-Maske zum Einsatz, das andere Mal shiften wir einfach um acht Bits nach rechts. Damit sind wir soweit fertig und führen das Programm abermals aus. Das Resultat zeigt Abbildung 4.

Abb. 4: Achtung auf den Rucksack!
Abb. 4: Achtung auf den Rucksack!
Und jetzt bitte mit Swap!

Wenn man in Abbildung 4 auf die Position vom Rucksack und Sessel achtet (und nicht auf die zugegebenermaßen selten geschmackfreie Art-Deco-Tapete der Buchhaltung), erkennt man eine Spiegelung. Im Videostream ist der Rucksack links vom Sessel, im Tiefenstream ist die Lage genau andersrum. Wer sich nur mit Skelett-Tracking befasst, hat damit keine Probleme. Da wir später die von Tiefen- und von Videostream gelieferten Daten „überlagern“ wollen, müssen wir die Reihen „umkehren“. Das erledigen wir mit einer for()-Schleife, wie in Listing 9.

Listing 9

for (int y = 0; y < 470; y++)
{
  for (int x = 0; x < 160; x++)
  {
    byte lowerC = myImg.Bits[y * 320 + x * myImg.BytesPerPixel];
    byte higherC = myImg.Bits[y * 320 + x * myImg.BytesPerPixel + 1];

    myImg.Bits[y * 320 + x * myImg.BytesPerPixel] = myImg.Bits[y * 320 + (320 - x) * myImg.BytesPerPixel];
    myImg.Bits[y * 320 + x * myImg.BytesPerPixel + 1] = myImg.Bits[y * 320 + (320 - x) * myImg.BytesPerPixel + 1];

    myImg.Bits[y * 320 + (320 - x) * myImg.BytesPerPixel] = lowerC;
    myImg.Bits[y * 320 + (320 - x) * myImg.BytesPerPixel + 1] = higherC;
  }
}  

Auch hier scheint der Code komplexer, als er in der Realität ist. Die äußere Schleife durchläuft abermals alle Zeilen, während die innere Schleife diesmal nur vom linken Zeilenrand bis zur Mitte der Zeile "vorrückt". Jeder dieser Pixel wird aus dem Array gepuhlt und mit seinem Äquivalent vom anderen Ende der Zeile ausgetauscht. Nachdem wir die Hälfte der Pixel mit ihren Konterparts vertauscht haben, haben wir effektiv die ganze Zeile vertauscht und können zur nächsten Zeile fortschreiten. Damit ist unser Beispiel fürs erste fertig und kann, wie in Abbildung 5 gezeigt, ausgeführt werden. Die unter Umständen auftretenden "Ungenauigkeiten" liegen an kleineren Unstimmigkeiten zwischen den beiden Kameraströmen (Parallaxfehler), ihre Behebung behandeln wir in einer folgenden Ausgabe. Das auf der Heft-CD befindliche Beispiel implementiert zusätzlich ein Histogramm und eine Tiefen-Freistellungsfunktion, die wir im nächsten Teil dieser Artikelserie besprechen werden.

Abb. 5: Jetzt ist der Rucksack da, wo er sein soll
Abb. 5: Jetzt ist der Rucksack da, wo er sein soll

Ob man Kinect nun mag oder nicht, das Gerät ermöglicht faszinierende neue Interaktionsformen zwischen Mensch und Maschine. Ob sich diese in der Praxis durchsetzen werden, ist zwar noch nicht entschieden, die Technologie ist aber auf jeden Fall mehr als ausgereift. Unsere Reise durch das Land von Kinect ist noch nicht vorbei. Im nächsten Teil werden wir wie angekündigt unser kleines Beispiel um ein Histogramm und eine Funktion zum Freistellen von Objekten erweitern. Danach werden wir uns mit dem eigentlichen, wesentlich simpler zu bedienenden Skeleton API befassen und unser Alter Ego mit Bällen interagieren lassen.

Tam Hanna befasst sich seit der Zeit des Palm IIIc mit Programmierung und Anwendung von Handcomputern. Er entwickelt Programme für diverse Plattformen, betreibt Onlinenewsdienste zum Thema und steht unter tamhan@tamoggemon.com für Fragen, Trainings und Vorträge gern zur Verfügung.
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -