Skip to content

Commit b3f2550

Browse files
committed
BIDS import: Added support for ACPC and CapTrak coordinate systems
1 parent 19cfcaf commit b3f2550

File tree

5 files changed

+98
-11
lines changed

5 files changed

+98
-11
lines changed

doc/updates.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
November 2022
22
- Forward: Display leadfield sensitivity (surface, MRI, isosurface)
3+
- BIDS import: Added support for ACPC and CapTrak coordinate systems
34
--------------------------------------------------------------
45
October 2022
56
- EEG: Project channel files between subjects

toolbox/anatomy/cs_compute.m

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
%
66
% INPUT:
77
% - sMri : Brainstorm MRI structure
8-
% - csname : Coordinate system for which we need to evaluate the transformation {'scs','mni','acpc','tal'}
8+
% - csname : Coordinate system for which we need to evaluate the transformation {'scs','mni','acpc','tal','captrak'}
99

1010
% @=============================================================================
1111
% This function is part of the Brainstorm software:
@@ -71,7 +71,6 @@
7171
sMri.SCS.T = Transf.T;
7272
end
7373

74-
7574
% ===== MRI => MNI =====
7675
case 'mni'
7776
error('To estimate the MNI coordinates: right-click on the MRI > MNI normalization.');
@@ -80,7 +79,7 @@
8079
case 'acpc'
8180
% The necessary points are not defined
8281
if isempty(sMri) || ~isfield(sMri, 'NCS') || ~isfield(sMri.NCS, 'AC') || ~isfield(sMri.NCS, 'PC') || ~isfield(sMri.NCS, 'IH') || (length(sMri.NCS.AC)~=3) || (length(sMri.NCS.PC)~=3) || (length(sMri.NCS.IH)~=3)
83-
disp('BST> Cannot compute MRI=>TAL transformation: Missing fiducial points.');
82+
disp('BST> Cannot compute MRI=>ACPC transformation: Missing fiducial points.');
8483
return;
8584
end
8685
% Get coordinates in meters
@@ -108,6 +107,38 @@
108107
Transf.R = transform(1:3,1:3);
109108
Transf.T = transform(1:3,4);
110109

110+
% ===== MRI => CapTrak =====
111+
case 'captrak'
112+
% The necessary points are not defined
113+
if isempty(sMri) || ~isfield(sMri, 'SCS') || ~isfield(sMri.SCS, 'NAS') || ~isfield(sMri.SCS, 'LPA') || ~isfield(sMri.SCS, 'RPA') || (length(sMri.SCS.NAS)~=3) || (length(sMri.SCS.LPA)~=3) || (length(sMri.SCS.RPA)~=3)
114+
disp('BST> Cannot compute MRI=>SCS transformation: Missing fiducial points.');
115+
return;
116+
end
117+
% Get coordinates in meters
118+
NAS = double(sMri.SCS.NAS(:))' ./ 1000;
119+
LPA = double(sMri.SCS.LPA(:))' ./ 1000;
120+
RPA = double(sMri.SCS.RPA(:))' ./ 1000;
121+
% X axis: From LPA through RPA exactly
122+
dirx = RPA - LPA;
123+
dirx = dirx/norm(dirx);
124+
% Y axis: Orthogonal to the X-axis through the nasion (NAS)
125+
origin = LPA + dirx * sum((NAS - LPA) .* dirx);
126+
diry = NAS - origin;
127+
diry = diry/norm(diry);
128+
% Z axis: Orthogonal to the XY-plane through the vertex of the head
129+
dirz = cross(dirx,diry);
130+
% Compute the rotation matrix
131+
rot = eye(4);
132+
rot(1:3,1:3) = inv(eye(3) / [dirx; diry; dirz]);
133+
% compute the translation matrix
134+
tra = eye(4);
135+
tra(1:4,4) = [-origin(:); 1];
136+
% Combine these to compute the full homogeneous transformation matrix
137+
transform = rot * tra;
138+
% Return in split format
139+
Transf.R = transform(1:3,1:3);
140+
Transf.T = transform(1:3,4);
141+
111142
% ===== SCS => TAL =====
112143
% Not a real TALAIRACH system, used only from tess_envelope.m
113144
case 'tal'

toolbox/anatomy/cs_convert.m

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
%
77
% INPUT:
88
% - sMri : Brainstorm MRI structure
9-
% - src : Current coordinate system {'voxel','mri','scs','mni','acpc','world'}
10-
% - dest : Target coordinate system {'voxel','mri','scs','mni','acpc','world'}
9+
% - src : Current coordinate system {'voxel','mri','scs','mni','acpc','world','captrak'}
10+
% - dest : Target coordinate system {'voxel','mri','scs','mni','acpc','world','captrak'}
1111
% - P : a Nx3 matrix of point coordinates to convert
1212
% - isNanAllowed : If 0, throw an error and if the conversion of some points P leads to NaN,
1313
% and if there is no linear MNI registration to be used instead
@@ -29,6 +29,10 @@
2929
% Axis Y: Negative y-axis is passing from AC through PC
3030
% Axis Z: Passing through a mid-hemispheric point in the superior direction
3131
% - world : Transformation available in the initial file loaded as the default MRI (vox2ras/qform/world transformation)
32+
% - captrak: RAS orientation and the origin approximately between LPA and RPA
33+
% Axis X: From LPA through RPA exactly
34+
% Axis Y: Orthogonal to the X-axis through the nasion (NAS)
35+
% Axis Z: Orthogonal to the XY-plane through the vertex of the head
3236

