12.8. Streaming dei frame

L’uso reale piu comune di un canale personalizzato e lo streaming dei frame immagine dalla camera a un programma host alla frequenza dei frame della camera. La meccanica e piu sottile di quanto sembri: un JPEG puo arrivare a 25 KB o piu, quindi l’host lo legge come diversi frammenti, e il ciclo di acquisizione della camera deve essere impedito dal sovrascrivere il buffer durante la lettura. Lo schema corretto – mostrato qui e usato dagli strumenti in openmv-projects/tools/blocca (latch) il buffer finche l’host non ha estratto l’ultimo byte.

12.8.1. Il lato camera

Un canale per i frame che acquisisce in un unico framebuffer, lo blocca alla prima lettura dell’host, e scatta lo snapshot successivo solo dopo che l’host ha consumato l’intera immagine:

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

Qui ci sono quattro elementi che svolgono un lavoro concreto:

  • frame_available e il latch. Il ciclo di acquisizione scatta un nuovo snapshot solo quando vale False – ovvero quando l’host ha estratto l’ultimo byte del frame precedente. La lettura dell’host lo riporta a False dall’interno di readp una volta servito l’offset finale. Senza questa protezione, il successivo csi0.snapshot() sovrascriverebbe il buffer durante la lettura e l’host riceverebbe un frame ricucito da due acquisizioni.

  • readp invece di read e cio che il backend implementa. La libreria del protocollo tratta il buffer restituito come autorevole e ne legge i byte direttamente nel pacchetto in uscita – senza copia. Per payload di dimensioni pari a un frame, readp e sensibilmente piu veloce di read, che impone una copia intermedia.

  • size restituisce la lunghezza JPEG memorizzata nella cache senza ricalcolare nulla; il ciclo di acquisizione la mantiene aggiornata ogni volta che rinfresca il buffer. L’host chiama size tra poll e readp per sapere quanti byte estrarre.

  • send_event() notifica all’host nell’istante in cui arriva un nuovo frame, cosi puo iniziare a estrarlo senza fare polling. L’ID dell’evento 0x01 e definito dall’applicazione («frame pronto» in questo caso); usa un intero piccolo diverso per ogni tipo di notifica.

12.8.2. Frammentazione

Un’immagine QVGA RGB565 a qualita JPEG 85 si comprime a circa 10-25 KB, a seconda della scena – molto piu grande del payload massimo negoziato su qualsiasi camera (vedi la tabella per scheda in protocol.init()). Una lettura JPEG non entra in un solo pacchetto, e va bene cosi, perche la libreria del protocollo la frammenta in modo trasparente.

Quando l’host richiede channel_read('frame', 12000):

  1. Il readp della camera viene chiamato una sola volta con offset=0 e l’intera richiesta di 12000 byte. Restituisce una sola memoryview che copre l’intero intervallo.

  2. La libreria del protocollo suddivide quella memoryview in frammenti di dimensione pari al payload massimo sul filo, un pacchetto di risposta CHANNEL_READ per frammento, ciascuno con la propria intestazione e CRC. I byte vengono trasmessi direttamente dal buffer del backend – senza copia.

  3. L’host riceve i frammenti in ordine, il livello di affidabilita ritrasmette qualsiasi blocco che non superi il proprio CRC, e l’SDK host riunisce i blocchi nel risultato di 12000 byte restituito al chiamante.

Nota

Questa e la differenza pratica fondamentale tra readp e read. readp viene chiamato una volta per richiesta dell’host; il livello del protocollo frammenta e trasmette a partire dall’unico buffer restituito. read viene chiamato una volta per frammento, e la libreria copia ciascun blocco restituito nel proprio buffer di pacchetto. Per payload di dimensioni pari a un frame, readp risparmia sia l’overhead della chiamata a livello Python per ogni frammento sia la copia.

Suggerimento

Vuoi constatare di persona la differenza? Rinomina il metodo readp del backend in read – non cambia nient’altro; la libreria rilevera invece la capacita read – e confronta il contatore della frequenza dei frame dell’host prima e dopo. Il numero piu basso e il costo della copia per frammento e della chiamata Python che eviti usando readp.

Il latch in FrameChannel.readp rilascia il buffer quando offset + size == img_size – nel momento in cui l’host ha estratto l’ultimo byte. Fino ad allora il buffer deve restare valido, ed e per questo che il ciclo di acquisizione scatta lo snapshot successivo solo dopo che frame_available torna a False.

12.8.3. Il lato host

L’host estrae i frame in un ciclo serrato:

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

La chiamata channel_size() funge anche da verifica «c’e qualcosa di pronto» – zero significa che la camera non ha ancora acquisito – cosi il ciclo salta i tentativi di lettura su un buffer vuoto. Per le applicazioni GUI che gia fanno polling su un timer, questo e lo schema naturale.

L”Image.open di Pillow decodifica il JPEG; la camera lo ha gia compresso in JPEG quindi l’host non deve rifare il costoso bit-packing su RGB565. Lo script host potrebbe altrettanto facilmente salvare i byte su disco, passarli a OpenCV o inviarli a una vista web.

12.8.4. Ragionare sul throughput

Tre fattori limitano la frequenza dei frame ottenibile:

  • La frequenza di acquisizione della camera. Il protocollo non puo consegnare i frame piu velocemente di quanto il sensore li produca; qualunque limite il formato dei pixel e la dimensione del frame scelti impongano all’acquisizione e il tetto massimo.

  • Il payload massimo negoziato. Payload piu grandi significano meno frammenti per frame e meno overhead di framing, quindi le camere con buffer di protocollo piu grandi spostano i byte piu velocemente di quelle piu piccole.

  • L’overhead di CRC e ACK. Ogni pacchetto costa 14 byte di framing piu un round-trip di ACK. Per i frammenti lunghi l’overhead per payload e piccolo; per i payload minuscoli e dominante.

Per la maggior parte del lavoro GUI da camera a laptop il fattore limitante e il tempo di acquisizione e di compressione JPEG della camera, non lo stack del protocollo. Dove il protocollo diventa effettivamente il collo di bottiglia – ad esempio lo streaming di frame raw non compressi ad alte frequenze – le leve sono disattivare gli ACK (protocol.init(ack=False)), aumentare il buffer di protocollo se la camera lo supporta, o acquisire in GRAYSCALE in modo che ogni JPEG compresso trasporti un solo canale invece di tre e il frame codificato risulti sensibilmente piu piccolo sul filo.

Il canale dei frame e il flusso di dati canonico da camera a host. La stessa interfaccia di backend, con l’aggiunta di un metodo write, permette all’host di inviare dati anche nell’altra direzione – cosa di cui uno strumento camera interattivo ha bisogno non appena l’operatore vuole modificare qualcosa anziche limitarsi a guardare.