如何使用 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 教學目錄

這個網誌中的熱門文章

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

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

Android Studio 歷代版本下載點

nano 文字編輯器

2022 最新入門零基礎 Flutter教學 【Flutter 程式設計入門實戰 30 天】Flutter 教學課程目錄 (IntelliJ IDEA 開發教學)