Жестикуляция. Первые шаги.
Запись от elder_Nosferatu размещена 18.10.2015 в 00:58
Всякие новомодные тачскрин-девайсы познакомили нас с жестами, как способом взаимодействия с приложениями, но... Предложенное нам определение жеста слегка обрезано. Никаких тебе спиралей, звездочек и прочих крякозябр, которые можно изобразить единственным взмахом пальца (манипулятора). А были же и хорошие времена!
Не знаю, как у кого, а моему первому знакомству с жестами я обязан замечательной игре Black&White славно/печально известного Питера Молинье. В ней нужно выполнять роль бестелесного божества. ГУИ позволял кликами по иконкам создавать чудеса, но так же была возможность чудесить и жестами. Такой способ был сложноват, так как без должной сноровки было трудно воспроизводить нужные жесты. Само собой управление мышкой не такое уж и удобное, но больше всего мешало то, что фигуру отображаемого жеста можно было увидеть лишь после удачного выполнения.
Был еще один отличный пример внедрения жестикуляций но теперь уже в качестве львиной доли взаимодействия с миром игры. Речь идет об игре Тургор (eng: [COLOR="rgb(255, 140, 0)"]Tension[/color]) от московской студии Ice-Pick Lodge. Здесь жестикуляция была безальтернативным методом управления, но и реализация была более удобной. Все взмахи мышью имитровали рисование кистью, так что след нанесенной краски четко давал понять, что я рисую, а значит и рисовать то, что нужно стало немного легче. Трудности управления мышкой это не убрало, но тем не менее.
Эти примеры вроде бы сильно меня вдохновляли, но я не знал куда можно прикрутить такую фичу, да и задачу "распознания жестов" я воспринимал слишком буквально, чтобы представить, с какой стороны к ней подобраться. Но все же я нашел лазейку, о которой стоит предупредить.
/* - DISCLAIMER - */
На офф-сайте игры Тургор я нашел флеш-рисовалку (http://www.tension-game.com/drawing_ru.php), которая демонстрирует основную фишку геймплея. В то время я уже был знаком с флешом и с... декомпиляторами. Так что я сразу взял себе эту рисовалку на прицел в качестве пособия по распознанию жестов. И вот прошло несколько лет и я таки совершил богомерзкий акт взлома этого продукта с целью познания чужой реализации решения этой интересной задачи. Тех, кто копит бонусные баллы для попадания в рай для флешеров, прошу покинуть мой блог, так как я собираюсь поделиться результатами флеш-декомпиляции.
/* - END OF DISCLAIMER - */
Короче, ковырнул я немного полученный код и увидел хитрую идею! Жесты? А зачем? Пусть игрок думает, что он имеет дело с жестами, а мы будем просто работать с набором точек (не обязательно упорядоченным), через которые может пройти этот жест. И тогда просто остается сравнить готовый (эталонный) набор точек с тем, который введет своим взмахом игрок. И не нужно следить за следованием от точки к точке. И нейросетей для распознания тоже ваять не придется. Да, решение далеко от идеала, но оно проверено в боевых условиях (игра вышла и в СНГ, и в Европе), а результаты оказались неплохими.
Суть метода:
* Рисуем жест и делим весь путь на множество точек.
* Сжимаем полученную фигуру до определенных габаритов (1х1), чтобы при сравнении отпали претензии типа "у меня выше", "у меня шире" или "а у меня еле на экране поместилась" - главное, чтобы было похоже.
* Определяем максимальную погрешность сравнения (допустимое расстояние между ближайшими точками сравниваемых жестов).
* Просим кого либо нарисовать одним росчерком похожую фигуру (регулярно хватаем точи по сигналам MOUSE_MOVE или ENTER_FRAME).
* Так же сжимаем вторую фигуру до оговоренных габаритов.
* Сравниваем две полученные фигуры, то есть два набора точек: для каждой точки одной фигуры пытаемся найти хотя бы одну точку другой фигуры, которая лежит не далее определенного допуска. Если это удается, то меняем фигуры местами и повторяем проверку. Две успешных проверки подсказывают, что оба набора точек могут лежать на похожих жестах (с заданной погрешностью конечно).
Вот и вся премудрость.
Зачем две проверки? Пример:
1. Шаблон - окружность, а введенный жест - дуга на 270 градусов. Каждая точка жеста найдет соседа при наложении, но жест не закончен;
2. Шаблон - дуга на 270 градусов, а введенный жест - окружность. Каждая точка шаблона найдет соседа при наложении, но жест немного мудренее (как сравнивать "0" и "9" - у обеих петля, но в "9" есть еще и хвостик);
Проанализируем же этот метод распознания:
-Достоинства-
* Такой простой, что и не стразу в голову приходит

-Недостатки-
* По сути не является распознанием именно ЖЕСТА (пути, который проходит через определенные точки).
* Хоть и игнорирует горизонтально-вертикальные искажения всей фигуры, но не учитывает поворот.
* Для удобства юзверя нужно много вариантов "почерка" (парни из Ice-Pick Lodge соорудили по 30-40 вариантов начертания для каждого жеста).
* Даже визуально совпадающие фигуры (хоть бы даже с нулевой погрешностью) могут не пройти проверку сравнения, если на одном участке сильно отличается плотность опорных точек у сравниваемых жестов. К примеру в шаблонном жесте точки равномерно распределены на расстоянии погрешности. И в этом же шаблоне есть почти прямой участок, который при попытке воспроизведения вручную можно пройти резким росчерком. Если ФПС будет не достаточно высок, то такой резкий росчерк зафиксирует не так уж и много точек, хоть и пройдет он тютилька в тютильку с шаблоном. Может оказаться, что между двумя ближайшими точками жеста, расстояние между которыми составляет скажем размер десятикратной погрешности лежит добрая сотня точек шаблона. А это значит, что мало какой из этих точек достанется на столько близкий сосед, чтобы она Станиславского цитировала. Это можно конечно обойти, если самостоятельно напихать точек между сильно удаленными, но в этом то и недостаток метода, что без лишних телодвижений ему не понять очевидных вещей.
* Вроде были еще претензии, но и так хватает.
Чтобы немного продемонстрировать описанную ересь предлагаю поюзать мой тестовый полигон, а потом и с самым важным классом познакомиться, который повторяет спионеренную идею, но в вольном пересказе (Инфу о жесте завернул в класс, отдельные задачки разбил на функции избавился от лишних объектов и т.д.). Класс как бы рабочий, что доказывает испытательный полигон, но он писался с прицелом на читабельность и понимабельность, так что есть еще куда выкручивать производябельность (если библиотека жестов будет внушительной, то лаги во время сравнения будут обеспечены).
-Тестовый полигон-
В приложении 2 окна - холст и библиотека шаблонных жестов.
С холстом все понятно, указал погрешность и айда рисовать. Получилось - увидишь шаблон, которому соответствует рисованное, а нет - просто сетку 10х10 натянутую на жест. Ну и название вверху будет светиться.
В библиотеке же рассортированы жесты за авторством Ice-Pick Lodge. Жесты сложены в группы, каждый элемент которой представляет разное начертание одного и того же символа. Клики по этих элементах нарисуют жест в окошке, а если окошко не очищать (автоочищать), то можно наложить несколько жестов из одной группы для наглядности различия.
-Собственно класс-
package { /** * Экземпляр класса умеет хранить инфу о точках жеста, а так же сравнивать себя с другим экземпляром. * ... * @author elder_Nosferatu */ public class GuestureData { /** X-координаты точек жеста. Модификатор internal нужен лишь для визуализации. */ internal var pntsX:Array; /** Y-координаты точек жеста. Модификатор internal нужен лишь для визуализации. */ internal var pntsY:Array; /** Количестов точек жеста. */ private var _size:uint; /** Имя жеста */ private var _name:String; /** Признак компрессии (нормализации) данных жеста. */ private var _compressed:Boolean; /** * Собственно конструктор... * @param data готовая инфа о жесте в формате JSON или XML. * JSON: * { * "guestures":[ * { * "name":"guesture0", * "points":[ * [pnt0x, pnt0y], * [pnt1x, pnt1y], * ... * ] * }, * ... * ] * } * XML: * <guestures> * <guesture name="guesture0" > * <pnt "x"="pnt0x" "y"="pnt0y" /> * <pnt "x"="pnt1x" "y"="pnt1y" /> * ... * </guesture> * </guestures> */ public function GuestureData(data:* = null) { _name = null; pntsX = []; pntsY = []; if (data is XML) parseXML(data); else if (data is Object) parseObject(data); } /** * Парсинг данных о жесте в формате JSON. * @param data обьект с инфой. */ public function parseObject(data:Object):void { var arr:Array = data["points"]; var item:Array; _name = data["name"]; _size = arr.length; pntsX.length = _size; pntsY.length = _size; for (var i:uint = 0, li:uint = arr.length; i < li; ++i) { item = arr[i]; pntsX[i] = item[0]; pntsY[i] = item[1]; } compress(); } /** * Парсинг данных о жесте в формате XML (не проверял правильность ибо влом и не люблю XML). * @param data XML-обьект с инфой. */ public function parseXML(data:XML):void { var arr:XMLList = data.children(); var item:XML; _name = data.attribute("name"); _size = arr.length; pntsX.length = _size; pntsY.length = _size; for (var i:uint = 0, li:uint = arr.length; i < li; ++i) { item = arr[i] as XML; pntsX[i] = item.attribute("x"); pntsY[i] = item.attribute("y"); } compress(); } /** * Сбрасывает инфу о жесте. */ public function clear():void { _name = null; _size = 0; _compressed = false; pntsX.length = 0; pntsY.length = 0; } /** * Ручное добавление инфы о следующей точке жеста. * @param x x-координата точки. * @param y y-координата точки. */ public function addPoint(x:Number, y:Number):void { pntsX[_size] = x; pntsY[_size] = y; ++_size; } /** * Компрессия (нормализация) данных о жесте. Выполнять после полного заполнения инфы о жесте. * Компрессия преобразует прямоугольник, в который вписан жест в квадрат со стороной, равной единице. */ public function compress():void { var minX:Number = Math.min.apply(null, pntsX); var minY:Number = Math.min.apply(null, pntsY); var maxX:Number = Math.max.apply(null, pntsX); var maxY:Number = Math.max.apply(null, pntsY); var dx:Number = (maxX - minX) || 1; var dy:Number = (maxY - minY) || 1; if (minX == 0 || minY == 0 || maxX == 1 || maxY == 1) return; for (var i:uint = 0; i < _size; ++i) { pntsX[i] = (pntsX[i] - minX) / dx; pntsY[i] = (pntsY[i] - minY) / dy; } _compressed = true; } /** Хвастаемся именем жеста. */ public function get name():String { return _name; } /** Хвастаемся размером (количеством точек) жеста. */ public function get size():uint { return _size; } /** * Сравниваемся на другой жест с учетом допустимого отклонения. * @param instance жест для сравнения. * @param accuracy максимальное отклонение ближайших точек нормализованых жестов. * @return результат сравнения. */ public function compare(instance:GuestureData, accuracy:Number):Boolean { var sqr:Number = accuracy * accuracy; var res:Boolean; // А все ли наши точки "совпадают" с точками жеста instance. res = checkMatching(this, instance, sqr); // Если нет, то что мы ждесь забыли?!! if (!res) return false; // А что если в жесте instance, есть точки, которые не "совпадают" с нашими. res = checkMatching(instance, this, sqr); return res; } /** * Проверяем существование собственной точки, квадрат расстояния от (x;y) которой не превышает sqrDistance. * @param pntX x-координата точки для проверки. * @param pntY y-координата точки для проверки. * @param sqrDistance квадрат максимально допустимого расстояния. * @return результат поиска достаточно близкой точки. */ private function isCloseEnough(pntX:Number, pntY:Number, sqrDistance:Number):Boolean { for (var i:uint = 0; i < _size; ++i) { var dx:Number = pntX - pntsX[i]; var dy:Number = pntY - pntsY[i]; var sqr:Number = dx * dx + dy * dy; if (sqr <= sqrDistance) return true; } return false; } /** * Проверка точек жеста sample, на "соответсвие" точкам жеста standart с учетом квадрата максимально допустимого расстояния. * @param standart жест-эталон. * @param sample жест-пробник. * @param sqrDistance квадрат масимально допустимого рассояния между точками. * @return результат проверки. */ private static function checkMatching(standart:GuestureData, sample:GuestureData, sqrDistance:Number):Boolean { for (var i:uint = 0, li:uint = standart.size; i < li; ++i) { var stdX:Number = standart.pntsX[i]; var stdY:Number = standart.pntsY[i]; var isClose:Boolean = sample.isCloseEnough(stdX, stdY, sqrDistance); if (!isClose) return false; } return true; } } }
Всего комментариев 4
Комментарии
![]() ![]() |
|
Не нашел в примере квадрата, он определяется как круг всегда? И получилось нарисовать AboutWorld только с десятого раза )
|
![]() ![]() |
|
Насчёт квадрата тоже интересно. А вот AboutWorld определяется нормально каждый раз, гы
![]() |
Последние записи от elder_Nosferatu
- Жестикуляция. Первые шаги. (18.10.2015)