I demonstrate how to analyze pupil data from a GazePoint tracker with my eye-tracking R package gazeR.
Published
April 21, 2021
In this vignette I am going to show you how to read in a GazePoint data file along with some behavioral data and use gazeR to preprocess the data.
Special thanks to Matthew K Robinson (Twitter:@matthewkrobinson) for letting me use some data from an auditory oddball task he conducted on himself (we do what we have to do as researchers :D): see Tweet below.
subject trial tone rt response
<int> <int> <char> <int> <char>
1: 13 1 lo 2113 None
2: 13 2 lo 2102 None
3: 13 3 lo 2107 None
4: 13 4 lo 2108 None
5: 13 5 lo 2107 None
6: 13 6 lo 2103 None
What we are going to do is run the GazePoint file through the merge_gazepoint function. The function below takes a list of files called file_list and merges all the files together, appends a subject column, creates a trial column using the USER column (GazePoint only allows messages through this channel), creates a time variable (in milliseconds). In the merge_gazepoint function the trail_msg argument requires users to denote a message used in the USER column that references the start of the trial–in our case the START message denotes the start of a new trial. This is a solution by Matt Robinson, but there are other ways one could extract the trial number. What I have done in the past is append a message with the trial iteration (e.g., START_1) in Python and use the separate function to get the trial number.
# A "monocular mean" averages both eyes together. If data is available in just# one eye, use the available value as the mean, unless we need_both is TRUE.#' @param x1 pupil left#' @param x2 pupil right#' @return vector with monocular mean valuescompute_monocular_mean <-function(x1, x2) { xm <-rowMeans(cbind(x1, x2), na.rm =TRUE)# NaN => NAifelse(is.nan(xm), NA, xm)}# function for processing GazePoint datamerge_gazepoint <-function (file_list, trial_msg ="START"){#file list is path to .xls files#vroom is fasterlibrary(data.table) file_ids=str_replace_all(basename(file_list),"([:alpha:]|[:punct:])","") # remove everything but numeric values data <-map2(file_list, file_ids, ~fread(.x) %>%mutate(id = .y)) %>%bind_rows() d = data %>% dplyr::rowwise() %>% dplyr::mutate(pupil=compute_monocular_mean(RPMM, LPMM)) %>%# average both eyes dplyr::ungroup() %>% dplyr::mutate(pupil =ifelse(RPMMV ==0|LPMMV ==0, 0, pupil), #missing data labeled as blinksnew_trial =ifelse(USER == trial_msg &lag(USER) != trial_msg, 1, 0), # Label new trialstrial =cumsum(new_trial), # Create a trial variabletime =floor(TIME*1000)) %>%group_by(trial) %>% dplyr::mutate(time=time -min(time)) %>%ungroup() %>% dplyr::select(id, time,trial,pupil,BPOGX, BPOGY, USER) %>% dplyr::rename("message"="USER", "subject"="id", "x"="BPOGX", "y"="BPOGY") %>% dplyr::filter(trial >0)return(d)}
# A tibble: 73,724 × 10
subject trial tone rt response time pupil x y message
<dbl> <dbl> <chr> <int> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
1 13 1 lo 2113 None 0 4.80 -4.45 -14.1 START
2 13 1 lo 2113 None 16 4.78 -4.45 -14.1 START
3 13 1 lo 2113 None 32 4.79 -4.32 -13.7 START
4 13 1 lo 2113 None 48 4.80 -4.58 -14.5 START
5 13 1 lo 2113 None 64 4.80 -4.58 -14.5 START
6 13 1 lo 2113 None 81 4.79 -4.63 -14.7 START
7 13 1 lo 2113 None 97 4.81 -4.42 -14.0 START
8 13 1 lo 2113 None 113 4.80 -4.42 -14.0 TONE
9 13 1 lo 2113 None 129 4.78 -4.23 -13.5 TONE
10 13 1 lo 2113 None 145 4.77 -4.34 -13.8 TONE
# ℹ 73,714 more rows
Blinks
Finding Blinks
The GazePoint data does not indicate where blinks occurred. What we are going to do is use the blink_detect function in gazer. This relies on the saccades package (https://github.com/tmalsburg/saccades) which uses a velocity based measure based on X,Y coordinates to find blinks. Once we find the blinks we can change the pupil size at that time point as NA and interpolate over it.
As a note, the GazePoint does not seem to sample consistently. In this case, it samples every 16 or 17 ms. This is a problem for some other blink detection measures (e.g., the noise based pupil function). One soultion would be to downsample the data at the onset so there is consistancy from sample to sample.
blinks_merge<-blink_detect(pdb) blinks <- blinks_merge %>% dplyr::group_by(grp =cumsum(!is.na(startend))) %>% dplyr::mutate(Label =replace(startend, first(startend) =='start', 'start')) %>%#extends the start message forward until end message dplyr::ungroup() %>%# label blinks as 1 dplyr::select(subject, trial, time, x, y, pupil, message, tone, Label, -grp) blinks_data <- blinks %>% dplyr::mutate(blink=ifelse(!is.na(Label), 1, 0), pupil=ifelse(blink==1| pupil==0, NA, pupil))%>% dplyr::ungroup()%>% dplyr::select(subject, time, trial, pupil, x, y, trial, message, tone, blink, -Label)