Введение
В этой статье речь пойдёт о распознавании жестов. Я считаю, что эта тема на сегодняшний день очень актуальна, потому что этот способ ввода информации более удобен для человека. В YouTube можно увидеть много роликов про распознавание, отслеживание предметов, в хабре тоже есть статьи по этой теме, так вот, я решил поэкспериментировать и сделать что-то своё, полезное и нужное. Я решил сделать видеоплеер, которым можно управлять жестами, потому что сам иногда очень ленюсь взяться за мышку, найти этот ползунок и перемотать чуть-чуть вперёд или чуть-чуть назад, особенно, когда смотрю фильмы на иностранном языке (там приходится часто перематывать назад).
В статье, в основном, речь будет идти о том, как я реализовал распознавание жестов, а о видеоплеере я только скажу в общем.
Итак, что мы хотим?
Мы хотим иметь возможность давать следующее команды с помощью жестов:
- Перемотка вперёд/назад
- Пауза/продолжить
- Добавить/убавить звук
- Следующая/предыдущая дорожка.
Сопоставим перечисленные действия жестам:
- Движение слева направо/справа налево
- Движение из центра вверх-направо
- Движение снизу вверх/сверху вниз
- Движение из центра вниз-вправо/с центра вниз-влево
Наблюдать жесты будем через веб-камеру. У меня на ноутбуке встроенная камера 0,3 мп. Она слабенькая, но при хорошем освещении, ее тоже можно использовать для таких целей. Но я буду использовать внешнюю USB вер камеру. Жесты будем показывать какой-нибудь одноцветной палочкой, потому что будем выделять его из фона путём фильтрации по цвету. Например, в качестве такого предмета будем использовать обычный карандаш. Конечно, это не самый лучший способ распознать предмет, но я хотел сделать акцент именно на распознавании жеста (движения), а не предмета на рисунке.
Инструменты
Я буду использовать Qt framework 5.2, в качестве среды разработки. Для обработки потока видео из веб-камеры буду использовать OpenCV 4.6. GUI полностью будет на QML, а блок распознавания будет на С++.
Оба инструмента с открытым исходным кодом, оба кроссплатформы. Я разрабатывал плеер под линукс, но его можно перенести и на любую другую платформу, нужно будет только скомпилировать OpenCV с поддержкой Qt под нужную платформу и перекомпилировать, пересобрать плеер. Я пробовал перенести плеер на Виндовс, но у меня не получилось скомпилировать OpenCV с поддержкой Qt под него. Кто попробует и у кого получится, просьба поделиться мануалом или бинарниками.
Структура плеера
На рисунке ниже представлена структура плеера. Плеер работает в двух потоках. В основном потоке находится GUI и видеоплеер. В отдельный поток вынесен блок распознавания для того, чтобы предотвратить подвисание интерфейса и воспроизведения видео. Интерфейс я написал на QML, логику плеера я написал на JS, а блок распознавания на C++ (всем ясно, почему). Плеер «общается» с блоком распознавания при помощи сигналов и слотов. Обёртку для класса распознавания я сделал для того, чтобы облегчить разделение приложения на 2 потока. На самом деле, обёртка находится в основном потоке(т.е не так, как показано на рисунке). Обёртка создаёт экземпляр класса распознавания и помещает его в новый, дополнительный поток. Собственно, о плеере все, дальше буду говорить о распознавании и приводить код.
Распознавание
Для того, чтобы распознать, будем собирать кадры и обрабатывать их методами теории вероятностей. Будем собирать двадцать кадров в секунду(больше веб камера не позволяет). Обрабатывать будем по десять кадров.
Алгоритм:
- получаем кадр с веб камеры и отправляем его на фильтр;
- фильтр нам возвращает бинарное изображение, где изображён только карандаш в виде белого прямоугольника на чёрном фоне;
- бинарное изображение отправляется в анализатор, где вычисляются вершины карандаша. Верхняя вершина заносится в массив.
- если массив достиг размера в 10 элементов, то этот массив отправляется в вероятностный анализатор, где происходит анализ последовательности пар чисел методом наименьших квадратов.
- если анализ распознал какую-нибудь команду, то эта команда отправляется в видеоплеер.
Приведу только 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/
Добавить комментарий