5.33. Fluxuri ImageIO

save() și to_jpeg() acoperă cazul de I/O cu un singur cadru: o aplicație capturează un cadru, îl codifică și îl trimite undeva. O altă categorie de aplicații are nevoie de cazul cu secvențe: înregistrarea mai multor cadre consecutive la rata naturală de captură, stocarea lor undeva de unde pot fi recuperate ulterior și redarea lor la viteza corectă. Un script de colectare a datelor de antrenament capturează câteva sute de cadre exemplu pentru un flux de învățare automată; un jurnal al unei stații de inspecție înregistrează fiecare piesă capturată pentru trasabilitate; un script de dezvoltare reia o secvență stocată pentru a testa un algoritm nou pe date care au fost capturate anterior în direct.

Clasa ImageIO este înregistratorul / player-ul modulului image. Un singur flux conține o secvență de cadre Image – posibil de dimensiuni și formate de pixeli diferite – împreună cu intervalul dintre cadre al fiecăruia, astfel încât redarea să poată recrea rata de cadre originală. Sunt disponibile două spații de stocare de bază: un fișier pe sistemul de fișiere sau un tampon (buffer) de dimensiune fixă în RAM.

5.33.1. Cele două spații de stocare de bază

Un flux de tip fișier păstrează înregistrarea peste cicluri de pornire/oprire și este limitat ca dimensiune doar de stocarea care îl susține. Acesta începe cu un antet magic de 16 octeți OMV IMG STR Vx.y urmat de un bloc per cadru; scriitorul actual emite V2.0, iar cititorul acceptă în continuare fișiere V1.0 și V1.1 pentru compatibilitate retroactivă. Calea fișierului este argumentul constructorului; modul este modul de deschidere a fișierului ('r' pentru a citi un flux existent, 'w' pentru a trunchia și a scrie de la zero).

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

Un flux de memorie trăiește într-un tampon (buffer) RAM alocat la construire. Constructorul primește un 3-tuplu (w, h, pixformat) în loc de o cale, iar argumentul mode devine numărul prealocat de sloturi de cadre. Tamponul (buffer) este dimensionat exact pentru acel număr de cadre la dimensiunile furnizate și nu i se permite să crească odată alocat – scrierea dincolo de ultimul slot ridică EOFError, iar scrierea unui cadru mai mare decât tamponul (buffer) per slot ridică ValueError. Fluxurile de memorie sunt instrumentul potrivit când aplicația trebuie să predea o înregistrare unei etape din aval fără a trece prin sistemul de fișiere (de exemplu, un tampon (buffer) inelar scurt cu cadrele recente pentru un model de tip declanșare-și-redare).

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

Pentru formatele de pixeli comprimate (image.JPEG, image.PNG) dimensiunea per slot este estimată la 2 biți pe pixel; un cadru codificat mai mare decât estimarea ridică ValueError la momentul scrierii, astfel că o aplicație care se așteaptă să stocheze fișiere JPEG de înaltă calitate trebuie fie să supraaloce numărul de sloturi, fie să codifice mai întâi la o calitate inferioară.

type() returnează image.ImageIO.FILE_STREAM sau image.ImageIO.MEMORY_STREAM, astfel încât codul din aval să se poată adapta oricărui spațiu de stocare de bază i s-ar oferi.

5.33.2. Înregistrare

write() adaugă o imagine Image capturată la un flux de tip fișier (sau o stochează la slotul curent al unui flux de memorie) și avansează poziția cu unu. Același apel înregistrează intervalul dintre cadre de la ultima scriere, astfel încât partea de redare să poată face o pauză de durata potrivită între cadre, iar rata naturală de cadre a înregistrării este păstrată.

Cadrele eterogene sunt permise în cadrul unui singur flux de tip fișier: o înregistrare poate amesteca liber capturi RGB565, decupaje în tonuri de gri și miniaturi codificate JPEG, iar cititorul va decoda fiecare la dimensiunea și formatul său original. Fluxurile de memorie sunt omogene (toate sloturile împărtășesc valoarea (w, h, pixformat) furnizată constructorului), așa că o înregistrare în memorie este restricționată la o singură configurație de cadru.

write() returnează obiectul flux, astfel încât apelurile să poată fi înlănțuite. Scrierea la o poziție care nu este la sfârșitul unui flux de tip fișier trunchiază restul fișierului – utilă pentru editarea unei secvențe stocate, riscantă dacă poziția următoarei scrieri a fost mutată neintenționat de un seek() anterior.

sync() golește scrierile în așteptare pe disc pentru fluxurile de tip fișier (este o operație fără efect pe fluxurile de memorie) și ar trebui apelat periodic când înregistrarea durează mult, pentru a evita pierderea cozii înregistrării dacă camera repornește înainte ca fișierul să fie închis. Destructorul închide automat fluxul când ImageIO iese din domeniul de vizibilitate, dar un apel explicit close() este disciplina corectă.

5.33.3. Redare

read() citește cadrul de la poziția curentă, avansează poziția și returnează noua imagine Image. Cadrul primit rămâne în tamponul de cadre (frame buffer) când copy_to_fb=True (valoarea implicită), astfel încât imaginea returnată poate fi desenată prin previzualizarea din IDE; cu copy_to_fb=False cadrul ajunge pe heap-ul MicroPython.

# 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

Două cuvinte-cheie controlează comportamentul redării. loop=True (valoarea implicită pentru fluxurile de tip fișier) readuce indicatorul de citire la început când se atinge sfârșitul înregistrării, astfel încât apelul nu returnează niciodată None; loop=False returnează None odată ce înregistrarea este epuizată, iar bucla apelantului se încheie. pause=True (valoarea implicită) blochează apelul până când a trecut intervalul dintre cadre înregistrat la momentul scrierii, astfel încât rata de cadre la redare să corespundă ratei de cadre originale de la captură; pause=False returnează imediat, util pentru fluxurile de analiză care doresc să parcurgă înregistrarea cât mai repede posibil, fără a respecta sincronizarea originală.

Același model de buclă funcționează pentru fluxurile de memorie, cu excepția faptului că loop este ignorat – citirea dincolo de sfârșitul unui flux de memorie ridică EOFError. Modelul așteptat pentru un inel de memorie este să folosești seek() pentru a reveni explicit la zero când se dorește reluarea.

5.33.5. Înregistrări redabile pe gazdă

Fluxurile ImageIO sunt instrumentul potrivit când înregistrarea urmează să fie redată pe cameră – ele păstrează fiecare cadru capturat în formatul său nativ de pixeli, intervalul dintre cadre este înregistrat exact, iar un script din aval poate parcurge cadrele, căuta și reanaliza fără pierderi. Totuși, nu sunt instrumentul potrivit când înregistrarea trebuie să fie redabilă pe o gazdă – o stație de lucru, un telefon, un player web. O gazdă se așteaptă la un container video standard, nu la formatul OpenMV de pe disc cu antet magic.

Două module separate acoperă cazul redabil pe gazdă. Modulul mjpeg înregistrează Motion JPEG: o secvență de cadre comprimate JPEG împachetate într-un singur container de tip AVI, pe care VLC, QuickTime, ffmpeg și eticheta standard video din web îl redau direct. Modulul gif înregistrează un GIF animat: o secvență de cadre necomprimate (sau comprimate cu paletă) cu întârzieri explicite per cadru, redabil în orice browser web sau vizualizator de imagini care gestionează GIF-uri animate.

Modulul mjpeg este alegerea naturală pentru înregistrările lungi. Compresia JPEG menține dimensiunea fișierului gestionabilă – comparabilă cu to_jpeg() la calitatea configurată, cadru după cadru – astfel încât o sesiune de captură prelungită rămâne în limitele bugetului cardului SD. Utilizarea oglindește îndeaproape înregistrarea cu ImageIO:

import mjpeg

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

mjpeg.Mjpeg acceptă aceleași cuvinte-cheie poziționale și de scalare în stil de desenare pe care le primesc și alte metode din image, astfel încât o înregistrare poate fi scalată, decupată sau mapată cu paletă per cadru pe parcursul intrării. Argumentele width și height ale constructorului iau implicit dimensiunile tamponului de cadre (frame buffer) principal și fixează rezoluția de ieșire; fiecare cadru adăugat este scalat (păstrând raportul de aspect) pentru a se potrivi. sync() golește fișierul pe disc în timpul unei înregistrări lungi, iar close() finalizează containerul – un fișier Motion JPEG care nu a fost închis corect nu poate fi redat, deci disciplina contează.

Modulul gif este alegerea naturală pentru înregistrările scurte partajate ca atare cu un privitor non-tehnic – câteva secunde de acțiune capturate pentru o demonstrație, o ilustrație animată pentru documentație, un clip cu un eveniment încorporat într-un mesaj de chat. Cadrele GIF sunt stocate necomprimate (sau comprimate cu paletă la o adâncime de culoare de 7 biți), ceea ce face fișierele mult mai mari pe secundă decât Motion JPEG și exclude formatul pentru înregistrări mai lungi de câteva secunde, dar rezultatul se inserează direct în orice browser:

import gif

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

Argumentul delay din add_frame() reprezintă timpul de afișare per cadru în centisecunde (10 înseamnă 100 ms per cadru, adică 10 fps), care este controlul standard de redare GIF. Cuvântul-cheie loop al constructorului stabilește dacă clipul rezultat se reia automat în vizualizatoare (valoarea implicită este True, ceea ce corespunde așteptării convenționale de „GIF animat”).

Cele trei căi de înregistrare acoperă împreună cazurile comune: ImageIO pentru reprocesare pe cameră, Motion JPEG pentru înregistrări lungi redabile pe gazdă, GIF animat pentru clipuri scurte redabile pe gazdă. Alegerea între ele se reduce la cine redă înregistrarea. O etapă din aval care rulează pe cameră citește ImageIO; o stație de lucru gazdă sau un vizualizator web citește MJPEG sau GIF.

5.33.6. Un model de declanșare-și-redare

Un model util combină un flux de memorie cu o condiție de declanșare. Camera înregistrează continuu într-un tampon (buffer) inelar de memorie cu count sloturi, suprascriind cel mai vechi slot de fiecare dată. Când o condiție de declanșare se activează (un blob intră în cadru, un eveniment de mișcare depășește pragul, se apasă un buton) aplicația face un instantaneu al conținutului inelului – cele mai recente count cadre – și le scrie într-un flux de tip fișier pe cardul SD. Rezultatul este o înregistrare pre-declanșare care surprinde secundele dinaintea evenimentului pe care camera l-a observat efectiv, nu doar secundele de după, ceea ce reprezintă limitarea clasică a unui înregistrator naiv de tip „captură-la-declanșare”.

Implementarea este simplă odată ce ai la dispoziție clasele de flux: un flux de memorie de dimensiune fixă servește drept inel (cu un seek() explicit la zero când poziția atinge numărul de sloturi), bucla principală capturează în el la fiecare iterație, iar gestionarul de declanșare citește fluxul de memorie cadru cu cadru și scrie fiecare cadru într-un flux de tip fișier denumit după marcajul temporal al declanșării.