We’ll use test data supplied by SR Research (which I found in the cili package for Python). The test data can be found in the extdata/ directory of the package.
require(eyelinker)
require(dplyr)
#Look for file
fpath <- system.file("extdata/mono500.asc.gz",package="eyelinker")
asc files can be gigantic, so it’s a good idea to compress them, R doesn’t mind (here they’re compressed in gzip format, hence the .gz).
To read the file just call read.asc:
dat <- read.asc(fpath)
dat is a list with fields:
names(dat)
## [1] "raw" "msg" "sacc" "fix" "blinks" "info"
Some meta-data can be read from the “SAMPLES” lines in the asc file.
str(dat$info)
## List of 8
## $ velocity : logi FALSE
## $ resolution: logi FALSE
## $ htarg : logi FALSE
## $ input : logi FALSE
## $ left : logi TRUE
## $ right : logi FALSE
## $ cr : logi TRUE
## $ mono : logi TRUE
Here we have a monocular recording of the left eye.
Depending on how the Eyelink is set up, positions can be reported in pixels or degrees, relative to the head, the screen or the camera. I’m guessing the most common case is to use screen coordinates, but I don’t know whether the coordinate system is stored in a predictable manner in asc files. If you have any suggestions please email me. I’ll assume you know what the relevant units are.
The raw data has a simple structure:
raw <- dat$raw
head(raw,3)
## time xp yp ps cr.info block
## 1 7196720 512.8 394.5 1063 ... 1
## 2 7196722 513.3 395.4 1064 ... 1
## 3 7196724 513.9 397.0 1066 ... 1
In a binocular recording the raw data has the following structure:
dat.bi <- system.file("extdata/bino1000.asc.gz",package="eyelinker") %>% read.asc
head(dat.bi$raw,3)
## time xpl ypl psl xpr ypr psr cr.info block
## 1 7427362 502.3 411.1 1103 512.8 395.9 1094 ..... 1
## 2 7427363 500.2 411.7 1103 511.7 395.6 1094 ..... 1
## 3 7427364 498.0 412.3 1104 510.5 394.5 1094 ..... 1
The variables are the same as before, with the addition of a postfix corresponding to the eye (i.e. xpl is the x position of the left eye).
It’s sometimes more convenient for plotting and analysis if the raw data are in “long” rather than “wide” format, as in the following example:
library(tidyr)
##
## Attaching package: 'tidyr'
## The following object is masked from 'package:magrittr':
##
## extract
raw.long <- dplyr::select(raw,time,xp,yp,block) %>% gather("coord","pos",xp,yp)
head(raw.long,2)
## time block coord pos
## 1 7196720 1 xp 512.8
## 2 7196722 1 xp 513.3
tail(raw.long,2)
## time block coord pos
## 3667 7205382 4 yp 364.6
## 3668 7205384 4 yp 364.9
The eye position is now in a single column rather than two, and the column “coord” tells us if the valuye corresponds to the x or y position. The benefits may not be obvious now, but it does make plotting the traces via ggplot2 a lot easier:
require(ggplot2)
## Loading required package: ggplot2
raw.long <- mutate(raw.long,ts=(time-min(time))/1e3) #let's have time in sec.
ggplot(raw.long,aes(ts,pos,col=coord))+geom_point()
In this particular file there are four separate recording periods, corresponding to different “blocks” in the asc file, which we can check using:
ggplot(raw.long,aes(ts,pos,col=coord))+geom_line()+facet_wrap(~ block)
The Eyelink automatically detects saccades in an online fashion. The results are converted to a data.frame:
sac <- dat$sac
head(sac,2)
## stime etime dur sxp syp exp eyp ampl pv eye block
## 1 7197124 7197134 12 513.8 395.9 509.2 380.4 0.46 57 L 1
## 2 7197510 7197546 38 510.8 383.0 735.8 373.2 6.38 313 L 1
Each line corresponds to a saccade, and the different columns are:
In the binocular case, we have:
head(dat.bi$sac,3)
## stime etime dur sxp syp exp eyp ampl pv eye block
## 1 7428104 7428157 54 494.3 401.6 224.1 367.4 7.68 400 L 1
## 2 7428104 7428157 54 508.4 403.5 245.3 389.6 7.43 348 R 1
## 3 7430690 7430726 37 518.7 394.2 805.6 396.3 8.08 442 R 2
The only difference is in the “eye” column, which tells you in which eye the saccade was first recorded.
To see if the saccades have been labelled correctly, we’ll have to find the corresponding time samples in the raw data.
The easiest way to achieve this is to view the detected saccades as a set of temporal intervals, with endpoints given by stime and etime. We’ll use function “%In%” to check if each time point in the raw data can be found in one of these intervals.
Sac <- cbind(sac$stime,sac$etime) #Define a set of intervals with these endpoints
#See also: intervals package
raw <- mutate(raw,saccade=time %In% Sac)
head(raw,3)
## time xp yp ps cr.info block saccade
## 1 7196720 512.8 394.5 1063 ... 1 FALSE
## 2 7196722 513.3 395.4 1064 ... 1 FALSE
## 3 7196724 513.9 397.0 1066 ... 1 FALSE
mean(raw$saccade)*100 #6% of time samples correspond to saccades
## [1] 6.161396
Now each time point labelled with “saccade==TRUE” corresponds to a saccade detected by the eye tracker.
Let’s plot traces again:
mutate(raw.long,saccade=time %In% Sac) %>% filter(block==1) %>% ggplot(aes(ts,pos,group=coord,col=saccade))+geom_line()
Fixations are stored in a very similar way to saccades:
fix <- dat$fix
head(fix,3)
## stime etime dur axp ayp aps eye block
## 1 7196724 7197122 400 515.1 396.3 1050 L 1
## 2 7197136 7197508 374 512.6 384.3 988 L 1
## 3 7197548 7197696 150 734.0 375.8 918 L 1
Each line is a fixation, and the columns are:
We can re-use essentially the same code to label fixations as we did to label saccades:
Fix <- cbind(fix$stime,fix$etime) #Define a set of intervals
mutate(raw.long,fixation=time %In% Fix) %>% filter(block==1) %>% ggplot(aes(ts,pos,group=coord,col=fixation))+geom_line()
We can get a fixation index using whichInterval:
mutate(raw,fix.index=whichInterval(time,Fix)) %>% head(4)
## time xp yp ps cr.info block saccade fix.index
## 1 7196720 512.8 394.5 1063 ... 1 FALSE NA
## 2 7196722 513.3 395.4 1064 ... 1 FALSE NA
## 3 7196724 513.9 397.0 1066 ... 1 FALSE 1
## 4 7196726 513.2 397.6 1064 ... 1 FALSE 1
Let’s check that the average x and y positions are correct:
raw <- mutate(raw,fix.index=whichInterval(time,Fix))
fix.check <- filter(raw,!is.na(fix.index)) %>% group_by(fix.index) %>% summarise(axp=mean(xp),ayp=mean(yp)) %>% ungroup
head(fix.check,3)
## Source: local data frame [3 x 3]
##
## fix.index axp ayp
## (int) (dbl) (dbl)
## 1 1 515.0930 396.2695
## 2 2 512.6283 384.2984
## 3 3 734.0107 375.8333
We grouped all time samples according to fixation index, and computed mean x and y positions.
We verify that we recovered the right values:
all.equal(fix.check$axp,fix$axp)
## [1] "Mean relative difference: 4.48531e-05"
all.equal(fix.check$ayp,fix$ayp)
## [1] "Mean relative difference: 7.397594e-05"
Blinks are detected automatically, and stored similarly to saccades and fixations. We load a different dataset:
fpath <- system.file("extdata/monoRemote500.asc.gz",package="eyelinker")
dat <- read.asc(fpath)
dat$blinks
## stime etime dur eye block
## 1 12151796 12151850 56 L 1
## 2 12169510 12169532 24 L 2
## 3 12218674 12218694 22 L 4
The fields should be self-explanatory. We’ll re-use some the code above to label the blinks:
Blk <- cbind(dat$blinks$stime,dat$blinks$etime) #Define a set of intervals
filter(dat$raw,time %In% Blk) %>% head
## time xp yp ps cr.info tx ty td remote.info block
## 1 12151796 NA NA NA <NA> NA NA NA <NA> 1
## 2 12151798 NA NA NA <NA> NA NA NA <NA> 1
## 3 12151800 NA NA NA <NA> NA NA NA <NA> 1
## 4 12151802 NA NA NA <NA> NA NA NA <NA> 1
## 5 12151804 NA NA NA <NA> NA NA NA <NA> 1
## 6 12151806 NA NA NA <NA> NA NA NA <NA> 1
Not surprisingly, during blinks, eye position data is unavailable. Unfortunately, it takes the eyetracker a bit of time to detect blinks, and the eye position data around blinks may be suspect. The eyelink manual suggests that getting rid of samples that are within 100ms of a blink should eliminate most problems. We’ll use some functions from package intervals to expand our blinks by 100ms:
require(intervals)
## Loading required package: intervals
##
## Attaching package: 'intervals'
## The following object is masked from 'package:tidyr':
##
## expand
Suspect <- Intervals(Blk) %>% expand(100,"absolute")
Suspect
## Object of class Intervals
## 3 intervals over R:
## [12151696, 12151950]
## [12169410, 12169632]
## [12218574, 12218794]
Here’s an example of a trace around a blink:
raw.long <- dplyr::select(dat$raw,time,xp,yp,block) %>% gather("coord","pos",xp,yp)
raw.long <- mutate(raw.long,ts=(time-min(time))/1e3) #let's have time in sec.
ex <- mutate(raw.long,suspect=time %In% Suspect) %>% filter(block==2)
ggplot(ex,aes(ts,pos,group=coord,col=suspect))+geom_line()+coord_cartesian(xlim=c(34,40))+labs(x="time (s)")
The traces around the blink are indeed spurious.
The last data structure we need to cover contains messages:
head(dat$msg)
## time text
## 1 12134177 -8 SYNCTIME
## 2 12134177 -8 !V DRAW_LIST ../../runtime/dataviewer/js/graphics/VC_1.vcl
## 3 12134177 -7 !V IAREA FILE ../../runtime/dataviewer/js/aoi/IA_1.ias
## 4 12152026 -8 blank_screen
## 5 12153648 -3 SYNCTIME
## 6 12153648 -2 !V DRAW_LIST ../../runtime/dataviewer/js/graphics/VC_2.vcl
## block
## 1 1
## 2 1
## 3 1
## 4 1
## 5 2
## 6 2
The lines correspond to “MSG” lines in the original asc file. Since messages can be anything read.asc leaves them unparsed. If you’re interested in certain event types (e.g., time stamps), you’ll have to parse msg$text yourself. Here for example we extract all messages that contain the words “Saccade_target”:
library(stringr)
filter(dat$msg,str_detect(text,fixed("blank_screen")))
## time text block
## 1 12152026 -8 blank_screen 1
## 2 12175944 -5 blank_screen 2
## 3 12198433 -15 blank_screen 3
## 4 12223186 -10 blank_screen 4