12.8. Transmiterea cadrelor în flux

Cea mai frecventă utilizare reală a unui canal personalizat este transmiterea în flux a cadrelor de imagine de la cameră către un program gazdă, la rata de cadre a camerei. Mecanismul este mai subtil decât pare: un JPEG poate ajunge la 25 KB sau mai mult, așa că gazda îl citește sub forma mai multor fragmente, iar bucla de captură a camerei trebuie împiedicată să suprascrie tamponul (buffer) în mijlocul citirii. Modelul corect – prezentat aici și folosit de instrumentele din openmv-projects/tools/blochează tamponul (buffer) până când gazda termină de extras ultimul octet.

12.8.1. Partea camerei

Un canal de cadre care capturează într-un singur framebuffer, îl blochează la prima citire a gazdei și ia următorul instantaneu doar după ce gazda a consumat imaginea completă:

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

Patru elemente fac muncă reală aici:

  • frame_available este zăvorul (latch). Bucla de captură ia un nou instantaneu doar atunci când acesta este False – adică gazda a extras ultimul octet al cadrului anterior. Citirea gazdei îl readuce la False din interiorul readp, odată ce a fost servit decalajul final. Fără această protecție, următorul csi0.snapshot() ar suprascrie tamponul (buffer) în mijlocul citirii, iar gazda ar primi un cadru asamblat din două capturi.

  • readp în loc de read este ceea ce implementează backend-ul. Biblioteca de protocol tratează tamponul (buffer) returnat ca fiind autoritativ și îi citește octeții direct în pachetul de ieșire – fără copiere. Pentru sarcini utile de dimensiunea unui cadru, readp este vizibil mai rapid decât read, care impune o copie intermediară.

  • size returnează lungimea JPEG memorată în cache fără a recalcula nimic; bucla de captură o menține ori de câte ori reîmprospătează tamponul (buffer). Gazda apelează size între poll și readp pentru a ști câți octeți trebuie să extragă.

  • send_event() notifică gazda în clipa în care sosește un nou cadru, astfel încât aceasta să poată începe extragerea fără sondare (polling). ID-ul evenimentului 0x01 este definit de aplicație („cadru gata” în acest caz); folosiți un alt număr întreg mic pentru fiecare tip de notificare.

12.8.2. Fragmentarea

QVGA RGB565 la calitatea JPEG 85 se comprimă la aproximativ 10-25 KB, în funcție de scenă – mult mai mare decât sarcina utilă maximă negociată pe orice cameră (vedeți tabelul per placă din protocol.init()). O citire JPEG nu va încăpea într-un singur pachet, iar acest lucru este în regulă, deoarece biblioteca de protocol o fragmentează în mod transparent.

Când gazda solicită channel_read('frame', 12000):

  1. Funcția readp a camerei este apelată o singură dată cu offset=0 și solicitarea completă de 12000 de octeți. Aceasta returnează un singur memoryview care acoperă întregul interval.

  2. Biblioteca de protocol împarte acel memoryview în fragmente de dimensiunea sarcinii utile maxime pe fir, câte un pachet de răspuns CHANNEL_READ per fragment, fiecare cu propriul antet și CRC. Octeții sunt transmiși în flux direct din tamponul (buffer) backend-ului – fără copiere.

  3. Gazda primește fragmentele în ordine, stratul de fiabilitate retransmite orice fragment care eșuează la verificarea CRC, iar SDK-ul gazdei lipește fragmentele într-un rezultat de 12000 de octeți returnat apelantului.

Notă

Aceasta este diferența practică esențială dintre readp și read. readp este apelat o singură dată per solicitare a gazdei; stratul de protocol fragmentează și transmite din unicul tampon (buffer) returnat. read este apelat o dată per fragment, iar biblioteca copiază fiecare fragment returnat în propriul tampon de pachet. Pentru sarcini utile de dimensiunea unui cadru, readp economisește atât supraîncărcarea apelului la nivel de Python per fragment, cât și copierea.

Sfat

Vreți să vedeți singuri diferența? Redenumiți metoda readp a backend-ului în read – nimic altceva nu se schimbă; biblioteca va prelua în schimb capabilitatea read – și comparați contorul de rată de cadre al gazdei înainte și după. Numărul mai mic reprezintă costul copierii per fragment și al apelului Python pe care îl evitați folosind readp.

Zăvorul (latch) din FrameChannel.readp eliberează tamponul (buffer) atunci când offset + size == img_size – în momentul în care gazda a extras ultimul octet. Până atunci, tamponul (buffer) trebuie să rămână valid, motiv pentru care bucla de captură ia următorul instantaneu doar după ce frame_available revine la False.

12.8.3. Partea gazdei

Gazda extrage cadre într-o buclă strânsă:

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

Apelul channel_size() servește totodată drept verificare de tip „este ceva gata” – zero înseamnă că camera nu a capturat încă – astfel încât bucla sare peste tentativele de citire pe un tampon (buffer) gol. Pentru aplicațiile GUI care sondează deja pe baza unui temporizator, acesta este modelul firesc.

Funcția Image.open din Pillow decodează JPEG-ul; camera l-a comprimat deja în JPEG, astfel încât gazda nu trebuie să refacă împachetarea costisitoare a biților pe RGB565. Scriptul gazdei ar putea la fel de ușor să salveze octeții pe disc, să îi predea către OpenCV sau să îi trimită printr-o vizualizare web.

12.8.4. Gândirea în termeni de debit

Trei lucruri limitează rata de cadre realizabilă:

  • Rata de captură a camerei. Protocolul nu poate livra cadre mai repede decât le produce senzorul; orice plafon impus capturii de formatul de pixel și de dimensiunea cadrului ales reprezintă limita superioară.

  • Sarcina utilă maximă negociată. Sarcini utile mai mari înseamnă mai puține fragmente per cadru și o supraîncărcare mai mică de încadrare, astfel încât camerele cu tampoane de protocol mai mari transferă octeții mai repede decât cele cu tampoane mai mici.

  • Supraîncărcarea CRC și ACK. Fiecare pachet costă 14 octeți de încadrare plus o întârziere dus-întors pentru ACK. Pentru fragmente lungi, supraîncărcarea per sarcină utilă este mică; pentru sarcini utile minuscule, aceasta domină.

Pentru majoritatea lucrărilor GUI de tip cameră-către-laptop, factorul limitativ este timpul de captură și de compresie JPEG al camerei, nu stiva de protocol. Acolo unde protocolul devine totuși blocajul – de exemplu, transmiterea în flux a cadrelor brute necomprimate la rate de cadre ridicate – pârghiile sunt dezactivarea ACK-urilor (protocol.init(ack=False)), mărirea tamponului de protocol dacă camera o permite sau capturarea în GRAYSCALE, astfel încât fiecare JPEG comprimat să transporte un singur canal în loc de trei, iar cadrul codificat să ajungă vizibil mai mic pe fir.

Canalul de cadre este fluxul de date canonic de la cameră către gazdă. Aceeași interfață de backend, cu o metodă write adăugată, permite gazdei să transmită date și în sens invers – ceea ce are nevoie un instrument interactiv pentru cameră imediat ce operatorul vrea să schimbe ceva în loc să doar privească.