Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 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)
Comment on lines +110 to +111
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ends up a bit wacky:

image

If you set the units and scalings it will probably be more reasonable I think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @larsoner Im not sure what you mean by setting the units and if you are referring to the eyegaze channel or pupil channel (I assume eyegaze) - Can you clarify?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant for plot_raw but I see now that we only have it for other viz functions like plot_evoked, so never mind! Also not sure it would actually help...


# %%
# 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