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

Photobucket

今天主題是「如何使用 Kinect 來錄影,錄製儲存彩色影片」,



這話題是很妙的,因為一旦打開這個"影像"潘多拉的盒子,

KT 就有說不完的故事,所以KT在 Kinect 教學目錄 未來章節裡,

加了很多有關影像處理的主題 ,如果時間允許的話,

KT 很願意跟大家分享,過去在影像處理,學到的一些小小心得,

如:圖片、圖形、文字和人臉等影像辨識,這些都是非常有趣的話題。


但坦白說這些影像處理概念,其實早在以前,

用一台小小的 Webcam就都可以辦到了,

所以你知道的,一台小小的Webcam 就可以讓KT 玩的很開心,哈哈哈~



而今天我們拿來跟 Kinect 做結合,看能不能擦出不一樣的火花,

而談到影像處理,當然不能不去提到 OpenCV (開放式源碼電腦視覺)

但KT怕範圍太廣,發散掉,所以KT也只大概介紹一下OpenCV的學習資源,

所以對影像處理有興趣的玩家,

可以去「OpenCV 英文官網」 或 「OpenCV中文官網」 瞧一瞧,

超過500項的應用函數:
Photobucket

英文官網上推薦的 OpenCV 相關書籍

Learning OpenCV: Computer Vision with the OpenCV Library
Photobucket

OpenCV 2 Computer Vision Application Programming Cookbook
Photobucket



而我們今天則是要使用,特地為.NET平台量身訂做,

包裝OpenCV處理影像函數的 Emgu CV  ,

細節部分可以連過去官網看詳細介紹。

Emgu CV 架構圖 (最少瞄一下橘色和藍色字):


Photobucket

所以我們今天處裡影像串流部分,

則是要將影像框架(Frame) 丟給 Emgu CV 幫我們處理,

將影像框架(Frame) 組合成一部影片,

這樣做法,好比像是拍照一樣,將每張圖片依照時間先後順序存起來,

然後快速每秒以30張(30 FPS,Frames per second) 或15張(15 FPS)連續撥放,

看起來就像動畫一樣。



所以我們會去引用到 Emgu CV裡的Emgu.CV、Emgu.UI、Emgu.Util等dll,

所以在專案裡,要記得加入參考:

Photobucket

而因為怕客戶端,電腦裡不見得有安裝 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來解決這件事。

這次程式設計的畫面:
Photobucket
右邊紅色圓圈錄影按鈕按下,會先詢問儲存位置
(當然你也可以改成固定儲存位置與自動命名影片檔名),


選擇確定儲放位置後,即開始錄影,
Photobucket



此時紅色圓圈錄影按鈕,會變成停止錄影按鈕,
Photobucket
所以按一下停止錄影按鈕,即會停止錄影。



而左邊調整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

Photobucket



  • 錄影按鈕與停止按鈕:
//錄影按鈕
        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),或自己製作,
Photobucket

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 教學目錄

這個網誌中的熱門文章

【從零開始學 Java 程式設計】 進階佈局管理器 - GridBagLayout

nano 文字編輯器

16天記下7000單字

最新入門零基礎 Java 教學【從零開始學 Java 程式設計】Java教學課程目錄 (IntelliJ IDEA 開發教學)

2023 最新入門零基礎 Kotlin教學【從零開始學 Kotlin 程式設計】Kotlin 教學課程目錄 (Android Kotlin, IntelliJ IDEA, Android Studio, Android APP 開發教學)