Wednesday, April 15, 2015

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

ลองมาทำ mirror effect ให้กับวิดีโอและทำปุ่มสำหรับบันทึกรูป ให้ได้ผลลัพธ์ประมาณนี้กันครับ

เมื่อคลิกที่ checkbox ที่มีข้อความว่า Reflect ผลลัพธ์ก็จะเปลี่ยนเป็น

นอกจากนี้เรายังสามารถกดปุ่ม Save เพื่อบันทึกรูปขณะนั้นได้

ส่วนของ UI
<Window x:Class="KinectMirrorSave.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">
    <StackPanel Orientation="Horizontal">
        <Image Name="kinectVideo" Height="480" Width="640" />
        <StackPanel Width="80">
            <CheckBox Content="Reflect" Name="cbReflect" Margin="10,20"/>
            <Button Name="bttSave" Content="Save" Margin="10,20" Click="onSave" HorizontalAlignment="Center" VerticalAlignment="Center"></Button>            
        </StackPanel>
    </StackPanel>
</Window>
โค้ด (รายละเอียดจะอธิบายในตอนที่ 2 นะครับ)
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;
using System.IO;

namespace KinectMirrorSave
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        KinectSensor myKinect;

        public MainWindow()
        {
            InitializeComponent();
        }

        //When a window loads
        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();
            }

            //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();
            }

            //Video event handler
            myKinect.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(myKinect_ColorFrameReady);
        }

        //******** Update display when video frame is ready*********
        //color data array
        byte[] colorData = null;
        WriteableBitmap imageBitmap = null;

        private void myKinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            using (ColorImageFrame colorFrame = e.OpenColorImageFrame())
            {
                //if get new video frame
                if (colorFrame == null)
                    return;
                //create an array of pixel data
                if (colorData == null)
                    colorData = new byte[colorFrame.PixelDataLength];
                //extract pixel data from the frame to the array
                colorFrame.CopyPixelDataTo(colorData);

                //call unsafe method to modify the pixel data
                if (cbReflect.IsChecked == true)
                {
                    //reflect a half left image to a half right
                    reflectImage(colorData, colorFrame.Width, colorFrame.Height);
                }

                //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;
                }

                //For the first video frame, no bitmap, create a writable bitmap of video size
                if (imageBitmap == null)
                {
                    imageBitmap = new WriteableBitmap(
                        colorFrame.Width,
                        colorFrame.Height,
                        96,     //dpiX
                        96,     //dpiY
                        PixelFormats.Bgr32,
                        null    //palette:none
                        );
                }
                //For other video frames, write video data to bitmap
                imageBitmap.WritePixels(
                    new Int32Rect(0, 0, colorFrame.Width, colorFrame.Height),
                    colorData,  //video data
                    colorFrame.Width * colorFrame.BytesPerPixel,      //stride
                    0   //offset to the array
                    );
                //set bitmap to image view
                kinectVideo.Source = imageBitmap;
            }
        }

        //************ unsafe method to modify pixel data **************
        //Reflect the half left of the image to the half right
        private unsafe void reflectImage(byte[] colorData, int width, int height)
        {
            //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 < height; row++)
                {
                    //set a start pointer to the beginning of each row
                    int* pStart = (int*)pImage + (row * width);
                    //set an end pointer to the end of each row
                    int* pEnd = pStart + width - 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 method

        //*********** Click a save button *********************
        bool takePicture;
        BitmapSource capBitmap = null;      //bitmap for 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);
                }
            }
        }
    }
}

Tuesday, April 14, 2015

การเพิ่มประสิทธิภาพในการประมวลผลภาพจาก Kinect ด้วย Unsafe build และ pointer

ตัวอย่างที่ผ่านมา เราทำการอ่านค่าภาพวิดีโอจาก Kinect มาเก็บไว้ในอาร์เรย์หนึ่งมิติ หลังจากนั้นเราจึงทำการเปลี่ยนค่าในอาร์เรย์แล้วนำไปสร้างเป็นบิตแมพเพื่อแสดงผล

การเข้าถึงข้อมูลภาพวิดีโอด้วยดัชนีของอาร์เรย์ทำได้ค่อนข้างง่ายและสะดวก แต่เมื่อมีภาพจำนวนมากและต้องประมวลผลทุกภาพ ก็จะต้องใช้พลังสูง โดยเฉพาะเมื่อใช้อาร์เรย์ เพราะ .NET framework จำเป็นต้องตรวจสอบว่าดัชนีของอาร์เรย์อยู่ในช่วงที่เป็นไปได้หรือไม่ตลอดเป็นต้น

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

ในการกำหนดให้การ build เป็นแบบ unsafe ทำได้ดังรูป


จากนั้นทำการบันทึกไฟล์นี้

ต่อไปเราก็จะมาปรับโค้ดใหม่ โดยแยกส่วนที่เป็นการแก้ไขอาร์เรย์ของภาพออกมาเป็นฟังก์ชันใหม่ ดังนี้

        unsafe void updateImage(byte[] colorData)
        {
        }
โดยการใช้คำสั่ง unsafe คือการระบุว่าโค้ดที่อยู่ใน block นี้จะมีส่วนที่ unsafe นั่นคือเปิดให้ใช้ pointer ในการแก้ไขหน่วยความจำได้โดยตรง และไม่ต้องตรวจสอบเงื่อนไขบางประการ

จากนั้น จะต้องใช้คำสั่ง fixed เพื่อกำหนดให้อาร์เรย์ถูกล็อคไว้ในหน่วยความจำที่ตำแหน่งคงที่ ถ้าไม่ใช้คำสั่งนี้ เมื่อรันโปรแกรมอาร์เรย์อาจจะถูกสร้างไว้ที่ตำแหน่งของหน่วยความจำที่เปลี่ยนไปเรื่อยๆ ทำให้การเข้าถึงหน่วยความจำด้วย pointer เกิดความผิดพลาด

        unsafe void updateImage(byte[] colorData)
        {
            //fixed pixel data array in memory, otherwise it can be moved and be inaccessible by a pointer
            fixed (byte* pImage = colorData)
            {
            }
        }
โค้ัดที่เหลือของฟังก์ชันนี้ ก็จะมีประมาณนี้ครับ สังเกตการใช้พอยน์เตอร์ และการใช้สัญลักษณ์ * เพื่อเข้าถึงค่าที่พอยน์เตอร์ชี้อยู่
        unsafe void updateImage(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
                //set a start pointer to the beginning of image data
                byte* pStart = pImage;
                //set an end pointer to the end of image data
                byte* pEnd = pImage + colorData.Length;

                int newValue=0;
                //loop through each byte of image data
                while (pStart != pEnd)
                {
                    //Each byte is B G R A
                    //blue
                    newValue = *pStart + blueOffset;
                    //clamping
                    if (newValue < 0)
                        newValue = 0;
                    else if (newValue > 255)
                        newValue = 255;
                    *pStart = (byte)newValue;

                    //green
                    //move to next byte
                    pStart++;
                    newValue = *pStart + greenOffset;
                    //clamping
                    if (newValue < 0)
                        newValue = 0;
                    else if (newValue > 255)
                        newValue = 255;
                    *pStart = (byte)newValue;

                    //red
                    //move to next byte
                    pStart++;
                    newValue = *pStart + redOffset;
                    //clamping
                    if (newValue < 0)
                        newValue = 0;
                    else if (newValue > 255)
                        newValue = 255;
                    *pStart = (byte)newValue;

                    //move to next 2 bytes ->alpha then blue again
                    pStart += 2;
                }
            }
        }
