Skip to content

Commit ef0b0dd

Browse files
committed
Merge remote-tracking branch 'origin/develop' into 876_bids_taskid
2 parents fa4a674 + 18c64f6 commit ef0b0dd

17 files changed

Lines changed: 418 additions & 47 deletions

doc/Figures/screenshot-000.png

38.4 KB
Loading

doc/Figures/screenshot-002.png

424 KB
Loading

doc/Figures/screenshot-004.png

325 KB
Loading

doc/PsPM_Developers_Guide.lyx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29400,7 +29400,8 @@ In the line 1-5 of a markdown file,
2940029400

2940129401
\begin_layout Standard
2940229402
\begin_inset Graphics
29403-
filename /Users/teddy/Desktop/Screenshot 2025-02-06 at 14.51.44.png
29403+
filename Figures/screenshot-000.png
29404+
scale 50
2940429405

2940529406
\end_inset
2940629407

@@ -29468,7 +29469,7 @@ The second line is the title of this webpage.
2946829469

2946929470
\begin_layout Standard
2947029471
\begin_inset Graphics
29471-
filename /Users/teddy/Desktop/Screenshot 2025-02-06 at 15.14.02.png
29472+
filename Figures/screenshot-002.png
2947229473
width 90text%
2947329474

2947429475
\end_inset
@@ -29612,7 +29613,7 @@ toc-date.html
2961229613

2961329614
\begin_layout Standard
2961429615
\begin_inset Graphics
29615-
filename /Users/teddy/Desktop/Screenshot 2025-02-07 at 03.32.35.png
29616+
filename Figures/screenshot-004.png
2961629617
width 95text%
2961729618

2961829619
\end_inset

doc/PsPM_Developers_Guide.pdf

44.4 KB
Binary file not shown.

doc/PsPM_Manual.lyx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5584,7 +5584,12 @@ Here,
55845584
\end_layout
55855585

55865586
\begin_layout Paragraph
5587-
CED Spike
5587+
CED Spike
5588+
\end_layout
5589+
5590+
\begin_layout Standard
5591+
When importing SMR files,
5592+
channels that are empty are still imported as empty channels rather than being removed.
55885593
\end_layout
55895594

55905595
\begin_layout Paragraph

doc/PsPM_Manual.pdf

125 Bytes
Binary file not shown.

src/bids_importer/lib/get_physio_eye_data.m

Lines changed: 225 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,231 @@
226226
file_paths{1,1} = eye_data_cell{1}.source.file ;
227227
end
228228

229-
% Check if the first data has the StartTime field
230-
if isfield(data{1}.header, 'StartTime')
231-
% Check if all StartTimes are the same
232-
start_times = cellfun(@(x) x.header.StartTime, data, 'UniformOutput', false);
233-
if ~isequal(start_times{:}) ; warning('Not all data have the same StartTime. Please check the input data.'); end
234-
else
235-
% If there is no StartTime field start time will set to 0
236-
for i = 1:length(data); data{i}.header.StartTime = 0; end
237-
end
229+
%% --- Add the eye data to the channels ---
230+
num_eyes = length(eye_data_cell);
231+
switch num_eyes
232+
case 0; warning('No eye data available.');
233+
case 1
234+
eyeSide = lower(eye_data_cell{1}.RecordedEye);
235+
warning('Only %s eye data available.', eyeSide);
236+
237+
if strcmp(eyeSide, 'right')
238+
pupil_r = eye_data_cell{1}.Columns{:,'pupil_size'};
239+
gaze_x_r = eye_data_cell{1}.Columns{:,'x_coordinate'};
240+
gaze_y_r = eye_data_cell{1}.Columns{:,'y_coordinate'};
241+
242+
data{1}.data = pupil_r;
243+
data{1}.header.chantype = 'pupil_r';
244+
data{2}.data = gaze_x_r;
245+
data{2}.header.chantype = 'gaze_x_r';
246+
data{3}.data = gaze_y_r;
247+
data{3}.header.chantype = 'gaze_y_r';
248+
249+
elseif strcmp(eyeSide, 'left')
250+
pupil_l = eye_data_cell{1}.Columns{:,'pupil_size'};
251+
gaze_x_l = eye_data_cell{1}.Columns{:,'x_coordinate'};
252+
gaze_y_l = eye_data_cell{1}.Columns{:,'y_coordinate'};
253+
254+
data{1}.data = pupil_l;
255+
data{1}.header.chantype = 'pupil_l';
256+
data{2}.data = gaze_x_l;
257+
data{2}.header.chantype = 'gaze_x_l';
258+
data{3}.data = gaze_y_l;
259+
data{3}.header.chantype = 'gaze_y_l';
260+
261+
else
262+
warning('Unknown RecordedEye eye_data_cell.');
263+
return
264+
end
265+
case 2
266+
eyes = lower({eye_data_cell{1}.RecordedEye, eye_data_cell{2}.RecordedEye});
267+
if strcmp(eyes{1}, eyes{2})
268+
warning('Both recorded eyes are %s.', eyes{1});
269+
% Maybe choose the better eye? -> it chooses the better depends
270+
% on l or eye
271+
else
272+
% Correctly assign each cell to the corresponding eye.
273+
idxRight = find(strcmp(eyes, 'right'), 1);
274+
idxLeft = find(strcmp(eyes, 'left'), 1);
275+
276+
if isempty(idxRight) || isempty(idxLeft); warning('...');end % ???
277+
278+
pupil_r = eye_data_cell{idxRight}.Columns{:,'pupil_size'};
279+
gaze_x_r = eye_data_cell{idxRight}.Columns{:,'x_coordinate'};
280+
gaze_y_r = eye_data_cell{idxRight}.Columns{:,'y_coordinate'};
281+
282+
pupil_l = eye_data_cell{idxLeft}.Columns{:,'pupil_size'};
283+
gaze_x_l = eye_data_cell{idxLeft}.Columns{:,'x_coordinate'};
284+
gaze_y_l = eye_data_cell{idxLeft}.Columns{:,'y_coordinate'};
285+
286+
% right eye channels
287+
data{1}.header.chantype = 'pupil_r';
288+
data{1}.data = pupil_r;
289+
290+
data{2}.header.chantype = 'gaze_x_r';
291+
data{2}.data = gaze_x_r;
292+
data{3}.header.chantype = 'gaze_y_r';
293+
data{3}.data = gaze_y_r;
294+
295+
% left eye channels
296+
data{4}.header.chantype = 'pupil_l';
297+
data{4}.data = pupil_l;
298+
data{5}.header.chantype = 'gaze_x_l';
299+
data{5}.data = gaze_x_l;
300+
data{6}.header.chantype = 'gaze_y_l';
301+
data{6}.data = gaze_y_l;
302+
303+
end
304+
305+
otherwise; error('Unexpected number of eye data cells.');
306+
307+
308+
end
309+
310+
data = data';
311+
312+
%% Add header data for pupil and gaze data
313+
314+
% For one eye
315+
if num_eyes == 1; idxRight = 1; idxLeft = 1; end
316+
317+
for i = 1:length(data)
318+
% pupil
319+
if strcmp(data{i}.header.chantype(1:end-1) , 'pupil_')
320+
if strcmp(data{i}.header.chantype(end:end) , 'r')
321+
data{i}.header.Description = eye_data_cell{idxRight}.pupil_size.Description;
322+
data{i}.header.units = eye_data_cell{idxRight}.pupil_size.Units;
323+
data{i}.header.sr = eye_data_cell{idxRight}.SamplingFrequency;
324+
325+
elseif strcmp(data{i}.header.chantype(end:end) , 'l')
326+
data{i}.header.Description = eye_data_cell{idxLeft}.pupil_size.Description;
327+
data{i}.header.units = eye_data_cell{idxLeft}.pupil_size.Units;
328+
data{i}.header.sr = eye_data_cell{idxLeft}.SamplingFrequency;
329+
330+
else
331+
warning('No valid pupil channel found.');
332+
end
333+
% gaze
334+
elseif strcmp(data{i}.header.chantype(1:end-4) , 'gaze')
335+
if strcmp(data{i}.header.chantype(6) , 'x')
336+
if strcmp(data{i}.header.chantype(8) , 'r')
337+
% gaze_x_r
338+
if any(strcmp(fieldnames(eye_data_cell{idxRight}),'SampleCoordinateUnits'))
339+
data{i}.header.units = eye_data_cell{idxRight}.SampleCoordinateUnits; % "pixel"
340+
elseif any(strcmp(fieldnames(eye_data_cell{idxRight}),'x_coordinate'))
341+
data{i}.header.units = eye_data_cell{idxRight}.x_coordinate.Units;
342+
else
343+
warning('ID:missing_units', 'Units could not be determined for gaze_x_r channel.');
344+
end
345+
346+
data{i}.header.sr = eye_data_cell{idxRight}.SamplingFrequency;
347+
data{i}.header.range = [eye_data_cell{idxRight}.GazeRange.xmin, eye_data_cell{idxRight}.GazeRange.xmax] ; % e.g. [0 1151]
348+
elseif strcmp(data{i}.header.chantype(8) , 'l')
349+
% gaze_x_l
350+
if any(strcmp(fieldnames(eye_data_cell{idxLeft}),'SampleCoordinateUnits'))
351+
data{i}.header.units = eye_data_cell{idxLeft}.SampleCoordinateUnits; % "pixel"
352+
elseif any(strcmp(fieldnames(eye_data_cell{idxLeft}),'x_coordinate'))
353+
data{i}.header.units = eye_data_cell{idxLeft}.x_coordinate.Units;
354+
else
355+
warning('ID:missing_units', 'Units could not be determined for gaze_x_l channel.');
356+
end
357+
358+
data{i}.header.sr = eye_data_cell{idxLeft}.SamplingFrequency;
359+
data{i}.header.range = [eye_data_cell{idxLeft}.GazeRange.xmin, eye_data_cell{idxLeft}.GazeRange.xmax] ; % e.g. [0 1151]
360+
else
361+
warning('Something went worng with gaze x channels')
362+
end
363+
364+
elseif strcmp(data{i}.header.chantype(6) , 'y')
365+
if strcmp(data{i}.header.chantype(8) , 'r')
366+
% gaze_y_r
367+
if any(strcmp(fieldnames(eye_data_cell{idxRight}),'SampleCoordinateUnits'))
368+
data{i}.header.units = eye_data_cell{idxRight}.SampleCoordinateUnits; % "pixel"
369+
elseif any(strcmp(fieldnames(eye_data_cell{idxRight}),'y_coordinate'))
370+
data{i}.header.units = eye_data_cell{idxRight}.y_coordinate.Units;
371+
else
372+
warning('ID:missing_units', 'Units could not be determined for gaze_y_r channel.');
373+
end
374+
data{i}.header.sr = eye_data_cell{idxRight}.SamplingFrequency;
375+
data{i}.header.range = [eye_data_cell{idxRight}.GazeRange.ymin, eye_data_cell{idxRight}.GazeRange.ymax] ; % e.g. [0 1151]
376+
377+
elseif strcmp(data{i}.header.chantype(8) , 'l')
378+
% gaze_y_l
379+
if any(strcmp(fieldnames(eye_data_cell{idxLeft}),'SampleCoordinateUnits'))
380+
data{i}.header.units = eye_data_cell{idxLeft}.SampleCoordinateUnits; % "pixel"
381+
elseif any(strcmp(fieldnames(eye_data_cell{idxLeft}),'y_coordinate'))
382+
data{i}.header.units = eye_data_cell{idxLeft}.y_coordinate.Units; % should i add a check that x and y are the same units?
383+
else
384+
warning('ID:missing_units', 'Units could not be determined for gaze_y_l channel.');
385+
end
386+
387+
data{i}.header.sr = eye_data_cell{idxLeft}.SamplingFrequency;
388+
data{i}.header.range = [eye_data_cell{idxLeft}.GazeRange.ymin, eye_data_cell{idxLeft}.GazeRange.ymax] ; % e.g. [0 1151]
389+
390+
else
391+
warning('Something went worng with gaze y channels')
392+
end
393+
end
394+
end
395+
end
396+
397+
%% --- Build the eye infos.source ----
398+
399+
% --- infos.source ---
400+
infos.source = struct();
401+
infos.source.chan = {} ;% {'Column 02'} {'Column 01'}?
402+
infos.source.chan_stats = cell(length(data), 1); % nan_stats
403+
404+
% Calculating the nan ratio
405+
for i = 1:length(data)
406+
n_data = size(data{i}.data, 1);
407+
n_inv = sum(isnan(data{i}.data));
408+
infos.source.chan_stats{i,1} = struct();
409+
infos.source.chan_stats{i,1}.nan_ratio = n_inv / n_data;
410+
end
411+
412+
if ~isequal(eye_data_cell{idxRight}.GazeRange, eye_data_cell{idxLeft}.GazeRange)
413+
warning("GazeRange is not equal");
414+
end
415+
416+
infos.source.gaze_coords = eye_data_cell{idxRight}.GazeRange;
417+
418+
if any(strcmp(fieldnames(eye_data_cell{idxRight}),'PupilFitMethod'))
419+
infos.source.elcl_proc = lower(eye_data_cell{idxRight}.PupilFitMethod); % or should it be called PupilFitMethod? lowercase!
420+
elseif any(strcmp(fieldnames(eye_data_cell{idxRight}),'ElclProc'))
421+
infos.source.elcl_proc = lower(eye_data_cell{idxRight}.ElclProc); % like in the Calinet dataset
422+
end
423+
424+
% eyesObserved and best_eye
425+
if num_eyes == 2
426+
infos.source.eyesObserved = 'lr';
427+
elseif num_eyes == 1
428+
infos.source.eyesObserved = data{1}.header.chantype(end);
429+
end
430+
431+
infos.source.best_eye = eye_with_smaller_nan_ratio(data, infos.source.eyesObserved);
432+
infos.source.type = 'BIDS (json/tsv)' ;
433+
434+
435+
if num_eyes == 2
436+
% physio_infos.source.file = [eye_data_cell{1}.source.file, eye_data_cell{2}.source.file] ; % {1},{2} gives the right order
437+
file_paths{1,1} = eye_data_cell{1}.source.file;
438+
file_paths{2,1} = eye_data_cell{2}.source.file;
439+
else
440+
file_paths{1,1} = eye_data_cell{1}.source.file ;
441+
end
442+
443+
444+
445+
% Check if the first data has the StartTime field
446+
if isfield(data{1}.header, 'StartTime')
447+
% Check if all StartTimes are the same
448+
start_times = cellfun(@(x) x.header.StartTime, data, 'UniformOutput', false);
449+
if ~isequal(start_times{:}) ; warning('Not all data have the same StartTime. Please check the input data.'); end
450+
else
451+
% If there is no StartTime field start time will set to 0
452+
for i = 1:length(data); data{i}.header.StartTime = 0; end
453+
end
238454

239455
else
240456
warning('No data for physio eye data was imported.');

src/ext/pupil-size/code/helperFunctions/rawDataFilter.m

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,3 @@
559559
end
560560

561561
end
562-
563-
564-

src/pspm_get_smr.m

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
% ● Arguments
88
% * datafile : the data file to be imported.
99
% ┌───import
10-
% ├───channel : X
10+
% ├───channel : the number of the channel to load.
1111
% └───denoise : for marker channels in CED spike format (recorded as 'level'),
1212
% only retains markers with duration longer than the value given here (in ms).
1313
% ● Outputs
@@ -44,17 +44,10 @@
4444
errorflag(channel)=1;
4545
chandata{channel}=[];
4646
chanhead{channel}.title='';
47+
chanhead{channel}.kind = -1 ;
4748
end
4849
end
4950
fclose(fid);
50-
% 2.5 delete empty channels
51-
if ~isempty(errorflag)
52-
ind=find(errorflag);
53-
for channel=ind(end:-1:1)
54-
chandata(channel)=[];
55-
chanhead(channel)=[];
56-
end
57-
end
5851
warning on;
5952
%% 3 extract individual channels
6053
% 3.1 loop through import jobs
@@ -75,7 +68,11 @@
7568
sourceinfo.channel{iImport, 1} = sprintf('Channel %02.0f: %s', channel, chanhead{channel}.title);
7669
% 3.1.2 convert to waveform or get sample rate for wave channel types
7770
if strcmpi(settings.channeltypes(import{iImport}.typeno).data, 'wave')
78-
if chanhead{channel}.kind == 1 % waveform
71+
if chanhead{channel}.kind == -1 % empty channel
72+
import{iImport}.data = zeros(1,0);
73+
import{iImport}.units = '';
74+
import{iImport}.sr = 1;
75+
elseif chanhead{channel}.kind == 1 % waveform
7976
import{iImport}.data = chandata{channel};
8077
import{iImport}.sr = 1./chanhead{channel}.sampleinterval;
8178
elseif chanhead{channel}.kind == 3 % timestamps
@@ -100,7 +97,12 @@
10097
end
10198
% extract, and possibly denoise event channels
10299
elseif strcmpi(settings.channeltypes(import{iImport}.typeno).data, 'events')
103-
if chanhead{channel}.kind == 1 % waveform
100+
if chanhead{channel}.kind == -1 % empty channel
101+
import{iImport}.marker = 'continuous' ; % or ''
102+
import{iImport}.data = zeros(1,0);
103+
import{iImport}.units = 'event';
104+
import{iImport}.sr = 1;
105+
elseif chanhead{channel}.kind == 1 % waveform
104106
import{iImport}.marker = 'continuous';
105107
import{iImport}.data = chandata{channel};
106108
import{iImport}.sr = 1./chanhead{channel}.sampleinterval;

0 commit comments

Comments
 (0)