AtasoyWeb - Hüseyin Atasoy
AtasoyWeb
Hüseyin Atasoy'un Programlama Günlüğü

Bitmapleri .Net Çatısı Altında Hızlıca İşleme

Bir bitmapin piksellerini Bitmap.GetPixel() ve Bitmap.SetPixel() fonksiyonlarından yaklaşık 25 kat daha hızlı bir şekilde okuyup yazmak mümkün.

Bugünlerde, muhtemelen OpenCV'de implementasyonlarından en çok faydalanılan algoritmalardan birisi olan Viola-Jones nesne tespit algoritmasını (kademeli haar sınıflandırıcıları) VB.Net ile yazmaya çalışıyorum. Yöntemi uygulamaya çalışırken bazı noktaları yanlış anlamış olduğumu da farkettim. Bu çalışma o açıdan da faydalı oluyor benim için. Ayrıca hazırlamakta olduğum kütüphaneyi tamamlayabilirsem dökümantasyonunu da güzelce hazırlayıp bu sefer açık kaynak kodlu olarak dağıtmayı planlıyorum... [ -> HaarCascadeClassifier.dll ]

.Net'te, çok acelesi olmayan işlemlerde SetPixel() ve GetPixel() fonksiyonları işimizi iyi görüyor. Ama gerçek zamanlı işlemler için bu fonksiyonlar birer kaplumbağa. Kütüphanede bu fonksiyonları kullanırsam, onu gerçek zamanlı uygulamalarda kullanmak mümkün olmayacak.

Fazla miktarda verinin hızlıca işlenmesi gerektiği durumlarda belleğe yakınlığınız oldukça önemli. SetPixel() ve GetPixel() fonksiyonları programcıyı bellek üzerindeki hesaplardan kurtardığı için hızın pek önemli olmadığı durumlarda kullanışlı olsa da hız söz konusu olduğunda başka bir yol aramaya başlıyoruz. Bellek ile aramızdaki aracıları ortadan kaldırarak görüntülerin bellekteki verilerine direkt olarak erişebilmemizi sağlayacak bir yola ihtiyacımız var.

Çözüm .Net'in Bitmap sınıfında saklı. LockBits() fonksiyonu ile bitmapin verilerinin tutulduğu bellek alanını geçici bir alana kopyalayıp, oraya direkt erişim imkanı elde edebiliyoruz. Bu yol dikenli olsa da, SetPixel() ve GetPixel() fonksiyonlarına göre çok daha hızlı sonuç alabilmemizi sağlıyor ...

"Goruntu" isminde bir bitmapimiz olsun. İlk işimiz bitmapin, üzerinde çalışacağımız kısmını geçici bir bellek alanına kopyalamak ve bu alanın çöp toplayıcısı tarafından sahipsiz sanılıp temzilenmesini önlemek için onu kilitlemek. Bu işlerin tümünü LockBits() fonksiyonu hallediyor:

Dim BmpVerileri As BitmapData = Goruntu.LockBits(New Rectangle(0, 0, Width, Height), _
   ImageLockMode.ReadWrite, Goruntu.PixelFormat)

Bitmapin, üzerinde işlem yapacağımız bölgesini yeni Rectangle nesnesine girdiğimiz değerlerle istediğimiz gibi ayarlayabiliyoruz. Geçici bellek alanını sadece okuyacaksak ImageLockMode.ReadOnly, bu alana sadece veri yazacaksak ImageLockMode.WriteOnly ifadelerini kullanabiliyoruz. Son parametrede de bitmapimizin piksel formatını belirtiyoruz.

Çok sayıda piksel formatı var. Yazının devamında bitmaplerimizin "Format24bppRgb" formatını kullandığını varsayacağım. (Amacım olayın mantığını vermek, diğer formatlar da benzer şekilde ele alınabilir.) "Format24bppRgb", bitmapte piksel başına 24 bit kullanıldığını (bbp, bit per pixel) ve bu 24 bit ile renklerin r g b bileşenlerinin tutulduğunu ifade eder. Her bir renk bileşeni 1 bayt ile temsil edildiği için, renkleri elde etmek diğer formatların bir kısmına göre biraz daha kolay olacak.

Verileri bellekte geçici bir alana kopyalayıp kilitledik. Kopyalanan veriler hafızada scan line (tarama satırı) veya stride (adım) denen satırlar halinde tutulur. Satır sayısı, bitmapin yüksekliği kadardır. 24 bitlik bir bitmapin tarama satırlarının genişliğini de hesaplayalım; her piksel 3 bayt ile gösterildiğine göre tarama satırının genişliği 3*BitmapinGenişliği ifadesine eşit olmalı. Ama bu her zaman doğru değil. Burada şöyle bir sıkıntımız var:

32 bitlik bir işlemci, her seferinde 1 DWORD (4 bayt=32 bit) kadar veri işler. GDI+, işlemcinin verimli olarak kullanılabilmesi için veri bloklarını hafızada 4 baytın katları halinde tutar. (.Net, Drawing sınıfında GDI+'ı kullanır.) Dolayısıyla tarama satırlarının genişliği, her zaman 4'ün katıdır. Ancak, nasıl ki yazı yazarken bir heceyi bölüp alt satıra geçmiyorsak, GDI+ da pikselin baytlarının bir kısmını bir satıra, diğer kısmını sonraki satıra yazmaz. Bir pikselin tüm baytları mutlaka aynı tarama satırı içinde yer alır. Bu nedenlerden dolayı 3*Genişlik sayısının 4'ün katı olmaması halinde, tarama satırlarına boş baytlar yerleştirilerek satır genişlikleri 4'ün katına tamamlanır.

BitmapData sınıfının "Scan0" üyesi kilitlediğimiz verinin ilk baytının işaretçisini, "Stride" üyesi tarama satırının genişliğini verir. Bir işaretçinin işaret ettiği bellek alanından veri okumak için .NET'in Marshal sınıfının üyelerini kullanabiliyoruz:

Dim BaytSayisi As Integer = Math.Abs(BmpVerileri.Stride) * BmpVerileri.Height
Dim Isaretci As IntPtr = BmpVerileri.Scan0
Dim Renkler(BaytSayisi - 1) As Byte
Marshal.Copy(Isaretci , Renkler, 0, BaytSayisi)

Bu arada bahsetmeyi unttum; stride, negatif çıkabilir. Bu durum satırların ters dizildiğini, yani görüntünün ters saklandığını gösterir. Stride'ın mutlak değerini bu yüzden aldık.

Bitmapin verilerini yönetilmeyen bellek alanından, üzerinde işlem yapabileceğimiz dizimize aktardık. Tarama satırlarının genişliğinin 4'ün katı olabilmesi için gerektiğinde boş baytlarla genişletildiğini de öğrendik. O halde görüntüyü işlemek için verileri gezmeye başlamadan önce, son olarak her bir tarama satırına kaç boş bayt yerleştirildiğini hesaplamamız gerekiyor:

BosBaytSayisi = BmpVerileri.Stride - BmpVerileri.Width * 3

Genişliğin 3 katı, 4'ün katı ise bu, veriler arasında ekstradan konmuş hiçbir bayta rastlamayacağımız anlamına geliyor. Aksi halde, verileri dolaşırken bitmap genişliğinin 3 katına her ulaştığımızda bulunduğumuz konumdan, hesapladığımız boş bayt sayısı kadar ileriye atlayıp oradan devam etmemiz gerekir.

Tüm bunları göz önünde bulundurarak verilerimizi işlemeye başlayabiliriz. İşlemleri tamamladığımızda, UnlockBits() fonksiyonu ile, geçici bellek alanımızın bitmapimize yazılmasını sağlayıp, alanın kilidini kaldırmayı unutmuyoruz:

Marshal.Copy(Renkler, 0, Isaretci, BaytSayisi)
Goruntu.UnlockBits(BmpVerileri)

Kafa karışıklığını gidermek ve bu zahmete değip değmeyeceğini görmek adına her iki yöntemi de kullanarak 24 bitlik bir bitmapi griye dönüştüren bir örnek yapalım ve işlem sürelerini karşılaştıralım.

GetPixel(), SetPixel():

Dim Goruntu As New Bitmap("D:\Visual Basic\Basic .Net\YuzTanima\YuzTanima\Test\lise3-640x480.jpg")

Dim BaslamaZ As DateTime = Now

For i As Integer = 0 To Goruntu.Width - 1
    For j = 0 To Goruntu.Height - 1
        Dim Renk As Color = Goruntu.GetPixel(i, j)
        Dim Gri As Integer = Renk.R * 0.299 + Renk.G * 0.587 + Renk.B * 0.114 ' Matlabteki gibi
        Goruntu.SetPixel(i, j, Color.FromArgb(Gri, Gri, Gri))
    Next
Next

MessageBox.Show("İşlem " & (Now - BaslamaZ).TotalMilliseconds & " milisaniye sürdü.")

Daha hızlı sonuç vermesini beklediğimiz yöntem:

Dim Goruntu As New Bitmap("D:\Visual Basic\Basic .Net\YuzTanima\YuzTanima\Test\lise3-640x480.jpg")

Dim BaslamaZ As DateTime = Now

Dim BmpVerileri As BitmapData = Goruntu.LockBits(New Rectangle(0, 0, Goruntu.Width, _
    Goruntu.Height), ImageLockMode.ReadWrite, Goruntu.PixelFormat)
Dim TaramaSatiriG As Integer = Math.Abs(BmpVerileri.Stride)
Dim BosBaytSayisi = TaramaSatiriG - BmpVerileri.Width * 3
Dim BosluksuzTSGenisligi As Integer = TaramaSatiriG - BosBaytSayisi
Dim Isaretci As IntPtr = BmpVerileri.Scan0
Dim BaytSayisi As Integer = TaramaSatiriG * BmpVerileri.Height
Dim Renkler(BaytSayisi - 1) As Byte
Marshal.Copy(Isaretci, Renkler, 0, BaytSayisi)

Dim i As Integer = 0
Dim PozKontrol As Integer = 0 ' Boş baytlara gelip gelmediğimizi kontrol etmek için
While i < BaytSayisi
    Dim GriDeger As Integer = 0.114 * Renkler(i)
    i = i + 1

    GriDeger = GriDeger + 0.587 * Renkler(i)
    i = i + 1

    GriDeger = GriDeger + 0.299 * Renkler(i)
    i = i + 1

    Renkler(i - 3) = GriDeger
    Renkler(i - 2) = GriDeger
    Renkler(i - 1) = GriDeger

    PozKontrol = PozKontrol + 3 ' 3 bayt (1 piksel) okuduğumuz için pozisyonumuzu 3 arttırıyoruz
    If PozKontrol = BosluksuzTSGenisligi Then ' Boş baytları atlama zamanımız gelmişse
        PozKontrol = 0
        i = i + BosBaytSayisi
    End If
End While

Marshal.Copy(Renkler, 0, Isaretci, BaytSayisi)
Goruntu.UnlockBits(BmpVerileri)

MessageBox.Show("İşlem " & (Now - BaslamaZ).TotalMilliseconds & " milisaniye sürdü.")

640x480 boyutlarındaki renkli görüntünün griye dönüşümü, ilk kod ile 1016.0581, ikinci kod ile 41.0023 milisaniyede tamamlandı. Bu da ikinci kodun yaklaşık 25 kat daha hızlı sonuç verdiğini gösteriyor...

Yazar: Hüseyin Atasoy
Posted: 31/07/2012 17:47
Keywords: pikselleri hızlıca okuma, görüntü işleme hızını arttırma

Leave Comment

 
You are replying to comment #-1. Click here if you want to cancel replying.

 

Comments (10)

Hüseyin
Reply
12/12/2012 14:08
#1

Merhaba,
Öncelikle sitenizdeki bilgiler için çok teşekkür ediyorum. Bu güzel bilgileri paylaşmanız beni çok sevindirdi. Hele ki bu konularda yapılan çalışmaların sürekli olarak matlab gibi hazır programlara yaptırılmasının nedenini bir türlü anlayamadığım bir zamanda çok keyif aldım okurken.
Görüntü işleme ile profesyönel olmasa da amatör olarak ilgilenmiyorum. Ancak kendimce birşeyler yapmayı da seviyorum. Bu güne kadar VB 6.0 kullandım ancak hız ve tabi işlem olarak çok çok yavaş kalıyor ve çok zorlar oldu. C# a geçmek içinde çok bir zaman bulamadım ancak siz sanırım son çalışmalarınızı VB.net ile yapıyorsunuz. Bu konuda C# ın bir üstünlüğü varmı acaba ? veya OpenCV gibi kütüphaneleri Vb.net ile kontrol etmek C# ta olduğu gibi kolay mı ?

Hüseyin Atasoy
Reply
06/03/2013 15:20
#2

C# ile yazabileceğiniz herşeyi vb.net ile yazabilirsiniz. Bunun tersi de doğru. Arkaplanda aynı şeyler dönüyor ama dil farklı. Yani biri diğerinden üstün değil.
Ama gördüğüm kadarıyla C# daha popüler.

Ferhat
Reply
03/01/2015 02:24
#3

Hüseyin bey, çok mühendis ve programcı gördüm, sizin gibi paylaşımcı ve bilgilisini görmedim. Burada anlattıklarınızı şuanda çok büyük bir projede kullanıyorum. Gerçekten resim işleme işi çok kısa sürede bitiyor. Allah her zaman yar ve yardımcınız olsun.

Hüseyin Atasoy
Reply
04/01/2015 22:32
#4

İyi dilekleriniz için teşekkür ederim, işinize yaramasına sevindim.

Mücahit
Reply
05/09/2018 09:19
#5

Merhaba Hüseyin bey; paylaşımlarınız gerçekten çok güzel.
Teşekkür ederim.
Benim bu hususta şöyle bir ihtiyacım var. Kodlarımı da paylaşacağım. bir fotoğraf üzerine gri tonlamadan ziyade farklı bir resmi giydiriyorum. (https://st3.depositphotos.com/4702235/13688/v/1600/depositphotos_136888910-stock-illustration-classic-shirt-vector-illustration.jpg) burda örneği mevcuttur.
Şuan bu işlemi yapıyorum fakat 5-6-7 snye civarında zaman geçiyor. Fakat bu bahsettiğiniz yöntem çok süper. Acaba kullanılabilir süreyi 25 kat daha hızlı kullanabilir miyim bu konuda sadece gri tonlama mı yapılabilir yada efekt mi verilebilir yoksa farklı resim giydirilebilir mi? Fikriniz ve yardımlarınız bizim için çok değerli olacaktır. Teşekkür ederim.

Function giydir(ByVal mokap As Bitmap, ByVal arkap As Bitmap, ByVal desen As Bitmap) As Bitmap
        Try
            Dim x As Integer
            Dim y As Integer
            For x = 0 To mokap.Width - 1
                For y = 0 To mokap.Height - 1
                    If mokap.GetPixel(x, y).A <> 0 Then
                        arkap.SetPixel(x, y, Color.FromArgb(255, desen.GetPixel(x Mod desen.Width, y Mod desen.Height).R, desen.GetPixel(x Mod desen.Width, y Mod desen.Height).G, desen.GetPixel(x Mod desen.Width, y Mod desen.Height).B))
                    End If
                Next
            Next
        Catch
        End Try
        Return arkap
    End Function

Hüseyin Atasoy
Reply
06/09/2018 23:49
#6

Tabi, bu yöntemi herhangi bir amaçla kullanabilirsiniz. Yazdığınız kodta, GetPixel() fonksiyonunu her piksel için birden fazla defa çağırmışsınız, bir defa çağırmanız yeterli. Yazıdaki yaklaşımı kullanmak için, GetPixel() fonksiyonunu çağırıp .R .G ve .B bileşenlerini almak yerine, yazıdaki gibi bir döngü kurarak Renkler(i) ve i=i+1 ifadelerini R G ve B için 3 defa kullanabilirsiniz. i her artışta, sonraki rengin değerini tutan elemana ilerlenmesini sağlayacak.

Ufuk Durgun
Reply
19/10/2018 12:38
#7

Merhaba Hüseyin Bey,
Size Bitmap.Lockbit hakkında birşey sormak istiyorum. Örneğin 512*512 boyutlarında bir bitmap resim düşünün. Bu resmin içinden sadece benim seçtiğim farklı pixellerdeki 100 adet pixelin RGB değerini okumak istiyorum.(Örneğin x,y olarak 10,10 - 15,25 - 22,35 vb gibi farklı piksellerde) Okuma işlemi sıralı olmayacağı için kod dizilimini bir türlü yapamamaktayım. Böyle bir okumada lockbit hızı yinede .getpixele göre çok çok hızlı olurmu. VB.net diline oldukça hakimim ama bu konuda önerinizi bekliyorum. İlgi ve alakanız için şimdiden çok teşekkür ederim

Hüseyin Atasoy
Reply
19/10/2018 18:48
#8

100 adet piksel için çok aşırı bir fark olmaz ama eğer piksel sayısı fazlaysa, getpixel yine yavaş kalacak. Şöyle düşünün, mesela 1 pikseli okurken diğer yöntem getpixelden, 1 ms daha hızlı okuma yapıyorsa (örnek olsun diye 1 ms diyorum, yoksa tam ölçmedim), 100 pikselde 100 ms fark olurdu. Çok büyük bir fark değil. Ama mesela 1000 pikselde 1 saniye fark olurdu. Büyük bir fark.
Ben okuyacağınız piksellerin x ve y koordinatlarını iki diziye atıp (ya da List(of Point) yapısını kullanabilirsiniz) bir döngü ile getpixel'i kullanmanızı öneririm. 100 piksel için yukarıda yazdığım nedenle çok büyük bir fark olacağını sanmam.

Ufuk Durgun
Reply
19/10/2018 19:42
#9

Hüseyin Bey,
Öncelikle ilginiz ve hızlı cevabınız için çok teşekkür ederim. 512 x 512 piksel çözünürlüğünde bir resmi
.getpixel ile çok yavaş okumakta. Bu nedenle bitmap.lockbits yöntemini kullanıyorum. Aralarında çok fark var. Sadece mantık olarak bir resmi lockbits ile kilitledikten sonra kilit açılıncaya kadar sıradan değilde rasgele okuma yapabilirmiyim. Lockbit 512 x 512 bir resmi 33ms de  tamamlayabiliyor ama Scan0 sıra ile gidiyor galiba.
For y = 0 To bmd.Height - 1
            For x = 0 To bmd.Width - 1
                      Color = Marshal.ReadInt32(bmd.Scan0)           
            Next
Next

Bu şekilde bir taramada satır ve sütun olarak döngü ile taranıyor ve yaklaşık 33 ms de bitiyor. bir resim için hız tahminim aslında 20ms civarında. Size 100 adet okumayı tahmini olarak vermiştim Lütfen kusura bakmayın yanlış bir yönlendirme yaptıysam. Bu taramayı özel bir projede kullanacağım. 512 * 512 = 262144 pixel tarama. Ben 262144 pikselden yaklaşık olarak 100000 kadarını okuyacağım. Bu sorunu bu akşam çözeceğim. Ben sizin önerinizi merak etmekteyim. Siz olsaydınız sıralı okuma yapmamak kaydıyla belirlenen 100000 adet pikseli lockbit ile ve hız düşümü yaşamadan nasıl yapardınız.Kod önemli değil sadece mantığı benim için yeterli. Hem sizi yormamış olurum hemde tasarımı kendim yapmış olurum. Anlayış ve ilginiz için çok teşekkür ederim.

Hüseyin Atasoy
Reply
27/10/2018 09:31
#10

Rastgele okumak da mümkün tabi ama kodta verdiğiniz şekilde değil. Çünkü Scan0 bilgilerin tutulduğu bellek bölgesinin başlangıç adresi. Sürekli Scan0dan okuma yapmak size hep ilk 4 baytlık bloğu verecek.
Rastgele okuma yapılması istenirse, okunması istenen pikselin renk bilgilerinin tutulduğu konumun doğru hesaplanması gerekiyor. Yazıda bahsi geçen boş baytlar doğru şekilde atlanmalı. Bu da görüntünün formatına bağlı.

 
Şu an bu sayfada 1, blog genelinde 11 çevrimiçi ziyaretçi bulunuyor. Ziyaretçiler bugün toplam 2344 sayfa görüntüledi.
 
Sayfa 54 sorgu ile 0.056 saniyede oluşturuldu.
Atasoy Blog v4 © 2008-2024 Hüseyin Atasoy