12.8. Képkockák streamelése

Egy egyéni csatorna legjellemzőbb valós felhasználása a képkockák streamelése a kamerától egy gazdaprogram felé, a kamera képsebességén. A mechanika finomabb a látszatnál: egy JPEG akár 25 KB-ot vagy többet is kitehet, így a gazdagép több töredékként olvassa be, a kamera rögzítési ciklusát pedig meg kell akadályozni abban, hogy olvasás közben felülírja a puffert. A helyes minta – amelyet itt bemutatunk, és amelyet az openmv-projects/tools/ mappában lévő eszközök is használnak – megreteszeli a puffert, amíg a gazdagép ki nem olvassa az utolsó bájtot.

12.8.1. A kamera oldala

Egy képkocka-csatorna, amely egyetlen képkocka-pufferbe rögzít, megreteszeli azt a gazdagép első olvasásakor, és csak akkor készíti el a következő pillanatképet, amikor a gazdagép a teljes képet felhasználta:

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

Itt négy elem végez tényleges munkát:

  • A frame_available a retesz. A rögzítési ciklus csak akkor készít új pillanatképet, amikor ez False – vagyis amikor a gazdagép kiolvasta az előző képkocka utolsó bájtját. A gazdagép olvasása a readp belsejéből állítja vissza False értékre, miután az utolsó eltolást is kiszolgálta. E védelem nélkül a következő csi0.snapshot() olvasás közben felülírná a puffert, és a gazdagép két rögzítésből összevarrt képkockát kapna.

  • A háttérprogram a readp metódust valósítja meg, nem a read metódust. A protokollkönyvtár a visszaadott puffert mérvadónak tekinti, és annak bájtjait közvetlenül a kimenő csomagba olvassa be – másolás nélkül. Képkocka méretű hasznos terhelés esetén a readp érezhetően gyorsabb, mint a read, amely egy köztes másolatot kényszerít ki.

  • A size a gyorsítótárazott JPEG-hosszt adja vissza anélkül, hogy bármit újraszámolna; a rögzítési ciklus karbantartja, valahányszor frissíti a puffert. A gazdagép a poll és a readp között hívja meg a size metódust, hogy megtudja, hány bájtot kell lekérnie.

  • A send_event() abban a pillanatban értesíti a gazdagépet, amint új képkocka érkezik, így az lekérdezés nélkül megkezdheti a lehívást. A 0x01 eseményazonosító alkalmazásfüggő (ebben az esetben „frame ready”); minden értesítéstípushoz használj eltérő, kis egész számot.

12.8.2. Töredezettség

A QVGA RGB565 JPEG 85-ös minőségen nagyjából 10-25 KB-ra tömörödik, a jelenettől függően – jóval nagyobbra, mint a kialkudott maximális hasznos terhelés bármely kamerán (lásd a kártyánkénti táblázatot itt: protocol.init()). Egyetlen JPEG-olvasás nem fér el egyetlen csomagban, és ez rendben is van, mert a protokollkönyvtár átláthatóan töredezi azt.

Amikor a gazdagép a channel_read('frame', 12000) hívást kéri:

  1. A kamera readp metódusa egyszer hívódik meg offset=0 értékkel és a teljes 12000 bájtos kéréssel. Egyetlen memoryview-t ad vissza, amely a teljes tartományt lefedi.

  2. A protokollkönyvtár ezt a memoryview-t a vezetéken maximális hasznos terhelés méretű töredékekre bontja, töredékenként egy CHANNEL_READ válaszcsomaggal, mindegyik a saját fejlécével és CRC-jével. A bájtok közvetlenül a háttérprogram pufferéből streamelődnek ki – másolás nélkül.

  3. A gazdagép sorrendben kapja meg a töredékeket, a megbízhatósági réteg újraküld minden olyan darabot, amely megbukik a CRC-ellenőrzésen, a gazda SDK pedig a darabokat összeragasztja a hívónak visszaadott 12000 bájtos eredménnyé.

Megjegyzés

Ez a fő gyakorlati különbség a readp és a read között. A readp gazdagép-kérésenként egyszer hívódik meg; a protokollréteg az egyetlen visszaadott pufferből töredezik és továbbít. A read töredékenként egyszer hívódik meg, a könyvtár pedig minden visszaadott darabot a saját csomagpufferébe másol. Képkocka méretű hasznos terhelés esetén a readp megtakarítja mind a töredékenkénti Python-szintű hívás többletköltségét, mind a másolást.

Javaslat

Szeretnéd magad látni a különbséget? Nevezd át a háttérprogram readp metódusát read névre – semmi más nem változik; a könyvtár ehelyett a read képességet veszi át –, és hasonlítsd össze a gazdagép képsebesség-számlálóját előtte és utána. A lassabb szám az a töredékenkénti másolási és Python-hívási költség, amelyet a readp használatával elkerülsz.

A FrameChannel.readp retesze akkor szabadítja fel a puffert, amikor offset + size == img_size – abban a pillanatban, amikor a gazdagép kiolvasta az utolsó bájtot. Addig a puffernek érvényesnek kell maradnia, ezért is készíti el a rögzítési ciklus a következő pillanatképet csak akkor, amikor a frame_available visszabillen False értékre.

12.8.3. A gazdagép oldala

A gazdagép szoros ciklusban hívja le a képkockákat:

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

A channel_size() hívás egyúttal „van-e bármi készen” ellenőrzésként is szolgál – a nulla azt jelenti, hogy a kamera még nem rögzített –, így a ciklus üres puffer esetén kihagyja az olvasási kísérleteket. Az időzítőn már amúgy is lekérdező grafikus felhasználói felületű alkalmazások számára ez a természetes minta.

A Pillow Image.open metódusa dekódolja a JPEG-et; a kamera már JPEG-tömörítette, így a gazdagépnek nem kell újra elvégeznie a költséges bitcsomagolást az RGB565-ön. A gazdaszkript ugyanilyen könnyen lemezre is menthetné a bájtokat, átadhatná őket az OpenCV-nek, vagy átküldhetné egy webes nézeten.

12.8.4. Az átviteli teljesítmény átgondolása

Az elérhető képsebességet három dolog korlátozza:

  • A kamera rögzítési sebessége. A protokoll nem tud gyorsabban szállítani képkockákat, mint ahogy az érzékelő előállítja őket; bármilyen korlátot is szab a választott képpontformátum és képkockaméret a rögzítésre, az a felső határ.

  • A kialkudott maximális hasznos terhelés. A nagyobb hasznos terhelés képkockánként kevesebb töredéket és kevesebb keretezési többletköltséget jelent, így a nagyobb protokollpufferrel rendelkező kamerák gyorsabban mozgatják a bájtokat, mint a kisebbek.

  • A CRC- és ACK-többletköltség. Minden csomag 14 bájt keretezésbe és egy ACK oda-vissza fordulóba kerül. Hosszú töredékek esetén a hasznos terhelésenkénti többletköltség kicsi; apró hasznos terhelés esetén dominál.

A legtöbb kamera-laptop grafikus felületű munka esetén a korlátozó tényező a kamera rögzítési és JPEG-tömörítési ideje, nem a protokollverem. Ahol a protokoll mégis szűk keresztmetszetté válik – például tömörítetlen nyers képkockák streamelésekor nagy képsebességen – ott a megoldás az ACK-k kikapcsolása (protocol.init(ack=False)), a protokollpuffer növelése, ha a kamera támogatja, vagy GRAYSCALE módban való rögzítés, hogy minden tömörített JPEG három helyett egyetlen csatornát hordozzon, és a kódolt képkocka érezhetően kisebb legyen a vezetéken.

A képkocka-csatorna a kanonikus kamera-gazda adatfolyam. Ugyanaz a háttérprogram-interfész egy hozzáadott write metódussal lehetővé teszi, hogy a gazdagép a másik irányba is adatot küldjön – pontosan ez az, amire egy interaktív kameraeszköznek szüksége van, amint a kezelő megváltoztatni szeretne valamit, nem csak figyelni.