12.8. Streaming bingkai¶
Penggunaan nyata yang paling umum dari saluran kustom adalah streaming bingkai citra dari kamera ke program host sesuai dengan laju bingkai kamera. Mekanismenya lebih halus dari yang terlihat: sebuah JPEG dapat mencapai 25 KB atau lebih, sehingga host membacanya dalam beberapa fragmen, dan loop pengambilan gambar kamera harus dicegah dari menimpa buffer di tengah proses baca. Pola yang tepat -- ditunjukkan di sini dan digunakan oleh alat-alat di openmv-projects/tools/ -- mengunci buffer hingga host selesai menarik byte terakhir.
12.8.1. Sisi kamera¶
Saluran bingkai yang mengambil gambar ke dalam satu framebuffer, menguncinya pada pembacaan pertama host, dan hanya mengambil snapshot berikutnya setelah host mengonsumsi citra penuh:
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
Empat bagian yang bekerja nyata di sini:
frame_availableadalah latch (pengunci). Loop pengambilan hanya mengambil snapshot baru ketika nilainyaFalse-- artinya host telah menarik byte terakhir dari bingkai sebelumnya. Pembacaan host menyetelnya kembali keFalsedari dalamreadpsetelah offset terakhir dilayani. Tanpa pengaman ini,csi0.snapshot()berikutnya akan menimpa buffer di tengah proses baca dan host akan menerima bingkai yang dijahit dari dua pengambilan gambar.readpbukanreadadalah apa yang diimplementasikan backend. Library protokol memperlakukan buffer yang dikembalikan sebagai otoritatif dan membaca byte-nya langsung ke dalam paket keluar -- tanpa salinan. Untuk payload berukuran bingkai,readpjauh lebih cepat dariread, yang memaksa salinan perantara.sizemengembalikan panjang JPEG yang di-cache tanpa menghitung ulang apa pun; loop pengambilan memeliharanya setiap kali menyegarkan buffer. Host memanggilsizeantarapolldanreadpuntuk mengetahui berapa banyak byte yang harus ditarik.send_event()memberi tahu host seketika begitu bingkai baru tiba sehingga host bisa mulai menarik tanpa polling. ID event0x01didefinisikan oleh aplikasi ("bingkai siap" dalam kasus ini); gunakan bilangan bulat kecil yang berbeda untuk setiap jenis notifikasi.
12.8.2. Fragmentasi¶
QVGA RGB565 pada kualitas JPEG 85 dikompresi menjadi sekitar 10-25 KB, tergantung pada adegan -- jauh lebih besar dari payload maksimum yang dinegosiasikan pada kamera mana pun (lihat tabel per-papan di protocol.init()). Satu pembacaan JPEG tidak akan muat dalam satu paket, dan itu tidak masalah, karena library protokol memfragmentasinya secara transparan.
Ketika host meminta channel_read('frame', 12000):
readpkamera dipanggil sekali denganoffset=0dan permintaan penuh 12000 byte. Ini mengembalikan satu memoryview yang mencakup seluruh rentang.Library protokol memecah memoryview tersebut menjadi fragmen berukuran payload-maksimum di kawat, satu paket respons
CHANNEL_READper fragmen, masing-masing dengan header dan CRC sendiri. Byte-byte dialirkan keluar dari buffer backend secara langsung -- tanpa salinan.Host menerima fragmen secara berurutan, lapisan keandalan mentransmisi ulang setiap satu fragmen yang gagal CRC-nya, dan host SDK menyatukan fragmen-fragmen menjadi hasil 12000-byte yang dikembalikan ke pemanggil.
Catatan
Inilah perbedaan praktis utama antara readp dan read. readp dipanggil sekali per permintaan host; lapisan protokol memfragmentasi dan mengirimkan dari satu buffer yang dikembalikan. read dipanggil sekali per fragmen, dan library menyalin setiap fragmen yang dikembalikan ke dalam buffer paketnya sendiri. Untuk payload berukuran bingkai, readp menghemat overhead panggilan tingkat Python per fragmen sekaligus salinannya.
Tip
Ingin melihat perbedaannya sendiri? Ubah nama metode readp backend menjadi read -- tidak ada yang lain berubah; library akan menggunakan kemampuan read sebagai gantinya -- dan bandingkan penghitung laju bingkai host sebelum dan sesudah. Angka yang lebih lambat adalah biaya salinan per fragmen dan panggilan Python yang Anda hindari dengan menggunakan readp.
Latch di FrameChannel.readp melepas buffer ketika offset + size == img_size -- saat host telah menarik byte terakhir. Sampai saat itu, buffer harus tetap valid, itulah mengapa loop pengambilan hanya mengambil snapshot berikutnya setelah frame_available berubah kembali ke False.
12.8.3. Sisi host¶
Host menarik bingkai dalam loop ketat:
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
Panggilan channel_size() berfungsi ganda sebagai pemeriksaan "apakah ada yang siap" -- nol berarti kamera belum mengambil gambar -- sehingga loop melewati upaya baca pada buffer kosong. Untuk aplikasi GUI yang sudah melakukan polling pada timer, ini adalah pola yang alami.
Image.open dari Pillow mendekode JPEG; kamera sudah mengompresnya dengan JPEG sehingga host tidak perlu melakukan bit-packing RGB565 yang mahal lagi. Skrip host bisa dengan mudah menyimpan byte ke disk, meneruskannya ke OpenCV, atau mendorongnya melalui tampilan web.
12.8.4. Pemikiran tentang throughput¶
Tiga hal membatasi laju bingkai yang dapat dicapai:
Laju pengambilan kamera. Protokol tidak dapat mengirimkan bingkai lebih cepat dari yang dihasilkan sensor; batas apapun yang ditentukan oleh format piksel dan ukuran bingkai yang dipilih pada pengambilan gambar adalah batas atasnya.
Payload maksimum yang dinegosiasikan. Payload yang lebih besar berarti lebih sedikit fragmen per bingkai dan overhead framing yang lebih sedikit, sehingga kamera dengan buffer protokol yang lebih besar memindahkan byte lebih cepat dari yang lebih kecil.
Overhead CRC dan ACK. Setiap paket membutuhkan 14 byte framing ditambah satu round-trip ACK. Untuk fragmen panjang, overhead per payload kecil; untuk payload kecil, overhead mendominasi.
Untuk sebagian besar pekerjaan GUI kamera-ke-laptop, faktor pembatas adalah waktu pengambilan dan kompresi JPEG kamera, bukan tumpukan protokol. Di mana protokol menjadi bottleneck -- streaming bingkai mentah tidak terkompresi pada laju bingkai tinggi, misalnya -- pengungkitnya adalah mematikan ACK (protocol.init(ack=False)), meningkatkan buffer protokol jika kamera mendukungnya, atau mengambil gambar dalam GRAYSCALE sehingga setiap JPEG terkompresi membawa satu saluran alih-alih tiga dan bingkai yang dikodekan berakhir lebih kecil di kawat.
Saluran bingkai adalah aliran data kamera-ke-host yang kanonik. Antarmuka backend yang sama, dengan metode write yang ditambahkan, memungkinkan host mendorong data ke arah sebaliknya juga -- itulah yang dibutuhkan alat kamera interaktif begitu operator ingin mengubah sesuatu daripada sekadar menonton.