Skip to content

[BIDS] import NIRS dataset (fixes #469) #589

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

Merged
merged 13 commits into from
Nov 21, 2022
2 changes: 2 additions & 0 deletions toolbox/io/in_channel_bids.m
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
ChannelMat.Channel(iChan).Type = 'ECOG';
elseif ~isempty(strfind(ChannelFile, '/ieeg/')) || ~isempty(strfind(ChannelFile, '\\ieeg\\'))
ChannelMat.Channel(iChan).Type = 'SEEG';
elseif isequal(chType, 'source') || isequal(chType, 'detector')
ChannelMat.Channel(iChan).Type = 'NIRS';
else
ChannelMat.Channel(iChan).Type = 'EEG';
end
Expand Down
17 changes: 15 additions & 2 deletions toolbox/io/in_data_snirf.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@
% Load file header with the JSNIRF Toolbox (https://github.com/fangq/jsnirfy)
jnirs = loadsnirf(DataFile);

if isempty(jnirs) || ~isfield(jnirs, 'nirs')
error('The file doesnt seems to be a valid SNIRF file')
end

if ~isfield(jnirs.nirs.probe,'sourceLabels') || ~isfield(jnirs.nirs.probe,'detectorLabels')
warning('SNIRF format doesnt contains source or detector name. Name of the channels might be wrong');
jnirs.nirs.probe.sourceLabels = {};
jnirs.nirs.probe.detectorLabels = {};

end

%% ===== CHANNEL FILE ====
Expand Down Expand Up @@ -205,8 +208,18 @@


%% ===== EVENTS =====
DataMat.Events = repmat(db_template('event'), 1, length(jnirs.nirs.stim));

% Read events (SNIRF created by Homer3)
if ~isfield(jnirs.nirs,'stim') && any(contains(fieldnames(jnirs.nirs),'stim'))
nirs_fields = fieldnames(jnirs.nirs);
sim_key = nirs_fields(contains(fieldnames(jnirs.nirs),'stim'));
jnirs.nirs.stim = jnirs.nirs.( sim_key{1});
for iStim = 2:length(sim_key)
jnirs.nirs.stim(iStim) = jnirs.nirs.( sim_key{iStim});
end
end

DataMat.Events = repmat(db_template('event'), 1, length(jnirs.nirs.stim));
for iEvt = 1:length(jnirs.nirs.stim)

DataMat.Events(iEvt).label = strtrim(str_remove_spec_chars(toLine(jnirs.nirs.stim(iEvt).name)));
Expand Down
39 changes: 36 additions & 3 deletions toolbox/process/functions/process_import_bids.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
% - Tutorial FEM : https://neuroimage.usc.edu/brainstorm/Tutorials/FemMedianNerve :
% - Tutorial ECOG : https://neuroimage.usc.edu/brainstorm/Tutorials/ECoG :
% - Tutorial SEEG : https://neuroimage.usc.edu/brainstorm/Tutorials/Epileptogenicity :
% - NIRS : https://github.com/rob-luke/BIDS-NIRS-Tapping/tree/388d2cdc3ae831fc767e06d9b77298e9c5cd307b :
% - NIRS : https://osf.io/b4wck/ :


% @=============================================================================
% This function is part of the Brainstorm software:
Expand Down Expand Up @@ -209,6 +212,13 @@
end
return;
end

% Add BIDS subject tag "sub-" if missing
for iSubject = 1:length(OPTIONS.SelectedSubjects)
if (length(OPTIONS.SelectedSubjects{iSubject}) <= 4) || ~strcmpi(OPTIONS.SelectedSubjects{iSubject}(1:4), 'sub-')
OPTIONS.SelectedSubjects{iSubject} = ['sub-' OPTIONS.SelectedSubjects{iSubject}];
end
end
OPTIONS.SelectedSubjects = unique([OPTIONS.SelectedSubjects, selSubjects]);

% ===== FIND SUBJECTS =====
Expand Down Expand Up @@ -670,7 +680,7 @@
end
end
% Loop on the supported modalities
for mod = {'meg', 'eeg', 'ieeg'}
for mod = {'meg', 'eeg', 'ieeg','nirs'}
posUnits = 'mm';
electrodesFile = [];
electrodesSpace = 'ScanRAS';
Expand Down Expand Up @@ -709,6 +719,8 @@
posUnits = sCoordsystem.EEGCoordinateUnits;
elseif isfield(sCoordsystem, 'MEGCoordinateUnits') && ~isempty(sCoordsystem.MEGCoordinateUnits) && ismember(sCoordsystem.MEGCoordinateUnits, {'mm','cm','m'})
posUnits = sCoordsystem.MEGCoordinateUnits;
elseif isfield(sCoordsystem, 'NIRSCoordinateUnits') && ~isempty(sCoordsystem.NIRSCoordinateUnits) && ismember(sCoordsystem.NIRSCoordinateUnits, {'mm','cm','m'})
posUnits = sCoordsystem.NIRSCoordinateUnits;
end
% Get fiducials structure
sFid = GetFiducials(sCoordsystem, posUnits);
Expand All @@ -720,6 +732,8 @@
electrodesCoordSystem = sCoordsystem.EEGCoordinateSystem;
elseif isfield(sCoordsystem, 'MEGCoordinateSystem') && ~isempty(sCoordsystem.MEGCoordinateSystem)
electrodesCoordSystem = sCoordsystem.MEGCoordinateSystem;
elseif isfield(sCoordsystem, 'NIRSCoordinateSystem') && ~isempty(sCoordsystem.NIRSCoordinateSystem)
electrodesCoordSystem = sCoordsystem.NIRSCoordinateSystem;
elseif ~isempty(coordsystemSpace)
electrodesCoordSystem = coordsystemSpace;
end
Expand Down Expand Up @@ -751,7 +765,11 @@

% === ELECTRODES.TSV ===
% Get electrodes positions
electrodesDir = dir(bst_fullfile(SubjectSessDir{iSubj}{isess}, mod{1}, '*_electrodes.tsv'));
if strcmp(mod,'nirs')
electrodesDir = dir(bst_fullfile(SubjectSessDir{iSubj}{isess}, mod{1}, '*_optodes.tsv'));
else
electrodesDir = dir(bst_fullfile(SubjectSessDir{iSubj}{isess}, mod{1}, '*_electrodes.tsv'));
end
% If multiple positions in the same folder: not expected unless multiple coordinate systems are available
if (length(electrodesDir) > 1)
% Select by order of preference: subject space, MNI space or first in the list
Expand Down Expand Up @@ -822,6 +840,7 @@
case '.eeg', FileFormat = 'EEG-BRAINAMP';
case '.edf', FileFormat = 'EEG-EDF';
case '.set', FileFormat = 'EEG-EEGLAB';
case '.snirf', FileFormat = 'NIRS-SNIRF';
otherwise, FileFormat = [];
end
% Import file if file was identified
Expand Down Expand Up @@ -872,7 +891,19 @@
% Read tsv file
% For _channels.tsv, 'name', 'type' and 'units' are required.
% 'group' and 'status' are fields added by Brainstorm export to BIDS.
ChanInfo = in_tsv(ChannelsFile, {'name', 'type', 'group', 'status'}, 0);
if strcmp(mod,'nirs')
ChanInfo_tmp = in_tsv(ChannelsFile, {'name','type','source','detector','wavelength_nominal', 'status'});
ChanInfo = cell(size(ChanInfo_tmp,1), 4); % {'name', 'type', 'group', 'status'}
ChanInfo(:,2) = ChanInfo_tmp(:,2);
ChanInfo(:,4) = ChanInfo_tmp(:,6);
for i = 1:size(ChanInfo,1)
ChanInfo{i,1} = sprintf('%s%sWL%d',ChanInfo_tmp{i,3},ChanInfo_tmp{i,4},str2double(ChanInfo_tmp{i,5}));
ChanInfo{i,3} = sprintf('WL%d', str2double(ChanInfo_tmp{i,5}));
end
else
ChanInfo = in_tsv(ChannelsFile, {'name', 'type', 'group', 'status'});
end

% Try to add info to the existing Brainstorm channel file
% Note: this does not work if channel names different in data and metadata - see note in the function header
if ~isempty(ChanInfo) || ~isempty(ChanInfo{1,1})
Expand Down Expand Up @@ -906,6 +937,8 @@
chanType = 'MEG';
case {'MEGREFMAG', 'MEGREFGRADAXIAL', 'MEGREFGRADPLANAR'} % CTF/4D references
chanType = 'MEG REF';
case {'NIRSCWAMPLITUDE'}
chanType = 'NIRS';
end
ChannelMat.Channel(iChanBst).Type = chanType;
isModifiedChan = 1;
Expand Down
47 changes: 33 additions & 14 deletions toolbox/sensors/channel_add_loc.m
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,35 @@ function channel_add_loc(iStudies, LocChannelFile, isInteractive, isMni)
% For all the channels, look for its definition in the LOC EEG cap
for ic = 1:length(ChannelMat.Channel)
chName = ChannelMat.Channel(ic).Name;
% Replace "'" with "p"
chName = strrep(chName, '''', 'p');
% Look for the exact channel name
idef = find(strcmpi(chName, locChanNames));
% If not found, look for an alternate version (with or without trailing zeros...)
if isempty(idef) && ismember(lower(chName(1)), 'abcdefghijklmnopqrstuvwxyz') && ismember(lower(chName(end)), '0123456789')
[chGroup, chTag, chInd] = panel_montage('ParseSensorNames', ChannelMat.Channel(ic));
% Look for "A01"
idef = find(strcmpi(sprintf('%s%02d', strrep(chTag{1}, '''', 'p'), chInd(1)), locChanNames));
if isempty(idef)
% Look for "A1"
idef = find(strcmpi(sprintf('%s%d', strrep(chTag{1}, '''', 'p'), chInd(1)), locChanNames));

if strcmp(ChannelMat.Channel(ic).Type,'NIRS')
toks = regexp(chName, '^S([0-9]+)D([0-9]+)(WL\d+|HbO|HbR|HbT)$', 'tokens');

idef = find(strcmpi(sprintf('S%s', toks{1}{1}) , locChanNames));
if ~isempty(idef)
ChannelMat.Channel(ic).Loc(:,1) = LocChannelMat.Channel(idef).Loc;
nUpdated = nUpdated + 1;
end
idef = find(strcmpi(sprintf('D%s', toks{1}{2}) , locChanNames));
if ~isempty(idef)
ChannelMat.Channel(ic).Loc(:,2) = LocChannelMat.Channel(idef).Loc;
nUpdated = nUpdated + 1;
end
else
% Replace "'" with "p"
chName = strrep(chName, '''', 'p');
% Look for the exact channel name
idef = find(strcmpi(chName, locChanNames));

% If not found, look for an alternate version (with or without trailing zeros...)
if isempty(idef) && ismember(lower(chName(1)), 'abcdefghijklmnopqrstuvwxyz') && ismember(lower(chName(end)), '0123456789')
[chGroup, chTag, chInd] = panel_montage('ParseSensorNames', ChannelMat.Channel(ic));
% Look for "A01"
idef = find(strcmpi(sprintf('%s%02d', strrep(chTag{1}, '''', 'p'), chInd(1)), locChanNames));
if isempty(idef)
% Look for "A1"
idef = find(strcmpi(sprintf('%s%d', strrep(chTag{1}, '''', 'p'), chInd(1)), locChanNames));
end
end
end
% If the channel is found has a valid 3D position
Expand All @@ -135,6 +152,7 @@ function channel_add_loc(iStudies, LocChannelFile, isInteractive, isMni)
ChannelMat.Channel(ic).Orient = LocChannelMat.Channel(idef).Orient;
ChannelMat.Channel(ic).Weight = LocChannelMat.Channel(idef).Weight;
nUpdated = nUpdated + 1;

% Convert from MNI to subject space, if needed
if isMni
P = ChannelMat.Channel(ic).Loc';
Expand All @@ -154,7 +172,7 @@ function channel_add_loc(iStudies, LocChannelFile, isInteractive, isMni)
ChannelMat.HeadPoints.Type = {};
end
% Add as head points (if doesn't exist yet)
if isempty(ChannelMat.HeadPoints.Loc) || all(sqrt(sum(bst_bsxfun(@minus, ChannelMat.HeadPoints.Loc, ChannelMat.Channel(ic).Loc) .^ 2, 1)) > 0.0001)
if isempty(ChannelMat.HeadPoints.Loc) || all(sqrt(sum(bst_bsxfun(@minus, ChannelMat.HeadPoints.Loc, ChannelMat.Channel(ic).Loc(:,1)) .^ 2, 1)) > 0.0001)
ChannelMat.HeadPoints.Loc = [ChannelMat.HeadPoints.Loc, ChannelMat.Channel(ic).Loc];
ChannelMat.HeadPoints.Label = [ChannelMat.HeadPoints.Label, ChannelMat.Channel(ic).Name];
ChannelMat.HeadPoints.Type = [ChannelMat.HeadPoints.Type, 'EXTRA'];
Expand Down Expand Up @@ -193,6 +211,7 @@ function channel_add_loc(iStudies, LocChannelFile, isInteractive, isMni)
ChannelMat = panel_ieeg('DetectElectrodes', ChannelMat, Modality{1}, [], 1);
end
end

% History: Added channel locations
ChannelMat = bst_history('add', ChannelMat, 'addloc', ['Added EEG positions from "' LocChannelMat.Comment '"']);
% Save modified file
Expand All @@ -207,5 +226,5 @@ function channel_add_loc(iStudies, LocChannelFile, isInteractive, isMni)
java_dialog('msgbox', Messages, 'Add EEG positions');
end


end

2 changes: 1 addition & 1 deletion toolbox/tree/tree_callbacks.m
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,7 @@
% === ADD EEG POSITIONS ===
if ismember('EEG', AllMod)
fcnPopupImportChannel(bstNodes, jPopup, 2);
elseif ~isempty(AllMod) && any(ismember({'SEEG','ECOG','ECOG+SEEG'}, AllMod))
elseif ~isempty(AllMod) && any(ismember({'SEEG','ECOG','ECOG+SEEG','NIRS'}, AllMod))
fcnPopupImportChannel(bstNodes, jPopup, 1);
end
% === SEEG CONTACT LABELLING ===
Expand Down