Nexus 5 + JavaScript + 48 часов = сенсорная поверхность?

    Несколько недель назад в Минске проходил хакатон WTH.BY, в котором я решил принять участие. Его основной идеей было то, что это хакатон для разработчиков. Мы могли делать все, что угодно, лишь бы нам это было весело и интересно. Никаких монетизаций, инвестиций и менторов. Всё весело и круто!

    Идей для реализации у меня было много, но все они не дотягивали до какого-то «Вау!». Именно поэтому накануне мероприятия я пролистывал старые статьи хабра из раздела DIY и наткнулся на статью "Опыт создания multitouch стола". Это было то, что вызвало тот самый отсутствующий «Вау!» и я решил сделать отдаленный аналог из того, чтобы под рукой.

    Под рукой у меня оказалось стекло формата примерно А3, обычная бумага, маркер, мобильный телефон и ноутбук. Я быстро нашел себе сообщника Егора и началась активная работа.

    Картинки нет. И счастья нет. И денег тоже нет. И дальше будет только хуже.


    В общем, было решено сделать сенсорную поверхность, касания на которой распознавались бы нашей системой. Для этого я стащил из дома кусок обычного стекла, бумагу и маркер. Стекло мы положили на две стопки книг, на него приклеили скотчем лист бумаги, а снизу положили телефон фронтальной камерой вверх. Камера снимает изображение снизу, распознает изображение места прикосновения и передает их на ноутбук. Уже по ходу дела идея немного трансформировалась: распознавать нарисованные маркером на бумаге кнопки и определять нажатия на них. В первую очередь это случилось из-за того, что распознать точное место прикосновения проблематично из-за тени руки. А нарисованные маркером кнопки видны отчетливо и выделить их на изображении было легко.

    И тут изображения тоже нету. Мне вас очень жаль.
    Учитывая, что мой профиль в программировании — JavaScript, мы решили, что это будет веб-страница, которая открывается на телефоне. На ней захватывается видео изображение с фронтальной камеры, распознаются кнопки и ожидаются нажатия. При возникновении события информация передается с помощью сокетов на другую страницу на ноутбуке, которая делает, что ей понравится прикажут.

    Такую систему можно разбить на несколько логичных частей:
    • Захват видео
    • Предварительная обработка изображения
    • Поиск контуров
    • Определение нахождение пальца в контуре
    • Передача событий клиентской странице


    Рассмотрим каждую часть немного подробнее.

    Захват видео


    Уверен, что для вас не будет секретом, что используя метод getUserMedia можно получить изображение с видеокамеры и транслировать его в теге video. Поэтому создаем тег video, просим у пользователя разрешение на захват видео и видим себя в камеру.

    Немного кода
    var video = (function() {
            var video = document.createElement("video");
            video.setAttribute("width", options.width.toString());
            video.setAttribute("height", options.height.toString());
            video.className = (!options.showVideo) ? "hidden" : "";
            video.setAttribute("loop", "");
            video.setAttribute("muted", "");
            container.appendChild(video);
            return video
        })(),
        initVideo = function() {
            // initialize web camera or upload video
            video.addEventListener('loadeddata', startLoop);
            window.navigator.webkitGetUserMedia({video: true}, function(stream) {
                try {
                    video.src = window.URL.createObjectURL(stream);
                } catch (error) {
                    video.src = stream;
                }
                setTimeout(function() {
                    video.play();
                }, 500);
            }, function (error) {});
        };
    
    //...
    initVideo();
    



    Чтобы получить отдельный кадр из видео, будем использовать canvas и метод drawImage. Этот метод может принимать первым параметром тег видео и рисовать в canvas текущий кадр из указанного видео. Это как раз то, что нам нужно. Эту операцию мы будем повторять через определенные интервалы времени.

    var captureFrame = function() {
        ctx.drawImage(video, 0, 0, options.width, options.height);
        return ctx.getImageData(0, 0, options.width, options.height);
    };
    
    window.setInterval(function() {
        captureFrame();
    }, 50);
    


    Предварительная обработка изображения


    Теперь у нас есть элемент canvas, а в нем текущий кадр из видеопотока. Следующая задача — распознавание нарисованных кнопок.
    На самом деле вид, в котором возвращает данные метод ctx.getImageData(...), совсем неудобный для решения поставленной задачи. Поэтому прежде, чем приступить к непосредственному поиску контуров, приведем изображение к удобному формату.

    Метод getImageData возвращает большой массив данных, где последовательно описаны каналы каждого пикселя. А под удобным форматом я понимаю двумерный массив пикселей. Он интуитивно понятен и работать с ним гораздо приятнее.

    Грустная история об отсутствующей картинке или интернетыше-плохише


    Напишем небольшую функцию, которая преобразует данные в удобный для нас вид. При этом можно учитывать, что изображение, проходящее сквозь бумагу, очень похоже на черно-белое. Поэтому для каждого пикселя мы посчитаем среднюю сумму каналов и запишем ее в результирующий массив. В результате получаем массив, где каждый пиксель представлен значением от 0 до 255. По координатам можно обратиться к нужному пикселю и получить его значение: data[y][x].

    Удобный избыточный формат


    Мы пошли еще дальше и решили, что для каждого пикселя 255 возможных значений — это слишком много. Для распознавания контуров и нажатий достаточно двух значений — 1 и 0. Так в нашем проекте появилась функция getContours, которая получала на вход массив пикселей и переменную limit. Если значение конкретного пикселя больше переменной limit, то он превращается в ноль (светлый лист), в противном случае становился единицей (часть контура или пальца).

    Удобный неизбыточный формат


    Код функции getContours
    var getContours = function(matrix, limit) {
        var x, y;
        for (y = 0; y < options.height; y++) {
            for (x = 0; x < options.width; x++) {
                matrix[y][x] = (matrix[y][x] > limit) ? 0 : 1;
            }
        }
        return matrix;
    };
    



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

    Некрасивое изображение превратилось в красивое черно-белое. Жаль, что вы этого не видете.


    Поиск контуров


    Вы когда-нибудь распознавали контуры и предметы на изображении? Я раньше никогда такого не делал. Быстрое гугление показало, что OpenCV должен решать эти задачи без особых проблем. На деле же оказалось, что портированные библиотеки имеют какие-то ограничения, а классификаторы нужно обучать. Все это было похоже на использование Grails для создания landing page.
    Именно поэтому мы продолжили поиски более простых решений и наткнулись на алгоритм жука (не уверен, что это общепринятое название, но в статье он назывался именно так).

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

    • Находим граничную точку. Граничная точка — это переход с белой точки на черную. Можно просто пройтись по массиву и найти первую попавшуюся.
    • Начинаем обход контура по двум простым правилам:
      • Если мы находимся на белой точке, то поворачиваем направо
      • Если мы находимся на черной точке, то поворачиваем налево
      При движении по точкам не забываем записывать координаты черных точек, на которых мы находимся, в результирующий массив. Впоследствии этот массив и будет контуром.
    • Завершаем обход контура в граничной точке, с которой начали.


    Без картинки вы все равно ничего не поймете


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

    Ничего не вижу, ничего не слышу


    Но кому нужен интерфейс из одной кнопки? Если уж делать, то по полной! Так и возникла задача поиска всех нарисованных кнопок. Решение оказалось простым: находим кнопку, запоминаем ее в массив, прямоугольник с кнопкой в данных заливаем нулями. Повторяем поиск до тех пор, пока массив не станет пустым. В результате получаем массив, содержащий все найденные кнопки.

    Я не буду заботиться об альтернативных подписях к изображениям


    Кстати, в процессе тестирования алгоритма пострадало одно стекло. К счастью был поздний вечер и я собирался домой. Утром я достал из окна раздобыл дома еще одно стекло и отправился продолжать разработку.

    Мне жаль. Кстати, я продаю автомобиль в Минске. Если вам это интересно, пишите мне в личку.


    Определение нахождение пальца в контуре


    Как же быть с нажатием кнопок? Тут все оказалось просто. При нахождении кнопки посчитаем сумму черных точек внутри нее. Я для себя эту величину называл «хэш кнопки». Так вот если на кнопку нажали, то хэш кнопки вырастает на ощутимое количество, которое явно превышает случайные шумы, помехи и минимальные движения бумаги и телефона относительно друг друга. Получается, что в каждом фрейме нужно считать хэш существующей кнопки и сравнивать его с исходным значением:
    • Если разница между значениями больше заданного значения, то считаем, что кнопка нажата и вызываем событие touchstart.
    • Если же до этого кнопка была нажата, а теперь сумма вернулась в норму, то считаем, что нажатие прекратилось и случилось событие touchend.


    Картинки больше нет. Но она обещала вернуться.


    Такой вот тач-скрин.

    Режим занудства
    Конечно же пытливый ум поймет, что такой подход — это огромный простор для ложных срабатываний. Если случайно создать тень над рядом находящейся кнопкой, то она тоже окажется нажатой.
    Ну в общем-то да. С этим можно пробовать бороться, устанавливая дополнительные проверки. Например можно создавать второй массив данных из нулей и единиц, но с более строгим лимитом черного цвета. Тогда только «наиболее черный» цвет останется на изображении. Это даст возможность предполагать, что в данных останется только место прикосновения пальцем к бумаге, отсеивая тень.
    Ну или можно воспользоваться правилами хакатона «делай, что хочешь» и сказать, что так задумано.


    Передача событий клиентской странице


    Уверен, что все знают, что такое Socket.io. А если еще не знаете, то можете почитать у них на сайте http://socket.io/. Если вкратце, то это библиотека, дающая возможность обмениваться данными между сервером node.js и клиентом в двухстороннем порядке. В нашем случае мы используем их, чтобы переслать информацию о событиях другой веб-странице через сервер с минимальной задержкой.

    Видео


    Не дожидаясь вопроса в комментариях, где же видео, представляю вам видео с демонстрацией работы системы.



    Выводы


    • За два дня мы можем разработать сколь угодно бесполезную систему
    • и получить за нее приз в номинации «Самый эффектный хак»
    • Система работает на Nexus 5 в браузере Google Chrome. Я не тестировал ее на других устройствах и в других браузерах.
    • Наша разработка не дотягивает до оригинала, зато дешево. Сенсорный стол для бедных.


    Полезные ссылки


    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 20
    • +4
      Ух ты! Выглядит здорово!
      А мультитач и прочие фишки планируете дорабатывать?
      • +5
        Спасибо. Если честно, эта штука выглядит, как абсолютно бесполезная. Так что не уверен, что буду дорабатывать ее. Но мультитач работает и в текущей версии.
        • 0
          С точки зрения не-программиста штука может выглядеть и как абсолютно полезная, если ее возможно «перевернуть». Сейчас ведь время какое — считай у каждого дома валяется старый, но рабочий смартфон. Или можно купить какой-нибудь простейший китайский по цене той же веб-камеры.

          А если поставить смартфон как веб-камеру, направив на участок, скажем, наблюдения, то на стенке поблизости (и в поле зрения камеры) можно приклеить и «панель управления» из листка с кнопками. И уже появляется возможность дешево, без всякого там ремонта и прочего джамшутинга рулить какой-нибудь домашней автоматикой.

          Или вообще не использовать смартфон для видеонаблюдения, а сделать его ответственным за панель управления (с другой стороны, все равно ему дела найдутся — то же руление автоматикой, радио какое-нибудь и т.п.).

          Это, конечно, если характеристики распознавания достаточны, чтобы отслеживать «нажатия» с приличного расстояния и одновременно фильтровать стадо, скажем, слонов, периодически загораживающих обзор. И, да, понятно, что «панель» потребует специфических нажатий, но это плата за простоту, дешевизну и универсальность.
      • НЛО прилетело и опубликовало эту надпись здесь
        • +5
          Этот проект был моим фаворитом, но судьям понравились моргающие лампочки. Странные они…
          • 0
            Спасибо. На самом деле там было много классных проектов. Мигающие лампочки тоже были классные.
          • +5
            • +7
              Что вы! Все читают alt картинок. А граммар-наци даже ставят за ошибки в них минусы, как если бы это был обычный текст.
              • +3
                Вы заставили меня по-новому взглянуть на пост.
              • 0
                www.youtube.com/watch?feature=player_embedded&v=pQpr3W-YmcQ
                Однако вариация с телефоном выглядит более экономично :)
                К слову вроде на основе того, что представлено в видео есть вариации с проектором. Т.е. люди реально делали что то вроде сарфейс стола, но достаточно дешево. Из проектора, камеры и этого ПО.
                • 0
                  Насколько много света должно быть, чтобы оно работало?
                  • 0
                    Пробовал в нескольких помещениях. Если лампа расположена строго над стеклом, то верхнего света достаточно. Дома в условиях слабой освещенности я расположил надо конструкцией настольную лампу. Никаких особенных ламп и прожекторов не использовалось.
                    • 0
                      А без настольной лампы работает, или всё-таки нужно напрямленный свет?
                      • 0
                        Да, работает. Настольную лампу дома я использовал, потому что не было возможности расположиться прямо под верхним освещением.
                        Вообще же хорошее освещение нужно, чтобы точно распознавать контуры кнопок. Если подойти к задаче более основательно, то уверен, что шумы и всю ненужную информацию можно отфильтровать даже при достаточно слабом освещении. Просто во время хакатона мы ограничили требования к системе, чтобы успеть выполнить ее за 2 дня.
                  • +2
                    Кто сказал, что это бесполезняк? Это отличный проект для компьютерного кружка!
                    • +1
                      Отличный проект, а его полезность, по-моему, определяется полученным опытом… Вы же над ним не 10 лет впустую работали)
                      • 0
                        Проект отличный и применение найдет, рано или поздно. Легко переделать в сторожа — реагировать на движение… Хочу добавить — работая в МАИ, я, когда-то решал задачу построения изолиний для массива данных (линии равного давления по датчикам потока воздуха в трубе). Есть кое-что общее…
                        • +1
                          Отличная идея. Вот вариант применения (уже коммерциализированный) — www.youtube.com/watch?v=Ir7mPebyY8I
                          • 0
                            Видео не воспроизводится (по крайне мере на данный момент).
                            • 0
                              Возможно были какие-то проблемы с vimeo. Тоже застал этот момент, открыл dev tools, перезагрузил страницу и видео вернулось. Сомневаюсь, что это секретный способ чинить vimeo, так что скорее всего «оно само».

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.