Kinect SDK – Określanie odległości Udostępnij na: Facebook

Autor: Tomasz Kowalczyk

Opublikowano: 2011-09-19

Jest to trzecia część artykułów z serii Kinect SDK, wprowadzających w możliwości, jakie oferuje programistom sensor Kinect po podłączeniu go do komputera. W tym artykule opisano, w jaki sposób określać odległości za pomocą sensora Kinect.

Po zapoznaniu się z poniższą treścią czytelnik będzie wiedział, jak za pomocą użycia Kinect SDK reagować we własnym programie na położenie śledzonego człowieka. Opisano również, jak programowo przechwycić dane, dotyczące odległości z sensora, i w zależności od wartości tej wielkości, jak wykonać określone instrukcje.

Wprowadzenie

Określanie odległości za pomocą użycia Kinect SDK nie jest trudną czynnością. Należy jedynie uświadomić sobie, że dane, pochodzące ze strumienia pobieranego z kamery odległości, nie niosą ze sobą informacji, dotyczących samego obrazu, ale mają zapisane w sobie odległości pojedynczych pikseli. Warto również zwrócić uwagę na fakt, że wartości pikseli są określane za pomocą emitera promieni podczerwonych, których prędkość odbicia od obiektu pozwala na określenie odległości danego punktu.

Organizacja kodu, odpowiedzialna za powyżej opisaną czynność, przedstawiona jest na Rys. 1.

Rys. 1. Diagram klas – kamera głębokości.

Powyższe zależności zostały już wykorzystane w poprzedniej części artykułów z serii Kinect SDK, teraz należy usystematyzować nabytą wiedzę.

  1. Po inicjalizacji NUI, należy określić tryb pracy przechwytywania danych, dotyczących odległości oraz śledzenia szkieletu:
    nui.Initialize(RuntimeOptions.UseDepthAndPlayerIndex|RuntimeOptions.UseSkeletalTracking);
    Powyższy kod wywołuje metodę inicjującą NUI na wcześniej utworzonym obiekcie. Jako argumenty metody, należy podać, jakich opcji przechwytywania ta aplikacja będzie używać. Pozwoli to na powołanie instancji obiektów, utworzonych w momencie użycia danego podzespołu sensora.
  2. W kolejnym kroku należy zdefiniować zdarzenie, wywołane pobraniem danych z kamery głębokości, następnie przekazać te dane do metody (nui_DepthFrameReady), w której to należy zaimplementować odpowiedzi na zdarzenie:
    nui.DepthFrameReady+=newEventHandler<ImageFrameReadyEventArgs>(nui_DepthFrameReady);
    Podobnie jak w projekcie wykonanym w poprzednim artykule, zastosowano tutaj aspekty programowania zdarzeniowego w odpowiedzi na przechwycony obraz.
  3. Następnie należy zdefiniować, jakiego typu obraz zostanie przechwycony oraz określić jego parametry:
    nui.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.DepthAndPlayerIndex);

Powyższy kod prezentuje otworzenie strumienia przechwytywania danych, gdzie dla metody Open() zostały podane wymagane argumenty. Aby wszystko prawidłowo funkcjonowało, należy do metody przekazać, jakiego typu obrazu się spodziewamy (ImageStreamType.Depth), w jakiej ma on być rozdzielczości (ImageResolution.Resolution320x240) oraz odwołać się do opcji inicjalizacji NUI, zdefiniowanej na początku programu (DepthAndPlayerIndex).

Poniżej przedstawiony został algorytm postępowania, dotyczący przetwarzania pobranych danych z przechwyconego strumienia kamery głębokości.

Struktura danych

Każda ramka, przechwyconych danych, zawiera informacje o odległości pojedynczych pikseli, które ją tworzą. Dane te zapisane są w tablicy:

imageFrame.Image.Bits.

Tablica ta przechowuje dane w następujący sposób: dane zapisane są w kolejności od lewego, górnego rogu do prawego, dolnego rogu. Przeglądanie tablicy następuje od strony lewej do prawej i od góry do dołu. Każdy piksel definiują dwa bajty (16 bitów), które przechowują informacje o jego odległości. Sposób organizacji bitów w każdej ramce zależny jest od opcji inicjalizacyjnych, wybranych przez programistę:

  • RuntimeOptions.UseDepth
    Pierwsze 12 bitów (bity od 0 do 11) zawierają dane, dotyczące odległości, pozostałe 4 są nieużywane.
  • RuntimeOptions.UseDepthAndPlayerIndex
    Pierwsze 3 bity (bity od 0 do 2) mówią nam o tym, którego gracza dotyczą kolejne bity, opisujące odległość poszczególnych pikseli, pozostałe bity zawierają dane, dotyczące odległości.

Zakres odległości, jaką jest w stanie określić sensor Kinect, wynosi od 850mm do 4000mm. Jeśli wartość ta wynosi zero, oznacza to, że obiekt jest albo zbyt blisko, albo zbyt daleko od sensora i nie jest on w stanie określić odległości obiektu.

W wersji beta Kinect SDK sensor jest w stanie zidentyfikować 2 postacie. Dla każdej postaci tworzona jest bitmapa, w której zapisane bity mówią nam o odległości postaci od sensora, jest to tzw. Player Segmentation Data. Bitmapy, pochodzące od dwóch postaci, jakie wykryje sensor, są na siebie nakładane i tworzą strumień danych, przesyłanych do programu.

Przechwytywanie obrazu poprzez aplikacje może być realizowane na dwa sposoby. Następuje ono po tym, gdy ramka jest przechwycona i dane, składające się na nią, kopiowane są do buforu.

  • Polling model
    Jest to najprostsza metoda do czytania danych. Aplikacja najpierw pobiera strumień danych. Następnie określa, w jakim odstępie czasu spodziewa się kolejnej ramki (między 0 a liczbą wyrażoną w milisekundach), po czym pobiera kolejną sekwencję danych lub czeka na nią. Kiedy żądanie, dotyczące pobrania kolejnej ramki zakończone zostaje sukcesem, strumień danych, składających się na nią, jest gotowy do przetworzenia.

  • Event model
    Ten model przechwytywania obrazu zwykle jest wykorzystywany w momencie korzystania z tzw. Skeletal tracking. Jest on bardziej kompleksowy i zapewnia większą skalowalność, co pozwala na łatwiejsze jego rozbudowywanie i dostosowanie do własnych potrzeb.

    W środowisku kodu zarządzanego (C#), należy wywołać ten model poprzez Runtime.DepthFrameReady lub Runtime.ImageFrameReady. Następnie, gdy zachodzi określone zdarzenie, zdefiniowane w kodzie, model ten wywołuje metodę ImageStream.GetNextStream.

Implementacja

Po tym krótkim wstępie teoretycznym, pora na uruchomienie Visual Studio i napisanie odrobiny kodu, który pozwoli przetestować w praktyce nabytą wcześniej wiedzę.

Projekt, utworzony na potrzeby tego artykułu, będzie pozwalał na przechwycenie obrazu z kamery głębokości, a następnie przetworzenie bitów, z jakich będzie się składał. W zależności od odległości, w jakiej będziemy się znajdować od sensora Kinect, obraz na ekranie komputera, przedstawiający śledzoną sylwetkę lub części ciała, będzie różnił się kolorem. Będzie on przedstawiony od koloru niebieskiego dla sylwetki, znajdującej się w odległości do 900mm, poprzez kolor zielony, do koloru czerwonego dla odległości powyżej 2000mm.

Aplikacja, wykonana na podstawie informacji zawartych w tym artykule, pokazuje również, jak odróżnić sylwetkę gracza i pokolorować ją w celu rozróżnienia jej od otoczenia.

Informacja
Wszystkie kody źródłowe projektów, utworzonych w ramach artykułów, będą dostępne na tej stronie.

Metoda, która będzie stosowana w momencie pojawienia się zdarzenia, jakim jest przechwycenie obrazu z kamery głębokości, powinna wyglądać następująco:

void nui_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)

        {

            byte[] ColoredBytes=GenerateColoredBytes(e.ImageFrame);

            PlanarImage image=e.ImageFrame.Image;

            image1.Source=BitmapSource.Create(image.Width, image.Height, 96, 96,

                PixelFormats.Bgr32, null, ColoredBytes, image.Width * PixelFormats.Bgr32.BitsPerPixel / 8);

        }

Nie różni się ona niczym, w porównaniu z tą wykorzystywaną w poprzednim projekcie, z wyjątkiem deklaracji tablicy ColoredBytes, która będzie zawierać dane, dotyczące koloru poszczególnych pikseli w zależności od odległości, w jakiej się znajdują.

Powołano również obiekt klasyPlanarImage, który będzie wykorzystywany do określania parametrów obrazu na podstawie przechwyconych ramek z kamery głębokości. Następnie przypisano do kontrolki WPF typu Image, identyfikowanej jako image1, źródło, które tworzy poszczególne ramki, wyświetlane w zainicjowanej kontrolce, na podstawie przechwyconych danych.

Definicja metody GenerateColoredBytes(), a głównie jej ważniejszych fragmentów i ich omówienie, znajduje się poniżej.

Przede wszystkim należy pobrać dane, dotyczące odległości, i zapisać je w tablicy:

Byte[] depthData = imageFrame.Image.Bits;

Następnie należy zadeklarować tablicę, która będzie odpowiadała za kolor pikseli, generowany na podstawie odległości:

Byte[] colorFrame = new byte[imageFrame.Image.Height * imageFrame.Image.Width * 4];

Rozmiar tej tablicy został określony na podstawie wcześniej zadeklarowanych parametrów, dotyczących pojedynczej ramki przechwyconego obrazu. Warto również zadeklarować początkowe wartości indeksów, na podstawie których będą później definiowane kolory:

const int BlueIndex=0;

const int GreenIndex=1;

const int RedIndex=2;

W kolejnym kroku należy zaimplementować zagnieżdżone pętle, które będą przechodzić po całej ramce pobranej z kamery. Dla bitów, umieszczonych na odpowiednich miejscach (zgodnie z zapisem przedstawionym wyżej), będzie wyliczany dystans obiektu w milimetrach. Następnie należy sprawdzić, w jakim przedziale odległości się mieści i nadać mu odpowiedni kolor.

for (var y=0; y < height; y++)

            {

                var heightOffset=y * width;

                for (var x=0; x < width; x++)

                {

var index=((width-x-1)+heightOffset) * 4;

Określenie odległości punktu, odwzorowywanego przez dany piksel, realizowane jest przez poniższy kod, umieszczony wewnątrz powyższych pętli:

var distance=GetDistancePlayerIndex(depthData[depthIndex], depthData[depthIndex+1]);

Implementacja metody wyznaczającej odległość:

private int GetDistancePlayerIndex(byte firstFrame, byte secondFrame)

        {

            int distance=(int) (firstFrame>>3 | secondFrame<<5);

            return distance;

        }

Powyższa metoda i sposób jej implementacji jest prostym przełożeniem na kod źródłowy własności, dotyczących sposobu organizacji bitów w przechwyconej ramce.

Zmiana koloru danego piksela dla odległości mniejszej niż 900mm:

if (distance<=900)

                    {

                        colorFrame[index+BlueIndex]=255;

                        colorFrame[index+GreenIndex]=0;

                        colorFrame[index+RedIndex]=0;

                    }

Ciekawą funkcją, zaimplementowaną w realizowanym projekcie, jest również możliwość pokolorowania gracza, można to wykonać zgodnie z zapisem i strukturą bitów, przedstawioną wcześniej (aby użyć tego kodu, należy go jedynie odkomentować w repozytorium projektu).

if (GetPlayerIndex(depthData[depthIndex])>0)

                    {

                        colorFrame[index+BlueIndex]=0;

                        colorFrame[index+GreenIndex]=255;

                        colorFrame[index+RedIndex]=255;

                    }

Implementacja metody GetPlayerIndex() powinna wyglądać następująco:

private int GetPlayerIndex(byte firstFrame)

        {

            return (int)firstFrame&7;

        }

Po skompilowaniu i uruchomieniu projektu można zaobserwować zmianę koloru przedmiotów, znajdujących się w różnej odległości od sensora, oraz pokolorowania sylwetki na żółto w momencie pojawienia się jej przed sensorem.

Podsumowanie

W tym artykule opisano sposób dostępu do danych, pochodzących z przechwyconej ramki kamery głębokości, pozwalający na określenie odległości, w jakiej znajduje się dany obiekt od sensora. Przedstawiono również, w jaki sposób manipulować bitami oraz wyjaśniono ich strukturę, co ułatwi późniejsze implementowanie funkcjonalności oprogramowania, opartej o odległość śledzonych sylwetek.

W następnym artykule zostanie opisany tryb pracy Skeletal tracking, co pozwoli na implementację reakcji na zdarzenia, wywołane ruchem poszczególnych części ciała człowieka.