12.8. การสตรีมเฟรม¶
การใช้งานช่องสัญญาณแบบกำหนดเองที่พบบ่อยที่สุดคือการสตรีมเฟรมภาพจากกล้องไปยังโปรแกรมโฮสต์ตามอัตราเฟรมของกล้อง กลไกนี้ซับซ้อนกว่าที่เห็น: JPEG อาจมีขนาดถึง 25 KB หรือมากกว่า ดังนั้นโฮสต์จึงอ่านเป็นหลายชิ้นส่วน และลูปการจับภาพของกล้องต้องถูกป้องกันไม่ให้เขียนทับบัฟเฟอร์ระหว่างการอ่าน รูปแบบที่ถูกต้อง -- แสดงไว้ที่นี่และใช้โดยเครื่องมือใน openmv-projects/tools/ -- ล็อก บัฟเฟอร์ไว้จนกว่าโฮสต์จะดึงไบต์สุดท้ายเสร็จ
12.8.1. ด้านกล้อง¶
ช่องสัญญาณเฟรมที่จับภาพลงในบัฟเฟอร์เฟรมเดียว ล็อกบัฟเฟอร์เมื่อโฮสต์อ่านครั้งแรก และจะรับสแนปช็อตถัดไปก็ต่อเมื่อโฮสต์ดึงข้อมูลภาพทั้งหมดแล้วเท่านั้น:
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
มีสี่ส่วนที่ทำงานจริงที่นี่:
frame_availableคือ กลไกล็อก ลูปการจับภาพจะรับสแนปช็อตใหม่ก็ต่อเมื่อค่านี้เป็นFalseเท่านั้น -- หมายความว่าโฮสต์ดึงไบต์สุดท้ายของเฟรมก่อนหน้าแล้ว การอ่านของโฮสต์จะตั้งค่ากลับเป็นFalseภายในreadpเมื่อ offset สุดท้ายถูกส่งออกไป หากไม่มีการป้องกันนี้csi0.snapshot()ถัดไปจะเขียนทับบัฟเฟอร์ระหว่างการอ่าน และโฮสต์จะได้รับเฟรมที่ประกอบขึ้นจากการจับภาพสองครั้งreadprather thanreadคือสิ่งที่แบ็กเอนด์ใช้งาน ไลบรารีโปรโตคอลจะถือว่าบัฟเฟอร์ที่ส่งคืนมีความถูกต้องและอ่านไบต์โดยตรงลงในแพ็กเก็ตขาออก -- ไม่มีการคัดลอก สำหรับเพย์โหลดขนาดเท่าเฟรมreadpจะเร็วกว่าreadอย่างเห็นได้ชัด ซึ่งบังคับให้มีการคัดลอกชั่วคราวsizeคืนค่าความยาว JPEG ที่แคชไว้โดยไม่คำนวณใหม่ ลูปการจับภาพจะอัปเดตค่านี้เมื่อรีเฟรชบัฟเฟอร์ โฮสต์เรียกsizeระหว่างpollและreadpเพื่อทราบจำนวนไบต์ที่ต้องดึงsend_event()แจ้งโฮสต์ทันทีที่เฟรมใหม่มาถึงเพื่อให้โฮสต์เริ่มดึงข้อมูลได้โดยไม่ต้องโพล รหัสเหตุการณ์0x01ถูกกำหนดโดยแอปพลิเคชัน ("เฟรมพร้อม" ในกรณีนี้) ใช้จำนวนเต็มขนาดเล็กที่แตกต่างกันสำหรับการแจ้งเตือนแต่ละประเภท
12.8.2. การแบ่งส่วนข้อมูล¶
QVGA RGB565 ที่คุณภาพ JPEG 85 บีบอัดได้ประมาณ 10-25 KB ขึ้นอยู่กับฉาก -- ใหญ่กว่าเพย์โหลดสูงสุดที่เจรจาไว้บนกล้องใดๆ มาก (ดูตารางต่อบอร์ดใน protocol.init()) การอ่าน JPEG หนึ่งครั้งจะไม่พอดีในหนึ่งแพ็กเก็ต และนั่นก็ไม่เป็นปัญหา เพราะไลบรารีโปรโตคอลแบ่งส่วนข้อมูลแบบโปร่งใส
เมื่อโฮสต์ขอ channel_read('frame', 12000):
readpของกล้องถูกเรียก ครั้งเดียว ด้วยoffset=0และคำขอขนาด 12000 ไบต์เต็ม โดยส่งคืน memoryview หนึ่งตัวที่ครอบคลุมช่วงทั้งหมดไลบรารีโปรโตคอลแบ่ง memoryview นั้นออกเป็นชิ้นส่วนขนาดเพย์โหลดสูงสุดบนสายสัญญาณ โดยมีหนึ่งแพ็กเก็ตตอบกลับ
CHANNEL_READต่อชิ้นส่วน แต่ละชิ้นมีส่วนหัวและ CRC ของตัวเอง ไบต์จะถูกสตรีมออกจากบัฟเฟอร์ของแบ็กเอนด์โดยตรง -- ไม่มีการคัดลอกโฮสต์รับชิ้นส่วนตามลำดับ ชั้นความน่าเชื่อถือจะส่งซ้ำชิ้นส่วนที่ CRC ล้มเหลว และ SDK ของโฮสต์จะประกอบชิ้นส่วนเป็นผลลัพธ์ขนาด 12000 ไบต์ที่ส่งคืนให้ผู้เรียก
Note
นี่คือความแตกต่างที่สำคัญในทางปฏิบัติระหว่าง readp และ read readp ถูกเรียก ครั้งเดียวต่อคำขอโฮสต์ โดยชั้นโปรโตคอลจะแบ่งส่วนและส่งจากบัฟเฟอร์ที่ส่งคืนเพียงตัวเดียว read ถูกเรียก ครั้งเดียวต่อชิ้นส่วน และไลบรารีจะคัดลอกแต่ละชิ้นส่วนที่ส่งคืนลงในบัฟเฟอร์แพ็กเก็ตของตัวเอง สำหรับเพย์โหลดขนาดเท่าเฟรม readp ช่วยประหยัดทั้งค่าใช้จ่ายในการเรียก Python ต่อชิ้นส่วนและการคัดลอก
Tip
ต้องการดูความแตกต่างด้วยตัวเองไหม? เปลี่ยนชื่อเมธอด readp ของแบ็กเอนด์เป็น read -- ไม่มีอะไรเปลี่ยนแปลงอื่น ไลบรารีจะใช้ความสามารถ read แทน -- แล้วเปรียบเทียบตัวนับอัตราเฟรมของโฮสต์ก่อนและหลัง ตัวเลขที่ช้ากว่าคือค่าใช้จ่ายจากการคัดลอกและการเรียก Python ต่อชิ้นส่วนที่คุณหลีกเลี่ยงได้ด้วย readp
การล็อกใน FrameChannel.readp จะปล่อยบัฟเฟอร์เมื่อ offset + size == img_size -- ขณะที่โฮสต์ดึงไบต์สุดท้ายแล้ว จนกว่าจะถึงจุดนั้น บัฟเฟอร์ต้องยังคงใช้งานได้ นั่นเป็นเหตุผลว่าทำไมลูปการจับภาพจึงรับสแนปช็อตถัดไปก็ต่อเมื่อ frame_available กลับเป็น False
12.8.3. ด้านโฮสต์¶
โฮสต์ดึงเฟรมในลูปแน่น:
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
การเรียก channel_size() ทำหน้าที่เป็นการตรวจสอบ "มีอะไรพร้อมหรือไม่" ด้วย -- ศูนย์หมายความว่ากล้องยังไม่ได้จับภาพ -- ดังนั้นลูปจึงข้ามการพยายามอ่านบัฟเฟอร์ว่าง สำหรับแอปพลิเคชัน GUI ที่โพลอยู่บนตัวจับเวลาอยู่แล้ว นี่คือรูปแบบที่เป็นธรรมชาติ
Image.open ของ Pillow ถอดรหัส JPEG กล้องบีบอัดเป็น JPEG ไว้แล้วดังนั้นโฮสต์ไม่ต้องทำการบีบแพ็กบิตบน RGB565 ซ้ำ สคริปต์โฮสต์อาจบันทึกไบต์ลงดิสก์ ส่งให้ OpenCV หรือแสดงผ่านเว็บวิวก็ได้
12.8.4. การคิดเรื่องปริมาณงานที่ผ่านได้¶
สามสิ่งที่จำกัดอัตราเฟรมที่ทำได้:
อัตราการจับภาพของกล้อง โปรโตคอลไม่สามารถส่งเฟรมได้เร็วกว่าที่เซนเซอร์ผลิต ไม่ว่าขีดจำกัดที่รูปแบบพิกเซลและขนาดเฟรมที่เลือกกำหนดให้กับการจับภาพคือเพดาน
เพย์โหลดสูงสุดที่เจรจาไว้ เพย์โหลดที่ใหญ่กว่าหมายถึงชิ้นส่วนต่อเฟรมน้อยลงและค่าใช้จ่ายในการกำหนดกรอบน้อยลง ดังนั้นกล้องที่มีบัฟเฟอร์โปรโตคอลขนาดใหญ่จึงเคลื่อนย้ายไบต์ได้เร็วกว่ากล้องที่บัฟเฟอร์ขนาดเล็ก
ค่าใช้จ่าย CRC และ ACK แต่ละแพ็กเก็ตใช้ 14 ไบต์สำหรับกรอบและ ACK หนึ่งรอบทริป สำหรับชิ้นส่วนยาว ค่าใช้จ่ายต่อเพย์โหลดน้อย สำหรับเพย์โหลดเล็กจะครอบงำ
สำหรับงาน GUI กล้องถึงแล็ปท็อปส่วนใหญ่ ปัจจัยจำกัดคือเวลาการจับภาพและการบีบอัด JPEG ของกล้อง ไม่ใช่สแต็กโปรโตคอล เมื่อโปรโตคอลกลายเป็นคอขวด -- เช่น การสตรีมเฟรมดิบที่ไม่ได้บีบอัดในอัตราเฟรมสูง -- คันโยกคือการปิด ACK (protocol.init(ack=False)), เพิ่มบัฟเฟอร์โปรโตคอลหากกล้องรองรับ หรือจับภาพใน GRAYSCALE เพื่อให้แต่ละ JPEG บีบอัดมีช่องสัญญาณเดียวแทนสามช่องและเฟรมที่เข้ารหัสมีขนาดเล็กลงอย่างเห็นได้ชัดบนสายสัญญาณ
ช่องสัญญาณเฟรมคือการไหลข้อมูลมาตรฐานจากกล้องไปโฮสต์ อินเทอร์เฟซแบ็กเอนด์เดียวกัน โดยเพิ่มเมธอด write ช่วยให้โฮสต์ส่งข้อมูลในทิศทางตรงกันข้ามด้วย -- ซึ่งเป็นสิ่งที่เครื่องมือกล้องแบบโต้ตอบต้องการทันทีที่ผู้ใช้ต้องการ เปลี่ยนแปลง บางสิ่ง ไม่ใช่แค่ดูเฉยๆ