1. 程式人生 > >Kinect嚐鮮(1)——第一個程式

Kinect嚐鮮(1)——第一個程式

       曾經微軟宣傳Kinect宣傳的很火,但一直沒有捨得買一臺。第一次接觸是在某個hackathon上,想做一個空氣滑鼠的專案,藉助Kinect實現的,感覺這個產品挺驚豔。最近想方設法借到一臺一代的Kinect for Windows,還有微軟官方的開發書籍(《Kinect應用開發實戰——用最自然的方式與機器對話》),略研究了下Kinect的開發。

一、環境配置

       關於Kinect的介紹網上有很多資料,這裡不再贅述。既然是開發微軟自家的產品,肯定要上微軟全家桶,VS2015(C#)+SDK V1.8+Developer toolkit V1.8。其中SDK可以直接在微軟官網上下載,除了官方SDK,還有其它的SDK,我不是很瞭解,所以不敢妄言介紹。一代Kinect有windows和xbox 360兩個版本,windows版本的Kinect前面寫著“Kinect”,而xbox 360版本前面寫著“xbox 360”,xbox版的連線電腦需要有轉接線,但是很詭異的是我曾經直接用xbox版的連線電腦也成功了。並且我最開始安裝的SDK是V2.0,也能成功跑起來Kinect V1……雖說SDK V2.0只能驅動二代Kinect,但也許微軟還是照顧了舊版本的硬體吧。不過為了穩妥,還是安裝SDK V1.8,並且使用Kinect for Windows。

       將Kinect連線上電腦之後,可以開啟Developer toolkit browser,執行其中某一個demo,來檢驗Kinect是否正常工作。一般情況下,正常工作是Kinect正面綠燈一直亮。在這裡不得不吐槽下Kinect的電源線質量問題,兩次接觸Kinect都是電源線有問題。這時只有USB供電,電壓不足,狀態是紅燈一直亮,這種情況下更換電源線就好了。

二、正式開發

       環境配好之後,開啟VS2015,新建一個WPF窗體工程的解決方案,然後在引用裡面新增Kinect v1.8,然後在程式中using Microsoft.Kinect即可。Kinect視訊方面主要包括採集彩色資料、採集深度資料、追蹤骨骼三個功能,此外還有通過麥克風陣列採集聲音資料。

三、第一個程式

      Kinect有兩個攝像頭,分別是彩色攝像頭和深度攝像頭,所以第一個程式就是實現獲取兩個攝像頭採集到的彩色視訊流和深度視訊流。在MainWindow.xaml檔案裡,在工具箱中選中Image,向窗體中新增兩個大小為640*480的Image,不重疊,分別命名為depthImage和colorImage;在Window標籤中新增屬性Loaded="Window_Loaded" Closed=Window_Closed,最終Xaml檔案程式碼如下:

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:KinectWpfApplication1"
        xmlns:WpfViewers="clr-namespace:Microsoft.Samples.Kinect.WpfViewers;assembly=Microsoft.Samples.Kinect.WpfViewers" x:Class="KinectWpfApplication1.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="590" Width="1296"
        Loaded="Window_Loaded"
        Closed="Window_Closed">
    <Grid>
        <Image x:Name="depthImage" HorizontalAlignment="Left" Height="480" Margin="650,0,-0.4,0" VerticalAlignment="Top" Width="640"/>

            <Image x:Name="colorImage" HorizontalAlignment="Left" Height="480" VerticalAlignment="Top" Width="640"/>
    </Grid>
</Window>

      Kinect的呼叫是使用已經封裝好的KinectSensor類,用於管理Kinect資源。該類同樣支援多個Kinect同時工作,因為我只弄到一臺,所以多臺Kinect的情況不予考慮。定義KinectSensor  _kinect;在Window_Load()中新增函式StartKinect(),然後定義StartKinect函式如下:

        private void StartKinect()
        {
            if (KinectSensor.KinectSensors.Count <= 0)
            {
                MessageBox.Show("No Kinect device foound!");
                return;
            }
            _kinect = KinectSensor.KinectSensors[0];
            //MessageBox.Show("Status:" + _kinect.Status);
            _kinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);
            _kinect.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);

            _kinect.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(KinectColorFrameReady);
            _kinect.DepthFrameReady += new EventHandler<DepthImageFrameReadyEventArgs>(KinectDepthFrameReady);

            //_kinect.AllFramesReady += new EventHandler<AllFramesReadyEventArgs>(_kinect_AllFrameReady);
            _kinect.Start();
        }

       第一個if可以判斷有幾臺Kinect工作,如果沒有就提示,然後獲取第一臺Kinect裝置。定義彩色視訊流和深度視訊流的格式,包括顏色格式、視訊大小和幀速。一代Kinect只有640*480FPS30和1280*720FPS12,二代的影象解析度和幀速都比一代優秀。接下來註冊事件,這裡要介紹一下Kinect的兩種模型——事件模型和輪詢模型,事件模型就如同上述程式碼中,彩色視訊採集到一幀之後會觸發事件ColorFrameReady,然後在事件屬性ColorFrameReadyArgs中處理資料,深度視訊和骨骼追蹤也是如此,除了分別處理事件,還有三種幀都採集完畢後觸發的AllFramesReady,但是集中處理的程式碼執行後十分卡頓,所以我沒有使用這一事件;另一種是輪詢模型,與事件模型的“等待Kinect給資料”不同,該模型是去向Kinect“主動要資料”,這種方法更快,也更適合多執行緒,這一模型以後會介紹。事件模型的優點在於程式碼可讀性好,對程式語言來說顯得更加優雅。我們註冊的處理深度資料和彩色視訊資料的方法程式碼如下:

        private void KinectColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            using (ColorImageFrame colorImageFrame = e.OpenColorImageFrame())
            {
                if (colorImageFrame == null)
                    return;
                byte[] pixels = new byte[colorImageFrame.PixelDataLength];
                colorImageFrame.CopyPixelDataTo(pixels);
                int stride = colorImageFrame.Width * 4;
                colorImage.Source = BitmapSource.Create(colorImageFrame.Width, colorImageFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride);
            }
        }

        private void KinectDepthFrameReady(object sender, DepthImageFrameReadyEventArgs e)
        {
            using (DepthImageFrame depthImageFrame = e.OpenDepthImageFrame())
            {
                if (depthImageFrame == null)
                    return;
                short[] depthPixelData = new short[depthImageFrame.PixelDataLength];
                depthImageFrame.CopyPixelDataTo(depthPixelData);
                byte[] pixels = ConvertDepthFrameToColorFrame(depthPixelData, ((KinectSensor)sender).DepthStream);
                int stride = depthImageFrame.Width * 4;
                depthImage.Source = BitmapSource.Create(depthImageFrame.Width, depthImageFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride);
            }
        }


       ConvertDepthFrameToColorFrame()是將深度資料流轉為彩色資料,以便在Image控制元件上顯示。

        /// <summary>
        /// 將16位灰階深度圖轉為32位彩色深度圖
        /// </summary>
        /// <param name="depthImageFrame">16位灰階深度圖</param>
        /// <param name="depthImageStream">用於獲得深度資料流的相關屬性</param>
        /// <returns></returns>
        private byte[] ConvertDepthFrameToColorFrame(short[] depthImageFrame, DepthImageStream depthImageStream)
        {
            byte[] depthFrame32 = new byte[depthImageStream.FrameWidth * depthImageStream.FrameHeight * bgr32BytesPerPixel];
            //通過常量獲取有效視距,不用硬編碼
            int tooNearDepth = depthImageStream.TooNearDepth;
            int tooFarDepth = depthImageStream.TooFarDepth;
            int unknowDepth = depthImageStream.UnknownDepth;
            for (int i16 = 0, i32 = 0; i16 < depthImageFrame.Length && i32 < depthFrame32.Length; i16++, i32 += 4)
            {
                int player = depthImageFrame[i16] & DepthImageFrame.PlayerIndexBitmask;
                int realDepth = depthImageFrame[i16] >> DepthImageFrame.PlayerIndexBitmaskWidth;
                //通過位運算,將13位的深度圖裁剪位8位
                byte intensity = (byte)(~(realDepth >> 4));
                if (player == 0 && realDepth == 0)
                {
                    depthFrame32[i32 + redIndex] = 255;
                    depthFrame32[i32 + greenIndex] = 255;
                    depthFrame32[i32 + blueIndex] = 255;
                }
                else if (player == 0 && realDepth == tooFarDepth)
                {
                    //深紫色
                    depthFrame32[i32 + redIndex] = 66;
                    depthFrame32[i32 + greenIndex] = 0;
                    depthFrame32[i32 + blueIndex] = 66;
                }
                else if (player == 0 && realDepth == unknowDepth)
                {
                    //深棕色
                    depthFrame32[i32 + redIndex] = 66;
                    depthFrame32[i32 + greenIndex] = 66;
                    depthFrame32[i32 + blueIndex] = 33;
                }
                else
                {
                    depthFrame32[i32 + redIndex] = (byte)(intensity >> intensityShiftByPlayerR[player]);
                    depthFrame32[i32 + greenIndex] = (byte)(intensity >> intensityShiftByPlayerG[player]);
                    depthFrame32[i32 + blueIndex] = (byte)(intensity >> intensityShiftByPlayerB[player]);
                }
            }
            return depthFrame32;
        }

       其中player是Kinect通過深度資料判斷出視野內有多少人,人體區域用鮮豔的顏色標記。

       最後,在 Window_Closed 中關閉Kinect:

        private void Window_Closed(object sender, EventArgs e)
        {
            if (_kinect != null)
            {
                if (_kinect.Status == KinectStatus.Connected)
                {
                    _kinect.Stop();
                }
            }
        }

       完成程式碼後,就可以生成並運行了。這裡暫時不貼圖了,等以後有圖了再貼。文章末尾會附上完整程式碼。

四、一點感想

       Kinect SDK支援用C++和C#開發,因為C#比較簡單再加上VS2015的足夠智慧化,許多方法直接看函式名就知道用處,所以我選擇使用C#。微軟在那本書中介紹了NUI的概念,再加上對Kinect開發的瞭解,以及最近 HoloLens 發行,我感覺Kinect + HoloLens 才是絕配——一個負責處理資料和顯示,一個負責人機互動連線現實世界和虛擬世界。NUI必然是未來的趨勢,而實現NUI 90%會依靠Kinect或者其它功能類似 Kinect 的裝置來實現。雖然 Kinect 市場佔有率很小,應用也非常少,但不得不令我猜測微軟在下很大的一盤棋,藉此來定義未來的作業系統。

PS. 完整程式碼(XAML完整程式碼在上面):

程式碼中還包括將深度資料轉為256色灰階影象並用亮綠色標記人體區域的方法。但這個方法不知為什麼一識別出人體就會變得非常卡頓,希望有大神看到後能告知一下。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Kinect;
using System.Threading;

namespace KinectWpfApplication1
{
    /// <summary>
    /// MainWindow.xaml 的互動邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        private KinectSensor _kinect;

        const float maxDepthDistance = 4095;
        const float minDepthDistance = 850;
        const float maxDepthDistancOddset = maxDepthDistance - minDepthDistance;

