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)
Esempio n. 2
0
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']
    }