pupil_eye_offset#
- pypillometry.signal.pupil_eye_offset(left, right, fs, left_mask=None, right_mask=None, lowpass_cutoff=0.2, max_iter=10)[source]#
Compute a slowly-varying offset between left and right pupil signals.
This function calculates the local inter-ocular difference (left - right) to account for drift over an experimental session. The offset is computed where both eyes have valid data, and lowpass filtered using an iterative method that properly handles missing values (NaN gaps).
- Parameters:
left (np.ndarray or np.ma.MaskedArray) – Left eye pupil signal. Can be: - A regular numpy array (requires left_mask) - A numpy masked array (mask is extracted automatically)
right (np.ndarray or np.ma.MaskedArray) – Right eye pupil signal. Same format options as left.
fs (float) – Sampling rate in Hz.
left_mask (np.ndarray, optional) – Boolean mask for left eye where True = invalid/masked. Required if left is a regular numpy array. Ignored if left is a masked array.
right_mask (np.ndarray, optional) – Boolean mask for right eye where True = invalid/masked. Required if right is a regular numpy array. Ignored if right is a masked array.
lowpass_cutoff (float, optional) – Cutoff frequency for lowpass filtering the offset (default: 0.2 Hz). Lower values produce a more stable offset that only tracks very slow drift. Higher values allow faster changes but may introduce noise.
max_iter (int, optional) – Maximum iterations for iterative lowpass filtering (default: 10). The iterative filter fills NaN gaps by repeatedly filtering and replacing NaN values until convergence.
- Returns:
offset (np.ndarray) – The local offset signal (left - right), smoothly lowpass filtered. Can be added to right eye data to align it with left eye level: right_corrected = right + offset.
See also
lowpass_filter_iterativeLowpass filter that handles NaN values
Notes
The offset is computed as follows: 1. Calculate raw difference (left - right) where both eyes are valid 2. Apply iterative lowpass filter that handles NaN gaps properly
The iterative filtering approach (see lowpass_filter_iterative) ensures that only actual measurements influence the result, not interpolated values. NaN gaps are filled with values consistent with the surrounding signal through an iterative process that converges to a smooth solution.
This handles session-long drift by allowing the offset to vary slowly over time, rather than assuming a constant inter-ocular difference.
For typical pupillometry experiments, a lowpass cutoff of 0.1-0.5 Hz is recommended. This corresponds to changes over 2-10 seconds, which captures drift while ignoring trial-by-trial noise.
Examples
>>> rng = np.random.default_rng(0) >>> left = 4000 + 100 * np.sin(np.arange(5000) / 500) + rng.normal(0, 10, 5000) >>> right = left - 200 + rng.normal(0, 10, 5000) >>> left_mask = np.zeros(5000, dtype=bool) >>> left_mask[1000:1100] = True >>> right_mask = np.zeros(5000, dtype=bool) >>> right_mask[3000:3100] = True >>> left_ma = np.ma.array(left, mask=left_mask) >>> right_ma = np.ma.array(right, mask=right_mask) >>> # With masked arrays >>> offset = pupil_eye_offset(left_ma, right_ma, fs=500) >>> right_corrected = right_ma + offset >>> round(float(np.mean(offset)), -1) 200.0
>>> # With explicit masks >>> offset = pupil_eye_offset(left, right, fs=500, ... left_mask=left_mask, right_mask=right_mask)
>>> # Merge eyes using the offset >>> both_valid = ~left_mask & ~right_mask >>> merged = np.where(both_valid, ... (left + right + offset) / 2, ... np.where(~left_mask, left, right + offset))