Einführung in die Kinect-Programmierung

Seite 2: Skelettales Tracking

Inhaltsverzeichnis

Die eigentliche Referenzanwendung des Sensors ist und bleibt das skelettale Tracking. Dabei nutzt Kinect eine von Microsoft entwickelte "Wissensdatenbank", die aus den von der Hardware angelieferten Tiefen- und Farbdaten ein Modell des menschlichen Skeletts mit realen Koordinaten realisiert. Dabei werden die in Abbildung 4 gezeigten Punkte verfolgt.

Der Sensor zerbricht das Skelett in mehrere Punkte (Abb. 4).

Skelettal-Daten behandelt der Sensor ebenfalls als "Datenstrom". Das ist im folgenden Beispiel ersichtlich – die Aktivierung erfolgt durch das Aufrufen von SkeletonStream.Enable(). Der Rest der Funktion beschäftigt sich mit dem Anlegen der für die eingehenden Daten notwendigen Speicherfelder:

public partial class MainWindow : Window
{
KinectSensor mySensor;
WriteableBitmap myBitmap;
byte[] myColorArray;
Skeleton[] mySkeletonArray;
KinectSensorChooser myChooser;

void myChooser_KinectChanged(object sender, KinectChangedEventArgs e)
{
...

if (null != e.NewSensor)
{
mySensor = e.NewSensor;
mySensor.SkeletonStream.Enable();
mySensor.ColorStream.Enable(ColorImageFormat.
RgbResolution640x480Fps30);
myColorArray = new
byte[this.mySensor.ColorStream.FramePixelDataLength];
mySkeletonArray = new
Skeleton[this.mySensor.SkeletonStream
.FrameSkeletonArrayLength];
myBitmap = new WriteableBitmap(this.mySensor.
DepthStream.FrameWidth,
this.mySensor.DepthStream.FrameHeight,
96.0, 96.0, PixelFormats.Pbgra32, null);
image1.Source = myBitmap;
mySensor.AllFramesReady +=
+=new EventHandler<AllFramesReadyEventArgs>
(mySensor_AllFramesReady);
try
{
this.mySensor.Start();
SensorChooserUI.Visibility = Visibility.Hidden;
}
catch (IOException)
{
this.mySensor = null;
}
}
}

Das Beispiel fordert Farb- und Skelettaldaten an. Zum Vereinfachen der Verarbeitung bietet Microsoft das AllFramesReady-Ereignis an, das bei der Fertigstellung aller angeforderten Daten aufgerufen wird. Aufgrund einer Schwäche des SDKs erfolgen die ersten Aufrufe auch dann, wenn noch nicht alle Felder initialisiert wurden – die Prüfung gegen null verhindert in diesem Fall Abstürze:

void mySensor_AllFramesReady(object sender, AllFramesReadyEventArgs e)
{
ColorImageFrame c = e.OpenColorImageFrame();
SkeletonFrame s = e.OpenSkeletonFrame();

if (c == null || s == null) return;

c.CopyPixelDataTo(myColorArray);
s.CopySkeletonDataTo(mySkeletonArray);

Der Rest der Routine beschäftigt sich damit, eine für die WPF PictureBox einlesbare und für das Beispielprogramm beschreibbare Bitmap-Datei zu generieren. Der dazu notwendige Code ist lang, ändert seine Struktur aber nur marginal:

//Aufbauen
BitmapSource bs = BitmapSource.Create(640, 480, 96, 96,
PixelFormats.Bgr32, null, myColorArray, 640 * 4);
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);
Pen legPen = new System.Windows.Media.Pen(new
SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 0, 255)), 2);
Pen spinePen = new System.Windows.Media.Pen(new
SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 255, 0)), 2);

foreach (Skeleton aSkeleton in mySkeletonArray)
{

DrawBone(aSkeleton.Joints[JointType.HandLeft],
aSkeleton.Joints[JointType.WristLeft], armPen, drawingContext);
DrawBone(aSkeleton.Joints[JointType.WristLeft],
aSkeleton.Joints[JointType.ElbowLeft], armPen, drawingContext);
DrawBone(aSkeleton.Joints[JointType.ElbowLeft],
aSkeleton.Joints[JointType.ShoulderLeft], armPen, drawingContext);
DrawBone(aSkeleton.Joints[JointType.ShoulderLeft],
aSkeleton.Joints[JointType.ShoulderCenter], armPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.HandRight],
aSkeleton.Joints[ JointType.WristRight], armPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.WristRight],
aSkeleton.Joints[ JointType.ElbowRight], armPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.ElbowRight],
aSkeleton.Joints[ JointType.ShoulderRight], armPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.ShoulderRight],
aSkeleton.Joints[ JointType.ShoulderCenter], armPen, drawingContext);

