ImaGIN_SpikesDetection.m
13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
function ImaGIN_SpikesDetection(S)
% This function extracts and concatenates the baselines from cropped, qualitatively controlled, stimulation files.
% It runs Delphos' spike detection on extracted baselines after bipolar montage computation.
% It then computes corrected spike rates for each bipolar recording channel.
% /!\ Cropped files CAN have a variable number of channels AND different sampling rates, the monopolar <-> bipolar mapping has to be established for each cropped file
% and taken into account during baseline concatenation for consistency AND cropped files with the same sampling rate have to be concatenated together. /!\
%% Vars parsing
stims_files = S.stims_files;
path_out = S.path_out;
channels = struct;
%% Extract data from monopolar stimulation files
for ff=1:numel(stims_files)
% Establish the mappping for the stimulation
stim_mapping = monopolar_to_bipolar_mapping(stims_files{ff},path_out);
% Load the stimulation for data extraction
D = spm_eeg_load(stims_files{ff});
% Get the samping frequency
sampling(ff) = fsample(D);
% Get the coordinates
sensors_info = sensors(D,'EEG');
% Get the list of bad monopolar channels
all_channels_idxes = indchantype(D,'ALL');
good_eeg_channels_idxes = indchantype(D,'EEG','GOOD');
mono_bad_channel_idxes = setdiff(all_channels_idxes,good_eeg_channels_idxes);
% Use the mapping to identify monopolar channels later coupled with bad channels
bp_bad_channels_idxes = [];
for cc=1:numel(mono_bad_channel_idxes)
bad_channel_1_map = find(mono_bad_channel_idxes(cc) == [stim_mapping.mono_idx1]);
bad_channel_2_map = find(mono_bad_channel_idxes(cc) == [stim_mapping.mono_idx2]);
circular_bp_bad_channels_idxes = unique([stim_mapping(bad_channel_1_map).mono_idx1,...
stim_mapping(bad_channel_1_map).mono_idx2,...
stim_mapping(bad_channel_2_map).mono_idx1,...
stim_mapping(bad_channel_2_map).mono_idx2]);
bp_bad_channels_idxes = unique([bp_bad_channels_idxes circular_bp_bad_channels_idxes]);
end
% Concatenate all identified bad channels
all_bad_channels_idxes = unique([mono_bad_channel_idxes bp_bad_channels_idxes]);
% Extract baseline of the stimulation and zero all bad channels
time_axis = time(D);
onset = timeonset(D);
negative_time_idxes = find(time_axis > onset+1 & time_axis < -1); % Arbitrary time margins of 1 s around prestimulus period
file_baselines = D(:,negative_time_idxes,1);
file_baselines(all_bad_channels_idxes,:,1) = zeros(numel(all_bad_channels_idxes),numel(negative_time_idxes));
% Generate the corresponding boolean matrix to identify good (1) versus bad (0) samples
file_zeroed = ones(size(file_baselines));
file_zeroed(all_bad_channels_idxes,:,1) = zeros(numel(all_bad_channels_idxes),numel(negative_time_idxes));
% Update the channels struct
eeg_chans_idxes = indchantype(D,'EEG');
for cc=1:numel(eeg_chans_idxes)
channel_name = chanlabels(D,eeg_chans_idxes(cc));
if isvarname(channel_name{:})
channels.(channel_name{:}).timeserie{ff,:} = file_baselines(eeg_chans_idxes(cc),:,:);
channels.(channel_name{:}).zeroed{ff,:} = file_zeroed(eeg_chans_idxes(cc),:,:);
channels.(channel_name{:}).coordinates{ff,:} = sensors_info.elecpos(find(strcmp(channel_name,sensors_info.label)),:);
end
end
% Tidy the channels struct with empty inputs for consistency
temp_montage = fieldnames(channels);
for nn=1:numel(temp_montage)
if sum(strcmp(temp_montage{nn},chanlabels(D))) == 0
channels.(temp_montage{nn}).timeserie{ff} = [];
channels.(temp_montage{nn}).zeroed{ff} = [];
channels.(temp_montage{nn}).coordinates{ff} = [];
end
end
end
%% Test for extracted baseline sizes and fill-in missing channels
global_montage = fieldnames(channels);
for ff=1:numel(stims_files)
for nn=1:numel(global_montage)
series_length(nn) = numel(channels.(global_montage{nn}).timeserie{ff,:});
end
empty_series = find(series_length == 0);
nonempty_series = find(series_length);
valid_length = series_length(nonempty_series);
if isempty(valid_length) % If valid_length is empty that means there are no extracted baselines in any channels for this stimulation (baseline is too short).
valid_length = 1;
end
if all(valid_length == valid_length(1))
for ee=1:numel(empty_series)
channels.(global_montage{empty_series(ee)}).timeserie{ff,:} = zeros(1,valid_length(1));
channels.(global_montage{empty_series(ee)}).zeroed{ff,:} = zeros(1,valid_length(1));
channels.(global_montage{empty_series(ee)}).coordinates{ff,:} = NaN(1,3);
end
else
error('Inconsistent length of channels.');
end
end
%% Create monopolar baseline objects
unique_srates = unique(sampling);
for ss=1:numel(unique_srates)
clear srate labels timeseries zeroed coordinates;
srate = unique_srates(ss);
for nn=1:numel(global_montage)
labels{nn} = global_montage{nn};
timeseries(nn,:) = [channels.(global_montage{nn}).timeserie{find(sampling==srate)}];
zeroed(nn,:) = [channels.(global_montage{nn}).zeroed{find(sampling==srate)}];
all_coordinates = cell2mat(channels.(global_montage{nn}).coordinates(:));
[lines cols] = find(~isnan(all_coordinates));
coordinates_with_values = all_coordinates(unique(lines),:);
if isempty(coordinates_with_values)
coordinates(nn,:) = NaN(1,3);
elseif sum([range(coordinates_with_values(:,1)) == 0, range(coordinates_with_values(:,2)) == 0, range(coordinates_with_values(:,3)) == 0]) == 3
coordinates(nn,:) = coordinates_with_values(1,:);
else
error('Inconsistent electrodes coordinates between cropped files.');
end
end
rates_tables{ss} = spikes_from_monopolar(srate,labels,timeseries,zeroed,coordinates,path_out);
end
rates_labels = {};
rates_scores = [];
for rr=1:numel(rates_tables)
rates_labels = [rates_labels rates_tables{rr}{:,1}];
current_rates = rates_tables{rr}{:,2};
current_rates(isinf(current_rates)) = NaN;
rates_scores = [rates_scores current_rates];
end
for ll=1:size(rates_labels,1)
if all(strcmp(rates_labels(ll,:),rates_labels{ll,1}))
bipolar_labels{ll} = rates_labels{ll,1};
else
error('Inconsistent labels between bipolar baselines');
end
end
mean_rates = mean(rates_scores,2,'omitnan');
% Store rate table in an individual files for easy extraction
rate_table_file = 'SPK_bBaseline_rates.mat';
spike_rates = [bipolar_labels',num2cell(mean_rates)];
save(fullfile(path_out,rate_table_file), 'spike_rates');
% Ending step
set_final_status('OK');
disp('Done');
end
function channels_map = monopolar_to_bipolar_mapping(monopolar_file,path_out)
mono_stimulation_obj = spm_eeg_load(monopolar_file);
monopolar_sensors = sensors(mono_stimulation_obj,'EEG');
[mono_path,mono_name,mono_ext] = fileparts(monopolar_file);
Sbp.Fname = monopolar_file;
Sbp.FileOut = fullfile(path_out,['b' mono_name]);
bp_stimulation_obj = ImaGIN_BipolarMontage(Sbp);
for cc=1:nchannels(bp_stimulation_obj)
bipolar_chan = chanlabels(bp_stimulation_obj,cc);
mono_chans = regexp(bipolar_chan,'-','split');
channels_map(cc).bp_chan = bipolar_chan;
channels_map(cc).bp_idx = cc;
channels_map(cc).mono_chan1 = mono_chans{1}{1};
channels_map(cc).mono_chan2 = mono_chans{1}{2};
% Get the monopolar channels idxes.
chan1_idx = find(strcmp(monopolar_sensors.label,mono_chans{1}{1}));
chan2_idx = find(strcmp(monopolar_sensors.label,mono_chans{1}{2}));
% Test and correct for duplicated labels in the monopolar object
if numel(chan1_idx) == 0
error('Could not find the monopolar channel from the bipolar montage.');
elseif numel(chan1_idx) == 1 % If 1 match, as expected, do nothing
%pass
else
error('2 or more channels with the same label')
end
% Same logic as for chan1_idx
if numel(chan2_idx) == 0
error('Could not find the monopolar channel from the bipolar montage.');
elseif numel(chan2_idx) == 1
%pass
else
error('2 or more channels with the same label')
end
channels_map(cc).mono_idx1 = chan1_idx;
channels_map(cc).mono_idx2 = chan2_idx;
end
delete([Sbp.FileOut '.mat']);delete([Sbp.FileOut '.dat']);delete([Sbp.FileOut '_log.txt']);
end
function rates_table = spikes_from_monopolar(srate,labels,timeseries,zeroed,coordinates,path_out)
% Create the new sensor
new_sensors_info.balance.current = 'none';
new_sensors_info.chanpos = coordinates;
new_sensors_info.chantype = cell(1,numel(labels)); new_sensors_info.chantype(:) = {'eeg'};
new_sensors_info.chanunit = cell(1,numel(labels)); new_sensors_info.chanunit(:) = {'V'};
new_sensors_info.elecpos = coordinates;
new_sensors_info.label = labels;
new_sensors_info.type = 'ctf';
new_sensors_info.unit = 'mm';
% Create the new files
file_header = fullfile(path_out,['monopolar_baseline_' num2str(srate) 'Hz.mat']);
file_data = fullfile(path_out,['monopolar_baseline_' num2str(srate) 'Hz.dat']);
if exist(file_header, 'file') == 2
delete(file_header);
end
if exist(file_data, 'file') == 2
delete(file_data);
end
% Create the new object
D = meeg(size(timeseries,1),size(timeseries,2),1);
D = fname(D,['monopolar_baseline_' num2str(srate) 'Hz']);
D = path(D,path_out);
D = fsample(D,srate);
D = sensors(D,'EEG',new_sensors_info)
D = blank(D) ;
D = link(D,file_data);
D(:,:,1) = timeseries;
save(D);
%% Compute bipolar montage on baseline object to get the bipolar baseline object
Sbp.Fname = file_header;
Sbp.FileOut = fullfile(path_out,['bipolar_baseline_' num2str(srate) 'Hz']);
bp_baseline_obj = ImaGIN_BipolarMontage(Sbp);
global_mapping = monopolar_to_bipolar_mapping(Sbp.Fname,path_out);
% Estimate bipolar boolean matrix from monopolar boolean matrix
bp_concat_zeroed = zeros(size(bp_baseline_obj));
for cc=1:numel(global_mapping)
both_good = (zeroed(global_mapping(cc).mono_idx1,:,1) & zeroed(global_mapping(cc).mono_idx2,:,1));
bp_concat_zeroed(cc,find(both_good)) = ones(1,numel(find(both_good)));
end
% Run Delphos' spike detection on baselines
fs = bp_baseline_obj.fsample;
results = Delphos_detector(bp_baseline_obj,chanlabels(bp_baseline_obj),'SEEG',fs,{'Spk'},[],[],40,[]);
% Calculate and store corrected spike rates in a table
total_samples = nsamples(bp_baseline_obj);
total_times = [];
for cc=1:nchannels(bp_baseline_obj)
zeroed_samples = numel(find(~bp_concat_zeroed(cc,:,1)));
total_times(cc) = (total_samples - zeroed_samples) /fs/60; % Total recording time in minutes
end
chans = [results.labels]';
rates = round(results.n_Spk./total_times',1); % Round to nearest decimal
rates_table = table(chans,rates);
% Store spike features in a table
spks_positions = [results.markers.position]';
spks_type = {results.markers.label}';
spks_channels = {results.markers.channels}';
spks_values = {results.markers.value}';
spks_table = table(spks_channels,spks_type,spks_positions,spks_values);
% Store all tables in a .mat
[~,bname,~] = fileparts(bp_baseline_obj.fname);
spkFileName = ['Results_' bname '.mat'];
spkFile.spikeRate = rates_table;
spkFile.markers = spks_table;
spkFile.baseline = ['Total duration of ' num2str((nsamples(bp_baseline_obj)/fsample(bp_baseline_obj))/60) ' minutes'];
spkFile.comment = 'Spike rate is number of spikes per minute whithin a bipolar channel';
results_file = fullfile(path_out,spkFileName);
save(results_file, 'spkFile');
end