In questo articolo cominceremo ad analizzare le funzionalità esposte dall’SDK ed in particolare cominceremo a vedere la classe PXCMSenseManager e le funzionalità riguardanti gli stream video.
L’articolo si riferisce alla versione 6.0.x dell’SDK di Intel® RealSense™ attualmente scaricabile all’indirizzo https://software.intel.com/en-us/intel-realsense-sdk/download
La classe PXCMSenseManager
La classe PXCMSenseManager, come già visto in un precedente articolo, ci fornisce delle funzionalità di base da utilizzare negli scenari più comuni e accessibili in maniera più semplice ed immediata di quanto non si possa fare utilizzando la classe PXCMSession.
La classe PXCMSenseManager, di fatto, consente di utilizzare facilmente gli stream provenienti dalla camera o i moduli face tracking o di gesture recognition.
In questo articolo cominceremo a dare un’occhiata alla classe PXCMSenseManager e cercheremo di utilizzarla per recuperare gli stream video colore, profondità e infrarossi.
Nella figura seguente è mostrato lo schema della classe PXCMSenseManager:
La PXCMSenseManager espone anche un’istanza di PXCMSession e una istanza di PXCMCaptureManager.
La prima viene creata al momento della creazione dell’istanza di PXCMSenseManager, mentre la seconda viene creata solamente se si legge la proprietà di sola lettura captureManager.
La creazione di una nuova istanza di PXCMSenseManager avviene, come di consuetudine all’interno dell’SDK di RealSense™, utilizzando un factory method della classe PXCMSenseManager stessa:
SenseManager = PXCMSenseManager.CreateInstance()
Una volta istanziata la classe PXCMSenseManager, dobbiamo abilitare i moduli che ci interessano e lo possiamo fare utilizzando uno dei metodi EnableXXX. Nel caso degli stream video, il metodo che utilizzeremo sarà EnableStream o EnableStreams.
Nell’esempio seguente vediamo come abilitare tutti gli stream disponibili nella camera F200 (colori, infrarossi e profondità) utilizzando il metodo EnableStream:
SenseManager.EnableStream(PXCMCapture.StreamType.STREAM_TYPE_COLOR, 1280, 720) SenseManager.EnableStream(PXCMCapture.StreamType.STREAM_TYPE_DEPTH, 640, 480) SenseManager.EnableStream(PXCMCapture.StreamType.STREAM_TYPE_IR, 640, 480)
La firma del metodo EnableStream prevede la tipologia dello stream, le dimensioni del frame che si desiderano e, eventualmente, il frame rate con cui si desidera lavorare.
Se non mettiamo il frame rate, sarà l’SDK a decidere quello migliore in base alle caratteristiche di impegno della macchina su cui sta girando l’applicazione.
L’elenco degli stream possibili si trova nell’enumerazione PXCMCapture.StreamType:
I valori STREAM_TYPE_LEFT e STREAM_TYPE_RIGHT rappresentano, rispettivamente, lo stream sinistro e destro della camera stereoscopica disponibile nel modello R200.
In alternativa al metodo EnableStream possiamo utilizzare il metodo EnableStreams che accetta in input una struttura dati PXCMVideoModule.DataDesc al cui interno sono inserite tutte le informazioni necessarie all’abilitazione degli stream (di fatto le stesse che passeremmo ai singoli metodi EnabeStream).
Una volta abilitati gli stream, dobbiamo inizializzare la PXCMSenseManager utilizzando il metodo Init(). Questo metodo, come la stragrande maggioranza dei metodi dell’SDK, ritorna un valore di tipo pxcmStatus che identifica se si è verificato un errore o meno:
If SenseManager.Init() <> pxcmStatus.PXCM_STATUS_NO_ERROR Then MessageBox.Show("Errore nell'inizializzazione della camera") Close() End If
Oppure, utilizzando un metodo di estensione messo a disposizione dal wrapper della libreria C++ dell’SDK verso il mondo .NET:
If SenseManager.Init().IsError() Then MessageBox.Show("Errore nell'inizializzazione della camera") Close() End If
Se la chiamata al metodo Init() ha successo, la camera avvia i propri stream e la nostra applicazione ha la possibilità di ottenere i vari frame che li compongono.
Per fare ciò possiamo utilizzare due approcci:
- Polling: implementiamo una sorta di loop (tipo il game loop che si utilizza nei giochi) all’interno del quale recuperiamo i frame, li elaboriamo (ad esempio creando l’immagine da visualizzare) e aggiorniamo l’interfaccia;
- CallBack: utilizziamo l’infrastruttura basata su call-back messa disposizione dalla classe PCXMSenseManager per essere informati dall’SDK quando nuovi frame sono disponibili. A quel punto possiamo elaborarli e aggiornare la UI (una sorta di eventi). Vedremo che anche in questo caso abbiamo la necessità di avere un loop “infinito” ma l’onere di recuperare i frame e restituirceli è a carico dell’SDK (il loop ha lo scopo di far partire l’acquisizione dei frame.
L’approccio Polling ha senso nei giochi e in tutte quelle applicazioni in cui abbiamo necessità del pieno controllo sul recupero dei frame.
L’approccio “ad eventi”, invece, svincola lo sviluppatore dall’implementare la logica di recupero dei frame nel loop, lasciandogli solo l’incombenza di elaborarli all’interno della calback assegnata al SenseManager.
Polling
Come già detto in precedenza, in questo tipo di approccio, è nostro compito istruire un loop “infinito” che, ad ogni iterazione, recuperi i frame video che ci interessano, li elabori costruendo delle immagini (o qualsiasi altra elaborazione) e visualizzi queste immagini all’interno della UI.
Il loop, inoltre, deve “girare” in un thread separato rispetto all’interfaccia grafica per non bloccare la stessa.
Fortunatamente il framework .NET ci viene in aiuto fornendoci la classe Task e, quindi, realizzare un loop “infinito” in un thread separato si riduce a scrivere:
Private PollingTask As Task Private PollingTaskCancellationToken As CancellationToken Private TaskCancellationTokenSource As CancellationTokenSource Private Sub ConfigurePollingTask() TaskCancellationTokenSource = New CancellationTokenSource() PollingTaskCancellationToken = TaskCancellationTokenSource.Token PollingTask = New Task(AddressOf PollingCode) PollingTask.Start() End Sub Private Sub PollingCode() While Not PollingTaskCancellationToken.IsCancellationRequested '' Aquisizione frame'' Elaborazione frame'' Visualizzazione frame End While End Sub
PollingTask è l’istanza di Task che si occupa di “contenere” il thread del loop mentre PollingTaskCancellationToken è il cancellation token utilizzato dall’applicazione per comunicare al loop “infinito” (che, quindi, infinito non è) che deve chiudere (ad esempio quando l’applicazione viene chiusa).
Il loop che ci interessa e, in cui, viene interrogata la camera, si trova si trova nel metodo PollingCode.
Il primo step da eseguire è quello di richiedere all’SDK di poter accedere ai frame correnti e per fare ciò facciamo uso del metodo AcquireFrame() della classe PXCMSenseManager:
If SenseManager.AcquireFrame().IsSuccessful() Then'' SenseManager.ReleaseFrame() End If
Il metodo AcquireFrame() non restituisce alcun frame, semplicemente dice all’SDK di recuperare i frame che ci interessano. In questo modo l’SDK non alloca alcuna memoria fino a che non abbiamo veramente la necessità di recuperare i frame.
Il metodo ReleaseFrame() è fondamentale perché’ comunica all’SDK che abbiamo terminato il nostro lavoro e che può rilasciare gli eventuali frame da noi recuperati.
Per recuperare effettivamente i frame possiamo utilizzare il metodo QuerySample() della PXCMSenseManager:
Dim sample = SenseManager.QuerySample()
Il risultato di questa chiamata è un’istanza della classe PXCMCapture.Sample:
La classe Sample espone i 5 possibili stream già visti in precedenza in 5 differenti proprietà che possiamo interrogare per ottenere istanze della classe PXCMImage:
La classe PXCMImage astrae il concetto di immagine, espone dei metadati della stessa (nella proprietà info) e fornisce una serie di metodi utili per poterla effettivamente recuperare e gestire.
Tra questi citiamo:
- AcquireAccess: permette di ottenere l’istanza di PXCMImage.ImageData in cui sono effettivamente contenuti i byte dello stream
- ReleaseAccess: permette di rilasciare, in maniera corretta, una istanza di ImageData precedentemente recuperata.
Grazie a questi due metodi possiamo recuperare effettivamente la bitmap da visualizzare:
Private Function GetImage(image As PXCMImage) As WriteableBitmap Dim imageData As PXCMImage.ImageData = Nothing Dim returnImage As WriteableBitmap = Nothing Dim width = 0 Dim height = 0 If image.AcquireAccess(PXCMImage.Access.ACCESS_READ, PXCMImage.PixelFormat.PIXEL_FORMAT_RGB32, imageData).IsSuccessful() Then width = Convert.ToInt32(imageData.pitches(0) \ 4) height = image.info.height returnImage = imageData.ToWritableBitmap(width, height, 96, 96) image.ReleaseAccess(imageData) End If Return returnImage End Function
Nella funzione sfruttiamo il metodo ToWritableBitmap() per ottenere una istanza di WritableBitmap da poter visualizzare nella UI.
Questa funzione è utilizzabile per ogni stream della camera, in questo modo il nostro metodo che elabora il Sample ricavato dalla PXCMSenseManager diventa:
Private Sub ElaborateSample(ByVal sample As PXCMCapture.Sample) If sample Is Nothing Then Return Dim imageRGB As WriteableBitmap = Nothing Dim imageIR As WriteableBitmap = Nothing Dim imageDepth As WriteableBitmap = Nothing Dim pcxmImage As PXCMImage pcxmImage = sample.color If sample.color IsNot Nothing Then imageRGB = GetImage(sample.color) imageRGB.Freeze() End If If sample.ir IsNot Nothing Then imageIR = GetImage(sample.ir) imageIR.Freeze() End If If sample.depth IsNot Nothing Then imageDepth = GetImage(sample.depth) imageDepth.Freeze() End If Dispatcher.Invoke(Sub() Me.ImageRGB = imageRGB Me.ImageIR = imageIR Me.ImageDepth = imageDepth End Sub) End Sub
Se i sample degli stream sono presenti (non Nothing), creiamo l’immagine e impostiamo delle proprietà che finiranno sull’interfaccia grazie al binding di WPF.
Osserviamo che la chiamata al metodo Freeze() e l’uso del Dispatcher è necessario in quanto stiamo girando in un thread che non è quello principale della UI.
Callback
La gestione con callback prevede la possibilità di impostare, in una proprietà della PXCMSenseManager, un handler ad una funzione che verrà richiamata tutte le volte che un frame o un insieme di frame è pronto.
Il procedimento si basa sul creare una istanza della classe PXCMSenseManager.Handler
Ed impostare l’opportuna proprietà con il delegate che verrà invocato nel momento in cui l’istanza di PXCMSenseManager ha la necessità di comunicare con l’esterno.
Nel nostro esempio imposteremo la proprietà onNewSample:
Dim handler = New PXCMSenseManager.Handler() handler.onNewSample = AddressOf OnNewSample dove il nostro delegate altro non è che la funzione: Private Function OnNewSample(mid As Integer, sample As PXCMCapture.Sample) As pxcmStatus ElaborateSample(sample) Return pxcmStatus.PXCM_STATUS_NO_ERROR End Function
Questa sfrutta di nuovo il metodo ElaborateSample() visto in precedenza e restituisce al chiamante che l’operazione è andata a buon fine.
Per poter inizializzare l’utilizzo dell’handler (o degli handler se ne utilizziamo più d’uno), è necessario invocare la funzione Init():
Dim handler = New PXCMSenseManager.Handler() handler.onNewSample = AddressOf OnNewSample If SenseManager.Init(handler).IsError() Then MessageBox.Show("Errore nell'inizializzazione della camera") Close() End If
Come già accennato in precedenza, anche in questo caso è necessario un thread separato per poter dare il via al meccanismo di ricezione dei frame (che arriveranno con la chiamata al delegate). Possiamo decidere di utilizzare la coppia di chiamate AcquireFrame()/ReleaseFrame() o l’unica StreamFrames(). Le modalità sono esclusive, o si utilizza una o l’altra.
Chiudere il SenseManager
Quando la nostra applicazione viene chiusa, è importantissimo rilasciare correttamente l’istanza di PXCMSenseManager che si sta utilizzando.
Abbiamo già visto come rilasciare i frame ogni volta che ne richiediamo l’accesso. Per il SenseManager la situazione è analoga: nel momento in cui la nostra applicazione chiude, è necessario richiamare il metodo Close():
SenseManager.Close() SenseManager.Dispose()