5.33. Потоки ImageIO¶
save() та to_jpeg() вирішують задачу введення/виведення одного кадру: застосунок захоплює кадр, кодує його та передає кудись. Інший клас застосунків потребує обробки послідовностей: запис багатьох кадрів підряд з природньою частотою захоплення, збереження їх у місці, звідки їх можна отримати пізніше, та відтворення з правильною швидкістю. Скрипт збору навчальних даних захоплює кілька сотень прикладних кадрів для конвеєра машинного навчання; журнал інспекційної станції записує кожну захоплену деталь для відстежуваності; скрипт розробки відтворює збережену послідовність для тестування нового алгоритму на даних, що були раніше захоплені в реальному часі.
Клас ImageIO є рекордером/програвачем модуля image. Один потік зберігає послідовність кадрів Image – можливо, різних розмірів та форматів пікселів – разом із міжкадровим інтервалом кожного з них, тож відтворення може відновити вихідну частоту кадрів. Доступно два сховища: файл у файловій системі або буфер фіксованого розміру у RAM.
5.33.1. Два типи сховищ¶
Файловий потік зберігає запис між циклами живлення та обмежений лише розміром доступного сховища. Він починається з 16-байтового магічного заголовку OMV IMG STR Vx.y, за яким слідує один блок на кадр; поточний запис створює V2.0, а читач також приймає файли V1.0 і V1.1 для зворотної сумісності. Шлях до файлу є аргументом конструктора; режим — це режим відкриття файлу ('r' для читання наявного потоку, 'w' для скидання та нового запису).
# Recording to /sdcard/run.bin
stream = image.ImageIO("/sdcard/run.bin", "w")
for _ in range(120):
img = csi0.snapshot()
stream.write(img)
stream.close()
Потік у пам’яті живе у RAM-буфері, що виділяється під час конструювання. Конструктор приймає 3-кортеж (w, h, pixformat) замість шляху, а аргумент mode стає попередньо виділеною кількістю слотів кадрів. Буфер розмірується точно для вказаної кількості кадрів із заданими розмірами та не може збільшуватись після виділення – запис за межі останнього слота викидає EOFError, а запис кадру, більшого за буфер слота, викидає ValueError. Потоки в пам’яті є правильним інструментом, коли застосунку потрібно передати запис у подальшу стадію без проходження через файлову систему (наприклад, короткий кільцевий буфер останніх кадрів для шаблону «тригер та відтворення»).
# Pre-allocate space for 32 QVGA RGB565 frames in RAM
stream = image.ImageIO((320, 240, image.RGB565), 32)
for _ in range(32):
stream.write(csi0.snapshot())
Для стиснутих форматів пікселів (image.JPEG, image.PNG) розмір слота оцінюється у 2 біти на піксель; закодований кадр, що перевищує оцінку, викидає ValueError під час запису, тому застосунок, що планує зберігати JPEG-зображення високої якості, має або збільшити кількість слотів, або кодувати з нижчою якістю.
type() повертає image.ImageIO.FILE_STREAM або image.ImageIO.MEMORY_STREAM, щоб подальший код міг адаптуватись до будь-якого типу сховища.
5.33.2. Запис¶
write() додає захоплений Image до файлового потоку (або зберігає його у поточному слоті потоку в пам’яті) та просуває зміщення на одиницю. Той самий виклик записує міжкадровий інтервал з моменту останнього запису, тож при відтворенні можна робити правильні паузи між кадрами і природня частота кадрів запису зберігається.
Різнорідні кадри допускаються в межах одного файлового потоку: запис може вільно поєднувати захоплення RGB565, обрізки у відтінках сірого та JPEG-мініатюри, і читач декодує кожен у його вихідному розмірі та форматі. Потоки в пам’яті є однорідними (всі слоти мають конструктор-задані (w, h, pixformat)), тому запис у пам’яті обмежений однією конфігурацією кадрів.
write() повертає об’єкт потоку, щоб можна було ланцюжково з’єднувати виклики. Запис не з кінцевого зміщення файлового потоку обрізає решту файлу – корисно для редагування збереженої послідовності, але ризиковано, якщо позиція наступного запису була ненавмисно змінена попереднім seek().
sync() скидає очікуючі записи на диск для файлових потоків (для потоків у пам’яті це пуста операція) та має викликатись periodично при тривалому записі, щоб уникнути втрати хвостової частини запису у разі перезавантаження камери до закриття файлу. Деструктор автоматично закриває потік, коли ImageIO виходить із зони видимості, але явне close() є правильною практикою.
5.33.3. Відтворення¶
read() читає кадр за поточним зміщенням, просуває зміщення та повертає новий Image. Результат залишається у кадровому буфері при copy_to_fb=True (за замовчуванням), тому повернуте зображення доступне для малювання через попередній перегляд IDE; при copy_to_fb=False кадр розміщується у купі MicroPython.
# Loop a recorded stream at its natural frame rate
stream = image.ImageIO("/sdcard/run.bin", "r")
while True:
img = stream.read()
# img is now in the frame buffer; the IDE shows it
# and the script can run any analysis it likes
Два ключових аргументи керують поведінкою відтворення. loop=True (за замовчуванням для файлових потоків) повертає покажчик читання на початок після досягнення кінця запису, тому виклик ніколи не повертає None; loop=False повертає None після вичерпання запису і цикл викликача завершується. pause=True (за замовчуванням) блокує виклик до закінчення міжкадрового інтервалу, записаного під час запису, тому частота кадрів відтворення відповідає вихідній частоті захоплення; pause=False повертає результат негайно, що корисно для аналітичних конвеєрів, які хочуть обробити запис якомога швидше, не дотримуючись вихідного синхронізму.
Той самий шаблон циклу працює для потоків у пам’яті, за винятком того, що loop ігнорується – читання за межі потоку в пам’яті викидає EOFError. Очікуваний шаблон для кільця в пам’яті – це явне seek() назад на нуль при необхідності зациклення.
5.33.5. Записи, відтворювані на хості¶
Потоки ImageIO є правильним інструментом, коли запис буде відтворюватись на камері – вони зберігають кожен захоплений кадр у його рідному форматі пікселів, міжкадровий інтервал записується точно, а подальший скрипт може переглядати їх покадрово, виконувати переходи та повторний аналіз без втрат. Однак вони не є правильним інструментом, коли запис має відтворюватись на хості – робочій станції, телефоні, веб-плеєрі. Хост очікує стандартний відеоконтейнер, а не формат на диску OpenMV з магічним заголовком.
Два окремих модулі охоплюють випадок відтворення на хості. Модуль mjpeg записує Motion JPEG: послідовність JPEG-стиснутих кадрів, запакованих у єдиний AVI-подібний контейнер, який VLC, QuickTime, ffmpeg та стандартний веб-тег відео відтворюють безпосередньо. Модуль gif записує анімований GIF: послідовність нестиснутих (або стиснутих з палітрою) кадрів із явними затримками на кадр, відтворюваних у будь-якому веб-браузері або засобі перегляду зображень, що підтримує анімовані GIF.
Модуль mjpeg є природним вибором для тривалих записів. JPEG-стиснення тримає розмір файлу в розумних межах – порівнянно з to_jpeg() при заданій якості, кадр за кадром – тому розширені сеанси захоплення залишаються в межах бюджету SD-карти. Використання майже відповідає записуванню через ImageIO:
import mjpeg
m = mjpeg.Mjpeg("/sdcard/run.mjpeg")
while running:
m.add_frame(csi0.snapshot(), quality=85)
m.close()
mjpeg.Mjpeg приймає ті самі позиційні аргументи та ключові аргументи масштабування в стилі малювання, що й інші методи image, тому запис може масштабуватись, обрізатись або перетворюватись за палітрою для кожного кадру під час вставки. Аргументи конструктора width та height за замовчуванням відповідають розмірам основного кадрового буфера та фіксують вихідну роздільну здатність; кожен доданий кадр масштабується (зі збереженням пропорцій) для вписування. sync() скидає файл на диск під час тривалого запису, а close() завершує контейнер – файл Motion JPEG, що не був коректно закритий, не придатний до відтворення, тому дисципліна тут важлива.
Модуль gif є природним вибором для коротких записів, які поширюються без обробки нетехнічному глядачу – кілька секунд дії, захоплені для демонстрації, анімована ілюстрація для документації, кліп події, вбудований у повідомлення чату. Кадри GIF зберігаються нестиснутими (або стисненими з палітрою при 7-бітній глибині кольору), що робить файли значно більшими на секунду, ніж Motion JPEG, і унеможливлює формат для записів довших кількох секунд, але результат відразу відкривається у будь-якому браузері:
import gif
g = gif.Gif("/sdcard/clip.gif")
while running:
g.add_frame(csi0.snapshot(), delay=10)
g.close()
Аргумент delay у add_frame() – це час відображення кадру у сотих долях секунди (10 означає 100 мс на кадр, або 10 кадрів/с), що є стандартним засобом керування відтворенням GIF. Ключовий аргумент loop конструктора встановлює, чи буде отриманий кліп автоматично зациклюватись у переглядачах (за замовчуванням True, що відповідає стандартному очікуванню від «анімованого GIF»).
Три шляхи запису охоплюють між собою типові випадки: ImageIO – для повторного оброблення на камері, Motion JPEG – для тривалих записів на хості, анімований GIF – для коротких записів на хості. Вибір між ними залежить від того, хто відтворює запис. Подальша стадія, що виконується на самій камері, читає ImageIO; хостова робоча станція або веб-переглядач читає MJPEG або GIF.
5.33.6. Шаблон «тригер та відтворення»¶
Корисний шаблон поєднує потік у пам’яті з умовою тригера. Камера безперервно записує у кільцевий буфер у пам’яті з count слотами, перезаписуючи найстаріший слот при кожному оберті. Коли спрацьовує умова тригера (пляма з’являється у кадрі, подія руху перевищує поріг, натискається кнопка), застосунок робить знімок вмісту кільця – count найостанніших кадрів – і записує їх у файловий потік на SD-карті. Результатом є попередній запис тригера, що захоплює секунди до події, яку камера фактично помітила, а не лише секунди після – це класичне обмеження примітивного запису «захоплення за тригером».
Реалізація проста, як тільки в руках є класи потоків: потік у пам’яті фіксованого розміру слугує кільцем (з явним seek() до нуля при досягненні кількості слотів), основний цикл захоплює дані в нього на кожній ітерації, а обробник тригера зчитує потік у пам’яті кадр за кадром і записує кожний у файловий потік, іменований за мітками часу тригера.