12.8. Streamování snímků

Nejčastějším reálným využitím vlastního kanálu je streamování obrazových snímků z kamery do hostitelského programu rychlostí, kterou kamera snímá. Mechanika je rafinovanější, než se zdá: JPEG může mít 25 KB i více, takže jej hostitel čte jako několik fragmentů a smyčce snímání na kameře musí být zabráněno přepsat buffer uprostřed čtení. Správný vzor – zde ukázaný a používaný nástroji v openmv-projects/tools/zamyká buffer, dokud hostitel nedotáhne poslední bajt.

12.8.1. Strana kamery

Kanál snímků, který snímá do jediného framebufferu, při prvním čtení hostitelem jej zamkne a další snímek pořídí teprve poté, co hostitel spotřebuje celý obraz:

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

Skutečnou práci zde odvádějí čtyři součásti:

  • frame_available je zámek. Smyčka snímání pořídí nový snímek pouze tehdy, když má hodnotu False – což znamená, že hostitel dotáhl poslední bajt předchozího snímku. Čtení hostitele jej uvnitř readp nastaví zpět na False, jakmile je obsloužen poslední offset. Bez této pojistky by další csi0.snapshot() přepsal buffer uprostřed čtení a hostitel by obdržel snímek poskládaný ze dvou pořízení.

  • Backend implementuje readp namísto read. Knihovna protokolu považuje vrácený buffer za směrodatný a jeho bajty čte přímo do odchozího paketu – bez kopírování. Pro payloady velikosti snímku je readp znatelně rychlejší než read, který si vynucuje mezikopii.

  • size vrací cachovanou délku JPEG bez jakéhokoli přepočítávání; smyčka snímání ji udržuje pokaždé, když obnoví buffer. Hostitel volá size mezi poll a readp, aby věděl, kolik bajtů má dotáhnout.

  • send_event() upozorní hostitele v okamžiku, kdy dorazí nový snímek, takže může začít dotahovat bez dotazování. ID události 0x01 je definováno aplikací (v tomto případě „snímek připraven“); pro každý druh upozornění použijte jiné malé celé číslo.

12.8.2. Fragmentace

QVGA RGB565 při kvalitě JPEG 85 se komprimuje zhruba na 10-25 KB v závislosti na scéně – mnohem více než vyjednaný maximální payload na kterékoli kameře (viz tabulka podle desky v protocol.init()). Jedno čtení JPEG se do jednoho paketu nevejde, a to je v pořádku, protože knihovna protokolu jej transparentně fragmentuje.

Když hostitel požádá o channel_read('frame', 12000):

  1. readp kamery je voláno jednou s offset=0 a celým 12000bajtovým požadavkem. Vrátí jeden memoryview pokrývající celý rozsah.

  2. Knihovna protokolu tento memoryview rozdělí na fragmenty o velikosti maximálního payloadu, jeden paket odpovědi CHANNEL_READ na fragment, každý s vlastní hlavičkou a CRC. Bajty se streamují přímo z bufferu backendu – bez kopírování.

  3. Hostitel přijímá fragmenty v pořadí, vrstva spolehlivosti znovu odešle kterýkoli blok, který neprojde svým CRC, a hostitelské SDK slepí bloky do 12000bajtového výsledku vráceného volajícímu.

Poznámka

Toto je klíčový praktický rozdíl mezi readp a read. readp je voláno jednou na požadavek hostitele; vrstva protokolu fragmentuje a vysílá z jediného vráceného bufferu. read je voláno jednou na fragment a knihovna kopíruje každý vrácený blok do vlastního paketového bufferu. Pro payloady velikosti snímku readp šetří jak režii volání na úrovni Pythonu pro každý fragment, tak kopírování.

Tip

Chcete ten rozdíl vidět na vlastní oči? Přejmenujte metodu readp backendu na read – nic dalšího se nemění; knihovna místo toho použije schopnost read – a porovnejte čítač snímkové frekvence na hostiteli před a po. Pomalejší číslo představuje náklady na kopírování každého fragmentu a volání Pythonu, kterým se použitím readp vyhnete.

Zámek v FrameChannel.readp uvolní buffer, když offset + size == img_size – v okamžiku, kdy hostitel dotáhl poslední bajt. Do té doby musí buffer zůstat platný, a proto smyčka snímání pořídí další snímek teprve poté, co se frame_available překlopí zpět na False.

12.8.3. Strana hostitele

Hostitel dotahuje snímky v těsné smyčce:

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

Volání channel_size() zároveň slouží jako kontrola „je něco připraveno“ – nula znamená, že kamera ještě nesnímala – takže smyčka přeskakuje pokusy o čtení z prázdného bufferu. Pro GUI aplikace, které již dotazují na časovači, je to přirozený vzor.

Image.open z Pillow dekóduje JPEG; kamera jej již JPEG-komprimovala, takže hostitel nemusí znovu provádět nákladné bitové balení RGB565. Hostitelský skript by mohl stejně snadno uložit bajty na disk, předat je OpenCV nebo je poslat do webového náhledu.

12.8.4. Úvahy o propustnosti

Dosažitelnou snímkovou frekvenci omezují tři věci:

  • Rychlost snímání kamery. Protokol nemůže doručovat snímky rychleji, než je senzor produkuje; ať už je strop snímání daný zvoleným formátem pixelů a velikostí snímku jakýkoli, je to právě toto omezení.

  • Vyjednaný maximální payload. Větší payloady znamenají méně fragmentů na snímek a méně rámcovací režie, takže kamery s většími buffery protokolu přenášejí bajty rychleji než kamery s menšími.

  • Režie CRC a ACK. Každý paket stojí 14 bajtů rámcování plus jednu obrátku ACK. U dlouhých fragmentů je režie na payload malá; u drobných payloadů dominuje.

U většiny práce s GUI mezi kamerou a notebookem je omezujícím faktorem doba snímání a JPEG komprese kamery, nikoli zásobník protokolu. Tam, kde se protokol skutečně stane úzkým hrdlem – například při streamování nekomprimovaných surových snímků při vysokých snímkových frekvencích – jsou pákami vypnutí ACK (protocol.init(ack=False)), zvětšení bufferu protokolu, pokud to kamera podporuje, nebo snímání v GRAYSCALE, takže každý komprimovaný JPEG nese jeden kanál místo tří a zakódovaný snímek skončí na drátě znatelně menší.

Kanál snímků je kanonický datový tok z kamery do hostitele. Totéž rozhraní backendu s přidanou metodou write umožňuje hostiteli posílat data i opačným směrem – což je přesně to, co interaktivní nástroj pro kameru potřebuje, jakmile chce obsluha něco měnit, a nejen sledovat.