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.

 

lunedì 22 agosto 2011

Alla scoperta del Kinect: video e depth insieme per eliminare lo sfondo

Nel precedenti post (qui e qui) abbiamo visto singolarmente come utilizzare lo stream video e i dati di “distanza” che provengono dal nostro Kinect.

In questo post cercheremo di utilizzare contemporaneamente i due stream in modo da utilizzare le informazioni di distanza per modificare l’immagine video.

In particolare vedremo come (in maniera molto rozza e, sicuramente, ottimizzabile) riuscire a modificare l’immagine proveniente dallo stream video per lasciare visibili i player uniformando lo sfondo con un colore a nostra scelta.

Quello che faremo è prendere le singole immagini video e depth fornite dagli eventi del Runtime del Kinect, scandire l’immagine di depth e, ogni volta che si ha un player index pari a 0, colorare il corrispondente pixel dell’immagine video con il colore da noi scelto.

Uno dei problemi che si pongono immediatamente è che la risoluzione dell’immagine video sia diversa dalla risoluzione dell’immagine depth.

Ad esempio nel seguente pezzo di codice l’immagine video è 640x480 mentre l’immagine depth è 320x240.

  1. Try
  2.     Nui = New Runtime
  3.  
  4.     AddHandler Nui.VideoFrameReady, AddressOf VideoFrameHandler
  5.     AddHandler Nui.DepthFrameReady, AddressOf DepthFrameReadyHandler
  6.  
  7.     Nui.Initialize(RuntimeOptions.UseDepthAndPlayerIndex Or RuntimeOptions.UseColor)
  8.  
  9.     Nui.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color)
  10.     Nui.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.DepthAndPlayerIndex)
  11.  
  12. Catch ex As Exception
  13.  
  14. End Try

In alcuni casi non si può avere uguaglianza di risoluzione anche forzando i parametri del metodo Open degli stream.

Ad esempio nel mio pc non è ammesso avere 320x240 per entrambe le sorgenti (ottengo una eccezione in fase di open).

Nel caso mostrato nel codice precedente dobbiamo trovare qualche modo per correlare un pixel dell’immagine depth con uno o più pixel dell’immagine video.

L’SDK del Kinect ci mette a disposizione il metodo GetColorPixelCoordinatesFromDepthPixel della classe NuiCamera che consente di ottenere le coordinate x,y del punto video a partire dalle coordinate x,y del punto depth.

Nel nostro caso non utilizzeremo questa funzione perché andremo a calcolarci da soli i punti corrispondenti sull’immagine video.

In maniera molto grossolana (ci manterremo sul semplice essendo questo post didattico), ogni punto dell’immagine depth corrisponde a 4 punti dell’immagine video (visto che la risoluzione della video è 4 volte quella della depth).

Per questo dovremo ricavare le coordinate del punto in alto a sinistra di questo quadrato e prendere i 3 punti adiacenti.

Se indichiamo con xd e yd le coordinate di un punto dell’immagine depth, e con xv e yv le coordinate del punto in alto a sinistra dell’immagine video, potremmo considerare:

clip_image002

e, di conseguenza dovremmo elaborare i 4 punti:

clip_image002[4]

L’algoritmo che utilizzeremo sarà quindi:

  1. per ogni x, y dell’immagine depth (con x compreso tra 0 e 319 e y compreso tra 0 e 239)
  2. recupereremo il player index come visto nel precedente post;
  3. se il player index è zero, andremo ad impostare i bytes dell’immagine video corrispondenti ai 4 punti di cui sopra con i valori corrispondenti al colore di sfondo desiderato.

Il codice che implementa ciò è il seguente:

  1. Private Sub ElaborateImage(videoFrame As ImageFrame, depthFrame As ImageFrame)
  2.     Dim depthIndex = 0
  3.     Dim videoIndex = 0
  4.     For x = 0 To depthFrame.Image.Width - 1
  5.         For y = 0 To depthFrame.Image.Height - 1
  6.             depthIndex = (y * depthFrame.Image.Width + x) * depthFrame.Image.BytesPerPixel
  7.             Dim playerIndex = depthFrame.Image.Bits(depthIndex) And 7
  8.             If playerIndex = 0 Then
  9.                 Dim x1 = 2 * x
  10.                 Dim x2 = 2 * x + 1
  11.                 Dim y1 = 2 * y
  12.                 Dim y2 = 2 * y + 1
  13.  
  14.                 SetPixelValue(videoFrame, x1, y1, Colors.Red)
  15.                 SetPixelValue(videoFrame, x1, y2, Colors.Red)
  16.                 SetPixelValue(videoFrame, x2, y1, Colors.Red)
  17.                 SetPixelValue(videoFrame, x2, y2, Colors.Red)
  18.             End If
  19.         Next
  20.     Next
  21.  
  22.     Me.ElaboratedImage.Source = videoFrame.ToBitmapSource()
  23. End Sub

dove il metodo SetPixelValue altro non fa che impostare i 4 bytes del pixel video con le componenti del colore scelto:

  1. Private Sub SetPixelValue(imageFrame As ImageFrame, x As Integer, y As Integer, color As Color)
  2.     Dim index = (y * imageFrame.Image.Width + x) * imageFrame.Image.BytesPerPixel
  3.     imageFrame.Image.Bits(index) = color.B
  4.     imageFrame.Image.Bits(index + 1) = color.G
  5.     imageFrame.Image.Bits(index + 2) = color.R
  6.     imageFrame.Image.Bits(index + 3) = color.A
  7. End Sub

Il risultato che otteniamo è un’immagine rossa che vedrà apparire le figure dei player nel momento in cui il KInect le “aggancia”.

image

Il bordo che si forma attorno alla figura è dovuto all’algoritmo di recupero dei punti video molto semplicistico e al fatto che le immagini che andiamo a prendere potrebbero non essere sincronizzate tra loro. Nel nostro semplice esempio non andiamo assolutamente a controllare il timestamp delle due immagini e, poichè i due stream potrebbero avere frame rate differente, le immagini potrebbero non coincidere e creare la fastidiosa scia.

Inoltre teniamo presente che i sensori video e di profondità che monta il Kinect sono posizionati in due differenti punti dell’”aggeggio” e, quindi, soffrono dell’errore di parallasse che fa si che le due immagini non possono mai essere sovrapponibili senza errore.



lunedì 8 agosto 2011

Alla scoperta del Kinect : questione di profondità

Nei due precedenti post (link e link) abbiamo fatto conoscenza con “l’aggeggio” kinect e visto come sia possibile, in maniera molto semplice, gestire lo stream video proveniente dalla camera.

In questo post diamo un’occhiata alla capacità che ha il Kinect di fornire frame in cui l’immagine non è la rappresentazione fedele della realtà che ci circonda ma la rappresentazione bidimensionale della distanza degli oggetti dai sensori di profondità.

L’aggeggio, infatti, dispone di un sensore di profondità che è in grado di fornirci la distanza dei punti inquadrati da se stesso e, in più, è in grado di dirci a quale “player” fa riferimento ogni singolo pixel.

Ma andiamo con ordine.

Per abilitare la ricezione del depth stream è necessario:

  1. Istanziare la classe Runtime;
  2. Agganciare il gestore dell’evento DepthFrameReady;
  3. Inizializzare l’istanza della Runtime scegliendo una delle opzioni che abilitano il sensore di profondità;
  4. Aprire lo stream dei dati relativi alla profondità.

A livello di codice (abbiamo già visto nei precedenti post) significa:

  1. Nui = New Runtime
  2. AddHandler Nui.DepthFrameReady, AddressOf DepthFrameHandler
  3. Nui.Initialize(RuntimeOptions.UseDepthAndPlayerIndex)
  4. Nui.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.DepthAndPlayerIndex)

dove Nui è una variabile di tipo Runtime.

Per quanto riguarda le RuntimeOptions, abbiamo la possibilità di scegliere di ricevere i soli dati di profondità (RuntimeOptions.UseDepth) o i dati di profondità e il numero del player (RuntimeOptions.UseDepthAndPlayerIndex).

La differenza tra le due opzioni si manifesta nella differenza della struttura dei dati che ci arrivano all’interno dell’evento DepthFrameReady.

In questo evento, come già visto nei precedenti post, riceviamo l’array dei bytes costuenti l’immagine recuperata dai sensori del Kinect.

L’array è contenuto nella proprietà Image (di tipo PlanarImage) dell’istanza di ImageFrame contenuta all’interno dell’argomento dell’evento DepthFrameReady:

image

