Spaces:
Running
Running
PerceptionLabPortable
/
python_embed
/Lib
/site-packages
/mne
/preprocessing
/eyetracking
/calibration.py
| """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 | |
| 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 | |
| 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) | |