EEG preprocessing functions
A collection of functions and code-snippets useful for EEG preprocessing with EEGLAB (Matlab). Note, I distribute these functions under the GNU General Public License. I removed the statement to slim the code and the page. However, if you use or modify any of these functions in your projects, please add the GNU licence statement at the end of the function help. You can find the statement at the end of this page.
Counting triggers
This function takes an EEGLAB EEG structure and returns a cell array containing the number of triggers (aka events) divided for each trigger type. I use it as a sanity check to ensure that my data does not contain missing triggers and that the participant completed the correct number of trials/blocks.
% nTrig = count_triggers(EEG)
%
% Count the number of unique triggers contained in the EEG.event
% structure.
%
% In:
% EEG - An EEGLAB data structure
%
% Out:
% nTrig - Cell array containing the unique trigger names and how many
% times they occur in the recording.
% Author: Daniele Scanzi
function nTrig = count_triggers(EEG)
% Check that event structure is in EEG structure
if ~isfield(EEG, "event")
error("Cannot find event structure. Is EEG an EEGLAB structure?")
end
% Find unique events
= unique({EEG.event.type})';
nTrig if isempty(nTrig)
error("Cannot find any events in the event structure. Do you need to load them?")
end
% Create cell array storing name of event and their number
for iEvent = 1:size(nTrig,1)
, 2} = sum(strcmp({EEG.event.type}, nTrig{iEvent, 1}));
nTrig{iEventend
end
Add extra trigger information
This function is useful if you have triggers represented as codes (eg. T100) and you would like to add their meaning in the EEG.event structure. However, you can add any other information.
% EEG = add_trigger_info(EEG, trigInfo, varargin)
%
% Add extra information to the EEG.event structure. It is useful to add
% trigger information for each trigger. For instance, you can add the
% meaning of each trigger if trigger codes have been used.
%
% In:
% EEG - An EEGLAB data structure
% trigInfo - Cell array of size NxM. N should reflect the number of
% triggers for which extra information should be added. M
% should reflect the nuber of extra information to add. M new
% columns will be added to the EEG.event structure. M must be
% at least 2, where the first column should contain the
% names of the triggers (one for each row) and the second
% column contains the information to add. For instance, if
% you have three triggers and you want to add their meaning,
% you could pass a cell array such as:
% {'T100' 'start_recording';
% 'T150' 'pause_recording';
% 'T200' 'stop_recording'}
%
% Optional:
% fieldName - Cell array containing the names of the new columns to add
% to the EEG.event structure. The cell array should have
% size of 1xM, where M is the number of the extra
% information columns to add. M must match the number of
% columns of trigInfo. If no names are provided, the new
% columns will be named 'X1', 'X2',..., 'XM'
%
% Out:
% EEG - Modified copy of the EEG structure
% Author: Daniele Scanzi
function EEG = add_trigger_info(EEG, trigInfo, varargin)
= inputParser;
p
% Mandatory
, 'EEG', @isstruct); % EEG structure
addRequired(p, 'trigInfo', @iscell); % Cell array containing tirgger info
addRequired(p
% Optional
, 'fieldName', [], @iscell); % Name/s to provide to the fields to add
addParameter(p
% Parse user input
, EEG, trigInfo, varargin{:});
parse(p
= p.Results.EEG;
EEG = p.Results.trigInfo;
triggInfo = p.Results.fieldName;
fieldName
%% Checks
if size(trigInfo, 2) <= 1
error('triggerInfo should be a cell array with at least two columns: Trigger names - New field1 \n');
end
% Create names for field if not provided, check otherwise
if isempty(fieldName)
= cell(size(trigInfo, 2) - 1, 1);
fieldName for iField = 1:( size(trigInfo, 2) - 1 )
, 1} = strcat('X', num2str(iField));
fieldName{iFieldend
else
if length(fieldName) ~= ( size(trigInfo, 2) - 1 )
error('fieldNames should match the number of new fields contained in trigInfo \n');
end
end
for iEvent = 1:length(EEG.event)
= EEG.event(iEvent).type;
currentEvent = find(strcmp(trigInfo(:, 1), currentEvent));
currentTrigInfoIdx
% Add info
for iInfo = 1:length(fieldName)
if isempty(currentTrigInfoIdx)
= 'n/a';
EEG.event(iEvent).(fieldName{iInfo}) else
= trigInfo{currentTrigInfoIdx, iInfo+1};
EEG.event(iEvent).(fieldName{iInfo}) end
end
end
end
Compute latencies with photosensor
In our current EEG setup, we have a Brainproducts system where triggers are sent through a triggerbox. We also have a photosensor, which allows us to detect with precision when a stimulus is presented on the screen. Unfortunately, we do not have the StimTrack used to convert the photosensor data into triggers. So, we need to do this ourselves.
The photosensor data is stored as an extra channel in the EEG data. If the photosensor is set up to detect a switch from a dark to a light stimulus, then it creates a trace with a spike (NOTE: not a square wave) when the switch happens. The spike begins at the onset of the light stimulus and peaks just after that.
I did not test this function with other systems, but I would assume it would work (grating that the photosensor records luminance and not just the change in luminance).
Briefly, this function does the following: 1. Detect the photosensor spikes onsets 2. Add triggers at the detected onset latencies 3. For each trigger, check whether there is an associated photosensor trigger, accounting for the fact that the photosensor could appear before or after the associated trigger.
The order of photosensor-trigger seems to depend on how the experiment is coded and reflects whether the trigger lags behind the stimulus or not. I’m still discussing this point with friends, but we don’t have a conclusive explanation for this. Any insight on this is welcomed.
% [eventLatenciesInfo, EEG] = trigger_photo_latency(EEG, eventNames, varargin)
%
% Align the recorded triggers to the onset times recorded by
% Brainproducts' photosensor. Conversion follows the procedure:
% 1. Extract data from photosensor
% 2. Find onset of spikes - spikes reflect changes in contrast
% 3. Add found onsets to EEG.event structure in chronological order
% 4. For eah trigger, find closest spike (within defined tolerance)
% 5. Convert the original onset time of each trigger to its
% assciated spike onset time
%
% In:
% EEG - An EEGLAB data structure
% eventNames - Cell array of strings represenitng the names of the
% triggers to align
%
% Optional:
% photoTrigger - Name to provide to the triggers representing the
% photosensor event onsets. Default: 'photo'
% photoChannel - Channel number (int) or channel name (string) of
% the channel containing the photosensor data
% normalisePhotoData - Logical (true|false). Whether to normalise the
% photosensor data prior to find the peaks. If true,
% data is normalised in range [0,1]. Default: true
%
% peakHeightThresh - Minimum peak height for a photosensor spike to be
% considered an event. This value is empirical and
% it depends on multiple factors (eg. colour of the
% photosensor stimulus). Can be useful to evoid
% considering events changes in diplay contrast not
% associated with the experiment (eg. experiment
% window opening). Deafult: 0
% missedTrigTresh - Maximum acceptable lag (in ms) between recorded
% trigger and photosensor spike. If the lag is
% higher than thism a warning is produced. This
% often occurs if you try to align a trigger that
% does not have an associated photosensor.
% modifyOriginal - Logical (true|false). Whether to return a modified
% copy of the EEG structure containin the
% re-aligned events. Default: true
%
% Out:
% eventLatenciesInfo - Cell array containing delays information for each
% trigger included in eventNames. Each row
% represents a different trigger. Columns
% represent:
% 1. Trigger name
% 2. Trigger idx in modified EEG.event structure
% 3. Array of delays (ms) for each trigger
% 4. Average delay (ms) for each trigger
% EEG - Modified copy of the EEG structure where the
% EEG.event structure now contains:
% 1. Photosensor events as triggers
% 2. Onset of triggers alligned with the
% photosensor events
% Author: Daniele Scanzi
function [eventLatenciesInfo, EEG] = trigger_photo_latency(EEG, eventNames, varargin)
% INPUTS
= inputParser;
p
% Mandatory
, 'EEG', @isstruct); % EEG structure with EEG data
addRequired(p, 'eventNames', @iscellstr); % Signal matrix
addRequired(p
% Optional
, 'photoTrigger', 'photo', @ischar); % Name of the trigger to add
addParameter(p, 'photoChannel', 64); % Channel containing the photosensor data
addParameter(p, 'normalisePhotoData', true, @islogical); % Whether to normalise the data or not
addParameter(p, 'peakHeightThresh', 0, @isnumeric); % Minimum height to consider something a peak
addParameter(p, 'missedTrigTresh', 16, @isnumeric); % Max delay (in ms) acceptable
addParameter(p, 'modifyOriginal', true, @islogical); % Whether to modify the original EEG structure or not
addParameter(p
, EEG, eventNames, varargin{:});
parse(p
= p.Results.EEG;
EEG = p.Results.eventNames;
eventNames = p.Results.photoTrigger;
photoTrigger = p.Results.photoChannel;
photoChannel = p.Results.normalisePhotoData;
normalisePhotoData = p.Results.peakHeightThresh;
peakHeightThresh = p.Results.missedTrigTresh;
missedTrigTresh = p.Results.modifyOriginal;
modifyOriginal
%% MAIN FUNCTION
% Check that event structure is in EEG structure
if ~isfield(EEG, "event")
error("Cannot find event structure. Is EEG an EEGLAB structure?")
end
% Check that provided events exist
for iEvent = 1:length(eventNames)
if ~any(strcmp({EEG.event.type}, eventNames{iEvent}))
error("Cannot find %s in EEG.event structure", eventNames{iEvent})
end
end
% Check that data contains the channel requested
if isinteger(photoChannel)
if ~size(EEG.data, 1) < photoChannel
error("Channel %i out of bound of data size %i", photoChannel, size(EEG.data, 1))
end
elseif ischar(photoChannel) || isstring(photoChannel)
if ~any(strcmp({EEG.chanlocs.labels}, photoChannel))
error("Channel %s not found", photoChannel)
else
% find channel number and overwrite string
= find(strcmp({EEG.chanlocs.labels}, photoChannel));
photoChannel end
end
% The function should run even with epoched data, but this has not been
% tested yet. There are no many reasons for using this function with
% epoched data anyway
if ndims(EEG.data) > 2
warning("Function not tested with epoched data (or data with more " + ...
"than two dimensions in general" )
end
% Extract photsensor data (account for possibility of data being
% epoched). Correct peaks so to find photosensor onset
= EEG.data(photoChannel, :, :);
photoData = diff(photoData);
photoData
if normalisePhotoData
= normalize(photoData, 'range');
photoData end
% Find peaKs
~, peaksLocs] = findpeaks(photoData, 'MinPeakHeight', peakHeightThresh);
[
% Add peaks to the event structure data
fprintf("Adding photosensor events to EEG.event structure")
% Add peaks to chanloc values
for iPeak = 1:length(peaksLocs)
end+1).latency = peaksLocs(iPeak);
EEG.event(end).type = photoTrigger;
EEG.event(end
% Reorder events by latency
= eeg_checkset(EEG,'eventconsistency');
EEG
% First find triggers indices
= {length(eventNames), 4};
eventLatenciesInfo for iEvent = 1:length(eventNames)
, 1} = eventNames{iEvent};
eventLatenciesInfo{iEvent, 2} = find(strcmp({EEG.event.type}, eventNames{iEvent}));
eventLatenciesInfo{iEventend
% Compute event latencies finding the nearest photosensor event
for iEvent = 1:size(eventLatenciesInfo, 1)
% create arry to store latencies
= nan(length(eventLatenciesInfo{iEvent, 2}), 1);
currentLatencies for iTrig = 1:length(eventLatenciesInfo{iEvent, 2})
% Check previous event
= EEG.event(eventLatenciesInfo{iEvent, 2}(iTrig)-1);
previousTrigger = EEG.event(eventLatenciesInfo{iEvent, 2}(iTrig)+1);
nextTrigger if strcmp({previousTrigger.type}, photoTrigger)
= EEG.event(eventLatenciesInfo{iEvent, 2}(iTrig)).latency - EEG.event(eventLatenciesInfo{iEvent, 2}(iTrig)-1).latency;
previousLatencyDiff else
% if there is no photosensor trigger before, set this diff
% tp -Inf
= nan;
previousLatencyDiff end
% Check next event
if strcmp({nextTrigger.type}, photoTrigger)
= EEG.event(eventLatenciesInfo{iEvent, 2}(iTrig)+1).latency - EEG.event(eventLatenciesInfo{iEvent, 2}(iTrig)).latency;
nextLatencyDiff else
% if there is no photosensor trigger before, set this diff
% tp -Inf
= nan;
nextLatencyDiff end
% Find minimum between the two
, minIdx] = min([previousLatencyDiff nextLatencyDiff]);
[currentMinif currentMin > missedTrigTresh
warning("Possible missing triggers around trigger number %i. Event not included in latency calculation \n", eventLatenciesInfo{iEvent, 2}(iTrig));
= nan;
currentLatencies(iTrig) else
= currentMin;
currentLatencies(iTrig) end
% Modify the original dataset event structure if requested
if modifyOriginal
if minIdx == 1
, 2}(iTrig)).latency = previousTrigger.latency;
EEG.event(eventLatenciesInfo{iEventelseif minIdx == 2
, 2}(iTrig)).latency = nextTrigger.latency;
EEG.event(eventLatenciesInfo{iEventend
end
end
% Compute average latency excluding missing triggers
= currentLatencies(~isnan(currentLatencies));
currentLatencies , 3} = currentLatencies;
eventLatenciesInfo{iEvent, 4} = mean(currentLatencies);
eventLatenciesInfo{iEventend
= eeg_checkset(EEG);
EEG end
% Helper
function isText(myVar)
|| ischar(myVar);
isstring(myVar) end
GNU statement
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 (https://www.gnu.org/licenses).