星期日, 4月 15, 2012

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

28 則留言 :

  1. KT有興趣去做kinect virtual camera 不用openNI的版本嗎?

    回覆刪除
  2. ^o^謝謝KT老大熱心教學^o^

    回覆刪除
  3. 那就期待您的作品了,我都還沒看過有人去作,真是奇怪,明明有很多人都在找

    回覆刪除
  4. 不好意思,再提問一下,如果想在Beta1使用,
    將參考換成using Microsoft.Research.Kinect.Nui;
    runtime_VideoFrameReady() 這個方法要怎麼修改,謝謝您。

    回覆刪除
  5. 可以參考這篇移植教學文章
    http://robrelyea.wordpress.com/2012/02/01/k4w-code-migration-from-beta2-to-v1-0-managed/

    可以參考到互相對應API

    回覆刪除
  6. 你好,
    第一個請想問的是:錄出來的色彩似乎有些變調,這是什麼造成的?
    第二個請想問的是:加上人體骨架的錄影,該怎麼修改或增加?

    謝謝您

    回覆刪除
  7. 哈囉~阿皮,

    A1:
    色彩差異,主要原因是影像壓縮比造成,詳細可以參考Emgu CV 等相關說明文件。

    A2:
    人體骨架與彩色影像,在同一個imagebox裡面,錄影擷取此imagebox每張圖片。

    回覆刪除
  8. 您好,我叫做小花,
    目前有關於Kinect SDK v1 for WindowForm 的教學嗎??

    回覆刪除
  9. 哈囉~
    我想要用彩色+深度影像作3D的影像,但是在我修改了這個程式之後,發現我所加入的深度影像感覺不是很正確,執行結果會是深度影像在彩色影像中一直閃爍,看過Heresy大大利用openNI所做的彩色+深度影像重疊似乎就是彩色和深度交疊在一起,並不會一閃一閃的情況發生,再者使用錄影程式下去錄,發現錄出來的影像只有彩色畫面的說,是這邊的程式還要再修改錄彩色+深度嗎??還是前面的深度影像就有問題了呢??導致他錄不到??

    回覆刪除
  10. Kinect for windows SDK 1.5,
    裡面有附一個彩色+深度影像的精采範例。

    供你參考

    回覆刪除
  11. 感謝KT的回覆.
    後來發現我現在只要抓下彩色及深度兩個視窗的影像即可.請問如果使用彩色+深度影像的範例及錄影的範例去改,在我加入錄影功能後我該如何同時錄下兩邊的影像輸出成兩邊的影像串流呢??還有深度功能的錄影是否要對這個程式範例中的錄影程式做修改呢? 如何修改呢?? 不好意思,問題頗多 打擾了!

    回覆刪除
  12. 哈囉 sheldon,

    一樣的處理概念,
    只是 pixelData 換成是深度影像的 pixelData。

    回覆刪除
  13. 可否請KT在這個地方講更詳細一點呢? 有一點不懂,我的深度影像函數是使用另一篇彩色+深度的範例加入的,不知道您說的pixelData要從哪邊替換呢? 錄影函數那邊要怎麼處理呢?才能同時錄下彩色影片及深度影片呢??

    回覆刪除
  14. 哈囉sheldon,

    彩色和深度只是擷取到的資料不同,以彩色色影像來講,
    即是透過RGB的攝影機擷取彩色影像資料,這資料即是pixelData,
    我們將這pixelData,也就是這一點一點的資料位元,將他組合起來,
    並轉檔存成bmp,成為一張張圖片,
    丟到imagebox上,imagebox 將陸陸續續收到每張 bmp,畫面就會一直變換,
    這畫面我們看起來就好像看到動畫影片一樣,好像小時候看到一隻扇子,
    一面是籠子,一面是一隻鳥,快速旋轉之下,視覺暫留的關係,鳥就好像就在籠子裡面。

    而我們目前擷取影像都喜歡設30FPS(每秒在畫面秀30張圖片),
    即是一秒就有30張圖片從你眼睛晃過,

    所以如果我們將這pixelData全給存下來,即是錄影下來的影片。
    而KT這篇文的例子即是丟給 OpenCV幫我們把 pixelData全存下來,
    相對的深度影像也是一樣的做法。
    更多影像處理,可以參考 OpenCV等相關影像書籍。

    回覆刪除
  15. sheldon 提到...感謝KT的回覆!
    因為深度影像的範例他是以Gary16來輸出灰階圖,請問這樣的話錄影函數的這個部分:
    _videoArray.Add(image.ToOpenCVImage());
    該怎麼更改呢?? 因為這個部分是為了彩色影像所寫的,不知道深度影像要怎麼寫下錄影函數??
    不好意思,問題很多,打擾您

    回覆刪除
  16. 請問一下,為什麼錄出的影片顏色會變青色?請問要去哪兒查相關資料可以將此問題改掉呢?謝謝。

    回覆刪除
  17. 我的影片也會變成青色,請問一下要修改哪裡呢? 謝謝。

    回覆刪除
  18. 我上面是想說重新編輯一下問題,不小心按到刪除,我的問題和前面一樣~輸出是藍色畫面,不知要改哪裡??請大大指教。

    回覆刪除
  19. KT您好~電腦已安裝了Emgu CV,可是錄製的顏色卻變了調,對於新手能否提供多一點的資訊呢? 謝謝~

    回覆刪除
  20. 把所有的改為就可以改善顏色青青的問題

    回覆刪除
  21. 把所有的Rgb改為Bgr就可以改善顏色青青的問題

    回覆刪除

回覆意見時,麻煩輸入一下暱稱
(隨便取個名字也好~ ^_^)
好讓我方便回覆您的問題,
選擇「名稱/網址」輸入您的暱稱,
麻煩一下,謝謝大家。

關閉廣告 [X]