From f54c4a21035c417ce747fd298e322177459409ef Mon Sep 17 00:00:00 2001 From: Kinkini Date: Wed, 25 Mar 2026 07:11:19 -0400 Subject: [PATCH 1/3] create function to extract aoi dwell time --- mne/viz/eyetracking/__init__.py | 2 +- mne/viz/eyetracking/heatmap.py | 58 ++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/mne/viz/eyetracking/__init__.py b/mne/viz/eyetracking/__init__.py index 04bbb1c7216..c3547d0dcf8 100644 --- a/mne/viz/eyetracking/__init__.py +++ b/mne/viz/eyetracking/__init__.py @@ -4,4 +4,4 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -from .heatmap import plot_gaze +from .heatmap import plot_gaze, aoi_dwell_time diff --git a/mne/viz/eyetracking/heatmap.py b/mne/viz/eyetracking/heatmap.py index 46aa34b7d31..c0db7059e27 100644 --- a/mne/viz/eyetracking/heatmap.py +++ b/mne/viz/eyetracking/heatmap.py @@ -11,6 +11,63 @@ @fill_doc +def aoi_dwell_time( + epochs, + *, + xrange=None, + yrange=None, + avg_samples=False, +): + """ + Compute total dwell time within a rectangular area of interest (AOI). + + Parameters + ---------- + epochs : mne.Epochs + Epochs object containing gaze data and sampling frequency. + xrange : tuple of int | None + Minimum and maximum x-coordinates defining the AOI (xmin, xmax). + yrange : tuple of int | None + Minimum and maximum y-coordinates defining the AOI (ymin, ymax). + avg_samples : bool + If True, return the mean dwell time across trials. If False, return + dwell time per trial. + + Returns + ------- + dwell_times : ndarray, shape (n_trials,) | float + Total dwell time in seconds. Returns an array of per-trial dwell times + if `avg_samples=False`, or a single float (mean dwell time across trials) + if `avg_samples=True`. + """ + from mne._fiff.pick import _picks_to_idx + + # Get the gaze data + pos_picks = _picks_to_idx(epochs.info, "eyegaze") + gaze_data = epochs.get_data(picks=pos_picks) + gaze_ch_loc = np.array([epochs.info["chs"][idx]["loc"] for idx in pos_picks]) + x_data = gaze_data[:, np.where(gaze_ch_loc[:, 4] == -1)[0], :] + y_data = gaze_data[:, np.where(gaze_ch_loc[:, 4] == 1)[0], :] + + if x_data.shape[1] > 1: # binocular recording. Average across eyes + logger.info("Detected binocular recording. Averaging positions across eyes.") + x_data = np.nanmean(x_data, axis=1) # shape (n_epochs, n_samples) + y_data = np.nanmean(y_data, axis=1) + + # check if outside the aoi range + aoi_hitboxes = ((x_data >= xrange[0]) & (x_data <= xrange[1]) & + (y_data >= yrange[0]) & (y_data <= yrange[1])).astype(int) + + if avg_samples == False: + # aoi total dwell time per sample + dwell_times = np.sum(np.squeeze(aoi_hitboxes), axis=1) / epochs.info["sfreq"] + else: + dwell_times = np.mean(np.sum(np.squeeze(aoi_hitboxes), axis=1)) / epochs.info["sfreq"] + + return dwell_times + + + def plot_gaze( epochs, *, @@ -153,7 +210,6 @@ def plot_gaze( show=show, ) - def _plot_heatmap_array( data, width, From 0b57e10600a52b897ddc4a32d549257d2f0f1acd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:27:08 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/viz/eyetracking/heatmap.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/mne/viz/eyetracking/heatmap.py b/mne/viz/eyetracking/heatmap.py index c0db7059e27..0abbaa932f5 100644 --- a/mne/viz/eyetracking/heatmap.py +++ b/mne/viz/eyetracking/heatmap.py @@ -55,17 +55,22 @@ def aoi_dwell_time( y_data = np.nanmean(y_data, axis=1) # check if outside the aoi range - aoi_hitboxes = ((x_data >= xrange[0]) & (x_data <= xrange[1]) & - (y_data >= yrange[0]) & (y_data <= yrange[1])).astype(int) + aoi_hitboxes = ( + (x_data >= xrange[0]) + & (x_data <= xrange[1]) + & (y_data >= yrange[0]) + & (y_data <= yrange[1]) + ).astype(int) if avg_samples == False: # aoi total dwell time per sample dwell_times = np.sum(np.squeeze(aoi_hitboxes), axis=1) / epochs.info["sfreq"] else: - dwell_times = np.mean(np.sum(np.squeeze(aoi_hitboxes), axis=1)) / epochs.info["sfreq"] - - return dwell_times + dwell_times = ( + np.mean(np.sum(np.squeeze(aoi_hitboxes), axis=1)) / epochs.info["sfreq"] + ) + return dwell_times def plot_gaze( @@ -210,6 +215,7 @@ def plot_gaze( show=show, ) + def _plot_heatmap_array( data, width, From 5dc3d37bbf9f25f022fe631b0b6e41b988a0e82b Mon Sep 17 00:00:00 2001 From: Kinkini Date: Wed, 25 Mar 2026 07:39:19 -0400 Subject: [PATCH 3/3] clean up with ruff --- mne/viz/eyetracking/heatmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/eyetracking/heatmap.py b/mne/viz/eyetracking/heatmap.py index 0abbaa932f5..790d026bda8 100644 --- a/mne/viz/eyetracking/heatmap.py +++ b/mne/viz/eyetracking/heatmap.py @@ -62,7 +62,7 @@ def aoi_dwell_time( & (y_data <= yrange[1]) ).astype(int) - if avg_samples == False: + if not avg_samples: # aoi total dwell time per sample dwell_times = np.sum(np.squeeze(aoi_hitboxes), axis=1) / epochs.info["sfreq"] else: