Regions of interest#

A region of interest (ROI) is a part of the screen, e.g., where a stimulus was shown. With ROIs, the gaze position becomes a question that can be answered per sample or per trial: did the participant look at the stimulus, when, and for how long? This page explains how ROIs are defined and how they are used with continuous data and with segments around events. The tutorial Gaze in regions of interest around events shows a complete analysis.

The examples use the short example dataset, which has the gaze of both eyes (in screen pixels) and an experimental setup:

import numpy as np
import pandas as pd
import pypillometry as pp
from pypillometry import u
d = pp.get_example_data("rlmw_002_short")

Defining ROIs#

CircularROI (center and radius) and RectangularROI (two corners) are stored in pixels, like the gaze: x from left to right, y from top to bottom. from_degrees() and from_mm() create them from degrees of visual angle or mm relative to the screen center (y up); this needs the geometry of the setup (see Gaze data and the experimental setup):

setup = d.experimental_setup
left = pp.CircularROI.from_degrees(("-8 deg", "0 deg"), "3 deg", setup=setup,
                                   name="leftopt")
right = pp.CircularROI.from_degrees(("8 deg", "0 deg"), "3 deg", setup=setup,
                                    name="rightopt")
target = pp.RectangularROI((540, 412), (740, 612), name="target")   # pixels

The name is used for the channels and table columns of the analyses below (e.g., left_leftopt); it is needed there, or can be given as name=.

CircularROI

Circular Region of Interest defined by center point and radius.

RectangularROI

Rectangular Region of Interest defined by two corners.

ROIs in continuous data#

ROI.contains() tests an array of (x, y) positions; the gaze plots draw ROIs with rois=:

gaze = np.column_stack([d["left_x"], d["left_y"]])
in_left = left.contains(gaze)                    # one bool per sample
d.plot.plot_heatmap(eyes="left", rois=[left, right])

The boolean array can become Intervals (the visits to the ROI, with Intervals.from_mask()) or a channel of the data (see Experimental setup and regions of interest).

The heatmap of the left eye with the two ROIs (the options of the task, 8° to the left and right of the center):

d = pp.get_example_data("rlmw_002_short")
setup = d.experimental_setup
left = pp.CircularROI.from_degrees(("-8 deg", "0 deg"), "3 deg", setup=setup, name="leftopt")
right = pp.CircularROI.from_degrees(("8 deg", "0 deg"), "3 deg", setup=setup, name="rightopt")
d.plot.plot_heatmap(eyes="left", rois=[left, right])
../_images/rois-1.png

ROIs around events#

For event-related questions, cut the gaze into segments around the events with get_segments() (see Events, intervals and segments). The segments keep x and y of each eye (and the pupil) with the same epochs, and the namespace seg.gaze analyses them with ROIs:

segmented.SegmentedGazeNamespace.roi_hits

Add channels that say where the gaze is in a region of interest.

segmented.SegmentedGazeNamespace.plot_roi

Plot the share of trials in which the gaze is in a region of interest over time (fixation curve), with an error ribbon.

segmented.SegmentedGazeNamespace.dwell_time

Time the gaze spends in a region of interest in each trial (ms).

segmented.SegmentedGazeNamespace.first_entry

Time of the first usable sample in a region of interest in each trial (ms).

epochs = d.get_epochs("S_", (-500, 3000) * u.ms)
seg = d.get_segments(epochs)                     # pupil, x and y of both eyes

Where the gaze is: ROI channels#

roi_hits() adds a channel for each eye, <eye>_<ROI name>: 1 where the gaze is in the ROI, 0 where it is outside, masked where x or y are masked (e.g., during blinks or outside the screen, see Missing data):

seg.gaze.roi_hits(left)
seg.gaze.roi_hits(right)
seg.channels        # [..., 'left_leftopt', 'right_leftopt', 'left_rightopt', 'right_rightopt']

ROI channels are channels like the others: they are selected, saved and averaged with the segments. Their mean over the trials at a time point is the share of the trials in which the gaze is in the ROI (among the trials whose gaze is usable at that time point). Over time, this is a fixation curve; plot_roi() plots it with its standard error, one line per eye:

seg.gaze.plot_roi("leftopt")         # from the channels added above
seg.gaze.plot_roi(target)            # or directly from an ROI (no channel is added)

New curves can be computed from ROI channels and added as channels, e.g., the gaze on the option that was chosen in each trial (with a metadata column side of the epochs). Use np.ma.where, which keeps the masks (np.where drops them):

chose_left = seg.epochs.meta["side"] == 1
seg["left_chosen"] = np.ma.where(chose_left, seg["left_leftopt"], seg["left_rightopt"])
seg.plot("left_chosen")

Measures per trial#

dwell_time() gives the time (ms) the gaze is in the ROI in each trial, counting only usable samples; first_entry() the time (ms, relative to the event) of the first usable sample in the ROI, from the event onset on by default. Trials without usable samples, or that never enter the ROI, get NaN. Both take an ROI or the name of ROI channels, a time window and eyes, and return a table with the onsets, labels and metadata of the epochs and one column per eye (<eye>_<ROI name>):

dwell = pd.DataFrame(seg.gaze.dwell_time(left, window=(0, 1000)))
first = pd.DataFrame(seg.gaze.first_entry("rightopt", eyes="left"))

These tables can be used directly for statistical models of the trials, like the results of stat_per_event().

Selecting trials for gaze and pupil#

All channels of the segments share their trials, so a selection made with the gaze also applies to the pupil, e.g., the pupil response in the trials in which the participant looked at the left option within the first second:

looked = seg.gaze.dwell_time(left, eyes="left", window=(0, 1000))["left_leftopt"] > 0
erpd = seg.keep(looked).pupil.baseline_correct((-200, 0))
erpd.pupil.plot()

filter() selects by the labels and metadata of the epochs (seg.filter(side=1)).

Several participants#

group_segments() averages the segments of each participant (see Events, intervals and segments), so the fixation curve of the group is the mean of the participants’ curves (with their standard error). Choose the channels to combine with channels=:

group = pp.group_segments({"P01": seg1, "P02": seg2},
                          channels=["left_leftopt", "left_rightopt"])
group.gaze.plot_roi("leftopt")

ROI channels must be added before averaging: averaged x and y say nothing about where the gaze was in the trials, so roi_hits() and first_entry() raise an error for averages. dwell_time() of an average is the mean dwell time of its trials.

What is not included#

  • ROIs that move or change during a trial: the ROI is the same at all time points of the segments. Several ROIs (one per phase or per stimulus position) and the trial metadata can be combined as shown above for the chosen option.

  • Fixation detection: the ROI channels use the gaze samples, not fixations.

  • Heatmaps per condition of the segments; use EyeData.plot.plot_heatmap on the continuous data (with plot_range) for now.