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
}

Friday, September 11, 2015

การตรวจจับตำแหน่งศีรษะด้วย Kinect

นอกจาก Kinect จะสามารถอ่านภาพวิดีโอ มันยังสามารถตรวจจับข้อต่อต่างๆของมนุษย์ด้วยกล้อง infrared ซึ่งจะสามารถอ่านได้ถึง 20 ข้อต่อ (Joints) ได้แก่ หัว คอ ไหล่ ข้อศอก ข้อมือ มือ เอว สะโพก ท่อนขา หัวเข่า ข้อเท้า และ เท้า

ในตอนนี้เราจะมาลองตรวจจับตำแหน่งของศีรษะกันครับ

พิกัดในการอ่านตำแหน่งของ Kinect จะมีแกนดังนี้ ถ้าวาง Kinect ไว้หน้าเรา จุดกำเนิดจะอยู่ที่กล้อง แกน X จะมีทิศไปทางขวา แกน Y จะชี้ขึ้น และแกน Z จะพุ่งจากกล้องเข้าหาเรา

นั่นคือถ้าเราขยับไปทางขวามือของเรา เมื่ออ่านค่าข้อต่อ ค่า X ก็จะเพิ่ม หรือ ถ้าเรากระโดด ค่า Y ก็จะเพิ่ม และถ้าเราถอยห่างออกจากกล้อง ค่า Z ก็จะเพิ่ม

สำหรับการตรวจจับศีรษะ เราจะสร้าง interface ของโปรแกรมให้อ่านค่าได้ประมาณนี้

ในกรณีที่กล้องจับตำแหน่งศีรษะไม่ได้

ในกรณีที่กล้องจับตำแหน่งศีรษะได้

ซึ่งค่าพิกัดที่ได้คือตำแหน่งของศีรษะเมื่อเทียบกับตัวกล้อง ในหน่วยเมตร

โค้ดของส่วน 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="72" HorizontalAlignment="Center"/>
    </StackPanel>
</Window>
โค้ดที่เหลือก็จะประมาณนี้ครับ ลองสังเกตคำอธิบายที่หมายเหตุที่แทรกไว้ได้ครับ

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 HeadTracking
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    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";
                //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)
                {
                    //focus only the trackable skeletons
                    if (skeleton.TrackingState == SkeletonTrackingState.Tracked)
                    {
                        //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);
                    }
                }

                //show message on textblock
                TextHead.Text = message;
            }
        }
    }
}

Sunday, September 6, 2015

เพิ่มประสิทธิภาพการแสดงและประมวลผลภาพจาก Kinect ด้วย OpenNextFrame และ Thread

จนถึงตอนนี้ เราสามารถเชื่อมต่อ อ่านค่า และประมวลผลภาพจาก Kinect ได้ แต่มีข้อจำกัดเรื่องประสิทธิภาพอยู่บ้าง สำหรับคอมพิวเตอร์ที่มี CPU ไม่สูงพอ นั่นคือ การได้ภาพจาก Kinect จะมาจาก Event ชื่อ ColorImageFrameReadyEventArgs ซึ่งจะมาอย่างต่อเนื่องเมื่อกล้องของ Kinect ทำงาน

ปัญหาจะเกิดในกรณีที่ CPU ประมวลผลภาพในเฟรมไม่ทัน ในขณะที่เฟรมใหม่ก็เข้ามาแล้ว

เพื่อจะแก้ปัญหานี้ เราสามารถบังคับให้รับเฟรมใหม่เข้ามา ตามเวลาที่ระบุไว้ (ไม่ต้องตามกล้องของ Kinect อย่างเดียว) โดยใช้ method ชื่อ ColorStream.OpenNextFrame(time) ซึ่งจะมี argument เป็น เวลาในหน่วยมิลลิวินาที

นอกจากนี้แล้ว เรายังสามารถเพิ่มประสิทธิภาพการทำงาน โดยกำหนดให้การแสดงและประมวลผลภาพเป็น thread ใหม่ แยกออกจาก thread ในการจัดการ User Interface ได้ ซึ่งทั้งสอง thread สามารถทำงานควบคู่กันไปได้

โค้ดใหม่ที่ปรับปรุงก็จะประมาณนี้ครับ ดูคำอธิบายที่ comment ในโค้ดได้เลยครับ

ปล. โค้ดนี้ยังมีส่วนผิดพลาดในการบันทึกภาพ เนื่องจากการใช้งาน thread อยู่ ถ้าผมเจอวิธีแก้จะกลับมาปรับปรุงอีกทีครับ

ส่วนของ UI
<Window x:Class="KinectPerformance.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" SizeToContent="WidthAndHeight" Loaded="Window_Loaded" Closing="Window_Closing">
    <StackPanel Orientation="Horizontal" >
        <Image Name="kinectVideo" Height="480" Width="640" />
        <StackPanel Width="80">
            <CheckBox Content="Reflect" Name="cbReflect" Margin="10,20" Click="cbReflect_Click"/>
            <Button Name="bttSave" Content="Save" Margin="10,20" Click="onSave" HorizontalAlignment="Center" VerticalAlignment="Center"></Button>
        </StackPanel>
    </StackPanel>
</Window>

ส่วนโค้ด
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;

//import additional references
using Microsoft.Kinect;     //for Kinect
using System.IO;            //for saving image
using System.Threading;     //for separating video display process

namespace KinectPerformance
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        //Variables
        KinectSensor myKinect;
        Thread updateVideoThread;
        bool displayActive = true;      //flag to show/not show video
        byte[] colorData = null;        //array to keep image pixel data
        bool reflect = false;           //flag to reflect image
        bool takePicture = false;       //flag to capture a video frame
        BitmapSource capBitmap = null;      //bitmap for a captured frame
        WriteableBitmap imageBitmap = null; //bitmap for keeping processed image
        int frameWidth, frameHeight, frameBytePerPixel;

        public MainWindow()
        {
            InitializeComponent();
        }

        //===================== when window is loaded =======================
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            //if Kinect is not found
            if (KinectSensor.KinectSensors.Count == 0)
            {
                MessageBox.Show("No Kinects 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 color video stream
                myKinect.ColorStream.Enable();
                //Start the sensor
                myKinect.Start();
            }
            catch
            {
                MessageBox.Show("Initialize Kinect failed!", "Error");
                //end this app
                Application.Current.Shutdown();
                return;
            }

            //create a thread to display video
            updateVideoThread = new Thread(new ThreadStart(videoDisplay));
            //updateVideoThread.IsBackground = true;

            //start the video display thread
            updateVideoThread.Start();
        }
        //----------------- End window loaded --------------------
        
        //========================= Method to display video ===========================
        void videoDisplay()
        {
            while (displayActive)
            {
                //request next video frame in 10 millisec
                using (ColorImageFrame colorFrame = myKinect.ColorStream.OpenNextFrame(10))
                {
                    //if the next video frame is not ready or not completely processed
                    if (colorFrame == null) 
                        continue;
                    //if not exists, create an array of pixel data and get frame's properties
                    if (colorData == null)
                    {
                        colorData = new byte[colorFrame.PixelDataLength];
                        frameWidth = colorFrame.Width;
                        frameHeight = colorFrame.Height;
                        frameBytePerPixel = colorFrame.BytesPerPixel;
                    }

                    //extract pixel data from the frame to the array
                    colorFrame.CopyPixelDataTo(colorData);

                    //call unsafe method to modify the pixel data
                    if (reflect)
                    {
                        //reflect a half left image to a half right
                        reflectImage(colorData);
                    }

                    //prepare a saved frame
                    if (takePicture)
                    {
                        //create a bitmap from the current frame
                        capBitmap = BitmapSource.Create(
                            colorFrame.Width, colorFrame.Height, 96, 96, PixelFormats.Bgr32, null,
                            colorData, colorFrame.Width * colorFrame.BytesPerPixel);
                        //switch a photo captured flag
                        takePicture = false;
                    }

                    //This thread is not a main thread. It cannot update the UI, must use this technique:
                    Dispatcher.Invoke(new Action(() => updateDisplay()));
                }
            }
            // when we get here the program is ending – stop the sensor
            myKinect.Stop();
        } 
        //-------------- end videoDisplay() method ------------------------------

        //================= unsafe method to modify pixel data ===================
        //Reflect the half left of the image to the half right
        unsafe void reflectImage(byte[] colorData)
        {
            //fixed pixel data array in memory, otherwise it can be moved and be inaccessible by a pointer
            fixed (byte* pImage = colorData)
            {
                //in this block the data array in memory will be fixed                
                //we use integer pointer to move by one pixel, faster than by one byte
                //however, we cannot get color data, OK for reflection                

                //loop through each row
                for (int row = 0; row < frameHeight; row++)
                {
                    //set a start pointer to the beginning of each row
                    int* pStart = (int*)pImage + (row * frameWidth);
                    //set an end pointer to the end of each row
                    int* pEnd = pStart + frameWidth - 1;
                    //loop through each column of the row
                    while (pStart < pEnd)
                    {
                        //assign right pixel to left pixel
                        *pEnd = *pStart;
                        //move start pointer to right
                        pStart++;
                        //move end pointer to left
                        pEnd--;
                    }
                }//end for                               
            }
        }
        //-------------- end unsafe image processing method ---------------------

        //==================== Method to update display ======================
        void updateDisplay()
        {
            //For the first video frame, no bitmap, create a writable bitmap of video size
            if (imageBitmap == null)
            {
                imageBitmap = new WriteableBitmap(
                    frameWidth,
                    frameHeight,
                    96,     //dpiX
                    96,     //dpiY
                    PixelFormats.Bgr32,
                    null    //palette:none
                    );
            }
            //For other video frames, write video data to bitmap
            imageBitmap.WritePixels(
                new Int32Rect(0, 0, frameWidth, frameHeight),
                colorData,  //video data
                frameWidth * frameBytePerPixel,      //stride
                0   //offset to the array
                );
            //set bitmap to image view
            kinectVideo.Source = imageBitmap;
        }
        //---------------------- end update display ---------------------------

        //================= Method to save a captured frame ========================
        private void onSave(object sender, RoutedEventArgs e)
        {
            //set a flag to capture image in video frame event
            takePicture = true;
            // Configure save file dialog box
            Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
            dlg.FileName = "capture"; // Default file name
            dlg.DefaultExt = ".jpg"; // Default file extension
            dlg.Filter = "Pictures (.jpg)|*.jpg"; // Filter files by extension

            //if select Dialog OK
            if (dlg.ShowDialog() == true)
            {
                // Save document
                string filename = dlg.FileName;
                using (FileStream stream = new FileStream(filename, FileMode.Create))
                {
                    //encode and save to JPG format
                    JpegBitmapEncoder encoder = new JpegBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(capBitmap));
                    encoder.Save(stream);
                }
            }            
        }
        //------------------------ end save image -----------------

        //================== When window is closed ================
        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            //set flag to end display loop in video thread
            displayActive = false;
        }

        //------------------- end closing window -------------------

        //======================== Checkbox click ==================
        private void cbReflect_Click(object sender, RoutedEventArgs e)
        {
            if (cbReflect.IsChecked == true)
                reflect = true;
            else
                reflect = false;
            //We can replace the if...else with
            //reflect = (cbReflect.IsChecked == true);
        }
        
    }//end class
}//end namespace

การประมวลผลภาพโดยการสะท้อน (Mirror) และการจับภาพจากวิดีโอเฟรมของ Kinect ตอนที่ 2

ตอนที่แล้ว เราเขียนโค้ดเพื่อทำ mirror effect ให้กับวิดีโอ ดังนี้

มาดูคำอธิบายคร่าวๆกันครับ

Method หลักก็จะมี 4 Method ด้วยกันดังนี้

1. Window_Loaded()
อันนี้ก็ตามชื่อ เมื่อหน้าต่างถูกโหลดขึ้นมาเรียบร้อย ก็ไปเรียก Video Event ของ Kinect เพื่อตรวจสอบว่าวิดีโอพร้อมหรือไม่ ถ้าพร้อมก็เรียก Method ในการแสดงผลเพื่อเอาข้อมูลภาพมาแสดงบน Image UI ที่เตรียมไว้

2. myKinect_ColorFrameReady()
ซึ่งจะถูกเรียกจาก Window_Loaded()
ใน Method นี้ก็ทำการอ่านค่าวิดีโอจาก Kinect รวมถึงประมวลผลภาพต่างๆ แล้วแสดงผลบน Image UI

นอกจากนี้ ยังมีการตรวจสอบว่า Checkbox ถูกเลือกหรือไม่ รวมถึงมีการกดปุ่มบันทึกภาพหรือไม่

3. reflectImage()
Method นี้เป็นกระบวนการประมวลผลภาพแบบ Mirror โดยมีหลักการดังนี้
3.1 ได้ข้อมูลภาพจาก Kinect มาเป็นแบบ byte array
3.2 เอาพอยน์เตอร์ pImage ไปชี้ไว้ที่ตำแหน่งเริ่มต้นของ array
3.3 วนลูปสำหรับทุกแถว
   3.3.1 ให้พอยน์เตอร์ pStart ชี้ที่หัวแถว และ pEnd ชี้ที่ท้ายแถว
   3.3.2 เปลี่ยนค่าใน array ณ ตำแหน่งที่ pEnd ชี้อยู่ ให้มีค่าเท่ากับค่าในตำแหน่งที่ pStart ชี้อยู่ (ทำ Mirror นั่นเอง)
   3.3.3 ขยับ pStart ไปคอลัมน์ถัดไป และ ถอย pEnd มายังคอลัมน์ก่อนหน้า
   3.3.4 ทำจนกระทั่ง pStart ชี้ที่เดียวกับ pEnd จึงไปเริ่มต้นใหม่ในแถวถัดไป


4. onSave()
อันนี้ก็สร้าง dialog มาถามว่าจะบันทึกไฟล์ชื่ออะไร ที่ไหน แล้ว encode array ให้เป็นรูป รวมถึงเขียนไฟล์

โดยรวมก็ประมาณนี้ครับ

Monday, July 27, 2015

Example Environment ใน LaTeX

อยากได้ Example แบบมีตัวเลขให้ตามบทแบบนี้


ซึ่งพอไปค้นหาดูในคำสั่ง LaTeX แล้วก็ไม่เจอ อันที่ใกล้และง่ายที่สุดก็อาจจะใช้คำสั่ง

\newtheorem{example}{Example}[chapter]

ในส่วนของ preamble และพอจะใช้งานจริงก็จะต้องใช้

\begin{example}
...
\end{example}

แต่ก็จะพบว่าตัวอักษรจะเป็นตัวเอียงทั้งหมด ตามรูปแบบปกติของ theorem

ถ้าอยากให้เป็นตัวอักษรปกติ ก็จะต้องใช้ package "amsthm" ดังนี้ในส่วนของ preamble

\usepackage{amsthm}

\theoremstyle{definition}
\newtheorem{example}{Example}[chapter]

อย่างไรก็ตาม ผมอยากได้เส้นขีดแสดงว่า Example นี้สิ้นสุดแล้ว ซึ่งไม่รู้จะทำอย่างไรใน package ข้างต้น

เลยมาเลือกวิธีใหม่ โดยการกำหนด Environment ใหม่ซะเอง ซึ่งดัดแปลงมาจากหลายๆเว็บ (ของเค้าซับซ้อน ผมไม่เข้าใจ T_T) ได้แบบนี้ครับ วางโค้ดไว้ที่ส่วนของ preamble เหมือนเดิม

%=== example environment===
\newcounter{example}[chapter]
\renewcommand{\theexample}{\thechapter.\arabic{example}}
\newenvironment{example}{
 \refstepcounter{example}
 \noindent \textbf{Example~\theexample}
}
{
 \begin{center}
  ******************* 
 \end{center}
}
%==========================

เวลาใช้งานจริงก็ใช้เหมือนเดิมครับ คือ
\begin{example}
 The program xyz below shows how to create an animation.
 
 My program here ...
\end{example}
ลองทดสอบและปรับคำสั่งให้ได้รูปแบบตามใจชอบเลยครับ

อ้างอิง
https://en.wikibooks.org/wiki/LaTeX/Theorems