Skip to content

Instantly share code, notes, and snippets.

@neuromechanist
Last active December 12, 2023 20:04
Show Gist options
  • Save neuromechanist/c51d93575c7ad6af565d93a2d3b11cc6 to your computer and use it in GitHub Desktop.
Save neuromechanist/c51d93575c7ad6af565d93a2d3b11cc6 to your computer and use it in GitHub Desktop.
Expand the Present Movie annotation
function EEG = expand_events(EEG, stim_file, stim_column, type_name, resample_beyond_thres, remove_unused_cols, cols_to_keep)
%EXPAND_EVENTS Expands EEG.event structure with contencts of the stim_file
% With complex electrophys tasks such as watching a movie, the task events could
% be very long and repetitive across subjects. Instead, we can put the
% events in a separate STIM_FILE and expand EEGLAB's EEG.event strucutre
% only for processing. This feature will also help to plugin alternative
% events, if a researcher comes with their own event markers and
% annotation for the same stimulation.
%
% INPUTS:
% EEG: EEGLAB's EEG structure
% STIM_FILE: The path to the stimulation file needed for EEG.event
% expansion.
% STIM_COLUMN: The column that should be used form the stim file to
% expand the EEG.event. default is "VALUE".
% TYPE_NAME: Since the type column in EEG.event is usually filled
% with the Value column in the BIDS *_events.tsv, we expect not fill
% this column at all here. However, this results in a EMPTY flag
% generated by EEGLAB. To avoid this issue, user can create a filler.
% Default is 'stimuli'.
% RESAMPLE_BEYOND_5p: In case there is discrepancy between the
% key_point times, should the STIM_FILE be resampled beyon 5% of it's
% length. If the difference is <5%, the resmapling will be
% performed. Default is 0.
% REMOVE_UNUSED_COLS: If set to 1, it will remove the EEG.event columns that
% were not explicitly used by EEGLAB, or requested by user via COLS_TO_KEEP
% from the EEG.event structure. Defaults is 0.
% COLS_TO_KEEP: The columns beyond the EEGLAB builin colums that user
% want to keep. in EEG.event EEGLAB has reseverd columns in EEG.event,
% that are 'type', d'uration', 'latency', 'urevent', and 'epoch'. THE
% EEGLAB COLUMNS ARE PROTECTED from removal. Add any other column
% that you WANT TO STAY. Defauls is STIM_COLUMN.
%
% OUTPUTS:
% EEG: EEGLAB's EEG strucutre with the expanded EEG.event.
%
% (c) Seyed Yahya Shirazi, 12/2023 SCCN, INC, UCSD
%% Initialize
if ~exist('EEG','var') || isempty(EEG) || ~isstruct(EEG), error("No EEG strucutre is detected!"); end
if ~exist('stim_file','var') || isempty(stim_file)
warning("No explicit STIM_FILE provided, will try to get it from BIDS stim directory");
stim_file = []; % not yet implemented
end
if ~exist('stim_column','var') || isempty(stim_column)
warning("No STIM_COLUMN is provided, will use the VALUE column of the STIM_FILE by default")
stim_column = "value";
end
if ~exist('type_name','var') || isempty(type_name), type_name = 'stimuli'; else, type_name = char(type_name); end
if ~exist('resample_beyond_thres','var') || isempty(resample_beyond_thres), resample_beyond_thres = 0; end
if ~exist('remove_unused_cols','var') || isempty(remove_unused_cols), remove_unused_cols = 0; end
if ~exist('cols_to_keep','var') || isempty(cols_to_keep), cols_to_keep = ["type", "latency", "urevent", "duration", "epoch", stim_column]; end
required_columns = ["onset", "duration"];
discrepancy_threshold = 0.05;
%% load and extract STIM-FILE events
opts = detectImportOptions(stim_file, "FileType", "text");
opts = setvartype(opts, 'string'); % crtitical to import everything as string/char
stim_table = readtable(stim_file, opts);
% check if the required columns and stim_colums are presents
if ~all(contains(required_columns,stim_table.Properties.VariableNames))
error("required columns (i.e., ONSET and DURATION) are not in the stim file.")
end
if ~all(contains(stim_column,stim_table.Properties.VariableNames))
error("The stim_columns provided as an input is not included in the stim file.")
end
% convert onset and duration to double entities
stim_table = convertvars(stim_table, required_columns, "double");
%% check the time discrepancy
% first find the keys points shared in EEG.event and stim_table
EEG_event_keys = string({EEG.event.type});
% find the entries with the same name in each column
for s = stim_column
keys_present.(s).idx = find(contains(stim_table{:, s}, EEG_event_keys));
if isempty(keys_present.(s).idx)
warning("column" + s + " does not contain any of the keywords in the EEG.event.type. Skipping the column.")
stim_column(s==stim_column) = [];
else
% If line below is confusing, EEG_event_keys: row vector, the other variable: column vector.
keys_present.(s).map = (stim_table{:, s} == EEG_event_keys);
keys_present.(s).eeg_event_idx = find(any(keys_present.(s).map,1));
end
end
%% identify the time difference
discrepancy_flag = 0;
for s = stim_column
if length(keys_present.(s).idx) == 1
disp("The length of common values for " + s + " is ONE, so there is no time discrepancy.")
else
keys_present.(s).eeg_timediff = diff([EEG.event(keys_present.(s).eeg_event_idx).latency])/EEG.srate;
keys_present.(s).timediff = diff(stim_table{keys_present.(s).idx, "onset"});
keys_present.(s).discrepancy = abs(keys_present.(s).eeg_timediff - keys_present.(s).timediff);
disp("The EEG.event and the " + s + " colomn has " + ...
string(mean(keys_present.(s).discrepancy)) + " seconds difference");
end
if (mean(keys_present.(s).discrepancy) / max(keys_present.(s).eeg_timediff)) > (max(keys_present.(s).eeg_timediff) * discrepancy_threshold)
discrepancy_flag = 1;
warning("The difference between the EEG.event length and the corresponding events in the TSV file are beyon the threshold")
if ~resample_beyond_thres, error("can't correct the timestamps, so will exit w/o results"); end
end
end
%% correct the time difference
duplicate_flag = 0;
for s = stim_column
% this loop should be peformed once for columns with the same insetion points.
if find(s==stim_column)>1 && all(keys_present.(s).map == keys_present.(stim_column(1)).map,"all")
duplicate_flag = 1;
break;
end
if ~(length(keys_present.(s).idx) == 1) && (discrepancy_flag == 0 || resample_beyond_thres)
for i = 1:length(keys_present.(s).discrepancy)
correct_ratio = keys_present.(s).eeg_timediff/keys_present.(s).timediff;
stim_table{:, "onset"} = (stim_table{:, "onset"} - stim_table{keys_present.(s).idx(2*(i-1)+1), "onset"}) * correct_ratio ...
+ stim_table{keys_present.(s).idx(2*(i-1)+1), "onset"};
stim_table{:, "duration"} = stim_table{:, "duration"} * correct_ratio;
end
end
end
%% pull a uniform idx to import to EEG.event
keys_present.summary.idx = [];
keys_present.summary.eeg_event_idx = [];
for s = stim_column
keys_present.summary.idx = [keys_present.summary.idx, keys_present.(s).idx'];
keys_present.summary.eeg_event_idx = [keys_present.summary.eeg_event_idx keys_present.(s).eeg_event_idx];
end
[uidx, ia] = unique(keys_present.summary.idx); eeg_uidx = keys_present.summary.eeg_event_idx(ia);
[keys_present.summary.uidx, is] = sort(uidx); keys_present.summary.eeg_event_uidx = eeg_uidx(is);
%% import the events to EEG.event
if length(keys_present.summary.idx) == 1, keys_present.summary.idx(end+1) = height(stim_table); end
for i = 1:length(keys_present.summary.uidx) / 2
e0idx = keys_present.summary.eeg_event_uidx(2*(i-1)+1); % idx0 of the segment in EEG.event
t0idx = keys_present.summary.uidx(2*(i-1)+1); % idx0 of the segment in the table
temp_events = EEG.event(keys_present.summary.eeg_event_uidx(2*i):end);
EEG.event(keys_present.summary.eeg_event_uidx(2*i):end) = [];
for j = 0:(keys_present.summary.uidx(2*i) - keys_present.summary.uidx(2*(i-1)+1))
EEG.event(e0idx+j).latency = EEG.event(e0idx).latency + ...
round((stim_table{t0idx+j,"onset"} - stim_table{t0idx,"onset"}) * EEG.srate);
if j ~= 0 && j ~= (keys_present.summary.uidx(2*i) - keys_present.summary.uidx(2*(i-1)+1)) % do not replace EEG.type for the first and last
EEG.event(e0idx+j).type = type_name;
end
for s = stim_column
EEG.event(e0idx+j).(s) = char(stim_table{t0idx+j, s});
end
end
for t = string(fieldnames(temp_events))'
for k = 0: (length(temp_events)-1)
EEG.event(end+k).(t) = temp_events(k+1).(t);
end
end
end
%% delete unused columns
% To ensure smooth operation of the EEGLAB suite, sometimes it is good to
% remove unnecassary columns.
if remove_unused_cols
event_fields = string(fieldnames(EEG.event))';
fields_to_remove = event_fields(~contains(event_fields, cols_to_keep));
EEG.event = rmfield(EEG.event, fields_to_remove);
end
EEG = eeg_checkset(EEG, 'makeur');
expand_table = "the_present_stimulus-LogLumRatio.tsv";
for e = 1:length(EEG)
EEG(e) = expand_events(EEG(e), expand_table, ["shot_number", "LLR"],'shots', 0, 1);
end
ALLEEG = EEG;
onset duration shot_number LLR
0 n/a video_start video_start
0 7.25 1 n/a
7.25 3.542 2 -1.557820733
10.792 5.208 3 0.3358234903
16 5 4 -0.03306866929
21 4.208 5 -0.2070276568
25.208 9.375 6 0.04327900913
34.583 0.792 7 -0.1644478802
35.375 1.333 8 -0.06762669846
36.708 3.167 9 0.1579466073
39.875 3.292 10 0.2663627968
43.167 3.5 11 -0.2664832696
46.667 2.333 12 0.04315495832
49 1.917 13 -0.04155285906
50.917 4.125 14 0.04001208653
55.042 2.917 15 -0.08485806694
57.958 2.042 16 0.008346347475
60 3.958 17 0.02230250862
63.958 1.458 18 -0.06699408597
65.417 1.083 19 0.04519596032
66.5 1.917 20 0.07562682269
68.417 2.208 21 -0.09718166316
70.625 4.667 22 0.04645434208
75.292 1.667 23 0.01865315886
76.958 0.833 24 0.02023945652
77.792 1.583 25 -0.01044289204
79.375 2.292 26 0.07107341152
81.667 4.5 27 -0.07068154365
86.167 2.625 28 -0.09355068459
88.792 2.542 29 0.127618289
91.333 5.583 30 -0.03570772929
96.917 4.458 31 0.1251853136
101.375 3.042 32 -0.1428285775
104.417 1.25 33 0.0998938008
105.667 0.792 34 -0.007754262065
106.458 1.5 35 -0.06120459462
107.958 0.667 36 -0.02361673076
108.625 2.208 37 0.01752653383
110.833 3 38 0.07973702237
113.833 2.292 39 -0.08752530512
116.125 1 40 0.05044258775
117.125 2.25 41 -0.0466125638
119.375 2.708 42 0.09235464595
122.083 3.375 43 -0.1067268907
125.458 6.292 44 0.02212250449
131.75 4.833 45 0.1053584879
136.583 3.583 46 -0.06164775268
140.167 1.708 47 0.001148701321
141.875 4.292 48 -0.1751332264
146.167 4.708 49 0.1760542644
150.875 3.667 50 -0.06821788465
154.542 2.958 51 0.01782704926
157.5 3.125 52 -0.0693816018
160.625 1.833 53 0.0899746917
162.458 2.792 54 0.09733030527
165.25 6.667 55 -0.2270603551
171.917 31.292 56 0.1188704433
203.208 n/a video_stop video_stop
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment