5.33. Потоки ImageIO¶
Методы save() и to_jpeg() покрывают случай ввода-вывода для одного кадра: приложение захватывает кадр, кодирует его и куда-то отправляет. Другой класс приложений требует случая последовательности: записать множество кадров подряд с естественной частотой захвата, сохранить их там, откуда их можно будет извлечь позже, и воспроизвести с правильной скоростью. Скрипт для сбора обучающих данных захватывает несколько сотен примерных кадров для конвейера машинного обучения; журнал станции контроля записывает каждую захваченную деталь для прослеживаемости; скрипт разработки воспроизводит сохранённую последовательность, чтобы протестировать новый алгоритм на данных, которые ранее были захвачены в реальном времени.
Класс ImageIO — это устройство записи/воспроизведения модуля image. Один поток содержит последовательность кадров Image — возможно, разных размеров и форматов пикселей — вместе с межкадровым интервалом каждого из них, так что воспроизведение может воссоздать исходную частоту кадров. Доступны два хранилища: файл в файловой системе или буфер фиксированного размера в оперативной памяти.
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()
Поток в памяти располагается в буфере оперативной памяти, выделяемом при создании. Конструктор принимает кортеж из трёх элементов (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() сбрасывает ожидающие записи на диск для файловых потоков (для потоков в памяти он не выполняет никаких действий) и должен вызываться периодически при длительной записи, чтобы избежать потери конца записи, если камера перезагрузится до закрытия файла. Деструктор автоматически закрывает поток, когда 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 и стандартный веб-тег video воспроизводят напрямую. Модуль 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 принимает те же позиционные ключевые слова и ключевые слова масштаба в стиле рисования, что и другие методы изображения, так что запись можно масштабировать, обрезать или отображать через палитру для каждого кадра при добавлении. Аргументы 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() к нулю, когда смещение достигает числа слотов), главный цикл захватывает в него на каждой итерации, а обработчик триггера читает поток в памяти кадр за кадром и записывает каждый в файловый поток, названный по временной метке триггера.