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_availablea retesz. A rögzítési ciklus csak akkor készít új pillanatképet, amikor ezFalse– vagyis amikor a gazdagép kiolvasta az előző képkocka utolsó bájtját. A gazdagép olvasása areadpbelsejéből állítja visszaFalseé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
readpmetódust valósítja meg, nem areadmetó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 areadpérezhetően gyorsabb, mint aread, amely egy köztes másolatot kényszerít ki.A
sizea 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 apollés areadpközött hívja meg asizemetó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. A0x01esemé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:
A kamera
readpmetódusa egyszer hívódik megoffset=0értékkel és a teljes 12000 bájtos kéréssel. Egyetlen memoryview-t ad vissza, amely a teljes tartományt lefedi.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_READvá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.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.