9 min read

Sıfırdan Bilgisayarlı Görü ve Görüntü İşleme: Dijital İmgelerde İşlemler

Sıfırdan Bilgisayarlı Görü ve Görüntü İşleme: Dijital İmgelerde İşlemler
Photo by Roman Skrypnyk / Unsplash

Serimizin ikinci yazısı, bir çok sonraki yazıdan daha yoğun bir içeriğe sahip olacak diye düşünüyorum. Bunun nedeni, bir imge ile yapabileceğimiz operasyon sayısının çok fazla olması. Bunların başlıklarını kısaca görelim:

  • Filtereleme (Görüntü iyileştirme)
  • Gürültü azaltma (Görüntü iyileştirme)
  • Moire etkisi azalatma (Görüntü iyileştirme)
  • Kontrast arttırma (Görüntü iyileştirme)
  • Renk modelini değiştirme
  • Sıkıştırma (kayıplı, kayıpsız)
  • Koordinat sistemi değiştirme
  • Çembersel öteleme
  • Aritmetik işlemler (İki imge gereklidir.)
  • Mantıksal işlemler (İki imge gereklidir.)
  • Döndürme (Geometrik işlem)
  • Yeniden boyutlandırma (Geometrik işlem)
  • İteleme (Geometrik işlem)
  • ve daha bir çoğu...

Sırasıyla değil ama elimizden geldiğince değinmeye başlayalım.

Çembersel Öteleme

Çembersel öteleme, bir görseldeki tüm piksellerin $\alpha$ birim yana kaydığında, dışarıda kalan piksellerin görselin başındaki yeni boş yerlere eklenmesi ile yapılan işlemdir. Bu işlem çift yönelimde olabilir, bunlar dikey ve yatay silindirik öteleme olarak belirtilir. Aşağıda her iki durumu da kapsayan bir Python betiği bulunmaktadır.

Öncelikle program, bir Image sınıfı yaratarak başlıyor. Bu sınıfa ait bir nesne yaratabilmek için üç veri kullanıcıya soruluyor.

  1. Size: Her bir imge aslında bir üç boyutlu matristir - tensördür. Bu boyutlardan ikisi bizim gördüğümüz en ve boydaki pikselleri kapsarken, biri renkleri kapsar. Size niteliği ile bu konstraktör fonksiyonu kullanıcıdan görselin en ve boy oranlarını bir tuple (liste benzeri, Python'a özel bir veri tipi) olarak istemiştir. Örneğin size = (3, 4).
  2. ChannelSize: Bu nitelik görselde kaç boyutlu renk olacağını saklamak için bulunmaktadır. Genel kullanımda iki tip renk ailesi yaygındır: RGB ve CMYK. RGB renk ailesi, her bir rengin [0-255] arasındaki kırmızı, yeşil ve mavi oranlarının karıştırılarak elde edilebileceği metottur. CMYK renk ailesi ise yine aynı aralıktaki ancak farklı renklerde -camgöbeği, magenta, sarı, siyah- karışımların kullanılarak her bir rengin yapılabileceği metottur. Bunlara ek olarak kimi zaman, rengin saydamlığının bir ölçüsü olan, alpha adında bir başka renk niteliği de bulunur.
  3. Görselin renkleri: Evet, üçüncü parametrede istendiği gibi kullanıcıya aynı zamanda tek tek renklerin de sorulduğunu görüyoruz. Ancak bunun bir zorunluluk olmadığını, örneğimizde ana fonksiyonda çağırdığımız gibi, rastgele süreçlerle atama yapabilme özelliğinin de eklendiğini görüyoruz.

Ardından bizi ilgilendiren metodumuz kendini gösteriyor. Bu metotta algoritma olarak görselin $n$ kadar $\beta$ yönüne kaydırıldığında, aslında sondan $n$'e kadar olan elemanların matrisin en baştan başlayarak sondan $n$ kadar geriye yapıştırıldığı bilgisini kullanıyoruz. Geriye kalan son $n$ kadarlık boşluğa ise asıl görselimizdeki $n$ kadarlık kısmı koyuyoruz. Böylece aslında bir sürkülasyon şeklinde elemanlarımızı $\beta$ yönünde kaydırmış oluyoruz.

Not: Her ne kadar kodun en üstünde numpy kütüphanesini projeye eklemiş olsam da, yazı serisi boyunca anlattığım konuların tamamını ellerimle kendim yazıp sizinle paylaşacağım.
Örneğin yukarıda yazdığım metot yerine, numpy kütüphanesinin içinden hazır olarak bizlere sunulan roll() metotu da kullanılabilirdi.

PyCharm Professional 2020.3'ün öğrenci lisanslı sürümündeki SciView aracı ile matrislerin renklendirilmiş hallerinin ekran görüntülerini konuyu daha iyi anlamanız için hemen aşağıya ekliyorum.

Mantıksal İşlemler

Matematikten tanıdığımız VEYA (OR), VE (AND) ve YA DA (XOR) işlemleri, imgeler üzerinde de gerçekleştirilebilirdir. Genel kullanım kanısı ikili imgelerdedir (en. binary images).

Örneğin imgenin dış hatlarını bulmak istersek aşağıdaki işlemi yapabiliriz:

$$
K = (X^↔ + X) \text{mod} 2 || (X^↕ + X) \text{mod} 2
$$

Formülde gösterilen $X^↔$ işareti X görselinin çembersel öteleme yolu ile bir birim yatay ötelendiğini, $X^↕$ işareti ise aynı görselin çembersel öteleme yolu ile dikey ötelendiğini ifade etsin.

Renkli imgelerde mantıksal işlemler için şu bağlantıya göz atabalirsiniz ancak dersimiz kapsamında daha kolay olduğu için -evet kabul ediyorum- ikili imgelerde işlem yapacağız. Bir ikili imge, her pikseli yalnızca bir kanala sahip ve bu kanalda da $0$ veya $1$ değerlerini alabilen görüntülerdir.

Eğer ki 0'lar ve 1'ler ile işlem yapıyorsak, görüntü işlemede matematik aynı öğrendiğiniz gibi kalacaktır! Mantık kurallarını hatırlamanız için sizlere aşağıdaki tabloyu incelemenizi öneriyorum.

ab
~aa & ba | ba ^ b
110
110
100011
011011
0
01000

Her bir pikseli kendisiyle eşleşen diğer imgedeki pikselle mantıksal işleme sokarız. Oluşan çıktıları yine aynı şekilde toplar ve bir imge haline getiririz.

Çembersel öteleme örneğinde verdiğimiz kodu ilk olarak ikili imgeler ve gri-tonlu imgeler için de kullanılabilir şekilde uyarladım. numpy matrislerinde boyut algısı iki ve üç boyutta değiştiği için, yani üçüncü boyut aslında ilk indisle ifade edildiği için, kodlarda tek boyutlu matrislere özel kontrol koşulları eklemem gerekti. Daha sonra ise mantıksal işlemleri sınıfa tanıtabilmek için Python'daki işlem fonksiyonu geliştirme (en. operator overloading) özelliğinden yararlandım. __and__ metodu A & B durumunda, __or__ metodu A | B durumunda, __xor__ metodu A ^ B durumunda ve __invert__ metodu ise ~A durumunda çağırılacak işlevlerdir.

Yukarıdaki kodu incelediğinizde birkaç getter fonksiyon eklediğimi görebilirsiniz, bunu nesne yönelimli programlamaya saygımdan dolayı yaptığımı ve aslında Python'da private nitelikler bulunmadığını belirtmek isterim.

Programın hızlı bir şekilde iki ikili imgeyi kıyaslayabilmesi için birçoğunuzun MATLAB'den aşina da olabileceği vektörize fonksiyonları kullandım. Bu fonksiyonları Python'daki map()'e benzetebilirsiniz. Kodun ilgili yorum satırları dolayısı ile yeterince açıklayıcı olduğunu düşünüyorum. Programı kullanarak iki tane ikili imge oluturuyorum ve her birinin görüntüsünü, yine SciView aracıyla, ekliyorum. Bu sayede sizler de sınıfımızın düzgün çalıştığına şahit olabilirsiniz.

İkili imgelerimizle VE (&) işlemimizin sonucu olan ikili imgemiz.
İkili imgelelerimizle VEYA (|) işlemimizin sonucu olan ikili imgemiz.
İkili imgelerimizle YA DA (^) işlemimizin sonucu olan ikili imgemiz.

Yukarıdaki örneklerdeki pek gerçekçi durmayan imgeler bir kenara, gerçek bir gri-tonlu (en. grayscale) imge ile de deneme yapmak istedim açıkçası. Bunun için öncelikle bir görseli gri-tonlu haline çevirmemiz ve daha sonra bir Image nesnesine verilerini yedirmemiz gerektiğini bilerek işe koyulalım.

Seçtiğim görsel (veya imge), görüntü işlemede sıkça kullanılan Mandrill maymununun yüzü.

Mandrill, köpeksi maymunlar familyasına ait bir tür primattır.

Yazı serimin başında zorunda kalmadıkça -ve hepimizin yapabileceği işlemlerde- hiçbir şekilde bir görüntü işleme kütüphanesi kullanmayacağımı söylediğim için görseli gri-tonluya çevirirken opencv'yi kullanamayız. Tahmin ettiğiniz aksine henüz bir renkli resmi nasıl gri-tonluya çevireceğinize dair algoritma da yazmayacağım. Çünkü bu ders, ileride anlatacağımıız Görsel Segmentasyonu konusu kapsamına girmektedir. Bu site aracılığı ile BT-709 standardına göre görseli gri-tonlulaştırıyoruz.

BT-709 standardı, üç kanal olan kırmızı, mavi ve yeşili $0.21R + 0.72G + 0.07B$ formülüne göre ağırlıklı toplar ve çıktıyı 0-255 arasında tek kanalda verir.

Bu görseli ise numpy matrisine çevirmek hiç kolay olmayacak! İlk yazımızda anlattığım gibi her bir dosya tipinin kendisine göre bir kodlama metodu var. Bir resim dosyasını açmak için öncelikle o dosyanın kodlamasını bilmeli, ters kodlama ile matrisi elde etmeliyiz. Yasoob.me sitesinde anlatıldığı üzere bu bir postun alt başlığı değil, bir postun kendisi olacak kadar derin bir konu. Bu yüzden istemeden de olsa PIL kütüphanesini kullanacağım ancak yalnızca dosyamızı içeri aktarıp numpy matrisine çevirebilmek için.

Eğer ki programımız düzgün bir şekilde çalışıyor ise $A & ~A = 0$ olduğundan and_img_g [AND].png dosyamızda tamamen siyah renk, $A | ~A = 1$ olduğu için or_img_g [OR].png dosyamızda tamamen beyaz renk görmeliyiz. Çıktıları sizin de huzurunuzda aşağı ekliyorum.

Pek tabi ki main fonksiyonunun içinde sürekli her imge için aynı satırları yazmak doğru bir davranış olmazdı. Bundan dolayı, ileride hazırlamamızın gerekli olduğu bir imge dosyasını okuyup matrise çevirmeyi anlatacağımız yazımız hazırlanana kadar harici bir modülden yardım aldığım iki metot oluşturdum. Bunları aşağıdaki kod bloğunda görebilirsiniz. Artık her imge çağırmak istediğimizde img = Image.open("dosya_adi.jpg"), Image nesnesini kaydetmek istediğimizde ise img.save("kaydet.png") kodlarını kullanacağız.

Aritmetik İşlemler

Aynı boyutlardaki iki görüntüye yapılabilecek bir diğer grup işlem ise toplama, çıkarma ve çarpmadan oluşan aritmetik işlemlerdir. Her bir piksel çifti için bilinen matematik kuralının işlenmesinden farklı bir şey değildir. Bu basitliğinin yanı sıra asıl sorunu limitleri aşmasında yatmaktadır. Burada anlatmak istediğim nokta, img[47, 58] = 128 olan bir pikselin img2[47, 58] = 200 olan bir başka piksel ile toplandığında 328 gibi mümkün olmayan bir değere sahip olmasıdır. Aynı şey çıkartma işlemi için de negatif sayılara inilebileceği için geçerlidir. Piksellerdeki herbir renk değeri [0, 255] sınırları arasında olmak zorundadır.

Bu problemin çözümü için iki yol kullanılır. Bunlardan ilki en basit şekilde 255 üstü her değer 255'e, 0 altı her değer ise 0'a çevrilir çözümüdür. Ancak bu yol ile genelde imgelerin gerçek toplamından sapılmış olunur, bilgi kaybına uğramış oluruz. Bir diğer yol

Geometrik İşlemler

Hepimizin günlük hayatlarında düzenli olarak imgelere uyguladığı üç geometrik operasyon bulunuyor. Bunlardan ilki, kaydırma. Herhangi bir imgedeki her bir vektörü belirli adım yana kaydırdığınızı varsayım. İmge halen aynı olur değil mi? Düşündüğünüz kesinlikle doğru! Kaydırma işlemi sonucu imgemizin yalnızca konumu değişir.

Diğer iki işlem türü ise ölçeklendirme (büyütme/küçültme) ve döndürme. Sırası ile bunları inceleyelim.

Geometrik işlemlerde öğrenceğimiz tüm işlemler aslında kanvas üzerinde mantıklı olduğu için (örneğin bir tuval üzerinde resimi sağ kaydırmak, sola kaydırmak, döndürmek, ölçeklendirmek, vs.) bu bölümde yazdığımız resim nesnemize herhangi bir implementasyon yapmayacağız.

Ölçeklendirme

Bir resmi büyütmek istediğimizde iki parmağımızla resmi açma hareketi yapmamız gerektiği artık genlerimize işlemiş durumda. Peki ya arka planında yatan olayı biliyor muyuz?

Bir pikseli büyütmek, aslında onu parçalara ayırıp parçalar arasındaki mesafeyi açmak demektir. Ancak günümüz teknolojisi ile çekilen çoğu fotoğraf yüksek çözünürlükte olduğu için ve halihazırda yakınlaşmadan tüm piksellere erişmediğimiz için, yakınlaşma hareketi, ayrılmış parçalar arasındaki mesafelere detay pikselleri koyabiliyoruz. Böylece bir görüntüyü yaklaştırırken sorun yaşamıyoruz.

$$
x' = r \cdot x
$$
$$
y' = s \cdot y \n
$$
$$
z' = t \cdot z
$$

Yukarıda gördüğünüz denklemler, pikselleri şimdiki konumları $[x, y, z]$ olmak üzere $[x', y', z']$ ile değiştirir. Böylece yeni görüntü, öncekisine göre "büyümüş" olur. Belirtmekte yarar görüyorum ki buradaki x, y ve z değişkenleri renk kanallarını değil uzaydaki piksel pozisyonlarını ifade etmektedir. Örnek görüntüyü aşağıda bulacaksınız.

Döndürme

İmge döndürme işlemi diğer iki geometrik işleme göre daha karmaşık bir yapıya sahiptir.


En yakın zamanda güncellenecektir! :-) (1 yıl önce söyledim bunu ama)