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

Всім привіт!

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

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

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

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



Таким чином, розробку можна розділити на наступні етапи:

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) в одному з напрямів (відповідна компонента координати пальця збільшується або зменшується).



Тому кожен жест буде описуватися набором примітивів, які складаються з:

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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.