Обработка custom-жестов для Leap Motion. Часть 1

в 19:34, , рубрики: .net, gesture recognition, Leap Motion

Всем привет!

На время праздников мне в руки попал сенсор Leap Motion. Довольно давно хотел поработать с ним, но основная работа и бесполезное времяпрепровождение сессия не позволяли.

Когда-то, лет 10 назад, когда я был школьником и ничем не занимался, я покупал журнал «Игромания», в комплекте с которым поставлялся диск с всякими игровыми интересностями и shareware-софтом. И в этом журнале была рубрика о полезном софте. Одной из програм оказался Symbol Commander — утилита, позволяющая записывать движения мышью, распознавать записанные движения и при распознавании выполнять действия, назначенные на это движения.

Сейчас, при развитии бесконтактных сенсоров (Leap Motion, Microsoft Kinect, PrimeSence Carmine) возникла идея повторить подобный функционал для одного из них. Выбор пал на Leap Motion.

Итак, что необходимо для обработки custom-жестов? Модель представления жестов и процессор обработки данных. Схематично схема работы выглядит так:

Обработка custom-жестов для Leap Motion. Часть 1 - 1

Таким образом, разработку можно разделить на следующие этапы:

1. Проектирование модели описания жестов
2. Реализация процессора распознавания описанных жестов
3. Реализация соответствия жеста и команды для ОС
4. UI для возможности записи жестов и задания команд.

Начнем с модели.

Официальный SDK от Leap Motion предоставляет набор предустановленных жестов. Для этого предоставляется перечисление GestureType, содержащее следующие значения:

GestureType.TYPE_CIRCLE
GestureType.TYPE_KEY_TAP
GestureType.TYPE_SCREEN_TAP
GestureType.TYPE_SWIPE

Поскольку для себя я поставил задачу обработки custom-жестов, это не интересно, поэтому модель описания жестов будет своя.

Итак, что такое жест для Leap Motion?

SDK предоставляет Frame, в котором можно получить FingerList, содержащий в себе коллекцию описания текущего состояния каждого пальца. Поэтому для начала было решено пойти по пути наименьшего сопротивления и рассматривать жесть как набор движений пальца по одной из осей (XYZ) в одном из направлений (соответствующая компонента координаты пальца увеличивается или уменьшается).

Обработка custom-жестов для Leap Motion. Часть 1 - 2

Поэтому каждый жест будет описываться набором примитивов, состоящих из:

1. Оси движения пальца
2. Направления движения
3. Порядка выполнения примитива в жесте
4. Количества кадров, в течении которых жест примитив должен быть выполнен.

Для жеста также необходимо задать:

1. Название
2. Индекс пальца, которым выполняется этот жест.

Жесты в этой версии будут описаны в XML-формате. Для примера приведу XML-описание всем известного жеста Tap («клик» пальцем):

<Gesture>
   <GestureName>Tap</GestureName>
   <FingerIndex>1</FingerIndex>
   <Primitives>
     <Primitive>
       <Axis>Z</Axis>
       <Direction>-1</Direction>
       <Order>0</Order>
       <FramesCount>10</FramesCount>
     </Primitive>
     <Primitive>
       <Axis>Z</Axis>
       <Direction>1</Direction>
       <Order>1</Order>
       <FramesCount>10</FramesCount>
     </Primitive>
   </Primitives>
</Gesture>

Этот фрагмент задает жест Tap, состоящих из двух примитивов — опускание пальца в течении 10 кадров и, соответственно, поднятие пальца.

Опишем эту модель для библиотеки распознавания:

public class Primitive
{
    [XmlElement(ElementName = "Axis", Type = typeof(Axis))]         //ось выполнения движения
    public Axis Axis { get; set; }

    [XmlElement(ElementName = "Direction")]                         //направление: +1 -> положительное изменение
    public int Direction { get; set; }                              //             -1 -> отрицательное

    [XmlElement(ElementName = "Order", IsNullable = true)]          //порядок выполнения части движения
    public int? Order { get; set; }

    [XmlElement(ElementName = "FramesCount")]                       //количество кадров для выполнения части движения
    public int FramesCount { get; set; }
}

/// <summary>
/// ось выполнения движения
/// </summary>
public enum Axis
{
    [XmlEnum("X")]
    X,
    [XmlEnum("Y")]
    Y,
    [XmlEnum("Z")]
    Z
};