ส่วนโค้ดเต็มทั้งหมดของโปรแกรมนี้ เพื่อให้ได้ผลลัพธ์เหมือนตอนที่แล้วมีดังนี้ครับ
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 KinectCam
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        KinectSensor myKinect;
        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();
            }

            //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();
            }

            //Video event handler
            myKinect.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(myKinect_ColorFrameReady);           
        }

        //------------------------- Update color value from slider ----------------------------------
        //color value
        private int blueOffset = 0;
        private int greenOffset = 0;
        private int redOffset = 0;

        private void BlueSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            //get slider value
            blueOffset = (int)BlueSlider.Value;
        }

        private void GreenSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            //get slider value
            greenOffset = (int)GreenSlider.Value;
        }

        private void RedSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            //get slider value
            redOffset = (int)RedSlider.Value;
        }
        //--------------------------------------------------------------------------------------------

        //************************ Update display when video frame is ready********************* 
        //color data array
        byte[] colorData = null;
        WriteableBitmap imageBitmap = null;
        void myKinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            using (ColorImageFrame colorFrame = e.OpenColorImageFrame())
            {
                //if get new video frame
                if (colorFrame == null)
                    return;
                //create an array of pixel data
                if(colorData==null)
                    colorData = new byte[colorFrame.PixelDataLength];
                //extract pixel data from the frame to the array
                colorFrame.CopyPixelDataTo(colorData);

                //call unsafe method to modify the pixel data
                updateImage(colorData);

                //For the first video frame, no bitmap, create a writable bitmap of video size
                if (imageBitmap == null)
                {
                    imageBitmap = new WriteableBitmap(
                        colorFrame.Width,
                        colorFrame.Height,
                        96,     //dpiX
                        96,     //dpiY
                        PixelFormats.Bgr32,
                        null    //palette:none
                        );
                }
                //For other video frames, write video data to bitmap
                imageBitmap.WritePixels(
                    new Int32Rect(0, 0, colorFrame.Width, colorFrame.Height),
                    colorData,  //video data
                    colorFrame.Width * colorFrame.BytesPerPixel,      //stride
                    0   //offset to the array
                    );
                //set bitmap to image view
                kinectVideo.Source = imageBitmap;
            }
        }
        //**********************************************************************************

        //unsafe method to modify pixel data
        //------------------------------------------------------------------------------
        unsafe void updateImage(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
                //set a start pointer to the beginning of image data
                byte* pStart = pImage;
                //set an end pointer to the end of image data
                byte* pEnd = pImage + colorData.Length;

                int newValue=0;
                //loop through each byte of image data
                while (pStart != pEnd)
                {
                    //Each byte is B G R A
                    //blue
                    newValue = *pStart + blueOffset;
                    //clamping
                    if (newValue < 0)
                        newValue = 0;
                    else if (newValue > 255)
                        newValue = 255;
                    *pStart = (byte)newValue;

                    //green
                    //move to next byte
                    pStart++;
                    newValue = *pStart + greenOffset;
                    //clamping
                    if (newValue < 0)
                        newValue = 0;
                    else if (newValue > 255)
                        newValue = 255;
                    *pStart = (byte)newValue;

                    //red
                    //move to next byte
                    pStart++;
                    newValue = *pStart + redOffset;
                    //clamping
                    if (newValue < 0)
                        newValue = 0;
                    else if (newValue > 255)
                        newValue = 255;
                    *pStart = (byte)newValue;

                    //move to next 2 bytes ->alpha then blue again
                    pStart += 2;
                }
            }
        }
        //------------------------------------------------------------------------------
    }
}

Monday, April 13, 2015

การประมวลผลภาพจาก Kinect ตัวอย่างการปรับค่าสีด้วย slider

ลองมาสร้าง slider เพื่อใช้ปรับค่าสีกันครับ

ตัวอย่างก่อนปรับ

ตัวอย่างหลังปรับเพิ่มค่าสีน้ำเงิน

ก่อนอื่นก็ไปสร้างหน้าอินเตอร์เฟสก่อนครับ โดยไปปรับแก้ไฟล์ XAML ให้ได้ประมาณนี้
<Window x:Class="KinectCam.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
    <StackPanel Orientation="Horizontal">
        <Image Name="kinectVideo" Width="440"/>
        <Slider Name="BlueSlider" Background="Blue" Maximum="255" Minimum="-255" Orientation="Vertical" ValueChanged="BlueSlider_ValueChanged"/>
        <Slider Name="GreenSlider" Background="Green" Maximum="255" Minimum="-255" Orientation="Vertical" ValueChanged="GreenSlider_ValueChanged"/>
        <Slider Name="RedSlider" Background="Red" Maximum="255" Minimum="-255" Orientation="Vertical" ValueChanged="RedSlider_ValueChanged"/>
    </StackPanel>
</Window>

สังเกตว่าเรากำหนดให้ช่วงของ slider คือ -255 ถึง 255

หลักการค่อนข้างตรงไปตรงมาครับ เมื่อขยับ slider ก็ให้หาว่าค่าของ slider เป็นเท่าไหร่ สมมติเรียกว่าค่า offset

จากนั้นค่าของสีใหม่ = สีเดิม + offset
ถ้าค่านี้เกิน 255 ก็ให้เป็น 255 หรือถ้าต่ำกว่า 0 ก็ให้เป็น 0 ซึ่งเราเรียกวิธีการนี้ว่า Clamping หรือ Clipping ครับ

โค้ดทั้งหมดก็เพิ่มมานิดหน่อยจากของเดิมดังนี้ครับ
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 KinectCam
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        KinectSensor myKinect;
        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();
            }

            //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();
            }

            //Video event handler
            myKinect.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(myKinect_ColorFrameReady);           
        }

        //color value
        private int blueOffset = 0;
        private int greenOffset = 0;
        private int redOffset = 0;

        //color data array
        byte[] colorData = null;
        WriteableBitmap imageBitmap = null;
        void myKinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            using (ColorImageFrame colorFrame = e.OpenColorImageFrame())
            {
                //if get new video frame
                if (colorFrame == null)
                    return;
                //create an array of pixel data
                if(colorData==null)
                    colorData = new byte[colorFrame.PixelDataLength];
                //extract pixel data from the frame to the array
                colorFrame.CopyPixelDataTo(colorData);

                //------------------------------------------------------
                //modify the pixel data
                //0=Blue,1=Green,2=Red,3=Alpha
                int newValue = 0;
                for (int i = 0; i < colorData.Length; i = i + 4)
                {
                    //blue
                    newValue = colorData[i] + blueOffset;
                    //clamping
                    if (newValue > 255)
                        newValue = 255;
                    if (newValue < 0)
                        newValue = 0;
                    colorData[i] = (byte)(newValue);

                    //green
                    newValue = colorData[i + 1] + greenOffset;
                    if (newValue > 255)
                        newValue = 255;
                    if (newValue < 0)
                        newValue = 0;
                    colorData[i + 1] = (byte)(newValue);

                    //red
                    newValue = colorData[i + 2] + redOffset;
                    if (newValue > 255)
                        newValue = 255;
                    if (newValue < 0)
                        newValue = 0;
                    colorData[i + 2] = (byte)(newValue);
                }
                //------------------------------------------------------

                //For the first video frame, no bitmap, create a writable bitmap of video size
                if (imageBitmap == null)
                {
                    imageBitmap = new WriteableBitmap(
                        colorFrame.Width,
                        colorFrame.Height,
                        96,     //dpiX
                        96,     //dpiY
                        PixelFormats.Bgr32,
                        null    //palette:none
                        );
                }
                //For other video frames, write video data to bitmap
                imageBitmap.WritePixels(
                    new Int32Rect(0, 0, colorFrame.Width, colorFrame.Height),
                    colorData,  //video data
                    colorFrame.Width * colorFrame.BytesPerPixel,      //stride
                    0   //offset to the array
                    );
                //set bitmap to image view
                kinectVideo.Source = imageBitmap;
            }
        }

        private void BlueSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            //get slider value
            blueOffset = (int)BlueSlider.Value;
        }

        private void GreenSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            //get slider value
            greenOffset = (int)GreenSlider.Value;
        }

        private void RedSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            //get slider value
            redOffset = (int)RedSlider.Value;
        }
    }
}

การประมวลผลภาพวิดีโอจาก Kinect ตัวอย่างการสร้างภาพกลับสี (Negative)

เมื่อเราอ่านค่าวิดีโอจาก Kinect มาเก็บในตัวแปรชุด หรืออาร์เรย์ (Array) เพื่อจะไปสร้างเป็นบิตแมพ  ขณะที่เก็บข้อมูลในอาร์เรย์ เราสามารถปรับเปลี่ยนหรือแก้ไขข้อมูลพิกเซลของภาพได้

โครงสร้างการเก็บข้อมูลพิกเซล ที่เราได้มาจาก Kinect ด้วยคำสั่ง colorFrame.CopyPixelDataTo(colorData); นั้น จะเป็นข้อมูลแบบไบต์ (byte) ที่เก็บในอาร์เรย์หนึ่งมิติ โดยหนึ่งพิกเซลจะประกอบไปด้วยอาร์เรย์สี่ช่อง แบบ BGRA นั่นคือ
0-Blue
1-Green
2-Red
3-Alpha
โดยเลข 0,1,2,... เป็น index ของอาร์เรย์ และวนไปเรื่อยๆ คือ 4-Blue, 5-Green, 6-Red, 7-Alpha...


แต่ละช่องสีก็จะถูกเก็บด้วยตัวแปรชนิดไบต์ นั่นคือ 1 ช่องสี = 1 byte = 8 bits และเก็บค่าช่วงสีได้ 2^8 = 256 ค่า (0-255) ดังนั้น หนึ่งพิกเซลจะใช้อาร์เรย์ 4 ช่อง = 4 bytes = 32 bits

ในที่นี้ เราจะลองมาเปลี่ยนค่าสีของพิกเซล โดยสร้างภาพกลับสี (Negative) กันครับ หลักการคือถ้าอ่านค่าสีพิกเซลได้เท่าใด ให้นำไปหักออกจาก 255 ก็จะได้ค่าสีที่อยู่ตรงข้ามครับ

สมมติว่ารูปปกติคือ

ตัวอย่างผลลัพธ์ที่ได้ก็จะเป็น

การแก้ไขก็จะแก้เฉพาะส่วนของ byte array ก่อนที่จะสร้างบิตแมพ ซึ่งจะแก้ในเฉพาะส่วนของฟังก์ชันที่เป็นผลของ ColorFrameReady ดังนี้ครับ

สังเกตว่าส่วนที่แก้ไขมีนิดเดียว อยู่ระหว่าเส้นประที่ขีดไว้ครับ
        void myKinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            using (ColorImageFrame colorFrame = e.OpenColorImageFrame())
            {
                //if get new video frame
                if (colorFrame == null)
                    return;
                //create an array of pixel data
                if(colorData==null)
                    colorData = new byte[colorFrame.PixelDataLength];
                //extract pixel data from the frame to the array
                colorFrame.CopyPixelDataTo(colorData);

                //------------------------------------------------------
                //modify the pixel data
                //0=Blue,1=Green,2=Red,3=Alpha
                for (int i = 0; i < colorData.Length; i = i + 4)
                {
                    //image negative
                    //blue
                    colorData[i] = (byte)(255 - colorData[i]);
                    //green
                    colorData[i + 1] = (byte)(255 - colorData[i + 1]);
                    //red
                    colorData[i + 2] = (byte)(255 - colorData[i + 2]);
                }
                //------------------------------------------------------

                //For the first video frame, no bitmap, create a writable bitmap of video size
                if (imageBitmap == null)
                {
                    imageBitmap = new WriteableBitmap(
                        colorFrame.Width,
                        colorFrame.Height,
                        96,     //dpiX
                        96,     //dpiY
                        PixelFormats.Bgr32,
                        null    //palette:none
                        );
                }
                //For other video frames, write video data to bitmap
                imageBitmap.WritePixels(
                    new Int32Rect(0, 0, colorFrame.Width, colorFrame.Height),
                    colorData,  //video data
                    colorFrame.Width * colorFrame.BytesPerPixel,      //stride
                    0   //offset to the array
                    );
                //set bitmap to image view
                kinectVideo.Source = imageBitmap;
            }
        }

การปรับการแสดงผลวิดีโอจาก Kinect ให้ใช้ Memory ได้มีประสิทธิภาพขึ้น

ก่อนหน้านี้ เราได้เขียนโค้ดเพื่อรองรับความผิดพลาดที่อาจเกิดขึ้นจากการเชื่อมต่อกับ Kinect แล้วสามารถแสดงผลภาพวิดีโอได้

หากสังเกตโค้ดเดิม จะเห็นว่าทุกครั้งที่มีการอ่าน frame ของวิดีโอจาก Kinect จะมีการสร้างบิตแมพด้วยคำสั่ง BitmapSource.Create() เพื่อนำไปแสดงผล

สมมติว่าในหนึ่งวินาทีเราทำการอ่านภาพจากวิดีโอ 30 ภาพ ก็จะมีการสร้างบิตแมพถึง 30 ครั้ง ซึ่งก็จะใช้หน่วยความจำเพิ่มขึ้นเรื่อยๆ จนกระทั่ง .NET framework เห็นว่าใช้มากไป ก็จะเรียก garbage collector มาทำลายหน่วยความจำของบิตแมพที่เราไม่ได้ใช้แล้ว หน่วยความจำที่ใช้ก็จะลดลง แต่ต่อมาก็จะใช้เพิ่มขึ้นอีกตามบิตแมพที่สร้าง วนไปแบบนี้เรื่อยๆ

