CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
eyetracking.py328 linesDownload Raw Back to eyetracking
1# Authors: The MNE-Python contributors.2# License: BSD-3-Clause3# Copyright the MNE-Python contributors.4 5 6import numpy as np7 8from ..._fiff.constants import FIFF9from ...epochs import BaseEpochs10from ...evoked import Evoked11from ...io import BaseRaw12from ...utils import _check_option, _validate_type, logger, warn13from .calibration import Calibration14from .utils import _check_calibration15 16 17# specific function to set eyetrack channels18def set_channel_types_eyetrack(inst, mapping):19    """Define sensor type for eyetrack channels.20 21    This function can set all eye tracking specific information:22    channel type, unit, eye (and x/y component; only for gaze channels)23 24    Supported channel types:25    ``'eyegaze'`` and ``'pupil'``26 27    Supported units:28    ``'au'``, ``'px'``, ``'deg'``, ``'rad'`` (for eyegaze)29    ``'au'``, ``'mm'``, ``'m'`` (for pupil)30 31    Parameters32    ----------33    inst : instance of Raw, Epochs, or Evoked34        The data instance.35    mapping : dict36        A dictionary mapping a channel to a list/tuple including37        channel type, unit, eye, [and x/y component] (all as str),  e.g.,38        ``{'l_x': ('eyegaze', 'deg', 'left', 'x')}`` or39        ``{'r_pupil': ('pupil', 'au', 'right')}``.40 41    Returns42    -------43    inst : instance of Raw | Epochs | Evoked44        The instance, modified in place.45 46    Notes47    -----48    ``inst.set_channel_types()`` to ``'eyegaze'`` or ``'pupil'``49    works as well, but cannot correctly set unit, eye and x/y component.50 51    Data will be stored in SI units:52    if your data comes in ``deg`` (visual angle) it will be converted to53    ``rad``, if it is in ``mm`` it will be converted to ``m``.54    """55    ch_names = inst.info["ch_names"]56 57    # allowed58    valid_types = ["eyegaze", "pupil"]  # ch_type59    valid_units = {60        "px": ["px", "pixel"],61        "rad": ["rad", "radian", "radians"],62        "deg": ["deg", "degree", "degrees"],63        "m": ["m", "meter", "meters"],64        "mm": ["mm", "millimeter", "millimeters"],65        "au": [None, "none", "au", "arbitrary"],66    }67    valid_units["all"] = [item for sublist in valid_units.values() for item in sublist]68    valid_eye = {"l": ["left", "l"], "r": ["right", "r"]}69    valid_eye["all"] = [item for sublist in valid_eye.values() for item in sublist]70    valid_xy = {"x": ["x", "h", "horizontal"], "y": ["y", "v", "vertical"]}71    valid_xy["all"] = [item for sublist in valid_xy.values() for item in sublist]72 73    # loop over channels74    for ch_name, ch_desc in mapping.items():75        if ch_name not in ch_names:76            raise ValueError(f"This channel name ({ch_name}) doesn't exist in info.")77        c_ind = ch_names.index(ch_name)78 79        # set ch_type and unit80        ch_type = ch_desc[0].lower()81        if ch_type not in valid_types:82            raise ValueError(83                f"ch_type must be one of {valid_types}. Got '{ch_type}' instead."84            )85        if ch_type == "eyegaze":86            coil_type = FIFF.FIFFV_COIL_EYETRACK_POS87        elif ch_type == "pupil":88            coil_type = FIFF.FIFFV_COIL_EYETRACK_PUPIL89        inst.info["chs"][c_ind]["coil_type"] = coil_type90        inst.info["chs"][c_ind]["kind"] = FIFF.FIFFV_EYETRACK_CH91 92        ch_unit = None if (ch_desc[1] is None) else ch_desc[1].lower()93        if ch_unit not in valid_units["all"]:94            raise ValueError(95                "unit must be one of {}. Got '{}' instead.".format(96                    valid_units["all"], ch_unit97                )98            )99        if ch_unit in valid_units["px"]:100            unit_new = FIFF.FIFF_UNIT_PX101        elif ch_unit in valid_units["rad"]:102            unit_new = FIFF.FIFF_UNIT_RAD103        elif ch_unit in valid_units["deg"]:  # convert deg to rad (SI)104            inst = inst.apply_function(_convert_deg_to_rad, picks=ch_name)105            unit_new = FIFF.FIFF_UNIT_RAD106        elif ch_unit in valid_units["m"]:107            unit_new = FIFF.FIFF_UNIT_M108        elif ch_unit in valid_units["mm"]:  # convert mm to m (SI)109            inst = inst.apply_function(_convert_mm_to_m, picks=ch_name)110            unit_new = FIFF.FIFF_UNIT_M111        elif ch_unit in valid_units["au"]:112            unit_new = FIFF.FIFF_UNIT_NONE113        inst.info["chs"][c_ind]["unit"] = unit_new114 115        # set eye (and x/y-component)116        loc = np.array(117            [118                np.nan,119                np.nan,120                np.nan,121                np.nan,122                np.nan,123                np.nan,124                np.nan,125                np.nan,126                np.nan,127                np.nan,128                np.nan,129                np.nan,130            ]131        )132 133        ch_eye = ch_desc[2].lower()134        if ch_eye not in valid_eye["all"]:135            raise ValueError(136                "eye must be one of {}. Got '{}' instead.".format(137                    valid_eye["all"], ch_eye138                )139            )140        if ch_eye in valid_eye["l"]:141            loc[3] = -1142        elif ch_eye in valid_eye["r"]:143            loc[3] = 1144 145        if ch_type == "eyegaze":146            ch_xy = ch_desc[3].lower()147            if ch_xy not in valid_xy["all"]:148                raise ValueError(149                    "x/y must be one of {}. Got '{}' instead.".format(150                        valid_xy["all"], ch_xy151                    )152                )153            if ch_xy in valid_xy["x"]:154                loc[4] = -1155            elif ch_xy in valid_xy["y"]:156                loc[4] = 1157 158        inst.info["chs"][c_ind]["loc"] = loc159 160    return inst161 162 163def _convert_mm_to_m(array):164    return array * 0.001165 166 167def _convert_deg_to_rad(array):168    return array * np.pi / 180.0169 170 171def convert_units(inst, calibration, to="radians"):172    """Convert Eyegaze data from pixels to radians of visual angle or vice versa.173 174    .. warning::175        Currently, depending on the units (pixels or radians), eyegaze channels may not176        be reported correctly in visualization functions like :meth:`mne.io.Raw.plot`.177        They will be shown  correctly in :func:`mne.viz.eyetracking.plot_gaze`.178        See :gh:`11879` for more information.179 180    .. Important::181       There are important considerations to keep in mind when using this function,182       see the Notes section below.183 184    Parameters185    ----------186    inst : instance of Raw, Epochs, or Evoked187        The Raw, Epochs, or Evoked instance with eyegaze channels.188    calibration : Calibration189        Instance of  Calibration, containing information about the screen size190        (in meters), viewing distance (in meters), and the screen resolution191        (in pixels).192    to : str193        Must be either ``"radians"`` or ``"pixels"``, indicating the desired unit.194 195    Returns196    -------197    inst : instance of Raw | Epochs | Evoked198        The Raw, Epochs, or Evoked instance, modified in place.199 200    Notes201    -----202    There are at least two important considerations to keep in mind when using this203    function:204 205    1. Converting between on-screen pixels and visual angle is not a linear206       transformation. If the visual angle subtends less than approximately ``.44``207       radians (``25`` degrees), the conversion could be considered to be approximately208       linear. However, as the visual angle increases, the conversion becomes209       increasingly non-linear. This may lead to unexpected results after converting210       between pixels and visual angle.211 212    * This function assumes that the head is fixed in place and aligned with the center213      of the screen, such that gaze to the center of the screen results in a visual214      angle of ``0`` radians.215 216    .. versionadded:: 1.7217    """218    _validate_type(inst, (BaseRaw, BaseEpochs, Evoked), "inst")219    _validate_type(calibration, Calibration, "calibration")220    _check_option("to", to, ("radians", "pixels"))221    _check_calibration(calibration)222 223    # get screen parameters224    screen_size = calibration["screen_size"]225    screen_resolution = calibration["screen_resolution"]226    dist = calibration["screen_distance"]227 228    # loop through channels and convert units229    converted_chs = []230    for ch_dict in inst.info["chs"]:231        if ch_dict["coil_type"] != FIFF.FIFFV_COIL_EYETRACK_POS:232            continue233        unit = ch_dict["unit"]234        name = ch_dict["ch_name"]235 236        if ch_dict["loc"][4] == -1:  # x-coordinate237            size = screen_size[0]238            res = screen_resolution[0]239        elif ch_dict["loc"][4] == 1:  # y-coordinate240            size = screen_size[1]241            res = screen_resolution[1]242        else:243            raise ValueError(244                f"loc array not set properly for channel '{name}'. Index 4 should"245                f"  be -1 or 1, but got {ch_dict['loc'][4]}"246            )247        # check unit, convert, and set new unit248        if to == "radians":249            if unit != FIFF.FIFF_UNIT_PX:250                raise ValueError(251                    f"Data must be in pixels in order to convert to radians."252                    f" Got {unit} for {name}"253                )254            inst.apply_function(_pix_to_rad, picks=name, size=size, res=res, dist=dist)255            ch_dict["unit"] = FIFF.FIFF_UNIT_RAD256        elif to == "pixels":257            if unit != FIFF.FIFF_UNIT_RAD:258                raise ValueError(259                    f"Data must be in radians in order to convert to pixels."260                    f" Got {unit} for {name}"261                )262            inst.apply_function(_rad_to_pix, picks=name, size=size, res=res, dist=dist)263            ch_dict["unit"] = FIFF.FIFF_UNIT_PX264        converted_chs.append(name)265    if converted_chs:266        logger.info(f"Converted {converted_chs} to {to}.")267        if to == "radians":268            # check if any values are greaater than .44 radians269            # (25 degrees) and warn user270            data = inst.get_data(picks=converted_chs)271            if np.any(np.abs(data) > 0.52):272                warn(273                    "Some visual angle values subtend greater than .52 radians "274                    "(30 degrees), meaning that the conversion between pixels "275                    "and visual angle may be very non-linear. Take caution when "276                    "interpreting these values. Max visual angle value in data:"277                    f" {np.nanmax(data):0.2f} radians.",278                    UserWarning,279                )280    else:281        warn("Could not find any eyegaze channels. Doing nothing.", UserWarning)282    return inst283 284 285def _pix_to_rad(data, size, res, dist):286    """Convert pixel coordinates to radians of visual angle.287 288    Parameters289    ----------290    data : array-like, shape (n_samples,)291        A vector of pixel coordinates.292    size : float293        The width or height of the screen, in meters.294    res : int295        The screen resolution in pixels, along the x or y axis.296    dist : float297        The viewing distance from the screen, in meters.298 299    Returns300    -------301    rad : ndarray, shape (n_samples)302        the data in radians.303    """304    # Center the data so that 0 radians will be the center of the screen305    data -= res / 2306    # How many meters is the pixel width or height307    px_size = size / res308    # Convert to radians309    return np.arctan((data * px_size) / dist)310 311 312def _rad_to_pix(data, size, res, dist):313    """Convert radians of visual angle to pixel coordinates.314 315    See the parameters section of _pix_to_rad for more information.316 317    Returns318    -------319    pix : ndarray, shape (n_samples)320        the data in pixels.321    """322    # How many meters is the pixel width or height323    px_size = size / res324    # 1. calculate length of opposite side of triangle (in meters)325    # 2. convert meters to pixel coordinates326    # 3. add half of screen resolution to uncenter the pixel data (0,0 is top left)327    return np.tan(data) * dist / px_size + res / 2328 
Aluode/PerceptionLabPortable · CoolFace