5.33. ImageIO streamy

save() a to_jpeg() pokrývají případ I/O jednoho snímku: aplikace zachytí snímek, zakóduje jej a někam jej odešle. Jiná třída aplikací potřebuje případ sekvence: zaznamenat mnoho snímků po sobě v přirozené rychlosti zachytávání, uložit je tam, odkud je lze později získat, a přehrát je správnou rychlostí. Skript pro sběr trénovacích dat zachytí několik set ukázkových snímků pro pipeline strojového učení; protokol inspekční stanice zaznamenává každý zachycený díl kvůli sledovatelnosti; vývojový skript přehrává uloženou sekvenci, aby otestoval nový algoritmus proti datům, která byla dříve zachycena živě.

Třída ImageIO je rekordér / přehrávač modulu image. Jeden stream uchovává sekvenci snímků Image – případně různých velikostí a pixelových formátů – spolu s intervalem mezi snímky u každého z nich, takže přehrávání může znovu vytvořit původní snímkovou frekvenci. K dispozici jsou dvě úložiště: soubor na souborovém systému nebo buffer pevné velikosti v RAM.

5.33.1. Dvě úložiště

Souborový stream uchovává záznam i po vypnutí napájení a jeho velikost je omezena pouze úložištěm, na kterém je uložen. Začíná 16bajtovou magickou hlavičkou OMV IMG STR Vx.y, za níž následuje jeden chunk na snímek; aktuální zapisovač generuje V2.0 a čtečka kvůli zpětné kompatibilitě stále přijímá soubory V1.0 a V1.1. Cesta k souboru je argumentem konstruktoru; režim je režim otevření souboru ('r' pro čtení existujícího streamu, 'w' pro oříznutí a nový zápis).

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

Paměťový stream žije v RAM bufferu alokovaném při konstrukci. Konstruktor přijímá místo cesty 3-tici (w, h, pixformat) a argument mode se stává předem alokovaným počtem slotů pro snímky. Buffer je dimenzován přesně pro tento počet snímků při zadaných rozměrech a po alokaci nesmí růst – zápis za poslední slot vyvolá EOFError a zápis snímku většího než buffer slotu vyvolá ValueError. Paměťové streamy jsou tím správným nástrojem, když aplikace potřebuje předat záznam navazujícímu stupni bez procházení souborovým systémem (například krátký kruhový buffer nedávných snímků pro vzor trigger-a-přehrání).

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

U komprimovaných pixelových formátů (image.JPEG, image.PNG) je velikost na slot odhadována na 2 bity na pixel; zakódovaný snímek větší než odhad vyvolá při zápisu ValueError, takže aplikace, která očekává ukládání vysoce kvalitních JPEGů, musí buď naddimenzovat počet slotů, nebo nejprve zakódovat s nižší kvalitou.

type() vrací image.ImageIO.FILE_STREAM nebo image.ImageIO.MEMORY_STREAM, takže se navazující kód může přizpůsobit tomu, které úložiště dostal.

5.33.2. Záznam

write() připojí zachycený Image k souborovému streamu (nebo jej uloží do aktuálního slotu paměťového streamu) a posune offset o jedničku. Stejné volání zaznamená interval mezi snímky od posledního zápisu, takže přehrávací část může mezi snímky pozastavit na správnou dobu a přirozená snímková frekvence záznamu je zachována.

V rámci jediného souborového streamu jsou povoleny heterogenní snímky: záznam může volně mísit zachycení RGB565, oříznutí ve stupních šedi a JPEG zakódované náhledy a čtečka každý z nich dekóduje v jeho původní velikosti a formátu. Paměťové streamy jsou homogenní (všechny sloty sdílejí (w, h, pixformat) zadané konstruktorem), takže paměťový záznam je omezen na jednu konfiguraci snímku.

write() vrací objekt streamu, takže lze volání řetězit. Zápis na offset jiný než koncový u souborového streamu ořízne zbytek souboru – užitečné pro úpravu uložené sekvence, riskantní, pokud byla pozice následujícího zápisu neúmyslně posunuta dřívějším seek().

sync() u souborových streamů vyprázdní čekající zápisy na disk (u paměťových streamů je to no-op) a měla by být volána periodicky, když je záznam dlouhotrvající, aby se předešlo ztrátě konce záznamu, pokud se kamera restartuje dříve, než je soubor uzavřen. Destruktor stream automaticky uzavře, když se ImageIO dostane mimo rozsah platnosti, ale explicitní close() je tou správnou disciplínou.

5.33.3. Přehrávání

read() přečte snímek na aktuálním offsetu, posune offset a vrátí nový Image. Příjemce zůstává v snímkovém bufferu (frame buffer), když copy_to_fb=True (výchozí), takže vrácený obraz je vykreslitelný v náhledu IDE; s copy_to_fb=False snímek skončí na haldě MicroPythonu.

# 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

Chování přehrávání řídí dvě klíčová slova. loop=True (výchozí pro souborové streamy) obtočí ukazatel čtení zpět na začátek, když je dosaženo konce záznamu, takže volání nikdy nevrátí None; loop=False vrátí None, jakmile je záznam vyčerpán, a smyčka volajícího skončí. pause=True (výchozí) zablokuje volání, dokud neuplyne interval mezi snímky zaznamenaný v době zápisu, takže snímková frekvence přehrávání odpovídá původní snímkové frekvenci zachytávání; pause=False se vrátí okamžitě, což je užitečné pro analytické pipeline, které chtějí záznam zpracovat co nejrychleji bez respektování původního časování.

