5.33. ImageIO-streamit

save() ja to_jpeg() kattavat yhden kehyksen I/O-tapauksen: sovellus kaappaa kehyksen, koodaa sen ja työntää sen jonnekin. Toisenlainen sovellusluokka tarvitsee sekvenssi-tapauksen: tallennetaan monta kehystä peräkkäin luonnollisella kaappausnopeudella, säilytetään ne paikassa josta ne voidaan myöhemmin hakea, ja toistetaan ne oikealla nopeudella. Opetusaineiston keräysskripti kaappaa muutaman sadan esimerkkikehyksen koneoppimisputkea varten; tarkastusaseman loki tallentaa jokaisen kaapatun osan jäljitettävyyttä varten; kehitysskripti toistaa tallennetun sekvenssin testatakseen uutta algoritmia aineistolla joka aiemmin kaapattiin reaaliaikaisesti.

ImageIO -luokka on image-moduulin tallennin / toistin. Yksi streami sisältää sekvenssin Image -kehyksiä – mahdollisesti erikokoisia ja eri pikseliformaateissa – yhdessä kunkin kehysten välisen aikavälin kanssa, jotta toisto voi luoda alkuperäisen kehysnopeuden uudelleen. Käytettävissä on kaksi taustasäilöä: tiedosto tiedostojärjestelmässä tai kiinteäkokoinen puskuri RAM-muistissa.

5.33.1. Kaksi taustasäilöä

Tiedostostreami säilyttää tallenteen virrankatkaisujen yli, ja sen kokoa rajoittaa vain sitä tukeva tallennustila. Se alkaa 16-tavuisella maagisella otsikolla OMV IMG STR Vx.y, jota seuraa yksi lohko kutakin kehystä kohden; nykyinen kirjoittaja tuottaa version V2.0, ja lukija hyväksyy yhä V1.0- ja V1.1-tiedostot taaksepäin yhteensopivuuden vuoksi. Tiedostopolku on konstruktorin argumentti; tila on tiedoston avaustila ('r' olemassa olevan streamin lukemiseen, 'w' tyhjentämiseen ja uudelleenkirjoittamiseen).

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

Muististreami sijaitsee RAM-puskurissa, joka varataan rakennusvaiheessa. Konstruktori ottaa polun sijaan (w, h, pixformat) -kolmikon, ja mode-argumentista tulee esivarattujen kehyspaikkojen lukumäärä. Puskuri mitoitetaan täsmälleen tuolle määrälle kehyksiä annetuilla mitoilla, eikä sen sallita kasvaa varauksen jälkeen – kirjoittaminen viimeisen paikan yli nostaa EOFError-virheen, ja paikkakohtaista puskuria suuremman kehyksen kirjoittaminen nostaa ValueError-virheen. Muististreamit ovat oikea työkalu kun sovelluksen täytyy luovuttaa tallenne jatkokäsittelyvaiheelle ilman tiedostojärjestelmän kautta kulkemista (esimerkiksi lyhyt rengaspuskuri viimeaikaisista kehyksistä laukaise-ja-toista -mallia varten).

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

Pakatuille pikseliformaateille (image.JPEG, image.PNG) paikkakohtainen koko arvioidaan 2 bitiksi pikseliä kohden; arviota suuremman koodatun kehyksen kirjoittaminen nostaa ValueError-virheen kirjoitushetkellä, joten sovelluksen joka aikoo tallentaa korkealaatuisia JPEG-kuvia täytyy joko ylivarata paikkamäärä tai koodata ensin matalammalla laadulla.

type() palauttaa arvon image.ImageIO.FILE_STREAM tai image.ImageIO.MEMORY_STREAM, jotta jatkokäsittelykoodi voi mukautua siihen taustasäilöön joka sille annetaan.

5.33.2. Tallentaminen

write() lisää kaapatun Image -kuvan tiedostostreamiin (tai tallentaa sen muististreamin nykyiseen paikkaan) ja siirtää siirtymää yhdellä eteenpäin. Sama kutsu tallentaa kehysten välisen aikavälin edellisestä kirjoituksesta, jotta toistopuoli voi tauottaa oikean ajan kehysten välillä ja tallenteen luonnollinen kehysnopeus säilyy.

Yhden tiedostostreamin sisällä sallitaan heterogeeniset kehykset: tallenne voi vapaasti sekoittaa RGB565-kaappauksia, harmaasävyrajauksia ja JPEG-koodattuja pikkukuvia, ja lukija dekoodaa kunkin alkuperäisessä koossaan ja formaatissaan. Muististreamit ovat homogeenisia (kaikki paikat jakavat konstruktorin antaman (w, h, pixformat) -kolmikon), joten muistitallenne on rajoitettu yhteen kehyskonfiguraatioon.

write() palauttaa streamiobjektin, joten kutsut voidaan ketjuttaa. Kirjoittaminen tiedostostreamin ei-loppukohtaan typistää loput tiedostosta – hyödyllistä tallennetun sekvenssin muokkaamiseen, riskialtista jos seuraavan kirjoituksen sijaintia siirrettiin tahattomasti aiemmalla seek() -kutsulla.

sync() huuhtelee odottavat kirjoitukset levylle tiedostostreameille (muististreameilla se ei tee mitään) ja sitä tulisi kutsua ajoittain pitkäkestoisen tallennuksen aikana, jotta tallenteen loppuosaa ei menetetä mikäli kamera käynnistyy uudelleen ennen tiedoston sulkemista. Destruktori sulkee streamin automaattisesti kun ImageIO poistuu näkyvyysalueelta, mutta eksplisiittinen close() on oikea käytäntö.

5.33.3. Toisto

read() lukee nykyisessä siirtymässä olevan kehyksen, siirtää siirtymää eteenpäin ja palauttaa uuden Image -kuvan. Vastaanottaja jää kehyspuskuriin kun copy_to_fb=True (oletus), joten palautettu kuva on piirrettävissä IDE:n esikatselun kautta; arvolla copy_to_fb=False kehys päätyy MicroPython-keolle.

# 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

Kaksi avainsanaa ohjaa toiston käyttäytymistä. loop=True (oletus tiedostostreameille) kietoo lukuosoittimen takaisin alkuun kun tallenteen loppu saavutetaan, joten kutsu ei koskaan palauta None-arvoa; loop=False palauttaa None-arvon kun tallenne on käytetty loppuun ja kutsujan silmukka päättyy. pause=True (oletus) estää kutsun kunnes kirjoitushetkellä tallennettu kehysten välinen aikaväli on kulunut, joten toiston kehysnopeus vastaa alkuperäistä kaappausnopeutta; pause=False palaa välittömästi, mikä on hyödyllistä analyysiputkille jotka haluavat käydä tallenteen läpi mahdollisimman nopeasti välittämättä alkuperäisestä ajoituksesta.

Sama silmukkamalli toimii muististreameille paitsi että loop jätetään huomiotta – muististreamin lopun yli lukeminen nostaa EOFError-virheen. Odotettu malli muistirenkaalle on kutsua seek() eksplisiittisesti takaisin nollaan kun kiertäminen halutaan.

5.33.5. Isäntälaitteella toistettavat tallenteet

ImageIO-streamit ovat oikea työkalu kun tallenne aiotaan toistaa kameralla – ne säilyttävät jokaisen kaapatun kehyksen sen natiivissa pikseliformaatissa, kehysten välinen aikaväli tallennetaan täsmällisesti, ja jatkokäsittelyskripti voi käydä ne läpi askel kerrallaan, hakea ja analysoida uudelleen ilman häviötä. Ne eivät kuitenkaan ole oikea työkalu kun tallenteen täytyy olla toistettavissa isäntälaitteella – työasemalla, puhelimella, web-soittimessa. Isäntälaite odottaa standardia videosäiliötä, ei OpenMV:n levymuotoista maagisen otsikon formaattia.

Kaksi erillistä moduulia kattaa isäntälaitteella toistettavan tapauksen. mjpeg -moduuli tallentaa Motion JPEG -muotoa: sekvenssin JPEG-pakattuja kehyksiä yhteen AVI-tyyliseen säiliöön pakattuna, jonka VLC, QuickTime, ffmpeg ja standardi web-videotunniste kaikki toistavat suoraan. gif -moduuli tallentaa animoidun GIF:n: sekvenssin pakkaamattomia (tai palettipakattuja) kehyksiä eksplisiittisin kehyskohtaisin viivein, toistettavissa missä tahansa web-selaimessa tai kuvankatseluohjelmassa joka käsittelee animoituja GIF-kuvia.

mjpeg -moduuli on luonnollinen valinta pitkiin tallenteisiin. JPEG-pakkaus pitää tiedostokoon hallittavana – verrattavissa to_jpeg() -metodiin määritetyllä laadulla, kehys kehyksen jälkeen – joten pitkitetty kaappausistunto pysyy SD-kortin budjetin sisällä. Käyttö heijastelee tarkasti ImageIO -tallennusta:

import mjpeg

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

mjpeg.Mjpeg hyväksyy samat piirtotyyliset sijainti- ja skaalausavainsanat jotka muutkin image-metodit ottavat, joten tallennetta voidaan skaalata, rajata tai palettikartoittaa kehyskohtaisesti sisääntulon yhteydessä. Konstruktorin width- ja height-argumentit oletusarvoistuvat pääkehyspuskurin mittoihin ja kiinnittävät ulostuloresoluution; jokainen lisätty kehys skaalataan (kuvasuhde säilyttäen) sopimaan. sync() huuhtelee tiedoston levylle pitkän tallennuksen aikana, ja close() viimeistelee säiliön – Motion JPEG -tiedosto jota ei ole suljettu siististi ei ole toistettavissa, joten käytännöllä on merkitystä.

gif -moduuli on luonnollinen valinta lyhyisiin tallenteisiin jotka jaetaan sellaisinaan ei-tekniselle katsojalle – muutama sekunti toimintaa kaapattuna demoa varten, animoitu havainnekuva dokumentaatiota varten, tapahtumaleike upotettuna chat-viestiin. GIF-kehykset tallennetaan pakkaamattomina (tai palettipakattuina 7-bittisellä värisyvyydellä), mikä tekee tiedostoista paljon suurempia sekuntia kohden kuin Motion JPEG ja sulkee formaatin pois muutamaa sekuntia pidemmistä tallenteista, mutta tulos pudotetaan suoraan mihin tahansa selaimeen:

import gif

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

delay-argumentti add_frame() -metodissa on kehyskohtainen näyttöaika senttisekunteina (10 on 100 ms kehystä kohden, eli 10 fps), mikä on standardi GIF:n toiston ohjaus. Konstruktorin loop-avainsana asettaa kiertääkö tuloksena oleva leike automaattisesti katseluohjelmissa (oletus on True, mikä vastaa tavanomaista ”animoitu GIF” -odotusta).

Kolme tallennuspolkua kattavat keskenään yleiset tapaukset: ImageIO kameralla tapahtuvaan uudelleenkäsittelyyn, Motion JPEG pitkiin isäntälaitteella toistettaviin tallenteisiin, animoitu GIF lyhyisiin isäntälaitteella toistettaviin leikkeisiin. Valinta niiden välillä riippuu siitä kuka tallenteen toistaa. Kameralla itsellään ajettava jatkokäsittelyvaihe lukee ImageIO:ta; isäntätyöasema tai web-katselija lukee MJPEG:tä tai GIF:iä.

5.33.6. Laukaise-ja-toista -malli

Hyödyllinen malli yhdistää muististreamin ja laukaisuehdon. Kamera tallentaa jatkuvasti count-paikkaiseen muistirengaspuskuriin, korvaten vanhimman paikan joka kierroksella. Kun laukaisuehto laukeaa (blob saapuu kehykseen, liiketapahtuma ylittää kynnysarvon, painiketta painetaan) sovellus ottaa tilannekuvan renkaan sisällöstä – viimeisimmistä count-kehyksistä – ja kirjoittaa ne tiedostostreamiin SD-kortille. Tuloksena on esilaukaisutallenne joka kaappaa sekunnit ennen tapahtumaa jonka kamera todella huomasi, ei vain sen jälkeisiä sekunteja, mikä on perinteinen rajoite naiivissa ”kaappaa-laukaistaessa” -tallentimessa.

Toteutus on suoraviivainen kun streamiluokat ovat käytössä: kiinteäkokoinen muististreami toimii renkaana (eksplisiittisellä seek() -kutsulla nollaan kun siirtymä saavuttaa paikkamäärän), pääsilmukka kaappaa siihen joka iteraatiolla, ja laukaisukäsittelijä lukee muististreamin ulos kehys kerrallaan ja kirjoittaa kunkin tiedostostreamiin joka on nimetty laukaisun aikaleiman mukaan.