星期四, 3月 08, 2012

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

11 則留言 :

  1. KT真厲害,終於找到研究KINECT SDK V1有中文的了...好感動。

    YAN其實是KT說的"萌萌",能跟著KT一起學KINECT真是太好了...。

    本身對KINECT有興趣也從BATA摸到現在還是沒有一點成就(YAN我英文很差)。

    本身雖然是資工系畢業,但程式語言上也遇到很多難關(C++,C都被當過,唯獨C#有過..)但是我還是對KINECT感興趣。

    看到KT的這些V1版的文章,真的是讓我太感動了~謝謝KT,今後將跟著KT學習~

    回覆刪除
  2. 感謝KT的分享,讓"萌萌"學到很多

    回覆刪除
  3. 原作者沒有加入平滑處理參數
    我的猜想是因為他用的是 kinect for "windows"
    所以在深度影像上的誤差已經很小了
    我問過有在玩的人
    聽說誤差來到0.5
    跟之前10-15跳有顯著差別

    回覆刪除
  4. 非常感谢KT讲解,
    我想请教问题如何识别握手手势?
    非常感谢

    回覆刪除
  5. 感謝KT大大的認真教學
    不過我有幾個問題想請教:
    1.CheckGesture這個method雖然可以防止一次判斷出多個動作,但好比說我今天兩手一起打開,這個method會選擇稍微比較早的那隻顯示揮動message,但當我接下來把那隻手收回來而另一隻手不動時,卻又會跳出另一隻手揮動的message,是不是有其他寫法比較好呢?
    2.KT大大目前寫出來的功能都是單階段判斷,只要任一個時間點手的X或Y座標軸跟頭的X.Y坐標軸有一定關係,就會判斷為某手勢。但我最近試著在寫多階段的手勢,猜想應該會需要用到時間的功能,好比說要擺動右手:就先判斷rightHand是否在rightShoulder右邊,接著看他有沒有移動到兩個shoulder中間,最後如果他有跑到leftShoulder左邊那就表示我成功做出一個右手擺動的手勢,如此之類的。不知道KT大大有沒有興趣也研究一下呢?

    回覆刪除
  6. 請問KT,這部分的範例程式碼能提供嗎?
    我找不到@@
    謝謝~~

    回覆刪除
  7. 請問~輸出左右鍵的功能,為甚麼我把註解拿掉,他說找不到forms的namespace???

    回覆刪除
  8. 打擾了~
    想請問一下您上面打的head.Position.X + 0.5
    請問0.5的單位是什麼?
    @_@我只是"萌萌"等級的~看不懂

    回覆刪除
  9. 打擾嚕
    想問一下這篇是在講C#
    那可以用C++寫嗎?!

    回覆刪除
  10. 請問要怎麼識別手掌是張開還是握拳?
    謝謝

    回覆刪除
  11. 手掌張開握拳是1.7版SDK的Interaction Stream的功能,自己聲明一個InteractionStream然後把深度與骨骼註冊到這個InteractionStream,在InteractionFrameReady事件中查看,這個Interaction數據與骨骼數據類似,但用戶中包含的是雙手的信息裏面有這樣一個HandEventType屬性

    回覆刪除

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

關閉廣告 [X]