Die Leser unter Ihnen, die, so wie ich, als Kind der 80er aufgewachsen sind, werden sich sicherlich erinnern. Damals rannte ein blaues pelziges Etwas namens Grobi hektisch durch das Programm der Sesamstraße und versuchte uns die Begriffe "Nah & Fern" zu erklären. Heute, also gute 30 Jahre später, habe ich das Vergnügen diese Begriffe noch mal neu für mich zu entdecken. Und wie damals sitze ich wieder gebannt vor dem Bildschirm und erfreue mich der Dinge. Kinect sei Dank!
Sie können sich sicherlich denken worum es geht. Während wir uns im letzten Artikel Über die Kinect im Bilde noch mit Oberflächlichkeiten begnügen mussten, gehen wir heute einen Schritt weiter. Wir verpassen einer Anwendung den nötigen Tiefgang indem wir uns dieses Mal den Stream des Tiefensensors zu nutze machen.
Eine Sache des Blickwinkels
Im ersten Teil dieser Serie hatte ich geschrieben, dass der maßgebliche Unterschied zwischen der Kinect für Windows verglichen mit der Xbox 360 Variante im Tiefensensor liegt. Heute möchte ich dies ein wenig besser spezifizieren, denn Microsoft hat hier nicht – wie man glauben könnte – einen verbesserten Sensor verbaut. Nein, stattdessen wurde eine neue Firmware entwickelt die besser mit den Lichtverhältnissen bezogen auf nahe Objekte umgehen kann.
Dies hat zur Folge dass die Kinect je nach Aufgabe im Default Mode und im Near Mode betrieben werden kann. Auf das Blickfeld (field of view), oder präziser gesagt auf die Distanz in der Objekte erkannt werden, hat das folgenden Einfluss:
Quelle: Kinect for Windows Blog, Near Mode – What t is (and isn’t)
Wie man also sieht gibt es, trotz der räumlichen Einschränkungen die durch die Kinect gegeben sind, genug Freiraum um ein wenig Kreativität in Sachen Interaktion ausleben zu können.
Alles grau in grau
So, nach all den einleitenden Worten raus aus der grauen Theorie und rein in die graue Praxis. Wir wollen wie auch im letzten Teil dieser Blogserie eine einfache WPF Applikation erstellen in der unser Antlitz erstrahlt. Dieses mal wird selbiges allerdings nicht ganz so farbgewaltig sein wie zuletzt.
Nach dem erklärten Schema erstellen wir also zuerst eine WPF Applikation mit einem klangvollen Namen, platzieren ein Image Control, hängen uns an den Window_Loaded bzw. Window_Closed Event und fügen noch ein wenig Kinect Voodoo als letzte Zutat hinzu um zu folgendem Ergebnis zu kommen:
1: <Window x:Class="DepthSensorFundamentals.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="540" Width="640" MinWidth="640" MinHeight="480"
5: Loaded="Window_Loaded" Closed="Window_Closed">
6: <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
7: <Image Name="ImageDepth" Width="640" Height="480" MouseUp="ImageDepth_MouseUp" MouseMove="ImageDepth_MouseMove" />
8: </StackPanel>
9: </Window>
1: /// <summary>
2: /// Interaction logic for MainWindow.xaml
3: /// </summary>
4: public partial class MainWindow : Window
5: {
6: /// <summary>
7: /// Kinect Sensor
8: /// </summary>
9: public KinectSensor Kinect { get; set; }
10:
11: /// <summary>
12: /// Die Bitmap die in der Anwendung angezeigt wird
13: /// </summary>
14: public WriteableBitmap DepthBitmap { get; set; }
15:
16: /// <summary>
17: /// Pixel Array welches ein Image des Streams enthält
18: /// </summary>
19: public short[] PixelData { 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 an einem Rechner zu betreiben. Für
29: // unsere Zwecke reicht es deshalb aus den ersten verfügbaren Sensor mit Status Connected
30: // zu ermitteln.
31: this.Kinect = KinectSensor.KinectSensors.FirstOrDefault(ks => ks.Status == KinectStatus.Connected);
32:
33: if (this.Kinect != null)
34: {
35: try
36: {
37: // Initialisieren der DepthStreams
38: InitializeDepthStream();
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: private void InitializeDepthStream()
60: {
61: // Fenstertitel dahingehend ändern das der aktuelle Modus angezeigt wird
62: this.Title = this.Kinect.DepthStream.Range.ToString();
63:
64: // Der jeweilige Stream muß für die Verarbeitung enabled werden. Wahlweise können auch
65: // die Formate Resolution320x240Fps30 sowie Resolution80x60Fps30 verarbeitet werden
66: // YuvResolution640x480Fps15 verarbeitet werden. Sie können Enable auch ohne ImageFormat
67: // aufrufen dann wird den das unten angegebene Format ist per defualt gesetzt.
68: this.Kinect.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);
69:
70: // Zwar könnte man auch eine normale Bitmap nehmen aber die WriteableBitmap eignet sich
71: // durch die Benutzun von internen Puffern besser zum rendern von Frames. Da wir den Depth
72: // Stream nutzen wir das PixelFormat Gray16. Dieses besagt dass 16 Pixel pro Tiefeninformation
73: // genutzt werden, was insgesamt 65536 Schattierungen von Grau bedeutet. Alternativ können
74: // Sie natürlich auch andere Formate nutzen wie z.B: BGR32, allerdings sollten Sie vorher
75: // darüber nachdenken wie sinnvoll das für Ihren Fall ist. Wenn Sie dies tun, dann sollten
76: // Sie allerdings daran denken der Pixel Array entsprechend zu dimensionieren. Bei BGR32
77: // wäre dies z.B. byte[] pixelData = new byte[frame.PixelDataLength*sizeof(int)] weil das
78: // BGR32 Format ja 32 Bit anstatt 16 Bit ist. Hinzu kommt dass man den Intensitätsgrad dann
79: // immer manuell für Rot, Grün und Blau setzen muss.
80: this.DepthBitmap = new WriteableBitmap(this.Kinect.DepthStream.FrameWidth, this.Kinect.DepthStream.FrameHeight,
81: 96.0, 96.0, PixelFormats.Gray16, null);
82:
83: // Die Source des Images in der Anwendung ist natürlich die vorher definierte WriteableBitmap
84: this.ImageDepth.Source = this.DepthBitmap;
85:
86: // Last-but-not-least sorgen wir dafür dass die FrameReady Events in der Methode
87: // Kinect_DepthFrameReady verarbeitet werden können
88: this.Kinect.DepthFrameReady += this.Kinect_DepthFrameReady;
89: }
90:
91: private void Kinect_DepthFrameReady(object sender, DepthImageFrameReadyEventArgs e)
92: {
93: // Der DepthImageFrame stellt den Container für die Daten des Sensors
94: using (DepthImageFrame frame = e.OpenDepthImageFrame())
95: {
96: if (frame != null)
97: {
98: // Der Byte Array der vom Tiefensensor geliefert wird ist Array of short wobei
99: // jeder Wert des Arrays zusammengesetzt ist aus Tiefeninformation und PlayerIndex
100: PixelData = new short[frame.PixelDataLength];
101:
102: // Übername der Daten aus dem Frame
103: frame.CopyPixelDataTo(PixelData);
104:
105: // Die WriteableBitmap benötigt beim rendern die Angabe welcher Bereich aktualisiert werden
106: // soll. Hier benutzen wir die gesamte Fläche der Bitmap
107: Int32Rect depthBitmapRect = new Int32Rect(0, 0, this.Kinect.DepthStream.FrameWidth, this.Kinect.DepthStream.FrameHeight);
108:
109: // Die Angabe wieviele Pixel jeweils auf ein mal upgedated werden sollen. Mit der vorliegenden
110: // Definition geschieht dies also Zeilenweise
111: int depthBitmapStride = this.Kinect.DepthStream.FrameWidth * this.Kinect.DepthStream.FrameBytesPerPixel;
112:
113: // Erstellen der neuen Bitmap
114: this.DepthBitmap.WritePixels(depthBitmapRect, PixelData, depthBitmapStride, 0);
115: }
116: }
117: }
118: }
So weit, so gut, was aber noch fehlt ist die Initialisierung sowie die Verarbeitung des Tiefensensor-Streams. Bevor ich jedoch dazu komme, möchte ich Ihnen noch den grundlegendsten Unterschied zum RGB Stream aufzeigen. Jedes DepthFrame wird nämlich durch ein short Array repräsentiert bei dem jedes Element eine einzelne Tiefeninformation enthält. Und da die 16 Bit mehr als genug Platz haben die Tiefe zu speichern, ist in jedem Element noch zusätzlich der sogenannte Playerindex zu finden (auf den ich ein anderes mal eingehe). Dies sieht dann in der Praxis wie folgt aus:
Mit diesen Informationen im Kopf und mit ein bisschen Kinect API Know-How können wir schließlich unsere Applikation vollenden. Und auch wenn wir damit sicherlich nicht den Olymp der Kinect Applikationen erklimmen werden, klar ist das wir mit recht einfachen Mitteln sehr schnell ans Ziel gelangen. Und weil dies der Fall ist fügen wir unserer Applikation noch ein Textfeld unterhalb des Image ein in der wir noch gerne den Abstand des Punktes in Millimetern zum Sensor ausgeben lassen wollen. Dazu fügen wir folgenden Code für den MouseMove-Event hinzu:
1: private void ImageDepth_MouseMove(object sender, MouseEventArgs e)
2: {
3: // Position innerhalb des Image ermitteln
4: Point point = e.GetPosition(ImageDepth);
5:
6: // Index des Pixels berechnen
7: int pixelIndex = (int)(point.X + ((int)point.Y * this.DepthBitmap.Width));
8:
9: // Tiefeninformation extrahieren (letzte drei Bits im short Wert sind ja der Player index
10: int depth = this.PixelData[pixelIndex] >> DepthImageFrame.PlayerIndexBitmaskWidth;
11:
12: // Player index extrahieren
13: int playerIndex = this.PixelData[pixelIndex] & DepthImageFrame.PlayerIndexBitmask;
14:
15: TextDepthInfo.Text = string.Format("Depth {0} mm - Player {1} ", depth, playerIndex);
16: }
Aus der Nähe betrachtet
Haben Sie die Anwendung schon einmal gestartet? Nun … wenn Sie selbst stolzer Besitzer einer Kinect für Windows sind, dann könnte das Fenster das Ihren Bildschirm ziert in etwa so aussehen:
Da wir oben keine konkrete Initialisierung vorgenommen haben arbeitet die Kinect hier gerade im Default Mode. Stellt sich die Frage wie wir nun in den Near Mode wechseln und was für Auswirkungen das hat. Die Antwort ist denkbar einfach, aber bevor ich Sie Ihnen gebe sollten wir das WPF Fenster noch schnell erweitern. Der Einfachheit halber ergänzen wir die Anwendung einfach um ein Mouseup-Event in dem wir folgende Zeilen hinzufügen:
1: private void ImageDepth_MouseUp(object sender, MouseButtonEventArgs e)
2: {
3: if (this.Kinect != null)
4: {
5: // Zu beachten ist dass dieser Code eine InvalidOperationException werfen würde
6: // wenn er mit einer Xbox 360 Kinect ausgeführt werden würde
7: if (this.Kinect.DepthStream.Range == DepthRange.Default)
8: {
9: this.Kinect.DepthStream.Range = DepthRange.Near;
10: }
11: else
12: {
13: this.Kinect.DepthStream.Range = DepthRange.Default;
14: }
15:
16: this.Title = this.Kinect.DepthStream.Range.ToString();
17: }
18: }
Klicken wir nun zur Laufzeit auf das Image dann switchen wir zwischen Default- und Near-Mode hin und her. Optisch lässt sich die Veränderung hier sehr gut erkennen:.
Der Vollständigkeit halber finden sie den kompletten Source-Code hier.
Ausblick
Aus dem heutigen Artikel haben Sie wahrscheinlich zwei Dinge mitgenommen. Einerseits wie man den Zugriff auf die Daten vom Tiefensensor realisiert und andererseits, dass die guten alten Lehrstunden der Sesamstraße immer noch gut genug sind den Artikel eines Blogs einzuleiten. Ich hoffe Ihnen hat der Artikel genauso viel Spaß gemacht wie mir und ich würde mich freuen wenn Sie beim nächsten Mal wieder ein wenig Bewegungsdrang verspüren. Dann wird Ihnen die Kinect unter die Haut gehen.
Sie sehen gerade einen Platzhalterinhalt von Facebook. Um auf den eigentlichen Inhalt zuzugreifen, klicken Sie auf die Schaltfläche unten. Bitte beachten Sie, dass dabei Daten an Drittanbieter weitergegeben werden.
Mehr InformationenSie sehen gerade einen Platzhalterinhalt von Instagram. Um auf den eigentlichen Inhalt zuzugreifen, klicken Sie auf die Schaltfläche unten. Bitte beachten Sie, dass dabei Daten an Drittanbieter weitergegeben werden.
Mehr InformationenSie sehen gerade einen Platzhalterinhalt von X. Um auf den eigentlichen Inhalt zuzugreifen, klicken Sie auf die Schaltfläche unten. Bitte beachten Sie, dass dabei Daten an Drittanbieter weitergegeben werden.
Mehr Informationen