From 8e6d19e58fbc68c43d6a0a006622bbf03d1e7bd2 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Sat, 19 Feb 2022 22:45:25 -0500 Subject: [PATCH 01/32] Option to change MRI fids to match digitized ones --- toolbox/db/db_set_channel.m | 14 ++- toolbox/gui/figure_mri.m | 20 +++- .../functions/process_adjust_coordinates.m | 42 ++++++-- .../process/functions/process_import_bids.m | 10 +- toolbox/sensors/channel_align_auto.m | 97 ++++++++++++++----- 5 files changed, 139 insertions(+), 44 deletions(-) diff --git a/toolbox/db/db_set_channel.m b/toolbox/db/db_set_channel.m index 447af48c1..ddd35767e 100644 --- a/toolbox/db/db_set_channel.m +++ b/toolbox/db/db_set_channel.m @@ -16,6 +16,7 @@ % - ChannelAlign : 0, do not perform automatic headpoints-based alignment % 1, perform automatic alignment after user confirmation % 2, perform automatic alignment without user confirmation +% 3, as 2, but also updating MRI SCS from digitized points % OUTPUT: % - OutputFile: Newly created channel file (empty is no file created) @@ -175,15 +176,18 @@ end % Call automatic registration for MEG - [ChannelMat, R, T, isSkip, isUserCancel] = channel_align_auto(OutputFile, [], 0, isConfirm); + if ChannelAlign >= 3 + % Also adjust MRI SCS from digitized points. + [ChannelMat, R, T, isSkip, isUserCancel] = channel_align_auto(OutputFile, [], 0, isConfirm, [], 1); + else + [ChannelMat, R, T, isSkip, isUserCancel] = channel_align_auto(OutputFile, [], 0, isConfirm); + end % User validated: keep this answer for the next round (force alignment for next call) if ~isSkip - if isUserCancel + if isUserCancel || isempty(ChannelMat) ChannelAlign = 0; - elseif ~isempty(ChannelMat) + elseif ChannelAlign < 2 ChannelAlign = 2; - else - ChannelAlign = 0; end end end diff --git a/toolbox/gui/figure_mri.m b/toolbox/gui/figure_mri.m index 9d46eeebb..c5fe40fbc 100644 --- a/toolbox/gui/figure_mri.m +++ b/toolbox/gui/figure_mri.m @@ -2413,10 +2413,22 @@ function ButtonSave_Callback(hFig, varargin) %% ===== SAVE MRI ===== +% Update MRI fiducials and adjust surfaces. +% Input can be figure handle or sMri. function [isCloseAccepted, MriFile] = SaveMri(hFig) ProtocolInfo = bst_get('ProtocolInfo'); % Get MRI - sMri = panel_surface('GetSurfaceMri', hFig); + if ishandle(hFig) + sMri = panel_surface('GetSurfaceMri', hFig); + isUser = true; + elseif isstruct(hFig) + sMri = hFig; + isUser = false; + else + bst_error('SaveMri: Unexpected input: %s.', class(hFig)); + isCloseAccepted = 0; + return; + end MriFile = sMri.FileName; MriFileFull = bst_fullfile(ProtocolInfo.SUBJECTS, MriFile); % Do not accept "Save" if user did not select all the fiducials @@ -2448,7 +2460,11 @@ function ButtonSave_Callback(hFig, varargin) % === HISTORY === % History: Edited the fiducials if ~isfield(sMriOld, 'SCS') || ~isequal(sMriOld.SCS, sMri.SCS) || ~isfield(sMriOld, 'NCS') || ~isequal(sMriOld.NCS, sMri.NCS) - sMri = bst_history('add', sMri, 'edit', 'User edited the fiducials'); + if isUser + sMri = bst_history('add', sMri, 'edit', 'User edited the fiducials'); + else + sMri = bst_history('add', sMri, 'edit', 'Applied digitized anatomical fiducials'); % string used to verify elsewhere + end end % ==== SAVE MRI ==== diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index bf861a247..17cfa566e 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -3,7 +3,7 @@ % % Native coordinates are based on system fiducials (e.g. MEG head coils), % whereas Brainstorm's SCS coordinates are based on the anatomical fiducial -% points from the .pos file. +% points set on the MRI. % @============================================================================= % This function is part of the Brainstorm software: @@ -23,14 +23,14 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Marc Lalancette, 2018-2020 +% Authors: Marc Lalancette, 2018-2022 eval(macro_method); end -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description of the process sProcess.Comment = 'Adjust coordinate system'; sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/HeadMotion#Adjust_the_reference_head_position'; @@ -64,6 +64,11 @@ sProcess.options.points.Type = 'checkbox'; sProcess.options.points.Comment = 'Refine MRI coregistration using digitized head points.'; sProcess.options.points.Value = 0; + sProcess.options.points.Controller = 'Refine'; + sProcess.options.scs.Type = 'checkbox'; + sProcess.options.scs.Comment = 'Also ajust MRI nasion and ear points.'; + sProcess.options.scs.Value = 0; + sProcess.options.scs.Class = 'Refine'; sProcess.options.remove.Type = 'checkbox'; sProcess.options.remove.Comment = 'Remove selected adjustments (if present) instead of adding them.'; sProcess.options.remove.Value = 0; @@ -101,6 +106,18 @@ bst_report('Info', sProcess, sInputs, ... 'Multiple inputs were found for a single channel file. They will be concatenated for adjusting the head position.'); end + + if ~sProcess.options.remove.Value && sProcess.options.points.Value && sProcess.options.scs.Value + % Warning and confirmation dialog. + isConfirmed = java_dialog('confirm', 'Ajusting MRI nasion and ear points will break previous alignment with head points for files not included here. Proceed?', ... + 'Adjust MRI nasion and ear points?'); + if ~isConfirmed + bst_report('User cancelled.'); + OutputFiles = {}; + return; + end + end + bst_progress('start', 'Adjust coordinate system', ... ' ', 0, nFiles); % If resetting, in case the original data moved, and because the same @@ -159,14 +176,14 @@ Which = {}; if sProcess.options.head.Value - Which{end+1} = 'AdjustedNative'; + Which{end+1} = 'AdjustedNative'; %#ok<*AGROW> end if sProcess.options.points.Value Which{end+1} = 'refine registration: head points'; end for TransfLabel = Which - TransfLabel = TransfLabel{1}; + TransfLabel = TransfLabel{1}; %#ok ChannelMat = RemoveTransformation(ChannelMat, TransfLabel, sInputs(iFile), sProcess); end % TransfLabel loop @@ -187,8 +204,13 @@ % Redundant, but makes sense to have it here also. bst_progress('text', 'Fitting head surface to points...'); - [ChannelMat, R, T, isSkip] = ... - channel_align_auto(sInputs(iFile).ChannelFile, ChannelMat, 0, 0); % No warning or confirmation + if sProcess.options.scs.Value + [ChannelMat, R, T, isSkip] = ... + channel_align_auto(sInputs(iFile).ChannelFile, ChannelMat, 0, 0, [], 1); % No warning or confirmation, adjust scs + else + [ChannelMat, R, T, isSkip] = ... + channel_align_auto(sInputs(iFile).ChannelFile, ChannelMat, 0, 0); % No warning or confirmation + end % ChannelFile needed to find subject and scalp surface, but not % used otherwise when ChannelMat is provided. if isSkip @@ -266,8 +288,8 @@ % end -function [ChannelMat, NewChannelFiles, Failed] = ... - ResetChannelFile(ChannelMat, NewChannelFiles, sInput, sProcess) +function [ChannelMat, NewChannelFiles, Failed] = ResetChannelFile(... + ChannelMat, NewChannelFiles, sInput, sProcess) if nargin < 4 sProcess = []; end @@ -506,7 +528,7 @@ iHeadSamples = 1 + ((1:(nHeadSamples*nEpochs)) - 1) * HeadSamplePeriod; % first is 1 iBad = []; for iSeg = 1:size(BadSegments, 2) - iBad = [iBad, nSamplesPerEpoch * (BadEpoch(1,iSeg) - 1) + (BadSegments(1,iSeg):BadSegments(2,iSeg))]; %#ok + iBad = [iBad, nSamplesPerEpoch * (BadEpoch(1,iSeg) - 1) + (BadSegments(1,iSeg):BadSegments(2,iSeg))]; % iBad = [iBad, find((DataMat.Time >= badTimes(1,iSeg)) & (DataMat.Time <= badTimes(2,iSeg)))]; end % Exclude bad samples. diff --git a/toolbox/process/functions/process_import_bids.m b/toolbox/process/functions/process_import_bids.m index ba8bf1318..2d8a57854 100644 --- a/toolbox/process/functions/process_import_bids.m +++ b/toolbox/process/functions/process_import_bids.m @@ -68,6 +68,11 @@ sProcess.options.channelalign.Comment = 'Align sensors using headpoints'; sProcess.options.channelalign.Type = 'checkbox'; sProcess.options.channelalign.Value = 1; + sProcess.options.channelalign.Controller = 'Align'; + sProcess.options.scs.Type = 'checkbox'; + sProcess.options.scs.Comment = 'Also ajust MRI nasion and ear points from digitized points.'; + sProcess.options.scs.Value = 1; + sProcess.options.scs.Class = 'Align'; % Group sessions sProcess.options.groupsessions.Comment = 'Import multiple anat sessions to the same subject'; sProcess.options.groupsessions.Type = 'checkbox'; @@ -109,7 +114,8 @@ end % Other options OPTIONS.isInteractive = 0; - OPTIONS.ChannelAlign = 2 * double(sProcess.options.channelalign.Value); + % 2=align without confirmation, 3=also adjust MRI SCS from digitized points + OPTIONS.ChannelAlign = (2 + double(sProcess.options.scs.Value)) * double(sProcess.options.channelalign.Value); OPTIONS.SelectedSubjects = strtrim(str_split(sProcess.options.selectsubj.Value, ',')); OPTIONS.isGroupSessions = sProcess.options.groupsessions.Value; OPTIONS.isGenerateBem = sProcess.options.bem.Value; @@ -488,7 +494,7 @@ % Import options ImportOptions = db_template('ImportOptions'); ImportOptions.ChannelReplace = 1; - ImportOptions.ChannelAlign = 2 * (OPTIONS.ChannelAlign >= 1) * ~sSubject.UseDefaultAnat; + ImportOptions.ChannelAlign = OPTIONS.ChannelAlign * ~sSubject.UseDefaultAnat; ImportOptions.DisplayMessages = OPTIONS.isInteractive; ImportOptions.EventsMode = 'ignore'; ImportOptions.EventsTrackMode = 'value'; diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index cbfa9dc96..926d7eb51 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -1,7 +1,7 @@ -function [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(ChannelFile, ChannelMat, isWarning, isConfirm, tolerance) +function [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(ChannelFile, ChannelMat, isWarning, isConfirm, tolerance, isAdjustScs) % CHANNEL_ALIGN_AUTO: Aligns the channels to the scalp using Polhemus points. % -% USAGE: [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(ChannelFile, ChannelMat=[], isWarning=1, isConfirm=1, tolerance=0) +% USAGE: [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(ChannelFile, ChannelMat=[], isWarning=1, isConfirm=1, tolerance=0, isAdjustScs=0) % % DESCRIPTION: % Aligns the channels to the scalp using Polhemus points stored in channel structure. @@ -14,10 +14,11 @@ % % INPUTS: % - ChannelFile : Channel file to align on its anatomy -% - ChannelMat : If specified, do not read or write any information from/to ChannelFile +% - ChannelMat : If specified, do not read or write any information from/to ChannelFile (except to get scalp surface). % - isWarning : If 1, display warning in case of errors (default = 1) % - isConfirm : If 1, ask the user for confirmation before proceeding % - tolerance : Percentage of outliers head points, ignored in the final fit +% - isAdjustScs : If 1 and not already done for this subject, update MRI to use digitized nasion and ear points. % % OUTPUTS: % - ChannelMat : The same ChannelMat structure input in, with the head points and sensors rotated and translated to match the head points to the scalp. @@ -46,10 +47,12 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Syed Ashrafulla, 2009 -% Francois Tadel, 2009-2021 +% Authors: Syed Ashrafulla, 2009, Francois Tadel, 2009-2021, Marc Lalancette 2022 %% ===== PARSE INPUTS ===== +if (nargin < 6) || isempty(isAdjustScs) + isAdjustScs = 0; +end if (nargin < 5) || isempty(tolerance) tolerance = 0; end @@ -69,6 +72,7 @@ T = []; isSkip = 0; isUserCancel = 0; +strReport = ''; %% ===== LOAD CHANNELS ===== @@ -96,10 +100,18 @@ % Get study sStudy = bst_get('ChannelFile', ChannelFile); % Get subject -sSubject = bst_get('Subject', sStudy.BrainStormSubject); +[sSubject, iSubject] = bst_get('Subject', sStudy.BrainStormSubject); +% Check if default anatomy. (Usually also checked before calling this function.) +if iSubject == 0 + if isWarning + bst_error('Digitized nasion and ear points cannot be applied to default anatomy.', 'Automatic EEG-MEG/MRI registration', 0); + end + bst_progress('stop'); + return +end if isempty(sSubject) || isempty(sSubject.iScalp) if isWarning - bst_error('No scalp surface available for this subject', 'Align EEG sensors', 0); + bst_error('No scalp surface available for this subject', 'Automatic EEG-MEG/MRI registration', 0); else disp('BST> No scalp surface available for this subject.'); end @@ -185,24 +197,63 @@ ' | Number of outlier points removed: ' sprintf('%d (%d%%)', nRemove, round(tolerance*100)), 10 ... ' | Initial number of head points: ' num2str(size(HeadPoints.Loc,2))]; +% Create [4,4] transform matrix from digitized SCS to MRI SCS according to this fit. +DigToMriTransf = eye(4); +DigToMriTransf(1:3,1:3) = R; +DigToMriTransf(1:3,4) = T; + + +%% ===== ADJUST MRI FIDUCIALS AND SCS ===== +if isAdjustScs + % Check if already adjusted, in which case the transformation above is correct (identity if same head points). + sMriOld = in_mri_bst(sSubject.Anatomy(sSubject.iAnatomy).FileName); + % History string is set in figure_mri SaveMri. + if isfield(sMriOld, 'History') && ~isempty(sMriOld.History) && any(strcmpi(sMriOld.History(:,2), 'Applied digitized anatomical fiducials')) + if isWarning + bst_warning('Nasion and ear points already adjusted.', 'Automatic EEG-MEG/MRI registration', 0); + end + % Check if digitized anat points present, saved in ChannelMat.SCS. + % Note that these coordinates are NOT currently updated when doing refine with head points (below). + elseif all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) && (length(ChannelMat.SCS.NAS) == 3) && (length(ChannelMat.SCS.LPA) == 3) && (length(ChannelMat.SCS.RPA) == 3) + % Convert to MRI SCS coordinates. + % To do this we need to apply the transformation computed above. + sMri = sMriOld; + sMri.SCS.NAS = DigToMriTransf(1:3,:) * [ChannelMat.SCS.NAS; 1]; + sMri.SCS.LPA = DigToMriTransf(1:3,:) * [ChannelMat.SCS.LPA; 1]; + sMri.SCS.RPA = DigToMriTransf(1:3,:) * [ChannelMat.SCS.RPA; 1]; + + % Compare with existing MRI fids, replace if changed, and update surfaces. + figure_mri('SaveMri', sMri); + + % Adjust transformation from fit above. MRI SCS now matches Digitized SCS. + DigToMriTransf = eye(4); + R = eye(3); + T = zeros(3,1); + end +end + + %% ===== ROTATE SENSORS AND HEADPOINTS ===== -for i = 1:length(ChannelMat.Channel) - % Rotate and translate location of channel - if ~isempty(ChannelMat.Channel(i).Loc) && ~all(ChannelMat.Channel(i).Loc(:) == 0) - ChannelMat.Channel(i).Loc = R * ChannelMat.Channel(i).Loc + T * ones(1,size(ChannelMat.Channel(i).Loc, 2)); +if ~isequal(DigToMriTransf, eye(4)) + for i = 1:length(ChannelMat.Channel) + % Rotate and translate location of channel + if ~isempty(ChannelMat.Channel(i).Loc) && ~all(ChannelMat.Channel(i).Loc(:) == 0) + ChannelMat.Channel(i).Loc = R * ChannelMat.Channel(i).Loc + T * ones(1,size(ChannelMat.Channel(i).Loc, 2)); + end + % Only rotate normal vector to channel + if ~isempty(ChannelMat.Channel(i).Orient) && ~all(ChannelMat.Channel(i).Orient(:) == 0) + ChannelMat.Channel(i).Orient = R * ChannelMat.Channel(i).Orient; + end end - % Only rotate normal vector to channel - if ~isempty(ChannelMat.Channel(i).Orient) && ~all(ChannelMat.Channel(i).Orient(:) == 0) - ChannelMat.Channel(i).Orient = R * ChannelMat.Channel(i).Orient; + % Rotate and translate head points + if isfield(ChannelMat, 'HeadPoints') && ~isempty(ChannelMat.HeadPoints) && ~isempty(ChannelMat.HeadPoints.Loc) + ChannelMat.HeadPoints.Loc = R * ChannelMat.HeadPoints.Loc + ... + T * ones(1, size(ChannelMat.HeadPoints.Loc, 2)); end end -% Rotate and translate head points -if isfield(ChannelMat, 'HeadPoints') && ~isempty(ChannelMat.HeadPoints) && ~isempty(ChannelMat.HeadPoints.Loc) - ChannelMat.HeadPoints.Loc = R * ChannelMat.HeadPoints.Loc + ... - T * ones(1, size(ChannelMat.HeadPoints.Loc, 2)); -end %% ===== SAVE TRANSFORMATION ===== +% We could decide to skip this if the transformation is identity. % Initialize fields if ~isfield(ChannelMat, 'TransfEeg') || ~iscell(ChannelMat.TransfEeg) ChannelMat.TransfEeg = {}; @@ -216,13 +267,9 @@ if ~isfield(ChannelMat, 'TransfEegLabels') || ~iscell(ChannelMat.TransfEegLabels) || (length(ChannelMat.TransfEeg) ~= length(ChannelMat.TransfEegLabels)) ChannelMat.TransfEegLabels = cell(size(ChannelMat.TransfEeg)); end -% Create [4,4] transform matrix -newtransf = eye(4); -newtransf(1:3,1:3) = R; -newtransf(1:3,4) = T; % Add a rotation/translation to the lists -ChannelMat.TransfMeg{end+1} = newtransf; -ChannelMat.TransfEeg{end+1} = newtransf; +ChannelMat.TransfMeg{end+1} = DigToMriTransf; +ChannelMat.TransfEeg{end+1} = DigToMriTransf; % Add the comments ChannelMat.TransfMegLabels{end+1} = 'refine registration: head points'; ChannelMat.TransfEegLabels{end+1} = 'refine registration: head points'; From c40c8fe426d4bb6d800b757b4cc90c23b97a7cdb Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Sat, 19 Feb 2022 23:48:54 -0500 Subject: [PATCH 02/32] can repeat new option by using remove first --- .../functions/process_adjust_coordinates.m | 128 ++++++++++-------- .../functions/process_headpoints_refine.m | 2 +- toolbox/sensors/channel_align_auto.m | 2 +- 3 files changed, 73 insertions(+), 59 deletions(-) diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index 17cfa566e..1239665d5 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -3,7 +3,9 @@ % % Native coordinates are based on system fiducials (e.g. MEG head coils), % whereas Brainstorm's SCS coordinates are based on the anatomical fiducial -% points set on the MRI. +% points. After alignment between MRI and headpoints, the anatomical fiducials +% on the MRI side define the SCS and the ones in the channel files +% (ChannelMat.SCS) are ignored. % @============================================================================= % This function is part of the Brainstorm software: @@ -65,6 +67,10 @@ sProcess.options.points.Comment = 'Refine MRI coregistration using digitized head points.'; sProcess.options.points.Value = 0; sProcess.options.points.Controller = 'Refine'; + sProcess.options.tolerance.Comment = 'Tolerance (outlier points to ignore):'; + sProcess.options.tolerance.Type = 'value'; + sProcess.options.tolerance.Value = {0, '%', 0}; + sProcess.options.tolerance.Class = 'Refine'; sProcess.options.scs.Type = 'checkbox'; sProcess.options.scs.Comment = 'Also ajust MRI nasion and ear points.'; sProcess.options.scs.Value = 0; @@ -121,8 +127,8 @@ bst_progress('start', 'Adjust coordinate system', ... ' ', 0, nFiles); % If resetting, in case the original data moved, and because the same - % channel file may appear in many places for processed data, keep track - % of user file selections. + % channel file may appear in many places for processed data, keep track of + % user file selections. NewChannelFiles = cell(0, 2); for iFile = iUniqFiles(:)' % no need to repeat on same channel file. @@ -151,11 +157,11 @@ if sProcess.options.reset.Value % The main goal of this option is to fix a bug in a previous % version: when importing a channel file, when going to SCS - % coordinates based on digitized coils and anatomical - % fiducials, the channel orientation was wrong. We wish to fix - % this but keep as much pre-processing that was previously - % done. Thus we will re-import the channel file, and copy the - % projectors (and history) from the old one. + % coordinates based on digitized coils and anatomical fiducials, the + % channel orientation was wrong. We wish to fix this but keep as + % much pre-processing that was previously done. Thus we will + % re-import the channel file, and copy the projectors (and history) + % from the old one. [ChannelMat, NewChannelFiles, Failed] = ... ResetChannelFile(ChannelMat, NewChannelFiles, sInputs(iFile), sProcess); @@ -166,13 +172,12 @@ % ---------------------------------------------------------------- elseif sProcess.options.remove.Value % Because channel_align_manual does not consistently apply the - % manual transformation to all sensors or save it in both - % TransfMeg and TransfEeg, it could lead to confusion and - % errors when playing with transforms. Therefore, if we detect - % a difference between the MEG and EEG transforms when trying - % to remove one that applies to both (currently only refine - % with head points), we don't proceed and recommend resetting - % with the original channel file instead. + % manual transformation to all sensors or save it in both TransfMeg + % and TransfEeg, it could lead to confusion and errors when playing + % with transforms. Therefore, if we detect a difference between the + % MEG and EEG transforms when trying to remove one that applies to + % both (currently only refine with head points), we don't proceed + % and recommend resetting with the original channel file instead. Which = {}; if sProcess.options.head.Value @@ -186,6 +191,26 @@ TransfLabel = TransfLabel{1}; %#ok ChannelMat = RemoveTransformation(ChannelMat, TransfLabel, sInputs(iFile), sProcess); end % TransfLabel loop + + % We cannot change back the MRI fiducials, but in order to be able + % to update it again from digitized fids, we must edit the MRI + % history. + if sProcess.options.points.Value && sProcess.options.scs.Value + % Get subject in database, with subject directory + sSubject = bst_get('Subject', sInputs(iFile).FileName); + sMri = in_mri_bst(sSubject.Anatomy(sSubject.iAnatomy).FileName); + % Slightly change the string we use to verify if it was done: append " (hidden)". + for iH = find(strcmpi(sMri.History(:,3), 'Applied digitized anatomical fiducials')) + sMri.History{iH,3} = [sMri.History{iH,3}, ' (hidden)']; + end + try + bst_save(file_fullpath(sMri.FileName), sMri, 'v7'); + catch + bst_report('Error', sProcess, sInputs(iFile), ... + sprintf('Unable to save MRI file %s.', sMri.FileName)); + continue; + end + end end % reset channel file or remove transformations @@ -204,13 +229,8 @@ % Redundant, but makes sense to have it here also. bst_progress('text', 'Fitting head surface to points...'); - if sProcess.options.scs.Value - [ChannelMat, R, T, isSkip] = ... - channel_align_auto(sInputs(iFile).ChannelFile, ChannelMat, 0, 0, [], 1); % No warning or confirmation, adjust scs - else - [ChannelMat, R, T, isSkip] = ... - channel_align_auto(sInputs(iFile).ChannelFile, ChannelMat, 0, 0); % No warning or confirmation - end + [ChannelMat, R, T, isSkip] = channel_align_auto(sInputs(iFile).ChannelFile, ... + ChannelMat, 0, 0, sProcess.options.tolerance.Value, sProcess.options.scs.Value); % No warning or confirmation % ChannelFile needed to find subject and scalp surface, but not % used otherwise when ChannelMat is provided. if isSkip @@ -234,14 +254,14 @@ end % file loop bst_progress('stop'); - % Return the input files that were processed properly. Include those - % that were removed due to sharing a channel file, where appropriate. - % The complicated indexing picks the first input of those with the same - % channel file, i.e. the one that was marked ok. + % Return the input files that were processed properly. Include those that + % were removed due to sharing a channel file, where appropriate. The + % complicated indexing picks the first input of those with the same channel + % file, i.e. the one that was marked ok. OutputFiles = {sInputs(isFileOk(iUniqInputs(iUniqFiles))).FileName}; end -% if ~sProcess.options.remove.Value && sProcess.options.scs.Value +% if ~sProcess.options.remove.Value && sProcess.options.newpoints.Value % % This not yet implemented option could apply the Native to SCS % % transformation for head points loaded after the raw data was % % imported. @@ -402,9 +422,8 @@ % Need to check for empty, otherwise applies to all channels! else iChan = []; % All channels. - % Note: NIRS doesn't have a separate set of - % transformations, but "refine" and "SCS" are applied - % to NIRS as well. + % Note: NIRS doesn't have a separate set of transformations, but + % "refine" and "SCS" are applied to NIRS as well. end while ~isempty(iUndoMeg) if isMegOnly && isempty(iChan) @@ -479,9 +498,9 @@ return; end - % The data could be changed such that the head position could be - % readjusted (e.g. by deleting segments). This is allowed and the - % previous adjustment will be replaced. + % The data could be changed such that the head position could be readjusted + % (e.g. by deleting segments). This is allowed and the previous adjustment + % will be replaced. if isfield(ChannelMat, 'TransfMegLabels') && iscell(ChannelMat.TransfMegLabels) && ... ismember('AdjustedNative', ChannelMat.TransfMegLabels) bst_report('Info', sProcess, sInputs, ... @@ -554,9 +573,9 @@ return; end - % Extract transformations that are applied before and after the - % head position adjustment. Any previous adjustment will be - % ignored here and replaced later. + % Extract transformations that are applied before and after the head + % position adjustment. Any previous adjustment will be ignored here and + % replaced later. [TransfBefore, TransfAdjust, TransfAfter, iAdjust, iDewToNat] = ... GetTransforms(ChannelMat, sInputs); if isempty(TransfBefore) @@ -567,28 +586,24 @@ % Compute transformation corresponding to coil position. [TransfMat, TransfAdjust] = LocationTransform(MedianLoc, ... TransfBefore, TransfAdjust, TransfAfter); - % This TransfMat would automatically give an identity - % transformation if the process is run multiple times, and - % TransfAdjust would not change. - - % Apply this transformation to the current head position. - % This is a correction to the 'Dewar=>Native' - % transformation so it applies to MEG channels only and not - % to EEG or head points, which start in Native. + % This TransfMat would automatically give an identity transformation if the + % process is run multiple times, and TransfAdjust would not change. + + % Apply this transformation to the current head position. This is a + % correction to the 'Dewar=>Native' transformation so it applies to MEG + % channels only and not to EEG or head points, which start in Native. iMeg = sort([good_channel(ChannelMat.Channel, [], 'MEG'), ... good_channel(ChannelMat.Channel, [], 'MEG REF')]); ChannelMat = channel_apply_transf(ChannelMat, TransfMat, iMeg, false); % Don't apply to head points. ChannelMat = ChannelMat{1}; - % After much thought, it was decided to save this - % adjustment transformation separately and at its logical - % place: between 'Dewar=>Native' and - % 'Native=>Brainstorm/CTF'. In particular, this allows us - % to use it directly when displaying head motion distance. - % This however means we must correctly move the - % transformation from the end where it was just applied to - % its logical place. This "moved" transformation is also - % computed in LocationTransform above. + % After much thought, it was decided to save this adjustment transformation + % separately and at its logical place: between 'Dewar=>Native' and + % 'Native=>Brainstorm/CTF'. In particular, this allows us to use it + % directly when displaying head motion distance. This however means we must + % correctly move the transformation from the end where it was just applied + % to its logical place. This "moved" transformation is also computed in + % LocationTransform above. if isempty(iAdjust) iAdjust = iDewToNat + 1; % Shift transformations to make room for the new @@ -627,10 +642,9 @@ function [InitLoc, Message] = ReferenceHeadLocation(ChannelMat, sInput) % Compute initial head location in Dewar coordinates. - % Here we want to recreate the correct triangle shape from the relative - % head coil locations and in the position saved as the reference - % (initial) head position according to Brainstorm coordinate - % transformation matrices. + % Here we want to recreate the correct triangle shape from the relative head + % coil locations and in the position saved as the reference (initial) head + % position according to Brainstorm coordinate transformation matrices. if nargin < 2 sInput = []; diff --git a/toolbox/process/functions/process_headpoints_refine.m b/toolbox/process/functions/process_headpoints_refine.m index 9cf995a6f..5c8622455 100644 --- a/toolbox/process/functions/process_headpoints_refine.m +++ b/toolbox/process/functions/process_headpoints_refine.m @@ -41,7 +41,7 @@ sProcess.options.title.Comment = [... 'Refine the MEG/MRI registration using digitized head points.
' ... 'If (tolerance > 0): fit the head points, remove the digitized points the most
' ... - 'distant to the scalp surface, and fit again the the head points on the scalp.


']; + 'distant to the scalp surface, and fit again the head points on the scalp.

']; sProcess.options.title.Type = 'label'; % Tolerance sProcess.options.tolerance.Comment = 'Tolerance (outlier points to ignore):'; diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index 25e05ddff..cb465e946 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -207,7 +207,7 @@ % Check if already adjusted, in which case the transformation above is correct (identity if same head points). sMriOld = in_mri_bst(sSubject.Anatomy(sSubject.iAnatomy).FileName); % History string is set in figure_mri SaveMri. - if isfield(sMriOld, 'History') && ~isempty(sMriOld.History) && any(strcmpi(sMriOld.History(:,2), 'Applied digitized anatomical fiducials')) + if isfield(sMriOld, 'History') && ~isempty(sMriOld.History) && any(strcmpi(sMriOld.History(:,3), 'Applied digitized anatomical fiducials')) if isWarning bst_warning('Nasion and ear points already adjusted.', 'Automatic EEG-MEG/MRI registration', 0); end From c3894baf05445f2b750961368efd5b860b65eaee Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Sun, 20 Feb 2022 15:12:25 -0500 Subject: [PATCH 03/32] debugging --- toolbox/gui/figure_mri.m | 7 +-- .../functions/process_adjust_coordinates.m | 27 +++++----- toolbox/sensors/channel_align_auto.m | 53 +++++++++++-------- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/toolbox/gui/figure_mri.m b/toolbox/gui/figure_mri.m index c5fe40fbc..bb53b3f38 100644 --- a/toolbox/gui/figure_mri.m +++ b/toolbox/gui/figure_mri.m @@ -2449,11 +2449,12 @@ function ButtonSave_Callback(hFig, varargin) % If the fiducials were modified if isfield(sMriOld, 'SCS') && all(isfield(sMriOld.SCS,{'NAS','LPA','RPA'})) ... && ~isempty(sMriOld.SCS.NAS) && ~isempty(sMriOld.SCS.LPA) && ~isempty(sMriOld.SCS.RPA) ... - && ((max(sMri.SCS.NAS - sMriOld.SCS.NAS) > 1e-3) || ... - (max(sMri.SCS.LPA - sMriOld.SCS.LPA) > 1e-3) || ... - (max(sMri.SCS.RPA - sMriOld.SCS.RPA) > 1e-3)) + && ((max(abs(sMri.SCS.NAS - sMriOld.SCS.NAS)) > 1e-3) || ... + (max(abs(sMri.SCS.LPA - sMriOld.SCS.LPA)) > 1e-3) || ... + (max(abs(sMri.SCS.RPA - sMriOld.SCS.RPA)) > 1e-3)) % Nothing to do... else + % sMri.SCS.R, T and Origin are updated before calling this function. sMriOld = []; end diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index 1239665d5..c3bd836ed 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -31,7 +31,6 @@ end - function sProcess = GetDescription() % Description of the process sProcess.Comment = 'Adjust coordinate system'; @@ -197,18 +196,21 @@ % history. if sProcess.options.points.Value && sProcess.options.scs.Value % Get subject in database, with subject directory - sSubject = bst_get('Subject', sInputs(iFile).FileName); + sSubject = bst_get('Subject', sInputs(iFile).SubjectFile); sMri = in_mri_bst(sSubject.Anatomy(sSubject.iAnatomy).FileName); % Slightly change the string we use to verify if it was done: append " (hidden)". - for iH = find(strcmpi(sMri.History(:,3), 'Applied digitized anatomical fiducials')) - sMri.History{iH,3} = [sMri.History{iH,3}, ' (hidden)']; - end - try - bst_save(file_fullpath(sMri.FileName), sMri, 'v7'); - catch - bst_report('Error', sProcess, sInputs(iFile), ... - sprintf('Unable to save MRI file %s.', sMri.FileName)); - continue; + iHist = find(strcmpi(sMri.History(:,3), 'Applied digitized anatomical fiducials')); + if ~isempty(iHist) + for iH = 1:numel(iHist) + sMri.History{iHist(iH),3} = [sMri.History{iHist(iH),3}, ' (hidden)']; + end + try + bst_save(file_fullpath(sMri.FileName), sMri, 'v7'); + catch + bst_report('Error', sProcess, sInputs(iFile), ... + sprintf('Unable to save MRI file %s.', sMri.FileName)); + continue; + end end end @@ -228,9 +230,10 @@ if ~sProcess.options.remove.Value && sProcess.options.points.Value % Redundant, but makes sense to have it here also. + Tolerance = sProcess.options.tolerance.Value{1} / 100; bst_progress('text', 'Fitting head surface to points...'); [ChannelMat, R, T, isSkip] = channel_align_auto(sInputs(iFile).ChannelFile, ... - ChannelMat, 0, 0, sProcess.options.tolerance.Value, sProcess.options.scs.Value); % No warning or confirmation + ChannelMat, 0, 0, Tolerance, sProcess.options.scs.Value); % No warning or confirmation % ChannelFile needed to find subject and scalp surface, but not % used otherwise when ChannelMat is provided. if isSkip diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index cb465e946..cc173ae77 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -101,7 +101,7 @@ % Get subject [sSubject, iSubject] = bst_get('Subject', sStudy.BrainStormSubject); % Check if default anatomy. (Usually also checked before calling this function.) -if iSubject == 0 +if sSubject.UseDefaultAnat if isWarning bst_error('Digitized nasion and ear points cannot be applied to default anatomy.', 'Automatic EEG-MEG/MRI registration', 0); end @@ -204,31 +204,38 @@ %% ===== ADJUST MRI FIDUCIALS AND SCS ===== if isAdjustScs - % Check if already adjusted, in which case the transformation above is correct (identity if same head points). - sMriOld = in_mri_bst(sSubject.Anatomy(sSubject.iAnatomy).FileName); - % History string is set in figure_mri SaveMri. - if isfield(sMriOld, 'History') && ~isempty(sMriOld.History) && any(strcmpi(sMriOld.History(:,3), 'Applied digitized anatomical fiducials')) - if isWarning - bst_warning('Nasion and ear points already adjusted.', 'Automatic EEG-MEG/MRI registration', 0); - end + % Check if already adjusted, in which case the transformation above is correct (identity if same head points). + sMriOld = in_mri_bst(sSubject.Anatomy(sSubject.iAnatomy).FileName); + % History string is set in figure_mri SaveMri. + if isfield(sMriOld, 'History') && ~isempty(sMriOld.History) && any(strcmpi(sMriOld.History(:,3), 'Applied digitized anatomical fiducials')) + if isWarning + bst_warning('Nasion and ear points already adjusted.', 'Automatic EEG-MEG/MRI registration', 0); + end % Check if digitized anat points present, saved in ChannelMat.SCS. % Note that these coordinates are NOT currently updated when doing refine with head points (below). - elseif all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) && (length(ChannelMat.SCS.NAS) == 3) && (length(ChannelMat.SCS.LPA) == 3) && (length(ChannelMat.SCS.RPA) == 3) - % Convert to MRI SCS coordinates. - % To do this we need to apply the transformation computed above. - sMri = sMriOld; - sMri.SCS.NAS = DigToMriTransf(1:3,:) * [ChannelMat.SCS.NAS; 1]; - sMri.SCS.LPA = DigToMriTransf(1:3,:) * [ChannelMat.SCS.LPA; 1]; - sMri.SCS.RPA = DigToMriTransf(1:3,:) * [ChannelMat.SCS.RPA; 1]; - - % Compare with existing MRI fids, replace if changed, and update surfaces. - figure_mri('SaveMri', sMri); + elseif all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) && (length(ChannelMat.SCS.NAS) == 3) && (length(ChannelMat.SCS.LPA) == 3) && (length(ChannelMat.SCS.RPA) == 3) + % Convert to MRI SCS coordinates. + % To do this we need to apply the transformation computed above. + sMri = sMriOld; + sMri.SCS.NAS = (DigToMriTransf(1:3,:) * [ChannelMat.SCS.NAS'; 1])'; + sMri.SCS.LPA = (DigToMriTransf(1:3,:) * [ChannelMat.SCS.LPA'; 1])'; + sMri.SCS.RPA = (DigToMriTransf(1:3,:) * [ChannelMat.SCS.RPA'; 1])'; + % Then convert to MRI coordinates (mm), this is how sMri.SCS is saved. + sMri.SCS.NAS = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.NAS) .* 1000; + sMri.SCS.LPA = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.LPA) .* 1000; + sMri.SCS.RPA = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.RPA) .* 1000; + % Re-compute transformation + [unused, sMri] = cs_compute(sMri, 'scs'); - % Adjust transformation from fit above. MRI SCS now matches Digitized SCS. - DigToMriTransf = eye(4); - R = eye(3); - T = zeros(3,1); - end + % Compare with existing MRI fids, replace if changed, and update surfaces. + sMri.FileName = sSubject.Anatomy(sSubject.iAnatomy).FileName; + figure_mri('SaveMri', sMri); + + % Adjust transformation from headpoints fit above. MRI SCS now matches digitized SCS (defined from same points). + DigToMriTransf = eye(4); + R = eye(3); + T = zeros(3,1); + end end From b95846ea88ab256cf600e866acbf7f5e0bd8e459 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Sun, 20 Feb 2022 15:20:59 -0500 Subject: [PATCH 04/32] add option to process_refine --- toolbox/process/functions/process_headpoints_refine.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/toolbox/process/functions/process_headpoints_refine.m b/toolbox/process/functions/process_headpoints_refine.m index 5c8622455..0e9d85a8a 100644 --- a/toolbox/process/functions/process_headpoints_refine.m +++ b/toolbox/process/functions/process_headpoints_refine.m @@ -47,6 +47,9 @@ sProcess.options.tolerance.Comment = 'Tolerance (outlier points to ignore):'; sProcess.options.tolerance.Type = 'value'; sProcess.options.tolerance.Value = {0, '%', 0}; + sProcess.options.scs.Type = 'checkbox'; + sProcess.options.scs.Comment = 'Also ajust MRI nasion and ear points.'; + sProcess.options.scs.Value = 0; end @@ -65,7 +68,7 @@ % Loop on all the channel files for i = 1:length(uniqueChan) % Refine registration - [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(uniqueChan{i}, [], 0, 0, tolerance); + [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(uniqueChan{i}, [], 0, 0, tolerance, sProcess.options.scs.Value); if ~isempty(strReport) bst_report('Info', sProcess, sInputs, strReport); end From cf5a1410011dec1171bc780f61d5b212d5ae9c15 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Sun, 20 Feb 2022 16:09:17 -0500 Subject: [PATCH 05/32] improved report --- toolbox/process/functions/process_adjust_coordinates.m | 10 ++++++---- toolbox/process/functions/process_headpoints_refine.m | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index c3bd836ed..c6594c320 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -232,13 +232,15 @@ Tolerance = sProcess.options.tolerance.Value{1} / 100; bst_progress('text', 'Fitting head surface to points...'); - [ChannelMat, R, T, isSkip] = channel_align_auto(sInputs(iFile).ChannelFile, ... + [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(sInputs(iFile).ChannelFile, ... ChannelMat, 0, 0, Tolerance, sProcess.options.scs.Value); % No warning or confirmation % ChannelFile needed to find subject and scalp surface, but not % used otherwise when ChannelMat is provided. - if isSkip - bst_report('Error', sProcess, sInputs(iFile), ... - 'Error trying to refine registration using head points.'); + if ~isempty(strReport) + bst_report('Info', sProcess, sInputs(iFile), strReport); + elseif isSkip + bst_report('Warning', sProcess, sInputs(iFile), ... + 'Refine registration using head points, failed finding a better fit.'); continue; end diff --git a/toolbox/process/functions/process_headpoints_refine.m b/toolbox/process/functions/process_headpoints_refine.m index 0e9d85a8a..5d862fbc8 100644 --- a/toolbox/process/functions/process_headpoints_refine.m +++ b/toolbox/process/functions/process_headpoints_refine.m @@ -64,13 +64,13 @@ % Get options tolerance = sProcess.options.tolerance.Value{1} / 100; % Get all the channel files - uniqueChan = unique({sInputs.ChannelFile}); + [uniqueChan, iUniqFiles] = unique({sInputs.ChannelFile}); % Loop on all the channel files for i = 1:length(uniqueChan) % Refine registration [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(uniqueChan{i}, [], 0, 0, tolerance, sProcess.options.scs.Value); if ~isempty(strReport) - bst_report('Info', sProcess, sInputs, strReport); + bst_report('Info', sProcess, sInputs(iUniqFiles(i)), strReport); end end % Return all the files in input From 19fa381bbd96f64397f281008c4bb0f8e9c51fe5 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Fri, 25 Feb 2022 11:23:31 -0500 Subject: [PATCH 06/32] improved scalp surface fit --- toolbox/math/bst_meshfit.m | 222 +++++++++++------- .../functions/process_adjust_coordinates.m | 5 +- toolbox/sensors/channel_align_auto.m | 24 +- 3 files changed, 151 insertions(+), 100 deletions(-) diff --git a/toolbox/math/bst_meshfit.m b/toolbox/math/bst_meshfit.m index 5ad40a256..5868809f4 100644 --- a/toolbox/math/bst_meshfit.m +++ b/toolbox/math/bst_meshfit.m @@ -1,18 +1,19 @@ -function [R, T, newP, distFinal] = bst_meshfit(Vertices, Faces, P) +function [R, T, newP, distFinal] = bst_meshfit(Vertices, Faces, P, Outliers) % BST_MESHFIT: Find the best possible rotation-translation to fit a point cloud on a mesh. % % USAGE: [R, T, newP, distFinal] = bst_meshfit(Vertices, Faces, P) % -% DESCRIPTION: +% DESCRIPTION: % A Gauss-Newton method is used for the optimization of the distance points/mesh. -% The Gauss-Newton algorithm used here was initially implemented by +% The Gauss-Newton algorithm used here was initially implemented by % Qianqian Fang (fangq at nmr.mgh.harvard.edu) and distributed under a GPL license -% as part of the Metch toolbox (http://iso2mesh.sf.net, regpt2surf.m). +% as part of the Metch toolbox (http://iso2mesh.sf.net, regpt2m). % % INPUTS: % - Vertices : [Mx3] double matrix % - Faces : [Nx3] double matrix % - P : [Qx3] double matrix, points to fit on the mesh defined by Vertices/Faces +% - Outliers : proportion of outlier points to ignore (between 0 and 1) % % OUTPUTS: % - R : [3x3] rotation matrix from the original P to the fitted positions. @@ -23,12 +24,12 @@ % @============================================================================= % This function is part of the Brainstorm software: % https://neuroimage.usc.edu/brainstorm -% +% % Copyright (c) University of Southern California & McGill University % This software is distributed under the terms of the GNU General Public License % as published by the Free Software Foundation. Further details on the GPLv3 % license can be found at http://www.gnu.org/copyleft/gpl.html. -% +% % FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE % UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY % WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF @@ -40,20 +41,63 @@ % % Authors: Qianqian Fang, 2008 % Francois Tadel, 2013-2021 +% Marc Lalancette, 2022 + +% Coordinates are in m. + +if nargin < 4 || isempty(Outliers) + Outliers = 0; +end + +% nV = size(Vertices, 1); +nF = size(Faces, 1); +nP = size(P, 1); +Outliers = ceil(Outliers * nP); + +% Edges as indices +Edges = unique(sort([Faces(:,[1,2]); Faces(:,[2,3]); Faces(:,[3,1])], 2), 'rows'); +% Edge direction "doubly normalized" so that later projection should be between 0 and 1. +EdgeDir = Vertices(Edges(:,2),:) - Vertices(Edges(:,1),:); +EdgeL = sqrt(sum(EdgeDir.^2, 2)); +EdgeDir = bsxfun(@rdivide, EdgeDir, EdgeL); +% Edges as vectors +EdgesV = zeros(nF, 3, 3); +EdgesV(:,:,1) = Vertices(Faces(:,2),:) - Vertices(Faces(:,1),:); +EdgesV(:,:,2) = Vertices(Faces(:,3),:) - Vertices(Faces(:,2),:); +EdgesV(:,:,3) = Vertices(Faces(:,1),:) - Vertices(Faces(:,3),:); +% First edge to second edge: counter clockwise = up +FaceNormals = normr(cross(EdgesV(:,:,1), EdgesV(:,:,2))); +%FaceArea = sqrt(sum(FaceNormalsA.^2, 2)); +% Perpendicular vectors to edges, pointing inside triangular face. +for e = 3:-1:1 + EdgeTriNormals(:,:,e) = cross(FaceNormals, EdgesV(:,:,e)); +end +FaceVertices = zeros(nF, 3, 3); +FaceVertices(:,:,1) = Vertices(Faces(:,1),:); +FaceVertices(:,:,2) = Vertices(Faces(:,2),:); +FaceVertices(:,:,3) = Vertices(Faces(:,3),:); -% Calculate norms -VertNorm = tess_normals(Vertices, Faces); % Calculate the initial error -[distInit, dt] = get_distance(Vertices, VertNorm, P, []); -errInit = sum(abs(distInit)); +InitParams = zeros(6,1); +errInit = CostFunction(InitParams); % Fit points -[R,T,newP] = fit_points(Vertices, VertNorm, P, dt); +% [R,T,newP] = fit_points(Vertices, VertNorm, P, dt); +% Do optimization +% Stop at 0.1 mm total distance, or 0.1 mm displacement. +OptimOptions = optimoptions(@fminunc, 'MaxFunctionEvaluations', 1000, 'MaxIterations', 200, ... + 'FiniteDifferenceStepSize', 1e-3, ... + 'FunctionTolerance', 1e-4, 'StepTolerance', 5e-2, 'Display', 'none'); % 'OptimalityTolerance', 1e-15, 'final-detailed' + +BestParams = fminunc(@CostFunction, InitParams, OptimOptions); +[R,T,newP] = Transform(BestParams, P); +T = T'; +distFinal = PointSurfDistance(newP); % Calculate the final error -distFinal = get_distance(Vertices, VertNorm, newP, dt); -errFinal = sum(abs(distFinal)); +errFinal = CostFunction(BestParams); % If the error is larger than at the beginning: cancel the modifications +% Should no longer occur. if (errFinal > errInit) disp('BST> The optimization failed finding a better fit'); R = []; @@ -61,88 +105,94 @@ newP = P; end -end - +% Better cost function for points fitting: higher cost for points inside the +% head > 1mm, (better distance calculation). + function [Cost, Dist] = CostFunction(Params) + [~,~,Points] = Transform(Params, P); + Dist = PointSurfDistance(Points); + isInside = inpolyhedron(Faces, Vertices, Points, 'FaceNormals', FaceNormals, 'FlipNormals', true); + %patch('Faces',Faces,'Vertices',Vertices); hold on; light; axis equal; + %quiver3(FaceVertices(:,1,1), FaceVertices(:,2,1), FaceVertices(:,3,1), FaceNormals(:,1,1), FaceNormals(:,2,1), FaceNormals(:,3,1)); + %scatter3(Points(1,1),Points(1,2),Points(1,3)); + iSquare = isInside & Dist > 0.001; + Dist(iSquare) = Dist(iSquare).^2 *1e3; % factor for "squaring mm" + Cost = sum(Dist); + for iP = 1:Outliers + [MaxD, iMaxD] = max(Dist); + Dist(iMaxD) = 0; + Cost = Cost - MaxD; + end + end +%% TODO Slow, look for alternatives. +% This seems similar: https://www.mathworks.com/matlabcentral/fileexchange/52882-point2trimesh-distance-between-point-and-triangulated-surface % ===== COMPUTE POINTS/MESH DISTANCE ===== -% Approximates the distance to the mesh by the projection on the norm vector of the nearest neighbor -function [dist,dt] = get_distance(Vertices, VertNorm, P, dt) - % Find the nearest neighbor - [iNearest, dist_pt, dt] = bst_nearest(Vertices, P, 1, 0, dt); - % Distance = projection of the distance between the point and its nearest - % neighbor in the surface on the vertex normal - % As the head surface is supposed to be very smooth, it should be a good approximation - % of the distance from the point to the surface. - dist = abs(sum(VertNorm(iNearest,:) .* (P - Vertices(iNearest,:)),2)); -end - -% ===== COMPUTE TRANSFORMATION ===== -% Gauss-Newton optimization algorithm -% Based on work from Qianqian Fang, 2008 -function [R,T,newP] = fit_points(Vertices, VertNorm, P, dt) - % Initial parameters: no rotation, no translation - C = zeros(6,1); - newP = P; - % Sensitivity - delta = 1e-4; - % Maximum number of iterations - maxiter = 20; - % Initialize error at the previous iteration - errPrev = Inf; - % Start Gauss-Newton iterations - for iter = 1:maxiter - % Calculate the current residual: the sum of distances to the surface - dist0 = get_distance(Vertices, VertNorm, newP, dt); - err = sum(abs(dist0)); - % If global error is going up: stop - if (err > errPrev) - break; +% For exact distance computation, we need to check all 3: vertices, edges and faces. + function Dist = PointSurfDistance(Points) + Epsilon = 1e-9; % nanometer + % Check distance to vertices + if license('test','statistics_toolbox') + DistVert = pdist2(Vertices, Points, 'euclidean', 'Smallest', 1)'; + else + DistVert = zeros(nP, 1); + for iP = 1:nP + % Find closest surface vertex. + DistVert(iP) = sqrt(min(sum(bsxfun(@minus, Points(iP, :), Vertices).^2, 2))); + end + end + % Check distance to faces + DistFace = inf(nP, 1); + for iP = 1:nP + % Considered Möller and Trumbore 1997, Ray-Triangle Intersection (https://stackoverflow.com/questions/42740765/intersection-between-line-and-triangle-in-3d), but this is simpler still. + % Vectors from triangle vertices to point. + Pyramid = bsxfun(@minus, Points(iP, :), FaceVertices); + % Does the point project inside each face? + InFace = all(sum(Pyramid .* EdgeTriNormals, 2) > -Epsilon, 3); + if any(InFace) + DistFace(iP) = min(abs(sum(Pyramid(InFace,:,1) .* FaceNormals(InFace,:), 2))); + end end - errPrev = err; - % fprintf('iter=%d error=%f\n', iter, err); - % Build the Jacobian (sensitivity) matrix - J = zeros(length(dist0),length(C)); - for i = 1:length(C) - dC = C; - if (C(i)) - dC(i) = C(i) * (1+delta); - else - dC(i) = C(i) + delta; + % Check distance to edges + DistEdge = inf(nP, 1); + for iP = 1:nP + % Vector from first edge vertex to point. + Pyramid = bsxfun(@minus, Points(iP, :), Vertices(Edges(:, 1), :)); + Projection = sum(Pyramid .* EdgeDir, 2); + InEdge = Projection > -Epsilon & Projection < (EdgeL + Epsilon); + if any(InEdge) + DistEdge(iP) = sqrt(min(sum((Pyramid(InEdge,:) - bsxfun(@times, Projection(InEdge), EdgeDir(InEdge,:))).^2, 2))); end - % Apply this new transformation to the points - [tmpR,tmpT,tmpP] = get_transform(dC, P); - % Calculate the distance for this new transformation - dist = get_distance(Vertices, VertNorm, tmpP, dt); - % J=dL/dC - J(:,i) = (dist-dist0) / (dC(i)-C(i)); end - % Weight the matrix (normalization) - wj = sqrt(sum(J.*J)); - J = J ./ repmat(wj,length(dist0),1); - % Calculate the update: J*dC=dL - dC = (J\dist0) ./ wj'; - C = C - 0.5*dC; - % Get the updated positions with the calculated A and b - [R,T,newP] = get_transform(C, P); + + Dist = min([DistVert, DistEdge, DistFace], [], 2); end -end + + +% % Approximates the distance to the mesh by the projection on the norm vector of the nearest neighbor +% function [dist,dt] = get_distance(Vertices, VertNorm, P, dt) +% % Find the nearest neighbor +% [iNearest, dist_pt, dt] = bst_nearest(Vertices, P, 1, 0, dt); +% % Distance = projection of the distance between the point and its nearest +% % neighbor in the surface on the vertex normal +% % As the head surface is supposed to be very smooth, it should be a good approximation +% % of the distance from the point to the surface. +% dist = abs(sum(VertNorm(iNearest,:) .* (P - Vertices(iNearest,:)),2)); +% end % ===== GET TRANSFORMATION ===== -function [R,T,P] = get_transform(params, P) - % Get values - mx = params(1); my = params(2); mz = params(3); % Translation parameters - x = params(4); y = params(5); z = params(6); % Rotation parameters - % Rotation - Rx = [1 0 0 ; 0 cos(x) sin(x) ; 0 -sin(x) cos(x)]; % Rotation over x - Ry = [cos(y) 0 -sin(y); 0 1 0; sin(y) 0 cos(y)]; % Rotation over y - Rz = [cos(z) sin(z) 0; -sin(z) cos(z) 0; 0 0 1]; % Rotation over z - R = Rx*Ry*Rz; - % Translation - T = [mx; my; mz]; - % Apply to points - if (nargin >= 2) - P = (R * P' + T * ones(1,size(P,1)))'; + function [R,T,Points] = Transform(Params, Points) + % Translation in mm (to use default TypicalX of 1) + T = Params(1:3)'/1e3; + % Rotation in degrees (again for expected order of magnitude of 1) + x = Params(4)*pi/180; y = Params(5)*pi/180; z = Params(6)*pi/180; % Rotation parameters + Rx = [1 0 0 ; 0 cos(x) sin(x) ; 0 -sin(x) cos(x)]; % Rotation over x + Ry = [cos(y) 0 -sin(y); 0 1 0; sin(y) 0 cos(y)]; % Rotation over y + Rz = [cos(z) sin(z) 0; -sin(z) cos(z) 0; 0 0 1]; % Rotation over z + R = Rx*Ry*Rz; + % Apply to points + Points = bsxfun(@plus, Points * R', T); end + end diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index c6594c320..5e4214b97 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -197,6 +197,7 @@ if sProcess.options.points.Value && sProcess.options.scs.Value % Get subject in database, with subject directory sSubject = bst_get('Subject', sInputs(iFile).SubjectFile); + MriFile = sSubject.Anatomy(sSubject.iAnatomy).FileName; sMri = in_mri_bst(sSubject.Anatomy(sSubject.iAnatomy).FileName); % Slightly change the string we use to verify if it was done: append " (hidden)". iHist = find(strcmpi(sMri.History(:,3), 'Applied digitized anatomical fiducials')); @@ -205,10 +206,10 @@ sMri.History{iHist(iH),3} = [sMri.History{iHist(iH),3}, ' (hidden)']; end try - bst_save(file_fullpath(sMri.FileName), sMri, 'v7'); + bst_save(file_fullpath(MriFile), sMri, 'v7'); catch bst_report('Error', sProcess, sInputs(iFile), ... - sprintf('Unable to save MRI file %s.', sMri.FileName)); + sprintf('Unable to save MRI file %s.', MriFile)); continue; end end diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index cc173ae77..5e70469c9 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -169,19 +169,19 @@ %% ===== FIND OPTIMAL FIT ===== % Find best possible rigid transformation (rotation+translation) -[R,T,tmp,dist] = bst_meshfit(SurfaceMat.Vertices, SurfaceMat.Faces, HP); +[R,T,tmp,dist] = bst_meshfit(SurfaceMat.Vertices, SurfaceMat.Faces, HP, tolerance); % Remove outliers and fit again -if ~isempty(dist) && ~isempty(tolerance) && (tolerance > 0) - % Sort points by distance to scalp - [tmp__, iSort] = sort(dist, 1, 'descend'); - iRemove = iSort(1:nRemove); - % Remove from list of destination points - HP(iRemove,:) = []; - % Fit again - [R,T,tmp,dist] = bst_meshfit(SurfaceMat.Vertices, SurfaceMat.Faces, HP); -else - nRemove = 0; -end +% if ~isempty(dist) && ~isempty(tolerance) && (tolerance > 0) +% % Sort points by distance to scalp +% [tmp__, iSort] = sort(dist, 1, 'descend'); +% iRemove = iSort(1:nRemove); +% % Remove from list of destination points +% HP(iRemove,:) = []; +% % Fit again +% [R,T,tmp,dist] = bst_meshfit(SurfaceMat.Vertices, SurfaceMat.Faces, HP); +% else +% nRemove = 0; +% end % Current position cannot be optimized if isempty(R) bst_progress('stop'); From 02f02d45afcdd38d8dc3f622e6ef56bfb6901612 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Wed, 2 Mar 2022 16:05:11 -0500 Subject: [PATCH 07/32] wip head surface & fit head points --- toolbox/anatomy/tess_isohead.m | 213 ++++++++++++++++++++++----- toolbox/math/bst_meshfit.m | 31 ++-- toolbox/sensors/channel_align_auto.m | 8 + 3 files changed, 202 insertions(+), 50 deletions(-) diff --git a/toolbox/anatomy/tess_isohead.m b/toolbox/anatomy/tess_isohead.m index 5c5343eb5..25c0c560c 100644 --- a/toolbox/anatomy/tess_isohead.m +++ b/toolbox/anatomy/tess_isohead.m @@ -7,12 +7,12 @@ % @============================================================================= % This function is part of the Brainstorm software: % https://neuroimage.usc.edu/brainstorm -% +% % Copyright (c) University of Southern California & McGill University % This software is distributed under the terms of the GNU General Public License % as published by the Free Software Foundation. Further details on the GPLv3 % license can be found at http://www.gnu.org/copyleft/gpl.html. -% +% % FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE % UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY % WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF @@ -43,7 +43,7 @@ end %% ===== LOAD MRI ===== -% Load MRI +% Load MRI bst_progress('start', 'Generate head surface', 'Loading MRI...'); sMri = bst_memory('LoadMri', MriFile); bst_progress('stop'); @@ -94,24 +94,62 @@ % Progress bar bst_progress('start', 'Generate head surface', 'Creating head mask...', 0, 100); % Threshold mri to the level estimated in the histogram -headmask = (sMri.Cube(:,:,:,1) > bgLevel); +headmask = sMri.Cube(:,:,:,1) > bgLevel; % Closing all the faces of the cube -headmask(1,:,:) = 0*headmask(1,:,:); -headmask(end,:,:) = 0*headmask(1,:,:); -headmask(:,1,:) = 0*headmask(:,1,:); -headmask(:,end,:) = 0*headmask(:,1,:); -headmask(:,:,1) = 0*headmask(:,:,1); -headmask(:,:,end) = 0*headmask(:,:,1); +headmask(1,:,:) = 0; %*headmask(1,:,:); +headmask(end,:,:) = 0; %*headmask(1,:,:); +headmask(:,1,:) = 0; %*headmask(:,1,:); +headmask(:,end,:) = 0; %*headmask(:,1,:); +headmask(:,:,1) = 0; %*headmask(:,:,1); +headmask(:,:,end) = 0; %*headmask(:,:,1); % Erode + dilate, to remove small components -if (erodeFactor > 0) - headmask = headmask & ~mri_dilate(~headmask, erodeFactor); - headmask = mri_dilate(headmask, erodeFactor); +% if (erodeFactor > 0) +% headmask = headmask & ~mri_dilate(~headmask, erodeFactor); +% headmask = mri_dilate(headmask, erodeFactor); +% end +% bst_progress('inc', 10); + +% Remove isolated voxels (dots or holes) from 5 out of 6 sides +% isFill = false(size(headmask)); +% isFill(2:end-1,2:end-1,2:end-1) = (headmask(1:end-2,2:end-1,2:end-1) + headmask(3:end,2:end-1,2:end-1) + ... +% headmask(2:end-1,1:end-2,2:end-1) + headmask(2:end-1,3:end,2:end-1) + ... +% headmask(2:end-1,2:end-1,1:end-2) + headmask(2:end-1,2:end-1,3:end)) >= 5 & ... +% ~headmask(2:end-1,2:end-1,2:end-1); +% headmask(isFill) = 1; +% isFill = false(size(headmask)); +% isFill(2:end-1,2:end-1,2:end-1) = (headmask(1:end-2,2:end-1,2:end-1) + headmask(3:end,2:end-1,2:end-1) + ... +% headmask(2:end-1,1:end-2,2:end-1) + headmask(2:end-1,3:end,2:end-1) + ... +% headmask(2:end-1,2:end-1,1:end-2) + headmask(2:end-1,2:end-1,3:end)) <= 1 & ... +% headmask(2:end-1,2:end-1,2:end-1); +% headmask(isFill) = 0; + +% Fill neck holes (bones, etc.) where it is cut at edge of volume. +bst_progress('text', 'Filling holes and removing disconnected parts...'); +for iDim = 1:3 + % Swap slice dimension into first position. + switch iDim + case 1 + Perm = 1:3; + case 2 + Perm = [2, 1, 3]; + case 3 + Perm = [3, 2, 1]; + end + TempMask = permute(headmask, Perm); + % Edit second and second-to-last slices + Slice = TempMask(2, :, :); + TempMask(2, :, :) = Slice | (Fill(Slice, 2) & Fill(Slice, 3)); + Slice = TempMask(end-1, :, :); + TempMask(end-1, :, :) = Slice | (Fill(Slice, 2) & Fill(Slice, 3)); + % Permute back + headmask = permute(TempMask, Perm); end -bst_progress('inc', 10); % Fill holes -bst_progress('text', 'Filling holes...'); -headmask = (mri_fillholes(headmask, 1) & mri_fillholes(headmask, 2) & mri_fillholes(headmask, 3)); -bst_progress('inc', 10); +InsideMask = (Fill(headmask, 1) & Fill(headmask, 2) & Fill(headmask, 3)); +headmask = InsideMask | (Dilate(InsideMask) & headmask); +% Keep only central connected volume (trim "beard" or bubbles) +headmask = CenterSpread(headmask); +bst_progress('inc', 20); % view_mri_slices(headmask, 'x', 20) @@ -119,45 +157,66 @@ %% ===== CREATE SURFACE ===== % Compute isosurface bst_progress('text', 'Creating isosurface...'); +% Could have avoided x-y flip by specifying XYZ in isosurface... [sHead.Faces, sHead.Vertices] = mri_isosurface(headmask, 0.5); -bst_progress('inc', 10); +% Flip x-y back to our voxel coordinates. +sHead.Vertices = sHead.Vertices(:, [2, 1, 3]); +bst_progress('inc', 20); % Downsample to a maximum number of vertices -maxIsoVert = 60000; -if (length(sHead.Vertices) > maxIsoVert) - bst_progress('text', 'Downsampling isosurface...'); - [sHead.Faces, sHead.Vertices] = reducepatch(sHead.Faces, sHead.Vertices, maxIsoVert./length(sHead.Vertices)); - bst_progress('inc', 10); -end +% maxIsoVert = 60000; +% if (length(sHead.Vertices) > maxIsoVert) +% bst_progress('text', 'Downsampling isosurface...'); +% [sHead.Faces, sHead.Vertices] = reducepatch(sHead.Faces, sHead.Vertices, maxIsoVert./length(sHead.Vertices)); +% bst_progress('inc', 10); +% end % Remove small objects bst_progress('text', 'Removing small patches...'); [sHead.Vertices, sHead.Faces] = tess_remove_small(sHead.Vertices, sHead.Faces); -bst_progress('inc', 10); +bst_progress('inc', 20); + +% Clean final surface +% This is very strange, it doesn't look at face locations, only the normals. +% After isosurface, many many faces are parallel. +% bst_progress('text', 'Fill: Cleaning surface...'); +% [sHead.Vertices, sHead.Faces] = tess_clean(sHead.Vertices, sHead.Faces); + +% Smooth voxel artefacts, but preserve shape and volume. +bst_progress('text', 'Smoothing voxel artefacts...'); +% Should normally use 1 as voxel size, but using a larger value smooths. +sHead.Vertices = SurfaceSmooth(sHead.Vertices, sHead.Faces, 2, [], 50, [], false); % voxel/smoothing size, iterations, verbose % Downsampling isosurface if (length(sHead.Vertices) > nVertices) bst_progress('text', 'Downsampling surface...'); [sHead.Faces, sHead.Vertices] = reducepatch(sHead.Faces, sHead.Vertices, nVertices./length(sHead.Vertices)); - bst_progress('inc', 10); + bst_progress('inc', 20); end -% Convert to millimeters -sHead.Vertices = sHead.Vertices(:,[2,1,3]); -sHead.Faces = sHead.Faces(:,[2,1,3]); -sHead.Vertices = bst_bsxfun(@times, sHead.Vertices, sMri.Voxsize); % Convert to SCS -sHead.Vertices = cs_convert(sMri, 'mri', 'scs', sHead.Vertices ./ 1000); - -% Reduce the final size of the meshed volume -erodeFinal = 3; -% Fill holes in surface -%if (fillFactor > 0) - bst_progress('text', 'Filling holes...'); - [sHead.Vertices, sHead.Faces] = tess_fillholes(sMri, sHead.Vertices, sHead.Faces, fillFactor, erodeFinal); - bst_progress('inc', 30); +sHead.Vertices = cs_convert(sMri, 'voxel', 'scs', sHead.Vertices); +% Flip face order to Brainstorm convention +sHead.Faces = sHead.Faces(:,[2,1,3]); + +% % Smooth isosurface +% bst_progress('text', 'Fill: Smoothing surface...'); +% VertConn = tess_vertconn(Vertices, Faces); +% Vertices = tess_smooth(Vertices, 1, 10, VertConn, 0); +% % One final round of smoothing +% VertConn = tess_vertconn(Vertices, Faces); +% Vertices = tess_smooth(Vertices, 0.2, 3, VertConn, 0); +% +% % Reduce the final size of the meshed volume +% erodeFinal = 3; +% % Fill holes in surface +% if (fillFactor > 0) +% bst_progress('text', 'Filling holes...'); +% [sHead.Vertices, sHead.Faces] = tess_fillholes(sMri, sHead.Vertices, sHead.Faces, fillFactor, erodeFinal); +% bst_progress('inc', 30); % end %% ===== SAVE FILES ===== bst_progress('text', 'Saving new file...'); +bst_progress('inc', 15); % Create output filenames ProtocolInfo = bst_get('ProtocolInfo'); SurfaceDir = bst_fullfile(ProtocolInfo.SUBJECTS, bst_fileparts(MriFile)); @@ -174,7 +233,83 @@ % Close, success bst_progress('stop'); +end +%% ===== Subfunctions ===== +function mask = Fill(mask, dim) +% Modified to exclude boundaries, so we can get rid of external junk as well as +% internal holes easily. +% Initialize two accumulators, for the two directions +acc1 = false(size(mask)); +acc2 = false(size(mask)); +n = size(mask,dim); +% Process in required direction +switch dim + case 1 + for i = 2:n + acc1(i,:,:) = acc1(i-1,:,:) | mask(i-1,:,:); + end + for i = n-1:-1:1 + acc2(i,:,:) = acc2(i+1,:,:) | mask(i+1,:,:); + end + case 2 + for i = 2:n + acc1(:,i,:) = acc1(:,i-1,:) | mask(:,i-1,:); + end + for i = n-1:-1:1 + acc2(:,i,:) = acc2(:,i+1,:) | mask(:,i+1,:); + end + case 3 + for i = 2:n + acc1(:,:,i) = acc1(:,:,i-1) | mask(:,:,i-1); + end + for i = n-1:-1:1 + acc2(:,:,i) = acc2(:,:,i+1) | mask(:,:,i+1); + end +end +% Combine two accumulators +mask = acc1 & acc2; +end + +function mask = Dilate(mask) +% Dilate by 1 voxel in 6 directions, except at volume edges +mask(2:end-1,2:end-1,2:end-1) = mask(1:end-2,2:end-1,2:end-1) | mask(3:end,2:end-1,2:end-1) | ... + mask(2:end-1,1:end-2,2:end-1) | mask(2:end-1,3:end,2:end-1) | ... + mask(2:end-1,2:end-1,1:end-2) | mask(2:end-1,2:end-1,3:end); +end +function OutMask = CenterSpread(InMask) +% Similar to Fill, but from a central starting point and intersecting with the input "reference" mask. +OutMask = false(size(InMask)); +iStart = round(size(OutMask)/2); +nVox = size(OutMask); +OutMask(iStart(1), iStart(2), iStart(3)) = true; +nPrev = 0; +nOut = 1; +while nOut > nPrev + % Dilation loop was very slow. + % OutMask = OutMask | (Dilate(OutMask) & InMask); + for x = 2:nVox(1) + OutMask(x,:,:) = OutMask(x,:,:) | (OutMask(x-1,:,:) & InMask(x,:,:)); + end + for x = nVox(1)-1:-1:1 + OutMask(x,:,:) = OutMask(x,:,:) | (OutMask(x+1,:,:) & InMask(x,:,:)); + end + for y = 2:nVox(2) + OutMask(:,y,:) = OutMask(:,y,:) | (OutMask(:,y-1,:) & InMask(:,y,:)); + end + for y = nVox(2)-1:-1:1 + OutMask(:,y,:) = OutMask(:,y,:) | (OutMask(:,y+1,:) & InMask(:,y,:)); + end + for z = 2:nVox(3) + OutMask(:,:,z) = OutMask(:,:,z) | (OutMask(:,:,z-1) & InMask(:,:,z)); + end + for z = nVox(3)-1:-1:1 + OutMask(:,:,z) = OutMask(:,:,z) | (OutMask(:,:,z+1) & InMask(:,:,z)); + end + nPrev = nOut; + nOut = sum(OutMask(:)); +end +end diff --git a/toolbox/math/bst_meshfit.m b/toolbox/math/bst_meshfit.m index 5868809f4..0ec577892 100644 --- a/toolbox/math/bst_meshfit.m +++ b/toolbox/math/bst_meshfit.m @@ -44,7 +44,7 @@ % Marc Lalancette, 2022 % Coordinates are in m. - +PenalizeInside = true; if nargin < 4 || isempty(Outliers) Outliers = 0; end @@ -84,10 +84,10 @@ % Fit points % [R,T,newP] = fit_points(Vertices, VertNorm, P, dt); % Do optimization -% Stop at 0.1 mm total distance, or 0.1 mm displacement. +% Stop at 0.1 mm total distance, or 0.02 mm displacement. OptimOptions = optimoptions(@fminunc, 'MaxFunctionEvaluations', 1000, 'MaxIterations', 200, ... 'FiniteDifferenceStepSize', 1e-3, ... - 'FunctionTolerance', 1e-4, 'StepTolerance', 5e-2, 'Display', 'none'); % 'OptimalityTolerance', 1e-15, 'final-detailed' + 'FunctionTolerance', 1e-4, 'StepTolerance', 2e-2, 'Display', 'none'); % 'OptimalityTolerance', 1e-15, 'final-detailed' BestParams = fminunc(@CostFunction, InitParams, OptimOptions); [R,T,newP] = Transform(BestParams, P); @@ -110,16 +110,25 @@ function [Cost, Dist] = CostFunction(Params) [~,~,Points] = Transform(Params, P); Dist = PointSurfDistance(Points); - isInside = inpolyhedron(Faces, Vertices, Points, 'FaceNormals', FaceNormals, 'FlipNormals', true); - %patch('Faces',Faces,'Vertices',Vertices); hold on; light; axis equal; - %quiver3(FaceVertices(:,1,1), FaceVertices(:,2,1), FaceVertices(:,3,1), FaceNormals(:,1,1), FaceNormals(:,2,1), FaceNormals(:,3,1)); - %scatter3(Points(1,1),Points(1,2),Points(1,3)); - iSquare = isInside & Dist > 0.001; - Dist(iSquare) = Dist(iSquare).^2 *1e3; % factor for "squaring mm" + if PenalizeInside + isInside = inpolyhedron(Faces, Vertices, Points, 'FaceNormals', FaceNormals, 'FlipNormals', true); + %patch('Faces',Faces,'Vertices',Vertices); hold on; light; axis equal; + %quiver3(FaceVertices(:,1,1), FaceVertices(:,2,1), FaceVertices(:,3,1), FaceNormals(:,1,1), FaceNormals(:,2,1), FaceNormals(:,3,1)); + %scatter3(Points(1,1),Points(1,2),Points(1,3)); + iSquare = isInside & Dist > 0.001; + Dist(iSquare) = Dist(iSquare).^2 *1e3; % factor for "squaring mm" + iOutside = find(~isInside); + end Cost = sum(Dist); for iP = 1:Outliers - [MaxD, iMaxD] = max(Dist); - Dist(iMaxD) = 0; + if PenalizeInside + % Only remove outside points. + [MaxD, iMaxD] = max(Dist(~isInside)); + Dist(iOutside(iMaxD)) = 0; + else + [MaxD, iMaxD] = max(Dist); + Dist(iMaxD) = 0; + end Cost = Cost - MaxD; end end diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index 5e70469c9..176f749aa 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -94,6 +94,13 @@ end % M x 3 matrix of head points HP = double(HeadPoints.Loc'); +% % Add anatomical points. +% if isfield(ChannelMat, 'SCS') && all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) && ... +% (length(ChannelMat.SCS.NAS) == 3) && (length(ChannelMat.SCS.LPA) == 3) && (length(ChannelMat.SCS.RPA) == 3) +% HP(end+1,:) = ChannelMat.SCS.NAS; +% HP(end+1,:) = ChannelMat.SCS.LPA; +% HP(end+1,:) = ChannelMat.SCS.RPA; +% end %% ===== LOAD SCALP SURFACE ===== % Get study @@ -213,6 +220,7 @@ end % Check if digitized anat points present, saved in ChannelMat.SCS. % Note that these coordinates are NOT currently updated when doing refine with head points (below). + % They are in "initial SCS" coordinates, updated in channel_detect_type. elseif all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) && (length(ChannelMat.SCS.NAS) == 3) && (length(ChannelMat.SCS.LPA) == 3) && (length(ChannelMat.SCS.RPA) == 3) % Convert to MRI SCS coordinates. % To do this we need to apply the transformation computed above. From f09944cb32523cfab024f1dd85f756af2bb2b84b Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Thu, 2 Jun 2022 11:44:52 -0400 Subject: [PATCH 08/32] threshold input for tess_isohead --- toolbox/anatomy/tess_isohead.m | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/toolbox/anatomy/tess_isohead.m b/toolbox/anatomy/tess_isohead.m index 25c0c560c..21a8bc5f4 100644 --- a/toolbox/anatomy/tess_isohead.m +++ b/toolbox/anatomy/tess_isohead.m @@ -1,4 +1,4 @@ -function [HeadFile, iSurface] = tess_isohead(iSubject, nVertices, erodeFactor, fillFactor, Comment) +function [HeadFile, iSurface] = tess_isohead(iSubject, nVertices, erodeFactor, fillFactor, Comment, bgLevel) % TESS_GENERATE: Reconstruct a head surface based on the MRI, based on an isosurface % % USAGE: [HeadFile, iSurface] = tess_isohead(iSubject, nVertices=10000, erodeFactor=0, fillFactor=2, Comment) @@ -29,6 +29,9 @@ HeadFile = []; iSurface = []; % Parse inputs +if (nargin < 6) || isempty(bgLevel) + bgLevel = []; +end if (nargin < 5) || isempty(Comment) Comment = []; end @@ -80,7 +83,7 @@ if isempty(bgLevel) bgLevel = sMri.Histogram.bgLevel; end -else +elseif isempty(bgLevel) bgLevel = sMri.Histogram.bgLevel; end % Check parameters values @@ -149,7 +152,7 @@ headmask = InsideMask | (Dilate(InsideMask) & headmask); % Keep only central connected volume (trim "beard" or bubbles) headmask = CenterSpread(headmask); -bst_progress('inc', 20); +bst_progress('inc', 15); % view_mri_slices(headmask, 'x', 20) @@ -161,7 +164,7 @@ [sHead.Faces, sHead.Vertices] = mri_isosurface(headmask, 0.5); % Flip x-y back to our voxel coordinates. sHead.Vertices = sHead.Vertices(:, [2, 1, 3]); -bst_progress('inc', 20); +bst_progress('inc', 10); % Downsample to a maximum number of vertices % maxIsoVert = 60000; % if (length(sHead.Vertices) > maxIsoVert) @@ -172,7 +175,7 @@ % Remove small objects bst_progress('text', 'Removing small patches...'); [sHead.Vertices, sHead.Faces] = tess_remove_small(sHead.Vertices, sHead.Faces); -bst_progress('inc', 20); +bst_progress('inc', 15); % Clean final surface % This is very strange, it doesn't look at face locations, only the normals. @@ -183,14 +186,22 @@ % Smooth voxel artefacts, but preserve shape and volume. bst_progress('text', 'Smoothing voxel artefacts...'); % Should normally use 1 as voxel size, but using a larger value smooths. -sHead.Vertices = SurfaceSmooth(sHead.Vertices, sHead.Faces, 2, [], 50, [], false); % voxel/smoothing size, iterations, verbose +% Restrict iterations to make it faster, smooth a bit more (normal to surface +% only) after downsampling. +sHead.Vertices = SurfaceSmooth(sHead.Vertices, sHead.Faces, 2, [], 5, [], false); % voxel/smoothing size, iterations, verbose +bst_progress('inc', 20); % Downsampling isosurface if (length(sHead.Vertices) > nVertices) bst_progress('text', 'Downsampling surface...'); [sHead.Faces, sHead.Vertices] = reducepatch(sHead.Faces, sHead.Vertices, nVertices./length(sHead.Vertices)); - bst_progress('inc', 20); + bst_progress('inc', 15); end + +bst_progress('text', 'Smoothing...'); +sHead.Vertices = SurfaceSmooth(sHead.Vertices, sHead.Faces, 2, [], 45, 0, false); % voxel/smoothing size, iterations, freedom (normal), verbose +bst_progress('inc', 10); + % Convert to SCS sHead.Vertices = cs_convert(sMri, 'voxel', 'scs', sHead.Vertices); % Flip face order to Brainstorm convention @@ -216,7 +227,7 @@ %% ===== SAVE FILES ===== bst_progress('text', 'Saving new file...'); -bst_progress('inc', 15); +bst_progress('inc', 10); % Create output filenames ProtocolInfo = bst_get('ProtocolInfo'); SurfaceDir = bst_fullfile(ProtocolInfo.SUBJECTS, bst_fileparts(MriFile)); From c778b8a2b5d4400e70d15a9cc25e2bf0fe3641ad Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Tue, 9 Aug 2022 15:08:08 -0400 Subject: [PATCH 09/32] head shape from spatial gradient threshold --- toolbox/anatomy/mri_histogram.m | 32 +++++++++++++------- toolbox/anatomy/tess_isohead.m | 51 +++++++++++++++++++++++++++++--- toolbox/gui/view_mri_histogram.m | 25 ++++++++++------ 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/toolbox/anatomy/mri_histogram.m b/toolbox/anatomy/mri_histogram.m index 6d985006f..1badf8695 100644 --- a/toolbox/anatomy/mri_histogram.m +++ b/toolbox/anatomy/mri_histogram.m @@ -23,11 +23,11 @@ % |- max[4] : array of the 4 most important maxima (structure) % | |- x : intensity of the given maximum % | |- y : amplitude of this maximum (number of MRI voxels with this value) -% | |- power : difference of this maximum and the adjacent minima +% | |- power : difference of this maximum and the adjacent minimum % |- min[4] : array of the 3 most important minima (structure) % | |- x : intensity of the given minimum % | |- y : amplitude of this minimum (number of MRI voxels with this value) -% | |- power : difference of this minimum and the adjacent maxima +% | |- power : difference of this minimum and the adjacent maximum % |- bgLevel : intensity value that separates the background and the objects (estimation) % |- whiteLevel : white matter threshold % |- intensityMax : maximum value in the volume @@ -179,7 +179,7 @@ Histogram.max(i).y = histoY(maxIndex(i)); if(length(minIndex)>=1) % If there is at least a minimum, power = distance between - % maximum and adjacent minima + % maximum and adjacent minimum Histogram.max(i).power = histoY(maxIndex(i)) - (histoY(minIndex(max(1, i-1))) + histoY(minIndex(min(length(minIndex), i))))./2; else % Else power = maximum value @@ -237,12 +237,12 @@ elseif (length(cat(1,Histogram.max.x)) < 2) Histogram.bgLevel = defaultBg; Histogram.whiteLevel = defaultWhite; - % Else if there is more than one maxima : + % Else if there is more than one maximum : else - % If the highest maxima is > (3*second highest maxima) : - % it is a background maxima : use the first minima after the - % background maxima as background threshold - % (and if this minima exist) + % If the highest maximum is > (3*second highest maximum) : + % it is a background maximum : use the first minimum after the + % background maximum as background threshold + % (and if this minimum exist) [orderedMaxVal, orderedMaxInd] = sort(cat(1,Histogram.max.y), 'descend'); if ((orderedMaxVal(1) > 3*orderedMaxVal(2)) && (length(Histogram.min) >= orderedMaxInd(1))) Histogram.bgLevel = Histogram.min(orderedMaxInd(1)).x; @@ -251,7 +251,20 @@ Histogram.bgLevel = defaultBg; end end - + + case 'headgrad' + dX = mean(diff(Histogram.smoothFncX)); + %Deriv = gradient(Histogram.smoothFncY, dX); + %SecondDeriv = gradient(Deriv, dX); + RemainderCumul = Histogram.smoothFncY ./ cumsum(Histogram.smoothFncY, 2, 'reverse'); + DerivRC = gradient(RemainderCumul, dX); + %DerivRC2 = Deriv ./ cumsum(Histogram.smoothFncY, 2, 'reverse'); + %figure; plot(Histogram.smoothFncX', [RemainderCumul', DerivRC']); legend({'hist/remaining', 'derivative'}); + % Pick point where things flatten. + Histogram.bgLevel = Histogram.smoothFncX(find(DerivRC > -0.005 & DerivRC < DerivRC([2:end, end]), 1) + 2); + % Can't get white matter with gradient. + Histogram.whiteLevel = 0; + case 'brain' % Determine an intensity value for the background/gray matter limit % and the gray matter/white matter level @@ -328,5 +341,4 @@ end - \ No newline at end of file diff --git a/toolbox/anatomy/tess_isohead.m b/toolbox/anatomy/tess_isohead.m index 4e59e93c0..53f8876e5 100644 --- a/toolbox/anatomy/tess_isohead.m +++ b/toolbox/anatomy/tess_isohead.m @@ -1,4 +1,4 @@ -function [HeadFile, iSurface] = tess_isohead(iSubject, nVertices, erodeFactor, fillFactor, Comment, bgLevel) +function [HeadFile, iSurface] = tess_isohead(iSubject, nVertices, erodeFactor, fillFactor, Comment, bgLevel, isGradient) % TESS_GENERATE: Reconstruct a head surface based on the MRI, based on an isosurface % % USAGE: [HeadFile, iSurface] = tess_isohead(iSubject, nVertices=10000, erodeFactor=0, fillFactor=2, Comment) @@ -33,6 +33,9 @@ iSurface = []; isSave = true; % Parse inputs +if (nargin < 7) || isempty(isGradient) + isGradient = false; +end if (nargin < 6) || isempty(bgLevel) bgLevel = []; end @@ -85,7 +88,7 @@ %% ===== ASK PARAMETERS ===== % Ask user to set the parameters if they are not set if (nargin < 4) || isempty(erodeFactor) || isempty(nVertices) - res = java_dialog('input', {'Number of vertices [integer]:', 'Erode factor [0,1,2,3]:', 'Fill holes factor [0,1,2,3]:', 'Background threshold:
(guessed from MRI histogram)'}, 'Generate head surface', [], {'10000', '0', '2', num2str(sMri.Histogram.bgLevel)}); + res = java_dialog('input', {'Number of vertices [integer]:', 'Erode factor [0,1,2,3]:', 'Fill holes factor [0,1,2,3]:', 'Background threshold:
(guessed from MRI histogram)'}, 'Generate head surface', [], {'15000', '0', '0', num2str(sMri.Histogram.bgLevel)}); % If user cancelled: return if isempty(res) return @@ -98,7 +101,7 @@ if isempty(bgLevel) bgLevel = sMri.Histogram.bgLevel; end -elseif isempty(bgLevel) +elseif isempty(bgLevel) && ~isGradient bgLevel = sMri.Histogram.bgLevel; end % Check parameters values @@ -112,7 +115,39 @@ % Progress bar bst_progress('start', 'Generate head surface', 'Creating head mask...', 0, 100); % Threshold mri to the level estimated in the histogram -headmask = sMri.Cube(:,:,:,1) > bgLevel; +if isGradient + isGradLocalMax = false; + % Compute gradient + % Find appropriate threshold from gradient histogram + % TODO: need to find a robust way to do this. Only verified on one + % relatively bad MRI sequence with preprocessing (debias, denoise). + [Grad, VectGrad] = NormGradient(sMri.Cube(:,:,:,1)); + if isempty(bgLevel) + Hist = mri_histogram(Grad, [], 'headgrad'); + bgLevel = Hist.bgLevel; + end + if isGradLocalMax + % Index gymnastics... Is there a simpler way to do this (other than looping)? + nVol = [1, cumprod(size(Grad))]'; + [unused, UpDir] = max(abs(reshape(VectGrad, nVol(4), [])), [], 2); % (nVol, 1) + UpDirSign = sign(VectGrad((1:nVol(4))' + (UpDir-1) * nVol(4))); + % Get neighboring value of the gradient in the increasing gradiant direction. + % Using linear indices shaped as 3d array, which will give back a 3d array. + iUpGrad = zeros(size(Grad)); + iUpGrad(:) = UpDirSign .* nVol(UpDir); % change in index: +-1 along appropriate dimension for each voxel, in linear indices + % Removing problematic indices at edges. + iUpGrad([1, end], :, :) = 0; + iUpGrad(:, [1, end], :) = 0; + iUpGrad(:, :, [1, end]) = 0; + iUpGrad(:) = iUpGrad(:) + (1:nVol(4))'; % adding change to each element index + UpGrad = Grad(iUpGrad); + headmask = Grad > bgLevel & Grad >= UpGrad; + else + headmask = Grad > bgLevel; + end +else + headmask = sMri.Cube(:,:,:,1) > bgLevel; +end % Closing all the faces of the cube headmask(1,:,:) = 0; %*headmask(1,:,:); headmask(end,:,:) = 0; %*headmask(1,:,:); @@ -344,3 +379,11 @@ nOut = sum(OutMask(:)); end end + + +function [Vol, Vect] = NormGradient(Vol) +% Norm of the spatial gradient vector field in a regular 3D volume. +[x,y,z] = gradient(Vol); +Vect = cat(4,x,y,z); +Vol = sqrt(sum(Vect.^2, 4)); +end diff --git a/toolbox/gui/view_mri_histogram.m b/toolbox/gui/view_mri_histogram.m index b2a13ff58..daa8b4f78 100644 --- a/toolbox/gui/view_mri_histogram.m +++ b/toolbox/gui/view_mri_histogram.m @@ -27,17 +27,24 @@ % % Authors: Francois Tadel, 2006-2020 -%% ===== COMPUTE HISTOGRAM ===== +%% ===== LOAD OR COMPUTE HISTOGRAM ===== % Display progress bar bst_progress('start', 'View MRI historgram', 'Computing histogram...'); -% Load full MRI -MRI = load(MriFile); -% Compute histogram -Histogram = mri_histogram(MRI.Cube(:,:,:,1)); -% Save histogram -s.Histogram = Histogram; -bst_save(MriFile, s, 'v7', 1); - +if isstruct(MriFile) && isfield(MriFile, 'intensityMax') + Histogram = MriFile; +else + % Load full MRI + MRI = load(MriFile); + % Compute histogram if missing + if ~isfield(MRI, 'Histogram') || isempty(MRI.Histogram) || ~isfield(MRI.Histogram, 'intensityMax') + Histogram = mri_histogram(MRI.Cube(:,:,:,1)); + % Save histogram + s.Histogram = Histogram; + bst_save(MriFile, s, 'v7', 1); + else + Histogram = MRI.Histogram; + end +end %% ===== DISPLAY HISTOGRAM ===== % Create figure From 529fb4431404852c46f7bdb727e045972f086fd0 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Tue, 9 Aug 2022 15:10:09 -0400 Subject: [PATCH 10/32] small fix for old pos files --- toolbox/gui/figure_3d.m | 30 ++++++++++++++++++++++++++-- toolbox/gui/view_headpoints.m | 2 ++ toolbox/io/in_channel_pos.m | 4 ++-- toolbox/sensors/channel_align_auto.m | 1 + 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index 28b757236..275ca9945 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -3721,9 +3721,9 @@ function ViewHeadPoints(hFig, isVisible) % If head points graphic objects already exist: set the "Visible" property if ~isempty(hHeadPointsMarkers) if isVisible - set([hHeadPointsMarkers hHeadPointsLabels], 'Visible', 'on'); + set([hHeadPointsMarkers(:)' hHeadPointsLabels(:)'], 'Visible', 'on'); else - set([hHeadPointsMarkers hHeadPointsLabels], 'Visible', 'off'); + set([hHeadPointsMarkers(:)' hHeadPointsLabels(:)'], 'Visible', 'off'); end % If head points objects were not created yet: create them elseif isVisible @@ -3801,6 +3801,31 @@ function ViewHeadPoints(hFig, isVisible) end % Plot extra head points if ~isempty(iExtra) + % If distances, color code points. + if isfield(HeadPoints, 'Dist') + patch(digLoc(iExtra,1), digLoc(iExtra,2), digLoc(iExtra,3), ... + HeadPoints.Dist(iExtra)*1000, ... % mm + 'Marker', 'o', ... + 'MarkerSize', 6, ... + 'FaceColor', 'none', ... + 'EdgeColor', 'none', ... + 'MarkerFaceColor', 'flat', ... + 'MarkerEdgeColor', 'flat', ... + 'Parent', hAxes, ... + 'UserData', iExtra, ... + 'Tag', 'HeadPointsMarkers'); + ColormapType = 'stat1'; + set(hAxes, 'CLim', [0, 10]); + bst_colormaps('AddColormapToFigure', hFig, ColormapType); + bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); +% hColorbar = findobj(hFig, '-depth', 1, 'Tag', 'Colorbar'); +% set(hColorbar, 'YTick', 0:64:256, 'YTickLabel', {'0', '2.5', '5', '7.5', '>10'}); +% xlabel(hColorbar, 'mm'); + %sColormap = bst_colormaps('GetColormap', hFig); + bst_colormaps('SetColorbarVisible', hFig, 1); + + else + % Display markers line(digLoc(iExtra,1), digLoc(iExtra,2), digLoc(iExtra,3), ... 'Parent', hAxes, ... @@ -3812,6 +3837,7 @@ function ViewHeadPoints(hFig, isVisible) 'Marker', 'o', ... 'UserData', iExtra, ... 'Tag', 'HeadPointsMarkers'); + end end end end diff --git a/toolbox/gui/view_headpoints.m b/toolbox/gui/view_headpoints.m index 09f0168e7..9a97adf2d 100644 --- a/toolbox/gui/view_headpoints.m +++ b/toolbox/gui/view_headpoints.m @@ -58,6 +58,7 @@ HeadPoints = channel_get_headpoints(ChannelFile, 1); if isempty(HeadPoints) bst_error('No digitized head points to display for this file.', 'Add head points', 0); + hFig = []; iDS = []; iFig = []; return; end % Load full channel file @@ -65,6 +66,7 @@ % View scalp surface if available [hFig, iDS, iFig] = view_surface(ScalpFile, .2); +figure_3d('SetStandardView', hFig, 'front'); % Extend figure and dataset for this particular channel file GlobalData.DataSet(iDS).StudyFile = sStudy.FileName; diff --git a/toolbox/io/in_channel_pos.m b/toolbox/io/in_channel_pos.m index 5cff8bffd..00441f657 100644 --- a/toolbox/io/in_channel_pos.m +++ b/toolbox/io/in_channel_pos.m @@ -53,7 +53,7 @@ ChannelMat.HeadPoints.Type{end+1} = 'EXTRA'; case {4,7} % Name X Y Z ... => Headpoint or fiducial - if ~isnan(str2double(ss{1})) + if ~isnan(str2double(ss{1})) || strcmpi(ss{1}, 'EXTRA') ChannelMat.HeadPoints.Label{end+1} = 'EXTRA'; ChannelMat.HeadPoints.Type{end+1} = 'EXTRA'; else @@ -66,7 +66,7 @@ end ChannelMat.HeadPoints.Loc(:,end+1) = cellfun(@str2num, ss(2:4))' ./ 100; case 5 - % Indice Name X Y Z => EEG + % Index Name X Y Z => EEG i = length(ChannelMat.Channel) + 1; ChannelMat.Channel(i).Type = 'EEG'; ChannelMat.Channel(i).Name = ss{2}; diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index 86671ea89..910ae54a8 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -176,6 +176,7 @@ nRemove = 0; end % Current position cannot be optimized +ChannelMat.HeadPoints.Dist = dist'; if isempty(R) bst_progress('stop'); isSkip = 1; From 59317182e9d2f9f01c68aaca73da60be48327cb0 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Tue, 9 Aug 2022 15:16:07 -0400 Subject: [PATCH 11/32] cleanup --- toolbox/gui/figure_3d.m | 6 +----- toolbox/sensors/channel_align_auto.m | 3 ++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index 275ca9945..ebb935f1a 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -3814,14 +3814,10 @@ function ViewHeadPoints(hFig, isVisible) 'Parent', hAxes, ... 'UserData', iExtra, ... 'Tag', 'HeadPointsMarkers'); - ColormapType = 'stat1'; set(hAxes, 'CLim', [0, 10]); + ColormapType = 'stat1'; bst_colormaps('AddColormapToFigure', hFig, ColormapType); bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); -% hColorbar = findobj(hFig, '-depth', 1, 'Tag', 'Colorbar'); -% set(hColorbar, 'YTick', 0:64:256, 'YTickLabel', {'0', '2.5', '5', '7.5', '>10'}); -% xlabel(hColorbar, 'mm'); - %sColormap = bst_colormaps('GetColormap', hFig); bst_colormaps('SetColorbarVisible', hFig, 1); else diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index 910ae54a8..a2a74a150 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -175,8 +175,9 @@ else nRemove = 0; end -% Current position cannot be optimized +% Save point-to-scalp distances for display and quality control ChannelMat.HeadPoints.Dist = dist'; +% Current position cannot be optimized if isempty(R) bst_progress('stop'); isSkip = 1; From 6133b7ff0e9c5f153bcbbb3975598a87e93c618a Mon Sep 17 00:00:00 2001 From: Francois Date: Wed, 10 Aug 2022 15:09:34 +0200 Subject: [PATCH 12/32] Fix indentation --- toolbox/gui/figure_3d.m | 50 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index ebb935f1a..45d5bba30 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -3801,38 +3801,36 @@ function ViewHeadPoints(hFig, isVisible) end % Plot extra head points if ~isempty(iExtra) - % If distances, color code points. - if isfield(HeadPoints, 'Dist') + % If distances are available, color-code the points + if isfield(HeadPoints, 'Dist') && ~isempty(HeadPoints.Dist) patch(digLoc(iExtra,1), digLoc(iExtra,2), digLoc(iExtra,3), ... - HeadPoints.Dist(iExtra)*1000, ... % mm - 'Marker', 'o', ... - 'MarkerSize', 6, ... - 'FaceColor', 'none', ... - 'EdgeColor', 'none', ... - 'MarkerFaceColor', 'flat', ... - 'MarkerEdgeColor', 'flat', ... - 'Parent', hAxes, ... - 'UserData', iExtra, ... - 'Tag', 'HeadPointsMarkers'); + HeadPoints.Dist(iExtra)*1000, ... % mm + 'Marker', 'o', ... + 'MarkerSize', 6, ... + 'FaceColor', 'none', ... + 'EdgeColor', 'none', ... + 'MarkerFaceColor', 'flat', ... + 'MarkerEdgeColor', 'flat', ... + 'Parent', hAxes, ... + 'UserData', iExtra, ... + 'Tag', 'HeadPointsMarkers'); set(hAxes, 'CLim', [0, 10]); ColormapType = 'stat1'; bst_colormaps('AddColormapToFigure', hFig, ColormapType); bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); bst_colormaps('SetColorbarVisible', hFig, 1); - - else - - % Display markers - line(digLoc(iExtra,1), digLoc(iExtra,2), digLoc(iExtra,3), ... - 'Parent', hAxes, ... - 'LineWidth', 2, ... - 'LineStyle', 'none', ... - 'MarkerFaceColor', [.3 1 .3], ... - 'MarkerEdgeColor', [.4 .7 .4], ... - 'MarkerSize', 6, ... - 'Marker', 'o', ... - 'UserData', iExtra, ... - 'Tag', 'HeadPointsMarkers'); + else + % Display markers + line(digLoc(iExtra,1), digLoc(iExtra,2), digLoc(iExtra,3), ... + 'Parent', hAxes, ... + 'LineWidth', 2, ... + 'LineStyle', 'none', ... + 'MarkerFaceColor', [.3 1 .3], ... + 'MarkerEdgeColor', [.4 .7 .4], ... + 'MarkerSize', 6, ... + 'Marker', 'o', ... + 'UserData', iExtra, ... + 'Tag', 'HeadPointsMarkers'); end end end From 8f967a9107b53da6c927fd309409ebcb3e2694f1 Mon Sep 17 00:00:00 2001 From: ftadel Date: Wed, 10 Aug 2022 16:51:17 +0200 Subject: [PATCH 13/32] Recompute distance dynamically --- toolbox/gui/figure_3d.m | 29 +++++++++++++++++++--------- toolbox/gui/view_headpoints.m | 12 ++++++++---- toolbox/sensors/channel_align_auto.m | 2 -- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index 45d5bba30..fd3f380dc 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -3685,8 +3685,12 @@ function ViewSensors(hFig, isMarkers, isLabels, isMesh, Modality) %% ===== VIEW HEAD POINTS ===== -function ViewHeadPoints(hFig, isVisible) +function ViewHeadPoints(hFig, isVisible, isColorDist) global GlobalData; + % Parse inputs + if (nargin < 3) || isempty(isColorDist) + isColorDist = 0; + end % Get figure description [hFig, iFig, iDS] = bst_figures('GetFigure', hFig); if isempty(iDS) @@ -3801,10 +3805,22 @@ function ViewHeadPoints(hFig, isVisible) end % Plot extra head points if ~isempty(iExtra) - % If distances are available, color-code the points - if isfield(HeadPoints, 'Dist') && ~isempty(HeadPoints.Dist) + % Color-code the points according to the distance to the displayed surface + if isColorDist + % Get selected surface + [iTess, TessInfo, hFig, sSurf] = panel_surface('GetSelectedSurface', hFig); + % Compute the distance as in bst_meshfit + VertNorm = tess_normals(sSurf.Vertices, sSurf.Faces); + iNearest = bst_nearest(sSurf.Vertices, digLoc(iExtra,:), 1, 0, []); + dist = abs(sum(VertNorm(iNearest,:) .* (digLoc(iExtra,:) - sSurf.Vertices(iNearest,:)),2)); + % Compute color array + iColor = round((dist - min(dist)) / (max(dist) - min(dist)) * 255) + 1; + CMap = jet(512); + CData = CMap(iColor+256,:); + % Plot colored dots patch(digLoc(iExtra,1), digLoc(iExtra,2), digLoc(iExtra,3), ... - HeadPoints.Dist(iExtra)*1000, ... % mm + dist, ... + 'FaceVertexCData', CData, ... 'Marker', 'o', ... 'MarkerSize', 6, ... 'FaceColor', 'none', ... @@ -3814,11 +3830,6 @@ function ViewHeadPoints(hFig, isVisible) 'Parent', hAxes, ... 'UserData', iExtra, ... 'Tag', 'HeadPointsMarkers'); - set(hAxes, 'CLim', [0, 10]); - ColormapType = 'stat1'; - bst_colormaps('AddColormapToFigure', hFig, ColormapType); - bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); - bst_colormaps('SetColorbarVisible', hFig, 1); else % Display markers line(digLoc(iExtra,1), digLoc(iExtra,2), digLoc(iExtra,3), ... diff --git a/toolbox/gui/view_headpoints.m b/toolbox/gui/view_headpoints.m index 9a97adf2d..99bdb5c0a 100644 --- a/toolbox/gui/view_headpoints.m +++ b/toolbox/gui/view_headpoints.m @@ -1,8 +1,8 @@ -function [hFig, iDS, iFig] = view_headpoints(ChannelFile, ScalpFile, isInterp) +function [hFig, iDS, iFig] = view_headpoints(ChannelFile, ScalpFile, isInterp, isColorDist) % VIEW_HEADPOINTS: View surface file and head points. % % USAGE: view_headpoints(ChannelFile) -% view_headpoints(ChannelFile, ScalpFile) +% view_headpoints(ChannelFile, ScalpFile=[], isInterp=0, isColorDist=0) % % OUTPUT: % - hFig : Matlab handle to the 3DViz figure that was created or updated @@ -28,10 +28,14 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Francois Tadel, 2010 +% Authors: Francois Tadel, 2010-2022 global GlobalData; +% Default: no color for the distance between the scalp and the points +if (nargin < 4) || isempty(isColorDist) + isColorDist = 0; +end % Default: no spherical harmonics if (nargin < 3) || isempty(isInterp) isInterp = 0; @@ -78,7 +82,7 @@ GlobalData.DataSet(iDS).HeadPoints = ChannelMat.HeadPoints; % View HeadPoints -figure_3d('ViewHeadPoints', hFig, 1); +figure_3d('ViewHeadPoints', hFig, 1, isColorDist); % Show a spherical harmonic fit to the landmark data if isInterp diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index a2a74a150..86671ea89 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -175,8 +175,6 @@ else nRemove = 0; end -% Save point-to-scalp distances for display and quality control -ChannelMat.HeadPoints.Dist = dist'; % Current position cannot be optimized if isempty(R) bst_progress('stop'); From 31e3177a021888156e57d93d39c38fe2078f05be Mon Sep 17 00:00:00 2001 From: ftadel Date: Wed, 10 Aug 2022 16:58:01 +0200 Subject: [PATCH 14/32] Added popup menu for color-coded dots --- toolbox/tree/tree_callbacks.m | 1 + 1 file changed, 1 insertion(+) diff --git a/toolbox/tree/tree_callbacks.m b/toolbox/tree/tree_callbacks.m index 584441f7c..b3b34a4cf 100644 --- a/toolbox/tree/tree_callbacks.m +++ b/toolbox/tree/tree_callbacks.m @@ -969,6 +969,7 @@ jMenuHeadPoints = gui_component('Menu', jPopup, [], 'Digitized head points', IconLoader.ICON_CHANNEL, [], []); % View head points gui_component('MenuItem', jMenuHeadPoints, [], 'View head points', IconLoader.ICON_SURFACE_SCALP, [], @(h,ev)view_headpoints(filenameFull, [], 0)); + gui_component('MenuItem', jMenuHeadPoints, [], 'View head points (color=distance)', IconLoader.ICON_SURFACE_SCALP, [], @(h,ev)view_headpoints(filenameFull, [], 0, 1)); % Edit head points if ~bst_get('ReadOnly') % Add head points From b2b4a7a9c9dc083776f8e79fedefac7ed57da9dc Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Wed, 10 Aug 2022 15:30:40 -0400 Subject: [PATCH 15/32] wip, head points as patch --- toolbox/gui/figure_3d.m | 46 +++++------ toolbox/math/bst_surfdist.m | 105 +++++++++++++++++++++++++ toolbox/sensors/channel_align_auto.m | 2 - toolbox/sensors/channel_align_manual.m | 47 ++++++++--- 4 files changed, 165 insertions(+), 35 deletions(-) create mode 100644 toolbox/math/bst_surfdist.m diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index ebb935f1a..fc29494a4 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -1242,7 +1242,9 @@ function ResetView(hFig) % Get axes hAxes = findobj(hFig, 'Tag', 'Axes3D'); % Reset zoom - zoom(hAxes, 'out'); + zoom(hAxes, 'out'); + % Enforce camera target at (0,0,0) + camtarget(hAxes, [0 0 0]); % 2D LAYOUT: separate function if strcmpi(GlobalData.DataSet(iDS).Figure(iFig).Id.SubType, '2DLayout') GlobalData.DataSet(iDS).Figure(iFig).Handles.DisplayFactor = 1; @@ -1333,6 +1335,8 @@ function SetStandardView(hFig, viewNames) end % Update camera position view(hAxes, newView * R); + % view() changes the camera target. Enforce (0,0,0). + camtarget(hAxes, [0,0,0]); camup(hAxes, double(newCamup * R)); % Update head light position camlight(findobj(hAxes, '-depth', 1, 'Tag', 'FrontLight'), 'headlight'); @@ -3802,38 +3806,36 @@ function ViewHeadPoints(hFig, isVisible) % Plot extra head points if ~isempty(iExtra) % If distances, color code points. - if isfield(HeadPoints, 'Dist') - patch(digLoc(iExtra,1), digLoc(iExtra,2), digLoc(iExtra,3), ... - HeadPoints.Dist(iExtra)*1000, ... % mm - 'Marker', 'o', ... - 'MarkerSize', 6, ... - 'FaceColor', 'none', ... - 'EdgeColor', 'none', ... - 'MarkerFaceColor', 'flat', ... - 'MarkerEdgeColor', 'flat', ... - 'Parent', hAxes, ... - 'UserData', iExtra, ... - 'Tag', 'HeadPointsMarkers'); - set(hAxes, 'CLim', [0, 10]); + if isColorDist + % Get selected surface + [iTess, TessInfo, hFig, sSurf] = panel_surface('GetSelectedSurface', hFig); + % Compute the distance + Dist = bst_surfdist(digLoc(iExtra, :), sSurf.Vertices, sSurf.Faces); + CData = Dist * 1000; % mm + MarkerFaceColor = 'flat'; + MarkerEdgeColor = 'flat'; + % TBD if we can use colormaps here... ColormapType = 'stat1'; bst_colormaps('AddColormapToFigure', hFig, ColormapType); + % How can the units be retained when changing colormap through the GUI? bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); bst_colormaps('SetColorbarVisible', hFig, 1); - else - - % Display markers - line(digLoc(iExtra,1), digLoc(iExtra,2), digLoc(iExtra,3), ... + CData = 'w'; % any color, not displayed + MarkerFaceColor = [.3 1 .3]; + MarkerEdgeColor = [.4 .7 .4]; + end + patch(digLoc(iExtra,1), digLoc(iExtra,2), digLoc(iExtra,3), CData, ... 'Parent', hAxes, ... 'LineWidth', 2, ... - 'LineStyle', 'none', ... - 'MarkerFaceColor', [.3 1 .3], ... - 'MarkerEdgeColor', [.4 .7 .4], ... + 'FaceColor', 'none', ... + 'EdgeColor', 'none', ... + 'MarkerFaceColor', MarkerFaceColor, ... + 'MarkerEdgeColor', MarkerEdgeColor, ... 'MarkerSize', 6, ... 'Marker', 'o', ... 'UserData', iExtra, ... 'Tag', 'HeadPointsMarkers'); - end end end end diff --git a/toolbox/math/bst_surfdist.m b/toolbox/math/bst_surfdist.m new file mode 100644 index 000000000..06550e4e3 --- /dev/null +++ b/toolbox/math/bst_surfdist.m @@ -0,0 +1,105 @@ +function Dist = bst_surfdist(Points, Vertices, Faces) +% BST_SURFDIST: Compute the distances between points and a surface. +% +% USAGE: Dist = bst_surfdist(Points, Vertices, Faces) +% +% DESCRIPTION: +% Exact distance computation, which checks all 3 sets of distances: points +% to vertices, points to edges, and points to faces, keeping the smallest +% for each point. +% +% INPUTS: +% - Points : [Qx3] double matrix, points to compare to the mesh defined by Vertices/Faces +% - Vertices : [Mx3] double matrix +% - Faces : [Nx3] double matrix +% +% OUTPUTS: +% - Dist : [Qx1] final distance between points and mesh + +% @============================================================================= +% This function is part of the Brainstorm software: +% https://neuroimage.usc.edu/brainstorm +% +% Copyright (c) University of Southern California & McGill University +% This software is distributed under the terms of the GNU General Public License +% as published by the Free Software Foundation. Further details on the GPLv3 +% license can be found at http://www.gnu.org/copyleft/gpl.html. +% +% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE +% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY +% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY +% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. +% +% For more information type "brainstorm license" at command prompt. +% =============================================================================@ +% +% Authors: Marc Lalancette, 2022 + +% TODO: A bit slow, look for alternatives +% This seems similar: https://www.mathworks.com/matlabcentral/fileexchange/52882-point2trimesh-distance-between-point-and-triangulated-surface + Epsilon = 1e-9; % nanometer + nP = size(Points, 1); + + % Prepare surface quantities, independent of points + % (In bst_meshfit, this can be done only once before iterative fitting.) + % Edges as indices + Edges = unique(sort([Faces(:,[1,2]); Faces(:,[2,3]); Faces(:,[3,1])], 2), 'rows'); + % Edge direction "doubly normalized" so that later projection should be between 0 and 1. + EdgeDir = Vertices(Edges(:,2),:) - Vertices(Edges(:,1),:); + EdgeL = sqrt(sum(EdgeDir.^2, 2)); + EdgeDir = bsxfun(@rdivide, EdgeDir, EdgeL); + % Edges as vectors + EdgesV = zeros(nF, 3, 3); + EdgesV(:,:,1) = Vertices(Faces(:,2),:) - Vertices(Faces(:,1),:); + EdgesV(:,:,2) = Vertices(Faces(:,3),:) - Vertices(Faces(:,2),:); + EdgesV(:,:,3) = Vertices(Faces(:,1),:) - Vertices(Faces(:,3),:); + % First edge to second edge: counter clockwise = up + FaceNormals = normr(cross(EdgesV(:,:,1), EdgesV(:,:,2))); + %FaceArea = sqrt(sum(FaceNormalsA.^2, 2)); + % Perpendicular vectors to edges, pointing inside triangular face. + for e = 3:-1:1 + EdgeTriNormals(:,:,e) = cross(FaceNormals, EdgesV(:,:,e)); + end + FaceVertices = zeros(nF, 3, 3); + FaceVertices(:,:,1) = Vertices(Faces(:,1),:); + FaceVertices(:,:,2) = Vertices(Faces(:,2),:); + FaceVertices(:,:,3) = Vertices(Faces(:,3),:); + + + % Check distance to vertices + if license('test','statistics_toolbox') + DistVert = pdist2(Vertices, Points, 'euclidean', 'Smallest', 1)'; + else + DistVert = zeros(nP, 1); + for iP = 1:nP + % Find closest surface vertex. + DistVert(iP) = sqrt(min(sum(bsxfun(@minus, Points(iP, :), Vertices).^2, 2))); + end + end + % Check distance to faces + DistFace = inf(nP, 1); + for iP = 1:nP + % Considered Möller and Trumbore 1997, Ray-Triangle Intersection (https://stackoverflow.com/questions/42740765/intersection-between-line-and-triangle-in-3d), but this is simpler still. + % Vectors from triangle vertices to point. + Pyramid = bsxfun(@minus, Points(iP, :), FaceVertices); + % Does the point project inside each face? + InFace = all(sum(Pyramid .* EdgeTriNormals, 2) > -Epsilon, 3); + if any(InFace) + DistFace(iP) = min(abs(sum(Pyramid(InFace,:,1) .* FaceNormals(InFace,:), 2))); + end + end + % Check distance to edges + DistEdge = inf(nP, 1); + for iP = 1:nP + % Vector from first edge vertex to point. + Pyramid = bsxfun(@minus, Points(iP, :), Vertices(Edges(:, 1), :)); + Projection = sum(Pyramid .* EdgeDir, 2); + InEdge = Projection > -Epsilon & Projection < (EdgeL + Epsilon); + if any(InEdge) + DistEdge(iP) = sqrt(min(sum((Pyramid(InEdge,:) - bsxfun(@times, Projection(InEdge), EdgeDir(InEdge,:))).^2, 2))); + end + end + + Dist = min([DistVert, DistEdge, DistFace], [], 2); + end diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index a2a74a150..86671ea89 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -175,8 +175,6 @@ else nRemove = 0; end -% Save point-to-scalp distances for display and quality control -ChannelMat.HeadPoints.Dist = dist'; % Current position cannot be optimized if isempty(R) bst_progress('stop'); diff --git a/toolbox/sensors/channel_align_manual.m b/toolbox/sensors/channel_align_manual.m index 02d7522c5..626a2bb4a 100644 --- a/toolbox/sensors/channel_align_manual.m +++ b/toolbox/sensors/channel_align_manual.m @@ -202,10 +202,14 @@ HeadPointsFidLoc = []; HeadPointsHpiLoc = []; if isHeadPoints + % More transparency to view points inside. + panel_surface('SetSurfaceTransparency', hFig, 1, 0.5); + % Hide helmet by default to align with points. + if ~isempty(hHelmetPatch) + set(hHelmetPatch, 'Visible', 'off'); + end % Get markers positions - HeadPointsMarkersLoc = [get(hHeadPointsMarkers, 'XData')', ... - get(hHeadPointsMarkers, 'YData')', ... - get(hHeadPointsMarkers, 'ZData')']; + HeadPointsMarkersLoc = get(hHeadPointsMarkers, 'Vertices'); % Hide HeadPoints when looking at EEG and number of EEG channels is the same as headpoints if isEeg && ~isempty(HeadPointsMarkersLoc) && ~isempty(SensorsVertices) && (length(SensorsVertices) == length(HeadPointsMarkersLoc)) && (max(abs(SensorsVertices(:) - HeadPointsMarkersLoc(:))) < 0.001) set(hHeadPointsMarkers, 'Visible', 'off'); @@ -343,11 +347,12 @@ gChanAlign.hButtonEditLabel = []; gChanAlign.hButtonHelmet = []; if gChanAlign.isMeg - gChanAlign.hButtonHelmet = uitoggletool(hToolbar, 'CData', java_geticon('ICON_DISPLAY'), 'TooltipString', 'Show/Hide MEG helmet', 'ClickedCallback', @ToggleHelmet, 'State', 'on'); + gChanAlign.hButtonHelmet = uitoggletool(hToolbar, 'CData', java_geticon('ICON_DISPLAY'), 'TooltipString', 'Show/Hide MEG helmet', 'ClickedCallback', @ToggleHelmet, 'State', get(hHelmetPatch, 'Visible')); elseif gChanAlign.isEeg gChanAlign.hButtonLabels = uitoggletool(hToolbar, 'CData', java_geticon('ICON_LABELS'), 'TooltipString', 'Show/Hide electrodes labels', 'ClickedCallback', @ToggleLabels); gChanAlign.hButtonEditLabel = uipushtool( hToolbar, 'CData', java_geticon('ICON_EDIT'), 'TooltipString', 'Edit selected channel label', 'ClickedCallback', @EditLabel); end +gChanAlign.hButtonColorDist = uitoggletool(hToolbar, 'CData', java_geticon('ICON_CHANNEL'), 'TooltipString', 'Color head points by distance', 'ClickedCallback', @ToggleColorDist, 'State', 'on'); gChanAlign.hButtonTransX = uitoggletool(hToolbar, 'CData', java_geticon('ICON_TRANSLATION_X'), 'TooltipString', 'Translation/X: Press right button and move mouse up/down', 'ClickedCallback', @SelectOperation, 'separator', 'on'); gChanAlign.hButtonTransY = uitoggletool(hToolbar, 'CData', java_geticon('ICON_TRANSLATION_Y'), 'TooltipString', 'Translation/Y: Press right button and move mouse up/down', 'ClickedCallback', @SelectOperation); gChanAlign.hButtonTransZ = uitoggletool(hToolbar, 'CData', java_geticon('ICON_TRANSLATION_Z'), 'TooltipString', 'Translation/Z: Press right button and move mouse up/down', 'ClickedCallback', @SelectOperation); @@ -640,10 +645,13 @@ function UpdatePoints(iSelChan) % Update headpoints markers and labels if gChanAlign.isHeadPoints % Extra head points - set(gChanAlign.hHeadPointsMarkers, ... - 'XData', gChanAlign.HeadPointsMarkersLoc(:,1), ... - 'YData', gChanAlign.HeadPointsMarkersLoc(:,2), ... - 'ZData', gChanAlign.HeadPointsMarkersLoc(:,3)); + set(gChanAlign.hHeadPointsMarkers, 'Vertices', gChanAlign.HeadPointsMarkersLoc); + if strcmpi(get(gChanAlign.hButtonColorDist, 'State'), 'on') + % Update distance color + Dist = bst_surfdist(gChanAlign.HeadPointsMarkersLoc, ... + get(gChanAlign.hSurfacePatch, 'Vertices'), get(gChanAlign.hSurfacePatch, 'Faces')); + set(gChanAlign.hHeadPointsMarkers, 'CData', Dist * 1000); + end % Fiducials if ~isempty(gChanAlign.hHeadPointsFid) set(gChanAlign.hHeadPointsFid, ... @@ -986,6 +994,25 @@ function ToggleHelmet(varargin) end +%% ===== COLOR HEAD POINTS ===== +function ToggleColorDist(varargin) + global gChanAlign; + % Update button color + gui_update_toggle(gChanAlign.hButtonColorDist); + if strcmpi(get(gChanAlign.hButtonColorDist, 'State'), 'on') + % Color points according to distance to surface + Dist = bst_surfdist(gChanAlign.HeadPointsMarkersLoc, ... + get(gChanAlign.hSurfacePatch, 'Vertices'), get(gChanAlign.hSurfacePatch, 'Faces')); + set(gChanAlign.hHeadPointsMarkers, 'CData', Dist * 1000, ... + 'MarkerFaceColor', 'flat', 'MarkerEdgeColor', 'flat'); + else + % Conventional fixed point color + set(gChanAlign.hHeadPointsMarkers, ...% 'CData', 'w', ... + 'MarkerFaceColor', [.3 1 .3], 'MarkerEdgeColor', [.4 .7 .4]); + end +end + + %% ===== EDIT LABEL ===== function EditLabel(varargin) global GlobalData gChanAlign; @@ -1107,9 +1134,7 @@ function ProjectElectrodesOnSurface(varargin) % Copy modification to the head points if gChanAlign.isEeg && ~isempty(gChanAlign.SensorsVertices) && ~isempty(gChanAlign.HeadPointsMarkersLoc) && (length(gChanAlign.SensorsVertices) == length(gChanAlign.HeadPointsMarkersLoc)) gChanAlign.HeadPointsMarkersLoc = gChanAlign.SensorsVertices; - set(gChanAlign.hHeadPointsMarkers, 'XData', gChanAlign.HeadPointsMarkersLoc(:,1), ... - 'YData', gChanAlign.HeadPointsMarkersLoc(:,2), ... - 'ZData', gChanAlign.HeadPointsMarkersLoc(:,3)); + set(gChanAlign.hHeadPointsMarkers, 'Vertices', gChanAlign.HeadPointsMarkersLoc); end % Mark current channel file as modified gChanAlign.isChanged = 1; From 2e8bc0946c20a24fc0be8c49b94cfb24d22c59c8 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Wed, 10 Aug 2022 19:43:34 -0400 Subject: [PATCH 16/32] cleanup --- toolbox/gui/figure_3d.m | 3 ++- toolbox/sensors/channel_align_manual.m | 11 ++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index ed33bccee..2a8c9331d 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -3768,9 +3768,10 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) Dist = bst_surfdist(get(hHeadPointsMarkers, 'Vertices'), sSurf.Vertices, sSurf.Faces); set(hHeadPointsMarkers, 'CData', Dist * 1000, ... 'MarkerFaceColor', 'flat', 'MarkerEdgeColor', 'flat'); - if ~ismember(ColormapInfo.AllTypes, ColormapType) + if ~ismember(ColormapType, ColormapInfo.AllTypes) % Add missing colormap (color was toggled after points were displayed) bst_colormaps('AddColormapToFigure', hFig, ColormapType, 'mm'); + ColormapInfo = getappdata(hFig, 'Colormap'); end if strcmpi(ColormapInfo.Type, ColormapType) bst_colormaps('SetColorbarVisible', hFig, 1); diff --git a/toolbox/sensors/channel_align_manual.m b/toolbox/sensors/channel_align_manual.m index 994758635..01103b1b5 100644 --- a/toolbox/sensors/channel_align_manual.m +++ b/toolbox/sensors/channel_align_manual.m @@ -1,4 +1,4 @@ -function hFig = channel_align_manual( ChannelFile, Modality, isEdit, SurfaceType, isColorDist ) +function hFig = channel_align_manual( ChannelFile, Modality, isEdit, SurfaceType ) % CHANNEL_ALIGN_MANUAL: Align manually an electrodes net on the scalp surface of the subject. % % USAGE: hFig = channel_align_manual( ChannelFile, Modality, isEdit, SurfaceType='cortex') @@ -34,9 +34,6 @@ % Parse inputs hFig = []; -if (nargin < 5) || isempty(isColorDist) - isColorDist = 1; -end if (nargin < 4) || isempty(SurfaceType) if ismember(Modality, {'SEEG'}) SurfaceType = 'cortex'; @@ -193,7 +190,7 @@ % ===== DISPLAY HEAD POINTS ===== % Display head points -figure_3d('ViewHeadPoints', hFig, 1, isColorDist); +figure_3d('ViewHeadPoints', hFig, 1); % Get patch and vertices hHeadPointsMarkers = findobj(hFig, 'Tag', 'HeadPointsMarkers'); hHeadPointsLabels = findobj(hFig, 'Tag', 'HeadPointsLabels'); @@ -207,10 +204,6 @@ if isHeadPoints % More transparency to view points inside. panel_surface('SetSurfaceTransparency', hFig, 1, 0.5); - % Hide helmet by default to align with points. - if ~isempty(hHelmetPatch) - set(hHelmetPatch, 'Visible', 'off'); - end % Get markers positions HeadPointsMarkersLoc = get(hHeadPointsMarkers, 'Vertices'); % Hide HeadPoints when looking at EEG and number of EEG channels is the same as headpoints From 74e02daede898998d527fd3c01ffa1c82d7e6127 Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 12 Aug 2022 13:27:31 +0200 Subject: [PATCH 17/32] Enforce target (0,0,0) only when the axes are visible --- toolbox/gui/figure_3d.m | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index 2a8c9331d..f1cdc85cb 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -1261,8 +1261,10 @@ function ResetView(hFig) hAxes = findobj(hFig, 'Tag', 'Axes3D'); % Reset zoom zoom(hAxes, 'out'); - % Enforce camera target at (0,0,0) - camtarget(hAxes, [0 0 0]); + % Enforce camera target at (0,0,0) when the axes are visible + if ~isempty(findobj(hAxes, 'Tag', 'AxisXYZ')) + camtarget(hAxes, [0 0 0]); + end % 2D LAYOUT: separate function if strcmpi(GlobalData.DataSet(iDS).Figure(iFig).Id.SubType, '2DLayout') GlobalData.DataSet(iDS).Figure(iFig).Handles.DisplayFactor = 1; @@ -1353,8 +1355,10 @@ function SetStandardView(hFig, viewNames) end % Update camera position view(hAxes, newView * R); - % view() changes the camera target. Enforce (0,0,0). - camtarget(hAxes, [0,0,0]); + % Enforce camera target at (0,0,0) when the axes are visible + if ~isempty(findobj(hAxes, 'Tag', 'AxisXYZ')) + camtarget(hAxes, [0 0 0]); + end camup(hAxes, double(newCamup * R)); % Update head light position camlight(findobj(hAxes, '-depth', 1, 'Tag', 'FrontLight'), 'headlight'); From 89a9b98a90fd087e7ce347ccea358aebc27c18ec Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 12 Aug 2022 15:10:47 +0200 Subject: [PATCH 18/32] Disabling camera target at (0,0,0) --- toolbox/gui/figure_3d.m | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index f1cdc85cb..f8eb9d648 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -1261,10 +1261,6 @@ function ResetView(hFig) hAxes = findobj(hFig, 'Tag', 'Axes3D'); % Reset zoom zoom(hAxes, 'out'); - % Enforce camera target at (0,0,0) when the axes are visible - if ~isempty(findobj(hAxes, 'Tag', 'AxisXYZ')) - camtarget(hAxes, [0 0 0]); - end % 2D LAYOUT: separate function if strcmpi(GlobalData.DataSet(iDS).Figure(iFig).Id.SubType, '2DLayout') GlobalData.DataSet(iDS).Figure(iFig).Handles.DisplayFactor = 1; @@ -1355,10 +1351,6 @@ function SetStandardView(hFig, viewNames) end % Update camera position view(hAxes, newView * R); - % Enforce camera target at (0,0,0) when the axes are visible - if ~isempty(findobj(hAxes, 'Tag', 'AxisXYZ')) - camtarget(hAxes, [0 0 0]); - end camup(hAxes, double(newCamup * R)); % Update head light position camlight(findobj(hAxes, '-depth', 1, 'Tag', 'FrontLight'), 'headlight'); @@ -3921,7 +3913,7 @@ function ViewAxis(hFig, isVisible) text(0, d+0.002, 0, 'Y', 'Color', [0 1 0], 'Parent', hAxes, 'Tag', 'AxisXYZ'); text(0, 0, d+0.002, 'Z', 'Color', [0 0 1], 'Parent', hAxes, 'Tag', 'AxisXYZ'); % Enforce camera target at (0,0,0) - camtarget(hAxes, [0,0,0]); + % camtarget(hAxes, [0,0,0]); else hAxisXYZ = findobj(hAxes, 'Tag', 'AxisXYZ'); if ~isempty(hAxisXYZ) From f510e909aa73b8a4787763dfff856b8142cb0557 Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 12 Aug 2022 15:22:23 +0200 Subject: [PATCH 19/32] Get rid of normr --- toolbox/math/bst_surfdist.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/toolbox/math/bst_surfdist.m b/toolbox/math/bst_surfdist.m index db24d0944..47787de41 100644 --- a/toolbox/math/bst_surfdist.m +++ b/toolbox/math/bst_surfdist.m @@ -56,7 +56,8 @@ EdgesV(:,:,2) = Vertices(Faces(:,3),:) - Vertices(Faces(:,2),:); EdgesV(:,:,3) = Vertices(Faces(:,1),:) - Vertices(Faces(:,3),:); % First edge to second edge: counter clockwise = up -FaceNormals = normr(cross(EdgesV(:,:,1), EdgesV(:,:,2))); +m = cross(EdgesV(:,:,1), EdgesV(:,:,2)); +FaceNormals = sqrt(ones./(sum((m.*m)')))'*ones(1,size(m,2)).*m; %FaceArea = sqrt(sum(FaceNormalsA.^2, 2)); % Perpendicular vectors to edges, pointing inside triangular face. for e = 3:-1:1 @@ -104,3 +105,4 @@ Dist = min([DistVert, DistEdge, DistFace], [], 2); end + From 382ca4485417891d5958ced6ee2152099283a470 Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 12 Aug 2022 15:42:13 +0200 Subject: [PATCH 20/32] Moved popup menu to Channel submenu (Figure is independent from the data) --- toolbox/gui/figure_3d.m | 60 ++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index f8eb9d648..b90c43124 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -1754,6 +1754,23 @@ function DisplayFigurePopup(hFig) gui_component('MenuItem', jMenuChannels, [], 'SEEG contacts', IconLoader.ICON_CHANNEL, [], @(h,ev)view_channels(ChannelFile, 'SEEG', 1, 0, hFig, 1)); end end + + % Show Head points + isHeadPoints = ~isempty(GlobalData.DataSet(iDS).HeadPoints) && ~isempty(GlobalData.DataSet(iDS).HeadPoints.Loc); + if isHeadPoints && ~strcmpi(FigureType, 'Topography') + jMenuChannels.addSeparator(); + % Are head points visible + hHeadPointsMarkers = findobj(GlobalData.DataSet(iDS).Figure(iFig).hFigure, 'Tag', 'HeadPointsMarkers'); + isVisible = ~isempty(hHeadPointsMarkers) && strcmpi(get(hHeadPointsMarkers, 'Visible'), 'on'); + jItem = gui_component('CheckBoxMenuItem', jMenuChannels, [], 'View head points', IconLoader.ICON_CHANNEL, [], @(h,ev)ViewHeadPoints(hFig, ~isVisible)); + jItem.setSelected(isVisible); + jItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, KeyEvent.CTRL_MASK)); + % Are head points color coded by distance + isColorDist = ~isempty(hHeadPointsMarkers) && strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat'); + jItem = gui_component('CheckBoxMenuItem', jMenuChannels, [], 'Color head points by distance', IconLoader.ICON_CHANNEL, [], @(h,ev)ViewHeadPoints(hFig, isVisible, ~isColorDist)); + jItem.setSelected(isColorDist); + jItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, KeyEvent.CTRL_MASK)); + end end % ==== MENU: MONTAGE ==== @@ -1958,22 +1975,7 @@ function DisplayFigurePopup(hFig) isAxis = ~isempty(findobj(hFig, 'Tag', 'AxisXYZ')); jItem = gui_component('CheckBoxMenuItem', jMenuFigure, [], 'View axis', IconLoader.ICON_AXES, [], @(h,ev)ViewAxis(hFig, ~isAxis)); jItem.setSelected(isAxis); - jItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.CTRL_MASK)); - % Show Head points - isHeadPoints = ~isempty(GlobalData.DataSet(iDS).HeadPoints) && ~isempty(GlobalData.DataSet(iDS).HeadPoints.Loc); - if isHeadPoints && ~strcmpi(FigureType, 'Topography') - % Are head points visible - hHeadPointsMarkers = findobj(GlobalData.DataSet(iDS).Figure(iFig).hFigure, 'Tag', 'HeadPointsMarkers'); - isVisible = ~isempty(hHeadPointsMarkers) && strcmpi(get(hHeadPointsMarkers, 'Visible'), 'on'); - jItem = gui_component('CheckBoxMenuItem', jMenuFigure, [], 'View head points', IconLoader.ICON_CHANNEL, [], @(h,ev)ViewHeadPoints(hFig, ~isVisible)); - jItem.setSelected(isVisible); - jItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, KeyEvent.CTRL_MASK)); - % Are head points color coded by distance - isColorDist = ~isempty(hHeadPointsMarkers) && strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat'); - jItem = gui_component('CheckBoxMenuItem', jMenuFigure, [], 'Color head points by distance', IconLoader.ICON_CHANNEL, [], @(h,ev)ViewHeadPoints(hFig, isVisible, ~isColorDist)); - jItem.setSelected(isColorDist); - jItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, KeyEvent.CTRL_MASK)); - end + jItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.CTRL_MASK)); jMenuFigure.addSeparator(); % Change background color gui_component('MenuItem', jMenuFigure, [], 'Change background color', IconLoader.ICON_COLOR_SELECTION, [], @(h,ev)bst_figures('SetBackgroundColor', hFig)); @@ -3760,18 +3762,20 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) % Color points according to distance to surface % Get selected surface [iTess, TessInfo, hFig, sSurf] = panel_surface('GetSelectedSurface', hFig); - % Compute the distance - Dist = bst_surfdist(get(hHeadPointsMarkers, 'Vertices'), sSurf.Vertices, sSurf.Faces); - set(hHeadPointsMarkers, 'CData', Dist * 1000, ... - 'MarkerFaceColor', 'flat', 'MarkerEdgeColor', 'flat'); - if ~ismember(ColormapType, ColormapInfo.AllTypes) - % Add missing colormap (color was toggled after points were displayed) - bst_colormaps('AddColormapToFigure', hFig, ColormapType, 'mm'); - ColormapInfo = getappdata(hFig, 'Colormap'); - end - if strcmpi(ColormapInfo.Type, ColormapType) - bst_colormaps('SetColorbarVisible', hFig, 1); - bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); + if ~isempty(sSurf) && isfield(sSurf, 'Vertices') && ~isempty(sSurf.Vertices) + % Compute the distance + Dist = bst_surfdist(get(hHeadPointsMarkers, 'Vertices'), sSurf.Vertices, sSurf.Faces); + set(hHeadPointsMarkers, 'CData', Dist * 1000, ... + 'MarkerFaceColor', 'flat', 'MarkerEdgeColor', 'flat'); + if ~ismember(ColormapType, ColormapInfo.AllTypes) + % Add missing colormap (color was toggled after points were displayed) + bst_colormaps('AddColormapToFigure', hFig, ColormapType, 'mm'); + ColormapInfo = getappdata(hFig, 'Colormap'); + end + if strcmpi(ColormapInfo.Type, ColormapType) + bst_colormaps('SetColorbarVisible', hFig, 1); + bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); + end end elseif ~isColorDist && strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat') % Conventional fixed color From 9faac455aec8bf503b2c56e8e08a09bbd80906d8 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Wed, 31 Aug 2022 17:26:20 -0400 Subject: [PATCH 21/32] colorbar fix --- toolbox/gui/figure_3d.m | 19 ++++++++++--------- toolbox/math/bst_surfdist.m | 6 +++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index b90c43124..c4b196e12 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -3763,15 +3763,16 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) % Get selected surface [iTess, TessInfo, hFig, sSurf] = panel_surface('GetSelectedSurface', hFig); if ~isempty(sSurf) && isfield(sSurf, 'Vertices') && ~isempty(sSurf.Vertices) - % Compute the distance - Dist = bst_surfdist(get(hHeadPointsMarkers, 'Vertices'), sSurf.Vertices, sSurf.Faces); - set(hHeadPointsMarkers, 'CData', Dist * 1000, ... - 'MarkerFaceColor', 'flat', 'MarkerEdgeColor', 'flat'); if ~ismember(ColormapType, ColormapInfo.AllTypes) % Add missing colormap (color was toggled after points were displayed) bst_colormaps('AddColormapToFigure', hFig, ColormapType, 'mm'); ColormapInfo = getappdata(hFig, 'Colormap'); + ColormapChangedCallback(iDS, iFig); end + % Compute the distance + Dist = bst_surfdist(get(hHeadPointsMarkers, 'Vertices'), sSurf.Vertices, sSurf.Faces); + set(hHeadPointsMarkers, 'CData', Dist * 1000, ... + 'MarkerFaceColor', 'flat', 'MarkerEdgeColor', 'flat'); if strcmpi(ColormapInfo.Type, ColormapType) bst_colormaps('SetColorbarVisible', hFig, 1); bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); @@ -3869,6 +3870,8 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) CData = Dist * 1000; % mm MarkerFaceColor = 'flat'; MarkerEdgeColor = 'flat'; + ColormapType = 'stat1'; + bst_colormaps('AddColormapToFigure', hFig, ColormapType, 'mm'); else CData = 'w'; % any color, not displayed MarkerFaceColor = [.3 1 .3]; @@ -3886,11 +3889,9 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) 'UserData', iExtra, ... 'Tag', 'HeadPointsMarkers'); if isColorDist - % TBD if we should use colormaps or not here. - ColormapType = 'stat1'; - bst_colormaps('AddColormapToFigure', hFig, ColormapType, 'mm'); - bst_colormaps('SetColorbarVisible', hFig, 1); - bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); + ColormapChangedCallback(iDS, iFig); +% bst_colormaps('SetColorbarVisible', hFig, 1); +% bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); end end end diff --git a/toolbox/math/bst_surfdist.m b/toolbox/math/bst_surfdist.m index 47787de41..4dab27212 100644 --- a/toolbox/math/bst_surfdist.m +++ b/toolbox/math/bst_surfdist.m @@ -56,9 +56,9 @@ EdgesV(:,:,2) = Vertices(Faces(:,3),:) - Vertices(Faces(:,2),:); EdgesV(:,:,3) = Vertices(Faces(:,1),:) - Vertices(Faces(:,3),:); % First edge to second edge: counter clockwise = up -m = cross(EdgesV(:,:,1), EdgesV(:,:,2)); -FaceNormals = sqrt(ones./(sum((m.*m)')))'*ones(1,size(m,2)).*m; -%FaceArea = sqrt(sum(FaceNormalsA.^2, 2)); +FaceNormals = cross(EdgesV(:,:,1), EdgesV(:,:,2)); +%FaceArea = sqrt(sum(FaceNormals.^2, 2)); +FaceNormals = bsxfun(@rdivide, FaceNormals, sqrt(sum(FaceNormals.^2, 2))); % Perpendicular vectors to edges, pointing inside triangular face. for e = 3:-1:1 EdgeTriNormals(:,:,e) = cross(FaceNormals, EdgesV(:,:,e)); From 90b4dacdeaf8dcf0f63466b58b15a0c0fde6bac6 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Fri, 18 Nov 2022 16:50:19 -0500 Subject: [PATCH 22/32] tweaks to manual registration figure, head points distance coloring --- toolbox/core/bst_colormaps.m | 24 +- toolbox/gui/figure_3d.m | 42 ++-- toolbox/gui/figure_mri.m | 2 +- toolbox/gui/panel_surface.m | 87 ++++++- toolbox/math/bst_surfdist.m | 8 - .../functions/process_adjust_coordinates.m | 217 ++++++++---------- toolbox/sensors/channel_align_auto.m | 36 +-- toolbox/sensors/channel_align_manual.m | 132 +++++++++-- toolbox/sensors/channel_align_scs.m | 122 ++++++++++ toolbox/tree/tree_callbacks.m | 2 + 10 files changed, 446 insertions(+), 226 deletions(-) create mode 100644 toolbox/sensors/channel_align_scs.m diff --git a/toolbox/core/bst_colormaps.m b/toolbox/core/bst_colormaps.m index d48956357..d3f626c87 100644 --- a/toolbox/core/bst_colormaps.m +++ b/toolbox/core/bst_colormaps.m @@ -376,12 +376,20 @@ function SetMaxCustom(ColormapType, DisplayUnits, newMin, newMax) case {'3DViz', 'MriViewer'} % Get surfaces defined in this figure TessInfo = getappdata(sFigure.hFigure, 'Surface'); - DataFig = TessInfo.DataMinMax; - if ~isempty(TessInfo.DataSource.Type) - DataType = TessInfo.DataSource.Type; - isSLORETA = strcmpi(DataType, 'Source') && ~isempty(strfind(lower(TessInfo.DataSource.FileName), 'sloreta')); - if isSLORETA - DataType = 'sLORETA'; + % Find surface(s) that matches this ColormapType + iSurfaces = find(strcmpi({TessInfo.ColormapType}, ColormapType)); + DataFig = []; + for i = 1:length(iSurfaces) + iTess = iSurfaces(i); + DataFig = [min([DataFig(:); TessInfo(iTess).DataMinMax(:)]), ... + max([DataFig(:); TessInfo(iTess).DataMinMax(:)])]; + if ~isempty(TessInfo(iTess).DataSource.Type) + % We'll keep the last non-empty DataType + DataType = TessInfo(iTess).DataSource.Type; + isSLORETA = strcmpi(DataType, 'Source') && ~isempty(strfind(lower(TessInfo(iTess).DataSource.FileName), 'sloreta')); + if isSLORETA + DataType = 'sLORETA'; + end end end @@ -443,10 +451,10 @@ function SetMaxCustom(ColormapType, DisplayUnits, newMin, newMax) if isinf(amplitudeMax) fFactor = 1; fUnits = 'Inf'; - elseif isequal(DisplayUnits, '%') + elseif isequal(DisplayUnits, '%') || isequal(DisplayUnits, 'mm') fFactor = 1; fUnits = DisplayUnits; - else + else % Guess the display units [tmp, fFactor, fUnits ] = bst_getunits(amplitudeMax, DataType); % For readability: replace '\sigma' with 'no units' diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index 42c32b246..7fafa8733 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -1755,10 +1755,17 @@ function DisplayFigurePopup(hFig) end end + end + + if ~isempty(GlobalData.DataSet(iDS).ChannelFile) % Show Head points isHeadPoints = ~isempty(GlobalData.DataSet(iDS).HeadPoints) && ~isempty(GlobalData.DataSet(iDS).HeadPoints.Loc); if isHeadPoints && ~strcmpi(FigureType, 'Topography') - jMenuChannels.addSeparator(); + if isAlignFig + jMenuChannels = gui_component('Menu', jPopup, [], 'Channels', IconLoader.ICON_CHANNEL); + else + jMenuChannels.addSeparator(); + end % Are head points visible hHeadPointsMarkers = findobj(GlobalData.DataSet(iDS).Figure(iFig).hFigure, 'Tag', 'HeadPointsMarkers'); isVisible = ~isempty(hHeadPointsMarkers) && strcmpi(get(hHeadPointsMarkers, 'Visible'), 'on'); @@ -3746,7 +3753,7 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) [HeadPoints.Loc(1,iDupli), HeadPoints.Loc(2,iDupli), HeadPoints.Loc(3,iDupli)] = sph2cart(th, phi, r - 0.0001); end - % Else, get previous head points + % Look for previous head points hHeadPointsMarkers = findobj(hAxes, 'Tag', 'HeadPointsMarkers'); hHeadPointsLabels = findobj(hAxes, 'Tag', 'HeadPointsLabels'); % If head points graphic objects already exist: set the "Visible" property @@ -3761,22 +3768,19 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) ColormapType = 'stat1'; if isColorDist && ~strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat') % Color points according to distance to surface - % Get selected surface - [iTess, TessInfo, hFig, sSurf] = panel_surface('GetSelectedSurface', hFig); + % Get scalp surface + [~, ~, hFig, sSurf] = panel_surface('GetSurface', hFig, '', 'Scalp'); if ~isempty(sSurf) && isfield(sSurf, 'Vertices') && ~isempty(sSurf.Vertices) - if ~ismember(ColormapType, ColormapInfo.AllTypes) - % Add missing colormap (color was toggled after points were displayed) - bst_colormaps('AddColormapToFigure', hFig, ColormapType, 'mm'); - ColormapInfo = getappdata(hFig, 'Colormap'); - ColormapChangedCallback(iDS, iFig); - end % Compute the distance Dist = bst_surfdist(get(hHeadPointsMarkers, 'Vertices'), sSurf.Vertices, sSurf.Faces); set(hHeadPointsMarkers, 'CData', Dist * 1000, ... 'MarkerFaceColor', 'flat', 'MarkerEdgeColor', 'flat'); - if strcmpi(ColormapInfo.Type, ColormapType) - bst_colormaps('SetColorbarVisible', hFig, 1); - bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); + if ~ismember(ColormapType, ColormapInfo.AllTypes) + iTess = panel_surface('GetSurface', hFig, '', 'HeadPoints'); + if isempty(iTess) + error('HeadPoints surface not found.'); + end + panel_surface('SetSurfaceData', hFig, iTess, 'HeadPointsDistance', '', 1); % isStat=1 end end elseif ~isColorDist && strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat') @@ -3864,15 +3868,13 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) if ~isempty(iExtra) % Color code points by distance if isColorDist - % Get selected surface - [iTess, TessInfo, hFig, sSurf] = panel_surface('GetSelectedSurface', hFig); + % Get scalp surface + [~, ~, hFig, sSurf] = panel_surface('GetSurface', hFig, '', 'Scalp'); % Compute the distance Dist = bst_surfdist(digLoc(iExtra, :), sSurf.Vertices, sSurf.Faces); CData = Dist * 1000; % mm MarkerFaceColor = 'flat'; MarkerEdgeColor = 'flat'; - ColormapType = 'stat1'; - bst_colormaps('AddColormapToFigure', hFig, ColormapType, 'mm'); else CData = 'w'; % any color, not displayed MarkerFaceColor = [.3 1 .3]; @@ -3889,10 +3891,10 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) 'Marker', 'o', ... 'UserData', iExtra, ... 'Tag', 'HeadPointsMarkers'); + % Add points patch to figure surfaces so all color bar functionality work. + iTess = panel_surface('AddSurface', hFig, '', 'HeadPoints'); if isColorDist - ColormapChangedCallback(iDS, iFig); -% bst_colormaps('SetColorbarVisible', hFig, 1); -% bst_colormaps('ConfigureColorbar', hFig, ColormapType, 'stat', 'mm'); + panel_surface('SetSurfaceData', hFig, iTess, 'HeadPointsDistance', '', 1); % isStat=1 end end end diff --git a/toolbox/gui/figure_mri.m b/toolbox/gui/figure_mri.m index d702baad4..b4c565ed8 100644 --- a/toolbox/gui/figure_mri.m +++ b/toolbox/gui/figure_mri.m @@ -2583,7 +2583,7 @@ function ButtonSave_Callback(hFig, varargin) warning('off', 'MATLAB:load:variableNotFound'); sMriOld = load(MriFileFull, 'SCS'); warning('on', 'MATLAB:load:variableNotFound'); - % If the fiducials were modified + % If the fiducials were modified (> 1um) if isfield(sMriOld, 'SCS') && all(isfield(sMriOld.SCS,{'NAS','LPA','RPA'})) ... && ~isempty(sMriOld.SCS.NAS) && ~isempty(sMriOld.SCS.LPA) && ~isempty(sMriOld.SCS.RPA) ... && ((max(abs(sMri.SCS.NAS - sMriOld.SCS.NAS)) > 1e-3) || ... diff --git a/toolbox/gui/panel_surface.m b/toolbox/gui/panel_surface.m index f775e50eb..12537050c 100644 --- a/toolbox/gui/panel_surface.m +++ b/toolbox/gui/panel_surface.m @@ -1130,15 +1130,20 @@ function UpdateSurfaceProperties() %% ===== ADD A SURFACE ===== % Add a surface to a given 3DViz figure % USAGE : [iTess, TessInfo] = panel_surface('AddSurface', hFig, surfaceFile) +% [iTess, TessInfo] = panel_surface('AddSurface', hFig, [], surfaceType) % OUTPUT: Indice of the surface in the figure's surface array -function [iTess, TessInfo] = AddSurface(hFig, surfaceFile) +function [iTess, TessInfo] = AddSurface(hFig, surfaceFile, surfaceType) % ===== CHECK EXISTENCE ===== - % Check whether filename is an absolute or relative path - surfaceFile = file_short(surfaceFile); % Get figure appdata (surfaces configuration) TessInfo = getappdata(hFig, 'Surface'); % Check that this surface is not already displayed in 3DViz figure - iTess = find(file_compare({TessInfo.SurfaceFile}, surfaceFile)); + if isempty(surfaceFile) && nargin > 2 + iTess = find(file_compare({TessInfo.Name}, surfaceType)); + else + % Check whether filename is an absolute or relative path + surfaceFile = file_short(surfaceFile); + iTess = find(file_compare({TessInfo.SurfaceFile}, surfaceFile)); + end if ~isempty(iTess) disp('BST> This surface is already displayed. Ignoring...'); return @@ -1161,6 +1166,9 @@ function UpdateSurfaceProperties() % ===== PLOT OBJECT ===== % Get file type (tessalation or MRI) fileType = file_gettype(surfaceFile); + if strcmpi(fileType, 'unknown') && nargin > 2 + fileType = surfaceType; + end % === TESSELATION === if any(strcmpi(fileType, {'cortex','scalp','innerskull','outerskull','tess'})) % === LOAD SURFACE === @@ -1291,8 +1299,18 @@ function UpdateSurfaceProperties() setappdata(hFig, 'Surface', TessInfo); end + % === NO FILE: HeadPoints === + elseif strcmpi(fileType, 'HeadPoints') + % Points were already displayed; just add the patch to the figure surfaces. + TessInfo(iTess).DataSource.Type = 'HeadPointsDistance'; + TessInfo(iTess).Name = 'HeadPoints'; + TessInfo(iTess).ColormapType = 'stat1'; + TessInfo(iTess).hPatch = findobj(hFig, 'Tag', 'HeadPointsMarkers'); + % Update figure's surfaces list and current surface pointer + setappdata(hFig, 'Surface', TessInfo); + % === FEM === - else + else % TODO: Check for FEM fileType explicitly view_surface_fem(surfaceFile, [], [], [], hFig); end % Update default surface @@ -1496,6 +1514,11 @@ function UpdateSurfaceProperties() DisplayUnits = []; TessInfo(iTess).Data = []; TessInfo(iTess).DataWmat = []; + + case 'HeadPointsDistance' + ColormapType = 'stat1'; + DisplayUnits = 'mm'; + % Data is actually added in UpdateSurfaceData below. otherwise ColormapType = ''; @@ -1878,6 +1901,15 @@ function UpdateSurfaceProperties() figure_callback(hFig, 'UpdateSurfaceColor', hFig, iTess); % Get updated surface definition TessInfo = getappdata(hFig, 'Surface'); + + case 'HeadPointsDistance' + TessInfo(iTess).Data = get(TessInfo(iTess).hPatch, 'CData'); + if isempty(TessInfo(iTess).Data) || ~isnumeric(TessInfo(iTess).Data) + isOk = 0; + return; + end + TessInfo(iTess).DataMinMax = [min(TessInfo(iTess).Data(:)), max(TessInfo(iTess).Data(:))]; + otherwise % Nothing to do end @@ -2001,7 +2033,7 @@ function UpdateSurfaceColormap(hFig, iSurfaces) TessInfo(iTess).Data = abs(TessInfo(iTess).Data); end % If current colormap is the default colormap for this figure (for colorbar) - if strcmpi(ColormapInfo.Type, TessInfo(iTess).ColormapType) && ~isempty(TessInfo(iTess).DataSource.FileName) + if strcmpi(ColormapInfo.Type, TessInfo(iTess).ColormapType) && ~isempty(TessInfo(iTess).DataSource.Type) if all(~isnan(TessInfo(iTess).DataLimitValue)) && (TessInfo(iTess).DataLimitValue(1) < TessInfo(iTess).DataLimitValue(2)) set(hAxes, 'CLim', TessInfo(iTess).DataLimitValue); else @@ -2017,7 +2049,8 @@ function UpdateSurfaceColormap(hFig, iSurfaces) end end % === DISPLAY ON MRI === - if strcmpi(TessInfo(iTess).Name, 'Anatomy') && ~isempty(TessInfo(iTess).DataSource.Type) && ~strcmpi(TessInfo(iTess).DataSource.Type, 'MriTime') && isempty(TessInfo(iTess).OverlayCube) + if strcmpi(TessInfo(iTess).Name, 'Anatomy') && ~isempty(TessInfo(iTess).DataSource.Type) && ... + ~strcmpi(TessInfo(iTess).DataSource.Type, 'MriTime') && isempty(TessInfo(iTess).OverlayCube) % Progress bar isProgressBar = bst_progress('isVisible'); bst_progress('start', 'Display MRI', 'Updating values...'); @@ -2029,6 +2062,10 @@ function UpdateSurfaceColormap(hFig, iSurfaces) if ~isProgressBar bst_progress('stop'); end + elseif strcmpi(TessInfo(iTess).Name, 'HeadPoints') + % No need to update surface color here, data already updated. + % Update figure's appdata (surface list) + setappdata(hFig, 'Surface', TessInfo); else % Update figure's appdata (surface list) setappdata(hFig, 'Surface', TessInfo); @@ -2047,13 +2084,39 @@ function UpdateSurfaceColormap(hFig, iSurfaces) %% ===== GET SURFACE ===== % Find a surface in a given 3DViz figure -function iTess = GetSurface(hFig, SurfaceFile) - % Check whether filename is an absolute or relative path - SurfaceFile = file_short(SurfaceFile); +% Usage: [iTess, TessInfo, hFig, sSurf] = GetSurface(hFig, SurfaceFile) +% [iTess, TessInfo, hFig, sSurf] = GetSurface(hFig, [], SurfaceType) +function [iTess, TessInfo, hFig, sSurf] = GetSurface(hFig, SurfaceFile, SurfaceType) + iTess = []; + sSurf = []; % Get figure appdata (surfaces configuration) TessInfo = getappdata(hFig, 'Surface'); - % Find the surface in the 3DViz figure - iTess = find(file_compare({TessInfo.SurfaceFile}, SurfaceFile)); + if nargin < 3 || (isempty(SurfaceType) && isempty(SurfaceFile)) + return; + end + if isempty(SurfaceFile) + % Search by type. + iTess = find(strcmpi({TessInfo.Name}, SurfaceType)); + if isempty(iTess) + return; + elseif numel(iTess) > 1 + % See if selected is one of them, otherwise return last. + iTessSel = getappdata(hFig, 'iSurface'); + if ismember(iTessSel, iTess) + iTess = iTessSel; + else + iTess = iTess(end); + end + end + else + % Check whether filename is an absolute or relative path + SurfaceFile = file_short(SurfaceFile); + % Find the surface in the 3DViz figure + iTess = find(file_compare({TessInfo.SurfaceFile}, SurfaceFile)); + end + if (nargout >= 4) && ~isempty(TessInfo) && ~isempty(iTess) + sSurf = bst_memory('GetSurface', TessInfo(iTess).SurfaceFile); + end end diff --git a/toolbox/math/bst_surfdist.m b/toolbox/math/bst_surfdist.m index 432a5fdfc..2db5580a5 100644 --- a/toolbox/math/bst_surfdist.m +++ b/toolbox/math/bst_surfdist.m @@ -19,20 +19,12 @@ % @============================================================================= % This function is part of the Brainstorm software: % https://neuroimage.usc.edu/brainstorm -<<<<<<< HEAD -% -======= % ->>>>>>> upstream/master % Copyright (c) University of Southern California & McGill University % This software is distributed under the terms of the GNU General Public License % as published by the Free Software Foundation. Further details on the GPLv3 % license can be found at http://www.gnu.org/copyleft/gpl.html. -<<<<<<< HEAD -% -======= % ->>>>>>> upstream/master % FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE % UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY % WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index 5e4214b97..e41dc8cee 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -1,11 +1,10 @@ function varargout = process_adjust_coordinates(varargin) % PROCESS_ADJUST_COORDINATES: Adjust, recompute, or remove various coordinate transformations. % -% Native coordinates are based on system fiducials (e.g. MEG head coils), -% whereas Brainstorm's SCS coordinates are based on the anatomical fiducial -% points. After alignment between MRI and headpoints, the anatomical fiducials -% on the MRI side define the SCS and the ones in the channel files -% (ChannelMat.SCS) are ignored. +% Native coordinates are based on system fiducials (e.g. MEG head coils), whereas Brainstorm's SCS +% coordinates are based on the anatomical fiducial points. After alignment between MRI and +% headpoints, the anatomical fiducials on the MRI side define the SCS and the ones in the channel +% files (ChannelMat.SCS) are ignored. % @============================================================================= % This function is part of the Brainstorm software: @@ -125,9 +124,8 @@ bst_progress('start', 'Adjust coordinate system', ... ' ', 0, nFiles); - % If resetting, in case the original data moved, and because the same - % channel file may appear in many places for processed data, keep track of - % user file selections. + % If resetting, in case the original data moved, and because the same channel file may appear in + % many places for processed data, keep track of user file selections. NewChannelFiles = cell(0, 2); for iFile = iUniqFiles(:)' % no need to repeat on same channel file. @@ -154,13 +152,11 @@ % ---------------------------------------------------------------- if sProcess.options.reset.Value - % The main goal of this option is to fix a bug in a previous - % version: when importing a channel file, when going to SCS - % coordinates based on digitized coils and anatomical fiducials, the - % channel orientation was wrong. We wish to fix this but keep as - % much pre-processing that was previously done. Thus we will - % re-import the channel file, and copy the projectors (and history) - % from the old one. + % The main goal of this option is to fix a bug in a previous version: when importing a + % channel file, when going to SCS coordinates based on digitized coils and anatomical + % fiducials, the channel orientation was wrong. We wish to fix this but keep as much + % pre-processing that was previously done. Thus we will re-import the channel file, and + % copy the projectors (and history) from the old one. [ChannelMat, NewChannelFiles, Failed] = ... ResetChannelFile(ChannelMat, NewChannelFiles, sInputs(iFile), sProcess); @@ -170,13 +166,12 @@ % ---------------------------------------------------------------- elseif sProcess.options.remove.Value - % Because channel_align_manual does not consistently apply the - % manual transformation to all sensors or save it in both TransfMeg - % and TransfEeg, it could lead to confusion and errors when playing - % with transforms. Therefore, if we detect a difference between the - % MEG and EEG transforms when trying to remove one that applies to - % both (currently only refine with head points), we don't proceed - % and recommend resetting with the original channel file instead. + % Because channel_align_manual does not consistently apply the manual transformation to + % all sensors or save it in both TransfMeg and TransfEeg, it could lead to confusion and + % errors when playing with transforms. Therefore, if we detect a difference between the + % MEG and EEG transforms when trying to remove one that applies to both (currently only + % refine with head points), we don't proceed and recommend resetting with the original + % channel file instead. Which = {}; if sProcess.options.head.Value @@ -191,9 +186,8 @@ ChannelMat = RemoveTransformation(ChannelMat, TransfLabel, sInputs(iFile), sProcess); end % TransfLabel loop - % We cannot change back the MRI fiducials, but in order to be able - % to update it again from digitized fids, we must edit the MRI - % history. + % We cannot change back the MRI fiducials, but in order to be able to update it again + % from digitized fids, we must edit the MRI history. if sProcess.options.points.Value && sProcess.options.scs.Value % Get subject in database, with subject directory sSubject = bst_get('Subject', sInputs(iFile).SubjectFile); @@ -231,12 +225,19 @@ if ~sProcess.options.remove.Value && sProcess.options.points.Value % Redundant, but makes sense to have it here also. - Tolerance = sProcess.options.tolerance.Value{1} / 100; bst_progress('text', 'Fitting head surface to points...'); + % If called externally without a tolerance value, set isWarning true so it asks. + if isempty(sProcess.options.tolerance.Value) + isWarning = true; + Tolerance = 0; + else + isWarning = false; + Tolerance = sProcess.options.tolerance.Value{1} / 100; + end [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(sInputs(iFile).ChannelFile, ... - ChannelMat, 0, 0, Tolerance, sProcess.options.scs.Value); % No warning or confirmation - % ChannelFile needed to find subject and scalp surface, but not - % used otherwise when ChannelMat is provided. + ChannelMat, isWarning, 0, Tolerance, sProcess.options.scs.Value); % No confirmation + % ChannelFile needed to find subject and scalp surface, but not used otherwise when + % ChannelMat is provided. if ~isempty(strReport) bst_report('Info', sProcess, sInputs(iFile), strReport); elseif isSkip @@ -260,10 +261,9 @@ end % file loop bst_progress('stop'); - % Return the input files that were processed properly. Include those that - % were removed due to sharing a channel file, where appropriate. The - % complicated indexing picks the first input of those with the same channel - % file, i.e. the one that was marked ok. + % Return the input files that were processed properly. Include those that were removed due to + % sharing a channel file, where appropriate. The complicated indexing picks the first input of + % those with the same channel file, i.e. the one that was marked ok. OutputFiles = {sInputs(isFileOk(iUniqInputs(iUniqFiles))).FileName}; end @@ -504,9 +504,8 @@ return; end - % The data could be changed such that the head position could be readjusted - % (e.g. by deleting segments). This is allowed and the previous adjustment - % will be replaced. + % The data could be changed such that the head position could be readjusted (e.g. by deleting + % segments). This is allowed and the previous adjustment will be replaced. if isfield(ChannelMat, 'TransfMegLabels') && iscell(ChannelMat.TransfMegLabels) && ... ismember('AdjustedNative', ChannelMat.TransfMegLabels) bst_report('Info', sProcess, sInputs, ... @@ -579,9 +578,8 @@ return; end - % Extract transformations that are applied before and after the head - % position adjustment. Any previous adjustment will be ignored here and - % replaced later. + % Extract transformations that are applied before and after the head position adjustment. Any + % previous adjustment will be ignored here and replaced later. [TransfBefore, TransfAdjust, TransfAfter, iAdjust, iDewToNat] = ... GetTransforms(ChannelMat, sInputs); if isempty(TransfBefore) @@ -603,13 +601,11 @@ ChannelMat = channel_apply_transf(ChannelMat, TransfMat, iMeg, false); % Don't apply to head points. ChannelMat = ChannelMat{1}; - % After much thought, it was decided to save this adjustment transformation - % separately and at its logical place: between 'Dewar=>Native' and - % 'Native=>Brainstorm/CTF'. In particular, this allows us to use it - % directly when displaying head motion distance. This however means we must - % correctly move the transformation from the end where it was just applied - % to its logical place. This "moved" transformation is also computed in - % LocationTransform above. + % After much thought, it was decided to save this adjustment transformation separately and at + % its logical place: between 'Dewar=>Native' and 'Native=>Brainstorm/CTF'. In particular, this + % allows us to use it directly when displaying head motion distance. This however means we must + % correctly move the transformation from the end where it was just applied to its logical place. + % This "moved" transformation is also computed in LocationTransform above. if isempty(iAdjust) iAdjust = iDewToNat + 1; % Shift transformations to make room for the new @@ -644,13 +640,12 @@ end % AdjustHeadPosition - function [InitLoc, Message] = ReferenceHeadLocation(ChannelMat, sInput) % Compute initial head location in Dewar coordinates. - % Here we want to recreate the correct triangle shape from the relative head - % coil locations and in the position saved as the reference (initial) head - % position according to Brainstorm coordinate transformation matrices. + % Here we want to recreate the correct triangle shape from the relative head coil locations and + % in the position saved as the reference (initial) head position according to Brainstorm + % coordinate transformation matrices. if nargin < 2 sInput = []; @@ -659,9 +654,9 @@ end Message = ''; - % These aren't exactly the coil positions in the .hc file, which are not saved - % anywhere in Brainstorm, but was verified to give the same transformation. - % The SCS coil coordinates are from the digitized coil positions. + % These aren't exactly the coil positions in the .hc file, which are not saved anywhere in + % Brainstorm, but was verified to give the same transformation. The SCS coil coordinates are + % from the digitized coil positions. if isfield(ChannelMat, 'SCS') && all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) && ... (length(ChannelMat.SCS.NAS) == 3) && (length(ChannelMat.SCS.LPA) == 3) && (length(ChannelMat.SCS.RPA) == 3) % % Use the SCS distances from origin, with left and right PA points symmetrical. @@ -685,11 +680,9 @@ % InitLoc above is in Native coordiates (if pre head loc didn't fail). % Bring it back to Dewar coordinates to compare with HLU channels. % - % Take into account if the initial/reference head position was - % "adjusted", i.e. replaced by the median position throughout the - % recording. If so, use all transformations from 'Dewar=>Native' to - % this adjustment transformation. (In practice there shouldn't be any - % between them.) + % Take into account if the initial/reference head position was "adjusted", i.e. replaced by the + % median position throughout the recording. If so, use all transformations from 'Dewar=>Native' + % to this adjustment transformation. (In practice there shouldn't be any between them.) [TransfBefore, TransfAdjust] = GetTransforms(ChannelMat, sInput); InitLoc = TransfBefore \ (TransfAdjust \ InitLoc); InitLoc(4, :) = []; @@ -699,23 +692,21 @@ function [TransfBefore, TransfAdjust, TransfAfter, iAdjust, iDewToNat] = ... GetTransforms(ChannelMat, sInputs) - % Extract transformations that are applied before and after the head - % position adjustment we are creating now. We keep the 'Dewar=>Native' - % transformation intact and separate from the adjustment for no deep - % reason, but it is the only remaining trace of the initial head coil + % Extract transformations that are applied before and after the head position adjustment we are + % creating now. We keep the 'Dewar=>Native' transformation intact and separate from the + % adjustment for no deep reason, but it is the only remaining trace of the initial head coil % positions in Brainstorm. - % The reason this function was split from LocationTransform is that it - % can be called only once outside the head sample loop in process_sss, - % whereas LocationTransform is called many times within the loop. + % The reason this function was split from LocationTransform is that it can be called only once + % outside the head sample loop in process_sss, whereas LocationTransform is called many times + % within the loop. - % When this is called from process_sss, we are possibly working on a - % second head adjustment, this time based on the instantaneous head - % position, so we need to keep the global adjustment based on the entire - % recording if it is there. + % When this is called from process_sss, we are possibly working on a second head adjustment, + % this time based on the instantaneous head position, so we need to keep the global adjustment + % based on the entire recording if it is there. - % Check order of transformations. These situations should not happen - % unless there was some manual editing. + % Check order of transformations. These situations should not happen unless there was some + % manual editing. iDewToNat = find(strcmpi(ChannelMat.TransfMegLabels, 'Dewar=>Native')); iAdjust = find(strcmpi(ChannelMat.TransfMegLabels, 'AdjustedNative')); TransfBefore = []; @@ -766,11 +757,9 @@ end % GetTransforms -function [TransfMat, TransfAdjust] = LocationTransform(Loc, ... - TransfBefore, TransfAdjust, TransfAfter) - % Compute transformation corresponding to head coil positions. - % We want this to be as efficient as possible, since used many times by - % process_sss. +function [TransfMat, TransfAdjust] = LocationTransform(Loc, TransfBefore, TransfAdjust, TransfAfter) + % Compute transformation corresponding to head coil positions. We want this to be as efficient + % as possible, since used many times by process_sss. % Check for previous version. if nargin < 4 @@ -778,10 +767,9 @@ end % Transformation matrices are in m, as are HLU channels. - % The HLU channels (here Loc) are in dewar coordinates. Bring them to - % the current system by applying all saved transformations, starting with - % 'Dewar=>Native'. This will save us from having to use inverse - % transformations later. + % The HLU channels (here Loc) are in dewar coordinates. Bring them to the current system by + % applying all saved transformations, starting with 'Dewar=>Native'. This will save us from + % having to use inverse transformations later. Loc = TransfAfter(1:3, :) * TransfAdjust * TransfBefore * [reshape(Loc, 3, 3); 1, 1, 1]; % [[Loc(1:3), Loc(4:6), Loc(5:9)]; 1, 1, 1]; % test if efficiency difference. @@ -801,14 +789,12 @@ TransfMat(1:3,1:3) = [X, Y, Z]'; TransfMat(1:3,4) = - [X, Y, Z]' * Origin; - % TransfMat at this stage is a transformation from the current system - % back to the now adjusted Native system. We thus need to reapply the - % following tranformations. + % TransfMat at this stage is a transformation from the current system back to the now adjusted + % Native system. We thus need to reapply the following tranformations. if nargout > 1 - % Transform from non-adjusted native coordinates to newly adjusted native - % coordinates. To be saved in channel file between "Dewar=>Native" and - % "Native=>Brainstorm/CTF". + % Transform from non-adjusted native coordinates to newly adjusted native coordinates. To + % be saved in channel file between "Dewar=>Native" and "Native=>Brainstorm/CTF". TransfAdjust = TransfMat * TransfAfter * TransfAdjust; end @@ -838,34 +824,17 @@ % % M = GeoMedian(X, Precision) % - % Calculate the geometric median: the point that minimizes sum of - % Euclidean distances to all points. size(X) = [n, d, ...], where n is - % the number of data points, d is the number of components for each point - % and any additional array dimension is treated as independent sets of - % data and a median is calculated for each element along those dimensions - % sequentially; size(M) = [1, d, ...]. This is an approximate iterative - % procedure that stops once the desired precision is achieved. If - % Precision is not provided, 1e-4 of the max distance from the centroid - % is used. - % - % Weiszfeld's algorithm is used, which is a subgradient algorithm; with - % (Verdi & Zhang 2001)'s modification to avoid non-optimal fixed points - % (if at any iteration the approximation of M equals a data point). - % - % - % (c) Copyright 2018 Marc Lalancette - % The Hospital for Sick Children, Toronto, Canada - % - % This file is part of a free repository of Matlab tools for MEG - % data processing and analysis . - % You can redistribute it and/or modify it under the terms of the GNU - % General Public License as published by the Free Software Foundation, - % either version 3 of the License, or (at your option) a later version. - % - % This program is distributed WITHOUT ANY WARRANTY. - % See the LICENSE file, or for details. + % Calculate the geometric median: the point that minimizes sum of Euclidean distances to all + % points. size(X) = [n, d, ...], where n is the number of data points, d is the number of + % components for each point and any additional array dimension is treated as independent sets of + % data and a median is calculated for each element along those dimensions sequentially; size(M) + % = [1, d, ...]. This is an approximate iterative procedure that stops once the desired + % precision is achieved. If Precision is not provided, 1e-4 of the max distance from the + % centroid is used. % - % 2012-05 + % Weiszfeld's algorithm is used, which is a subgradient algorithm; with (Verdi & Zhang 2001)'s + % modification to avoid non-optimal fixed points (if at any iteration the approximation of M + % equals a data point). nDims = ndims(X); XSize = size(X); @@ -892,17 +861,15 @@ Precision = bsxfun(@rdivide, Precision, Scale); % Precision ./ Scale; % [1, 1, nSets] end - % Initial estimate: median in each dimension separately. Though this - % gives a chance of picking one of the data points, which requires - % special treatment. + % Initial estimate: median in each dimension separately. Though this gives a chance of picking + % one of the data points, which requires special treatment. M2 = median(X, 1); - % It might be better to calculate separately each independent set, - % otherwise, they are all iterated until the worst case converges. + % It might be better to calculate separately each independent set, otherwise, they are all + % iterated until the worst case converges. for s = 1:nSets - % For convenience, pick another point far enough so the loop will always - % start. + % For convenience, pick another point far enough so the loop will always start. M = bsxfun(@plus, M2(:, :, s), Precision(:, :, s)); % Iterate. while sum((M - M2(:, :, s)).^2 , 2) > Precision(s)^2 % any()scalar @@ -910,8 +877,7 @@ % Distances from M. % R = sqrt(sum( (M(ones(n, 1), :) - X(:, :, s)).^2 , 2 )); % [n, 1] R = sqrt(sum( bsxfun(@minus, M, X(:, :, s)).^2 , 2 )); % [n, 1] - % Find data points not equal to M, that we use in the computation - % below. + % Find data points not equal to M, that we use in the computation below. Good = logical(R); nG = sum(Good); if nG % > 0 @@ -925,10 +891,9 @@ end % New estimate. - % Note the possibility of D = 0 and (n - nG) = 0, in which case 0/0 - % should be 0, but here gives NaN, which the max function ignores, - % returning 0 instead of 1. This is fine however since this - % multiplies D (=0 in that case). + % Note the possibility of D = 0 and (n - nG) = 0, in which case 0/0 should be 0, but + % here gives NaN, which the max function ignores, returning 0 instead of 1. This is fine + % however since this multiplies D (=0 in that case). M2(:, :, s) = M - max(0, 1 - (n - nG)/sqrt(sum( D.^2 , 2 ))) * ... D / sum(1 ./ R, 1); end diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index 77de94ab1..aa0f7931d 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -216,39 +216,9 @@ %% ===== ADJUST MRI FIDUCIALS AND SCS ===== if isAdjustScs - % Check if already adjusted, in which case the transformation above is correct (identity if same head points). - sMriOld = in_mri_bst(sSubject.Anatomy(sSubject.iAnatomy).FileName); - % History string is set in figure_mri SaveMri. - if isfield(sMriOld, 'History') && ~isempty(sMriOld.History) && any(strcmpi(sMriOld.History(:,3), 'Applied digitized anatomical fiducials')) - if isWarning - bst_warning('Nasion and ear points already adjusted.', 'Automatic EEG-MEG/MRI registration', 0); - end - % Check if digitized anat points present, saved in ChannelMat.SCS. - % Note that these coordinates are NOT currently updated when doing refine with head points (below). - % They are in "initial SCS" coordinates, updated in channel_detect_type. - elseif all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) && (length(ChannelMat.SCS.NAS) == 3) && (length(ChannelMat.SCS.LPA) == 3) && (length(ChannelMat.SCS.RPA) == 3) - % Convert to MRI SCS coordinates. - % To do this we need to apply the transformation computed above. - sMri = sMriOld; - sMri.SCS.NAS = (DigToMriTransf(1:3,:) * [ChannelMat.SCS.NAS'; 1])'; - sMri.SCS.LPA = (DigToMriTransf(1:3,:) * [ChannelMat.SCS.LPA'; 1])'; - sMri.SCS.RPA = (DigToMriTransf(1:3,:) * [ChannelMat.SCS.RPA'; 1])'; - % Then convert to MRI coordinates (mm), this is how sMri.SCS is saved. - sMri.SCS.NAS = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.NAS) .* 1000; - sMri.SCS.LPA = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.LPA) .* 1000; - sMri.SCS.RPA = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.RPA) .* 1000; - % Re-compute transformation - [unused, sMri] = cs_compute(sMri, 'scs'); - - % Compare with existing MRI fids, replace if changed, and update surfaces. - sMri.FileName = sSubject.Anatomy(sSubject.iAnatomy).FileName; - figure_mri('SaveMri', sMri); - - % Adjust transformation from headpoints fit above. MRI SCS now matches digitized SCS (defined from same points). - DigToMriTransf = eye(4); - R = eye(3); - T = zeros(3,1); - end + DigToMriTransf = channel_align_scs(ChannelFile, DigToMriTransf, isWarning, isConfirm); + R = DigToMriTransf(1:3,1:3); + T = DigToMriTransf(1:3,4); end diff --git a/toolbox/sensors/channel_align_manual.m b/toolbox/sensors/channel_align_manual.m index 01103b1b5..99426d66e 100644 --- a/toolbox/sensors/channel_align_manual.m +++ b/toolbox/sensors/channel_align_manual.m @@ -231,7 +231,7 @@ % ===== DISPLAY MRI FIDUCIALS ===== % Get the fiducials positions defined in the MRI volume -sMri = load(file_fullpath(sSubject.Anatomy(sSubject.iAnatomy).FileName), 'SCS'); +sMri = load(file_fullpath(sSubject.Anatomy(sSubject.iAnatomy).FileName), 'SCS', 'History'); if ~isempty(sMri.SCS.NAS) && ~isempty(sMri.SCS.LPA) && ~isempty(sMri.SCS.RPA) % Convert coordinates MRI => SCS MriFidLoc = [cs_convert(sMri, 'mri', 'scs', sMri.SCS.NAS ./ 1000); ... @@ -364,7 +364,7 @@ gChanAlign.hButtonRefine = uipushtool(hToolbar, 'CData', java_geticon('ICON_ALIGN_CHANNELS'), 'TooltipString', 'Refine registration using head points', 'ClickedCallback', @RefineWithHeadPoints, 'separator', 'on'); gChanAlign.hButtonMoveChan = []; gChanAlign.hButtonProject = []; -else +else % isEeg gChanAlign.hButtonResizeX = uitoggletool(hToolbar, 'CData', java_geticon('ICON_RESIZE_X'), 'TooltipString', 'Resize/X: Press right button and move mouse up/down', 'ClickedCallback', @SelectOperation, 'separator', 'on'); gChanAlign.hButtonResizeY = uitoggletool(hToolbar, 'CData', java_geticon('ICON_RESIZE_Y'), 'TooltipString', 'Resize/Y: Press right button and move mouse up/down', 'ClickedCallback', @SelectOperation); gChanAlign.hButtonResizeZ = uitoggletool(hToolbar, 'CData', java_geticon('ICON_RESIZE_Z'), 'TooltipString', 'Resize/Z: Press right button and move mouse up/down', 'ClickedCallback', @SelectOperation); @@ -392,7 +392,8 @@ % else gChanAlign.hButtonAlign = []; % end -gChanAlign.hButtonOk = uipushtool( hToolbar, 'CData', java_geticon( 'ICON_OK'), 'separator', 'on', 'ClickedCallback', @buttonOk_Callback);% Update figure localization +gChanAlign.hButtonReset = uipushtool( hToolbar, 'CData', java_geticon('ICON_RELOAD'), 'separator', 'on', 'TooltipString', 'Reset: discard all changes', 'ClickedCallback', @buttonReset_Callback); +gChanAlign.hButtonOk = uipushtool( hToolbar, 'CData', java_geticon('ICON_OK'), 'TooltipString', 'Save & close', 'ClickedCallback', @buttonOk_Callback);% Update figure localization gui_layout('Update'); % Move a bit the figure to refresh it on all systems pos = get(gChanAlign.hFig, 'Position'); @@ -404,6 +405,60 @@ bst_progress('stop'); end +% Flag if auto or manual registration performed, and if MRI fids updated. Print to command +% window for now. +ChannelMat = in_bst_channel(ChannelFile); +iMriHist = find(strcmpi(sMri.History(:,3), 'Applied digitized anatomical fiducials'), 1, 'last'); +% Can also be reset, so check for 'import' action and ignore previous alignments. +iImport = find(strcmpi(ChannelMat.History(:,2), 'import'), 1, 'last'); +iAlign = find(strcmpi(ChannelMat.History(:,2), 'align')); +iAlign(iAlign < iImport) = []; +AlignType = 'none'; +while ~isempty(iAlign) + % Check which adjustment was done last. + switch lower(ChannelMat.History{iAlign(end),3}(1:5)) + case 'remov' + % Removed a previous step. Ignore corresponding adjustment and look again. + iAlign(end) = []; + if contains(ChannelMat.History{iAlign(end),3}, 'AdjustedNative') + iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); + elseif contains(ChannelMat.History{iAlign(end),3}, 'refine registration: head points') + iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); + elseif contains(ChannelMat.History{iAlign(end),3}, 'manual correction') + iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); + else + bst_error('Unrecognized removed transformation in history.'); + end + if isempty(iAlignRemoved) + bst_error('Missing removed transformation in history.'); + else + iAlign(iAlignRemoved) = []; + end + case 'added' + % This alignment is between points and functional dataset, ignore here. + iAlign(end) = []; + case 'refin' + % Automatic MRI-points alignment + AlignType = 'auto'; + break; + case 'align' + % Manual MRI-points alignment + AlignType = 'manual'; + break; + end +end +disp(['BST> Previous registration adjustment: ' AlignType]); +if ~isempty(iMriHist) + % Compare digitized fids to MRI fids (in MRI coordinates, mm). + if any(abs(sMri.SCS.NAS - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.NAS) .* 1000) > 1e-3) || ... + any(abs(sMri.SCS.LPA - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.LPA) .* 1000) > 1e-3) || ... + any(abs(sMri.SCS.RPA - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.RPA) .* 1000) > 1e-3) + disp('BST> MRI fiducials updated, but different than digitized fiducials.'); + else + disp('BST> MRI fiducials updated, and match digitized fiducials.'); + end +end + end @@ -646,12 +701,10 @@ function UpdatePoints(iSelChan) Dist = bst_surfdist(gChanAlign.HeadPointsMarkersLoc, ... get(gChanAlign.hSurfacePatch, 'Vertices'), get(gChanAlign.hSurfacePatch, 'Faces')); set(gChanAlign.hHeadPointsMarkers, 'CData', Dist * 1000); - % Update colorbar scale - ColormapInfo = getappdata(gChanAlign.hFig, 'Colormap'); - ColormapType = 'stat1'; - if strcmpi(ColormapInfo.Type, ColormapType) - bst_colormaps('ConfigureColorbar', gChanAlign.hFig, ColormapType, 'stat', 'mm'); - end + % Update surface data and colorbar + TessInfo = getappdata(gChanAlign.hFig, 'Surface'); + iTessPoints = find(strcmpi({TessInfo.Name}, 'HeadPoints')); + panel_surface('UpdateSurfaceData', gChanAlign.hFig, iTessPoints); end % Fiducials if ~isempty(gChanAlign.hHeadPointsFid) @@ -854,17 +907,52 @@ function AlignKeyPress_Callback(hFig, keyEvent) function AlignClose_Callback(varargin) global gChanAlign; if gChanAlign.isChanged + isCancel = false; % Ask user to save changes (only if called as a callback) if (nargin == 3) SaveChanged = 1; else - SaveChanged = java_dialog('confirm', ['The sensors locations changed.' 10 10 ... - 'Would you like to save changes? ' 10 10], 'Align sensors'); + % If head points present, offer to update MRI anat fids to match digitized ones. + if gChanAlign.isHeadPoints + [Choice, isCancel] = java_dialog('question', ['The sensors locations changed.' 10 ... + 'Would you like to save changes?' 10 10], 'Align sensors', [], {'Yes', 'Update MRI', 'No'}, 'Yes'); + if strcmpi(Choice, 'Yes') + SaveChanged = 1; + else + SaveChanged = 0; + end + if strcmpi(Choice, 'Update MRI') + % If EEG, warn that only linear transformation would be saved this way. + if gChanAlign.isEeg + [Proceed, isCancel] = java_dialog('confirm', ['Updating the MRI fiducial points NAS/LPA/RPA will only save' 10 ... + 'global rotations and translations. Any other changes to EEG channels will be lost.' 10 10 ... + 'Proceed and update MRI now?' 10], 'Head points/anatomy registration'); + if ~Proceed || isCancel + isCancel = true; + end + end + if ~isCancel + % Get final transformation matrix + Transform = eye(4); + Transform(1:3,1:3) = gChanAlign.FinalTransf(1:3,1:3); + Transform(1:3,4) = gChanAlign.FinalTransf(1:3,4); + % Update MRI (and surfaces) + [~, isCancel] = channel_align_scs(gChanAlign.ChannelFile, Transform, 1, 1); % warn & confirm + end + end + else % no head points + [SaveChanged, isCancel] = java_dialog('confirm', ['The sensors locations changed.' 10 10 ... + 'Would you like to save changes? ' 10 10], 'Align sensors'); + end end - % Progress bar - bst_progress('start', 'Align sensors', 'Updating channel file...'); - % Save changes and close figure + % Don't close figure if cancelled. + if isCancel + return; + end + % Save changes to channel file and close figure if SaveChanged + % Progress bar + bst_progress('start', 'Align sensors', 'Updating channel file...'); % Restore standard close callback for 3DViz figures set(gChanAlign.hFig, 'CloseRequestFcn', gChanAlign.Figure3DCloseRequest_Bak); drawnow; @@ -878,14 +966,14 @@ function AlignClose_Callback(varargin) [sStudy, iStudy] = bst_get('ChannelFile', gChanAlign.ChannelFile); % Reload study file db_reload_studies(iStudy); + bst_progress('stop'); end - bst_progress('stop'); else SaveChanged = 0; end % Only close figure gChanAlign.Figure3DCloseRequest_Bak(varargin{1:2}); - % Apply to other recordings in the same subject + % Apply to other recordings with same sensor locations in the same subject if SaveChanged CopyToOtherFolders(ChannelMatOrig, iStudy, Transf, iChannels); end @@ -930,7 +1018,7 @@ function CopyToOtherFolders(ChannelMatSrc, iStudySrc, Transf, iChannels) end % Check if the positions of the sensors are similar distLoc = sqrt((locDest(1,:) - locSrc(1,:)).^2 + (locDest(2,:) - locSrc(2,:)).^2 + (locDest(3,:) - locSrc(3,:)).^2); - % If the sensors are more than 5mm apart in average: skip + % If any sensors are more than 5mm apart: skip if any(distLoc > 0.005) continue; end @@ -1153,13 +1241,21 @@ function RefineWithHeadPoints(varargin) end -%% ===== VALIDATION BUTTONS ===== +%% ===== VALIDATION BUTTON ===== function buttonOk_Callback(varargin) global gChanAlign; % Close 3DViz figure close(gChanAlign.hFig); end +%% ===== RESET BUTTON ===== +function buttonReset_Callback(varargin) + global gChanAlign; + % Close figure + gChanAlign.Figure3DCloseRequest_Bak(gChanAlign.hFig, []); + % Call function again, which resets gChanAlign. + channel_align_manual(gChanAlign.ChannelFile, gChanAlign.Modality, 1); +end %% ===== REMOVE ELECTRODES ===== function RemoveElectrodes(varargin) diff --git a/toolbox/sensors/channel_align_scs.m b/toolbox/sensors/channel_align_scs.m new file mode 100644 index 000000000..14a439079 --- /dev/null +++ b/toolbox/sensors/channel_align_scs.m @@ -0,0 +1,122 @@ +function [Transform, isCancel] = channel_align_scs(ChannelFile, Transform, isWarning, isConfirm) +% CHANNEL_ALIGN_SCS: Saves new MRI anatomical points after manual or auto registration adjustment. +% +% USAGE: Transform = channel_align_scs(ChannelFile, isWarning=1, isConfirm=1) +% +% DESCRIPTION: +% After modifying registration between digitized head points and MRI (with "refine with head +% points" or manually), this function allows saving the change in the MRI fiducials so that +% they exactly match the digitized anatomical points (nasion and ears), instead of saving a +% registration adjustment transformation for a single functional dataset. This affects all +% files registered to the MRI and should therefore be done as one of the first steps after +% importing, and with only one set of digitized points (one session). Surfaces are adjusted to +% maintain alignment with the MRI. Additional sessions for the same subject, with separate +% digitized points, will still need the usual "per dataset" registration adjustment to align +% with the same MRI. +% +% This function will not modify an MRI that it changed previously without user confirmation +% (if both isWarning and isConfirm are false). In that case, the Transform is returned unaltered. +% +% INPUTS: +% - ChannelFile : Channel file to align with its anatomy +% - Transform : Transformation matrix from digitized SCS coordinates to MRI SCS coordinates, +% after some alignment is made (auto or manual) and the two no longer match. +% - isWarning : If 1, display warning in case of errors, or if this was already done +% previously for this MRI. +% - isConfirm : If 1, ask the user for confirmation before proceeding. +% +% OUTPUTS: +% - Transform : If the MRI fiducial points and coordinate system are updated, the transform +% becomes the identity. If not, it is the same as the input Transform. + +% @============================================================================= +% This function is part of the Brainstorm software: +% https://neuroimage.usc.edu/brainstorm +% +% Copyright (c) University of Southern California & McGill University +% This software is distributed under the terms of the GNU General Public License +% as published by the Free Software Foundation. Further details on the GPLv3 +% license can be found at http://www.gnu.org/copyleft/gpl.html. +% +% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE +% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY +% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY +% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. +% +% For more information type "brainstorm license" at command prompt. +% =============================================================================@ +% +% Authors: Marc Lalancette 2022 + +isCancel = false; +% Get study +sStudy = bst_get('ChannelFile', ChannelFile); +% Get subject +[sSubject, iSubject] = bst_get('Subject', sStudy.BrainStormSubject); +% Get Channels +ChannelMat = in_bst_channel(ChannelFile); + +% Check if digitized anat points present, saved in ChannelMat.SCS. +% Note that these coordinates are NOT currently updated when doing refine with head points (below). +% They are in "initial SCS" coordinates, updated in channel_detect_type. +if ~all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) || ~(length(ChannelMat.SCS.NAS) == 3) || ~(length(ChannelMat.SCS.LPA) == 3) || ~(length(ChannelMat.SCS.RPA) == 3) + if isWarning + bst_error('Digitized nasion and ear points not found.', 'Apply digitized anatomical fiducials to MRI', 0); + else + disp('BST> Digitized nasion and ear points not found.'); + end + isCancel = true; + return; +end + +% Check if already adjusted. + sMriOld = in_mri_bst(sSubject.Anatomy(sSubject.iAnatomy).FileName); + % History string is set in figure_mri SaveMri. + if isfield(sMriOld, 'History') && ~isempty(sMriOld.History) && any(strcmpi(sMriOld.History(:,3), 'Applied digitized anatomical fiducials')) + % Already done previously. + if isWarning || isConfirm + % Request confirmation. + [Proceed, isCancel] = java_dialog('confirm', ['The MRI fiducial points NAS/LPA/RPA were previously updated from a set of' 10 ... + 'aligned digitized points. Updating them again will break any previous alignment' 10 ... + 'with other sets of digitized points and associated functional datasets.' 10 10 ... + 'Proceed and overwrite previous alignment?' 10], 'Head points/anatomy registration'); + if ~Proceed || isCancel + isCancel = true; + return; + end + else + % Do not proceed. + disp('BST> Digitized nasion and ear points previously applied to this MRI. Not applying again.'); + return; + end + elseif isConfirm + % Request confirmation. + [Proceed, isCancel] = java_dialog('confirm', ['Updating the MRI fiducial points NAS/LPA/RPA will break any' 10 ... + 'previous alignment with functional datasets.' 10 10 ... + 'Proceed and update MRI now?' 10], 'Head points/anatomy registration'); + if ~Proceed || isCancel + isCancel = true; + return; + end + end + % Convert to MRI SCS coordinates. + % To do this we need to apply the transformation provided. + sMri = sMriOld; + sMri.SCS.NAS = (Transform(1:3,:) * [ChannelMat.SCS.NAS'; 1])'; + sMri.SCS.LPA = (Transform(1:3,:) * [ChannelMat.SCS.LPA'; 1])'; + sMri.SCS.RPA = (Transform(1:3,:) * [ChannelMat.SCS.RPA'; 1])'; + % Then convert to MRI coordinates (mm), this is how sMri.SCS is saved. + sMri.SCS.NAS = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.NAS) .* 1000; + sMri.SCS.LPA = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.LPA) .* 1000; + sMri.SCS.RPA = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.RPA) .* 1000; + % Re-compute transformation + [unused, sMri] = cs_compute(sMri, 'scs'); + + % Compare with existing MRI fids, replace if changed (> 1um), and update surfaces. + sMri.FileName = sSubject.Anatomy(sSubject.iAnatomy).FileName; + figure_mri('SaveMri', sMri); + + % Adjust transformation. MRI SCS now matches digitized SCS (defined from same points). + Transform = eye(4); +end diff --git a/toolbox/tree/tree_callbacks.m b/toolbox/tree/tree_callbacks.m index 5d3fd6e87..fc75049fa 100644 --- a/toolbox/tree/tree_callbacks.m +++ b/toolbox/tree/tree_callbacks.m @@ -224,6 +224,8 @@ DisplayChannels(bstNodes, DisplayMod{1}, 'anatomy', 1); elseif strcmpi(DisplayMod{1}, 'ECOG') DisplayChannels(bstNodes, DisplayMod{1}, 'cortex', 1); + elseif ismember(DisplayMod{1}, {'MEG','MEG GRAD','MEG MAG'}) + channel_align_manual(filenameRelative, DisplayMod{1}, 0) elseif strcmpi(DisplayMod{1}, 'NIRS') DisplayChannels(bstNodes, 'NIRS-BRS', 'scalp', [], 1); else From a91ac60d390fc93670d386aa137a461f4cf058a8 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Fri, 18 Nov 2022 17:14:43 -0500 Subject: [PATCH 23/32] fix head point distance coloring & add manual registration reset button --- toolbox/core/bst_colormaps.m | 28 +++---- toolbox/gui/figure_3d.m | 58 ++++---------- toolbox/gui/panel_surface.m | 87 ++++++++++++++++++--- toolbox/sensors/channel_align_manual.m | 104 ++++++++++++++++++++----- 4 files changed, 188 insertions(+), 89 deletions(-) diff --git a/toolbox/core/bst_colormaps.m b/toolbox/core/bst_colormaps.m index ce73922b9..47d27ef65 100644 --- a/toolbox/core/bst_colormaps.m +++ b/toolbox/core/bst_colormaps.m @@ -376,18 +376,20 @@ function SetMaxCustom(ColormapType, DisplayUnits, newMin, newMax) case {'3DViz', 'MriViewer'} % Get surfaces defined in this figure TessInfo = getappdata(sFigure.hFigure, 'Surface'); - DataFig = TessInfo.DataMinMax; - if ~isempty(TessInfo.DataSource.Type) - DataType = TessInfo.DataSource.Type; - isSLORETA = strcmpi(DataType, 'Source') && ~isempty(strfind(lower(TessInfo.DataSource.FileName), 'sloreta')); - if isSLORETA - DataType = 'sLORETA'; - end - elseif isempty(DataFig) - % If displaying color-coded head points (see channel_align_manual) - HeadpointsDistMax = getappdata(sFigure.hFigure, 'HeadpointsDistMax'); - if ~isempty(HeadpointsDistMax) - DataFig = [0, HeadpointsDistMax * 1000]; + % Find surface(s) that matches this ColormapType + iSurfaces = find(strcmpi({TessInfo.ColormapType}, ColormapType)); + DataFig = []; + for i = 1:length(iSurfaces) + iTess = iSurfaces(i); + DataFig = [min([DataFig(:); TessInfo(iTess).DataMinMax(:)]), ... + max([DataFig(:); TessInfo(iTess).DataMinMax(:)])]; + if ~isempty(TessInfo(iTess).DataSource.Type) + % We'll keep the last non-empty DataType + DataType = TessInfo(iTess).DataSource.Type; + isSLORETA = strcmpi(DataType, 'Source') && ~isempty(strfind(lower(TessInfo(iTess).DataSource.FileName), 'sloreta')); + if isSLORETA + DataType = 'sLORETA'; + end end end @@ -452,7 +454,7 @@ function SetMaxCustom(ColormapType, DisplayUnits, newMin, newMax) elseif isequal(DisplayUnits, '%') || isequal(DisplayUnits, 'mm') fFactor = 1; fUnits = DisplayUnits; - else + else % Guess the display units [tmp, fFactor, fUnits ] = bst_getunits(amplitudeMax, DataType); % For readability: replace '\sigma' with 'no units' diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index eeb1c3baf..19184d41f 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -169,11 +169,6 @@ function ColormapChangedCallback(iDS, iFig) %#ok if ~isempty(getappdata(hFig, 'Dipoles')) && gui_brainstorm('isTabVisible', 'Dipoles') panel_dipoles('PlotSelectedDipoles', hFig); end - % If displaying color-coded head points (see channel_align_manual) - HeadpointsDistMax = getappdata(hFig, 'HeadpointsDistMax'); - if ~isempty(HeadpointsDistMax) - UpdateHeadPointsColormap(hFig); - end end @@ -3772,7 +3767,7 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) [HeadPoints.Loc(1,iDupli), HeadPoints.Loc(2,iDupli), HeadPoints.Loc(3,iDupli)] = sph2cart(th, phi, r - 0.0001); end - % Else, get previous head points + % Look for previous head points hHeadPointsMarkers = findobj(hAxes, 'Tag', 'HeadPointsMarkers'); hHeadPointsLabels = findobj(hAxes, 'Tag', 'HeadPointsLabels'); % If head points graphic objects already exist: set the "Visible" property @@ -3787,22 +3782,19 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) ColormapType = 'stat1'; if isColorDist && ~strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat') % Color points according to distance to surface - % Get selected surface - [iTess, TessInfo, hFig, sSurf] = panel_surface('GetSelectedSurface', hFig); + % Get scalp surface + [~, ~, hFig, sSurf] = panel_surface('GetSurface', hFig, '', 'Scalp'); if ~isempty(sSurf) && isfield(sSurf, 'Vertices') && ~isempty(sSurf.Vertices) - if ~ismember(ColormapType, ColormapInfo.AllTypes) - % Add missing colormap (color was toggled after points were displayed) - bst_colormaps('AddColormapToFigure', hFig, ColormapType, 'mm'); - ColormapInfo = getappdata(hFig, 'Colormap'); - end % Compute the distance Dist = bst_surfdist(get(hHeadPointsMarkers, 'Vertices'), sSurf.Vertices, sSurf.Faces); set(hHeadPointsMarkers, 'CData', Dist * 1000, ... 'MarkerFaceColor', 'flat', 'MarkerEdgeColor', 'flat'); - setappdata(hFig, 'HeadpointsDistMax', max(Dist)); - if strcmpi(ColormapInfo.Type, ColormapType) - ColormapChangedCallback(iDS, iFig); - bst_colormaps('SetColorbarVisible', hFig, 1); + if ~ismember(ColormapType, ColormapInfo.AllTypes) + iTess = panel_surface('GetSurface', hFig, '', 'HeadPoints'); + if isempty(iTess) + error('HeadPoints surface not found.'); + end + panel_surface('SetSurfaceData', hFig, iTess, 'HeadPointsDistance', '', 1); % isStat=1 end end elseif ~isColorDist && strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat') @@ -3890,15 +3882,13 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) if ~isempty(iExtra) % Color code points by distance if isColorDist - % Get selected surface - [iTess, TessInfo, hFig, sSurf] = panel_surface('GetSelectedSurface', hFig); + % Get scalp surface + [~, ~, hFig, sSurf] = panel_surface('GetSurface', hFig, '', 'Scalp'); % Compute the distance Dist = bst_surfdist(digLoc(iExtra, :), sSurf.Vertices, sSurf.Faces); CData = Dist * 1000; % mm - setappdata(hFig, 'HeadpointsDistMax', max(Dist)); MarkerFaceColor = 'flat'; MarkerEdgeColor = 'flat'; - bst_colormaps('AddColormapToFigure', hFig, 'stat1', 'mm'); else CData = 'w'; % any color, not displayed MarkerFaceColor = [.3 1 .3]; @@ -3915,36 +3905,16 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) 'Marker', 'o', ... 'UserData', iExtra, ... 'Tag', 'HeadPointsMarkers'); + % Add points patch to figure surfaces so all color bar functionality work. + iTess = panel_surface('AddSurface', hFig, '', 'HeadPoints'); if isColorDist - ColormapChangedCallback(iDS, iFig); + panel_surface('SetSurfaceData', hFig, iTess, 'HeadPointsDistance', '', 1); % isStat=1 end end end end -%% ===== UPDATE HEADPOINTS COLORMAP ===== -function UpdateHeadPointsColormap(hFig) - % If not using color-coded display - hHeadPointsMarkers = findobj(hFig, 'Tag', 'HeadPointsMarkers'); - if ~strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat') - return; - end - % Get colormap configuration - sColormap = bst_colormaps('GetColormap', 'stat1'); - % Update axes color limits, which will update de colorbar - hAxes = get(hHeadPointsMarkers, 'Parent'); - if strcmpi(sColormap.MaxMode, 'custom') - set(hAxes, 'CLim', [sColormap.MinValue, sColormap.MaxValue]); - else - HeadpointsDistMax = getappdata(hFig, 'HeadpointsDistMax'); - set(hAxes, 'CLim', [0, HeadpointsDistMax * 1000]); - end - % Update colorbar - bst_colormaps('ConfigureColorbar', hFig, 'stat1', 'stat', 'mm'); -end - - %% ===== VIEW AXIS ===== function ViewAxis(hFig, isVisible) hAxes = findobj(hFig, '-depth', 1, 'Tag', 'Axes3D'); diff --git a/toolbox/gui/panel_surface.m b/toolbox/gui/panel_surface.m index 7460f5c24..36adc98ca 100644 --- a/toolbox/gui/panel_surface.m +++ b/toolbox/gui/panel_surface.m @@ -1130,15 +1130,20 @@ function UpdateSurfaceProperties() %% ===== ADD A SURFACE ===== % Add a surface to a given 3DViz figure % USAGE : [iTess, TessInfo] = panel_surface('AddSurface', hFig, surfaceFile) +% [iTess, TessInfo] = panel_surface('AddSurface', hFig, [], surfaceType) % OUTPUT: Indice of the surface in the figure's surface array -function [iTess, TessInfo] = AddSurface(hFig, surfaceFile) +function [iTess, TessInfo] = AddSurface(hFig, surfaceFile, surfaceType) % ===== CHECK EXISTENCE ===== - % Check whether filename is an absolute or relative path - surfaceFile = file_short(surfaceFile); % Get figure appdata (surfaces configuration) TessInfo = getappdata(hFig, 'Surface'); % Check that this surface is not already displayed in 3DViz figure - iTess = find(file_compare({TessInfo.SurfaceFile}, surfaceFile)); + if isempty(surfaceFile) && nargin > 2 + iTess = find(file_compare({TessInfo.Name}, surfaceType)); + else + % Check whether filename is an absolute or relative path + surfaceFile = file_short(surfaceFile); + iTess = find(file_compare({TessInfo.SurfaceFile}, surfaceFile)); + end if ~isempty(iTess) disp('BST> This surface is already displayed. Ignoring...'); return @@ -1161,6 +1166,9 @@ function UpdateSurfaceProperties() % ===== PLOT OBJECT ===== % Get file type (tessalation or MRI) fileType = file_gettype(surfaceFile); + if strcmpi(fileType, 'unknown') && nargin > 2 + fileType = surfaceType; + end % === TESSELATION === if any(strcmpi(fileType, {'cortex','scalp','innerskull','outerskull','tess'})) % === LOAD SURFACE === @@ -1291,8 +1299,18 @@ function UpdateSurfaceProperties() setappdata(hFig, 'Surface', TessInfo); end + % === NO FILE: HeadPoints === + elseif strcmpi(fileType, 'HeadPoints') + % Points were already displayed; just add the patch to the figure surfaces. + TessInfo(iTess).DataSource.Type = 'HeadPointsDistance'; + TessInfo(iTess).Name = 'HeadPoints'; + TessInfo(iTess).ColormapType = 'stat1'; + TessInfo(iTess).hPatch = findobj(hFig, 'Tag', 'HeadPointsMarkers'); + % Update figure's surfaces list and current surface pointer + setappdata(hFig, 'Surface', TessInfo); + % === FEM === - else + else % TODO: Check for FEM fileType explicitly view_surface_fem(surfaceFile, [], [], [], hFig); end % Update default surface @@ -1496,6 +1514,11 @@ function UpdateSurfaceProperties() DisplayUnits = []; TessInfo(iTess).Data = []; TessInfo(iTess).DataWmat = []; + + case 'HeadPointsDistance' + ColormapType = 'stat1'; + DisplayUnits = 'mm'; + % Data is actually added in UpdateSurfaceData below. otherwise ColormapType = ''; @@ -1878,6 +1901,15 @@ function UpdateSurfaceProperties() figure_callback(hFig, 'UpdateSurfaceColor', hFig, iTess); % Get updated surface definition TessInfo = getappdata(hFig, 'Surface'); + + case 'HeadPointsDistance' + TessInfo(iTess).Data = get(TessInfo(iTess).hPatch, 'CData'); + if isempty(TessInfo(iTess).Data) || ~isnumeric(TessInfo(iTess).Data) + isOk = 0; + return; + end + TessInfo(iTess).DataMinMax = [min(TessInfo(iTess).Data(:)), max(TessInfo(iTess).Data(:))]; + otherwise % Nothing to do end @@ -2001,7 +2033,7 @@ function UpdateSurfaceColormap(hFig, iSurfaces) TessInfo(iTess).Data = abs(TessInfo(iTess).Data); end % If current colormap is the default colormap for this figure (for colorbar) - if strcmpi(ColormapInfo.Type, TessInfo(iTess).ColormapType) && ~isempty(TessInfo(iTess).DataSource.FileName) + if strcmpi(ColormapInfo.Type, TessInfo(iTess).ColormapType) && ~isempty(TessInfo(iTess).DataSource.Type) if all(~isnan(TessInfo(iTess).DataLimitValue)) && (TessInfo(iTess).DataLimitValue(1) < TessInfo(iTess).DataLimitValue(2)) set(hAxes, 'CLim', TessInfo(iTess).DataLimitValue); else @@ -2017,7 +2049,8 @@ function UpdateSurfaceColormap(hFig, iSurfaces) end end % === DISPLAY ON MRI === - if strcmpi(TessInfo(iTess).Name, 'Anatomy') && ~isempty(TessInfo(iTess).DataSource.Type) && ~strcmpi(TessInfo(iTess).DataSource.Type, 'MriTime') && isempty(TessInfo(iTess).OverlayCube) + if strcmpi(TessInfo(iTess).Name, 'Anatomy') && ~isempty(TessInfo(iTess).DataSource.Type) && ... + ~strcmpi(TessInfo(iTess).DataSource.Type, 'MriTime') && isempty(TessInfo(iTess).OverlayCube) % Progress bar isProgressBar = bst_progress('isVisible'); bst_progress('start', 'Display MRI', 'Updating values...'); @@ -2029,6 +2062,10 @@ function UpdateSurfaceColormap(hFig, iSurfaces) if ~isProgressBar bst_progress('stop'); end + elseif strcmpi(TessInfo(iTess).Name, 'HeadPoints') + % No need to update surface color here, data already updated. + % Update figure's appdata (surface list) + setappdata(hFig, 'Surface', TessInfo); else % Update figure's appdata (surface list) setappdata(hFig, 'Surface', TessInfo); @@ -2047,13 +2084,39 @@ function UpdateSurfaceColormap(hFig, iSurfaces) %% ===== GET SURFACE ===== % Find a surface in a given 3DViz figure -function iTess = GetSurface(hFig, SurfaceFile) - % Check whether filename is an absolute or relative path - SurfaceFile = file_short(SurfaceFile); +% Usage: [iTess, TessInfo, hFig, sSurf] = GetSurface(hFig, SurfaceFile) +% [iTess, TessInfo, hFig, sSurf] = GetSurface(hFig, [], SurfaceType) +function [iTess, TessInfo, hFig, sSurf] = GetSurface(hFig, SurfaceFile, SurfaceType) + iTess = []; + sSurf = []; % Get figure appdata (surfaces configuration) TessInfo = getappdata(hFig, 'Surface'); - % Find the surface in the 3DViz figure - iTess = find(file_compare({TessInfo.SurfaceFile}, SurfaceFile)); + if nargin < 3 || (isempty(SurfaceType) && isempty(SurfaceFile)) + return; + end + if isempty(SurfaceFile) + % Search by type. + iTess = find(strcmpi({TessInfo.Name}, SurfaceType)); + if isempty(iTess) + return; + elseif numel(iTess) > 1 + % See if selected is one of them, otherwise return last. + iTessSel = getappdata(hFig, 'iSurface'); + if ismember(iTessSel, iTess) + iTess = iTessSel; + else + iTess = iTess(end); + end + end + else + % Check whether filename is an absolute or relative path + SurfaceFile = file_short(SurfaceFile); + % Find the surface in the 3DViz figure + iTess = find(file_compare({TessInfo.SurfaceFile}, SurfaceFile)); + end + if (nargout >= 4) && ~isempty(TessInfo) && ~isempty(iTess) + sSurf = bst_memory('GetSurface', TessInfo(iTess).SurfaceFile); + end end diff --git a/toolbox/sensors/channel_align_manual.m b/toolbox/sensors/channel_align_manual.m index c4b5e5cef..0b1fce4d0 100644 --- a/toolbox/sensors/channel_align_manual.m +++ b/toolbox/sensors/channel_align_manual.m @@ -231,7 +231,7 @@ % ===== DISPLAY MRI FIDUCIALS ===== % Get the fiducials positions defined in the MRI volume -sMri = load(file_fullpath(sSubject.Anatomy(sSubject.iAnatomy).FileName), 'SCS'); +sMri = load(file_fullpath(sSubject.Anatomy(sSubject.iAnatomy).FileName), 'SCS', 'History'); if ~isempty(sMri.SCS.NAS) && ~isempty(sMri.SCS.LPA) && ~isempty(sMri.SCS.RPA) % Convert coordinates MRI => SCS MriFidLoc = [cs_convert(sMri, 'mri', 'scs', sMri.SCS.NAS ./ 1000); ... @@ -364,7 +364,7 @@ gChanAlign.hButtonRefine = uipushtool(hToolbar, 'CData', java_geticon('ICON_ALIGN_CHANNELS'), 'TooltipString', 'Refine registration using head points', 'ClickedCallback', @RefineWithHeadPoints, 'separator', 'on'); gChanAlign.hButtonMoveChan = []; gChanAlign.hButtonProject = []; -else +else % isEeg gChanAlign.hButtonResizeX = uitoggletool(hToolbar, 'CData', java_geticon('ICON_RESIZE_X'), 'TooltipString', 'Resize/X: Press right button and move mouse up/down', 'ClickedCallback', @SelectOperation, 'separator', 'on'); gChanAlign.hButtonResizeY = uitoggletool(hToolbar, 'CData', java_geticon('ICON_RESIZE_Y'), 'TooltipString', 'Resize/Y: Press right button and move mouse up/down', 'ClickedCallback', @SelectOperation); gChanAlign.hButtonResizeZ = uitoggletool(hToolbar, 'CData', java_geticon('ICON_RESIZE_Z'), 'TooltipString', 'Resize/Z: Press right button and move mouse up/down', 'ClickedCallback', @SelectOperation); @@ -392,7 +392,8 @@ % else gChanAlign.hButtonAlign = []; % end -gChanAlign.hButtonOk = uipushtool( hToolbar, 'CData', java_geticon( 'ICON_OK'), 'separator', 'on', 'ClickedCallback', @buttonOk_Callback);% Update figure localization +gChanAlign.hButtonReset = uipushtool( hToolbar, 'CData', java_geticon('ICON_RELOAD'), 'separator', 'on', 'TooltipString', 'Reset: discard all changes', 'ClickedCallback', @buttonReset_Callback); +gChanAlign.hButtonOk = uipushtool( hToolbar, 'CData', java_geticon('ICON_OK'), 'TooltipString', 'Save & close', 'ClickedCallback', @buttonOk_Callback);% Update figure localization gui_layout('Update'); % Move a bit the figure to refresh it on all systems pos = get(gChanAlign.hFig, 'Position'); @@ -404,6 +405,60 @@ bst_progress('stop'); end +% Flag if auto or manual registration performed, and if MRI fids updated. Print to command +% window for now. +ChannelMat = in_bst_channel(ChannelFile); +iMriHist = find(strcmpi(sMri.History(:,3), 'Applied digitized anatomical fiducials'), 1, 'last'); +% Can also be reset, so check for 'import' action and ignore previous alignments. +iImport = find(strcmpi(ChannelMat.History(:,2), 'import'), 1, 'last'); +iAlign = find(strcmpi(ChannelMat.History(:,2), 'align')); +iAlign(iAlign < iImport) = []; +AlignType = 'none'; +while ~isempty(iAlign) + % Check which adjustment was done last. + switch lower(ChannelMat.History{iAlign(end),3}(1:5)) + case 'remov' + % Removed a previous step. Ignore corresponding adjustment and look again. + iAlign(end) = []; + if contains(ChannelMat.History{iAlign(end),3}, 'AdjustedNative') + iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); + elseif contains(ChannelMat.History{iAlign(end),3}, 'refine registration: head points') + iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); + elseif contains(ChannelMat.History{iAlign(end),3}, 'manual correction') + iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); + else + bst_error('Unrecognized removed transformation in history.'); + end + if isempty(iAlignRemoved) + bst_error('Missing removed transformation in history.'); + else + iAlign(iAlignRemoved) = []; + end + case 'added' + % This alignment is between points and functional dataset, ignore here. + iAlign(end) = []; + case 'refin' + % Automatic MRI-points alignment + AlignType = 'auto'; + break; + case 'align' + % Manual MRI-points alignment + AlignType = 'manual'; + break; + end +end +disp(['BST> Previous registration adjustment: ' AlignType]); +if ~isempty(iMriHist) + % Compare digitized fids to MRI fids (in MRI coordinates, mm). + if any(abs(sMri.SCS.NAS - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.NAS) .* 1000) > 1e-3) || ... + any(abs(sMri.SCS.LPA - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.LPA) .* 1000) > 1e-3) || ... + any(abs(sMri.SCS.RPA - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.RPA) .* 1000) > 1e-3) + disp('BST> MRI fiducials updated, but different than digitized fiducials.'); + else + disp('BST> MRI fiducials updated, and match digitized fiducials.'); + end +end + end @@ -646,15 +701,10 @@ function UpdatePoints(iSelChan) Dist = bst_surfdist(gChanAlign.HeadPointsMarkersLoc, ... get(gChanAlign.hSurfacePatch, 'Vertices'), get(gChanAlign.hSurfacePatch, 'Faces')); set(gChanAlign.hHeadPointsMarkers, 'CData', Dist * 1000); - % Update axes maximum - setappdata(gChanAlign.hFig, 'HeadpointsDistMax', max(Dist)); - figure_3d('UpdateHeadPointsColormap', gChanAlign.hFig); - % Update colorbar scale - ColormapInfo = getappdata(gChanAlign.hFig, 'Colormap'); - ColormapType = 'stat1'; - if strcmpi(ColormapInfo.Type, ColormapType) - bst_colormaps('ConfigureColorbar', gChanAlign.hFig, ColormapType, 'stat', 'mm'); - end + % Update surface data and colorbar + TessInfo = getappdata(gChanAlign.hFig, 'Surface'); + iTessPoints = find(strcmpi({TessInfo.Name}, 'HeadPoints')); + panel_surface('UpdateSurfaceData', gChanAlign.hFig, iTessPoints); end % Fiducials if ~isempty(gChanAlign.hHeadPointsFid) @@ -857,17 +907,23 @@ function AlignKeyPress_Callback(hFig, keyEvent) function AlignClose_Callback(varargin) global gChanAlign; if gChanAlign.isChanged + isCancel = false; % Ask user to save changes (only if called as a callback) if (nargin == 3) SaveChanged = 1; else SaveChanged = java_dialog('confirm', ['The sensors locations changed.' 10 10 ... - 'Would you like to save changes? ' 10 10], 'Align sensors'); + 'Would you like to save changes? ' 10 10], 'Align sensors'); + end end - % Progress bar - bst_progress('start', 'Align sensors', 'Updating channel file...'); - % Save changes and close figure + % Don't close figure if cancelled. + if isCancel + return; + end + % Save changes to channel file and close figure if SaveChanged + % Progress bar + bst_progress('start', 'Align sensors', 'Updating channel file...'); % Restore standard close callback for 3DViz figures set(gChanAlign.hFig, 'CloseRequestFcn', gChanAlign.Figure3DCloseRequest_Bak); drawnow; @@ -881,14 +937,14 @@ function AlignClose_Callback(varargin) [sStudy, iStudy] = bst_get('ChannelFile', gChanAlign.ChannelFile); % Reload study file db_reload_studies(iStudy); + bst_progress('stop'); end - bst_progress('stop'); else SaveChanged = 0; end % Only close figure gChanAlign.Figure3DCloseRequest_Bak(varargin{1:2}); - % Apply to other recordings in the same subject + % Apply to other recordings with same sensor locations in the same subject if SaveChanged CopyToOtherFolders(ChannelMatOrig, iStudy, Transf, iChannels); end @@ -933,7 +989,7 @@ function CopyToOtherFolders(ChannelMatSrc, iStudySrc, Transf, iChannels) end % Check if the positions of the sensors are similar distLoc = sqrt((locDest(1,:) - locSrc(1,:)).^2 + (locDest(2,:) - locSrc(2,:)).^2 + (locDest(3,:) - locSrc(3,:)).^2); - % If the sensors are more than 5mm apart in average: skip + % If any sensors are more than 5mm apart: skip if any(distLoc > 0.005) continue; end @@ -1156,13 +1212,21 @@ function RefineWithHeadPoints(varargin) end -%% ===== VALIDATION BUTTONS ===== +%% ===== VALIDATION BUTTON ===== function buttonOk_Callback(varargin) global gChanAlign; % Close 3DViz figure close(gChanAlign.hFig); end +%% ===== RESET BUTTON ===== +function buttonReset_Callback(varargin) + global gChanAlign; + % Close figure + gChanAlign.Figure3DCloseRequest_Bak(gChanAlign.hFig, []); + % Call function again, which resets gChanAlign. + channel_align_manual(gChanAlign.ChannelFile, gChanAlign.Modality, 1); +end %% ===== REMOVE ELECTRODES ===== function RemoveElectrodes(varargin) From ea3ea6d46b4043dfaa84ea5d5c09e8292aaa3628 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Fri, 18 Nov 2022 17:42:15 -0500 Subject: [PATCH 24/32] small code cleaning --- toolbox/core/bst_colormaps.m | 13 ++++++++----- toolbox/gui/panel_surface.m | 2 +- toolbox/sensors/channel_align_manual.m | 1 - 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/toolbox/core/bst_colormaps.m b/toolbox/core/bst_colormaps.m index 47d27ef65..24ad47480 100644 --- a/toolbox/core/bst_colormaps.m +++ b/toolbox/core/bst_colormaps.m @@ -381,13 +381,16 @@ function SetMaxCustom(ColormapType, DisplayUnits, newMin, newMax) DataFig = []; for i = 1:length(iSurfaces) iTess = iSurfaces(i); - DataFig = [min([DataFig(:); TessInfo(iTess).DataMinMax(:)]), ... - max([DataFig(:); TessInfo(iTess).DataMinMax(:)])]; if ~isempty(TessInfo(iTess).DataSource.Type) + DataFig = [min([DataFig(:); TessInfo(iTess).DataMinMax(:)]), ... + max([DataFig(:); TessInfo(iTess).DataMinMax(:)])]; % We'll keep the last non-empty DataType DataType = TessInfo(iTess).DataSource.Type; - isSLORETA = strcmpi(DataType, 'Source') && ~isempty(strfind(lower(TessInfo(iTess).DataSource.FileName), 'sloreta')); - if isSLORETA + % For Data: use the modality instead + if strcmpi(DataType, 'Data') && ~isempty(ColormapInfo.Type) && ismember(ColormapInfo.Type, {'eeg', 'meg', 'nirs'}) + DataType = upper(ColormapInfo.Type); + % sLORETA: Do not use regular source scaling (pAm) + elseif strcmpi(DataType, 'Source') && ~isempty(strfind(lower(TessInfo(iTess).DataSource.FileName), 'sloreta')) DataType = 'sLORETA'; end end @@ -454,7 +457,7 @@ function SetMaxCustom(ColormapType, DisplayUnits, newMin, newMax) elseif isequal(DisplayUnits, '%') || isequal(DisplayUnits, 'mm') fFactor = 1; fUnits = DisplayUnits; - else + else % Guess the display units [tmp, fFactor, fUnits ] = bst_getunits(amplitudeMax, DataType); % For readability: replace '\sigma' with 'no units' diff --git a/toolbox/gui/panel_surface.m b/toolbox/gui/panel_surface.m index 36adc98ca..48c3eee15 100644 --- a/toolbox/gui/panel_surface.m +++ b/toolbox/gui/panel_surface.m @@ -1302,7 +1302,7 @@ function UpdateSurfaceProperties() % === NO FILE: HeadPoints === elseif strcmpi(fileType, 'HeadPoints') % Points were already displayed; just add the patch to the figure surfaces. - TessInfo(iTess).DataSource.Type = 'HeadPointsDistance'; + %TessInfo(iTess).DataSource.Type = 'HeadPointsDistance'; TessInfo(iTess).Name = 'HeadPoints'; TessInfo(iTess).ColormapType = 'stat1'; TessInfo(iTess).hPatch = findobj(hFig, 'Tag', 'HeadPointsMarkers'); diff --git a/toolbox/sensors/channel_align_manual.m b/toolbox/sensors/channel_align_manual.m index 0b1fce4d0..f1e820db0 100644 --- a/toolbox/sensors/channel_align_manual.m +++ b/toolbox/sensors/channel_align_manual.m @@ -914,7 +914,6 @@ function AlignClose_Callback(varargin) else SaveChanged = java_dialog('confirm', ['The sensors locations changed.' 10 10 ... 'Would you like to save changes? ' 10 10], 'Align sensors'); - end end % Don't close figure if cancelled. if isCancel From e8ceedb21721202492d67ab51b7c2623e2eee966 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Tue, 22 Nov 2022 15:47:31 -0500 Subject: [PATCH 25/32] undo adding head points as figure surface --- toolbox/core/bst_colormaps.m | 9 +- toolbox/gui/figure_3d.m | 54 ++- toolbox/gui/panel_surface.m | 49 +-- .../functions/process_adjust_coordinates.m | 352 ++++++++++-------- toolbox/sensors/channel_align_manual.m | 70 +--- 5 files changed, 252 insertions(+), 282 deletions(-) diff --git a/toolbox/core/bst_colormaps.m b/toolbox/core/bst_colormaps.m index 24ad47480..aefe462aa 100644 --- a/toolbox/core/bst_colormaps.m +++ b/toolbox/core/bst_colormaps.m @@ -376,7 +376,7 @@ function SetMaxCustom(ColormapType, DisplayUnits, newMin, newMax) case {'3DViz', 'MriViewer'} % Get surfaces defined in this figure TessInfo = getappdata(sFigure.hFigure, 'Surface'); - % Find surface(s) that matches this ColormapType + % Find surfaces that match this ColormapType iSurfaces = find(strcmpi({TessInfo.ColormapType}, ColormapType)); DataFig = []; for i = 1:length(iSurfaces) @@ -395,6 +395,13 @@ function SetMaxCustom(ColormapType, DisplayUnits, newMin, newMax) end end end + if isempty(DataFig) + % If displaying color-coded head points (see channel_align_manual) + HeadpointsDistMax = getappdata(sFigure.hFigure, 'HeadpointsDistMax'); + if ~isempty(HeadpointsDistMax) + DataFig = [0, HeadpointsDistMax * 1000]; + end + end case 'Pac' DataFig = GlobalData.DataSet(iDS).Figure(iFig).Handles.DataMinMax; diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index 19184d41f..82f890f9f 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -169,6 +169,11 @@ function ColormapChangedCallback(iDS, iFig) %#ok if ~isempty(getappdata(hFig, 'Dipoles')) && gui_brainstorm('isTabVisible', 'Dipoles') panel_dipoles('PlotSelectedDipoles', hFig); end + % If displaying color-coded head points (see channel_align_manual) + HeadpointsDistMax = getappdata(hFig, 'HeadpointsDistMax'); + if ~isempty(HeadpointsDistMax) + UpdateHeadPointsColormap(hFig); + end end @@ -3738,7 +3743,15 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) % Parse inputs if (nargin < 3) || isempty(isColorDist) isColorDist = 0; + elseif isColorDist + % Find scalp surface + [iTess, ~, hFig, sSurf] = panel_surface('GetSurface', hFig, '', 'Scalp'); + if isempty(iTess) + % Can't use color distance without scalp surface. + isColorDist = 0; + end end + % Get figure description [hFig, iFig, iDS] = bst_figures('GetFigure', hFig); if isempty(iDS) @@ -3783,19 +3796,18 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) if isColorDist && ~strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat') % Color points according to distance to surface % Get scalp surface - [~, ~, hFig, sSurf] = panel_surface('GetSurface', hFig, '', 'Scalp'); if ~isempty(sSurf) && isfield(sSurf, 'Vertices') && ~isempty(sSurf.Vertices) % Compute the distance Dist = bst_surfdist(get(hHeadPointsMarkers, 'Vertices'), sSurf.Vertices, sSurf.Faces); set(hHeadPointsMarkers, 'CData', Dist * 1000, ... 'MarkerFaceColor', 'flat', 'MarkerEdgeColor', 'flat'); + setappdata(hFig, 'HeadpointsDistMax', max(Dist)); if ~ismember(ColormapType, ColormapInfo.AllTypes) - iTess = panel_surface('GetSurface', hFig, '', 'HeadPoints'); - if isempty(iTess) - error('HeadPoints surface not found.'); - end - panel_surface('SetSurfaceData', hFig, iTess, 'HeadPointsDistance', '', 1); % isStat=1 + % Add missing colormap (color was toggled after points were displayed) + bst_colormaps('AddColormapToFigure', hFig, ColormapType, 'mm'); end + ColormapChangedCallback(iDS, iFig); + bst_colormaps('SetColorbarVisible', hFig, 1); end elseif ~isColorDist && strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat') % Conventional fixed color @@ -3882,13 +3894,13 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) if ~isempty(iExtra) % Color code points by distance if isColorDist - % Get scalp surface - [~, ~, hFig, sSurf] = panel_surface('GetSurface', hFig, '', 'Scalp'); % Compute the distance Dist = bst_surfdist(digLoc(iExtra, :), sSurf.Vertices, sSurf.Faces); CData = Dist * 1000; % mm + setappdata(hFig, 'HeadpointsDistMax', max(Dist)); MarkerFaceColor = 'flat'; MarkerEdgeColor = 'flat'; + bst_colormaps('AddColormapToFigure', hFig, 'stat1', 'mm'); else CData = 'w'; % any color, not displayed MarkerFaceColor = [.3 1 .3]; @@ -3905,16 +3917,36 @@ function ViewHeadPoints(hFig, isVisible, isColorDist) 'Marker', 'o', ... 'UserData', iExtra, ... 'Tag', 'HeadPointsMarkers'); - % Add points patch to figure surfaces so all color bar functionality work. - iTess = panel_surface('AddSurface', hFig, '', 'HeadPoints'); if isColorDist - panel_surface('SetSurfaceData', hFig, iTess, 'HeadPointsDistance', '', 1); % isStat=1 + ColormapChangedCallback(iDS, iFig); end end end end +%% ===== UPDATE HEADPOINTS COLORMAP ===== +function UpdateHeadPointsColormap(hFig) + % If not using color-coded display + hHeadPointsMarkers = findobj(hFig, 'Tag', 'HeadPointsMarkers'); + if ~strcmpi(get(hHeadPointsMarkers, 'MarkerFaceColor'), 'flat') + return; + end + % Get colormap configuration + sColormap = bst_colormaps('GetColormap', 'stat1'); + % Update axes color limits, which will update de colorbar + hAxes = get(hHeadPointsMarkers, 'Parent'); + if strcmpi(sColormap.MaxMode, 'custom') + set(hAxes, 'CLim', [sColormap.MinValue, sColormap.MaxValue]); + else + HeadpointsDistMax = getappdata(hFig, 'HeadpointsDistMax'); + set(hAxes, 'CLim', [0, HeadpointsDistMax * 1000]); + end + % Update colorbar + bst_colormaps('ConfigureColorbar', hFig, 'stat1', 'stat', 'mm'); +end + + %% ===== VIEW AXIS ===== function ViewAxis(hFig, isVisible) hAxes = findobj(hFig, '-depth', 1, 'Tag', 'Axes3D'); diff --git a/toolbox/gui/panel_surface.m b/toolbox/gui/panel_surface.m index 48c3eee15..32b8dbf4a 100644 --- a/toolbox/gui/panel_surface.m +++ b/toolbox/gui/panel_surface.m @@ -1130,20 +1130,15 @@ function UpdateSurfaceProperties() %% ===== ADD A SURFACE ===== % Add a surface to a given 3DViz figure % USAGE : [iTess, TessInfo] = panel_surface('AddSurface', hFig, surfaceFile) -% [iTess, TessInfo] = panel_surface('AddSurface', hFig, [], surfaceType) % OUTPUT: Indice of the surface in the figure's surface array -function [iTess, TessInfo] = AddSurface(hFig, surfaceFile, surfaceType) +function [iTess, TessInfo] = AddSurface(hFig, surfaceFile) % ===== CHECK EXISTENCE ===== + % Check whether filename is an absolute or relative path + surfaceFile = file_short(surfaceFile); % Get figure appdata (surfaces configuration) TessInfo = getappdata(hFig, 'Surface'); % Check that this surface is not already displayed in 3DViz figure - if isempty(surfaceFile) && nargin > 2 - iTess = find(file_compare({TessInfo.Name}, surfaceType)); - else - % Check whether filename is an absolute or relative path - surfaceFile = file_short(surfaceFile); - iTess = find(file_compare({TessInfo.SurfaceFile}, surfaceFile)); - end + iTess = find(file_compare({TessInfo.SurfaceFile}, surfaceFile)); if ~isempty(iTess) disp('BST> This surface is already displayed. Ignoring...'); return @@ -1166,9 +1161,6 @@ function UpdateSurfaceProperties() % ===== PLOT OBJECT ===== % Get file type (tessalation or MRI) fileType = file_gettype(surfaceFile); - if strcmpi(fileType, 'unknown') && nargin > 2 - fileType = surfaceType; - end % === TESSELATION === if any(strcmpi(fileType, {'cortex','scalp','innerskull','outerskull','tess'})) % === LOAD SURFACE === @@ -1299,16 +1291,6 @@ function UpdateSurfaceProperties() setappdata(hFig, 'Surface', TessInfo); end - % === NO FILE: HeadPoints === - elseif strcmpi(fileType, 'HeadPoints') - % Points were already displayed; just add the patch to the figure surfaces. - %TessInfo(iTess).DataSource.Type = 'HeadPointsDistance'; - TessInfo(iTess).Name = 'HeadPoints'; - TessInfo(iTess).ColormapType = 'stat1'; - TessInfo(iTess).hPatch = findobj(hFig, 'Tag', 'HeadPointsMarkers'); - % Update figure's surfaces list and current surface pointer - setappdata(hFig, 'Surface', TessInfo); - % === FEM === else % TODO: Check for FEM fileType explicitly view_surface_fem(surfaceFile, [], [], [], hFig); @@ -1515,11 +1497,6 @@ function UpdateSurfaceProperties() TessInfo(iTess).Data = []; TessInfo(iTess).DataWmat = []; - case 'HeadPointsDistance' - ColormapType = 'stat1'; - DisplayUnits = 'mm'; - % Data is actually added in UpdateSurfaceData below. - otherwise ColormapType = ''; DisplayUnits = []; @@ -1901,15 +1878,6 @@ function UpdateSurfaceProperties() figure_callback(hFig, 'UpdateSurfaceColor', hFig, iTess); % Get updated surface definition TessInfo = getappdata(hFig, 'Surface'); - - case 'HeadPointsDistance' - TessInfo(iTess).Data = get(TessInfo(iTess).hPatch, 'CData'); - if isempty(TessInfo(iTess).Data) || ~isnumeric(TessInfo(iTess).Data) - isOk = 0; - return; - end - TessInfo(iTess).DataMinMax = [min(TessInfo(iTess).Data(:)), max(TessInfo(iTess).Data(:))]; - otherwise % Nothing to do end @@ -2033,7 +2001,7 @@ function UpdateSurfaceColormap(hFig, iSurfaces) TessInfo(iTess).Data = abs(TessInfo(iTess).Data); end % If current colormap is the default colormap for this figure (for colorbar) - if strcmpi(ColormapInfo.Type, TessInfo(iTess).ColormapType) && ~isempty(TessInfo(iTess).DataSource.Type) + if strcmpi(ColormapInfo.Type, TessInfo(iTess).ColormapType) && ~isempty(TessInfo(iTess).DataSource.FileName) if all(~isnan(TessInfo(iTess).DataLimitValue)) && (TessInfo(iTess).DataLimitValue(1) < TessInfo(iTess).DataLimitValue(2)) set(hAxes, 'CLim', TessInfo(iTess).DataLimitValue); else @@ -2049,8 +2017,7 @@ function UpdateSurfaceColormap(hFig, iSurfaces) end end % === DISPLAY ON MRI === - if strcmpi(TessInfo(iTess).Name, 'Anatomy') && ~isempty(TessInfo(iTess).DataSource.Type) && ... - ~strcmpi(TessInfo(iTess).DataSource.Type, 'MriTime') && isempty(TessInfo(iTess).OverlayCube) + if strcmpi(TessInfo(iTess).Name, 'Anatomy') && ~isempty(TessInfo(iTess).DataSource.Type) && ~strcmpi(TessInfo(iTess).DataSource.Type, 'MriTime') && isempty(TessInfo(iTess).OverlayCube) % Progress bar isProgressBar = bst_progress('isVisible'); bst_progress('start', 'Display MRI', 'Updating values...'); @@ -2062,10 +2029,6 @@ function UpdateSurfaceColormap(hFig, iSurfaces) if ~isProgressBar bst_progress('stop'); end - elseif strcmpi(TessInfo(iTess).Name, 'HeadPoints') - % No need to update surface color here, data already updated. - % Update figure's appdata (surface list) - setappdata(hFig, 'Surface', TessInfo); else % Update figure's appdata (surface list) setappdata(hFig, 'Surface', TessInfo); diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index bf861a247..4e9f0d0eb 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -1,9 +1,8 @@ function varargout = process_adjust_coordinates(varargin) % PROCESS_ADJUST_COORDINATES: Adjust, recompute, or remove various coordinate transformations. % -% Native coordinates are based on system fiducials (e.g. MEG head coils), -% whereas Brainstorm's SCS coordinates are based on the anatomical fiducial -% points from the .pos file. +% Native coordinates are based on system fiducials (e.g. MEG head coils), whereas Brainstorm's SCS +% coordinates are based on the anatomical fiducial points from the .pos file. % @============================================================================= % This function is part of the Brainstorm software: @@ -30,7 +29,7 @@ -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description of the process sProcess.Comment = 'Adjust coordinate system'; sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/HeadMotion#Adjust_the_reference_head_position'; @@ -42,7 +41,7 @@ sProcess.OutputTypes = {'raw', 'data'}; sProcess.nInputs = 1; sProcess.nMinFiles = 1; - % Option [to do: ignore bad segments] + % Option sProcess.options.reset.Type = 'checkbox'; sProcess.options.reset.Comment = 'Reset coordinates using original channel file (removes all adjustments: head, points, manual).'; sProcess.options.reset.Value = 0; @@ -60,7 +59,7 @@ sProcess.options.bad.Type = 'checkbox'; sProcess.options.bad.Comment = 'For adjust option, exclude bad segments.'; sProcess.options.bad.Value = 1; - sProcess.options.bad.Class = 'Adjust'; + sProcess.options.bad.Class = 'Adjust'; sProcess.options.points.Type = 'checkbox'; sProcess.options.points.Comment = 'Refine MRI coregistration using digitized head points.'; sProcess.options.points.Value = 0; @@ -103,9 +102,8 @@ end bst_progress('start', 'Adjust coordinate system', ... ' ', 0, nFiles); - % If resetting, in case the original data moved, and because the same - % channel file may appear in many places for processed data, keep track - % of user file selections. + % If resetting, in case the original data moved, and because the same channel file may appear in + % many places for processed data, keep track of user file selections. NewChannelFiles = cell(0, 2); for iFile = iUniqFiles(:)' % no need to repeat on same channel file. @@ -132,13 +130,11 @@ % ---------------------------------------------------------------- if sProcess.options.reset.Value - % The main goal of this option is to fix a bug in a previous - % version: when importing a channel file, when going to SCS - % coordinates based on digitized coils and anatomical - % fiducials, the channel orientation was wrong. We wish to fix - % this but keep as much pre-processing that was previously - % done. Thus we will re-import the channel file, and copy the - % projectors (and history) from the old one. + % The main goal of this option is to fix a bug in a previous version: when importing a + % channel file, when going to SCS coordinates based on digitized coils and anatomical + % fiducials, the channel orientation was wrong. We wish to fix this but keep as much + % pre-processing that was previously done. Thus we will re-import the channel file, and + % copy the projectors (and history) from the old one. [ChannelMat, NewChannelFiles, Failed] = ... ResetChannelFile(ChannelMat, NewChannelFiles, sInputs(iFile), sProcess); @@ -148,25 +144,23 @@ % ---------------------------------------------------------------- elseif sProcess.options.remove.Value - % Because channel_align_manual does not consistently apply the - % manual transformation to all sensors or save it in both - % TransfMeg and TransfEeg, it could lead to confusion and - % errors when playing with transforms. Therefore, if we detect - % a difference between the MEG and EEG transforms when trying - % to remove one that applies to both (currently only refine - % with head points), we don't proceed and recommend resetting - % with the original channel file instead. + % Because channel_align_manual does not consistently apply the manual transformation to + % all sensors or save it in both TransfMeg and TransfEeg, it could lead to confusion and + % errors when playing with transforms. Therefore, if we detect a difference between the + % MEG and EEG transforms when trying to remove one that applies to both (currently only + % refine with head points), we don't proceed and recommend resetting with the original + % channel file instead. Which = {}; if sProcess.options.head.Value - Which{end+1} = 'AdjustedNative'; + Which{end+1} = 'AdjustedNative'; %#ok end if sProcess.options.points.Value - Which{end+1} = 'refine registration: head points'; + Which{end+1} = 'refine registration: head points'; %#ok end for TransfLabel = Which - TransfLabel = TransfLabel{1}; + TransfLabel = TransfLabel{1}; %#ok ChannelMat = RemoveTransformation(ChannelMat, TransfLabel, sInputs(iFile), sProcess); end % TransfLabel loop @@ -189,8 +183,8 @@ bst_progress('text', 'Fitting head surface to points...'); [ChannelMat, R, T, isSkip] = ... channel_align_auto(sInputs(iFile).ChannelFile, ChannelMat, 0, 0); % No warning or confirmation - % ChannelFile needed to find subject and scalp surface, but not - % used otherwise when ChannelMat is provided. + % ChannelFile needed to find subject and scalp surface, but not used otherwise when + % ChannelMat is provided. if isSkip bst_report('Error', sProcess, sInputs(iFile), ... 'Error trying to refine registration using head points.'); @@ -212,10 +206,9 @@ end % file loop bst_progress('stop'); - % Return the input files that were processed properly. Include those - % that were removed due to sharing a channel file, where appropriate. - % The complicated indexing picks the first input of those with the same - % channel file, i.e. the one that was marked ok. + % Return the input files that were processed properly. Include those that were removed due to + % sharing a channel file, where appropriate. The complicated indexing picks the first input of + % those with the same channel file, i.e. the one that was marked ok. OutputFiles = {sInputs(isFileOk(iUniqInputs(iUniqFiles))).FileName}; end @@ -266,8 +259,7 @@ % end -function [ChannelMat, NewChannelFiles, Failed] = ... - ResetChannelFile(ChannelMat, NewChannelFiles, sInput, sProcess) +function [ChannelMat, NewChannelFiles, Failed] = ResetChannelFile(ChannelMat, NewChannelFiles, sInput, sProcess) if nargin < 4 sProcess = []; end @@ -307,10 +299,9 @@ if NotFound bst_report('Info', sProcess, sInput, ... sprintf('Could not find original channel file: %s.', ChannelFile)); - % import_channel will prompt the user, but they will not - % know which file to pick! And prompt is modal for Matlab, - % so likely can't look at command window (e.g. if - % Brainstorm is in front). + % import_channel will prompt the user, but they will not know which file to pick! And + % prompt is modal for Matlab, so likely can't look at command window (e.g. if Brainstorm is + % in front). [ChanPath, ChanName, ChanExt] = fileparts(ChannelFile); MsgFig = msgbox(sprintf('Select the new location of channel file %s %s to reset %s.', ... ChanPath, [ChanName, ChanExt], sInput.ChannelFile), ... @@ -380,9 +371,8 @@ % Need to check for empty, otherwise applies to all channels! else iChan = []; % All channels. - % Note: NIRS doesn't have a separate set of - % transformations, but "refine" and "SCS" are applied - % to NIRS as well. + % Note: NIRS doesn't have a separate set of transformations, but "refine" and "SCS" are + % applied to NIRS as well. end while ~isempty(iUndoMeg) if isMegOnly && isempty(iChan) @@ -457,9 +447,8 @@ return; end - % The data could be changed such that the head position could be - % readjusted (e.g. by deleting segments). This is allowed and the - % previous adjustment will be replaced. + % The data could be changed such that the head position could be readjusted (e.g. by deleting + % segments). This is allowed and the previous adjustment will be replaced. if isfield(ChannelMat, 'TransfMegLabels') && iscell(ChannelMat.TransfMegLabels) && ... ismember('AdjustedNative', ChannelMat.TransfMegLabels) bst_report('Info', sProcess, sInputs, ... @@ -513,7 +502,7 @@ Locs(:, ismember(iHeadSamples, iBad)) = []; end end - Locations = [Locations, Locs]; + Locations = [Locations, Locs]; %#ok end % If a collection was aborted, the channels will be filled with zeros. Remove these. @@ -523,8 +512,7 @@ MedianLoc = MedianLocation(Locations); % disp(MedianLoc); - % Also get the initial reference position. We only use it to see - % how much the adjustment moves. + % Also get the initial reference position. We only use it to estimate how much the adjustment moves. InitRefLoc = ReferenceHeadLocation(ChannelMat, sInputs); if isempty(InitRefLoc) % There was an error, already reported. Skip this file. @@ -532,9 +520,8 @@ return; end - % Extract transformations that are applied before and after the - % head position adjustment. Any previous adjustment will be - % ignored here and replaced later. + % Extract transformations that are applied before and after the head position adjustment. Any + % previous adjustment will be ignored here and replaced later. [TransfBefore, TransfAdjust, TransfAfter, iAdjust, iDewToNat] = ... GetTransforms(ChannelMat, sInputs); if isempty(TransfBefore) @@ -545,33 +532,26 @@ % Compute transformation corresponding to coil position. [TransfMat, TransfAdjust] = LocationTransform(MedianLoc, ... TransfBefore, TransfAdjust, TransfAfter); - % This TransfMat would automatically give an identity - % transformation if the process is run multiple times, and - % TransfAdjust would not change. - - % Apply this transformation to the current head position. - % This is a correction to the 'Dewar=>Native' - % transformation so it applies to MEG channels only and not - % to EEG or head points, which start in Native. + % This TransfMat would automatically give an identity transformation if the process is run + % multiple times, and TransfAdjust would not change. + + % Apply this transformation to the current head position. This is a correction to the + % 'Dewar=>Native' transformation so it applies to MEG channels only and not to EEG or head + % points, which start in Native. iMeg = sort([good_channel(ChannelMat.Channel, [], 'MEG'), ... good_channel(ChannelMat.Channel, [], 'MEG REF')]); ChannelMat = channel_apply_transf(ChannelMat, TransfMat, iMeg, false); % Don't apply to head points. ChannelMat = ChannelMat{1}; - % After much thought, it was decided to save this - % adjustment transformation separately and at its logical - % place: between 'Dewar=>Native' and - % 'Native=>Brainstorm/CTF'. In particular, this allows us - % to use it directly when displaying head motion distance. - % This however means we must correctly move the - % transformation from the end where it was just applied to - % its logical place. This "moved" transformation is also - % computed in LocationTransform above. + % After much thought, it was decided to save this adjustment transformation separately and at + % its logical place: between 'Dewar=>Native' and 'Native=>Brainstorm/CTF'. In particular, this + % allows us to use it directly when displaying head motion distance. This however means we must + % correctly move the transformation from the end where it was just applied to its logical place. + % This "moved" transformation is also computed in LocationTransform above. if isempty(iAdjust) iAdjust = iDewToNat + 1; - % Shift transformations to make room for the new - % adjustment, and reject the last one, that we just - % applied. + % Shift transformations to make room for the new adjustment, and reject the last one, that + % we just applied. ChannelMat.TransfMegLabels(iDewToNat+2:end) = ... ChannelMat.TransfMegLabels(iDewToNat+1:end-1); % reject last one ChannelMat.TransfMeg(iDewToNat+2:end) = ChannelMat.TransfMeg(iDewToNat+1:end-1); @@ -601,14 +581,12 @@ end % AdjustHeadPosition - function [InitLoc, Message] = ReferenceHeadLocation(ChannelMat, sInput) % Compute initial head location in Dewar coordinates. - % Here we want to recreate the correct triangle shape from the relative - % head coil locations and in the position saved as the reference - % (initial) head position according to Brainstorm coordinate - % transformation matrices. + % Here we want to recreate the correct triangle shape from the relative head coil locations and + % in the position saved as the reference (initial) head position according to Brainstorm + % coordinate transformation matrices. if nargin < 2 sInput = []; @@ -616,23 +594,24 @@ sInput = sInput(1); end Message = ''; - - % These aren't exactly the coil positions in the .hc file, which are not saved - % anywhere in Brainstorm, but was verified to give the same transformation. - % The SCS coil coordinates are from the digitized coil positions. - if isfield(ChannelMat, 'SCS') && all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) && ... - (length(ChannelMat.SCS.NAS) == 3) && (length(ChannelMat.SCS.LPA) == 3) && (length(ChannelMat.SCS.RPA) == 3) - % % Use the SCS distances from origin, with left and right PA points symmetrical. - % LeftRightDist = sqrt(sum((ChannelMat.SCS.LPA - ChannelMat.SCS.RPA).^2)); - % NasDist = ChannelMat.SCS.NAS(1); - InitLoc = [ChannelMat.SCS.NAS(:), ChannelMat.SCS.LPA(:), ChannelMat.SCS.RPA(:); ones(1, 3)]; - elseif ~isempty(sInput) && isfield(sInput, 'header') && isfield(sInput.header, 'hc') && isfield(sInput.header.hc, 'SCS') && ... + + % From recent investigations, digitized locations are probably not as robust/accurate as those + % measured by the MEG. So use the .hc positions if available. + if ~isempty(sInput) && isfield(sInput, 'header') && isfield(sInput.header, 'hc') && isfield(sInput.header.hc, 'SCS') && ... all(isfield(sInput.header.hc.SCS, {'NAS','LPA','RPA'})) && length(sInput.header.hc.SCS.NAS) == 3 % Initial head coil locations from the CTF .hc file, but in dewar coordinates, NOT in SCS coordinates! InitLoc = [sInput.header.hc.SCS.NAS(:), sInput.header.hc.SCS.LPA(:), sInput.header.hc.SCS.RPA(:)]; % 3x3 by columns InitLoc = InitLoc(:); return; - %InitLoc = TransfAdjust * TransfBefore * [InitLoc; ones(1, 3)]; + % ChannelMat.SCS are not the coil positions in the .hc file, which are not saved in Brainstorm, + % but the digitized coil positions, if present. However, both are saved in "Native" coordinates + % and thus give the same transformation. + elseif isfield(ChannelMat, 'SCS') && all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) && ... + (length(ChannelMat.SCS.NAS) == 3) && (length(ChannelMat.SCS.LPA) == 3) && (length(ChannelMat.SCS.RPA) == 3) + % % Use the SCS distances from origin, with left and right PA points symmetrical. + % LeftRightDist = sqrt(sum((ChannelMat.SCS.LPA - ChannelMat.SCS.RPA).^2)); + % NasDist = ChannelMat.SCS.NAS(1); + InitLoc = [ChannelMat.SCS.NAS(:), ChannelMat.SCS.LPA(:), ChannelMat.SCS.RPA(:); ones(1, 3)]; else % Just use some reasonable distances, with a warning. Message = 'Exact reference head coil locations not available. Using reasonable (adult) locations according to head position.'; @@ -643,11 +622,9 @@ % InitLoc above is in Native coordiates (if pre head loc didn't fail). % Bring it back to Dewar coordinates to compare with HLU channels. % - % Take into account if the initial/reference head position was - % "adjusted", i.e. replaced by the median position throughout the - % recording. If so, use all transformations from 'Dewar=>Native' to - % this adjustment transformation. (In practice there shouldn't be any - % between them.) + % Take into account if the initial/reference head position was "adjusted", i.e. replaced by the + % median position throughout the recording. If so, use all transformations from 'Dewar=>Native' + % to this adjustment transformation. (In practice there shouldn't be any between them.) [TransfBefore, TransfAdjust] = GetTransforms(ChannelMat, sInput); InitLoc = TransfBefore \ (TransfAdjust \ InitLoc); InitLoc(4, :) = []; @@ -655,25 +632,22 @@ end % ReferenceHeadLocation -function [TransfBefore, TransfAdjust, TransfAfter, iAdjust, iDewToNat] = ... - GetTransforms(ChannelMat, sInputs) - % Extract transformations that are applied before and after the head - % position adjustment we are creating now. We keep the 'Dewar=>Native' - % transformation intact and separate from the adjustment for no deep - % reason, but it is the only remaining trace of the initial head coil +function [TransfBefore, TransfAdjust, TransfAfter, iAdjust, iDewToNat] = GetTransforms(ChannelMat, sInputs) + % Extract transformations that are applied before and after the head position adjustment we are + % creating now. We keep the 'Dewar=>Native' transformation intact and separate from the + % adjustment for no deep reason, but it is the only remaining trace of the initial head coil % positions in Brainstorm. - % The reason this function was split from LocationTransform is that it - % can be called only once outside the head sample loop in process_sss, - % whereas LocationTransform is called many times within the loop. + % The reason this function was split from LocationTransform is that it can be called only once + % outside the head sample loop in process_sss, whereas LocationTransform is called many times + % within the loop. - % When this is called from process_sss, we are possibly working on a - % second head adjustment, this time based on the instantaneous head - % position, so we need to keep the global adjustment based on the entire - % recording if it is there. + % When this is called from process_sss, we are possibly working on a second head adjustment, + % this time based on the instantaneous head position, so we need to keep the global adjustment + % based on the entire recording if it is there. - % Check order of transformations. These situations should not happen - % unless there was some manual editing. + % Check order of transformations. These situations should not happen unless there was some + % manual editing. iDewToNat = find(strcmpi(ChannelMat.TransfMegLabels, 'Dewar=>Native')); iAdjust = find(strcmpi(ChannelMat.TransfMegLabels, 'AdjustedNative')); TransfBefore = []; @@ -724,11 +698,9 @@ end % GetTransforms -function [TransfMat, TransfAdjust] = LocationTransform(Loc, ... - TransfBefore, TransfAdjust, TransfAfter) - % Compute transformation corresponding to head coil positions. - % We want this to be as efficient as possible, since used many times by - % process_sss. +function [TransfMat, TransfAdjust] = LocationTransform(Loc, TransfBefore, TransfAdjust, TransfAfter) + % Compute transformation corresponding to head coil positions. We want this to be as efficient + % as possible, since used many times by process_sss. % Check for previous version. if nargin < 4 @@ -736,10 +708,10 @@ end % Transformation matrices are in m, as are HLU channels. - % The HLU channels (here Loc) are in dewar coordinates. Bring them to - % the current system by applying all saved transformations, starting with - % 'Dewar=>Native'. This will save us from having to use inverse - % transformations later. + % + % The HLU channels (here Loc) are in dewar coordinates. Bring them to the current system by + % applying all saved transformations, starting with 'Dewar=>Native'. This will save us from + % having to use inverse transformations later. Loc = TransfAfter(1:3, :) * TransfAdjust * TransfBefore * [reshape(Loc, 3, 3); 1, 1, 1]; % [[Loc(1:3), Loc(4:6), Loc(5:9)]; 1, 1, 1]; % test if efficiency difference. @@ -759,14 +731,12 @@ TransfMat(1:3,1:3) = [X, Y, Z]'; TransfMat(1:3,4) = - [X, Y, Z]' * Origin; - % TransfMat at this stage is a transformation from the current system - % back to the now adjusted Native system. We thus need to reapply the - % following tranformations. + % TransfMat at this stage is a transformation from the current system back to the now adjusted + % Native system. We thus need to reapply the following tranformations. if nargout > 1 - % Transform from non-adjusted native coordinates to newly adjusted native - % coordinates. To be saved in channel file between "Dewar=>Native" and - % "Native=>Brainstorm/CTF". + % Transform from non-adjusted native coordinates to newly adjusted native coordinates. To + % be saved in channel file between "Dewar=>Native" and "Native=>Brainstorm/CTF". TransfAdjust = TransfMat * TransfAfter * TransfAdjust; end @@ -796,34 +766,19 @@ % % M = GeoMedian(X, Precision) % - % Calculate the geometric median: the point that minimizes sum of - % Euclidean distances to all points. size(X) = [n, d, ...], where n is - % the number of data points, d is the number of components for each point - % and any additional array dimension is treated as independent sets of - % data and a median is calculated for each element along those dimensions - % sequentially; size(M) = [1, d, ...]. This is an approximate iterative - % procedure that stops once the desired precision is achieved. If - % Precision is not provided, 1e-4 of the max distance from the centroid - % is used. - % - % Weiszfeld's algorithm is used, which is a subgradient algorithm; with - % (Verdi & Zhang 2001)'s modification to avoid non-optimal fixed points - % (if at any iteration the approximation of M equals a data point). - % + % Calculate the geometric median: the point that minimizes sum of Euclidean distances to all + % points. size(X) = [n, d, ...], where n is the number of data points, d is the number of + % components for each point and any additional array dimension is treated as independent sets of + % data and a median is calculated for each element along those dimensions sequentially; size(M) + % = [1, d, ...]. This is an approximate iterative procedure that stops once the desired + % precision is achieved. If Precision is not provided, 1e-4 of the max distance from the + % centroid is used. % - % (c) Copyright 2018 Marc Lalancette - % The Hospital for Sick Children, Toronto, Canada + % Weiszfeld's algorithm is used, which is a subgradient algorithm; with (Verdi & Zhang 2001)'s + % modification to avoid non-optimal fixed points (if at any iteration the approximation of M + % equals a data point). % - % This file is part of a free repository of Matlab tools for MEG - % data processing and analysis . - % You can redistribute it and/or modify it under the terms of the GNU - % General Public License as published by the Free Software Foundation, - % either version 3 of the License, or (at your option) a later version. - % - % This program is distributed WITHOUT ANY WARRANTY. - % See the LICENSE file, or for details. - % - % 2012-05 + % Marc Lalancette 2012-05 nDims = ndims(X); XSize = size(X); @@ -850,17 +805,15 @@ Precision = bsxfun(@rdivide, Precision, Scale); % Precision ./ Scale; % [1, 1, nSets] end - % Initial estimate: median in each dimension separately. Though this - % gives a chance of picking one of the data points, which requires - % special treatment. + % Initial estimate: median in each dimension separately. Though this gives a chance of picking + % one of the data points, which requires special treatment. M2 = median(X, 1); - % It might be better to calculate separately each independent set, - % otherwise, they are all iterated until the worst case converges. + % It might be better to calculate separately each independent set, otherwise, they are all + % iterated until the worst case converges. for s = 1:nSets - % For convenience, pick another point far enough so the loop will always - % start. + % For convenience, pick another point far enough so the loop will always start. M = bsxfun(@plus, M2(:, :, s), Precision(:, :, s)); % Iterate. while sum((M - M2(:, :, s)).^2 , 2) > Precision(s)^2 % any()scalar @@ -868,8 +821,7 @@ % Distances from M. % R = sqrt(sum( (M(ones(n, 1), :) - X(:, :, s)).^2 , 2 )); % [n, 1] R = sqrt(sum( bsxfun(@minus, M, X(:, :, s)).^2 , 2 )); % [n, 1] - % Find data points not equal to M, that we use in the computation - % below. + % Find data points not equal to M, that we use in the computation below. Good = logical(R); nG = sum(Good); if nG % > 0 @@ -883,10 +835,10 @@ end % New estimate. - % Note the possibility of D = 0 and (n - nG) = 0, in which case 0/0 - % should be 0, but here gives NaN, which the max function ignores, - % returning 0 instead of 1. This is fine however since this - % multiplies D (=0 in that case). + % + % Note the possibility of D = 0 and (n - nG) = 0, in which case 0/0 should be 0, but + % here gives NaN, which the max function ignores, returning 0 instead of 1. This is fine + % however since this multiplies D (=0 in that case). M2(:, :, s) = M - max(0, 1 - (n - nG)/sqrt(sum( D.^2 , 2 ))) * ... D / sum(1 ./ R, 1); end @@ -903,3 +855,79 @@ end % GeoMedian +function CheckPrevAdjustments(ChannelMat, sMri) + % Flag if auto or manual registration performed, and if MRI fids updated. Print to command + % window for now. + if any(~isfield(ChannelMat, {'History', 'HeadPoints'})) + % Nothing to check. + return; + end + if nargin < 2 || isempty(sMri) || ~isfield(sMri, 'History') + iMriHist = []; + else + iMriHist = find(strcmpi(sMri.History(:,3), 'Applied digitized anatomical fiducials'), 1, 'last'); + end + % Can also be reset, so check for 'import' action and ignore previous alignments. + iImport = find(strcmpi(ChannelMat.History(:,2), 'import'), 1, 'last'); + iAlign = find(strcmpi(ChannelMat.History(:,2), 'align')); + iAlign(iAlign < iImport) = []; + AlignType = 'none'; + while ~isempty(iAlign) + % Check which adjustment was done last. + switch lower(ChannelMat.History{iAlign(end),3}(1:5)) + case 'remov' + % Removed a previous step. Ignore corresponding adjustment and look again. + iAlign(end) = []; + if contains(ChannelMat.History{iAlign(end),3}, 'AdjustedNative') + iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); + elseif contains(ChannelMat.History{iAlign(end),3}, 'refine registration: head points') + iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); + elseif contains(ChannelMat.History{iAlign(end),3}, 'manual correction') + iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); + else + bst_error('Unrecognized removed transformation in history.'); + end + if isempty(iAlignRemoved) + bst_error('Missing removed transformation in history.'); + else + iAlign(iAlignRemoved) = []; + end + case 'added' + % This alignment is between points and functional dataset, ignore here. + iAlign(end) = []; + case 'refin' + % Automatic MRI-points alignment + AlignType = 'auto'; + break; + case 'align' + % Manual MRI-points alignment + AlignType = 'manual'; + break; + end + end + disp(['BST> Previous registration adjustment: ' AlignType]); + if ~isempty(iMriHist) + % Compare digitized fids to MRI fids (in MRI coordinates, mm). ChannelMat.SCS fids are NOT + % kept up to date when adjusting registration (manual or auto), so get them from head points + % again. + % Get the three fiducials in the head points + iNas = find(strcmpi(ChannelMat.HeadPoints.Label, 'Nasion') | strcmpi(ChannelMat.HeadPoints.Label, 'NAS')); + iLpa = find(strcmpi(ChannelMat.HeadPoints.Label, 'Left') | strcmpi(ChannelMat.HeadPoints.Label, 'LPA')); + iRpa = find(strcmpi(ChannelMat.HeadPoints.Label, 'Right') | strcmpi(ChannelMat.HeadPoints.Label, 'RPA')); + if ~isempty(iNas) && ~isempty(iLpa) && ~isempty(iRpa) + ChannelMat.SCS.NAS = mean(ChannelMat.HeadPoints.Loc(:,iNas)', 1); + ChannelMat.SCS.LPA = mean(ChannelMat.HeadPoints.Loc(:,iLpa)', 1); + ChannelMat.SCS.RPA = mean(ChannelMat.HeadPoints.Loc(:,iRpa)', 1); + end + if any(abs(sMri.SCS.NAS - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.NAS) .* 1000) > 1e-3) || ... + any(abs(sMri.SCS.LPA - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.LPA) .* 1000) > 1e-3) || ... + any(abs(sMri.SCS.RPA - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.RPA) .* 1000) > 1e-3) + disp('BST> MRI fiducials previously updated, but different than current digitized fiducials.'); + else + disp('BST> MRI fiducials previously updated, and match current digitized fiducials.'); + end + end + +end + + diff --git a/toolbox/sensors/channel_align_manual.m b/toolbox/sensors/channel_align_manual.m index f1e820db0..672e710e7 100644 --- a/toolbox/sensors/channel_align_manual.m +++ b/toolbox/sensors/channel_align_manual.m @@ -404,65 +404,11 @@ if isProgress bst_progress('stop'); end - -% Flag if auto or manual registration performed, and if MRI fids updated. Print to command -% window for now. -ChannelMat = in_bst_channel(ChannelFile); -iMriHist = find(strcmpi(sMri.History(:,3), 'Applied digitized anatomical fiducials'), 1, 'last'); -% Can also be reset, so check for 'import' action and ignore previous alignments. -iImport = find(strcmpi(ChannelMat.History(:,2), 'import'), 1, 'last'); -iAlign = find(strcmpi(ChannelMat.History(:,2), 'align')); -iAlign(iAlign < iImport) = []; -AlignType = 'none'; -while ~isempty(iAlign) - % Check which adjustment was done last. - switch lower(ChannelMat.History{iAlign(end),3}(1:5)) - case 'remov' - % Removed a previous step. Ignore corresponding adjustment and look again. - iAlign(end) = []; - if contains(ChannelMat.History{iAlign(end),3}, 'AdjustedNative') - iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); - elseif contains(ChannelMat.History{iAlign(end),3}, 'refine registration: head points') - iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); - elseif contains(ChannelMat.History{iAlign(end),3}, 'manual correction') - iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); - else - bst_error('Unrecognized removed transformation in history.'); - end - if isempty(iAlignRemoved) - bst_error('Missing removed transformation in history.'); - else - iAlign(iAlignRemoved) = []; - end - case 'added' - % This alignment is between points and functional dataset, ignore here. - iAlign(end) = []; - case 'refin' - % Automatic MRI-points alignment - AlignType = 'auto'; - break; - case 'align' - % Manual MRI-points alignment - AlignType = 'manual'; - break; - end -end -disp(['BST> Previous registration adjustment: ' AlignType]); -if ~isempty(iMriHist) - % Compare digitized fids to MRI fids (in MRI coordinates, mm). - if any(abs(sMri.SCS.NAS - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.NAS) .* 1000) > 1e-3) || ... - any(abs(sMri.SCS.LPA - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.LPA) .* 1000) > 1e-3) || ... - any(abs(sMri.SCS.RPA - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.RPA) .* 1000) > 1e-3) - disp('BST> MRI fiducials updated, but different than digitized fiducials.'); - else - disp('BST> MRI fiducials updated, and match digitized fiducials.'); - end -end +% Check and print to command window if previously auto/manual registration, and if MRI fids updated. +process_adjust_coordinates('CheckPrevAdjustments', in_bst_channel(ChannelFile), sMri); end - - %% ===== MOUSE CALLBACKS ===== %% ===== MOUSE DOWN ===== function AlignButtonDown_Callback(hObject, ev) @@ -701,10 +647,9 @@ function UpdatePoints(iSelChan) Dist = bst_surfdist(gChanAlign.HeadPointsMarkersLoc, ... get(gChanAlign.hSurfacePatch, 'Vertices'), get(gChanAlign.hSurfacePatch, 'Faces')); set(gChanAlign.hHeadPointsMarkers, 'CData', Dist * 1000); - % Update surface data and colorbar - TessInfo = getappdata(gChanAlign.hFig, 'Surface'); - iTessPoints = find(strcmpi({TessInfo.Name}, 'HeadPoints')); - panel_surface('UpdateSurfaceData', gChanAlign.hFig, iTessPoints); + % Update axes maximum + setappdata(gChanAlign.hFig, 'HeadpointsDistMax', max(Dist)); + figure_3d('UpdateHeadPointsColormap', gChanAlign.hFig); end % Fiducials if ~isempty(gChanAlign.hHeadPointsFid) @@ -907,7 +852,6 @@ function AlignKeyPress_Callback(hFig, keyEvent) function AlignClose_Callback(varargin) global gChanAlign; if gChanAlign.isChanged - isCancel = false; % Ask user to save changes (only if called as a callback) if (nargin == 3) SaveChanged = 1; @@ -915,10 +859,6 @@ function AlignClose_Callback(varargin) SaveChanged = java_dialog('confirm', ['The sensors locations changed.' 10 10 ... 'Would you like to save changes? ' 10 10], 'Align sensors'); end - % Don't close figure if cancelled. - if isCancel - return; - end % Save changes to channel file and close figure if SaveChanged % Progress bar From af062b384931efa30b67a2a4f1962f8df6f6cefc Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Mon, 28 Nov 2022 12:04:52 -0500 Subject: [PATCH 26/32] minor fix to adjust coordinates scs option --- .../functions/process_adjust_coordinates.m | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index 0f55b9da0..58aef299c 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -110,18 +110,6 @@ bst_report('Info', sProcess, sInputs, ... 'Multiple inputs were found for a single channel file. They will be concatenated for adjusting the head position.'); end - - if ~sProcess.options.remove.Value && sProcess.options.points.Value && sProcess.options.scs.Value - % Warning and confirmation dialog. - isConfirmed = java_dialog('confirm', 'Ajusting MRI nasion and ear points will break previous alignment with head points for files not included here. Proceed?', ... - 'Adjust MRI nasion and ear points?'); - if ~isConfirmed - bst_report('User cancelled.'); - OutputFiles = {}; - return; - end - end - bst_progress('start', 'Adjust coordinate system', ... ' ', 0, nFiles); % If resetting, in case the original data moved, and because the same channel file may appear in @@ -265,7 +253,7 @@ OutputFiles = {sInputs(isFileOk(iUniqInputs(iUniqFiles))).FileName}; end -% if ~sProcess.options.remove.Value && sProcess.options.newpoints.Value +% if ~sProcess.options.remove.Value && sProcess.options.scs.Value % % This not yet implemented option could apply the Native to SCS % % transformation for head points loaded after the raw data was % % imported. @@ -1054,4 +1042,4 @@ ChannelMat.SCS.LPA = mean(ChannelMat.HeadPoints.Loc(:,iLpa)', 1); ChannelMat.SCS.RPA = mean(ChannelMat.HeadPoints.Loc(:,iRpa)', 1); end -end \ No newline at end of file +end From 242f9d5fa4aaa79e8093a2f48ddd64fd481305cd Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Mon, 28 Nov 2022 12:50:00 -0500 Subject: [PATCH 27/32] compat fix --- .../functions/process_adjust_coordinates.m | 26 ++++++++++++------- toolbox/tree/tree_callbacks.m | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index 58aef299c..e88c8a76e 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -930,15 +930,15 @@ while ~isempty(iAlign) % Check which adjustment was done last. switch lower(ChannelMat.History{iAlign(end),3}(1:5)) - case 'remov' + case 'remov' % ['Removed transform: ' TransfLabel] % Removed a previous step. Ignore corresponding adjustment and look again. iAlign(end) = []; - if contains(ChannelMat.History{iAlign(end),3}, 'AdjustedNative') - iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); - elseif contains(ChannelMat.History{iAlign(end),3}, 'refine registration: head points') - iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); - elseif contains(ChannelMat.History{iAlign(end),3}, 'manual correction') - iAlignRemoved = find(contains(ChannelMat.History(iAlign,3), 'AdjustedNative'), 1, 'last'); + if strncmpi(ChannelMat.History{iAlign(end),3}(20:24), 'AdjustedNative', 5) + iAlignRemoved = find(cellfun(@(c)strcmpi(c(1:5), 'added'), ChannelMat.History(iAlign,3)), 1, 'last'); + elseif strncmpi(ChannelMat.History{iAlign(end),3}(20:24), 'refine registration: head points', 5) + iAlignRemoved = find(cellfun(@(c)strcmpi(c(1:5), 'refin'), ChannelMat.History(iAlign,3)), 1, 'last'); + elseif strncmpi(ChannelMat.History{iAlign(end),3}(20:24), 'manual correction', 5) + iAlignRemoved = find(cellfun(@(c)strcmpi(c(1:5), 'align'), ChannelMat.History(iAlign,3)), 1, 'last'); else bst_error('Unrecognized removed transformation in history.'); end @@ -947,17 +947,23 @@ else iAlign(iAlignRemoved) = []; end - case 'added' + case 'added' % 'Added adjustment to Native coordinates based on median head position' % This alignment is between points and functional dataset, ignore here. iAlign(end) = []; - case 'refin' + case 'refin' % 'Refining the registration using the head points:' % Automatic MRI-points alignment AlignType = 'auto'; break; - case 'align' + case 'align' % 'Align channels manually:' % Manual MRI-points alignment AlignType = 'manual'; break; + case 'non-l' % 'Non-linear transformation' + AlignType = 'non-linear'; + break; + otherwise + AlignType = 'unrecognized'; + break; end end if isPrint diff --git a/toolbox/tree/tree_callbacks.m b/toolbox/tree/tree_callbacks.m index 3568ba9ae..4a2a2befe 100644 --- a/toolbox/tree/tree_callbacks.m +++ b/toolbox/tree/tree_callbacks.m @@ -225,7 +225,7 @@ elseif strcmpi(DisplayMod{1}, 'ECOG') DisplayChannels(bstNodes, DisplayMod{1}, 'cortex', 1); elseif ismember(DisplayMod{1}, {'MEG','MEG GRAD','MEG MAG'}) - channel_align_manual(filenameRelative, DisplayMod{1}, 0) + channel_align_manual(filenameRelative, DisplayMod{1}, 0); elseif strcmpi(DisplayMod{1}, 'NIRS') DisplayChannels(bstNodes, 'NIRS-BRS', 'scalp', [], 1); else From be598422f0c6da5f4af1d7793e50f680a96433e8 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Fri, 2 Dec 2022 17:45:44 -0500 Subject: [PATCH 28/32] wip exporting registration to BIDS --- toolbox/anatomy/cs_convert.m | 6 ++-- toolbox/gui/figure_3d.m | 16 ++++++---- .../functions/process_adjust_coordinates.m | 29 +++++++++++++++---- toolbox/sensors/channel_align_manual.m | 20 ++++++------- toolbox/sensors/channel_align_scs.m | 1 + 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/toolbox/anatomy/cs_convert.m b/toolbox/anatomy/cs_convert.m index 250fa170b..706e4cb15 100644 --- a/toolbox/anatomy/cs_convert.m +++ b/toolbox/anatomy/cs_convert.m @@ -16,7 +16,7 @@ % DESCRIPTION: https://neuroimage.usc.edu/brainstorm/CoordinateSystems % - voxel : X=left>right, Y=posterior>anterior, Z=bottom>top % Coordinate of the center of the first voxel at the bottom-left-posterior of the MRI volume: (1,1,1) -% - mri : Same as 'voxel' but in millimeters instead of voxels: mriXYZ = voxelXYZ * Voxsize +% - mri : Same as 'voxel' but in meters (not mm here) instead of voxels: mriXYZ = voxelXYZ * Voxsize % - scs : Based on: Nasion, left pre-auricular point (LPA), and right pre-auricular point (RPA). % Origin: Midway on the line joining LPA and RPA % Axis X: From the origin towards the Nasion (exactly through) @@ -122,7 +122,7 @@ scs2captrak = [tCapTrak.R, tCapTrak.T; 0 0 0 1]; end -% ===== CONVERT SRC => MRI ===== +% ===== CONVERT SRC => MRI (m) ===== % Evaluate the transformation to apply switch lower(src) case 'voxel' @@ -174,7 +174,7 @@ error(['Invalid coordinate system: ' src]); end -% ===== CONVERT MRI => DEST ===== +% ===== CONVERT MRI (m) => DEST ===== % Evaluate the transformation to apply switch lower(dest) case 'voxel' diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m index 82f890f9f..61b37b3f0 100644 --- a/toolbox/gui/figure_3d.m +++ b/toolbox/gui/figure_3d.m @@ -3954,18 +3954,22 @@ function ViewAxis(hFig, isVisible) isVisible = isempty(findobj(hAxes, 'Tag', 'AxisXYZ')); end if isVisible + % Works but now default zoom is too tight >:( % Get dimensions of current axes + set(hAxes, 'XLimitMethod', 'tight', 'YLimitMethod', 'tight', 'ZLimitMethod', 'tight'); XLim = get(hAxes, 'XLim'); - YLim = get(hAxes, 'XLim'); - ZLim = get(hAxes, 'XLim'); - d = max(abs([XLim(:); YLim(:); ZLim(:)])); + YLim = get(hAxes, 'YLim'); + ZLim = get(hAxes, 'ZLim'); % Draw axis lines + d = XLim(2) * 1.04; line([0 d], [0 0], [0 0], 'Color', [1 0 0], 'Marker', '>', 'Parent', hAxes, 'Tag', 'AxisXYZ'); + text(d * 1.04, 0, 0, 'X', 'Color', [1 0 0], 'Parent', hAxes, 'Tag', 'AxisXYZ'); + d = YLim(2) * 1.04; line([0 0], [0 d], [0 0], 'Color', [0 1 0], 'Marker', '>', 'Parent', hAxes, 'Tag', 'AxisXYZ'); + text(0, d * 1.04, 0, 'Y', 'Color', [0 1 0], 'Parent', hAxes, 'Tag', 'AxisXYZ'); + d = ZLim(2) * 1.04; line([0 0], [0 0], [0 d], 'Color', [0 0 1], 'Marker', '>', 'Parent', hAxes, 'Tag', 'AxisXYZ'); - text(d+0.002, 0, 0, 'X', 'Color', [1 0 0], 'Parent', hAxes, 'Tag', 'AxisXYZ'); - text(0, d+0.002, 0, 'Y', 'Color', [0 1 0], 'Parent', hAxes, 'Tag', 'AxisXYZ'); - text(0, 0, d+0.002, 'Z', 'Color', [0 0 1], 'Parent', hAxes, 'Tag', 'AxisXYZ'); + text(0, 0, d * 1.04, 'Z', 'Color', [0 0 1], 'Parent', hAxes, 'Tag', 'AxisXYZ'); % Enforce camera target at (0,0,0) % camtarget(hAxes, [0,0,0]); else diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index e88c8a76e..08617212f 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -923,10 +923,14 @@ iMriHist = find(strcmpi(sMri.History(:,3), 'Applied digitized anatomical fiducials'), 1, 'last'); end % Can also be reset, so check for 'import' action and ignore previous alignments. - iImport = find(strcmpi(ChannelMat.History(:,2), 'import'), 1, 'last'); + iImport = find(strcmpi(ChannelMat.History(:,2), 'import')); iAlign = find(strcmpi(ChannelMat.History(:,2), 'align')); - iAlign(iAlign < iImport) = []; - AlignType = 'none'; + iAlign(iAlign < iImport(end)) = []; + if numel(iImport) > 1 + AlignType = 'none/reset'; + else + AlignType = 'none'; + end while ~isempty(iAlign) % Check which adjustment was done last. switch lower(ChannelMat.History{iAlign(end),3}(1:5)) @@ -1039,13 +1043,28 @@ if ~isfield(ChannelMat, 'HeadPoints') return; end - % Get the three fiducials in the head points + % Get the three anatomical fiducials in the head points iNas = find(strcmpi(ChannelMat.HeadPoints.Label, 'Nasion') | strcmpi(ChannelMat.HeadPoints.Label, 'NAS')); iLpa = find(strcmpi(ChannelMat.HeadPoints.Label, 'Left') | strcmpi(ChannelMat.HeadPoints.Label, 'LPA')); iRpa = find(strcmpi(ChannelMat.HeadPoints.Label, 'Right') | strcmpi(ChannelMat.HeadPoints.Label, 'RPA')); if ~isempty(iNas) && ~isempty(iLpa) && ~isempty(iRpa) - ChannelMat.SCS.NAS = mean(ChannelMat.HeadPoints.Loc(:,iNas)', 1); + ChannelMat.SCS.NAS = mean(ChannelMat.HeadPoints.Loc(:,iNas)', 1); %#ok<*UDIM> ChannelMat.SCS.LPA = mean(ChannelMat.HeadPoints.Loc(:,iLpa)', 1); ChannelMat.SCS.RPA = mean(ChannelMat.HeadPoints.Loc(:,iRpa)', 1); end + % Do the same with head coils, used when exporting coregistration to BIDS + iHpiN = find(strcmpi(ChannelMat.HeadPoints.Label, 'HPI-N')); + iHpiL = find(strcmpi(ChannelMat.HeadPoints.Label, 'HPI-L')); + iHpiR = find(strcmpi(ChannelMat.HeadPoints.Label, 'HPI-R')); + if ~isempty(iHpiN) && ~isempty(iHpiL) && ~isempty(iHpiR) + ChannelMat.Native.NAS = mean(ChannelMat.HeadPoints.Loc(:,iHpiN)', 1); + ChannelMat.Native.LPA = mean(ChannelMat.HeadPoints.Loc(:,iHpiL)', 1); + ChannelMat.Native.RPA = mean(ChannelMat.HeadPoints.Loc(:,iHpiR)', 1); + end + % Get "current" SCS to Native transformation. + TmpChanMat = ChannelMat; + TmpChanMat.SCS = ChannelMat.Native; + % cs_compute doesn't change coordinates, only adds the R,T,Origin fields + [~, TmpChanMat] = cs_compute(TmpChanMat, 'scs'); + ChannelMat.Native = TmpChanMat.SCS; end diff --git a/toolbox/sensors/channel_align_manual.m b/toolbox/sensors/channel_align_manual.m index c846bb1c5..e919e53f6 100644 --- a/toolbox/sensors/channel_align_manual.m +++ b/toolbox/sensors/channel_align_manual.m @@ -190,7 +190,7 @@ % ===== DISPLAY HEAD POINTS ===== % Display head points -figure_3d('ViewHeadPoints', hFig, 1); +figure_3d('ViewHeadPoints', hFig, 1, 1); % visible and color-coded % Get patch and vertices hHeadPointsMarkers = findobj(hFig, 'Tag', 'HeadPointsMarkers'); hHeadPointsLabels = findobj(hFig, 'Tag', 'HeadPointsLabels'); @@ -203,7 +203,9 @@ HeadPointsHpiLoc = []; if isHeadPoints % More transparency to view points inside. - panel_surface('SetSurfaceTransparency', hFig, 1, 0.5); + panel_surface('SetSurfaceTransparency', hFig, 1, 0.4); + % Hide MEG helmet + set(hHelmetPatch, 'visible', 'off'); % Get markers positions HeadPointsMarkersLoc = get(hHeadPointsMarkers, 'Vertices'); % Hide HeadPoints when looking at EEG and number of EEG channels is the same as headpoints @@ -857,8 +859,6 @@ function AlignClose_Callback(varargin) [ChannelMat, Transf, iChannels] = GetCurrentChannelMat(); % Load original channel file ChannelMatOrig = in_bst_channel(gChanAlign.ChannelFile); - % Report (in command window) max head and sensor displacements from changes. - CheckCurrentAdjustments(ChannelMat, ChannelMatOrig); % Ask user to save changes (only if called as a callback) if (nargin == 3) @@ -902,6 +902,10 @@ function AlignClose_Callback(varargin) if isCancel return; end + % Report (in command window) max head and sensor displacements from changes. + if SaveChanges || (gChanAlign.isHeadPoints && ~strcmpi(Choice, 'No')) + process_adjust_coordinates('CheckCurrentAdjustments', ChannelMat, ChannelMatOrig); + end % Save changes to channel file and close figure if SaveChanges % Progress bar @@ -915,17 +919,13 @@ function AlignClose_Callback(varargin) [sStudy, iStudy] = bst_get('ChannelFile', gChanAlign.ChannelFile); % Reload study file db_reload_studies(iStudy); + % Apply to other recordings with same sensor locations in the same subject + CopyToOtherFolders(ChannelMatOrig, iStudy, Transf, iChannels); bst_progress('stop'); end - else - SaveChanges = 0; end % Only close figure gChanAlign.Figure3DCloseRequest_Bak(varargin{1:2}); - % Apply to other recordings with same sensor locations in the same subject - if SaveChanges - CopyToOtherFolders(ChannelMatOrig, iStudy, Transf, iChannels); - end end diff --git a/toolbox/sensors/channel_align_scs.m b/toolbox/sensors/channel_align_scs.m index ed85aa651..63ea7c0bc 100644 --- a/toolbox/sensors/channel_align_scs.m +++ b/toolbox/sensors/channel_align_scs.m @@ -117,6 +117,7 @@ sMri.SCS.LPA = (Transform(1:3,:) * [ChannelMat.SCS.LPA'; 1])'; sMri.SCS.RPA = (Transform(1:3,:) * [ChannelMat.SCS.RPA'; 1])'; % Then convert to MRI coordinates (mm), this is how sMri.SCS is saved. +% cs_convert mri is in meters sMri.SCS.NAS = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.NAS) .* 1000; sMri.SCS.LPA = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.LPA) .* 1000; sMri.SCS.RPA = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.RPA) .* 1000; From 9f3be2b7c6ce415522079c40912aa28f30987bf5 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:05:11 -0500 Subject: [PATCH 29/32] wip coregistration --- toolbox/core/bst_colormaps.m | 4 +-- .../functions/process_adjust_coordinates.m | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/toolbox/core/bst_colormaps.m b/toolbox/core/bst_colormaps.m index aefe462aa..4aea9d87a 100644 --- a/toolbox/core/bst_colormaps.m +++ b/toolbox/core/bst_colormaps.m @@ -1334,7 +1334,7 @@ function SetColormapRealMin(ColormapType, status) % Fire change notificiation to all figures (3DViz and Topography) FireColormapChanged(ColormapType); end -function SetMaxMode(ColormapType, maxmode, DisplayUnits) +function SetMaxMode(ColormapType, maxmode, DisplayUnits, varargin) % Parse inputs if (nargin < 3) || isempty(DisplayUnits) DisplayUnits = []; @@ -1345,7 +1345,7 @@ function SetMaxMode(ColormapType, maxmode, DisplayUnits) end % Custom: ask for custom values if strcmpi(maxmode, 'custom') - SetMaxCustom(ColormapType, DisplayUnits); + SetMaxCustom(ColormapType, DisplayUnits, varargin{:}); else % Update colormap sColormap = GetColormap(ColormapType); diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index 08617212f..ce2fcab0a 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -1000,7 +1000,7 @@ end -function [DistHead, DistSens] = CheckCurrentAdjustments(ChannelMat, ChannelMatRef) +function [DistHead, DistSens, Message] = CheckCurrentAdjustments(ChannelMat, ChannelMatRef) % Display max displacement from registration adjustments, in command window. % If second ChannelMat is provided as reference, get displacement between the two. isPrint = nargout == 0; @@ -1023,17 +1023,27 @@ % Implicitly using actual (MRI) SCS as reference, this includes all adjustments. DistHead = process_evt_head_motion('RigidDistances', ... [ChannelMat.SCS.NAS(:); ChannelMat.SCS.LPA(:); ChannelMat.SCS.RPA(:)]); - % Get equivalent transform for all adjustments to "undo" on sensors for comparison. - [~, ~, Transf] = process_adjust_coordinates('GetTransforms', ChannelMat); - Loc = [ChannelMat.Channel.Loc]; - % Inverse transf: subtract translation first, then rotate the "other way" (transpose). - LocRef = Transf(1:3,1:3)' * bsxfun(@minus, Loc, Transf(1:3,4)); - DistSens = max(sqrt(sum((Loc - LocRef).^2))); + % Get equivalent transform for all adjustments to "undo" on sensors for comparison. The + % adjustments we want come after 'Native=>Brainstorm/CTF' + iNatToScs = find(strcmpi(ChannelMat.TransfMegLabels, 'Native=>Brainstorm/CTF')); + if iNatToScs < numel(ChannelMat.TransfMeg) + Transf = eye(4); + for t = iNatToScs+1:numel(ChannelMat.TransfMeg) + Transf = ChannelMat.TransfMeg{t} * Transf; + end + Loc = [ChannelMat.Channel.Loc]; + % Inverse transf: subtract translation first, then rotate the "other way" (transpose). + LocRef = Transf(1:3,1:3)' * bsxfun(@minus, Loc, Transf(1:3,4)); + DistSens = max(sqrt(sum((Loc - LocRef).^2))); + else + DistSens = 0; + end end + Message = sprintf('BST> Max displacement for registration adjustment:\n head: %1.1f mm\n sensors: %1.1f cm\n', ... + DistHead*1000, DistSens*100); if isPrint - fprintf('BST> Max displacement for registration adjustment:\n head: %1.1f mm\n sensors: %1.1f mm\n', ... - DistHead*1000, DistSens*1000); + fprintf(Message); end end From e9d0e46ab4cf9669e01add1ed2b29c399597d732 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:10:35 -0400 Subject: [PATCH 30/32] adding coregistration export for BIDS --- toolbox/io/bst_save_coregistration.m | 212 ++++++++++++++++++ .../functions/process_adjust_coordinates.m | 32 ++- toolbox/sensors/channel_align_auto.m | 37 +-- toolbox/sensors/channel_align_scs.m | 28 ++- toolbox/sensors/channel_apply_transf.m | 6 +- 5 files changed, 260 insertions(+), 55 deletions(-) create mode 100644 toolbox/io/bst_save_coregistration.m diff --git a/toolbox/io/bst_save_coregistration.m b/toolbox/io/bst_save_coregistration.m new file mode 100644 index 000000000..0d06b8802 --- /dev/null +++ b/toolbox/io/bst_save_coregistration.m @@ -0,0 +1,212 @@ +function [isSuccess, OutFilesMri, OutFilesMeg] = bst_save_coregistration(iSubjects, isBids) +% Save MRI-MEG coregistration info in imported raw BIDS dataset, or MRI fiducials only if not BIDS. +% +% Save MRI-MEG coregistration by adding AnatomicalLandmarkCoordinates to the +% _T1w.json MRI metadata, in 0-indexed voxel coordinates, and to the +% _coordsystem.json files for functional data, in native coordinates (e.g. CTF). +% The points used are the anatomical fiducials marked in Brainstorm on the MRI +% that define the Brainstorm subject coordinate system (SCS). +% +% If the raw data is not BIDS, the anatomical fiducials are saved in a +% fiducials.m file next to the raw MRI file, in Brainstorm MRI coordinates. +% +% Discussion about saving MRI-MEG coregistration in BIDS: +% https://groups.google.com/g/bids-discussion/c/BeyUeuNGl7I + +if nargin < 2 || isempty(isBids) + isBids = false; +end +sSubjects = bst_get('ProtocolSubjects'); +if nargin < 1 || isempty(iSubjects) + % Try to get all subjects from currently loaded protocol. + nSub = numel(sSubjects.Subject); + iSubjects = 1:nSub; +else + nSub = numel(iSubjects); +end + +bst_progress('start', 'Save co-registration', ' ', 0, nSub); + +OutFilesMri = cell(nSub, 1); +OutFilesMeg = cell(nSub, 1); +isSuccess = false(nSub, 1); +BidsRoot = ''; +for iOutSub = 1:nSub + iSub = iSubjects(iOutSub); + % Get anatomical file. + if ~contains(sSubjects.Subject(iSub).Anatomy(sSubjects.Subject(iSub).iAnatomy).Comment, 'MRI', 'ignorecase', true) && ... + ~contains(sSubjects.Subject(iSub).Anatomy(sSubjects.Subject(iSub).iAnatomy).Comment, 't1w', 'ignorecase', true) + warning('Selected anatomy is not ''MRI''. Skipping subject %s.', sSubjects.Subject(iSub).Name); + continue; + end + sMri = load(file_fullpath(sSubjects.Subject(iSub).Anatomy(sSubjects.Subject(iSub).iAnatomy).FileName)); + ImportedFile = strrep(sMri.History{1,3}, 'Import from: ', ''); + if ~exist(ImportedFile, 'file') + warning('Imported anatomy file not found. Skipping subject %s.', sSubjects.Subject(iSub).Name); + continue; + end + % Get all linked raw data files. + sStudies = bst_get('StudyWithSubject', sSubjects.Subject(iSub).FileName); + if isBids + if isempty(BidsRoot) + BidsRoot = bst_fileparts(bst_fileparts(bst_fileparts(ImportedFile))); + while ~exist(fullfile(BidsRoot, 'dataset_description.json'), 'file') + if isempty(BidsRoot) || ~exist(BidsRoot, 'dir') + error('Cannot find BIDS root folder and dataset_description.json file; subject %s.', sSubjects.Subject(iSub).Name); + end + BidsRoot = bst_fileparts(BidsRoot); + end + end + + % MRI _t1w.json + % Save anatomical landmarks in Nifti voxel coordinates + [MriPath, MriName, MriExt] = bst_fileparts(ImportedFile); + if strcmpi(MriExt, '.gz') + [~, MriName, MriExt2] = fileparts(MriName); + MriExt = [MriExt2, MriExt]; %#ok + end + if ~strncmpi(MriExt, '.nii', 4) + warning('Imported anatomy not BIDS. Skipping subject %s.', sSubjects.Subject(iSub).Name); + continue; + end + MriJsonFile = fullfile(MriPath, [MriName, '.json']); + if ~exist(MriJsonFile, 'file') + warning('Imported anatomy BIDS json file not found. Skipping subject %s.', sSubjects.Subject(iSub).Name); + continue; + end + sMriJson = bst_jsondecode(MriJsonFile, false); + BstFids = {'NAS', 'LPA', 'RPA', 'AC', 'PC', 'IH'}; + isLandmarksFound = true; + for iFid = 1:numel(BstFids) + if iFid < 4 + CS = 'SCS'; + else + CS = 'NCS'; + end + Fid = BstFids{iFid}; + % Voxel coordinates (Nifti: RAS and 0-indexed) + % Bst MRI coordinates are in mm and voxels are 1-indexed, so subtract 1 voxel after going from mm to voxels. + if isfield(sMri, CS) && isfield(sMri.(CS), Fid) && ~isempty(sMri.(CS).(Fid)) && any(sMri.(CS).(Fid)) + % Round to 0.001 voxel. + sMriJson.AnatomicalLandmarkCoordinates.(Fid) = round(1000 * (sMri.(CS).(Fid)./sMri.Voxsize - 1)) / 1000; + else + isLandmarksFound = false; + break; + end + end + if ~isLandmarksFound + warning('MRI landmark coordinates not found. Skipping subject %s.', sSubjects.Subject(iSub).Name); + continue; + end + WriteJson(MriJsonFile, sMriJson); + OutFilesMri{iOutSub} = MriJsonFile; + + % MEG _coordsystem.json + % Save MRI anatomical landmarks in SCS coordinates and link to MRI. + % This includes coregistration refinement using head points, if used. + + % Convert from mri to scs. + for iFid = 1:3 + Fid = BstFids{iFid}; + % cs_convert mri is in meters + sMriScs.SCS.(Fid) = cs_convert(sMri, 'mri', 'scs', sMri.SCS.(Fid) ./ 1000); + end + sMriNative = sMriScs; + + for iStudy = 1:numel(sStudies) + % Is it a link to raw file? + isLinkToRaw = false; + for iData = 1:numel(sStudies(iStudy).Data) + if strcmpi(sStudies(iStudy).Data(iData).DataType, 'raw') + isLinkToRaw = true; + break; + end + end + if ~isLinkToRaw + continue; + end + + % Find MEG _coordsystem.json + Link = load(file_fullpath(sStudies(iStudy).Data(iData).FileName)); + if ~exist(Link.F.filename, 'file') + warning('Missing raw MEG file. Skipping study %s.', Link.F.filename); + continue; + end + [MegPath, MegName, MegExt] = bst_fileparts(Link.F.filename); + if strcmpi(MegExt, '.meg4') + [MegPath, MegName, MegExt] = bst_fileparts(MegPath); + end + MegCoordJsonFile = file_find(MegPath, '*_coordsystem.json', 1, false); % max depth 1, not just one file + + if isempty(MegCoordJsonFile) + warning('Imported MEG BIDS _coordsystem.json file not found. Skipping study %s.', Link.F.filename); + continue; + end + + ChannelMat = in_bst_channel(sStudies(iStudy).Channel.FileName); + % ChannelMat.SCS are *digitized* anatomical landmarks (if present, otherwise might be + % digitized head coils) in Brainstorm/SCS coordinates (CTF from anatomical landmarks). + % Not updated after refine with head points, so we don't rely on them but use those + % saved in sMri. + % + % We applied MRI=>SCS from sMri to MRI anat landmarks above, and now need to apply + % SCS=>Native from ChannelMat. We ignore head motion related adjustments, which are + % dataset specific. We need original raw Native coordinates. + ChannelMat = process_adjust_coordinates('UpdateChannelMatScs', ChannelMat); + % Convert from (possibly adjusted) SCS to Native, and m to cm. + for iFid = 1:3 + Fid = BstFids{iFid}; + sMriNative.SCS.(Fid)(:) = 100 * [ChannelMat.Native.R, ChannelMat.Native.T] * [sMriScs.SCS.(Fid)'; 1]; + end + + for c = 1:numel(MegCoordJsonFile) + sMegJson = bst_jsondecode(MegCoordJsonFile{c}); + if ~isfield(sMegJson, 'IntendedFor') || isempty(sMegJson.IntendedFor) + sMegJson.IntendedFor = strrep(ImportedFile, [BidsRoot filesep], 'bids::'); + end + for iFid = 1:3 + Fid = BstFids{iFid}; + %if isfield(sMri, 'SCS') && isfield(sMri.SCS, Fid) && ~isempty(sMri.SCS.(Fid)) && any(sMri.SCS.(Fid)) + % Round to um. + sMegJson.AnatomicalLandmarkCoordinates.(Fid) = round(sMriNative.SCS.(Fid) * 10000) / 10000; + end + sMegJson.AnatomicalLandmarkCoordinateSystem = 'CTF'; + sMegJson.AnatomicalLandmarkCoordinateUnits = 'cm'; + %sMegJson.AnatomicalLandmarkCoordinateSystemDescription = 'Based on the digitized locations of the head coils. The origin is exactly between the left ear head coil (coilL near LPA) and the right ear head coil (coilR near RPA); the X-axis goes towards the nasion head coil (coilN near NAS); the Y-axis goes approximately towards coilL, orthogonal to X and in the plane spanned by the 3 head coils; the Z-axis goes approximately towards the vertex, orthogonal to X and Y'; + %sMegJson.HeadCoilCoordinateSystemDescription = sMegJson.AnatomicalLandmarkCoordinateSystemDescription; + [~, isMriUpdated, isMriMatch, ChannelMat] = process_adjust_coordinates('CheckPrevAdjustments', ChannelMat, sMri); + if ~isfield(sMegJson, 'FiducialsDescription') + sMegJson.FiducialsDescription = ''; + end + if isMriUpdated && isMriMatch + AddFidDescrip = ' The anatomical landmarks saved here match those in the associated T1w image (see IntendedFor field). They correspond to the anatomical landmarks from the digitized head points, averaged if measured more than once. Coregistration with the T1w image was performed with Brainstorm before defacing, initially with an automatic procedure fitting head points to the scalp surface, but often adjusted manually, and validated with pictures of MEG head coils on the participant. As such, these landmarks and the corresponding alignment should be preferred.'; + else + AddFidDescrip = ' The anatomical landmarks saved here match those in the associated T1w image (see IntendedFor field). They do not correspond to the digitized landmarks from this session. Coregistration with the T1w image was performed with Brainstorm before defacing, initially with an automatic procedure fitting head points to the scalp surface, but often adjusted manually, and validated with pictures of MEG head coils on the participant. As such, these landmarks and the corresponding alignment should be preferred.'; + end + if isempty(strfind(sMegJson.FiducialsDescription, AddFidDescrip)) + sMegJson.FiducialsDescription = strtrim([sMegJson.FiducialsDescription, AddFidDescrip]); + end + WriteJson(MegCoordJsonFile{c}, sMegJson); + OutFilesMeg{iOutSub}{c} = MegCoordJsonFile{c}; + end + end + else + % Not BIDS, save in fiducials.m file. + FidsFile = fullfile(bst_fileparts(ImportedFile), 'fiducials.m'); + FidsFile = figure_mri('SaveFiducialsFile', sMri, FidsFile); + if ~exist(FidsFile, 'file') + warning('Fiducials.m file not written for subject %s.', sSubjects.Subject(iSub).Name); + continue; + end + OutFilesMri{iOutSub} = FidsFile; + end + + isSuccess(iOutSub) = true; + bst_progress('inc', 1); +end % subject loop + +bst_progress('stop'); + +end + + diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index ce2fcab0a..fb8e005a4 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -70,9 +70,8 @@ sProcess.options.tolerance.Value = {0, '%', 0}; sProcess.options.tolerance.Class = 'Refine'; sProcess.options.scs.Type = 'checkbox'; - sProcess.options.scs.Comment = 'Also ajust MRI nasion and ear points.'; + sProcess.options.scs.Comment = 'Replace MRI nasion and ear points with digitized landmarks (cannot undo).'; sProcess.options.scs.Value = 0; - sProcess.options.scs.Class = 'Refine'; sProcess.options.remove.Type = 'checkbox'; sProcess.options.remove.Comment = 'Remove selected adjustments (if present) instead of adding them.'; sProcess.options.remove.Value = 0; @@ -173,8 +172,8 @@ end % TransfLabel loop % We cannot change back the MRI fiducials, but in order to be able to update it again - % from digitized fids, we must edit the MRI history. - if sProcess.options.points.Value && sProcess.options.scs.Value + % from digitized fids without warnings, edit the MRI history. + if sProcess.options.scs.Value % Get subject in database, with subject directory sSubject = bst_get('Subject', sInputs(iFile).SubjectFile); MriFile = sSubject.Anatomy(sSubject.iAnatomy).FileName; @@ -221,7 +220,7 @@ Tolerance = sProcess.options.tolerance.Value{1} / 100; end [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(sInputs(iFile).ChannelFile, ... - ChannelMat, isWarning, 0, Tolerance, sProcess.options.scs.Value); % No confirmation + ChannelMat, isWarning, 0, Tolerance); % No confirmation % ChannelFile needed to find subject and scalp surface, but not used otherwise when % ChannelMat is provided. if ~isempty(strReport) @@ -234,6 +233,23 @@ end % refine registration with head points + % ---------------------------------------------------------------- + if ~sProcess.options.remove.Value && sProcess.options.scs.Value + % TODO Maybe make this a separate process, since it should only run on one channel file + % per subject. + + % Get subject + sSubject = bst_get('Subject', sInputs(iFile).SubjectFile); + % Check if default anatomy. + if sSubject.UseDefaultAnat + bst_report('Error', sProcess, sInputs(iFile), ... + 'Digitized nasion and ear points cannot be applied to default anatomy.'); + continue; + end + DigToMriTransf = channel_align_scs(ChannelFile, eye(4), true, false); % interactive warnings but no confirmation + % TODO Verify if it worked. + end + % ---------------------------------------------------------------- % Save channel file. bst_save(file_fullpath(sInputs(iFile).ChannelFile), ChannelMat, 'v7'); @@ -983,16 +999,14 @@ if any(abs(sMri.SCS.NAS - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.NAS) .* 1000) > 1e-3) || ... any(abs(sMri.SCS.LPA - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.LPA) .* 1000) > 1e-3) || ... any(abs(sMri.SCS.RPA - cs_convert(sMri, 'scs', 'mri', ChannelMat.SCS.RPA) .* 1000) > 1e-3) + isMriMatch = false; if isPrint disp('BST> MRI fiducials previously updated, but different than current digitized fiducials.'); - else - isMriMatch = false; end else + isMriMatch = true; if isPrint disp('BST> MRI fiducials previously updated, and match current digitized fiducials.'); - else - isMriMatch = true; end end end diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m index aa0f7931d..71d8fcd48 100644 --- a/toolbox/sensors/channel_align_auto.m +++ b/toolbox/sensors/channel_align_auto.m @@ -1,7 +1,7 @@ -function [ChannelMat, R, T, isSkip, isUserCancel, strReport, tolerance] = channel_align_auto(ChannelFile, ChannelMat, isWarning, isConfirm, tolerance, isAdjustScs) +function [ChannelMat, R, T, isSkip, isUserCancel, strReport, tolerance] = channel_align_auto(ChannelFile, ChannelMat, isWarning, isConfirm, tolerance) % CHANNEL_ALIGN_AUTO: Aligns the channels to the scalp using Polhemus points. % -% USAGE: [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(ChannelFile, ChannelMat=[], isWarning=1, isConfirm=1, tolerance=0, isAdjustScs=0) +% USAGE: [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(ChannelFile, ChannelMat=[], isWarning=1, isConfirm=1, tolerance=0) % % DESCRIPTION: % Aligns the channels to the scalp using Polhemus points stored in channel structure. @@ -18,7 +18,6 @@ % - isWarning : If 1, display warning in case of errors (default = 1) % - isConfirm : If 1, ask the user for confirmation before proceeding % - tolerance : Percentage of outliers head points, ignored in the final fit -% - isAdjustScs : If 1 and not already done for this subject, update MRI to use digitized nasion and ear points. % % OUTPUTS: % - ChannelMat : The same ChannelMat structure input in, with the head points and sensors rotated and translated to match the head points to the scalp. @@ -49,12 +48,10 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Syed Ashrafulla, 2009, Francois Tadel, 2009-2021, Marc Lalancette 2022 +% Authors: Syed Ashrafulla, 2009 +% Francois Tadel, 2009-2021 %% ===== PARSE INPUTS ===== -if (nargin < 6) || isempty(isAdjustScs) - isAdjustScs = 0; -end if (nargin < 5) || isempty(tolerance) tolerance = 0; end @@ -96,27 +93,12 @@ end % M x 3 matrix of head points HP = double(HeadPoints.Loc'); -% % Add anatomical points. -% if isfield(ChannelMat, 'SCS') && all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) && ... -% (length(ChannelMat.SCS.NAS) == 3) && (length(ChannelMat.SCS.LPA) == 3) && (length(ChannelMat.SCS.RPA) == 3) -% HP(end+1,:) = ChannelMat.SCS.NAS; -% HP(end+1,:) = ChannelMat.SCS.LPA; -% HP(end+1,:) = ChannelMat.SCS.RPA; -% end %% ===== LOAD SCALP SURFACE ===== % Get study sStudy = bst_get('ChannelFile', ChannelFile); % Get subject -[sSubject, iSubject] = bst_get('Subject', sStudy.BrainStormSubject); -% Check if default anatomy. (Usually also checked before calling this function.) -if sSubject.UseDefaultAnat - if isWarning - bst_error('Digitized nasion and ear points cannot be applied to default anatomy.', 'Automatic EEG-MEG/MRI registration', 0); - end - bst_progress('stop'); - return -end +sSubject = bst_get('Subject', sStudy.BrainStormSubject); if isempty(sSubject) || isempty(sSubject.iScalp) if isWarning bst_error('No scalp surface available for this subject', 'Automatic EEG-MEG/MRI registration', 0); @@ -213,15 +195,6 @@ DigToMriTransf(1:3,1:3) = R; DigToMriTransf(1:3,4) = T; - -%% ===== ADJUST MRI FIDUCIALS AND SCS ===== -if isAdjustScs - DigToMriTransf = channel_align_scs(ChannelFile, DigToMriTransf, isWarning, isConfirm); - R = DigToMriTransf(1:3,1:3); - T = DigToMriTransf(1:3,4); -end - - %% ===== ROTATE SENSORS AND HEADPOINTS ===== if ~isequal(DigToMriTransf, eye(4)) for i = 1:length(ChannelMat.Channel) diff --git a/toolbox/sensors/channel_align_scs.m b/toolbox/sensors/channel_align_scs.m index 63ea7c0bc..ee520a97d 100644 --- a/toolbox/sensors/channel_align_scs.m +++ b/toolbox/sensors/channel_align_scs.m @@ -6,13 +6,13 @@ % DESCRIPTION: % After modifying registration between digitized head points and MRI (with "refine with head % points" or manually), this function allows saving the change in the MRI fiducials so that -% they exactly match the digitized anatomical points (nasion and ears), instead of saving a -% registration adjustment transformation for a single functional dataset. This affects all -% files registered to the MRI and should therefore be done as one of the first steps after -% importing, and with only one set of digitized points (one session). Surfaces are adjusted to -% maintain alignment with the MRI. Additional sessions for the same subject, with separate -% digitized points, will still need the usual "per dataset" registration adjustment to align -% with the same MRI. +% they exactly match the digitized anatomical points (nasion and ears). This would replace +% having to save a registration adjustment transformation for each functional dataset sharing +% this set of digitized points. This affects all files registered to the MRI and should +% therefore be done as one of the first steps after importing, and with only one set of +% digitized points (one session). Surfaces are adjusted to maintain alignment with the MRI. +% Additional sessions for the same subject, with separate digitized points, will still need +% the usual "per dataset" registration adjustment to align with the same MRI. % % This function will not modify an MRI that it changed previously without user confirmation % (if both isWarning and isConfirm are false). In that case, the Transform is returned unaltered. @@ -21,6 +21,9 @@ % - ChannelFile : Channel file to align with its anatomy % - Transform : Transformation matrix from digitized SCS coordinates to MRI SCS coordinates, % after some alignment is made (auto or manual) and the two no longer match. +% This transform should not already be saved in the ChannelFile, though the +% file may already contain similar adjustments, in which case Transform would be +% an additional adjustment to add. % - isWarning : If 1, display warning in case of errors, or if this was already done % previously for this MRI. % - isConfirm : If 1, ask the user for confirmation before proceeding. @@ -51,7 +54,9 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Marc Lalancette 2022 +% Authors: Marc Lalancette 2022-2023 + +% TODO if Transform is missing, get equivalent from ChannelMat, from all auto/manual adjustments. isCancel = false; % Get study @@ -110,7 +115,8 @@ end end -% Convert to MRI SCS coordinates. +% Convert digitized fids to MRI SCS coordinates. +% Here, ChannelMat.SCS already may contain some auto/manual adjustment, and we're adding a new one. % To do this we need to apply the transformation provided. sMri = sMriOld; sMri.SCS.NAS = (Transform(1:3,:) * [ChannelMat.SCS.NAS'; 1])'; @@ -121,7 +127,7 @@ sMri.SCS.NAS = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.NAS) .* 1000; sMri.SCS.LPA = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.LPA) .* 1000; sMri.SCS.RPA = cs_convert(sMriOld, 'scs', 'mri', sMri.SCS.RPA) .* 1000; -% Re-compute transformation +% Re-compute transformation in this struct [~, sMri] = cs_compute(sMri, 'scs'); % Compare with existing MRI fids, replace if changed (> 1um), and update surfaces. @@ -194,7 +200,7 @@ % Progress bar bst_progress('start', 'Align sensors', 'Updating other datasets...'); % Update files - channel_apply_transf(ChannelFiles, Transf, iChannels, 1); + channel_apply_transf(ChannelFiles, Transf); % Give report to the user bst_progress('stop'); java_dialog('msgbox', sprintf('Updated %d additional file(s):\n%s', length(ChannelFiles), strMsg)); diff --git a/toolbox/sensors/channel_apply_transf.m b/toolbox/sensors/channel_apply_transf.m index 75f312b62..a7871c45b 100644 --- a/toolbox/sensors/channel_apply_transf.m +++ b/toolbox/sensors/channel_apply_transf.m @@ -47,7 +47,7 @@ if isnumeric(Transf) R = Transf(1:3,1:3); T = Transf(1:3,4); - Transf = @(Loc)(R * Loc + T * ones(1, size(Loc,2))); + TransfFunc = @(Loc)(R * Loc + T * ones(1, size(Loc,2))); else R = []; end @@ -82,7 +82,7 @@ Orient = ChannelMat.Channel(iChan(i)).Orient; % Update location if ~isempty(Loc) && ~isequal(Loc, [0;0;0]) - ChannelMat.Channel(iChan(i)).Loc = Transf(Loc); + ChannelMat.Channel(iChan(i)).Loc = TransfFunc(Loc); end % Update orientation if ~isempty(Orient) && ~isequal(Orient, [0;0;0]) @@ -95,7 +95,7 @@ end % If needed: transform the digitized head points if isHeadPoints && ~isempty(ChannelMat.HeadPoints.Loc) - ChannelMat.HeadPoints.Loc = Transf(ChannelMat.HeadPoints.Loc); + ChannelMat.HeadPoints.Loc = TransfFunc(ChannelMat.HeadPoints.Loc); end % If a TransfMeg field with translations/rotations available From 6ab9794022061f615812ee074c74b82b09d2c9e8 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:57:50 -0400 Subject: [PATCH 31/32] fix apply digitized fids to MRI --- .../functions/process_adjust_coordinates.m | 13 +++------- toolbox/sensors/channel_align_manual.m | 1 + toolbox/sensors/channel_align_scs.m | 26 +++++++++++++++++-- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m index fb8e005a4..71d7c73cb 100644 --- a/toolbox/process/functions/process_adjust_coordinates.m +++ b/toolbox/process/functions/process_adjust_coordinates.m @@ -236,18 +236,13 @@ % ---------------------------------------------------------------- if ~sProcess.options.remove.Value && sProcess.options.scs.Value % TODO Maybe make this a separate process, since it should only run on one channel file - % per subject. + % per subject. Possibly have hidden option for no interactive warnings. - % Get subject - sSubject = bst_get('Subject', sInputs(iFile).SubjectFile); - % Check if default anatomy. - if sSubject.UseDefaultAnat - bst_report('Error', sProcess, sInputs(iFile), ... - 'Digitized nasion and ear points cannot be applied to default anatomy.'); + [~, isCancel] = channel_align_scs(ChannelFile, eye(4), true, false); % interactive warnings but no confirmation + if isCancel continue; end - DigToMriTransf = channel_align_scs(ChannelFile, eye(4), true, false); % interactive warnings but no confirmation - % TODO Verify if it worked. + % TODO Verify end % ---------------------------------------------------------------- diff --git a/toolbox/sensors/channel_align_manual.m b/toolbox/sensors/channel_align_manual.m index f6dc4cedf..77f57e157 100644 --- a/toolbox/sensors/channel_align_manual.m +++ b/toolbox/sensors/channel_align_manual.m @@ -746,6 +746,7 @@ function AlignKeyPress_Callback(hFig, keyEvent) end % Ask if needed to update also the other modalities if isempty(isAll) + % TODO We might have < 10 but still want to update electrodes. Verify instead if there are real EEG (not just ECG EOG) if (gChanAlign.isMeg || gChanAlign.isNirs) && (length(iEeg) > 10) isAll = java_dialog('confirm', 'Do you want to apply the same transformation to the EEG electrodes ?', 'Align sensors'); elseif ~gChanAlign.isMeg && ~isempty(iMeg) diff --git a/toolbox/sensors/channel_align_scs.m b/toolbox/sensors/channel_align_scs.m index ee520a97d..450cc3d32 100644 --- a/toolbox/sensors/channel_align_scs.m +++ b/toolbox/sensors/channel_align_scs.m @@ -63,6 +63,17 @@ sStudy = bst_get('ChannelFile', ChannelFile); % Get subject [sSubject, iSubject] = bst_get('Subject', sStudy.BrainStormSubject); +% Check if default anatomy. +if sSubject.UseDefaultAnat + Message = 'Digitized nasion and ear points cannot be applied to default anatomy.'; + if isWarning + bst_error(Message, 'Apply digitized anatomical fiducials to MRI', 0); + else + disp(Message); + end + isCancel = true; + return; +end % Get Channels ChannelMat = in_bst_channel(ChannelFile); @@ -70,10 +81,11 @@ % Note that these coordinates are NOT currently updated when doing refine with head points (below). % They are in "initial SCS" coordinates, updated in channel_detect_type. if ~all(isfield(ChannelMat.SCS, {'NAS','LPA','RPA'})) || ~(length(ChannelMat.SCS.NAS) == 3) || ~(length(ChannelMat.SCS.LPA) == 3) || ~(length(ChannelMat.SCS.RPA) == 3) + Message = 'Digitized nasion and ear points not found.'; if isWarning - bst_error('Digitized nasion and ear points not found.', 'Apply digitized anatomical fiducials to MRI', 0); + bst_error(Message, 'Apply digitized anatomical fiducials to MRI', 0); else - disp('BST> Digitized nasion and ear points not found.'); + disp(Message); end isCancel = true; return; @@ -114,6 +126,16 @@ return; end end +% If EEG, warn that only linear transformation would be saved this way. +if ~isempty([good_channel(ChannelMat.Channel, [], 'EEG'), good_channel(ChannelMat.Channel, [], 'SEEG'), good_channel(ChannelMat.Channel, [], 'ECOG')]) + [Proceed, isCancel] = java_dialog('confirm', ['Updating the MRI fiducial points NAS/LPA/RPA will only save' 10 ... + 'global rotations and translations. Any other changes to EEG channels will be lost.' 10 10 ... + 'Proceed and update MRI now?' 10], 'Head points/anatomy registration'); + if ~Proceed || isCancel + isCancel = true; + return; + end +end % Convert digitized fids to MRI SCS coordinates. % Here, ChannelMat.SCS already may contain some auto/manual adjustment, and we're adding a new one. From ca6c15292ff35c9f75066d713b540b308b043d13 Mon Sep 17 00:00:00 2001 From: Marc Lalancette <31040756+Moo-Marc@users.noreply.github.com> Date: Wed, 27 Sep 2023 18:39:52 -0400 Subject: [PATCH 32/32] coregistration branch cleanup --- toolbox/db/db_set_channel.m | 8 +------- toolbox/process/functions/process_headpoints_refine.m | 5 +---- toolbox/process/functions/process_import_bids.m | 10 ++-------- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/toolbox/db/db_set_channel.m b/toolbox/db/db_set_channel.m index 0d282dba0..d866171e4 100644 --- a/toolbox/db/db_set_channel.m +++ b/toolbox/db/db_set_channel.m @@ -16,7 +16,6 @@ % - ChannelAlign : 0, do not perform automatic headpoints-based alignment % 1, perform automatic alignment after user confirmation % 2, perform automatic alignment without user confirmation -% 3, as 2, but also updating MRI SCS from digitized points % - Tolerance : Percentage of outliers head points, ignored in the final fit % % OUTPUT: @@ -181,12 +180,7 @@ end % Call automatic registration for MEG - if ChannelAlign >= 3 - % Also adjust MRI SCS from digitized points. - [ChannelMat, R, T, isSkip, isUserCancel, strReport, Tolerance] = channel_align_auto(OutputFile, [], 0, isConfirm, Tolerance, 1); - else - [ChannelMat, R, T, isSkip, isUserCancel, strReport, Tolerance] = channel_align_auto(OutputFile, [], 0, isConfirm, Tolerance); - end + [ChannelMat, R, T, isSkip, isUserCancel, strReport, Tolerance] = channel_align_auto(OutputFile, [], 0, isConfirm, Tolerance); % User validated: keep this answer for the next round (force alignment for next call) if ~isSkip if isUserCancel || isempty(ChannelMat) diff --git a/toolbox/process/functions/process_headpoints_refine.m b/toolbox/process/functions/process_headpoints_refine.m index 5d862fbc8..f6e4f6db9 100644 --- a/toolbox/process/functions/process_headpoints_refine.m +++ b/toolbox/process/functions/process_headpoints_refine.m @@ -47,9 +47,6 @@ sProcess.options.tolerance.Comment = 'Tolerance (outlier points to ignore):'; sProcess.options.tolerance.Type = 'value'; sProcess.options.tolerance.Value = {0, '%', 0}; - sProcess.options.scs.Type = 'checkbox'; - sProcess.options.scs.Comment = 'Also ajust MRI nasion and ear points.'; - sProcess.options.scs.Value = 0; end @@ -68,7 +65,7 @@ % Loop on all the channel files for i = 1:length(uniqueChan) % Refine registration - [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(uniqueChan{i}, [], 0, 0, tolerance, sProcess.options.scs.Value); + [ChannelMat, R, T, isSkip, isUserCancel, strReport] = channel_align_auto(uniqueChan{i}, [], 0, 0, tolerance); if ~isempty(strReport) bst_report('Info', sProcess, sInputs(iUniqFiles(i)), strReport); end diff --git a/toolbox/process/functions/process_import_bids.m b/toolbox/process/functions/process_import_bids.m index d3744f8d9..5997290c5 100644 --- a/toolbox/process/functions/process_import_bids.m +++ b/toolbox/process/functions/process_import_bids.m @@ -113,11 +113,6 @@ sProcess.options.channelalign.Comment = 'Align sensors using headpoints'; sProcess.options.channelalign.Type = 'checkbox'; sProcess.options.channelalign.Value = 1; - sProcess.options.channelalign.Controller = 'Align'; - sProcess.options.scs.Type = 'checkbox'; - sProcess.options.scs.Comment = 'Also ajust MRI nasion and ear points from digitized points.'; - sProcess.options.scs.Value = 1; - sProcess.options.scs.Class = 'Align'; end @@ -146,8 +141,7 @@ end % Other options OPTIONS.isInteractive = 0; - % 2=align without confirmation, 3=also adjust MRI SCS from digitized points - OPTIONS.ChannelAlign = (2 + double(sProcess.options.scs.Value)) * double(sProcess.options.channelalign.Value); + OPTIONS.ChannelAlign = 2 * double(sProcess.options.channelalign.Value); OPTIONS.SelectedSubjects = strtrim(str_split(sProcess.options.selectsubj.Value, ',')); OPTIONS.isGroupSessions = sProcess.options.groupsessions.Value; OPTIONS.MniMethod = sProcess.options.mni.Value; @@ -641,7 +635,7 @@ % Import options ImportOptions = db_template('ImportOptions'); ImportOptions.ChannelReplace = 1; - ImportOptions.ChannelAlign = OPTIONS.ChannelAlign * ~sSubject.UseDefaultAnat; + ImportOptions.ChannelAlign = 2 * (OPTIONS.ChannelAlign >= 1) * ~sSubject.UseDefaultAnat; ImportOptions.DisplayMessages = OPTIONS.isInteractive; ImportOptions.EventsMode = 'ignore'; ImportOptions.EventsTrackMode = 'value';