Source code for kivy.gesture

Gesture recognition

This class allows you to easily create new
gestures and compare them::

    from kivy.gesture import Gesture, GestureDatabase

    # Create a gesture
    g = Gesture()
    g.add_stroke(point_list=[(1,1), (3,4), (2,1)])

    # Add it to the database
    gdb = GestureDatabase()

    # And for the next gesture, try to find it!
    g2 = Gesture()
    # ...

.. warning::

   You don't really want to do this: it's more of an example of how
   to construct gestures dynamically. Typically, you would
   need a lot more points, so it's better to record gestures in a file and
   reload them to compare later. Look in the examples/gestures directory for
   an example of how to do that.


__all__ = ('Gesture', 'GestureDatabase', 'GesturePoint', 'GestureStroke')

import pickle
import base64
import zlib
import math

from kivy.vector import Vector

from io import BytesIO

[docs]class GestureDatabase(object): '''Class to handle a gesture database.''' def __init__(self): self.db = []
[docs] def add_gesture(self, gesture): '''Add a new gesture to the database.''' self.db.append(gesture)
[docs] def find(self, gesture, minscore=0.9, rotation_invariant=True): '''Find a matching gesture in the database.''' if not gesture: return best = None bestscore = minscore for g in self.db: score = g.get_score(gesture, rotation_invariant) if score < bestscore: continue bestscore = score best = g if not best: return return (bestscore, best)
[docs] def gesture_to_str(self, gesture): '''Convert a gesture into a unique string.''' io = BytesIO() p = pickle.Pickler(io) p.dump(gesture) data = base64.b64encode(zlib.compress(io.getvalue(), 9)) return data
[docs] def str_to_gesture(self, data): '''Convert a unique string to a gesture.''' io = BytesIO(zlib.decompress(base64.b64decode(data))) p = pickle.Unpickler(io) gesture = p.load() return gesture
class GesturePoint: def __init__(self, x, y): '''Stores the x,y coordinates of a point in the gesture.''' self.x = float(x) self.y = float(y) def scale(self, factor): ''' Scales the point by the given factor.''' self.x *= factor self.y *= factor return self def __repr__(self): return 'Mouse_point: %f,%f' % (self.x, self.y)
[docs]class GestureStroke: ''' Gestures can be made up of multiple strokes.''' def __init__(self): ''' A stroke in the gesture.''' self.points = list() self.screenpoints = list() # These return the min and max coordinates of the stroke @property def max_x(self): if len(self.points) == 0: return 0 return max(self.points, key=lambda pt: pt.x).x @property def min_x(self): if len(self.points) == 0: return 0 return min(self.points, key=lambda pt: pt.x).x @property def max_y(self): if len(self.points) == 0: return 0 return max(self.points, key=lambda pt: pt.y).y @property def min_y(self): if len(self.points) == 0: return 0 return min(self.points, key=lambda pt: pt.y).y
[docs] def add_point(self, x, y): ''' add_point(x=x_pos, y=y_pos) Adds a point to the stroke. ''' self.points.append(GesturePoint(x, y)) self.screenpoints.append((x, y))
[docs] def scale_stroke(self, scale_factor): ''' scale_stroke(scale_factor=float) Scales the stroke down by scale_factor. ''' self.points = [pt.scale(scale_factor) for pt in self.points]
[docs] def points_distance(self, point1, point2): ''' points_distance(point1=GesturePoint, point2=GesturePoint) Returns the distance between two GesturePoints. ''' x = point1.x - point2.x y = point1.y - point2.y return math.sqrt(x * x + y * y)
[docs] def stroke_length(self, point_list=None): '''Finds the length of the stroke. If a point list is given, finds the length of that list. ''' if point_list is None: point_list = self.points gesture_length = 0.0 if len(point_list) <= 1: # If there is only one point -> no length return gesture_length for i in range(len(point_list) - 1): gesture_length += self.points_distance( point_list[i], point_list[i + 1]) return gesture_length
[docs] def normalize_stroke(self, sample_points=32): '''Normalizes strokes so that every stroke has a standard number of points. Returns True if stroke is normalized, False if it can't be normalized. sample_points controls the resolution of the stroke. ''' # If there is only one point or the length is 0, don't normalize if len(self.points) <= 1 or self.stroke_length(self.points) == 0.0: return False # Calculate how long each point should be in the stroke target_stroke_size = \ self.stroke_length(self.points) / float(sample_points) new_points = list() new_points.append(self.points[0]) # We loop on the points prev = self.points[0] src_distance = 0.0 dst_distance = target_stroke_size for curr in self.points[1:]: d = self.points_distance(prev, curr) if d > 0: prev = curr src_distance = src_distance + d # The new point need to be inserted into the # segment [prev, curr] while dst_distance < src_distance: x_dir = curr.x - prev.x y_dir = curr.y - prev.y ratio = (src_distance - dst_distance) / d to_x = x_dir * ratio + prev.x to_y = y_dir * ratio + prev.y new_points.append(GesturePoint(to_x, to_y)) dst_distance = self.stroke_length(self.points) / \ float(sample_points) * len(new_points) # If this happens, we are into troubles... if not len(new_points) == sample_points: raise ValueError('Invalid number of strokes points; got ' '%d while it should be %d' % (len(new_points), sample_points)) self.points = new_points return True
[docs] def center_stroke(self, offset_x, offset_y): '''Centers the stroke by offsetting the points.''' for point in self.points: point.x -= offset_x point.y -= offset_y
[docs]class Gesture: '''A python implementation of a gesture recognition algorithm by Oleg Dopertchouk: Implemented by Jeiel Aranal (, released into the public domain. ''' # Tolerance for evaluation using the '==' operator DEFAULT_TOLERANCE = 0.1 def __init__(self, tolerance=None): ''' Gesture([tolerance=float]) Creates a new gesture with an optional matching tolerance value. ''' self.width = 0. self.height = 0. self.gesture_product = 0. self.strokes = list() if tolerance is None: self.tolerance = Gesture.DEFAULT_TOLERANCE else: self.tolerance = tolerance def _scale_gesture(self): ''' Scales down the gesture to a unit of 1.''' # map() creates a list of min/max coordinates of the strokes # in the gesture and min()/max() pulls the lowest/highest value min_x = min([stroke.min_x for stroke in self.strokes]) max_x = max([stroke.max_x for stroke in self.strokes]) min_y = min([stroke.min_y for stroke in self.strokes]) max_y = max([stroke.max_y for stroke in self.strokes]) x_len = max_x - min_x self.width = x_len y_len = max_y - min_y self.height = y_len scale_factor = max(x_len, y_len) if scale_factor <= 0.0: return False scale_factor = 1.0 / scale_factor for stroke in self.strokes: stroke.scale_stroke(scale_factor) return True def _center_gesture(self): ''' Centers the Gesture.points of the gesture.''' total_x = 0.0 total_y = 0.0 total_points = 0 for stroke in self.strokes: # adds up all the points inside the stroke stroke_y = sum([pt.y for pt in stroke.points]) stroke_x = sum([pt.x for pt in stroke.points]) total_y += stroke_y total_x += stroke_x total_points += len(stroke.points) if total_points == 0: return False # Average to get the offset total_x /= total_points total_y /= total_points # Apply the offset to the strokes for stroke in self.strokes: stroke.center_stroke(total_x, total_y) return True
[docs] def add_stroke(self, point_list=None): '''Adds a stroke to the gesture and returns the Stroke instance. Optional point_list argument is a list of the mouse points for the stroke. ''' self.strokes.append(GestureStroke()) if isinstance(point_list, list) or isinstance(point_list, tuple): for point in point_list: if isinstance(point, GesturePoint): self.strokes[-1].points.append(point) elif isinstance(point, list) or isinstance(point, tuple): if len(point) != 2: raise ValueError("Stroke entry must have 2 values max") self.strokes[-1].add_point(point[0], point[1]) else: raise TypeError("The point list should either be " "tuples of x and y or a list of " "GesturePoint objects") elif point_list is not None: raise ValueError("point_list should be a tuple/list") return self.strokes[-1]
[docs] def normalize(self, stroke_samples=32): '''Runs the gesture normalization algorithm and calculates the dot product with self. ''' if not self._scale_gesture() or not self._center_gesture(): self.gesture_product = False return False for stroke in self.strokes: stroke.normalize_stroke(stroke_samples) self.gesture_product = self.dot_product(self)
[docs] def get_rigid_rotation(self, dstpts): ''' Extract the rotation to apply to a group of points to minimize the distance to a second group of points. The two groups of points are assumed to be centered. This is a simple version that just picks an angle based on the first point of the gesture. ''' if len(self.strokes) < 1 or len(self.strokes[0].points) < 1: return 0 if len(dstpts.strokes) < 1 or len(dstpts.strokes[0].points) < 1: return 0 p = dstpts.strokes[0].points[0] target = Vector([p.x, p.y]) source = Vector([p.x, p.y]) return source.angle(target)
[docs] def dot_product(self, comparison_gesture): ''' Calculates the dot product of the gesture with another gesture.''' if len(comparison_gesture.strokes) != len(self.strokes): return -1 if getattr(comparison_gesture, 'gesture_product', True) is False or \ getattr(self, 'gesture_product', True) is False: return -1 dot_product = 0.0 for stroke_index, (my_stroke, cmp_stroke) in enumerate( list(zip(self.strokes, comparison_gesture.strokes))): for pt_index, (my_point, cmp_point) in enumerate( list(zip(my_stroke.points, cmp_stroke.points))): dot_product += (my_point.x * cmp_point.x + my_point.y * cmp_point.y) return dot_product
def rotate(self, angle): g = Gesture() for stroke in self.strokes: tmp = [] for j in stroke.points: v = Vector([j.x, j.y]).rotate(angle) tmp.append(v) g.add_stroke(tmp) g.gesture_product = g.dot_product(g) return g
[docs] def get_score(self, comparison_gesture, rotation_invariant=True): ''' Returns the matching score of the gesture against another gesture. ''' if isinstance(comparison_gesture, Gesture): if rotation_invariant: # get orientation angle = self.get_rigid_rotation(comparison_gesture) # rotate the gesture to be in the same frame. comparison_gesture = comparison_gesture.rotate(angle) # this is the normal "orientation" code. score = self.dot_product(comparison_gesture) if score <= 0: return score score /= math.sqrt( self.gesture_product * comparison_gesture.gesture_product) return score
def __eq__(self, comparison_gesture): ''' Allows easy comparisons between gesture instances.''' if isinstance(comparison_gesture, Gesture): # If the gestures don't have the same number of strokes, its # definitely not the same gesture score = self.get_score(comparison_gesture) if (score > (1.0 - self.tolerance) and score < (1.0 + self.tolerance)): return True else: return False else: return NotImplemented def __ne__(self, comparison_gesture): result = self.__eq__(comparison_gesture) if result is NotImplemented: return result else: return not result def __lt__(self, comparison_gesture): raise TypeError("Gesture cannot be evaluated with <") def __gt__(self, comparison_gesture): raise TypeError("Gesture cannot be evaluated with >") def __le__(self, comparison_gesture): raise TypeError("Gesture cannot be evaluated with <=") def __ge__(self, comparison_gesture): raise TypeError("Gesture cannot be evaluated with >=")