[Из песочницы] Интерактивный пол на Android

Наверное, многие из вас видели интерактивные игры для детей в торговых центрах. Где динамическая сцена проецируется на пол, а рядом установленный сенсор определяет точки касания с поверхностью и преобразует их в события для приложения на управляющем компьютере. После поиска в интернете информации об этом устройстве оказалось, что это довольно дорогая игрушка. Например, китайские клоны стартуют с ценника в $1200, а что-то более оригинальное стоит уже $10 тыс. После анализа технической составляющей продукта было решено сделать аналогичное устройство самому.Железо проекта состоит из трех частей:

Сенсор глубины (в оригинале это ASUS Xtion); Управляющий компьютер (Cubieboard A80, ODROID-U3); Проектор. e12cc4ec57d14d998410c7845092f32c.gifВ идеале все железки вместе не должны стоить больше 700 долларов. Предполагалось, что соединить все три части должно быть относительно легко, так как в интернете есть такие библиотеки, как OpenNI и libfreenect, которые работают и на Android, и на Linux. Из-за недостатка опыта на раннем этапе казалось, что есть выбор и в железе, и в ОС; есть примеры открытого кода и соединить все вместе не составит большого труда. Через некоторое время после начала проекта оказалось, что это не так. Интеграция всех частей и даже запуск библиотек на целевом устройстве есть самая сложная задача. Пришлось выбирать между доступностью информации по настройке Linux и обилием приложений в маркете под платформу Android.

Однако, обо всем по порядку.

Для того, что бы сразу начать экспериментировать с железом, был куплен б/у сенсор Microsoft Kinect и проектор. Затем из квадратной профилированной трубы изготовлено вот такое крепление для проектора и сенсора:

cab44072aa464b1d9bb48178a26f4e84.JPG

В верхней части крепления приварен небольшой кусок уголка для монтажа к потолку. В местах изгибов трубы для усиления конструкции приварены пластинки в виде косынок. Проектор соединяется с креплением через треугольную пластину из фанеры. Для соединения сенсора с креплением используется специальный аксессуар для Kinect, который можно без проблем найти на ebay. В качестве управляющего компьютера для удешевления была выбрана плата Cubieboard A10, которую также без проблем можно найти на ebay. На момент написания статьи уже вышли Cubieboard A20 и A80, соответственно двух и восьми-ядерные аналоги. Если позволяет бюджет, то желательно купить A80, чтобы у системы был запас мощности для одновременной работы пользовательских приложений и сервиса по захвату и обработке данных от сенсора глубины. За питание платы и сенсора отвечает USB блок питания с выходным током на 4A. Проектор и сенсор соединяются с креплением так, что бы камера глубины была в одной плоскости с объективом проектора:

278e7b3f583149ebbe58fa571e852a7a.png

Модель проектора лучше выбрать такую, чтобы картинка получалась максимально большой с малого расстояния. На этом описание железной части можно закончить. Теперь о программном обеспечении.

В качестве операционной системы была выбрана сборка Android под Cubieboard с забавной заставкой на рабочем столе. Мне пришлось немного подправить файлы конфигурации и скомпилировать сборку самому из-за того, что в Android нельзя изменить последовательность загрузки модулей, точнее можно, но до следующей перезагрузки системы.

Для внедрения событий потребовался модуль драйвера сенсорного экрана sun4i-ts. На самом деле, тестовое приложение реализует TUIO клиент, но, как оказалось, даже с драйвером сенсорной панели существующий под Android сервер TUIO не поддерживает multitouch события. Возможно, это связано с самим драйвером сенсорной панели sun4i-ts под Allwinner. Исходя из этих фактов был выбран вариант с прямым внедрением событий.

Для захвата данных о глубине используется легковесная и быстрая библиотека libfreenect, которая, в свою очередь, использует libusb для передачи данных по USB. Полученные данные о глубине обрабатываются с помощью OpenCV для Android. Суть обработки довольно простая: карту глубины необходимо преобразовать в замкнутые контуры с длиной не меньше, чем пороговая, для исключения ложных срабатываний — и найти их геометрические центры.

В самом начале работы, когда на сцене нет ни одного объекта, приложение строит карту глубины фона, затем в процессе работы карта используется для отделения целевых объектов от фона. Приложение представляет собой управляющую часть и сервис с кодом на С/C++. Вся логика обработки и захвата данных о глубине реализована на С/C++. Часть кода по работе с TUIO и OpenCV была взята из этого проекта на github.

Рассмотрим код более подробно. В коде, как я уже говорил, используется OpenCV. В самом начале работы приложение строит карту глубины:

1 void STouchDetector: process (const uint16_t& depthData) { 2 frmCount++; 3 // create background model (average depth) 4 if (frmCount < BackgroundTrain) { 5 depth.data = (uchar*)(&depthData); 6 buffer[frmCount] = depth; 7 } 8 else { 9 if (frmCount == BackgroundTrain) { 10 // Calculate average depth based on all frames from buffer 11 average(buffer, background); 12 Scalar bmeanVal = mean(background(roi)); 13 double bminVal = 0.0, bmaxVal = 0.0; 14 minMaxLoc(background(roi), &bminVal, &bmaxVal); 15 LOGD("Background extraction completed. Average depth is %f min %f max %f", bmeanVal.val[0], bminVal, bmaxVal); 16 } В строке 6 данные о глубине сохраняются в буфере. Следует отметить, что буфер имеет тип std::vector. Это массив из матриц и присваивание в строке 6 — это фактически копирование всех пикселей кадра в буфер. После достижения счетчиком кадров порогового значения BackgroundTrain вызывается функция подсчета среднего значения глубины по всем кадрам в строке 11: 1 void STouchDetector::average(vector& frames, Mat1s& mean) { 2 Mat1d acc (mean.size ()); 3 Mat1d frame (mean.size ()); 4 for (unsigned int i=0; i

1 // Update 16 bit depth matrix 2 depth.data = (uchar*)(&depthData); 3 // Extract foreground by simple subtraction of very basic background model 4 foreground = background — depth; 5 6 // Find touch mask by thresholding (points that are close to background = touch points) 7 touch = (foreground > TouchDepthMin) & (foreground < TouchDepthMax); 8 9 // Extract ROI 10 Mat touchRoi = touch(roi); 11 12 // Find contours by depth data 13 vector< vector > contours; 14 vector touchPoints; 15 findContours (touchRoi, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE, Point2i (xMin, yMin)); 16 17 for (unsigned int i=0; i < contours.size(); i++) { 18 Mat contourMat(contours[i]); 19 // Find touch points by area thresholding 20 if ( contourArea(contourMat) > ContourAreaThreshold) { 21 Scalar center = mean (contourMat); 22 Point2i touchPoint (center[0], center[1]); 23 touchPoints.push_back (touchPoint); 24 } 25 } В последней части происходит отправка событий в систему. Координаты для событий нажатия на поверхность берутся из ранее созданного массива touchPoints. 1 // Send TUIO cursors 2 tuioTime = TuioTime: getSessionTime (); 3 tuio→initFrame (tuioTime); 4 5 for (unsigned int i=0; i < touchPoints.size(); i++) { // touch points 6 float cursorX = (touchPoints[i].x - xMin) / (xMax - xMin); 7 float cursorY = 1 - (touchPoints[i].y - yMin) / (yMax - yMin); 8 TuioCursor* cursor = tuio->getClosestTuioCursor (cursorX, cursorY); 9 10 LOGD («Touch detected %d %d», (int)touchPoints[i].x, (int)touchPoints[i].y); 11 12 // TODO improve tracking (don’t move cursors away, that might be closer to another touch point) 13 if (cursor == nullptr || cursor→getTuioTime () == tuioTime) { 14 tuio→addTuioCursor (cursorX, cursorY); 15 eventInjector→sendEventToTouchDevice ((int)(touchPoints[i].x — xMin), 16 (int)(touchPoints[i].y — yMin)); 17 LOGD («TUIO cursor was added at %d %d», (int)touchPoints[i].x, (int)touchPoints[i].y); 18 } else { 19 tuio→updateTuioCursor (cursor, cursorX, cursorY); 20 } 21 } Для отправки событий в систему вызывается функция sendEventToTouchDriver (), а для отправки сообщения TUIO серверу функции addTuioCursor () и updateTuioCursor ().В конце обсуждения кода хотелось бы рассказать о модуле отправки событий системе. Модуль называется stouchEventInjector.cpp. В самом начале работы в конструкторе с помощью функции open () открывается файл событий устройства ввода /dev/input/eventX, где X — это число. Модуль сам пытается найти дескриптор, связанный с нужным драйвером (sun4i_ts). Для этого последовательно вызывается функция getevent с ключем -pl для каждого существующего файла /dev/input/eventX. Отправка события, на самом деле, — это запись в файл /dev/input/eventX структуы uinput_event с помощью функции write (). У тачскрина имеется своя система координат с максимальным и минимальным значением по осям, в случае с sun4i-ts максимальное число по обеим осям ох и оу равно 4095. Последовательность команд, которую нужно отправить для симуляции нажатия на тачскрин можно найти в исходниках в функции sendTouchDownAbs ().

Для автоматического запуска драйвера тачскрина после старта устройства, как я говорил в начале, нужно изменить конфигурацию сборки Android. Для сборки Android последнюю версию Ubuntu в моем случае версия была 14.10. Исходный код берем отсюда Cubieboard A10 Android и распаковываем. Нам необходимо изменить два файла:

android/device/softwinner/apollo-cubieboard/init.sun4i.rc android/frameworks/base/data/etc/platform.xml В файле init.sun4i.rc необходимо раскоментировать строку insmod /system/vendor/modules/sun4i-ts.ko. В файле platform.xml необходимо добавить группы usb, input и shell в секцию INTERNET: После внесения изменений запускаем сборку командой: ./build.sh -p sun4i_crane -k 3.0 Для сборки версии Android ICS необходим компилятор GCC версии 4.6 и make версии 3.81. Если версия компилятора и make отличается от необходимой, то ее можно изменить командами: sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.6 60 --slave /usr/bin/g++ g++ /usr/bin/g++-4.6 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.9 40 --slave /usr/bin/g++ g++ /usr/bin/g++-4.9 sudo update-alternatives --config gcc sudo mv /usr/bin/make /usr/bin/make40 sudo update-alternatives --install /usr/bin/make make /usr/local/bin/make 60 sudo update-alternatives --install /usr/bin/make make /usr/bin/make40 40 sudo update-alternatives --config make Далее следуем инструкциям на странице Cubieboard A10 Android. В процессе сборки могут возникнуть ошибки компиляции. Подсказки для исправления ошибок можно найти в секции Fix building issues в файле fix_android_firmware.readme в репозитории с исходным кодом. Для подключения платы к ПК необходимо добавить правила для подключения устройства по USB для этого создаем файл: /etc/udev/rules.d/51-android.rules И добавляем следующую строку: SUBSYSTEM==«usb», ATTRS{idVendor}==»18d1», ATTRS{idProduct}==»0003», MODE=»0666» Чтобы изменения вступили в силу, перезапускаем сервис udev: $sudo chmod a+rx /etc/udev/rules.d/51-android.rules $sudo service udev restart Подключаем плату к ПК и заливаем образ прошивки sun4i_crane_cubieboard.img с помощью утилиты LiveSuit. Перед установкой внимательно прочитайте инструкцию к LiveSuit, если установить неправильно, то приложение не сможет загрузить образ на устройство. После загрузки образа и перезапуска платы можно установить и запустить приложение SimpleTouch. Приложение автоматически запустит сервис, который захватывает/обрабатывает данные от Kinect и отправляет события системе. Приложение можно просто свернуть и запустить какую-нибудь игру из PlayMarket.Исходный код можете скачать с bitbucket.

Видео демонстрации работы:

[embedded content]

© Habrahabr.ru