Einführung in die Kinect-Programmierung

Seite 3: Mimikanalyse

Inhaltsverzeichnis

Mimik ist in der zwischenmenschlichen Interaktion wichtig: Nach einer Botox-Injektion werden Personen von ihrer Umgebung beispielsweise als kälter und distanzierter wahrgenommen. Kinect kann seit einiger Zeit die Mimik des Nutzers grob erkennen. Zur Quantifizierung der Emotionen kommt dabei die vom amerikanischenPsychologen Paul Ekman spezifizierte Methode der AUs (Action Units) zum Einsatz.

Microsoft liefert die Gesichtserkennungs-Engine als native C++-DLL aus, die durch einen Wrapper angesprochen wird. Dieser findet sich im Kinect Toolkit unter dem Namen Microsoft.Kinect.Toolkit.FaceTracking. Nach dem Einbinden des Projekts ist noch einiges an Konfigurationsarbeit zu leisten. Wichtig ist es, die Prozessorkonfiguration (32 oder 64 Bit) des Wrappers richtig einzustellen. Ansonsten wirft der Compiler eine verwirrende Warnung.

Ärgerlicherweise lässt sich die Anwendung trotzdem kompilieren und stürzt zur Laufzeit mit einer nicht auf das Problem verweisenden Fehlermeldung ab. Der Workaround besteht im Anpassen der Bittigkeit. Das geht leider nur im Konfigurationsmanager, der in Visual Studio von Haus aus versteckt ist. Der erste Schritt zum funktionierenden Programm ist deshalb das Aktivieren des Expertenmodus, was im Menü unter Extras | Einstellungen | Experteneinstellungen erfolgt.

Nach einigen Sekunden ist die Menüstruktur von Visual Studio um einige Einträge reicher. Unter Erstellen | Konfigurations-Manager findet sich ein Dialog, der die Konfiguration der Bittigkeit erlaubt. Hier ist es wichtig, dass alle Subprojekte auf den gleichen Prozessortyp (32 oder 64 Bit) eingestellt sind. Nach der Korrektur der Einstellungen lässt sich zur eigentlichen Programmrealisierung übergehen. Im ersten Schritt steht die Initialisierung der erforderlichen Flags:

public partial class MainWindow : Window
{
KinectSensor mySensor;
short[] myArray;
byte[] myColorArray;
Skeleton[] mySkeletonArray;
KinectSensorChooser myChooser;
FaceTracker myFaceTracker;
FaceTrackFrame myTrackCache = null;
bool myCalibratedFlag = false;
float myCalibratedMaxval;

Der Konstruktor fordert alle verfügbaren Kinect-Ströme an und erstellt eine neue Instanz des FaceTracker:

if (null != e.NewSensor)
{
mySensor = e.NewSensor;
//mySensor.SkeletonStream.TrackingMode = SkeletonTrackingMode.Seated;
mySensor.SkeletonStream.Enable();
mySensor.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);
mySensor.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);
myArray = new short[this.mySensor.DepthStream.FramePixelDataLength];
myColorArray = new byte[this.mySensor.ColorStream.FramePixelDataLength];
mySkeletonArray = new
Skeleton[this.mySensor.SkeletonStream.FrameSkeletonArrayLength];
mySensor.AllFramesReady += new EventHandler<AllFramesReadyEventArgs>
(mySensor_AllFramesReady);
myFaceTracker = new FaceTracker(mySensor);

try
{
this.mySensor.Start();
SensorChooserUI.Visibility = Visibility.Hidden;
}
catch (IOException)
{
this.mySensor = null;
}
}

Das Verarbeiten der eingehenden Daten erfolgt abermals im AllFramesReady-Event:

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

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

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

Das FaceTracker-Objekt bietet per se mehrere Arten zum Erfassen von Gesichtern an. Es ist theoretisch auch möglich, nur anhand von Tiefen- und Farbdaten auf die Jagd zu gehen. Leider liefert das nur unbefriedigende Ergebnisse, weshalb das Beispiel auf die Vollversion der Methode setzt:

FaceTrackFrame myFrame = null;
foreach (Skeleton aSkeleton in mySkeletonArray)
{
if (aSkeleton.TrackingState == SkeletonTrackingState.Tracked)
{
myFrame =
myFaceTracker.Track(ColorImageFormat.RgbResolution640x480Fps30,
myColorArray, DepthImageFormat.Resolution640x480Fps30,
myArray, aSkeleton);
if (myFrame.TrackSuccessful == true)
{
break;
}
}
}

Im Fall der erfolgreichen Erkennung eines Gesichts werden die im AU-Array befindlichen Daten ausgewertet. Der Zeichencode entspricht eins zu eins dem im vorigen Beispiel verwendeten. Nun ist eine Begrenzungsbox um das Gesicht zu zeichnen und der Wert der AU für "Mund offen" zu visualisieren:

    if (myFrame != null && myFrame.TrackSuccessful == true)
{//Gesicht entdeckt
//Aufbauen
myTrackCache = myFrame;
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 System.Windows.Rect(0, 0, 640, 480));

//Rendern
Pen boxPen = new System.Windows.Media.Pen(new
SolidColorBrush(System.Windows.Media.Color.FromRgb(255,
0, 0)), 2);
System.Windows.Rect newRect = new
System.Windows.Rect(myFrame.FaceRect.Left,
myFrame.FaceRect.Top, myFrame.FaceRect.Width,
myFrame.FaceRect.Height);
drawingContext.DrawRectangle(null, boxPen, newRect);

SolidColorBrush myBrush =
new SolidColorBrush(Color.FromRgb(255, 0, 255));
EnumIndexableCollection<AnimationUnit,float> myAUs =
myFrame.GetAnimationUnitCoefficients();

float mundOffen = myAUs[AnimationUnit.JawLower];
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(myFrame.FaceRect.Left +
myFrame.FaceRect.Width, myFrame.FaceRect.Top +
myFrame.FaceRect.Height, 180, 20);
System.Windows.Rect fillRect;
drawingContext.DrawRectangle(null, boxPen, boundaryRect);
fillRect = new System.Windows.Rect(myFrame.FaceRect.Left +
myFrame.FaceRect.Width, myFrame.FaceRect.Top +
myFrame.FaceRect.Height, mundOffen, 20);
drawingContext.DrawRectangle(myBrush, null, fillRect);

//Abbauen
drawingContext.Close();
RenderTargetBitmap myTarget =
new RenderTargetBitmap(640, 480, 96, 96, PixelFormats.Pbgra32);
myTarget.Render(drawingVisual);
image1.Source = myTarget;
}
else
{
//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 System.Windows.Rect(0, 0, 640, 480));

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

c.Dispose();
d.Dispose();
s.Dispose();
}

Zu guter Letzt noch ein Blick auf die Kalibrierungsroutine. Sie ist notwendig, da den Maximalwert der jeweiligen AUs nur die wenigsten Nutzer erreichen. Der Sensor kann ja keine Werte wie den maximalen Mundöffnungswinkel programmatisch ermitteln:

private void CmdCalibrate_Click(object sender, RoutedEventArgs e)
{
if(myTrackCache!=null)
{
if(myTrackCache.TrackSuccessful==true)
{
myCalibratedFlag=true;
EnumIndexableCollection<AnimationUnit,float> myAUs =
myTrackCache.GetAnimationUnitCoefficients();
myCalibratedMaxval = myAUs[AnimationUnit.JawLower];
}
}

Wichtig ist, dass der Rechenleistungsbedarf der Gesichtserkennung alles andere als gering ist. Ein leistungsstarker Zweikernprozessor (ab etwa 2,5 GHz) stellt eine vernünftige Basis dar – für aufwendige Animationen bleibt dabei nur wenig Rechenleistung.