3337
% @=============================================================================
3438
% This function is part of the Brainstorm software:
@@ -108,6 +112,16 @@
108112
scs2acpc = [tACPC.R, tACPC.T; 0 0 0 1];
109113
end
110114

115+
% ===== COMPUTE CAPTRAK TRANSFORMATION =====
116+
if strcmpi(src, 'captrak') || strcmpi(dest, 'captrak')
117+
tCapTrak = cs_compute(sMri, 'captrak');
118+
if isempty(tCapTrak) || isempty(tCapTrak.R)
119+
P = [];
120+
return;
121+
end
122+
scs2captrak = [tCapTrak.R, tCapTrak.T; 0 0 0 1];
123+
end
124+
111125
% ===== CONVERT SRC => MRI =====
112126
% Evaluate the transformation to apply
113127
switch lower(src)
@@ -151,6 +165,9 @@
151165
case 'acpc'
152166
% ACPC => SCS => MRI
153167
RT1 = inv(scs2acpc);
168+
case 'captrak'
169+
% CapTrak => SCS => MRI
170+
RT1 = inv(scs2captrak);
154171
case 'world'
155172
RT1 = world2mri;
156173
otherwise
@@ -196,6 +213,9 @@
196213
case 'acpc'
197214
% MRI => SCS => ACPC
198215
RT2 = scs2acpc;
216+
case 'captrak'
217+
% MRI => SCS => CapTrak
218+
RT2 = scs2captrak;
199219
case 'world'
200220
RT2 = mri2world;
201221
otherwise

toolbox/io/import_channel.m

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,13 @@
136136
FileUnits = 'm';
137137

138138
% ===== EEG ONLY =====
139-
case {'BIDS-SCANRAS-MM', 'BIDS-MNI-MM', 'BIDS-ACPC-MM', 'BIDS-ALS-MM'}
139+
case {'BIDS-SCANRAS-MM', 'BIDS-MNI-MM', 'BIDS-ACPC-MM', 'BIDS-ALS-MM', 'BIDS-CAPTRAK-MM'}
140140
ChannelMat = in_channel_bids(ChannelFile, 0.001);
141141
FileUnits = 'mm';
142-
case {'BIDS-SCANRAS-CM', 'BIDS-MNI-CM', 'BIDS-ACPC-CM', 'BIDS-ALS-CM'}
142+
case {'BIDS-SCANRAS-CM', 'BIDS-MNI-CM', 'BIDS-ACPC-CM', 'BIDS-ALS-CM', 'BIDS-CAPTRAK-CM'}
143143
ChannelMat = in_channel_bids(ChannelFile, 0.01);
144144
FileUnits = 'cm';
145-
case {'BIDS-SCANRAS-M', 'BIDS-MNI-M', 'BIDS-ACPC-M', 'BIDS-ALS-M'}
145+
case {'BIDS-SCANRAS-M', 'BIDS-MNI-M', 'BIDS-ACPC-M', 'BIDS-ALS-M', 'BIDS-CAPTRAK-M'}
146146
ChannelMat = in_channel_bids(ChannelFile, 1);
147147
FileUnits = 'm';
148148

@@ -410,7 +410,7 @@
410410
warning(['WARNING: When importing ACPC positions for multiple subjects: the ACPC transformation from the first subject is used for all of them.' 10 ...
411411
'Please consider importing your subjects seprately.']);
412412
end
413-
% If we know the destination study: convert from MNI to SCS coordinates
413+
% If we know the destination study: convert from ACPC to SCS coordinates
414414
if ~isempty(iStudies)
415415
% Get the subject for the first study
416416
sStudy = bst_get('Study', iStudies(1));
@@ -433,6 +433,36 @@
433433
% Do not convert the positions to SCS
434434
isAlignScs = 0;
435435

436+
%% ===== CAPTRAK TRANSFORMATION =====
437+
elseif ismember(FileFormat, {'BIDS-CAPTRAK-MM', 'BIDS-CAPTRAK-CM', 'BIDS-CAPTRAK-M'})
438+
% Warning for multiple studies
439+
if (length(iStudies) > 1)
440+
warning(['WARNING: When importing CapTrak positions for multiple subjects: the CapTrak transformation from the first subject is used for all of them.' 10 ...
441+
'Please consider importing your subjects seprately.']);
442+
end
443+
% If we know the destination study: convert from CapTrak to SCS coordinates
444+
if ~isempty(iStudies)
445+
% Get the subject for the first study
446+
sStudy = bst_get('Study', iStudies(1));
447+
sSubject = bst_get('Subject', sStudy.BrainStormSubject);
448+
% Get the subject's MRI
449+
if isempty(sSubject.Anatomy) || isempty(sSubject.Anatomy(1).FileName)
450+
error('You need the subject anatomy in order to load sensor positions in CapTrak coordinates.');
451+
end
452+
% Load the MRI
453+
MriFile = file_fullpath(sSubject.Anatomy(1).FileName);
454+
sMri = in_mri_bst(MriFile);
455+
if ~isfield(sMri, 'SCS') || ~isfield(sMri.SCS, 'R') || isempty(sMri.SCS.R) || ~isfield(sMri.SCS, 'NAS') || isempty(sMri.SCS.NAS) || ~isfield(sMri.SCS, 'LPA') || isempty(sMri.SCS.LPA) || ~isfield(sMri.SCS, 'RPA') || isempty(sMri.SCS.RPA)
456+
error(['All fiducials must be defined for this subject (NAS,LPA,RPA)' 10 'in order to load sensor positions in CapTrak coordinates.']);
457+
end
458+
% Convert all the coordinates: CapTrak => SCS
459+
fcnTransf = @(Loc)cs_convert(sMri, 'captrak', 'scs', Loc')';
460+
AllChannelMats = channel_apply_transf(ChannelMat, fcnTransf, [], 1);
461+
ChannelMat = AllChannelMats{1};
462+
end
463+
% Do not convert the positions to SCS
464+
isAlignScs = 0;
465+
436466
%% ===== MRI/NII TRANSFORMATION =====
437467
% If the SCS coordinates are not defined (NAS/LPA/RPA fiducials), try to use the MRI=>subject transformation available in the MRI (eg. NIfTI sform/qform)
438468
% Only available if there is one study in output

toolbox/process/functions/process_import_bids.m

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
% - EEG : https://openneuro.org/datasets/ds004024 : OK
2121
% - iEEG : https://openneuro.org/datasets/ds003688 : ERROR: Wrong interpretation of ACPC coordinates (easier to see in ECOG for sub-02)
2222
% - iEEG : https://openneuro.org/datasets/ds003848 : WARNING: Impossible to interepret correctly the coordinates in electrodes.tsv
23-
% - iEEG : https://openneuro.org/datasets/ds004085 :
23+
% - iEEG : https://openneuro.org/datasets/ds004085 : ERROR: FreeSurfer not imported (not in a "freesurfer" pipeline subfolder)
24+
% ERROR: Some subjects (1,2,10,11) have incorrect channel names in the electrodes.tsv => Not imported
25+
% ERROR: Inaccurate electrode positioning because AC/PC landmarks are not defined in the MRI
2426
% - iEEG : https://openneuro.org/datasets/ds004126 : OK (ACPC OK)
2527
% - STIM : https://openneuro.org/datasets/ds002799 : WARNING: No channel file imported because there are no SEEG recordings
2628
% - MEG : https://openneuro.org/datasets/ds000117 :
29+
% - MEG : https://openneuro.org/datasets/ds000246 :
2730
% - MEG : https://openneuro.org/datasets/ds000247 :
28-
% - MEG : https://openneuro.org/datasets/ds004107 :
31+
% - MEG : https://openneuro.org/datasets/ds004107 : WARNING: Multiple NAS/LPA/RPA in T1w.json, one for each MEG session => Used the average for both sessions
2932
% - Tutorial FEM : https://neuroimage.usc.edu/brainstorm/Tutorials/FemMedianNerve :
3033
% - Tutorial ECOG : https://neuroimage.usc.edu/brainstorm/Tutorials/ECoG :
3134
% - Tutorial SEEG : https://neuroimage.usc.edu/brainstorm/Tutorials/Epileptogenicity :
@@ -762,6 +765,8 @@
762765
if ~isempty(electrodesCoordSystem)
763766
if strcmpi(electrodesCoordSystem, 'ACPC')
764767
electrodesSpace = 'ACPC';
768+
elseif strcmpi(electrodesCoordSystem, 'CapTrak')
769+
electrodesSpace = 'CapTrak';
765770
elseif ~isempty(strfind(electrodesCoordSystem, 'MNI')) || ~isempty(strfind(electrodesCoordSystem, 'IXI')) || ~isempty(strfind(electrodesCoordSystem, 'ICBM')) || ~isempty(strfind(electrodesCoordSystem, 'fs'))
766771
electrodesSpace = 'MNI';
767772
elseif ismember(upper(electrodesCoordSystem), {'CTF', 'EEGLAB', 'EEGLAB-HJ', 'ElektaNeuromag', '4DBti', 'KitYokogawa', 'ChietiItab'})

0 commit comments

Comments
 (0)