Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update eyetracker to eyetrack as BIDS data type in line with bep020 #2391

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
81 changes: 45 additions & 36 deletions data2bids.m
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
% cfg.ses = string, optional session name
% cfg.run = number, optional
% cfg.task = string, task name is required for functional data
% cfg.suffix = string, can be any of 'FLAIR', 'FLASH', 'PD', 'PDT2', 'PDmap', 'T1map', 'T1rho', 'T1w', 'T2map', 'T2star', 'T2w', 'angio', 'audio', 'bold', 'bval', 'bvec', 'channels', 'coordsystem', 'defacemask', 'dwi', 'eeg', 'emg', 'epi', 'events', 'eyetracker', 'fieldmap', 'headshape', 'ieeg', 'inplaneT1', 'inplaneT2', 'magnitude', 'magnitude1', 'magnitude2', 'meg', 'motion', 'nirs', 'phase1', 'phase2', 'phasediff', 'photo', 'physio', 'sbref', 'stim', 'video'
% cfg.suffix = string, can be any of 'FLAIR', 'FLASH', 'PD', 'PDT2', 'PDmap', 'T1map', 'T1rho', 'T1w', 'T2map', 'T2star', 'T2w', 'angio', 'audio', 'bold', 'bval', 'bvec', 'channels', 'coordsystem', 'defacemask', 'dwi', 'eeg', 'emg', 'epi', 'events', 'eyetrack', 'fieldmap', 'headshape', 'ieeg', 'inplaneT1', 'inplaneT2', 'magnitude', 'magnitude1', 'magnitude2', 'meg', 'motion', 'nirs', 'phase1', 'phase2', 'phasediff', 'photo', 'physio', 'sbref', 'stim', 'video'
% cfg.acq = string
% cfg.ce = string
% cfg.rec = string
Expand Down Expand Up @@ -237,6 +237,8 @@
cfg = ft_checkconfig(cfg, 'renamed', {'electrodes.writesidecar', 'writejson'});
cfg = ft_checkconfig(cfg, 'renamed', {'coordsystem.writesidecar', 'writejson'});
cfg = ft_checkconfig(cfg, 'renamed', {'event', 'events'}); % cfg.event is used elsewhere in FieldTrip, but here it should be cfg.events with an s
cfg = ft_checkconfig(cfg, 'renamedval', {'suffix', 'eyetracker', 'eyetrack'});
cfg = ft_checkconfig(cfg, 'renamed', {'eyetracker', 'eyetrack'});

% prevent some common errors
cfg = ft_checkconfig(cfg, 'forbidden', {'acq_time'}); % this should be in cfg.scans or in cfg.sessions
Expand Down Expand Up @@ -286,7 +288,7 @@
end

if isempty(cfg.suffix)
modality = {'meg', 'eeg', 'ieeg', 'emg', 'motion', 'audio', 'video', 'eyetracker', 'physio', 'stim', 'motion', 'nirs'};
modality = {'meg', 'eeg', 'ieeg', 'emg', 'motion', 'audio', 'video', 'eyetrack', 'physio', 'stim', 'motion', 'nirs'};
for i=1:numel(modality)
if isfield(cfg, modality{i}) && ~isempty(cfg.(modality{i}))
% the user specified modality-specific options, assume that the datatype matches
Expand All @@ -310,9 +312,10 @@
cfg.nirs = ft_getopt(cfg, 'nirs');
cfg.audio = ft_getopt(cfg, 'audio');
cfg.video = ft_getopt(cfg, 'video');
cfg.eyetracker = ft_getopt(cfg, 'eyetracker');
cfg.eyetrack = ft_getopt(cfg, 'eyetrack');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add prior to this a line

cfg = ft_checkconfig('renamedval', {'suffix', 'eyetracker', 'eye track'});

it can go before setting all defaults. This allows old scripts to keep working, and prints a warning. Otherwise people would probably get an error further down in the code.

cfg.physio = ft_getopt(cfg, 'physio');
cfg.stim = ft_getopt(cfg, 'stim');
cfg.datatypedir = ft_getopt(cfg, 'datatypedir'); % This specifies the main imaging modality whose dir will be the dest. audio, video, eyetrack, physio, stim
cfg.motion = ft_getopt(cfg, 'motion');
cfg.channels = ft_getopt(cfg, 'channels');
cfg.electrodes = ft_getopt(cfg, 'electrodes');
Expand Down Expand Up @@ -593,11 +596,13 @@
cfg.stim.StartTime = ft_getopt(cfg.stim, 'StartTime' );
cfg.stim.SamplingFrequency = ft_getopt(cfg.stim, 'SamplingFrequency' );

%% eyetracker is not part of the official BIDS specification
% this follows https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/06-physiological-and-other-continuous-recordings.html
cfg.eyetracker.Columns = ft_getopt(cfg.eyetracker, 'Columns' );
cfg.eyetracker.StartTime = ft_getopt(cfg.eyetracker, 'StartTime' );
cfg.eyetracker.SamplingFrequency = ft_getopt(cfg.eyetracker, 'SamplingFrequency' );
%% eyetracker is not part of the official BIDS specification, but is included in proposal BEP020:
% https://docs.google.com/document/d/1eggzTCzSHG3AEKhtnEDbcdk-2avXN6I94X8aUPEBVsw/edit#heading=h.9tphvz6ot0j1
% The current implementation follows:
% https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/06-physiological-and-other-continuous-recordings.html
cfg.eyetrack.Columns = ft_getopt(cfg.eyetrack, 'Columns' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please also add somewhere at the top

cfg = ft_checkconfig('renamed', {'eyetracker', 'eye track'});

to rename this field in case the user supplied it with the old name

cfg.eyetrack.StartTime = ft_getopt(cfg.eyetrack, 'StartTime' );
cfg.eyetrack.SamplingFrequency = ft_getopt(cfg.eyetrack, 'SamplingFrequency' );

%% motion is not part of the official BIDS specification
% this follows extension proposal 029 https://bids.neuroimaging.io/bep029
Expand Down Expand Up @@ -722,7 +727,7 @@
elseif isempty(cfg.suffix)
ft_error('cfg.suffix is required to construct BIDS output directory and file');
else
dirname = datatype2dirname(cfg.suffix);
dirname = datatype2dirname(cfg);
filename = ['sub-' cfg.sub];
filename = add_entity(filename, 'ses', cfg.ses);
filename = add_entity(filename, 'task', cfg.task);
Expand Down Expand Up @@ -821,7 +826,7 @@
need_video_json = false;
need_physio_json = false;
need_stim_json = false;
need_eyetracker_json = false;
need_eyetrack_json = false;
need_motion_json = false;
need_coordsystem_json = false;
% determine the tsv files that are required
Expand Down Expand Up @@ -938,8 +943,8 @@
need_physio_json = true;
elseif isequal(cfg.suffix, 'stim')
need_stim_json = true;
elseif isequal(cfg.suffix, 'eyetracker')
need_eyetracker_json = true;
elseif isequal(cfg.suffix, 'eyetrack')
need_eyetrack_json = true;
elseif isequal(cfg.suffix, 'motion')
need_motion_json = true;
else
Expand Down Expand Up @@ -975,8 +980,8 @@
need_physio_json = true;
elseif isequal(cfg.suffix, 'stim')
need_stim_json = true;
elseif isequal(cfg.suffix, 'eyetracker')
need_eyetracker_json = true;
elseif isequal(cfg.suffix, 'eyetrack')
need_eyetrack_json = true;
elseif isequal(cfg.suffix, 'motion')
need_motion_json = true;
elseif isequal(cfg.suffix, 'events')
Expand Down Expand Up @@ -1037,7 +1042,7 @@
end
end

need_events_tsv = need_events_tsv || need_meg_json || need_eeg_json || need_ieeg_json || need_emg_json || need_nirs_json || need_eyetracker_json || need_motion_json || (contains(cfg.outputfile, 'task') || ~isempty(cfg.TaskName) || ~isempty(cfg.task)) || ~isempty(cfg.events);
need_events_tsv = (need_events_tsv || need_meg_json || need_eeg_json || need_ieeg_json || need_emg_json || need_nirs_json || need_motion_json || (contains(cfg.outputfile, 'task') || ~isempty(cfg.TaskName) || ~isempty(cfg.task)) || ~isempty(cfg.events)) && ~need_eyetrack_json;
need_channels_tsv = need_channels_tsv || need_meg_json || need_eeg_json || need_ieeg_json || need_emg_json || need_nirs_json || need_motion_json ;
need_coordsystem_json = need_coordsystem_json || need_meg_json || need_electrodes_tsv || need_nirs_json ;

Expand All @@ -1050,7 +1055,7 @@
elseif need_video_json
ft_warning('video data is not yet part of the official BIDS specification');
cfg.dataset_description.BIDSVersion = 'n/a';
elseif need_eyetracker_json
elseif need_eyetrack_json
ft_warning('eyetracker data is not yet part of the official BIDS specification');
cfg.dataset_description.BIDSVersion = 'n/a';
elseif need_motion_json
Expand Down Expand Up @@ -1123,9 +1128,9 @@
stim_settings = keepfields(cfg.stim, fn);

% make the relevant selection, all json fields start with a capital letter
fn = fieldnames(cfg.eyetracker);
fn = fieldnames(cfg.eyetrack);
fn = fn(~cellfun(@isempty, regexp(fn, '^[A-Z].*')));
eyetracker_settings = keepfields(cfg.eyetracker, fn);
eyetrack_settings = keepfields(cfg.eyetrack, fn);

% make the relevant selection, all json fields start with a capital letter
fn = fieldnames(cfg.motion);
Expand Down Expand Up @@ -1386,16 +1391,16 @@
stim_json = mergestruct(generic_settings, stim_json, false);
end

%% need_eyetracker_json
if need_eyetracker_json
eyetracker_json.SamplingFrequency = hdr.Fs;
eyetracker_json.StartTime = nan;
eyetracker_json.Columns = hdr.label;
%% need_eyetrack_json
if need_eyetrack_json
eyetrack_json.SamplingFrequency = hdr.Fs;
eyetrack_json.StartTime = nan;
eyetrack_json.Columns = hdr.label;

% merge the information specified by the user with that from the data
% in case fields appear in both, the first input overrules the second
eyetracker_json = mergestruct(eyetracker_settings, eyetracker_json, false);
eyetracker_json = mergestruct(generic_settings, eyetracker_json, false);
eyetrack_json = mergestruct(eyetrack_settings, eyetrack_json, false);
eyetrack_json = mergestruct(generic_settings, eyetrack_json, false);
end

%% need_motion_json
Expand Down Expand Up @@ -1889,7 +1894,7 @@
ft_info('writing ''%s''\n', cfg.outputfile);
ft_write_data(cfg.outputfile, dat, 'dataformat', 'snirf', 'header', hdr, 'event', trigger);

case {'physio', 'stim', 'eyetracker', 'motion'}
case {'physio', 'stim', 'eyetrack', 'motion'}
% write the data according to the Stim and Physio format as specified at
% https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/06-physiological-and-other-continuous-recordings.html
[p, f, x] = fileparts(cfg.outputfile);
Expand Down Expand Up @@ -1954,7 +1959,7 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% each of these has a corresponding json file
modality = {'mri', 'meg', 'eeg', 'ieeg', 'nirs', 'physio', 'stim', 'emg', 'audio', 'video', 'eyetracker', 'motion', 'coordsystem'};
modality = {'mri', 'meg', 'eeg', 'ieeg', 'nirs', 'physio', 'stim', 'emg', 'audio', 'video', 'eyetrack', 'motion', 'coordsystem'};
for i=1:numel(modality)
if eval(sprintf('need_%s_json', modality{i}))
modality_json = eval(sprintf('%s_json', modality{i}));
Expand Down Expand Up @@ -2154,7 +2159,7 @@
% get filename
this = table();
[p, f, x] = fileparts(cfg.outputfile);
this.filename = {fullfile(datatype2dirname(cfg.suffix), [f x])};
this.filename = {fullfile(datatype2dirname(cfg), [f x])};

fn = fieldnames(cfg.scans);
for i=1:numel(fn)
Expand Down Expand Up @@ -2257,7 +2262,7 @@
f = [f '_' typ];

function f = remove_datatype(f)
typ = {'FLAIR', 'FLASH', 'PD', 'PDT2', 'PDmap', 'T1map', 'T1rho', 'T1w', 'T2map', 'T2star', 'T2w', 'angio', 'audio', 'bold', 'bval', 'bvec', 'channels', 'coordsystem', 'defacemask', 'dwi', 'eeg', 'emg', 'epi', 'events', 'eyetracker', 'fieldmap', 'headshape', 'ieeg', 'inplaneT1', 'inplaneT2', 'magnitude', 'magnitude1', 'magnitude2', 'meg', 'motion', 'nirs', 'phase1', 'phase2', 'phasediff', 'photo', 'physio', 'sbref', 'stim', 'video'};
typ = {'FLAIR', 'FLASH', 'PD', 'PDT2', 'PDmap', 'T1map', 'T1rho', 'T1w', 'T2map', 'T2star', 'T2w', 'angio', 'audio', 'bold', 'bval', 'bvec', 'channels', 'coordsystem', 'defacemask', 'dwi', 'eeg', 'emg', 'epi', 'events', 'eyetrack', 'fieldmap', 'headshape', 'ieeg', 'inplaneT1', 'inplaneT2', 'magnitude', 'magnitude1', 'magnitude2', 'meg', 'motion', 'nirs', 'phase1', 'phase2', 'phasediff', 'photo', 'physio', 'sbref', 'stim', 'video'};
for i=1:numel(typ)
if endsWith(f, ['_' typ{i}])
f = f(1:end-length(typ{i})-1); % also the '_'
Expand Down Expand Up @@ -2574,9 +2579,10 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% SUBFUNCTION
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function dir = datatype2dirname(typ)
function dir = datatype2dirname(cfg)
% see https://bids-specification.readthedocs.io/en/stable/99-appendices/04-entity-table.html
% motion, emg, eyetracker, audio, and video are not part of the official specification
typ = cfg.suffix;
switch typ
case {'T1w' 'T2w' 'T1rho' 'T1map' 'T2map' 'T2star' 'FLAIR' 'FLASH' 'PD' 'PDmap' 'PDT2' 'inplaneT1' 'inplaneT2' 'angio' 'defacemask'}
dir = 'anat';
Expand All @@ -2586,8 +2592,11 @@
dir = 'dwi';
case {'phasediff' 'phase1' 'phase2' 'magnitude1' 'magnitude2' 'magnitude' 'fieldmap' 'epi'}
dir = 'fmap';
case {'events' 'stim' 'physio' 'eyetracker' 'audio' 'video'} % these could also all be stored in 'func' or one of the other directories with brain data
dir = 'beh';
case {'events' 'stim' 'physio' 'audio' 'video' 'eyetrack'} % these should be recorded in the main imaging modality directory according to BEP020 https://bids-specification--1128.org.readthedocs.build/en/1128/modality-specific-files/eye-tracking.html#eye-tracking-data
if isempty(cfg.datatypedir)
ft_error('main imaging modality must be specifed in cfg.datatypedir for data of type ''%s''', typ);
end
dir = cfg.datatypedir;
case {'meg'} % this could also include 'events' or other non-brain data
dir = 'meg';
case {'eeg'} % this could also include 'events' or other non-brain data
Expand Down Expand Up @@ -2695,12 +2704,12 @@
% Assume naïvely that if not semi-colon delimination is used, then
% commas are used to separate elements
if contains(tmp, ';')
tmp = strtrim(strsplit(tmp,';'))
dataset_description.(fn{i}) = tmp
tmp = strtrim(strsplit(tmp,';'));
dataset_description.(fn{i}) = tmp;
ft_warning(sprintf('Multiple entries to %s field should be an array-of-strings, splitting on '';''', fn{i}));
elseif contains(tmp, ',')
tmp = strtrim(strsplit(tmp,','))
dataset_description.(fn{i}) = tmp
tmp = strtrim(strsplit(tmp,','));
dataset_description.(fn{i}) = tmp;
ft_warning(sprintf('Multiple entries to %s field should be an array-of-strings, splitting on '',''', fn{i}));
else
dataset_description.(fn{i}) = {tmp};
Expand Down
Loading