วิธีที่สามารถทำให้การใช้หน่วยความจำมีประสิทธิภาพมากขึ้น โดยไม่ขึ้นๆลงๆตามที่ได้อธิบายไป คือ การเปลี่ยนจากการใช้ BitmapSource มาเป็น WritableBitmap ซึ่งสามารถเขียนทับได้เรื่อยๆโดยไม่ต้องสร้างบิตแมพใหม่ ก็จะทำให้การใช้หน่วยความจำลดลงและคงที่มากกว่า

โค้ดใหม่ที่ปรับปรุงก็จะเป็นตามนี้ครับ สังเกตว่ามีการเปลี่ยนเฉพาะส่วนของเหตุการณ์ ColorImageFrameReadyEvent
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 KinectCam
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        KinectSensor myKinect;
        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();
            }

            //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();
            }

            //Video event handler
            myKinect.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(myKinect_ColorFrameReady);           
        }

        byte[] colorData = null;
        WriteableBitmap imageBitmap = null;
        void myKinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            using (ColorImageFrame colorFrame = e.OpenColorImageFrame())
            {
                //if get new video frame
                if (colorFrame == null)
                    return;
                //create an array of pixel data
                if(colorData==null)
                    colorData = new byte[colorFrame.PixelDataLength];
                //extract pixel data from the frame to the array
                colorFrame.CopyPixelDataTo(colorData);

                //For the first video frame, no bitmap, create a writable bitmap of video size
                if (imageBitmap == null)
                {
                    imageBitmap = new WriteableBitmap(
                        colorFrame.Width,
                        colorFrame.Height,
                        96,     //dpiX
                        96,     //dpiY
                        PixelFormats.Bgr32,
                        null    //palette:none
                        );
                }
                //For other video frames, write video data to bitmap
                imageBitmap.WritePixels(
                    new Int32Rect(0, 0, colorFrame.Width, colorFrame.Height),
                    colorData,  //video data
                    colorFrame.Width * colorFrame.BytesPerPixel,      //stride
                    0   //offset to the array
                    );
                //set bitmap to image view
                kinectVideo.Source = imageBitmap;
            }
        }
    }
}

การตรวจสอบความผิดพลาดจากการเชื่อมต่อกับ Kinect

ตอนที่แล้วเราได้ลองเขียนโปรแกรมเพื่อเชื่อมต่อกับ Kinect แล้วดึงภาพจากกล้องมาแสดงผล

จะเกิดอะไรขึ้นถ้า Kinect ไม่ได้ต่ออยู่ หรือแม้ว่าต่ออยู่ แต่ไม่สามารถเชื่อมต่อได้

แน่นอนว่าโปรแกรมก็จะผิดพลาดแล้วจบไปแบบไม่สวย ทำอย่างไรให้โปรแกรมแจ้งเตือนแล้วค่อยปิดไปแบบนี้

ลองมาเขียนโค้ดตรวจสอบข้อผิดพลาดในการเชื่อมต่อเหล่านี้กันดีกว่าครับ โดยฟังก์ชันที่ต้องไปแก้ไข ก็คือ Window_Loaded() ที่จะถูกเรียกเมื่อหน้าต่างของโปรแกรมถูกสร้างขึ้น

ความผิดพลาดจากการเชื่อมต่อ ตามตำราบอกไว้ว่าอาจเกิดได้สองกรณี คือ
1. ไม่ได้ต่อ Kinect หรือการเชื่อมต่อไม่สมบูรณ์ (เช่นยังไม่ได้ติดตั้ง driver)
อันนี้เราสามารถเช็คได้โดยใช้คำสั่ง KinectSensor.KinectSensors.Count ซึ่งจะบอกว่าตอนนี้มี Kinect ต่อกับคอมของเรากี่อัน (สูงสุดได้ 4 อัน) ถ้าได้ค่า 0 แสดงว่าไม่มี Kinect ต่ออยู่เลย

2. เชื่อมต่อ Kinect ได้ แต่ไม่สามารถเรียกใช้งานได้ ซึ่งอาจเกิดจาก มีแอพอื่นใช้กล้องอยู่ หรือ memory ไม่พอ เป็นต้น
สามารถตรวจสอบได้โดยใช้ try...catch ครอบคำสั่งที่ลองติดต่อ Kinect ได้แก่ KinectSensor.KinectSensors[0] และ myKinect.Start() เป็นต้น

