Commit 1aff9a2e authored by Taddeus Kroes's avatar Taddeus Kroes

Code cleanup.

parent 9807d347
......@@ -6,9 +6,9 @@ from threading import Thread
from tuio_server import TuioServer2D
from logger import Logger
from events import BasicEvent, DownEvent, UpEvent, MoveEvent, TapEvent, \
SingleTapEvent, DoubleTapEvent, FlickEvent, RotateEvent, PinchEvent, \
PanEvent
from events import Event, DownEvent, UpEvent, MoveEvent, Tap, \
SingleTap, DoubleTap, Flick, Rotate, Pinch, \
Pan
# get screen resolution
pygame.display.init()
......@@ -18,14 +18,17 @@ pygame.display.quit()
# Heuristic constants
# TODO: Encapsulate screen resolution in distance heuristics
SUPPORTED_GESTURES = ('down', 'up', 'move', 'tap', 'single_tap', 'double_tap',
'pan', 'flick', 'rotate', 'pinch')
SUPPORTED_EVENTS = ('down', 'up', 'move', 'tap', 'single_tap', 'double_tap',
'pan', 'flick', 'rotate', 'pinch')
DOUBLE_TAP_DISTANCE = .05
FLICK_VELOCITY_TRESHOLD = 20
TAP_TIMEOUT = .2
MAX_MULTI_DRAG_DISTANCE = .05
STATIONARY_TIME = .01
# Detect gestures 60 times per second
GESTURE_UPDATE_RATE = 60
# Minimum distance for two coordinates to be considered different
# Theoretically, this should be one pixel because that is the minimal movement
# of a mouse cursor on the screen
......@@ -34,7 +37,8 @@ DIST_THRESHOLD = 1. / max(screen_size)
def distance(a, b):
"""
Calculate the distance between points a and b.
Calculate the Pythagorian distance between points a and b (which are (x, y)
tuples).
"""
xa, ya = a
xb, yb = b
......@@ -43,6 +47,9 @@ def distance(a, b):
def add(a, b):
"""
Add a an b, used for some functional programming calls.
"""
return a + b
......@@ -77,39 +84,30 @@ class TouchPoint(object):
def distance_to_prev(self):
return self.distance_to(self.px, self.py)
def init_gesture_data(self, cx, cy):
self.pinch = self.old_pinch = self.distance_to(cx, cy)
self.angle = self.old_angle = atan2(self.y - cy, self.x - cx)
def rotation_around(cx, cy):
angle = atan2(cy - self.y, self.x - cx)
prev_angle = atan2(cy - self.py, self.px - cx)
da = angle - prev_angle
# Assert that angle is in [-pi, pi]
if da > pi:
da -= 2 * pi
elif da < pi:
da += 2 * pi
def set_centroid(self, cx, cy):
self.pinch = self.distance_to(cx, cy)
self.angle = atan2(cy - self.y, self.x - cx)
return da
def set_angle(self, angle):
self.old_angle = self.angle
self.angle = angle
def set_pinch(self, pinch):
self.old_pinch = self.pinch
self.pinch = pinch
if self.pinch != None:
self.old_pinch = self.pinch
self.old_angle = self.angle
self.pinch = pinch
self.angle = angle
else:
self.old_pinch = self.pinch = pinch
self.old_angle = self.angle = angle
def angle_diff(self):
return self.angle - self.old_angle
def pinch_diff(self):
return self.pinch - self.old_pinch
def dx(self):
return int(self.x - self.px)
return self.x - self.px
def dy(self):
return int(self.y - self.py)
return self.y - self.py
def down_time(self):
return time.time() - self.start_time
......@@ -118,9 +116,6 @@ class TouchPoint(object):
return self.distance_to_prev() < TAP_TIME \
and self.distance_to(self.start_x, self.start_y) < TAP_DISTANCE
def movement(self):
return self.x - self.px, self.y - self.py
def is_stationary(self):
return self.distance_to_prev() < DIST_THRESHOLD
......@@ -129,9 +124,11 @@ class MultiTouchListener(Logger):
def __init__(self, verbose=0, tuio_verbose=0, **kwargs):
super(MultiTouchListener, self).__init__(**kwargs)
self.verbose = verbose
self.tuio_verbose = tuio_verbose
self.last_tap = None
self.last_tap_time = 0
self.handlers = {}
self.thread = None
self.points_changed = False
# Session id's pointing to point coordinates
self.points = []
......@@ -139,22 +136,74 @@ class MultiTouchListener(Logger):
# Put centroid outside screen to prevent misinterpretation
self.centroid = (-1., -1.)
def update_centroid(self, moving=None):
self.old_centroid = self.centroid
self.server = TuioServer2D(self, verbose=tuio_verbose)
if not len(self.points):
self.centroid = (-1., -1.)
return
def point_down(self, sid, x, y):
"""
Called by TUIO listener when a new touch point is created, triggers a
DownEvent.
"""
if self.find_point(sid):
raise ValueError('Point with session id "%d" already exists.'
% sid)
#use = filter(TouchPoint.is_stationary, self.points)
use = filter(lambda p: p != moving, self.points)
p = TouchPoint(sid, x, y)
self.points.append(p)
self.trigger(DownEvent(p))
self.points_changed = True
if not use:
use = self.points
def point_up(self, sid):
"""
Called by TUIO listener when a touch point is removed, triggers an
UpEvent. Also, simple/double tap detection is located here instead of
in the gesture thread (for responsiveness reasons).
"""
i, p = self.find_point(sid, index=True)
l = len(use)
cx, cy = zip(*[(p.x, p.y) for p in use])
self.centroid = (reduce(add, cx, 0) / l, reduce(add, cy, 0) / l)
if not p:
raise KeyError('No point with session id "%d".' % sid)
del self.points[i]
self.trigger(UpEvent(p))
if p.is_tap():
# Always trigger a regular tap event, also in case of double tap
# (use the 'single_tap' event to keep single/double apart from
# eachother)
self.trigger(Tap(p.x, p.y))
# Detect double tap by comparing time and distance to last tap
# event
t = time.time()
if t - self.last_tap_time < TAP_TIMEOUT \
and p.distance_to(*self.last_tap.xy) < DOUBLE_TAP_DISTANCE:
self.trigger(DoubleTap(p.x, p.y))
self.last_tap = None
self.last_tap_time = 0
else:
self.last_tap = p
self.last_tap_time = t
# TODO: Detect flick
#elif p.is_flick():
# self.trigger(Flick(p.x, p.y))
self.points_changed = True
def point_move(self, sid, x, y):
"""
Called by TUIO listener when a touch point moves, triggers a MoveEvent.
The move event is only used if the movement distance is greater that a
preset constant, so that negligible movement is ignored. This prevents
unnecessary gesture detection.
"""
p = self.find_point(sid)
if p.distance_to(x, y) > DIST_THRESHOLD:
p.update(x, y)
self.trigger(MoveEvent(p))
self.points_changed = True
def detect_rotation_and_pinch(self):
"""
......@@ -164,32 +213,35 @@ class MultiTouchListener(Logger):
"""
l = len(self.points)
if 'pinch' not in self.handlers or l < 2:
if l < 2 or ('pinch' not in self.handlers \
and 'rotate' not in self.handlers):
return
rotation = pinch = 0
cx, cy = self.centroid
#rotation = pinch = 0
for p in self.points:
p.set_angle(atan2(p.y - cy, p.x - cx))
da = p.angle_diff()
#for p in self.points:
# da = p.angle_diff()
# Assert that angle is in [-pi, pi]
if da > pi:
da -= 2 * pi
elif da < pi:
da += 2 * pi
# # Assert that angle is in [-pi, pi]
# if da > pi:
# da -= 2 * pi
# elif da < pi:
# da += 2 * pi
rotation += da
# rotation += da
# pinch += p.pinch_diff()
p.set_pinch(p.distance_to(cx, cy))
pinch += p.pinch_diff()
angles, pinches = zip(*[(p.angle_diff(), p.pinch_diff())
for p in self.points])
rotation = reduce(add, angles)
pinch = reduce(add, pinches)
if rotation:
self.trigger(RotateEvent(cx, cy, rotation / l, l))
self.trigger(Rotate(cx, cy, rotation / l, l))
if pinch:
self.trigger(PinchEvent(cx, cy, pinch / l, l))
self.trigger(Pinch(cx, cy, pinch / l * 2, l))
def detect_pan(self):
"""
......@@ -197,22 +249,30 @@ class MultiTouchListener(Logger):
fingers moving close-ish together in the same direction.
"""
l = len(self.points)
if not l:
return False
m = MAX_MULTI_DRAG_DISTANCE
clustered = l == 1 or all([p.distance_to(*self.centroid) <= m \
for p in self.points])
directions = [(cmp(p.dx(), 0), cmp(p.dy(), 0)) \
for p in self.points]
if any(map(all, zip(*directions))) and clustered:
if l == 1:
p = self.points[0]
cx, cy, dx, dy = p.x, p.y, p.dx(), p.dy()
else:
cx, cy = self.centroid
old_cx, old_cy = self.old_centroid
dx, dy = cx - old_cx, cy - old_cy
if not clustered or not any(map(all, zip(*directions))):
return False
self.trigger(PanEvent(cx, cy, dx, dy, l))
if l == 1:
p = self.points[0]
cx, cy, dx, dy = p.x, p.y, p.dx(), p.dy()
else:
cx, cy = self.centroid
old_cx, old_cy = self.old_centroid
dx, dy = cx - old_cx, cy - old_cy
self.trigger(Pan(cx, cy, dx, dy, l))
return True
def find_point(self, sid, index=False):
for i, p in enumerate(self.points):
......@@ -222,123 +282,158 @@ class MultiTouchListener(Logger):
if index:
return -1, None
def point_down(self, sid, x, y):
if self.find_point(sid):
raise ValueError('Point with session id "%d" already exists.' % sid)
def detect_pinch(self, moved):
cx, cy = self.centroid
dist = moved.distance_to(cx, cy)
old_dist = distance((moved.px, moved.py), self.centroid)
p = TouchPoint(sid, x, y)
self.points.append(p)
self.update_centroid()
self.trigger(DownEvent(p))
if abs(dist - old_dist) > DIST_THRESHOLD:
self.trigger(Pinch(cx, cy, dist / old_dist,
len(self.points)))
def point_up(self, sid):
i, p = self.find_point(sid, index=True)
def detect_single_tap(self):
"""
Check if a single tap event should be triggered by checking is the last
tap.
"""
if self.last_tap and time.time() - self.last_tap_time >= TAP_TIMEOUT:
self.trigger(SingleTap(*self.last_tap.xy))
self.last_tap = None
self.last_tap_time = 0
if not p:
raise KeyError('No point with session id "%d".' % sid)
def update_centroid(self):
"""
Calculate the centroid of all current touch points.
"""
self.old_centroid = self.centroid
l = len(self.points)
del self.points[i]
self.update_centroid()
self.trigger(UpEvent(p))
# If there are no touch points, move the entroid to outside the screen
if not l:
self.centroid = (-1., -1.)
return
if p.is_tap():
# Always trigger a regular tap event, also in case of double tap
# (use the 'single_tap' event to keep single/double apart from
# eachother)
self.trigger(TapEvent(p.x, p.y))
cx, cy = zip(*[(p.x, p.y) for p in self.points])
self.centroid = (reduce(add, cx, 0) / l, reduce(add, cy, 0) / l)
# Detect double tap by comparing time and distance from last tap
# event
t = time.time()
# Update angle and pinch of all touch points
for p in self.points:
p.set_centroid(*self.centroid)
if t - self.last_tap_time < TAP_TIMEOUT \
and p.distance_to(*self.last_tap.xy) < DOUBLE_TAP_DISTANCE:
self.trigger(DoubleTapEvent(p.x, p.y))
else:
self.last_tap = p
self.last_tap_time = t
def centroid_movement(self):
cx, cy = self.centroid
ocx, ocy = self.old_centroid
# TODO: Detect flick
#elif p.is_flick():
# self.trigger(FlickEvent(p.x, p.y))
return cx - ocx, cy - ocy
def point_move(self, sid, x, y):
p = self.find_point(sid)
def detect_gestures(self):
"""
Detect if any gestures have occured in the past gesture frame. This
method is called in each time interval of the gesture thread, for
gestures that can only be detected using accumulated point down/up/move
events.
"""
# Simple and double taps are detected in the main thread, specific
# single-tap in the gesture thread
self.detect_single_tap()
# Optimization: only update if the point has moved far enough
if p.distance_to(x, y) > DIST_THRESHOLD:
p.update(x, y)
self.update_centroid(moving=p)
self.trigger(MoveEvent(p))
self.detect_pinch(p)
# If the touch points have not been updated, neither have the gestures
if self.points_changed:
# The new centroid is used for pan/rotate/pinch detection
self.update_centroid()
# TODO: Detect pan
# If a pan event is detected, ignore any rotate or pinch movement
# (they are considered noise)
if not self.detect_pan():
self.detect_rotation_and_pinch()
def detect_pinch(self, moved):
cx, cy = self.centroid
dist = moved.distance_to(cx, cy)
old_dist = distance((moved.px, moved.py), self.centroid)
self.points_changed = False
if abs(dist - old_dist) > DIST_THRESHOLD:
self.trigger(PinchEvent(cx, cy, dist / old_dist,
len(self.points)))
def start_gesture_thread(self):
"""
Loop of the gesture thread.
"""
interval = 1. / GESTURE_UPDATE_RATE
while True:
self.detect_gestures()
time.sleep(interval)
def stop(self):
self.log('Stopping event loop')
"""
Stop main event loop.
"""
self.log('Stopping TUIO server')
self.server.stop()
if self.thread:
self.log('Stopping main loop thread')
self.thread.join()
self.thread = False
else:
self.server.stop()
self.thread = None
def start(self, threaded=False):
def start(self, threaded=False, daemon=False):
"""
Start event loop.
Start main event loop. If threaded is set to True, the main loop is
started in a new thread. If daemon is also set to True, that thread
will be daemonic. The daemon option makes a call to stop() unnecessary.
"""
if threaded:
self.thread = Thread(target=self.start, kwargs={'threaded': False})
self.thread.daemon = True
self.log('Creating %sthread for main loop'
% ('daemon ' if daemon else ''))
self.thread = Thread(target=self.start)
self.thread.daemon = daemon
self.thread.start()
return
# Start gesture thread
self.log('Starting gesture thread')
gesture_thread = Thread(target=self.start_gesture_thread)
gesture_thread.daemon = True
gesture_thread.start()
# Start TUIO listener
try:
self.log('Starting event loop')
self.server = TuioServer2D(self, verbose=self.tuio_verbose)
self.log('Starting TUIO server')
self.server.start()
except KeyboardInterrupt:
except SystemExit:
self.stop()
def bind(self, gesture, handler):
if gesture not in SUPPORTED_GESTURES:
def bind(self, gesture, handler, *args, **kwargs):
"""
Bind a handler to an event or gesture.
"""
if gesture not in SUPPORTED_EVENTS:
raise ValueError('Unsupported gesture "%s".' % gesture)
if gesture not in self.handlers:
self.handlers[gesture] = []
self.handlers[gesture].append(handler)
self.handlers[gesture].append((handler, args, kwargs))
def trigger(self, event):
"""
Call all handlers bound to the name of the triggered event.
"""
if event.__class__._name in self.handlers:
h = self.handlers[event.__class__._name]
self.log('Event triggered: "%s" (%d handlers)' % (event, len(h)),
1 + int(isinstance(event, BasicEvent)))
for handler in h:
handler(event)
def centroid_movement(self):
cx, cy = self.centroid
ocx, ocy = self.old_centroid
1 + int(isinstance(event, Event)))
return cx - ocx, cy - ocy
for handler, args, kwargs in h:
handler(event, *args, **kwargs)
if __name__ == '__main__':
def tap(event):
print 'tap:', event
loop = MultiTouchListener(verbose=1, tuio_verbose=0)
loop.bind('tap', tap)
loop.bind('double_tap', tap)
loop.start()
def tap(event, tap_type):
print 'tap:', tap_type
listener = MultiTouchListener(verbose=1, tuio_verbose=0)
listener.bind('tap', tap, 0)
listener.bind('single_tap', tap, 1)
listener.bind('double_tap', tap, 2)
listener.bind('rotate', lambda e: 0)
try:
listener.start()
except KeyboardInterrupt:
listener.stop()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment