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)
|