如何使用 Kinect 辨識判斷手勢動作(For Windows SDK V1)

Photobucket

最近KT 陸陸續續收到一些問題反應,



KT在這邊"痾浪死"( announce ,翻譯:宣布) 一下,

讓後來的人,也可以得到解答。

反應: 降低文章等級


先謝謝大家的反應,因為KT之前一直鎖定不到讀者(好久貼的文都沒人反應),
怕大家都是"猛猛"級的,之前文章廢話就太多了,
直接貼CODE就好了,不用太多贅述說明~
結果兩三個來信,信中帶到是"萌萌"級的,希望KT可以再多一點說明。
OK~了解!!!
"英文"與"程式"的說明,未來盡可能再白話一點,不整份貼CODE。
之前V1版的文章,KT會再找時間,補拍影片來操作說明一下。

V1之前版本的就...呵呵呵~


好~今天要來談一下「如何使用 Kinect 辨識手勢動作」,

之前有幾位"達人"分享相關文章如 Kinect Toolbox

但有人反應那判斷演算法太複雜,對於"萌萌"有點吃力,

但其實那是滿嚴謹且精準的判斷方式。

我們聞香看看:
// Swipe to right
            if (ScanPositions((p1, p2) => Math.Abs(p2.Y - p1.Y) < SwipeMaximalHeight, // Height
                (p1, p2) => p2.X - p1.X > -0.01f, // Progression to right
                (p1, p2) => Math.Abs(p2.X - p1.X) > SwipeMinimalLength, // Length
                SwipeMininalDuration, SwipeMaximalDuration)) // Duration
            {
                RaiseGestureDetected("SwipeToRight");
                return;
            }

            // Swipe to left
            if (ScanPositions((p1, p2) => Math.Abs(p2.Y - p1.Y) < SwipeMaximalHeight,  // Height
                (p1, p2) => p2.X - p1.X < 0.01f, // Progression to right
                (p1, p2) => Math.Abs(p2.X - p1.X) > SwipeMinimalLength, // Length
                SwipeMininalDuration, SwipeMaximalDuration))// Duration
            {
                RaiseGestureDetected("SwipeToLeft");
                return;
            }

哇~還ABSㄟ這可不是什麼新款的煞車系統,而是傳回絕對值的數學函式,

萌萌看到這裡可能就開始倒胃,

想繼續往下看的,幾乎掛蛋,更何況這只是一小角的關鍵演算法。

別急著關閉這一個繁體中文僅搜尋到,可能還有一線生機的網頁,

Photobucket

秀上面這張圖,並非是獻寶說什麼KT搜尋排名第一位,

而是讓KT感到有點寒心,繁體中文玩家研究最新V1版的,

到今天搜尋為止,竟然沒半個人@@,

只有一個完全不正經、半調子、愛啦哩啦喳的KT文章在分享心得,

所以谷哥指引來KT這必然有一定的道理,我們應該也有一定的緣分在。

就姑且繼續看KT拉賽下去吧!!!



之前 Kinect PowerPoint Control 這位達人,

寫出一套易懂易用的辨識判斷手勢動作演算法,

但可惜目前這位作者無繼續更新。

SDK程式版本停留在beta 2版,V1版不在適用,需做移植更新的動作,

KT這邊就改一下將他升級到V1版,順便優化一下這位作者的程式,

延續這位達人的精神,但好像程式差異度高達80%,

等於快全部改寫,這是因為beta 2移植到V1的寫法與宣告幾乎完全不同 。




首先我們先討論一下怎麼使用Kinect 判斷手勢,

Kinect 的強項,最大特色即是可以做到骨架追蹤(Skeleton tracking)這件事,

可以判斷身體共20個關節點。

我們即是要用骨架追蹤這個原理,來做出追蹤手勢動作等相關應用程式,

從底下這張圖片得知,

Photobucket

骨架追蹤手掌節點名稱為HAND_RIGHT (右手)和HAND_LEFT (左手),

而以頭(HEAD)為中心點,來判斷手勢移動上下左右的位置。

所以我們將開啟骨架追蹤功能
(之前KT有討論過 如何使用 Kinect 骨架追蹤(For Windows SDK V1)
所以骨架追蹤基礎概念,這篇略過,詳細請參考那篇文章)

優化骨架追蹤方式,載入平滑處理參數,這是滿常見的作法:
原作者無加入此法,頗讓KT驚訝,若無此舉,還滿容易受雜訊干擾。
//平滑處理,防止高頻率微小抖動和突發大跳動造成的關節雜訊
            var parameters = new TransformSmoothParameters
            {
                Smoothing = 0.3f,
                Correction = 0.0f,
                Prediction = 0.0f,
                JitterRadius = 1.0f,
                MaxDeviationRadius = 0.5f
            };
            sensor.SkeletonStream.Enable(parameters);//載入平滑處理參數
            sensor.SkeletonStream.Enable();//開啟,骨架追蹤


改採用Coding4Fun的2D座標向量轉換式:

//處理螢幕大小2D座標值
        private float ScaleVector(int length, float position)
        {
            float value = (((((float)length) / 1f) / 2f) * position) + (length / 2);
            if (value > length)
            {
                return (float)length;
            }
            if (value < 0f)
            {
                return 0f;
            }
            return value;
        }

KT這邊判斷手勢動作共四種:右手揮動、左手揮動、右手舉高和左手舉高,
(延伸了原作者只做兩種手勢判斷式) 

圖解座標說明解釋部分,

KT這邊摒棄傳統會讓"萌萌"看了毛骨悚然的令什麼為X1和X2,

X2減掉X1又會變出啥挖糕,Y2>Y1成立後...等說明解釋,

改採用"萌萌專用"傻瓜標示法來說明。

  • 右手揮動 
Photobucket

以右手揮動判斷來看,抓取到右手與頭X座標關節點,
判斷右手X關節點是否有大於頭X座標關節點+0.5點的位置,
若有,則判斷為右手揮動。
//==="右手"揮動判斷式===
            if (rightHand.Position.X > head.Position.X + 0.5)
            {
                CheckGesture();
                if (CheckGesture_ready)
                {
                    isRightHelloGestureActive = true;                 

                    MessageBox.Show("右手揮動 000 ");
                }
            }
            else
            {               
                isRightHelloGestureActive = false;
            }

  • 左手揮動  
Photobucket
相對的左手揮動,抓取到左手與頭X座標關節點,
判斷左手X關節點是否有大於頭X座標關節點+0.5點的位置,
若有,則判斷為左手揮動。

//==="左手"揮動判斷式===
            if (leftHand.Position.X < head.Position.X - 0.5)
            {
                CheckGesture();
                if (CheckGesture_ready)
                {
                    isLeftHelloGestureActive = true;                    

                    MessageBox.Show("000 左手揮動");
                }
            }
            else
            {
                isLeftHelloGestureActive = false;
            }

  • 右手舉高  
Photobucket

右手舉高,則是抓取右手Y座標關節點,
判斷右手Y座標關節點是否大於(高於)頭Y座標節點位置,
若有,則判斷為右手舉高 。
//==="右手"舉高判斷式===
            if (rightHand.Position.Y > head.Position.Y)
            {
                CheckGesture();
                if (CheckGesture_ready)
                {
                    isRightHandOverHead = true;
                    MessageBox.Show("右手舉高 000 000 000 ");
                }
            }            
            else
            {
                isRightHandOverHead = false;
            }



  • 左手舉高  
Photobucket

左手舉高,則是抓取左手Y座標關節點,
判斷左手Y座標關節點是否大於(高於)頭Y座標節點位置,
若有,則判斷為左手舉高 。
//==="左手"舉高判斷式===
            if (leftHand.Position.Y > head.Position.Y)
            {
                CheckGesture();
                if (CheckGesture_ready)
                {
                    isLeftHandOverHead = true;
                    MessageBox.Show("000 000 000 左手舉高");
                }
            }
            else
            {
                isLeftHandOverHead = false;
            }    


