如何使用 Kinect 來錄影,錄製儲存彩色影片(For Windows SDK V1)

今天主題是「如何使用 Kinect 來錄影,錄製儲存彩色影片」,
這話題是很妙的,因為一旦打開這個"影像"潘多拉的盒子,
KT 就有說不完的故事,所以KT在 Kinect 教學目錄 未來章節裡,
加了很多有關影像處理的主題 ,如果時間允許的話,
KT 很願意跟大家分享,過去在影像處理,學到的一些小小心得,
如:圖片、圖形、文字和人臉等影像辨識,這些都是非常有趣的話題。
但坦白說這些影像處理概念,其實早在以前,
用一台小小的 Webcam就都可以辦到了,
所以你知道的,一台小小的Webcam 就可以讓KT 玩的很開心,哈哈哈~
而今天我們拿來跟 Kinect 做結合,看能不能擦出不一樣的火花,
而談到影像處理,當然不能不去提到 OpenCV (開放式源碼電腦視覺),
但KT怕範圍太廣,發散掉,所以KT也只大概介紹一下OpenCV的學習資源,
所以對影像處理有興趣的玩家,
可以去「OpenCV 英文官網」 或 「OpenCV中文官網」 瞧一瞧,
超過500項的應用函數:

英文官網上推薦的 OpenCV 相關書籍
Learning OpenCV: Computer Vision with the OpenCV Library

OpenCV 2 Computer Vision Application Programming Cookbook

而我們今天則是要使用,特地為.NET平台量身訂做,
包裝OpenCV處理影像函數的 Emgu CV ,
細節部分可以連過去官網看詳細介紹。
Emgu CV 架構圖 (最少瞄一下橘色和藍色字):

所以我們今天處裡影像串流部分,
則是要將影像框架(Frame) 丟給 Emgu CV 幫我們處理,
將影像框架(Frame) 組合成一部影片,
這樣做法,好比像是拍照一樣,將每張圖片依照時間先後順序存起來,
然後快速每秒以30張(30 FPS,Frames per second) 或15張(15 FPS)連續撥放,
看起來就像動畫一樣。
所以我們會去引用到 Emgu CV裡的Emgu.CV、Emgu.UI、Emgu.Util等dll,
所以在專案裡,要記得加入參考:

而因為怕客戶端,電腦裡不見得有安裝 Emgu CV,
所以你可以將這些dll一起包到安裝檔裡或放在bin資料夾與執行檔在一起,
縮短減少環境造成的臭蟲(bug)率,
否則換了電腦,就像移植心臟一樣,不一定能動(執行)。
而我們需要Emgu CV裡面的 image 型別,所以要特別要將他給擴展出來,
否則會於內建C# image 型別衝突,這邊KT採用Jarrett Webb & James Ashley在
Beginning Kinect Programming with the Microsoft Kinect SDK ,
所附的EmguImageExtensions.cs來解決這件事。
這次程式設計的畫面:

右邊紅色圓圈錄影按鈕按下,會先詢問儲存位置
(當然你也可以改成固定儲存位置與自動命名影片檔名),
選擇確定儲放位置後,即開始錄影,

此時紅色圓圈錄影按鈕,會變成停止錄影按鈕,

所以按一下停止錄影按鈕,即會停止錄影。
而左邊調整Kinect上下角度設定部分,麻煩參考:
1.如何調整 Kinect 底座馬達上下旋轉角度 (For Windows SDK V1)
2.如何使用 Kinect 來拍照,儲存彩色影像圖片(For Windows SDK V1)
KT這邊就不在重覆贅述了。
而KT這邊採用Emgu CV 2.3.0 ,最新且是穩定版 (Stable Version),
詳細API 使用說明文件可以參考:Emgu CV 2.3.0 Online Documentation


右邊紅色圓圈錄影按鈕按下,會先詢問儲存位置
(當然你也可以改成固定儲存位置與自動命名影片檔名),
選擇確定儲放位置後,即開始錄影,

此時紅色圓圈錄影按鈕,會變成停止錄影按鈕,

所以按一下停止錄影按鈕,即會停止錄影。
而左邊調整Kinect上下角度設定部分,麻煩參考:
1.如何調整 Kinect 底座馬達上下旋轉角度 (For Windows SDK V1)
2.如何使用 Kinect 來拍照,儲存彩色影像圖片(For Windows SDK V1)
KT這邊就不在重覆贅述了。
而KT這邊採用Emgu CV 2.3.0 ,最新且是穩定版 (Stable Version),
詳細API 使用說明文件可以參考:Emgu CV 2.3.0 Online Documentation

- 錄影按鈕與停止按鈕:
 
//錄影按鈕
        private void btn_Rec(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            if (!_isRecording)
            {                
                //開啟儲存位置詢問視窗
                Microsoft.Win32.SaveFileDialog openFileDialog = new Microsoft.Win32.SaveFileDialog();
                openFileDialog.FileName = "Video";
                openFileDialog.DefaultExt = ".avi";
                openFileDialog.Filter = "AVI文件|*.avi|所有文件|*.*";
                openFileDialog.ShowDialog();
                _fileName = openFileDialog.FileName;
                _isRecording = true;//啟動錄影
                SetShootImage(1);//設定錄影時,按鈕為停止圖案
            }
            else
            {
                //呼叫停止錄影
                StopRecording();
                SetShootImage(0);////設定停止錄影時,按鈕為錄影圖案
            }
           
        }
- 開始錄影與停止錄影函數
 
 //開始錄影函數
        void Record(ColorImageFrame image)
        {
            if (_isRecording)
            {
                _videoArray.Add(image.ToOpenCVImage());
            }
        }
        //停止錄影函數
        void StopRecording()
        {
            if (!_isRecording)
                return;
            //using (VideoWriter vw = new VideoWriter(_fileName, 0, 30, 640, 480, true))
            using (VideoWriter vw = new VideoWriter(_fileName, 30, 640, 480, true))
            {
                for (int i = 0; i < _videoArray.Count(); i++)
                    vw.WriteFrame(_videoArray[i]);
            }
            _fileName = string.Empty;
            _videoArray.Clear();
            _isRecording = false;
        }     
  
其中當按下停止錄影按鈕時,會花一點處理等待時間,
此時你可以去呼叫忙碌指示(Busy Indicator),或自己製作,

KT這邊沒加入這個,建議加入,優化User Experience (使用者介面經驗)。
影片教學:
(KT下次補上)
C# 完整程式碼:
using System;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Kinect;
using System.Collections.Generic;
using Emgu.CV;
using Emgu.CV.VideoSurveillance;
using Emgu.CV.Structure;
using System.Drawing.Imaging;
using System.Drawing;
using System.ComponentModel;
using System.Runtime.InteropServices;
using ImageManipulationExtensionMethods;
namespace KinectRecordingVideo_Demo
{
    public partial class MainWindow : Window
    {
        //宣告Kinect裝置變數名稱
        KinectSensor sensor = null;
        byte[] pixelData;
        bool _isRecording = false;
        public MainWindow()
        {
            InitializeComponent();
            //宣告視窗載入與卸載事件
            this.Loaded += new RoutedEventHandler(MainWindow_Loaded);
            this.Unloaded += new RoutedEventHandler(MainWindow_Unloaded);
        }
        void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            InitialKinect();//當視窗載入時,初始化 Kinect裝置
        }
        void MainWindow_Unloaded(object sender, RoutedEventArgs e)
        {
            UninitiaKinect();//視窗關閉時,關閉 Kinect裝置
        }
        //初始化 Kinect裝置
        private void InitialKinect()
        {
            //Save.Visibility = Visibility.Hidden;
            if (sensor != null)
            {
                UninitiaKinect();
            }
            sensor = KinectSensor.KinectSensors[0];
            sensor.Start();
            sensor.ColorFrameReady += runtime_VideoFrameReady;
            sensor.ColorStream.Enable();
        }
        //關閉 Kinect裝置
        private void UninitiaKinect()
        {
            if (sensor == null)
            {
                return;
            }
            sensor.ColorFrameReady -= runtime_VideoFrameReady;
            sensor.Stop();
            sensor = null;
        }
        //彩色影像處理函數
        void runtime_VideoFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            bool receivedData = false;
            using (ColorImageFrame CFrame = e.OpenColorImageFrame())
            {
                if (CFrame == null)
                {
                    // The image processing took too long. More than 2 frames behind.
                }
                else
                {
                    pixelData = new byte[CFrame.PixelDataLength];
                    CFrame.CopyPixelDataTo(pixelData);
                    receivedData = true;
                    Record(CFrame);
                }
            }
            if (receivedData)
            {   //將彩色影像資料,轉成點陣圖(Bitmap)
                BitmapSource source = BitmapSource.Create(640, 480, 96, 96,
                        PixelFormats.Bgr32, null, pixelData, 640 * 4);
                //將Bitmap 影像秀到Image控制項上
                videoImage.Source = source;
            }
        }
        string _fileName;
        List> _videoArray = new List>();
        //開始錄影函數
        void Record(ColorImageFrame image)
        {
            if (_isRecording)
            {
                _videoArray.Add(image.ToOpenCVImage());
            }
        }
        //停止錄影函數
        void StopRecording()
        {
            if (!_isRecording)
                return;
            //using (VideoWriter vw = new VideoWriter(_fileName, 0, 30, 640, 480, true))
            using (VideoWriter vw = new VideoWriter(_fileName, 30, 640, 480, true))
            {
                for (int i = 0; i < _videoArray.Count(); i++)
                    vw.WriteFrame(_videoArray[i]);
            }
            _fileName = string.Empty;
            _videoArray.Clear();
            _isRecording = false;
        }     
        //錄影按鈕
        private void btn_Rec(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            if (!_isRecording)
            {                
                //開啟儲存位置詢問視窗
                Microsoft.Win32.SaveFileDialog openFileDialog = new Microsoft.Win32.SaveFileDialog();
                openFileDialog.FileName = "Video";
                openFileDialog.DefaultExt = ".avi";
                openFileDialog.Filter = "AVI文件|*.avi|所有文件|*.*";
                openFileDialog.ShowDialog();
                _fileName = openFileDialog.FileName;
                _isRecording = true;//啟動錄影
                SetShootImage(1);//設定錄影時,按鈕為停止圖案
            }
            else
            {
                //呼叫停止錄影
                StopRecording();
                SetShootImage(0);////設定停止錄影時,按鈕為錄影圖案
            }
           
        }
        //===設定 Shoot 狀態圖片===Start===
        void SetShootImage(int State)
        {
            // Create source.
            BitmapImage bi = new BitmapImage();
            // BitmapImage.UriSource must be in a BeginInit/EndInit block.
            bi.BeginInit();
            if (State == 0)
            {
                //未 Rec,秀shoot               
                bi.UriSource = new Uri(@"/Image/Rec.png", UriKind.RelativeOrAbsolute);
            }
            else if (State == 1)
            {
                //已 Rec,秀Stop
                bi.UriSource = new Uri(@"/Image/Stop.png", UriKind.RelativeOrAbsolute);
            }
            bi.EndInit();
            // Set the image source.
            Rec.Source = bi;
        }
        //調整Kinect上下角度
        private void AdjustAngle(object sender, RoutedEventArgs e)
        {
            Btn_AdjustAngle.IsEnabled = false;//將按鈕設為失能(Disable),等角度整套設定完再致能(Enabled)
            //設定角度
            if (sensor != null && sensor.IsRunning)
            {
                //將滑桿的值存到 ElevationAngle
                sensor.ElevationAngle = (int)slider1.Value;
            }
            Btn_AdjustAngle.IsEnabled = true;//恢復按鈕設定功能 
        }
     
    }
}
    
範例程式碼下載:
相關文章參考:
HKT線上教學教室 - Kinect 教學目錄