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