有了以上概念,就可以完成Kinect 辨識判斷手勢動作等應用程式,

這邊KT示範當偵測到手勢動作時彈出訊息視窗(MessageBox),

當然如果你希望可以偵測到手勢動作時有其他動作,

如原作者,初始概念是用Kinect 判斷手勢揮動,

可以來切換投影片上一頁或下一頁。

此做法是當偵測到手勢動作時, 模擬送出鍵盤的動作如:

System.Windows.Forms.SendKeys.SendWait("{Left}");//模擬鍵盤左鍵按下動作
System.Windows.Forms.SendKeys.SendWait("{Right}");//模擬鍵盤右鍵按下動作

此舉當你打開PowerPoint(投影片)時,

即可做出利用Kinect判斷手勢來切換投影片。

影片教學:


本範例完整程式碼如下:
XAML CODE:

    
        
        
            
            
            
        
    



C# CODE:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Microsoft.Kinect;
using System.Linq;

namespace Kinect_GestureRecognition_Demo
{
    public partial class MainWindow : Window
    {
        //Instantiate the Kinect runtime. Required to initialize the device.
        //IMPORTANT NOTE: You can pass the device ID here, in case more than one Kinect device is connected.
        KinectSensor sensor = KinectSensor.KinectSensors[0]; //宣告 KinectSensor

        //變數初始化定義
        byte[] pixelData;
        Skeleton[] skeletons;
        bool isRightHelloGestureActive = false;
        bool isLeftHelloGestureActive = false;
        bool isRightHandOverHead = false;
        bool isLeftHandOverHead = false;
        bool CheckGesture_ready = false;

        public MainWindow()
        {
            InitializeComponent();

            //載入與卸載
            this.Loaded += new RoutedEventHandler(MainWindow_Loaded);
            this.Unloaded += new RoutedEventHandler(MainWindow_Unloaded);

            sensor.ColorStream.Enable();//開啟,彩色影像

            //平滑處理,防止高頻率微小抖動和突發大跳動造成的關節雜訊
            var parameters = new TransformSmoothParameters
            {
                Smoothing = 0.3f,
                Correction = 0.0f,
                Prediction = 0.0f,
                JitterRadius = 1.0f,
                MaxDeviationRadius = 0.5f
            };
            sensor.SkeletonStream.Enable(parameters);//載入平滑處理參數
            sensor.SkeletonStream.Enable();//開啟,骨架追蹤
        }

        //MainWindow 載入
        void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            sensor.SkeletonFrameReady += runtime_SkeletonFrameReady;
            sensor.ColorFrameReady += runtime_VideoFrameReady;
            sensor.Start();
        }

        //MainWindow 卸載
        void MainWindow_Unloaded(object sender, RoutedEventArgs e)
        {
            sensor.Stop();
        }   

        //彩色影像,處理函數
        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;
                }
            }

            if (receivedData)
            {
                BitmapSource source = BitmapSource.Create(640, 480, 96, 96,
                        PixelFormats.Bgr32, null, pixelData, 640 * 4);

                videoImage.Source = source;
            }
        }

        //骨架追蹤,處理函數
        void runtime_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
        {
            bool receivedData = false;

            using (SkeletonFrame SFrame = e.OpenSkeletonFrame())
            {
                if (SFrame == null)
                {
                    // The image processing took too long. More than 2 frames behind.
                }
                else
                {
                    skeletons = new Skeleton[SFrame.SkeletonArrayLength];
                    SFrame.CopySkeletonDataTo(skeletons);
                    receivedData = true;
                }
            }

            if (receivedData)
            {

                Skeleton currentSkeleton = (from s in skeletons
                                            where s.TrackingState == SkeletonTrackingState.Tracked
                                            select s).FirstOrDefault();

                if (currentSkeleton != null)
                {
                    //取得骨架關節點 3D(X、Y、Z)座標值。
                    var head = currentSkeleton.Joints[JointType.Head];
                    var rightHand = currentSkeleton.Joints[JointType.HandRight];
                    var leftHand = currentSkeleton.Joints[JointType.HandLeft];
                    
                    SetEllipsePosition(ellipseHead, head, false);
                    SetEllipsePosition(ellipseLeftHand, leftHand, isLeftHelloGestureActive);
                    SetEllipsePosition(ellipseRightHand, rightHand, isRightHelloGestureActive);

                  
                   ProcessGesture(head, rightHand, leftHand);//呼叫處理手勢函數
                 
                    
                }
            }
        }

        

        //設定圖案位置
        private void SetEllipsePosition(Ellipse ellipse, Joint joint, bool isHighlighted)
        {
            //將3D 座標轉換成螢幕上大小,如640*320 的 2D 座標值
            Microsoft.Kinect.SkeletonPoint vector = new Microsoft.Kinect.SkeletonPoint();
            vector.X = ScaleVector(640, joint.Position.X);
            vector.Y = ScaleVector(480, -joint.Position.Y);
            vector.Z = joint.Position.Z; // Z值原封不動

            Joint updatedJoint = new Joint();
            updatedJoint = joint;
            updatedJoint.TrackingState = JointTrackingState.Tracked;
            updatedJoint.Position = vector;

            //得到 2D座標值(X、Y)後,將值設定為圖案顯示的位置
            Canvas.SetLeft(ellipse, updatedJoint.Position.X);
            Canvas.SetTop(ellipse, updatedJoint.Position.Y);
        }

        //處理螢幕大小2D座標值
        private float ScaleVector(int length, float position)
        {
            float value = (((((float)length) / 1f) / 2f) * position) + (length / 2);
            if (value > length)
            {
                return (float)length;
            }
            if (value < 0f)
            {
                return 0f;
            }
            return value;
        }

        //判斷處理手勢函數
        private void ProcessGesture(Joint head, Joint rightHand, Joint leftHand)
        {
            //==="右手"揮動判斷式===
            if (rightHand.Position.X > head.Position.X + 0.5)
            {
                CheckGesture();
                if (CheckGesture_ready)
                {
                    isRightHelloGestureActive = true;                 

                    MessageBox.Show("右手揮動 000 ");
                    //System.Windows.Forms.SendKeys.SendWait("{Right}");

                }
            }
            else
            {               
                isRightHelloGestureActive = false;
            }

            //==="左手"揮動判斷式===
            if (leftHand.Position.X < head.Position.X - 0.5)
            {
                CheckGesture();
                if (CheckGesture_ready)
                {
                    isLeftHelloGestureActive = true;          

                    MessageBox.Show("000 左手揮動");
                    //System.Windows.Forms.SendKeys.SendWait("{Left}");
                }
            }
            else
            {
                isLeftHelloGestureActive = false;
            }

            //==="右手"舉高判斷式===
            if (rightHand.Position.Y > head.Position.Y)
            {
                CheckGesture();
                if (CheckGesture_ready)
                {
                    isRightHandOverHead = true;
                    MessageBox.Show("右手舉高 000 000 000 ");
                }
            }            
            else
            {
                isRightHandOverHead = false;
            }

            //==="左手"舉高判斷式===
            if (leftHand.Position.Y > head.Position.Y)
            {
                CheckGesture();
                if (CheckGesture_ready)
                {
                    isLeftHandOverHead = true;
                    MessageBox.Show("000 000 000 左手舉高");
                }
            }
            else
            {
                isLeftHandOverHead = false;
            }    
        }

        //判斷手勢是否就緒,防止同一個動作誤判
        private void CheckGesture()
        {
            if (!isLeftHelloGestureActive && !isRightHelloGestureActive && !isRightHandOverHead && !isLeftHandOverHead)
            {
                CheckGesture_ready = true;
            }
            else 
            {
                CheckGesture_ready = false;
            }
        }
    }
}


範例程式碼下載:



更多相關參考文章:
1.Kinect Toolbox
2.Kinect PowerPoint Control

3.HKT線上教學教室 - Kinect 教學目錄

這個網誌中的熱門文章

16天記下7000單字

nano 文字編輯器

Android Studio 歷代版本下載點

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

ProGuard 程式碼混淆保護