Kinect, Kinect One, OpenCV e OpenNI, come siamo messi?

Di recente ho giocato un pochino con i dispositivi del titolo: Kinect per Xbox 360 e Kinect per Xbox One. Ma non ci ho giocato né con l’Xbox, né per scopo (puramente) ludico: potrebbero diventare l’argomento della mia tesi di laurea.

Ma torniamo indietro un secondo, anzi di qualche mese. A metà febbraio ho cominciato il mio tirocinio obbligatorio per la laurea, e ho deciso di farlo presso un’azienda che sviluppa un software di modellazione 3D per Windows e Mac. Più precisamente, io mi sto concentrando sulla possibilità di creare plugin per questo software in Python, in aggiunta alla possibilità di farlo in C++, già esistente.

La cosa più diffusa per il mio corso di laurea è quella di fare il tirocinio lungo e poi la tesi che lo riguardi. Siccome il mio relatore si occupa di Computer Vision (il corso più bello che io abbia fatto
in 5 anni di università, davvero) abbiamo pensato di fare un qualche plugin che riguardi la computer vision per il software, almeno per scopo didattico.

Abbiamo avuto diverse idee, e alcune di queste riguardano l’uso di sensori che possano rilevare la profondità, tra questi ci sono le due Kinect, che sono abbastanza diffuse. In ufficio avevamo già una Kinect per Xbox 360, poi il mio amico Giacomo mi ha prestato una Kinect per Xbox One.

I due dispositivi in realtà sono molto diversi, e quasi subito ho capito che, avendo una Kinect One a disposizione, è meglio lasciare perdere la prima Kinect. È molto meno precisa, presenta un rumore molto variabile col tempo, dovuto alla tecnologia stessa che usa, più un rumore che io ho notato dipendere più che altro dallo spazio, ovvero ho riscontrato una discontinuità periodica lungo delle linee verticali.

La Kinect One invece è molto più precisa, grazie alla rilevazione della profondità con il Time of Flight.

A parte queste differenze evidenti non ho approfondito ancora molto le tematiche strettamente legate alle perfiriche. Tra queste ci possono essere l’affidabilità della calibrazione di fabbrica e l’effetto della distorsione delle lenti nei dati ottenuti. Inoltre, se per la calibrazione della telecamera normale so di poter usare il metodo di Zhang, non saprei nemmeno da che parte cominciare per il sensore di profondità.

Kinect, OpenCV e il povero OpenNI

Una cosa di cui mi sono subito preoccupato è l’acquisizione dei dati dai sensori, possibilmente in Python. La prima idea è stata quella di usare le API di OpenCV, che in realtà chiamano a loro volta OpenNI.

Purtroppo l’OpenCV incluso su PyPI non è compilato con il supporto alle API di OpenNI. Io ho provato a compilarlo, e ce l’ho anche fatta, ma i risultati sono stati davvero deludenti.

Innanzitutto la Kinect per Xbox 360 semplicemente non può funzionare con il codice così come scritto nel codice sorgente di OpenCV, almeno per questa versione (la 4.1.0, al momento in cui sto scrivendo l’articolo).

Infatti, nel costruttore della classe CvCapture_OpenNI2, la variabile needIR è impostata a true, senza mai verificare se sia effettivamente così, quindi ad un certo punto il costruttore stesso cerca di aprire uno stream IR, cosa impossibile con la Kinect 360, e viene lanciata un’eccezione.

L’unico modo per ovviare al problema è sistemare quella parte di codice, commentandola, oppure togliendo l’eccezione e lanciando un warning o una cosa del genere. Io avevo provato a farlo, funzionava, ma l’immagine della camera a colori non funzionava correttamente.

Guarando le varie demo, mi sono accorto che provavano ad inizializzare OpenNI 2, ma poi, in caso di fallimento, provavano ad usare OpenNI. Questa cosa però con OpenCV 4 non è possibile, perché non c’è più il backend per OpenNI, che era invece presente fino alla 3.4. I file sono stati proprio cancellati, sebbene OpenNI rimanga nella lista di cose che si possono abilitare tramite CMake.

Non ho approfondito, ma immagino che la colpa sia del fatto che Apple ha chiuso il progetto, dopo aver acquisito principale contributore, PrimeSense, che erano anche i proprietari della tecnologia usata da Kinect.

Il dominio openni.org adesso rimanda al sito Apple, il repository Git di OpenNI esiste ancora ma l’ultimo commit risale al 12 Novembre 2013. La decisione di OpenCV mi sembra dunque comprensibile.

OpenNI è stato poi forkato in OpenNI 2 da Occipital, che ha l’interesse nello svilupparlo perché loro stessi producono dei sensori, che però sono fuori budget per il nostro scopo.

OpenNI 2 supporta senza problemi il Kinect per Xbox 360, quando usato direttamente. Inoltre esitono dei binding Python che consentono di acquisire direttamente dei fotogrammi e in caso trasformarli in matrice OpenCV manualmente.

Ho provato questa soluzione e funzionerebbe, però avendo a disposizione anche una Kinect One penso non la porterò avanti. Inoltre non escludo per il futuro di abbandonare anche Kinect One in favore di altro, come gli Intel RealSense.

Però per il momento la coppia OpenNI 2 e Kinect 2 l’ho provata, ma il fatto che entrambe abbiano il numero 2 non vuol dire che siano fatte l’una per l’altra: il pacchetto binario ufficiale di OpenNI 2 non include infatti nessun driver per Kinect 2.

Ma fortunatamente viene ancora fuori il numero 2, sono infatti 2 i driver che si possono usare per OpenNI 2 e la Kinect 2, almeno se siete su Windows: uno è presente su un branch sul repo OpenNI 2 di occipital ed è basato sul driver ufficiale Microsoft, la seconda è usare Freenect 2, una libreria Open Source non ufficiale ma cross platform per la Kinect 2.

La prima soluzione usa del codice datato, ma funziona, l’ho testato su Windows 10 con Visual Studio 2015.

Ho provato anche Freenect 2, sempre su Windows 10 con Visual Studio 2015, ma questa anche su Linux (Debian Buster), avendo successo in entrambi i casi. Se CMake rileva OpenNI 2, provvede anche ad abilitare la compilazione dei driver Freenect2/OpenNI 2, tuttavia secondo me non ha senso aggiungere un passaggio in più a questo punto, ma usare direttamente Freenect 2, che offre funzionalità in più, una su tutte, la registrazione (cioè far coincidere i dati RGB e profondità) fatta direttamente da loro, basandosi sulla matrice di calibrazione memorizzata sul dispositivo.

Ad ogni modo, entrambi i driver funzionano chiamando direttamente le API di OpenNI 2, ma non usando VideoCapture di OpenCV.

Kinect 2 e Python

Esclusa la via di OpenNI, ho comunque provato ad acquisire i dati della Kinect 2 con Python. Le strade, su Windows, in potenza sono addirittura 3.

La prima è usare PyKinect2, dei binding del driver ufficiale, rilasciati direttamente da Microsoft. Sono disponibili su PyPI e li ho provati, senza riuscire a farli andare. Inoltre sono privi di documentazione, forse anche perché venivano dal periodo precedente della Microsoft, non da questo attuale più amichevole verso lo spirito Open Source.

La seconda possibilità è usare Freenect2 con dei binding chiamati freenect2-python, disponibili anche su PyPI.

Ci sono tre possibili approcci per questo tipo di pacchetti Python: uno è quello di fornire, assieme al binding, anche le librerie, e viene seguito, per esempio da OpenCV. Un altro è quello di fornire una libreria leggera e di attaccarci in runtime la libreria principale, che è l’approccio di OpenNI. Infine, il modo seguito da freenect2-python è quello di includere il codice sorgente e lasciare all’utente il compito di compilarselo.

Infatti questo binding è composto da due parti. La prima è un wrapper C a Libfreenect2, invece scritta in C++, il cui scopo è quello di usare CFFI, un modo di fare binding in Python leggero e interessante perché necessita solo dei prototipi in puro C. Una libreria C++ può essere usata, purché si faccia appunto un wrapper con export C si ricrei la struttura delle classi in Python. freenect2-python non è un’eccezione, infatti la sua seconda parte è il rifacimento della struttura delle classi C++ in Python.

Vedendo il codice sorgente che esegue questo passo, si può notare che cerca Freenect 2 tramite pkg-config. Questo dovrebbe funzionare alla grande sui sistemi Linux con Freenect 2 installata come libreria di sistema. In caso contrario, potrebbero essere necessarie delle modifiche allo script affinché passi i parametri corretti al compilatore/linker, i cui eseguibili almeno vengono trovati automaticamente dal sistema di build Python.

Nonostante questo inconveniente devo dire che sono la mia soluzione preferita, tanto che ne ho fatto anche un fork! 😂️

