Skip to content

Commit

Permalink
ENH: Read eyetracking data (Eyelink) (Fork of #10855 ) (#11152)
Browse files Browse the repository at this point in the history
Co-authored-by: dominikwelke <[email protected]>
  • Loading branch information
scott-huberty and dominikwelke authored Mar 27, 2023
1 parent 2679679 commit 62af4ac
Show file tree
Hide file tree
Showing 34 changed files with 1,665 additions and 41 deletions.
2 changes: 2 additions & 0 deletions doc/_includes/data_formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ EEG :ref:`Persyst <import-persyst>` .lay :func:`mn
NIRS :ref:`NIRx <import-nirx>` directory :func:`mne.io.read_raw_nirx`

NIRS :ref:`BOXY <import-boxy>` directory :func:`mne.io.read_raw_boxy`

EYETRACK SR eyelink ASCII files .asc :func:`mne.io.read_raw_eyelink`
============ ============================================ ========= ===================================

More details are provided in the tutorials in the :ref:`tut-data-formats`
Expand Down
1 change: 1 addition & 0 deletions doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Enhancements
- Add automatic projection of sEEG contact onto the inflated surface for :meth:`mne.viz.Brain.add_sensors` (:gh:`11436` by `Alex Rockhill`_)
- Allow an image with intracranial electrode contacts (e.g. computed tomography) to be used without the freesurfer recon-all surfaces to locate contacts so that it doesn't have to be downsampled to freesurfer dimensions (for microelectrodes) and show an example :ref:`ex-ieeg-micro` with :func:`mne.transforms.apply_volume_registration_points` added to aid this transform (:gh:`11567` by `Alex Rockhill`_)
- Use new :meth:`dipy.workflows.align.DiffeomorphicMap.transform_points` to transform a montage of intracranial contacts more efficiently (:gh:`11572` by `Alex Rockhill`_)
- Add support for eyetracking data using :func:`mne.io.read_raw_eyelink` (:gh:`11152` by `Dominik Welke`_ and `Scott Huberty`_)

Bugs
~~~~
Expand Down
3 changes: 2 additions & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@
# Undocumented (on purpose)
'RawKIT', 'RawEximia', 'RawEGI', 'RawEEGLAB', 'RawEDF', 'RawCTF', 'RawBTi',
'RawBrainVision', 'RawCurry', 'RawNIRX', 'RawGDF', 'RawSNIRF', 'RawBOXY',
'RawPersyst', 'RawNihon', 'RawNedf', 'RawHitachi', 'RawFIL',
'RawPersyst', 'RawNihon', 'RawNedf', 'RawHitachi', 'RawFIL', 'RawEyelink',
# sklearn subclasses
'mapping', 'to', 'any',
# unlinkable
Expand Down Expand Up @@ -1231,6 +1231,7 @@ def reset_warnings(gallery_conf, fname):
f'{tu}/{si}/plot_creating_data_structures.html': f'{tu}/{si}/10_array_objs.html', # noqa E501
f'{tu}/{si}/plot_point_spread.html': f'{tu}/{si}/70_point_spread.html',
f'{tu}/{si}/plot_dics.html': f'{tu}/{si}/80_dics.html',
f'{tu}/{tf}/plot_eyetracking.html': f'{tu}/preprocessing/90_eyetracking_data.html', # noqa E501
f'{ex}/{co}/mne_inverse_label_connectivity.html': f'{mne_conn}/{ex}/mne_inverse_label_connectivity.html', # noqa E501
f'{ex}/{co}/cwt_sensor_connectivity.html': f'{mne_conn}/{ex}/cwt_sensor_connectivity.html', # noqa E501
f'{ex}/{co}/mixed_source_space_connectivity.html': f'{mne_conn}/{ex}/mixed_source_space_connectivity.html', # noqa E501
Expand Down
3 changes: 2 additions & 1 deletion doc/datasets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ Datasets
refmeg_noise.data_path
ssvep.data_path
erp_core.data_path
epilepsy_ecog.data_path
epilepsy_ecog.data_path
eyelink.data_path
13 changes: 13 additions & 0 deletions doc/overview/datasets_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,19 @@ standard.

* :ref:`tut-ssvep`

EYELINK
=======
:func:`mne.datasets.eyelink.data_path`

A small example dataset in SR research's proprietary .asc format.
1 participant fixated on the screen while short light flashes appeared.
Monocular recording of gaze position and pupil size, 1000 Hz sampling
frequency.

.. topic:: Examples

* :ref:`tut-eyetrack`

References
==========

Expand Down
13 changes: 13 additions & 0 deletions doc/preprocessing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,19 @@ Projections:
make_montage_volume
warp_montage

:py:mod:`mne.preprocessing.eyetracking`:

.. currentmodule:: mne.preprocessing.eyetracking

.. automodule:: mne.preprocessing.eyetracking
:no-members:
:no-inherited-members:

.. autosummary::
:toctree: generated/

set_channel_types_eyetrack

EEG referencing:

.. currentmodule:: mne
Expand Down
1 change: 1 addition & 0 deletions doc/reading_raw_data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Reading raw data
read_raw_ctf
read_raw_curry
read_raw_edf
read_raw_eyelink
read_raw_bdf
read_raw_gdf
read_raw_kit
Expand Down
18 changes: 12 additions & 6 deletions mne/channels/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ def equalize_channels(instances, copy=True, verbose=None):
FIFF.FIFF_UNIT_MOL: 'M',
FIFF.FIFF_UNIT_NONE: 'NA',
FIFF.FIFF_UNIT_CEL: 'C',
FIFF.FIFF_UNIT_S: 'S'}
FIFF.FIFF_UNIT_S: 'S',
FIFF.FIFF_UNIT_PX: 'px'}


def _check_set(ch, projs, ch_type):
Expand Down Expand Up @@ -331,7 +332,8 @@ def set_channel_types(self, mapping, verbose=None):
ecg, eeg, emg, eog, exci, ias, misc, resp, seeg, dbs, stim, syst,
ecog, hbo, hbr, fnirs_cw_amplitude, fnirs_fd_ac_amplitude,
fnirs_fd_phase, fnirs_od, temperature, gsr
fnirs_fd_phase, fnirs_od, eyetrack_pos, eyetrack_pupil,
temperature, gsr
.. versionadded:: 0.9.0
"""
Expand Down Expand Up @@ -379,6 +381,10 @@ def set_channel_types(self, mapping, verbose=None):
coil_type = FIFF.FIFFV_COIL_FNIRS_FD_PHASE
elif ch_type == 'fnirs_od':
coil_type = FIFF.FIFFV_COIL_FNIRS_OD
elif ch_type == 'eyetrack_pos':
coil_type = FIFF.FIFFV_COIL_EYETRACK_POS
elif ch_type == 'eyetrack_pupil':
coil_type = FIFF.FIFFV_COIL_EYETRACK_PUPIL
else:
coil_type = FIFF.FIFFV_COIL_NONE
self.info['chs'][c_ind]['coil_type'] = coil_type
Expand Down Expand Up @@ -595,7 +601,7 @@ def pick_types(self, meg=False, eeg=False, stim=False, eog=False,
resp=False, chpi=False, exci=False, ias=False, syst=False,
seeg=False, dipole=False, gof=False, bio=False,
ecog=False, fnirs=False, csd=False, dbs=False,
temperature=False, gsr=False,
temperature=False, gsr=False, eyetrack=False,
include=(), exclude='bads', selection=None, verbose=None):
"""Pick some channels by type and names.
Expand All @@ -621,9 +627,9 @@ def pick_types(self, meg=False, eeg=False, stim=False, eog=False,
self.info, meg=meg, eeg=eeg, stim=stim, eog=eog, ecg=ecg, emg=emg,
ref_meg=ref_meg, misc=misc, resp=resp, chpi=chpi, exci=exci,
ias=ias, syst=syst, seeg=seeg, dipole=dipole, gof=gof, bio=bio,
ecog=ecog, fnirs=fnirs, csd=csd, dbs=dbs, include=include,
exclude=exclude, selection=selection, temperature=temperature,
gsr=gsr)
ecog=ecog, fnirs=fnirs, csd=csd, dbs=dbs, temperature=temperature,
gsr=gsr, eyetrack=eyetrack, include=include, exclude=exclude,
selection=selection)

self._pick_drop_channels(idx)

Expand Down
3 changes: 2 additions & 1 deletion mne/datasets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from . import ssvep
from . import erp_core
from . import epilepsy_ecog
from . import eyelink
from . import ucl_opm_auditory
from ._fetch import fetch_dataset
from .utils import (_download_all_example_data, fetch_hcp_mmp_parcellation,
Expand All @@ -42,5 +43,5 @@
'sleep_physionet', 'somato', 'spm_face', 'ssvep', 'testing',
'visual_92_categories', 'limo', 'erp_core', 'epilepsy_ecog',
'fetch_dataset', 'fetch_phantom', 'has_dataset', 'refmeg_noise',
'fnirs_motor'
'fnirs_motor', 'eyelink'
]
15 changes: 12 additions & 3 deletions mne/datasets/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
# respective repos, and make a new release of the dataset on GitHub. Then
# update the checksum in the MNE_DATASETS dict below, and change version
# here: ↓↓↓↓↓ ↓↓↓
RELEASES = dict(testing='0.142', misc='0.24')
RELEASES = dict(testing='0.144', misc='0.26')
TESTING_VERSIONED = f'mne-testing-data-{RELEASES["testing"]}'
MISC_VERSIONED = f'mne-misc-data-{RELEASES["misc"]}'

Expand All @@ -111,7 +111,7 @@
# Testing and misc are at the top as they're updated most often
MNE_DATASETS['testing'] = dict(
archive_name=f'{TESTING_VERSIONED}.tar.gz',
hash='md5:44b857ddb34aefd752e4f5b19d625dee',
hash='md5:fb546f44dba3310945225ed8fdab4a91',
url=('https://codeload.github.com/mne-tools/mne-testing-data/'
f'tar.gz/{RELEASES["testing"]}'),
# In case we ever have to resort to osf.io again...
Expand All @@ -123,7 +123,7 @@
)
MNE_DATASETS['misc'] = dict(
archive_name=f'{MISC_VERSIONED}.tar.gz', # 'mne-misc-data',
hash='md5:eb017a919939511932bd683f26f97490',
hash='md5:868b484fadd73b1d1a3535b7194a0d03',
url=('https://codeload.github.com/mne-tools/mne-misc-data/tar.gz/'
f'{RELEASES["misc"]}'),
folder_name='MNE-misc-data',
Expand Down Expand Up @@ -335,3 +335,12 @@
folder_name='foo',
config_key='MNE_DATASETS_FAKE_PATH'
)

# eyelink dataset
MNE_DATASETS['eyelink'] = dict(
archive_name='eyelink_example_data.zip',
hash='md5:081950c05f35267458d9c751e178f161',
url=('https://osf.io/r5ndq/download?version=1'),
folder_name='eyelink-example-data',
config_key='MNE_DATASETS_EYELINK_PATH'
)
3 changes: 3 additions & 0 deletions mne/datasets/eyelink/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Eyelink test dataset."""

from .eyelink import data_path, get_version
26 changes: 26 additions & 0 deletions mne/datasets/eyelink/eyelink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Authors: Dominik Welke <[email protected]>
# License: BSD Style.

from ...utils import verbose
from ..utils import (_data_path_doc, _get_version, _version_doc,
_download_mne_dataset)


@verbose
def data_path(path=None, force_update=False, update_path=True,
download=True, *, verbose=None): # noqa: D103
return _download_mne_dataset(
name='eyelink', processor='unzip', path=path,
force_update=force_update, update_path=update_path,
download=download)


data_path.__doc__ = _data_path_doc.format(name='eyelink',
conf='MNE_DATASETS_EYELINK_PATH')


def get_version(): # noqa: D103
return _get_version('eyelink')


get_version.__doc__ = _version_doc.format(name='eyelink')
3 changes: 2 additions & 1 deletion mne/datasets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ def _download_all_example_data(verbose=True):
kiloword, phantom_4dbti, sleep_physionet, limo,
fnirs_motor, refmeg_noise, fetch_infant_template,
fetch_fsaverage, ssvep, erp_core, epilepsy_ecog,
fetch_phantom, ucl_opm_auditory)
fetch_phantom, eyelink, ucl_opm_auditory)
sample_path = sample.data_path()
testing.data_path()
misc.data_path()
Expand All @@ -327,6 +327,7 @@ def _download_all_example_data(verbose=True):
brainstorm.bst_resting.data_path(accept=True)
phantom_path = brainstorm.bst_phantom_elekta.data_path(accept=True)
fetch_phantom('otaniemi', subjects_dir=phantom_path)
eyelink.data_path()
brainstorm.bst_phantom_ctf.data_path(accept=True)
eegbci.load_data(1, [6, 10, 14], update_path=True)
for subj in range(4):
Expand Down
19 changes: 13 additions & 6 deletions mne/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,29 @@
dipole='k', gof='k', bio='k', ecog='k', hbo='#AA3377', hbr='b',
fnirs_cw_amplitude='k', fnirs_fd_ac_amplitude='k',
fnirs_fd_phase='k', fnirs_od='k', csd='k', whitened='k',
gsr='#666633', temperature='#663333'),
gsr='#666633', temperature='#663333',
eyegaze='k', pupil='k'),
si_units=dict(mag='T', grad='T/m', eeg='V', eog='V', ecg='V', emg='V',
misc='AU', seeg='V', dbs='V', dipole='Am', gof='GOF',
bio='V', ecog='V', hbo='M', hbr='M', ref_meg='T',
fnirs_cw_amplitude='V', fnirs_fd_ac_amplitude='V',
fnirs_fd_phase='rad', fnirs_od='V', csd='V/m²',
whitened='Z', gsr='S', temperature='C'),
whitened='Z', gsr='S', temperature='C',
eyegaze='AU', pupil='AU'),
units=dict(mag='fT', grad='fT/cm', eeg='µV', eog='µV', ecg='µV', emg='µV',
misc='AU', seeg='mV', dbs='µV', dipole='nAm', gof='GOF',
bio='µV', ecog='µV', hbo='µM', hbr='µM', ref_meg='fT',
fnirs_cw_amplitude='V', fnirs_fd_ac_amplitude='V',
fnirs_fd_phase='rad', fnirs_od='V', csd='mV/m²',
whitened='Z', gsr='S', temperature='C'),
whitened='Z', gsr='S', temperature='C',
eyegaze='AU', pupil='AU'),
# scalings for the units
scalings=dict(mag=1e15, grad=1e13, eeg=1e6, eog=1e6, emg=1e6, ecg=1e6,
misc=1.0, seeg=1e3, dbs=1e6, ecog=1e6, dipole=1e9, gof=1.0,
bio=1e6, hbo=1e6, hbr=1e6, ref_meg=1e15,
fnirs_cw_amplitude=1.0, fnirs_fd_ac_amplitude=1.0,
fnirs_fd_phase=1., fnirs_od=1.0, csd=1e3, whitened=1.,
gsr=1., temperature=1.),
gsr=1., temperature=1., eyegaze=1., pupil=1.),
# rough guess for a good plot
scalings_plot_raw=dict(mag=1e-12, grad=4e-11, eeg=20e-6, eog=150e-6,
ecg=5e-4, emg=1e-3, ref_meg=1e-12, misc='auto',
Expand All @@ -42,13 +45,15 @@
fnirs_fd_ac_amplitude=2e-2, fnirs_fd_phase=2e-1,
fnirs_od=2e-2, csd=200e-4,
dipole=1e-7, gof=1e2,
gsr=1., temperature=0.1),
gsr=1., temperature=0.1,
eyegaze=3e-1, pupil=1e3),
scalings_cov_rank=dict(mag=1e12, grad=1e11, eeg=1e5, # ~100x scalings
seeg=1e1, dbs=1e4, ecog=1e4, hbo=1e4, hbr=1e4),
ylim=dict(mag=(-600., 600.), grad=(-200., 200.), eeg=(-200., 200.),
misc=(-5., 5.), seeg=(-20., 20.), dbs=(-200., 200.),
dipole=(-100., 100.), gof=(0., 1.), bio=(-500., 500.),
ecog=(-200., 200.), hbo=(0, 20), hbr=(0, 20), csd=(-50., 50.)),
ecog=(-200., 200.), hbo=(0, 20), hbr=(0, 20), csd=(-50., 50.),
eyegaze=(0., 5000.), pupil=(0., 5000.)),
titles=dict(mag='Magnetometers', grad='Gradiometers', eeg='EEG', eog='EOG',
ecg='ECG', emg='EMG', misc='misc', seeg='sEEG', dbs='DBS',
bio='BIO', dipole='Dipole', ecog='ECoG', hbo='Oxyhemoglobin',
Expand All @@ -60,6 +65,8 @@
gof='Goodness of fit', csd='Current source density',
stim='Stimulus', gsr='Galvanic skin response',
temperature='Temperature',
eyegaze='Eye-tracking (Gaze position)',
pupil='Eye-tracking (Pupil size)',
),
mask_params=dict(marker='o',
markerfacecolor='w',
Expand Down
1 change: 1 addition & 0 deletions mne/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
read_evoked_fieldtrip)
from .nihon import read_raw_nihon
from ._read_raw import read_raw
from .eyelink import read_raw_eyelink


# for backward compatibility
Expand Down
15 changes: 14 additions & 1 deletion mne/io/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@
FIFF.FIFFV_FNIRS_CH = 1100 # Functional near-infrared spectroscopy
FIFF.FIFFV_TEMPERATURE_CH = 1200 # Functional near-infrared spectroscopy
FIFF.FIFFV_GALVANIC_CH = 1300 # Galvanic skin response
FIFF.FIFFV_EYETRACK_CH = 1400 # Eye-tracking

_ch_kind_named = {key: key for key in (
FIFF.FIFFV_BIO_CH,
FIFF.FIFFV_MEG_CH,
Expand All @@ -227,6 +229,7 @@
FIFF.FIFFV_FNIRS_CH,
FIFF.FIFFV_GALVANIC_CH,
FIFF.FIFFV_TEMPERATURE_CH,
FIFF.FIFFV_EYETRACK_CH
)}

#
Expand Down Expand Up @@ -854,6 +857,8 @@
FIFF.FIFF_UNIT_AM = 202 # Am
FIFF.FIFF_UNIT_AM_M2 = 203 # Am/m^2
FIFF.FIFF_UNIT_AM_M3 = 204 # Am/m^3

FIFF.FIFF_UNIT_PX = 210 # Pixel
_ch_unit_named = {key: key for key in(
FIFF.FIFF_UNIT_NONE, FIFF.FIFF_UNIT_UNITLESS, FIFF.FIFF_UNIT_M,
FIFF.FIFF_UNIT_KG, FIFF.FIFF_UNIT_SEC, FIFF.FIFF_UNIT_A, FIFF.FIFF_UNIT_K,
Expand All @@ -865,6 +870,7 @@
FIFF.FIFF_UNIT_CEL, FIFF.FIFF_UNIT_LM, FIFF.FIFF_UNIT_LX,
FIFF.FIFF_UNIT_V_M2, FIFF.FIFF_UNIT_T_M, FIFF.FIFF_UNIT_AM,
FIFF.FIFF_UNIT_AM_M2, FIFF.FIFF_UNIT_AM_M3,
FIFF.FIFF_UNIT_PX,
)}
#
# Multipliers
Expand Down Expand Up @@ -916,6 +922,11 @@
FIFF.FIFFV_COIL_FNIRS_FD_AC_AMPLITUDE = 304 # fNIRS frequency domain AC amplitude
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_FNIRS_TD_GATED_AMPLITUDE = 306 # fNIRS time-domain gated amplitude
FIFF.FIFFV_COIL_FNIRS_TD_MOMENTS_AMPLITUDE = 307 # fNIRS time-domain moments amplitude

FIFF.FIFFV_COIL_EYETRACK_POS = 400 # Eye-tracking gaze position
FIFF.FIFFV_COIL_EYETRACK_PUPIL = 401 # Eye-tracking pupil size

FIFF.FIFFV_COIL_MCG_42 = 1000 # For testing the MCG software

Expand Down Expand Up @@ -1002,7 +1013,9 @@
FIFF.FIFFV_COIL_DIPOLE, FIFF.FIFFV_COIL_FNIRS_HBO,
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_FNIRS_FD_PHASE, FIFF.FIFFV_COIL_FNIRS_TD_GATED_AMPLITUDE,
FIFF.FIFFV_COIL_FNIRS_TD_MOMENTS_AMPLITUDE, FIFF.FIFFV_COIL_MCG_42,
FIFF.FIFFV_COIL_EYETRACK_POS, 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
7 changes: 7 additions & 0 deletions mne/io/eyelink/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Module for loading Eye-Tracker data."""

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

from .eyelink import read_raw_eyelink
Loading

0 comments on commit 62af4ac

Please sign in to comment.