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 ärFalse– vilket betyder att värden har hämtat den sista byten av föregående bildruta. Värdens läsning återställer den tillFalseinifrånreadpnär den sista förskjutningen har levererats. Utan denna spärr skulle nästacsi0.snapshot()skriva över bufferten mitt under en läsning, och värden skulle ta emot en bildruta hopsatt av två infångningar.readpi stället förreadä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 ärreadpmärkbart snabbare änread, som tvingar fram en mellanliggande kopia.sizereturnerar 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 anroparsizemellanpollochreadpfö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:t0x01ä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):
Kamerans
readpanropas en gång medoffset=0och hela 12000-byte-begäran. Den returnerar en enda memoryview som täcker hela intervallet.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.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.