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.

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.

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.

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.

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!