12.8. Потокова передача кадрів¶
Найпоширеніше практичне використання власного каналу — це потокова передача кадрів зображення з камери до хост-програми зі швидкістю захоплення камери. Механіка тут тонша, ніж здається: JPEG може займати 25 КБ і більше, тому хост зчитує його частинами, а цикл захоплення камери не повинен перезаписувати буфер під час читання. Правильний шаблон — показаний тут і використовуваний інструментами з 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після передачі фінального зміщення. Без цього захисту наступнийcsi0.snapshot()перезаписав би буфер під час читання, і хост отримав би кадр, зшитий із двох захоплень.readpзамістьread— це те, що реалізує бекенд. Бібліотека протоколу розглядає повернений буфер як авторитетний і зчитує його байти безпосередньо у вихідний пакет — без копіювання. Для навантажень розміром кадруreadpпомітно швидший заread, який вимагає проміжного копіювання.sizeповертає кешовану довжину JPEG без повторного обчислення; цикл захоплення оновлює її щоразу, коли оновлює буфер. Хост викликаєsizeміжpollіreadp, щоб знати, скільки байтів потрібно отримати.send_event()повідомляє хост у момент, коли надходить новий кадр, щоб він міг почати читання без опитування. Ідентифікатор події0x01визначається застосунком («кадр готовий» у цьому випадку); для кожного типу сповіщення використовуйте інше мале ціле число.
12.8.2. Фрагментація¶
QVGA RGB565 при якості JPEG 85 стискається приблизно до 10–25 КБ залежно від сцени — значно більше за узгоджений максимальний розмір корисного навантаження на будь-якій камері (дивіться таблицю для кожної плати у protocol.init()). Один запит читання JPEG не вміститься в один пакет, але це нормально, оскільки бібліотека протоколу автоматично виконує фрагментацію.
Коли хост запитує channel_read('frame', 12000):
readpкамери викликається один раз ізoffset=0і повним запитом на 12000 байтів. Він повертає один memoryview, що охоплює весь діапазон.Бібліотека протоколу розбиває цей memoryview на фрагменти максимального розміру корисного навантаження в дроті, по одному пакету відповіді
CHANNEL_READна фрагмент, кожен зі своїм заголовком і CRC. Байти передаються безпосередньо з буфера бекенда — без копіювання.Хост отримує фрагменти по порядку, рівень надійності повторно передає будь-який фрагмент, що не пройшов перевірку CRC, а SDK хоста збирає фрагменти в результат з 12000 байтів, що повертається викликачу.
Примітка
Це ключова практична різниця між readp і read. readp викликається один раз на запит хоста; рівень протоколу фрагментує і передає дані з єдиного поверненого буфера. read викликається один раз на фрагмент, і бібліотека копіює кожен повернений фрагмент у власний буфер пакета. Для навантажень розміром кадру readp економить як накладні витрати на виклик Python на рівні фрагмента, так і копіювання.
Порада
Хочете побачити різницю самостійно? Перейменуйте метод 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 дозволяє хосту відправляти дані у зворотному напрямку — що і потрібно інтерактивному інструменту для роботи з камерою, щойно оператор захоче щось змінити, а не просто спостерігати.