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_availableje zámek. Smyčka snímání pořídí nový snímek pouze tehdy, když má hodnotuFalse– což znamená, že hostitel dotáhl poslední bajt předchozího snímku. Čtení hostitele jej uvnitřreadpnastaví zpět naFalse, 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
readpnamístoread. 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 jereadpznatelně rychlejší nežread, který si vynucuje mezikopii.sizevrací 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ásizemezipollareadp, 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álosti0x01je 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):
readpkamery je voláno jednou soffset=0a celým 12000bajtovým požadavkem. Vrátí jeden memoryview pokrývající celý rozsah.Knihovna protokolu tento memoryview rozdělí na fragmenty o velikosti maximálního payloadu, jeden paket odpovědi
CHANNEL_READna fragment, každý s vlastní hlavičkou a CRC. Bajty se streamují přímo z bufferu backendu – bez kopírování.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.