Teil 2: Skeleton API und Interaktion

Kinectologie zum Zweiten (Teil 3)
Kommentare

Im zweiten Teil der Artikelserie „Kinectologie“ beschäftigen wir uns mit skelettalem Tracking und Tiefenframes, die zur Interaktion mit Objekten aus der realen Welt eingesetzt werden können.

Da wir diesmal höhere Ansprüche an uns selbst stellen, verwenden wir die Methode GetColorPixelCoordinatesFromDepthPixel. Sie verlangt eine Vielzahl von selbsterklärenden Parametern nebst einer frisch erstellten ImageViewArea-Instanz und liefert danach die zum Tiefenpixel gehörenden Koordinaten im Farbpixel zurück. Haben wir diese Koordinaten, ist der Rest simpel. Wir zeichnen ein Kreuz an die Stelle des Kopfes und schreiben das Bitmap in das Formular. Die Routine colorizePoint färbt den ihr übergebenen Punkt im die Videodaten enthaltenden PlanarImage ein (Listing 5).

Listing 5

void colorizePoint(ref PlanarImage _img, int x, int y, byte r, byte g, byte b)
{
  if (x >= 640) return;
  if (y >= 480) return;
  _img.Bits[(x + y * 640) * 4 + 2] = r;
  _img.Bits[(x + y * 640) * 4 + 1] = g;
  _img.Bits[(x + y * 640) * 4 + 0] = b;
  _img.Bits[(x + y * 640) * 4 + 3] = 255;
}  

Auch hier findet sich keine Differenzialrechnung: Nach einem Sanity Check finden wir das zu bearbeitende Pixel und überschreiben seine Werte mit den eingegebenen. Als Transparenzwert setzen wir 255, um die Farbe in voller Stärke darzustellen. Führen wir SusKinect4 aus, erhalten wir das in Abbildung 3 gezeigte Resultat.

Abb. 3: Das Kreuz ist über dem Kopf
Abb. 3: Das Kreuz ist über dem Kopf
Von der Quelle bis zur Bitmap

Bis jetzt litten unsere Beispiele unter einer ärgerlichen Schwäche: Wollten wir in den Videostrom schreiben, mussten wir die in der BitmapSource enthaltenen Bits direkt manipulieren. Da Bitmaps wesentlich bequemer zu handeln sind, konvertieren wir im Beispiel SusKinect5 die eingehenden Videoframes zuerst in ein angenehmer handhabbares Format. Danach passen wir die Skelettverarbeitungsroutine an, um die gesamten vorhandenen Skelettdaten ins Bitmap zu schreiben. Dazu brauchen wir als erstes ein wenig Übersicht über das Anatomieverständnis des Sensors. Die aus dem SDK stammende Abbildung 4 zeigt die vom Kinect verfolgten „Punkte“ des menschlichen Körpers.

Abb. 4: Die folgenden Punkte sind dem Kinect bekannt (Bild: Microsoft)
Abb. 4: Die folgenden Punkte sind dem Kinect bekannt (Bild: Microsoft)

Mit dieser Information ausgestattet, adaptieren wir den Handler des Skelett-Komplett-Events, wie in Listing 6 zu sehen.

Listing 6

void myRuntime_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
  SkeletonFrame mySkf = e.SkeletonFrame;
  foreach (SkeletonData sd in mySkf.Skeletons)
  {
    if (sd.TrackingState == SkeletonTrackingState.Tracked)
    {
      //Aufbauen
      BitmapSource bs = BitmapSource.Create(myImageCache.Width, myImageCache.Height, 96, 96, PixelFormats.Bgr32, null, myImageCache.Bits, myImageCache.Width * myImageCache.BytesPerPixel);
      DrawingVisual drawingVisual = new DrawingVisual();
      DrawingContext drawingContext = drawingVisual.RenderOpen();
      drawingContext.DrawImage(bs,new Rect(0,0,640,480));

      //Rendern
      Pen armPen=new System.Windows.Media.Pen(new SolidColorBrush(System.Windows.Media.Color.FromRgb(255,0,0)),2);
      drawingContext.DrawLine(armPen, BodyPartToCoords(sd, JointID.HandLeft), BodyPartToCoords(sd, JointID.WristLeft));
      drawingContext.DrawLine(armPen, BodyPartToCoords(sd, JointID.WristLeft), BodyPartToCoords(sd, JointID.ElbowLeft));
      drawingContext.DrawLine(armPen, BodyPartToCoords(sd, JointID.ElbowLeft), BodyPartToCoords(sd, JointID.ShoulderLeft));
      drawingContext.DrawLine(armPen, BodyPartToCoords(sd, JointID.ShoulderLeft), BodyPartToCoords(sd, JointID.ShoulderCenter));
      drawingContext.DrawLine(armPen, BodyPartToCoords(sd, JointID.HandRight), BodyPartToCoords(sd, JointID.WristRight));
      drawingContext.DrawLine(armPen, BodyPartToCoords(sd, JointID.WristRight), BodyPartToCoords(sd, JointID.ElbowRight));
      drawingContext.DrawLine(armPen, BodyPartToCoords(sd, JointID.ElbowRight), BodyPartToCoords(sd, JointID.ShoulderRight));
      drawingContext.DrawLine(armPen, BodyPartToCoords(sd, JointID.ShoulderRight), BodyPartToCoords(sd, JointID.ShoulderCenter));

      Pen legPen = new System.Windows.Media.Pen(new SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 0, 255)), 2);
      drawingContext.DrawLine(legPen, BodyPartToCoords(sd, JointID.HipCenter), BodyPartToCoords(sd, JointID.HipLeft));
      drawingContext.DrawLine(legPen, BodyPartToCoords(sd, JointID.HipLeft), BodyPartToCoords(sd, JointID.KneeLeft));
      drawingContext.DrawLine(legPen, BodyPartToCoords(sd, JointID.KneeLeft), BodyPartToCoords(sd, JointID.AnkleLeft));
      drawingContext.DrawLine(legPen, BodyPartToCoords(sd, JointID.AnkleLeft), BodyPartToCoords(sd, JointID.FootLeft));
      drawingContext.DrawLine(legPen, BodyPartToCoords(sd, JointID.HipCenter), BodyPartToCoords(sd, JointID.HipRight));
      drawingContext.DrawLine(legPen, BodyPartToCoords(sd, JointID.HipRight), BodyPartToCoords(sd, JointID.KneeRight));
      drawingContext.DrawLine(legPen, BodyPartToCoords(sd, JointID.KneeRight), BodyPartToCoords(sd, JointID.AnkleRight));
      drawingContext.DrawLine(legPen, BodyPartToCoords(sd, JointID.AnkleRight), BodyPartToCoords(sd, JointID.FootRight));

      Pen spinePen = new System.Windows.Media.Pen(new SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 255, 0)), 2);
      drawingContext.DrawLine(spinePen, BodyPartToCoords(sd, JointID.Head), BodyPartToCoords(sd, JointID.ShoulderCenter));
      drawingContext.DrawLine(spinePen, BodyPartToCoords(sd, JointID.ShoulderCenter), BodyPartToCoords(sd, JointID.Spine));
      drawingContext.DrawLine(spinePen, BodyPartToCoords(sd, JointID.Spine), BodyPartToCoords(sd, JointID.HipCenter));
      
      //Abbauen
      drawingContext.Close();
      RenderTargetBitmap myTarget = new RenderTargetBitmap(640, 480, 96, 96, PixelFormats.Pbgra32);
      myTarget.Render(drawingVisual);
      image1.Source = myTarget;
    }
  }
}  

Diese Monsterroutine besteht, wie schon in den Kommentaren angedeutet, aus insgesamt drei Teilen: Der erste Teil konvertiert das aus dem PlanarImage errichtete BitmapSource-Objekt in einen DrawingContext, mit dem wir wie schon weiter oben beim Histogramm zeichnen können.

Im zweiten Teil der Routine rendern wir das Skelett als eine Art manuell gezeichnete PolyLine. Für jeden der drei Linienzüge (Bein, Arm oder Rückgrat) erstellen wir je eine Instanz der Pen-Klasse, mit der wir danach die durch die in Abbildung 4 gezeigten Punkte beschriebenen Linienzüge „nachziehen“. Die Methode BodyPartToCoords ist eine vom Autor selbst erstellte Hilfsfunktion, die die in der vorigen Routine von Hand implementierte Koordinatentransformation automatisch erledigt, wenn man ihr SkeletonData und eine JointID übergibt. Der dritte Teil ist ebenfalls ein alter Bekannter: Er weist der ImageView das Gezeichnete als Bilddatenquelle zu. Damit wären wir auch mit SusKinect5 fertig. Führen wir das Programm aus, erhalten wir das in Abbildung 5 gezeigte Resultat.

Abb. 5: Jetzt erkennt man die Testperson zur Gänze
Abb. 5: Jetzt erkennt man die Testperson zur Gänze
Exkurs: Was für Daten sind das?

Obwohl wir bis jetzt durchaus eindrucksvolle Programme zusammengebaut haben, verstehen wir noch nicht wirklich viel über die vom Sensor zurückgelieferten Daten. OK, der Videoframe enthält Farbinformationen. Doch welche Informationen liefern uns die in den DepthFrames und SkeletonFrames enthaltenen Datenstrukturen über die reale Welt? Als erstes wollen wir uns den Tiefenframes zuwenden: Je nach Betriebsmodus sind die Nutzinformationen unterschiedlich angeordnet. Bei UseDepth sitzen die Nutzdaten in den niederwertigen Bits 0 bis 11. Die höherwertigen Bits verfallen ungenutzt. Aktivieren wir hingegen UseDepthAndPlayerIndex, enthalten die drei niederwertigen Bits Informationen über den an dieser „Position“ anzutreffenden Spieler. Die höherwertigen Bytes enthalten in diesem Fall die Tiefeninformationen. In beiden Fällen handelt es sich dabei um die kartesische Distanz zwischen dem Ursprung des Sensors und dem nächsten Objekt, das ein aus dem Ursprung durch den benannten Pixel austretender Lichtstrahl als erstes berührt. Die Maßeinheit ist Millimeter. Wird ein Wert von 0 retourniert, wird der Sensor an dieser Stelle nichts erkennen. Die beim skelettalen Tracking zurückgelieferten Vektoren haben ihren Ursprung an der Stelle des Sensors. Abbildung 6 zeigt das so genannte Skelettkoordinatensystem.

Abb. 6: Das Skelettalkoordinatensystem (Bild: Microsoft)
Abb. 6: Das Skelettalkoordinatensystem (Bild: Microsoft)

Wichtig ist, dass die in der Klasse Kinect.Nui.Vector zurückgelieferten Werte in der Einheit Meter aufscheinen. Die Vectorklasse wirft erfreulicherweise floats aus.

Fazit

In Teil zwei unserer gemeinsamen Reise durch die Welt von Kinect haben wir uns mit skelettalem Tracking auseinandergesetzt. Ab sofort können Sie Kinect zur Interaktion mit Objekten aus der realen Welt einsetzen. Teil drei dieser Serie „bindet lose Enden zusammen“ und zeigt einige nützliche Tipps und Tricks. Man liest sich!

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 -