test-kivy-app/kivy_venv/lib/python3.11/site-packages/kivy/gesture.py

425 lines
14 KiB
Python
Raw Permalink Normal View History

2024-09-15 12:12:16 +00:00
'''
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)])
g.normalize()
# Add it to the database
gdb = GestureDatabase()
gdb.add_gesture(g)
# And for the next gesture, try to find it!
g2 = Gesture()
# ...
gdb.find(g2)
.. 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
class GestureDatabase(object):
'''Class to handle a gesture database.'''
def __init__(self):
self.db = []
def add_gesture(self, gesture):
'''Add a new gesture to the database.'''
self.db.append(gesture)
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)
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
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)
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
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))
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]
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)
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
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
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
class Gesture:
'''A python implementation of a gesture recognition algorithm by
Oleg Dopertchouk: http://www.gamedev.net/reference/articles/article2039.asp
Implemented by Jeiel Aranal (chemikhazi@gmail.com),
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
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]
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)
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)
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
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 >=")