Введение
В этой статье речь пойдёт о распознавании жестов. Я считаю, что эта тема на сегодняшний день очень актуальна, потому что этот способ ввода информации более удобен для человека. В 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: Перепоробовал много цветов, самым хорошим(устойчивым к быстрым движениям) оказался оранжевый.
Автор: nicktrandafil