4.15. Буферы кадров

После инициализации датчика камеры он непрерывно выдаёт кадры со своей частотой кадров – по одному новому кадру в каждый период кадра, независимо от того, готово ли приложение его принять. Каждому кадру нужно место в RAM, куда он попадёт, иначе он будет потерян. Пул буферов кадров – это место, где эти кадры находятся между выходом из DMA и обработкой пользовательским кодом, а количество буферов кадров, которое камера держит в этом пуле, определяет, как DMA и приложение разделяют их между собой. Этот выбор предоставляется через framebuffers(), и доступны четыре режима, выбираемые по количеству буферов.

4.15.1. Один буфер (count = 1)

Один буфер кадра в RAM. DMA заполняет его; приложение читает из него; следующий вызов snapshot() не может начаться, пока приложение не освободит буфер, потому что один и тот же буфер нужен для обоих.

Камера и приложение работают синхронно, шаг в шаг. DMA вынужден ждать, пока приложение завершит работу, а приложение вынуждено ждать, пока завершит работу DMA, что означает, что достижимая частота кадров в лучшем случае составляет половину частоты кадров датчика – каждый второй кадр, выдаваемый датчиком, приходит, когда буфер занят, и теряется.

Этот режим самый экономичный по RAM и самый медленный по пропускной способности. Используйте его только тогда, когда RAM слишком мало, чтобы выделить второй буфер.

4.15.2. Двойная буферизация (count = 2)

Два буфера кадров в RAM: один задний буфер, который заполняет DMA, и один передний буфер, из которого читает приложение. Когда приложение завершает работу с передним буфером, две роли меняются местами, и DMA начинает заполнять только что освобождённый буфер, пока приложение читает из только что заполненного.

Пока приложение обрабатывает каждый кадр менее чем за один период кадра камеры, приложение видит полную частоту кадров датчика – следующий кадр DMA уже ждёт в заднем буфере, когда приложение снова вызывает snapshot(). Однако в тот момент, когда время обработки превышает один период кадра, частота уменьшается вдвое: камера произведёт два кадра за то время, которое приложение тратит на обработку одного, и доставлен будет только второй из этих двух.

После этой точки частота плавно ухудшается с увеличением времени обработки. Каждый раз, когда DMA завершает новый кадр в заднем буфере, пока приложение ещё работает над передним буфером, новый кадр перезаписывает предыдущий захват на месте, а не отбрасывается. Приложение всегда получает самый свежий кадр, произведённый камерой, при следующем вызове snapshot(), и достижимая частота приложения становится обратной величиной времени его обработки.

4.15.3. Тройная буферизация (count = 3)

Три буфера кадров в RAM: два задних буфера, по которым DMA проходит циклически, и один передний буфер, над которым приложение работает в данный момент. Это режим по умолчанию, который выбирает OpenMV Cam, когда RAM достаточно, с автоматическим переходом на двойную или одиночную буферизацию, когда её нет.

Третий буфер полностью развязывает частоту кадров камеры от частоты кадров приложения. У DMA всегда есть буфер для записи; у приложения всегда есть буфер для чтения; при каждом вызове snapshot() самый свежий готовый задний буфер становится новым передним буфером, а предыдущий передний буфер освобождается для DMA. Частота кадров приложения соответствует времени, которое фактически требуется на обработку каждого кадра – без перехода на 1/2 частоты, в который попадает двойная буферизация, когда время обработки лишь немного превышает один период кадра.

4.15.4. Видео-FIFO (count = 4 или более)

Четыре или более буферов кадров в RAM, организованных в виде кольца кадров, захваченных один за другим. Каждый кадр, который доставляет камера, ставится в очередь FIFO, и snapshot() возвращает самый старый кадр из очереди, а не самый свежий. Приложение проходит по захваченным кадрам в порядке захвата, за то время, которое фактически есть на каждый из них.

Этот режим – правильный выбор, когда важен каждый кадр и ожидаются кратковременные задержки обработки: запись видео на SD-карту, чей стек хранения может блокироваться на десятки миллисекунд во время стирания, потоковая передача по USB на хост, который ненадолго прекращает чтение, или буферизация короткой серии быстрого события для анализа в коде.

Две политики обрабатывают случай, когда FIFO заполняется до того, как приложение его опустошило.

  • Отбрасывать старые кадры (по умолчанию). Когда FIFO заполняется, все кадры в очереди, кроме активного, отбрасываются, чтобы следующий вызов snapshot() возвращал свежий кадр, а не устаревший. DMA продолжает захват всё это время, поэтому приложение всегда видит свежие данные после задержки. Это правильная политика, когда цель – поддерживать захваченный поток актуальным: запись видео, прямая потоковая трансляция.

  • Прекращать захват при переполнении. Передайте fflush=False в конструктор CSI, и DMA прекратит заполнять FIFO, когда он заполнен, оставив кадры в очереди нетронутыми. snapshot() продолжает возвращать кадры в порядке захвата, пока приложение их не опустошит, после чего DMA возобновляет работу. Это правильная политика, когда цель – сохранить каждый кадр короткой серии: захват быстрого движения для покадрового анализа в коде впоследствии.

См. csi.CSI.framebuffers() для полного описания API.

4.15.5. Триггерный режим

Альтернативой постоянно работающим режимам выше является триггерный захват, при котором датчик выдаёт кадр только тогда, когда snapshot() запрашивает его. Камера простаивает между снимками и начинает новую экспозицию каждый раз, когда приложение обращается к ней.

Цена этого – пропускная способность: триггерный захват не может перекрываться с предыдущим, поэтому максимальная достижимая частота кадров составляет половину обычной частоты датчика. Преимущество – в выборе момента экспозиции. Снимок точно контролирует, когда начинается экспозиция, что нужно приложению, когда экспозиция должна совпадать с внешним событием – вспышкой стробоскопа, датчиком положения конвейера, импульсом на линии GPIO – а не приходиться на тот момент скользящего кадра свободно работающего датчика, который случается, когда приложение готово его прочитать.

Триггерный режим зависит от датчика. На поддерживаемых датчиках он включается вызовом csi0.ioctl(csi.IOCTL_SET_TRIGGERED_MODE, True) и отключается передачей False.