public class Gesture
{
    [XmlElement(ElementName = "GestureIndex")]                     //порядковый номер жеста
    public int GestureIndex { get; set; }

    [XmlElement(ElementName = "GestureName")]                      //название жеста
    public string GestureName { get; set; }

    [XmlElement(ElementName = "FingerIndex")]                      //порядковый номер пальца
    public int FingerIndex { get; set; }

    [XmlElement(ElementName = "PrimitivesCount")]                  //количество составны частей
    public int PrimitivesCount { get; set; }

    [XmlArray(ElementName = "Primitives")]                         //описание составных частей для жеста
    public Primitive[] Primitives { get; set; }
}

Ок, модель готова. Перейдем к процессору распознавания.

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

Поэтому создадим класс, унаследованный от Leap.Listener, и переопределим в нем метод OnFrame:

public override void OnFrame(Leap.Controller ctrl)
{
    Leap.Frame frame = ctrl.Frame();

    currentFrameTime = frame.Timestamp;
    frameTimeChange = currentFrameTime - previousFrameTime;

    if (frameTimeChange > FRAME_INTERVAL)
    {
        foreach (Gesture gesture in _registry.Gestures)
        {
            Task.Factory.StartNew(() => 
                {
                    Leap.Finger finger = frame.Fingers[gesture.FingerIndex];
                    CheckFinger(gesture, finger);
                });
        }

        previousFrameTime = currentFrameTime;
    }
}

Тут мы проверяем состояния пальцев раз в промежуток времени, равный FRAME_INTERVAL. Для тестов FRAME_INTERVAL = 5000 (количество микросекунд между обрабатываемыми фреймами).

Из кода очевидно, что распознавание реализовывается в методе CheckFinger. Параметрами этого метода являются жест, который проверяется в данный момент, и Leap.Finger — объект, представляющий текущее состояние пальца.

Как работает распознавание?

Я решил сделать три контейнера — контейнер координат для контроля направления, контейнер счетчиков фреймов, положение пальцев которых удовлетворяет условиям распознавания жестов и контейнер счетчиков распознанных примитивов для контроля выполнения жестов.

Таким образом:

public void CheckFinger(Gesture gesture, Leap.Finger finger)
{
    int recognitionValue = _recognized.ElementAt(gesture.GestureIndex);
    Primitive primitive = gesture.Primitives[recognitionValue];
    CheckDirection(gesture.GestureIndex, primitive, finger);
    CheckGesture(gesture);
}

public void CheckDirection(int gestureIndex, Primitive primitive, Leap.Finger finger)
{
    float pointCoordinates = float.NaN;

    switch(primitive.Axis)
    {
        case Axis.X:
            pointCoordinates = finger.TipPosition.x;
            break;
        case Axis.Y:
            pointCoordinates = finger.TipPosition.y;
            break;
        case Axis.Z:
            pointCoordinates = finger.TipPosition.z;
            break;
    }

    if (_coordinates[gestureIndex] == INIT_COUNTER)
        _coordinates[gestureIndex] = pointCoordinates;

    else
    {
        switch (primitive.Direction)
        {
            case 1:
                if (_coordinates[gestureIndex] < pointCoordinates)
                {
                    _coordinates[gestureIndex] = pointCoordinates;
                    _number[gestureIndex]++;
                }
                else
                    _coordinates[gestureIndex] = INIT_COORDINATES;
                break;
            case -1:
                if (_coordinates[gestureIndex] > pointCoordinates)
                {
                    _coordinates[gestureIndex] = pointCoordinates;
                    _number[gestureIndex]++;
                }
                else
                    _coordinates[gestureIndex] = INIT_COORDINATES;
                break;
        }
    }

    if(_number[gestureIndex] == primitive.FramesCount)
    {
        _number[gestureIndex] = INIT_COUNTER;
        _recognized[gestureIndex]++;
    }
}

public void CheckGesture(Gesture gesture)
{
    if(_recognized[gesture.GestureIndex] == (gesture.PrimitivesCount - 1))
    {
        FireEvent(gesture);
        _recognized[gesture.GestureIndex] = INIT_COUNTER;
    }
}

На данный момент описаны жесты Tap (нажатие пальцем) и Round (круговое движение).

Следующими этапами станут:

1. Стабилизация распознавания (да, сейчас оно не стабильно. Обдумываю варианты).
2. Реализация UI-приложения для нормальной работы пользователя.

Исходный код доступен на github

Автор: MZhukov

Источник

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


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