def test_fill_gaps(self): a = [1, None, 2] b = [None, 1, 2] c = [None, None, None] fa = unit.fill_gaps(a) self.assertEqual(fa, [1, 1, 2]) fb = unit.fill_gaps(b) self.assertEqual(fb, [1, 1, 2]) f = lambda: unit.fill_gaps(c) self.assertRaises(unit.ExtrapolationError, f)
def render_overview(common, output_html_filepath, title='Overview', emphasize_gaps=False): ''' Create HTML-based visualization from all streams and events. Does not understand stream or event semantics like gaze paths for example. Can be slow if streams contain large number of gaps. Parameters: common: A CommonV1 object output_html_filepath: a filepath as string title: HTML page title as string emphasize_gaps: if False, do not show red gaps. Makes it quicker. ''' # Collect figures together figs = [] def to_ms(micros): return int(round(micros / 1000)) def pick_color(d): ''' We visually discriminate original and derived data. Parameters: d: event or stream object. Return color string ''' if 'derived' in d: return 'blue' return 'black' ######### # Environment constants ######### # Build dict env_names = common.list_environment_names() envs = {name: common.get_environment(name) for name in env_names} # Make human readable # yaml.safe_dump instad of yaml.dump to avoid annoying unicode tags # See http://stackoverflow.com/a/1950399/638546 env_yaml = yaml.safe_dump(envs, default_flow_style=False) env_html = '<pre>' + env_yaml + '</pre>' ######### # Streams ######### stream_names = sorted(common.list_stream_names()) for stream_name in stream_names: stream = common.get_stream(stream_name) x = list(map(to_ms, common.get_timeline(stream['timeline']))) y = stream['values'] color = pick_color(stream) fig = plotting.figure(title=stream_name, x_axis_label='time (ms)', plot_width=1000, plot_height=300, toolbar_location=None) if emphasize_gaps: # Emphasize gaps with red. Slow if data very gapped. # Get valid substreams sl = utils.get_valid_sublists_2d(x, y) for xs, ys in sl: fig.line(xs, ys, line_color=color) # Emphasize gaps with red line. # Loop pairwise, fill gaps. # TODO Optimize. Currently very slow. for i in range(len(sl) - 1): xs0, ys0 = sl[i] xs1, ys1 = sl[i + 1] # Extrapolate from last known value x0 = xs0[-1] x1 = xs1[0] y = ys0[-1] fig.line(x=[x0, x1], y=[y, y], line_width=1, line_color='red') else: # Fill gaps and draw single line. # This should be much faster. yy = gpre.fill_gaps(y) fig.line(x=x, y=yy, line_width=1, line_color=color) figs.append(fig) ######## # Events ######## # Create a row for each event. X is time. evs = common.list_events() # Visualize in start time, then duration order. # Recent topmost, hence minus. If start time same, longest topmost. def order_key(ev): t0 = -ev['range'][0] dur = ev['range'][1] + t0 return (t0, dur) evs = sorted(evs, key=order_key) # Plot height: it is dependent on number of events. # With one event, we want it still be visible, thus the constant. plot_height = (100 + 50 * len(evs)) fig = plotting.figure(title='Events', y_range=(-1, len(evs)), plot_width=1000, plot_height=plot_height, x_axis_label='time (ms)', toolbar_location=None) # Hide event indices and draw custom text instead. fig.yaxis.visible = None def get_text_position(x0, x1, minx, maxx): '''Change text position based on event's horizontal position so that the rightmost text do not go over the right edge.''' # Event mean, normalized to range c = (x0 + x1) / 2 - minx # Max range r = maxx - minx if c < 2 * r / 3: return (x0, 'left') # Center align was not good because text formed # visually annoying justification. # if c < 2 * r / 3: # return ((x0 + x1) / 2, 'center') return (x1, 'right') # Limit for horizontal coordinates. Helps to position text. evs_minx = to_ms(evs[-1]['range'][0]) evs_maxx = to_ms(evs[0]['range'][1]) for i, ev in enumerate(evs): t0 = to_ms(ev['range'][0]) t1 = to_ms(ev['range'][1]) color = pick_color(ev) x, align = get_text_position(t0, t1, evs_minx, evs_maxx) fig.text(text=[', '.join(ev['tags'])], y=[i + 0.1], x=[x], text_align=align, text_color=color) fig.line(x=[t0, t1], y=[i, i], line_width=6, line_color=color) # Mark where events start and end. fig.line(x=[t0, t0], y=[i - 0.1, i + 0.1], line_width=2, line_color=color) fig.line(x=[t1, t1], y=[i - 0.1, i + 0.1], line_width=2, line_color=color) figs.append(fig) # Make human readable evs_rev = list(reversed(evs)) evs_yaml = yaml.safe_dump(evs_rev, default_flow_style=False) evs_html = '<pre>' + evs_yaml + '</pre>' ########### # Render HTML ########## this_dir = os.path.dirname(os.path.abspath(__file__)) template_dir = os.path.join(this_dir, 'templates') jinja2loader = FileSystemLoader(template_dir) jinja2env = Environment(loader=jinja2loader) try: overview_template = jinja2env.get_template('overview.html') except TemplateNotFound as ex: print('Tried template dir: ' + template_dir) raise ex html = file_html(figs, CDN, title=title, template=overview_template, template_variables={'environment': env_html, 'events': evs_html}) # Save as html file. with open(output_html_filepath, 'w') as f: f.write(html)
def fit(g): ''' Parameter: g: Gaze data as CommonV1 object Require streams: gazelib/gaze/left_eye_x_relative gazelib/gaze/left_eye_y_relative gazelib/gaze/right_eye_x_relative gazelib/gaze/right_eye_y_relative Raise: InsufficientDataException: if streams are missing or they are empty. Return:: { 'type': 'gazelib/gaze/saccade', 'start_time_relative': <int microseconds> 'end_time_relative': <int microseconds> 'mean_squared_error': <float> } ''' g.assert_has_streams([ 'gazelib/gaze/left_eye_x_relative', 'gazelib/gaze/left_eye_y_relative', 'gazelib/gaze/right_eye_x_relative', 'gazelib/gaze/right_eye_y_relative' ]) # Timeline names l_tl_name = g.get_stream_timeline_name('gazelib/gaze/left_eye_x_relative') r_tl_name = g.get_stream_timeline_name('gazelib/gaze/right_eye_x_relative') lx = g.raw['streams']['gazelib/gaze/left_eye_x_relative']['values'] ly = g.raw['streams']['gazelib/gaze/left_eye_y_relative']['values'] rx = g.raw['streams']['gazelib/gaze/right_eye_x_relative']['values'] ry = g.raw['streams']['gazelib/gaze/right_eye_y_relative']['values'] # Forward fill try: lx_fill = fill_gaps(lx) ly_fill = fill_gaps(ly) rx_fill = fill_gaps(rx) ry_fill = fill_gaps(ry) except ExtrapolationError: # Only nones or empty msg = 'Cannot find saccade from empty data.' raise CommonV1.InsufficientDataException(msg) # Median filter # Required to remove non-Gaussian noise i.e. random outliers # Saccademodel handles Gaussian noise. lx_filt = scipy.signal.medfilt(lx_fill, 5) ly_filt = scipy.signal.medfilt(ly_fill, 5) rx_filt = scipy.signal.medfilt(rx_fill, 5) ry_filt = scipy.signal.medfilt(ry_fill, 5) # Pointlists for saccademodel lpl = [[x, y] for x, y in zip(lx_filt, ly_filt)] rpl = [[x, y] for x, y in zip(rx_filt, ry_filt)] # Results try: lresults = saccademodel.fit(lpl) rresults = saccademodel.fit(rpl) except saccademodel.interpolate.InterpolationError: msg = 'Cannot find saccade from empty data.' raise CommonV1.InsufficientDataException(msg) # Pick one with smallest error lerr = lresults['mean_squared_error'] rerr = rresults['mean_squared_error'] if lerr < rerr: results = lresults tl_name = l_tl_name else: results = rresults tl_name = r_tl_name # Convert measured saccade end and start to times. lensource = len(results['source_points']) lensaccade = len(results['saccade_points']) start_index = max(0, lensource - 1) # do not let below zero end_index = max(0, lensource + lensaccade - 1) # do not let above length start = g.get_relative_time_by_index(tl_name, start_index) end = g.get_relative_time_by_index(tl_name, end_index) return { 'type': 'gazelib/gaze/saccade', 'start_time_relative': start, # microseconds 'end_time_relative': end, 'mean_squared_error': results['mean_squared_error'] }