เรามาลองตรวจจับเหตุการณ์ข้างต้น โดยการแก้ไขเฉพาะฟังก์ชัน Window_Loaded() ดังนี้ครับ

        private void Window_Loaded_1(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();
            }

            //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();
            }

            //Video event handler
            myKinect.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(myKinect_ColorFrameReady);           
        }

Sunday, April 12, 2015

การอ่านและแสดงผลภาพจาก Kinect

ลองมาเขียนโค้ดเชื่อมต่อกับ Kinect เพื่อแสดงผลวิดีโอจากกล้อง ประมาณรูปด้านล่างกันครับ

อันดับแรก เปิด Visual Studio ก่อน เปิดกล้องและเสียบเชื่อมต่อกับพอร์ต USB ให้เรียบร้อย

สร้างโปรเจคใหม่ของ C# สมมติว่าใช้ชื่อว่า KinectCam

ลองสังเกตดูแถบ Solution Explorer ซึ่งอาจจะอยู่ทางด้านขวามือ คลิกขวาที่ References แล้วเลือก Add Reference


เลือกหัวข้อ Browse แล้วเลือกปุ่ม Browse... ด้านล่างเพื่อหาไฟล์ Microsoft.Kinect.dll ซึ่งจะอยู่ที่ C:\Program Files\Microsoft SDKs\Kinect\v1.8\Assemblies ถ้าติดตั้งตามปกติ จะได้ผลลัพธ์ดังภาพ

ที่ Solution explorer ดับเบิลคลิกที่ MainWindow.xaml.cs เพื่อจะเขียนโค้ด

ที่ Solution explorer ดับเบิลคลิกที่ MainWindow.xaml เพื่อจะแก้ไข หน้านี้จะเป็น Interface หลักของโปรแกรม

ให้เพิ่มแท็กชื่อ Image เพื่อจะแสดงวิดีโอจาก Kinect ลงบนหน้าต่างดังนี้

"< Image Name="kinectVideo" Width="640" Height="480" />"

ต่อไป ให้ทำการคลิกบริเวณโค้ดที่เขียนว่า "< Window x:Class=... " จากนั้นก็เลื่อนไปดูทางขวามือ ในส่วนของแถบ Properties ให้คลิกสัญลักษณ์ที่เป็นเหมือนสายฟ้า แล้วเลื่อนหา Event ชื่อ Loaded จากนั้นก็ดับเบิลคลิกในช่องว่างที่อยู่ถัดจาก Event Loaded นี้ มันก็จะเกิด Method ใหม่ขึ้นมาชื่อ Window_Loaded_1 โดยอัตโนมัติในไฟล์โค้ด MainWindow.xaml.cs ในที่นี้ ผมเปลี่ยนให้เป็นชื่อ Window_Loaded ทั้งในส่วนของการออกแบบ และส่วนโค้ด




จากนั้น เราก็จะทำการเพิ่มโค้ดต่างๆ ดังนี้

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 KinectCam
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        KinectSensor myKinect;
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            //Get the first Kinect connected to this computer
            myKinect = KinectSensor.KinectSensors[0];
            //Enable the color video stream
            myKinect.ColorStream.Enable();
            //Video event handler
            myKinect.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(myKinect_ColorFrameReady);
            //Start the sensor
            myKinect.Start();
        }

        void myKinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            using (ColorImageFrame colorFrame = e.OpenColorImageFrame())
            {
                //if get new video frame
                if (colorFrame == null)
                    return;
                //create an array of pixel data
                byte[] colorData = new byte[colorFrame.PixelDataLength];
                //extract pixel data from the frame to the array
                colorFrame.CopyPixelDataTo(colorData);
                //create a bitmap from the video array and set it to Image UI
                kinectVideo.Source = BitmapSource.Create(
                    colorFrame.Width, colorFrame.Height,    //image dimension
                    96, 96,                 //resolution 96 dpi for video frames
                    PixelFormats.Bgr32,     //image format
                    null,                   //palette - none
                    colorData,              //video data
                    colorFrame.Width * colorFrame.BytesPerPixel     //stride
                    );
            }
        }
    }
}
แล้วลองรันโปรแกรมดูครับ น่าจะเห็นหน้าต่างแสดงผลภาพวิดีโอจาก Kinect แล้วล่ะครับ