5.33. ImageIO adatfolyamok

save() és to_jpeg() az egyetlen képkockás I/O esetet fedi le: az alkalmazás rögzít egy képkockát, kódolja, majd elküldi valahová. Az alkalmazások egy másik osztálya a szekvencia esetet igényli: sok képkocka egymás utáni rögzítése a természetes rögzítési sebességen, eltárolásuk egy olyan helyen, ahonnan később visszanyerhetők, és lejátszásuk a megfelelő sebességen. Egy tanítóadat-gyűjtő szkript néhány száz példa-képkockát rögzít egy gépi tanulási folyamathoz; egy ellenőrzőállomás naplója minden rögzített alkatrészt feljegyez a nyomonkövethetőség érdekében; egy fejlesztői szkript egy tárolt szekvenciát játszik vissza, hogy egy új algoritmust korábban élőben rögzített adatokkal teszteljen.

Az ImageIO osztály az image modul felvevője/lejátszója. Egyetlen adatfolyam Image képkockák egy szekvenciáját tartja – amelyek esetleg különböző méretűek és képpontformátumúak lehetnek – mindegyikhez tartozó képkockák közötti intervallummal együtt, így a lejátszás újra létrehozhatja az eredeti képkockasebességet. Két háttértár áll rendelkezésre: egy fájl a fájlrendszeren vagy egy fix méretű puffer a RAM-ban.

5.33.1. A két háttértár

Egy fájl-adatfolyam a felvételt áramkimaradásokon át megőrzi, és méretét csak a mögötte lévő tárhely korlátozza. Egy 16 bájtos varázsfejléccel kezdődik (OMV IMG STR Vx.y), amelyet képkockánként egy adatdarab követ; a jelenlegi író V2.0 formátumot bocsát ki, az olvasó pedig visszafelé kompatibilitásból még elfogadja a V1.0 és V1.1 fájlokat. A fájl elérési útja a konstruktor argumentuma; a mód a fájlmegnyitási mód ('r' egy meglévő adatfolyam olvasásához, 'w' a csonkoláshoz és új írásához).

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

Egy memória-adatfolyam egy konstrukciókor lefoglalt RAM-pufferben él. A konstruktor egy elérési út helyett egy (w, h, pixformat) 3-elemű rendezett n-est vesz át, a mode argumentum pedig az előre lefoglalt képkockahelyek számává válik. A puffer pontosan annyi képkockára van méretezve a megadott méretekkel, és lefoglalás után nem növekedhet – az utolsó helyen túli írás EOFError kivételt vált ki, a helyenkénti puffernél nagyobb képkocka írása pedig ValueError kivételt. A memória-adatfolyamok a megfelelő eszközök, amikor az alkalmazásnak egy felvételt kell átadnia egy alsóbb fázisnak a fájlrendszer megkerülésével (például egy rövid gyűrűs puffer a legutóbbi képkockákból egy esemény-kiváltás-és-visszajátszás mintázathoz).

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

A tömörített képpontformátumok (image.JPEG, image.PNG) esetén a helyenkénti méret képpontonként 2 bittel van becsülve; a becslésnél nagyobb kódolt képkocka íráskor ValueError kivételt vált ki, így egy olyan alkalmazásnak, amely jó minőségű JPEG-ek tárolására számít, vagy túl kell méreteznie a helyek számát, vagy előbb alacsonyabb minőségen kell kódolnia.

Az type() az image.ImageIO.FILE_STREAM vagy az image.ImageIO.MEMORY_STREAM értéket adja vissza, így az alsóbb kód alkalmazkodhat ahhoz, hogy melyik háttértárat kapta.

5.33.2. Felvétel

Az write() hozzáfűz egy rögzített Image képet egy fájl-adatfolyamhoz (vagy egy memória-adatfolyam aktuális helyén tárolja), és eggyel előrelépteti az eltolást. Ugyanez a hívás rögzíti az utolsó írás óta eltelt képkockák közötti intervallumot is, így a lejátszási oldal a megfelelő ideig szüneteltethet a képkockák között, és a felvétel természetes képkockasebessége megőrződik.

Egyetlen fájl-adatfolyamon belül megengedettek a heterogén képkockák: egy felvétel szabadon keverhet RGB565 felvételeket, szürkeárnyalatos kivágatokat és JPEG-kódolt bélyegképeket, az olvasó pedig mindegyiket az eredeti méretén és formátumában dekódolja. A memória-adatfolyamok homogének (minden hely osztozik a konstruktorban megadott (w, h, pixformat) értékeken), így egy memóriafelvétel egyetlen képkocka-konfigurációra korlátozott.

Az write() visszaadja az adatfolyam-objektumot, így a hívások láncolhatók. Egy fájl-adatfolyam nem-vég eltolásánál való írás levágja a fájl maradékát – ez hasznos egy tárolt szekvencia szerkesztéséhez, de kockázatos, ha a következő-írás pozícióját egy korábbi seek() szándékolatlanul elmozdította.

Az sync() a függőben lévő írásokat a lemezre üríti a fájl-adatfolyamok esetén (a memória-adatfolyamoknál nincs hatása), és időnként meg kell hívni, ha a felvétel hosszan tartó, hogy elkerüljük a felvétel végének elvesztését, ha a kamera a fájl bezárása előtt újraindul. A destruktor automatikusan bezárja az adatfolyamot, amikor az ImageIO kikerül az érvényességi köréből, de a kifejezett close() a helyes fegyelem.

5.33.3. Lejátszás

Az read() beolvassa az aktuális eltolásnál lévő képkockát, előrelépteti az eltolást, és visszaadja az új Image képet. A fogadott kép a képkocka-pufferben marad, ha copy_to_fb=True (az alapértelmezett), így a visszaadott kép rajzolható az IDE előnézetén keresztül; copy_to_fb=False esetén a képkocka a MicroPython heapen landol.

# 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

Két kulcsszó vezérli a lejátszási viselkedést. A loop=True (a fájl-adatfolyamok alapértelmezése) az olvasómutatót visszacsavarja az elejére, amikor a felvétel végéhez ér, így a hívás soha nem ad vissza None értéket; a loop=False None értéket ad vissza, amint a felvétel kimerül, és a hívó ciklusa befejeződik. A pause=True (az alapértelmezett) blokkolja a hívást, amíg az íráskor rögzített képkockák közötti intervallum el nem telik, így a lejátszási képkockasebesség megegyezik az eredeti rögzítési képkockasebességgel; a pause=False azonnal visszatér, ami hasznos azoknál az elemzési folyamatoknál, amelyek a lehető leggyorsabban szeretnék végigrágni a felvételt az eredeti időzítés figyelembevétele nélkül.

Ugyanaz a ciklusmintázat működik a memória-adatfolyamoknál is, azzal a kivétellel, hogy a loop figyelmen kívül marad – egy memória-adatfolyam végén túli olvasás EOFError kivételt vált ki. A memóriagyűrűnél elvárt mintázat az, hogy kifejezetten seek() hívással térjünk vissza a nullára, amikor körbefordulásra van szükség.

5.33.5. Gazdagépen lejátszható felvételek

Az ImageIO adatfolyamok a megfelelő eszközök, amikor a felvételt a kamerán fogjuk visszajátszani – minden rögzített képkockát natív képpontformátumában őriznek meg, a képkockák közötti intervallumot pontosan rögzítik, és egy alsóbb szkript veszteség nélkül léptetheti, keresheti és újraelemezheti őket. Nem ezek a megfelelő eszközök azonban, amikor a felvételnek egy gazdagépen kell lejátszhatónak lennie – egy munkaállomáson, egy telefonon, egy webes lejátszóban. Egy gazdagép egy szabványos videótárolót vár, nem pedig az OpenMV lemezen lévő varázsfejléces formátumát.

Két különálló modul fedi le a gazdagépen lejátszható esetet. A mjpeg modul Motion JPEG-et rögzít: JPEG-tömörített képkockák szekvenciáját egyetlen AVI-stílusú tárolóba csomagolva, amelyet a VLC, a QuickTime, az ffmpeg és a szabványos webes videocímke is közvetlenül lejátszik. A gif modul animált GIF-et rögzít: tömörítetlen (vagy palettatömörített) képkockák szekvenciáját kifejezett képkockánkénti késleltetésekkel, amely bármely olyan webböngészőben vagy képnézegetőben lejátszható, amely kezeli az animált GIF-eket.

A mjpeg modul a természetes választás hosszú felvételekhez. A JPEG-tömörítés kezelhető szinten tartja a fájlméretet – a beállított minőségen képkockáról képkockára összemérhető az to_jpeg() eredményével – így egy elnyúló rögzítési munkamenet az SD-kártya keretein belül marad. A használat szorosan tükrözi az ImageIO felvételét:

import mjpeg

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

A mjpeg.Mjpeg ugyanazokat a rajzolási stílusú pozicionális és skálázási kulcsszavakat fogadja el, mint más image metódusok, így egy felvétel beérkezéskor képkockánként skálázható, kivágható vagy palettához rendelhető. A konstruktor width és height argumentumai alapértelmezetten a fő képkocka-puffer méreteit veszik fel, és rögzítik a kimeneti felbontást; minden hozzáfűzött képkocka (a képarányt megőrizve) az illeszkedéshez van skálázva. A sync() egy hosszú felvétel közben a lemezre üríti a fájlt, a close() pedig véglegesíti a tárolót – egy nem tisztán lezárt Motion JPEG fájl nem lejátszható, így a fegyelem számít.

A gif modul a természetes választás rövid felvételekhez, amelyeket szó szerint megosztanak egy nem műszaki nézővel – néhány másodpercnyi cselekvés rögzítve egy bemutatóhoz, egy animált illusztráció a dokumentációhoz, egy eseményklip beágyazva egy csevegőüzenetbe. A GIF-képkockák tömörítetlenül (vagy 7 bites színmélységen palettatömörítve) tárolódnak, ami a fájlokat másodpercenként sokkal nagyobbá teszi a Motion JPEG-nél, és kizárja a formátumot a néhány másodpercnél hosszabb felvételeknél, de az eredmény közvetlenül bármely böngészőbe helyezhető:

import gif

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

A delay argumentum a add_frame() metóduson a képkockánkénti megjelenítési idő századmásodpercben (10 az 100 ms képkockánként, azaz 10 fps), ami a szabványos GIF-lejátszási vezérlés. A konstruktor loop kulcsszava beállítja, hogy a kapott klip automatikusan ismétlődjön-e a nézegetőkben (az alapértelmezés True, ami megfelel a megszokott „animált GIF” elvárásnak).

A három felvételi útvonal együttesen lefedi a gyakori eseteket: ImageIO a kamerán történő újrafeldolgozáshoz, Motion JPEG a hosszú, gazdagépen lejátszható felvételekhez, animált GIF a rövid, gazdagépen lejátszható klipekhez. A köztük lévő választás azon múlik, hogy ki játssza vissza a felvételt. Egy magán a kamerán futó alsóbb fázis ImageIO-t olvas; egy gazdagép-munkaállomás vagy webes nézegető MJPEG-et vagy GIF-et olvas.

5.33.6. Esemény-kiváltás-és-visszajátszás mintázat

Egy hasznos mintázat egy memória-adatfolyamot kombinál egy kiváltási feltétellel. A kamera folyamatosan rögzít egy count helyes memóriagyűrűs pufferbe, minden körben felülírva a legrégebbi helyet. Amikor egy kiváltási feltétel teljesül (egy folt belép a képkockába, egy mozgásesemény meghaladja a küszöbértéket, megnyomnak egy gombot), az alkalmazás pillanatképet készít a gyűrű tartalmáról – a legutóbbi count képkockáról – és kiírja őket egy fájl-adatfolyamba az SD-kártyán. Az eredmény egy kiváltás előtti felvétel, amely megörökíti az eseményt megelőző másodperceket, amelyet a kamera ténylegesen észrevett, nem csak az utána következő másodperceket, ami a naiv „rögzítés-kiváltáskor” felvevő klasszikus korlátja.

A megvalósítás egyszerű, amint az adatfolyam-osztályok rendelkezésre állnak: egy fix méretű memória-adatfolyam szolgál gyűrűként (kifejezett seek() hívással a nullára, amikor az eltolás eléri a helyek számát), a fő ciklus minden iterációban belerögzít, a kiváltáskezelő pedig képkockáról képkockára olvassa ki a memória-adatfolyamot, és mindegyiket egy, a kiváltás időbélyegéről elnevezett fájl-adatfolyamba írja.