12.8. Prijenos sličica u stvarnom vremenu¶
Najčešća stvarna primjena prilagođenog kanala jest prijenos sličica slike s kamere prema host programu pri brzini sličica kamere. Mehanika je suptilnija nego što se čini: JPEG može doseći 25 KB ili više, pa ga host čita kao nekoliko fragmenata, a petlju snimanja kamere treba spriječiti da prepiše međuspremnik usred čitanja. Ispravan obrazac – prikazan ovdje i korišten od strane alata u openmv-projects/tools/ – zaključava međuspremnik dok host ne dohvati posljednji bajt.
12.8.1. Strana kamere¶
Kanal za sličice koji snima u jedan međuspremnik slike, zaključava ga pri prvom čitanju hosta i tek nakon što host potroši cijelu sliku uzima sljedeću snimku:
import csi
import protocol
csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)
csi0.framebuffers(1)
img = csi0.snapshot()
img.compress(quality=85)
img_mv = memoryview(img.bytearray())
img_size = len(img_mv)
frame_available = True
class FrameChannel:
def poll(self):
return frame_available
def size(self):
return img_size
def readp(self, offset, size):
global frame_available
end = offset + size
mv = img_mv[offset:end]
if end == img_size:
# Host has just read the last byte of this frame --
# release the buffer so the capture loop can refresh.
frame_available = False
return mv
ch = protocol.register(name='frame', backend=FrameChannel())
while True:
if not frame_available:
img = csi0.snapshot()
img.compress(quality=85)
img_mv = memoryview(img.bytearray())
img_size = len(img_mv)
frame_available = True
ch.send_event(0x01) # notify host that a new frame is ready
Ovdje stvarni posao obavljaju četiri dijela:
frame_availableje zasun. Petlja snimanja uzima novu snimku samo kada jeFalse– što znači da je host dohvatio posljednji bajt prethodne sličice. Čitanje hosta ga vraća naFalseiznutra ureadpkada je posljednji odmak posljužen. Bez ove zaštite, sljedećicsi0.snapshot()prepisao bi međuspremnik usred čitanja i host bi primio sličicu spojenu od dvije snimke.readp, a neread, jest ono što pozadinski sustav implementira. Protokolarna biblioteka tretira vraćeni međuspremnik kao mjerodavan i čita njegove bajtove izravno u izlazni paket – bez kopiranja. Za sadržaje veličine sličicereadpje primjetno brži odread, koji prisiljava na međukopiranje.sizevraća predmemoriranu duljinu JPEG-a bez ponovnog izračunavanja bilo čega; petlja snimanja ju održava svaki put kada osvježi međuspremnik. Host pozivasizeizmeđupollireadpkako bi znao koliko bajtova treba dohvatiti.send_event()obavještava host onog trenutka kada stigne nova sličica kako bi mogao početi dohvaćati bez ispitivanja. ID događaja0x01definira aplikacija („sličica spremna” u ovom slučaju); za svaku vrstu obavijesti koristite drugačiji mali cijeli broj.
12.8.2. Fragmentacija¶
QVGA RGB565 pri JPEG kvaliteti 85 komprimira se na otprilike 10-25 KB, ovisno o sceni – mnogo više od dogovorenog maksimalnog sadržaja na bilo kojoj kameri (vidi tablicu po pločama u protocol.init()). Jedno JPEG čitanje neće stati u jedan paket, i to je u redu, jer ga protokolarna biblioteka transparentno fragmentira.
Kada host zatraži channel_read('frame', 12000):
Kameri se
readppoziva jednom soffset=0i punim zahtjevom od 12000 bajtova. Vraća jedan memoryview koji pokriva cijeli raspon.Protokolarna biblioteka razbija taj memoryview na fragmente veličine maksimalnog sadržaja na vezi, jedan
CHANNEL_READodgovorni paket po fragmentu, svaki sa svojim zaglavljem i CRC-om. Bajtovi se prenose izravno iz međuspremnika pozadinskog sustava – bez kopiranja.Host prima fragmente redoslijedom, sloj pouzdanosti ponovno šalje svaki dio koji ne prođe CRC, a host SDK lijepi dijelove u rezultat od 12000 bajtova koji se vraća pozivatelju.
Napomena
Ovo je ključna praktična razlika između readp i read. readp se poziva jednom po zahtjevu hosta; protokolarni sloj fragmentira i prenosi iz jednog vraćenog međuspremnika. read se poziva jednom po fragmentu, a biblioteka kopira svaki vraćeni dio u vlastiti međuspremnik paketa. Za sadržaje veličine sličice readp štedi i nadograđeni trošak Python poziva po fragmentu i kopiranje.
Savjet
Želite sami vidjeti razliku? Preimenujte metodu readp pozadinskog sustava u read – ništa se drugo ne mijenja; biblioteka će umjesto toga preuzeti sposobnost read – i usporedite brojač brzine sličica hosta prije i poslije. Sporiji broj jest trošak kopiranja po fragmentu i Python poziva koji izbjegavate koristeći readp.
Zasun u FrameChannel.readp otpušta međuspremnik kada je offset + size == img_size – u trenutku kada je host dohvatio posljednji bajt. Do tada međuspremnik mora ostati valjan, zbog čega petlja snimanja uzima sljedeću snimku tek kada se frame_available vrati na False.
12.8.3. Strana hosta¶
Host dohvaća sličice u uskoj petlji:
import io
from PIL import Image
from openmv.camera import Camera
with Camera('/dev/ttyACM0', baudrate=921600) as cam:
cam.update_channels()
while True:
size = cam.channel_size('frame')
if not size:
continue
data = cam.channel_read('frame', size)
img = Image.open(io.BytesIO(data))
img.show() # or feed to a GUI
Poziv channel_size() ujedno služi i kao provjera „je li išta spremno” – nula znači da kamera još nije snimila – pa petlja preskače pokušaje čitanja na praznom međuspremniku. Za GUI aplikacije koje već ispituju na mjeraču vremena ovo je prirodan obrazac.
Pillowov Image.open dekodira JPEG; kamera ga je već JPEG-komprimirala pa host ne mora ponovno raditi skupo pakiranje bitova na RGB565. Host skripta mogla bi jednako lako spremiti bajtove na disk, predati ih OpenCV-u ili ih proslijediti kroz web prikaz.
12.8.4. Razmišljanje o propusnosti¶
Tri stvari ograničavaju postizivu brzinu sličica:
Brzina snimanja kamere. Protokol ne može isporučiti sličice brže nego što ih senzor proizvodi; koje god ograničenje odabrani format piksela i veličina sličice nameću snimanju, to je gornja granica.
Dogovoreni maksimalni sadržaj. Veći sadržaji znače manje fragmenata po sličici i manje opterećenja okvirima, pa kamere s većim protokolarnim međuspremnicima pomiču bajtove brže od onih manjih.
Opterećenje CRC-om i ACK-om. Svaki paket košta 14 bajtova okvira plus jedno ACK kruženje. Za duge fragmente opterećenje po sadržaju je malo; za sitne sadržaje ono prevladava.
Za većinu GUI rada kamera-na-prijenosnik ograničavajući čimbenik je vrijeme snimanja i JPEG kompresije kamere, a ne protokolarni stog. Ondje gdje protokol postaje usko grlo – primjerice prijenos nekomprimiranih sirovih sličica pri visokim brzinama sličica – poluge su isključivanje ACK-ova (protocol.init(ack=False)), povećavanje protokolarnog međuspremnika ako ga kamera podržava, ili snimanje u GRAYSCALE tako da svaki komprimirani JPEG nosi jedan kanal umjesto tri, a kodirana sličica završi primjetno manja na vezi.
Kanal sličica je kanonski tok podataka kamera-na-host. Isto sučelje pozadinskog sustava, s dodanom metodom write, omogućuje hostu da gura podatke i u drugom smjeru – a to je upravo ono što interaktivnom alatu za kameru treba čim operater želi nešto promijeniti, a ne samo gledati.