12.8. Frames streamen¶
Het meest voorkomende praktische gebruik van een aangepast kanaal is het streamen van afbeeldingsframes van de cam naar een hostprogramma met de framesnelheid van de cam. De mechaniek is subtieler dan ze lijkt: een JPEG kan oplopen tot 25 KB of meer, dus de host leest deze als meerdere fragmenten, en de capture-lus van de cam moet worden verhinderd de buffer halverwege het lezen te overschrijven. Het juiste patroon – hier getoond en gebruikt door de tools in openmv-projects/tools/ – vergrendelt de buffer totdat de host de laatste byte heeft opgehaald.
12.8.1. De cam-zijde¶
Een framekanaal dat in een enkele framebuffer vastlegt, deze vergrendelt bij de eerste leesactie van de host, en pas de volgende momentopname maakt nadat de host de volledige afbeelding heeft verbruikt:
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
Vier onderdelen doen hier het echte werk:
frame_availableis de vergrendeling. De capture-lus maakt alleen een nieuwe momentopname wanneer dezeFalseis – wat betekent dat de host de laatste byte van het vorige frame heeft opgehaald. De leesactie van de host zet deze terug opFalsevan binnenuitreadpzodra de laatste offset is geserveerd. Zonder deze beveiliging zou de volgendecsi0.snapshot()de buffer halverwege het lezen overschrijven en zou de host een frame ontvangen dat uit twee opnamen aan elkaar is gezet.readpin plaats vanreadis wat de backend implementeert. De protocolbibliotheek behandelt de geretourneerde buffer als gezaghebbend en leest de bytes ervan rechtstreeks in het uitgaande pakket – zonder kopie. Voor framegrote payloads isreadpmerkbaar sneller danread, dat een tussenliggende kopie afdwingt.sizeretourneert de gecachete JPEG-lengte zonder iets opnieuw te berekenen; de capture-lus onderhoudt deze telkens wanneer ze de buffer ververst. De host roeptsizeaan tussenpollenreadpom te weten hoeveel bytes opgehaald moeten worden.send_event()stelt de host op de hoogte zodra een nieuw frame binnenkomt, zodat deze kan beginnen met ophalen zonder te pollen. De event-ID0x01is toepassingsgedefinieerd (“frame gereed” in dit geval); gebruik voor elk soort melding een ander klein geheel getal.
12.8.2. Fragmentatie¶
QVGA RGB565 op JPEG-kwaliteit 85 comprimeert tot ongeveer 10-25 KB, afhankelijk van het tafereel – veel groter dan de onderhandelde maximale payload op elke cam (zie de tabel per board in protocol.init()). Eén JPEG-leesactie past niet in één pakket, en dat is prima, want de protocolbibliotheek fragmenteert deze transparant.
Wanneer de host om channel_read('frame', 12000) vraagt:
De
readpvan de cam wordt eenmaal aangeroepen metoffset=0en het volledige verzoek van 12000 bytes. Deze retourneert één memoryview die het hele bereik beslaat.De protocolbibliotheek breekt die memoryview op de draad op in fragmenten ter grootte van de maximale payload, één
CHANNEL_READ-antwoordpakket per fragment, elk met een eigen header en CRC. De bytes worden rechtstreeks uit de buffer van de backend gestreamd – zonder kopie.De host ontvangt de fragmenten op volgorde, de betrouwbaarheidslaag verzendt elk stuk dat zijn CRC niet doorstaat opnieuw, en de host-SDK lijmt de stukken aan elkaar tot het resultaat van 12000 bytes dat aan de aanroeper wordt geretourneerd.
Notitie
Dit is het belangrijkste praktische verschil tussen readp en read. readp wordt eenmaal per hostverzoek aangeroepen; de protocollaag fragmenteert en verzendt vanuit de enkele geretourneerde buffer. read wordt eenmaal per fragment aangeroepen, en de bibliotheek kopieert elk geretourneerd stuk in zijn eigen pakketbuffer. Voor framegrote payloads bespaart readp zowel de Python-aanroepoverhead per fragment als de kopie.
Tip
Wil je het verschil zelf zien? Hernoem de readp-methode van de backend naar read – verder verandert er niets; de bibliotheek zal in plaats daarvan de read-mogelijkheid oppikken – en vergelijk de framesnelheidsteller van de host voor en na. Het tragere getal is de kopie per fragment en de Python-aanroepkost die je vermijdt door readp te gebruiken.
De vergrendeling in FrameChannel.readp geeft de buffer vrij wanneer offset + size == img_size – het moment waarop de host de laatste byte heeft opgehaald. Tot dan moet de buffer geldig blijven, en daarom maakt de capture-lus pas de volgende momentopname zodra frame_available terugspringt naar False.
12.8.3. De host-zijde¶
De host haalt frames op in een strakke lus:
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
De aanroep channel_size() fungeert tegelijk als een “is er iets gereed”-controle – nul betekent dat de cam nog niet heeft opgenomen – zodat de lus leespogingen op een lege buffer overslaat. Voor GUI-toepassingen die al op een timer pollen, is dit het natuurlijke patroon.
Pillow’s Image.open decodeert de JPEG; de cam heeft deze al JPEG-gecomprimeerd, zodat de host het dure bit-packing op RGB565 niet hoeft over te doen. Het hostscript zou de bytes net zo gemakkelijk naar schijf kunnen opslaan, aan OpenCV kunnen doorgeven, of door een webweergave kunnen sturen.
12.8.4. Nadenken over doorvoer¶
Drie dingen begrenzen de haalbare framesnelheid:
De capture-snelheid van de cam. Het protocol kan geen frames sneller leveren dan de sensor ze produceert; welke limiet het gekozen pixelformaat en de framegrootte ook op de capture leggen, dat is het plafond.
De onderhandelde maximale payload. Grotere payloads betekenen minder fragmenten per frame en minder framing-overhead, dus cams met grotere protocolbuffers verplaatsen bytes sneller dan kleinere.
CRC- en ACK-overhead. Elk pakket kost 14 bytes framing plus één ACK-retour. Voor lange fragmenten is de overhead per payload klein; voor kleine payloads domineert deze.
Voor de meeste cam-naar-laptop GUI-werk is de beperkende factor de capture- en JPEG-compressietijd van de cam, niet de protocolstack. Waar het protocol wel de bottleneck wordt – bijvoorbeeld bij het streamen van ongecomprimeerde ruwe frames met hoge framesnelheden – zijn de hefbomen het uitschakelen van ACKs (protocol.init(ack=False)), het vergroten van de protocolbuffer als de cam dit ondersteunt, of het opnemen in GRAYSCALE zodat elke gecomprimeerde JPEG één kanaal draagt in plaats van drie en het gecodeerde frame merkbaar kleiner op de draad uitkomt.
Het framekanaal is de canonieke cam-naar-host datastroom. Dezelfde backend-interface, met een write-methode toegevoegd, laat de host ook data de andere kant op sturen – wat een interactieve cam-tool nodig heeft zodra de operator iets wil veranderen in plaats van alleen toekijken.