DrawBone(aSkeleton.Joints[ JointType.HipCenter],
aSkeleton.Joints[ JointType.HipLeft], legPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.HipLeft],
aSkeleton.Joints[ JointType.KneeLeft], legPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.KneeLeft],
aSkeleton.Joints[ JointType.AnkleLeft], legPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.AnkleLeft],
aSkeleton.Joints[ JointType.FootLeft], legPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.HipCenter],
aSkeleton.Joints[ JointType.HipRight], legPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.HipRight],
aSkeleton.Joints[ JointType.KneeRight], legPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.KneeRight],
aSkeleton.Joints[ JointType.AnkleRight], legPen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.AnkleRight],
aSkeleton.Joints[ JointType.FootRight], legPen, drawingContext);

DrawBone(aSkeleton.Joints[ JointType.Head],
aSkeleton.Joints[ JointType.ShoulderCenter], spinePen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.ShoulderCenter],
aSkeleton.Joints[ JointType.Spine], spinePen, drawingContext);
DrawBone(aSkeleton.Joints[ JointType.Spine],
aSkeleton.Joints[ JointType.HipCenter], spinePen, drawingContext);

}

//Abbauen
drawingContext.Close();
RenderTargetBitmap myTarget =
new RenderTargetBitmap(640, 480, 96, 96, PixelFormats.Pbgra32);
myTarget.Render(drawingVisual);
image1.Source = myTarget;


c.Dispose();
s.Dispose();

}

Die Funktion DrawBone hat die Ausgabe, die Joints auszuwerten und auf den Bildschirm zu schreiben. Hierbei ist das Verwenden der Konversionsmethode relevant, die die Korrelation zwischen der Position des Körperteils und dem dazugehörenden Pixel im Farbstrom erledigt:

private void DrawBone(Joint jointFrom, Joint jointTo, Pen aPen,
DrawingContext aContext)
{
if (jointFrom.TrackingState == JointTrackingState.NotTracked ||
jointTo.TrackingState == JointTrackingState.NotTracked)
{
return;
}

if (jointFrom.TrackingState == JointTrackingState.Inferred ||
jointTo.TrackingState == JointTrackingState.Inferred)
{
ColorImagePoint p1 =
mySensor.CoordinateMapper.MapSkeletonPointToColorPoint
(jointFrom.Position, ColorImageFormat.RgbResolution640x480Fps30);
ColorImagePoint p2 =
mySensor.CoordinateMapper.MapSkeletonPointToColorPoint
(jointTo.Position, ColorImageFormat.RgbResolution640x480Fps30);
//Thin line
aPen.DashStyle = DashStyles.Dash;
aContext.DrawLine(aPen, new Point(p1.X, p1.Y),
new Point(p2.X, p2.Y));

}
if (jointFrom.TrackingState == JointTrackingState.Tracked ||
jointTo.TrackingState == JointTrackingState.Tracked)
{
ColorImagePoint p1 =
mySensor.CoordinateMapper.MapSkeletonPointToColorPoint
(jointFrom.Position, ColorImageFormat.RgbResolution640x480Fps30);
ColorImagePoint p2 =
mySensor.CoordinateMapper.MapSkeletonPointToColorPoint
(jointTo.Position, ColorImageFormat.RgbResolution640x480Fps30);
//Thick line
aPen.DashStyle = DashStyles.Solid;
aContext.DrawLine(aPen, new Point(p1.X, p1.Y),
new Point(p2.X, p2.Y));
}
}

Damit ist das Programm einsatzbereit. Abbildung 5 zeigt ein Beispiel für die Bildschirmausgabe. Zum Verfolgen sitzender Personen empfiehlt sich die Aktivierung des "Seated Mode".

Im Seated Mode ist das Skelett weniger feingranular (Abb. 5).

Wichtig ist, dass das skelettale Tracking ausschließlich durch ein neuronales Netz erfolgt. Kinect hat keine Ahnung, was ein Knie und was eine Hand darstellt – die Verfolgung erfolgt sozusagen "auf gut Glück". Deshalb ist der Sensor für alle medizinischen Anwendungen ungeeignet, das Risiko von Fehlberechnungen ist einfach zu hoch.

