12.8. Kehysten suoratoisto#

Mukautetun kanavan yleisin käytännön sovellus on kuvakehysten suoratoisto kamerasta isäntäohjelmaan kameran kehysnopeudella. Mekaniikka on hienovaraisempaa kuin miltä näyttää: JPEG voi olla 25 KB tai enemmän, joten isäntä lukee sen useina paloina, ja kameran kaappaussilmukka on estettävä ylikirjoittamasta puskuria kesken luvun. Oikea malli – joka esitetään tässä ja jota työkalut hakemistossa openmv-projects/tools/ käyttävät – lukitsee puskurin, kunnes isäntä on hakenut viimeisen tavun.

12.8.1. Kameran puoli#

Kehyskanava, joka kaappaa yhteen kehyspuskuriin, lukitsee sen isännän ensimmäisellä lukukerralla ja ottaa seuraavan tilannekuvan vasta kun isäntä on kuluttanut koko kuvan:

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

Neljä osaa tekee tässä varsinaisen työn:

  • frame_available on lukko. Kaappaussilmukka ottaa uuden tilannekuvan vain, kun se on False – eli kun isäntä on hakenut edellisen kehyksen viimeisen tavun. Isännän luku palauttaa sen takaisin arvoon False readp-metodin sisältä, kun viimeinen siirtymä on toimitettu. Ilman tätä suojausta seuraava csi0.snapshot() ylikirjoittaisi puskurin kesken luvun, ja isäntä saisi kahdesta kaappauksesta yhteen ommellun kehyksen.

  • Backend toteuttaa readp-metodin eikä read-metodia. Protokollakirjasto kohtelee palautettua puskuria auktoritatiivisena ja lukee sen tavut suoraan lähtevään pakettiin – ilman kopiointia. Kehyksen kokoisille hyötykuormille readp on huomattavasti nopeampi kuin read, joka pakottaa välikopion.

  • size palauttaa välimuistissa olevan JPEG-pituuden laskematta mitään uudelleen; kaappaussilmukka ylläpitää sitä aina kun se päivittää puskurin. Isäntä kutsuu size-metodia poll- ja readp-kutsujen välissä tietääkseen, montako tavua hakea.

  • send_event() ilmoittaa isännälle heti uuden kehyksen saapuessa, jotta se voi alkaa hakea ilman pollausta. Tapahtuman tunnus 0x01 on sovelluskohtainen (tässä tapauksessa ”kehys valmis”); käytä kullekin ilmoitustyypille eri pientä kokonaislukua.

12.8.2. Pirstoutuminen#

QVGA RGB565 JPEG-laadulla 85 pakkautuu noin 10-25 KB:hen näkymästä riippuen – paljon suuremmaksi kuin neuvoteltu enimmäishyötykuorma millään kameralla (katso laitekohtainen taulukko protocol.init()). Yksi JPEG-luku ei mahdu yhteen pakettiin, ja se on ihan hyvä, koska protokollakirjasto pirstoo sen läpinäkyvästi.

Kun isäntä pyytää channel_read('frame', 12000):

  1. Kameran readp kutsutaan kerran arvoilla offset=0 ja koko 12000 tavun pyynnöllä. Se palauttaa yhden memoryview-objektin, joka kattaa koko alueen.

  2. Protokollakirjasto pilkkoo tuon memoryview-objektin enimmäishyötykuorman kokoisiksi paloiksi linjalle, yksi CHANNEL_READ-vastauspaketti palaa kohti, kullakin oma otsikko ja CRC. Tavut suoratoistetaan suoraan backendin puskurista – ilman kopiointia.

  3. Isäntä vastaanottaa palat järjestyksessä, luotettavuuskerros lähettää uudelleen jokaisen palan, joka epäonnistuu CRC-tarkistuksessaan, ja isännän SDK liimaa palat 12000 tavun tulokseksi, joka palautetaan kutsujalle.

Muista

Tämä on keskeinen käytännön ero readp- ja read-metodien välillä. readp kutsutaan kerran isäntäpyyntöä kohti; protokollakerros pirstoo ja lähettää yhdestä palautetusta puskurista. read kutsutaan kerran palaa kohti, ja kirjasto kopioi jokaisen palautetun palan omaan pakettipuskuriinsa. Kehyksen kokoisille hyötykuormille readp säästää sekä palakohtaisen Python-tason kutsun yleiskustannuksen että kopioinnin.

Vihje

Haluatko nähdä eron itse? Nimeä backendin readp-metodi uudelleen muotoon read – mikään muu ei muutu; kirjasto poimii sen sijaan read-kyvyn – ja vertaa isännän kehysnopeuslaskuria ennen ja jälkeen. Hitaampi luku on palakohtainen kopiointi- ja Python-kutsukustannus, jonka vältät käyttämällä readp-metodia.

FrameChannel.readp-metodin lukko vapauttaa puskurin, kun offset + size == img_size – sillä hetkellä, kun isäntä on hakenut viimeisen tavun. Siihen asti puskurin täytyy pysyä voimassa, minkä vuoksi kaappaussilmukka ottaa seuraavan tilannekuvan vasta kun frame_available kääntyy takaisin arvoon False.

12.8.3. Isännän puoli#

Isäntä hakee kehyksiä tiukassa silmukassa:

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

channel_size()-kutsu toimii samalla ”onko mitään valmiina” -tarkistuksena – nolla tarkoittaa, ettei kamera ole vielä kaapannut – joten silmukka ohittaa lukuyritykset tyhjällä puskurilla. GUI-sovelluksille, jotka jo pollaavat ajastimella, tämä on luonteva malli.

Pillow’n Image.open purkaa JPEG:n; kamera on jo JPEG-pakannut sen, joten isännän ei tarvitse tehdä uudelleen kallista bittien pakkausta RGB565:lle. Isäntäskripti voisi yhtä helposti tallentaa tavut levylle, antaa ne OpenCV:lle tai työntää ne web-näkymän läpi.

12.8.4. Läpäisykyvyn pohdintaa#

Kolme asiaa rajoittaa saavutettavaa kehysnopeutta:

  • Kameran kaappausnopeus. Protokolla ei voi toimittaa kehyksiä nopeammin kuin sensori tuottaa niitä; mikä tahansa katto, jonka valittu pikseliformaatti ja kehyskoko asettavat kaappaukselle, on yläraja.

  • Neuvoteltu enimmäishyötykuorma. Suuremmat hyötykuormat tarkoittavat vähemmän paloja kehystä kohti ja vähemmän kehystyksen yleiskustannusta, joten kamerat, joilla on suuremmat protokollapuskurit, siirtävät tavuja nopeammin kuin pienemmät.

  • CRC:n ja ACK:n yleiskustannus. Jokainen paketti maksaa 14 tavua kehystystä plus yhden ACK-edestakaisen kierroksen. Pitkille paloille hyötykuormakohtainen yleiskustannus on pieni; pienille hyötykuormille se hallitsee.

Useimmassa kamerasta-kannettavalle GUI-työssä rajoittava tekijä on kameran kaappaus- ja JPEG-pakkausaika, ei protokollapino. Siellä missä protokollasta tulee pullonkaula – esimerkiksi pakkaamattomien raakojen kehysten suoratoistossa korkeilla kehysnopeuksilla – vivut ovat ACK:ien sammuttaminen (protocol.init(ack=False)), protokollapuskurin kasvattaminen jos kamera tukee sitä, tai kaappaus GRAYSCALE-tilassa, jolloin jokainen pakattu JPEG sisältää yhden kanavan kolmen sijaan ja koodattu kehys päätyy huomattavasti pienemmäksi linjalla.

Kehyskanava on kanoninen kamerasta-isännälle datavirta. Sama backend-rajapinta, johon on lisätty write-metodi, antaa isännän työntää dataa myös toiseen suuntaan – mikä on juuri se mitä interaktiivinen kameratyökalu tarvitsee heti kun käyttäjä haluaa muuttaa jotain eikä vain katsoa.