[Из песочницы] Обработка custom-жестов для Leap Motion. Часть 1

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

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

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

66293ba3b5f94f1e82bdb214b819307b.png

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

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) в одном из направлений (соответствующая компонента координаты пальца увеличивается или уменьшается).

998b186b9298482595add62b86ffa2eb.png

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

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

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

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

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

Tap 1 Z -1 0 10 Z 1 1 10 Этот фрагмент задает жест 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; } }

///

/// ось выполнения движения /// 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

© Habrahabr.ru