5.1. Image nesnesi¶
Bir görüntü işleme algoritması, bir görüntünün üzerinde her seferinde bir piksel olacak şekilde ilerler. Her konumda basit bir şey yapar – bir değer okur, onu bir eşikle karşılaştırır, ikinci bir görüntünün karşılık gelen pikseliyle birleştirir, sonucu geri yazar. Bütün bir çerçeve boyunca tekrarlanan bu basit piksel başına kararlar; kenar tespiti, nokta (blob) takibi, QR kodu çözümü ve diğer her klasik bilgisayar görüşü tekniğinin üzerine inşa edildiği şeylerdir. Bu işi verimli bir şekilde yapabilmek için algoritmanın her pikselin bellekte nerede oturduğunu, her pikselin değerinin gerçekte ne anlama geldiğini ve görüntünün hangi bölümüne bakması gerektiğini bilmesi gerekir. image.Image, bu bilgileri düzenleyen nesnedir.
Vision Sensors, csi.CSI.snapshot() döndüğü anda sona erdi. Kamera tarafındaki mekanizmanın yakalanan çerçeveyi üretmek için yaptığı her şey zaten yapılmıştır; uygulama elinde Image nesnesini tutar ve onunla ne yapacağını bilmesi gerekir.
5.1.1. Arabellek ve özellikleri¶
Image nesnesinin içinde, RAM’deki bitişik bir bayt bloğuna işaret eden bir gösterici ve üç parça meta veri taşıyan küçük bir başlık bulunur: görüntünün piksel cinsinden genişliği, piksel cinsinden yüksekliği ve baytların bulunduğu piksel formatı. Baytlar, satır öncelikli sırada saklanan piksellerin kendisidir – önce en üst satırın tüm pikselleri, ardından ikinci satırın tüm pikselleri ve böyle aşağıya doğru en alta kadar. Özellikler ise bunların nasıl okunacağını tanımlar.
Genişlik ve yükseklik basit tam sayı saymalarıdır. Piksel formatı daha ilginç bir özelliktir, çünkü her pikselin kaç bayt aldığını ve bu baytların neyi kodladığını belirler. Bir gri tonlama görüntüsü, piksel başına bir parlaklık değeri tutan bir bayt taşır. Bir RGB565 görüntüsü, 16 bitlik bir sözcüğe paketlenmiş kırmızı, yeşil ve mavi alanları tutan piksel başına iki bayt taşır. Bir Bayer görüntüsü piksel başına bir bayt taşır, ancak her piksel, mozaikteki konumuna göre seçilen üç renk filtresinden biri aracılığıyla örneklenir. Vision Sensors tüm kataloğu sıralamıştı; burada önemli olan, bu formatlardan tam olarak birinin her Image üzerinde ayarlı olması ve bu seçimin piksel başına bayt aritmetiğini ve arabellekteki herhangi bir tek baytın anlamını yönlendirmesidir.
Arabelleğe bir gösterici, genişlik, yükseklik ve format ile, bir algoritmanın isteyebileceği diğer her özellik kısa bir hesaplama olarak ortaya çıkar. (x, y) pikselini başlatan bayt, arabelleğin başlangıcından (y * width + x) * bytes_per_pixel ofsetinde oturur. Toplam bayt sayısı width * height * bytes_per_pixel değeridir. Bir sonraki alt satırın adresi, mevcut satırın başlangıcından tam olarak width * bytes_per_pixel bayt sonradır. Image, üç özelliği basit metot çağrıları aracılığıyla açığa çıkarır – width(), height(), format() – ayrıca türetilmiş size değerini size() aracılığıyla sunar. Modülün başka yerlerindeki metotlar, ofset aritmetiğini kendileri yapmak için bu değerleri kullanır; uygulama kodunun bunu yapması nadiren gerekir.
Bir Image, bitişik bir bellek bloğuna işaret eden küçük bir Python sarmalayıcısıdır: genişlik, yükseklik ve piksel formatını taşıyan bir başlık ve ardından piksel arabelleğinin kendisi.¶
5.1.2. Arabellek nereden gelir¶
Bu bölüm boyunca varsayılan senaryo, Vision Sensors’ın halihazırda ele aldığı senaryodur: yakalanan bir çerçeve snapshot işleminden gelir, baytlar kameranın çerçeve arabelleğinde (frame buffer) bulunur ve döndürülen Image onlara işaret eder. Bir tane elde etmenin üç başka yolu da düzenli olarak ortaya çıkar ve her biri arabelleğin nerede sonlanacağı hakkında farklı bir şey ima eder.
Bir dosyadan yükleme, yapıcıya bir yol geçirmek gibi görünür: image.Image("/sdcard/saved.jpg"). Modül, dosyayı Python yığınında yeni ayrılmış bir arabelleğe okur. BMP, PGM ve PPM dosyaları giriş sırasında çözülür ve elde edilen Image, sıkıştırılmamış bir piksel formatı taşır. JPEG ve PNG dosyaları sıkıştırılmış kalır – Image, JPEG veya PNG formatını taşır ve arabellek, dosyanın bayt akışını esasen değiştirilmemiş halde tutar. Sıkıştırılmış bir görüntü üzerinde herhangi bir piksel düzeyinde iş yapmak için uygulama onu önce to_rgb565() veya to_grayscale() aracılığıyla dönüştürür ve sıkıştırmanın açılması – ve buna karşılık gelen yığın şişmesi, ki 30 KB’lık bir JPEG 600 KB’lık RGB565’e dönüşebilir – gerçekten o dönüştürme sırasında gerçekleşir. Dosyadan yükleme, bir algoritmanın betiğin yanında saklanan bilinen bir referans çerçeveye karşı test edilmesi gerektiğinde, geliştirme sırasında en kullanışlıdır.
Sıfırdan bir tane oluşturmak tuval senaryosudur: image.Image(320, 240, image.RGB565), modülden o kadar baytı o formatta ayırmasını, içeriği sıfırlamasını ve sarmalayıcıyı geri vermesini ister. Pikseller henüz hiçbir anlam ifade etmez – hepsi sıfırdır – ancak boş görüntü, sık karşılaşılan birkaç desen için iş gücüdür: mevcut bir çerçevenin çıkarıldığı referans çerçeveler, grafik bindirmelerinin oluşturulduğu tuvaller, doldurulup maske olarak kullanılan ikili arabellekler.
Bir ndarray’den oluşturma, diğer yönde köprü kurar: herhangi bir sayısal hesaplamadan tekrar image modülüne. Yapıcıya bir float32 ulab.numpy.ndarray geçirmek, boyutları ndarray ile eşleşen bir Image üretir – iki eksenli bir (h, w) şekli gri tonlama görüntüsü olur, üç eksenli bir (h, w, 3) şekli RGB565 olur – float değerleri 0.0 – 255.0 aralığından tam sayı piksel aralığına ölçeklenerek. Bir sinir ağı ısı haritası, herhangi bir türde sayısal bir dizi, ml veya ulab tarafından üretilen herhangi bir şey, image modülünün çizim ve inceleme tarafının kullanabileceği bir şeye dönüşür.
Dört kaynağın tümü aynı türde bir Image geri verir. Döndürülen nesneyi kullanan kodun onun nereden geldiğini izlemesi asla gerekmez.
5.1.3. Baytlar üzerinde iki görünüm¶
Çoğu zaman uygulama kodu bir Image nesnesini türlenmiş bir görüntü nesnesi olarak ele alır – adlandırılmış metotlara sahip bir şey. Hikayenin diğer yarısı, aynı nesnenin, bytes argümanı alan herhangi bir MicroPython API’sine de saydam bir şekilde düz bir bayt dizisi olarak görünmesidir. Baytlar, arabelleğin bir kopyası değildir; onun doğrudan bir görünümüdür.
Bu düzen, yakalanan bir çerçeveyi kameradan dışarı göndermeyi tek satırlık hale getiren şeydir. Onu özetlemek (hash), bir seri bağlantı noktası üzerinden göndermek, bir ağ yuvasına iletmek – bunların hiçbiri ayrı bir “görüntüyü baytlara dönüştür” adımına ihtiyaç duymaz:
import csi
import hashlib
csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QQVGA)
img = csi0.snapshot()
uart.write(img) # transmits the raw pixel bytes
hashlib.sha256(img) # hashes the same bytes
sock.send(img) # sends them over a socket
Bayt benzeri görünüm, amaca uygun olarak varsayılan olarak salt okunurdur. Görüntü arabellekleri büyüktür ve bazen görüntüleme yığınının katmanları arasında paylaşılır; bu nedenle, bir çağrı yığınının derinliklerinde bir yerdeki gelişigüzel bir buf[0] = 0 ifadesine bir tanesini sessizce bozma gücü vermek, açıkta bırakılamayacak kadar keskin bir kenardır. Uygulamanın gerçekten ihtiyaç duyduğu şey okuma-yazma bayt düzeyinde erişim olduğunda – örneğin bilinen bir ofsete bir kalibrasyon değeri yazmak – bytearray(), aynı bellek üzerinde ayrı, açıkça okuma-yazma bir görünüm döndürür ve çağrı noktasında niyeti işaret eder.
5.1.4. Arabellek nerede yaşar¶
Piksel arabellekleri, RAM’de nerede oturdukları önemli olacak kadar büyüktür. Bir QQVGA RGB565 çerçevesi 160 × 120 × 2 = 38.400 bayttır; bir VGA RGB565 çerçevesi 614.400 bayttır; bir sinir ağı sınıflandırıcısının tüketebileceği 224 × 224 RGB565 girişi yaklaşık 100 KB’dir. En küçük kameralardaki Python yığını, çalışma zamanı önyüklendikten sonra yalnızca birkaç on kilobayt olabilir. Yığın üzerinde bir veya iki çerçeveden fazla görüntü verisi tutmak, diğer her şeyi oradan dışarı iter.
Bunun çıkış yolu, görüntü arabelleklerinin çoğunlukla Python yığınında yaşamamasıdır. Bunlar, Vision Sensors bölümünde çerçeve arabelleği (frame buffer) olarak tanıtılan özel RAM bölgesinde yaşarlar – kamera DMA’sının yakalanan çerçeveleri yazdığı ve IDE önizlemesinin bitmiş çerçeveleri okuduğu aynı bellek. Bir Image üzerindeki çoğu işlem, kaynağını yerinde değiştirir: algoritma pikselleri okur, karar verir, yeni değerleri geri yazar ve ayrı bir sonuç görüntüsü ayrılmaz. Ayrı bir sonuç üreten işlemler – format dönüştürmeleri ve birkaç tane daha – copy_to_fb anahtar sözcüğü argümanı aracılığıyla bu sonucu çerçeve arabelleğine (frame buffer) yerleştirmeye yönlendirilebilir. copy_to_fb=True aynı anda iki şey yapar: sonuç görüntüsünü yığın yerine çerçeve arabelleğine (frame buffer) koyar (yığın baskısını atlatarak) ve sonucu IDE önizlemesinin görüntüleyeceği bir sonraki çerçeve yapar. Bir işlem hattının son adımına copy_to_fb=True eklemek, sonucun ekranda görünmesini izlemek ve oradan yinelemek, görüntü işlemedeki en kullanışlı hata ayıklama deyimlerinden biridir.
Etiketli bir arabellek tutan bir sarmalayıcı, bir tanesini var etmenin dört yolu, baytları üzerinde iki görünüm ve yenilerinin nereye ineceğine karar veren bir anahtar ile, Image artık bir gizem değildir. Geriye kalan temel sorular – bir piksel konumunun nasıl adlandırıldığı, her pikselin gerçekte ne tuttuğu, bir işlemin onun bir bölümüne nasıl sınırlandırılacağı – bunun üzerine inşa edilir.