In realtà la vera motivazione del fork è che c’era un memory leak dovuto a una piccola distrazione, che ho risolto. Ho fatto anche una pull request, ma ancora non è stata approvata.

Un’ulteriore accorgimento da adottare con Freenect 2, è che essa è asincrona di natura, e lo è anche in Python, ma non usando le funzionalità di 3.4 e successivi, ma usando il multi threading. La brutta notizia è che potrebbero servirvi dei lock (a me sono serviti), ma la notizia buona è che il meccanismo incluso nella libreria standard di Python funziona tranquillamente.

Infine, Freenect 2 stessa include anche un wrapper Python. Non me ne sono accorto subito e quindi non ho neanche provato a usarlo. Lo cito solo per completezza.

Abbiamo i dati, e adesso?

Acquisire dei dati tutto sommato è semplice e, se vi fidate della calibrazione di fabbrica, lo è anche sovrapporre profondità e colore. Ciò significa che possiamo tranquillamente lavorare sui dati di frame singoli.

Tuttavia è molto più interessante mettere assieme frame successivi per ampliare l’informazione disponibile e per esempio arrivare a fare una ricostruzione di un oggetto.

Ho avuto diverse idee, d’altronde OpenCV per questo campo può essere veramente un ottimo supporto.

La mia prima idea era quella di cercare di fare un po’ a mano questo procedimento, ma il mio relatore me l’ha caldamente sconsigliato, in favore di soluzioni già esistenti, perché tutto sommato, a suo dire, questo è un problema standard per cui esistono soluzioni molto buone, difficili da ottenere partendo da zero.

Su OpenCV contrib stessa c’è un modulo per usare RGB più la profondità, ed in particolare ce n’è anche uno specializzato per Kinect, ma dai miei primi test i risultati non sono buonissimi. Inoltre non è neanche direttamente usabile in Python: per come è stato programmato il binding di OpenCV, la matrice dei parametri intrinseci non è scrivibile da Python. Il problema può essere risolto, ma richiede la ricompilazione di OpenCV. Potrei aprire un’issue magari.

Inoltre, sembrano mancare alcune classi per la gestione di Viz/VTK nei binding Python/OpenCV, che sarebbero parecchio comodi per visualizzare i dati mentre vengono acquisiti.

Per il momento la mia ricerca si conclude qui, alla ricerca di un altro visualizzatrore di nuvole di punti in tempo reale per Python.

Avevo però fatto in precedenza altri tentativi. Uno prevedeva di tenere fermo il sensore e far ruotare un oggetto, pe poi sfruttare un algoritmo di feature detection e matching, come ORB o SIFT, per trovare una corrispondenza tra punti in frame successivi e costruire delle matrici di trasformazione. In un certo senso funziona, sia sfruttando i dati RGB (eventualmente convertiti a scala di grigi), che la profondità (appiattita a un byte, anziché 16 bit), però ci sono delle cose a cui prestare attenzione. Per esempio questa tecnica necessità di un bounding box dell’oggetto da scannerizzare delle giuste dimensioni e ha bisogno di un metodo per escludere gli outlier (io ho provato RANSAC, ma non ho ancora trovato una soluzione ottimale).

Il mio relatore mi ha consigliato di eseguire il matching direttamente sui dati 3D, con algoritmi come ICP (Iterative Closest Points). Il problema, in questo caso è che, almeno dall’implementazione di OpenCV, sono richieste le normali dei punti, e stimarle potrebbe non essere semplice, ma lui mi ha detto che di per sé le non dovrebbero essere richieste per l’algoritmo.

Altre tecniche a cui avevo pensato sono SFM (Structure From Motion) o usare il modulo di tracking di OpenCV. La prima non sono riuscito a compilarla, né su Linux, né su Windows; sono arrivato a pensare che ci siano dei problemi nei file CMake di questo modulo di OpenCV nella gestione della dipendenza da Ceres solver. La seconda, ancora una volta, era solo una bozza di idea, ma non penso nemmeno che la proverò e che lascerò strare la ricostruzione manuale.

Infatti, quello di trovare le matrici di trasformazione è solo il primo passo e già è pieno di incertezze a causa del rumore dei dati, poi mettere insieme i punti dai vari frame e ricavarne una mesh potrebbe essere un problema ancora più difficile.

Quindi non ho ancora le idee molto chiare, ma intanto ho fatto un po’ di ricerche di base, ma mi sa che avrò di che divertirmi con questa tesi 😁️.

Riferimenti utili