Wie sich Kinect-1-Apps nach Kinect 2 portieren lassen

Facial Tracking

Als zweites Beispiel befasst sich der Autor mit dem Facial Tracking. Wer mit einer Kinect 1 for Xbox arbeitete, litt aufgrund des fehlenden Near Modes unter eher bescheidener Genauigkeit: Dass der Sensor trotzdem bis zu 100 Punkte pro Gesicht erkennen konnte, ist eine algorithmische Meisterleistung von Microsoft.

Dank der wesentlich gesteigerten Auflösung des Sensors ist die Hardware der zweiten Generation deutlich besser für Facial Tracking geeignet. Um die Möglichkeiten der neuen API zu verdeutlichen, soll ein weiteres für den Kinect 1 vorgesehenes Programm adaptiert werden.

Die neue Lösung analysiert den Mundöffnungswinkel des vor dem Sensor stehenden Users. Nach dem Öffnen der Projektmappe müssen Entwickler die Verweise anpassen und das Kinect-Toolkit entfernen. Der Namespace Microsoft.Kinect.Toolkit.FaceTracking ist samt dem Wrapper-Projekt nicht mehr notwendig und wird durch einen Verweis auf Microsoft.Kinect.Face ersetzt.

Das SDK der Kinect 2 wird mit zwei verschiedenen Face-Trackern ausgeliefert. Der HighDefinition-Face-Tracker liefert Animation Units (AU) und genaue Gesichtskoordinaten, während der hier nicht weiter besprochene normale Face Tracker "zusammengefasste" Informationen liefert.

Aufgrund der weiter unten im Detail besprochenen Datenbank schließt die Nutzung des einen Trackers den anderen aus. Die Initialisierung erfolgt in beiden Fällen zweistufig: Das Source-Objekt generiert im nächsten Schritt einen Reader:

void setupStreams() 
{
myBodyReader = mySensor.BodyFrameSource.OpenReader();
myBodyReader.FrameArrived += myBodyReader_FrameArrived;

myColReader=mySensor.ColorFrameSource.OpenReader();
myColorArray = new byte[1920 * 1080 * 4];
myColReader.FrameArrived += myColReader_FrameArrived;

FrameDescription colorFrameDescription =
mySensor.ColorFrameSource.CreateFrameDescription
(ColorImageFormat.Bgra);
myBitmap= new WriteableBitmap(colorFrameDescription.Width,
colorFrameDescription.Height, 96.0, 96.0,
PixelFormats.Pbgra32, null);

myFFSource = new HighDefinitionFaceFrameSource(mySensor);
myFFReader = myFFSource.OpenReader();
myFace = new FaceAlignment();
myHasFaceFlag = false;
myFFReader.FrameArrived += myFFReader_FrameArrived;
}

Im SDK von Kinect 1 implementierte Face Tracker mussten Entwickler mit Tiefen-, Farb- und Skelettal-Frames versorgen. Das entfällt nun mehr oder weniger komplett – die Anforderung des Skelettal-Streams dient nur zum Ermitteln der Body-ID, die den Sensor über das zu bearbeitende Gesicht informiert:

void myBodyReader_FrameArrived(object sender, BodyFrameArrivedEventArgs e) 
{
BodyFrame myBodyFrame = e.FrameReference.AcquireFrame();
if (myBodyFrame == null) return;

Body selectedBody = FindClosestBody(myBodyFrame);

if (selectedBody != null)
{
myFFSource.TrackingId = selectedBody.TrackingId;
}
myBodyFrame.Dispose();
}

Eingehende Gesichts-Frames werden durch das Auslösen des FrameArrived-Events
angezeigt. Die Methode GetAndRefreshFaceAlignmentResult sorgt dafür, dass die Matrix mit den diversen AUs und Gesichtskoordinaten in ein Objekt herausgeschrieben wird.

Da die im MSDN-Portal beschriebene Eigenschaft für die Bounding-Box noch nicht implementiert ist, wird der Mittelpunkt des Gesichts ermittelt und als Basis für das zu zeichnende Rechteck genutzt. Die Umwandlung von Kamera- in Farbkoordinaten erfolgt über den CoordinateMapper, der ein Member des Sensorobjekts ist:

void myFFReader_FrameArrived(object sender, 
HighDefinitionFaceFrameArrivedEventArgs e)
{
using (var frame = e.FrameReference.AcquireFrame())
{
if (frame == null || !frame.IsFaceTracked)
{
return;
}

frame.GetAndRefreshFaceAlignmentResult(myFace);

ColorSpacePoint aPoint =
mySensor.CoordinateMapper.MapCameraPointToColorSpace
(myFace.HeadPivotPoint);


myRect=new Rect(aPoint.X-40,aPoint.Y-40,80,80);

myHasFaceFlag = true;
}
}

Die Zerlegen der Gesichtsdaten setzt Möglichkeiten voraus, die im Moment nicht als Teil der DLLs angeboten wird. Sie ist vor der Ausführung von Hand in das Zielverzeichnis zu kopieren. Dazu genügt ein Rechtsklick auf das Projekt im Solution Explorer. Im Eigenschaftendialog lassen sich unter Build-Ereignisse die Befehlszeile für Post-Build-Ereignisse parametrisieren. Dort muss folgendes Kommando eingegeben werden:

xcopy "$(KINECTSDK20_DIR)Redist\Face\$(Platform)\NuiDatabase" 
"$(TargetDir)\NuiDatabase" /S /R /Y /I

Aufgrund der um den Faktor vier höheren Datenmenge ist die bei Kinect-1-Applikationen angemessene Vorgehensweise mit der BitmapSource eher ungeeignet: Auf der vergleichsweise schnellen Workstation des Autors kam es mit dem ursprünglichen Algorithmus immer wieder zu massiven Rucklern. Zudem versagte der Face Tracker den Dienst, da ihm die verbleibende Rechenleistung nicht ausreichte.

Zur Lösung dieses Problems empfiehlt es sich, die Render-Pipeline auf ein WriteableBitmap umzustellen und dieses über die Erweiterungsbibliothek WriteableBitmapEx ansprechbar zu machen. Die neue Rendering-Routine sieht dann wie im folgenden Code aus – die einzige für mit WriteableBitmapEx erfahrene Entwickler interessante Passage ist die nach der Deklaration von mundOffen erfolgende Auswertung der AU:

void myColReader_FrameArrived(object sender, ColorFrameArrivedEventArgs e) 
{

ColorFrame myFrame = e.FrameReference.AcquireFrame();

if (myFrame == null) return;
myBitmap.Lock();
KinectBuffer b = myFrame.LockRawImageBuffer();
myFrame.CopyConvertedFrameDataToIntPtr(myBitmap.BackBuffer,
(uint)(1920 * 1080 * 4),
ColorImageFormat.Bgra);
myBitmap.AddDirtyRect(new Int32Rect(0, 0, myBitmap.PixelWidth,
myBitmap.PixelHeight));
b.Dispose();
myFrame.Dispose();

if (myHasFaceFlag == true)
{//Gesicht detektiert
//Rendern

Pen boxPen = new System.Windows.Media.Pen(new
SolidColorBrush(System.Windows.Media.Color.FromRgb
(255, 0, 0)), 2);
myBitmap.DrawRectangle((int)myRect.Left, (int)myRect.Top,
(int)myRect.Right, (int)myRect.Bottom, Color.FromRgb
(255, 0, 0));

float mundOffen = myFace.AnimationUnits[FaceShapeAnimations.JawOpen];
if (mundOffen < 0) mundOffen = 0;
if (myCalibratedFlag == true)
{
mundOffen = mundOffen * (1 / myCalibratedMaxval) * 180;
}
else
{
mundOffen = mundOffen * 180;
}
System.Windows.Rect boundaryRect =
new System.Windows.Rect(myRect.Right, myRect.Bottom, 180, 20);
System.Windows.Rect fillRect;

myBitmap.DrawRectangle((int)myRect.Right, (int)myRect.Bottom,
(int)myRect.Right + (int)180, (int)myRect.Bottom+20,
Color.FromRgb(255,0,0));


myBitmap.FillRectangle((int)myRect.Right, (int)myRect.Bottom,
(int)myRect.Right + (int)mundOffen, (int)myRect.Bottom + 20,
Color.FromRgb(255, 0, 0));

}
else
{

}

image1.Source = myBitmap;
myBitmap.Unlock();
}

An dieser Stelle gibt es eine kleine Falle. Die diversen in WriteableBitmapEx implementierten Zeichenfunktionen setzen ein Bitmap vom Typ Pbrga voraus, während die Kinect-Beispiele mit Rgba32 arbeiten. Da die Speicherformate weitgehend kompatibel zueinander sind, genügt es, die Deklaration des WriteableBitmap nach folgendem Schema anzupassen:

myBitmap= new WriteableBitmap(colorFrameDescription.Width, 
colorFrameDescription.Height, 96.0, 96.0, PixelFormats.Pbgra32, null);

Damit ist auch das zweite Beispiel – bis auf die nicht angepasste Kalibrationsroutine – einsatzbereit. Nach der Ausführung kann man sich an der in Abbildung 2 gezeigten Szene erfreuen.

Der Mundöffnungswinkel wird präzise erkannt (Abb. 2)