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

Code cleanup.

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