initial commit
This commit is contained in:
222
mne/preprocessing/eyetracking/calibration.py
Normal file
222
mne/preprocessing/eyetracking/calibration.py
Normal file
@@ -0,0 +1,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)
|
||||
Reference in New Issue
Block a user