From 055e9ba8fdfe4e7825a5c565fbcfd72f6e0c74cf Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 6 Jul 2024 01:31:41 -0400 Subject: [PATCH 01/10] Add process --- .../functions/process_combine_recordings.m | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 toolbox/process/functions/process_combine_recordings.m diff --git a/toolbox/process/functions/process_combine_recordings.m b/toolbox/process/functions/process_combine_recordings.m new file mode 100644 index 000000000..90f1eb3ac --- /dev/null +++ b/toolbox/process/functions/process_combine_recordings.m @@ -0,0 +1,162 @@ +function varargout = process_combine_recordings(varargin) +% process_combine_recordings: Combine multiple synchronized signals into +% one recording (resampling the signals to the highest sampling frequency) + +% @============================================================================= +% 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: Edouard Delaire, 2024 +% Raymundo Cassani, 2024 + +eval(macro_method); +end + + +%% ===== GET DESCRIPTION ===== +function sProcess = GetDescription() + % Description the process + sProcess.Comment = 'Combine files'; + sProcess.Category = 'Custom'; + sProcess.SubGroup = 'Synchronize'; + sProcess.Index = 682; + % Definition of the input accepted by this process + sProcess.InputTypes = { 'raw'}; + sProcess.OutputTypes = { 'raw'}; + sProcess.nInputs = 1; + sProcess.nMinFiles = 2; + + +end + + +%% ===== FORMAT COMMENT ===== +function Comment = FormatComment(sProcess) %#ok + Comment = sProcess.Comment; +end + + +%% ===== RUN ===== +function OutputFiles = Run(sProcess, sInputs) + OutputFiles = {}; + + % === Sync event management === % + nInputs = length(sInputs); + fs = zeros(1, nInputs); + sOldTiming = cell(1, nInputs); + sIdxChAn = cell(1, nInputs); + + bst_progress('start', 'Combining files', 'Loading data...', 0, 3*nInputs); + + % Get Time vector, events and sampling frequency for each file + for iInput = 1:nInputs + + sDataRaw = in_bst_data(sInputs(iInput).FileName, 'Time', 'F'); + sOldTiming{iInput}.Time = sDataRaw.Time; + sOldTiming{iInput}.Events = sDataRaw.F.events; + + fs(iInput) = 1/(sOldTiming{iInput}.Time(2) - sOldTiming{iInput}.Time(1)); % in Hz + end + + bst_progress('inc', nInputs); + bst_progress('text', 'Synchronizing...'); + + % First Input is the one wiht highest sampling frequency + [~, im] = max(fs); + sInputs([1, im]) = sInputs([im, 1]); + sOldTiming([1, im]) = sOldTiming([im, 1]); + fs([1, im]) = fs([im, 1]); + + % Compute shifiting between file i and first file + new_times = sOldTiming{1}.Time; + + newCondition = [sInputs(iInput).Condition '_combined']; + iNewStudy = db_add_condition(sInputs(iInput).SubjectName, newCondition); + sNewStudy = bst_get('Study', iNewStudy); + + + % Save channel definition + bst_progress('text', 'Combining channels files...'); + + NewChannelMat = in_bst_channel(sInputs(1).ChannelFile); + + sIdxChAn{1} = 1:length(NewChannelMat.Channel); + % Save sync data to file + for iInput = 2:nInputs + ChannelMat = in_bst_channel(sInputs(iInput).ChannelFile); + + sIdxChAn{iInput} = length(NewChannelMat.Channel) + 1:length(ChannelMat.Channel); + NewChannelMat.Channel = [ NewChannelMat.Channel , ChannelMat.Channel ]; + end + + [~, iChannelStudy] = bst_get('ChannelForStudy', iNewStudy); + db_set_channel(iChannelStudy, NewChannelMat, 0, 0); + newStudyPath = bst_fileparts(file_fullpath(sNewStudy.FileName)); + + % Link to raw file + OutputFile = bst_process('GetNewFilename', bst_fileparts(sNewStudy.FileName), 'data_raw_combned'); + + % Raw file + [~, rawBaseOut, rawBaseExt] = bst_fileparts(newStudyPath); + rawBaseOut = strrep([rawBaseOut rawBaseExt], '@raw', ''); + RawFileOut = bst_fullfile(newStudyPath, [rawBaseOut '.bst']); + + bst_progress('inc', nInputs); + bst_progress('text', 'Saving files...'); + + + sDataRawSync = in_bst_data(sInputs(1).FileName, 'F'); + sFileIn = sDataRawSync.F; + % Set new time and events + sFileIn.header.nsamples = length(new_times); + sFileIn.prop.times = [ new_times(1), new_times(end)]; + sFileIn.channelflag = ones(1,length(NewChannelMat.Channel)); + sFileOut = out_fopen(RawFileOut, 'BST-BIN', sFileIn, NewChannelMat); + + % Set Output sFile structure + sDataSync = in_bst(sInputs(1).FileName, [], 1, 1, 'no'); + sOutMat = rmfield(sDataSync, 'F'); + sOutMat.format = 'BST-BIN'; + sOutMat.DataType = 'raw'; + sOutMat.F = sFileOut; + sOutMat.ChannelFlag = ones(1,length(NewChannelMat.Channel)); + sOutMat.Comment = [sDataSync.Comment ' | Combined']; + + bst_save(OutputFile, sOutMat, 'v6'); + + % Save sync data to file + for iInput = 1:nInputs + + sDataSync = in_bst(sInputs(iInput).FileName, [], 1, 1, 'no'); + + % Update raw data + sDataSync.F = interp1(sOldTiming{iInput}.Time, sDataSync.F', new_times)'; + % Save new link to raw .mat file + % Write block + out_fwrite(sFileOut, NewChannelMat, 1, [], sIdxChAn{iInput}, sDataSync.F); + + bst_progress('inc', 1); + + end + + % Register in BST database + db_add_data(iNewStudy, OutputFile, sOutMat); + OutputFiles{iInput} = OutputFile; + + bst_progress('stop'); +end + From c4ac4d61757479c35dbaa9a09f6884ee4cdd1661 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 6 Jul 2024 12:23:52 -0400 Subject: [PATCH 02/10] bugfix: correctly get chan idx --- toolbox/process/functions/process_combine_recordings.m | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/toolbox/process/functions/process_combine_recordings.m b/toolbox/process/functions/process_combine_recordings.m index 90f1eb3ac..7ca7e3bf1 100644 --- a/toolbox/process/functions/process_combine_recordings.m +++ b/toolbox/process/functions/process_combine_recordings.m @@ -99,7 +99,7 @@ for iInput = 2:nInputs ChannelMat = in_bst_channel(sInputs(iInput).ChannelFile); - sIdxChAn{iInput} = length(NewChannelMat.Channel) + 1:length(ChannelMat.Channel); + sIdxChAn{iInput} = length(NewChannelMat.Channel) + (1:length(ChannelMat.Channel)); NewChannelMat.Channel = [ NewChannelMat.Channel , ChannelMat.Channel ]; end @@ -144,7 +144,9 @@ sDataSync = in_bst(sInputs(iInput).FileName, [], 1, 1, 'no'); % Update raw data - sDataSync.F = interp1(sOldTiming{iInput}.Time, sDataSync.F', new_times)'; + if iInput > 1 + sDataSync.F = interp1(sDataSync.Time, sDataSync.F', new_times)'; + end % Save new link to raw .mat file % Write block out_fwrite(sFileOut, NewChannelMat, 1, [], sIdxChAn{iInput}, sDataSync.F); From 8ae4f7811b511e831bc45ca29fe51a1e6ed04d37 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 6 Jul 2024 12:48:21 -0400 Subject: [PATCH 03/10] Copy nirs information --- toolbox/process/functions/process_combine_recordings.m | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/toolbox/process/functions/process_combine_recordings.m b/toolbox/process/functions/process_combine_recordings.m index 7ca7e3bf1..693e354bb 100644 --- a/toolbox/process/functions/process_combine_recordings.m +++ b/toolbox/process/functions/process_combine_recordings.m @@ -98,9 +98,16 @@ % Save sync data to file for iInput = 2:nInputs ChannelMat = in_bst_channel(sInputs(iInput).ChannelFile); - + sIdxChAn{iInput} = length(NewChannelMat.Channel) + (1:length(ChannelMat.Channel)); NewChannelMat.Channel = [ NewChannelMat.Channel , ChannelMat.Channel ]; + + + if isfield(ChannelMat,'Nirs') && isfield(NewChannelMat,'Nirs') + NewChannelMat.NIRS = sort( union(ChannelMat.Nirs, NewChannelMat.Nirs)); + elseif isfield(ChannelMat,'Nirs') && ~isfield(NewChannelMat,'Nirs') + NewChannelMat.NIRS = ChannelMat.Nirs; + end end [~, iChannelStudy] = bst_get('ChannelForStudy', iNewStudy); From 437274b0f59ef6be91900432c06554ac33467a04 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 6 Jul 2024 14:16:16 -0400 Subject: [PATCH 04/10] spelling,,,, --- toolbox/process/functions/process_combine_recordings.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/toolbox/process/functions/process_combine_recordings.m b/toolbox/process/functions/process_combine_recordings.m index 693e354bb..acf3506e3 100644 --- a/toolbox/process/functions/process_combine_recordings.m +++ b/toolbox/process/functions/process_combine_recordings.m @@ -104,9 +104,9 @@ if isfield(ChannelMat,'Nirs') && isfield(NewChannelMat,'Nirs') - NewChannelMat.NIRS = sort( union(ChannelMat.Nirs, NewChannelMat.Nirs)); + NewChannelMat.Nirs = sort( union(ChannelMat.Nirs, NewChannelMat.Nirs)); elseif isfield(ChannelMat,'Nirs') && ~isfield(NewChannelMat,'Nirs') - NewChannelMat.NIRS = ChannelMat.Nirs; + NewChannelMat.Nirs = ChannelMat.Nirs; end end From 0b0106c79c5c9d2766c0f29afa7639c546475cb2 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 6 Jul 2024 14:16:26 -0400 Subject: [PATCH 05/10] Fix nirs visualization --- toolbox/tree/tree_callbacks.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/toolbox/tree/tree_callbacks.m b/toolbox/tree/tree_callbacks.m index 74ce8e986..8108437c4 100644 --- a/toolbox/tree/tree_callbacks.m +++ b/toolbox/tree/tree_callbacks.m @@ -896,8 +896,8 @@ end end elseif ismember('NIRS', DisplayMod{iType}) - gui_component('MenuItem', jMenuDisplay, [], 'NIRS (scalp)', IconLoader.ICON_CHANNEL, [], @(h,ev)DisplayChannels(bstNodes, Device, 'scalp', 0, 0)); - gui_component('MenuItem', jMenuDisplay, [], 'NIRS (pairs)', IconLoader.ICON_CHANNEL, [], @(h,ev)DisplayChannels(bstNodes, Device, 'scalp', 0, 1)); + gui_component('MenuItem', jMenuDisplay, [], 'NIRS (scalp)', IconLoader.ICON_CHANNEL, [], @(h,ev)DisplayChannels(bstNodes, 'NIRS-BRS', 'scalp', 0, 0)); + gui_component('MenuItem', jMenuDisplay, [], 'NIRS (pairs)', IconLoader.ICON_CHANNEL, [], @(h,ev)DisplayChannels(bstNodes, 'NIRS-BRS', 'scalp', 0, 1)); else gui_component('MenuItem', jMenuDisplay, [], channelTypeDisplay, IconLoader.ICON_CHANNEL, [], @(h,ev)DisplayChannels(bstNodes, DisplayMod{iType}, 'scalp')); end From 4a89c7d245fcdffee2bb5f7566ed876fac321915 Mon Sep 17 00:00:00 2001 From: Edouard Date: Mon, 8 Jul 2024 16:04:41 -0400 Subject: [PATCH 06/10] Fix filename --- toolbox/process/functions/process_combine_recordings.m | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/toolbox/process/functions/process_combine_recordings.m b/toolbox/process/functions/process_combine_recordings.m index acf3506e3..e440a9fe5 100644 --- a/toolbox/process/functions/process_combine_recordings.m +++ b/toolbox/process/functions/process_combine_recordings.m @@ -115,7 +115,7 @@ newStudyPath = bst_fileparts(file_fullpath(sNewStudy.FileName)); % Link to raw file - OutputFile = bst_process('GetNewFilename', bst_fileparts(sNewStudy.FileName), 'data_raw_combned'); + OutputFile = bst_process('GetNewFilename', bst_fileparts(sNewStudy.FileName), 'data_0raw_combned'); % Raw file [~, rawBaseOut, rawBaseExt] = bst_fileparts(newStudyPath); @@ -126,15 +126,15 @@ bst_progress('text', 'Saving files...'); + % Set Output sFile structure sDataRawSync = in_bst_data(sInputs(1).FileName, 'F'); sFileIn = sDataRawSync.F; - % Set new time and events sFileIn.header.nsamples = length(new_times); sFileIn.prop.times = [ new_times(1), new_times(end)]; sFileIn.channelflag = ones(1,length(NewChannelMat.Channel)); sFileOut = out_fopen(RawFileOut, 'BST-BIN', sFileIn, NewChannelMat); - % Set Output sFile structure + sDataSync = in_bst(sInputs(1).FileName, [], 1, 1, 'no'); sOutMat = rmfield(sDataSync, 'F'); sOutMat.format = 'BST-BIN'; @@ -147,19 +147,17 @@ % Save sync data to file for iInput = 1:nInputs - + % Load raw data sDataSync = in_bst(sInputs(iInput).FileName, [], 1, 1, 'no'); % Update raw data if iInput > 1 sDataSync.F = interp1(sDataSync.Time, sDataSync.F', new_times)'; end - % Save new link to raw .mat file % Write block out_fwrite(sFileOut, NewChannelMat, 1, [], sIdxChAn{iInput}, sDataSync.F); bst_progress('inc', 1); - end % Register in BST database From 55878dcf79c3bc9109cbaeecf5d2cd5bb579b4bf Mon Sep 17 00:00:00 2001 From: Edouard Date: Thu, 5 Sep 2024 14:18:07 -0400 Subject: [PATCH 07/10] specify output condition --- toolbox/process/functions/process_combine_recordings.m | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/toolbox/process/functions/process_combine_recordings.m b/toolbox/process/functions/process_combine_recordings.m index e440a9fe5..0e51e7d8d 100644 --- a/toolbox/process/functions/process_combine_recordings.m +++ b/toolbox/process/functions/process_combine_recordings.m @@ -40,7 +40,10 @@ sProcess.nInputs = 1; sProcess.nMinFiles = 2; - + % Option: Condition + sProcess.options.condition.Comment = 'Condition name:'; + sProcess.options.condition.Type = 'text'; + sProcess.options.condition.Value = ''; end @@ -84,8 +87,7 @@ % Compute shifiting between file i and first file new_times = sOldTiming{1}.Time; - newCondition = [sInputs(iInput).Condition '_combined']; - iNewStudy = db_add_condition(sInputs(iInput).SubjectName, newCondition); + iNewStudy = db_add_condition(sInputs(iInput).SubjectName, sProcess.options.condition.Value); sNewStudy = bst_get('Study', iNewStudy); From 40645fbc500ffc11d2742d9d2322e0e415614370 Mon Sep 17 00:00:00 2001 From: Edouard Date: Wed, 25 Sep 2024 15:31:32 -0400 Subject: [PATCH 08/10] [bugfix] folder name should start with @raw --- toolbox/process/functions/process_combine_recordings.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/toolbox/process/functions/process_combine_recordings.m b/toolbox/process/functions/process_combine_recordings.m index 0e51e7d8d..febc8187c 100644 --- a/toolbox/process/functions/process_combine_recordings.m +++ b/toolbox/process/functions/process_combine_recordings.m @@ -87,16 +87,16 @@ % Compute shifiting between file i and first file new_times = sOldTiming{1}.Time; - iNewStudy = db_add_condition(sInputs(iInput).SubjectName, sProcess.options.condition.Value); + iNewStudy = db_add_condition(sInputs(iInput).SubjectName, ['@raw' sProcess.options.condition.Value]); sNewStudy = bst_get('Study', iNewStudy); % Save channel definition bst_progress('text', 'Combining channels files...'); - NewChannelMat = in_bst_channel(sInputs(1).ChannelFile); + NewChannelMat = in_bst_channel(sInputs(1).ChannelFile); + sIdxChAn{1} = 1:length(NewChannelMat.Channel); - sIdxChAn{1} = 1:length(NewChannelMat.Channel); % Save sync data to file for iInput = 2:nInputs ChannelMat = in_bst_channel(sInputs(iInput).ChannelFile); From 5a9ecbc02e2107631b83fd45ad5a3eefecbcd567 Mon Sep 17 00:00:00 2001 From: Edouard Date: Wed, 25 Sep 2024 16:31:41 -0400 Subject: [PATCH 09/10] copy video if present --- .../functions/process_combine_recordings.m | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/toolbox/process/functions/process_combine_recordings.m b/toolbox/process/functions/process_combine_recordings.m index febc8187c..8bf207fbd 100644 --- a/toolbox/process/functions/process_combine_recordings.m +++ b/toolbox/process/functions/process_combine_recordings.m @@ -97,8 +97,36 @@ NewChannelMat = in_bst_channel(sInputs(1).ChannelFile); sIdxChAn{1} = 1:length(NewChannelMat.Channel); + % Sync videos + sOldStudy = bst_get('Study', sInputs(1).iStudy); + if isfield(sOldStudy,'Image') && ~isempty(sOldStudy.Image) + for iOldVideo = 1 : length(sOldStudy.Image) + sOldVideo = load(file_fullpath(sOldStudy.Image(iOldVideo).FileName)); + if isempty(sOldVideo.VideoStart) + sOldVideo.VideoStart = 0; + end + iNewVideo = import_video(iNewStudy, sOldVideo.LinkTo); + sNewStudy = bst_get('Study', iNewStudy); + figure_video('SetVideoStart', file_fullpath(sNewStudy.Image(iNewVideo).FileName), sprintf('%.3f', sOldVideo.VideoStart)); + end + end + % Save sync data to file for iInput = 2:nInputs + % Sync videos + sOldStudy = bst_get('Study', sInputs(iInput).iStudy); + if isfield(sOldStudy,'Image') && ~isempty(sOldStudy.Image) + for iOldVideo = 1 : length(sOldStudy.Image) + sOldVideo = load(file_fullpath(sOldStudy.Image(iOldVideo).FileName)); + if isempty(sOldVideo.VideoStart) + sOldVideo.VideoStart = 0; + end + iNewVideo = import_video(iNewStudy, sOldVideo.LinkTo); + sNewStudy = bst_get('Study', iNewStudy); + figure_video('SetVideoStart', file_fullpath(sNewStudy.Image(iNewVideo).FileName), sprintf('%.3f', sOldVideo.VideoStart)); + end + end + ChannelMat = in_bst_channel(sInputs(iInput).ChannelFile); sIdxChAn{iInput} = length(NewChannelMat.Channel) + (1:length(ChannelMat.Channel)); From 9f574855f8a448e96aa66bfe3cbc2886c0d7fe2a Mon Sep 17 00:00:00 2001 From: Edouard Date: Wed, 25 Sep 2024 17:08:48 -0400 Subject: [PATCH 10/10] Copy channel flag --- .../functions/process_combine_recordings.m | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/toolbox/process/functions/process_combine_recordings.m b/toolbox/process/functions/process_combine_recordings.m index 8bf207fbd..a0d06274a 100644 --- a/toolbox/process/functions/process_combine_recordings.m +++ b/toolbox/process/functions/process_combine_recordings.m @@ -80,24 +80,21 @@ % First Input is the one wiht highest sampling frequency [~, im] = max(fs); + sInputs([1, im]) = sInputs([im, 1]); sOldTiming([1, im]) = sOldTiming([im, 1]); - fs([1, im]) = fs([im, 1]); - - % Compute shifiting between file i and first file - new_times = sOldTiming{1}.Time; + new_times = sOldTiming{1}.Time; iNewStudy = db_add_condition(sInputs(iInput).SubjectName, ['@raw' sProcess.options.condition.Value]); sNewStudy = bst_get('Study', iNewStudy); - % Save channel definition bst_progress('text', 'Combining channels files...'); NewChannelMat = in_bst_channel(sInputs(1).ChannelFile); sIdxChAn{1} = 1:length(NewChannelMat.Channel); - % Sync videos + % Copy videos sOldStudy = bst_get('Study', sInputs(1).iStudy); if isfield(sOldStudy,'Image') && ~isempty(sOldStudy.Image) for iOldVideo = 1 : length(sOldStudy.Image) @@ -113,7 +110,8 @@ % Save sync data to file for iInput = 2:nInputs - % Sync videos + + % Copy videos sOldStudy = bst_get('Study', sInputs(iInput).iStudy); if isfield(sOldStudy,'Image') && ~isempty(sOldStudy.Image) for iOldVideo = 1 : length(sOldStudy.Image) @@ -155,23 +153,28 @@ bst_progress('inc', nInputs); bst_progress('text', 'Saving files...'); + % Read channel flags + channelflag = []; + for iInput = 1:nInputs + sDataRawSync = in_bst_data(sInputs(iInput).FileName, 'ChannelFlag'); + channelflag = [channelflag; sDataRawSync.ChannelFlag]; + end % Set Output sFile structure sDataRawSync = in_bst_data(sInputs(1).FileName, 'F'); sFileIn = sDataRawSync.F; sFileIn.header.nsamples = length(new_times); sFileIn.prop.times = [ new_times(1), new_times(end)]; - sFileIn.channelflag = ones(1,length(NewChannelMat.Channel)); + sFileIn.channelflag = channelflag; sFileOut = out_fopen(RawFileOut, 'BST-BIN', sFileIn, NewChannelMat); - sDataSync = in_bst(sInputs(1).FileName, [], 1, 1, 'no'); sOutMat = rmfield(sDataSync, 'F'); sOutMat.format = 'BST-BIN'; sOutMat.DataType = 'raw'; sOutMat.F = sFileOut; - sOutMat.ChannelFlag = ones(1,length(NewChannelMat.Channel)); - sOutMat.Comment = [sDataSync.Comment ' | Combined']; + sOutMat.ChannelFlag = channelflag; + sOutMat.Comment = [sDataSync.Comment ' | Combined']; bst_save(OutputFile, sOutMat, 'v6');