sobota, 14 kwietnia 2012

Podstawy XNA i oprogramowywanie sensora Kinect.

Witam wszystkich po dość długiej nieobecności. Ostatnio brakowało mi pomysłów na jakieś ciekawe artykuły, a nie chciałem też pisać o drobnostkach. Wpadłem ostatnio na pomysł podzielenia się swoją wiedzą o programowaniu na Kinect’a.

Kinect jest sensorem ruchu od firmy Microsoft. Jest wyposażony m. in. w kamerę VGA, oraz zestaw do wykrywania załamań promieniowania czerwonego: emiter oraz kamerę z filtrem podczerwieni. Dzięki temu urządzenie jest w stanie spełniać rolę zwykłej kamerki internetowej (przechwytywanie obrazu), jak i określania odległości między sensorem a obiektami przed nim. Dodatkowo oprogramowanie zawiera algorytmy potrafiące ‘wyłapać’ sylwetkę człowieka. Postaram się opisać sposób zaprogramowania w.w. czynności.


---

Od czego zacząć?
Przede wszystkim należy ściągnąć ze strony Microsoftu nowy Kinect SDK do Visual Studio 2010 (w lutym 2012 Microsoft wypuścił wersję finalną, wcześniej dostępne były bety). Dodatkowo bardzo polecam ściągnąć pakiet XNA Game Studio 4.0. Teoretycznie nie jest to konieczne, gdyż oprogramowanie na Kinecta można pisać również C# w na silniku WPF (lub w Windows Forms w Visual C++, ale to już inna bajka). Mimo to, to właśnie XNA jest framework'iem przeznaczoną właśnie do tworzenia gier, przez co nie trzeba się męczyć z pisaniem lawiny kodu. Dodatkowo w XNA możemy pisać bezpośrednio na konsolę Xbox360, co też jest ciekawym doświadczeniem (postaram się opisać to w najbliższym czasie). 

---

OK, zainstalowaliśmy XNA i Kinect SDK. Pora rozpocząć nasz projekt! Wchodzimy w File -> New Project -> Visual C# -> XNA Game Studio 4.0 -> Windows Game (4.0). Klikamy OK i oto naszym oczom ukazuje się pusty projekt XNA.

Błyskawicznie: o co w tym wszystkim chodzi?
Mamy klasę Game1. Zawiera ona metody Initialize(), LoadContent(), UnloadContent(), Update(), Draw().
Służą one do:
Initialize() - inicjalizacja niegraficznych obiektów
LoadContent() - inicjalizacja graficznych obiektów (nakładanie tekstur, itp)
UnloadContent() - nazwa mówi sama za siebie
Update() - aktualizacji świata gry
Draw() - wyświetlanie obiektów na ekranie (wywoływana jako ostatnia w cyklu)

Naszym zadaniem będzie teraz pobranie obrazu z kamery Kinect'a i wyświetlenie go na ekranie. Wobec tego będziemy musieli: 
1. Utworzyć 2 składowe klasy Game1: teksturę (obraz z kamery) i sensor Kinect'a (traktowany jak obiekt)
2. W Initialize()  uruchomić sensor
3. Utworzyć obsługę zdarzenia pobrania obrazu z kamery
4. Wyświetlić obraz na ekranie w funkcji Draw()
5. Wyłączyć obsługę sensora w UnloadContent - wywołane przy wyjściu z gry.

Zanim jednak przejdziemy do zabawy, musimy dodać obsługę Kinect'a do naszego projektu. W Solution Explorer wchodzimy w nasz projekt i klikamy prawym przyciskiem myszy na  References -> Add Reference... . Teraz mamy do wyboru - albo w zakładce .NET znajdziemy pole Microsoft.Kinect (co trwa chwilę, gdyż środowisko przeszukuje wszystkie swoje biblioteki), albo ręcznie wyszukamy odpowiednią bibliotekę. Znajduje się ona zazwyczaj w  C:\Program Files\Microsoft SDKs\Kinect\v1.0\Assemblies\Microsoft.Kinect.dll. Po dodaniu biblioteki należy jeszcze wpisać w naszym pliku linię 
using Microsoft.Kinect;
Po tym możemy już przejść do właściwego programowania.

---

Musimy dodać zmienną reprezentującą nasz sensor oraz teksturę przechowującą obraz z kamery Kinect'a (przypomnienie - urządzenie traktowane jest jak zwykły obiekt klasy).
Umieszczamy je w ciele klasy Game1 :
public class Game1 : Microsoft.Xna.Framework.Game
{
  GraphicsDeviceManager graphics;
  SpriteBatch spriteBatch;

  KinectSensor kinectSensor;
  Texture2D kinectRGBVideo;
// (…)

Dodatkowo w funkcji Initialize() należy wyzerować nasz sensor: 
protected override void Initialize()
{
  // TODO: Add your initialization logic here
  kinectSensor = null;
  base.Initialize();
}

Teraz zacznie się właściwa zabawa. Musimy znaleźć naszego podłączonego Kinecta i przypisać go do zmiennej kinectSensor. Lista wszystkich aktualnie podłączonych Kinectów znajduje się w kolekcji KinectSensor.KinectSensors . Może ona być też pusta, jeżeli aktualnie żaden Kinect nie jest podłączony do komputera. Napiszmy więc funkcję AssignSensor(), która szuka dostępnych sensorów. Jeżeli znajdzie, to przypisze go do zmiennej podanej jako argument funkcji. Wobec tego argument nie może zostać podany normalnie (przez kopię), lecz przez referencję (słowo kluczowe ref). Kod naszej funkcji:
void AssignSensor(ref KinectSensor SensorVariable)
{
  foreach (var Sensor in KinectSensor.KinectSensors)
    {
      SensorVariable = Sensor;
      break;
    }
}

Wywołanie tej funkcji umieszczamy w Initialize(). Wygląda ona teraz tak:
protected override void Initialize()
{
  // TODO: Add your initialization logic here
  kinectSensor = null;
  AssignSensor(ref kinectSensor);
  base.Initialize();
}


OK, mamy sensor przypisany do zmiennej. Jeżeli nie ma podłączonych urządzeń, jego wartość będzie pusta (null).  Musimy jeszcze teraz napisać funkcję uruchamiającą Kinecta i rozpoczynającą pobieranie obrazu przez kamerę VGA. Napiszmy funkcję InitializeKinect(), która (tak jak poprzednia) pobiera jako argument referencję do KinectSensor. Trzeba pamiętać, żeby później wywołać ja w Initialize(), zaraz pod wywołaniem funkcji AssignSensor()!

void InitializeKinect(ref KinectSensor SensorVariable)
{
  if (SensorVariable == null)
  {
    throw new Exception("Parametr funkcji nie zawiera podłączonego sensora Kinect!");
  }
  SensorVariable.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);
  SensorVariable.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>
                                        (SensorVariable_ColorFrameReady);
}

Co tu zaszło? Na początku sprawdzamy, czy podany argument nie jest pusty. Następnie tworzymy streaming obrazu przez kamerę VGA, po czym tworzymy EventHandler, uruchamiany kiedy przechwycony zostanie obraz z kamery. 

Porada - w Visual C# przy tworzeniu eventów, po wpisaniu operatora += można nacisnąć dwukrotnie klawisz TAB. Środowisko wówczas samo utworzy szkielet potrzebnej funkcji.

Oprogramujmy teraz nasz event. Kod powinien wyglądać tak:

void SensorVariable_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
{
  using (ColorImageFrame colorImageFrame = e.OpenColorImageFrame())
  {
    if (colorImageFrame == null)
    {
      return;
    }
    //Constants:
    int ToRedByte = 2;
    int ToGreenByte = 1;
    int ToBlueByte = 0;
    int BytesToNextPixel = 4;

    byte[] pixelsFromFrame = new byte[colorImageFrame.PixelDataLength];
    colorImageFrame.CopyPixelDataTo(pixelsFromFrame);
    Color[] color = new Color[colorImageFrame.Height * colorImageFrame.Width];
    kinectRGBVideo = new Texture2D(graphics.GraphicsDevice, colorImageFrame.Width, colorImageFrame.Height);

    int CurrentByteNumber = 0;
    for (int y = 0; y < colorImageFrame.Height; y++)
    {
      for (int x = 0; x < colorImageFrame.Width; x++, CurrentByteNumber += BytesToNextPixel)
      {
        int KolejnyPixel = y * colorImageFrame.Width + x;
        color[KolejnyPixel] = new Color(pixelsFromFrame[CurrentByteNumber + ToRedByte],    pixelsFromFrame[CurrentByteNumber + ToGreenByte], pixelsFromFrame[CurrentByteNumber + ToBlueByte]);
      }
    }
    kinectRGBVideo.SetData(color);
  }
}


Jest to chyba najtrudniejsza rzecz w całym kodzie. Musimy pobrać obraz z argumentu EventHandlera i przekonwertować go do postaci Texture2D. CO tu zaszło? Od góry: najpierw pobieramy obraz z argumentu.  Jezeli jest pusty - nic do roboty, return;. Jeżeli obraz nie jest pusty, to utwórzmy kilka stałych pomocniczych i przejdźmy do konwersji. 

Dygresja - ktoś może spytać: po co zmienne pomocnicze? Odpowiedź jest prosta - bardzo nie lubię w kodzie Magic Numbers. Wolę mieć zmienną o nazwie BytesToNextPixels, która mówi mi wszystko, zamiast natrafić nagle na mylące += 4.

Sama konwersja polega na:
1. Stworzenie tablicy byte[] i wpisania do niej obrazu
2. Utworzenie pustej tablicy kolorów
3. Zainicjowaniu pustej tekstury kinectRGBVideo
4. Przepisaniu z tablicy byte[] do tablicy kolorów składowych RGB każdego piksela po kolei
5. Zapisaniu informacji o kolorach do tekstury kinectRGBVideo.

Kod tak naprawdę nie jest tak straszny, jak się na początku wydaje. Główny problem polega na tym, że klasa ColorImageFrame zawiera parametry w dość nietypowej kolejności: BGRA zamaist standardowego RGBA. Musimy więc przejść po całej tablicy kolorów skacząc co 4 bajty i odczytywać wartości RGB.

---

OK, kolejny etap za nami. Program jest już prawie gotowy, musimy jeszcze zadbać o czyszczeniu po sobie pamięci. Klasa KinectSensor dziedziczy po interfejsie IDisposable. Wobec tego w UnloadContent() zatrzymajmy pracę Kinecta i usuńmy pamięć:
protected override void UnloadContent()
{
  // TODO: Unload any non ContentManager content here
  kinectSensor.Stop();
  kinectSensor.Dispose();
}

Po tym wszystkim możemy już wesoło nacisnąć Ctrl+F5 i uruchomić program. Kompilacja powinna odbyć się bez problemów i zobaczymy PUSTE okno XNA. Musimy jeszcze wyświetlić w nim naszą teksturę kinectRGBVideo. Wyłączamy zatem program i wchodzimy w funkcję Draw().  Po zaprogramowaniu, będzie wyglądała ona tak:
protected override void Draw(GameTime gameTime)
{
  GraphicsDevice.Clear(Color.CornflowerBlue);
  // TODO: Add your drawing code here
  spriteBatch.Begin();
  spriteBatch.Draw(kinectRGBVideo, new Rectangle(0, 0, 640, 480), Color.White);
  spriteBatch.End();
  base.Draw(gameTime);
}

Uwaga - w XNA rysujemy za pomocą obiektu klasy SpriteBatch. Zanim zaczniemy rysować, musimy użyć jego metody Begin(), a na końcu użyć metody End(). Oczywiście nic nie stoi na przeszkodzie, żeby między Begin() i End() rysować kilka obiektów.

---

I to by było na tyle! Kompilujemy program i powinniśmy zobaczyć obraz przechwycony przez Kinecta, w rozdzielczości VGA i 30 fps. W następnym odcinku napiszę o śledzeniu ruchu poszczególnych kończyn :)
W raazie jakichkolwiek pytań / uwag - proszę o wpis w komentarzach lub maila na ktwarogal@gmail.com.





Brak komentarzy:

Prześlij komentarz