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() ถัดไปจะเขียนทับบัฟเฟอร์ระหว่างการอ่าน และโฮสต์จะได้รับเฟรมที่ประกอบขึ้นจากการจับภาพสองครั้ง

  • readp rather than read คือสิ่งที่แบ็กเอนด์ใช้งาน ไลบรารีโปรโตคอลจะถือว่าบัฟเฟอร์ที่ส่งคืนมีความถูกต้องและอ่านไบต์โดยตรงลงในแพ็กเก็ตขาออก -- ไม่มีการคัดลอก สำหรับเพย์โหลดขนาดเท่าเฟรม 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):

  1. readp ของกล้องถูกเรียก ครั้งเดียว ด้วย offset=0 และคำขอขนาด 12000 ไบต์เต็ม โดยส่งคืน memoryview หนึ่งตัวที่ครอบคลุมช่วงทั้งหมด

  2. ไลบรารีโปรโตคอลแบ่ง memoryview นั้นออกเป็นชิ้นส่วนขนาดเพย์โหลดสูงสุดบนสายสัญญาณ โดยมีหนึ่งแพ็กเก็ตตอบกลับ CHANNEL_READ ต่อชิ้นส่วน แต่ละชิ้นมีส่วนหัวและ CRC ของตัวเอง ไบต์จะถูกสตรีมออกจากบัฟเฟอร์ของแบ็กเอนด์โดยตรง -- ไม่มีการคัดลอก

  3. โฮสต์รับชิ้นส่วนตามลำดับ ชั้นความน่าเชื่อถือจะส่งซ้ำชิ้นส่วนที่ 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 ช่วยให้โฮสต์ส่งข้อมูลในทิศทางตรงกันข้ามด้วย -- ซึ่งเป็นสิ่งที่เครื่องมือกล้องแบบโต้ตอบต้องการทันทีที่ผู้ใช้ต้องการ เปลี่ยนแปลง บางสิ่ง ไม่ใช่แค่ดูเฉยๆ