Skip to content

Commit

Permalink
Use SI units for eyetracking data, update tutorials (mne-tools#12846)
Browse files Browse the repository at this point in the history
  • Loading branch information
scott-huberty authored Sep 13, 2024
1 parent 2f29927 commit 670330a
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 34 deletions.
1 change: 1 addition & 0 deletions doc/changes/devel/12846.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enforce SI units for Eyetracking data (eyegaze data should be radians of visual angle, not pixels. Pupil size data should be meters). Updated tutorials so demonstrate how to convert data to SI units before analyses (:gh:`12846`` by `Scott Huberty`_)
4 changes: 4 additions & 0 deletions examples/visualization/eyetracking_plot_heatmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@
cmap = plt.get_cmap("viridis")
plot_gaze(epochs["natural"], calibration=calibration, cmap=cmap, sigma=50)

# %%
# .. note:: The (0, 0) pixel coordinates are at the top-left of the
# trackable area of the screen for many eye trackers.

# %%
# Overlaying plots with images
# ----------------------------
Expand Down
18 changes: 9 additions & 9 deletions mne/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@
whitened="Z",
gsr="S",
temperature="C",
eyegaze="AU",
pupil="AU",
eyegaze="rad",
pupil="M",
),
units=dict(
mag="fT",
Expand All @@ -92,8 +92,8 @@
whitened="Z",
gsr="S",
temperature="C",
eyegaze="AU",
pupil="AU",
eyegaze="rad",
pupil="µM",
),
# scalings for the units
scalings=dict(
Expand Down Expand Up @@ -122,7 +122,7 @@
gsr=1.0,
temperature=1.0,
eyegaze=1.0,
pupil=1.0,
pupil=1e6,
),
# rough guess for a good plot
scalings_plot_raw=dict(
Expand Down Expand Up @@ -156,8 +156,8 @@
gof=1e2,
gsr=1.0,
temperature=0.1,
eyegaze=3e-1,
pupil=1e3,
eyegaze=2e-1,
pupil=10e-6,
),
scalings_cov_rank=dict(
mag=1e12,
Expand All @@ -183,8 +183,8 @@
hbo=(0, 20),
hbr=(0, 20),
csd=(-50.0, 50.0),
eyegaze=(0.0, 5000.0),
pupil=(0.0, 5000.0),
eyegaze=(-1, 1),
pupil=(0.0, 20),
),
titles=dict(
mag="Magnetometers",
Expand Down
50 changes: 33 additions & 17 deletions tutorials/io/70_reading_eyetracking_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,29 +78,43 @@
new line, the y-coordinate *increased*, which is why the ``ypos_right`` channel
in the plot below increases over time (for example, at about 4-seconds, and
at about 8-seconds).
.. seealso::
:ref:`tut-eyetrack`
"""

# %%
from mne.datasets import misc
from mne.io import read_raw_eyelink
import mne

# %%
fpath = misc.data_path() / "eyetracking" / "eyelink"
raw = read_raw_eyelink(fpath / "px_textpage_ws.asc", create_annotations=["blinks"])
custom_scalings = dict(eyegaze=1e3)
raw.pick(picks="eyetrack").plot(scalings=custom_scalings)
fpath = mne.datasets.misc.data_path() / "eyetracking" / "eyelink"
fname = fpath / "px_textpage_ws.asc"
raw = mne.io.read_raw_eyelink(fname, create_annotations=["blinks"])
cal = mne.preprocessing.eyetracking.read_eyelink_calibration(
fname,
screen_distance=0.7,
screen_size=(0.53, 0.3),
screen_resolution=(1920, 1080),
)[0]
mne.preprocessing.eyetracking.convert_units(raw, calibration=cal, to="radians")

# %%
# Visualizing the data
# ^^^^^^^^^^^^^^^^^^^^

# %%
# .. important:: The (0, 0) pixel coordinates are at the top-left of the
# trackable area of the screen. Gaze towards lower areas of the
# the screen will yield a relatively higher y-coordinate.
#
# Note that we passed a custom `dict` to the ``'scalings'`` argument of
# `mne.io.Raw.plot`. This is because MNE's default plot scalings for eye
# position data are calibrated for HREF data, which are stored in radians
# (read below).
cal.plot()

# %%
custom_scalings = dict(pupil=1e3)
raw.pick(picks="eyetrack").plot(scalings=custom_scalings)

# %%
# Note that we passed a custom `dict` to the ``'scalings'`` argument of
# `mne.io.Raw.plot`. This is because MNE expects the data to be in SI units
# (radians for eyegaze data, and meters for pupil size data), but we did not convert
# the pupil size data in this example.

# %%
# Head-Referenced Eye Angle (HREF)
Expand All @@ -124,9 +138,11 @@


# %%
fpath = misc.data_path() / "eyetracking" / "eyelink"
raw = read_raw_eyelink(fpath / "HREF_textpage_ws.asc", create_annotations=["blinks"])
raw.pick(picks="eyetrack").plot()
fpath = mne.datasets.misc.data_path() / "eyetracking" / "eyelink"
fname_href = fpath / "HREF_textpage_ws.asc"
raw = mne.io.read_raw_eyelink(fname_href, create_annotations=["blinks"])
custom_scalings = dict(pupil=1e3)
raw.pick(picks="eyetrack").plot(scalings=custom_scalings)

# %%
# Pupil Position
Expand Down
40 changes: 32 additions & 8 deletions tutorials/preprocessing/90_eyetracking_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,32 @@

first_cal.plot()

# %%
# Standardizing eyetracking data to SI units
# ------------------------------------------
#
# EyeLink stores eyegaze positions in pixels, and pupil size in arbitrary units.
# MNE-Python expects eyegaze positions to be in radians of visual angle, and pupil
# size to be in meters. We can convert the eyegaze positions to radians using
# :func:`~mne.preprocessing.eyetracking.convert_units`. We'll pass the calibration
# object we created above, after specifying the screen resolution, screen size, and
# screen distance.

first_cal["screen_resolution"] = (1920, 1080)
first_cal["screen_size"] = (0.53, 0.3)
first_cal["screen_distance"] = 0.9
mne.preprocessing.eyetracking.convert_units(raw_et, calibration=first_cal, to="radians")

# %%
# Plot the raw eye-tracking data
# ------------------------------
#
# Let's plot the raw eye-tracking data. We'll pass a custom `dict` into
# the scalings argument to make the eyegaze channel traces legible when plotting,
# since this file contains pixel position data (as opposed to eye angles,
# which are reported in radians).
# Let's plot the raw eye-tracking data. Since we did not convert the pupil size to
# meters, we'll pass a custom `dict` into the scalings argument to make the pupil size
# traces legible when plotting.

raw_et.plot(scalings=dict(eyegaze=1e3))
ps_scalings = dict(pupil=1e3)
raw_et.plot(scalings=ps_scalings)

# %%
# Handling blink artifacts
Expand Down Expand Up @@ -189,7 +205,13 @@
picks_idx = mne.pick_channels(
raw_et.ch_names, frontal + occipital + pupil, ordered=True
)
raw_et.plot(events=et_events, event_id=event_dict, event_color="g", order=picks_idx)
raw_et.plot(
events=et_events,
event_id=event_dict,
event_color="g",
order=picks_idx,
scalings=ps_scalings,
)


# %%
Expand All @@ -203,14 +225,16 @@
raw_et, events=et_events, event_id=event_dict, tmin=-0.3, tmax=3, baseline=None
)
del raw_et # free up some memory
epochs[:8].plot(events=et_events, event_id=event_dict, order=picks_idx)
epochs[:8].plot(
events=et_events, event_id=event_dict, order=picks_idx, scalings=ps_scalings
)

# %%
# For this experiment, the participant was instructed to fixate on a crosshair in the
# center of the screen. Let's plot the gaze position data to confirm that the
# participant primarily kept their gaze fixated at the center of the screen.

plot_gaze(epochs, width=1920, height=1080)
plot_gaze(epochs, calibration=first_cal)

# %%
# .. seealso:: :ref:`tut-eyetrack-heatmap`
Expand Down

0 comments on commit 670330a

Please sign in to comment.