Aluode/PerceptionLabPortable
0
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 