File: calibration.py

package info (click to toggle)
python-mne 1.9.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 131,492 kB
  • sloc: python: 213,302; javascript: 12,910; sh: 447; makefile: 144
file content (222 lines) | stat: -rw-r--r-- 8,011 bytes parent folder | download
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)