Kinect ile Kamera ve Derinlik İşlemleri

   Kinect makale serisinde daha önce sizlere Kinect donanımı ve yapılabilecekleri hakkında bilgiler paylaşmış, ardında da birlikte geliştirme ortamımızı Kinect için hazırlamıştık. Bu makalemde, elimizi artık iyice koda bulaştırarak Kinect kamera kullanımının detaylarını inceliyor olacağım.

   Makalemizde ele alacağımız örneğimiz bir WPF uygulaması olacak, önceki makalemizde başladığımız MerhabaKinect uygulamamız. Geliştirme ortamımızı hazırlarken oluşturduğumuz MerhabaKinect uygulamamızı hatırlayacak olursak ana ekran kodumuz aşağıdaki şekildeydi;

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Research.Kinect.Nui;

namespace Com.Enterprisecoding.MerhabaKinect {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window {
        private Runtime kinect;

        public MainWindow() {
            InitializeComponent();

            kinect = new Runtime();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e) {
}

private void Window_Closed(object sender, System.EventArgs e) {
            kinect.Uninitialize();
        }
    }
}
<Window x:Class="Com.Enterprisecoding.MerhabaKinect.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Merhaba Kinect" Height="580" Width="710" Loaded="Window_Loaded" Closed="Window_Closed">
    <Grid>
        <Image Height="240" HorizontalAlignment="Left" Margin="12,20,0,0" Name="normalVideo" Stretch="Fill" VerticalAlignment="Top" Width="320" />
        <Image Height="240" HorizontalAlignment="Left" Margin="355,20,0,0" Name="cmyk32Video" Stretch="Fill" VerticalAlignment="Top" Width="320" />

        <Image Height="240" HorizontalAlignment="Left" Margin="12,280,0,0" Name="derinlikVideo" Stretch="Fill" VerticalAlignment="Top" Width="320" />
        <Image Height="240" HorizontalAlignment="Left" Margin="355,280,0,0" Name="islenmis" Stretch="Fill" VerticalAlignment="Top" Width="320" />
    </Grid>
</Window>

   Ana ekranımızda yer alan girid içerisinde 4 adet Image bileşeni bulunmakta. Bu bileşenleri Kinect üzerinden gelen verilerin işlenmesi sonucu oluşan görüntüleri göstermekte kullanacağız. Kinect üzerinden kamera ve derinlik bilgisi ile işlem yapılacaksa, ilk yapılması gereken yukarıdaki kodda Window_Load fonsiyonu içerisinde Kinect çalışma zamanına bu iki bilgiyi kullanarak işlem yapacağımızı bildirmek olmalı;

kinect.Initialize(RuntimeOptions.UseDepth | RuntimeOptions.UseColor);

   Ardından ise sırasıyla VideoFrameReady, DepthFrameReady olay bildirimlerini dinlemeli ve video, derinlik akışlarını başlatmalıyız;

kinect.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color); 
kinect.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.Depth);

   Ana ekranımızda yer alan 4 image bileşeninden ilki doğrudan kameradan gelen veri akışının görüntülenmesinde kullanılırken, ikincisi CMYK32 formatında gelen akışı görüntüleyecektir. Kinect SDK’sı ile belki de yapılabilecek işlerden en kolayı gelen görüntü akışını görüntülemektir. Bu işlem için her bir video karesi hazır olduğunda gelen görüntü verisini imaja çevirmemiz yeterli olacaktır;

private void kinect_VideoFrameReady(object sender, ImageFrameReadyEventArgs e) {
    PlanarImage Image = e.ImageFrame.Image;

    normalVideo.Source = BitmapSource.Create(Image.Width, Image.Height, 96, 96, PixelFormats.Bgr32, null, Image.Bits, Image.Width * Image.BytesPerPixel);
    cmyk32Video.Source = BitmapSource.Create(Image.Width, Image.Height, 96, 96, PixelFormats.Cmyk32, null, Image.Bits, Image.Width * Image.BytesPerPixel);
}

   Her bir video karesi hazır olduğunda Kinect çalışma-zamanı oluşan veriyi VideoFrameReady olayı üzerinden bizlere bildirecektir. Gelen veriler içerisinden görüntüye ulaşmak için ise e.ImageFrame.Image yolunu takip etmemiz gerekecektir. Daha sonrasında, görüntünün genişlik ve yükseklik bilgileri ile görüntü verisini kullanarak istediğimiz görüntü formatına çevirmemiz mümkün olacaktır. Yukarıdaki kod parçacığı görüntü verisini aynı anda BGR32 ve CMYK32 formatlarına dönüştürülerek aşağıdaki ekran görüntüsünde olduğu gibi formumuzda görüntülenecektir;

Kinect video akışının formumuz üzerinde görüntülenmesi

    Bir sonraki adımımız ise gelen derinlik bilgisi gösterebilmek olmalı. Kineck SDK üzerinden alınan derinlik bilgisi Kinect çalışma zamanının ilklenme parametrelerine bağlı olarak iki farklı formatta alınacaktır; yalnızca derinlik bilgisi (Depth) ya da derinlik ve oyuncu bilgisi (DepthAndPlayerIndex). Farklı bu iki tür verinin işlenmesi de küçük farklılıklar gösterecektir. Derinlik bilgisinin işlenmesinin yeni başlayanlar için karmaşık olabileceğini göz önüne alarak başlangıç olarak sadece derinlik bilgisini alarak ilerlememiz daha doğru olacaktır. Derinlik bilgisi bize DepthFrameReady olay bildirimi üzerinden ve byte dizisi olarak iletilecektir. İletilen bu byte dizisi bize her bir piksel için o piksel’de bulunan en yakın nesnenin milimetre cinsinden uzaklığını verecektir. Byte dizisiyle iletilen piksel bilgisi, alınan görüntünün sol üst köşesinden itibaren ve soldan-sağa, yukarıdan-aşağıya şekilde ilerleyerek her bir piksele denk gelen uzaklık bilgisini iletecektir. Akışı sadece derinlik bilgisi olarak talep etmemiz durumunda dizi içerisindeki her 16bitlik verinin low-order 12 bit’i (0-11) bize ilgili pixel için kameraya en yakın nesnenin milimetre cinsinden uzaklığını verecektir, geriye kalan 4 bit ise kullanılmayacaktır. Bu yazımda, aldığımız derinlik bilgisini işleyerek sizlere gri tonlanmış bir görüntü olarak vereceğim. Nesnelerin kameraya yakınlık-uzaklıklarına göre tonlamaları da değişkenlik gösterecektir.

   Olay bildirimi ile gelen derinlik bilgisini alarak, görüntü işlemede olduğu gibi image nesnesi ve genişlik/yükseklik bilgilerini aldıktan sonra oluşturacağımız görüntüyü saklamakta kullanacağımız bir byte dizini oluşturmalıyız. Görüntümüz ARGB formatında olacağından her bir pixel için 4 byte kullanmalıyız (Alfa, kırmızı, yeşil ve mavi kanalları için birer byte), dolayısıyla da görüntümüzü tutacak olan byte dizisi genişlik * yükselik * 4 boyutunda olmalıdır;

var bitmap = new byte[yukseklik * genislik * 4];

   Oluşturacağımız görüntüyü tutacağımız byte dizimizi hazırladıktan sonra, artık her bir piksel için kaynak dizimizi dönebiliriz.

   Bu aşamada dikkat edilmesi gereken bir nokta, bir önceki paragrafta da belirttiğim gibi derinlik bilgisinin 16bitlik parçalarda bize iletildiğidir; fakat iletilen bu derinlik bilgisinin byte dizisi olarak geldiğini göz önüne alacak olursak, veriyi 2 byte’lık (2 * 8bit = 16bit) parçalar halinde incelememiz gerektiği aşikar olacaktır. Yani her defasında iletilen byte dizinin iki indeksi ile muhatap olmalıyız.

   Dikkat edilmesi gereken bir başka noktada, her defasında muhatap olacağımız bu iki indeksin ters sıralanmış olduğudur. Dolayısıyla da işleme başlarken bit düzeyinde ufak bir kaydırma ve ardından da birleştirme yapıyor olmamız gerekecektir.

private void kinect_DepthFrameReady(object sender, ImageFrameReadyEventArgs e) {
    var image = e.ImageFrame.Image;
    var yukseklik = image.Height;
    var genislik = image.Width;
    var derinlikVerisi = image.Bits;

    var bitmap = new byte[yukseklik * genislik * 4];

    for (int y = 0, veriIndeksi = 0; y < yukseklik; y++) {
        var yukseklikOfseti = y * genislik;

        for (var x = 0; x < genislik; x++, veriIndeksi += 2) {
var gercekUzaklik = derinlikVerisi[veriIndeksi] | (derinlikVerisi[veriIndeksi + 1] << 8);

//....
}
}
}

   Gördüğünüz gibi aşağıdaki kod parçacığı ile gerçek uzunluk bilgisine erişebilmekteyiz;

var gercekUzaklik = derinlikVerisi[veriIndeksi] | (derinlikVerisi[veriIndeksi + 1] << 8);

   Buradaki mantığı sanırım örnek üzerinden açıklamak daha kolay olacaktır. işlemekte olduğumuz derinlik bilgisinin derinlikVerisi içerisinde aşağıdaki şekilde olduğunu düşünelim;

derinlikVerisi[veriIndeksi] = 135

derinlikVerisi[veriIndeksi + 1]  = 15

Bu durumda muhatap olduğumuz derinlik verisi şu şekilde çözümlenmeli;

derinlikVerisi[veriIndeksi] = 135 => 10000111

derinlikVerisi[veriIndeksi + 1] = 15 => 1111

  yani 111100000000 + 10000111 = 11110000111

Bu hesaplamalar aşina, olduğumuz şekliyle çarpma, toplama ve üst işlemleriyle yapılabilirdi;

derinlikVerisi[veriIndeksi +1] *  ( 2^8 )+ derinlikVerisi[veriIndeksi]

  Fakat bilgisayarların ne şekilde işlediğini biliyorsanız bunun daha az maliyetli yolunun aynı işlemi bit düzeyinde gerçekleştirmek olduğunu tahmin edeceksinizdir. Dolayısıyla da aynı işlemi bit düzeyinde gerçekleştirmek hiç şüphesiz ki bize zaman kazandıracaktır.

   Geldiğimiz noktada artık gerçek derinlik bilgisine ulaşmış oluyoruz. Sırada, bu bilgiyi kullanarak gri tonlamada bir piksel oluşturmak kalıyor. ARGB skalasında gri tonda bir renk oluşturabilmek için alfa kanalına 0 değerini verirken, diğer kanallara (kırmızı, yeşil ve mavi) aynı değeri vermeliyiz. Tonlama için ise uzaklık bilgisi kullanılabilir. ARGB skalasında her bir kanalın 0 ile 255 değer aralığında olması gerektiğini göz önüne alırsak uzaklık bilgisi de bu aralıkta olmalıdır. Bu hesaplama için aşağıdaki kod parçacığı kullanılabilir;

var yogunluk = (byte)(255 - (255 * gercekUzaklik / 0x0fff));

  Görüldüğü gibi burada gerçek uzaklığı olabilecek maksimum uzaklık değeriyle oranladıktan sonra 0-255 arasındaki değeri bulunuyor. Genelde gerçek uzaklık verisinin 255’ten çok daha büyük olacağını (yaklaşık 850-4000 milimetre arası) göz önüne alırsak derinlik bilgisini gri tonlama için 0-255 değer aralığına düşürerek aynı zamanda derinlikteki bazı detayları da kaybettiğimizi not olarak düşmeliyim. Alternatif olarak RGB kanallarının toplamı derinlik bilgisini temsil etmekte kullanılabilir; ki bu durumda da renkli bir görüntü alınacaktır. Konunun kolay anlaşılabilmesi için yazımda gri tonlamadan devam ediyor olacağım.

   Ardından, elde ettiğimiz bu yoğunluk bilgisini bitmap dizimizde yerleştireceğimiz noktayı bularak ekliyoruz;

var pikselIndeksi = ((genislik - x - 1) + yukseklikOfseti) * 4;

bitmap[pikselIndeksi] = yogunluk; //Mavi
bitmap[pikselIndeksi + 1] = yogunluk; // Yeşil
bitmap[pikselIndeksi + 2] = yogunluk; // Kırmızı 

   Bu işlemleri bir döngü içerisinde her bir piksele ait derinlik bilgisi için gerçekleştirmemiz sonrasında artık gri tonlamada gösterebileceğimiz bir görüntüye ait byte dizimiz olacaktır. Son adımda da bu diziyi BitmapSource’a dönüştürerek Image nesnemizin kaynağına atayabiliriz;

derinlikVideo.Source = BitmapSource.Create(genislik, yukseklik, 96, 96, PixelFormats.Bgr32, null, bitmap, genislik * PixelFormats.Bgr32.BitsPerPixel / 8);

   Toparlayacak olursak, şimdiye kadar yaptığımız işlemlerde her bir piksel için ilgili derinlik verisini bularak basit bir kaç işlem ardından bu veriye karşılık bir gri tonlama hesapladık ve ardından bu gri tonlanmış pikselleri bir görüntü kaynağı haline getirdik. Fonksiyonumuz bir bütün haline getirildiğinde aşağıdaki gibi olacaktır;

private void kinect_DepthFrameReady(object sender, ImageFrameReadyEventArgs e) {
    var image = e.ImageFrame.Image;
    var yukseklik = image.Height;
    var genislik = image.Width;
    var derinlikVerisi = image.Bits;

    var bitmap = new byte[yukseklik * genislik * 4];

    for (int y = 0, veriIndeksi = 0; y < yukseklik; y++) {
        var yukseklikOfseti = y * genislik;

        for (var x = 0; x < genislik; x++, veriIndeksi += 2) {
            var gercekUzaklik = derinlikVerisi[veriIndeksi] | (derinlikVerisi[veriIndeksi + 1] << 8);

            var yogunluk = (byte)(255 - (255 * gercekUzaklik / 0x0fff));
            var pikselIndeksi = ((genislik - x - 1) + yukseklikOfseti) * 4;

            bitmap[pikselIndeksi] = yogunluk; //Mavi
            bitmap[pikselIndeksi + 1] = yogunluk; // Yeşil
            bitmap[pikselIndeksi + 2] = yogunluk; // Kırmızı
        }
    }


    derinlikVideo.Source = BitmapSource.Create(genislik, yukseklik, 96, 96, PixelFormats.Bgr32, null, bitmap, genislik * PixelFormats.Bgr32.BitsPerPixel / 8);
}

   İsterseniz gri tonlama dışında mavi ,yeşil ya da kırmızı tonlarda bir görüntü de oluşturabilmeniz mümkün. Bunu yapabilmek için kodumuzu ilgili kanala 255 değerini atayacak şekilde düzenlemek yeterli olacaktır. Örneğin kırmızı kanalına 255 sabit değerini atayarak uygulamamızı çalıştırdığımızda karşımıza aşağıdaki gibi bir görüntü gelecektir;

Kinect derinlik akışının formumuz üzerinde görüntülenmesi

   Eğer yukarıdaki kodlamamızda salt derinlik bilgisi akışını (Depth) talep etmek yerine derinlik ve oyuncu indeksi veri akışını (DepthAndPlayerIndex) talep ediyor olsaydı gerçek uzaklık aşağıdaki şekilde hesaplanıyor olacaktır;

var gercekUzaklik =  derinlikVerisi[veriIndeksi] >> 3 | derinlikVerisi[veriIndeksi + 1] << 5;

   İlk byte sağa 3 bit kaydırılarak oyuncu indeksi bilgisinin sıfırlanması sağlanmakta, ardından ikinci byte 5 bit sağa kaydırılarak ilk byte ile toplanarak gerçek uzaklık bilgisi elde edilmekte.

Fatih Boy

Ankara'da yaşayan Fatih, bir kamu kurumunda danışman olarak çalışmaktadır. ALM süreçleri, kurumsal veri yolu sistemleri, kurumsal altyapı ve yazılım geliştirme konularında destek vermektedir. Boş zamanlarında açık kaynak kodlu projeler geliştirmeyi ve bilgisini yazdığı makalelerle paylaşmayı seven Fatih, aynı zamanda Visual C# ve Visual Studio teknolojileri konusundan Microsoft tarafından altı yıl üst üste MVP (En Değerli Profesyonel) ödülüne layık görülmüştür. İş hayatı boyunca masaüstü uygulamaları, web teknolojileri, akıllı istemciler gibi konularda Asp.Net, Php, C#, Java programlama dilleri ve MySql, MsSql ve Oracle gibi veritabanı yönetim yazılımları ile çalışmıştır. İngilizce ve Türkçe olarak yayınlanan makalelerini gerek İngilizce bloğunda, gerekse de Türkçe bloğunda bulabileceğiniz gibi web sitesinden de açık kaynak kodlu geliştirdiği yazılımlarına ulaşabilirsiniz. vCard - Twitter - Facebook - Google+

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir