touch.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. #!/usr/bin/env python
  2. import time
  3. from threading import Thread
  4. from math import atan2, pi
  5. from tuio_server import TuiListener
  6. from logger import Logger
  7. from events import TapEvent, FlickEvent, RotateEvent, PinchEvent, PanEvent
  8. def distance(a, b):
  9. """
  10. Calculate the distance between points a and b.
  11. """
  12. xa, ya = a
  13. xb, yb = b
  14. return ((xb - xa) ** 2 + (yb - ya) ** 2) ** .5
  15. def add(a, b):
  16. return a + b
  17. class TouchPoint(object):
  18. def __init__(self, sid, x, y):
  19. self.sid = sid
  20. self.x = x
  21. self.y = y
  22. def update(self, x, y):
  23. self.px = self.x
  24. self.py = self.y
  25. self.x = x
  26. self.y = y
  27. def distance_to(self, other_x, other_y):
  28. return distance(self.x, self.y, other_x, other_y)
  29. def init_gesture_data(self, cx, cy):
  30. self.pinch = self.old_pinch = self.distance_to(cx, cy)
  31. self.angle = self.old_angle = atan2(self.y - cy, self.x - cx)
  32. def set_angle(self, angle):
  33. self.old_angle = self.angle
  34. self.angle = angle
  35. def set_pinch(self, pinch):
  36. self.old_pinch = self.pinch
  37. self.pinch = pinch
  38. def angle_diff(self):
  39. return self.angle - self.old_angle
  40. def dx(self):
  41. return int(self.x - self.px)
  42. def dy(self):
  43. return int(self.y - self.py)
  44. # Heuristic constants
  45. # TODO: Encapsulate DPI resolution in distance heuristics
  46. SUPPORTED_GESTURES = ('tap', 'pan', 'flick', 'rotate', 'pinch')
  47. TUIO_ADDRESS = ('localhost', 3333)
  48. DOUBLE_TAP_DISTANCE_THRESHOLD = 30
  49. FLICK_VELOCITY_TRESHOLD = 20
  50. TAP_INTERVAL = .200
  51. TAP_TIMEOUT = .200
  52. MAX_MULTI_DRAG_DISTANCE = 100
  53. class MultiTouchListener(Logger):
  54. def __init__(self, verbose=0, update_rate=60, **kwargs):
  55. super(MultiTouchListener, self).__init__(**kwargs)
  56. self.verbose = verbose
  57. self.last_tap = 0
  58. self.update_rate = update_rate
  59. self.points_changed = False
  60. self.handlers = {}
  61. # Session id's pointing to point coordinates
  62. self.points = {}
  63. self.taps_down = []
  64. self.taps = []
  65. def update_centroid(self):
  66. self.old_centroid = self.centroid
  67. cx, cy = zip(*[(p.x, p.y) for p in self.points])
  68. l = len(self.points)
  69. self.centroid = (reduce(add, cx, 0) / l, reduce(add, cy, 0) / l)
  70. def analyze(self):
  71. self.detect_taps()
  72. if self.points_changed:
  73. self.update_centroid()
  74. # Do not try to rotate or pinch while panning
  75. # This gets rid of a lot of jittery events
  76. if not self.detect_pan():
  77. self.detect_rotation_and_pinch()
  78. self.points_changed = False
  79. def detect_taps(self):
  80. if len(self.taps) == 2:
  81. if distance(*self.taps) > DOUBLE_TAP_DISTANCE_THRESHOLD:
  82. # Taps are too far away too be a double tap, add 2 separate
  83. # events
  84. self.trigger(TapEvent(*self.taps[0]))
  85. self.trigger(TapEvent(*self.taps[1]))
  86. else:
  87. # Distance is within treshold, trigger a 'double tap' event
  88. self.trigger(TapEvent(*self.taps[0], double=True))
  89. self.taps = []
  90. elif len(self.taps) == 1:
  91. # FIXME: Ignore successive single- and double taps?
  92. if time.time() - self.last_tap > TAP_TIMEOUT:
  93. self.trigger(TapEvent(*self.taps[0]))
  94. self.taps = []
  95. def detect_rotation_and_pinch(self):
  96. """
  97. Rotation is the average angle change between each point and the
  98. centroid. Pinch is the average distance change from the points to the
  99. centroid.
  100. """
  101. l = len(self.points)
  102. if 'pinch' not in self.handlers or l < 2:
  103. return
  104. rotation = pinch = 0
  105. cx, cy = self.centroid
  106. for p in self.points:
  107. p.set_angle(atan2(p.y - cy, p.x - cx))
  108. da = p.angle_diff()
  109. # Assert that angle is in [-pi, pi]
  110. if da > pi:
  111. da -= 2 * pi
  112. elif da < pi:
  113. da += 2 * pi
  114. rotation += da
  115. p.set_pinch(distance(p.x, p.y, cx, cy))
  116. pinch += p.pinch_diff()
  117. if rotation:
  118. self.trigger(RotateEvent(cx, cy, rotation / l, l))
  119. if pinch:
  120. self.trigger(PinchEvent(cx, cy, pinch / l, l))
  121. def detect_pan(self):
  122. """
  123. Look for multi-finger drag events. Multi-drag is defined as all the
  124. fingers moving close-ish together in the same direction.
  125. """
  126. l = len(self.points)
  127. m = MAX_MULTI_DRAG_DISTANCE
  128. clustered = l == 1 or all([p.distance_to(*self.centroid) <= m \
  129. for p in self.points])
  130. directions = [(cmp(p.dx(), 0), cmp(p.dy(), 0)) for p in self.points]
  131. if any(map(all, zip(*directions))) and clustered:
  132. if l == 1:
  133. p = self.points[0]
  134. cx, cy, dx, dy = p.x, p.y, p.dx(), p.dy()
  135. else:
  136. cx, cy = self.centroid
  137. old_cx, old_cy = self.old_centroid
  138. dx, dy = cx - old_cx, cy - old_cy
  139. self.trigger(PanEvent(cx, cy, dx, dy, l))
  140. def point_down(self, sid, x, y):
  141. if sid in self.points:
  142. raise KeyError('Point with session id "%s" already exists.' % sid)
  143. self.points[sid] = p = TouchPoint(sid, x, y)
  144. self.update_centroid()
  145. # Detect multi-point gestures
  146. if len(self.points) > 1:
  147. p.init_gesture_data(*self.centroid)
  148. if len(self.points) == 2:
  149. first_p = self.points[0]
  150. first_p.init_gesture_data(*self.centroid)
  151. self.taps_down.append(p)
  152. self.last_tap = time.time()
  153. self.points_changed = True
  154. def point_up(self, sid):
  155. if sid not in self.points:
  156. raise KeyError('No point with session id "%s".' % sid)
  157. p = self.points[sid]
  158. del self.points[sid]
  159. # Tap/flick detection
  160. if p in self.taps_down:
  161. # Detect if Flick based on movement
  162. if distance(p.x, p.y, p.px, p.py) > FLICK_VELOCITY_TRESHOLD:
  163. self.trigger(FlickEvent(p.px, p.py, (p.dx(), p.dy())))
  164. else:
  165. if time.time() - self.last_tap < TAP_INTERVAL:
  166. # Move from taps_down to taps for us in tap detection
  167. self.taps_down.remove(p)
  168. self.taps.append((p.x, p.y))
  169. self.points_changed = True
  170. def point_move(self, sid, x, y):
  171. self.points[sid].update(x, y)
  172. self.points_changed = True
  173. def _tuio_handler(self, addr, tags, data, source):
  174. # TODO: Call self.point_{down,up,move}
  175. pass
  176. def _tuio_server(self):
  177. server = OSCServer(TUIO_ADDRESS)
  178. server.addDefaultHandlers()
  179. server.addMsgHandler('/tuio', self._tuio_handler)
  180. server.serve_forever()
  181. def start(self):
  182. """
  183. Start event loop.
  184. """
  185. self.log('Starting event loop')
  186. try:
  187. # Run TUIO message listener in a different thread
  188. #thread = Thread(target=self.__class__._tuio_server, args=(self))
  189. thread = Thread(target=self._tuio_server)
  190. thread.daemon = True
  191. thread.start()
  192. interval = 1. / self.update_rate
  193. while True:
  194. self.analyze()
  195. time.sleep(interval)
  196. except KeyboardInterrupt:
  197. self.log('Stopping event loop')
  198. def bind(self, gesture, handler):
  199. if gesture not in SUPPORTED_GESTURES:
  200. raise ValueError('Unsupported gesture "%s".' % gesture)
  201. if gesture not in self.handlers:
  202. self.handlers[gesture] = []
  203. self.handlers[gesture].append(handler)
  204. def trigger(self, event):
  205. if event.gesture in self.handlers:
  206. h = self.handlers[event.gesture]
  207. self.log('Event triggered: "%s" (%d handlers)' % (event, len(h)))
  208. for handler in h:
  209. handler(event)
  210. if __name__ == '__main__':
  211. def tap(event):
  212. print 'tap:', event
  213. loop = MultiTouchListener(verbose=2)
  214. loop.bind('tap', tap)
  215. loop.start()