Skip to content

Commit 7b0128b

Browse files
[BIDS] Import NIRS dataset (fixes #469) (#589)
1 parent ee6d93a commit 7b0128b

File tree

5 files changed

+87
-20
lines changed

5 files changed

+87
-20
lines changed

toolbox/io/in_channel_bids.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
ChannelMat.Channel(iChan).Type = 'ECOG';
6666
elseif ~isempty(strfind(ChannelFile, '/ieeg/')) || ~isempty(strfind(ChannelFile, '\\ieeg\\'))
6767
ChannelMat.Channel(iChan).Type = 'SEEG';
68+
elseif isequal(chType, 'source') || isequal(chType, 'detector')
69+
ChannelMat.Channel(iChan).Type = 'NIRS';
6870
else
6971
ChannelMat.Channel(iChan).Type = 'EEG';
7072
end

toolbox/io/in_data_snirf.m

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@
2828
% Load file header with the JSNIRF Toolbox (https://github.com/fangq/jsnirfy)
2929
jnirs = loadsnirf(DataFile);
3030

31+
if isempty(jnirs) || ~isfield(jnirs, 'nirs')
32+
error('The file doesnt seems to be a valid SNIRF file')
33+
end
34+
3135
if ~isfield(jnirs.nirs.probe,'sourceLabels') || ~isfield(jnirs.nirs.probe,'detectorLabels')
3236
warning('SNIRF format doesnt contains source or detector name. Name of the channels might be wrong');
3337
jnirs.nirs.probe.sourceLabels = {};
3438
jnirs.nirs.probe.detectorLabels = {};
35-
3639
end
3740

3841
%% ===== CHANNEL FILE ====
@@ -205,8 +208,18 @@
205208

206209

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

212+
% Read events (SNIRF created by Homer3)
213+
if ~isfield(jnirs.nirs,'stim') && any(contains(fieldnames(jnirs.nirs),'stim'))
214+
nirs_fields = fieldnames(jnirs.nirs);
215+
sim_key = nirs_fields(contains(fieldnames(jnirs.nirs),'stim'));
216+
jnirs.nirs.stim = jnirs.nirs.( sim_key{1});
217+
for iStim = 2:length(sim_key)
218+
jnirs.nirs.stim(iStim) = jnirs.nirs.( sim_key{iStim});
219+
end
220+
end
221+
222+
DataMat.Events = repmat(db_template('event'), 1, length(jnirs.nirs.stim));
210223
for iEvt = 1:length(jnirs.nirs.stim)
211224

212225
DataMat.Events(iEvt).label = strtrim(str_remove_spec_chars(toLine(jnirs.nirs.stim(iEvt).name)));

toolbox/process/functions/process_import_bids.m

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
% - Tutorial FEM : https://neuroimage.usc.edu/brainstorm/Tutorials/FemMedianNerve :
3333
% - Tutorial ECOG : https://neuroimage.usc.edu/brainstorm/Tutorials/ECoG :
3434
% - Tutorial SEEG : https://neuroimage.usc.edu/brainstorm/Tutorials/Epileptogenicity :
35+
% - NIRS : https://github.com/rob-luke/BIDS-NIRS-Tapping/tree/388d2cdc3ae831fc767e06d9b77298e9c5cd307b :
36+
% - NIRS : https://osf.io/b4wck/ :
37+
3538

3639
% @=============================================================================
3740
% This function is part of the Brainstorm software:
@@ -209,6 +212,13 @@
209212
end
210213
return;
211214
end
215+
216+
% Add BIDS subject tag "sub-" if missing
217+
for iSubject = 1:length(OPTIONS.SelectedSubjects)
218+
if (length(OPTIONS.SelectedSubjects{iSubject}) <= 4) || ~strcmpi(OPTIONS.SelectedSubjects{iSubject}(1:4), 'sub-')
219+
OPTIONS.SelectedSubjects{iSubject} = ['sub-' OPTIONS.SelectedSubjects{iSubject}];
220+
end
221+
end
212222
OPTIONS.SelectedSubjects = unique([OPTIONS.SelectedSubjects, selSubjects]);
213223

214224
% ===== FIND SUBJECTS =====
@@ -670,7 +680,7 @@
670680
end
671681
end
672682
% Loop on the supported modalities
673-
for mod = {'meg', 'eeg', 'ieeg'}
683+
for mod = {'meg', 'eeg', 'ieeg','nirs'}
674684
posUnits = 'mm';
675685
electrodesFile = [];
676686
electrodesSpace = 'ScanRAS';
@@ -709,6 +719,8 @@
709719
posUnits = sCoordsystem.EEGCoordinateUnits;
710720
elseif isfield(sCoordsystem, 'MEGCoordinateUnits') && ~isempty(sCoordsystem.MEGCoordinateUnits) && ismember(sCoordsystem.MEGCoordinateUnits, {'mm','cm','m'})
711721
posUnits = sCoordsystem.MEGCoordinateUnits;
722+
elseif isfield(sCoordsystem, 'NIRSCoordinateUnits') && ~isempty(sCoordsystem.NIRSCoordinateUnits) && ismember(sCoordsystem.NIRSCoordinateUnits, {'mm','cm','m'})
723+
posUnits = sCoordsystem.NIRSCoordinateUnits;
712724
end
713725
% Get fiducials structure
714726
sFid = GetFiducials(sCoordsystem, posUnits);
@@ -720,6 +732,8 @@
720732
electrodesCoordSystem = sCoordsystem.EEGCoordinateSystem;
721733
elseif isfield(sCoordsystem, 'MEGCoordinateSystem') && ~isempty(sCoordsystem.MEGCoordinateSystem)
722734
electrodesCoordSystem = sCoordsystem.MEGCoordinateSystem;
735+
elseif isfield(sCoordsystem, 'NIRSCoordinateSystem') && ~isempty(sCoordsystem.NIRSCoordinateSystem)
736+
electrodesCoordSystem = sCoordsystem.NIRSCoordinateSystem;
723737
elseif ~isempty(coordsystemSpace)
724738
electrodesCoordSystem = coordsystemSpace;
725739
end
@@ -751,7 +765,11 @@
751765

752766
% === ELECTRODES.TSV ===
753767
% Get electrodes positions
754-
electrodesDir = dir(bst_fullfile(SubjectSessDir{iSubj}{isess}, mod{1}, '*_electrodes.tsv'));
768+
if strcmp(mod,'nirs')
769+
electrodesDir = dir(bst_fullfile(SubjectSessDir{iSubj}{isess}, mod{1}, '*_optodes.tsv'));
770+
else
771+
electrodesDir = dir(bst_fullfile(SubjectSessDir{iSubj}{isess}, mod{1}, '*_electrodes.tsv'));
772+
end
755773
% If multiple positions in the same folder: not expected unless multiple coordinate systems are available
756774
if (length(electrodesDir) > 1)
757775
% Select by order of preference: subject space, MNI space or first in the list
@@ -822,6 +840,7 @@
822840
case '.eeg', FileFormat = 'EEG-BRAINAMP';
823841
case '.edf', FileFormat = 'EEG-EDF';
824842
case '.set', FileFormat = 'EEG-EEGLAB';
843+
case '.snirf', FileFormat = 'NIRS-SNIRF';
825844
otherwise, FileFormat = [];
826845
end
827846
% Import file if file was identified
@@ -872,7 +891,19 @@
872891
% Read tsv file
873892
% For _channels.tsv, 'name', 'type' and 'units' are required.
874893
% 'group' and 'status' are fields added by Brainstorm export to BIDS.
875-
ChanInfo = in_tsv(ChannelsFile, {'name', 'type', 'group', 'status'}, 0);
894+
if strcmp(mod,'nirs')
895+
ChanInfo_tmp = in_tsv(ChannelsFile, {'name','type','source','detector','wavelength_nominal', 'status'});
896+
ChanInfo = cell(size(ChanInfo_tmp,1), 4); % {'name', 'type', 'group', 'status'}
897+
ChanInfo(:,2) = ChanInfo_tmp(:,2);
898+
ChanInfo(:,4) = ChanInfo_tmp(:,6);
899+
for i = 1:size(ChanInfo,1)
900+
ChanInfo{i,1} = sprintf('%s%sWL%d',ChanInfo_tmp{i,3},ChanInfo_tmp{i,4},str2double(ChanInfo_tmp{i,5}));
901+
ChanInfo{i,3} = sprintf('WL%d', str2double(ChanInfo_tmp{i,5}));
902+
end
903+
else
904+
ChanInfo = in_tsv(ChannelsFile, {'name', 'type', 'group', 'status'});
905+
end
906+
876907
% Try to add info to the existing Brainstorm channel file
877908
% Note: this does not work if channel names different in data and metadata - see note in the function header
878909
if ~isempty(ChanInfo) || ~isempty(ChanInfo{1,1})
@@ -906,6 +937,8 @@
906937
chanType = 'MEG';
907938
case {'MEGREFMAG', 'MEGREFGRADAXIAL', 'MEGREFGRADPLANAR'} % CTF/4D references
908939
chanType = 'MEG REF';
940+
case {'NIRSCWAMPLITUDE'}
941+
chanType = 'NIRS';
909942
end
910943
ChannelMat.Channel(iChanBst).Type = chanType;
911944
isModifiedChan = 1;

toolbox/sensors/channel_add_loc.m

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,35 @@ function channel_add_loc(iStudies, LocChannelFile, isInteractive, isMni)
109109
% For all the channels, look for its definition in the LOC EEG cap
110110
for ic = 1:length(ChannelMat.Channel)
111111
chName = ChannelMat.Channel(ic).Name;
112-
% Replace "'" with "p"
113-
chName = strrep(chName, '''', 'p');
114-
% Look for the exact channel name
115-
idef = find(strcmpi(chName, locChanNames));
116-
% If not found, look for an alternate version (with or without trailing zeros...)
117-
if isempty(idef) && ismember(lower(chName(1)), 'abcdefghijklmnopqrstuvwxyz') && ismember(lower(chName(end)), '0123456789')
118-
[chGroup, chTag, chInd] = panel_montage('ParseSensorNames', ChannelMat.Channel(ic));
119-
% Look for "A01"
120-
idef = find(strcmpi(sprintf('%s%02d', strrep(chTag{1}, '''', 'p'), chInd(1)), locChanNames));
121-
if isempty(idef)
122-
% Look for "A1"
123-
idef = find(strcmpi(sprintf('%s%d', strrep(chTag{1}, '''', 'p'), chInd(1)), locChanNames));
112+
113+
if strcmp(ChannelMat.Channel(ic).Type,'NIRS')
114+
toks = regexp(chName, '^S([0-9]+)D([0-9]+)(WL\d+|HbO|HbR|HbT)$', 'tokens');
115+
116+
idef = find(strcmpi(sprintf('S%s', toks{1}{1}) , locChanNames));
117+
if ~isempty(idef)
118+
ChannelMat.Channel(ic).Loc(:,1) = LocChannelMat.Channel(idef).Loc;
119+
nUpdated = nUpdated + 1;
120+
end
121+
idef = find(strcmpi(sprintf('D%s', toks{1}{2}) , locChanNames));
122+
if ~isempty(idef)
123+
ChannelMat.Channel(ic).Loc(:,2) = LocChannelMat.Channel(idef).Loc;
124+
nUpdated = nUpdated + 1;
125+
end
126+
else
127+
% Replace "'" with "p"
128+
chName = strrep(chName, '''', 'p');
129+
% Look for the exact channel name
130+
idef = find(strcmpi(chName, locChanNames));
131+
132+
% If not found, look for an alternate version (with or without trailing zeros...)
133+
if isempty(idef) && ismember(lower(chName(1)), 'abcdefghijklmnopqrstuvwxyz') && ismember(lower(chName(end)), '0123456789')
134+
[chGroup, chTag, chInd] = panel_montage('ParseSensorNames', ChannelMat.Channel(ic));
135+
% Look for "A01"
136+
idef = find(strcmpi(sprintf('%s%02d', strrep(chTag{1}, '''', 'p'), chInd(1)), locChanNames));
137+
if isempty(idef)
138+
% Look for "A1"
139+
idef = find(strcmpi(sprintf('%s%d', strrep(chTag{1}, '''', 'p'), chInd(1)), locChanNames));
140+
end
124141
end
125142
end
126143
% If the channel is found has a valid 3D position
@@ -135,6 +152,7 @@ function channel_add_loc(iStudies, LocChannelFile, isInteractive, isMni)
135152
ChannelMat.Channel(ic).Orient = LocChannelMat.Channel(idef).Orient;
136153
ChannelMat.Channel(ic).Weight = LocChannelMat.Channel(idef).Weight;
137154
nUpdated = nUpdated + 1;
155+
138156
% Convert from MNI to subject space, if needed
139157
if isMni
140158
P = ChannelMat.Channel(ic).Loc';
@@ -154,7 +172,7 @@ function channel_add_loc(iStudies, LocChannelFile, isInteractive, isMni)
154172
ChannelMat.HeadPoints.Type = {};
155173
end
156174
% Add as head points (if doesn't exist yet)
157-
if isempty(ChannelMat.HeadPoints.Loc) || all(sqrt(sum(bst_bsxfun(@minus, ChannelMat.HeadPoints.Loc, ChannelMat.Channel(ic).Loc) .^ 2, 1)) > 0.0001)
175+
if isempty(ChannelMat.HeadPoints.Loc) || all(sqrt(sum(bst_bsxfun(@minus, ChannelMat.HeadPoints.Loc, ChannelMat.Channel(ic).Loc(:,1)) .^ 2, 1)) > 0.0001)
158176
ChannelMat.HeadPoints.Loc = [ChannelMat.HeadPoints.Loc, ChannelMat.Channel(ic).Loc];
159177
ChannelMat.HeadPoints.Label = [ChannelMat.HeadPoints.Label, ChannelMat.Channel(ic).Name];
160178
ChannelMat.HeadPoints.Type = [ChannelMat.HeadPoints.Type, 'EXTRA'];
@@ -193,6 +211,7 @@ function channel_add_loc(iStudies, LocChannelFile, isInteractive, isMni)
193211
ChannelMat = panel_ieeg('DetectElectrodes', ChannelMat, Modality{1}, [], 1);
194212
end
195213
end
214+
196215
% History: Added channel locations
197216
ChannelMat = bst_history('add', ChannelMat, 'addloc', ['Added EEG positions from "' LocChannelMat.Comment '"']);
198217
% Save modified file
@@ -207,5 +226,5 @@ function channel_add_loc(iStudies, LocChannelFile, isInteractive, isMni)
207226
java_dialog('msgbox', Messages, 'Add EEG positions');
208227
end
209228

210-
229+
end
211230

toolbox/tree/tree_callbacks.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,7 @@
900900
% === ADD EEG POSITIONS ===
901901
if ismember('EEG', AllMod)
902902
fcnPopupImportChannel(bstNodes, jPopup, 2);
903-
elseif ~isempty(AllMod) && any(ismember({'SEEG','ECOG','ECOG+SEEG'}, AllMod))
903+
elseif ~isempty(AllMod) && any(ismember({'SEEG','ECOG','ECOG+SEEG','NIRS'}, AllMod))
904904
fcnPopupImportChannel(bstNodes, jPopup, 1);
905905
end
906906
% === SEEG CONTACT LABELLING ===

0 commit comments

Comments
 (0)