5.33. ImageIO-strömmar

save() och to_jpeg() täcker fallet med I/O för en enskild bildruta: en applikation fångar en bildruta, kodar den och skickar den vidare någonstans. En annan klass av applikationer behöver sekvensfallet: spela in många bildrutor i följd i den naturliga infångningstakten, lagra dem någonstans där de kan hämtas senare och spela upp dem i rätt hastighet. Ett skript för insamling av träningsdata fångar några hundra exempelbildrutor till en maskininlärningspipeline; en logg vid en inspektionsstation registrerar varje infångad detalj för spårbarhet; ett utvecklingsskript spelar upp en lagrad sekvens på nytt för att testa en ny algoritm mot data som tidigare fångats live.

ImageIO-klassen är bildmodulens inspelare/uppspelare. En enda ström rymmer en sekvens av Image-bildrutor – möjligen av olika storlekar och pixelformat – tillsammans med intervallet mellan bildrutorna för var och en, så att uppspelningen kan återskapa den ursprungliga bildfrekvensen. Två lagringsbackends finns tillgängliga: en fil i filsystemet eller en buffert med fast storlek i RAM.

5.33.1. De två lagringsbackenderna

En filström bevarar inspelningen genom strömcykler och begränsas i storlek endast av lagringen bakom den. Den inleds med en 16-byte magisk header OMV IMG STR Vx.y följt av ett segment per bildruta; den nuvarande skrivaren genererar V2.0 och läsaren accepterar fortfarande filer av typen V1.0 och V1.1 för bakåtkompatibilitet. Filsökvägen är konstruktorns argument; läget är fil-öppningsläget ('r' för att läsa en befintlig ström, 'w' för att tömma och skriva på nytt).

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

En minnesström lever i en RAM-buffert som allokeras vid konstruktionen. Konstruktorn tar en (w, h, pixformat)-tupel med tre element i stället för en sökväg, och mode-argumentet blir det förallokerade antalet bildruteplatser. Bufferten dimensioneras exakt för så många bildrutor med de angivna dimensionerna och får inte växa när den väl allokerats – att skriva förbi den sista platsen ger upphov till EOFError, och att skriva en bildruta större än bufferten per plats ger upphov till ValueError. Minnesströmmar är rätt verktyg när applikationen behöver lämna över en inspelning till ett nedströmssteg utan att gå via filsystemet (en kort ringbuffert med de senaste bildrutorna för ett utlös-och-spela-upp-mönster, till exempel).

# 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())

För de komprimerade pixelformaten (image.JPEG, image.PNG) uppskattas storleken per plats till 2 bitar per pixel; en kodad bildruta större än uppskattningen ger upphov till ValueError vid skrivning, så en applikation som förväntar sig att lagra JPEG-bilder av hög kvalitet måste antingen överallokera antalet platser eller koda med lägre kvalitet först.

type() returnerar image.ImageIO.FILE_STREAM eller image.ImageIO.MEMORY_STREAM så att nedströmskod kan anpassa sig till vilken lagringsbackend den än får.

5.33.2. Inspelning

write() lägger till en infångad Image till en filström (eller lagrar den på den aktuella platsen i en minnesström) och flyttar fram offseten med ett steg. Samma anrop registrerar intervallet mellan bildrutorna sedan den senaste skrivningen, så att uppspelningssidan kan pausa rätt lång tid mellan bildrutorna och inspelningens naturliga bildfrekvens bevaras.

Heterogena bildrutor är tillåtna inom en enda filström: en inspelning kan fritt blanda RGB565-infångningar, beskärningar i gråskala och JPEG-kodade miniatyrer, och läsaren avkodar var och en i dess ursprungliga storlek och format. Minnesströmmar är homogena (alla platser delar den (w, h, pixformat) som angavs i konstruktorn), så en minnesinspelning är begränsad till en enda bildrutekonfiguration.

write() returnerar strömobjektet så att anrop kan kedjas. Att skriva vid en offset som inte är slutet av en filström trunkerar resten av filen – användbart för att redigera en lagrad sekvens, riskabelt om nästa-skriv-position oavsiktligt flyttades av en tidigare seek().

sync() spolar väntande skrivningar till disk för filströmmar (det är en operation utan effekt på minnesströmmar) och bör anropas regelbundet när inspelningen pågår länge, för att undvika att förlora slutet av inspelningen om kameran startas om innan filen stängts. Destruktorn stänger strömmen automatiskt när ImageIO går ur räckvidd, men ett uttryckligt close() är rätt disciplin.

5.33.3. Uppspelning

read() läser bildrutan vid den aktuella offseten, flyttar fram offseten och returnerar den nya Image. Mottagaren förblir i bildbufferten när copy_to_fb=True (standardvärdet) så att den returnerade bilden kan ritas via IDE-förhandsgranskningen; med copy_to_fb=False hamnar bildrutan på MicroPython-heapen.

# 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

Två nyckelord styr uppspelningsbeteendet. loop=True (standardvärdet för filströmmar) lindar tillbaka läspekaren till början när slutet av inspelningen nås, så att anropet aldrig returnerar None; loop=False returnerar None när inspelningen är uttömd och anroparens loop avslutas. pause=True (standardvärdet) blockerar anropet tills det intervall mellan bildrutorna som registrerades vid skrivningen har förlöpt, så att uppspelningens bildfrekvens matchar den ursprungliga infångningsfrekvensen; pause=False returnerar omedelbart, vilket är användbart för analyspipelines som vill arbeta sig igenom inspelningen så snabbt som möjligt utan att respektera den ursprungliga tidsstyrningen.

Samma loopmönster fungerar för minnesströmmar förutom att loop ignoreras – att läsa förbi slutet av en minnesström ger upphov till EOFError. Det förväntade mönstret för en minnesring är att uttryckligen göra seek() tillbaka till noll när omlindning önskas.

5.33.5. Inspelningar som kan spelas upp på en värd

ImageIO-strömmar är rätt verktyg när inspelningen ska spelas upp på kameran – de bevarar varje infångad bildruta i dess nativa pixelformat, intervallet mellan bildrutorna registreras exakt, och ett nedströmsskript kan stega igenom dem, söka och omanalysera utan förlust. De är dock inte rätt verktyg när inspelningen måste kunna spelas upp på en värd – en arbetsstation, en telefon, en webbspelare. En värd förväntar sig en standardiserad videocontainer, inte OpenMV:s format med magisk header på disk.

Två separata moduler täcker fallet med uppspelning på en värd. Modulen mjpeg spelar in Motion JPEG: en sekvens av JPEG-komprimerade bildrutor packade i en enda container i AVI-stil som VLC, QuickTime, ffmpeg och standardtaggen för webbvideo alla spelar upp direkt. Modulen gif spelar in en animerad GIF: en sekvens av okomprimerade (eller paletkomprimerade) bildrutor med uttryckliga fördröjningar per bildruta, som kan spelas upp i vilken webbläsare eller bildvisare som helst som hanterar animerade GIF-bilder.

Modulen mjpeg är det naturliga valet för långa inspelningar. JPEG-komprimering håller filstorleken hanterbar – jämförbar med to_jpeg() vid den konfigurerade kvaliteten, bildruta efter bildruta – så att en utdragen infångningssession håller sig inom SD-kortets budget. Användningen speglar inspelning med ImageIO nära:

import mjpeg

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

mjpeg.Mjpeg accepterar samma positions- och skalnyckelord i ritstil som andra bildmetoder tar, så att en inspelning kan skalas, beskäras eller paletmappas per bildruta på vägen in. Konstruktorns argument width och height har som standardvärde huvudbildbuffertens dimensioner och fastställer utdataupplösningen; varje tillagd bildruta skalas (med bibehållen bildförhållande) för att passa. sync() spolar filen till disk under en lång inspelning, och close() slutför containern – en Motion JPEG-fil som inte stängts korrekt kan inte spelas upp, så disciplinen är viktig.

Modulen gif är det naturliga valet för korta inspelningar som delas ordagrant med en icke-teknisk tittare – några sekunders skeende infångat för en demo, en animerad illustration för dokumentation, ett händelseklipp inbäddat i ett chattmeddelande. GIF-bildrutor lagras okomprimerade (eller paletkomprimerade med 7-bitars färgdjup), vilket gör filerna mycket större per sekund än Motion JPEG och utesluter formatet för inspelningar längre än några sekunder, men resultatet kan släppas direkt i vilken webbläsare som helst:

import gif

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

Argumentet delayadd_frame() är visningstiden per bildruta i centisekunder (10 är 100 ms per bildruta, eller 10 fps), vilket är standardstyrningen för GIF-uppspelning. Konstruktorns nyckelord loop anger om det resulterande klippet loopar automatiskt i visare (standardvärdet är True, vilket matchar den konventionella förväntningen på en ”animerad GIF”).

De tre inspelningsvägarna täcker tillsammans de vanliga fallen: ImageIO för bearbetning på kameran, Motion JPEG för långa inspelningar som spelas upp på en värd, animerad GIF för korta klipp som spelas upp på en värd. Valet mellan dem handlar om vem som spelar upp inspelningen. Ett nedströmssteg som körs på själva kameran läser ImageIO; en värdarbetsstation eller webbvisare läser MJPEG eller GIF.

5.33.6. Ett utlös-och-spela-upp-mönster

Ett användbart mönster kombinerar en minnesström med ett utlösningsvillkor. Kameran spelar in kontinuerligt till en minnesringbuffert med count platser och skriver över den äldsta platsen varje varv. När ett utlösningsvillkor inträffar (en blob kommer in i bildrutan, en rörelsehändelse överskrider tröskelvärdet, en knapp trycks in) tar applikationen en stillbild av ringens innehåll – de senaste count bildrutorna – och skriver dem till en filström på SD-kortet. Resultatet är en inspelning före utlösning som fångar sekunderna före den händelse som kameran faktiskt lade märke till, inte bara sekunderna efter, vilket är den klassiska begränsningen hos en naiv ”fånga-vid-utlösning”-inspelare.

Implementeringen är okomplicerad när väl strömklasserna finns till hands: en minnesström med fast storlek fungerar som ringen (med uttrycklig seek() till noll när offseten når antalet platser), huvudloopen fångar in i den vid varje iteration, och utlösningshanteraren läser ut minnesströmmen bildruta för bildruta och skriver varje till en filström namngiven efter tidsstämpeln för utlösningen.