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_available je zasun. Petlja snimanja uzima novu snimku samo kada je False – što znači da je host dohvatio posljednji bajt prethodne sličice. Čitanje hosta ga vraća na False iznutra u readp kada je posljednji odmak posljužen. Bez ove zaštite, sljedeći csi0.snapshot() prepisao bi međuspremnik usred čitanja i host bi primio sličicu spojenu od dvije snimke.

  • readp, a ne read, 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čice readp je primjetno brži od read, koji prisiljava na međukopiranje.

  • size vraća predmemoriranu duljinu JPEG-a bez ponovnog izračunavanja bilo čega; petlja snimanja ju održava svaki put kada osvježi međuspremnik. Host poziva size između poll i readp kako 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đaja 0x01 definira 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):

  1. Kameri se readp poziva jednom s offset=0 i punim zahtjevom od 12000 bajtova. Vraća jedan memoryview koji pokriva cijeli raspon.

  2. Protokolarna biblioteka razbija taj memoryview na fragmente veličine maksimalnog sadržaja na vezi, jedan CHANNEL_READ odgovorni paket po fragmentu, svaki sa svojim zaglavljem i CRC-om. Bajtovi se prenose izravno iz međuspremnika pozadinskog sustava – bez kopiranja.

  3. 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.