Module eyekit.fixation

Defines the Fixation and FixationSequence objects, which are used to represent fixation data.

Classes

class Fixation (index: int, x: int, y: int, start: int, end: int, pupil_size: int = None, *, discarded: bool = False, tags: dict = None)

Representation of a single fixation event. It is not usually necessary to create Fixation objects manually; they are created automatically during the instantiation of a FixationSequence.

Instance variables

var index : int

Index of the fixation in its parent FixationSequence

var x : int

X-coordinate of the fixation.

var y : int

Y-coordinate of the fixation.

var xy : tuple

XY-coordinates of the fixation.

var start : int

Start time of the fixation in milliseconds.

var end : int

End time of the fixation in milliseconds.

var duration : int

Duration of the fixation in milliseconds.

var pupil_size : int

Size of the pupil. None if no pupil size is recorded.

var discarded : bool

True if the fixation has been discarded, False otherwise.

var tags : dict

Tags applied to this fixation.

Methods

def discard(self)

Mark this fixation as discarded. To completely remove the fixation, you should also call FixationSequence.purge().

def add_tag(self, tag, value=True)

Tag this fixation with some arbitrary tag and (optionally) a value.

def has_tag(self, tag)

Returns True if the fixation has a given tag.

def shift_x(self, amount)

Shift the fixation's x-coordinate to the right (+) or left (-) by some amount (in pixels).

def shift_y(self, amount)

Shift the fixation's y-coordinate down (+) or up (-) by some amount (in pixels).

def shift_time(self, amount)

Shift this fixation forwards (+) or backwards (-) in time by some amount (in milliseconds).

def serialize(self)

Returns representation of the fixation as a tuple for serialization.

class FixationSequence (sequence: list)

Representation of a sequence of consecutive fixations, typically from a single trial.

Initialized with:

  • sequence List of tuples of ints, or something similar, that conforms to the following structure: [(106, 540, 100, 200), (190, 536, 200, 300), ..., (763, 529, 1000, 1100)], where each tuple contains the X-coordinate, Y-coordinate, start time, and end time of a fixation. Alternatively, sequence may be a list of dicts, where each dict is something like {'x': 106, 'y': 540, 'start': 100, 'end': 200}.

Instance variables

var start : int

Start time of the fixation sequence (in milliseconds).

var end : int

End time of the fixation sequence (in milliseconds).

var duration : int

Duration of the fixation sequence, incuding any gaps between fixations (in milliseconds).

Methods

def copy(self, include_discards=True)

Returns a copy of the fixation sequence.

def purge(self)

Permanently removes all discarded fixations from the sequence, and reindexes the fixations.

def iter_with_discards(self)

Iterates over the fixation sequence including any discarded fixations. This is also the default behavior when iterating over a FixationSequence directly.

def iter_without_discards(self)

Iterates over the fixation sequence without any discarded fixations.

def iter_pairs(self, include_discards=True)

Iterate over fixations in consecutive pairs. This is useful if you want to compare consecutive fixations in some way. For example, if you wanted to detect when a fixation leaves an interest area, you might do something like this:

for curr_fxn, next_fxn in seq.iter_pairs():
    if curr_fxn in interest_area and next_fxn not in interest_area:
        print('A fixation has left the interest area')
def shift_x(self, amount)

Shift all fixations' x-coordinates to the right (+) or left (-) by some amount (in pixels).

def shift_y(self, amount)

Shift all fixations' y-coordinates down (+) or up (-) by some amount (in pixels).

def shift_time(self, amount)

Shift all fixations forwards (+) or backwards (-) in time by some amount (in milliseconds).

def shift_start_time_to_zero(self)

Shift all fixations backwards in time, such that the first fixation in the sequence starts at time 0. Returns the amount (in milliseconds) by which the entire sequence was shifted.

def segment(self, time_intervals)

Given a list of time intervals, segment the fixation sequence into subsequences. This may be useful if you want to split the sequence based on, for example, page turns or subtitle timings. Returns a list of n FixationSequences, where n = len(time_intervals). The time intervals should be specified as a list of tuples or something similarly interpretable: [(500, 1000), (1500, 2000), (2500, 5000)].

def discard_short_fixations(self, threshold=50)

Discard all fixations that are shorter than some threshold value (defualt: 50ms). Note that this only flags fixations as discarded and doesn't actually remove them; to remove discarded fixations, call FixationSequence.purge() after discarding.

def discard_long_fixations(self, threshold=500)

Discard all fixations that are longer than some threshold value (defualt: 500ms). Note that this only flags fixations as discarded and doesn't actually remove them; to remove discarded fixations, call FixationSequence.purge() after discarding.

def discard_out_of_bounds_fixations(self, text_block, threshold=100)

Given a TextBlock, discard all fixations that do not fall within some threshold distance of any character in the text. Note that this only flags fixations as discarded and doesn't actually remove them; to remove discarded fixations, call FixationSequence.purge() after discarding.

def snap_to_lines(self, text_block, method='warp', **kwargs)

Given a TextBlock, snap each fixation to the line that it most likely belongs to, eliminating any y-axis variation. Several methods are available (see below), some of which take optional parameters or require NumPy/SciPy to be installed. See Carr et al. (2021) for a full description and evaluation of these methods. Note that in single-line TextBlocks, the method parameter has no effect: all fixations will be snapped to the one line. If a list of methods is passed in, each fixation will be snapped to the line with the most "votes" across the selection of methods (in case of ties, the left-most method takes priority). This "wisdom of the crowd" approach usually results in greater performance than any one algorithm individually; ideally, you should choose a selection of methods that have different general properties: for example, ['slice', 'cluster', 'warp']. The snap_to_lines() method returns two values, delta and kappa, which are indicators of data quality. Delta is the average amount by which a fixation is moved in the correction. If this value is high (e.g. > 30), this may indicate that the quality of the data or the correction is low. Kappa, which is only calculated when multiple methods are applied, is a measure of agreement among the methods. If this value is low (e.g. < 0.7), this may indicate that the correction is unreliable.

  • chain : Chain consecutive fixations that are sufficiently close to each other, and then assign chains to their closest text lines. Default params: x_thresh=192, y_thresh=32. Requires NumPy. Original method implemented in popEye.

  • cluster : Classify fixations into m clusters based on their Y-values, and then assign clusters to text lines in positional order. Requires SciPy. Original method implemented in popEye.

  • merge : Form a set of progressive sequences and then reduce the set to m by repeatedly merging those that appear to be on the same line. Merged sequences are then assigned to text lines in positional order. Default params: y_thresh=32, gradient_thresh=0.1, error_thresh=20. Requires NumPy. Original method by Špakov et al. (2019).

  • regress : Find m regression lines that best fit the fixations and group fixations according to best fit regression lines, and then assign groups to text lines in positional order. Default params: slope_bounds=(-0.1, 0.1), offset_bounds=(-50, 50), std_bounds=(1, 20). Requires SciPy. Original method by Cohen (2013).

  • segment : Segment fixation sequence into m subsequences based on m–1 most-likely return sweeps, and then assign subsequences to text lines in chronological order. Requires NumPy. Original method by Abdulin & Komogortsev (2015).

  • slice : Form a set of runs and then reduce the set to m by repeatedly merging those that appear to be on the same line. Merged sequences are then assigned to text lines in positional order. Default params: x_thresh=192, y_thresh=32, w_thresh=32, n_thresh=90. Requires NumPy. Original method by Glandorf & Schroeder (2021).

  • split : Split fixation sequence into subsequences based on best candidate return sweeps, and then assign subsequences to closest text lines. Requires SciPy. Original method by Carr et al. (2022).

  • stretch : Find a stretch factor and offset that results in a good alignment between the fixations and lines of text, and then assign the transformed fixations to the closest text lines. Default params: stretch_bounds=(0.9, 1.1), offset_bounds=(-50, 50). Requires SciPy. Original method by Lohmeier (2015).

  • warp : Map fixations to word centers using Dynamic Time Warping. This finds a monotonically increasing mapping between fixations and words with the shortest overall distance, effectively resulting in m subsequences. Fixations are then assigned to the lines that their mapped words belong to, effectively assigning subsequences to text lines in chronological order. Requires NumPy. Original method by Carr et al. (2022).

def serialize(self)

Returns representation of the fixation sequence in simple list format for serialization.