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...
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ı ?
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.
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.
İyi dilekleriniz için teşekkür ederim, işinize yaramasına sevindim.
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
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.
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 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.
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ı.