lunedì 29 agosto 2011

Alla scoperta del Kinect : gli scheletri e l’uomo vitruviano

Cosa c’entra Leonardo e il suo Uomo Vitruviano, gli scheletri ed il Kinect?

Apparentemente nulla, ma leggendo questo post scoprirete che, invece, un legame c’è.

Abbiamo visto in precedenti post che l’infernale aggeggio (il Kinect) ci permette di ottenere, in maniera molto facile, le immagini video e le immagini di profondità. Già con questi due insiemi di informazioni potremmo realizzare una sorta di interazione tra i player ed il nostro applicativo.

L’SDK del Kinect, però, fa molto di più perché è in grado di fornirci gli “scheletri” dei player disposti davanti ad esso.

Vediamo come procedere per ricavare ciò.

La classe Runtime (di cui abbiamo parlato in questo post) prevede un evento chiamato SkeletonFrameReady che viene sollevato quando SDK ha a disposizione un oggetto di tipo SkeletonFrame pronto per essere elaborato.

L’istanza di questo oggetto è passata come proprietà dell’argomento dell’evento SkeletonFrameReady:

image

La classe SkeletonFrame, come possiamo vedere, espone una serie di proprietà decisamente interessanti:

  • FloorClipPlane : l’oggetto Vector contenuto in questa proprietà ci fornisce i coefficienti dell’equazione del piano di terra (pavimento) stimata dall’SDK. L’equazione ha la forma:

Ax+By+Cz+D=0

dove:

A = FloorClipPlane.X

B = FloorClipPlane.Y

C = FloorClipPlane.Z

D = FloorClipPlane.W

L’equazione del piano è normalizzata, quindi D rappresenta, di fatto, l’altezza della camera rispetto al pavimento. Nella versione Beta dell’SDK il vettore è sempre nullo;

  • NormalToGravity : consente di avere informazioni sul vettore di  gravità. Nella versione Beta dell’SDK il vettore è sempre nullo.;
  • FrameNumber : è il numero del frame in esame;
  • TimeStamp : timestamp del frame (in maniera analoga a quanto visto per il video e la profondità);
  • Quality : indica la qualità del frame;
  • Skeletons : contiene un array di “scheletri” dei giocatori tracciati.

Il Kinect è in grado di tracciare fino a 6 giocatori contemporaneamente anche se:

  • solo 2 giocatori possono essere tracciati “attivamente”. Di questi si conoscono tantissime info all’interno della struttura SkeletonData;
  • fino a 4 giocatori possono essere tracciati “passivamente”. L’SDK mantiene solo alcune informazioni di questi giocatori.

La classe SkeletonData contiene, per ogni giocatore, i dati relativi alla posizione dello stesso nello spazio:

image

Lo scheletro è individuato da una collezione di punti del corpo umano, denominati Joint, e memorizzati nell’array Joints. Ogni Joint è individuato da un Id (di tipo JointID) e, allo stato attuale, possiamo contare su ben 20 punti mostrati nella seguente figura (ecco l’uomo vitruviano del titolo):

VitruvialMan

Ogni Joint fornisce la propria posizione nello spazio (proprietà Position) e lo stato di tracciamento (proprietà TrackingState). La posizione è identificata dalle tre coordinate spaziali secondo il seguente schema:

xyz

X e Y variano in un intervallo [-1,1] mentre Z esprime la distanza (in metri) dal sensore. Il valore di W, secondo quanto riportato dalla documentazione ufficiale dovrebbe variare tra 0 e 1 e identificare la precisione della posizione (1 posizione affidabile, 0 posizione totalmente inaffidabile) ma, allo stato attuale, il valore di W è sempre pari a 1.

La proprietà TrackingState della classe SkeletonData ci fornisce informazioni relative al tipo di tracciamento a cui è sottoposto lo scheletro e cioè se si tratta di un giocatore attivo (Tracked), passivo (PositionOnly) oppure se non è tracciato (NotTracked).

La proprietà Quality ci dice se lo scheletro è stato “tagliato”. Se, ad esempio, il Kinect riesce ad individuare lo scheletro dalle ginocchia in su (ad esempio perché caviglie e piedi sono nascosti), allora nella proprietà Quality avremo il valore ClippedBottom ad indicare che lo scheletro è stato “tagliato” in basso.

Interessante la proprietà Position di SkeletonData che contiene la posizione (approssimata) del centro di massa dello scheletro.

L’applicazione pratica che vi vorrei proporre a corollario di questo post è la realizzazione di una semplice applicazione che mostra, in tempo reale, la posizione dei venti punti dell’uomo vitruviano di cui sopra.

Cominciamo con il mostrare cosa vogliamo ottenere:

image

In pratica abbiamo, istantaneamente, la posizione di tutti i Joints disponibili del primo SkeletonData tracciato e, per ogni Joint, abbiamo un diverso colore di sfondo in base al loro stato di tracciamento.

In più visualizziamo il centro di massa (la TextBox in posizione “equivoca”),la qualità dello SkeletonFrame, il vettore NormalToGravity, il vettore FloorClipPlane (questi ultimi due sempre nulli).

L’applicazione è un’applicazione WPF in cui ho deciso di adottare il pattern MVVM, quindi, per spiegare il funzionamento, ci occuperemo, innanzitutto del ViewModel per poi usare il motore di binding di WPF per agganciare la UI.

Nel costruttore del View Model (MainWindowViewModel) avviamo l’istanza della classe Runtime dell’SDK del Kinect:

  1. Public Sub New()
  2.     If Not DesignerProperties.GetIsInDesignMode(New DependencyObject()) Then
  3.         Try
  4.             Nui = New Runtime
  5.             AddHandler Nui.DepthFrameReady, AddressOf DepthFrameReadyHandler
  6.             AddHandler Nui.SkeletonFrameReady, AddressOf SkeletonFrameHandler
  7.             Nui.Initialize(RuntimeOptions.UseDepthAndPlayerIndex Or RuntimeOptions.UseSkeletalTracking)
  8.             Nui.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.DepthAndPlayerIndex)
  9.             Catch ex As Exception
  10.  
  11.         End Try
  12.     End If
  13. End Sub

Se non ci troviamo in design mode (primo if) eseguiamo i seguenti passi:

  1. creiamo l’istanza di Runtime;
  2. agganciamo il gestore dell’evento DepthFrameReady che ci servirà esclusivamente per mostrare l’immagine di profondità a video;
  3. agganciamo il gestore dell’evento SkeletonFrameReady che ci servirà per elaborare gli scheletri che il Kinect ci fornirà nel tempo;
  4. inizializziamo la Runtime selezionando lo stream di profondità e di Skeletal tracking;
  5. apriamo lo stream di profondità decidendo una risoluzione di 320x240 (per la profondità abbiamo solamente 320x240 e 80x60).

Tralasciamo il gestore di evento per le immagini di profondità (che abbiamo già visto nel precedente post) e ci dedichiamo a quello dello Skeletal Tracking:

  1. Private Sub SkeletonFrameHandler(sender As Object, e As SkeletonFrameReadyEventArgs)
  2.     Me.NormalToGravity = e.SkeletonFrame.NormalToGravity
  3.     Me.FloorClipPlane = e.SkeletonFrame.FloorClipPlane
  4.     Me.Quality = e.SkeletonFrame.Quality.ToString()
  5.     Dim skeleton = (From s In e.SkeletonFrame.Skeletons
  6.                   Where s.TrackingState = SkeletonTrackingState.Tracked
  7.                   Select s).FirstOrDefault()
  8.  
  9.     If skeleton IsNot Nothing Then
  10.         Me.CenterPosition = skeleton.Position
  11.         Me.Joints = skeleton.Joints.OfType(Of Joint)()
  12.     Else
  13.         Me.Joints = Nothing
  14.         Me.CenterPosition = New Vector()
  15.     End If
  16. End Sub

Le prime tre istruzioni non fanno altro che recuperare, rispettivamente, NormalToGravity, FloorClipPlane e Quality e valorizzare le relative proprietà del View Model (che andranno in binding con opportuni controlli di visualizzazione).

Tramite la query LINQ seguente andiamo a selezionare, tra tutti gli scheletri forniti nell’argomento dell’evento, il primo che abbia stato di tracciamento pari a Tracked.
Se ne esiste almeno uno (abbiamo, quindi, una istanza valida di SkeletonData), andiamo a valorizzare le proprietà del View Model che ci consentono di visualizzare il valore del centro di massa (proprietà CenterPosition) e il valore di ogni singolo Joint dello scheletro (proprietà Joints).

A questo punto abbiamo due tipologie di proprietà nel nostro View Model: quelle di tipo Vector (NormalToGravity, FloorClipPlane e CenterPosition) e la collezione di Joint esposta dalla Joints.

Tutte queste proprietà verranno visualizzate tramite TextBox e utilizzeremo degli appositi Converter per formattare opportunamente le stesse.

Per le proprietà di tipo Vector creiamo il converter:

  1. Public Class VectorToStringConverter
  2.     Implements IValueConverter
  3.  
  4.     Public Function Convert(value As Object,
  5.                             targetType As System.Type,
  6.                             parameter As Object,
  7.                             culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert
  8.         If TypeOf value Is Vector Then
  9.             Dim vector = CType(value, Vector)
  10.             Dim strFormat = "{0:0.000} {1:0.000} {2:0.000} [{3:0.000}]"
  11.             If parameter IsNot Nothing Then
  12.                 strFormat = parameter.ToString()
  13.             End If
  14.             Dim str = String.Format(strFormat, vector.X, vector.Y, vector.Z, vector.W)
  15.             Return str
  16.         Else
  17.             Return Nothing
  18.         End If
  19.     End Function
  20.  
  21.     Public Function ConvertBack(value As Object,
  22.                                 targetType As System.Type,
  23.                                 parameter As Object,
  24.                                 culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack
  25.         Throw New NotImplementedException
  26.     End Function
  27. End Class

In sostanza, prendiamo il valore da convertire e se è di tipo Vector e eseguiamo il metodo String.Format utilizzando la stringa di formato passata come parametro (o quella di default se il parametro non è impostato) e i valori di X, Y, Z e W del vettore.
Grazie all’utilizzo di questo converter possiamo visualizzare il centro di massa con il seguente XAML:

  1. <TextBox Text="{Binding Path=CenterPosition, Converter={StaticResource VectorToStringConverter}, ConverterParameter=\\cf1 {0:0.000\\cf1 } \\cf1 {1:0.000\\cf1 } \\cf1 {2:0.000\\cf1 } }"
  2.          Canvas.Left="448" Canvas.Top="460" Style="{StaticResource PosizionTextBox}"
  3.          Background="LightSteelBlue" Foreground="White"/>

Più interessante è sicuramente il converter che ci permetterà di visualizzare i dati di posizione del singolo Joint all’interno dei 20 TextBox che circondano l’uomo vitruviano.
In questo caso il converter accetta come parametri la stringa identificante l’id del Joint (as esempio “HandRight” per la mano destra) e la stringa di formattazione (questa è opzionale) separati dal carattere “|”.
Il codice del converter è il seguente:

  1. Public Class SkeletonJointPositionToStringConverter
  2.     Implements IValueConverter
  3.  
  4.     Public Function Convert(value As Object,
  5.                             targetType As System.Type,
  6.                             parameter As Object,
  7.                             culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert
  8.         If TypeOf value Is IEnumerable(Of Joint) Then
  9.             Dim joints = CType(value, IEnumerable(Of Joint))
  10.             Dim parameters = parameter.ToString.Split("|"c)
  11.             If parameters.Count > 0 Then
  12.                 Dim jointId = parameters(0)
  13.                 Dim jointQuery = From j In joints
  14.                                 Where j.ID = CType([Enum].Parse(GetType(JointID), jointId), JointID)
  15.                                 Select j
  16.                 If jointQuery.Any() Then
  17.                     Dim joint = jointQuery.First()
  18.                     Dim strFormat = "{0:0.000} {1:0.000} {2:0.000}"
  19.                     If parameters.Count > 1 Then
  20.                         strFormat = parameters(1)
  21.                     End If
  22.                     Dim str = String.Format(strFormat, joint.Position.X, joint.Position.Y, joint.Position.Z)
  23.                     Return str
  24.                 Else
  25.                     Return Nothing
  26.                 End If
  27.             Else
  28.                 Return Nothing
  29.             End If
  30.             Else
  31.                 Return Nothing
  32.             End If
  33.     End Function
  34.  
  35.     Public Function ConvertBack(value As Object,
  36.                                 targetType As System.Type,
  37.                                 parameter As Object,
  38.                                 culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack
  39.         Throw New NotImplementedException
  40.     End Function
  41. End Class

In questo caso, se il valore che stiamo convertendo è di tipo IEnumerable(Of Joint) (come la proprietà Joints del ViewModel), andiamo a ricercare, tramite una query LINQ, il Joint che ha id passato come parametro (notare la chiamata al metodo Split di String per ricavare i due argomenti separati da “|”). Se la query LINQ restituisce un joint valido (se la stringa di ID passata per argomento è uno dei valori ammessi), allora creiamo la stringa da visualizzare utilizzando la posizione del joint stesso e la restituiamo al chiamante.

Grazie a questo converter possiamo utilizzare il binding per visualizzare tutti e 20 i punti dell’uomo vitruviano senza dover creare 20 differenti proprietà nel view model. Ad esempio la visualizzazione della posizione della testa (Head) è la seguente:

  1. <TextBox Canvas.Left="470" Canvas.Top="70" Style="{StaticResource PosizionTextBox}"
  2.          Text="{Binding Path=Joints, Converter={StaticResource SkeletonJointPositionToStringConverter1}, ConverterParameter=Head|\\cf1 {0:0.000\\cf1 } \\cf1 {1:0.000\\cf1 } \\cf1 {2:0.000\\cf1 }}"
  3.          Background="{Binding Path=Joints, Converter={StaticResource SkeletonJointTrackingStateToSolidColorBrushConverter}, ConverterParameter=Head|LightGreen|LightYellow|LightCoral}" />

Osservando lo XAML, possiamo notare l’utilizzo di un binding anche sul background del TextBox e, questo, per fare in modo che in base allo stato di tracciamento del joint, si abbia un differente colore di sfondo. Anche in questo caso ricorriamo ad un converter:

  1. Public Class SkeletonJointTrackingStateToSolidColorBrushConverter
  2.     Implements IValueConverter
  3.  
  4.     Public Function Convert(value As Object,
  5.                             targetType As System.Type,
  6.                             parameter As Object,
  7.                             culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert
  8.         If TypeOf value Is IEnumerable(Of Joint) Then
  9.             Dim joints = CType(value, IEnumerable(Of Joint))
  10.             Dim parameters = parameter.ToString.Split("|"c)
  11.             If parameters.Count > 0 Then
  12.                 Dim jointId = parameters(0)
  13.                 Dim jointQuery = From j In joints
  14.                                 Where j.ID = CType([Enum].Parse(GetType(JointID), jointId), JointID)
  15.                                 Select j
  16.  
  17.                 If jointQuery.Any() Then
  18.                     Dim joint = jointQuery.First()
  19.                     Dim color As Color?
  20.                     Select Case joint.TrackingState
  21.                         Case JointTrackingState.Tracked
  22.                             color = If(parameters.Count() < 1, Colors.LightGreen, ColorUtility.GetColorFromName(parameters(1)))
  23.                         Case JointTrackingState.Inferred
  24.                             color = If(parameters.Count() < 2, Colors.LightYellow, ColorUtility.GetColorFromName(parameters(2)))
  25.                         Case Else
  26.                             color = If(parameters.Count() < 3, Colors.LightCoral, ColorUtility.GetColorFromName(parameters(3)))
  27.                     End Select
  28.                     If color.HasValue Then
  29.                         Dim brush = New SolidColorBrush(color.Value)
  30.                         Return brush
  31.                     Else
  32.                         Return Nothing
  33.                     End If
  34.                 Else
  35.                     Return Nothing
  36.                 End If
  37.             Else
  38.                 Return Nothing
  39.             End If
  40.             Else
  41.                 Return Nothing
  42.             End If
  43.     End Function
  44.  
  45.     Public Function ConvertBack(value As Object,
  46.                                 targetType As System.Type,
  47.                                 parameter As Object,
  48.                                 culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack
  49.         Throw New NotImplementedException
  50.     End Function
  51. End Class

Il converter è del tutto analogo al precedente con la differenza che utilizza i parametri per ricevere la stringa identificante l’id del joint da analizzare e la terna di colori da utilizzare per gli stati Tracked, Inferred e NotTracked.

Ultima cosa prima di chiudere il post riguarda la possibilità di affinare la precisione dello skeletal tracking.

Se provate ad utilizzare lo skeletal tracking, infatti, vi renderete conto che anche rimanendo immobili, i valori di posizione oscillano.

E’ possibile intervenire su questo fenomeno di jittering impostando alcune proprietà del motore di skeletal tracking. La classe SkeletonEngine esposta dalla classe Runtime tramite la proprietà omonima, consente di impostare i parametri di Smooth Transform.

Questi parametri permettono il controllo della precisione del rilevamento dello scheletro da parte del KInect e la loro disamina accurata sarà oggetto di un altro post.

Nel nostro caso potremmo modificare il costruttore nel seguente modo:

  1. Public Sub New()
  2.     If Not DesignerProperties.GetIsInDesignMode(New DependencyObject()) Then
  3.         Try
  4.             Nui = New Runtime
  5.             AddHandler Nui.DepthFrameReady, AddressOf DepthFrameReadyHandler
  6.             AddHandler Nui.SkeletonFrameReady, AddressOf SkeletonFrameHandler
  7.             Nui.Initialize(RuntimeOptions.UseDepthAndPlayerIndex Or RuntimeOptions.UseSkeletalTracking)
  8.  
  9.             Nui.SkeletonEngine.TransformSmooth = True
  10.             Dim parameters = New TransformSmoothParameters With {.Smoothing = 0.75F,
  11.                                                                 .Correction = 0.0F,
  12.                                                                 .Prediction = 0.0F,
  13.                                                                 .JitterRadius = 0.05F,
  14.                                                                 .MaxDeviationRadius = 0.04F}
  15.             Nui.SkeletonEngine.SmoothParameters = parameters
  16.  
  17.             Nui.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.DepthAndPlayerIndex)
  18.         Catch ex As Exception
  19.  
  20.         End Try
  21.     End If
  22. End Sub

A seguito dell’initialize attiviamo la gestione dello smoothing (TransformSmooth=true), istanziamo un oggetto di classe TransformSmoothParameters e impostiamo la proprietà SmoothParameters dello SkeletonEngine.

La classe TransformSmoothParameters è poco documentata e per questo motivo cercheremo di approfondire il suo utilizzo in un ulteriore post.

 

Nessun commento: