12.8. Çerçeve akışı (streaming)

Özel bir kanalın en yaygın gerçek kullanımı, görüntü çerçevelerini kameranın çerçeve hızında kameradan bir ana bilgisayar (host) programına akıtmaktır. Mekanik, göründüğünden daha inceliklidir: bir JPEG 25 KB veya daha fazlasına ulaşabilir, bu nedenle host onu birkaç parça hâlinde okur ve kameranın yakalama döngüsünün arabelleği okuma ortasında üzerine yazması engellenmelidir. Doğru desen – burada gösterilen ve openmv-projects/tools/ içindeki araçların kullandığı – host son baytı çekmeyi bitirene kadar arabelleği kilitler (latch).

12.8.1. Kamera tarafı

Tek bir çerçeve arabelleğine (frame buffer) yakalama yapan, host’un ilk okumasında onu kilitleyen (latch) ve yalnızca host tüm görüntüyü tükettikten sonra bir sonraki anlık görüntüyü alan bir çerçeve kanalı:

import csi
import protocol

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)
csi0.framebuffers(1)

img = csi0.snapshot()
img.compress(quality=85)
img_mv = memoryview(img.bytearray())
img_size = len(img_mv)
frame_available = True


class FrameChannel:
    def poll(self):
        return frame_available

    def size(self):
        return img_size

    def readp(self, offset, size):
        global frame_available
        end = offset + size
        mv = img_mv[offset:end]
        if end == img_size:
            # Host has just read the last byte of this frame --
            # release the buffer so the capture loop can refresh.
            frame_available = False
        return mv


ch = protocol.register(name='frame', backend=FrameChannel())

while True:
    if not frame_available:
        img = csi0.snapshot()
        img.compress(quality=85)
        img_mv = memoryview(img.bytearray())
        img_size = len(img_mv)
        frame_available = True
        ch.send_event(0x01)   # notify host that a new frame is ready

Burada gerçek işi yapan dört parça var:

  • frame_available kilittir (latch). Yakalama döngüsü yalnızca o False olduğunda – yani host önceki çerçevenin son baytını çektiğinde – yeni bir anlık görüntü alır. Host’un okuması, son ofset sunulduktan sonra readp içinden onu yeniden False olarak ayarlar. Bu koruma olmadan, bir sonraki csi0.snapshot() arabelleği okuma ortasında üzerine yazar ve host iki yakalamadan birleştirilmiş bir çerçeve alırdı.

  • Arka ucun (backend) uyguladığı read değil readp yöntemidir. Protokol kütüphanesi döndürülen arabelleği yetkili sayar ve baytlarını doğrudan giden pakete okur – kopyalama yapmaz. Çerçeve boyutundaki yükler için readp, bir ara kopyalamaya zorlayan read‘den belirgin biçimde daha hızlıdır.

  • size, hiçbir şeyi yeniden hesaplamadan önbelleğe alınmış JPEG uzunluğunu döndürür; yakalama döngüsü arabelleği her tazelediğinde bunu günceller. Host, kaç bayt çekeceğini bilmek için poll ile readp arasında size‘ı çağırır.

  • send_event(), yeni bir çerçeve geldiği an host’a bildirir; böylece host yoklama (polling) yapmadan çekmeye başlayabilir. Olay kimliği 0x01 uygulama tanımlıdır (bu durumda “çerçeve hazır”); her bildirim türü için farklı küçük bir tam sayı kullanın.

12.8.2. Parçalama (fragmentation)

JPEG kalitesi 85’te QVGA RGB565, sahneye bağlı olarak kabaca 10-25 KB’a sıkışır – herhangi bir kameradaki anlaşılan maksimum yükten çok daha büyük (kart başına tabloya bakın: protocol.init()). Tek bir JPEG okuması tek bir pakete sığmaz ve bu sorun değildir, çünkü protokol kütüphanesi onu şeffaf biçimde parçalara böler.

Host channel_read('frame', 12000) istediğinde:

  1. Kameranın readp‘ı offset=0 ve 12000 baytlık tam istekle bir kez çağrılır. Tüm aralığı kapsayan tek bir memoryview döndürür.

  2. Protokol kütüphanesi bu memoryview’i kablo üzerinde maksimum yük boyutunda parçalara böler; her parça için kendi başlığı ve CRC’si olan bir CHANNEL_READ yanıt paketi gönderir. Baytlar doğrudan arka ucun arabelleğinden akıtılır – kopyalama yoktur.

  3. Host parçaları sırayla alır, güvenilirlik katmanı CRC’sini geçemeyen herhangi bir parçayı yeniden iletir ve host SDK’sı parçaları, çağırana döndürülen 12000 baytlık sonuca yapıştırır.

Not

Bu, readp ile read arasındaki temel pratik farktır. readp host isteği başına bir kez çağrılır; protokol katmanı, döndürülen tek arabellekten parçalara böler ve iletir. read ise parça başına bir kez çağrılır ve kütüphane döndürülen her parçayı kendi paket arabelleğine kopyalar. Çerçeve boyutundaki yükler için readp, hem parça başına Python düzeyindeki çağrı yükünü hem de kopyalamayı ortadan kaldırır.

Tüyo

Farkı kendiniz görmek ister misiniz? Arka ucun readp yöntemini read olarak yeniden adlandırın – başka hiçbir şey değişmez; kütüphane bunun yerine read yeteneğini alacaktır – ve host’un çerçeve hızı sayacını öncesi ve sonrasıyla karşılaştırın. Daha yavaş sayı, readp kullanarak kaçındığınız parça başına kopyalama ve Python çağrısı maliyetidir.

FrameChannel.readp içindeki kilit (latch), offset + size == img_size olduğunda – host’un son baytı çektiği anda – arabelleği serbest bırakır. O ana kadar arabellek geçerli kalmalıdır; bu nedenle yakalama döngüsü yalnızca frame_available yeniden False olduğunda bir sonraki anlık görüntüyü alır.

12.8.3. Host tarafı

Host, çerçeveleri sıkı bir döngüde çeker:

import io
from PIL import Image
from openmv.camera import Camera

with Camera('/dev/ttyACM0', baudrate=921600) as cam:
    cam.update_channels()

    while True:
        size = cam.channel_size('frame')
        if not size:
            continue
        data = cam.channel_read('frame', size)
        img = Image.open(io.BytesIO(data))
        img.show()                  # or feed to a GUI

channel_size() çağrısı aynı zamanda bir “hazır bir şey var mı” denetimi işlevi görür – sıfır, kameranın henüz yakalama yapmadığı anlamına gelir – böylece döngü boş bir arabellekte okuma denemelerini atlar. Zaten bir zamanlayıcı üzerinde yoklama yapan GUI uygulamaları için bu doğal desendir.

Pillow’un Image.open‘ı JPEG’i çözer; kamera onu zaten JPEG ile sıkıştırdığı için host, RGB565 üzerinde pahalı bit paketlemeyi yeniden yapmak zorunda kalmaz. Host betiği baytları diske kaydedebilir, OpenCV’ye verebilir veya bir web görünümüne aktarabilir; hepsi aynı kolaylıkta.

12.8.4. İş hacmi açısından düşünmek

Erişilebilir çerçeve hızını üç şey sınırlar:

  • Kameranın yakalama hızı. Protokol, çerçeveleri sensörün ürettiğinden daha hızlı teslim edemez; seçilen piksel biçiminin ve çerçeve boyutunun yakalamaya uyguladığı her sınır tavandır.

  • Anlaşılan maksimum yük. Daha büyük yükler, çerçeve başına daha az parça ve daha az çerçeveleme yükü anlamına gelir; bu nedenle daha büyük protokol arabellekleri olan kameralar baytları daha küçük olanlardan daha hızlı taşır.

  • CRC ve ACK yükü. Her paketin maliyeti 14 baytlık çerçeveleme artı bir ACK gidiş-dönüşüdür. Uzun parçalar için yük başına ek yük küçüktür; küçük yükler için ise baskındır.

Çoğu kameradan dizüstüne GUI çalışmasında sınırlayıcı etken, protokol yığını değil, kameranın yakalama ve JPEG sıkıştırma süresidir. Protokolün darboğaza dönüştüğü yerlerde – örneğin sıkıştırılmamış ham çerçeveleri yüksek çerçeve hızlarında akıtarken – kullanılabilecek kaldıraçlar şunlardır: ACK’leri kapatmak (protocol.init(ack=False)), kamera destekliyorsa protokol arabelleğini büyütmek veya GRAYSCALE’de yakalamak; böylece sıkıştırılmış her JPEG üç yerine tek kanal taşır ve kodlanmış çerçeve kabloda belirgin biçimde küçülür.

Çerçeve kanalı, kanonik kameradan host’a veri akışıdır. Aynı arka uç arabirimi, eklenen bir write yöntemiyle, host’un veriyi diğer yöne de göndermesine olanak tanır – ki bu, operatör yalnızca izlemek yerine bir şeyi değiştirmek istediği anda etkileşimli bir kamera aracının ihtiyaç duyduğu şeydir.