Eyeduino — техническое зрение на Ардуино. Часть вторая

Вторая часть статьи от Эдуарда Петренко, из которой вы узнаете, как самостоятельно собрать и запрограммировать робота, схожего по функционалу с Eyeduino.

zrenie-dlya-Arduino-ili-kak-rabotat-s-video-imeya-2-5-kb-operativnoi-pamyati-9

«… нет у революции конца» — так воспевали в советское время непреходящие последствия массового слома уклада и представлений в начале 20-го века. Мне ломать ничего не хотелось бы, хотелось бы, наоборот, созидать, поэтому мы только слегка раздвинем привычные представления о том, что можно делать при помощи Ардуино.

Оказывается, помимо элементов «умного дома» и мобильных роботов с простыми сенсорами Ардуино позволяет работать с самым, наверное, интересным датчиком — видеокамерой, правда, в весьма низком разрешении — 32х32 чёрное/белое. Возможно, из платформы можно выжать и побольше, даже скорее всего, но я пока не знаю задач, для которых, сажем, 32х32 — мало, а вот 64х64 — уже нормально.

Так вот, 32х32 чб картинки на Ардуино — реальность, и дальше я хочу показать, как можно жить с этой реальностью, причём получать от жизни удовлетворение. Платформу, позволяющую работать с видеоизображениями на Ардуино, продолжим называть Eyeduino (или Eyeduino+, поскольку некоторый прогресс после RobobtChallenge проекта уже есть).

Один вопрос, который придётся решить, чтобы оказаться в мире Eyeduino — hardware. Простой путь — «купи и пользуйся» — предложить я пока не могу: в планах есть выпуск Eyeduino shield, но для этого надо будет научиться разводить платы для производства. Кстати, если кто-то из читателей готов тут помогать — добро пожаловать! Есть интересные идеи, например: разместить на шилде ещё колодки для Arduino Nano или Pro Mini: тогда обработку изображений можно было бы возложить на «гостевую» Ардуино, а в основную передавать изображение или уже обработанные данные по UART или I2C интерфейсам, дабы не волноваться о постоянном «отвлечении» на прерывания.

Можно также добавить конвертацию питания для камер (типично для мелких охранных камер, которые мы используем, — 9/12 В). Так, я отвлёкся.

zrenie-dlya-Arduino-ili-kak-rabotat-s-video-imeya-2-5-kb-operativnoi-pamyati-5

Второй способ — спаять схему самому. Схему мы не скрываем, детали стоят копейки, часть мы покупали в «Электронщике», часть — в «Чип-Дипе». Готовы делиться подробностями. Какой путь выбрать — решать Вам, пока реальный — второй.

Итак, предположим, плата у нас есть. Мы её цепляем на какие-то порты. Какие порты использовать, определяется функциональным назначением портов, задействованных в общении контроллера с Eyeduino. Наиболее болезненное ограничение — использование двух портов, на которые можно повесить внешние прерывания (порты можно посмотреть в справке по функции attachInterrupt(). Более подробно порты описаны в материалах по конкретным платам.

Нам понадобятся два порта с прерываниями (для обработки событий нового кадра и новой (десятой) строки), цифровой порт для чтения данных с компаратора и порт для управления (enable/disable) прерываниями по строкам (реализовано благодаря замечательному свойству «двойного» входа счётчика К155ИЕ1, имеющего два входа, сопряжённых логическим «И». Т.е. когда мы не хотим отзываться на прерывание каждые 600 микросекунд (примерно), мы ставим один из входов в «0», когда хотим — в «1», для этого нам и нужен цифровой выход. Второй вход счётчика получает строчный сигнал с LM1881.

Привожу пример конфигурации портов для нашего робота «Стремительный», в связи с размещением на нём камеры повышенного до «Стремительный-Зоркий» (между прочим, довольно уверенно победил в номинации Дорога-2 с прерывистыми участками на Форуме МГУПИ в апреле 2014). Робот потсроен на Ардуино Мега 2560.

#define INTERRUPT_PORT_FRAME

#define INTERRUPT_PORT_LIN<BrE

#define PORT_PIXEL 5

#define PORT_LINE_ENABLE 6

Обратите внимание, константы INTERRUPT_* обозначают не номер порта, а номер прерывания (опять же, см. attachInterrupt()).

А теперь хорошие новости. Функциональность Eyeduino существует уже в виде библиотеки EyeduinoLight. Light — потому что есть идея сделать библиотеку более правильной, объектно-ориентированной, позволяющей применять алгоритмы обработки изображений как отдельные объекты-операторы (паттоерн «декоратор», по-моему), причём реализованные как на Processing, так и на C++ (под Ардуино).

Такой подход позволит визуально исследовать применимость операторов на Processing, а потом быстро переносить функциональность на Ардуино (робота). Но пока нужны базовые вещи: продемонстрировать работоспособность основных алгоритмов и увидеть мир глазами Eyeduino. Это и позволяет библиотека EyeduinoLight с прилагающимися примерами на Ардуино и Processing.

zrenie-dlya-Arduino-ili-kak-rabotat-s-video-imeya-2-5-kb-operativnoi-pamyati-3

Начнём с начала: установим библиотеку и научимся конфигурировать окружение. Для этого была разработана утилита eynopcfg (Eyeduino plus config). Открываем исходники утилиты и просталяем значения констант INTERRUPT_* и PORT_* в соответствии со своей разводкой. После этого загружаем программу и в SerialMonitor видим вывод, который надо просто скопировать в начало программы, которая будет использовать библиотеку.

Довольно просто по-моему. Конечно, здорово было бы обойтись одним #include, уверен, это даже вполне возможно, но с ходу я такое сделать не смог, а много времени тратить на то, чтобы #ifdef’ами забивать маппирования портов и пинов для разных вариантов я не готов. Если кто подскажет хороший способ — буду благодарен. Суть проблемы в следующем: в реализации библиотеки используются внутренности реализации digitalRead(), для скорости вызовы функций digitalPinToTimer(), digitalPinToBitMask(), digitalPinToPort() заменены макроподстановками.

Функции реализованы в runtime, compile-time замены с ходу я не придумал. Последствия: необходимость врезать макроподстдановки из SerialMonitor и невозможность отделения реализации в .cpp, поскольку непонятно, как заставить эти макроподстановки действовать при компиляции библиотеки.

zrenie-dlya-Arduino-ili-kak-rabotat-s-video-imeya-2-5-kb-operativnoi-pamyati-10

Итак, как использовать библиотеку, мы вроде бы поняли. Проверить это несложно: есть пример ELPrint, который выводит картинку с камеры при помощи символов ‘ ‘ (пробел)и ‘#’. Заодно видим основные функции EyeduinoLight: elInit() при инициализации (в setup()) , elRequestImage() и elIsImageReady() в loop() и функции для доступа к полученной картинке eliaIsDefined() — получено реально значение данного пикселя и eliaIsBlack(), обе эти функции — в showImage(). Если всё это полетело (заработало) и в SerialMonitor выводятся картинки — вот оно! Дверь в новый мир отперта, осталось только её подтолкнуть.

Толкать дверь мы будем при помощи другого примера для той же библиотеки: ELImage2Serial. Делает этот пример, по сути, то же, но в Serial выдаёт картинки в несколько закодированном виде, поэтому картинка занимает не 1KB, а вчетверо меньше (не в восемь раз. потому что для удобства восприятия передаётся шестнадцатиричной кодировкой). Если использовать скорость 115200, можно передать 57.6 fps, нам такая скорость не грозит. А сколько грозит? Увидите дальше.

Пример ELImage2Serial по общей логике построения выглядит почти двойником своего предшественника, только в Serial уходит не картинка, а строка примерно такого вида:

{EYNOIM}FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000800100008001FF010001FF0F0000003E0002007C0002007C000200F8000400F8000400F8000400F8000800F8001800FC001000FC013000FC006000FC01E000FC01E000FC01C000FE00C000FE01C000FC018000FE018000FE018001

{EYNOIM}FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000008000000080010000000100008001FF010001FF1F0001003E00020078000200F800020078000200F8000400F8000400F8000800FC001800FC003000FC013000FC016000FC006000FC01E000FC01C000FC01C000FC01C000FE018000FE018000FE018001

{EYNOIM}FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000080000000000100000001FF030001FF0F0001003E000200780002007C00020078000200F8000400F8000400F8000800FC000800FC003000FC013000FC007000FC006000FC01E000FC01C000FC01C000FC01C000FE018000FE018000FE01C001

В принципе, это — картинки, нужно только средство для просмотра этих картинок.

Это средство, да ещё такое, что позволило бы работать с Serial, мы получим у ближайшего родственника Ардуино — языка и платформы разработки Processing. Скачиваем его с processing.org и разрабатываем скетчи. Небольшой период знакомства с технологией, привыкания — и вот они, работают!

Как зачастую бывает, есть несколько «но», вот они:

    •  не удалось скомпилировать код как самостоятельное приложение, запускаемое вне среды Processing. Точнее, скомпилировать удаётся, но оно не работает. По нашим наблюдениям, происходит это когда мы используем библиотеку Serial (import processing.serial.*;), т.е. в случае Eyeduino+ всегда. Видимо, проблема — в возможности доступа из Java к оборудованию. Скорее всего, проблему можно решить, порывшись в форумах (среду Ардуино же как-то скомпилировали, в том числе SerialMonitor

 

  •  код упрощён в том плане, что из коллекции последовательных интерфейсов выбирается первый. Если интерфейсов несколько, это может не работать, решить проблему можно несколькими способами, хороший из них я не выбрал, поэтому и реализовывать не стал.

Смотрим примеры.

ElDrawImageсамое простое из приложений, декодирует картинку, отображает её и даже считает frame rate. У нас он оказался, кстати, 12-13 fps, т.е. передавался каждый второй кадр с камеры (прерывание кадров у нас настроено на бит чётности кадров, чтобы брать только прямые или только обратные кадры, таких 25 fps). Не густо? В общем, да, но вопрос: для каких применений. К тому же, возможно, что если всё вычислять на Eyeduino и ничего не передавать, половину кадров мы не потеряем. А можно ещё замахнуться на обратные кадры, но я пока не готов — пока не ясно, зачем. Но сам факт получения видеокартинки с Ардуино впечатляет!

ElDrawImage

Усугубим успех с  ElMassCenter. В проекте Eyeduino мы делали упражнение с охотой на таракана, там использовалось понятие центра масс тёмной области. В данном примере мы также находим и помечаем крестиком центр масс темноты. Вводится также интересное понятие ROI — region of interest, область изображения, на которой будем проводить свои вычисления. Для ROI и для центра масс создаём соответствующие локальные классы, правда, без внутренней логики, просто структуры. Для того, чтобы управлять границами ROI, используем обработчик нажатий клавиатуры. Работает!

ElMassCenter

Дальше — выделение отдельных объектов на картинке,  ElSpotIdentification. Функционал ROI сохранён, задача приложения — «раскрасить» разные области (пятна) разными номерами. Тут пришлось придумывать алгоритм, получился более-менее однопроходный, если не считать процедуры слияния пятен, но на практике она должна вызываться достаточно редко.

Для того, чтобы показать принадлежность точек пятнам, они выводятся разными цветами, цвет вычисляется следующим образом: компоненты R, G, B пропорциональны координатам центра масс пятна и его массе. Это сделано для того, чтобы снизить цветовое мерцание: если взять и зафиксировать палитру и привязать каждый цвет к номеру пятна, то появление одной отдельной чёрной точки приведёт к сдвигу цветов для некоторого набора пятен. В общем, мерцания практически нет, правда пятна одинакового размера и находящиеся рядом почти неотличимы по цвету.

ElSpotIdentification

Применить этот алгоритм мы планируем для Puck Collect: надо будет ехать туда, где больше всего пятен небольшого размера (шайб); есть ещё идеи.

Это мы ещё не пробовали всякие фильтры сглаживания, Собеля и т.п. Вы видите, дорогой читатель: дверь открыта, впереди увлекательный новый мир!

Автор: Эдуард Петренко  

  • Андрей Сурков

    Эдуард!
    Отличная статья! Так держать!