Commit ca154c8e authored by Taddeus Kroes's avatar Taddeus Kroes

Removed detection loop from MultiTouchListener, it now only responds to TUIO...

Removed detection loop from MultiTouchListener, it now only responds to TUIO events (needs to be extended).
parent 55071adc
#!/usr/bin/env python #!/usr/bin/env python
import time import time
from threading import Thread import pygame.display
from math import atan2, pi from math import atan2, pi
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 TapEvent, FlickEvent, RotateEvent, PinchEvent, PanEvent from events import BasicEvent, DownEvent, UpEvent, MoveEvent, TapEvent, \
SingleTapEvent, DoubleTapEvent, FlickEvent, RotateEvent, PinchEvent, \
PanEvent
# get screen resolution
pygame.display.init()
info = pygame.display.Info()
screen_size = info.current_w, info.current_h
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')
DOUBLE_TAP_DISTANCE = .05
FLICK_VELOCITY_TRESHOLD = 20
TAP_TIMEOUT = .2
MAX_MULTI_DRAG_DISTANCE = .05
STATIONARY_TIME = .01
# 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
DIST_THRESHOLD = 1. / max(screen_size)
def distance(a, b): def distance(a, b):
...@@ -22,13 +46,26 @@ def add(a, b): ...@@ -22,13 +46,26 @@ def add(a, b):
return a + b return a + b
# Maximum distance between touch- and release location of a tap event
TAP_DISTANCE = .01
# Maximum duration of a tap
TAP_TIME = .300
class TouchPoint(object): class TouchPoint(object):
def __init__(self, sid, x, y): def __init__(self, sid, x, y):
self.start_time = self.update_time = time.time()
self.sid = sid self.sid = sid
self.px = self.x = x self.start_x = self.px = self.x = x
self.py = self.y = y self.start_y = self.py = self.y = y
@property
def xy(self):
return self.x, self.y
def update(self, x, y): def update(self, x, y):
self.update_time = time.time()
self.px = self.x self.px = self.x
self.py = self.y self.py = self.y
self.x = x self.x = x
...@@ -44,6 +81,19 @@ class TouchPoint(object): ...@@ -44,6 +81,19 @@ class TouchPoint(object):
self.pinch = self.old_pinch = self.distance_to(cx, cy) self.pinch = self.old_pinch = self.distance_to(cx, cy)
self.angle = self.old_angle = atan2(self.y - cy, self.x - cx) 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
return da
def set_angle(self, angle): def set_angle(self, angle):
self.old_angle = self.angle self.old_angle = self.angle
self.angle = angle self.angle = angle
...@@ -61,75 +111,50 @@ class TouchPoint(object): ...@@ -61,75 +111,50 @@ class TouchPoint(object):
def dy(self): def dy(self):
return int(self.y - self.py) return int(self.y - self.py)
def down_time(self):
return time.time() - self.start_time
# Heuristic constants def is_tap(self):
# TODO: Encapsulate DPI resolution in distance heuristics return self.distance_to_prev() < TAP_TIME \
SUPPORTED_GESTURES = ('tap', 'pan', 'flick', 'rotate', 'pinch') and self.distance_to(self.start_x, self.start_y) < TAP_DISTANCE
DOUBLE_TAP_DISTANCE_THRESHOLD = 30
FLICK_VELOCITY_TRESHOLD = 20 def movement(self):
TAP_INTERVAL = .200 return self.x - self.px, self.y - self.py
TAP_TIMEOUT = .200
MAX_MULTI_DRAG_DISTANCE = 100 def is_stationary(self):
return self.distance_to_prev() < DIST_THRESHOLD
class MultiTouchListener(Logger): class MultiTouchListener(Logger):
def __init__(self, update_rate=60, 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.tuio_verbose = tuio_verbose
self.last_tap = 0 self.last_tap_time = 0
self.update_rate = update_rate
self.points_changed = False
self.handlers = {} self.handlers = {}
# Session id's pointing to point coordinates # Session id's pointing to point coordinates
self.points = [] self.points = []
self.taps_down = [] # Put centroid outside screen to prevent misinterpretation
self.taps = [] self.centroid = (-1., -1.)
self.centroid = (0, 0)
def update_centroid(self): def update_centroid(self, moving=None):
self.old_centroid = self.centroid self.old_centroid = self.centroid
l = len(self.points)
if not l: if not len(self.points):
self.centroid = (0, 0) self.centroid = (-1., -1.)
return return
cx, cy = zip(*[(p.x, p.y) for p in self.points]) #use = filter(TouchPoint.is_stationary, self.points)
self.centroid = (reduce(add, cx, 0) / l, reduce(add, cy, 0) / l) use = filter(lambda p: p != moving, self.points)
def analyze(self):
self.detect_taps()
if self.points_changed: if not use:
self.update_centroid() use = self.points
# Do not try to rotate or pinch while panning l = len(use)
# This gets rid of a lot of jittery events cx, cy = zip(*[(p.x, p.y) for p in use])
if not self.detect_pan(): self.centroid = (reduce(add, cx, 0) / l, reduce(add, cy, 0) / l)
self.detect_rotation_and_pinch()
self.points_changed = False
def detect_taps(self):
if len(self.taps) == 2:
if distance(*self.taps) > DOUBLE_TAP_DISTANCE_THRESHOLD:
# Taps are too far away too be a double tap, add 2 separate
# events
self.trigger(TapEvent(*self.taps[0]))
self.trigger(TapEvent(*self.taps[1]))
else:
# Distance is within treshold, trigger a 'double tap' event
self.trigger(TapEvent(*self.taps[0], double=True))
self.taps = []
elif len(self.taps) == 1:
# FIXME: Ignore successive single- and double taps?
if time.time() - self.last_tap > TAP_TIMEOUT:
self.trigger(TapEvent(*self.taps[0]))
self.taps = []
def detect_rotation_and_pinch(self): def detect_rotation_and_pinch(self):
""" """
...@@ -189,25 +214,6 @@ class MultiTouchListener(Logger): ...@@ -189,25 +214,6 @@ class MultiTouchListener(Logger):
self.trigger(PanEvent(cx, cy, dx, dy, l)) self.trigger(PanEvent(cx, cy, dx, dy, l))
def point_down(self, sid, x, y):
if self.find_point(sid):
raise ValueError('Point with session id "%s" already exists.' % sid)
p = TouchPoint(sid, x, y)
self.points.append(p)
self.update_centroid()
# Detect multi-point gestures
if len(self.points) > 1:
p.init_gesture_data(*self.centroid)
if len(self.points) == 2:
self.points[0].init_gesture_data(*self.centroid)
self.taps_down.append(p)
self.last_tap = time.time()
self.points_changed = 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):
if p.sid == sid: if p.sid == sid:
...@@ -216,55 +222,82 @@ class MultiTouchListener(Logger): ...@@ -216,55 +222,82 @@ class MultiTouchListener(Logger):
if index: if index:
return -1, None 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)
p = TouchPoint(sid, x, y)
self.points.append(p)
self.update_centroid()
self.trigger(DownEvent(p))
def point_up(self, sid): def point_up(self, sid):
i, p = self.find_point(sid, index=True) i, p = self.find_point(sid, index=True)
if not p: if not p:
raise KeyError('No point with session id "%s".' % sid) raise KeyError('No point with session id "%d".' % sid)
del self.points[i] del self.points[i]
self.update_centroid()
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(TapEvent(p.x, p.y))
# Detect double tap by comparing time and distance from last tap
# event
t = time.time()
# Tap/flick detection if t - self.last_tap_time < TAP_TIMEOUT \
if p in self.taps_down: and p.distance_to(*self.last_tap.xy) < DOUBLE_TAP_DISTANCE:
# Detect if Flick based on movement self.trigger(DoubleTapEvent(p.x, p.y))
if p.distance_to_prev() > FLICK_VELOCITY_TRESHOLD:
self.trigger(FlickEvent(p.px, p.py, (p.dx(), p.dy())))
else: else:
if time.time() - self.last_tap < TAP_INTERVAL: self.last_tap = p
# Move from taps_down to taps for us in tap detection self.last_tap_time = t
self.taps_down.remove(p)
self.taps.append((p.x, p.y))
self.points_changed = True # TODO: Detect flick
#elif p.is_flick():
# self.trigger(FlickEvent(p.x, p.y))
def point_move(self, sid, x, y): def point_move(self, sid, x, y):
self.find_point(sid).update(x, y) p = self.find_point(sid)
self.points_changed = True
# 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))
# TODO: Detect pan
def stop(self):
self.log('Stopping event loop')
def _tuio_server(self): if self.thread:
server = TuioServer2D(self, verbose=self.tuio_verbose) self.thread.join()
server.start() self.thread = False
else:
self.server.stop()
def start(self): def start(self, threaded=False):
""" """
Start event loop. Start event loop.
""" """
self.log('Starting event loop') if threaded:
self.thread = Thread(target=self.start, kwargs={'threaded': False})
self.thread.daemon = True
self.thread.start()
return
try: try:
# Run TUIO message listener in a different thread self.log('Starting event loop')
#thread = Thread(target=self.__class__._tuio_server, args=(self)) self.server = TuioServer2D(self, verbose=self.tuio_verbose)
thread = Thread(target=self._tuio_server) self.server.start()
thread.daemon = True
thread.start()
interval = 1. / self.update_rate
while True:
self.analyze()
time.sleep(interval)
except KeyboardInterrupt: except KeyboardInterrupt:
self.log('Stopping event loop') self.stop()
def bind(self, gesture, handler): def bind(self, gesture, handler):
if gesture not in SUPPORTED_GESTURES: if gesture not in SUPPORTED_GESTURES:
...@@ -276,18 +309,26 @@ class MultiTouchListener(Logger): ...@@ -276,18 +309,26 @@ class MultiTouchListener(Logger):
self.handlers[gesture].append(handler) self.handlers[gesture].append(handler)
def trigger(self, event): def trigger(self, event):
if event.gesture in self.handlers: if event.__class__._name in self.handlers:
h = self.handlers[event.gesture] 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)))
for handler in h: for handler in h:
handler(event) handler(event)
def centroid_movement(self):
cx, cy = self.centroid
ocx, ocy = self.old_centroid
return cx - ocx, cy - ocy
if __name__ == '__main__': if __name__ == '__main__':
def tap(event): def tap(event):
print 'tap:', event print 'tap:', event
loop = MultiTouchListener(verbose=2, tuio_verbose=1) loop = MultiTouchListener(verbose=1, tuio_verbose=0)
loop.bind('tap', tap) loop.bind('tap', tap)
loop.bind('double_tap', tap)
loop.start() loop.start()
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