5.33. ImageIO-streams

save() en to_jpeg() dekken het enkelvoudige-frame-I/O-geval: een toepassing legt een frame vast, codeert het en duwt het ergens naartoe. Een andere klasse toepassingen heeft het reeks-geval nodig: vele frames achter elkaar opnemen op de natuurlijke vastlegsnelheid, ze ergens opslaan waar ze later kunnen worden opgehaald, en ze met de juiste snelheid afspelen. Een script voor het verzamelen van trainingsgegevens legt enkele honderden voorbeeldframes vast voor een machine-learning-pijplijn; een inspectiestation-logboek registreert elk vastgelegd onderdeel voor traceerbaarheid; een ontwikkelscript speelt een opgeslagen reeks opnieuw af om een nieuw algoritme te testen tegen gegevens die eerder live werden vastgelegd.

De klasse ImageIO is de recorder/speler van de image-module. Eén stream bevat een reeks Image-frames – mogelijk van verschillende afmetingen en pixelformaten – samen met het inter-frame-interval van elk frame, zodat het afspelen de oorspronkelijke framesnelheid opnieuw kan creëren. Er zijn twee opslagvormen beschikbaar: een bestand op het bestandssysteem of een buffer van vaste grootte in RAM.

5.33.1. De twee opslagvormen

Een bestandsstream bewaart de opname over inschakelcycli heen en wordt alleen begrensd door de onderliggende opslag. Hij begint met een 16-byte magic-header OMV IMG STR Vx.y gevolgd door één chunk per frame; de huidige schrijver produceert V2.0 en de lezer accepteert nog steeds V1.0- en V1.1-bestanden voor achterwaartse compatibiliteit. Het bestandspad is het constructorargument; de modus is de bestandsopenmodus ('r' om een bestaande stream te lezen, 'w' om af te kappen en opnieuw te schrijven).

# Recording to /sdcard/run.bin
stream = image.ImageIO("/sdcard/run.bin", "w")
for _ in range(120):
    img = csi0.snapshot()
    stream.write(img)
stream.close()

Een geheugenstream leeft in een RAM-buffer die bij constructie wordt toegewezen. De constructor neemt een 3-tuple (w, h, pixformat) in plaats van een pad, en het argument mode wordt het vooraf toegewezen aantal frameslots. De buffer wordt precies bemeten voor dat aantal frames met de opgegeven afmetingen en mag niet groeien zodra hij is toegewezen – voorbij het laatste slot schrijven veroorzaakt een EOFError, en een frame schrijven dat groter is dan de per-slot-buffer veroorzaakt een ValueError. Geheugenstreams zijn het juiste gereedschap wanneer de toepassing een opname aan een stroomafwaartse fase moet overhandigen zonder het bestandssysteem te doorlopen (bijvoorbeeld een korte ringbuffer met recente frames voor een trigger-en-replay-patroon).

# Pre-allocate space for 32 QVGA RGB565 frames in RAM
stream = image.ImageIO((320, 240, image.RGB565), 32)
for _ in range(32):
    stream.write(csi0.snapshot())

Voor de gecomprimeerde pixelformaten (image.JPEG, image.PNG) wordt de per-slot-grootte geschat op 2 bits per pixel; een gecodeerd frame dat groter is dan de schatting veroorzaakt bij het schrijven een ValueError, dus een toepassing die hoogwaardige JPEG’s verwacht op te slaan moet ofwel het aantal slots te ruim toewijzen ofwel eerst op een lagere kwaliteit coderen.

type() retourneert image.ImageIO.FILE_STREAM of image.ImageIO.MEMORY_STREAM zodat stroomafwaartse code zich kan aanpassen aan welke opslagvorm hij ook krijgt.

5.33.2. Opnemen

write() voegt een vastgelegd Image toe aan een bestandsstream (of slaat het op in het huidige slot van een geheugenstream) en verhoogt de offset met één. Dezelfde aanroep registreert het inter-frame-interval sinds de laatste schrijfactie, zodat de afspeelhelft de juiste tijd tussen frames kan pauzeren en de natuurlijke framesnelheid van de opname behouden blijft.

Heterogene frames zijn toegestaan binnen één bestandsstream: een opname kan vrijelijk RGB565-vastleggingen, grijswaarden-uitsnedes en JPEG-gecodeerde miniaturen mengen, en de lezer zal elk frame op zijn oorspronkelijke afmeting en formaat decoderen. Geheugenstreams zijn homogeen (alle slots delen de door de constructor opgegeven (w, h, pixformat)), dus een geheugenopname is beperkt tot één frameconfiguratie.

write() retourneert het streamobject zodat aanroepen aan elkaar geketend kunnen worden. Schrijven op een niet-eindoffset van een bestandsstream kapt de rest van het bestand af – nuttig om een opgeslagen reeks te bewerken, riskant als de positie voor de volgende schrijfactie onbedoeld door een eerdere seek() is verplaatst.

sync() schrijft openstaande schrijfacties naar schijf voor bestandsstreams (bij geheugenstreams is het een no-op) en moet periodiek worden aangeroepen wanneer de opname langlopend is, om te voorkomen dat het staartstuk van de opname verloren gaat als de cam opnieuw opstart voordat het bestand is gesloten. De destructor sluit de stream automatisch wanneer de ImageIO buiten scope raakt, maar een expliciete close() is de juiste discipline.

5.33.3. Afspelen

read() leest het frame op de huidige offset, verhoogt de offset en retourneert het nieuwe Image. De ontvanger blijft in de framebuffer wanneer copy_to_fb=True (de standaard) zodat de geretourneerde afbeelding tekenbaar is via de IDE-preview; met copy_to_fb=False belandt het frame op de MicroPython-heap.

# Loop a recorded stream at its natural frame rate
stream = image.ImageIO("/sdcard/run.bin", "r")
while True:
    img = stream.read()
    # img is now in the frame buffer; the IDE shows it
    # and the script can run any analysis it likes

Twee keywords besturen het afspeelgedrag. loop=True (de standaard voor bestandsstreams) zet de leeswijzer terug naar het begin wanneer het einde van de opname wordt bereikt, zodat de aanroep nooit None retourneert; loop=False retourneert None zodra de opname is uitgeput en de lus van de aanroeper eindigt. pause=True (de standaard) blokkeert de aanroep totdat het bij het schrijven geregistreerde inter-frame-interval is verstreken, zodat de afspeelframesnelheid overeenkomt met de oorspronkelijke vastlegframesnelheid; pause=False keert onmiddellijk terug, nuttig voor analysepijplijnen die de opname zo snel mogelijk willen doorwerken zonder de oorspronkelijke timing te respecteren.

Hetzelfde luspatroon werkt voor geheugenstreams, behalve dat loop wordt genegeerd – voorbij het einde van een geheugenstream lezen veroorzaakt een EOFError. Het verwachte patroon voor een geheugenring is om expliciet met seek() terug naar nul te gaan wanneer terugkoppeling gewenst is.

5.33.5. Op een host afspeelbare opnames

ImageIO-streams zijn het juiste gereedschap wanneer de opname op de cam zal worden afgespeeld – ze behouden elk vastgelegd frame in zijn oorspronkelijke pixelformaat, het inter-frame-interval wordt exact geregistreerd, en een stroomafwaarts script kan er stap voor stap doorheen lopen, seeken en heranalyseren zonder verlies. Ze zijn echter niet het juiste gereedschap wanneer de opname afspeelbaar moet zijn op een host – een werkstation, een telefoon, een webspeler. Een host verwacht een standaard-videocontainer, niet het OpenMV-op-schijf-magic-header-formaat.

Twee aparte modules dekken het op een host afspeelbare geval. De module mjpeg neemt Motion JPEG op: een reeks JPEG-gecomprimeerde frames verpakt in één AVI-achtige container die VLC, QuickTime, ffmpeg en de standaard web-videotag allemaal rechtstreeks afspelen. De module gif neemt een geanimeerde GIF op: een reeks ongecomprimeerde (of palet-gecomprimeerde) frames met expliciete per-frame-vertragingen, afspeelbaar in elke webbrowser of afbeeldingsviewer die geanimeerde GIF’s ondersteunt.

De module mjpeg is de natuurlijke keuze voor lange opnames. JPEG-compressie houdt de bestandsgrootte beheersbaar – vergelijkbaar met to_jpeg() bij de geconfigureerde kwaliteit, frame na frame – zodat een uitgebreide vastlegsessie binnen het budget van de SD-kaart blijft. Het gebruik weerspiegelt het opnemen met ImageIO nauwgezet:

import mjpeg

m = mjpeg.Mjpeg("/sdcard/run.mjpeg")
while running:
    m.add_frame(csi0.snapshot(), quality=85)
m.close()

mjpeg.Mjpeg accepteert dezelfde positionele en schaal-keywords in tekenstijl die andere image-methoden gebruiken, zodat een opname per frame kan worden geschaald, uitgesneden of palet-gemapt onderweg naar binnen. De argumenten width en height van de constructor zijn standaard de afmetingen van de hoofdframebuffer en leggen de uitvoerresolutie vast; elk toegevoegd frame wordt geschaald (met behoud van de beeldverhouding) om te passen. sync() schrijft het bestand naar schijf tijdens een lange opname, en close() rondt de container af – een Motion JPEG-bestand dat niet netjes is gesloten is niet afspeelbaar, dus de discipline doet ertoe.

De module gif is de natuurlijke keuze voor korte opnames die letterlijk worden gedeeld met een niet-technische kijker – een paar seconden actie vastgelegd voor een demo, een geanimeerde illustratie voor documentatie, een gebeurtenisfragment ingesloten in een chatbericht. GIF-frames worden ongecomprimeerd opgeslagen (of palet-gecomprimeerd op een kleurdiepte van 7 bits), wat de bestanden per seconde veel groter maakt dan Motion JPEG en het formaat uitsluit voor opnames die langer zijn dan een paar seconden, maar het resultaat valt rechtstreeks in elke browser:

import gif

g = gif.Gif("/sdcard/clip.gif")
while running:
    g.add_frame(csi0.snapshot(), delay=10)
g.close()

Het argument delay op add_frame() is de per-frame-weergavetijd in centiseconden (10 is 100 ms per frame, oftewel 10 fps), wat de standaard-GIF-afspeelbesturing is. Het keyword loop van de constructor bepaalt of het resulterende fragment automatisch in lus afspeelt in viewers (de standaard is True, wat overeenkomt met de conventionele “geanimeerde GIF”-verwachting).

De drie opnameroutes dekken samen de gangbare gevallen: ImageIO voor herverwerking op de cam, Motion JPEG voor lange op een host afspeelbare opnames, geanimeerde GIF voor korte op een host afspeelbare fragmenten. De keuze tussen hen komt neer op wie de opname afspeelt. Een stroomafwaartse fase die op de cam zelf draait leest ImageIO; een host-werkstation of webviewer leest MJPEG of GIF.

5.33.6. Een trigger-en-replay-patroon

Een nuttig patroon combineert een geheugenstream met een triggervoorwaarde. De cam neemt continu op in een count-slot-geheugenringbuffer en overschrijft telkens het oudste slot. Wanneer een triggervoorwaarde afgaat (een blob komt het frame binnen, een bewegingsgebeurtenis overschrijdt de drempelwaarde, een knop wordt ingedrukt) maakt de toepassing een momentopname van de inhoud van de ring – de meest recente count-frames – en schrijft die naar een bestandsstream op de SD-kaart. Het resultaat is een pre-trigger-opname die de seconden vóór de gebeurtenis die de cam daadwerkelijk opmerkte vastlegt, niet alleen de seconden erna, wat de klassieke beperking is van een naïeve “vastleggen-bij-trigger”-recorder.

De implementatie is eenvoudig zodra de streamklassen voorhanden zijn: een geheugenstream van vaste grootte dient als de ring (met expliciete seek() naar nul wanneer de offset het aantal slots bereikt), de hoofdlus legt er bij elke iteratie in vast, en de triggerafhandelaar leest de geheugenstream frame voor frame uit en schrijft elk in een bestandsstream genoemd naar de tijdstempel van de trigger.