Teil 2: Skeleton API und Interaktion

Kinectologie zum Zweiten (Teil 2)
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.

Die dieser Methode zugrunde liegende Idee ist simpel: Jedes Pixel des Tiefenframes wird ausgewertet und seine Positionsdaten mit den vom Benutzer festgelegten Schwellwerten verglichen. Liegt der Wert nicht zwischen den beiden Stellwerten, ist das Pixel „unnötig“ und soll im Videostream weiß erscheinen. Dazu berechnen wir als erstes die Koordinaten, bezogen auf 320 x 240, und rechnen sie dann naiv auf den größeren Bildstrom um (ein Tiefenpixel entspricht vier Videopixeln). Die Hilfsroutine whitenPoint hat die Aufgabe, die zum Pixel gehörenden vier Bits im Bildstrom zu eliminieren. Die übergebenen Koordinaten werden dabei zum Berechnen der Position des ersten Bits im Array verwendet. Die darauffolgenden drei Bits finden wir durch Addition:

void whitenPoint(int x,int y)
{
  myVideoImage.Bits[(x + y * 640) * 4] = 255;
  myVideoImage.Bits[(x + y * 640) * 4 + 1] = 255;
  myVideoImage.Bits[(x + y * 640) * 4 + 2] = 255;
  myVideoImage.Bits[(x + y * 640) * 4 + 3] = 255;
}  

Auch hier erfolgt der Aufruf der Methode aus dem Event Handler des Tiefenframes: Zu guter Letzt müssen wir den von der Checkbox gesteuerten Swap-Block deaktivieren – vorausgesetzt, der Swap wurde schon vorher vorgenommen. Dazu greifen wir auf eine weitere Spezialität von C# zu, die es uns erlaubt, den Code so zu schreiben:

//Swap
if(counter!=0) // has already been swapped?
for (int y = 0; y < 470; y++)
{ //Process column
  . . .
}  

In der Praxis wäre dies natürlich aufgrund schlechterer Übersichtlichkeit eher unpopulär. Aber man sollte für den Ernstfall (und zum Herumhacken) wissen, wie es geht. Wenn wir das Programm nun ausführen, präsentiert es sich wie in Abbildung 2 gezeigt. Kleinere Unsauberkeiten an den Säumen der Objekte sind in unserer eher naiven Umrechnungsmethode begründet. Wir werden bald eine bessere Methode kennen lernen.

Abb. 2: Der Sessel ist freigestellt, samt einem Teil des Bücherregals, das gleich weit entfernt ist
Abb. 2: Der Sessel ist freigestellt, samt einem Teil des Bücherregals, das gleich weit entfernt ist
Skelettales Tracking

Doch damit vorerst genug vom manuellen Herumpuhlen in den von Kinect gelieferten Daten. Beispiel SusKinect4 implementiert das skelettale Tracking, das viele der populären Funktionen des Kinect überhaupt erst möglich macht. Wie im vorigen Beispiel initialisieren wir auch diesmal den Kinect-Sensor. Allerdings interessieren wir uns nun für skelettale Daten. Deshalb muss die übergebene Flag angepasst werden, wie in Listing 3 zu sehen.

Listing 3

public MainWindow()
{
  myRuntime = Microsoft.Research.Kinect.Nui.Runtime.Kinects[0];
  myRuntime.Initialize(RuntimeOptions.UseSkeletalTracking | RuntimeOptions.UseDepthAndPlayerIndex | RuntimeOptions.UseColor);
  myRuntime.SkeletonFrameReady += new EventHandler(myRuntime_SkeletonFrameReady);
  myRuntime.VideoFrameReady += new EventHandler(myRuntime_VideoFrameReady);
  myRuntime.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480,ImageType.Color);
  InitializeComponent();
}  

