5.33. ImageIO tokovi

save() i to_jpeg() pokrivaju slučaj I/O za jednu sličicu: aplikacija snima sličicu, kodira je i šalje je nekamo. Druga klasa aplikacija treba slučaj sekvence: snimanje mnogo sličica zaredom prirodnom brzinom snimanja, pohranjivanje negdje odakle se kasnije mogu dohvatiti i reprodukcija odgovarajućom brzinom. Skripta za prikupljanje podataka za treniranje snima nekoliko stotina primjernih sličica za cjevovod strojnog učenja; dnevnik inspekcijske postaje bilježi svaki snimljeni dio radi sljedivosti; razvojna skripta ponovno reproducira pohranjenu sekvencu kako bi testirala novi algoritam na podacima koji su prethodno snimljeni uživo.

Klasa ImageIO je snimač / reproduktor modula image. Pojedinačni tok sadrži sekvencu Image sličica – moguće različitih veličina i formata piksela – zajedno s intervalom između sličica za svaku od njih, tako da reprodukcija može ponovno stvoriti izvornu brzinu sličica. Dostupna su dva pozadinska spremišta: datoteka na datotečnom sustavu ili međuspremnik fiksne veličine u RAM-u.

5.33.1. Dva pozadinska spremišta

Tok datoteke zadržava snimku kroz cikluse napajanja i njegova je veličina ograničena samo spremištem koje ga podržava. Počinje s magičnim zaglavljem od 16 bajtova OMV IMG STR Vx.y nakon kojeg slijedi jedan blok po sličici; trenutni pisač emitira V2.0, a čitač i dalje prihvaća V1.0 i V1.1 datoteke radi povratne kompatibilnosti. Putanja datoteke je argument konstruktora; način je način otvaranja datoteke ('r' za čitanje postojećeg toka, 'w' za skraćivanje i svježe pisanje).

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

Memorijski tok živi u RAM međuspremniku dodijeljenom pri izgradnji. Konstruktor uzima 3-torku (w, h, pixformat) umjesto putanje, a argument mode postaje unaprijed dodijeljeni broj utora za sličice. Međuspremnik je dimenzioniran točno za toliki broj sličica pri zadanim dimenzijama i ne smije rasti nakon dodjele – pisanje iza zadnjeg utora podiže EOFError, a pisanje sličice veće od međuspremnika po utoru podiže ValueError. Memorijski tokovi su pravi alat kada aplikacija treba predati snimku sljedećoj fazi bez prolaska kroz datotečni sustav (kratki kružni međuspremnik nedavnih sličica za uzorak okidanja i ponovne reprodukcije, primjerice).

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

Za komprimirane formate piksela (image.JPEG, image.PNG) veličina po utoru procjenjuje se na 2 bita po pikselu; kodirana sličica veća od procjene podiže ValueError u trenutku pisanja, tako da aplikacija koja očekuje pohranjivanje JPEG-ova visoke kvalitete mora ili prekomjerno dodijeliti broj utora ili prvo kodirati pri nižoj kvaliteti.

type() vraća image.ImageIO.FILE_STREAM ili image.ImageIO.MEMORY_STREAM tako da se kôd niže u lancu može prilagoditi onom pozadinskom spremištu koje mu je dano.

5.33.2. Snimanje

write() dodaje snimljenu Image na tok datoteke (ili je pohranjuje na trenutni utor memorijskog toka) i pomiče pomak za jedan. Isti poziv bilježi interval između sličica od posljednjeg pisanja, tako da polovica za reprodukciju može pauzirati za pravu količinu vremena između sličica i očuvana je prirodna brzina sličica snimke.

Heterogene sličice dopuštene su unutar jednog toka datoteke: snimka može slobodno miješati RGB565 snimke, izreske u sivim tonovima i JPEG-kodirane sličice, a čitač će svaku dekodirati u njezinoj izvornoj veličini i formatu. Memorijski tokovi su homogeni (svi utori dijele (w, h, pixformat) zadan konstruktorom), pa je memorijska snimka ograničena na jednu konfiguraciju sličice.

write() vraća objekt toka tako da se pozivi mogu ulančati. Pisanje na pomaku koji nije na kraju toka datoteke skraćuje ostatak datoteke – korisno za uređivanje pohranjene sekvence, riskantno ako je položaj sljedećeg pisanja nenamjerno pomaknut ranijim pozivom seek().

sync() prazni pisanja na čekanju na disk za tokove datoteka (na memorijskim tokovima nema učinka) i treba ga pozivati periodički kada je snimanje dugotrajno, kako bi se izbjegao gubitak repa snimke ako se kamera ponovno pokrene prije nego što se datoteka zatvori. Destruktor automatski zatvara tok kada ImageIO izađe iz dosega, ali eksplicitni close() je pravilna disciplina.

5.33.3. Reprodukcija

read() čita sličicu na trenutnom pomaku, pomiče pomak i vraća novu Image. Primatelj ostaje u međuspremniku slike kada je copy_to_fb=True (zadano) tako da je vraćena slika iscrtljiva kroz pregled u IDE-u; s copy_to_fb=False sličica završava na MicroPython gomili.

# 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

