-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
ENH: Create a Calibrations class for eyetracking data #11719
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
Merged
Merged
Changes from all commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
5fb94fd
ENH: Create a Calibrations class for eyetracking and reader function …
scott-huberty 444f6d8
FIX, DOC: please review. add read_eyelink_calibration to mne namespace
scott-huberty 48a946c
ENH, STY: Refactor read_eyelink_calibration and update test
scott-huberty 000bc5c
ENH, STY: Remove Calibrations class in favor of simple list. Please R…
scott-huberty 1863b24
ENH: Add plot method and a test
scott-huberty e1115c1
FIX: Incorporate Britta's suggested revisions.
scott-huberty ea80885
ENH, STY: refactor code to create Calibration.points array
scott-huberty 7fac97e
FIX: nest matplotlib import under plot method
scott-huberty 71ccbac
FIX, DOC: Fixes to docstring that were raised by test_docstring_param…
scott-huberty 2bfff19
FIX, DOC: More docstring error fixes
scott-huberty ec13a2b
ENH, DOC: Nest Calibration class under mne.preprocessing.eyetracking
scott-huberty 1d09b00
Update mne/io/eyelink/_utils.py
scott-huberty dd25b35
Update mne/io/eyelink/_utils.py
scott-huberty ca01ed8
FIX: black error
scott-huberty f484759
FIX, DOC: More docstring error fixes, hopefully
scott-huberty 90fbdd2
FIX, DOC: more sphinx build fixes
scott-huberty 604e981
ENH: additional parameters to calibration.plot method
scott-huberty 57339b9
DOC: use no_inherited_members template for Calibration class
scott-huberty 03f5b6b
Revert "DOC: use no_inherited_members template for Calibration class"
scott-huberty 39d9357
Update mne/io/eyelink/eyelink.py
scott-huberty 156c2bd
Update mne/io/eyelink/eyelink.py
scott-huberty e5fecea
Update mne/io/eyelink/eyelink.py
scott-huberty ebcd020
Merge branch 'main' into et_calibration
larsoner 87320fb
FIX, DOC: Docstring fixes suggested by Mathieu
scott-huberty 781bc6f
Merge branch 'et_calibration' of github.com:scott-huberty/mne-python …
scott-huberty ca9b156
FIX: Remove Cruft
scott-huberty 5015c9b
FIX, DOC: apply suggestions made by Richard H
scott-huberty 17eca8d
FIX: Move mne.io.eyelink.read_eyelink_calibraiton to mne.preprocessin…
scott-huberty 296794b
DOC: Added PR to changelog
scott-huberty 11ec6b8
FIX, DOC: make calibration['points'] more user friendly and increase …
scott-huberty cda0e09
FIX: build_doc warnings caused by docstring mistakes in previous commit
scott-huberty 328c505
FIX: more fixes of docstring errors..
scott-huberty ccbd5ec
FIX: error in old-ubuntu CI where dtype is a list
scott-huberty e821ffe
DOC: add example of loading and visualizing Calibration object to tut…
scott-huberty f1c2c9d
FIX: more fixes
scott-huberty 6f1cffd
FIX: tutorial fix and another ubuntu-old fix attempt
scott-huberty 2853dff
FIX: Yet another tutorial fix
scott-huberty 4938d59
FIX, STY: re-run cis and some style revisions to tutorials
scott-huberty 2dee68e
rerun cis
scott-huberty ca9157d
FIX, DOC: fix backward incompatible code, finishing touches on doc
scott-huberty c972567
FIX, DOC: use BAD_ACQ_SKIP and improve docstring
scott-huberty d71709b
Update mne/preprocessing/eyetracking/calibration.py
scott-huberty 143c583
Apply suggestions from code review
scott-huberty 6ede0af
FIX: Integrate suggestions by Eric Larson
scott-huberty b256454
DOC: add bug fix to change log
scott-huberty cd3b34a
Merge branch 'main' into et_calibration
scott-huberty c0f8bac
Update doc/changes/latest.inc
scott-huberty 48fdd2e
DOC: Add API change to change log
scott-huberty 5b437fa
Update doc/changes/latest.inc
scott-huberty 86d0138
Apply suggestions from code review
scott-huberty 1184215
FIX: add _check_option import
scott-huberty 5781a59
Apply suggestions from code review
scott-huberty c7d7dd9
FIX: add Eric's suggestions
scott-huberty 0e5133c
Update mne/io/eyelink/_utils.py
scott-huberty 3d5d5d7
FIX, remove origin kwarg from plot method, add axes kwarg. doc upate …
scott-huberty 073fba9
Merge branch 'main' into et_calibration
scott-huberty File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,4 +61,4 @@ Base class: | |
| :toctree: generated | ||
| :template: autosummary/class_no_members.rst | ||
|
|
||
| BaseEpochs | ||
| BaseEpochs | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| """Helper functions for reading eyelink ASCII files.""" | ||
| # Authors: Scott Huberty <seh33@uw.edu> | ||
| # License: BSD-3-Clause | ||
|
|
||
| import re | ||
| import numpy as np | ||
|
|
||
|
|
||
| def _find_recording_start(lines): | ||
| """Return the first START line in an SR Research EyeLink ASCII file. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| lines: A list of strings, which are The lines in an eyelink ASCII file. | ||
|
|
||
| Returns | ||
| ------- | ||
| The line that contains the info on the start of the recording. | ||
| """ | ||
| for line in lines: | ||
| if line.startswith("START"): | ||
| return line | ||
| raise ValueError("Could not find the start of the recording.") | ||
|
|
||
|
|
||
| def _parse_validation_line(line): | ||
| """Parse a single line of eyelink validation data. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| line: A string containing a line of validation data from an eyelink | ||
| ASCII file. | ||
|
|
||
| Returns | ||
| ------- | ||
| A list of tuples containing the validation data. | ||
| """ | ||
| tokens = line.split() | ||
| xy = tokens[-6].strip("[]").split(",") # e.g. '960, 540' | ||
| xy_diff = tokens[-2].strip("[]").split(",") # e.g. '-1.5, -2.8' | ||
| vals = [float(v) for v in [*xy, tokens[-4], *xy_diff]] | ||
| vals[3] += vals[0] # pos_x + eye_x i.e. 960 + -1.5 | ||
| vals[4] += vals[1] # pos_y + eye_y | ||
|
|
||
| return tuple(vals) | ||
|
|
||
|
|
||
| def _parse_calibration( | ||
| lines, screen_size=None, screen_distance=None, screen_resolution=None | ||
| ): | ||
| """Parse the lines in the given list and returns a list of Calibration instances. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| lines: A list of strings, which are The lines in an eyelink ASCII file. | ||
|
|
||
| Returns | ||
| ------- | ||
| A list containing one or more Calibration instances, | ||
| one for each calibration that was recorded in the eyelink ASCII file | ||
| data. | ||
| """ | ||
scott-huberty marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| from ...preprocessing.eyetracking.calibration import Calibration | ||
|
|
||
| regex = re.compile(r"\d+") # for finding numeric characters | ||
| calibrations = list() | ||
| rec_start = float(_find_recording_start(lines).split()[1]) | ||
|
|
||
| for line_number, line in enumerate(lines): | ||
| if ( | ||
| "!CAL VALIDATION " in line and "ABORTED" not in line | ||
| ): # Start of a calibration | ||
| tokens = line.split() | ||
| model = tokens[4] # e.g. 'HV13' | ||
| this_eye = tokens[6].lower() # e.g. 'left' | ||
| timestamp = float(tokens[1]) | ||
| onset = (timestamp - rec_start) / 1000.0 # in seconds | ||
| avg_error = float(line.split("avg.")[0].split()[-1]) # e.g. 0.3 | ||
| max_error = float(line.split("max")[0].split()[-1]) # e.g. 0.9 | ||
|
|
||
| n_points = int(regex.search(model).group()) # e.g. 13 | ||
| n_points *= 2 if "LR" in line else 1 # one point per eye if "LR" | ||
| # The next n_point lines contain the validation data | ||
| points = [] | ||
| for validation_index in range(n_points): | ||
| subline = lines[line_number + validation_index + 1] | ||
| if "!CAL VALIDATION" in subline: | ||
| continue # for bino mode, skip the second eye's validation summary | ||
| subline_eye = subline.split("at")[0].split()[-1].lower() # e.g. 'left' | ||
| if subline_eye != this_eye: | ||
| continue # skip the validation lines for the other eye | ||
| point_info = _parse_validation_line(subline) | ||
| points.append(point_info) | ||
| # Convert the list of validation data into a numpy array | ||
| positions = np.array([point[:2] for point in points]) | ||
| offsets = np.array([point[2] for point in points]) | ||
| gaze = np.array([point[3:] for point in points]) | ||
| # create the Calibration instance | ||
| calibration = Calibration( | ||
| onset=max(0.0, onset), # 0 if calibrated before recording | ||
| model=model, | ||
| eye=this_eye, | ||
| avg_error=avg_error, | ||
| max_error=max_error, | ||
| positions=positions, | ||
| offsets=offsets, | ||
| gaze=gaze, | ||
| screen_size=screen_size, | ||
| screen_distance=screen_distance, | ||
| screen_resolution=screen_resolution, | ||
| ) | ||
| calibrations.append(calibration) | ||
| return calibrations | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| """SR Research Eyelink Load Function.""" | ||
|
|
||
| # Authors: Dominik Welke <dominik.welke@web.de> | ||
| # Scott Huberty <seh33@uw.edu> | ||
| # Christian O'Reilly <christian.oreilly@sc.edu> | ||
|
|
@@ -12,7 +14,7 @@ | |
| from ..base import BaseRaw | ||
| from ..meas_info import create_info | ||
| from ...annotations import Annotations | ||
| from ...utils import logger, verbose, fill_doc, _check_pandas_installed | ||
| from ...utils import _check_fname, _check_pandas_installed, fill_doc, logger, verbose | ||
|
|
||
| EYELINK_COLS = { | ||
| "timestamp": ("time",), | ||
|
|
@@ -293,13 +295,13 @@ def read_raw_eyelink( | |
| apply_offsets=False, | ||
| find_overlaps=False, | ||
| overlap_threshold=0.05, | ||
| gap_description="bad_rec_gap", | ||
| gap_description=None, | ||
| ): | ||
| """Reader for an Eyelink .asc file. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| fname : str | ||
| fname : path-like | ||
| Path to the eyelink file (.asc). | ||
| %(preload)s | ||
| %(verbose)s | ||
|
|
@@ -318,15 +320,20 @@ def read_raw_eyelink( | |
| saccades) if their start times and their stop times are both not | ||
| separated by more than overlap_threshold. | ||
| overlap_threshold : float (default 0.05) | ||
| Time in seconds. Threshold of allowable time-gap between the start and | ||
| stop times of the left and right eyes. If gap is larger than threshold, | ||
| the :class:`mne.Annotations` will be kept separate (i.e. "blink_L", | ||
| "blink_R"). If the gap is smaller than the threshold, the | ||
| :class:`mne.Annotations` will be merged (i.e. "blink_both"). | ||
| gap_description : str (default 'bad_rec_gap') | ||
| If there are multiple recording blocks in the file, the description of | ||
| Time in seconds. Threshold of allowable time-gap between both the start and | ||
| stop times of the left and right eyes. If the gap is larger than the threshold, | ||
| the :class:`mne.Annotations` will be kept separate (i.e. ``"blink_L"``, | ||
| ``"blink_R"``). If the gap is smaller than the threshold, the | ||
| :class:`mne.Annotations` will be merged and labeled as ``"blink_both"``. | ||
| Defaults to ``0.05`` seconds (50 ms), meaning that if the blink start times of | ||
| the left and right eyes are separated by less than 50 ms, and the blink stop | ||
| times of the left and right eyes are separated by less than 50 ms, then the | ||
| blink will be merged into a single :class:`mne.Annotations`. | ||
| gap_description : str (default 'BAD_ACQ_SKIP') | ||
scott-huberty marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| This parameter is deprecated and will be removed in 1.6. | ||
| Use :meth:`mne.Annotations.rename` instead. | ||
| the annotation that will span across the gap period between the | ||
| blocks. Uses 'bad_rec_gap' by default so that these time periods will | ||
| blocks. Uses ``'BAD_ACQ_SKIP'`` by default so that these time periods will | ||
| be considered bad by MNE and excluded from operations like epoching. | ||
|
|
||
| Returns | ||
|
|
@@ -337,17 +344,26 @@ def read_raw_eyelink( | |
| See Also | ||
| -------- | ||
| mne.io.Raw : Documentation of attribute and methods. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add |
||
|
|
||
| Notes | ||
| ----- | ||
| It is common for SR Research Eyelink eye trackers to only record data during trials. | ||
| To avoid frequent data discontinuities and to ensure that the data is continuous | ||
| so that it can be aligned with EEG and MEG data (if applicable), this reader will | ||
| preserve the times between recording trials and annotate them with | ||
| ``'BAD_ACQ_SKIP'``. | ||
| """ | ||
| extension = Path(fname).suffix | ||
| fname = _check_fname(fname, overwrite="read", must_exist=True, name="fname") | ||
| extension = fname.suffix | ||
| if extension not in ".asc": | ||
| raise ValueError( | ||
| "This reader can only read eyelink .asc files." | ||
| f" Got extension {extension} instead. consult eyelink" | ||
| " manual for converting eyelink data format (.edf)" | ||
| f" Got extension {extension} instead. consult EyeLink" | ||
| " manual for converting EyeLink data format (.edf)" | ||
| " files to .asc format." | ||
| ) | ||
|
|
||
| return RawEyelink( | ||
| raw_eyelink = RawEyelink( | ||
| fname, | ||
| preload=preload, | ||
| verbose=verbose, | ||
|
|
@@ -357,6 +373,7 @@ def read_raw_eyelink( | |
| overlap_threshold=overlap_threshold, | ||
| gap_desc=gap_description, | ||
| ) | ||
| return raw_eyelink | ||
|
|
||
|
|
||
| @fill_doc | ||
|
|
@@ -365,7 +382,7 @@ class RawEyelink(BaseRaw): | |
|
|
||
| Parameters | ||
| ---------- | ||
| fname : str | ||
| fname : path-like | ||
| Path to the data file (.XXX). | ||
| create_annotations : bool | list (default True) | ||
| Whether to create mne.Annotations from occular events | ||
|
|
@@ -387,11 +404,15 @@ class RawEyelink(BaseRaw): | |
| the :class:`mne.Annotations` will be kept separate (i.e. "blink_L", | ||
| "blink_R"). If the gap is smaller than the threshold, the | ||
| :class:`mne.Annotations` will be merged (i.e. "blink_both"). | ||
| gap_desc : str (default 'bad_rec_gap') | ||
| gap_desc : str | ||
| If there are multiple recording blocks in the file, the description of | ||
| the annotation that will span across the gap period between the | ||
| blocks. Uses 'bad_rec_gap' by default so that these time periods will | ||
| be considered bad by MNE and excluded from operations like epoching. | ||
| blocks. Default is ``None``, which uses 'BAD_ACQ_SKIP' by default so that these | ||
| timeperiods will be considered bad by MNE and excluded from operations like | ||
| epoching. Note that this parameter is deprecated and will be removed in 1.6. | ||
| Use ``mne.annotations.rename`` instead. | ||
|
|
||
|
|
||
| %(preload)s | ||
| %(verbose)s | ||
|
|
||
|
|
@@ -402,23 +423,6 @@ class RawEyelink(BaseRaw): | |
| dataframes : dict | ||
| Dictionary of pandas DataFrames. One for eyetracking samples, | ||
| and one for each type of eyelink event (blinks, messages, etc) | ||
| _sample_lines : list | ||
| List of lists, each list is one sample containing eyetracking | ||
| X/Y and pupil channel data (+ other channels, if they exist) | ||
| _event_lines : dict | ||
| Each key contains a list of lists, for an event-type that occurred | ||
| during the recording period. Events can vary, from occular events | ||
| (blinks, saccades, fixations), to messages from the stimulus | ||
| presentation software, or info from a response controller. | ||
| _system_lines : list | ||
| List of tab delimited strings. Each string is a system message, | ||
| that in most cases aren't needed. System messages occur for | ||
| Eyelinks DataViewer application. | ||
| _tracking_mode : str | ||
| Whether whether a single eye was tracked ('monocular'), or both | ||
| ('binocular'). | ||
| _gap_desc : str | ||
| The description to be used for annotations returned by _make_gap_annots | ||
|
|
||
| See Also | ||
| -------- | ||
|
|
@@ -435,17 +439,26 @@ def __init__( | |
| apply_offsets=False, | ||
| find_overlaps=False, | ||
| overlap_threshold=0.05, | ||
| gap_desc="bad_rec_gap", | ||
| gap_desc=None, | ||
| ): | ||
| logger.info("Loading {}".format(fname)) | ||
|
|
||
| self.fname = Path(fname) | ||
| self._sample_lines = None | ||
| self._event_lines = None | ||
| self._system_lines = None | ||
| self._sample_lines = None # sample lines from file | ||
| self._event_lines = None # event messages from file | ||
| self._system_lines = None # unparsed lines of system messages from file | ||
| self._tracking_mode = None # assigned in self._infer_col_names | ||
| self._meas_date = None | ||
| self._rec_info = None | ||
| if gap_desc is None: | ||
| gap_desc = "BAD_ACQ_SKIP" | ||
| else: | ||
| logger.warn( | ||
| "gap_description is deprecated in 1.5 and will be removed in 1.6, " | ||
| "use raw.annotations.rename to use a description other than " | ||
| "'BAD_ACQ_SKIP'", | ||
| FutureWarning, | ||
| ) | ||
| self._gap_desc = gap_desc | ||
| self.dataframes = {} | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.