12.8. Truyền phát khung hình

Cách sử dụng thực tế phổ biến nhất của kênh tùy chỉnh là truyền phát các khung hình ảnh từ cam đến chương trình host theo tốc độ khung hình của cam. Cơ chế hoạt động tinh tế hơn vẻ ngoài: một JPEG có thể đạt 25 KB hoặc hơn, vì vậy host đọc nó theo nhiều đoạn nhỏ, và vòng lặp chụp của cam phải được ngăn không cho ghi đè lên bộ đệm trong khi đang đọc. Mẫu đúng đắn -- được trình bày ở đây và sử dụng bởi các công cụ trong openmv-projects/tools/ -- chốt bộ đệm cho đến khi host hoàn tất việc kéo byte cuối cùng.

12.8.1. Phía cam

Một kênh khung hình chụp vào một bộ đệm khung hình đơn, chốt khi host đọc lần đầu, và chỉ chụp ảnh tiếp theo sau khi host đã tiêu thụ toàn bộ ảnh:

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

Bốn thành phần đang thực sự hoạt động ở đây:

  • frame_availablechốt. Vòng lặp chụp chỉ chụp ảnh mới khi nó là False -- nghĩa là host đã kéo byte cuối cùng của khung hình trước. Lệnh đọc của host đặt lại nó về False từ bên trong readp khi offset cuối cùng đã được phục vụ. Nếu không có bảo vệ này, csi0.snapshot() tiếp theo sẽ ghi đè lên bộ đệm trong khi đang đọc và host sẽ nhận được một khung hình được ghép từ hai lần chụp.

  • readp thay vì read là những gì backend triển khai. Thư viện giao thức coi bộ đệm được trả về là có thẩm quyền và đọc các byte của nó trực tiếp vào gói tin đi -- không sao chép. Đối với các tải trọng có kích thước khung hình, readp nhanh hơn đáng kể so với read, vốn buộc phải sao chép trung gian.

  • size trả về độ dài JPEG đã được cache mà không tính toán lại bất cứ điều gì; vòng lặp chụp duy trì nó mỗi khi làm mới bộ đệm. Host gọi size giữa pollreadp để biết cần kéo bao nhiêu byte.

  • send_event() thông báo ngay cho host khi một khung hình mới đến để host có thể bắt đầu kéo mà không cần bỏ phiếu. ID sự kiện 0x01 do ứng dụng định nghĩa ("frame ready" trong trường hợp này); hãy sử dụng một số nguyên nhỏ khác nhau cho mỗi loại thông báo.

12.8.2. Phân mảnh

QVGA RGB565 ở chất lượng JPEG 85 nén xuống khoảng 10-25 KB, tùy thuộc vào cảnh -- lớn hơn nhiều so với tải trọng tối đa đã thương lượng trên bất kỳ cam nào (xem bảng theo từng bo mạch trong protocol.init()). Một lần đọc JPEG sẽ không vừa trong một gói tin, và điều đó không sao, vì thư viện giao thức phân mảnh nó một cách minh bạch.

Khi host yêu cầu channel_read('frame', 12000):

  1. readp của cam được gọi một lần với offset=0 và toàn bộ yêu cầu 12000 byte. Nó trả về một memoryview bao phủ toàn bộ phạm vi.

  2. Thư viện giao thức chia memoryview đó thành các mảnh có kích thước tải trọng tối đa trên đường truyền, một gói tin phản hồi CHANNEL_READ cho mỗi mảnh, mỗi gói có header và CRC riêng. Các byte được truyền trực tiếp từ bộ đệm của backend -- không sao chép.

  3. Host nhận các mảnh theo thứ tự, lớp độ tin cậy truyền lại bất kỳ đoạn nào không qua kiểm tra CRC, và SDK của host ghép các đoạn lại thành kết quả 12000 byte được trả về cho người gọi.

Ghi chú

Đây là sự khác biệt thực tế quan trọng giữa readpread. readp được gọi một lần cho mỗi yêu cầu host; lớp giao thức phân mảnh và truyền từ bộ đệm được trả về duy nhất. read được gọi một lần cho mỗi mảnh, và thư viện sao chép mỗi đoạn được trả về vào bộ đệm gói tin riêng của nó. Đối với các tải trọng có kích thước khung hình, readp tiết kiệm cả chi phí gọi Python theo từng mảnh và chi phí sao chép.

Mẹo

Muốn tự xem sự khác biệt? Đổi tên phương thức readp của backend thành read -- không thay đổi gì khác; thư viện sẽ chọn khả năng read thay thế -- và so sánh bộ đếm tốc độ khung hình của host trước và sau. Con số chậm hơn là chi phí sao chép theo từng mảnh và gọi Python mà bạn tránh được khi sử dụng readp.

Chốt trong FrameChannel.readp giải phóng bộ đệm khi offset + size == img_size -- thời điểm host đã kéo byte cuối cùng. Cho đến lúc đó, bộ đệm phải giữ nguyên, đó là lý do tại sao vòng lặp chụp chỉ chụp ảnh tiếp theo sau khi frame_available lật lại về False.

12.8.3. Phía host

Host kéo các khung hình trong một vòng lặp chặt chẽ:

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

Lệnh gọi channel_size() đóng vai trò kiểm tra "có gì sẵn sàng chưa" -- giá trị không có nghĩa là cam chưa chụp -- vì vậy vòng lặp bỏ qua các lần thử đọc trên bộ đệm trống. Đối với các ứng dụng GUI đã bỏ phiếu trên bộ định thời, đây là mẫu tự nhiên.

Image.open của Pillow giải mã JPEG; cam đã nén JPEG nên host không phải đóng gói bit RGB565 tốn kém nữa. Tập lệnh host cũng có thể lưu các byte vào đĩa, truyền chúng cho OpenCV, hoặc đẩy chúng qua một web view.

12.8.4. Suy nghĩ về thông lượng

Ba yếu tố giới hạn tốc độ khung hình có thể đạt được:

  • Tốc độ chụp của cam. Giao thức không thể truyền khung hình nhanh hơn cảm biến tạo ra chúng; bất kỳ giới hạn nào mà định dạng điểm ảnh và kích thước khung hình được chọn áp đặt lên việc chụp đều là giới hạn trần.

  • Tải trọng tối đa đã thương lượng. Tải trọng lớn hơn có nghĩa là ít mảnh hơn cho mỗi khung hình và ít chi phí đóng gói hơn, vì vậy các cam có bộ đệm giao thức lớn hơn di chuyển byte nhanh hơn.

  • Chi phí CRC và ACK. Mỗi gói tin tốn 14 byte đóng gói cộng với một vòng ACK. Đối với các mảnh dài, chi phí theo tải trọng là nhỏ; đối với các tải trọng nhỏ, nó chiếm ưu thế.

Đối với hầu hết công việc GUI từ cam đến laptop, yếu tố giới hạn là thời gian chụp và nén JPEG của cam, không phải ngăn xếp giao thức. Khi giao thức thực sự trở thành nút thắt cổ chai -- ví dụ như truyền phát khung hình thô chưa nén ở tốc độ khung hình cao -- các đòn bẩy là tắt ACK (protocol.init(ack=False)), tăng bộ đệm giao thức nếu cam hỗ trợ, hoặc chụp ở GRAYSCALE để mỗi JPEG nén chỉ mang một kênh thay vì ba và khung hình được mã hóa trở nên nhỏ hơn đáng kể trên đường truyền.

Kênh khung hình là luồng dữ liệu từ cam đến host điển hình. Cùng giao diện backend, với phương thức write được thêm vào, cho phép host đẩy dữ liệu theo chiều ngược lại -- đó là những gì một công cụ cam tương tác cần ngay khi người vận hành muốn thay đổi điều gì đó thay vì chỉ quan sát.