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 แล้วล่ะครับ