Auf den ersten Blick erscheint es widersinnig, weshalb man zum Tracken des Skeletts auch Tiefen- und sogar Spielerindexdaten anfordern muss. Das ist aber eine von Microsoft festgelegte Bedingung. Fragt man nur nach Skelettaldaten, können laut diversen Blogs seltsame Sachen geschehen. In Tests des Autors trat übrigens nichts Seltsames auf, wenn man nur UseSkeletalTracking aktivierte. Aber auch hier gilt: Better safe than sorry. Auch die Signatur der Event-Handler-Routine sieht etwas anders aus, da sie ja andere Eingabeparameter verarbeitet. Zunächst wollen wir nur den Kopf der User auf den Bildschirm plotten, wie in Listing 4.

Listing 4

void myRuntime_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
  SkeletonFrame mySkf = e.SkeletonFrame;
  foreach (SkeletonData sd in mySkf.Skeletons)
  {
    if (sd.TrackingState == SkeletonTrackingState.Tracked)
    {
      float xZeroOne, yZeroOne;
      myRuntime.SkeletonEngine.SkeletonToDepthImage(sd.Joints[JointID.Head].Position, out xZeroOne, out yZeroOne);
      int x = (int) Math.Max(0, Math.Min(xZeroOne * 320, 320));
      int y = (int) Math.Max(0, Math.Min(yZeroOne * 240, 240));
      ImageViewArea iv=new ImageViewArea();
      int colX; int colY;
      myRuntime.NuiCamera.GetColorPixelCoordinatesFromDepthPixel(ImageResolution.Resolution640x480, iv, x, y, 0, out colX, out colY);

      for (int i = -8; i <= 8; i++)
      {
        colorizePoint(ref myImageCache, colX+i, colY+i, (byte)0, (byte)0, (byte)255);
        colorizePoint(ref myImageCache, colX + i, colY - i, (byte)0, (byte)0, (byte)255);
      }

    BitmapSource bs = BitmapSource.Create(myImageCache.Width, myImageCache.Height, 96, 96, PixelFormats.Bgr32, null, myImageCache.Bits, myImageCache.Width * myImageCache.BytesPerPixel);
    image1.Source = bs;
    }
  }
}  

Kinect-Sensoren sind für das Erstellen von Multiplayer-Spielen vorgesehen. Deshalb sind sie logischerweise zum Tracken mehrerer Skelette in der Lage. Deshalb brechen wir den eingehenden SkeletonFrame im ersten Schritt mit einer foreach-Schleife in die den jeweiligen Spielern zugehörigen SkeletonData-Objekte auf. Danach prüfen wir, ob die Skelettdaten ordentlich (oder nur partiell) erfasst wurden. Verlief die Akquise erfolgreich, müssen wir das die dreidimensionalen Koordinaten enthaltende Joints-Array nach den Koordinaten des Kopfes befragen. Diese Koordinaten wandeln wir danach mittels SkeletonToDepthImage in zweidimensionale, auf ein 320 x 240 Pixel großes Tiefenimage bezogene Koordinaten um. Um für zukünftige Steigerungen der Auflösung des Tiefenimages gerüstet zu sein, liefert diese Funktion Werte im Wertebereich von 0 bis 1 zurück. Diese transformieren wir mit einer durch Math.Max vor "Überläufen" geschützten Multiplikation in den Koordinatenbereich um. Damit hätten wir die Koordinaten des Kopfes im Tiefenbild. Leider haben wir schon im vorigen Beispiel festgestellt, dass die Korrelation zwischen Pixeln in den beiden Feldern aufgrund diverser physikalischer Effekte (z. B. Parralax-Fehler) nur für sehr anspruchslose Anwendungen als einfache Multiplikation realisierbar ist.

Hinweis

Die in diesem Beispiel verwendeten Methoden verlangen zwangsweise nach Tiefendaten, die auch einen Spielerindex enthalten. Aus diesem Grund darf man das die Tiefeninformationen enthaltende PlanarImage nicht über GetDepth, sondern nur über GetDepthAndPlayerIndex anfordern.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -