The gazeR package relied on the edfR package for data from SR Research Eyetrackers. The edfR package, however, is only available for Linux and Mac systems, and even there has been running into problems.
Therefore, the current functions emulate the workings of these functions using the *.asc files that can be generated from edf files using either the visual EDF2ASC tool that is distributed with each SR tracker or the command line version of EDF2ASC.
These functions handle both visual-world-paradigm (gaze) and pupillometry data, and support monocular as well as binocular recordings (see the binocular option below).
New in gazeR 0.2.5: read_asc() reads a
single .asc file straight into a data frame, with messages,
trial variables and blink flags already attached. It is much faster than
the parse_asc() workflow below and needs no
folder-per-participant setup, so it is now the recommended way to read
.asc files. See Reading ASC files with
read_asc() below.
It is critical that the EDF conversion puts out both events and samples!!
If the user copies the typical structure of an SR projects, the individual participants’ data files be organized in their respective directories, and this model is followed here.
The first function generates a batch file that allows the one-click generation of the asc files using the command line conversion tool in Windows. For this to work you must download the edf2asc tool from SR–this is free. It requires a main directory that contains all participants directories and a list of those directories. The user can then navigate with the command prompt to the output directory (provided in the output) and run the batch file.
This conversion will take quite long then!
read_asc() reads one .asc file (gzipped or
not) and returns one row per sample. gazeR includes an example: the
first 6 trials of one participant (PN010) from a Black-and-White Switch
task in the public OSF project Dynamics
of the Pupillary Consensual Response, recorded binocularly at 1000
Hz.
library(gazer)
library(dplyr)
asc_file <- system.file("extdata", "PN010BWS.asc.gz", package = "gazer")
pn010 <- read_asc(asc_file, eye = "left")
pn010
#> # A tibble: 34,390 × 19
#> subject trial time pupil x y blink message tracker_time
#> <chr> <int> <dbl> <dbl> <dbl> <dbl> <int> <chr> <dbl>
#> 1 PN010BWS 1 0 1657 385. 331. 0 <NA> 178884819
#> 2 PN010BWS 1 1 1657 385. 330 0 <NA> 178884820
#> 3 PN010BWS 1 2 1658 385. 329. 0 <NA> 178884821
#> 4 PN010BWS 1 3 1659 385. 329. 0 <NA> 178884822
#> 5 PN010BWS 1 4 1658 386 329. 0 <NA> 178884823
#> 6 PN010BWS 1 5 1657 387. 329. 0 <NA> 178884824
#> 7 PN010BWS 1 6 1656 387. 329 0 <NA> 178884825
#> 8 PN010BWS 1 7 1659 386. 329. 0 <NA> 178884826
#> 9 PN010BWS 1 8 1662 386. 329. 0 <NA> 178884827
#> 10 PN010BWS 1 9 1663 386. 330. 0 <NA> 178884828
#> # ℹ 34,380 more rows
#> # ℹ 10 more variables: LONG_JITTER_1 <int>, SHORT_JITTER_1 <int>,
#> # Session_Name <chr>, Trial_Index <int>, Trial_Recycled <chr>,
#> # black2white <int>, block <chr>, color <chr>, screen <chr>,
#> # white2black <int>The columns are:
subject: the file name without extension (or set
subject =)trial: a trial runs from each TRIALID
message to its TRIAL_RESULT messagetime: ms from the start of recording in that trial (the
!MODE RECORD message), as in parse_asc()pupil, x, y: NA
when the tracker lost the eye (instead of 0 and
1e8)blink: 1 during a blink, from the tracker’s
SBLINK/EBLINK eventsmessage: event messages, each attached to the first
sample at or after it, with Experiment Builder offsets appliedtracker_time: the original tracker clock!V TRIAL_VAR)The sampling rate, recorded eyes and pupil type are stored with the data:
attr(pn010, "asc_info")
#> $file
#> [1] "/tmp/Rtmp0PRiva/Rinst15d162d3fc1e/gazer/extdata/PN010BWS.asc.gz"
#>
#> $hz
#> [1] 1000
#>
#> $recorded_eyes
#> [1] "both"
#>
#> $eye_used
#> [1] "left"
#>
#> $pupil_type
#> [1] "area"
#>
#> $n_trials
#> [1] 6Trial variables and messages are ready to use, with no separate
find_messages_asc() or merge step:
pn010 |> distinct(trial, block, screen)
#> # A tibble: 6 × 3
#> trial block screen
#> <int> <chr> <chr>
#> 1 1 rest black
#> 2 2 short black
#> 3 3 short white
#> 4 4 short black
#> 5 5 short white
#> 6 6 short black
pn010 |> filter(!is.na(message)) |> select(trial, time, message)
#> # A tibble: 12 × 3
#> trial time message
#> <int> <dbl> <chr>
#> 1 1 41 LONG_TRIAL_START
#> 2 1 9041 LONG_TRIAL_END
#> 3 2 34 SHORT_TRIAL_START
#> 4 2 6034 SHORT_TRIAL_END
#> 5 3 48 SHORT_TRIAL_START
#> 6 3 5048 SHORT_TRIAL_END
#> 7 4 47 SHORT_TRIAL_START
#> 8 4 4047 SHORT_TRIAL_END
#> 9 5 47 SHORT_TRIAL_START
#> 10 5 4048 SHORT_TRIAL_END
#> 11 6 45 SHORT_TRIAL_START
#> 12 6 6046 SHORT_TRIAL_ENDMessages logged before recording starts in each trial
(TRIALID and the tracker’s setup messages) have no sample
to attach to. keep_messages = TRUE returns every message,
with its time relative to the start of recording:
msgs <- attr(read_asc(asc_file, keep_messages = TRUE), "messages")
msgs |> filter(trial == 1, time <= 0)
#> # A tibble: 11 × 4
#> trial time tracker_time text
#> <int> <dbl> <dbl> <chr>
#> 1 1 -17 178884802 TRIALID 0
#> 2 1 -1 178884818 RECCFG CR 1000 2 1 LR
#> 3 1 -1 178884818 ELCLCFG BTABLER
#> 4 1 -1 178884818 GAZE_COORDS 0.00 0.00 1023.00 767.00
#> 5 1 -1 178884818 THRESHOLDS L 98 209 R 115 211
#> 6 1 -1 178884818 ELCL_WINDOW_SIZES 160 160 0 0
#> 7 1 -1 178884818 CAMERA_LENS_FOCAL_LENGTH 38.00
#> 8 1 -1 178884818 PUPIL_DATA_TYPE RAW_AUTOSLIP
#> 9 1 -1 178884818 ELCL_PROC CENTROID (3)
#> 10 1 -1 178884818 ELCL_PCR_PARAM 5 3.0
#> 11 1 0 178884819 !MODE RECORD CR 1000 2 1 LRFor binocular files, eye chooses "left",
"right", "average" (the default), or
"both", which keeps pupil_left,
pupil_right, x_left, …,
blink_left and blink_right alongside the
average.
With "average" and "both":
pupil (and x, y) is the
monocular mean of the two eyes
(compute_monocular_mean()), as EyeLink reports the average
eye: the mean of both eyes, or the value of the one valid eye. It is
NA only when neither eye is valid.blink is 1 if either eye is in a blink
(blink_left | blink_right). If you treat blink samples as
missing, this also removes samples where pupil comes from
the one eye that was still open.If you analyse one eye, use that eye’s columns together,
e.g. pupil_left with blink_left.
read_asc(asc_file, eye = "both") |>
select(trial, time, pupil, pupil_left, pupil_right, blink_left, blink_right) |>
head()
#> # A tibble: 6 × 7
#> trial time pupil pupil_left pupil_right blink_left blink_right
#> <int> <dbl> <dbl> <dbl> <dbl> <int> <int>
#> 1 1 0 1776. 1657 1894 0 0
#> 2 1 1 1776 1657 1895 0 0
#> 3 1 2 1776. 1658 1895 0 0
#> 4 1 3 1776. 1659 1894 0 0
#> 5 1 4 1775 1658 1892 0 0
#> 6 1 5 1774. 1657 1890 0 0Read each file and stack the results. Keep each participant’s sampling rate and check that they match: blink extension, interpolation gaps and filtering all depend on it, so participants recorded at different rates (e.g. 500 vs 1000 Hz) should be processed separately.
files <- list.files("data/asc", pattern = "\\.asc$", full.names = TRUE)
all_data <- lapply(files, function(f) {
d <- read_asc(f, eye = "left")
d$hz <- attr(d, "asc_info")$hz
d
}) |>
bind_rows()
all_data |> distinct(subject, hz)At 500 Hz a sample arrives only every 2 ms, so a trial’s first sample is at 0 or 1 ms after recording starts, roughly half the time each. This is expected.
The output of read_asc() can go straight into the pupil
functions (extend_blinks(),
smooth_interpolate_pupil(),
baseline_correction_pupil_msg(), …); see the pupil
pre-processing vignette. shiny gazeR
provides a point-and-click interface that reads .asc files
with read_asc().
This function is quite analogous to the parse_edf
function in gazeR. As an additional convenience it will generate a list
of the type of messages that are found in the asc file to guide the
extraction of performance and trial-level data (i.e., where is the
target located on the screen, what is the condition of the trial,
etc.)
The parse_asc function requires a directory list as input, and overs the additional option of overwriting missing data from blinks with the last available data point (as in Mitterer and McQueen, 2009, Journal of Experimental Psychology: Human Percpetion and Performance). This option is set by default to false.
Moreover, the function offers an additional parameter to keep the file sizes small (and the subsequent processing faster). The user can opt to not write out scene preview time with the parameter cut preview. If that parameter is set to a time value (in milliseconds), the output will start after the preview time.
Note that the home directory needs to be specified including a final slash
parse_asc(myDirList, homeDir = "C:/Users/JG1890/Documents/gazer/" ,
overwriteBlinks = F, cutPreview = 1500)By default parse_asc returns a single (monocular) eye
with the columns time, trial, ID, subject, pupil, x, y. If
your recording tracked both eyes, set
binocular = TRUE. The output then contains separate
x/y/pupil columns for each eye plus a per-eye blink flag (0/1):
time, trial, ID, subject, x_left, y_left, pupil_left, x_right, y_right, pupil_right, blink_left, blink_right
The blink flags are derived from the
SBLINK/EBLINK events in the asc file and mark,
sample by sample, whether each eye was mid-blink.
parse_asc(myDirList, homeDir = "C:/Users/JG1890/Documents/gazer/",
overwriteBlinks = F, cutPreview = 1500, binocular = TRUE)This function will take quite long to complete. However, it will feed back whenever it has finished processing a trial from a given participants (which may take around 1s, depending on sample size and the machine the program is run on). That is, for a study with 40 participants and 160 trials, this may well take more than 100 minutes.
The function generates three files in the home directory:
Known issue: parse_asc() treats any
line whose first field is a whole number as a sample. Calibration-result
lines in the file header (e.g. 1 1 1 1094138081) match
this, so they end up as fake samples in trial 1. When several
participants are processed in one call, these rows get a large negative
time carried over from the previous participant, and two or
more of them can share the same time. In existing _eye.csv
and _combined.csv files, these are the rows with
time < 0 and can be deleted. read_asc()
only reads lines that start with a timestamp, so it is not affected.
The next step is to extract information about the trials from the messages. This will be specific to each study, but the overview files should help to remind the user of how the experiment was set up.
This will be achieved with the find_messages_asc
function, which is similar to the find_messages_edf
function working on edf files. However, it has a few more options to
facilitate further processing.
It has four parameters:
myDirList containing a list of directories to work
ontime2extract: a list of strings that match message
texts for which only the time should be retained. This is useful when
messages have been sent at critical time points (such as the
onset/offset of a target word) and times need to be normalized in
relation to this time point (analogue to markers generated from the
stimuli-providing machine send to the machine that records the EEG
data). That is, only the time will be retained, but is therefore
immediately available as numbermsg2extract: A list of messages to extract fully based
on the first word of that message (output will be string variables)vars2extrac: A list of Trial variables used by
Experiment Builder that should be extracted (value only, with column
names set by the variable name)myDirList = dir(mainDir,pattern = "^pp01$")
#the three types of information to be extracted
msg2extracts = c("clicked")
time2extracts = c("TargetOnset")
vars2extracts = c("targetpos", "targetw","competpos", "competw","condition", "item","trialtype", "wav")
find_messages_asc(myDirList, homeDir = "C:/Users/JG1890/Documents/gazer/",
time2extract = time2extracts,
msg2extract = msg2extracts,
vars2extract = vars2extracts)The next step is analogous to the merge_gazer_files
function, which merges messages with the eye-tracking data. The function
is called merge_asc_files and simply required one
parameter, the list of directories in which to find files.
For each directory, the function will feedback whether it found the expected files, and also produce a summary of how many merge attempts were succesful or not. In the example below, I purposefully included some entries in the directory list for which things dont work, so that the error handling becomes clear.
The files resulting from this process can be used with the general gazeR function, as explained in other vignettes. The code below how to read in data from multiple participants to process them further using gazeR.
Below is the data ready for analysis in gazeR.