12.8. Strömma bildrutor

Den vanligaste verkliga användningen av en anpassad kanal är att strömma bildrutor från kameran till ett värdprogram med kamerans bildhastighet. Mekaniken är subtilare än den ser ut: en JPEG kan bli 25 KB eller mer, så värden läser den i flera fragment, och kamerans inläsningsloop måste hindras från att skriva över bufferten mitt under en läsning. Rätt mönster – som visas här och används av verktygen i openmv-projects/tools/spärrar bufferten tills värden har hämtat den sista byten.

12.8.1. Kamerasidan

En bildrutekanal som fångar in i en enda bildbuffert, spärrar den vid värdens första läsning och tar nästa stillbild först när värden har konsumerat hela bilden:

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

Fyra delar gör det egentliga arbetet här:

  • frame_available är spärren. Inläsningsloopen tar en ny stillbild endast när den är False – vilket betyder att värden har hämtat den sista byten av föregående bildruta. Värdens läsning återställer den till False inifrån readp när den sista förskjutningen har levererats. Utan denna spärr skulle nästa csi0.snapshot() skriva över bufferten mitt under en läsning, och värden skulle ta emot en bildruta hopsatt av två infångningar.

  • readp i stället för read är det som backenden implementerar. Protokollbiblioteket behandlar den returnerade bufferten som auktoritativ och läser dess bytes direkt in i det utgående paketet – ingen kopia. För bildrutestora nyttolaster är readp märkbart snabbare än read, som tvingar fram en mellanliggande kopia.

  • size returnerar den cachade JPEG-längden utan att räkna om något; inläsningsloopen underhåller den varje gång den uppdaterar bufferten. Värden anropar size mellan poll och readp för att veta hur många bytes som ska hämtas.

  • send_event() underrättar värden i samma ögonblick som en ny bildruta landar, så att den kan börja hämta utan att avsöka. Händelse-ID:t 0x01 är applikationsdefinierat (”frame ready” i det här fallet); använd ett annat litet heltal för varje typ av notifiering.

12.8.2. Fragmentering

QVGA RGB565 vid JPEG-kvalitet 85 komprimeras till ungefär 10-25 KB beroende på scenen – mycket större än den förhandlade maximala nyttolasten på någon kamera (se tabellen per kort i protocol.init()). En JPEG-läsning får inte plats i ett paket, och det är okej, för protokollbiblioteket fragmenterar den transparent.

När värden ber om channel_read('frame', 12000):

  1. Kamerans readp anropas en gång med offset=0 och hela 12000-byte-begäran. Den returnerar en enda memoryview som täcker hela intervallet.

  2. Protokollbiblioteket delar upp den memoryview:en i fragment av maximal nyttolaststorlek på ledningen, ett CHANNEL_READ-svarspaket per fragment, vart och ett med sin egen header och CRC. Bytena strömmas ut direkt från backendens buffert – ingen kopia.

  3. Värden tar emot fragmenten i ordning, tillförlitlighetslagret skickar om varje enskilt block som misslyckas med sin CRC, och värd-SDK:n limmar ihop blocken till det 12000-byte-resultat som returneras till anroparen.

Anteckning

Detta är den centrala praktiska skillnaden mellan readp och read. readp anropas en gång per värdbegäran; protokolllagret fragmenterar och sänder ut ur den enda returnerade bufferten. read anropas en gång per fragment, och biblioteket kopierar varje returnerat block in i sin egen paketbuffert. För bildrutestora nyttolaster sparar readp både Python-anropskostnaden per fragment och kopian.

Tips

Vill du se skillnaden själv? Döp om backendens readp-metod till read – inget annat ändras; biblioteket plockar upp read-förmågan i stället – och jämför värdens bildhastighetsräknare före och efter. Det långsammare värdet är kopierings- och Python-anropskostnaden per fragment som du undviker genom att använda readp.

Spärren i FrameChannel.readp släpper bufferten när offset + size == img_size – i samma ögonblick som värden har hämtat den sista byten. Fram till dess måste bufferten förbli giltig, vilket är anledningen till att inläsningsloopen tar nästa stillbild först när frame_available slår tillbaka till False.

12.8.3. Värdsidan

Värden hämtar bildrutor i en tät loop:

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

Anropet channel_size() fungerar samtidigt som en ”är något klart”-kontroll – noll betyder att kameran ännu inte har fångat något – så loopen hoppar över läsförsök på en tom buffert. För GUI-applikationer som redan avsöker via en timer är detta det naturliga mönstret.

Pillows Image.open avkodar JPEG:en; kameran har redan JPEG-komprimerat den så värden behöver inte göra om dyr bit-packning på RGB565. Värdskriptet skulle lika gärna kunna spara bytena till disk, lämna dem till OpenCV eller skicka dem genom en webbvy.

12.8.4. Tänka kring genomströmning

Tre saker begränsar den uppnåeliga bildhastigheten:

  • Kamerans infångningshastighet. Protokollet kan inte leverera bildrutor snabbare än sensorn producerar dem; vilket tak än det valda pixelformatet och bildrutestorleken sätter på infångningen är detta taket.

  • Den förhandlade maximala nyttolasten. Större nyttolaster betyder färre fragment per bildruta och mindre ramningsöverhuvud, så kameror med större protokollbuffertar flyttar bytes snabbare än mindre.

  • CRC- och ACK-överhuvud. Varje paket kostar 14 bytes ramning plus en ACK-tur och retur. För långa fragment är överhuvudet per nyttolast litet; för små nyttolaster dominerar det.

För de flesta kamera-till-laptop-GUI-arbeten är den begränsande faktorn kamerans infångnings- och JPEG-komprimeringstid, inte protokollstacken. Där protokollet faktiskt blir flaskhalsen – till exempel vid strömning av okomprimerade råa bildrutor med hög bildhastighet – är spakarna att stänga av ACK:er (protocol.init(ack=False)), öka protokollbufferten om kameran stöder det, eller fånga i GRAYSCALE så att varje komprimerad JPEG bär en kanal i stället för tre och den kodade bildrutan slutar märkbart mindre på ledningen.

Bildrutekanalen är det kanoniska dataflödet från kamera till värd. Samma backend-gränssnitt, med en write-metod tillagd, låter värden skicka data åt andra hållet också – vilket är vad ett interaktivt kameraverktyg behöver så snart operatören vill ändra något i stället för att bara titta.