В этой статье я расскажу о продолжении работы над своим проектом стереокамеры на базе FPGA Gowin. В последней версии я добавил блок расчета попиксельной межкадровой разницы, используя встроенную в один корпус с ПЛИС SDR SDRAM память.
В предыдущей статье я рассказал о своем DIY проекте, стереокамере на базе FPGA, которая подключается к Arduino Portenta H7 и работает под управлением библиотеки машинного зрения OpenMV. FPGA в моем проекте управляет парой видео сенсоров, склеивает видео поток из двух источников и передает на Arduino. Поскольку в проекте оставались свободные ресурсы FPGA, я решил добавить аппаратную обработку видео в конвейер. В проекте используется чип GW2AR-LV18QN88 со встроенной SDRAM памятью, размера и быстродействия которой вполне хватит чтобы буферизировать кадры с камеры на максимальной скорости, это позволяет реализовать алгоритм межкадровой разности на стереоизображении. Чтож, начнем.
Для чего используется алгоритм
Очевидным образом покадровая разность изображений может быть использована для детекции движения в кадре. Для статичного положения камеры можно брать один референсный кадр за некоторый промежуток времени, и сравнивать входящие кадры с ним, в этом случае любой объект, появившийся в кадре будет определятся на кадре, даже если он прекратил движение после попадания в кадр. При перемещении камеры или изменении освещенности референсный кадр становится непригодным для дальнейшей работы, необходимо заменить его новым кадром. Библиотека OpenMV содержит пример такого алгоритма:


Другой вариант – сравнивать текущий кадр с предыдущим. В этом случае движение камеры не так критичны. Мой вариант реализации основан именно на этом принципе – я рассчитываю разность между текущим входящим кадром и предыдущим, записанным в SDRAM буфер, подменяю выходной поток пикселями, содержащими разность кадров и параллельно записываю необработанный входной поток в SDRAM буфер для расчета разности на следующем кадре. Ниже представлен пример вывода изображения в таком режиме

Реализация на FPGA
Как и было сказано ранее, алгоритм реализуется за счет вычитания текущего входного кадра и предыдущего, записанного в память. Структурная схема блока представлена ниже:

Никаких особых алгоритмических ухищрений тут не потребовалось, реализация довольно простая - копим во входном FIFO блок данных для записи, записываем в память, параллельно читаем из буфера предыдущий блок из такой же позиции в кадре, записываем результат разности в выходное FIFO. По достижению конца кадра меняем буфер чтения и записи местами. Для меня интерес представляло изучение SDRAM памяти, которая расположена в едином корпусе с FPGA, до этих пор я никогда с подобными чипами не работал. Характеристики встроенной SDRAM памяти:
Разрядность - 32 бита
Емкость - 64Мб
Максимальная частота работы - 166Мгц
Теоретически память может обеспечить пропускную способность в 540 кадров в секунду на монохромном разрешении 1280x480 и максимальной частоте, запас многократный.
Для работы с памятью используется специальный IP блок : SDRAM Controller (With embedded SDRAM)

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


Чтобы начать процедуру чтения или записи, необходимо сформировать импульс на входе rd_n или wr_n соответственно. Процедура не может быть начата при неактивном сигнале init_done или активном сигнале busy_n. Длина данных указывается со смещением в 1 слово, таким образом, чтобы записать 64 слова данных на входе data_len, нужно подать значение 63. Как видно из диаграмм чтения и записи, модуль не содержит сигналов обратной связи, это означает что весь блок данных длиной data_len должен быть предварительно готов к непрерывной выдаче из промежуточного буфера в случае записи, и приемный буфер должен быть готов принять весь блок данных из контроллера в случае чтения. В противном случае данные будут потеряны.Чтобы этого не происходило, управляющий автомат перед стартом каждой транзакции проверят накоплено ли во входном FIFO необходимое количество данных и есть ли в выходном FIFO свободное место для приема. Только после соблюдения этих условия запускаются транзакции записи и чтения.
Тестирование
Для теста я написал скрипт, который с помощью методов OpenMV find_blobs находит изображения отдельно на левом и на правом полукадре, и рассчитывает их смещение друг относительно друга. В верхнем левом углу я вывел число и полоску с индикатором, по ним можно определить относительное расстояние до движущегося объекта. В данном тесте я столкнулся с проблемой "фрагментации" разностного изображения, т.к. за один кадр некоторые области внутри объекта не успевают достаточно измениться и на выходном изображении они не видны. Для достижения более стабильных результатов расчета расстояния я планирую добавить функции морфологического преобразования в конвейер, для того чтобы соединить отдельные изолированные области пикселей одного объекта в единую область. Ссылка на видео с экспериментом: