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
import time
from threading import Thread
import pygame.display
from math import atan2, pi
from threading import Thread
from tuio_server import TuioServer2D
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):
......@@ -22,13 +46,26 @@ def add(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):
def __init__(self, sid, x, y):
self.start_time = self.update_time = time.time()
self.sid = sid
self.px = self.x = x
self.py = self.y = y
self.start_x = self.px = self.x = x
self.start_y = self.py = self.y = y
@property
def xy(self):
return self.x, self.y
def update(self, x, y):
self.update_time = time.time()
self.px = self.x
self.py = self.y
self.x = x
......@@ -44,6 +81,19 @@ class TouchPoint(object):
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
return da
def set_angle(self, angle):
self.old_angle = self.angle
self.angle = angle
......@@ -61,75 +111,50 @@ class TouchPoint(object):
def dy(self):
return int(self.y - self.py)
def down_time(self):
return time.time() - self.start_time
# Heuristic constants
# TODO: Encapsulate DPI resolution in distance heuristics
SUPPORTED_GESTURES = ('tap', 'pan', 'flick', 'rotate', 'pinch')
DOUBLE_TAP_DISTANCE_THRESHOLD = 30
FLICK_VELOCITY_TRESHOLD = 20
TAP_INTERVAL = .200
TAP_TIMEOUT = .200
MAX_MULTI_DRAG_DISTANCE = 100
def is_tap(self):
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
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)
self.verbose = verbose
self.tuio_verbose = tuio_verbose
self.last_tap = 0
self.update_rate = update_rate
self.points_changed = False
self.last_tap_time = 0
self.handlers = {}
# Session id's pointing to point coordinates
self.points = []
self.taps_down = []
self.taps = []
self.centroid = (0, 0)
# Put centroid outside screen to prevent misinterpretation
self.centroid = (-1., -1.)
def update_centroid(self):
def update_centroid(self, moving=None):
self.old_centroid = self.centroid
l = len(self.points)
if not l:
self.centroid = (0, 0)
if not len(self.points):
self.centroid = (-1., -1.)
return
cx, cy = zip(*[(p.x, p.y) for p in self.points])
self.centroid = (reduce(add, cx, 0) / l, reduce(add, cy, 0) / l)
def analyze(self):
self.detect_taps()
if self.points_changed:
self.update_centroid()
# Do not try to rotate or pinch while panning
# This gets rid of a lot of jittery events
if not self.detect_pan():
self.detect_rotation_and_pinch()
#use = filter(TouchPoint.is_stationary, self.points)
use = filter(lambda p: p != moving, self.points)
self.points_changed = False
if not use:
use = self.points
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 = []
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)
def detect_rotation_and_pinch(self):
"""
......@@ -189,25 +214,6 @@ class MultiTouchListener(Logger):
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):
for i, p in enumerate(self.points):
if p.sid == sid:
......@@ -216,55 +222,82 @@ 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)
p = TouchPoint(sid, x, y)
self.points.append(p)
self.update_centroid()
self.trigger(DownEvent(p))
def point_up(self, sid):
i, p = self.find_point(sid, index=True)
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]
self.update_centroid()
self.trigger(UpEvent(p))
# Tap/flick detection
if p in self.taps_down:
# Detect if Flick based on movement
if p.distance_to_prev() > FLICK_VELOCITY_TRESHOLD:
self.trigger(FlickEvent(p.px, p.py, (p.dx(), p.dy())))
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()
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:
if time.time() - self.last_tap < TAP_INTERVAL:
# Move from taps_down to taps for us in tap detection
self.taps_down.remove(p)
self.taps.append((p.x, p.y))
self.last_tap = p
self.last_tap_time = t
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):
self.find_point(sid).update(x, y)
self.points_changed = True
p = self.find_point(sid)
# 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 _tuio_server(self):
server = TuioServer2D(self, verbose=self.tuio_verbose)
server.start()
def stop(self):
self.log('Stopping event loop')
if self.thread:
self.thread.join()
self.thread = False
else:
self.server.stop()
def start(self):
def start(self, threaded=False):
"""
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:
# Run TUIO message listener in a different thread
#thread = Thread(target=self.__class__._tuio_server, args=(self))
thread = Thread(target=self._tuio_server)
thread.daemon = True
thread.start()
interval = 1. / self.update_rate
while True:
self.analyze()
time.sleep(interval)
self.log('Starting event loop')
self.server = TuioServer2D(self, verbose=self.tuio_verbose)
self.server.start()
except KeyboardInterrupt:
self.log('Stopping event loop')
self.stop()
def bind(self, gesture, handler):
if gesture not in SUPPORTED_GESTURES:
......@@ -276,18 +309,26 @@ class MultiTouchListener(Logger):
self.handlers[gesture].append(handler)
def trigger(self, event):
if event.gesture in self.handlers:
h = self.handlers[event.gesture]
self.log('Event triggered: "%s" (%d handlers)' % (event, len(h)))
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
return cx - ocx, cy - ocy
if __name__ == '__main__':
def 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('double_tap', tap)
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