Dvije ključne riječi kontroliraju ponašanje reprodukcije. loop=True (zadano za tokove datoteka) omata pokazivač čitanja natrag na početak kada se dosegne kraj snimke, tako da poziv nikada ne vraća None; loop=False vraća None čim je snimka iscrpljena i petlja pozivatelja se prekida. pause=True (zadano) blokira poziv dok ne prođe interval između sličica zabilježen u trenutku pisanja, tako da se brzina reprodukcije sličica podudara s izvornom brzinom snimanja sličica; pause=False vraća se odmah, korisno za analitičke cjevovode koji žele proći kroz snimku što je brže moguće bez poštivanja izvornog vremena.

Isti uzorak petlje radi za memorijske tokove osim što se loop zanemaruje – čitanje iza kraja memorijskog toka podiže EOFError. Očekivani uzorak za memorijski prsten jest eksplicitno pozvati seek() natrag na nulu kada se želi omatanje.

5.33.5. Snimke koje se mogu reproducirati na hostu

ImageIO tokovi su pravi alat kada će se snimka reproducirati na kameri – čuvaju svaku snimljenu sličicu u njezinom izvornom formatu piksela, interval između sličica zabilježen je točno, a skripta niže u lancu može koračati kroz njih, pretraživati i ponovno analizirati bez gubitka. Međutim, nisu pravi alat kada snimka mora biti reproduktivna na hostu – radnoj stanici, telefonu, web reproduktoru. Host očekuje standardni video kontejner, a ne OpenMV format magičnog zaglavlja na disku.

Dva odvojena modula pokrivaju slučaj reprodukcije na hostu. Modul mjpeg snima Motion JPEG: sekvencu JPEG-komprimiranih sličica spakiranih u jedan kontejner u AVI stilu koji VLC, QuickTime, ffmpeg i standardna web video oznaka svi izravno reproduciraju. Modul gif snima animirani GIF: sekvencu nekomprimiranih (ili paletno komprimiranih) sličica s eksplicitnim odgodama po sličici, reproduktivnu u bilo kojem web pregledniku ili preglednika slika koji rukuje animiranim GIF-ovima.

Modul mjpeg prirodan je izbor za duge snimke. JPEG kompresija održava veličinu datoteke upravljivom – usporedivo s to_jpeg() pri konfiguriranoj kvaliteti, sličica za sličicom – tako da produljena sesija snimanja ostaje unutar proračuna SD kartice. Korištenje pomno odražava snimanje pomoću ImageIO:

import mjpeg

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

mjpeg.Mjpeg prihvaća iste pozicijske i ključne riječi za mjerilo u stilu crtanja koje uzimaju druge metode slike, tako da se snimka može skalirati, izrezivati ili paletno mapirati po sličici na ulazu. Argumenti konstruktora width i height zadano odgovaraju dimenzijama glavnog međuspremnika slike i fiksiraju izlaznu razlučivost; svaka dodana sličica skalira se (uz očuvanje omjera stranica) tako da odgovara. sync() prazni datoteku na disk tijekom dugog snimanja, a close() finalizira kontejner – Motion JPEG datoteka koja nije čisto zatvorena nije reproduktivna, pa je disciplina važna.

Modul gif prirodan je izbor za kratke snimke dijeljene doslovno s netehničkim gledateljem – nekoliko sekundi akcije snimljene za demonstraciju, animirana ilustracija za dokumentaciju, isječak događaja ugrađen u poruku u razgovoru. GIF sličice pohranjuju se nekomprimirane (ili paletno komprimirane pri dubini boje od 7 bita), zbog čega su datoteke mnogo veće po sekundi od Motion JPEG-a i isključuju format za snimke duže od nekoliko sekundi, ali rezultat se izravno ubacuje u bilo koji preglednik:

import gif

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

Argument delay na add_frame() je vrijeme prikaza po sličici u centisekundama (10 je 100 ms po sličici, ili 10 fps), što je standardna kontrola reprodukcije GIF-a. Ključna riječ loop konstruktora postavlja hoće li se rezultirajući isječak automatski ponavljati u preglednicima (zadano je True, što odgovara uobičajenom očekivanju „animiranog GIF-a”).

Tri puta snimanja zajedno pokrivaju uobičajene slučajeve: ImageIO za ponovnu obradu na kameri, Motion JPEG za duge snimke reproduktivne na hostu, animirani GIF za kratke isječke reproduktivne na hostu. Izbor između njih svodi se na to tko reproducira snimku. Faza niže u lancu koja se izvodi na samoj kameri čita ImageIO; host radna stanica ili web preglednik čita MJPEG ili GIF.

5.33.6. Uzorak okidanja i ponovne reprodukcije

Koristan uzorak kombinira memorijski tok s uvjetom okidanja. Kamera kontinuirano snima u kružni memorijski međuspremnik s count utora, prepisujući najstariji utor svaki put pri prolasku. Kada se aktivira uvjet okidanja (mrlja ulazi u sličicu, događaj kretanja premašuje prag, pritisne se gumb) aplikacija snima sadržaj prstena – najnovijih count sličica – i zapisuje ih u tok datoteke na SD kartici. Rezultat je snimka prije okidanja koja hvata sekunde prije događaja koji je kamera zapravo primijetila, a ne samo sekunde nakon, što je klasično ograničenje naivnog snimača koji „snima-kada-je-okinut”.

Implementacija je jednostavna kada su klase toka pri ruci: memorijski tok fiksne veličine služi kao prsten (s eksplicitnim seek() na nulu kada pomak dosegne broj utora), glavna petlja snima u njega pri svakoj iteraciji, a rukovatelj okidanjem čita memorijski tok sličicu po sličicu i zapisuje svaku u tok datoteke imenovan prema vremenskoj oznaci okidanja.