Per le immagini fornite dal sensore di profondità, ogni pixel è descritto da 2 Bytes (totale 16 bits), la struttura dei quali cambia in base al fatto che stiamo chiedendo il player index o meno.

In entrambi i casi, l’array contiene la distanza (in mm) per ogni punto dell’immagine, e questa varia da un minimo di 800/850 mm ad un massimo di circa 4000 mm. Se “l’aggeggio” non riesce a recuperare la distanza (ad esempio per una fonte di luce forte alle spalle del soggetto inquadrato) la profondità restituita sarà 0.

La dimensione dell’array è, quindi, pari al prodotto delle dimensioni dell’immagine (ad esempio 320x240) per 2 ed è strutturato per righe a partire dall’angolo in alto a sinistra.

Se stiamo utilizzando RuntimeOptions.UseDepth, i due bytes contengono esattamente la distanza (espressa in mm) dal sensore) con il primo byte che contiene la parte meno significativa, mentre il secondo la più significativa:

Bytes senza player index

In questo caso, dati i due bytes dell’array, la distanza è calcolabile nel seguente modo:

  1. depth = CLng(frame.Image.Bits(arrayIndex)) + (CLng(frame.Image.Bits(arrayIndex + 1)) << 8)

La conversione a CLng è opportuna in quanto l’operazione di shift (<<) farebbe perdere i bit più significativi.

Se stiamo utilizzando RuntimeOptions.UseDepthAndPlayerIndex, invece, i primi tre bit del byte meno significativo (il primo dei due) sono utilizzati per contenere l’indice del player a cui fa riferimento il dato di profondità (se non c’è un player o Kinect non ha agganciato un player abbiamo il valore 0):

Bytes con player index

In questo caso la distanza si calcola nel seguente modo:

  1. depth = (CLng(frame.Image.Bits(arrayIndex)) >> 3) + (CLng(frame.Image.Bits(arrayIndex + 1)) << 5)

Per fare un esempio concreto, supponiamo di voler calcolare la distanza e il player index del punto (160,120) dell’immagine.

In questo caso i due bytes interessati sono il 160*320+120=51320 e 51321:

Esempio con player index

Nell’esempio abbiamo come player index il valore 2 (010 binario) mentre la profondità è 2009 (0011111011001 in binario).

Nella solution che allego al post è presente un semplice programma che ci consente di selezionare, tramite il mouse, un punto sull’immagine (identificato da un pallino giallo) e di avere in tempo reale le informazioni di player index e profondità:

SNAGHTML1c545cf




giovedì 4 agosto 2011

Alla scoperta del Kinect: calcoliamo l’FPS degli stream video.

Abbiamo visto nel precedente post della serie come poter utilizzare il nostro Kinect per catturare gli stream Video e Depth e visualizzarli.

Il questo post vogliamo vedere due possibili modi per calcolare l’effettivo FPS di tali stream, ovvero quanti frame nell’unità di tempo ci arrivano.

Questa funzione non esiste out-of-the-box tra le funzionalità esposte dall’SDK, ma è molto semplice calcolare tale numero basandoci sulle istanze della classe ImageFrame che ci arrivano nei gestori degli eventi DepthFrameReady e VideoFrameReady.

Per chi avesse perso il precedente post, la classe Runtime (fulcro dell’SDK di Kinect) solleva uno dei precedenti eventi ogni volta che è disponibile una immagine video o depth.

Possiamo gestire il flusso delle immagini tramite dei gestori di evento:

  1. Private Sub DepthFrameHandler(sender As Object, e As ImageFrameReadyEventArgs)
  2.     .
  3.     .
  4.     .
  5. End Sub
  6.  
  7. Private Sub VideoFrameHandler(sender As Object, e As ImageFrameReadyEventArgs)
  8.     .
  9.     .
  10.     .
  11. End Sub

L’argomento degli eventi ci fornisce un’istanza della classe ImageFrame che contiene informazioni molto importanti che ci possono permettere di calcolare il Frame rate del filmato.

La classe ImageFrameReadyEventArgs ha la seguente struttura:

image

Nel codice di seguito useremo l’attributo Timestamp della classe ImageFrame per il calcolo dell’Fps.

Cominciamo definendo l’interfaccia che deve implementare quello che noi definiremo “Calcolatore di FPS”:

  1. Imports Microsoft.Research.Kinect.Nui
  2. Imports System.Diagnostics.Contracts
  3.  
  4. <ContractClass(GetType(ContractIFpsCalculator))>
  5. Public Interface IFpsCalculator
  6.  
  7.     ReadOnly Property CurrentFps As Integer
  8.  
  9.     Sub SetCurrentImageFrame(image As ImageFrame)
  10.  
  11. End Interface
  12.  
  13. <ContractClassFor(GetType(IFpsCalculator))>
  14. Public Class ContractIFpsCalculator
  15.     Implements IFpsCalculator
  16.  
  17.  
  18.     Public ReadOnly Property CurrentFps() As Integer Implements IFpsCalculator.CurrentFps
  19.         Get
  20.             Throw New NotImplementedException()
  21.         End Get
  22.     End Property
  23.  
  24.     Public Sub SetCurrentImageFrame(ByVal image As ImageFrame) Implements IFpsCalculator.SetCurrentImageFrame
  25.         Contract.Requires(Of ArgumentNullException)(image IsNot Nothing, "Image must be not null")
  26.         Throw New NotImplementedException()
  27.     End Sub
  28. End Class

L’interfaccia IFpsCalculator espone una proprietà CurrentFps che restituisce, istante per istante, l’FPS dello streaming e un metodo SetCurrentImageFrame che utilizziamo per comunicare al calcolatore quale è l’ImageFrame che stiamo elaborando.

Su questa interfaccia ho definito dei contratti in modo da garantirmi che il metodo SetCurrentImageFrame non possa essere mai chiamato (pena la sollevazione di un eccezione ArgumentNullException) con un parametro Nothing.

La prima implementazione di FpsCalculator si basa sul calcolo “istantaneo” del frame rate: di fatto, dato l’intervallo di tempo (in millisecondi) tra le ultime due immagini, viene calcolato il frame rate con la seguente formula:

clip_image002[4]

dove:

  • FPSn è il framerate (calcolato) tra l’n-esimo e l’(n-1)-esimo frame;
  • tsn è il timestamp (in millisecondi) dell’n-esimo frame;
  • tsn-1 è il timestamp dell’(n-1)-esimo frame.

Evidentemente il framerate calcolato è un numero decimale che, per convenienza, approssimeremo ad un intero.

La classe che implementa questo algoritmo è la seguente:

  1. Public Class FpsCalculator
  2.     Implements IFpsCalculator
  3.  
  4.     Private _LastTimestampVideo As Long? = Nothing
  5.     Private _SecondLastTimestampVideo As Long? = Nothing
  6.  
  7.     Public ReadOnly Property Fps As Integer Implements IFpsCalculator.CurrentFps
  8.         Get
  9.             Return CalculateFps()
  10.         End Get
  11.     End Property
  12.  
  13.     Private Function CalculateFps() As Integer
  14.         Dim fps As Integer = 0
  15.         If _LastTimestampVideo.HasValue And _SecondLastTimestampVideo.HasValue Then
  16.             Dim deltaMillisec = _LastTimestampVideo.Value - _SecondLastTimestampVideo.Value
  17.             fps = CInt(Math.Floor(1000.0 / CDbl(deltaMillisec)))
  18.         End If
  19.         Return fps
  20.     End Function
  21.  
  22.     Public Sub SetCurrentImageFrame(ByVal image As ImageFrame) Implements IFpsCalculator.SetCurrentImageFrame
  23.         Me._SecondLastTimestampVideo = Me._LastTimestampVideo
  24.         Me._LastTimestampVideo = image.Timestamp
  25.     End Sub
  26. End Class

Per il calcolo effettivo tramite la precedente formula, memorizziamo gli ultimi due timestamp e, solo quando li abbiamo tutti e due, procediamo al calcolo (CalculateFps()).

Un altro modo per poter calcolare il framerate è quello di conteggiare quanti frame vengono elaborati nell’unità di tempo.

Per fare questo è sufficiente memorizzare il timestamp iniziale e, ad ogni frame elabrorato (metodo Set CurrentImageFrame()) verificare che il timestamp di tale frame non si discosti per più di un secondo da quello di partenza. Se lo scostamento è minore di un secondo viene incrementato il contatore di frame nell’unità di tempo mentre se lo scostamento è maggiore di un secondo (1000 millisecondi) viene valorizzata la proprietà CurrentFps con il valore del contatore.

La classe che implementa questo algoritmo è la seguente:

  1. Imports Microsoft.Research.Kinect.Nui
  2.  
  3. Public Class FpsCalculator2
  4.     Implements IFpsCalculator
  5.  
  6.     Private _StartTimeStamp As Long? = Nothing
  7.     Private _FrameCount As Integer = 0
  8.  
  9.     Private _CurrentFps As Integer = 0
  10.     Public ReadOnly Property CurrentFps As Integer Implements IFpsCalculator.CurrentFps
  11.         Get
  12.             Return _CurrentFps
  13.         End Get
  14.     End Property
  15.  
  16.     Public Sub SetCurrentImageFrame(ByVal image As ImageFrame) Implements IFpsCalculator.SetCurrentImageFrame
  17.         If _StartTimeStamp.HasValue Then
  18.             If image.Timestamp - _StartTimeStamp.Value > 1000 Then
  19.                 _CurrentFps = _FrameCount
  20.                 _StartTimeStamp = image.Timestamp
  21.                 _FrameCount = 0
  22.             Else
  23.                 _FrameCount += 1
  24.             End If
  25.         Else
  26.             _StartTimeStamp = image.Timestamp
  27.         End If
  28.     End Sub
  29. End Class

Il primo algoritmo aggiorna l’FPS ad ogni frame e genera, quindi una variazione costante dello stesso. Il secondo, invece, fornisce un valore nuovo di FPS ogni secondo dando luogo ad una rilevazione più stabile.

La classe ImageFrame mette a disposizione, come possiamo vedere dalla precedente immagine, la proprietà FrameNumber che contiene il numero di frame corrispondente all’immagine. Nella refresh dell’SDk appena uscita (scaricabile da qui) questo numero rappresenta realmente il numero del frame dello stream. Per questo, in teoria, possiamo utilizzare questo numero per calcolare l’FPS. In questo caso memorizziamo il numero del frame da cui partiamo, ragioniamo sui timestamp in maniera analoga a quanto fatto nell’FtpCalculator2 e facciamo la differenza tra il numero di frame:

  1. Imports Microsoft.Research.Kinect.Nui
  2.  
  3. Public Class FpsCalculator3
  4.     Implements IFpsCalculator
  5.  
  6.     Private _StartTimeStamp As Long? = Nothing
  7.     Private _FrameNumberStart As Integer = 0
  8.  
  9.     Private _CurrentFps As Integer = 0
  10.     Public ReadOnly Property CurrentFps As Integer Implements IFpsCalculator.CurrentFps
  11.         Get
  12.             Return _CurrentFps
  13.         End Get
  14.     End Property
  15.  
  16.  
  17.     Public Sub SetCurrentImageFrame(ByVal image As ImageFrame) Implements IFpsCalculator.SetCurrentImageFrame
  18.         If _StartTimeStamp.HasValue Then
  19.             If image.Timestamp - _StartTimeStamp.Value > 1000 Then
  20.                 _CurrentFps = image.FrameNumber - _FrameNumberStart
  21.                 _StartTimeStamp = image.Timestamp
  22.                 _FrameNumberStart = image.FrameNumber
  23.             End If
  24.         Else
  25.             _StartTimeStamp = image.Timestamp
  26.             _FrameNumberStart = image.FrameNumber
  27.         End If
  28.     End Sub
  29. End Class

 

lunedì 1 agosto 2011

Alla scoperta del Kinect: la classe Runtime

La classe Runtime (del namespace Microsoft.Research.KInect.Nui e contenuta nell’assemlby Microsoft.Research.Kinect) è per la nostra avventura come lo specchio per Alice: la porta peser entrare in un altro mondo……quello del Kinect!!!

A parte gli scherzi, la classe Runtime è fondamentale perché espone tutte le funzionalità che possiamo utilizzare del Kinect per quel che riguarda la parte delle gesture.

Di fatto un’istanza della classe Runtime gestisce un device connesso e la sua struttura è mostrata nella seguente immagine:

image

Per poter accedere alle funzionalità del sensore video, di profondità e allo skeleton engine (il “robo” che fa si che il Kinect individui la struttura di  un corpo umano che si agita come un forsennato davanti a lui) è sufficiente creare un’istanza della classe Runtime.

Ma procediamo con ordine, innanzitutto per avere visibilità sulla classe Runtime è necessario referenziare la libreria Microsoft.Research.Kinect e lo facciamo, come accade usualmente, utilizzando l’opzione di Visual Studio (menù Add Reference che si ottiene con il tasto destro):

SNAGHTML513572e

SNAGHTML515b803

A questo punto possiamo istanziare la classe:

  1. Dim nui = New Runtime

Istanziare la classe Runtime non è sufficiente per poter accedere ai sensori, è necessario inizializzare la classe con il metodo Initialize():

  1. Dim nui = New Runtime
  2. nui.Initialize(RuntimeOptions.UseColor or  RuntimeOptions.UseDepth)

In questo caso ho richiesto alla Runtime l’utilizzo dello stream video e di quello della profondità.

Prima di procedere analizziamo a fondo cosa ci mette a disposizione la Runtime a livello di proprietà, metodi ed eventi.

Le proprietà della Runtime

  • InstanceIndex : è un intero che identifica il numero di istanza della Runtime. Ogni device connesso al pc ha la sua Runtime e questo numero ci permette di identificare il singolo device;
  • NuiCamera : è la proprietà che ci consente di accedere all’oggetto Camera per poter, ad esempio, modificare l’inclinazione;
  • VideoStream : espone un’istanza di ImageStream che descive lo stream della video camera del Kinect. Vedremo in dettaglio in un altro post la classe ImageStream, per ora ci basti immaginarlo come uno stream che fornisce i fotogrammi rilevati dalla videocamera;
  • DepthStream : anche in questa proprietà espone un’istanza di ImageStream ma questa volta riguarda le immagini di profondità, ovvero le immagini in cui il Kinect traduce la distanza degli oggetti da se stesso;
  • SkeletonEngine : è la proprietà che espone il motore che gestisce il riconoscimento della figura umana (ce ne occuperemo in un altro post).

I metodi della Runtime

  • Initialize : abbiamo già visto questo metodo ed ha lo scopo di “avviare” l’aggetto richiedendo le funzionalità di cui abbiamo bisogno. Il parametro che accetta come argomento è del tipo RuntimeOptions, un’enumerazione flag con i seguenti valori:

image

  • Uninitialize : esegue lo shutdown dell’istanza di runtime. Una volta eseguito lo shutdown della Runtime, pur se l’oggetto managed non è Nothing, non è più possibile tentare di inizializzarlo in quanto viene rilasciata definitivamente tutta la parte unmanaged che c’è dietro. Se si prova si ottiene un’eccezione.

Gli eventi della Runtime

Gli eventi della classe Runtime sono i membri della classe che, di fatto, ci permettono un primo uso, anche se banale della stessa:

  • VideoFrameReady : viene sollevato quando un’immagine dello stream video è pronta per essere utilizzata;
  • DepthFrameReady : viene sollevato quando un’immagine dello stream di profondità è pronta per essere utilizzata;
  • SkeletonFrameReady : viene sollevato quando un’immagine del riconoscimento della figura umana è stata elaborata dallo skeleton engine ed è pronta per essere utilizzata.

I primi due eventi hanno per argomento la classe ImageFrameReadyEventArgs, mentre il terzo ha come argomento la classe SkeletonFrameReadyEventArgs.

Vedremo in dettaglio la struttura della SkeletonFrameReadyEventArgs in un prossimo post, mentre la ImageFrameReadyEventArgs ha la seguente struttura:

image

 

Di fatto l’argomento dell’evento ci fornisce l’immagine dello stream corrispondente che possiamo elaborare a nostro piacimento.

Quindi, istanziamo la Runtime, la inizializziamo, gestiamo gli eventi di cui sopra e siamo in grado, almeno per ora di riportare a video ciò che il Kinect sta inquadrando. In realtà, tra l’inizializzazione della Runtime e la fruizione delle immagini manca uno step fondamentale ovvero l’apertura degli stream VideoStream e DepthStream che se non aperti non comincerebbero a produrre immagini.

Il  metodo Open ha la seguente sintassi:

image

dove:

  • streamType : ci consente di definire la tipologia di stream che vogliamo ricevere;
  • poolSize : è il numero di frame che il runtime di NUI deve memorizzare. Il valore massimo è 4, nella maggior parte delle applicazioni è sufficiente 2;
  • resolution : dichiara la risoluzione che si vuole per lo stream. Sono disponibili svariate risoluzioni ma se si hanno più stream aperti (ad esempio video e depth) non tutte le combinazioni funzionano;
  • image : definisce la tipologia dell’immagine. Ad esempio DepthAndPlayerIndex cioè un’immagine che contiene al suo interno i dati di distanza e del numero del giocatore.