Eine der häufigsten Anwendungen für NUI-Sensoren ist das Interagieren mit Steuerelementen. Theoretisch lässt sich das von Hand realisieren. Die aktuelle Version des SDKs enthält eine Gruppe von Widgets, die die dazu nötige Intelligenz schon mitbringen. Hierfür sei nun ebenfalls ein kleines Beispiel zusammengezimmert. Die Widgets lassen sich einfach per XAML in die Applikation einbinden – die resultierende Datei sieht so aus:

<k:KinectRegion Name="KinectRegion">
<Grid>
<k:KinectTileButton Label="K.TileButton" Click="CButtonOnClick">
</k:KinectTileButton>
<k:KinectCircleButton Label="Circle" VerticalAlignment="Top"
Height="200" Click="CButtonOnClick">Hi</k:KinectCircleButton>
<k:KinectScrollViewer VerticalScrollBarVisibility="Disabled"
HorizontalScrollBarVisibility="Auto"
VerticalAlignment="Bottom">
<StackPanel Orientation="Horizontal" Name="scrollContent"
VerticalAlignment="Bottom">
<k:KinectCircleButton Label="1" VerticalAlignment="Top"
Height="200"
Click="CButtonOnClick">Hi</k:KinectCircleButton>
<k:KinectCircleButton Label="2" VerticalAlignment="Top"
Height="200"
Click="CButtonOnClick">Hi</k:KinectCircleButton>
<k:KinectCircleButton Label="3" VerticalAlignment="Top"
Height="200"
Click="CButtonOnClick">Hi</k:KinectCircleButton>
<k:KinectCircleButton Label="4" VerticalAlignment="Top"
Height="200"
Click="CButtonOnClick">Hi</k:KinectCircleButton>
<k:KinectCircleButton Label="5" VerticalAlignment="Top"
Height="200"
Click="CButtonOnClick">Hi</k:KinectCircleButton>
<k:KinectCircleButton Label="6" VerticalAlignment="Top"
Height="200"
Click="CButtonOnClick">Hi</k:KinectCircleButton>
</StackPanel>
</k:KinectScrollViewer>
</Grid>
</k:KinectRegion>
<k:KinectUserViewer k:KinectRegion.KinectRegion="{Binding
ElementName=KinectRegion}" Name="aKinectUserViewer"
VerticalAlignment="Top" HorizontalAlignment="Center"
Height="100"/>
<toolkit:KinectSensorChooserUI x:Name="SensorChooserUI"
IsListening="True" HorizontalAlignment="Center"
VerticalAlignment="Top" />
</Grid>

Der KinectTileButton steht in der "Nahrungskette" ganz unten. Er realisiert eine an Windows Phone erinnernde Kachel (Tile), die sich per Touch aktivieren lässt. Der CircleButton ist etwas aufwendiger animiert, unterscheidet sich sonst aber nicht wesentlich von seinem Kollegen.

Manchmal ist es wünschenswert, eine Liste scrollbarer Steuerelemente anzubieten. Dann empfehlen sich KinectScrollViewer. Der KinectUserViewer verdient besondere Aufmerksamkeit, da er eine Übersicht über die Position der vor dem Sensor befindlichen Nutzer anzeigt. Alle besprochenen Widgets müssen in einer KinectRegion sitzen – dieses Objekt ist für das Weiterleiten der Ereignisse verantwortlich.

Im CodeBehind-Bereich genügt es, einen Event-Handler anzulegen. Zudem ist die KinectRegion mit einem Verweis auf den zu verwendenden Sensor zu versehen:

private void CButtonOnClick(object sender, RoutedEventArgs e)
{
MessageBox.Show("Well done!");
}


void myChooser_KinectChanged(object sender, KinectChangedEventArgs e)
{
if (null != e.OldSensor)
{
//Alten Kinect deaktivieren
if (mySensor != null)
{
mySensor.Dispose();
}
}

if (null != e.NewSensor)
{
mySensor = e.NewSensor;

mySensor.DepthStream.Enable(DepthImageFormat.
Resolution640x480Fps30);
mySensor.SkeletonStream.Enable();
mySensor.Start();

KinectRegion.KinectSensor = mySensor;
}
}

Damit ist auch dieses Programmbeispiel einsatzbereit.