14 lutego 2013

Kinect - początek

Tak jak obiecałem, przyszedł ten czas by zająć się Kinectem. Zainstalujemy sterowniki, zapoznamy się z ogólna architekturą API oraz napiszemy prosty programik do szpiegowania ludzi. Na wstępie jednak, kilka uwag. Nie będzie tu omawiane API od Microsoftu. Nie chodzi tu o moją niechęć do monopolistów, tylko względy praktyczne. OpenNI ma lepszą licencję, daje wsparcie dla sensorów konkurencji (ktoś ma Asusa XTion lub PrimeSense Carmine?) i działa na Linuksie. Korzystać będziemy z wersji 1.5.4, a nie z wydanej ostatnio 2.1. Nowa wersja jest wciąż w fazie beta, poza tym wprowadza bardzo duże zmiany w samej organizacji kodu i po prostu nie miałem czasu by dokładniej się nimi zająć. Dodatkowo będziemy używać interfejsu napisanego w C a nie w C++. Jakiejś specjalnej awersji do klas nie czuję, ale w tym przypadku wersja strukturalna przedstawia się znacznie czytelniej.
Zatem do dzieła.

Sterowniki i architektura systemu

Architektura aplikacji zbudowanej w oparciu o OpenNI
Wszystko składa się w zasadzie z dwóch komponentów - NITE i OpenNI. Jak widać na obrazku obok, są one ze sobą dość ściśle powiązane i szczerze mówiąc, nie mam zielonego pojęcia czym kierowali się projektanci tego systemu, tworząc go właśnie w taki sposób. Na szczęście, nigdy nie będziemy musieli się nad tym zastanawiać. Opiszmy może elementy tego diagramu. Na samym dole mamy oczywiście sprzęt wyposażony w SoC od PrimeSense. Wyżej (w kształcie litery C) jest OpenNI. Biblioteka ta dostarcza interfejsy komunikacyjne dla sterowników oraz middleware, zajmującego się analizą obrazu i głębi. W środku znajdują się dwa komponenty: NITE Algorithms oraz Sensor Data Acquisition. Pierwszy z nich odpowiada za analizę danych odbieranych z czujników, zaś  Sensor Data Acquisition, wykonuje całą robotę związaną z bezpośrednią komunikacją komputera z sensorami. Wyżej już mamy tylko NITE Controls, czyli framework odpowiedzialny za rozpoznawanie i obsługę gestów oraz aplikację końcową - to czym będziemy zajmować się my.
Mając omówione podstawowe elementy, możemy zająć się praktyką. Przede wszystkim potrzebujemy sterowników i SDK. Ściągniemy je klikając tu i tu. Na początku instalujemy OpenNI SDK, zaś później pozostałe komponenty. Jeśli nie posiadamy Kinecta (lub mamy inny sprzęt) nie potrzebujemy pakietu z GitHuba.
Gdy wszystko przebiegnie pomyślnie, Kinect powinien zamrugać do nas zieloną diodą. Jesteśmy zatem gotowi, by okiełznać tego potwora!

Hello world

Stwórzmy zatem program do lokalizowania ludzi. Gdybyśmy chcieli wykorzystać do tego kamerę i zestaw bibliotek do analizy obrazu (np. OpenCV) z pewnością byłby to bardzo dobry temat na pracę magisterską. Pomijając fakt, że musielibyśmy zadbać o odpowiednie warunki oświetleniowe oraz jakość dostarczanych danych (rozdzielczość, FPS), nadal byłaby przed nami setki linii kodu wraz z opasłymi tomiskami opisującymi różne algorytmy analizujące obraz. Tutaj zaś, mamy bezpośredni dostęp do interesujących nas danych, zaś dzięki technologii zastosowanej w Kinectcie, nie musimy martwić się o oświetlenie.
Na początek więc, po instalacji całego pakietu, kopiujemy SDK (folder Include oraz Lib z katalogu OpenNI) do lokalizacji z komponentami zewnętrznymi (u nas jest to Externals). Ustawiamy ścieżki i robimy resztę magii związanej z IDE. Do main.cpp kopiujemy coś takiego:

bool bContextReady = false;
bool bDepthSensorReady = false;
 
XnContext *pContext;
XnNodeHandle depthHandle;
 
bContextReady = xnInit(&pContext) == XN_STATUS_OK;
 
if(bContextReady == true)
{
 bDepthSensorReady = xnCreateDepthGenerator(pContext, &depthHandle, NULL, NULL) == XN_STATUS_OK;
}
 
if(bContextReady == true && bDepthSensorReady == true)
{
 xnStartGeneratingAll(pContext);
 
 printf("Sensor started...\n");
 
 int iRet = 0;
 while(iRet == 0)
 {
  xnWaitAnyUpdateAll(pContext);
  iRet = _kbhit(); 
 }
 xnProductionNodeRelease(depthHandle);
 xnContextRelease(pContext);
}
Ogólna zasada działania programu jest prosta - inicjalizujemy wszystkie niezbędne zabawki, startujemy je, a później w pętli sprawdzamy stan sensorów, do których się wcześniej podłączyliśmy (w naszym przypadku jest to detektor głębi). W oczy rzucają się dwa typy: XnContext oraz XnNodeHandle. Kontekst to podstawowy obiekt OpenNI. Zawiera on stan całej aplikacji oraz wszystkich sensorów, do jakich podpięliśmy się podczas startu programu. XnNodeHandle to uchwyt do obiektu, dzięki któremu uzyskujemy interesujące nas dane. Aby uzyskać dostęp do dowolnego generatora musimy zawołać funkcję xnCreateXXX, gdzie pod XXX możemy wstawić: 
  • AudioGenerator
  • DepthGenerator
  • GestureGenerator
  • HandsGenerator
  • ImageGenerator
  • IRGenerator
  • Player
  • Recorder
  • SceneAnalizer
  • UserGenerator
Dziś oczywiście nie będziemy zajmować się wszystkimi wyżej przedstawionymi generatorami - skorzystamy tylko z ostatniego. Daje on możliwość detekcji i lokalizowania ludzi. Jego tworzenie, zgodnie z wyżej przedstawionym schematem wygląda następująco:
XnNodeHandle usersHandle;
xnCreateUserGenerator(pContext, &usersHandle, NULL, NULL);

Dzięki niemu będziemy mogli dowiedzieć się ile osób stoi w polu widzenia Kinecta, pobrać położenie ich geometrycznego środka lub dowiedzieć się, czy dany piksel transmitowanego obrazu należy do określonej osoby (choć można to zrobić również za pomocą analizatora sceny). 
Każdy aktor (bądź użytkownik, wedle nomenklatury OpenNI) posiada własny ID. Maksymalnie możemy wykryć do piętnastu osób. Wejście aktora na scenę (obszar widoczny przez Kinecta) wiąże się z automatycznym powiązaniem go z konkretnym numerem. ID jest zwalnianie wtedy gdy aktor zniknie ze sceny na dłużej niż pięć sekund. Jest to bardzo przydatne, bo chwilowe utracenie osoby nie oznacza wykrycia jej jako nowego użytkownika. Bardzo ładnie to widać na tym video. Przypatrzmy się zatem w jaki sposób możemy zaimplementować proste liczenie ludzi:
const unsigned int USERS_BUFFER_LENGTH = 15;
XnUserID users[USERS_BUFFER_LENGTH];

int iRet = 0;
while(iRet == 0)
{
 unsigned short int uUsersCount  = USERS_BUFFER_LENGTH;
 unsigned short int uVisibleUsersCount = 0;
 
 xnWaitAnyUpdateAll(pContext);
 
 memset(users, 0x0, sizeof(users));
 xnGetUsers(usersHandle, users, &uUsersCount);
 
 for(unsigned int i = 0; i < uUsersCount; i++)
 {
  XnPoint3D com;
  xnGetUserCoM(usersHandle, users[i], &com);
 
  if(com.Z > 0.0f)
  {
   uVisibleUsersCount++;
  }
 }
 
 printf("Users count: %d\r", uVisibleUsersCount);
 
 iRet = _kbhit();
}

Co my tu mamy. Przede wszystkim tworzymy bufor, w którym możemy przechować identyfikatory. Kluczową jest tutaj funkcja xnGetUsers. Przyjmuje ona uchwyt do generatora, wskaźnik na bufor oraz maksymalną liczbę elementów które mogą zostać zapisane. Przekazujemy tą zmienna przez wskaźnik, bo zwracana jest w niej rzeczywista ilość wykrytych ludzi. W pętli iterującej przez wszystkie elementy bufora users pobieramy pozycję każdej wykrytej osoby (xnGetUserCoM - Get User Center Of Mass). Zastanawiający jest tylko warunek, sprawdzający, czy współrzędna z jest większa od zera. Jak już wspominałem wcześniej, OpenNI przechowuje ID aktora, do pięciu sekund od jego zniknięcia ze sceny. Po tym, gdy aktor zejdzie ze sceny i jest ciągle w buforze jego pozycja z jest właśnie ustawiana na zero. 
Ostatnią rzeczą o której chciałbym opowiedzieć to magiczna metoda xnWaitAnyUpdateAll. Jak sama nazwa wskazuje, służy ona do aktualizacji danych, pochodzących z interesujących nas sensorów. Jest jednak cała rodzina tego typu funkcji:
  • xnWaitAnyUpdateAll - czeka aż dowolny z detektorów zgłosi, że ma nowe dane. Jeśli tak, reszta detektorów jest automatycznie aktualizowana
  • xnWaitOneUpdateAll - czeka aż jeden detektor (którego uchwyt przekazany jest w argumencie) zgłosi, że ma nowe dane. Jeśli tak, reszta detektorów jest automatycznie aktualizowana
  • xnWaitAndUpdateAll - czeka aż wszystkie detektory będą miały nowe dane i dopiero wtedy je aktualizuje
  • xnNoneUpdateAll - nie czeka na żaden z detektorów, tylko automatycznie aktualizuje wszystkie dostępne
To w zasadzie tyle. Wyświetlanie dokładnej pozycji przechodzących ludzi zostawiam na zadanie domowe. Nie będzie to trudne, tym bardziej, że mamy już wszystkie potrzebne informacje. Solucje można pobrać tu.  

Brak komentarzy:

Prześlij komentarz