Andremo in dettaglio dello stream di distanza in un altro post prossimamente.

Nell’esempio allegato a questo post trovate una banale applicazione WPF, sviluppata con il pattern MVVM, che visualizza i due stream illustrati in precedenza.

La creazione del ViewModel relativo alla finestra principale è la seguente:

  1. Public Sub New()
  2.     If Not DesignerProperties.GetIsInDesignMode(New DependencyObject()) Then
  3.         Nui = New Runtime
  4.  
  5.         AddHandler Nui.DepthFrameReady, AddressOf DepthFrameHandler
  6.         AddHandler Nui.VideoFrameReady, AddressOf VideoFrameHandler
  7.  
  8.         Nui.Initialize(RuntimeOptions.UseColor Or RuntimeOptions.UseDepthAndPlayerIndex)
  9.  
  10.         Nui.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution640x480, ImageType.DepthAndPlayerIndex)
  11.         Nui.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color)
  12.     End If
  13. End Sub

dove Nui è un attributo privato del ViewModel di tipo Runtime.

Per la gestione delle immagini provenienti dai due stream mi sono affidato ad un toolkit sviluppato da Coding4Fun che fornisce degli extension methods per la conversione tra immagini Kinect e immagini WPF. Il toolkit si chiama “Kinect toolkit for WPF” (ne esiste una versione anche per Windows Form) e si può referenziare nel nostro progetto tramite NuGet:

SNAGHTML5752519

In questo caso il ViewModel espone le ultime due immagini dei due stream che possono essere messe in binding con un normale controllo Image dell’interfaccia:

  1. Imports System.ComponentModel
  2. Imports Microsoft.Research.Kinect.Nui
  3. Imports Coding4Fun.Kinect.Wpf
  4.  
  5. Public Class MainWindowViewModel
  6.     Implements INotifyPropertyChanged
  7.  
  8.     Private Nui As Runtime
  9.  
  10.     Public Sub New()
  11.         If Not DesignerProperties.GetIsInDesignMode(New DependencyObject()) Then
  12.             Nui = New Runtime
  13.  
  14.             AddHandler Nui.DepthFrameReady, AddressOf DepthFrameHandler
  15.             AddHandler Nui.VideoFrameReady, AddressOf VideoFrameHandler
  16.  
  17.             Nui.Initialize(RuntimeOptions.UseColor Or RuntimeOptions.UseDepth)
  18.  
  19.             Nui.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color)
  20.             Nui.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.Depth)
  21.  
  22.         End If
  23.     End Sub
  24.  
  25.     Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
  26.  
  27.     Protected Sub OnNotifyPropertyChanged(propertyName As String)
  28.         RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
  29.     End Sub
  30.  
  31.     Private _VideoImage As ImageSource
  32.     Public Property VideoImage As ImageSource
  33.         Get
  34.             Return _VideoImage
  35.         End Get
  36.         Protected Set(value As ImageSource)
  37.             Me._VideoImage = value
  38.             OnNotifyPropertyChanged("VideoImage")
  39.         End Set
  40.     End Property
  41.  
  42.     Private _DepthImage As ImageSource
  43.     Public Property DepthImage As ImageSource
  44.         Get
  45.             Return _DepthImage
  46.         End Get
  47.         Protected Set(value As ImageSource)
  48.             Me._DepthImage = value
  49.             OnNotifyPropertyChanged("DepthImage")
  50.         End Set
  51.     End Property
  52.  
  53.     Private Sub DepthFrameHandler(sender As Object, e As ImageFrameReadyEventArgs)
  54.         Me.DepthImage = e.ImageFrame.ToBitmapSource()
  55.     End Sub
  56.  
  57.     Private Sub VideoFrameHandler(sender As Object, e As ImageFrameReadyEventArgs)
  58.         Me.VideoImage = e.ImageFrame.ToBitmapSource()
  59.     End Sub
  60.  
  61.  
  62.  
  63.     Protected Overrides Sub Finalize()
  64.         MyBase.Finalize()
  65.         Nui.Uninitialize()
  66.     End Sub
  67. End Class