Source code for pyavs.utils.eye_tracking
"""
Eye tracking utilities for pyAVS package.
This module provides utilities for processing and analyzing eye tracking data,
including functions for matching saccades to fixations and extracting temporal
relationships between eye movement events.
"""
import pandas as pd
import numpy as np
from typing import Literal
from ..utils.logging import get_logger
logger = get_logger('utils.eye_tracking')
[docs]
def match_saccades_to_fixations(
saccades_meta_df: pd.DataFrame,
fixations_meta_df: pd.DataFrame,
saccade_type: Literal["pre-saccade", "post-saccade"] = "pre-saccade"
) -> pd.DataFrame:
"""
Match saccades to fixations based on temporal adjacency.
This function identifies saccade-fixation pairs by analyzing the temporal
sequence of events within each scene. It matches events that occur
consecutively with zero time gap between them.
Parameters
----------
saccades_meta_df : pd.DataFrame
Metadata for saccades. Must contain columns: 'sceneID', 'type',
'start_time', 'end_time', 'duration'
fixations_meta_df : pd.DataFrame
Metadata for fixations. Must contain columns: 'sceneID', 'type',
'start_time', 'end_time', 'duration', 'fix_sequence'
saccade_type : Literal["pre-saccade", "post-saccade"], default="pre-saccade"
Type of matching to perform:
- "pre-saccade": Match saccade -> fixation sequences
- "post-saccade": Match fixation -> saccade sequences
Returns
-------
pd.DataFrame
Matched saccades with associated fixation information. Includes all
original saccade columns plus:
- 'associated_fix_sequence': Sequence number of matched fixation
- 'associated_fix_start_time': Start time of matched fixation
- 'associated_fixation_duration': Duration of matched fixation
Notes
-----
Only pairs with exactly 0 time difference between consecutive events are
included (i.e., saccade.end_time == fixation.start_time for pre-saccade,
or fixation.end_time == saccade.start_time for post-saccade).
Examples
--------
>>> # Match saccades to subsequent fixations
>>> matched_df = match_saccades_to_fixations(
... saccades_df, fixations_df, saccade_type="pre-saccade"
... )
>>>
>>> # Access matched fixation durations
>>> fixation_durations = matched_df['associated_fixation_duration']
"""
logger.info(f"Matching saccades to fixations ({saccade_type})...")
# Combine and sort by time within scenes
combined_df = pd.concat([fixations_meta_df, saccades_meta_df], axis=0)
selected_saccades_rows = []
time_differences = []
num_events_with_0_time_difference = 0
unique_sceneIDs = saccades_meta_df['sceneID'].unique()
for sceneID in unique_sceneIDs:
scene_group = combined_df[combined_df['sceneID'] == sceneID]
sorted_group = scene_group.sort_values(by='start_time')
types = sorted_group['type'].values
for i in range(len(types) - 1):
if saccade_type == "pre-saccade":
# Match: saccade -> fixation
if types[i] == "saccade" and types[i + 1] == "fixation":
saccade_end_time = sorted_group.iloc[i]['end_time']
fixation_start_time = sorted_group.iloc[i + 1]['start_time']
time_difference = fixation_start_time - saccade_end_time
time_differences.append(time_difference)
if time_difference == 0:
num_events_with_0_time_difference += 1
saccade_row_data = sorted_group.iloc[i].to_dict()
saccade_row_data['original_index'] = sorted_group.index[i]
saccade_row_data['associated_fix_sequence'] = sorted_group.iloc[i + 1]['fix_sequence']
saccade_row_data['associated_fix_start_time'] = sorted_group.iloc[i + 1]['start_time']
saccade_row_data['associated_fixation_duration'] = sorted_group.iloc[i + 1]['duration']
selected_saccades_rows.append(saccade_row_data)
elif saccade_type == "post-saccade":
# Match: fixation -> saccade
if types[i] == "fixation" and types[i + 1] == "saccade":
fixation_end_time = sorted_group.iloc[i]['end_time']
saccade_start_time = sorted_group.iloc[i + 1]['start_time']
time_difference = saccade_start_time - fixation_end_time
time_differences.append(time_difference)
if time_difference == 0:
num_events_with_0_time_difference += 1
saccade_row_data = sorted_group.iloc[i + 1].to_dict()
saccade_row_data['original_index'] = sorted_group.index[i + 1]
saccade_row_data['associated_fix_sequence'] = sorted_group.iloc[i]['fix_sequence']
saccade_row_data['associated_fix_start_time'] = sorted_group.iloc[i]['start_time']
saccade_row_data['associated_fixation_duration'] = sorted_group.iloc[i]['duration']
selected_saccades_rows.append(saccade_row_data)
selected_saccades_df = pd.DataFrame(selected_saccades_rows)
if len(selected_saccades_df) > 0:
selected_saccades_df.set_index('original_index', inplace=True)
logger.info(f"Matched {len(selected_saccades_df)} saccade-fixation pairs")
logger.info(f"Events with 0 time difference: {num_events_with_0_time_difference}")
return selected_saccades_df