CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_pupillometry.py122 linesDownload Raw Back to eyetracking
1# Authors: The MNE-Python contributors.2# License: BSD-3-Clause3# Copyright the MNE-Python contributors.4 5import numpy as np6 7from ..._fiff.constants import FIFF8from ...annotations import _annotations_starts_stops9from ...io import BaseRaw10from ...utils import _check_preload, _validate_type, logger, warn11 12 13def interpolate_blinks(raw, buffer=0.05, match="BAD_blink", interpolate_gaze=False):14    """Interpolate eyetracking signals during blinks.15 16    This function uses the timing of blink annotations to estimate missing17    data. Missing values are then interpolated linearly. Operates in place.18 19    Parameters20    ----------21    raw : instance of Raw22        The raw data with at least one ``'pupil'`` or ``'eyegaze'`` channel.23    buffer : float | array-like of float, shape ``(2,))``24        The time in seconds before and after a blink to consider invalid and25        include in the segment to be interpolated over. Default is ``0.05`` seconds26        (50 ms). If array-like, the first element is the time before the blink and the27        second element is the time after the blink to consider invalid, for example,28        ``(0.025, .1)``.29    match : str | list of str30        The description of annotations to interpolate over. If a list, the data within31        all annotations that match any of the strings in the list will be interpolated32        over. If a ``match`` starts with ``'BAD_'``, that part will be removed from the33        annotation description after interpolation. Defaults to ``'BAD_blink'``.34    interpolate_gaze : bool35        If False, only apply interpolation to ``'pupil channels'``. If True, interpolate36        over ``'eyegaze'`` channels as well. Defaults to False, because eye position can37        change in unpredictable ways during blinks.38 39    Returns40    -------41    self : instance of Raw42        Returns the modified instance.43 44    Notes45    -----46    .. versionadded:: 1.547    """48    _check_preload(raw, "interpolate_blinks")49    _validate_type(raw, BaseRaw, "raw")50    _validate_type(buffer, (float, tuple, list, np.ndarray), "buffer")51    _validate_type(match, (str, tuple, list, np.ndarray), "match")52 53    # determine the buffer around blinks to include in the interpolation54    buffer = np.array(buffer, dtype=float)55    if buffer.size == 1:56        buffer = np.array([buffer, buffer])57 58    if isinstance(match, str):59        match = [match]60 61    # get the blink annotations62    blink_annots = [annot for annot in raw.annotations if annot["description"] in match]63    if not blink_annots:64        warn(f"No annotations matching {match} found. Aborting.")65        return raw66    _interpolate_blinks(raw, buffer, blink_annots, interpolate_gaze=interpolate_gaze)67 68    # remove bad from the annotation description69    for desc in match:70        if desc.startswith("BAD_"):71            logger.info(f"Removing 'BAD_' from {desc}.")72            raw.annotations.rename({desc: desc.replace("BAD_", "")})73    return raw74 75 76def _interpolate_blinks(raw, buffer, blink_annots, interpolate_gaze):77    """Interpolate eyetracking signals during blinks in-place."""78    logger.info("Interpolating missing data during blinks...")79    pre_buffer, post_buffer = buffer80    # iterate over each eyetrack channel and interpolate the blinks81    interpolated_chs = []82    for ci, ch_info in enumerate(raw.info["chs"]):83        if interpolate_gaze:  # interpolate over all eyetrack channels84            if ch_info["kind"] != FIFF.FIFFV_EYETRACK_CH:85                continue86        else:  # interpolate over pupil channels only87            if ch_info["coil_type"] != FIFF.FIFFV_COIL_EYETRACK_PUPIL:88                continue89        # Create an empty boolean mask90        mask = np.zeros_like(raw.times, dtype=bool)91        starts, ends = _annotations_starts_stops(raw, "BAD_blink")92        starts = np.divide(starts, raw.info["sfreq"])93        ends = np.divide(ends, raw.info["sfreq"])94        for annot, start, end in zip(blink_annots, starts, ends):95            if "ch_names" not in annot or not annot["ch_names"]:96                msg = f"Blink annotation missing values for 'ch_names' key: {annot}"97                raise ValueError(msg)98            start -= pre_buffer99            end += post_buffer100            if ch_info["ch_name"] not in annot["ch_names"]:101                continue  # skip if the channel is not in the blink annotation102            # Update the mask for times within the current blink period103            mask |= (raw.times >= start) & (raw.times <= end)104        blink_indices = np.where(mask)[0]105        non_blink_indices = np.where(~mask)[0]106 107        # Linear interpolation108        interpolated_samples = np.interp(109            raw.times[blink_indices],110            raw.times[non_blink_indices],111            raw._data[ci, non_blink_indices],112        )113        # Replace the samples at the blink_indices with the interpolated values114        raw._data[ci, blink_indices] = interpolated_samples115        interpolated_chs.append(ch_info["ch_name"])116    if interpolated_chs:117        logger.info(118            f"Interpolated {len(interpolated_chs)} channels: {interpolated_chs}"119        )120    else:121        warn("No channels were interpolated.")122 
Aluode/PerceptionLabPortable · CoolFace