Ein verregneter Sonntag und im Fernsehen laufen zum x-ten mal Filme die ich schon zu Jugendzeiten in die Kategorie alte Schinken eingeordnet habe. Was also spricht dagegen einen eigenen kleinen Film zu machen? Schließlich steht da ein neues Spielzeug auf dem Tisch, welches mich im Stile von "Nummer 5 lebt!" mitleidig anschaut und darauf wartet in Betrieb genommen zu werden.
Gesagt, getan! Und so gibt ein kleines grünes Licht schließlich den Startschuss für den nächsten Flurfunk-Artikel, der sich wie in meinem letzten Beitrag "Hello (Kinect) World!" angekündigt ganz und gar um die bewegten Bilder dreht. Selbiges scheint mir wesentlich spannender als der Film der im Hintergrund läuft und dessen Dialoge mein Unterbewusstsein wahrscheinlich schon auswendig kennt. Ich hoffe Sie verspüren ähnlichen Bewegungsdrang!
Stromaufwärts
Als ich Ihnen vor ein paar Wochen den Kinect Controller vorgestellt hatte erwähnte ich schon, dass sowohl RGB-Kamera als auch der Tiefensensor mit 30fps arbeiten. Die Frage aber ist, wie gelangen diese Daten bzw. Bilder nun in die eigene Anwendung? Und wie Sie sich sicher vorstellen können ist die Antwort denkbar einfach und lautet Datenströme oder zu neudeutsch Streams.
Die NUI Library (Natural User Interface) ist an dieser Stelle vereinfacht gesagt nichts anderes als die Runtime der Kinect. Sie umfasst somit alles was für die Nutzung notwendig ist, angefangen vom Treiber, über den Kernel, bis hin zu Bibliotheken die in Visual Studio referenziert werden können. Anschaulich wird das Ganze in dem folgenden SDK Architektur Überblick:
Wie wir sehen sind die Daten also bereits durch einige Schichten "geströmt" bevor Sie in einer Anwendung über das NUI API das Licht der Welt erblicken. Selbiges möchte ich Ihnen in den nächsten Abschnitten für die RGB-Kamera veranschaulichen.
Lauter bunte Bilder
Schluss mit der Theorie, rein in die Praxis! Nachdem wir das SDK ja bereits installiert haben, starten wir Visual Studio und erstellen uns einfach eine neue WPF Applikation, dessen Name ich einfach mal Ihrer eigenen Kreativität überlasse. Ist dies vollbracht brauchen wir als erstes Zugriff auf das NUI API und das lösen wir durch einfaches einbinden der Kinect Library, die im Verzeichnis “C:Program FilesMicrosoft SDKsKinectv1.5Assemblies” zu finden ist.
Mit dem hochgesteckten Ziel ein bewegtes Bild zu sehen und mit dem Wissen dass die Kinect uns Frames durch Streams zur Verfügung stellt, platzieren wir in unserem WPF Hauptfenster wohlweislich schon mal ein Image Control. Ihr XAML sollte dann in etwa so aussehen:
1: <Window x:Class="RGBCameraFundamentals.MainWindow"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="MainWindow" Height="480" Width="640" MinWidth="640"
5: MinHeight="480" Loaded="Window_Loaded" Closed="Window_Closed">
6: <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
7: <Image Name="ImageRGB" Width="640" Height="480"/>
8: </StackPanel>
9: </Window>
Bevor jedoch unser Anlitz das soeben erstellte Control ziert, gilt es erst noch die ein oder andere Zeile Code zu implementieren. In unserem Fall wäre es also durchaus sinnvoll sich an das Window_Loaded bzw. Window_Closed Event zu hängen um die Frame Capture Engine der Kinect parallel zur Anwendung zu starten bzw. zu beenden.
1: using System;
2: using System.Linq;
3: using System.Windows;
4: using Microsoft.Kinect;
5: using System.IO;
6: using System.Windows.Media.Imaging;
7: using System.Windows.Media;
8:
9: namespace RGBCameraFundamentals
10: {
11: /// <summary>
12: /// Interaction logic for MainWindow.xaml
13: /// </summary>
14: public partial class MainWindow : Window
15: {
16: /// <summary>
17: /// Kinect Sensor
18: /// </summary>
19: public KinectSensor Kinect { get; set; }
20:
21: public MainWindow()
22: {
23: InitializeComponent();
24: }
25:
26: private void Window_Loaded(object sender, RoutedEventArgs e)
27: {
28: // Generell ist es möglich mehr als einen Kinect Sensor
29: // an einem Rechner zu betreiben. Für unsere Zwecke reicht
30: // es deshalb aus den ersten verfügbaren Sensor mit Status
31: // Connected zu ermitteln.
32: this.Kinect = KinectSensor.KinectSensors
33: .FirstOrDefault(ks => ks.Status
34: == KinectStatus.Connected);
35:
36: if (this.Kinect != null)
37: {
38: try
39: {
40: // Starten des Sensors
41: this.Kinect.Start();
42: }
43: catch (IOException)
44: {
45: this.Kinect = null;
46: }
47: }
48: }
49:
50: private void Window_Closed(object sender, EventArgs e)
51: {
52: if (this.Kinect != null)
53: {
54: // Stoppen des Sensors
55: this.Kinect.Stop();
56: }
57: }
58: }
59: }
So weit, so gut, zwar haben wir die Kinect jetzt eingebunden, aber unserem Ziel sind wir nicht sonderlich viel näher gekommen. Zeit für ein bisschen Initialisierung:
1: private void Window_Loaded(object sender, RoutedEventArgs e)
2: {
3: ....
4:
5: try
6: {
7: // Initialisieren der ColorStreams
8: InitializeColorStream();
9:
10: // Starten des Sensors
11: this.Kinect.Start();
12: }
13: catch (IOException)
14: {
15: this.Kinect = null;
16: }
17:
18: ....
19: }
20:
21: private void InitializeColorStream()
22: {
23: // Der jeweilige Stream muß für die Verarbeitung enabled werden.
24: // Wahlweise können auch die Formate RgbResolution1280x960Fps12,
25: // RawYuvResolution640x480Fps15 und YuvResolution640x480Fps15
26: // verarbeitet werden
27: this.Kinect.ColorStream.Enable
28: (ColorImageFormat.RgbResolution640x480Fps30);
29:
30: // Zwar könnte man auch eine normale Bitmap nehmen aber die
31: // WriteableBitmap eignet sich durch die Benutzun von internen
32: // Puffern besser zum rendern von Frames. Da wir einen RGB Stream
33: // benutzen ist das PixelFormat Bgr32 was einem Standard RGB Format
34: // mit 32 Bits pro Pixel entspricht. Falls Sie eine eigene Palette
35: // benutzen können Sie desweiteren den null Parameter ersetzen, in
36: // der Regel ist dies aber nicht nötig. Und um es nicht zu vergessen
37: // Windows samt WPF nutzen per Default 96dpi, so gesehen sollten
38: // Ihnen die zwei bisher nicht genannten Parameter durchaus klar
39: // sein.
40: this.ColorBitmap = new WriteableBitmap(
41: this.Kinect.ColorStream.FrameWidth,
42: this.Kinect.ColorStream.FrameHeight,
43: 96.0, 96.0, PixelFormats.Bgr32, null);
44:
45: // Die Source des Images in der Anwendung ist natürlich die vorher
46: // definierte WriteableBitmap
47: this.ImageRGB.Source = this.ColorBitmap;
48: }
Die Frage die sich jetzt noch stellt ist die, wie den nun der Zugriff auf die von der Kinect erstellten Frames aus dem ColorStream realisiert wird? Hierfür gibt es generell zwei Möglichkeiten. Einerseits lassen sich die Daten abrufen (Poll-Model) und andererseits können Sie die FrameReady-Events nutzen die seitens der Frame Capture Engine gefeuert werden (Event-Model).
Ich für meinen Teil bevorzuge letztere Variante, denn warum soll ich mich in diesem Kontext mit Threads, Timern oder gar Endlosschleifen herumschlagen wenn ich mich mit einfachsten Mitteln an einen bereits vorhandenen Eventhandler hängen kann? Sollten Sie das anders sehen oder gar zur seltenen Spezies der XNA Entwickler gehören (die Events pollen müssen), dann dürfen Sie gerne einen Blick auf die Methode OpenNextFrame der Klasse ColorImageStream werfen.
1: private void InitializeColorStream()
2: {
3: ....
4:
5: // Last-but-not-least sorgen wir dafür dass die FrameReady Events
6: // in der Methode Kinect_ColorFrameReady verarbeitet werden können
7: this.Kinect.ColorFrameReady += this.Kinect_ColorFrameReady;
8: }
9:
10: private void Kinect_ColorFrameReady(object sender,
11: ColorImageFrameReadyEventArgs e)
12: {
13: // Der ColorImageFrame stellt den Container für die Daten des Sensors
14: using (ColorImageFrame frame = e.OpenColorImageFrame())
15: {
16: if (frame != null)
17: {
18: // Ersten des für die Erstellung des Image nötigen Byte Arrays.
19: // Die Größe des Arrays von 1228800 Byte errechnet sich durch
20: // 640x480x4 (letzteres siehe BGR32)
21: byte[] pixelData = new byte[frame.PixelDataLength];
22:
23: // Übername der Daten aus dem Frame
24: frame.CopyPixelDataTo(pixelData);
25:
26: // Die WriteableBitmap benötigt beim rendern die Angabe welcher
27: // Bereich aktualisiert werden soll. Hier benutzen wir die gesamte
28: //Fläche der Bitmap
29: Int32Rect colorBitmapRect =
30: new Int32Rect(0, 0, this.Kinect.ColorStream.FrameWidth,
31: this.Kinect.ColorStream.FrameHeight);
32:
33: // Die Angabe wieviele Pixel jeweils auf ein mal upgedated werden
34: // sollen. Mit der vorliegenden Definition geschieht dies also
35: //Zeilenweise
36: int colorBitmapStride = this.Kinect.ColorStream.FrameWidth *
37: this.Kinect.ColorStream.FrameBytesPerPixel;
38:
39: // Erstellen der neuen Bitmap
40: this.ColorBitmap.WritePixels(colorBitmapRect, pixelData, colorBitmapStride, 0);
41: }
42: }
43: }
So, nun ist der große Moment gekommen in dem Sie Ihrer ersten Kinect Applikation eine Chance geben dürfen. Starten Sie die Anwendung und empfinden Sie Freude beim Anblick Ihres Gesichts.
Das kleine grüne Männchen in mir
Bevor ich das Projekt RGB Kamera vorerst schließe, will ich noch einen kurzen und einfachen Exkurs Richtung Imagemanipulation machen. Sicherlich können Sie mit GDI+ die Images vor der Anzeige manipulieren. Worauf ich aber in meinem Beispiel hinaus will, ist die einfache Manipulation des Farbschemas durch Anpassung der Frames bzw. des damit verbundenen Byte-Arrays.
Passen wir unsere Methode Kinect_ColorFrameReady doch einfach mal wie folgt an:
1: private void Kinect_ColorFrameReady(
2: object sender, ColorImageFrameReadyEventArgs e)
3: {
4: ....
5:
6: // Übername der Daten aus dem Frame
7: frame.CopyPixelDataTo(pixelData);
8:
9: // Wie BGR schon sagt sind die vier Byte pro Pixel Blau, Grün
10: // und Rot gefolgt von einer null die in anderen Formaten wie
11: // Bgra32, Pbgra32 den Alpha Wert darstellt. In unserem Fall
12: // reicht es also die Blau und Rot Werte auf 0 zu setzen
13: for (int i = 0; i < pixelData.Length; i += frame.BytesPerPixel)
14: {
15: pixelData[i] = 0x00;
16: pixelData[i + 2] = 0x00;
17: }
18:
19: ....
20: }
Wenn man die Anwendung nun startet sieht man sich, sofern man das so ausdrücken kann, plötzlich in einem ganz neuen Licht.
Der Vollständigkeit halber finden sie den kompletten Source-Code hier.
Ausblick
Der aufmerksame Leser mag gemerkt haben, dass ich ursprünglich angekündigt hatte auch auf den Tiefensensor einzugehen. Asche auf mein Haupt, aber letztlich sind die Themen einfach zu umfangreich (und zu spannend). Somit vertröste ich Sie an dieser Stelle auf den nächsten Artikel in dem ich der Entwicklung den nötigen Tiefgang verpasse. Ich würde mich freuen Sie auch dann wieder die Lust auf ein wenig Bewegungsdrang verspüren.