        private const int redIndex = 2;
        private const int greenIndex = 1;
        private const int blueIndex = 0;

        private static readonly int[] intensityShiftByPlayerR = { 1, 2, 0, 2, 0, 0, 2, 0 };
        private static readonly int[] intensityShiftByPlayerG = { 1, 2, 2, 0, 2, 0, 0, 1 };
        private static readonly int[] intensityShiftByPlayerB = { 1, 0, 2, 2, 0, 2, 0, 2 };
        private static readonly int bgr32BytesPerPixel = (PixelFormats.Bgr32.BitsPerPixel + 7) / 8;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            StartKinect();
        }

        private void StartKinect()
        {
            if (KinectSensor.KinectSensors.Count <= 0)
            {
                MessageBox.Show("No Kinect device foound!");
                return;
            }
            _kinect = KinectSensor.KinectSensors[0];
            //MessageBox.Show("Status:" + _kinect.Status);
            _kinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);
            _kinect.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);
            _kinect.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(KinectColorFrameReady);
            _kinect.DepthFrameReady += new EventHandler<DepthImageFrameReadyEventArgs>(KinectDepthFrameReady);

            //_kinect.AllFramesReady += new EventHandler<AllFramesReadyEventArgs>(_kinect_AllFrameReady);
            _kinect.Start();
        }

        private void KinectColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            using (ColorImageFrame colorImageFrame = e.OpenColorImageFrame())
            {
                if (colorImageFrame == null)
                    return;
                byte[] pixels = new byte[colorImageFrame.PixelDataLength];
                colorImageFrame.CopyPixelDataTo(pixels);
                int stride = colorImageFrame.Width * 4;
                colorImage.Source = BitmapSource.Create(colorImageFrame.Width, colorImageFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride);
            }
        }

        private void KinectDepthFrameReady(object sender, DepthImageFrameReadyEventArgs e)
        {
            using (DepthImageFrame depthImageFrame = e.OpenDepthImageFrame())
            {
                if (depthImageFrame == null)
                    return;
                short[] depthPixelData = new short[depthImageFrame.PixelDataLength];
                depthImageFrame.CopyPixelDataTo(depthPixelData);
                byte[] pixels = ConvertDepthFrameToColorFrame(depthPixelData, ((KinectSensor)sender).DepthStream);
                int stride = depthImageFrame.Width * 4;
                depthImage.Source = BitmapSource.Create(depthImageFrame.Width, depthImageFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride);
            }
            /*
            using (DepthImageFrame depthImageFrame = e.OpenDepthImageFrame())
            {
                if (depthImageFrame == null)
                    return;
                byte[] pixels = ConvertDepthFrameToGrayFrame(depthImageFrame);
                int stride = depthImageFrame.Width * 4;
                depthImage.Source = BitmapSource.Create(depthImageFrame.Width, depthImageFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride);
            }*/
        }

        /// <summary>
        /// 單色直方圖計算公式,返回256色灰階,顏色越黑越遠。
        /// </summary>
        /// <param name="dis">深度值,有效值為......</param>
        /// <returns></returns>
        private static byte CalculateIntensityFromDepth(int dis)
        {
            return (byte)(255 - (255 * Math.Max(dis - minDepthDistance, 0) / maxDepthDistancOddset));
        }
        /// <summary>
        /// 生成BGR32格式的圖片位元組陣列
        /// </summary>
        /// <param name="depthImageFrame"></param>
        /// <returns></returns>
        private byte[] ConvertDepthFrameToGrayFrame(DepthImageFrame depthImageFrame)
        {
            short[] rawDepthData = new short[depthImageFrame.PixelDataLength];
            depthImageFrame.CopyPixelDataTo(rawDepthData);
            byte[] pixels = new byte[depthImageFrame.Height * depthImageFrame.Width * 4];
            for (int depthIndex = 0, colorIndex = 0; depthIndex < rawDepthData.Length && colorIndex < pixels.Length; depthIndex++, colorIndex += 4)
            {
                int player = rawDepthData[depthIndex] & DepthImageFrame.PlayerIndexBitmask;
                int depth = rawDepthData[depthIndex] >> DepthImageFrame.PlayerIndexBitmaskWidth;
                if (depth <= 900)
                {
                    //離Kinect很近
                    pixels[colorIndex + blueIndex] = 255;
                    pixels[colorIndex + greenIndex] = 0;
                    pixels[colorIndex + redIndex] = 0;
                }
                else if (depth > 900 && depth < 2000)
                {
                    pixels[colorIndex + blueIndex] = 0;
                    pixels[colorIndex + greenIndex] = 255;
                    pixels[colorIndex + redIndex] = 0;
                }
                else if (depth >= 2000)
                {
                    //離Kinect超過2米
                    pixels[colorIndex + blueIndex] = 0;
                    pixels[colorIndex + greenIndex] = 0;
                    pixels[colorIndex + redIndex] = 255;
                }
                //單色直方圖著色
                byte intensity = CalculateIntensityFromDepth(depth);
                pixels[colorIndex + blueIndex] = intensity;
                pixels[colorIndex + greenIndex] = intensity;
                pixels[colorIndex + redIndex] = intensity;
                //如果是人體區域,用亮綠色標記
                /*if (player > 0)
                {
                    pixels[colorIndex + blueIndex] = Colors.LightGreen.B;
                    pixels[colorIndex + greenIndex] = Colors.LightGreen.G;
                    pixels[colorIndex + redIndex] = Colors.LightGreen.R;
                }*/
            }
            return pixels;
        }
        /// <summary>
        /// 將16位灰階深度圖轉為32位彩色深度圖
        /// </summary>
        /// <param name="depthImageFrame">16位灰階深度圖</param>
        /// <param name="depthImageStream">用於獲得深度資料流的相關屬性</param>
        /// <returns></returns>
        private byte[] ConvertDepthFrameToColorFrame(short[] depthImageFrame, DepthImageStream depthImageStream)
        {
            byte[] depthFrame32 = new byte[depthImageStream.FrameWidth * depthImageStream.FrameHeight * bgr32BytesPerPixel];
            //通過常量獲取有效視距,不用硬編碼
            int tooNearDepth = depthImageStream.TooNearDepth;
            int tooFarDepth = depthImageStream.TooFarDepth;
            int unknowDepth = depthImageStream.UnknownDepth;
            for (int i16 = 0, i32 = 0; i16 < depthImageFrame.Length && i32 < depthFrame32.Length; i16++, i32 += 4)
            {
                int player = depthImageFrame[i16] & DepthImageFrame.PlayerIndexBitmask;
                int realDepth = depthImageFrame[i16] >> DepthImageFrame.PlayerIndexBitmaskWidth;
                //通過位運算,將13位的深度圖裁剪位8位
                byte intensity = (byte)(~(realDepth >> 4));
                if (player == 0 && realDepth == 0)
                {
                    depthFrame32[i32 + redIndex] = 255;
                    depthFrame32[i32 + greenIndex] = 255;
                    depthFrame32[i32 + blueIndex] = 255;
                }
                else if (player == 0 && realDepth == tooFarDepth)
                {
                    //深紫色
                    depthFrame32[i32 + redIndex] = 66;
                    depthFrame32[i32 + greenIndex] = 0;
                    depthFrame32[i32 + blueIndex] = 66;
                }
                else if (player == 0 && realDepth == unknowDepth)
                {
                    //深棕色
                    depthFrame32[i32 + redIndex] = 66;
                    depthFrame32[i32 + greenIndex] = 66;
                    depthFrame32[i32 + blueIndex] = 33;
                }
                else
                {
                    depthFrame32[i32 + redIndex] = (byte)(intensity >> intensityShiftByPlayerR[player]);
                    depthFrame32[i32 + greenIndex] = (byte)(intensity >> intensityShiftByPlayerG[player]);
                    depthFrame32[i32 + blueIndex] = (byte)(intensity >> intensityShiftByPlayerB[player]);
                }
            }
            return depthFrame32;
        }
        
        private void Window_Closed(object sender, EventArgs e)
        {
            if (_kinect != null)
            {
                if (_kinect.Status == KinectStatus.Connected)
                {
                    _kinect.Stop();
                }
            }
        }
    }
}