Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic eyetracker functionality #10855

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4a390cc
initial commit:
dominikwelke Jun 28, 2022
7fcde1f
work on parsed data integrity
dominikwelke Jun 28, 2022
1a87a5e
init testing framework
dominikwelke Jun 28, 2022
748c8a2
adapt higher level files to new class:
dominikwelke Jun 29, 2022
7d04af5
assert integrity of parsed data (not finished)
dominikwelke Jun 29, 2022
26218b2
init annotation handling for asc reader (WIP)
dominikwelke Jun 29, 2022
419b2d5
lines too long in asc parser module
dominikwelke Jun 29, 2022
a71b33c
add meas_date from asc header
dominikwelke Jun 29, 2022
e6a58bb
pep style
dominikwelke Jun 29, 2022
baf74b1
fix setting annotation times
dominikwelke Jun 29, 2022
769b691
handle sample intervals for different sample frequencies
dominikwelke Jun 29, 2022
ce6d017
linearly interpolate missing data, for plot functions etc to work.
dominikwelke Jun 29, 2022
75104e6
add option to annotate missing data
dominikwelke Jun 30, 2022
e9d1f91
move nan interpolation function to preprocessing.interpolation
dominikwelke Jun 30, 2022
b20c887
fiff constants for new channel types
dominikwelke Jun 30, 2022
40f98d5
point test_constants to my fork of fiff-constants
dominikwelke Jun 30, 2022
214ccb5
set correct channel_type, loc, and coil_types
dominikwelke Jun 30, 2022
54e3abb
fix git target
dominikwelke Jun 30, 2022
21d8064
update used fiff-constants
dominikwelke Jul 1, 2022
b1c68ca
make plots work for ch_type='eyetrack'
dominikwelke Jul 1, 2022
13a51ef
let test use mne-testing-data
dominikwelke Jul 1, 2022
e0a838d
optional reading of blinks/saccades from eyelink data file
dominikwelke Jul 1, 2022
915416c
add eyetrack also to the instance.pick_types() method
dominikwelke Jul 1, 2022
432b289
add eyetracker units to fif-constants
dominikwelke Jul 1, 2022
a7a1553
function to find eyeblinks via nan values in eyetrack channels
dominikwelke Jul 2, 2022
c1470b7
improve setting fiff channel info
dominikwelke Jan 5, 2023
e80bbea
minor
dominikwelke Jan 6, 2023
b7265e1
make set_channel_types() work with eyetrack data (fiff-coil and fiff-…
dominikwelke Jan 6, 2023
a63ead9
minor fix FIFF channel setting
dominikwelke Jan 6, 2023
430d20d
improve plot defaults
dominikwelke Jan 6, 2023
7162c90
preprocessing: find blinks from missing data
dominikwelke Jan 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mne/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
read_evoked_fieldtrip)
from .nihon import read_raw_nihon
from ._read_raw import read_raw
from .eyetrack import read_raw_eyelink

# for backward compatibility
from .fiff import Raw
Expand Down
8 changes: 8 additions & 0 deletions mne/io/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
FIFF.FIFFV_DIPOLE_WAVE = 1000 # Dipole time curve (xplotter/xfit)
FIFF.FIFFV_GOODNESS_FIT = 1001 # Goodness of fit (xplotter/xfit)
FIFF.FIFFV_FNIRS_CH = 1100 # Functional near-infrared spectroscopy
FIFF.FIFFV_EYETRACK_CH = 1200 # Eye-tracking
_ch_kind_named = {key: key for key in (
FIFF.FIFFV_BIO_CH,
FIFF.FIFFV_MEG_CH,
Expand All @@ -223,6 +224,7 @@
FIFF.FIFFV_DIPOLE_WAVE,
FIFF.FIFFV_GOODNESS_FIT,
FIFF.FIFFV_FNIRS_CH,
FIFF.FIFFV_EYETRACK_CH,
)}

#
Expand Down Expand Up @@ -917,6 +919,10 @@
FIFF.FIFFV_COIL_FNIRS_FD_PHASE = 305 # fNIRS frequency domain phase
FIFF.FIFFV_COIL_FNIRS_RAW = FIFF.FIFFV_COIL_FNIRS_CW_AMPLITUDE # old alias

FIFF.FIFFV_COIL_EYETRACK_POSX = 400 # Eye-tracking gaze X position
FIFF.FIFFV_COIL_EYETRACK_POSY = 401 # Eye-tracking gaze Y position
FIFF.FIFFV_COIL_EYETRACK_PUPIL = 402 # Eye-tracking pupil size

FIFF.FIFFV_COIL_MCG_42 = 1000 # For testing the MCG software

FIFF.FIFFV_COIL_POINT_MAGNETOMETER = 2000 # Simple point magnetometer
Expand Down Expand Up @@ -1003,6 +1009,8 @@
FIFF.FIFFV_COIL_FNIRS_HBR, FIFF.FIFFV_COIL_FNIRS_RAW,
FIFF.FIFFV_COIL_FNIRS_OD, FIFF.FIFFV_COIL_FNIRS_FD_AC_AMPLITUDE,
FIFF.FIFFV_COIL_FNIRS_FD_PHASE, FIFF.FIFFV_COIL_MCG_42,
FIFF.FIFFV_COIL_EYETRACK_POSX, FIFF.FIFFV_COIL_EYETRACK_POSY,
FIFF.FIFFV_COIL_EYETRACK_PUPIL,
FIFF.FIFFV_COIL_POINT_MAGNETOMETER, FIFF.FIFFV_COIL_AXIAL_GRAD_5CM,
FIFF.FIFFV_COIL_VV_PLANAR_W, FIFF.FIFFV_COIL_VV_PLANAR_T1,
FIFF.FIFFV_COIL_VV_PLANAR_T2, FIFF.FIFFV_COIL_VV_PLANAR_T3,
Expand Down
179 changes: 179 additions & 0 deletions mne/io/eyetrack/ParseEyeLinkAscFiles_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# ParseEyeLinkAsc.py
# - Reads in .asc data files from EyeLink and produces pandas dataframes for
# further analysis
#
# Created 7/31/18-8/15/18 by DJ.
# Updated 7/4/19 by DJ - detects and handles monocular sample data.


def ParseEyeLinkAsc_(elFilename):
# dfRec,dfMsg,dfFix,dfSacc,dfBlink,dfSamples = ParseEyeLinkAsc(elFilename)
# -Reads in data files from EyeLink .asc file and produces readable
# dataframes for further analysis.
#
# INPUTS:
# -elFilename is a string indicating an EyeLink data file from an AX-CPT
# task in the current path.
#
# OUTPUTS:
# -dfRec contains information about recording periods (often trials)
# -dfMsg contains information about messages (usually sent from stimulus
# software)
# -dfFix contains information about fixations
# -dfSacc contains information about saccades
# -dfBlink contains information about blinks
# -dfSamples contains information about individual samples
#
# Created 7/31/18-8/15/18 by DJ.
# Updated 11/12/18 by DJ - switched from "trials" to "recording periods"
# for experiments with continuous recording
# Updated 9/??/19 by Dominik Welke - fixed read-in of data

# Import packages
import numpy as np
import pandas as pd
import time

# ===== READ IN FILES ===== #
# Read in EyeLink file
print('Reading in EyeLink file %s...' % elFilename)
t = time.time()
with open(elFilename, "r+") as f:
fileTxt0 = (line.rstrip() for line in f)
# fileTxt0 = [line for line in fileTxt0 if line] # Non-blank lines in
# a list
fileTxt0 = [line for line in fileTxt0] # lines in a list
fileTxt0 = np.array(fileTxt0)

print('Done! Took %f seconds.' % (time.time() - t))

# Separate lines into samples and messages
print('Sorting lines...')
nLines = len(fileTxt0)
lineType = np.array(['OTHER'] * nLines, dtype='object')
# iStartRec = None
iStartRec = [] # DW: make a list of all rec-starts
t = time.time()
for iLine in range(nLines):
if len(fileTxt0[iLine]) < 3:
lineType[iLine] = 'EMPTY'
elif (
fileTxt0[iLine].startswith('*') or
fileTxt0[iLine].startswith('>>>>>')):
lineType[iLine] = 'COMMENT'
elif (
fileTxt0[iLine].split()[0][0].isdigit() or
fileTxt0[iLine].split()[0].startswith('-')):
lineType[iLine] = 'SAMPLE'
else:
lineType[iLine] = fileTxt0[iLine].split()[0]
# TODO: Find more general way of determining if recording has started
# if '!CAL' in fileTxt0[iLine]:
# iStartRec = iLine + 1
if 'START' in fileTxt0[iLine]:
# DW: more general way of determining if recording has started..
iStartRec.append(iLine + 1)
print('Done! Took %f seconds.' % (time.time() - t))

# ===== PARSE EYELINK FILE ===== #
t = time.time()
# Trials
print('Parsing recording markers...')
iNotStart = np.nonzero(lineType != 'START')[0]
dfRecStart = pd.read_csv(elFilename, skiprows=iNotStart, header=None,
delim_whitespace=True, usecols=[1])
dfRecStart.columns = ['tStart']
iNotEnd = np.nonzero(lineType != 'END')[0]
dfRecEnd = pd.read_csv(elFilename, skiprows=iNotEnd, header=None,
delim_whitespace=True, usecols=[1, 5, 6])
dfRecEnd.columns = ['tEnd', 'xRes', 'yRes']
# combine trial info
dfRec = pd.concat([dfRecStart, dfRecEnd], axis=1)
nRec = dfRec.shape[0]
print('%d recording periods found.' % nRec)

# Import Messages
print('Parsing stimulus messages...')
t = time.time()
iMsg = np.nonzero(lineType == 'MSG')[0]
# set up
tMsg = []
txtMsg = []
t = time.time()
for i in range(len(iMsg)):
# separate MSG prefix and timestamp from rest of message
info = fileTxt0[iMsg[i]].split()
# extract info
tMsg.append(int(info[1]))
txtMsg.append(' '.join(info[2:]))
# Convert dict to dataframe
dfMsg = pd.DataFrame({'time': tMsg, 'text': txtMsg})
print('Done! Took %f seconds.' % (time.time() - t))

# Import Fixations
print('Parsing fixations...')
t = time.time()
iNotEfix = np.nonzero(lineType != 'EFIX')[0]
dfFix = pd.read_csv(elFilename, skiprows=iNotEfix, header=None,
delim_whitespace=True, usecols=range(1, 8))
dfFix.columns = ['eye', 'tStart', 'tEnd', 'duration',
'xAvg', 'yAvg', 'pupilAvg']
nFix = dfFix.shape[0]
print('Done! Took %f seconds.' % (time.time() - t))

# Saccades
print('Parsing saccades...')
t = time.time()
iNotEsacc = np.nonzero(lineType != 'ESACC')[0]
dfSacc = pd.read_csv(elFilename, skiprows=iNotEsacc, header=None,
delim_whitespace=True, usecols=range(1, 11))
dfSacc.columns = ['eye', 'tStart', 'tEnd', 'duration', 'xStart',
'yStart', 'xEnd', 'yEnd', 'ampDeg', 'vPeak']
print('Done! Took %f seconds.' % (time.time() - t))

# Blinks
print('Parsing blinks...')
iNotEblink = np.nonzero(lineType != 'EBLINK')[0]
dfBlink = pd.read_csv(elFilename, skiprows=iNotEblink, header=None,
delim_whitespace=True, usecols=range(1, 5))
dfBlink.columns = ['eye', 'tStart', 'tEnd', 'duration']
print('Done! Took %f seconds.' % (time.time() - t))

# determine sample columns based on eyes recorded in file
eyesInFile = np.unique(dfFix.eye)
if eyesInFile.size == 2:
print('binocular data detected.')
cols = ['tSample', 'LX', 'LY', 'LPupil', 'RX', 'RY', 'RPupil']
else:
eye = eyesInFile[0]
print('monocular data detected (%c eye).' % eye)
cols = ['tSample', '%cX' % eye, '%cY' % eye, '%cPupil' % eye]
# Import samples
print('Parsing samples...')
t = time.time()
# iNotSample = np.nonzero(np.logical_or(
# lineType != 'SAMPLE', np.arange(nLines) < iStartRec))[0]
iNotSample = np.nonzero( # DW: try this, to get ALL data
np.logical_or(
lineType != 'SAMPLE', np.arange(nLines) < iStartRec[0]))[0]
dfSamples = pd.read_csv(elFilename, skiprows=iNotSample, header=None,
delim_whitespace=True, usecols=range(0, len(cols)))
dfSamples.columns = cols
# Convert values to numbers
for eye in ['L', 'R']:
if eye in eyesInFile:
dfSamples['%cX' % eye] = pd.to_numeric(dfSamples['%cX' % eye],
errors='coerce')
dfSamples['%cY' % eye] = pd.to_numeric(dfSamples['%cY' % eye],
errors='coerce')
dfSamples['%cPupil' % eye] = pd.to_numeric(
dfSamples['%cPupil' % eye], errors='coerce')
else:
dfSamples['%cX' % eye] = np.nan
dfSamples['%cY' % eye] = np.nan
dfSamples['%cPupil' % eye] = np.nan

print('Done! Took %.1f seconds.' % (time.time() - t))

# Return new compilation dataframe
return dfRec, dfMsg, dfFix, dfSacc, dfBlink, dfSamples
8 changes: 8 additions & 0 deletions mne/io/eyetrack/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Module for loading Eye-Tracker data"""

# Author: Dominik Welke <[email protected]>
#
# License: BSD-3-Clause

from .eyetrack import read_raw_eyelink
from .ParseEyeLinkAscFiles_ import ParseEyeLinkAsc_
Loading