[074d3d]: / mne / preprocessing / eyetracking / calibration.py

Download this file

223 lines (194 with data), 8.0 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
"""Eyetracking Calibration(s) class constructor."""
# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
from copy import deepcopy
import numpy as np
from ...io.eyelink._utils import _parse_calibration
from ...utils import _check_fname, _validate_type, fill_doc, logger
from ...viz.utils import plt_show
@fill_doc
class Calibration(dict):
"""Eye-tracking calibration info.
This data structure behaves like a dictionary. It contains information regarding a
calibration that was conducted during an eye-tracking recording.
.. note::
When possible, a Calibration instance should be created with a helper function,
such as :func:`~mne.preprocessing.eyetracking.read_eyelink_calibration`.
Parameters
----------
onset : float
The onset of the calibration in seconds. If the calibration was
performed before the recording started, the the onset can be
negative.
model : str
A string, which is the model of the eye-tracking calibration that was applied.
For example ``'H3'`` for a horizontal only 3-point calibration, or ``'HV3'``
for a horizontal and vertical 3-point calibration.
eye : str
The eye that was calibrated. For example, ``'left'``, or ``'right'``.
avg_error : float
The average error in degrees between the calibration positions and the
actual gaze position.
max_error : float
The maximum error in degrees that occurred between the calibration
positions and the actual gaze position.
positions : array-like of float, shape ``(n_calibration_points, 2)``
The x and y coordinates of the calibration points.
offsets : array-like of float, shape ``(n_calibration_points,)``
The error in degrees between the calibration position and the actual
gaze position for each calibration point.
gaze : array-like of float, shape ``(n_calibration_points, 2)``
The x and y coordinates of the actual gaze position for each calibration point.
screen_size : array-like of shape ``(2,)``
The width and height (in meters) of the screen that the eyetracking
data was collected with. For example ``(.531, .298)`` for a monitor with
a display area of 531 x 298 mm.
screen_distance : float
The distance (in meters) from the participant's eyes to the screen.
screen_resolution : array-like of shape ``(2,)``
The resolution (in pixels) of the screen that the eyetracking data
was collected with. For example, ``(1920, 1080)`` for a 1920x1080
resolution display.
"""
def __init__(
self,
*,
onset,
model,
eye,
avg_error,
max_error,
positions,
offsets,
gaze,
screen_size=None,
screen_distance=None,
screen_resolution=None,
):
super().__init__(
onset=onset,
model=model,
eye=eye,
avg_error=avg_error,
max_error=max_error,
screen_size=screen_size,
screen_distance=screen_distance,
screen_resolution=screen_resolution,
positions=positions,
offsets=offsets,
gaze=gaze,
)
def __repr__(self):
"""Return a summary of the Calibration object."""
return (
f"Calibration |\n"
f" onset: {self['onset']} seconds\n"
f" model: {self['model']}\n"
f" eye: {self['eye']}\n"
f" average error: {self['avg_error']} degrees\n"
f" max error: {self['max_error']} degrees\n"
f" screen size: {self['screen_size']} meters\n"
f" screen distance: {self['screen_distance']} meters\n"
f" screen resolution: {self['screen_resolution']} pixels\n"
)
def copy(self):
"""Copy the instance.
Returns
-------
cal : instance of Calibration
The copied Calibration.
"""
return deepcopy(self)
def plot(self, show_offsets=True, axes=None, show=True):
"""Visualize calibration.
Parameters
----------
show_offsets : bool
Whether to display the offset (in visual degrees) of each calibration
point or not. Defaults to ``True``.
axes : instance of matplotlib.axes.Axes | None
Axes to draw the calibration positions to. If ``None`` (default), a new axes
will be created.
show : bool
Whether to show the figure or not. Defaults to ``True``.
Returns
-------
fig : instance of matplotlib.figure.Figure
The resulting figure object for the calibration plot.
"""
import matplotlib.pyplot as plt
msg = "positions and gaze keys must both be 2D numpy arrays."
assert isinstance(self["positions"], np.ndarray), msg
assert isinstance(self["gaze"], np.ndarray), msg
if axes is not None:
from matplotlib.axes import Axes
_validate_type(axes, Axes, "axes")
ax = axes
fig = ax.get_figure()
else: # create new figure and axes
fig, ax = plt.subplots(layout="constrained")
px, py = self["positions"].T
gaze_x, gaze_y = self["gaze"].T
ax.set_title(f"Calibration ({self['eye']} eye)")
ax.set_xlabel("x (pixels)")
ax.set_ylabel("y (pixels)")
# Display avg_error and max_error in the top left corner
text = (
f"avg_error: {self['avg_error']} deg.\nmax_error: {self['max_error']} deg."
)
ax.text(
0,
1.01,
text,
transform=ax.transAxes,
verticalalignment="baseline",
fontsize=8,
)
# Invert y-axis because the origin is in the top left corner
ax.invert_yaxis()
ax.scatter(px, py, color="gray")
ax.scatter(gaze_x, gaze_y, color="red", alpha=0.5)
if show_offsets:
for i in range(len(px)):
x_offset = 0.01 * gaze_x[i] # 1% to the right of the gazepoint
text = ax.text(
x=gaze_x[i] + x_offset,
y=gaze_y[i],
s=self["offsets"][i],
fontsize=8,
ha="left",
va="center",
)
plt_show(show)
return fig
@fill_doc
def read_eyelink_calibration(
fname, screen_size=None, screen_distance=None, screen_resolution=None
):
"""Return info on calibrations collected in an eyelink file.
Parameters
----------
fname : path-like
Path to the eyelink file (.asc).
screen_size : array-like of shape ``(2,)``
The width and height (in meters) of the screen that the eyetracking
data was collected with. For example ``(.531, .298)`` for a monitor with
a display area of 531 x 298 mm. Defaults to ``None``.
screen_distance : float
The distance (in meters) from the participant's eyes to the screen.
Defaults to ``None``.
screen_resolution : array-like of shape ``(2,)``
The resolution (in pixels) of the screen that the eyetracking data
was collected with. For example, ``(1920, 1080)`` for a 1920x1080
resolution display. Defaults to ``None``.
Returns
-------
calibrations : list
A list of :class:`~mne.preprocessing.eyetracking.Calibration` instances, one for
each eye of every calibration that was performed during the recording session.
"""
fname = _check_fname(fname, overwrite="read", must_exist=True, name="fname")
logger.info(f"Reading calibration data from {fname}")
lines = fname.read_text(encoding="ASCII").splitlines()
return _parse_calibration(lines, screen_size, screen_distance, screen_resolution)