Detecting Occumotoric Events#

Eye-tracking data are typically segmented into discrete events such as fixations and saccades. Fixations represent periods during which the eyes remain relatively stable, enabling visual information processing. Saccades are the rapid eye movements that shift gaze between fixations. Detecting these events and computing their properties, such as fixation duration and dispersion, or saccade amplitude and peak velocity, forms the foundation for analyzing visual behavior and understanding how participants explore a stimulus.

To showcase the event detection methods, we first create a Gaze object and do the necessary pre-processing as described in Working with Raw Gaze Samples.

import pymovements as pm
from pymovements.gaze.experiment import Experiment

# Define the experimental setup.
# The screen geometry and viewing distance are required
# to convert pixel coordinates (px) into degrees of visual angle (dva).
# The sampling rate is required for velocity computation and
# for time conversion if time_unit='step' is used.
experiment = Experiment(
    screen_width_px=1280,
    screen_height_px=1024,
    screen_width_cm=38,
    screen_height_cm=30.2,
    distance_cm=68,
    origin='upper left',
    sampling_rate=250.0,
)

# Load gaze data from a CSV file and initialize a Gaze object.
# - time_column specifies the timestamp column (standardized internally to 'time')
# - pixel_columns defines the flat CSV columns that should be grouped
#   into a structured 'pixel' component (monocular: x, y)
# - the Experiment attaches screen geometry and sampling metadata
gaze = pm.gaze.from_csv(
    '../examples/gaze-toy-example.csv',
    experiment=experiment,
    time_column='time',
    pixel_columns=['x', 'y']
)

# Convert pixel coordinates to degrees of visual angle (dva).
# Requires a valid Experiment with screen geometry and distance.
gaze.pix2deg()

# Compute velocity from the position signal.
# Velocity is derived using the sampling rate from the Experiment.
gaze.pos2vel()

Fixations#

Fixations can be detected using one of the following algorithms:

  • The I-VT (Velocity-Threshold Identification) method classifies each sample based on its velocity. Samples with velocities below a specified threshold are labeled as fixation points. Consecutive fixation samples are then merged into fixation events. A commonly used default threshold is 20 degrees per second, though this value may vary depending on the recording setup and research question. pymovements implements this methods with the ivt() function.

  • The I-DT (Dispersion-Threshold Identification) method groups consecutive samples whose spatial dispersion remains below a predefined threshold and whose duration exceeds a minimum value. The algorithm slides a moving window over the data: if the dispersion within the window is sufficiently small, the window is classified as a fixation and is expanded until the dispersion criterion is violated. pymovements function: idt().

# Detect fixations using the I-VT (velocity threshold) algorithm.
# Detected events are stored under the name 'fixation_ivt'.
gaze.detect('ivt', name='fixation_ivt')

# Inspect the first few detected events.
# Events are stored in a structured event table.
gaze.events.frame.head(5)
shape: (5, 4)
nameonsetoffsetduration
stri64i64i64
"fixation_ivt"8172164
"fixation_ivt"208396188
"fixation_ivt"440588148
"fixation_ivt"632864232
"fixation_ivt"9041024120

Saccades#

Saccades are rapid eye movements that shift the point of fixation from one location to another. In pymovements, saccades (including microsaccades) can be detected from the velocity signal using the microsaccades() function. This method implements a noise-adaptive velocity threshold. Instead of using a fixed velocity cutoff, the threshold is scaled relative to the noise level of the velocity signal. This makes the detection procedure more robust across recordings with different noise characteristics.

Two key parameters are necessary for the identification of saccades:

  • threshold_factor controls how strict the velocity threshold is (default: 6). Higher values detect fewer saccades (more conservative), lower values detect more (more sensitive).

  • minimum_duration defines the minimum length of a velocity peak to be considered a saccade (default: 6 samples). Shorter events are treated as noise and ignored.

gaze.detect('microsaccades', minimum_duration=6, threshold_factor=6)

# sort by onset in order to see both fixations and saccades
gaze.events.frame = gaze.events.frame.sort("onset")

gaze.events.frame.head(10)
shape: (10, 4)
nameonsetoffsetduration
stri64i64i64
"fixation_ivt"8172164
"saccade"17620832
"fixation_ivt"208396188
"saccade"40042020
"saccade"4284368
"fixation_ivt"440588148
"saccade"4444528
"saccade"58864860
"fixation_ivt"632864232
"saccade"86490440

For more information on the algorithms and additional parameters, see the following tutorials: Handling Gaze Events and Plotting Gaze Data.

Completing Event Segmentation with fill()#

After detecting fixations or saccades, some timesteps may remain unclassified. The fill method labels all previously unassigned timesteps as a new event type. Unlike ivt, idt, or microsaccades, fill does not analyze the gaze signal.

It simply:

  1. Marks timesteps already covered by existing events.

  2. Identifies remaining segments.

  3. Groups consecutive samples.

  4. Discards segments shorter than minimum_duration.

  5. Stores the remaining segments as new events.

This is useful to ensure complete temporal segmentation (e.g., labeling all non-saccade periods as fixations or all non-fixations as saccades).minimum_duration is interpreted in the same units as the provided timesteps.

# remove previously detected saccades
gaze.events.frame = gaze.events.fixations

# classify all remaining segments as saccades
try:
    gaze.detect(
        'fill',
        timesteps=gaze.samples['time'],
        minimum_duration=6,
        name='saccade_fill',
    )
except Exception as e:
    print('gaze.detect() failed')
    print(e)

gaze.events.frame = gaze.events.frame.sort("onset")

gaze.events.frame.head(10)
gaze.detect() failed
index 0 is out of bounds for axis 0 with size 0
shape: (10, 4)
nameonsetoffsetduration
stri64i64i64
"fixation_ivt"8172164
"fixation_ivt"208396188
"fixation_ivt"440588148
"fixation_ivt"632864232
"fixation_ivt"9041024120
"fixation_ivt"10601276216
"fixation_ivt"13121436124
"fixation_ivt"14841744260
"fixation_ivt"17841900116
"fixation_ivt"19362152216

The Events Object#

Event detection in pymovements returns an Events object. This object provides a structured representation of detected events and their properties. Each row in an Events object represents a single event and contains at least the following minimal schema:

  • name – the event type (e.g., "fixation_ivt", "saccade")

  • onset – event start time (in the same time unit as the gaze timestamps)

  • offset – event end time

  • duration – automatically computed as offset - onset

Additional event-specific properties (e.g., dispersion, amplitude, peak velocity) are stored as extra columns in the underlying polars.DataFrame.

gaze
Gaze
  • DataFrame (4 columns, 4306 rows)
    shape: (4_306, 4)
    timepixelpositionvelocity
    i64list[f64]list[f64]list[f64]
    0[206.8, 152.4][-10.697598, -8.852399][null, null]
    4[207.0, 151.5][-10.692768, -8.874233][null, null]
    8[207.6, 151.9][-10.678275, -8.86453][1.610284, -0.101097]
    12[207.6, 152.2][-10.678275, -8.857252][1.107104, -0.909672]
    16[207.8, 151.6][-10.673444, -8.871807][0.6039, -1.617239]
    17204[349.3, 420.0][-7.220662, -2.272553][29.879682, -16.852411]
    17208[362.7, 418.1][-6.89053, -2.319691][43.841686, -7.339814]
    17212[371.2, 419.0][-6.680877, -2.297363][9.957777, -7.442396]
    17216[365.9, 417.1][-6.811623, -2.3445][null, null]
    17220[355.8, 413.8][-7.060582, -2.426362][null, null]
  • Events
    Events
    • DataFrame (4 columns, 72 rows)
      shape: (72, 4)
      nameonsetoffsetduration
      stri64i64i64
      "fixation_ivt"8172164
      "fixation_ivt"208396188
      "fixation_ivt"440588148
      "fixation_ivt"632864232
      "fixation_ivt"9041024120
      "fixation_ivt"1597216200228
      "fixation_ivt"1624416536292
      "fixation_ivt"1656416728164
      "fixation_ivt"1677616960184
      "fixation_ivt"1699617136140
    • None
  • dict (0 items)
  • None
  • None
  • Experiment
    Experiment
    • EyeTracker
      EyeTracker
      • None
      • None
      • None
      • None
      • 250.0
      • None
      • None
    • Screen
      Screen
      • 68
      • 30.2
      • 1024
      • 'upper left'
      • tuple (2 items)
        • 1280
        • 1024
      • tuple (2 items)
        • 38
        • 30.2
      • 38
      • 1280
      • 15.599386487782953
      • -15.599386487782953
      • 12.508044410882546
      • -12.508044410882546