Умный видеоплеер или просто распознавание жестов

от автора

Введение

В этой статье речь пойдёт о распознавании жестов. Я считаю, что эта тема на сегодняшний день очень актуальна, потому что этот способ ввода информации более удобен для человека. В YouTube можно увидеть много роликов про распознавание, отслеживание предметов, в хабре тоже есть статьи по этой теме, так вот, я решил поэкспериментировать и сделать что-то своё, полезное и нужное. Я решил сделать видеоплеер, которым можно управлять жестами, потому что сам иногда очень ленюсь взяться за мышку, найти этот ползунок и перемотать чуть-чуть вперёд или чуть-чуть назад, особенно, когда смотрю фильмы на иностранном языке (там приходится часто перематывать назад).

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

Итак, что мы хотим?

Мы хотим иметь возможность давать следующее команды с помощью жестов:

  1. Перемотка вперёд/назад
  2. Пауза/продолжить
  3. Добавить/убавить звук
  4. Следующая/предыдущая дорожка.

Сопоставим перечисленные действия жестам:

  1. Движение слева направо/справа налево
  2. Движение из центра вверх-направо
  3. Движение снизу вверх/сверху вниз
  4. Движение из центра вниз-вправо/с центра вниз-влево

Наблюдать жесты будем через веб-камеру. У меня на ноутбуке встроенная камера 0,3 мп. Она слабенькая, но при хорошем освещении, ее тоже можно использовать для таких целей. Но я буду использовать внешнюю USB вер камеру. Жесты будем показывать какой-нибудь одноцветной палочкой, потому что будем выделять его из фона путём фильтрации по цвету. Например, в качестве такого предмета будем использовать обычный карандаш. Конечно, это не самый лучший способ распознать предмет, но я хотел сделать акцент именно на распознавании жеста (движения), а не предмета на рисунке.

Инструменты

Я буду использовать Qt framework 5.2, в качестве среды разработки. Для обработки потока видео из веб-камеры буду использовать OpenCV 4.6. GUI полностью будет на QML, а блок распознавания будет на С++.

Оба инструмента с открытым исходным кодом, оба кроссплатформы. Я разрабатывал плеер под линукс, но его можно перенести и на любую другую платформу, нужно будет только скомпилировать OpenCV с поддержкой Qt под нужную платформу и перекомпилировать, пересобрать плеер. Я пробовал перенести плеер на Виндовс, но у меня не получилось скомпилировать OpenCV с поддержкой Qt под него. Кто попробует и у кого получится, просьба поделиться мануалом или бинарниками.

Структура плеера

На рисунке ниже представлена структура плеера. Плеер работает в двух потоках. В основном потоке находится GUI и видеоплеер. В отдельный поток вынесен блок распознавания для того, чтобы предотвратить подвисание интерфейса и воспроизведения видео. Интерфейс я написал на QML, логику плеера я написал на JS, а блок распознавания на C++ (всем ясно, почему). Плеер «общается» с блоком распознавания при помощи сигналов и слотов. Обёртку для класса распознавания я сделал для того, чтобы облегчить разделение приложения на 2 потока. На самом деле, обёртка находится в основном потоке(т.е не так, как показано на рисунке). Обёртка создаёт экземпляр класса распознавания и помещает его в новый, дополнительный поток. Собственно, о плеере все, дальше буду говорить о распознавании и приводить код.

Распознавание

Для того, чтобы распознать, будем собирать кадры и обрабатывать их методами теории вероятностей. Будем собирать двадцать кадров в секунду(больше веб камера не позволяет). Обрабатывать будем по десять кадров.

Алгоритм:

  1. получаем кадр с веб камеры и отправляем его на фильтр;
  2. фильтр нам возвращает бинарное изображение, где изображён только карандаш в виде белого прямоугольника на чёрном фоне;
  3. бинарное изображение отправляется в анализатор, где вычисляются вершины карандаша. Верхняя вершина заносится в массив.
  4. если массив достиг размера в 10 элементов, то этот массив отправляется в вероятностный анализатор, где происходит анализ последовательности пар чисел методом наименьших квадратов.
  5. если анализ распознал какую-нибудь команду, то эта команда отправляется в видеоплеер.

Приведу только 3 основные функции распознавания.

Следующая функция следит за камерой, если жестовое управление включено:

void MotionDetector::observCam() {     m_cap >> m_frame;	// почучаем кадр с камеры     filterIm();		// получаем бинарное изображение     detectStick();	// распознаем, и иесли распознали отправляем команду     drawStick(m_binIm); // рисуем распознанный карандаш     showIms();		// показываем распознанный карандаш } 

Вот так выглядит функция распознавания:

void MotionDetector::detectStick() {     m_contours.clear();     cv::findContours(m_binIm.clone(), m_contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);     if (m_contours.empty())         return;     bool stickFound = false;     for(int i = 0; i < m_contours.size(); ++i)     {         // если объект очень маленький, то пропускаем его         if(cv::contourArea(m_contours[i]) < m_arAtThreshold)             continue;         // находим концы карандаша         m_stickRect = cv::minAreaRect(m_contours[i]);         cv::Point2f vertices[4];         cv::Point top;         cv::Point bottom;         m_stickRect.points(vertices);         if (lineLength(vertices[0], vertices[1]) > lineLength(vertices[1], vertices[2])){             top = cv::Point((vertices[1].x + vertices[2].x) / 2., (vertices[1].y + vertices[2].y) / 2.);             bottom = cv::Point((vertices[0].x + vertices[3].x) / 2., (vertices[0].y + vertices[3].y) / 2.);         } else{             top = cv::Point((vertices[0].x + vertices[1].x) / 2., (vertices[0].y + vertices[1].y) / 2.);             bottom = cv::Point((vertices[2].x + vertices[3].x) / 2., (vertices[2].y + vertices[3].y) / 2.);         }         if (top.y > bottom.y)             qSwap(top, bottom);         m_stick.setTop(top);         m_stick.setBottom(bottom);         stickFound = true;     }     // проверяем состояние     switch (m_state){     case ST_OBSERVING:         if (!stickFound){             m_state = ST_WAITING;             m_pointSeries.clear();             break;         }         m_pointSeries.append(QPair<double, double>(m_stick.top().x, m_stick.top().y));         if (m_pointSeries.size() >= 10){             m_actionPack = m_pSeriesAnaliser.analize(m_pointSeries);             if (!m_actionPack.isEmpty()){                 emit sendAction(m_actionPack);             }             m_pointSeries.clear();         }         break;     case ST_WAITING:         m_state = ST_OBSERVING;         break;     } } 

Про метод наименьших квадратов вы можете прочитать здесь. Сейчас же я покажу, как её реализовал я. Ниже представлен вероятностный анализатор ряда.

bool SeriesAnaliser::linerCheck(const QVector<QPair<double, double> > &source) {     int count = source.size();     // скопируем значения в 2 отдельных массива, чтобы понятнее было.     QVector<double> x(count);     QVector<double> y(count);     for (int i = 0; i < count; ++i){         x[i] = source[i].first;         y[i] = source[i].second;     }     double zX, zY, zX2, zXY;    // z - обнозначение знака суммы. zX - сумма x-ов и т.д.     QVector<double> yT(count);     // подготовка переданных     zX = 0;     for (int i = 0; i < count; ++i)         zX += x[i];     zY = 0;     for (int i = 0; i < count; ++i)         zY += y[i];     zX2 = 0;     for (int i = 0; i < count; ++i)         zX2 += x[i] * x[i];     zXY = 0;     for (int i = 0; i < count; ++i)         zXY += x[i] * y[i];     // вычисление коэффициетов уравнения     double a = (count * zXY - zX * zY) / (count * zX2 - zX * zX);     double b = (zX2 * zY - zX * zXY) / (count * zX2 - zX * zX);     // нахождение теоретического y     for (int i = 0; i < count; ++i)         yT[i] = x[i] * a + b;     double dif = 0;     for (int i = 0; i < count; ++i){         dif += qAbs(yT[i] - y[i]);     }     if (a == 0)         a = 10; #ifdef QT_DEBUG     qDebug() << QString("%1x+%2").arg(a).arg(b);     qDebug() << dif; #endif     // если а > vBorder, то это, сокорее всего, вертикальная линия     // если погрешность больше epsilan, то это, скорее всего, случайное движение     // если oblMovMin < a < oblMovMax, то это, скорее всего, косая линия     // если скорость больше 0.6, то это, скорее всего, случайное движеие     // если a < horMov, то это, скорее всего, горизонтально движение.     int vBorder = 3;     int epsilan = 50;     double oblMovMin = 0.5;     double oblMovMax = 1.5;     double horMov = 0.2;     // Если погрешность очень большая, то выход     if (qAbs(a) < vBorder && dif > epsilan)         return false;     // вычисление скорости     double msInFrame = 1000 / s_fps;     double dTime = msInFrame * count;   // ms     double dDistance;                   // px     double speed = 0;  /*px per ser*/     if (qAbs(a) < vBorder)         dDistance = x[count - 1] - x[0];            // если вертикальная линия     else         dDistance = y[count -1] - y[0];     speed = dDistance / dTime; //px per     // если палочка не двигается, выход     if (qSqrt(qPow(x[0] - x[count - 1], 2) + qPow(y[0] - y[count - 1], 2)) < 15){         return false;     }     // резкие движения вероятно случайные.     if (speed > 0.6)         return false;     // отправка пакета     if (qAbs(a) > oblMovMin && qAbs(a) < oblMovMax){         // Переключение         if (a < 0){             // следующая дорожка             s_actionPack = "next";         } else{             if (speed < 0)                 s_actionPack = "play";             else                 // предыдущая дорожка                 s_actionPack = "previous";         }     } else         if (qAbs(a) < horMov)         {             s_actionPack = QString("rewind %1").arg(speed * -30000);         } else             if (qAbs(a) > vBorder){                 s_actionPack = QString("volume %1").arg(speed * -1);             } else                 return false;     return true; } 

Следующий отрывок кода принимает распознанное действие(на стороне видеоплеера):

 function executeComand(comand){     var comandList = comand.split(' ');     console.log(comand);     switch (comandList[0]) {     case "next":         nextMedia();         break;     case "previous":         previousMedia();         break;     case "play":         playMedia();         break;     case "rewind":         mediaPlayer.seek(mediaPlayer.position + Number(comandList[1]));         break;     case "volume":         mediaPlayer.volume += Number(comandList[1]);         break;     default:         break;     } } 

Да, чуть не забыл, карандаш то у нас выделяется из фона по цвету, а это нужно как-то где-то настроить. Я решил проблему следующим образом:

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

Вот такие результаты у меня получились:

Вывод

Теперь видеоплеер может распознавать очень простые жесты. По-моему, самой удачной и удобной вещью в плеере является перемотка назад/вперёд жестами. И именно эта команда работает наиболее хорошо и стабильно. Хоть и для просмотра фильма жестовое управление придётся немного настроить, но потом можно не искать мышку, чтобы перемотать чуть-чуть назад.

P.S: Кому интересно, вот исходники SmartVP.
P.P.S: Перепоробовал много цветов, самым хорошим(устойчивым к быстрым движениям) оказался оранжевый.

ссылка на оригинал статьи http://habrahabr.ru/post/213417/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *