Dennis Kempin | 1a8a5be | 2013-06-18 11:00:02 -0700 | [diff] [blame] | 1 | # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | # |
| 5 | import decimal |
| 6 | import json |
| 7 | import math |
| 8 | |
| 9 | class GestureLog(object): |
| 10 | """ Represents the gestures in an activity log. |
| 11 | |
| 12 | The gesture log is a representation of an activity log as it is generated |
| 13 | by the replay tool or 'tpcontrol log'. |
| 14 | It converts all gestures into a list of events using the classes below. |
| 15 | To allow easier processing all events that belong together are merged into |
| 16 | gestures. For example all scroll events from one scroll motion on the touchpad |
| 17 | are merged to create a single scroll gesture. |
| 18 | - self.events will contain the list of events |
| 19 | - self.gestures the list of gestures. |
| 20 | """ |
| 21 | def __init__(self, log): |
| 22 | decimal.setcontext(decimal.Context(prec=8)) |
| 23 | self.raw = json.loads(log, parse_float=decimal.Decimal) |
| 24 | raw_events = filter(lambda e: e['type'] == 'gesture', self.raw['entries']) |
| 25 | self.events = self._ParseRawEvents(raw_events) |
| 26 | self.gestures = self._MergeGestures(self.events) |
| 27 | |
| 28 | self.properties = self.raw['properties'] |
| 29 | self.hwstates = filter(lambda e: e['type'] == 'hardwareState', |
| 30 | self.raw['entries']) |
| 31 | self.hwproperties = self.raw['hardwareProperties'] |
| 32 | |
| 33 | def _ParseRawEvents(self, gesture_list): |
| 34 | events = [] |
| 35 | |
| 36 | for gesture in gesture_list: |
| 37 | start_time = gesture['startTime'] |
| 38 | end_time = gesture['endTime'] |
| 39 | |
| 40 | if gesture['gestureType'] == 'move': |
| 41 | events.append(MotionGesture(gesture['dx'], gesture['dy'], start_time, |
| 42 | end_time)) |
| 43 | |
| 44 | elif gesture['gestureType'] == 'buttonsChange': |
| 45 | if gesture['down'] != 0: |
| 46 | button = gesture['down'] |
| 47 | events.append(ButtonDownGesture(button, start_time, end_time)) |
| 48 | if gesture['up'] != 0: |
| 49 | button = gesture['up'] |
| 50 | events.append(ButtonUpGesture(button, start_time, end_time)) |
| 51 | |
| 52 | elif gesture['gestureType'] == 'scroll': |
| 53 | events.append(ScrollGesture(gesture['dx'], gesture['dy'], start_time, |
| 54 | end_time)) |
| 55 | |
| 56 | elif gesture['gestureType'] == 'pinch': |
| 57 | events.append(PinchGesture(gesture['dz'], start_time, end_time)) |
| 58 | |
| 59 | elif gesture['gestureType'] == 'swipe': |
| 60 | events.append(SwipeGesture(gesture['dx'], gesture['dy'], start_time, |
| 61 | end_time)) |
| 62 | |
| 63 | elif gesture['gestureType'] == 'swipeLift': |
| 64 | events.append(SwipeLiftGesture(start_time, end_time)) |
| 65 | |
| 66 | elif gesture['gestureType'] == 'fling': |
| 67 | if gesture['flingState'] == 1: |
| 68 | events.append(FlingStopGesture(start_time, end_time)) |
| 69 | else: |
| 70 | events.append(FlingGesture(gesture['vx'], gesture['vy'], start_time, |
| 71 | end_time)) |
Dennis Kempin | 06a40b7 | 2013-07-17 14:10:06 -0700 | [diff] [blame] | 72 | elif gesture['gestureType'] == 'metrics': |
| 73 | # ignore |
| 74 | pass |
Dennis Kempin | 1a8a5be | 2013-06-18 11:00:02 -0700 | [diff] [blame] | 75 | else: |
| 76 | print 'Unknown gesture:', repr(gesture) |
| 77 | |
| 78 | return events |
| 79 | |
| 80 | def _MergeGestures(self, event_list): |
| 81 | gestures = [] |
| 82 | last_event_of_type = {} |
| 83 | |
| 84 | for event in event_list: |
| 85 | # merge motion and scroll events into gestures |
| 86 | if (event.type == 'Motion' or event.type == 'Scroll' or |
| 87 | event.type == 'Swipe' or event.type == 'Pinch'): |
| 88 | if event.type not in last_event_of_type: |
| 89 | last_event_of_type[event.type] = event |
| 90 | gestures.append(event) |
| 91 | else: |
| 92 | if float(event.start - last_event_of_type[event.type].end) < 0.1: |
| 93 | last_event_of_type[event.type].Append(event) |
| 94 | else: |
| 95 | last_event_of_type[event.type] = event |
| 96 | gestures.append(event) |
| 97 | else: |
| 98 | if event.type == 'ButtonUp' or event.type == 'ButtonDown': |
| 99 | last_event_of_type = {} |
| 100 | gestures.append(event) |
| 101 | return gestures |
| 102 | |
| 103 | |
| 104 | class AxisGesture(object): |
| 105 | """ Generic gesture class to describe gestures with x/y or z axis. """ |
| 106 | |
| 107 | def __init__(self, dx, dy, dz, start, end): |
| 108 | """ Create a new instance describing a single axis event. |
| 109 | |
| 110 | To describe a list of events that form a gesture use the Append method. |
| 111 | @param dx: movement in x coords |
| 112 | @param dy: movement in y coords |
| 113 | @param dz: movement in z coords |
| 114 | @param start: start timestamp |
| 115 | @param end: end timestamp |
| 116 | """ |
| 117 | self.dx = math.fabs(dx) |
| 118 | self.dy = math.fabs(dy) |
| 119 | self.dz = math.fabs(dz) |
| 120 | self.start = float(start) |
| 121 | self.end = float(end) |
| 122 | self.segments = [] |
| 123 | |
| 124 | self.distance = math.sqrt(self.dx * self.dx + self.dy * self.dy + |
| 125 | self.dz * self.dz) |
| 126 | self.segments.append(self.distance) |
| 127 | |
| 128 | def Append(self, motion): |
| 129 | """ Append an motion event to build a gesture. """ |
| 130 | self.dx = self.dx + motion.dx |
| 131 | self.dy = self.dy + motion.dy |
| 132 | self.dz = self.dz + motion.dz |
| 133 | self.distance = self.distance + motion.distance |
| 134 | self.segments.append(motion.distance) |
| 135 | self.end = motion.end |
| 136 | |
| 137 | def Speed(self): |
| 138 | """ Average speed of motion in mm/s. """ |
| 139 | if self.end > self.start: |
| 140 | return self.distance / (self.end - self.start) |
| 141 | else: |
| 142 | return float('+inf') |
| 143 | |
| 144 | def Roughness(self): |
| 145 | """ Returns the roughness of this gesture. |
| 146 | |
| 147 | The roughness measures the variability in the movement events. A continuous |
| 148 | stream of events with similar movement distances is considered to be smooth. |
| 149 | Choppy movement with a high variation in movement distances on the other |
| 150 | hand is considered as rough. i.e. a constant series of movement distances is |
| 151 | considered perfectly smooth and will result in a roughness of 0. |
| 152 | Whenever there are sudden changes, or very irregular movement distances |
| 153 | the roughness will increase. |
| 154 | """ |
| 155 | # Each event in the gesture resulted in a movement distance. These distances |
| 156 | # are treated as a signal and high pass filtered. This results in a signal |
| 157 | # containing only high frequency changes in the distance, i.e. the rough |
| 158 | # parts. |
| 159 | # The squared average of this signal is used as a measure for the roughness. |
| 160 | |
| 161 | # gaussian filter kernel with sigma=1: |
| 162 | # The kernel is calculated using this formula (with s=sigma): |
| 163 | # 1/sqrt(2*pi*s^2)*e^(-x^2/(2*s^2)) |
| 164 | # The array can be recalculated with modified sigma by entering the |
| 165 | # following equation into http://www.wolframalpha.com/: |
| 166 | # 1/sqrt(2*pi*s^2)*e^(-[-2, -1, 0, 1, 2]^2/(2*s^2)) |
| 167 | # (Replace s with the desired sigma value) |
| 168 | gaussian = [0.053991, 0.241971, 0.398942, 0.241971, 0.053991] |
| 169 | |
| 170 | # normalize gaussian filter kernel |
| 171 | gsum = sum(gaussian) |
| 172 | gaussian = map(lambda g: g / gsum, gaussian) |
| 173 | |
| 174 | # add padding to the front/end of the distances |
| 175 | segments = [] |
| 176 | segments.append(self.segments[0]) |
| 177 | segments.append(self.segments[0]) |
| 178 | segments.extend(self.segments) |
| 179 | segments.append(self.segments[-1]) |
| 180 | segments.append(self.segments[-1]) |
| 181 | |
| 182 | # low pass filter the distances |
| 183 | segments_lp = [] |
| 184 | for i in range(2, len(segments) - 2): |
| 185 | v = segments[i - 2] * gaussian[0] |
| 186 | v = v + segments[i - 1] * gaussian[1] |
| 187 | v = v + segments[i ] * gaussian[2] |
| 188 | v = v + segments[i + 1] * gaussian[3] |
| 189 | v = v + segments[i + 2] * gaussian[4] |
| 190 | segments_lp.append(v) |
| 191 | |
| 192 | # H_HP = 1 - H_LP |
| 193 | segments_hp = [] |
| 194 | for i in range(0, len(self.segments)): |
| 195 | segments_hp.append(self.segments[i] - segments_lp[i]) |
| 196 | # square signal and calculate squared average |
| 197 | segments_hp_sq = map(lambda v:v * v, segments_hp) |
| 198 | return math.sqrt(sum(segments_hp_sq) / len(segments_hp)) |
| 199 | |
| 200 | def __str__(self): |
| 201 | fstr = '{0} d={1:.4g} x={2:.4g} y={3:.4g} z={4:.4g} r={5:.4g} s={6:.4g}' |
| 202 | return fstr.format(self.__class__.type, self.distance, self.dx, |
| 203 | self.dy, self.dz, self.Roughness(), self.Speed()) |
| 204 | |
| 205 | def __repr__(self): |
| 206 | return str(self) |
| 207 | |
| 208 | class MotionGesture(AxisGesture): |
| 209 | """ The motion gesture is only using the X and Y axis. """ |
| 210 | type = 'Motion' |
| 211 | |
| 212 | def __init__(self, dx, dy, start, end): |
| 213 | AxisGesture.__init__(self, dx, dy, 0, start, end) |
| 214 | |
| 215 | def __str__(self): |
| 216 | fstr = '{0} d={1:.4g} x={2:.4g} y={3:.4g} r={4:.4g} s={5:.4g}' |
| 217 | return fstr.format(self.__class__.type, self.distance, self.dx, |
| 218 | self.dy, self.Roughness(), self.Speed()) |
| 219 | |
| 220 | class ScrollGesture(MotionGesture): |
| 221 | """ The scroll gesture is functionally the same as the MotionGesture """ |
| 222 | type = 'Scroll' |
| 223 | |
| 224 | |
| 225 | class PinchGesture(AxisGesture): |
| 226 | """ The pinch gesture is functionally the same as the MotionGesture. |
| 227 | |
| 228 | However only uses the dx variable to represent the zoom factor. |
| 229 | """ |
| 230 | type = 'Pinch' |
| 231 | |
| 232 | def __init__(self, dz, start, end): |
| 233 | AxisGesture.__init__(self, 0, 0, dz, start, end) |
| 234 | |
| 235 | def __str__(self): |
| 236 | fstr = '{0} dz={1:.4g} r={2:.4g}' |
| 237 | return fstr.format(self.__class__.type, self.dz, self.Roughness()) |
| 238 | |
| 239 | |
| 240 | class SwipeGesture(MotionGesture): |
| 241 | """ The swipe gesture is functionally the same as the MotionGesture """ |
| 242 | type = 'Swipe' |
| 243 | |
| 244 | |
| 245 | class FlingGesture(MotionGesture): |
| 246 | """ The scroll gesture is functionally the same as the MotionGesture """ |
| 247 | type = 'Fling' |
| 248 | |
| 249 | |
| 250 | class FlingStopGesture(object): |
| 251 | """ The FlingStop gesture only contains the start and end timestamp. """ |
| 252 | type = 'FlingStop' |
| 253 | |
| 254 | def __init__(self, start, end): |
| 255 | self.start = start |
| 256 | self.end = end |
| 257 | |
| 258 | def __str__(self): |
| 259 | return self.__class__.type |
| 260 | |
| 261 | def __repr__(self): |
| 262 | return str(self) |
| 263 | |
| 264 | |
| 265 | class SwipeLiftGesture(object): |
| 266 | """ The SwipeLift gesture only contains the start and end timestamp. """ |
| 267 | type = 'SwipeLift' |
| 268 | |
| 269 | def __init__(self, start, end): |
| 270 | self.start = start |
| 271 | self.end = end |
| 272 | |
| 273 | def __str__(self): |
| 274 | return self.__class__.type |
| 275 | |
| 276 | def __repr__(self): |
| 277 | return str(self) |
| 278 | |
| 279 | |
| 280 | class AbstractButtonGesture(object): |
| 281 | """ Abstract gesture for up and down button gestures. |
| 282 | |
| 283 | As both button down and up gestures are functionally identical it has |
| 284 | been extracted to this class. The AbstractButtonGesture stores a button ID |
| 285 | next to the start and end time of the gesture. |
| 286 | """ |
| 287 | type = 'Undefined' |
| 288 | |
| 289 | def __init__(self, button, start, end): |
| 290 | self.button = button |
| 291 | self.start = start |
| 292 | self.end = end |
| 293 | |
| 294 | def __str__(self): |
| 295 | return self.__class__.type + '(' + str(self.button) + ')' |
| 296 | |
| 297 | def __repr__(self): |
| 298 | return str(self) |
| 299 | |
| 300 | |
| 301 | class ButtonDownGesture(AbstractButtonGesture): |
| 302 | """ Functionally the same as AbstractButtonGesture """ |
| 303 | type = 'ButtonDown' |
| 304 | |
| 305 | |
| 306 | class ButtonUpGesture(AbstractButtonGesture): |
| 307 | """ Functionally the same as AbstractButtonGesture """ |
| 308 | type = 'ButtonUp' |