Skip to content

Commit d8fda76

Browse files
authored
Fix interpolate_blinks ignoring non-BAD_blink match descriptions (#13880) (#13940)
1 parent fb90a35 commit d8fda76

4 files changed

Lines changed: 55 additions & 6 deletions

File tree

doc/changes/dev/13940.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix :func:`mne.preprocessing.eyetracking.interpolate_blinks` so that all annotation descriptions passed via ``match`` are interpolated over, rather than only ``BAD_blink`` ones, by :newcontrib:`Vincent Gao`.

doc/changes/names.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@
365365
.. _Velu Prabhakar Kumaravel: https://github.com/vpKumaravel
366366
.. _Victor Ferat: https://github.com/vferat
367367
.. _Victoria Peterson: https://github.com/vpeterson
368+
.. _Vincent Gao: https://github.com/gaoflow
368369
.. _Wei Xu: https://github.com/psyxw
369370
.. _Will Turner: https://bootstrapbill.github.io
370371
.. _Wouter Kroot: https://github.com/WouterKroot

mne/preprocessing/eyetracking/_pupillometry.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ def interpolate_blinks(raw, buffer=0.05, match="BAD_blink", interpolate_gaze=Fal
6363
if not blink_annots:
6464
warn(f"No annotations matching {match} found. Aborting.")
6565
return raw
66-
_interpolate_blinks(raw, buffer, blink_annots, interpolate_gaze=interpolate_gaze)
66+
_interpolate_blinks(
67+
raw, buffer, blink_annots, interpolate_gaze=interpolate_gaze, match=match
68+
)
6769

6870
# remove bad from the annotation description
6971
for desc in match:
@@ -73,10 +75,17 @@ def interpolate_blinks(raw, buffer=0.05, match="BAD_blink", interpolate_gaze=Fal
7375
return raw
7476

7577

76-
def _interpolate_blinks(raw, buffer, blink_annots, interpolate_gaze):
78+
def _interpolate_blinks(raw, buffer, blink_annots, interpolate_gaze, match):
7779
"""Interpolate eyetracking signals during blinks in-place."""
7880
logger.info("Interpolating missing data during blinks...")
7981
pre_buffer, post_buffer = buffer
82+
# Derive the start/stop time (in seconds) of every matched annotation. Passing
83+
# ``match`` (rather than re-querying a hardcoded ``"BAD_blink"``) ensures all
84+
# descriptions in ``match`` are interpolated over and that the start/stop times
85+
# stay aligned with ``blink_annots`` in the loop below.
86+
starts, ends = _annotations_starts_stops(raw, match)
87+
starts = starts / raw.info["sfreq"]
88+
ends = ends / raw.info["sfreq"]
8089
# iterate over each eyetrack channel and interpolate the blinks
8190
interpolated_chs = []
8291
for ci, ch_info in enumerate(raw.info["chs"]):
@@ -88,9 +97,6 @@ def _interpolate_blinks(raw, buffer, blink_annots, interpolate_gaze):
8897
continue
8998
# Create an empty boolean mask
9099
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"])
94100
for annot, start, end in zip(blink_annots, starts, ends):
95101
if "ch_names" not in annot or not annot["ch_names"]:
96102
msg = f"Blink annotation missing values for 'ch_names' key: {annot}"

mne/preprocessing/eyetracking/tests/test_pupillometry.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import numpy as np
66
import pytest
77

8-
from mne import create_info
8+
from mne import Annotations, create_info
99
from mne.annotations import _annotations_starts_stops
1010
from mne.datasets.testing import data_path, requires_testing_data
1111
from mne.io import RawArray, read_raw_eyelink
@@ -71,3 +71,44 @@ def test_interpolate_blinks(buffer, match, cause_error, interpolate_gaze, crop):
7171
if interpolate_gaze:
7272
assert not np.isnan(data[0, blink_ind]).any() # left eye
7373
assert not np.isnan(data[1, blink_ind]).any() # right eye
74+
75+
76+
def _pupil_raw_with_gaps():
77+
"""Synthetic pupil Raw with two missing segments at known sample ranges."""
78+
info = create_info(["pupil_left", "pupil_right"], 100.0, ["pupil", "pupil"])
79+
data = np.ones((2, 1000)) * 5.0
80+
data[:, 100:150] = 0.0 # a blink
81+
data[:, 600:650] = 0.0 # a non-blink gap (e.g. NaN/dropout)
82+
raw = RawArray(data, info)
83+
raw.set_annotations(
84+
Annotations(
85+
onset=[1.0, 6.0],
86+
duration=[0.5, 0.5],
87+
description=["BAD_blink", "BAD_NAN"],
88+
ch_names=[("pupil_left", "pupil_right"), ("pupil_left", "pupil_right")],
89+
)
90+
)
91+
return raw
92+
93+
94+
def test_interpolate_blinks_respects_match():
95+
"""All descriptions in ``match`` are interpolated, not just ``BAD_blink``.
96+
97+
Regression test for #13880: ``interpolate_blinks`` exposes a ``match``
98+
parameter, but the segment start/stop times were derived from a hardcoded
99+
``"BAD_blink"`` query, so any other matched annotation (e.g. ``BAD_NAN``
100+
from :func:`mne.preprocessing.annotate_nan`) was silently ignored.
101+
"""
102+
# default match="BAD_blink": only the blink gap is filled, the other remains
103+
raw = _pupil_raw_with_gaps()
104+
interpolate_blinks(raw, buffer=(0.0, 0.0))
105+
data = raw.get_data()
106+
assert not np.any(data[:, 100:150] == 0.0) # blink interpolated
107+
assert np.all(data[:, 600:650] == 0.0) # unmatched gap untouched
108+
109+
# match both descriptions: both gaps are interpolated
110+
raw = _pupil_raw_with_gaps()
111+
interpolate_blinks(raw, buffer=(0.0, 0.0), match=["BAD_blink", "BAD_NAN"])
112+
data = raw.get_data()
113+
assert not np.any(data[:, 100:150] == 0.0)
114+
assert not np.any(data[:, 600:650] == 0.0)

0 commit comments

Comments
 (0)