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

в 9:54, , рубрики: c++, computer vision, qt, qt quick, Qt Software, обработка изображений, параллельное программирование, метки: , , , ,

Введение

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

Автор: nicktrandafil

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js