Thursday, October 22, 2015

คุณภาพและสถานะการตรวจจับ Skeleton และ ข้อต่อของ Kinect

ในการตรวจจับ Skeleton และข้อต่อ ของมนุษย์โดยใช้ Kinect สิ่งที่ควรคำนึงถึงตามลำดับคือ

1. สถานะในการตรวจจับ Skeleton (Skeleton tracking state)
เป็นสถานะที่บอกว่า Kinect ตรวจจับ Skeleton ได้หรือไม่ ซึ่งมีสามระดับคือ
  • ตรวจจับได้ (SkeletonTrackingState.Tracked หรือค่า 2)
  • ได้ตำแหน่งเท่านั้น (SkeletonTrackingState.PositionOnly หรือค่า 1) คือตรวจจับ Skeleton ไม่ได้ แต่ได้เฉพาะตำแหน่งโดยรวมของ Skeleton
  • ตรวจจับไม่ได้ (SkeletonTrackingState.NotTracked หรือค่า 0)

2. คุณภาพในการตรวจจับ Skeleton (Skeleton Information Quality)
การตรวจจับ skeleton จะมีประสิทธิภาพลดลง เมื่อเราอยู่นอกระยะที่กล้องสามารถตรวจจับได้ เช่น อยู่ใกล้หรือไกลเกินไป อยู่สูงหรือต่ำเกินไป หรือ อยู่ทางซ้ายหรือขวาของกล้องมากไป

Kinect จะมีการตรวจจับข้อมูลดังกล่าว โดยดูว่าเราอยู่ในตำแหน่งที่ตัดกับเฟรมของกล้องหรือไม่ โดยให้ผลลัพธ์ออกมาในตัวแปรที่ชื่อว่า ClippedEdges ซึ่งตัวแปรนี้จะเป็นผลรวมของสถานะ FrameEdges ที่มีค่าที่เป็นไปได้ดังนี้
0 หรือ 0000 ฐานสอง เท่ากับ สถานะปกติ ไม่อยู่ชิดขอบเฟรม (None)
1 หรือ 0001 ฐานสอง เท่ากับ อยู่ชิดขอบเฟรมด้านขวา (Right)
2 หรือ 0010 ฐานสอง เท่ากับ อยู่ชิดขอบเฟรมด้านซ้าย (Left)
4 หรือ 0100 ฐานสอง เท่ากับ อยู่ชิดขอบเฟรมด้านบน (Top)
8 หรือ 1000 ฐานสอง เท่ากับ อยู่ชิดขอบเฟรมด้านล่าง (Bottom)

สมมติว่า เราอยู่ชิดเฟรมไปทางด้านขวา และ อยู่ต่ำกว่าเฟรม ดังนั้น ผลรวมสถานะหรือค่า ClippedEdges ก็จะมีค่าเป็น 0001 & 1000 (ใช้เลขฐานสองมา bitwise and & กัน) ก็จะได้ผลลัพธ์เป็น 1001 หรือค่า 9

ในทางกลับกัน ถ้าเราต้องการบอกให้ผู้ใช้กล้องทราบว่า ควรจะขยับไปทางไหนเพื่อให้ไม่เลยหรือตัดกับเฟรมของกล้อง ก็เอาค่า ClippedEdges ไป bitwise and (&) กับทีละสถานะ เช่น ถ้ากล้องอ่านค่า ClippedEdges ได้ 9 (1001) จะได้ว่า
1001 & 0001 = 0001
1001 & 0010 = 0000
1001 & 0100 = 0000
1001 & 1000 = 1000

นั่นคือ ค่าที่ไม่เป็น 0 มีสองค่า คือค่า ขวา และ ล่าง ก็สามารถบอกผู้ใช้ให้ขยับไปทางซ้ายและขึ้นข้างบน เพื่อให้อยู่ในเฟรมของกล้อง

3. สถานะในการตรวจจับข้อต่อ (Joint Tracking State)
บางครั้ง Kinect ก็สามารถตรวจจับข้อต่อได้ชัดเจน หรือบางครั้งก็ไม่ได้ ขึ้นอยู่กับหลายปัจจัย ดังนั้น ตัวกล้องเองก็จะแสดงค่าสถานะ (JointTrackingState) ของแต่ละข้อต่ออยู่สามระดับคือ

  • ตรวจจับได้ (JointTrackingState.Tracked หรือค่า 2)
  • คาดการณ์ได้ (JointTrackingState.Inferred หรือค่า 1) คือตรวจจับข้อต่อไม่ได้ แต่พอคาดการณ์ได้ว่าข้อต่อนั้นอยู่ที่ไหน จากข้อต่อข้างเคียง สถานะนี้อาจเกิดขึ้นในกรณีที่ข้อต่อนั้นถูกบดบังด้วยวัตถุอื่นหรือด้วยเสื้อผ้าที่สวมใส่
  • ตรวจจับไม่ได้ (JointTrackingState.NotTracked หรือค่า 0)
โดยเราสามารถตรวจสอบได้จากตัวแปร TrackingState ว่ามีค่าเท่ากับค่าใดค่าหนึ่งในสามค่าข้างต้นหรือไม่

ในตัวอย่างนี้ เราจะมาลองเพิ่มโค้ดในการตรวจจับศีรษะ พร้อมๆกับแสดงคุณภาพและสถานะของการตรวจจับด้วย ผลลัพธ์ที่ได้คือ

กรณีหา Skeleton ไม่ได้

กรณีหา Skeleton ได้ และคุณภาพของ Skeleton และ Head Tracking ดี

กรณีหา Skeleton ได้ และคุณภาพของ Skeleton ดี แต่ว่า Head Tracking เป็นแบบ Inferred


กรณีหา Skeleton ได้ แต่ควรขยับตำแหน่ง และ Head Tracking ดี

โดยมีข้อจำกัดที่ว่า ผู้ทดสอบควรอยู่ห่างจาก Kinect ไม่ต่ำกว่าประมาณ 1.8 เมตร เพราะถ้าอยู่ใกล้กล้องเกินไป โปรแกรมอาจจะแสดงผลผิดพลาดได้

โค้ดของ UI
<Window x:Class="HeadDetection.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Head Detection" SizeToContent="Height" Width="800" Loaded="Window_Loaded">
    <StackPanel>
        <TextBlock Name="TextHead" Text="Head Position" FontSize="54" HorizontalAlignment="Center"/>
        <TextBlock Name="TextQuality" Text="Quality" FontSize="36" HorizontalAlignment="Center"/>
    </StackPanel>
</Window>

โค้ด C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using Microsoft.Kinect;

namespace HeadDetection
{
    public partial class MainWindow : Window
    {
        //variables
        KinectSensor myKinect;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            //Check if there is any connecting Kinect
            if (KinectSensor.KinectSensors.Count == 0)
            {
                MessageBox.Show("No Kinect detected!", "Error");
                //end this app
                Application.Current.Shutdown();
                return;
            }

            //Try to initialize Kinect
            try
            {
                //Get the first Kinect connected to this computer
                myKinect = KinectSensor.KinectSensors[0];
                //Enable the skeleton stream
                myKinect.SkeletonStream.Enable();
                //Start the sensor
                myKinect.Start();
            }
            catch
            {
                MessageBox.Show("Initialize Kinect failed!", "Error");
                //end this app
                Application.Current.Shutdown();
            }

            //Skeleton event handler
            myKinect.SkeletonFrameReady += new EventHandler<SkeletonFrameReadyEventArgs>(myKinect_SkeletonFrameReady);
        }

        private void myKinect_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
        {
            //array of skeleton data
            Skeleton[] skeletons = null;

            using (SkeletonFrame frame = e.OpenSkeletonFrame())
            {
                string message = "No Skeleton Data";
                string qualtiy = "";

                //if there is skeleton data
                if (frame != null)
                {
                    //create skeleton array
                    skeletons = new Skeleton[frame.SkeletonArrayLength];
                    //copy skeleton data to array
                    frame.CopySkeletonDataTo(skeletons);
                }

                //if no skeleton data
                if (skeletons == null)
                    return;

                //for each skeleton data in the skeleton array, there will be upto 6 possible skeletons (men)
                //only 2 skeletons are fully tracked, other 4 are for positions only (no joint info)
                foreach (Skeleton skeleton in skeletons)
                {
                    //============ 1. Check status of skeleon ==============
                    if (skeleton.TrackingState == SkeletonTrackingState.Tracked)
                    {
                        //===========2. check quality of skeleton==========
                        if(skeleton.ClippedEdges == FrameEdges.None)
                            qualtiy = "Good skeleton quality";
                        else
                        {
                            if((skeleton.ClippedEdges & FrameEdges.Right) != 0)
                                qualtiy += "Move left ";
                            if((skeleton.ClippedEdges & FrameEdges.Left) != 0)
                                qualtiy += "Move right ";
                            if((skeleton.ClippedEdges & FrameEdges.Top) != 0)
                                qualtiy += "Move down ";
                            if((skeleton.ClippedEdges & FrameEdges.Bottom) != 0)
                                qualtiy += "Move up ";
                        }

                        //get the head joint
                        Joint headJoint = skeleton.Joints[JointType.Head];
                        //get the head joint's position
                        SkeletonPoint headPosition = headJoint.Position;

                        //show the head position with one decimal or 0.1 meter
                        message = string.Format("Head: X:{0:0.0} Y:{1:0.0} Z:{2:0.0}",
                            headPosition.X,
                            headPosition.Y,
                            headPosition.Z);

                        //========3. check joint traking state (head only)======
                        if(headJoint.TrackingState == JointTrackingState.Inferred) 
                        {
                            message += " Inferred";
                        }
                    }
                }

                //show message on textblock
                TextHead.Text = message;
                TextQuality.Text = qualtiy;
            }
        }//end method

    }//end class
}

No comments:

Post a Comment