ImaGIN_Electrode.m
17.2 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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
function ImaGIN_Electrode(S)
% Set electrode positions.
% -=============================================================================
% This function is part of the ImaGIN software:
% https://f-tract.eu/
%
% This software is distributed under the terms of the GNU General Public License
% as published by the Free Software Foundation. Further details on the GPLv3
% license can be found at http://www.gnu.org/copyleft/gpl.html.
%
% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE AUTHORS
% DO NOT ASSUME ANY LIABILITY OR RESPONSIBILITY FOR ITS USE IN ANY CONTEXT.
%
% Copyright (c) 2000-2018 Inserm U1216
% =============================================================================-
%
% Authors: Olivier David, Francois Tadel
% % Test findChannel()
% ListCSV = {'A01', 'A02', 'A03', 'A04', 'A05', 'A18', 'Bp01', 'Bp02', 'BP01', 'BP04', 'Pp01', 'T101', 'T111', 'V101', 'V201'};
% disp(['pp1: ', ListCSV{findChannel('pp1', ListCSV)}])
% disp(['A01: ', ListCSV{findChannel('A01', ListCSV)}])
% disp(['a01: ', ListCSV{findChannel('a01', ListCSV)}])
% disp(['a1: ', ListCSV{findChannel('a1', ListCSV)}])
% disp(['a 1: ', ListCSV{findChannel('a 1', ListCSV)}])
% disp(['A18: ', ListCSV{findChannel('A18', ListCSV)}])
% disp(['B''1: ', ListCSV{findChannel('B''1', ListCSV)}])
% disp(['b''01: ', ListCSV{findChannel('b''01', ListCSV)}])
% disp(['Bp01: ', ListCSV{findChannel('Bp01', ListCSV)}])
% disp(['Bp1: ', ListCSV{findChannel('Bp1', ListCSV)}])
% disp(['bp1: ', ListCSV{findChannel('bp1', ListCSV)}])
% disp(['B,1: ', ListCSV{findChannel('B,1', ListCSV)}])
% disp(['B, 01: ', ListCSV{findChannel('B, 01', ListCSV)}])
% disp(['BP01: ', ListCSV{findChannel('BP01', ListCSV)}])
% disp(['Bp2: ', ListCSV{findChannel('Bp2', ListCSV)}])
% disp(['B''2: ', ListCSV{findChannel('B''2', ListCSV)}])
% disp(['B, 2: ', ListCSV{findChannel('B, 2', ListCSV)}])
% disp(['BP4: ', ListCSV{findChannel('BP4', ListCSV)}])
% disp(['T111: ', ListCSV{findChannel('T111', ListCSV)}])
% disp(['T11: ', ListCSV{findChannel('T11', ListCSV)}])
% disp(['V101: ', ListCSV{findChannel('V101', ListCSV)}])
% disp(['V11: ', ListCSV{findChannel('V11', ListCSV)}])
% disp(['V101: ', ListCSV{findChannel('V101', ListCSV)}])
% disp(['V21: ', ListCSV{findChannel('V21', ListCSV)}])
% return;
% Get file to edit
try
t = S.Fname;
catch
t = spm_select(Inf, '\.mat$', 'Select data file');
end
if isempty(t)
return;
end
P = spm_str_manip(deblank(t(1,:)),'h');
% Read sensor names
Position = [];
try
Name = S.Name;
catch
try
filename = S.filenameName;
catch
filename = spm_select(1, '\.(csv|txt)$', 'Select txt file for electrode names', {}, P);
end
% Test for input file
if isempty(filename) || ~exist(filename, 'file')
disp('ImaGIN> ERROR: Invalid input file name.');
return
end
% Detect file type: _Name.txt/_Pos.txt or .csv
[fPath, fBase, fExt] = fileparts(filename);
% Read file
switch lower(fExt)
case '.txt'
Name = readName(filename);
case '.csv'
[Name, Position] = readCsv(filename);
if all(isnan(Position))
warning('T1pre coordinates not found: possibly patient implanted before 2009.');
end
otherwise
disp('ImaGIN> ERROR: Invalid input file type.');
return
end
if isempty(Name)
disp('ImaGIN> ERROR: No contact names in input files.');
return
end
end
% Read sensor positions
if isempty(Position)
try
Position = S.Position;
catch
try
filename = S.filenamePos;
Position = load(filename);
catch
filename = spm_select(1, '\.txt$', 'Select txt file for electrode positions', {}, P);
Position = load(filename);
end
end
end
% Set positions
chNotFound = {};
chMatchLog = {};
for i0 = 1:size(t,1)
T = deblank(t(i0,:));
% Clone file if requested in input
if (nargin >= 1) && isfield(S, 'FileOut') && ~isempty(S.FileOut)
D = spm_eeg_load(T);
D2 = clone(D, S.FileOut, [D.nchannels D.nsamples D.ntrials]);
D2(:,:,:) = D(:,:,:);
save(D2);
SpmFile = S.FileOut;
else
SpmFile = T;
end
% Load channel names
SpmMat = load(SpmFile);
Sensors = SpmMat.D.sensors.eeg;
if length(unique(Sensors.label))~=length(Sensors.label)
warning('Repeated label in the file.')
end
% Loop on all channels available in the file
for i1 = 1:length(Sensors.label)
sensLtmp = Sensors.label{i1};
sensLtmp(ismember(double(sensLtmp),[',' ';' '-'])) ='';
Sensors.label{i1} = sensLtmp;
iChanPos = findChannel(Sensors.label{i1}, Name, 'all_upper');
% If the channel was already found in the list before: check the best option based on the case
if ~isempty(iChanPos) && ~isempty(chMatchLog)
iPrevious = find(strcmp(Name{iChanPos}, chMatchLog(:,2)));
if ~isempty(iPrevious)
% If the new channel has strictly the same case, or if it corresponds to a replaced "prime": remove the previous match
if isequal(Sensors.label{i1}, Name{iChanPos}) || ...
(any(Sensors.label{i1} == '''') && strcmp(strrep(upper(Sensors.label{i1}), '''', 'p'), Name{iChanPos}))
disp(['ImaGIN> WARNING: Channel name conflict: ' Sensors.label{i1} ' matched with ' Name{iChanPos} ', ' chMatchLog{iPrevious,1} ' discarded.']);
chMatchLog(iPrevious,:) = [];
% If both labels are the same regardless of the case, raises an error.
elseif strcmpi(chMatchLog{iPrevious,1},Sensors.label{i1})
error(['Duplicated label found: ' Sensors.label{i1}]);
else
disp(['ImaGIN> WARNING: Channel name conflict: ' chMatchLog{iPrevious,1} ' matched with ' Name{iChanPos} ', ' Sensors.label{i1} ' discarded.']);
iChanPos = [];
end
end
end
% If channel was found
if ~isempty(iChanPos)
% Copy position
Sensors.elecpos(i1,:) = Position(iChanPos,:);
Sensors.chanpos(i1,:) = Position(iChanPos,:);
% Copy channel name from input name file (ADDED BY FT 5-Oct-2018)
chMatchLog{end+1,1} = Sensors.label{i1};
chMatchLog{end,2} = Name{iChanPos};
Sensors.label{i1} = Name{iChanPos};
else
disp(['ImaGIN> WARNING: ' Sensors.label{i1} ' not assigned']);
Sensors.elecpos(i1,:) = NaN;
Sensors.chanpos(i1,:) = NaN;
chNotFound{end+1} = Sensors.label{i1};
end
end
if isempty(chMatchLog)
chMatchLog = repmat({''},1,2);
end
% Electrodes present in the CSV but not found in the SEEG recordings
[~, ~, chTagsCSV] = ImaGIN_select_channels(Name);
[~, ~, chTagsSEEG] = ImaGIN_select_channels(chMatchLog(:,2)');
elecUnused = setdiff(unique(chTagsCSV), unique(chTagsSEEG));
% Add entries in a NEW log file
if ~isempty(chMatchLog)
ImaGIN_save_log(SpmFile, 'Positions added for channels:', chMatchLog(:,1));
end
if ~isempty(chNotFound)
ImaGIN_save_log(SpmFile, 'Unmatched SEEG channels:', chNotFound);
% save unmatched channels in separate .txt file
[unPath, unFile, ~] = fileparts(SpmFile);
if numel(chNotFound) > numel(cell2mat(regexpi(chNotFound,'ecg')))
try
unmatchedFileName = fullfile(unPath, ['contactsunmatched_', unFile,'.txt']);
fid = fopen(unmatchedFileName,'w');
for i = 1:length(chNotFound)
if isempty(regexpi(chNotFound{i},'ecg'))
fprintf(fid,'%s\n', chNotFound{i});
end
end
fclose(fid);
catch
disp('Unmatched channels names SEEG-CSV not saved.')
end
end
end
if ~isempty(elecUnused)
ImaGIN_save_log(SpmFile, 'Unmatched CSV electrodes:', elecUnused);
end
% Match file: Correspondance between SEEG-CSV-LENA conventions
if isfield(S, 'FileTxtOut') && ~isempty(S.FileTxtOut)
try
fid = fopen(S.FileTxtOut,'w');
fprintf(fid,'SEEG,CSV\n');
for i = 1:size(chMatchLog,1)
fprintf(fid,'%s,%s\n', chMatchLog{i,1}, chMatchLog{i,2});
end
fclose(fid);
catch
disp('Log with matched channels names SEEG-CSV not saved.')
end
end
% Replace channel definition in input .mat/.dat file
SpmMat.D.sensors.eeg = Sensors;
% Replace labels in D.channels
for iChan = 1:length(SpmMat.D.channels)
spmLtmp = SpmMat.D.channels(iChan).label;
spmLtmp(ismember(double(spmLtmp),[',' ';' '-'])) ='';
SpmMat.D.channels(iChan).label = spmLtmp;
iChanMatch = find(strcmpi(SpmMat.D.channels(iChan).label, chMatchLog(:,1)));
if (length(iChanMatch) == 1)
SpmMat.D.channels(iChan).label = chMatchLog{iChanMatch,2};
end
end
% Replace labels in events
for iEvt = 1:length(SpmMat.D.trials.events)
% Get channel name from event name
EvtName = SpmMat.D.trials.events(iEvt).type;
[chLabel1, chLabel2, noteNameNew, chInd1, chInd2] = ImaGIN_CleanEventName(EvtName);
if ~isempty(chInd1) && ~isempty(chInd2)
if str2double(chInd1) < str2double(chInd2)
if str2double(chInd1) +1 ~= str2double(chInd2)
chInd2tmp = num2str(str2double(chInd1) + 1);
if numel(chInd1) ~= numel(chInd2)
chInd2tmp = strcat('0',chInd2tmp);
end
chLabel2 = strrep(chLabel2,chInd2,chInd2tmp);
chInd2 = chInd2tmp;
end
elseif str2double(chInd1) > str2double(chInd2)
if str2double(chInd1) ~= str2double(chInd2) + 1
chInd1tmp = num2str(str2double(chInd2) + 1);
if numel(chInd2) ~= numel(chInd1)
chInd1tmp = strcat('0',chInd1);
end
chLabel1 = strrep(chLabel1,chInd1,chInd1tmp);
chInd1 = chInd1tmp;
end
end
end
% Replace with matching CSV name
% This section now uses "Name" extracted from the csv to consider all electrode labels instead of the ones in "chMatchLog" as it used to do because it only considered electrodes which also recorded.
% Note by A. Boyer - 13/02/2020
elec_labels = Name';
elec_labels_no_primes = strrep(elec_labels,'p','''');
csv_all_electrodes = [elec_labels elec_labels_no_primes];
[iChanMatch1,~] = find(strcmpi(chLabel1, csv_all_electrodes));
[iChanMatch2,~] = find(strcmpi(chLabel2, csv_all_electrodes));
if isempty(iChanMatch1) && ~isempty(chInd1)
tmpchLabel1 = strrep(chLabel1, chInd1, num2str(str2double(chInd1)));
[iChanMatch1,~] = find(strcmpi(tmpchLabel1, csv_all_electrodes));
if isempty(iChanMatch1)
tmpchLabel1 = strrep(chLabel1, chInd1, ['0' chInd1]);
[iChanMatch1,~] = find(strcmpi(tmpchLabel1, csv_all_electrodes));
end
end
if isempty(iChanMatch2) && ~isempty(chInd2)
tmpchLabel2 = strrep(chLabel2, chInd2, num2str(str2double(chInd2)));
[iChanMatch2,~] = find(strcmpi(tmpchLabel2, csv_all_electrodes));
if isempty(iChanMatch2)
tmpchLabel2 = strrep(chLabel2, chInd2, ['0' chInd2]);
[iChanMatch2,~] = find(strcmpi(tmpchLabel2, csv_all_electrodes));
end
end
if ~isempty(iChanMatch1)
if sum(iChanMatch1==iChanMatch1(1)) == numel(iChanMatch1)
iChanMatch1 = iChanMatch1(1);
else
warning('Same label found for multiple electrodes');
end
end
if ~isempty(iChanMatch2)
if sum(iChanMatch2==iChanMatch2(1)) == numel(iChanMatch2)
iChanMatch2 = iChanMatch2(1);
else
warning('Same label found for multiple electrodes');
end
end
if (length(iChanMatch1) == 1)
noteNameNew = strrep(noteNameNew, chLabel1,csv_all_electrodes{iChanMatch1,1}); % chMatchLog{iChanMatch1,2}
end
if (length(iChanMatch2) == 1)
noteNameNew = strrep(noteNameNew, chLabel2,csv_all_electrodes{iChanMatch2,1}); % chMatchLog{iChanMatch2,2}
end
if (length(iChanMatch1) == 1) && (length(iChanMatch2) == 1)
SpmMat.D.trials.events(iEvt).type = noteNameNew;
end
end
csv_struct.csv_labels = csv_all_electrodes(:,1);
SpmMat.D.other = csv_struct; % Add an extra field to the .mat so we have a listing of all channel labels in the csv.
% Update existing .mat file
save(SpmFile, '-struct', 'SpmMat');
save(spm_eeg_load(SpmFile)); % SPM's save to create a valid SPM object
end
end
%% Read name from file
function Name = readName(filename)
fid = fopen(filename);
i1 = 1;
while 1
tmp = fgetl(fid);
if isequal(tmp, -1) || isempty(tmp)
break;
end
Name{i1} = tmp;
i1 = i1 + 1;
end
fclose(fid);
end
%% Read name and position from .csv
function [Name, Pos] = readCsv(filename)
Name = {};
Pos = [];
% Open file
fid = fopen(filename);
i1 = 1;
% Skip two header lines
fgetl(fid);
fgetl(fid);
% Read column names (tab-separated file)
colNames = strsplit(fgetl(fid), '\t');
iColName = find(strcmpi(colNames, 'contact'));
iColPos = find(strcmpi(colNames, 'T1pre Scanner Based'));
if isempty(iColName) || isempty(iColPos)
disp('ImaGIN> ERROR: Invalid CSV file: No columns "T1pre Scanner Based" or "contact".');
return;
end
% Read contact by contact
while 1
% Read line
tmp = fgetl(fid);
if isequal(tmp, -1) || isempty(tmp)
break;
end
% Split by columns (tab-separated file)
tmp = strsplit(tmp, '\t');
Name{i1} = tmp{iColName};
if ~isempty(str2num(tmp{iColPos}))
Pos(i1,1:3) = str2num(tmp{iColPos});
else
Pos(i1,1:3) = NaN;
end
i1 = i1 + 1;
end
fclose(fid);
end
%% Find channel name in a list
function iChanPos = findChannel(Label, List, caseType)
% Test three different case versions
% The goal is to be able to handle a List containing both electrodes L' ("Lp") and LP ("LP")
% The List provided here comes from a .csv with this convention: only capitals except for the "p" for "'"
if (nargin < 3) || isempty(caseType)
iChanPos = findChannel(Label, List, 'no_change');
if isempty(iChanPos)
iChanPos = findChannel(Label, List, 'all_upper');
end
if isempty(iChanPos)
iChanPos = findChannel(Label, List, 'upper_except_p');
end
return;
end
% Remove spaces
Label(Label == ' ') = [];
% Switch case type
switch (caseType)
case 'no_change'
% Nothing to change
case 'all_upper'
Label = upper(Label);
case 'upper_except_p'
iP = find(Label == 'p');
Label = upper(Label);
if any(iP >= 2)
Label(iP(end)) = 'p';
end
end
% Replacing ' with p
Label = strrep(Label, '''', 'p');
List = strrep(List, '''', 'p');
% Replacing , with p
Label = strrep(Label, ',', 'p');
List = strrep(List, ',', 'p');
% Look for channel in position file
iChanPos = find(strcmp(Label, List));
if ~isempty(iChanPos)
return;
end
% Channel not found: try adding missing 0 (A1=>A01, T11=>T101)
% Find the last letter in the name
iLastLetter = find(~ismember(Label, '0123456789'), 1, 'last');
% If there is not electrode label or no index: wrong naming
if isempty(iLastLetter) || (iLastLetter == length(Label))
return;
end
% Find channel index
chLabel = Label(1:iLastLetter);
chInd = str2num(Label(iLastLetter+1:end));
% If label is > 100: Add 1 or 2 at the end of the label
if (chInd > 220)
return;
elseif (chInd > 200)
chLabel = [chLabel '2'];
chInd = chInd - 200;
elseif (chInd > 100)
chLabel = [chLabel '1'];
chInd = chInd - 100;
elseif (chInd > 20)
chLabel = [chLabel '2'];
chInd = chInd - 20;
end
% Try with a zero
iChanPos = find(strcmp(sprintf('%s%02d', chLabel, chInd), List));
if ~isempty(iChanPos)
return;
end
% Try without the zero
iChanPos = find(strcmp(sprintf('%s%d', chLabel, chInd), List));
if ~isempty(iChanPos)
return;
end
% For channel indices between 11 and 19, try to interpret them as if electrode name was ending with a 1
if (chInd >= 11) && (chInd <= 19)
iChanPos = find(strcmp(sprintf('%s%02d', [chLabel '1'], chInd - 10), List));
if ~isempty(iChanPos)
return;
end
end
end