Stejný vzor smyčky funguje pro paměťové streamy s tím rozdílem, že loop je ignorováno – čtení za konec paměťového streamu vyvolá EOFError. Očekávaný vzor pro paměťový kruh je explicitně použít seek() zpět na nulu, když je požadováno obtočení.

5.33.5. Záznamy přehratelné na hostiteli

ImageIO streamy jsou tím správným nástrojem, když se záznam bude přehrávat na kameře – zachovávají každý zachycený snímek v jeho nativním pixelovém formátu, interval mezi snímky je zaznamenán přesně a navazující skript jimi může procházet, seekovat a znovu analyzovat bez ztráty. Nejsou však tím správným nástrojem, když má být záznam přehratelný na hostiteli – pracovní stanici, telefonu, webovém přehrávači. Hostitel očekává standardní video kontejner, nikoli OpenMV diskový formát s magickou hlavičkou.

Případ přehratelnosti na hostiteli pokrývají dva samostatné moduly. Modul mjpeg zaznamenává Motion JPEG: sekvenci JPEG komprimovaných snímků zabalených do jediného kontejneru ve stylu AVI, který VLC, QuickTime, ffmpeg i standardní webový video tag přehrávají přímo. Modul gif zaznamenává animovaný GIF: sekvenci nekomprimovaných (nebo paletově komprimovaných) snímků s explicitními zpožděními u jednotlivých snímků, přehratelnou v jakémkoli webovém prohlížeči nebo prohlížeči obrázků, který zvládá animované GIFy.

Modul mjpeg je přirozenou volbou pro dlouhé záznamy. JPEG komprese udržuje velikost souboru zvladatelnou – srovnatelnou s to_jpeg() při nastavené kvalitě, snímek za snímkem – takže prodloužená relace zachytávání zůstává v rámci kapacity SD karty. Použití úzce odpovídá záznamu pomocí ImageIO:

import mjpeg

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

mjpeg.Mjpeg přijímá stejná poziční a měřítková klíčová slova ve stylu kreslení jako ostatní metody image, takže záznam může být u každého snímku při vstupu škálován, oříznut nebo paletově mapován. Argumenty konstruktoru width a height mají výchozí hodnotu odpovídající rozměrům hlavního snímkového bufferu (frame buffer) a pevně určují výstupní rozlišení; každý připojený snímek je škálován (se zachováním poměru stran) tak, aby se vešel. sync() vyprázdní soubor na disk během dlouhého záznamu a close() finalizuje kontejner – Motion JPEG soubor, který nebyl řádně uzavřen, není přehratelný, takže na disciplíně záleží.

Modul gif je přirozenou volbou pro krátké záznamy sdílené doslovně s netechnickým divákem – pár sekund akce zachycených pro ukázku, animovaná ilustrace do dokumentace, klip události vložený do chatové zprávy. Snímky GIFu jsou ukládány nekomprimovaně (nebo paletově komprimovaně při 7bitové barevné hloubce), což činí soubory mnohem většími za sekundu než Motion JPEG a vylučuje formát z použití pro záznamy delší než pár sekund, ale výsledek se vloží přímo do jakéhokoli prohlížeče:

import gif

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

Argument delay u add_frame() je doba zobrazení jednoho snímku v setinách sekundy (10 je 100 ms na snímek neboli 10 fps), což je standardní řízení přehrávání GIFu. Klíčové slovo loop konstruktoru nastavuje, zda se výsledný klip v prohlížečích automaticky zacyklí (výchozí je True, což odpovídá konvenčnímu očekávání „animovaného GIFu“).

Tři cesty záznamu mezi sebou pokrývají běžné případy: ImageIO pro opětovné zpracování na kameře, Motion JPEG pro dlouhé záznamy přehratelné na hostiteli, animovaný GIF pro krátké klipy přehratelné na hostiteli. Volba mezi nimi se odvíjí od toho, kdo záznam přehrává. Navazující stupeň běžící na samotné kameře čte ImageIO; hostitelská pracovní stanice nebo webový prohlížeč čte MJPEG nebo GIF.

5.33.6. Vzor trigger-a-přehrání

Užitečný vzor kombinuje paměťový stream s podmínkou triggeru. Kamera nepřetržitě nahrává do paměťového kruhového bufferu s count sloty a při každém obtočení přepisuje nejstarší slot. Když se aktivuje podmínka triggeru (do snímku vstoupí blob, událost pohybu překročí práh, je stisknuto tlačítko), aplikace zachytí obsah kruhu – nejnovějších count snímků – a zapíše je do souborového streamu na SD kartě. Výsledkem je záznam před triggerem, který zachycuje sekundy před událostí, kterou kamera skutečně zaznamenala, nikoli pouze sekundy po ní, což je klasické omezení naivního rekordéru typu „zachytit-při-triggeru“.

Implementace je přímočará, jakmile máte třídy streamů k dispozici: paměťový stream pevné velikosti slouží jako kruh (s explicitním seek() na nulu, když offset dosáhne počtu slotů), hlavní smyčka do něj zachytává při každé iteraci a obsluha triggeru čte paměťový stream snímek po snímku a každý zapisuje do souborového streamu pojmenovaného podle časové značky triggeru.