You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
% detecteyemovements() - detect saccades & fixations in eye tracking data.
% Saccade detection is based on the algorithm by
% Engbert & Mergenthaler (2006). Saccades are defined as
% (monocular or binocular) outliers in 2D velocity space.
% Velocity thresholds for saccade detection are determined
% adaptively as a multiple of the (median-based) SD of all
% data samples in the epoch. Fixations are defined as
% intervals in-between saccades. Eye movements can be added
% as new events to EEGLAB's event structure. For various
% other options, see below.
%
% Usage:
% >> EEG = detecteyemovements(EEG,left_eye_xy,right_eye_xy,vfac,mindur,...
% degperpixel,smooth,globalthresh,clusterdist,clustermode,
% plotfig,writesac,writefix)
%
% Required inputs:
% EEG - [string] EEG struct, also containing synchronized eye
% tracking data (see pop_importeyetracker)
% left_eye_xy - [vector of two channel indices],
% specifying channel indices of X- (first value) and
% Y-component (second value) of left eye. Leave empty []
% if the left eye was not recorded.
% right_eye_xy - [vector of two channel indices],
% specifying channel indices of X- (first value) and
% Y-component (second value) of right eye. Leave empty []
% if the right eye was not recorded.
% vfac - [double] velocity factor ("lambda") to determine
% the velocity threshold for saccade detection
% (cf. Engbert & Mergenthaler, 2006)
% mindur - [integer] minimum saccade duration (in samples)
% (cf. Engbert & Mergenthaler, 2006)
% degperpixel - [double] visual angle of one screen pixel
% if this value is left empty [], saccade characteristics
% are reported in the original data metric (pixel?)
% instead of in degrees of visual angle
% smooth - [0/1] if set to 1, the raw data is smoothed over a
% 5-sample window to suppress noise
% noise. Recommended for high native ET sampling rates.
% globalthresh - [0/1]. Use the same thresholds for all epochs?
% 0: Adaptive velocity thresholds are computed
% individually for each data epoch.
% 1: Adaptive velocity thresholds are first computed for
% each epoch, but then the mean thresholds are applied to
% each epochs (i.e. same detection parameters are used for
% all epochs). Setting is irrelevant if the input data is
% still continuous (= only one data epoch).
% clusterdist - [integer] value in sampling points that defines the
% minimum allowed fixation duration between two saccades.
% If the off- and onsets of two temp. adjacent sacc. are
% closer together than 'clusterdist' samples, these
% saccades are regarded as a "cluster" and treated
% according to the 'clustermode' setting (see below).
% clusterdist is irrelevant if clustermode == 1.
% clustermode - [1,2,3,4]. Integer between 1 and 4.
% 1: keep all saccades, do nothing
% 2: keep only first saccade of each cluster
% 3: keep only largest sacc. of each cluster
% 4: combine all movements into one (longer) saccade
% this new saccade is defined as the movement that
% occurs between the onset of the 1st saccade in the
% cluster and the offset of the last sacc. in cluster
% WARNING: CLUSTERMODE 4 is experimental and untested!
% plotfig - [0/1] Show a figure with eye movement properties?
% 0: do not plot a figure.
% 1: plot a figure displaying properties of detected
% saccades & fixations
% writesac - [0/1]: Add saccades to EEG.event?
% 0: detect saccades, but do not store them in EEG.event.
% 1: add detected saccades as new events to EEG.event.
% writefix - [0/1]: Add fixations to EEG.event?
% 0: detect fixations, but do not add them to EEG.event.
% 1: add detected fixations as new events to EEG.event.
% Note: It is recommended to first test the parameters of
% saccade/fixation detection without adding events.
% For this, set writesac and writefix to 0.
%
% Outputs:
% EEG - EEG structure. If writesac or writefix were set to 1,
% the EEG structure (EEG.event/EEG.urevent/EEG.epoch)
% will contain additional "saccade" and "fixation" events
% with their respective properties
%
% See also: vecvel, velthresh, microsacc_plugin, binsacc, saccpar,
% mergesacc, addevents
%
%
% MAJOR UPDATE 03/2018: bad ET intervals, as marked by "bad_ET" events
% in EEG.event (see function pop_rej_contin.m) are excluded from eye movement
% detection. Bad intervals are now ignored when estimating velocity thresholds
% and saccade/fixation events overlapping with these intervals are removed
%
% An example call of the function might look like this:
% >> EEG = detecteyemovements(EEG,[],[33 34],6,4,0.037,1,0,25,4,1,1,0)
%
% In this example, the eye position data for the right eye is stored in
% channels 33 (horiz.) and 34 (vertical). The left eye was not recorded.
% The velocity threshold is set to 6 times the (median-based)
% SD of all velocity samples in the epoch. The minimum duration of
% saccades to be detected is 4 samples. In the experiment, one screen
% pixel corresponded to 0.037 degrees of visual angle.
% The raw data is smoothed prior to saccade detection (smooth: 1).
% Adaptive velocity thresholds (X and Y-threshold for each eye) are
% determined individually for each data epoch (globalthresh: 0). For saccades
% separated by fixations of less than 25 samples, only the first saccade
% is kept (clusterdist: 25, clustermode: 2). A figure with the
% results is plotted. Detected saccades are stored as new events in
% EEG.event, but fixations are not stored.
%
% The eye movement detection is based on:
%
% Engbert, R., & Kliegl, R. (2003). Microsaccades uncover the orientation
% of covert attention. Vision Research, Vol. 43, 1035-1045
%
% ...as well as...
%
% Engbert, R., & Mergenthaler, K. (2006). Microsaccades are triggered by
% low retinal image slip, PNAS, Vol. 103 (18), 7192-7197
%
% Author: od
% Copyright (C) 2009-2018 Olaf Dimigen, HU Berlin
% [email protected]
% This program is free software; you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation; either version 3 of the License, or
% (at your option) any later version.
%
% This program is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with this program; if not, write to the Free Software
% Foundation, 51 Franklin Street, Boston, MA 02110-1301, USA
function EEG = detecteyemovements(EEG,left_eye_xy,right_eye_xy,vfac,mindur,degperpixel,smooth,globalthresh,clusterdist,clustermode,plotfig,writesac,writefix)
allsac = [];
allfix = [];
% data of which eye is available?
ldata = false;
if length(left_eye_xy) == 2, ldata = true; end
rdata = false;
if length(right_eye_xy) == 2, rdata = true; end
if length(left_eye_xy) == 1 || length(right_eye_xy) == 1
error('%s(): For each recorded eye, horizontal (X) and vertical (Y) gaze channel must be specified.',mfilename);
end
% warn message if back-to-back saccades are detected
clusterwarning = false;
% critical bugfix 2013-10-01, by OD
% due to bug in third-party function "smoothdata":
% function smoothdata() was removed from the toolbox
% function vecvel() was updated to incorporate different levels of smoothing:
% options:
% - smoothlevel 0: no smoothing, simple diff()
% - smoothlevel 1: 3-point window
% - smoothlevel 2: 5-point window
if smooth
smoothlevel = 2; % 5-point smoothing
else
smoothlevel = 0; % no smoothing
end
%% screen feedback
% detection feedback
if ~ldata && ~rdata
error('%s(): Please correctly specify channels containing eye tracking data.\nFor each recorded eye, both horiz. (X) and vert. (Y) channel must be specified.',mfilename)
else
% summary of detection parameters
fprintf('\n--------------------------------------------------------------------')
fprintf('\nDetecting saccades after Engbert & Mergenthaler (2006)\n')
fprintf('\nVelocity threshold factor (vfac): %.2f SD',vfac);
fprintf('\nMinimum saccade duration (mindur): %.2f samples (%.2f ms)',mindur,mindur1000/EEG.srate);
if ~isempty(degperpixel) | isnan(degperpixel) % bugfix 2016-11-12 by OD: added case if degperpixel = NaN (from GUI input)
fprintf('\nVisual angle per screen pixel: %f°',degperpixel);
metric = 'deg';
else
fprintf('\nWARNING: No input provided for degperpixel!\nSpatial saccade properties are given in original metric (pixel?)');
degperpixel = 1;
metric = 'pix';
end
if nepochs < 2, fprintf('\n-- Using continuous data.'), else fprintf('\n-- Using epoched data.'); end
if ldata && rdata, fprintf('\n-- Using binocular data:'); else fprintf('\n-- Using monocular data:'); end
if ldata
fprintf('\n\tLeft horiz.: "%s"',EEG.chanlocs(left_eye_xy(1)).labels);
fprintf('\n\tLeft verti.: "%s"',EEG.chanlocs(left_eye_xy(2)).labels);
end
if rdata
fprintf('\n\tRight horiz.: "%s"',EEG.chanlocs(right_eye_xy(1)).labels);
fprintf('\n\tRight verti.: "%s"',EEG.chanlocs(right_eye_xy(2)).labels);
end
if smooth, fprintf('\n-- Raw data is smoothed in 5-sample window.'); else fprintf('\n-- Raw data is not smoothed.'); end
if nepochs > 1
if globalthresh
fprintf('\n-- Velocity thresholds computed globally across all %i epochs',nepochs);
else
fprintf('\n-- Velocity thresholds computed individually for each epoch');
end
end
fprintf('\n-- Treatment of saccade clusters:');
switch clustermode
case 1
fprintf('\n\tAll saccades are kept');
case 2
fprintf('\n\tSaccades separated by fixations < %i ms are clustered.',clusterdist(1000/EEG.srate));
fprintf('\n\tFirst sacc. of each cluster is kept.');
case 3
fprintf('\n\tSaccades separated by fixations < %i ms are clustered.',clusterdist*(1000/EEG.srate));
fprintf('\n\tLargest sacc. of each cluster is kept.');
case 4
fprintf('\n\tSaccades separated by fixations < %i ms are clustered.',clusterdist*(1000/EEG.srate));
fprintf('\n\tClusters are combined into one saccade.');
otherwise
error('%s(): Unknown input for clustermode, should be: 1,2,3,4.',mfilename)
end
if plotfig, fprintf('\n-- A figure with eye movement properties is plotted.'); end
if writesac && writefix, fprintf('\n-- Saccades and fixations will be added to EEG.event.');
elseif writesac, fprintf('\n-- Saccades will be added to EEG.event.');
elseif writefix, fprintf('\n-- Fixations will be added to EEG.event.');
else, fprintf('\n-- Saccades and fixations are detected, but NOT stored anywhere.');
end
end
%% get "bad_ET" intervals from EEG.event structure
badvector = zeros(1,size(EEG.data,2)*size(EEG.data,3));
ix_badETevent = find(ismember({EEG.event.type},'bad_ET'));
if ~isempty(ix_badETevent)
fprintf('\n--------------------------------------------------------------------')
fprintf('\nFound \"bad_ET\" events in EEG.event.\nThese intervals will be ignored for saccade detection!')
fprintf('\n--------------------------------------------------------------------')
bad_lat = [EEG.event(ix_badETevent).latency];
bad_dur = [EEG.event(ix_badETevent).duration];
bad_ET = [bad_lat; bad_dur]';
bad_ET(:,3) = bad_ET(:,1)+bad_ET(:,2)-1;
% create long vector (as long as EEG) indicating bad samples
for j = 1:size(bad_ET,1)
badvector(bad_ET(j,1):bad_ET(j,3)) = 1;
end
end
% reshape "badvector" to 3D if data is already epoched
badvector = reshape(badvector,1,size(EEG.data,2),size(EEG.data,3));
%% pre-compute saccade velocity thresholds for all epochs
% this enables the option to use the same threshold for all epochs, not
% provided by the original Engbert & Mergenthaler implementation
% new 03/2018: exclude bad ET intervals from velocity estimation
fprintf('\nComputing adaptive velocity thresholds...')
% if any(badvector(:))
% fprintf('\n-- Found %i "bad_ET" events marking bad eye-tracking intervals in EEG.event.',length(ix_badETevent))
% fprintf('\n-- These intervals (%.2f%% of data) will be ignored when computing velocity thresholds.',(sum(badvector)/length(badvector(:))*100))
% end
for e=1:nepochs
ix_goodET = ~badvector(:,:,e); % get index of non-bad ET samples
if ldata
l = EEG.data([left_eye_xy(1) left_eye_xy(2)],ix_goodET,e)';
vl = vecvel(l,EEG.srate,smoothlevel);
[l_msdx(e), l_msdy(e)] = velthresh(vl);
end
if rdata
r = EEG.data([right_eye_xy(1) right_eye_xy(2)],ix_goodET,e)';
vr = vecvel(r,EEG.srate,smoothlevel);
[r_msdx(e), r_msdy(e)] = velthresh(vr);
end
end
%% detect saccades & fixations
for e=1:nepochs
%% saccades of left eye
sac = [];
if ldata
l = EEG.data([left_eye_xy(1) left_eye_xy(2)],:,e)'; % don't exclude bad_ET samples here (otherwise discont. high-velocity jumps in time series)
% bad/missing samples in eye track?
badsmp = sum(sum(l<=0)); if badsmp > 0, badepochs(e) = 1; nbadsmp = nbadsmp + badsmp; end
vl = vecvel(l,EEG.srate,smoothlevel); % get eye velocities
% detect monocular saccades
if globalthresh % use precomputed velocity thresholds (mean of all epochs)
sacL = microsacc_plugin(l,vl,vfac,mindur,mean(l_msdx),mean(l_msdy));
else % compute velocity thresholds from this epoch only
sacL = microsacc_plugin(l,vl,vfac,mindur,l_msdx(e),l_msdy(e));
end
end
%% saccades of right eye
if rdata
r = EEG.data([right_eye_xy(1) right_eye_xy(2)],:,e)';
% bad/missing samples in eye track?
badsmp = sum(sum(r<=0)); if badsmp > 0, badepochs(e) = 1; nbadsmp = nbadsmp + badsmp; end
vr = vecvel(r,EEG.srate,smoothlevel); % get eye velocities
% detect monocular saccades
if globalthresh % use precomputed velocity thresholds (mean of all epochs)
sacR = microsacc_plugin(r,vr,vfac,mindur,mean(r_msdx),mean(r_msdy));
else
% compute velocity thresholds from this epoch only
sacR = microsacc_plugin(r,vr,vfac,mindur,r_msdx(e),r_msdy(e));
end
end
%% binocular saccades
if ldata && rdata
[sac, tmp, tmp] = binsacc(sacL,sacR); clear tmp
sac = saccpar(sac); % average saccade characteristics of both eyes
sac = mergesacc(sac,(l+r)./2,clusterdist,clustermode); % merge nearby saccades (e.g. glissades)
elseif ldata
sac = sacL; clear sacL;
sac = saccpar([sac sac]);
sac = mergesacc(sac,l,clusterdist,clustermode);
elseif rdata
sac = sacR; clear sacR;
sac = saccpar([sac sac]);
sac = mergesacc(sac,r,clusterdist,clustermode);
end
%% update various saccade metrics
if ~isempty(sac)
% define saccade duration as difference between saccade offset and
% saccade onset sample. In saccpar(), monocular saccade durations
% of both eyes are averaged, leading to uneven values (e.g.: 10.5
% samples) different from the difference between onset and offset
% values (which are the monocular extremes).
% Instead: use difference between offset and onset
sac(:,3) = sac(:,2)-sac(:,1)+1;
% report saccade velocity/distance/amplitude as visual angles
sac(:,[5 6 8]) = sac(:,[5 6 8]) .* degperpixel;
% report saccade angles in degree rather than radians
sac(:,[7 9]) = sac(:,[7 9]) * 180/pi;
% add index of corresp. data epoch
sac(:,10) = e;
% store screen location for start and end of saccade
if ldata && rdata
gazexy = (l+r)./2; % binoc. recordings: average across eyes
elseif ldata
gazexy = l;
elseif rdata
gazexy = r;
end
% get position immediatly before sacc. onset and after sacc. offset
startsmp = sac(:,1)-1; endsmp = sac(:,2)+1;
if startsmp(1) < 1, startsmp(1,1) = 1; end
if endsmp(end) > size(gazexy,2), endsmp(end) = size(gazexy,2); end
sac(:,11) = gazexy(startsmp,1);
sac(:,12) = gazexy(startsmp,2);
sac(:,13) = gazexy(endsmp ,1);
sac(:,14) = gazexy(endsmp ,2);
end
% columns of [sac]:
% 1: saccade onset (sample)
% 2: saccade offset (sample)
% 3: duration (samples)
% 4: delay between eyes (samples)
% 5: vpeak (peak velocity)
% 6: saccade "distance" (eucly. dist. between start and endpoint)
% 7: saccade angle (based on saccade "distance")
% 8: saccade "amplitude" (eucly. dist. of min/max in full saccade trajectory)
% 9: saccade angle (based on saccade "amplitude")
%10: index of corresponding data epoch (1 in case of contin. data)
%11: horizontal (x) gaze position before start of saccade (pixel)
%12: vertial (y) gaze position before start of saccade (pixel)
%13: horizontal (x) gaze position after end of saccade (pixel)
%14: vertical (y) gaze position after end of saccade (pixel)
%% remove saccades that occured during "bad_ET" intervals [*]
% Delete all saccades whos onset or offset occurs during a
% bad_ET interval (signal jumps to blinks are otherwise detected as
% saccades)
%
% [*] Note: It is not clear/trivial how to treat blinks in the context
% of EM detection, since we do not know what really happened during a
% blink or loss of the signal. For example, is a long fixation with a
% blink in the middle really two fixations? Or should it be treated as
% one long fixation? Or should all fixations that are ended or started
% by blinks or are interrupted by them be completely removed?
% The solution here is preliminary: I first remove all saccades that
% started or ended during a blink interval. The remaining saccades are
% used to define fixatinos. Finally, all fixations are removed that
% overlap with "bad_ET" intervals in EEG.event. So this approach
% removes any eye movement event that overlaps with a bad_ET interval.
% OD, 2018-03-08
%% delete saccades starting/ending during "bad_ET" intervals
badETsmp = find(badvector(:,:,e));
if ~isempty(sac)
ix_fakesac = find(ismember(sac(:,1),badETsmp) | ismember(sac(:,2),badETsmp));
sac(ix_fakesac,:) = [];
% if ~isempty(ix_fakesac)
% fprintf('\n\n-- Removed %i saccades that occured during "bad_ET" intervals',length(ix_fakesac))
% end
end
%% get fixations
nsac = size(sac,1);
fix = [];
if ~isempty(sac)
if nsac > 1
for f = 1:nsac-1
fix(f,1) = sac(f,2)+1;
fix(f,2) = sac(f+1,1)-1;
% catch special case: if Engbert algorithms are applied
% without any saccade clustering [>> mergesacc()] there can
% be back-to-back saccades with intervening "fixations" of
% zero sample duration. Catch this by setting the duration
% of these fixations to one sample
if fix(f,1) > fix(f,2)
fix(f,1) = fix(f,2); % 1-sample fixation
clusterwarning = true;
end
end
end
% if epoch does not begin with saccade, add first fixation
if sac(1,1) > 1
fix = [[1 sac(1,1)-1]; fix];
end
% if epoch does not end with saccade, add last fixation
if sac(end,2) < nsample
fix = [fix;[sac(end,2)+1 nsample]];
end
% add more fixation properties
for f = 1:size(fix,1)
% fixation duration (in samples!)
fix(f,3) = fix(f,2)-fix(f,1)+1;
% mean fix. position: left eye
if ldata
fix(f,4) = mean( l(fix(f,1):fix(f,2),1) );
fix(f,5) = mean( l(fix(f,1):fix(f,2),2) );
else
fix(f,4) = NaN;
fix(f,5) = NaN;
end
% mean fix. position: right eye
if rdata
fix(f,6) = mean( r(fix(f,1):fix(f,2),1) );
fix(f,7) = mean( r(fix(f,1):fix(f,2),2) );
else
fix(f,6) = NaN;
fix(f,7) = NaN;
end
% binocular fixation position
fix(f,8) = nanmean(fix(f,[4 6]));
fix(f,9) = nanmean(fix(f,[5 7]));
end
fix(:,10) = e; % add index of corresp. data epoch
% recompute latencies of eye movement events
% (only necessary for epoched data)
offset = (e-1)*nsample;
sac(:,[1 2]) = sac(:,[1 2])+offset;
% special case: single sacc. lasts entire epoch
if ~isempty(fix)
fix(:,[1 2]) = fix(:,[1 2])+offset;
end
end
% columns of [fix]:
% 1: fixation onset (sample)
% 2: fixation offset (sample)
% 3: duration (samples)
% 4: mean fix position (L eye X)
% 5: mean fix position (L eye Y)
% 6: mean fix position (R eye X)
% 7: mean fix position (R eye Y)
% 8: mean fix position (L/R average X)
% 9: mean fix position (L/R average Y)
%10: index of corresponding data epoch (1 in case of contin. data)
%% remove fixations overlapping with "bad_ET" intervals
% this includes fixations interrupted by a blink (!)
% development note: loop is slow, need to implement more efficiently
if ~isempty(fix) & any(badvector(:))
badfix = false(size(fix,1),1); % preallocate logical
% go tru fixations, check whether "bad"
for p = 1:size(fix,1)
fixsmp = fix(p,1):fix(p,2);
if any(ismember(fixsmp,badETsmp))
badfix(p) = true;
end
end
% remove fixations overlapping with "bad_ET" intervals
fix(badfix,:) = [];
% if any(badfix)
% fprintf('\n-- Removed %i fixations that overlapped with "bad_ET" intervals',sum(badfix))
% end
end
% slow, but simple:
allsac = [allsac;sac];
allfix = [allfix;fix];
end % epoch loop
%% remove artificial eye movements caused by boundaries (data breaks)
% Remove all EMs whose onset is detected in temporal proximity to boundary.
% Otherwise, data breaks will likely result in additional fake sacc./fix.
% Applies only if eye movements are detected in the continuous data
if nepochs == 1
ix_bnd = find(cellfun(@(x) strcmp(x,'boundary'),{EEG.event.type})); % bug fix: now robust against numeric types
% any data breaks?
% (due to manual rejections or function pop_rej_eyecontin)
if ~isempty(ix_bnd)
% minimum distance from data break in milliseconds (hard-coded)
BOUNDDIST_MS = 50;
BOUNDDIST = round(BOUNDDIST_MS / (1000/EEG.srate));
% data break latencies
bound_lats = round([EEG.event(ix_bnd).latency]);
% mark all samples close to data break
boundvector = zeros(1,EEG.pnts);
for b = 1:length(bound_lats)
lowr = bound_lats(b)-BOUNDDIST;
uppr = bound_lats(b)+BOUNDDIST;
if lowr <= 0, lowr = 1; end
if uppr > EEG.pnts, uppr = EEG.pnts; end
boundvector(lowr:uppr) = 1;
end
nearboundsmp = find(boundvector);
% option 1: event onset is close to boundary
% fakesac = find(ismember(allsac(:,1),nearboundsmp));
% fakefix = find(ismember(allfix(:,1),nearboundsmp));
% option 2: event on- or offset is close to boundary
fakesac = find(ismember(allsac(:,1),nearboundsmp) | ismember(allsac(:,2),nearboundsmp));
fakefix = find(ismember(allfix(:,1),nearboundsmp) | ismember(allfix(:,2),nearboundsmp));
allsac(fakesac,:) = [];
allfix(fakefix,:) = [];
fprintf('\n--------------------------------------------------------------------');
fprintf('\nFound %i data breaks (boundary events) in the continuous data',length(ix_bnd));
fprintf('\nRemoving eye movements that might be artifacts of data breaks:');
fprintf('\nRemoved %i saccades < %i ms away from a boundary',length(fakesac),BOUNDDIST_MS);
fprintf('\nRemoved %i fixations < %i ms away from a boundary',length(fakefix),BOUNDDIST_MS);
end
end
if clusterwarning && clustermode == 1
fprintf('\n--------------------------------------------------------------------');
fprintf('\n\n*** WARNING! ***\nDetected pairs of saccades that ended/started on adjacent data samples.');
fprintf('\nYou should probably use saccade clustering! (clustermode: 2,3,4)');
end
%% user feedback if bad eye-tracking data (values <= 0) but no bad_ET events
if sum(badepochs)>0 & isempty(ix_badETevent)
warning('\nI found bad or missing data (i.e. values <= 0) in the ET channels but no "bad_ET" events!');
fprintf('\nDetails:');
fprintf('\n-- %i of %i epochs contained gaze position values <= 0',sum(badepochs),nepochs);
fprintf('\n-- Total number of bad samples <= 0: %i',nbadsmp);
fprintf('\n-- Did you detect or reject bad intervals with out-of-range values (''Reject data based on eyetrack'')?');
fprintf('\n-- Did you subtract a baseline from eye channels (explaining negative values)?');
fprintf('\n-- Note that unmarked blinks or missing data in the eye-track will...:')
fprintf('\n ...distort the velocity threshold for saccade detection!');
fprintf('\n ...be erroneously detected as additional saccades/fixations!');
end
%% user feedback: saccade & fixation detection
fprintf('\n--------------------------------------------------------------------');
fprintf('\nVelocity thresholds used:'); if nepochs > 1, fprintf(' (mean across epochs):'); end;
if ldata, fprintf('\n\tLeft eye. Horiz.: %.2f %s/s. Vert.: %.2f %s/s',mean(l_msdx(e)vfacdegperpixel),metric,mean(l_msdyvfacdegperpixel),metric); end
if rdata, fprintf('\n\tRight eye. Horiz.: %.2f %s/s. Vert.: %.2f %s/s',mean(r_msdx(e)vfacdegperpixel),metric,mean(r_msdyvfacdegperpixel),metric); end
fprintf('\n--------------------------------------------------------------------')
if ~isempty(allsac)
fprintf('\n%i saccades detected:',size(allsac,1));
fprintf('\n\tMedian amplitude: %.2f %s',median(allsac(:,6)),metric);
fprintf('\n\tMedian duration: %.2f ms',median(allsac(:,3))*1000/EEG.srate);
fprintf('\n\tMedian peak veloc. %.2f %s/s',median(allsac(:,5)),metric);
end
if ~isempty(allfix)
fprintf('\n%i fixations detected:',size(allfix,1));
fprintf('\n\tMedian duration: %.2f ms',median(allfix(:,3))*1000/EEG.srate);
if ldata,fprintf('\n\tMedian fix. pos. left eye: Horiz.: %.2f px. Vert.: %.2f px',median(allfix(:,4)),median(allfix(:,5))); end
if rdata,fprintf('\n\tMedian fix. pos. right eye: Horiz.: %.2f px. Vert.: %.2f px',median(allfix(:,6)),median(allfix(:,7))); end
end
%% plot figure with eye movements properties
if plotfig
if ldata && rdata
fprintf('\n--------------------------------------------------------------------')
fprintf('\nPlotting eye movement properties...\nFor plotting, fixation locations are averaged across eyes.')
ploteyemovements(allsac(:,6),allsac(:,5),allsac(:,7),allfix(:,3)*1000/EEG.srate,mean(allfix(:,[4 6]),2),mean(allfix(:,[5 7]),2),metric);
elseif ldata
fprintf('\n--------------------------------------------------------------------')
fprintf('\nPlotting eye movement properties of left eye...')
ploteyemovements(allsac(:,6),allsac(:,5),allsac(:,7),allfix(:,3)*1000/EEG.srate,allfix(:,4),allfix(:,5),metric);
else
fprintf('\n--------------------------------------------------------------------')
fprintf('\nPlotting eye movement properties of right eye...')
ploteyemovements(allsac(:,6),allsac(:,5),allsac(:,7),allfix(:,3)*1000/EEG.srate,allfix(:,6),allfix(:,7),metric);
end
end
%% write eye movements to EEG.event & EEG.urevent
if writefix || writesac
% check: are there already eye movements events in EEG.event?
em_types = {'saccade','fixation','L_saccade','R_saccade','L_fixation','R_fixation'};
if any(cellfun(@(x) any(strcmp(x,em_types)),{EEG.event.type})) % updated [v.0.337]
fprintf('\n--------------------------------------------------------------------')
fprintf('\n*** WARNING! ***:\nFound existing eye movement events in EEG.event!');
fprintf('\nNew events will be added to the already existing events!');
end
% write saccades
if writesac
fprintf('\n--------------------------------------------------------------------')
fprintf('\nAdding %i saccades to EEG.event...\n',size(allsac,1))
EEG = addevents(EEG,allsac(:,[1 3 5 6 7 10 11:14]),{'latency','duration','sac_vmax','sac_amplitude','sac_angle','epoch','sac_startpos_x','sac_startpos_y','sac_endpos_x','sac_endpos_y'},'saccade');
end
% write fixations
if writefix
fprintf('\n--------------------------------------------------------------------')
fprintf('\nAdding %i fixations to EEG.event...\n',size(allfix,1))
if ldata && rdata % binocular
EEG = addevents(EEG,allfix(:,[1 3 8 9 10]),{'latency','duration','fix_avgpos_x','fix_avgpos_y','epoch'},'fixation');
%EEG = addevents(EEG,allfix(:,[1 3 4 5 6 7 10]),{'latency','duration','fixposition_ly','fixposition_lx','fixposition_ry','fixposition_rx','epoch'},'fixation');
elseif ldata % left eye recorded
EEG = addevents(EEG,allfix(:,[1 3 4 5 10]),{'latency','duration','fix_avgpos_x','fix_avgpos_y','epoch'},'fixation');
elseif rdata % right eye recorded
EEG = addevents(EEG,allfix(:,[1 3 6 7 10]),{'latency','duration','fix_avgpos_x','fix_avgpos_y','epoch'},'fixation');
end
end
fprintf('--------------------------------------------------------------------')
end
fprintf('\nDone.\n\n')
end
The text was updated successfully, but these errors were encountered:
% detecteyemovements() - detect saccades & fixations in eye tracking data.
% Saccade detection is based on the algorithm by
% Engbert & Mergenthaler (2006). Saccades are defined as
% (monocular or binocular) outliers in 2D velocity space.
% Velocity thresholds for saccade detection are determined
% adaptively as a multiple of the (median-based) SD of all
% data samples in the epoch. Fixations are defined as
% intervals in-between saccades. Eye movements can be added
% as new events to EEGLAB's event structure. For various
% other options, see below.
%
% Usage:
% >> EEG = detecteyemovements(EEG,left_eye_xy,right_eye_xy,vfac,mindur,...
% degperpixel,smooth,globalthresh,clusterdist,clustermode,
% plotfig,writesac,writefix)
%
% Required inputs:
% EEG - [string] EEG struct, also containing synchronized eye
% tracking data (see pop_importeyetracker)
% left_eye_xy - [vector of two channel indices],
% specifying channel indices of X- (first value) and
% Y-component (second value) of left eye. Leave empty []
% if the left eye was not recorded.
% right_eye_xy - [vector of two channel indices],
% specifying channel indices of X- (first value) and
% Y-component (second value) of right eye. Leave empty []
% if the right eye was not recorded.
% vfac - [double] velocity factor ("lambda") to determine
% the velocity threshold for saccade detection
% (cf. Engbert & Mergenthaler, 2006)
% mindur - [integer] minimum saccade duration (in samples)
% (cf. Engbert & Mergenthaler, 2006)
% degperpixel - [double] visual angle of one screen pixel
% if this value is left empty [], saccade characteristics
% are reported in the original data metric (pixel?)
% instead of in degrees of visual angle
% smooth - [0/1] if set to 1, the raw data is smoothed over a
% 5-sample window to suppress noise
% noise. Recommended for high native ET sampling rates.
% globalthresh - [0/1]. Use the same thresholds for all epochs?
% 0: Adaptive velocity thresholds are computed
% individually for each data epoch.
% 1: Adaptive velocity thresholds are first computed for
% each epoch, but then the mean thresholds are applied to
% each epochs (i.e. same detection parameters are used for
% all epochs). Setting is irrelevant if the input data is
% still continuous (= only one data epoch).
% clusterdist - [integer] value in sampling points that defines the
% minimum allowed fixation duration between two saccades.
% If the off- and onsets of two temp. adjacent sacc. are
% closer together than 'clusterdist' samples, these
% saccades are regarded as a "cluster" and treated
% according to the 'clustermode' setting (see below).
% clusterdist is irrelevant if clustermode == 1.
% clustermode - [1,2,3,4]. Integer between 1 and 4.
% 1: keep all saccades, do nothing
% 2: keep only first saccade of each cluster
% 3: keep only largest sacc. of each cluster
% 4: combine all movements into one (longer) saccade
% this new saccade is defined as the movement that
% occurs between the onset of the 1st saccade in the
% cluster and the offset of the last sacc. in cluster
% WARNING: CLUSTERMODE 4 is experimental and untested!
% plotfig - [0/1] Show a figure with eye movement properties?
% 0: do not plot a figure.
% 1: plot a figure displaying properties of detected
% saccades & fixations
% writesac - [0/1]: Add saccades to EEG.event?
% 0: detect saccades, but do not store them in EEG.event.
% 1: add detected saccades as new events to EEG.event.
% writefix - [0/1]: Add fixations to EEG.event?
% 0: detect fixations, but do not add them to EEG.event.
% 1: add detected fixations as new events to EEG.event.
% Note: It is recommended to first test the parameters of
% saccade/fixation detection without adding events.
% For this, set writesac and writefix to 0.
%
% Outputs:
% EEG - EEG structure. If writesac or writefix were set to 1,
% the EEG structure (EEG.event/EEG.urevent/EEG.epoch)
% will contain additional "saccade" and "fixation" events
% with their respective properties
%
% See also: vecvel, velthresh, microsacc_plugin, binsacc, saccpar,
% mergesacc, addevents
%
%
% MAJOR UPDATE 03/2018: bad ET intervals, as marked by "bad_ET" events
% in EEG.event (see function pop_rej_contin.m) are excluded from eye movement
% detection. Bad intervals are now ignored when estimating velocity thresholds
% and saccade/fixation events overlapping with these intervals are removed
%
% An example call of the function might look like this:
% >> EEG = detecteyemovements(EEG,[],[33 34],6,4,0.037,1,0,25,4,1,1,0)
%
% In this example, the eye position data for the right eye is stored in
% channels 33 (horiz.) and 34 (vertical). The left eye was not recorded.
% The velocity threshold is set to 6 times the (median-based)
% SD of all velocity samples in the epoch. The minimum duration of
% saccades to be detected is 4 samples. In the experiment, one screen
% pixel corresponded to 0.037 degrees of visual angle.
% The raw data is smoothed prior to saccade detection (smooth: 1).
% Adaptive velocity thresholds (X and Y-threshold for each eye) are
% determined individually for each data epoch (globalthresh: 0). For saccades
% separated by fixations of less than 25 samples, only the first saccade
% is kept (clusterdist: 25, clustermode: 2). A figure with the
% results is plotted. Detected saccades are stored as new events in
% EEG.event, but fixations are not stored.
%
% The eye movement detection is based on:
%
% Engbert, R., & Kliegl, R. (2003). Microsaccades uncover the orientation
% of covert attention. Vision Research, Vol. 43, 1035-1045
%
% ...as well as...
%
% Engbert, R., & Mergenthaler, K. (2006). Microsaccades are triggered by
% low retinal image slip, PNAS, Vol. 103 (18), 7192-7197
%
% Author: od
% Copyright (C) 2009-2018 Olaf Dimigen, HU Berlin
% [email protected]
% This program is free software; you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation; either version 3 of the License, or
% (at your option) any later version.
%
% This program is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with this program; if not, write to the Free Software
% Foundation, 51 Franklin Street, Boston, MA 02110-1301, USA
function EEG = detecteyemovements(EEG,left_eye_xy,right_eye_xy,vfac,mindur,degperpixel,smooth,globalthresh,clusterdist,clustermode,plotfig,writesac,writefix)
allsac = [];
allfix = [];
% data of which eye is available?
ldata = false;
if length(left_eye_xy) == 2, ldata = true; end
rdata = false;
if length(right_eye_xy) == 2, rdata = true; end
if length(left_eye_xy) == 1 || length(right_eye_xy) == 1
error('%s(): For each recorded eye, horizontal (X) and vertical (Y) gaze channel must be specified.',mfilename);
end
nsample = size(EEG.data,2);
nepochs = size(EEG.data,3);
badepochs = zeros(nepochs,1);
nbadsmp = 0;
% preallocate storage for sacc. detection thresholds
l_msdx = NaN(nepochs,1);
l_msdy = NaN(nepochs,1);
r_msdx = NaN(nepochs,1);
r_msdy = NaN(nepochs,1);
% warn message if back-to-back saccades are detected
clusterwarning = false;
% critical bugfix 2013-10-01, by OD
% due to bug in third-party function "smoothdata":
% function smoothdata() was removed from the toolbox
% function vecvel() was updated to incorporate different levels of smoothing:
% options:
% - smoothlevel 0: no smoothing, simple diff()
% - smoothlevel 1: 3-point window
% - smoothlevel 2: 5-point window
if smooth
smoothlevel = 2; % 5-point smoothing
else
smoothlevel = 0; % no smoothing
end
%% screen feedback
% detection feedback
if ~ldata && ~rdata
error('%s(): Please correctly specify channels containing eye tracking data.\nFor each recorded eye, both horiz. (X) and vert. (Y) channel must be specified.',mfilename)
else
% summary of detection parameters
fprintf('\n--------------------------------------------------------------------')
fprintf('\nDetecting saccades after Engbert & Mergenthaler (2006)\n')
fprintf('\nVelocity threshold factor (vfac): %.2f SD',vfac);
fprintf('\nMinimum saccade duration (mindur): %.2f samples (%.2f ms)',mindur,mindur1000/EEG.srate);
if ~isempty(degperpixel) | isnan(degperpixel) % bugfix 2016-11-12 by OD: added case if degperpixel = NaN (from GUI input)
fprintf('\nVisual angle per screen pixel: %f°',degperpixel);
metric = 'deg';
else
fprintf('\nWARNING: No input provided for degperpixel!\nSpatial saccade properties are given in original metric (pixel?)');
degperpixel = 1;
metric = 'pix';
end
if nepochs < 2, fprintf('\n-- Using continuous data.'), else fprintf('\n-- Using epoched data.'); end
if ldata && rdata, fprintf('\n-- Using binocular data:'); else fprintf('\n-- Using monocular data:'); end
if ldata
fprintf('\n\tLeft horiz.: "%s"',EEG.chanlocs(left_eye_xy(1)).labels);
fprintf('\n\tLeft verti.: "%s"',EEG.chanlocs(left_eye_xy(2)).labels);
end
if rdata
fprintf('\n\tRight horiz.: "%s"',EEG.chanlocs(right_eye_xy(1)).labels);
fprintf('\n\tRight verti.: "%s"',EEG.chanlocs(right_eye_xy(2)).labels);
end
if smooth, fprintf('\n-- Raw data is smoothed in 5-sample window.'); else fprintf('\n-- Raw data is not smoothed.'); end
if nepochs > 1
if globalthresh
fprintf('\n-- Velocity thresholds computed globally across all %i epochs',nepochs);
else
fprintf('\n-- Velocity thresholds computed individually for each epoch');
end
end
fprintf('\n-- Treatment of saccade clusters:');
switch clustermode
case 1
fprintf('\n\tAll saccades are kept');
case 2
fprintf('\n\tSaccades separated by fixations < %i ms are clustered.',clusterdist(1000/EEG.srate));
fprintf('\n\tFirst sacc. of each cluster is kept.');
case 3
fprintf('\n\tSaccades separated by fixations < %i ms are clustered.',clusterdist*(1000/EEG.srate));
fprintf('\n\tLargest sacc. of each cluster is kept.');
case 4
fprintf('\n\tSaccades separated by fixations < %i ms are clustered.',clusterdist*(1000/EEG.srate));
fprintf('\n\tClusters are combined into one saccade.');
otherwise
error('%s(): Unknown input for clustermode, should be: 1,2,3,4.',mfilename)
end
if plotfig, fprintf('\n-- A figure with eye movement properties is plotted.'); end
if writesac && writefix, fprintf('\n-- Saccades and fixations will be added to EEG.event.');
elseif writesac, fprintf('\n-- Saccades will be added to EEG.event.');
elseif writefix, fprintf('\n-- Fixations will be added to EEG.event.');
else, fprintf('\n-- Saccades and fixations are detected, but NOT stored anywhere.');
end
end
%% get "bad_ET" intervals from EEG.event structure
badvector = zeros(1,size(EEG.data,2)*size(EEG.data,3));
ix_badETevent = find(ismember({EEG.event.type},'bad_ET'));
if ~isempty(ix_badETevent)
end
% reshape "badvector" to 3D if data is already epoched
badvector = reshape(badvector,1,size(EEG.data,2),size(EEG.data,3));
%% pre-compute saccade velocity thresholds for all epochs
% this enables the option to use the same threshold for all epochs, not
% provided by the original Engbert & Mergenthaler implementation
% new 03/2018: exclude bad ET intervals from velocity estimation
fprintf('\nComputing adaptive velocity thresholds...')
% if any(badvector(:))
% fprintf('\n-- Found %i "bad_ET" events marking bad eye-tracking intervals in EEG.event.',length(ix_badETevent))
% fprintf('\n-- These intervals (%.2f%% of data) will be ignored when computing velocity thresholds.',(sum(badvector)/length(badvector(:))*100))
% end
for e=1:nepochs
ix_goodET = ~badvector(:,:,e); % get index of non-bad ET samples
if ldata
l = EEG.data([left_eye_xy(1) left_eye_xy(2)],ix_goodET,e)';
vl = vecvel(l,EEG.srate,smoothlevel);
[l_msdx(e), l_msdy(e)] = velthresh(vl);
end
if rdata
r = EEG.data([right_eye_xy(1) right_eye_xy(2)],ix_goodET,e)';
vr = vecvel(r,EEG.srate,smoothlevel);
[r_msdx(e), r_msdy(e)] = velthresh(vr);
end
end
%% detect saccades & fixations
for e=1:nepochs
end % epoch loop
%% remove artificial eye movements caused by boundaries (data breaks)
% Remove all EMs whose onset is detected in temporal proximity to boundary.
% Otherwise, data breaks will likely result in additional fake sacc./fix.
% Applies only if eye movements are detected in the continuous data
if nepochs == 1
end
if clusterwarning && clustermode == 1
fprintf('\n--------------------------------------------------------------------');
fprintf('\n\n*** WARNING! ***\nDetected pairs of saccades that ended/started on adjacent data samples.');
fprintf('\nYou should probably use saccade clustering! (clustermode: 2,3,4)');
end
%% user feedback if bad eye-tracking data (values <= 0) but no bad_ET events
if sum(badepochs)>0 & isempty(ix_badETevent)
warning('\nI found bad or missing data (i.e. values <= 0) in the ET channels but no "bad_ET" events!');
fprintf('\nDetails:');
fprintf('\n-- %i of %i epochs contained gaze position values <= 0',sum(badepochs),nepochs);
fprintf('\n-- Total number of bad samples <= 0: %i',nbadsmp);
fprintf('\n-- Did you detect or reject bad intervals with out-of-range values (''Reject data based on eyetrack'')?');
fprintf('\n-- Did you subtract a baseline from eye channels (explaining negative values)?');
fprintf('\n-- Note that unmarked blinks or missing data in the eye-track will...:')
fprintf('\n ...distort the velocity threshold for saccade detection!');
fprintf('\n ...be erroneously detected as additional saccades/fixations!');
end
%% user feedback: saccade & fixation detection
fprintf('\n--------------------------------------------------------------------');
fprintf('\nVelocity thresholds used:'); if nepochs > 1, fprintf(' (mean across epochs):'); end;
if ldata, fprintf('\n\tLeft eye. Horiz.: %.2f %s/s. Vert.: %.2f %s/s',mean(l_msdx(e)vfacdegperpixel),metric,mean(l_msdyvfacdegperpixel),metric); end
if rdata, fprintf('\n\tRight eye. Horiz.: %.2f %s/s. Vert.: %.2f %s/s',mean(r_msdx(e)vfacdegperpixel),metric,mean(r_msdyvfacdegperpixel),metric); end
fprintf('\n--------------------------------------------------------------------')
if ~isempty(allsac)
fprintf('\n%i saccades detected:',size(allsac,1));
fprintf('\n\tMedian amplitude: %.2f %s',median(allsac(:,6)),metric);
fprintf('\n\tMedian duration: %.2f ms',median(allsac(:,3))*1000/EEG.srate);
fprintf('\n\tMedian peak veloc. %.2f %s/s',median(allsac(:,5)),metric);
end
if ~isempty(allfix)
fprintf('\n%i fixations detected:',size(allfix,1));
fprintf('\n\tMedian duration: %.2f ms',median(allfix(:,3))*1000/EEG.srate);
if ldata,fprintf('\n\tMedian fix. pos. left eye: Horiz.: %.2f px. Vert.: %.2f px',median(allfix(:,4)),median(allfix(:,5))); end
if rdata,fprintf('\n\tMedian fix. pos. right eye: Horiz.: %.2f px. Vert.: %.2f px',median(allfix(:,6)),median(allfix(:,7))); end
end
%% plot figure with eye movements properties
if plotfig
if ldata && rdata
fprintf('\n--------------------------------------------------------------------')
fprintf('\nPlotting eye movement properties...\nFor plotting, fixation locations are averaged across eyes.')
ploteyemovements(allsac(:,6),allsac(:,5),allsac(:,7),allfix(:,3)*1000/EEG.srate,mean(allfix(:,[4 6]),2),mean(allfix(:,[5 7]),2),metric);
elseif ldata
fprintf('\n--------------------------------------------------------------------')
fprintf('\nPlotting eye movement properties of left eye...')
ploteyemovements(allsac(:,6),allsac(:,5),allsac(:,7),allfix(:,3)*1000/EEG.srate,allfix(:,4),allfix(:,5),metric);
else
fprintf('\n--------------------------------------------------------------------')
fprintf('\nPlotting eye movement properties of right eye...')
ploteyemovements(allsac(:,6),allsac(:,5),allsac(:,7),allfix(:,3)*1000/EEG.srate,allfix(:,6),allfix(:,7),metric);
end
end
%% write eye movements to EEG.event & EEG.urevent
if writefix || writesac
end
fprintf('\nDone.\n\n')
end
The text was updated successfully, but these errors were encountered: