diff --git a/doc/logo_splash.gif b/doc/logo_splash.gif
index 6ba1ae443..e6de3f5c3 100644
Binary files a/doc/logo_splash.gif and b/doc/logo_splash.gif differ
diff --git a/doc/plugins/brainsuite_logo.png b/doc/plugins/brainsuite_logo.png
new file mode 100644
index 000000000..16cb37001
Binary files /dev/null and b/doc/plugins/brainsuite_logo.png differ
diff --git a/doc/plugins/mtrf_logo.gif b/doc/plugins/mtrf_logo.gif
new file mode 100644
index 000000000..d2ab0c485
Binary files /dev/null and b/doc/plugins/mtrf_logo.gif differ
diff --git a/doc/plugins/neuromaps_logo.png b/doc/plugins/neuromaps_logo.png
new file mode 100644
index 000000000..872b17737
Binary files /dev/null and b/doc/plugins/neuromaps_logo.png differ
diff --git a/doc/plugins/zeffiro_logo.png b/doc/plugins/zeffiro_logo.png
new file mode 100644
index 000000000..45914a756
Binary files /dev/null and b/doc/plugins/zeffiro_logo.png differ
diff --git a/doc/updates.txt b/doc/updates.txt
index 307b2c79b..f5d6a924a 100644
--- a/doc/updates.txt
+++ b/doc/updates.txt
@@ -1,3 +1,132 @@
+December 2024
+- Anatomy: Copy/Paste scout operations
+- Anatomy: Scalp scouts from sensor positions
+- Distrib: Compilation with Matlab 2024a
+- Distrib: Compilation with Matlab 2024b
+--------------------------------------------------------------
+November 2024
+- SEEG/ECOG: Allow iEEG implantation using MRI and/or CT and/or IsoSurface
+- Registration: Manual and auto localization of EEG sensors with 3D scanner mesh
+- Registration: Project EEG sensors for default caps in Colin27 def anat
+- Anatomy: Add skull stripping in Brainstorm (uses SPM or BrainSuite)
+- Anatomy: Add template 'Colin27 4NIRS' (2024)
+- IO: Process to merge (channel-wise) raw continue recordings
+- Distrib: Update script for epilepsy tutorial, add source estimation with MEM
+- Distrib: Script to run the BEst (Brain Entropy in space and time) tutorial
+- Plugins: iso2mesh, brain2mesh and xdf now support Apple Silicon
+--------------------------------------------------------------
+October 2024
+- Anatomy: Spatial correlations and brain annotations: 'bst-neuromaps' plugin
+- Anatomy: Support exporting mixed sources as NIfTI
+- Registration: Allow selecting and deleting head points
+- TimeFreq: Add model selection to FOOOF and SPRiNT
+- IO: Support of EDF files with multiple sampling rates (using FielTrip plugin)
+- Bugfix: Importing MEG sensor locations from FieldTrip data
+--------------------------------------------------------------
+September 2024
+- Artifacts: ICA, display explaned variance for ICs
+- Distrib: Function 'test_tutorial.m' and GitHub workflow 'run_tutorial.yaml'
+- Plugins: Bugfix, interaction between SPM and FieldTrip
+- Plugins: Bugfix, always read FIFF with functions in 'brainstorm3/external'
+- GUI: Show/hide hidden files (Linux, Windows and macOS)
+--------------------------------------------------------------
+August 2024
+- Anatomy: Improve volume computation, use 'boundary'
+- Coreg: New digitization panel
+- SEEG/ECOG: Improvements and bugfix for IEEG panel
+- IO: EDF+ support negative gains
+- Distrib: Linux and macOS, find Matlab runtime in full Matlab installation
+- Distrib: Add deep-brain-activity script
+- Distrib: Add deviation-maps tutorial
+--------------------------------------------------------------
+July 2024
+- Anatomy: Import output from FreeSurfer recon-all-clinical
+- Bugfix: Importing non-Atlas MRI volumes given in MNI space
+- Inverse: Process to apply exclusion zone around sensors for volume grids
+- Artifacts: New process for automatic detection of artifacts on MEG
+- Plugins: Add mTRF-Toolbox and Zeffiro
+- Plugins: OpenMEEG and MCXLAB now support Apple Silicon
+- Distrib: Menu option to retrieve system information
+--------------------------------------------------------------
+June 2024
+- Anatomy: Bugfix, set proper subtype on importing Fibers and FEM
+- Viz: Allow combination of resection options in Fig3D
+- Viz: MRI viewer contactsheets support overlayed Atlases and Volumes
+- Viz: Improved figure positioning for Windows11
+- IO: SNIRF, improve import of Landmarks and Events
+- IO: Intan RHS, bugfix, incorrect block selection
+- IO: Neuralyn, bugfix reading NSE files, add support for NTT files
+- Distrib: Add brain fingerprinting tutorial script and page
+- Distrib: Brainstorm compiled with Matlab 2023a
+--------------------------------------------------------------
+May 2024
+- Anatomy: Bugfix at selecting proper int type when exporting MRI as NIfTI
+- Viz: Improved contactsheets from MRIviewer and 3D slices
+- IO: Updated support for Open Ephys
+- IO: Support double, complex-float and complex-double .fif data
+- Events: Added "Every sample" option for extended2simple conversion
+- Plugins: Remove npy-matlab code from Brainstorm, add it as plugin
+- Plugins: Allow user registering plugins as supported plugins in Brainstorm
+--------------------------------------------------------------
+April 2024
+- Anatomy: Added AAL1 MNI parcellation
+- PSD: Added 2D layout visualization
+- Artifacts: Detect bad channels: peak-to-peak for continuous data
+- Distrib: Update Matlab RunTime path binary in macOS and Linux
+--------------------------------------------------------------
+March 2024
+- SEEG/ECOG: Improved contact localization using 3D figures
+- SEEG/ECOG: Revamped iEEG panel
+- Anatomy: New scout operations (intersect and duplicate)
+- IO: Improved support for ITAB files
+- IO: Bugfix on importing Curry .pom channel files
+- Events: Allow digital mask for events from channel
+--------------------------------------------------------------
+February 2024
+- Database: Integration of CT volumes and their options in database explorer
+- Database: Links to raw files are updated if disk letter or mount point changes
+- IO: Syncronize raw data with common event
+--------------------------------------------------------------
+January 2024
+- IO: Import channel-wise events from BrainVision BrainAmp
+--------------------------------------------------------------
+December 2023
+- Bugfix: Keep BadTrial flags when merging Studies
+- Plugin: Improve handling of processes from installed plugins
+- Distrib: Basic support Apple silicon (OsType `mac64arm`)
+- IO: Add process Export to file
+--------------------------------------------------------------
+November 2023
+- Plugin: CT2MRIREG to perform CT to MRI co-registration
+- IO: Import Brainstorm sources
+- Plugins: Remove EASYH5 and JSNIRF code from Brainstorm, add them as plugins
+- Bugfix: Export EDF+ with UTF-8 encoding
+- IO: Export to .xlsx files for Matlab >= R2019a
+- IO: Export EEG as Brainsight format
+--------------------------------------------------------------
+October 2023
+- Distrib: Compilation with Matlab 2023a/2023b
+- Bugfix: Merging events with, and events without 'channels' and 'notes'
+- Connectivity: Reorganize different correlation options
+- iEEG: Save, Load, Export and Import electrode models
+--------------------------------------------------------------
+September 2023
+- Anatomy: Import resection mask from BrainSuite SVReg
+- IO: Export raw data as FieldTrip structure
+--------------------------------------------------------------
+August 2023
+- Scouts: Add 'power' as scout function
+- Reports: Send compact report by email if requested
+- Anatomy: Display anatomical atlases on 3D orthogonal slices view
+- Connectivity: Reorganize connectivity metrics
+- Connectivity: Fix across-trials average of phase metrics
+- Distrib: Add workflow to run tutorials GitHub runners
+--------------------------------------------------------------
+July 2023
+- Anatomy : Export scouts as FreeSurfer annotations
+- New process: Remove evoke response
+- Bugfix: (e-phys) Error computing tuning curves
+--------------------------------------------------------------
June 2023
- PCA: Major fixes and improvements for Scouts and Flattening unconstrained sources
| New tutorial page. Fix 1st PC sign inconsistency across epochs and conditions
diff --git a/doc/version.txt b/doc/version.txt
index aea2e169c..df76bca69 100644
--- a/doc/version.txt
+++ b/doc/version.txt
@@ -1,2 +1,2 @@
% Brainstorm
-% v. 3.230919 (19-Sep-2023)
\ No newline at end of file
+% v. 3.250107 (07-Jan-2025)
\ No newline at end of file
diff --git a/external/easyh5/ChangeLog.txt b/external/easyh5/ChangeLog.txt
deleted file mode 100644
index 217fa4cb2..000000000
--- a/external/easyh5/ChangeLog.txt
+++ /dev/null
@@ -1,23 +0,0 @@
-= Change Log =
-
-Major updates are marked with a "*"
-
-== EasyH5 v0.8 (Go - Japanese 5), FangQ ==
-
- 2019-09-30*[104a9ed] add regroup option for loadh5, change regexp, add demo
- 2019-09-30*[bb762a8] support saving and restoring non-ascii group and dataset names, like JSONLab
- 2019-09-29*[1486603] restore the original position for a grouped item
- 2019-09-29*[d420b3d] support data compression, close #2
- 2019-09-29*[04f12ee] transpose data when saving not loading, fix sparse array loading bug
- 2019-09-28*[77e0d47] support saving and restoring sparse array, both real and complex, close #3
- 2019-09-28 [f05e305] add helper functions copied from jsonlab
- 2019-09-28 [2e385ae] collapse a single numbered group with in the form of ...1
- 2019-09-23*[5e694b8] apply both order tracked and indexed when writing datasets
- 2019-09-22 [289d2b6] update readme
- 2019-09-22*[b09c80f] now reading data in creation order, fix #1, also reads specified node using rootpath
-
-== EasyH5 v0.5 (Cinco - Spanish 5), FangQ ==
-
- 2019-09-19 [dc62ed5] update code name and version number
- 2019-09-19 [66de6e2] tracking creation order, need to use links to read in loadh5
- 2019-09-19*[69979e4] initial but fully working version
diff --git a/external/easyh5/README.md b/external/easyh5/README.md
deleted file mode 100644
index f1b7daf31..000000000
--- a/external/easyh5/README.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# EasyH5 Toolbox - An easy-to-use HDF5 data interface (loadh5 and saveh5)
-
-* Copyright (C) 2019 Qianqian Fang
-* License: GNU General Public License version 3 (GPL v3) or 3-clause BSD license, see LICENSE*.txt
-* Version: 0.8 (code name: Go - Japanese 5)
-* URL: http://github.com/fangq/easyh5
-
-## Overview
-
-EasyH5 is a fully automated, fast, compact and portable MATLAB object to HDF5
-exporter/importer. It contains two easy-to-use functions - `loadh5.m` and
-`saveh5.m`. The `saveh5.m` can handle almost all MATLAB data types, including
-structs, struct arrays, cells, cell arrays, real and complex arrays, strings,
-and `containers.Map` objects. All other data classes (such as a table, digraph,
-etc) can also be stored/loaded seemlessly using an undocumented data serialization
-interface (MATLAB only).
-
-EasyH5 stores complex numerical arrays using a special compound data type in an
-HDF5 dataset. The real-part of the data are stored as `Real` and the imaginary
-part is stored as the `Imag` component. The `loadh5.m` automatically converts
-such data structure to a complex array. Starting from v0.8, EasyH5 also supports
-saving and loading sparse arrays using a compound dataset with 2 or 3
-specialized subfields: `SparseArray`, `Real`, and, in the case of a sparse
-complex array, `Imag`. The sparse array dimension is stored as an attribute
-named `SparseArraySize`, attached with the dataset. Using the `deflate` filter
-to save compressed arrays is supported in v0.8 and later.
-
-Because HDF5 does not directly support 1-D/N-D cell arrays or struct arrays,
-EasyH5 converts these data structures into data groups with names in the
-following format
-```
- ['/hdf5/path/.../varname',num2str(idx1d)]
-```
-where `varname` is the variable/field name to the cell/struct array object,
-and `idx1d` is the 1-D integer index of the cell/struct array. We also provide
-a function, `regrouph5.m` to automatically collapse these group/dataset names
-into 1-D cell/struct arrays after loading the data using `loadh5.m`. See examples
-below.
-
-## Installation
-
-The EasyH5 toolbox can be installed using a single command
-```
- addpath('/path/to/easyh5');
-```
-where the `/path/to/easyh5` should be replaced by the unzipped folder
-of the toolbox (i.e. the folder containing `loadh5.m/saveh5.m`).
-
-## Usage
-
-### `saveh5` - Save a MATLAB struct (array) or cell (array) into an HDF5 file
-Save a MATLAB struct (array) or cell (array) into an HDF5 file.
-
-Example:
-```
- a=struct('a',rand(5),'c','string','b',true,'d',2+3i,'e',{'test',[],1:5});
- saveh5(a,'test.h5');
- saveh5(a(1),'test2.h5','rootname','');
- saveh5(a(1),'test2.h5','compression','deflate','compressarraysize',1);
-```
-### `loadh5` - Load data in an HDF5 file to a MATLAB structure.
-Load data in an HDF5 file to a MATLAB structure.
-
-Example:
-```
- a={rand(2), struct('va',1,'vb','string'), 1+2i};
- saveh5(a,'test.h5');
- a2=loadh5('test.h5')
- a3=loadh5('test.h5','regroup',1)
- isequaln(a,a3.a)
- a4=loadh5('test.h5','/a1')
-```
-### `regrouph5` - Processing an HDF5 based data and group indexed datasets into a cell array
-Processing a loadh5 restored data and merge "indexed datasets", whose
-names start with an ASCII string followed by a contiguous integer
-sequence number starting from 1, into a cell array. For example,
-datasets {data.a1, data.a2, data.a3} will be merged into a cell/struct
-array data.a with 3 elements.
-
-Example:
-```
- a=struct('a1',rand(5),'a2','string','a3',true,'d',2+3i,'e',{'test',[],1:5});
- a(1).a1=0; a(2).a2='test';
- data=regrouph5(a)
- saveh5(a,'test.h5');
- rawdata=loadh5('test.h5')
- data=regrouph5(rawdata)
-```
-
-## Known problems
-- EasyH5 currently does not support 2D cell and struct arrays
-- If a cell name ends with a number, such as `a10={...}`; `regrouph5` can not group the cell correctly
-- If a database/group name is longer than 63 characters, it may have the risk of being truncated
-
-## Contribute to EasyH5
-
-Please submit your bug reports, feature requests and questions to the Github Issues page at
-
-https://github.com/fangq/easyh5/issues
-
-Please feel free to fork our software, making changes, and submit your revision back
-to us via "Pull Requests". EasyH5 is open-source and welcome to your contributions!
-
diff --git a/external/easyh5/decodevarname.m b/external/easyh5/decodevarname.m
deleted file mode 100644
index 66298d2b5..000000000
--- a/external/easyh5/decodevarname.m
+++ /dev/null
@@ -1,72 +0,0 @@
-function newname = decodevarname(name,varargin)
-%
-% newname = decodevarname(name)
-%
-% Decode a hex-encoded variable name (from encodevarname) and restore
-% its original form
-%
-% This function is sensitive to the default charset
-% settings in MATLAB, please call feature('DefaultCharacterSet','utf8')
-% to set the encoding to UTF-8 before calling this function.
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% name: a string output from encodevarname, which converts the leading non-ascii
-% letter into "x0xHH_" and non-ascii letters into "_0xHH_"
-% format, where hex key HH stores the ascii (or Unicode) value
-% of the character.
-%
-% output:
-% newname: the restored original string
-%
-% example:
-% decodevarname('x0x5F_a') % returns _a
-% decodevarname('a_') % returns a_ as it is a valid variable name
-% decodevarname('x0xE58F98__0xE9878F_') % returns '变量'
-%
-% this file is part of EasyH5 Toolbox: https://github.com/fangq/easyh5
-%
-% License: GPLv3 or 3-clause BSD license, see https://github.com/fangq/easyh5 for details
-%
-
-newname=name;
-isunpack=1;
-if(nargin==2 && ~isstruct(varargin{1}))
- isunpack=varargin{1};
-elseif(nargin>1)
- isunpack=jsonopt('UnpackHex',1,varargin{:});
-end
-
-if(isunpack)
- if(isempty(regexp(name,'0x([0-9a-fA-F]+)_','once')))
- return
- end
- if(exist('native2unicode','builtin'))
- h2u=@hex2unicode;
- newname=regexprep(name,'(^x|_){1}0x([0-9a-fA-F]+)_','${h2u($2)}');
- else
- pos=regexp(name,'(^x|_){1}0x([0-9a-fA-F]+)_','start');
- pend=regexp(name,'(^x|_){1}0x([0-9a-fA-F]+)_','end');
- if(isempty(pos))
- return;
- end
- str0=name;
- pos0=[0 pend(:)' length(name)];
- newname='';
- for i=1:length(pos)
- newname=[newname str0(pos0(i)+1:pos(i)-1) char(hex2dec(str0(pos(i)+3:pend(i)-1)))];
- end
- if(pos(end)~=length(name))
- newname=[newname str0(pos0(end-1)+1:pos0(end))];
- end
- end
-end
-
-%--------------------------------------------------------------------------
-function str=hex2unicode(hexstr)
-val=hex2dec(hexstr);
-id=histc(val,[0 2^8 2^16 2^32 2^64]);
-type={'uint8','uint16','uint32','uint64'};
-bytes=typecast(cast(val,type{id~=0}),'uint8');
-str=native2unicode(fliplr(bytes(:,1:find(bytes,1,'last'))));
diff --git a/external/easyh5/encodevarname.m b/external/easyh5/encodevarname.m
deleted file mode 100644
index 5534f8a7b..000000000
--- a/external/easyh5/encodevarname.m
+++ /dev/null
@@ -1,67 +0,0 @@
-function str = encodevarname(str,varargin)
-%
-% newname = encodevarname(name)
-%
-% Encode an invalid variable name using a hex-format for bi-directional
-% conversions.
-
-% This function is sensitive to the default charset
-% settings in MATLAB, please call feature('DefaultCharacterSet','utf8')
-% to set the encoding to UTF-8 before calling this function.
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% name: a string, can be either a valid or invalid variable name
-%
-% output:
-% newname: a valid variable name by converting the leading non-ascii
-% letter into "x0xHH_" and non-ascii letters into "_0xHH_"
-% format, where HH is the ascii (or Unicode) value of the
-% character.
-%
-% if the encoded variable name CAN NOT be longer than 63, i.e.
-% the maximum variable name specified by namelengthmax, and
-% one uses the output of this function as a struct or variable
-% name, the name will be trucated at 63. Please consider using
-% the name as a containers.Map key, which does not have such
-% limit.
-%
-% example:
-% encodevarname('_a') % returns x0x5F_a
-% encodevarname('a_') % returns a_ as it is a valid variable name
-% encodevarname('变量') % returns 'x0xE58F98__0xE9878F_'
-%
-% this file is part of EasyH5 Toolbox: https://github.com/fangq/easyh5
-%
-% License: GPLv3 or 3-clause BSD license, see https://github.com/fangq/easyh5 for details
-%
-
- if(~isvarname(str(1)))
- if(exist('unicode2native','builtin'))
- str=regexprep(str,'^([^A-Za-z])','x0x${sprintf(''%X'',unicode2native($1))}_','once');
- else
- str=sprintf('x0x%X_%s',char(str(1))+0,str(2:end));
- end
- end
- if(isvarname(str))
- return;
- end
- if(exist('unicode2native','builtin'))
- str=regexprep(str,'([^0-9A-Za-z_])','_0x${sprintf(''%X'',unicode2native($1))}_');
- else
- cpos=regexp(str,'[^0-9A-Za-z_]');
- if(isempty(cpos))
- return;
- end
- str0=str;
- pos0=[0 cpos(:)' length(str)];
- str='';
- for i=1:length(cpos)
- str=[str str0(pos0(i)+1:cpos(i)-1) sprintf('_0x%X_',str0(cpos(i))+0)];
- end
- if(cpos(end)~=length(str))
- str=[str str0(pos0(end-1)+1:pos0(end))];
- end
- end
-end
\ No newline at end of file
diff --git a/external/easyh5/jdatadecode.m b/external/easyh5/jdatadecode.m
deleted file mode 100644
index ade9ff9a6..000000000
--- a/external/easyh5/jdatadecode.m
+++ /dev/null
@@ -1,306 +0,0 @@
-function newdata=jdatadecode(data,varargin)
-%
-% newdata=jdatadecode(data,opt,...)
-%
-% Convert all JData object (in the form of a struct array) into an array
-% (accepts JData objects loaded from either loadjson/loadubjson or
-% jsondecode for MATLAB R2018a or later)
-%
-% This function implements the JData Specification Draft 2 (Oct. 2019)
-% see http://github.com/fangq/jdata for details
-%
-% authors:Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% data: a struct array. If data contains JData keywords in the first
-% level children, these fields are parsed and regrouped into a
-% data object (arrays, trees, graphs etc) based on JData
-% specification. The JData keywords are
-% "_ArrayType_", "_ArraySize_", "_ArrayData_"
-% "_ArrayIsSparse_", "_ArrayIsComplex_",
-% "_ArrayZipType_", "_ArrayZipSize", "_ArrayZipData_"
-% opt: (optional) a list of 'Param',value pairs for additional options
-% The supported options include
-% Recursive: [1|0] if set to 1, will apply the conversion to
-% every child; 0 to disable
-% Base64: [0|1] if set to 1, _ArrayZipData_ is assumed to
-% be encoded with base64 format and need to be
-% decoded first. This is needed for JSON but not
-% UBJSON data
-% Prefix: ['x0x5F'|'x'] for JData files loaded via loadjson/loadubjson, the
-% default JData keyword prefix is 'x0x5F'; if the
-% json file is loaded using matlab2018's
-% jsondecode(), the prefix is 'x'; this function
-% attempts to automatically determine the prefix;
-% for octave, the default value is an empty string ''.
-% FormatVersion: [2|float]: set the JSONLab output version;
-% since v2.0, JSONLab uses JData specification Draft 1
-% for output format, it is incompatible with all
-% previous releases; if old output is desired,
-% please set FormatVersion to 1
-%
-% output:
-% newdata: the covnerted data if the input data does contain a JData
-% structure; otherwise, the same as the input.
-%
-% examples:
-% obj={[],{'test'},true,struct('sparse',sparse(2,3),'magic',uint8(magic(5)))}
-% jdata=jdatadecode(jdataencode(obj))
-% isequaln(obj,jdata)
-%
-% license:
-% BSD or GPL version 3, see LICENSE_{BSD,GPLv3}.txt files for details
-%
-% -- this function is part of JSONLab toolbox (http://iso2mesh.sf.net/cgi-bin/index.cgi?jsonlab)
-%
-
- newdata=data;
- opt=struct;
- if(nargin==2)
- opt=varargin{1};
- elseif(nargin>2)
- opt=varargin2struct(varargin{:});
- end
-
- %% process non-structure inputs
- if(~isstruct(data))
- if(iscell(data))
- newdata=cellfun(@(x) jdatadecode(x,opt),data,'UniformOutput',false);
- elseif(isa(data,'containers.Map'))
- newdata=containers.Map('KeyType',data.KeyType,'ValueType','any');
- names=data.keys;
- for i=1:length(names)
- newdata(names{i})=jdatadecode(data(names{i}),opt);
- end
- end
- return;
- end
-
- %% assume the input is a struct below
- fn=fieldnames(data);
- len=length(data);
- needbase64=jsonopt('Base64',0,opt);
- format=jsonopt('FormatVersion',2,opt);
- if(isoctavemesh)
- prefix=jsonopt('Prefix','',opt);
- else
- prefix=jsonopt('Prefix','x0x5F',opt);
- end
- if(~isfield(data,N_('_ArrayType_')) && isfield(data,'x_ArrayType_'))
- prefix='x';
- opt.prefix='x';
- end
-
- %% recursively process subfields
- if(jsonopt('Recursive',1,opt)==1)
- for i=1:length(fn) % depth-first
- for j=1:len
- if(isstruct(data(j).(fn{i})) || isa(data(j).(fn{i}),'containers.Map'))
- newdata(j).(fn{i})=jdatadecode(data(j).(fn{i}),opt);
- elseif(iscell(data(j).(fn{i})))
- newdata(j).(fn{i})=cellfun(@(x) jdatadecode(x,opt),newdata(j).(fn{i}),'UniformOutput',false);
- end
- end
- end
- end
-
- %% handle array data
- if(isfield(data,N_('_ArrayType_')) && (isfield(data,N_('_ArrayData_')) || isfield(data,N_('_ArrayZipData_'))))
- newdata=cell(len,1);
- for j=1:len
- if(isfield(data,N_('_ArrayZipSize_')) && isfield(data,N_('_ArrayZipData_')))
- zipmethod='zip';
- if(isfield(data,N_('_ArrayZipType_')))
- zipmethod=data(j).(N_('_ArrayZipType_'));
- end
- if(~isempty(strmatch(zipmethod,{'zlib','gzip','lzma','lzip','lz4','lz4hc'})))
- decompfun=str2func([zipmethod 'decode']);
- if(needbase64)
- ndata=reshape(typecast(decompfun(base64decode(data(j).(N_('_ArrayZipData_')))),data(j).(N_('_ArrayType_'))),data(j).(N_('_ArrayZipSize_'))(:)');
- else
- ndata=reshape(typecast(decompfun(data(j).(N_('_ArrayZipData_'))),data(j).(N_('_ArrayType_'))),data(j).(N_('_ArrayZipSize_'))(:)');
- end
- else
- error('compression method is not supported');
- end
- else
- if(iscell(data(j).(N_('_ArrayData_'))))
- data(j).(N_('_ArrayData_'))=cell2mat(cellfun(@(x) double(x(:)),data(j).(N_('_ArrayData_')),'uniformoutput',0)).';
- end
- ndata=cast(data(j).(N_('_ArrayData_')),char(data(j).(N_('_ArrayType_'))));
- end
- if(isfield(data,N_('_ArrayZipSize_')))
- ndata=reshape(ndata(:),fliplr(data(j).(N_('_ArrayZipSize_'))(:)'));
- ndata=permute(ndata,ndims(ndata):-1:1);
- end
- iscpx=0;
- if(isfield(data,N_('_ArrayIsComplex_')))
- if(data(j).(N_('_ArrayIsComplex_')))
- iscpx=1;
- end
- end
- if(isfield(data,N_('_ArrayIsSparse_')) && data(j).(N_('_ArrayIsSparse_')))
- if(isfield(data,N_('_ArraySize_')))
- dim=double(data(j).(N_('_ArraySize_'))(:)');
- if(iscpx)
- ndata(end-1,:)=complex(ndata(end-1,:),ndata(end,:));
- end
- if isempty(ndata)
- % All-zeros sparse
- ndata=sparse(dim(1),prod(dim(2:end)));
- elseif dim(1)==1
- % Sparse row vector
- ndata=sparse(1,ndata(1,:),ndata(2,:),dim(1),prod(dim(2:end)));
- elseif dim(2)==1
- % Sparse column vector
- ndata=sparse(ndata(1,:),1,ndata(2,:),dim(1),prod(dim(2:end)));
- else
- % Generic sparse array.
- ndata=sparse(ndata(1,:),ndata(2,:),ndata(3,:),dim(1),prod(dim(2:end)));
- end
- else
- if(iscpx && size(ndata,2)==4)
- ndata(3,:)=complex(ndata(3,:),ndata(4,:));
- end
- ndata=sparse(ndata(1,:),ndata(2,:),ndata(3,:));
- end
- elseif(isfield(data,N_('_ArraySize_')))
- if(iscpx)
- ndata=complex(ndata(1,:),ndata(2,:));
- end
- if(format>1.9)
- data(j).(N_('_ArraySize_'))=data(j).(N_('_ArraySize_'))(end:-1:1);
- end
- dims=data(j).(N_('_ArraySize_'))(:)';
- ndata=reshape(ndata(:),dims(:)');
- if(format>1.9)
- ndata=permute(ndata,ndims(ndata):-1:1);
- end
- end
- newdata{j}=ndata;
- end
- if(len==1)
- newdata=newdata{1};
- end
- end
-
- %% handle table data
- if(isfield(data,N_('_TableRecords_')))
- newdata=cell(len,1);
- for j=1:len
- ndata=data(j).(N_('_TableRecords_'));
- if(iscell(ndata))
- rownum=length(ndata);
- colnum=length(ndata{1});
- nd=cell(rownum, colnum);
- for i1=1:rownum;
- for i2=1:colnum
- nd{i1,i2}=ndata{i1}{i2};
- end
- end
- newdata{j}=cell2table(nd);
- else
- newdata{j}=array2table(ndata);
- end
- if(isfield(data(j),N_('_TableRows_'))&& ~isempty(data(j).(N_('_TableRows_'))))
- newdata{j}.Properties.RowNames=data(j).(N_('_TableRows_'))(:);
- end
- if(isfield(data(j),N_('_TableCols_')) && ~isempty(data(j).(N_('_TableCols_'))))
- newdata{j}.Properties.VariableNames=data(j).(N_('_TableCols_'));
- end
- end
- if(len==1)
- newdata=newdata{1};
- end
- end
-
- %% handle map data
- if(isfield(data,N_('_MapData_')))
- newdata=cell(len,1);
- for j=1:len
- key=cell(1,length(data(j).(N_('_MapData_'))));
- val=cell(size(key));
- for k=1:length(data(j).(N_('_MapData_')))
- key{k}=data(j).(N_('_MapData_')){k}{1};
- val{k}=jdatadecode(data(j).(N_('_MapData_')){k}{2},opt);
- end
- ndata=containers.Map(key,val);
- newdata{j}=ndata;
- end
- if(len==1)
- newdata=newdata{1};
- end
- end
-
- %% handle graph data
- if(isfield(data,N_('_GraphNodes_')) && exist('graph','file') && exist('digraph','file'))
- newdata=cell(len,1);
- isdirected=1;
- for j=1:len
- nodedata=data(j).(N_('_GraphNodes_'));
- if(isstruct(nodedata))
- nodetable=struct2table(nodedata);
- elseif(isa(nodedata,'containers.Map'))
- nodetable=[keys(nodedata);values(nodedata)];
- if(strcmp(nodedata.KeyType,'char'))
- nodetable=table(nodetable(1,:)',nodetable(2,:)','VariableNames',{'Name','Data'});
- else
- nodetable=table(nodetable(2,:)','VariableNames',{'Data'});
- end
- else
- nodetable=table;
- end
-
- if(isfield(data,N_('_GraphEdges_')))
- edgedata=data(j).(N_('_GraphEdges_'));
- elseif(isfield(data,N_('_GraphEdges0_')))
- edgedata=data(j).(N_('_GraphEdges0_'));
- isdirected=0;
- elseif(isfield(data,N_('_GraphMatrix_')))
- edgedata=jdatadecode(data(j).(N_('_GraphMatrix_')),varargin{:});
- end
-
- if(exist('edgedata','var'))
- if(iscell(edgedata))
- endnodes=edgedata(:,1:2);
- endnodes=reshape([endnodes{:}],size(edgedata,1),2);
- weight=cell2mat(edgedata(:,3:end));
- edgetable=table(endnodes,[weight.Weight]','VariableNames',{'EndNodes','Weight'});
-
- if(isdirected)
- newdata{j}=digraph(edgetable,nodetable);
- else
- newdata{j}=graph(edgetable,nodetable);
- end
- elseif(ismatrix(edgedata) && isstruct(nodetable))
- newdata{j}=digraph(edgedata,fieldnames(nodetable));
- end
- end
- end
- if(len==1)
- newdata=newdata{1};
- end
- end
-
- %% handle bytestream and arbitrary matlab objects
- if(isfield(data,N_('_ByteStream_')) && isfield(data,N_('_DataInfo_'))==2)
- newdata=cell(len,1);
- for j=1:len
- if(isfield(data(j).(N_('_DataInfo_')),'MATLABObjectClass'))
- if(needbase64)
- newdata{j}=getArrayFromByteStream(base64decode(data(j).(N_('_ByteStream_'))));
- else
- newdata{j}=getArrayFromByteStream(data(j).(N_('_ByteStream_')));
- end
- end
- end
- if(len==1)
- newdata=newdata{1};
- end
- end
-
- %% subfunctions
- function escaped=N_(str)
- escaped=[prefix str];
- end
-end
diff --git a/external/easyh5/jdataencode.m b/external/easyh5/jdataencode.m
deleted file mode 100644
index 348664f61..000000000
--- a/external/easyh5/jdataencode.m
+++ /dev/null
@@ -1,301 +0,0 @@
-function jdata=jdataencode(data, varargin)
-%
-% jdata=jdataencode(data)
-% or
-% jdata=jdataencode(data, options)
-% jdata=jdataencode(data, 'Param1',value1, 'Param2',value2,...)
-%
-% Serialize a MATLAB struct or cell array into a JData-compliant
-% structure as defined in the JData spec: http://github.com/fangq/jdata
-%
-% This function implements the JData Specification Draft 2 (Oct. 2019)
-% see http://github.com/fangq/jdata for details
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% data: a structure (array) or cell (array) to be encoded.
-% options: (optional) a struct or Param/value pairs for user
-% specified options (first in [.|.] is the default)
-% Base64: [0|1] if set to 1, _ArrayZipData_ is assumed to
-% be encoded with base64 format and need to be
-% decoded first. This is needed for JSON but not
-% UBJSON data
-% Prefix: ['x0x5F'|'x'] for JData files loaded via loadjson/loadubjson, the
-% default JData keyword prefix is 'x0x5F'; if the
-% json file is loaded using matlab2018's
-% jsondecode(), the prefix is 'x'; this function
-% attempts to automatically determine the prefix;
-% for octave, the default value is an empty string ''.
-% UseArrayZipSize: [1|0] if set to 1, _ArrayZipSize_ will be added to
-% store the "pre-processed" data dimensions, i.e.
-% the original data stored in _ArrayData_, and then flaten
-% _ArrayData_ into a row vector using row-major
-% order; if set to 0, a 2D _ArrayData_ will be used
-% MapAsStruct: [0|1] if set to 1, convert containers.Map into
-% struct; otherwise, keep it as map
-% Compression: ['zlib'|'gzip','lzma','lz4','lz4hc'] - use zlib method
-% to compress data array
-% CompressArraySize: [100|int]: only to compress an array if the
-% total element count is larger than this number.
-% FormatVersion [2|float]: set the JSONLab output version; since
-% v2.0, JSONLab uses JData specification Draft 1
-% for output format, it is incompatible with all
-% previous releases; if old output is desired,
-% please set FormatVersion to 1.9 or earlier.
-%
-% example:
-% jd=jdataencode(struct('a',rand(5)+1i*rand(5),'b',[],'c',sparse(5,5)))
-%
-% license:
-% BSD or GPL version 3, see LICENSE_{BSD,GPLv3}.txt files for details
-%
-% -- this function is part of JSONLab toolbox (http://iso2mesh.sf.net/cgi-bin/index.cgi?jsonlab)
-%
-
-
-if(nargin==0)
- help jdataencode
- return;
-end
-
-opt=varargin2struct(varargin{:});
-if(isoctavemesh)
- opt.prefix=jsonopt('Prefix','',opt);
-else
- opt.prefix=jsonopt('Prefix',sprintf('x0x%X','_'+0),opt);
-end
-opt.compression=jsonopt('Compression','',opt);
-opt.nestarray=jsonopt('NestArray',0,opt);
-opt.formatversion=jsonopt('FormatVersion',2,opt);
-opt.compressarraysize=jsonopt('CompressArraySize',100,opt);
-opt.base64=jsonopt('Base64',0,opt);
-opt.mapasstruct=jsonopt('MapAsStruct',0,opt);
-opt.usearrayzipsize=jsonopt('UseArrayZipSize',1,opt);
-opt.messagepack=jsonopt('MessagePack',0,opt);
-
-jdata=obj2jd(data,opt);
-
-%%-------------------------------------------------------------------------
-function newitem=obj2jd(item,varargin)
-
-if(iscell(item))
- newitem=cell2jd(item,varargin{:});
-elseif(isstruct(item))
- newitem=struct2jd(item,varargin{:});
-elseif(isnumeric(item) || islogical(item))
- newitem=mat2jd(item,varargin{:});
-elseif(ischar(item) || isa(item,'string'))
- newitem=mat2jd(item,varargin{:});
-elseif(isa(item,'containers.Map'))
- newitem=map2jd(item,varargin{:});
-elseif(isa(item,'categorical'))
- newitem=cell2jd(cellstr(item),varargin{:});
-elseif(isa(item,'function_handle'))
- newitem=struct2jd(functions(item),varargin{:});
-elseif(isa(item,'table'))
- newitem=table2jd(item,varargin{:});
-elseif(isa(item,'digraph') || isa(item,'graph'))
- newitem=graph2jd(item,varargin{:});
-elseif(isobject(item))
- newitem=matlabobject2jd(item,varargin{:});
-else
- newitem=item;
-end
-
-%%-------------------------------------------------------------------------
-function newitem=cell2jd(item,varargin)
-
-newitem=cellfun(@(x) obj2jd(x, varargin{:}), item, 'UniformOutput',false);
-
-%%-------------------------------------------------------------------------
-function newitem=struct2jd(item,varargin)
-
-num=numel(item);
-if(num>1) % struct array
- newitem=obj2jd(num2cell(item),varargin{:});
- try
- newitem=cell2mat(newitem);
- catch
- end
-else % a single struct
- names=fieldnames(item);
- newitem=struct;
- for i=1:length(names)
- newitem.(names{i})=obj2jd(item.(names{i}),varargin{:});
- end
-end
-
-%%-------------------------------------------------------------------------
-function newitem=map2jd(item,varargin)
-
-names=item.keys;
-if(varargin{1}.mapasstruct) % convert a map to struct
- newitem=struct;
- if(~strcmp(item.KeyType,'char'))
- data=num2cell(reshape([names, item.values],length(names),2),2);
- for i=1:length(names)
- data{i}{2}=obj2jd(data{i}{2},varargin{:});
- end
- newitem.(N_('_MapData_',varargin{:}))=data;
- else
- for i=1:length(names)
- newitem.(N_(names{i},varargin{:}))=obj2jd(item(names{i}),varargin{:});
- end
- end
-else % keep as a map and only encode its values
- newitem=containers.Map('KeyType',item.KeyType,'ValueType','any');
- for i=1:length(names)
- newitem(names{i})=obj2jd(item(names{i}),varargin{:});
- end
-end
-%%-------------------------------------------------------------------------
-function newitem=mat2jd(item,varargin)
-
-if(isempty(item) || isa(item,'string') || ischar(item) || varargin{1}.nestarray || ...
- ((isvector(item) || ismatrix(item)) && isreal(item) && ~issparse(item)))
- newitem=item;
- if(~(varargin{1}.messagepack && size(item,1)>1))
- return;
- end
-end
-
-zipmethod=varargin{1}.compression;
-minsize=varargin{1}.compressarraysize;
-
-if(isa(item,'logical'))
- item=uint8(item);
-end
-
-N=@(x) N_(x,varargin{:});
-
-newitem=struct(N('_ArrayType_'),class(item),N('_ArraySize_'),size(item));
-
-if(isreal(item))
- if(issparse(item))
- fulldata=full(item(find(item)));
- newitem.(N('_ArrayIsSparse_'))=true;
- newitem.(N('_ArrayZipSize_'))=[2+(~isvector(item)),length(fulldata)];
- if(isvector(item))
- newitem.(N('_ArrayData_'))=[find(item(:))', fulldata(:)'];
- else
- [ix,iy]=find(item);
- newitem.(N('_ArrayData_'))=[ix(:)' , iy(:)', fulldata(:)'];
- end
- else
- if(varargin{1}.formatversion>1.9)
- item=permute(item,ndims(item):-1:1);
- end
- newitem.(N('_ArrayData_'))=item(:)';
- end
-else
- newitem.(N('_ArrayIsComplex_'))=true;
- if(issparse(item))
- fulldata=full(item(find(item)));
- newitem.(N('_ArrayIsSparse_'))=true;
- newitem.(N('_ArrayZipSize_'))=[3+(~isvector(item)),length(fulldata)];
- if(isvector(item))
- newitem.(N('_ArrayData_'))=[find(item(:))', real(fulldata(:))', imag(fulldata(:))'];
- else
- [ix,iy]=find(item);
- newitem.(N('_ArrayData_'))=[ix(:)' , iy(:)' , real(fulldata(:))', imag(fulldata(:))'];
- end
- else
- if(varargin{1}.formatversion>1.9)
- item=permute(item,ndims(item):-1:1);
- end
- newitem.(N('_ArrayZipSize_'))=[2,numel(item)];
- newitem.(N('_ArrayData_'))=[real(item(:))', imag(item(:))'];
- end
-end
-
-if(varargin{1}.usearrayzipsize==0 && isfield(newitem,N('_ArrayZipSize_')))
- data=newitem.(N('_ArrayData_'));
- data=reshape(data,fliplr(newitem.(N('_ArrayZipSize_'))));
- newitem.(N('_ArrayData_'))=permute(data,ndims(data):-1:1);
- newitem=rmfield(newitem,N('_ArrayZipSize_'));
-end
-
-if(~isempty(zipmethod) && numel(item)>minsize)
- compfun=str2func([zipmethod 'encode']);
- newitem.(N('_ArrayZipType_'))=lower(zipmethod);
- newitem.(N('_ArrayZipSize_'))=size(newitem.(N('_ArrayData_')));
- newitem.(N('_ArrayZipData_'))=compfun(typecast(newitem.(N('_ArrayData_')),'uint8'));
- newitem=rmfield(newitem,N('_ArrayData_'));
- if(varargin{1}.base64)
- newitem.(N('_ArrayZipData_'))=char(base64encode(newitem.(N('_ArrayZipData_'))));
- end
-end
-
-if(isfield(newitem,N('_ArrayData_')) && isempty(newitem.(N('_ArrayData_'))))
- newitem.(N('_ArrayData_'))=[];
-end
-
-%%-------------------------------------------------------------------------
-function newitem=table2jd(item,varargin)
-
-newitem=struct;
-newitem.(N_('_TableCols_',varargin{:}))=item.Properties.VariableNames;
-newitem.(N_('_TableRows_',varargin{:}))=item.Properties.RowNames';
-newitem.(N_('_TableRecords_',varargin{:}))=table2cell(item);
-
-%%-------------------------------------------------------------------------
-function newitem=graph2jd(item,varargin)
-
-newitem=struct;
-nodedata=table2struct(item.Nodes);
-if(isfield(nodedata,'Name'))
- nodedata=rmfield(nodedata,'Name');
- newitem.(N_('_GraphNodes_',varargin{:}))=containers.Map(item.Nodes.Name,num2cell(nodedata),'UniformValues',false);
-else
- newitem.(N_('_GraphNodes_',varargin{:}))=containers.Map(1:max(item.Edges.EndNodes(:)),num2cell(nodedata),'UniformValues',false);
-end
-edgenodes=num2cell(item.Edges.EndNodes);
-edgedata=table2struct(item.Edges);
-if(isfield(edgedata,'EndNodes'))
- edgedata=rmfield(edgedata,'EndNodes');
-end
-edgenodes(:,3)=num2cell(edgedata);
-if(isa(item,'graph'))
- if(strcmp(varargin{1}.prefix,'x'))
- newitem.(genvarname('_GraphEdges0_'))=edgenodes;
- else
- newitem.(encodevarname('_GraphEdges0_'))=edgenodes;
- end
-else
- newitem.(N_('_GraphEdges_',varargin{:}))=edgenodes;
-end
-
-%%-------------------------------------------------------------------------
-function newitem=matlabobject2jd(item,varargin)
-try
- if numel(item) == 0 %empty object
- newitem = struct();
- elseif numel(item) == 1 %
- newitem = char(item);
- else
- propertynames = properties(item);
- for p = 1:numel(propertynames)
- for o = numel(item):-1:1 % aray of objects
- newitem(o).(propertynames{p}) = item(o).(propertynames{p});
- end
- end
- end
-catch
- newitem=any2jd(item,varargin{:});
-end
-
-%%-------------------------------------------------------------------------
-function newitem=any2jd(item,varargin)
-
-N=@(x) N_(x,varargin{:});
-newitem.(N('_DataInfo_'))=struct('MATLABObjectClass',class(item),'MATLABObjectSize',size(item));
-newitem.(N('_ByteStream_'))=getByteStreamFromArray(item); % use undocumented matlab function
-if(varargin{1}.base64)
- newitem.(N('_ByteStream_'))=char(base64encode(newitem.(N('_ByteStream_'))));
-end
-
-%%-------------------------------------------------------------------------
-function newname=N_(name,varargin)
-
-newname=[varargin{1}.prefix name];
diff --git a/external/easyh5/jsonopt.m b/external/easyh5/jsonopt.m
deleted file mode 100644
index bd7ef6816..000000000
--- a/external/easyh5/jsonopt.m
+++ /dev/null
@@ -1,36 +0,0 @@
-function val=jsonopt(key,default,varargin)
-%
-% val=jsonopt(key,default,optstruct)
-%
-% setting options based on a struct. The struct can be produced
-% by varargin2struct from a list of 'param','value' pairs
-%
-% authors:Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% key: a string with which one look up a value from a struct
-% default: if the key does not exist, return default
-% optstruct: a struct where each sub-field is a key
-%
-% output:
-% val: if key exists, val=optstruct.key; otherwise val=default
-%
-% license:
-% BSD or GPL version 3, see LICENSE_{BSD,GPLv3}.txt files for details
-%
-% -- this function is part of JSONLab toolbox (http://iso2mesh.sf.net/cgi-bin/index.cgi?jsonlab)
-%
-
-val=default;
-if(nargin<=2)
- return;
-end
-key0=lower(key);
-opt=varargin{1};
-if(isstruct(opt))
- if(isfield(opt,key0))
- val=opt.(key0);
- elseif(isfield(opt,key))
- val=opt.(key);
- end
-end
diff --git a/external/easyh5/loadh5.m b/external/easyh5/loadh5.m
deleted file mode 100644
index 8ff263421..000000000
--- a/external/easyh5/loadh5.m
+++ /dev/null
@@ -1,281 +0,0 @@
-function varargout=loadh5(filename, varargin)
-%
-% [data, meta] = loadh5(filename)
-% [data, meta] = loadh5(root_id)
-% [data, meta] = loadh5(filename, rootpath)
-% [data, meta] = loadh5(filename, rootpath, options)
-% [data, meta] = loadh5(filename, options)
-% [data, meta] = loadh5(filename, 'Param1',value1, 'Param2',value2,...)
-%
-% Load data in an HDF5 file to a MATLAB structure.
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input
-% filename
-% Name of the file to load data from
-% root_id: an HDF5 handle (of type 'H5ML.id' in MATLAB)
-% rootpath : (optional)
-% Root path to read part of the HDF5 file to load
-% options: (optional) a struct or Param/value pairs for user specified options
-% Order: 'creation' - creation order (default), or 'alphabet' - alphabetic
-% Regroup: [0|1]: if 1, call regrouph5() to combine indexed
-% groups into a cell array
-% PackHex: [1|0]: convert invalid characters in the group/dataset
-% names to 0x[hex code] by calling encodevarname.m;
-% if set to 0, call getvarname
-% ComplexFormat: {'realKey','imagKey'}: use 'realKey' and 'imagKey'
-% as possible keywords for the real and the imaginary part
-% of a complex array, respectively (sparse arrays not supported);
-% a common list of keypairs is used even without this option
-%
-% output
-% data: a structure (array) or cell (array)
-% meta: optional output to store the attributes stored in the file
-%
-% example:
-% a={rand(2), struct('va',1,'vb','string'), 1+2i};
-% saveh5(a,'test.h5');
-% a2=loadh5('test.h5')
-% a3=loadh5('test.h5','regroup',1)
-% isequaln(a,a3.a)
-% a4=loadh5('test.h5','/a1')
-%
-% This function was adapted from h5load.m by Pauli Virtanen
-% This file is part of EasyH5 Toolbox: https://github.com/fangq/easyh5
-%
-% License: GPLv3 or 3-clause BSD license, see https://github.com/fangq/easyh5 for details
-%
-
-path = '';
-if(bitand(length(varargin),1)==0)
- opt=varargin2struct(varargin{:});
-elseif(length(varargin)>=3)
- path=varargin{1};
- opt=varargin2struct(varargin{2:end});
-elseif(length(varargin)==1)
- path=varargin{1};
-end
-
-if(isa(filename,'H5ML.id'))
- loc=filename;
-else
- if(exist('h5read','file'))
- loc = H5F.open(filename);
- else
- error('HDF5 is not supported');
- end
-end
-
-opt.rootpath=path;
-
-if(~(isfield(opt,'complexformat') && iscellstr(opt.complexformat) && numel(opt.complexformat)==2))
- opt.complexformat={'Real','Imag'};
-end
-
-opt.releaseid=0;
-vers=ver('MATLAB');
-if(~isempty(vers))
- opt.releaseid=datenum(vers(1).Date);
-end
-
-if((isfield(opt,'order') && strcmpi(opt.order,'alphabet')) || opt.releaseid1 && ~isempty(path))
- try
- rootgid=H5G.open(loc,path);
- [varargout{1:nargout}]=load_one(rootgid, opt);
- H5G.close(rootgid);
- catch
- [gname,dname]=fileparts(path);
- rootgid=H5G.open(loc,gname);
- [status, res]=group_iterate(rootgid,dname,struct('data',struct,'meta',struct,'opt',opt));
- if(nargout>0)
- varargout{1}=res.data;
- elseif(nargout>1)
- varargout{2}=res.meta;
- end
- H5G.close(rootgid);
- end
- else
- [varargout{1:nargout}]=load_one(loc, opt);
- end
- H5F.close(loc);
-catch ME
- H5F.close(loc);
- rethrow(ME);
-end
-
-if(jsonopt('Regroup',0,opt))
- if(nargout>=1)
- varargout{1}=regrouph5(varargout{1});
- elseif(nargout>=2)
- varargout{2}=regrouph5(varargout{2});
- end
-end
-
-if(isfield(opt,'jdata') && opt.jdata && nargout>=1)
- varargout{1}=jdatadecode(varargout{1},'Base64',0,opt);
-end
-
-%--------------------------------------------------------------------------
-function [data, meta]=load_one(loc, opt)
-
-data = struct();
-meta = struct();
-inputdata=struct('data',data,'meta',meta,'opt',opt);
-
-
-% Load groups and datasets
-try
- [status,count,inputdata] = H5L.iterate(loc,opt.order,'H5_ITER_INC',0,@group_iterate,inputdata);
-catch ME
- if(strcmp(opt.order,'H5_INDEX_CRT_ORDER'))
- [status,count,inputdata] = H5L.iterate(loc,'H5_INDEX_NAME','H5_ITER_INC',0,@group_iterate,inputdata);
- else
- rethrow(ME);
- end
-end
-
-data=inputdata.data;
-meta=inputdata.meta;
-
-%--------------------------------------------------------------------------
-function [status, res]=group_iterate(group_id,objname,inputdata)
-status=0;
-attr=struct();
-
-encodename=jsonopt('PackHex',1,inputdata.opt);
-
-try
- data=inputdata.data;
- meta=inputdata.meta;
-
- % objtype index
- info = H5G.get_objinfo(group_id,objname,0);
- objtype = info.type;
- objtype = objtype+1;
-
- if objtype == 1
- % Group
- name = regexprep(objname, '.*/', '');
-
- group_loc = H5G.open(group_id, name);
- try
- [sub_data, sub_meta] = load_one(group_loc, inputdata.opt);
- H5G.close(group_loc);
- catch ME
- H5G.close(group_loc);
- rethrow(ME);
- end
- if(encodename)
- name=encodevarname(name);
- else
- name=genvarname(name);
- end
- data.(name) = sub_data;
- meta.(name) = sub_meta;
-
- elseif objtype == 2
- % Dataset
- name = regexprep(objname, '.*/', '');
-
- dataset_loc = H5D.open(group_id, name);
- try
- sub_data = H5D.read(dataset_loc, ...
- 'H5ML_DEFAULT', 'H5S_ALL','H5S_ALL','H5P_DEFAULT');
- [status, count, attr]=H5A.iterate(dataset_loc, 'H5_INDEX_NAME', 'H5_ITER_INC', 0, @getattribute, attr);
- H5D.close(dataset_loc);
- catch exc
- H5D.close(dataset_loc);
- rethrow(exc);
- end
-
- sub_data = fix_data(sub_data, attr, inputdata.opt);
- if(encodename)
- name=encodevarname(name);
- else
- name=genvarname(name);
- end
- data.(name) = sub_data;
- meta.(name) = attr;
- end
-catch ME
- rethrow(ME);
-end
-res=struct('data',data,'meta',meta,'opt',inputdata.opt);
-
-%--------------------------------------------------------------------------
-function data=fix_data(data, attr, opt)
-% Fix some common types of data to more friendly form.
-
-if isstruct(data)
- fields = fieldnames(data);
-
- if(length(intersect(fields,{'SparseIndex',opt.complexformat{1}}))==2)
- if isnumeric(data.SparseIndex) && isnumeric(data.(opt.complexformat{1}))
- if(nargin>1 && isstruct(attr))
- if(isfield(attr,'SparseArraySize'))
- spd=sparse(1,prod(attr.SparseArraySize));
- if(isfield(data,opt.complexformat{2}))
- spd(data.SparseIndex)=complex(data.(opt.complexformat{1}),data.(opt.complexformat{2}));
- else
- spd(data.SparseIndex)=data.(opt.complexformat{1});
- end
- data=reshape(spd,attr.SparseArraySize(:)');
- return;
- end
- end
- end
- else
- if(numel(opt.complexformat)==2 && length(intersect(fields,opt.complexformat))==2)
- if isnumeric(data.(opt.complexformat{1})) && isnumeric(data.(opt.complexformat{2}))
- data = data.(opt.complexformat{1}) + 1j*data.(opt.complexformat{2});
- end
- else
- % if complexformat is not specified or not found, try some common complex number storage formats
- if(length(intersect(fields,{'Real','Imag'}))==2)
- if isnumeric(data.Real) && isnumeric(data.Imag)
- data = data.Real + 1j*data.Imag;
- end
- elseif(length(intersect(fields,{'real','imag'}))==2)
- if isnumeric(data.real) && isnumeric(data.imag)
- data = data.real + 1j*data.imag;
- end
- elseif(length(intersect(fields,{'Re','Im'}))==2)
- if isnumeric(data.Re) && isnumeric(data.Im)
- data = data.Re + 1j*data.Im;
- end
- elseif(length(intersect(fields,{'re','im'}))==2)
- if isnumeric(data.re) && isnumeric(data.im)
- data = data.re + 1j*data.im;
- end
- elseif(length(intersect(fields,{'r','i'}))==2)
- if isnumeric(data.r) && isnumeric(data.i)
- data = data.r + 1j*data.i;
- end
- end
- end
- end
-end
-
-if(isa(data,'uint8') || isa(data,'int8'))
- if(nargin>1 && isstruct(attr))
- if(isfield(attr,'MATLABObjectClass'))
- data=getArrayFromByteStream(data); % use undocumented function
- end
- end
-end
-
-%--------------------------------------------------------------------------
-function [status, dataout]= getattribute(loc_id,attr_name,info,datain)
-status=0;
-attr_id = H5A.open(loc_id, attr_name, 'H5P_DEFAULT');
-datain.(attr_name) = H5A.read(attr_id, 'H5ML_DEFAULT');
-H5A.close(attr_id);
-dataout=datain;
diff --git a/external/easyh5/mergestruct.m b/external/easyh5/mergestruct.m
deleted file mode 100644
index 3c5208390..000000000
--- a/external/easyh5/mergestruct.m
+++ /dev/null
@@ -1,33 +0,0 @@
-function s=mergestruct(s1,s2)
-%
-% s=mergestruct(s1,s2)
-%
-% merge two struct objects into one
-%
-% authors:Qianqian Fang (q.fang neu.edu)
-% date: 2012/12/22
-%
-% input:
-% s1,s2: a struct object, s1 and s2 can not be arrays
-%
-% output:
-% s: the merged struct object. fields in s1 and s2 will be combined in s.
-%
-% license:
-% BSD or GPL version 3, see LICENSE_{BSD,GPLv3}.txt files for details
-%
-% -- this function is part of JSONLab toolbox (http://iso2mesh.sf.net/cgi-bin/index.cgi?jsonlab)
-%
-
-if(~isstruct(s1) || ~isstruct(s2))
- error('input parameters contain non-struct');
-end
-if(length(s1)>1 || length(s2)>1)
- error('can not merge struct arrays');
-end
-fn=fieldnames(s2);
-s=s1;
-for i=1:length(fn)
- s.(fn{i})=s2.(fn{i});
-end
-
diff --git a/external/easyh5/regrouph5.m b/external/easyh5/regrouph5.m
deleted file mode 100644
index 99776f565..000000000
--- a/external/easyh5/regrouph5.m
+++ /dev/null
@@ -1,134 +0,0 @@
-function data=regrouph5(root, varargin)
-%
-% data=regrouph5(root)
-% or
-% data=regrouph5(root,type)
-% data=regrouph5(root,{'nameA','nameB',...})
-%
-% Processing a loadh5 restored data and merge "indexed datasets", whose
-% names start with an ASCII string followed by a contiguous integer
-% sequence number starting from 1, into a cell array. For example,
-% datasets {data.a1, data.a2, data.a3} will be merged into a cell/struct
-% array data.a with 3 elements.
-%
-% A single subfield .name1 will be renamed as .name. Items with
-% non-contigous numbering will not be grouped. If .name and
-% .name1/.name2 co-exist in the input struct, no grouping will be done.
-%
-% The grouped subfield will appear at the position of the first
-% pre-grouped item in the original input structure.
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% root: the raw input HDF5 data structure (loaded from loadh5.m)
-% type: if type is set as a cell array of strings, it restrict the
-% grouping only to the subset of field names in this list;
-% if type is a string as 'snirf', it is the same as setting
-% type as {'aux','data','nirs','stim','measurementList'}.
-%
-% output:
-% data: a reorganized matlab structure.
-%
-% example:
-% a=struct('a1',rand(5),'a2','string','a3',true,'d',2+3i,'e',{'test',[],1:5});
-% regrouph5(a)
-% saveh5(a,'test.h5');
-% rawdata=loadh5('test.h5')
-% data=regrouph5(rawdata)
-%
-% this file is part of EasyH5 Toolbox: https://github.com/fangq/easyh5
-%
-% License: GPLv3 or 3-clause BSD license, see https://github.com/fangq/easyh5 for details
-%
-
-if(nargin<1)
- help regrouph5;
- return;
-end
-
-dict={};
-if(~isempty(varargin))
- if(ischar(varargin{1}) && strcmpi(varargin{1},'snirf'))
- dict={'aux','data','nirs','stim','measurementList'};
- elseif(iscell(varargin{1}))
- dict=varargin{1};
- end
-end
-
-data=struct;
-if(isstruct(root))
- data=repmat(struct,size(root));
- names=fieldnames(root);
- newnames=struct();
- firstpos=struct();
-
- for i=1:length(names)
- item=regexp(names{i},'^(.*\D)(\d+)$','tokens');
- if(~isempty(item) && str2double(item{1}{2})~=0 && ~isfield(root,item{1}{1}))
- if(~isfield(newnames,item{1}{1}))
- newnames.(item{1}{1})=str2double(item{1}{2});
- else
- newnames.(item{1}{1})=[newnames.(item{1}{1}), str2double(item{1}{2})];
- end
- if(~isfield(firstpos,item{1}{1}))
- firstpos.(item{1}{1})=length(fieldnames(data(1)));
- end
- else
- for j=1:length(root)
- if(isstruct(root(j).(names{i})))
- data(j).(names{i})=regrouph5(root(j).(names{i}));
- else
- data(j).(names{i})=root(j).(names{i});
- end
- end
- end
- end
-
- names=fieldnames(newnames);
- if(~isempty(dict))
- names=intersect(names,dict);
- end
-
- for i=length(names):-1:1
- len=length(newnames.(names{i}));
- idx=newnames.(names{i});
- if((min(idx)~=1 || max(idx)~=len) && len~=1)
- for j=1:len
- dataname=sprintf('%s%d',names{i},idx(j));
- for k=1:length(root)
- if(isstruct(root(k).(dataname)))
- data(k).(dataname)=regrouph5(root(k).(dataname));
- else
- data(k).(dataname)=root(k).(dataname);
- end
- end
- end
- pos=firstpos.(names{i});
- len=length(fieldnames(data));
- data=orderfields(data,[1:pos,len,pos+1:len-1]);
- continue;
- end
- for j=1:length(data)
- data(j).(names{i})=cell(1,len);
- end
- idx=sort(idx);
- for j=1:len
- for k=1:length(root)
- obj=root(k).(sprintf('%s%d',names{i},idx(j)));
- if(isstruct(obj))
- data(k).(names{i}){j}=regrouph5(obj);
- else
- data(k).(names{i}){j}=obj;
- end
- end
- end
- pos=firstpos.(names{i});
- len=length(fieldnames(data));
- data=orderfields(data,[1:pos,len,pos+1:len-1]);
- try
- data.(names{i})=cell2mat(data.(names{i}));
- catch
- end
- end
-end
diff --git a/external/easyh5/saveh5.m b/external/easyh5/saveh5.m
deleted file mode 100644
index 31ac10cde..000000000
--- a/external/easyh5/saveh5.m
+++ /dev/null
@@ -1,377 +0,0 @@
-function saveh5(data, fname, varargin)
-%
-% saveh5(data, outputfile)
-% or
-% saveh5(data, outputfile, options)
-% saveh5(data, outputfile, 'Param1',value1, 'Param2',value2,...)
-%
-% Save a MATLAB struct (array) or cell (array) into an HDF5 file
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% data: a structure (array) or cell (array) to be stored.
-% fname: the output HDF5 (.h5) file name
-% options: (optional) a struct or Param/value pairs for user specified options
-% JData [0|1] use JData Specifiation to serialize complex data structures
-% such as complex/sparse arrays, tables, maps, graphs etc by
-% calling jdataencode before saving data to HDF5
-% RootName: the HDF5 path of the root object. If not given, the
-% actual variable name for the data input will be used as
-% the root object. The value shall not include '/'.
-% UnpackHex [1|0]: convert the 0x[hex code] in variable names
-% back to Unicode string using decodevarname.m
-% Compression: ['deflate'|''] - use zlib-deflate method
-% to compress data array
-% CompressArraySize: [100|int]: only to compress an array if the
-% total element count is larger than this number.
-% CompressLevel: [5|int] - a number between 1-9 to set
-% compression level
-% Chunk: a size vector or empty - breaking a large array into
-% small chunks of size specified by this parameter
-% ComplexFormat: {'realKey','imagKey'}: use 'realKey' and 'imagKey'
-% as keywords for the real and the imaginary part of a
-% complex array, respectively (sparse arrays not supported);
-% the default values are {'Real','Imag'}
-%
-% example:
-% a=struct('a',rand(5),'b','string','c',true,'d',2+3i,'e',{'test',[],1:5});
-% saveh5(a,'test.h5');
-% saveh5(a(1),'test2.h5','rootname','');
-% saveh5(a(1),'test2.h5','compression','deflate','compressarraysize',1);
-% saveh5(a,'test.h5j','jdata',1);
-%
-% this file is part of EasyH5 Toolbox: https://github.com/fangq/easyh5
-%
-% License: GPLv3 or 3-clause BSD license, see https://github.com/fangq/easyh5 for details
-%
-
-
-if(nargin<2)
- error('you must provide at least two inputs');
-end
-
-rootname=['/' inputname(1)];
-opt=struct;
-
-if(length(varargin)==1 && ischar(varargin{1}))
- rootname=[varargin{1} '/' inputname(1)];
-else
- opt=varargin2struct(varargin{:});
-end
-
-opt.compression=jsonopt('Compression','',opt);
-opt.compresslevel=jsonopt('CompressLevel',5,opt);
-opt.compressarraysize=jsonopt('CompressArraySize',100,opt);
-opt.unpackhex=jsonopt('UnpackHex',1,opt);
-
-opt.releaseid=0;
-vers=ver('MATLAB');
-if(~isempty(vers))
- opt.releaseid=datenum(vers(1).Date);
-end
-opt.skipempty=(opt.releaseid0)
- H5F.close(fid);
- end
- rethrow(ME);
-end
-
-if(~isa(fname,'H5ML.id'))
- H5F.close(fid);
-end
-
-%%-------------------------------------------------------------------------
-function oid=obj2h5(name, item,handle,level,varargin)
-
-if(iscell(item))
- oid=cell2h5(name,item,handle,level,varargin{:});
-elseif(isstruct(item))
- oid=struct2h5(name,item,handle,level,varargin{:});
-elseif(ischar(item) || isa(item,'string'))
- oid=mat2h5(name,item,handle,level,varargin{:});
-elseif(isa(item,'containers.Map'))
- oid=map2h5(name,item,handle,level,varargin{:});
-elseif(isa(item,'categorical'))
- oid=cell2h5(name,cellstr(item),handle,level,varargin{:});
-elseif(islogical(item) || isnumeric(item))
- oid=mat2h5(name,item,handle,level,varargin{:});
-else
- oid=any2h5(name,item,handle,level,varargin{:});
-end
-
-%%-------------------------------------------------------------------------
-function oid=idxobj2h5(name, idx, varargin)
-oid=obj2h5(sprintf('%s%d',name,idx), varargin{:});
-
-%%-------------------------------------------------------------------------
-function oid=cell2h5(name, item,handle,level,varargin)
-
-num=numel(item);
-if(num>1)
- idx=reshape(1:num,size(item));
- idx=num2cell(idx);
- oid=cellfun(@(x,id) idxobj2h5(name, id, x, handle,level,varargin{:}), item, idx, 'UniformOutput',false);
-else
- oid=cellfun(@(x) obj2h5(name, x, handle,level,varargin{:}), item, 'UniformOutput',false);
-end
-
-%%-------------------------------------------------------------------------
-function oid=struct2h5(name, item,handle,level,varargin)
-
-num=numel(item);
-if(num>1)
- oid=obj2h5(name, num2cell(item),handle,level,varargin{:});
-else
- pd = 'H5P_DEFAULT';
- gcpl = H5P.create('H5P_GROUP_CREATE');
- tracked = H5ML.get_constant_value('H5P_CRT_ORDER_TRACKED');
- indexed = H5ML.get_constant_value('H5P_CRT_ORDER_INDEXED');
- order = bitor(tracked,indexed);
- H5P.set_link_creation_order(gcpl,order);
- if(varargin{1}.unpackhex)
- name=decodevarname(name);
- end
- try
- handle=H5G.create(handle, name, pd,gcpl,pd);
- isnew=1;
- catch
- isnew=0;
- end
-
- names=fieldnames(item);
- oid=cell(1,length(names));
- for i=1:length(names)
- oid{i}=obj2h5(names{i},item.(names{i}),handle,level+1,varargin{:});
- end
-
- if(isnew)
- H5G.close(handle);
- end
-end
-
-%%-------------------------------------------------------------------------
-function oid=map2h5(name, item,handle,level,varargin)
-
-pd = 'H5P_DEFAULT';
-gcpl = H5P.create('H5P_GROUP_CREATE');
-tracked = H5ML.get_constant_value('H5P_CRT_ORDER_TRACKED');
-indexed = H5ML.get_constant_value('H5P_CRT_ORDER_INDEXED');
-order = bitor(tracked,indexed);
-H5P.set_link_creation_order(gcpl,order);
-try
- if(varargin{1}.unpackhex)
- name=decodevarname(name);
- end
- handle=H5G.create(handle, name, pd,gcpl,pd);
- isnew=1;
-catch
- isnew=0;
-end
-
-names=item.keys;
-oid=zeros(length(names));
-for i=1:length(names)
- oid(i)=obj2h5(names{i},item(names{i}),handle,level+1,varargin{:});
-end
-
-if(isnew)
- H5G.close(handle);
-end
-
-%%-------------------------------------------------------------------------
-function oid=mat2h5(name, item,handle,level,varargin)
-if(isa(item,'string'))
- item=char(item);
-end
-typemap=h5types;
-
-pd = 'H5P_DEFAULT';
-gcpl = H5P.create('H5P_GROUP_CREATE');
-tracked = H5ML.get_constant_value('H5P_CRT_ORDER_TRACKED');
-indexed = H5ML.get_constant_value('H5P_CRT_ORDER_INDEXED');
-order = bitor(tracked,indexed);
-H5P.set_link_creation_order(gcpl,order);
-
-opt=varargin{1};
-if(~(isfield(opt,'complexformat') && iscellstr(opt.complexformat) && numel(opt.complexformat)==2) || strcmp(opt.complexformat{1},opt.complexformat{2}))
- opt.complexformat={'Real','Imag'};
-end
-
-usefilter=opt.compression;
-complevel=opt.compresslevel;
-minsize=opt.compressarraysize;
-chunksize=jsonopt('Chunk',size(item),opt);
-
-if(isa(item,'logical'))
- item=uint8(item);
-end
-
-if(~isempty(usefilter) && numel(item)>=minsize)
- if(isnumeric(usefilter) && usefilter(1)==1)
- usefilter='deflate';
- end
- if(strcmpi(usefilter,'deflate'))
- pd = H5P.create('H5P_DATASET_CREATE');
- h5_chunk_dims = fliplr(chunksize);
- H5P.set_chunk(pd,h5_chunk_dims);
- H5P.set_deflate(pd,complevel);
- else
- error('Filter %s is unsupported',usefilter);
- end
-end
-
-if(opt.unpackhex)
- name=decodevarname(name);
-end
-
-oid=[];
-
-if(isempty(item) && opt.skipempty)
- warning('The HDF5 library is older than v1.8.7, and can not save empty datasets. Skip saving "%s"',name);
- return;
-end
-
-if(isreal(item))
- if(issparse(item))
- idx=find(item);
- oid=sparse2h5(name,struct('Size',size(item),'SparseIndex',idx,'Real',item(idx)),handle,level,varargin{:});
- else
- oid=H5D.create(handle,name,H5T.copy(typemap.(class(item))),H5S.create_simple(ndims(item), fliplr(size(item)),fliplr(size(item))),pd);
- H5D.write(oid,'H5ML_DEFAULT','H5S_ALL','H5S_ALL','H5P_DEFAULT',item);
- end
-else
- if(issparse(item))
- idx=find(item);
- oid=sparse2h5(name,struct('Size',size(item),'SparseIndex',idx,'Real',real(item(idx)),'Imag',imag(item(idx))),handle,level,varargin{:});
- else
- typeid=H5T.copy(typemap.(class(item)));
- elemsize=H5T.get_size(typeid);
- memtype = H5T.create ('H5T_COMPOUND', elemsize*2);
- H5T.insert (memtype,opt.complexformat{1}, 0, typeid);
- H5T.insert (memtype,opt.complexformat{2}, elemsize, typeid);
- oid=H5D.create(handle,name,memtype,H5S.create_simple(ndims(item), fliplr(size(item)),fliplr(size(item))),pd);
- H5D.write(oid,'H5ML_DEFAULT','H5S_ALL','H5S_ALL','H5P_DEFAULT',struct(opt.complexformat{1},real(item),opt.complexformat{2},imag(item)));
- end
-end
-if(~isempty(oid))
- H5D.close(oid);
-end
-
-%%-------------------------------------------------------------------------
-function oid=sparse2h5(name, item,handle,level,varargin)
-
-opt=varargin{1};
-
-idx=item.SparseIndex;
-
-if(isempty(idx) && opt.skipempty)
- warning('The HDF5 library is older than v1.8.7, and can not save empty datasets. Skip saving "%s"',name);
- oid=[];
- return;
-end
-
-adata=item.Size;
-item=rmfield(item,'Size');
-hasimag=isfield(item,'Imag');
-
-typemap=h5types;
-
-pd = 'H5P_DEFAULT';
-
-
-usefilter=opt.compression;
-complevel=opt.compresslevel;
-minsize=opt.compressarraysize;
-chunksize=jsonopt('Chunk',size(item),opt);
-
-if(~isempty(usefilter) && numel(idx)>=minsize)
- if(isnumeric(usefilter) && usefilter(1)==1)
- usefilter='deflate';
- end
- if(strcmpi(usefilter,'deflate'))
- pd = H5P.create('H5P_DATASET_CREATE');
- h5_chunk_dims = fliplr(chunksize);
- H5P.set_chunk(pd,h5_chunk_dims);
- H5P.set_deflate(pd,complevel);
- else
- error('Filter %s is unsupported',usefilter);
- end
-end
-
-idxtypeid=H5T.copy(typemap.(class(idx)));
-idxelemsize=H5T.get_size(idxtypeid);
-datatypeid=H5T.copy(typemap.(class(item.Real)));
-dataelemsize=H5T.get_size(datatypeid);
-memtype = H5T.create ('H5T_COMPOUND', idxelemsize+dataelemsize*(1+hasimag));
-H5T.insert (memtype,'SparseIndex', 0, idxtypeid);
-H5T.insert (memtype,'Real', idxelemsize, datatypeid);
-if(hasimag)
- H5T.insert (memtype,'Imag', idxelemsize+dataelemsize, datatypeid);
-end
-oid=H5D.create(handle,name,memtype,H5S.create_simple(ndims(idx), fliplr(size(idx)),fliplr(size(idx))),pd);
-H5D.write(oid,'H5ML_DEFAULT','H5S_ALL','H5S_ALL','H5P_DEFAULT',item);
-
-space_id=H5S.create_simple(ndims(adata), fliplr(size(adata)),fliplr(size(adata)));
-attr_size = H5A.create(oid,'SparseArraySize',H5T.copy('H5T_NATIVE_DOUBLE'),space_id,H5P.create('H5P_ATTRIBUTE_CREATE'));
-H5A.write(attr_size,'H5ML_DEFAULT',adata);
-H5A.close(attr_size);
-
-%%-------------------------------------------------------------------------
-function oid=any2h5(name, item,handle,level,varargin)
-pd = 'H5P_DEFAULT';
-
-if(varargin{1}.unpackhex)
- name=decodevarname(name);
-end
-
-rawdata=getByteStreamFromArray(item); % use undocumented matlab function
-oid=H5D.create(handle,name,H5T.copy('H5T_STD_U8LE'),H5S.create_simple(ndims(rawdata), size(rawdata),size(rawdata)),pd);
-H5D.write(oid,'H5ML_DEFAULT','H5S_ALL','H5S_ALL',pd,rawdata);
-
-adata=class(item);
-space_id=H5S.create_simple(ndims(adata), size(adata),size(adata));
-attr_type = H5A.create(oid,'MATLABObjectClass',H5T.copy('H5T_C_S1'),space_id,H5P.create('H5P_ATTRIBUTE_CREATE'));
-H5A.write(attr_type,'H5ML_DEFAULT',adata);
-H5A.close(attr_type);
-
-adata=size(item);
-space_id=H5S.create_simple(ndims(adata), size(adata),size(adata));
-attr_size = H5A.create(oid,'MATLABObjectSize',H5T.copy('H5T_NATIVE_DOUBLE'),space_id,H5P.create('H5P_ATTRIBUTE_CREATE'));
-H5A.write(attr_size,'H5ML_DEFAULT',adata);
-H5A.close(attr_size);
-
-H5D.close(oid);
-
-%%-------------------------------------------------------------------------
-function typemap=h5types
-typemap.char='H5T_C_S1';
-typemap.string='H5T_C_S1';
-typemap.double='H5T_IEEE_F64LE';
-typemap.single='H5T_IEEE_F32LE';
-typemap.logical='H5T_STD_U8LE';
-typemap.uint8='H5T_STD_U8LE';
-typemap.int8='H5T_STD_I8LE';
-typemap.uint16='H5T_STD_U16LE';
-typemap.int16='H5T_STD_I16LE';
-typemap.uint32='H5T_STD_U32LE';
-typemap.int32='H5T_STD_I32LE';
-typemap.uint64='H5T_STD_U64LE';
-typemap.int64='H5T_STD_I64LE';
diff --git a/external/easyh5/varargin2struct.m b/external/easyh5/varargin2struct.m
deleted file mode 100644
index 7cf618721..000000000
--- a/external/easyh5/varargin2struct.m
+++ /dev/null
@@ -1,40 +0,0 @@
-function opt=varargin2struct(varargin)
-%
-% opt=varargin2struct('param1',value1,'param2',value2,...)
-% or
-% opt=varargin2struct(...,optstruct,...)
-%
-% convert a series of input parameters into a structure
-%
-% authors:Qianqian Fang (q.fang neu.edu)
-% date: 2012/12/22
-%
-% input:
-% 'param', value: the input parameters should be pairs of a string and a value
-% optstruct: if a parameter is a struct, the fields will be merged to the output struct
-%
-% output:
-% opt: a struct where opt.param1=value1, opt.param2=value2 ...
-%
-% license:
-% BSD or GPL version 3, see LICENSE_{BSD,GPLv3}.txt files for details
-%
-% -- this function is part of JSONLab toolbox (http://iso2mesh.sf.net/cgi-bin/index.cgi?jsonlab)
-%
-
-len=length(varargin);
-opt=struct;
-if(len==0) return; end
-i=1;
-while(i<=len)
- if(isstruct(varargin{i}))
- opt=mergestruct(opt,varargin{i});
- elseif(ischar(varargin{i}) && i 0)
- if ismember(i, selectedBlocks)
- board_dig_in_raw(board_dig_in_index:(board_dig_in_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'uint16');
+ if ismember(i, selectedBlocks) && loadEvents
+ board_dig_in_raw(:, board_dig_in_index:(board_dig_in_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'uint16');
else
fseek(fid, num_samples_per_data_block*2,'cof');
end
end
if (num_board_dig_out_channels > 0)
- if ismember(i, selectedBlocks)
+ if ismember(i, selectedBlocks) && loadEvents
board_dig_out_raw(board_dig_out_index:(board_dig_out_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'uint16');
else
fseek(fid, num_samples_per_data_block*2,'cof');
diff --git a/external/jsnirfy/README.md b/external/jsnirfy/README.md
deleted file mode 100644
index 9101c8ffa..000000000
--- a/external/jsnirfy/README.md
+++ /dev/null
@@ -1,198 +0,0 @@
-# JSNIRF Toolbox - A portable MATLAB toolbox for parsing SNIRF (HDF5) and JSNIRF (JSON) files
-
-* Copyright (C) 2019 Qianqian Fang
-* License: GNU General Public License version 3 (GPL v3) or Apache License 2.0, see License*.txt
-* Version: 0.4 (code name: Amygdala - alpha)
-* URL: https://github.com/NeuroJSON/jsnirf/tree/master/lib/matlab
-
-## Overview
-
-JSNIRF is a portable format for storage, interchange and processing data generated
-from functional near-infrared spectroscopy, or fNIRS - an emerging functional neuroimaging
-technique. Built upon the JData and SNIRF specifications, a JSNIRF file has both a
-text-based interface using the JavaScript Object Notation (JSON) [RFC4627] format
-and a binary interface using the Universal Binary JSON (UBJSON, http://ubjson.org) derived
-Binary JData ([BJData](https://github.com/NeuroJSON/bjdata)) serialization format.
-It contains a compatibility layer to provide a 1-to-1 mapping to the existing
-HDF5 based SNIRF files. A JSNIRF file can be directly parsed by most existing
-JSON and BJData parsers. Advanced features include optional hierarchical data
-storage, grouping, compression, integration with heterogeneous scientific data
-enabled by JData data serialization framework.
-
-This toolbox also provides a fast/complete reader/writer for the HDF5-based SNIRF
-files (along with any HDF5 data) via the EazyH5 toolbox
-(http://github.com/fangq/eazyh5). The toolbox can read/write SNIRF v1.0 data
-files specified by the SNIRF specification http://github.com/fNIRS/snirf .
-
-This toolbox is selectively dependent on the below toolboxes
-- To read/write SNIRF/HDF5 files, one must install the EazyH5 toolbox at
- http://github.com/fangq/eazyh5 ; this is only supported on MATLAB, not Octave.
-- To create/read/write JSNIRF files, one must install the JSONLab toolbox
- http://github.com/NeuroJSON/jsonlab ; this is supported on both MATLAB and Octave.
-- To read/write JSNIRF files with internal data compression, one must install
- the JSONLab toolbox http://github.com/NeuroJSON/jsonlab as well as ZMat toolbox
- http://github.com/fangq/zmat ; this is supported on both MATLAB and Octave.
-
-## Why JSNIRF?
-
-A SNIRF data file is basically an HDF5 file. HDF5 (Hierarchical Data Format version 5)
-is a general purpose file format for storing flexible binary data. However, it has
-the below limitations:
-
-- it is binary, not human readable, you must use a parser to load the file
- and understand the content
-- it requires a spacial library, although widely and freely available, to load
- or save such file; dependeny to such library requires extra work for deployment
-- HDF5 is a very sophisticated format; writing your own parser is quite difficult
-- when storing a small dataset, an HDF5 file has an overhead in file size
-
-In comparison, the JSNIRF data format is defined based on the JData specification.
-and supports both a text-based interface and a binary interface. The text form
-JSNIRF file is a plain JSON file, and has various advantages
-
-- JSNIRF is human readable, you can read the data using an editor
-- JSNIRF is very simple (because JSON format is very simple)
-- JSNIRF is lightweight, little overhead for storing small datasets
-- JSNIRF can be readily parsed by numerous free JSON parsers available
-- Programming your own specialized JSNIRF parser is very easy to write
-
-The binary JSNIRF format uses a binary JSON format (BJData) which is also
-- quasi-human readable despite it is binary
-- free parsers available for [MATLAB](http://github.com/fangq/jsonlab),
- [Python](https://pypi.org/project/bjdata/), [C++](https://github.com/NeuroJSON/json),
- and [C](https://github.com/NeuroJSON/ubj)
-- easy to write your own parser because of the simplicity
-
-
-
-## SNIRF and JSNIRF format compatibility
-
-The JSNIRF data structure is highly compatible with the SNIRF data structure.
-This toolbox provides utilities convert from one form to the other losslessly.
-
-There are only two minor differences:
-* A JSNIRF data container renames the SNIRF `/nirs` root object as `SNIRFData`.
- If multiple measurement datasets are provided in the SNIRF data in the forms of
- `/nirs1`, `/nirs2` ..., or `/nirs/data1`. `/nirs/data2` ..., JSNIRF merges these
- data objects into struct/cell arrays, and removes the group indices from the
- group names. These grouped objects are stored as an JSON/BJData array object
- '[]' when saving to disk.
-* The `/formatVersion` object in the SNIRF data are moved from the root level
- to a subfield of `SNIRFData`, this allows the JSNIRF data files to be easily
- mixed/integrated with other JSON-based data containers, such as `SNIRFData`
- defined in other JData based data formats.
-
-To further illustrate the above data reorganization steps, please find below
-an example
-
-An original SNIRF/HDF5 data outline
-```
-/formatVersion
-/nirs1/
- /metaDataTags
- /data1
- /data2
- /aux1
- /aux2
- /probe
- ...
-/nirs2/
- /metaDataTags
- /data
- /aux1
- /aux2
- /aux3
- /probe
- ...
-```
-is converted to the below JSON/JSNIRF data structure
-```
-{
- "SNIRFData": [
- {
- "formatVersion": '1.0',
- "metaDataTags":{
- "SubjectID": ...
- },
- "data": [
- {..for data1 ...},
- {..for data2 ...}
- ],
- "aux": [
- {..for aux1 ...},
- {..for aux2 ...}
- ],
- "probe": ...
- },
- {
- "formatVersion": '1.0',
- "metaDataTags":{
- "SubjectID": ...
- },
- "data": {...},
- "aux": [
- {..for aux1 ...},
- {..for aux2 ...},
- {..for aux3 ...}
- ],
- "probe": ...
- },
- ...
- ]
-}
-```
-
-## Installation
-
-The JSNIRF toolbox can be installed using a single command
-```
- addpath('/path/to/jsnirf');
-```
-where the `/path/to/jsnirf` should be replaced by the unzipped folder
-of the toolbox (i.e. the folder containing `savejsnirf.m/loadjsnirf.m`).
-
-In order for this toolbox to work, one must install the below dependencies
-- the `saveh5/loadh5` functions are provided by the EazyH5 toolbox at
- http://github.com/fangq/eazyh5
-- the `savejson` and `savebj` functions are provided by the JSONLab
- toolbox at http://github.com/NeuroJSON/jsonlab
-- if data compression is specified by `'compression','zlib'` param/value
- pairs, ZMat toolbox will be needed, http://github.com/fangq/zmat
-
-
-## Usage
-
-### `snirfcreate/jsnirfcreate` - Create an empty SNIRF or JSNIRF data container (structure)
-Example:
-```
- data=snirfcreate; % create an empty SNIRF data structure
- data=snirfcreate('data',realdata,'aux',realauxdata); % setting the default values to user data
- data=jsnirfcreate('format','snirf'); % specify 'snirf' or 'jsnirf' using 'format' option
- jsn=snirfdecode(loadh5('mydata.snirf')); % load raw HDF5 data and convert to a JSNIRF struct
-```
-### `loadsnirf/loadjsnirf` - Loading SNIRF/JSNIRF files as in-memory MATLAB data structures
-Example:
-```
- data=loadsnirf('mydata.snirf'); % load an HDF5 SNIRF data file, same as loadh5+regrouph5
- jdata=loadjsnirf('mydata.bnirs'); % load a binary JSON/JSNIRF data file
-```
-### `savesnirf/savejsnirf` - Saving in-memory MATLAB data structure into SNIRF/HDF5 or JSNIRF/JSON files
-Example:
-```
- data=snirfcreate;
- data.nirs.data.dataTimeSeries=rand(100,5);
- data.nirs.metaDataTags.SubjectID='subj1';
- data.nirs.metaDataTags.MeasurementDate=date;
- data.nirs.metaDataTags.MeasurementTime=datestr(now,'HH:MM:SS');
- savesnirf(data,'test.snirf');
- savejsnirf(data,'test.jnirs');
-```
-
-## Contribute to JSNIRF
-
-Please submit your bug reports, feature requests and questions to the Github Issues page at
-
-https://github.com/NeuroJSON/jsnirf/issues
-
-Please feel free to fork our software, making changes, and submit your revision back
-to us via "Pull Requests". JSNIRF toolbox is open-source and we welcome your contributions!
diff --git a/external/jsnirfy/aos2soa.m b/external/jsnirfy/aos2soa.m
deleted file mode 100644
index 48f963692..000000000
--- a/external/jsnirfy/aos2soa.m
+++ /dev/null
@@ -1,40 +0,0 @@
-function st=aos2soa(starray)
-%
-% st=aos2soa(starray)
-%
-% Convert an array-of-structs (AoS) to a struct-of-arrays (SoA)
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% starray: a struct array, with each subfield a simple scalar
-%
-% output:
-% str: a struct, containing the same number of subfields as starray
-% with each subfield a horizontal-concatenation of the struct
-% array subfield values.
-%
-% example:
-% a=struct('a',1,'b','0','c',[1,3]');
-% st=aos2soa(repmat(a,1,10))
-%
-% this file is part of JSNIRF specification: https://github.com/NeuroJSON/jsnirf
-%
-% License: GPLv3 or Apache 2.0, see https://github.com/NeuroJSON/jsnirf for details
-%
-
-
-if(nargin<1 || ~isstruct(starray))
- error('you must give an array of struct');
-end
-
-if(numel(starray)==1)
- st=starray;
- return;
-end
-
-st=struct;
-fn=fieldnames(starray);
-for i=1:length(fn)
- st.(fn{i})=[starray(:).(fn{i})];
-end
\ No newline at end of file
diff --git a/external/jsnirfy/jsnirfcreate.m b/external/jsnirfy/jsnirfcreate.m
deleted file mode 100644
index e62853eb5..000000000
--- a/external/jsnirfy/jsnirfcreate.m
+++ /dev/null
@@ -1,78 +0,0 @@
-function jsn=jsnirfcreate(varargin)
-%
-% jsn=jsnirfcreate
-% or
-% jsn=jsnirfcreate(option)
-% jsn=jsnirfcreate('Format',format,'Param1',value1, 'Param2',value2,...)
-%
-% Create an empty JSNIRF data structure defined in the JSNIRF
-% specification: https://github.com/NeuroJSON/jsnirf or a SNIRF data structure
-% based on https://github.com/fNIRS/snirf
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% option (optional): option can be ignored. If it is a string with a
-% value 'snirf', this creates a default SNIRF data structure;
-% otherwise, a JSNIRF data structure is created.
-% format: same as option.
-% param/value: a list of name/value pairs specify
-% additional subfields to be stored under the /nirs object.
-%
-% output:
-% jsn: a default SNIRF or JSNIRF data structure.
-%
-% example:
-% jsn=jsnirfcreate('data',mydata,'aux',myauxdata,'comment','test');
-%
-% this file is part of JSNIRF specification: https://github.com/NeuroJSON/jsnirf
-%
-% License: GPLv3 or Apache 2.0, see https://github.com/NeuroJSON/jsnirf for details
-%
-
-% define empty SNIRF data structure with all required fields
-
-defaultmeta=struct('SubjectID','default','MeasurementDate',datestr(now,29),...
- 'MeasurementTime',datestr(now,'hh:mm:ss'),'LengthUnit','mm', ...
- 'TimeUnit','s', 'FrequencyUnit','Hz');
-defaultsrcmap=struct('sourceIndex',[],'detectorIndex',[],...
- 'wavelengthIndex',[],'dataType',1,'dataTypeIndex',1);
-defaultdata=struct('dataTimeSeries',[],'time',[],'measurementList',defaultsrcmap);
-defaultaux=struct('name','','dataTimeSeries',[],'time',[],'timeOffset',0);
-defaultstim=struct('name','','data',[]);
-defaultprobe=struct('wavelengths',[],'sourcePos2D',[],'detectorPos2D',[]);
-
-nirsdata=struct('metaDataTags',defaultmeta,...
- 'data',defaultdata,...
- 'aux',defaultaux,...
- 'stim',defaultstim,...
- 'probe',defaultprobe);
-
-% read user specified data fields - will validate format in future updates
-
-if(nargin>1 && bitand(nargin,1)==0)
- for i=1:nargin*0.5
- key=varargin{2*i-1};
- if(strcmpi(key,'format'))
- key='format';
- end
- nirsdata.(key)=varargin{2*i};
- end
-end
-
-jsn=struct();
-
-% return either a SNIRF data structure, or JSNIRF data (enclosed in SNIRFData tag)
-
-if((nargin==1 && strcmpi(varargin{1},'snirf')) || ...
- (isfield(nirsdata,'format') && strcmpi(nirsdata.format,'snirf')))
- if(isfield(nirsdata,'format'))
- nirsdata=rmfield(nirsdata,'format');
- end
- jsn=struct('formatVersion','1.0','nirs', nirsdata);
-else
- nirsdata.formatVersion='1.0';
- len=length(fieldnames(nirsdata));
- nirsdata=orderfields(nirsdata,[len,1:len-1]);
- jsn=struct('SNIRFData', nirsdata);
-end
diff --git a/external/jsnirfy/loadsnirf.m b/external/jsnirfy/loadsnirf.m
deleted file mode 100644
index 44704cf2c..000000000
--- a/external/jsnirfy/loadsnirf.m
+++ /dev/null
@@ -1,63 +0,0 @@
-function data=loadsnirf(fname,varargin)
-%
-% data=loadsnirf(fname)
-% or
-% jnirs=loadsnirf(fname, 'Param1',value1, 'Param2',value2,...)
-%
-% Load an HDF5 based SNIRF file, and optionally convert it to a JSON
-% file based on the JSNIRF specification:
-% https://github.com/NeuroJSON/jsnirf
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% fname: the input snirf data file name (HDF5 based)
-%
-% output:
-% data: a MATLAB structure with the grouped data fields
-%
-% dependency:
-% - the loadh5/regrouph5 functions are provided by the eazyh5
-% toolbox at http://github.com/fangq/eazyh5
-% - the varargin2struct and jsonopt functions are provided by the JSONLab
-% toolbox at http://github.com/NeuroJSON/jsonlab
-% - if data compression is specified by 'compression','zlib' param/value
-% pairs, ZMat toolbox will be needed, http://github.com/fangq/zmat
-%
-% example:
-% data=loadsnirf('test.snirf');
-%
-% this file is part of JSNIRF specification: https://github.com/NeuroJSON/jsnirf
-%
-% License: GPLv3 or Apache 2.0, see https://github.com/NeuroJSON/jsnirf for details
-%
-
-if(nargin==0 || ~ischar(fname))
- error('you must provide a file name');
-end
-
-data=loadh5(fname);
-
-opt=struct;
-
-if(length(varargin)==1)
- data=snirfdecode(data,varargin{:});
-elseif(length(varargin)>=2)
- opt=varargin2struct(varargin{:});
- data=snirfdecode(data,varargin{:});
-else
- data=snirfdecode(data);
-end
-
-outfile=jsonopt('FileName','',opt);
-if(~isempty(outfile))
- if(regexp(outfile,'\.[Bb][Nn][Ii][Rr][Ss]$'))
- savebj('SNIRFData',data,'FileName',outfile,opt);
- elseif(~isempty(regexp(outfile,'\.[Jj][Nn][Ii][Rr][Ss]$', 'once'))|| ~isempty(regexp(outfile,'\.[Jj][Ss][Oo][Nn]$', 'once')))
- savejson('SNIRFData',data,'FileName',outfile,opt);
- elseif(regexp(outfile,'\.[Mm][Aa][Tt]$'))
- save(outfile,'data');
- else
- error('only support .jnirs,.bnirs and .mat files');
- end
-end
diff --git a/external/jsnirfy/savesnirf.m b/external/jsnirfy/savesnirf.m
deleted file mode 100644
index 32f225754..000000000
--- a/external/jsnirfy/savesnirf.m
+++ /dev/null
@@ -1,77 +0,0 @@
-function savesnirf(data, outfile,varargin)
-%
-% savesnirf(snirfdata, fname)
-% or
-% savesnirf(snirfdata, fname, 'Param1',value1, 'Param2',value2,...)
-%
-% Load an HDF5 based SNIRF file, and optionally convert it to a JSON
-% file based on the JSNIRF specification:
-% https://github.com/NeuroJSON/jsnirf
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% snirfdata: a raw SNIRF data, preprocessed SNIRF data or JSNIRF
-% data (root object must be SNIRFData)
-% fname: the output SNIRF (.snirf) or JSNIRF data file name (.jnirs, .bnirs)
-%
-% output:
-% data: a MATLAB structure with the grouped data fields
-%
-% example:
-% data=loadsnirf('test.snirf');
-% savesnirf(data,'newfile.snirf');
-%
-% this file is part of JSNIRF specification: https://github.com/NeuroJSON/jsnirf
-%
-% License: GPLv3 or Apache 2.0, see https://github.com/NeuroJSON/jsnirf for details
-%
-
-if(nargin<2 || ~ischar(outfile))
- error('you must provide data and a file name');
-end
-
-opt=varargin2struct(varargin{:});
-if(~isfield(opt,'root'))
- opt.rootname='';
-end
-
-if(isfield(data,'SNIRFData'))
- data.nirs=data.SNIRFData;
- data.formatVersion=data.SNIRFData.formatVersion;
- data.nirs=rmfield(data.nirs,'formatVersion');
- data=rmfield(data,'SNIRFData');
-end
-
-if(~isempty(outfile))
- if(~isempty(regexp(outfile,'\.[Hh]5$', 'once')))
- saveh5(data,outfile,opt);
- elseif(~isempty(regexp(outfile,'\.[Ss][Nn][Ii][Rr][Ff]$', 'once')))
- data.nirs.data=forceindex(data.nirs.data,'measurementList');
- data.nirs=forceindex(data.nirs,'data');
- data.nirs=forceindex(data.nirs,'stim');
- data.nirs=forceindex(data.nirs,'aux');
- saveh5(data,outfile,opt);
- elseif(~isempty(regexp(outfile,'\.[Jj][Nn][Ii][Rr][Ss]$', 'once'))|| ~isempty(regexp(outfile,'\.[Jj][Ss][Oo][Nn]$', 'once')))
- savejson('SNIRFData',data,'FileName',outfile,opt);
- elseif(regexp(outfile,'\.[Mm][Aa][Tt]$'))
- save(outfile,'data');
- elseif(regexp(outfile,'\.[Bb][Nn][Ii][Rr][Ss]$'))
- savebj('SNIRFData',data,'FileName',outfile,opt);
- else
- error('only support .snirf, .h5, .jnirs, .bnirs and .mat files');
- end
-end
-
-% force adding index 1 to the group name for singular struct and cell
-function newroot=forceindex(root,name)
-newroot=root;
-fields=fieldnames(newroot);
-idx=find(ismember(fields,name));
-if(~isempty(idx) && length(newroot.(name))==1)
- newroot.(sprintf('%s1',name))=newroot.(name);
- newroot=rmfield(newroot,name);
- fields{idx(1)}=sprintf('%s1',name);
- newroot=orderfields(newroot,fields);
-end
-
diff --git a/external/jsnirfy/snirfcreate.m b/external/jsnirfy/snirfcreate.m
deleted file mode 100644
index 169d0ab0a..000000000
--- a/external/jsnirfy/snirfcreate.m
+++ /dev/null
@@ -1,36 +0,0 @@
-function snf=snirfcreate(varargin)
-%
-% snf=snirfcreate
-% or
-% snf=snirfcreate(option)
-% snf=snirfcreate('Format',format,'Param1',value1, 'Param2',value2,...)
-%
-% Create a empty SNIRF data structure defined in the SNIRF
-% specification: https://github.com/fNIRS/snirf
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% option (optional): option can be ignored. If it is a string with a
-% value 'snirf', this creates a default SNIRF data structure;
-% otherwise, a JSNIRF data structure is created.
-% format: save as option.
-% param/value: a list of name/value pairs specify
-% additional subfields to be stored under the /nirs object.
-%
-% output:
-% snf: a default SNIRF or JSNIRF data structure.
-%
-% example:
-% snf=snirfcreate('data',mydata,'aux',myauxdata,'comment','test');
-%
-% this file is part of JSNIRF specification: https://github.com/fangq/snirf
-%
-% License: GPLv3 or Apache 2.0, see https://github.com/NeuroJSON/jsnirf for details
-%
-
-if(nargin==1)
- snf=jsnirfcreate(varargin{:});
-else
- snf=jsnirfcreate('Format','snirf',varargin{:});
-end
diff --git a/external/jsnirfy/snirfdecode.m b/external/jsnirfy/snirfdecode.m
deleted file mode 100644
index e16469c26..000000000
--- a/external/jsnirfy/snirfdecode.m
+++ /dev/null
@@ -1,65 +0,0 @@
-function data=snirfdecode(root, varargin)
-%
-% data=snirfdecode(root)
-% or
-% data=snirfdecode(root,type)
-% data=snirfdecode(root,{'nameA','nameB',...})
-%
-% Processing an HDF5 based SNIRF data and group indexed datasets into a
-% cell array
-%
-% author: Qianqian Fang (q.fang neu.edu)
-%
-% input:
-% root: the raw input snirf data structure (loaded from loadh5.m)
-% type: if type is set as a cell array of strings, it restrict the
-% grouping only to the subset of field names in this list;
-% if type is a string as 'snirf', it is the same as setting
-% type as {'aux','data','nirs','stim','measurementList'}.
-%
-% output:
-% data: a reorganized matlab structure. Each SNIRF data chunk is
-% enclosed inside a 'SNIRFData' subfield or cell array.
-%
-% example:
-% rawdata=loadh5('mydata.snirf');
-% data=snirfdecode(rawdata);
-%
-% this file is part of JSNIRF specification: https://github.com/NeuroJSON/jsnirf
-%
-% License: Apache 2.0, see https://github.com/NeuroJSON/jsnirf for details
-%
-
-if(nargin<1)
- help snirfdecode;
- return;
-end
-
-data=regrouph5(root, varargin{:});
-
-issnirf=1;
-
-if(~isempty(varargin))
- if(ischar(varargin{1}) && strcmpi(varargin{1},'jsnirf'))
- issnirf=0;
- end
-end
-
-if(issnirf==0 && isfield(data,'nirs') && isfield(data,'formatVersion') && ~isfield(data,'SNIRFData'))
- data.SNIRFData=data.nirs;
- if(isfield(data.SNIRFData,'data') && isfield(data.SNIRFData.data,'measurementList'))
- data.SNIRFData.data.measurementList=aos2soa(data.nirs.data.measurementList);
- end
- if(iscell(data.nirs))
- for i=1:length(data.nirs)
- data.SNIRFData{i}.formatVersion=data.formatVersion;
- len=length(fieldnames(data.SNIRFData{i}));
- data.SNIRFData{i}=orderfields(data.SNIRFData{i},[len,1:len-1]);
- end
- else
- data.SNIRFData.formatVersion=data.formatVersion;
- len=length(fieldnames(data.SNIRFData));
- data.SNIRFData=orderfields(data.SNIRFData,[len,1:len-1]);
- end
- data=rmfield(data,{'nirs','formatVersion'});
-end
\ No newline at end of file
diff --git a/external/npy-matlab/readNPY.m b/external/npy-matlab/readNPY.m
deleted file mode 100644
index 9095d003a..000000000
--- a/external/npy-matlab/readNPY.m
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-function data = readNPY(filename)
-% Function to read NPY files into matlab.
-% *** Only reads a subset of all possible NPY files, specifically N-D arrays of certain data types.
-% See https://github.com/kwikteam/npy-matlab/blob/master/tests/npy.ipynb for
-% more.
-%
-
-[shape, dataType, fortranOrder, littleEndian, totalHeaderLength, ~] = readNPYheader(filename);
-
-if littleEndian
- fid = fopen(filename, 'r', 'l');
-else
- fid = fopen(filename, 'r', 'b');
-end
-
-try
-
- [~] = fread(fid, totalHeaderLength, 'uint8');
-
- % read the data
- data = fread(fid, prod(shape), [dataType '=>' dataType]);
-
- if length(shape)>1 && ~fortranOrder
- data = reshape(data, shape(end:-1:1));
- data = permute(data, [length(shape):-1:1]);
- elseif length(shape)>1
- data = reshape(data, shape);
- end
-
- fclose(fid);
-
-catch me
- fclose(fid);
- rethrow(me);
-end
diff --git a/external/npy-matlab/readNPYheader.m b/external/npy-matlab/readNPYheader.m
deleted file mode 100644
index 165a58c89..000000000
--- a/external/npy-matlab/readNPYheader.m
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-function [arrayShape, dataType, fortranOrder, littleEndian, totalHeaderLength, npyVersion] = readNPYheader(filename)
-% function [arrayShape, dataType, fortranOrder, littleEndian, ...
-% totalHeaderLength, npyVersion] = readNPYheader(filename)
-%
-% parse the header of a .npy file and return all the info contained
-% therein.
-%
-% Based on spec at http://docs.scipy.org/doc/numpy-dev/neps/npy-format.html
-
-fid = fopen(filename);
-
-% verify that the file exists
-if (fid == -1)
- if ~isempty(dir(filename))
- error('Permission denied: %s', filename);
- else
- error('File not found: %s', filename);
- end
-end
-
-try
-
- dtypesMatlab = {'uint8','uint16','uint32','uint64','int8','int16','int32','int64','single','double', 'logical'};
- dtypesNPY = {'u1', 'u2', 'u4', 'u8', 'i1', 'i2', 'i4', 'i8', 'f4', 'f8', 'b1'};
-
-
- magicString = fread(fid, [1 6], 'uint8=>uint8');
-
- if ~all(magicString == [147,78,85,77,80,89])
- error('readNPY:NotNUMPYFile', 'Error: This file does not appear to be NUMPY format based on the header.');
- end
-
- majorVersion = fread(fid, [1 1], 'uint8=>uint8');
- minorVersion = fread(fid, [1 1], 'uint8=>uint8');
-
- npyVersion = [majorVersion minorVersion];
-
- headerLength = fread(fid, [1 1], 'uint16=>uint16');
-
- totalHeaderLength = 10+headerLength;
-
- arrayFormat = fread(fid, [1 headerLength], 'char=>char');
-
- % to interpret the array format info, we make some fairly strict
- % assumptions about its format...
-
- r = regexp(arrayFormat, '''descr''\s*:\s*''(.*?)''', 'tokens');
- dtNPY = r{1}{1};
-
- littleEndian = ~strcmp(dtNPY(1), '>');
-
- dataType = dtypesMatlab{strcmp(dtNPY(2:3), dtypesNPY)};
-
- r = regexp(arrayFormat, '''fortran_order''\s*:\s*(\w+)', 'tokens');
- fortranOrder = strcmp(r{1}{1}, 'True');
-
- r = regexp(arrayFormat, '''shape''\s*:\s*\((.*?)\)', 'tokens');
- shapeStr = r{1}{1};
- arrayShape = str2num(shapeStr(shapeStr~='L'));
-
-
- fclose(fid);
-
-catch me
- fclose(fid);
- rethrow(me);
-end
diff --git a/external/piotr_toolbox/LICENCE.txt b/external/piotr_toolbox/LICENCE.txt
new file mode 100644
index 000000000..4b692e467
--- /dev/null
+++ b/external/piotr_toolbox/LICENCE.txt
@@ -0,0 +1,26 @@
+Copyright (c) 2012, Piotr Dollar
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+either expressed or implied, of the FreeBSD Project.
\ No newline at end of file
diff --git a/external/piotr_toolbox/tpsGetWarp.m b/external/piotr_toolbox/tpsGetWarp.m
new file mode 100644
index 000000000..a88563b82
--- /dev/null
+++ b/external/piotr_toolbox/tpsGetWarp.m
@@ -0,0 +1,77 @@
+function [warp,L,LnInv,bendE] = tpsGetWarp( lambda, xsS, ysS, xsD, ysD )
+% Given two sets of corresponding points, calculates warp between them.
+%
+% Uses booksteins PAMI89 method. Can then apply warp to a new set of
+% points (tpsInterpolate), or even an image (tpsInterpolateIm).
+% "Principal Warps: Thin-Plate Splines and the Decomposition of
+% Deformations". Bookstein. PAMI 1989.
+%
+% USAGE
+% [warp,L,LnInv,bendE] = tpsGetWarp( lambda, xsS, ysS, xsD, ysD )
+%
+% INPUTS
+% lambda - rigidity of warp (inf means warp becomes affine)
+% xsS, ysS - [1xn] correspondence points from source image
+% xsD, ysD - [1xn] correspondence points from destination image
+%
+% OUTPUTS
+% warp - bookstein warping parameters
+% L, LnInv - see bookstein
+% bendE - bending energy
+%
+% EXAMPLE - 1
+% xsS=[0 -1 0 1]; ysS=[1 0 -1 0]; xsD=xsS; ysD=[3/4 1/4 -5/4 1/4];
+% warp = tpsGetWarp( 0, xsS, ysS, xsD, ysD );
+% [gxs, gys] = meshgrid(-1.25:.25:1.25,-1.25:.25:1.25);
+% tpsInterpolate( warp, gxs, gys, 1 );
+%
+% EXAMPLE - 2
+% xsS = [3.6929 6.5827 6.7756 4.8189 5.6969];
+% ysS = [10.3819 8.8386 12.0866 11.2047 10.0748];
+% xsD = [3.9724 6.6969 6.5394 5.4016 5.7756];
+% ysD = [6.5354 4.1181 7.2362 6.4528 5.1142];
+% warp = tpsGetWarp( 0, xsS, ysS, xsD, ysD );
+% [gxs, gys] = meshgrid(3.5:.25:7, 8.5:.25: 12.5);
+% tpsInterpolate( warp, gxs, gys, 1 );
+%
+% See also TPSINTERPOLATE, TPSINTERPOLATEIM, TPSRANDOM
+%
+% Piotr's Computer Vision Matlab Toolbox Version 2.0
+% Copyright 2014 Piotr Dollar. [pdollar-at-gmail.com]
+% Licensed under the Simplified BSD License [see https://github.com/pdollar/toolbox/blob/master/external/bsd.txt]
+
+dim = size( xsS );
+if( all(size(xsS)~=dim) || all(size(ysS)~=dim) || all(size(xsD)~=dim))
+ error( 'argument sizes do not match' );
+end
+
+% get L
+n = size(xsS,2);
+deltaXs = xsS'*ones(1,n) - ones(n,1) * xsS;
+deltaYs = ysS'*ones(1,n) - ones(n,1) * ysS;
+Rsq = (deltaXs .* deltaXs + deltaYs .* deltaYs);
+Rsq = Rsq+eye(n); K = Rsq .* log( Rsq ); K( isnan(K) )=0;
+K = K + lambda * eye( n );
+P = [ ones(n,1), xsS', ysS' ];
+L = [ K, P; P', zeros(3,3) ];
+LInv = L^(-1);
+LnInv = LInv(1:n,1:n);
+
+% recover W's
+wx = LInv * [xsD 0 0 0]';
+affinex = wx(n+1:n+3);
+wx = wx(1:n);
+wy = LInv * [ysD 0 0 0]';
+affiney = wy(n+1:n+3);
+wy = wy(1:n);
+
+% record warp
+warp.wx = wx; warp.affinex = affinex;
+warp.wy = wy; warp.affiney = affiney;
+warp.xsS = xsS; warp.ysS = ysS;
+warp.xsD = xsD; warp.ysD = ysD;
+
+% get bending energy (without regularization)
+w = [wx'; wy'];
+K = K - lambda * eye( n );
+bendE = trace(w*K*w')/2;
\ No newline at end of file
diff --git a/external/piotr_toolbox/tpsInterpolate.m b/external/piotr_toolbox/tpsInterpolate.m
new file mode 100644
index 000000000..2c75ab4fd
--- /dev/null
+++ b/external/piotr_toolbox/tpsInterpolate.m
@@ -0,0 +1,52 @@
+function [xsR,ysR] = tpsInterpolate( warp, xs, ys, show )
+% Apply warp (obtained by tpsGetWarp) to a set of new points.
+%
+% USAGE
+% [xsR,ysR] = tpsInterpolate( warp, xs, ys, [show] )
+%
+% INPUTS
+% warp - [see tpsGetWarp] bookstein warping parameters
+% xs, ys - points to apply warp to
+% show - [1] will display results in figure(show)
+%
+% OUTPUTS
+% xsR, ysR - result of warp applied to xs, ys
+%
+% EXAMPLE
+%
+% See also TPSGETWARP
+%
+% Piotr's Computer Vision Matlab Toolbox Version 2.0
+% Copyright 2014 Piotr Dollar. [pdollar-at-gmail.com]
+% Licensed under the Simplified BSD License [see https://github.com/pdollar/toolbox/blob/master/external/bsd.txt]
+
+if( nargin<4 || isempty(show)); show = 1; end
+
+wx = warp.wx; affinex = warp.affinex;
+wy = warp.wy; affiney = warp.affiney;
+xsS = warp.xsS; ysS = warp.ysS;
+xsD = warp.xsD; ysD = warp.ysD;
+
+% interpolate points (xs,ys)
+xsR = f( wx, affinex, xsS, ysS, xs(:)', ys(:)' );
+ysR = f( wy, affiney, xsS, ysS, xs(:)', ys(:)' );
+
+% optionally show points (xsR, ysR)
+if( show )
+ figure(show);
+ subplot(2,1,1); plot( xs, ys, '.', 'color', [0 0 1] );
+ hold('on'); plot( xsS, ysS, '+' ); hold('off');
+ subplot(2,1,2); plot( xsR, ysR, '.' );
+ hold('on'); plot( xsD, ysD, '+' ); hold('off');
+end
+
+function zs = f( w, aff, xsS, ysS, xs, ys )
+% find f(x,y) for xs and ys given W and original points
+n = size(w,1); ns = size(xs,2);
+delXs = xs'*ones(1,n) - ones(ns,1)*xsS;
+delYs = ys'*ones(1,n) - ones(ns,1)*ysS;
+distSq = (delXs .* delXs + delYs .* delYs);
+distSq = distSq + eye(size(distSq)) + eps;
+U = distSq .* log( distSq ); U( isnan(U) )=0;
+zs = aff(1)*ones(ns,1)+aff(2)*xs'+aff(3)*ys';
+zs = zs + sum((U.*(ones(ns,1)*w')),2);
diff --git a/external/spm/spm_bsplinc.mexmaca64 b/external/spm/spm_bsplinc.mexmaca64
new file mode 100644
index 000000000..14dcc98ea
Binary files /dev/null and b/external/spm/spm_bsplinc.mexmaca64 differ
diff --git a/external/spm/spm_bsplins.mexmaca64 b/external/spm/spm_bsplins.mexmaca64
new file mode 100644
index 000000000..c76a423ea
Binary files /dev/null and b/external/spm/spm_bsplins.mexmaca64 differ
diff --git a/toolbox/anatomy/CalcVertexNormals.m b/toolbox/anatomy/CalcVertexNormals.m
new file mode 100644
index 000000000..f79530155
--- /dev/null
+++ b/toolbox/anatomy/CalcVertexNormals.m
@@ -0,0 +1,138 @@
+function [VertexNormals,VertexArea,FaceNormals,FaceArea]=CalcVertexNormals(FV,FaceNormals) % ,up,vp
+ % Very slightly modified for efficiency and convenience, and added normr. Marc L.
+%% Summary
+%Author: Itzik Ben Shabat
+%Last Update: July 2014
+
+%summary: CalcVertexNormals calculates the normals and voronoi areas at each vertex
+%INPUT:
+%FV - triangle mesh in face vertex structure
+%N - face normals
+%OUTPUT -
+%VertexNormals - [Nv X 3] matrix of normals at each vertex
+%Avertex - [NvX1] voronoi area at each vertex
+%Acorner - [NfX3] slice of the voronoi area at each face corner
+
+%% Code
+
+if nargin < 2 || isempty(FaceNormals)
+ [FaceNormals, FaceArea] = CalcFaceNormals(FV);
+end
+
+% disp('Calculating vertex normals... Please wait');
+% Get all edge vectors
+e0=FV.Vertices(FV.Faces(:,3),:)-FV.Vertices(FV.Faces(:,2),:);
+e1=FV.Vertices(FV.Faces(:,1),:)-FV.Vertices(FV.Faces(:,3),:);
+e2=FV.Vertices(FV.Faces(:,2),:)-FV.Vertices(FV.Faces(:,1),:);
+% Normalize edge vectors
+% e0_norm=normr(e0);
+% e1_norm=normr(e1);
+% e2_norm=normr(e2);
+
+%normalization procedure
+%calculate face Area
+%edge lengths
+de0=sqrt(e0(:,1).^2+e0(:,2).^2+e0(:,3).^2);
+de1=sqrt(e1(:,1).^2+e1(:,2).^2+e1(:,3).^2);
+de2=sqrt(e2(:,1).^2+e2(:,2).^2+e2(:,3).^2);
+l2=[de0.^2 de1.^2 de2.^2];
+
+%using ew to calulate the cot of the angles for the voronoi area
+%calculation. ew is the triangle barycenter, I later check if its inside or
+%outide the triangle
+ew=[l2(:,1).*(l2(:,2)+l2(:,3)-l2(:,1)) l2(:,2).*(l2(:,3)+l2(:,1)-l2(:,2)) l2(:,3).*(l2(:,1)+l2(:,2)-l2(:,3))];
+
+s=(de0+de1+de2)/2;
+%Af - face area vector
+FaceArea=sqrt(max(0, s.*(s-de0).*(s-de1).*(s-de2)));%herons formula for triangle area, could have also used 0.5*norm(cross(e0,e1))
+% if any(~Af) || any(~FaceArea)
+% error('Degenerate faces.');
+% end
+
+%calculate weights
+Acorner=zeros(size(FV.Faces,1),3);
+VertexArea=zeros(size(FV.Vertices,1),1);
+
+% Calculate Vertice Normals
+VertexNormals=zeros([size(FV.Vertices,1) 3]);
+
+% up=zeros([size(FV.Vertices,1) 3]);
+% vp=zeros([size(FV.Vertices,1) 3]);
+for i=1:size(FV.Faces,1)
+ %Calculate weights according to N.Max [1999]
+
+ wfv1=FaceArea(i)/(de1(i)^2*de2(i)^2);
+ wfv2=FaceArea(i)/(de0(i)^2*de2(i)^2);
+ wfv3=FaceArea(i)/(de1(i)^2*de0(i)^2);
+
+ VertexNormals(FV.Faces(i,1),:)=VertexNormals(FV.Faces(i,1),:)+wfv1*FaceNormals(i,:);
+ VertexNormals(FV.Faces(i,2),:)=VertexNormals(FV.Faces(i,2),:)+wfv2*FaceNormals(i,:);
+ VertexNormals(FV.Faces(i,3),:)=VertexNormals(FV.Faces(i,3),:)+wfv3*FaceNormals(i,:);
+ %Calculate areas for weights according to Meyer et al. [2002]
+ %check if the tringle is obtuse, right or acute
+
+ if ew(i,1)<=0
+ Acorner(i,2)=-0.25*l2(i,3)*FaceArea(i)/(e0(i,:)*e2(i,:)');
+ Acorner(i,3)=-0.25*l2(i,2)*FaceArea(i)/(e0(i,:)*e1(i,:)');
+ Acorner(i,1)=FaceArea(i)-Acorner(i,2)-Acorner(i,3);
+ elseif ew(i,2)<=0
+ Acorner(i,3)=-0.25*l2(i,1)*FaceArea(i)/(e1(i,:)*e0(i,:)');
+ Acorner(i,1)=-0.25*l2(i,3)*FaceArea(i)/(e1(i,:)*e2(i,:)');
+ Acorner(i,2)=FaceArea(i)-Acorner(i,1)-Acorner(i,3);
+ elseif ew(i,3)<=0
+ Acorner(i,1)=-0.25*l2(i,2)*FaceArea(i)/(e2(i,:)*e1(i,:)');
+ Acorner(i,2)=-0.25*l2(i,1)*FaceArea(i)/(e2(i,:)*e0(i,:)');
+ Acorner(i,3)=FaceArea(i)-Acorner(i,1)-Acorner(i,2);
+ else
+ ewscale=0.5*FaceArea(i)/(ew(i,1)+ew(i,2)+ew(i,3));
+ Acorner(i,1)=ewscale*(ew(i,2)+ew(i,3));
+ Acorner(i,2)=ewscale*(ew(i,1)+ew(i,3));
+ Acorner(i,3)=ewscale*(ew(i,2)+ew(i,1));
+ end
+ VertexArea(FV.Faces(i,1))=VertexArea(FV.Faces(i,1))+Acorner(i,1);
+ VertexArea(FV.Faces(i,2))=VertexArea(FV.Faces(i,2))+Acorner(i,2);
+ VertexArea(FV.Faces(i,3))=VertexArea(FV.Faces(i,3))+Acorner(i,3);
+
+% %Calculate initial coordinate system
+% up(FV.Faces(i,1),:)=e2_norm(i,:);
+% up(FV.Faces(i,2),:)=e0_norm(i,:);
+% up(FV.Faces(i,3),:)=e1_norm(i,:);
+end
+VertexNormals=normr(VertexNormals);
+
+% %Calculate initial vertex coordinate system
+% for i=1:size(FV.Vertices,1)
+% up(i,:)=cross(up(i,:),VertexNormals(i,:));
+% up(i,:)=up(i,:)/norm(up(i,:));
+% vp(i,:)=cross(VertexNormals(i,:),up(i,:));
+% end
+
+% disp('Finished Calculating vertex normals');
+end
+
+
+function [FaceNormals, FaceArea]=CalcFaceNormals(FV)
+%% Summary
+%Author: Itzik Ben Shabat
+%Last Update: July 2014
+
+%CalcFaceNormals recives a list of vrtexes and Faces in FV structure
+% and calculates the normal at each face and returns it as FaceNormals
+%INPUT:
+%FV - face-vertex data structure containing a list of Vertices and a list of Faces
+%OUTPUT:
+%FaceNormals - an nX3 matrix (n = number of Faces) containng the norml at each face
+%% Code
+% Get all edge vectors
+e0=FV.Vertices(FV.Faces(:,3),:)-FV.Vertices(FV.Faces(:,2),:);
+e1=FV.Vertices(FV.Faces(:,1),:)-FV.Vertices(FV.Faces(:,3),:);
+% Calculate normal of face
+FaceNormalsA=cross(e0,e1);
+FaceArea = sqrt(sum(FaceNormalsA.^2, 2)) / 2;
+FaceNormals=normr(FaceNormalsA);
+end
+
+function V = normr(V)
+ V = bsxfun(@rdivide, V, sqrt(sum(V.^2, 2)));
+end
+
diff --git a/toolbox/anatomy/SurfaceSmooth.m b/toolbox/anatomy/SurfaceSmooth.m
new file mode 100755
index 000000000..90be3eb74
--- /dev/null
+++ b/toolbox/anatomy/SurfaceSmooth.m
@@ -0,0 +1,544 @@
+function V = SurfaceSmooth(Surf, Faces, VoxSize, DisplTol, IterTol, Freedom, Verbose)
+ % Smooth closed triangulated surface to remove "voxel" artefacts.
+ %
+ % V = SurfaceSmooth(Surf, [], VoxSize, DisplTol, IterTol, Freedom, Verbose)
+ % V = SurfaceSmooth(Vertices, Faces, VoxSize, DisplTol, IterTol, Freedom, Verbose)
+ %
+ % Smooth a triangulated surface, trying to optimize getting rid of blocky
+ % voxel segmentation artefacts but still respect initial segmentation.
+ % This is achieved by restricting vertex displacement along the surface
+ % normal to half the voxel size, and by compensating normal displacement
+ % of a voxel by an opposite distributed shift in its neighbors. That
+ % way, the total normal displacement is approximately zero (before
+ % potential restrictions are applied).
+ %
+ % Tangential motion is currently left unrestricted, which means the mesh
+ % will readjust over many iterations, much more than necessary to obtain
+ % a smoothed surface. On the other hand, this produces a more uniform
+ % triangulation, which may be desirable in some cases, e.g. after a
+ % reducepatch operation. This tangential motion may also make the normal
+ % restriction a bit less accurate. This all depends on how irregular the
+ % mesh was to start with. To avoid long running times and some of the
+ % tangential deformation, DisplTol and IterTol can be used to limit the
+ % number of iterations.
+ %
+ % To separate these two effects (normal smoothing and tangential mesh
+ % uniformization), the function can be run to achieve each type of motion
+ % separately, by setting Freedom to the appropriate value (see below).
+ % Even if both effects are desired, but precision in the normal
+ % displacement restriction is preferred over running time, I would
+ % suggest running twice (norm., tang.) or three times (tang., norm.,
+ % tang.), but only once in the normal direction. Note that tangential
+ % motion is not perfect and may cause a small amount of smoothing as
+ % well.
+ %
+ % Input variables:
+ % Surf: Instead of Vertices and Faces, a single structure can be given
+ % with fields 'Vertices' and 'Faces' (lower-case v, f also work). In this
+ % case, leave Faces empty [].
+ % Vertices [nV, 3]: Point 3d coordinates.
+ % Faces [nF, 3]: Triangles, i.e. 3 point indices.
+ % VoxSize (default inf): Length of voxels, this determines the amount of
+ % smoothing. For a voxel size of 1, vertices are allowed to move only
+ % 0.5 units in the surface normal direction. This is somewhat optimal
+ % for getting rid of artefacts: it allows steps to become flat even at
+ % shallow angles and a single voxel cube would be transformed to a
+ % sphere of identical voxel volume.
+ % DisplTol (default 0.01*VoxSize): Once the maximum displacement of
+ % vertices is less than this distance, the algorithm stops. If two
+ % values are given, e.g. [0.01, 0.01], the second value is compared to
+ % normal displacement only. The first limit encountered stops
+ % iterating. This allows stopping earlier if only smoothing is
+ % desired and not mesh uniformity.
+ % IterTol (default 100): If the algorithm did not converge, it will stop
+ % after this many iterations.
+ % Freedom (default 2): Indicate which motion is allowed by the
+ % algorithm with an integer value: 0 for (restricted) normal
+ % smoothing, 1 for (unrestricted) tangential motion to get a more
+ % uniform triangulation, or 2 for both at the same time.
+ % Verbose (default 0): If 1, writes initial and final volumes and
+ % areas on the command line. Also gives the number of iterations and
+ % final displacement when the algorithm converged. (A warning is
+ % always given if convergence was not obtained in IterTol iterations.) If
+ % > 1, details are given at each iteration.
+ %
+ % Output: Modified voxel coordinates [nV, 3].
+ %
+ % Written by Marc Lalancette, Toronto, Canada, 2014-02-04
+ % Volume calculation from divergence theorem idea:
+ % http://www.mathworks.com/matlabcentral/fileexchange/26982-volume-of-a-surface-triangulation
+
+ % Note: Although this seems to work relatively well, it is still very new
+ % and not fully tested. Despite the description above which is what was
+ % intended, the algorithm had the tendency to drive growing oscillations
+ % (from iteration to iteration) on the surface. Thus a basic damping
+ % mechanism was added: I simply multiply each movement by a fraction that
+ % seems to avoid oscillations and still converge rapidly enough.
+
+ % Attempt at damping oscillations. Reduce any movement by a certain
+ % fraction. (Multiply movements by this factor.)
+ DampingFactor = 0.91;
+
+ % Add visualizations to debug.
+ isDebugFigures = true;
+
+ if ~isstruct(Surf)
+ if nargin < 2 || isempty(Faces)
+ error('Faces required as second input or "faces" field of first input.');
+ else
+ SurfV = Surf;
+ clear 'Surf';
+ Surf.Vertices = SurfV;
+ Surf.Faces = Faces;
+ clear SurfV Faces;
+ end
+ else
+ if isfield(Surf, 'faces')
+ Surf.Faces = Surf.faces;
+ Surf = rmfield(Surf, 'faces');
+ end
+ if isfield(Surf, 'vertices')
+ Surf.Vertices = Surf.vertices;
+ Surf = rmfield(Surf, 'vertices');
+ elseif ~isfield(Surf, 'Vertices')
+ error('Surf.Vertices field required when second input is empty.');
+ end
+ end
+ if nargin < 3 || isempty(VoxSize)
+ VoxSize = inf;
+ if nargin < 5 || isempty(IterTol)
+ error(['Unrestricted smoothing (no VoxSize) would lead to a sphere of similar volume, ', ...
+ 'unless limited by the number of iterations.']);
+ end
+ end
+ if nargin < 4 || isempty(DisplTol)
+ DisplTol = 0.01 * VoxSize;
+ end
+ if numel(DisplTol) == 1
+ % Only stop when total displacement reaches the limit, normal displacement alone
+ % isn't checked for stopping.
+ DisplTol = [DisplTol, 0];
+ end
+ if nargin < 5 || isempty(IterTol)
+ IterTol = 100;
+ end
+ if nargin < 6 || isempty(Freedom)
+ Freedom = 2; % 0=norm, 1=tang, 2=both.
+ end
+ if nargin < 7 || isempty(Verbose)
+ Verbose = false;
+ end
+
+ % Verify surface is a triangulation.
+ if size(Surf.Faces, 2) > 3
+ error('SurfaceSmooth only works with a triangulated surface.');
+ end
+
+ % Optimal allowed normal displacement, in units of voxel side length.
+ % Based on turning a single voxel into a sphere of same volume: max
+ % needed displacement is in corner:
+ % sqrt(3)/2 - 1/(4/3*pi)^(1/3) = 0.2457
+ % In middle of face it is rather:
+ % 1/(4/3*pi)^(1/3) - 1/2 = 0.1204
+ % Based on very gentle sloped staircase, it would be 0.5, but for 45
+ % degree steps, we only need cos(pi/4)/2 = 0.3536. So something along
+ % those lines seems like a good compromize. For now try to make steps
+ % completely disappear.
+ MaxNormDispl = 0.5 * VoxSize;
+ % MaxDispl = 2 * VoxSize; % To avoid large scale slow flows tangentially, which could distort.
+
+ nV = size(Surf.Vertices, 1);
+ %nF = size(Surf.Faces, 1);
+
+ % Remove duplicate faces. Not necessary considering we have to use
+ % unique on the edges later anyway.
+ % Surf.Faces = unique(Surf.Faces, 'rows');
+
+ if Verbose
+ if isDebugFigures
+ [FdA, VdA, FN, VN] = CalcVertexNormals(Surf);
+ ViewSurfWithNormals(Surf.Vertices, Surf.Faces, VN, FN, VdA, FdA)
+ else
+ [FdA, VdA, FN] = CalcAreas(Surf);
+ end
+ FaceCentroidZ = ( Surf.Vertices(Surf.Faces(:, 1), 3) + ...
+ Surf.Vertices(Surf.Faces(:, 2), 3) + Surf.Vertices(Surf.Faces(:, 3), 3) ) /3;
+ Pre.Volume = FaceCentroidZ' * (FN(:, 3) .* FdA);
+ Pre.Area = sum(FdA);
+ fprintf('Total enclosed volume before smoothing: %g\n', Pre.Volume);
+ fprintf('Total area before smoothing: %g\n', Pre.Area);
+ end
+
+ % Calculate connectivity matrix.
+
+ % Euler characteristic (2-2handles) = V - E + F
+ % For simple closed surface, E = 3*F/2
+ % disp(nV)
+ % disp(2 + nF/2)
+
+ % This expression works when each edge is found once in each direction,
+ % i.e. as long as all normals are consistently pointing in (or out).
+ % C = sparse(Faces(:), [Faces(:, 2); Faces(:, 3); Faces(:, 1)], true);
+ % Seems users had patches that didn't satisfy this restriction, or had
+ % duplicate faces or had possibly intersecting surfaces with 3 faces
+ % sharing an edge.
+ [Edges, ~, iE] = unique(sort([Surf.Faces(:), ...
+ [Surf.Faces(:, 2); Surf.Faces(:, 3); Surf.Faces(:, 1)]], 2), 'rows'); % [Surf.Faces...] = Edges(iE,:)
+ % Look for boundaries of open surface.
+ isBoundE = false(size(Edges, 1), 1);
+ isBoundV = false(nV, 1);
+ iE = sort(iE);
+ n = 1;
+ for i = 2:numel(iE)
+ if iE(i) ~= iE(i-1)
+ if n == 1
+ % Only one copy, boundary edge.
+ isBoundE(iE(i-1)) = true;
+ else
+ n = 1;
+ end
+ else
+ if n == 2
+ % This makes a 3rd copy of the same edge. Strange surface.
+ isBoundE(iE(i)) = true;
+ end
+ n = n + 1;
+ end
+ end
+ % This was very slow for many edges.
+ % for i = 1:size(Edges, 1)
+ % isBoundE(i) = sum(iE == i) < 2;
+ % end
+ if any(isBoundE)
+ if Verbose
+ warning('Open surface detected. Results may be unexpected.');
+ end
+ isBoundV(Edges(isBoundE, :)) = true;
+ end
+ iBoundV = find(isBoundV);
+ iBulkV = setdiff(1:nV, iBoundV);
+ % Vertex connectivity matrix, does not include diagonal (self)
+ C = sparse(Edges, Edges(:, [2,1]), true); % Fills symetrically 1>2, 2>1
+ %C = C | C';
+ % Logical matrix would be huge, so use sparse. However tests in R2011b
+ % indicate that using logical sparse indexing is sometimes slightly
+ % faster (possibly when using linear indexing) but sometimes noticeably
+ % slower. Seems here using a cell array is better.
+ CCell = cell(nV, 1);
+ CCellBulk = cell(nV, 1);
+ for v = 1:nV
+ CCell{v} = find(C(:, v));
+ CCellBulk{v} = setdiff(CCell{v}, iBoundV);
+ end
+ clear C
+ % Number of connected neighbors at each vertex.
+ % nC = full(sum(C, 1));
+
+ V = Surf.Vertices;
+ LastMaxDispl = [inf, inf]; % total, normal only
+ Iter = 0;
+ NormDispl = zeros(nV, 1);
+ while LastMaxDispl(1) > DisplTol(1) && LastMaxDispl(2) > DisplTol(2) && ...
+ Iter < IterTol
+ Iter = Iter + 1;
+ [~, VdA, ~, N] = CalcVertexNormals(Surf);
+ % Double boundary vertex areas to balance their "pull". But not very precise, depends on boundary shape.
+ VdA(isBoundV) = 2 * VdA(isBoundV);
+ VWeighted = bsxfun(@times, VdA, Surf.Vertices);
+
+ % Moving step. (This is slow.)
+ switch Freedom
+ case 2 % Both.
+ for v = iBulkV
+ % Neighborhood average. Improved to weigh by area element to avoid
+ % tangential deformation based on number of neighbors (e.g. shrinking
+ % towards vertices with fewer neighbors).
+ NeighdA = sum(VdA(CCell{v}));
+ NeighdABulk = sum(VdA(CCellBulk{v}));
+ NeighborAverage = sum(VWeighted(CCell{v}, :) / NeighdA, 1); % / nC(v);
+ % Neighborhood correction displacement along normal. Volume
+ % corresponding to this point's normal movement, distributed (divided)
+ % over neighborhood area + itself, which will be shifted inversely.
+ NormalDisplCorr = (NeighborAverage - Surf.Vertices(v, :)) * N(v, :)' / (NeighdABulk/VdA(v) + 1); % / (nC(v) + 1);
+ % Central point is moved to average of neighbors, but shifted back a
+ % bit as they all will be.
+ V(v, :) = V(v, :) + DampingFactor * ( NeighborAverage - Surf.Vertices(v, :) - NormalDisplCorr * N(v, :) );
+ % Neighbors are shifted a bit too along their own normals, such that
+ % the total change in volume (normal displacement times surface area)
+ % is close to zero.
+ V(CCellBulk{v}, :) = V(CCellBulk{v}, :) - DampingFactor * NormalDisplCorr * N(CCellBulk{v}, :);
+ end
+ case 0 % Normal motion only.
+ for v = iBulkV
+ NeighdA = sum(VdA(CCell{v}));
+ NeighdABulk = sum(VdA(CCellBulk{v}));
+ NeighborAverage = sum(VWeighted(CCell{v}, :) / NeighdA, 1); % / nC(v);
+ % d * a / (b+a) = d / (b/a + 1)
+ NormalDisplCorr = (NeighborAverage - Surf.Vertices(v, :)) * N(v, :)' / (NeighdABulk/VdA(v) + 1); % / (nC(v) + 1);
+ % The vertex should move the projected distance minus the correction.
+ % 1 - a/(b+a) = b/(b+a) = 1/(1+a/b) = (b/a) * 1/(b/a+1)
+ V(v, :) = V(v, :) + DampingFactor * NeighdABulk/VdA(v) * NormalDisplCorr * N(v, :);
+ V(CCellBulk{v}, :) = V(CCellBulk{v}, :) - DampingFactor * NormalDisplCorr * N(CCellBulk{v}, :);
+ end
+ case 1 % Tangential motion only. Unrestricted.
+ for v = iBulkV
+ NeighdA = sum(VdA(CCell{v}));
+ NeighborAverage = sum(VWeighted(CCell{v}, :) / NeighdA, 1); % / nC(v);
+ NormalDisplacement = (NeighborAverage - Surf.Vertices(v, :)) * N(v, :)'; % / (nC(v) + 1);
+ V(v, :) = V(v, :) + DampingFactor * ( (NeighborAverage - Surf.Vertices(v, :)) - NormalDisplacement * N(v, :) );
+ % No compensation among neighbors.
+ end
+ otherwise
+ error('Unrecognized Freedom parameter. Should be 0, 1 or 2.');
+ end
+ % Restricting step.
+ % Displacements along normals (N at last positions, Surf.Vertices), added to
+ % previous normal displacement since we want to restrict total normal
+ % displacement.
+ D = NormDispl + dot((V - Surf.Vertices), N, 2);
+ % New restricted total normal displacement.
+ NormDispl = sign(D) .* min(abs(D), MaxNormDispl);
+ % Amounts to move back if greater than allowed.
+ D = D - NormDispl;
+ Where = abs(D) > DisplTol(1) * 1e-6; % > 0, but ignore precision errors.
+ % Fix.
+ if any(Where)
+ V(Where, :) = V(Where, :) - [D(Where), D(Where), D(Where)] .* N(Where, :);
+ end
+ % New restriction on tangential displacement. [Not implemented.]
+ % MaxDispl
+
+ if Verbose > 1
+ [LastMaxDispl(1), iMax(1)] = max(sqrt( sum((V - Surf.Vertices).^2, 2)) );
+ [LastMaxDispl(2), iMax(2)] = max(abs(dot(V - Surf.Vertices, N, 2)));
+ TangDisplVec = CrossProduct(V - Surf.Vertices, N);
+ [LastMaxDispl(3), iMax(3)] = max(sqrt(TangDisplVec(:,1).^2 + TangDisplVec(:,2).^2 + TangDisplVec(:,3).^2));
+ fprintf('Iter %d: max displ %1.4g at vox %d; norm %1.4g (vox %d); tang %1.4g (vox %d)\n', ...
+ Iter, LastMaxDispl(1), iMax(1), ...
+ sign((V(iMax(2),:) - Surf.Vertices(iMax(2),:)) * N(iMax(2),:)')*LastMaxDispl(2), iMax(2), ...
+ sign(TangDisplVec(iMax(3), 1))*LastMaxDispl(3), iMax(3));
+ % Signs are to see if these are oscillations or translations.
+ else
+ LastMaxDispl(1) = sqrt( max(sum((V - Surf.Vertices).^2, 2)) );
+ LastMaxDispl(2) = max(dot(V - Surf.Vertices, N, 2));
+ end
+ Surf.Vertices = V;
+ end
+
+ if Iter >= IterTol && Verbose
+ warning('SurfaceSmooth did not converge within %d iterations. \nLast max point displacement = %f', ...
+ IterTol, LastMaxDispl(1));
+ elseif Verbose
+ fprintf('SurfaceSmooth converged in %d iterations. \nLast max point displacement = %f\n', ...
+ Iter, LastMaxDispl(1));
+ end
+ if Verbose && IterTol > 0
+ [FdA, ~, FN] = CalcVertexNormals(Surf);
+ FaceCentroidZ = ( Surf.Vertices(Surf.Faces(:, 1), 3) + ...
+ Surf.Vertices(Surf.Faces(:, 2), 3) + Surf.Vertices(Surf.Faces(:, 3), 3) ) /3;
+ Post.Volume = FaceCentroidZ' * (FN(:, 3) .* FdA);
+ Post.Area = sum(FdA);
+ fprintf('Total enclosed volume after smoothing: %g\n', Post.Volume);
+ fprintf('Relative volume change: %g %%\n', ...
+ 100 * (Post.Volume - Pre.Volume)/Pre.Volume);
+ fprintf('Total area after smoothing: %g\n', Post.Area);
+ fprintf('Relative area change: %g %%\n', ...
+ 100 * (Post.Area - Pre.Area)/Pre.Area);
+ end
+
+
+
+end
+
+% Much faster than using the Matlab version.
+function c = CrossProduct(a, b)
+ c = [a(:,2).*b(:,3)-a(:,3).*b(:,2), ...
+ a(:,3).*b(:,1)-a(:,1).*b(:,3), ...
+ a(:,1).*b(:,2)-a(:,2).*b(:,1)];
+end
+
+% ----------------------------------------------------------------------
+function [FaceArea, VertexArea, FaceNormals, VertexNormals] = CalcVertexNormals(Surf)
+ % Get face and vertex normals and areas
+
+ % First, see if Brainstorm function is present.
+ isBst = exist('tess_normals', 'file') == 2;
+
+ % Get areas first.
+ if isBst
+ [FaceArea, VertexArea] = CalcAreas(Surf);
+ % Might need to use connectivity matrix if we get bad normals.
+ [VertexNormals, FaceNormals] = tess_normals(Surf.Vertices, Surf.Faces); % , VertConn
+ else
+ [FaceArea, VertexArea, FaceNormals, VertexNormals] = CalcAreas(Surf);
+ end
+end
+
+% % Calculate dA normal vectors to each vertex.
+% function [N, VdA, FN, FdA] = CalcVertexNormals(S)
+% N = zeros(nV, 3);
+% % Get face normal vectors with length the size of the face area.
+% FNdA = CrossProduct( (S.Vertices(S.Faces(:, 2), :) - S.Vertices(S.Faces(:, 1), :)), ...
+% (S.Vertices(S.Faces(:, 3), :) - S.Vertices(S.Faces(:, 2), :)) ) / 2;
+% % For vertex normals, add adjacent face normals, then normalize. Also
+% % add 1/3 of each adjacent area element for vertex area.
+% FdA = sqrt(FNdA(:,1).^2 + FNdA(:,2).^2 + FNdA(:,3).^2);
+% VdA = zeros(nV, 1);
+% for ff = 1:size(S.Faces, 1) % (This is slow.)
+% N(S.Faces(ff, :), :) = N(S.Faces(ff, :), :) + FNdA([ff, ff, ff], :);
+% VdA(S.Faces(ff, :), :) = VdA(S.Faces(ff, :), :) + FdA(ff)/3;
+% end
+% N = bsxfun(@rdivide, N, sqrt(N(:,1).^2 + N(:,2).^2 + N(:,3).^2));
+% FN = bsxfun(@rdivide, FNdA, FdA);
+% end
+
+% % Calculate areas of faces and vertices.
+% function [FdA, VdA, FN] = CalcAreas(S)
+% % Get face normal vectors with length the size of the face area.
+% FN = CrossProduct( (S.Vertices(S.Faces(:, 2), :) - S.Vertices(S.Faces(:, 1), :)), ...
+% (S.Vertices(S.Faces(:, 3), :) - S.Vertices(S.Faces(:, 2), :)) ) / 2;
+% FdA = sqrt(FN(:,1).^2 + FN(:,2).^2 + FN(:,3).^2); % no sum for speed
+% if nargout > 2
+% FN = bsxfun(@rdivide, FN, FdA);
+% end
+% % For vertex areas, add 1/3 of each adjacent area element.
+% VdA = zeros(size(S.Vertices, 1), 1);
+% for iV = 1:3
+% VdA(s.Faces(:, iV)) = VdA(s.Faces(:, iV)) + FdA / 3;
+% end
+% end
+
+% % Find boundary vertices.
+% function isBound = FindBoundary(Faces)
+% nF = size(Faces, 1);
+% Found = logical(nF);
+% Inside = logical(nF);
+% for f = 1:nF
+% for e = 1:3
+% if Found(Faces(f, e), Faces(f, mod(e, 3)+1))
+% Inside(Faces(f, e), Faces(f, mod(e, 3)+1)) = true;
+% else
+% Found(Faces(f, e), Faces(f, mod(e, 3)+1)) = true;
+% end
+% end
+% end
+% isBound = Found & ~Inside;
+% end
+
+
+function ViewSurfWithNormals(Vertices, Faces, VNorm, FNorm, VArea, FArea)
+ figure;
+ hold on;
+ axis equal;
+
+ % Create surface patch
+ patch('Faces', Faces, 'Vertices', Vertices, ...
+ 'FaceColor', 'grey', 'EdgeColor', 'k', 'FaceAlpha', 0.6);
+
+ % Compute face centers
+ face_centers = mean(reshape(Vertices(Faces', :), [], 3, size(Faces, 2)), 2);
+ face_centers = squeeze(face_centers);
+
+ % Scale normals for visualization
+ normal_length = 0.05 * mean(range(Vertices)); % Adjust scaling as needed
+ FNorm = normal_length * bsxfun(@times, FNorm, FArea) / max(FArea);
+ VNorm = normal_length * bsxfun(@times, VNorm, VArea) / max(VArea);
+
+ % Plot face normals (Red)
+ quiver3(face_centers(:,1), face_centers(:,2), face_centers(:,3), ...
+ FNorm(:,1), FNorm(:,2), FNorm(:,3), ...
+ 'r', 'LineWidth', 1.5, 'MaxHeadSize', 0.5);
+
+ % Plot vertex normals (Blue)
+ quiver3(Vertices(:,1), Vertices(:,2), Vertices(:,3), ...
+ VNorm(:,1), VNorm(:,2), VNorm(:,3), ...
+ 'b', 'LineWidth', 1, 'MaxHeadSize', 0.5);
+
+ % Lighting and view settings
+ camlight;
+ lighting gouraud;
+ view(3);
+end
+
+
+function [FaceArea, VertexArea, FaceNormals, VertexNormals] = CalcAreas(S)
+ % Compute face areas, and divides them to assign vertex areas.
+ % Face areas are split into three parts such that each point is assigned to the
+ % vertex it is closest to, similar to Voronoi diagrams, using triangle
+ % circumcenters.
+
+ nV = size(S.Vertices, 1);
+ nF = size(S.Faces, 1);
+
+ % Extract triangle vertex positions
+ Vertices = reshape(S.Vertices(S.Faces(:), :), [nF, 3, 3]); % nF, 3 (x,y,z), 3 (iV)
+ % Edge vectors, ordered such that each is opposite to the correspondingly indexed
+ % vertex (edge 1 is from v2 to v3, opposite vertex 1).
+ % circshift +1 moves elements to the next (larger) index, so the last element
+ % gets to position 1. So here we're doing, e.g. v3 - v2 in first position.
+ Edges = circshift(Vertices, 1, 3) - circshift(Vertices, -1, 3); % nF, 3 (x,y,z), 3 (iE)
+
+ % Get face normal vectors with length the size of the face area.
+ FaceNormals = CrossProduct(Edges(:,:,3), Edges(:,:,1)) / 2;
+ FaceArea = sqrt(FaceNormals(:,1).^2 + FaceNormals(:,2).^2 + FaceNormals(:,3).^2); % no sum for speed
+ if nargout > 2
+ FaceNormals = bsxfun(@rdivide, FaceNormals, FaceArea);
+ end
+
+ % Edge lengths
+ EdgeSq = squeeze(Edges(:,1,:).^2 + Edges(:,2,:).^2 + Edges(:,3,:).^2); % nF, 3 (iE)
+ % Circumcenter barycentric weights
+ BaryWeights = EdgeSq .* (circshift(EdgeSq, 1, 2) + circshift(EdgeSq, -1, 2) - EdgeSq); % nF, 3 (iE)
+
+ FaceVertAreas = zeros(nF, 3);
+
+ % Process in 4 batches
+ % Logical index for faces that have not been processed
+ isRemain = true(nF, 1);
+ % First three cases: circumcenter outside triangular face, past one of 3 edges
+ % This divides the area into two triangles, and one pentagon (or rectangle if
+ % original face has a right angle).
+ for iLongest = 1:3
+ isDo = BaryWeights(:,iLongest) <= 0;
+ isRemain = isRemain & ~isDo;
+ iSh = circshift(1:3, 1-iLongest); % place iLongest in first position
+ % Two vertices of long edge; areas are triangles with right angle
+ % -1/4 face area * ratio of adjoining short edge to projection of long edge on that short edge.
+ % (show with similar right triangle and edge length ratios) Minus sign because of
+ % "dot product" with vectors always making obtuse angle.
+ FaceVertAreas(isDo,iSh(2)) = 0.25 * EdgeSq(isDo,iSh(3)) .* FaceArea(isDo,:) ./ -sum(Edges(isDo,:,iSh(1)) .* Edges(isDo,:,iSh(3)), 2);
+ FaceVertAreas(isDo,iSh(3)) = 0.25 * EdgeSq(isDo,iSh(2)) .* FaceArea(isDo,:) ./ -sum(Edges(isDo,:,iSh(1)) .* Edges(isDo,:,iSh(2)), 2);
+ % Vertex opposite long edge; area has 4 or 5 sides, just assign remaining area.
+ FaceVertAreas(isDo,iSh(1)) = FaceArea(isDo,:) - FaceVertAreas(isDo,iSh(2)) - FaceVertAreas(isDo,iSh(3));
+ end
+ % Last case: circumcenter is inside face, each region is a quadrilateral
+ % Normalize weights and scale with half face area
+ BaryWeights = BaryWeights ./ (BaryWeights(:,1) + BaryWeights(:,2) + BaryWeights(:,3));
+ FaceVertAreas(isRemain,:) = 1/2 * FaceArea(isRemain,:) .* (circshift(BaryWeights(isRemain,:), 1, 2) + circshift(BaryWeights(isRemain,:), -1, 2));
+
+ % Now sum back to single vertex area list
+ VertexArea = accumarray(S.Faces(:), FaceVertAreas(:), [nV, 1]);
+
+ % Use areas as weights to average face normals, if requested
+ if nargout > 3
+ VertexNormals = zeros(nV, 3);
+ % Have to do each coordinate (x,y,z) sequentially with accumarray
+ for i = 1:3
+ % Weight face normals by face vertex area, which we will normalize at the
+ % end by dividing by total vertex area.
+ WeightedFaceVertN = bsxfun(@times, FaceVertAreas, FaceNormals(:,i));
+ VertexNormals(:,i) = accumarray(S.Faces(:), WeightedFaceVertN(:), [nV, 1]);
+ end
+ % Normalize for the area weights
+ VertexNormals = bsxfun(@rdivide, VertexNormals, VertexArea);
+
+ % Final check no NaN
+ if any(isnan(VertexNormals))
+ error('NaN values in VertexNormals.');
+ end
+ end
+ if any(isnan(VertexArea))
+ error('NaN values in VertexArea.');
+ end
+
+end
diff --git a/toolbox/anatomy/bst_normalize_mni.m b/toolbox/anatomy/bst_normalize_mni.m
index db313340e..5a8645fed 100644
--- a/toolbox/anatomy/bst_normalize_mni.m
+++ b/toolbox/anatomy/bst_normalize_mni.m
@@ -82,16 +82,16 @@
TpmFile = bst_get('SpmTpmAtlas');
% If it is not found: download
if isempty(TpmFile)
- % Create folder
- if ~file_exist(bst_fileparts(TpmFile))
- mkdir(bst_fileparts(TpmFile));
- end
% URL to download
- tmpUrl = 'http://neuroimage.usc.edu/bst/getupdate.php?t=SPM_TPM';
+ tpmUrl = 'http://neuroimage.usc.edu/bst/getupdate.php?t=SPM_TPM';
% Path to downloaded file
- tpmZip = bst_fullfile(bst_get('BrainstormUserDir'), 'defaults', 'spm', 'SPM_TPM');
+ tpmZip = bst_fullfile(bst_get('BrainstormUserDir'), 'defaults', 'spm', 'SPM_TPM.zip');
+ % Create 'spm' folder if needed
+ if ~isdir(bst_fileparts(tpmZip))
+ mkdir(bst_fileparts(tpmZip));
+ end
% Download file
- errMsg = gui_brainstorm('DownloadFile', tmpUrl, tpmZip, 'Download template');
+ errMsg = gui_brainstorm('DownloadFile', tpmUrl, tpmZip, 'Download template');
% Error message
if ~isempty(errMsg)
errMsg = ['Impossible to download template:' 10 errMsg];
@@ -99,7 +99,7 @@
end
% Progress bar
bst_progress('text', 'Importing SPM template...');
- % URL: Download zip file
+ % Unzip file
try
unzip(tpmZip, bst_fileparts(tpmZip));
catch
diff --git a/toolbox/anatomy/bst_warp_prepare.m b/toolbox/anatomy/bst_warp_prepare.m
index 91d41ee56..254fac751 100644
--- a/toolbox/anatomy/bst_warp_prepare.m
+++ b/toolbox/anatomy/bst_warp_prepare.m
@@ -156,7 +156,7 @@
file_delete(MriFiles, 1);
end
end
-% Force subject to us default anatomy
+% Force subject to use default anatomy
s.UseDefaultAnat = 0;
s.Anatomy = [];
s.Surface = [];
@@ -268,10 +268,39 @@
file_copy(bst_fullfile(atlasDir, dirScout(i).name), bst_fullfile(OutputDir, dirScout(i).name))
end
-
%% ===== UPDATE DATABASE =====
% Reload subject
db_reload_subjects(iSubject);
+
+%% ===== SET DEFAULT SURFACES =====
+isUpdate = 0;
+sSubject = bst_get('Subject', iSubject);
+for surfaceCatergory = {'Anatomy' 'Scalp', 'Cortex', 'InnerSkull', 'OuterSkull', 'Fibers', 'FEM'}
+ if strcmp('Anatomy', surfaceCatergory{1})
+ surfaceGroup = surfaceCatergory{1};
+ else
+ surfaceGroup = 'Surface';
+ end
+ if ~isempty(sDefSubject.(['i', surfaceCatergory{1}]))
+ defDefFilename = sDefSubject.(surfaceGroup)(sDefSubject.(['i', surfaceCatergory{1}])).FileName;
+ [~, filename, ext] = bst_fileparts(defDefFilename);
+ iDef = find(file_compare({sSubject.(surfaceGroup).FileName}, bst_fullfile(fileparts(sSubject.FileName), [filename, OutputTag, ext] )), 1);
+ if ~isempty(iDef)
+ sSubject.(['i', surfaceCatergory{1}]) = iDef;
+ matUpdate.(surfaceCatergory{1}) = bst_fullfile(fileparts(sSubject.FileName), [filename, OutputTag, ext]);
+ isUpdate = 1;
+ end
+ end
+end
+if isUpdate
+ % Update Database
+ bst_set('Subject', iSubject, sSubject);
+ % Update SubjectFile
+ bst_save(file_fullpath(sSubject.FileName), matUpdate, 'v7', 1);
+end
+
+
+%% ===== DISPLAY WARPED HEAD AND CORTEX =====
% Unload all the surfaces, close all the figures
bst_memory('UnloadAll', 'Forced');
% Get subject again
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/anatomy/mri_coregister.m b/toolbox/anatomy/mri_coregister.m
index 291d1941b..87547a012 100644
--- a/toolbox/anatomy/mri_coregister.m
+++ b/toolbox/anatomy/mri_coregister.m
@@ -9,7 +9,7 @@
% - MriFileRef : Relative path to the Brainstorm MRI file used as a reference
% - sMriSrc : Brainstorm MRI structure to register (fields Cube, Voxsize, SCS, NCS...)
% - sMriRef : Brainstorm MRI structure used as a reference
-% - Method : Method used for the coregistration of the volume: 'spm', 'mni', 'vox2ras'
+% - Method : Method used for the coregistration of the volume: 'spm', 'mni', 'vox2ras', 'ct2mri'
% - isReslice : If 1, reslice the output volume to match dimensions of the reference volume
% - isAtlas : If 1, perform only integer/nearest neighbors interpolations (MNI and VOX2RAS registration only)
%
@@ -38,6 +38,7 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2016-2023
+% Chinmay Chinara, 2023
% ===== LOAD INPUTS =====
% Parse inputs
@@ -133,7 +134,8 @@
% Create coregistration batch
if isReslice
- % Coreg: Estimate and reslice
+ % Coregister: Estimate and reslice
+ bst_progress('text', 'Calling SPM batch...(Coregister: Estimate & Reslice)');
matlabbatch{1}.spm.spatial.coreg.estwrite.ref = {[NiiRefFile, ',1']};
matlabbatch{1}.spm.spatial.coreg.estwrite.source = {[NiiSrcFile, ',1']};
matlabbatch{1}.spm.spatial.coreg.estwrite.other = {''};
@@ -143,7 +145,8 @@
% Output file
NiiRegFile = bst_fullfile(TmpDir, 'rspm_src.nii');
else
- % Coreg: Estimate
+ % Coregister: Estimate
+ bst_progress('text', 'Calling SPM batch...(Coregister: Estimate)');
matlabbatch{1}.spm.spatial.coreg.estimate.ref = {[NiiRefFile, ',1']};
matlabbatch{1}.spm.spatial.coreg.estimate.source = {[NiiSrcFile, ',1']};
matlabbatch{1}.spm.spatial.coreg.estimate.other = {''};
@@ -233,7 +236,68 @@
end
% Output file tag
fileTag = '_mni';
+
+ % ===== CT2MRIREG =====
+ case 'ct2mri'
+ % Check if ct2mrireg plugin is installed
+ [isInstalled, errMsg] = bst_plugin('Install', 'ct2mrireg');
+ if ~isInstalled
+ if ~isProgress
+ bst_progress('stop');
+ end
+ return;
+ end
+
+ % Save files in tmp directory
+ bst_progress('text', 'Saving temporary files...');
+ % Get temporary folder
+ TmpDir = bst_get('BrainstormTmpDir', 0, 'ct2mrireg');
+ % Save source CT in .nii format
+ NiiSrcFile = bst_fullfile(TmpDir, 'ct2mri_src.nii');
+ out_mri_nii(sMriSrc, NiiSrcFile);
+ % Save reference MRI in .nii format
+ NiiRefFile = bst_fullfile(TmpDir, 'ct2mri_ref.nii');
+ out_mri_nii(sMriRef, NiiRefFile);
+
+ % Perform the coregistration of the CT to MRI
+ NiiRegFile = bst_fullfile(TmpDir, 'contrastmri2preMRI.nii.gz');
+ bst_progress('text', 'Performing co-registration using ct2mrireg plugin...');
+ NiiRegFile = ct2mrireg(NiiSrcFile, NiiRefFile, NiiRegFile);
+
+ % Read output volume
+ sMriReg = in_mri(NiiRegFile, 'ALL', 0, 0);
+
+ % Delete the temporary files
+ file_delete(TmpDir, 1, 1);
+ % Output file tag
+ fileTag = '_ct2mri';
+ % === UPDATE FIDUCIALS ===
+ if isReslice
+ % Use the reference SCS coordinates
+ if isfield(sMriRef, 'SCS')
+ sMriReg.SCS = sMriRef.SCS;
+ end
+ % Use the reference NCS coordinates
+ if isfield(sMriRef, 'NCS')
+ sMriReg.NCS = sMriRef.NCS;
+ end
+
+ % Reslice the volume
+ bst_progress('text', 'Performing Reslicing...');
+ [sMriReg, errMsg] = mri_reslice(sMriReg, sMriRef, 'scs', 'scs', isAtlas);
+ % Error handling
+ if isempty(sMriReg) || ~isempty(errMsg)
+ if ~isProgress
+ bst_progress('stop');
+ end
+ return
+ end
+ else
+ isUpdateScs = 1;
+ isUpdateNcs = 1;
+ end
+
% ===== VOX2RAS =====
case 'vox2ras'
% Nothing to do, just reslice if needed
@@ -334,6 +398,10 @@
% Add history entry
sMriReg.History = sMriSrc.History;
sMriReg = bst_history('add', sMriReg, 'resample', ['MRI co-registered on default file (' Method '): ' MriFileRef]);
+ % Add history entry (reslice)
+ if isReslice
+ sMriReg = bst_history('add', sMriReg, 'resample', ['MRI resliced to default file: ' MriFileRef]);
+ end
% Save new file
MriFileRegFull = file_unique(strrep(file_fullpath(MriFileSrc), '.mat', [fileTag '.mat']));
MriFileReg = file_short(MriFileRegFull);
diff --git a/toolbox/anatomy/mri_histogram.m b/toolbox/anatomy/mri_histogram.m
index 6d985006f..579008bfa 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
@@ -102,11 +102,11 @@
end
% Construct a regular Histogram function
-% Suppress all indices that has zero-values (to avoid previous normalizations)
-% NOTA : Do not consider the values at the intensity value 0, it may
+% Suppress all indices that have zero values (to avoid previous normalizations)
+% NOTA : Do not consider the first bin (intensity 0), it may
% not correspond to the real image Histogram...
index = find(Histogram.fncY > 10); % PREVIOUSLY: 100 instead of 10
-index = index(2:length(index));
+index = index(2:length(index)); % discard first bin
histoX = [0 Histogram.fncX(index)];
histoY = [0 Histogram.fncY(index)];
@@ -136,7 +136,7 @@
minIndex(diff(minIndex) == 1) = [];
end
-% Detect and deleting all "wrong" extrema (that are too close to each other)
+% Detect and delete all "wrong" extrema (that are too close to each other)
epsilon = max(histoX)*.02;
i = 1;
while(i <= length(maxIndex))
@@ -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
@@ -223,26 +223,28 @@
defaultWhite = round(interp1(unikCumulFncY, unikFncX, .8));
Histogram.bgLevel = defaultBg;
Histogram.whiteLevel = defaultWhite;
- % Detect if the background has already been removed :
- % ie. if there is a unique 0 valued interval a the beginning of the Histogram
- % Practically : - nzero = length of the first 0-valued interval
- % - nnonzero = length of the first non-0-valued interval
- % - bg removed if : (nzero > 1) and (nnonzero > nzero)
- nzero = find(Histogram.fncY(2:length(Histogram.fncY)) ~= 0);
- nnonzero = find(Histogram.fncY((nzero(1)+1):length(Histogram.fncY)) == 0);
- if ((nzero(1)>2) && ~isempty(nnonzero) && (nnonzero(1) > nzero(1)))
- Histogram.bgLevel = nzero(1);
+ % Detect if the background has already been removed, i.e. if a range of low intensity values
+ % were replaced by zeros in the volume, producing an interval of empty bins at the beginning
+ % of the Histogram, after the first (zero-intensity) bin. Require also that the zero bin be
+ % the largest, as it seems denoising can produce this background removal effect but with a
+ % very low threshold which is not appropriate.
+ [~, iMaxBin] = max(Histogram.fncY);
+ % First non-empty bin after first bin.
+ iNonZero = find(Histogram.fncY(2:end) ~= 0, 1) + 1;
+ if iMaxBin == 1 && ~isempty(iNonZero) && iNonZero > 2
+ % There was an interval of empty bins. Set threshold at last empty bin intensity.
+ Histogram.bgLevel = Histogram.fncX(iNonZero - 1);
% Else, background has not been removed yet
- % If there is less than two maxima : use the default background threshold
+ % If there are fewer than two maxima : use the default background threshold
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 exists)
[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 +253,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 +343,4 @@
end
-
\ No newline at end of file
diff --git a/toolbox/anatomy/mri_reslice.m b/toolbox/anatomy/mri_reslice.m
index e3144613f..0dd0b9318 100644
--- a/toolbox/anatomy/mri_reslice.m
+++ b/toolbox/anatomy/mri_reslice.m
@@ -274,7 +274,7 @@
% Update comment
sMriReg.Comment = file_unique(sMriReg.Comment, {sSubject.Anatomy.Comment});
% Add history entry
- sMriReg = bst_history('add', sMriReg, 'resample', ['MRI co-registered on default file: ' MriFileRef]);
+ sMriReg = bst_history('add', sMriReg, 'resample', ['MRI resliced to default file: ' MriFileRef]);
% Save new file
MriFileRegFull = file_unique(strrep(file_fullpath(MriFileSrc), '.mat', [fileTag '.mat']));
MriFileReg = file_short(MriFileRegFull);
diff --git a/toolbox/anatomy/mri_reslice_mni.m b/toolbox/anatomy/mri_reslice_mni.m
index e09bea32c..ba91f406a 100644
--- a/toolbox/anatomy/mri_reslice_mni.m
+++ b/toolbox/anatomy/mri_reslice_mni.m
@@ -74,7 +74,7 @@
newCube = interp3(Y1, X1, Z1, sMriMni.Cube, Xgrid2mni, Ygrid2mni, Zgrid2mni, 'nearest', NaN);
% Cubic interpolation for floating point values
else
- newCube = single(interp3(Y1, X1, Z1, double(sMriSrc.Cube), Xgrid2mni, Ygrid2mni, Zgrid2mni, 'cubic', 0));
+ newCube = single(interp3(Y1, X1, Z1, double(sMriMni.Cube), Xgrid2mni, Ygrid2mni, Zgrid2mni, 'cubic', 0));
end
% Replace bad values with 0 (points that do not have MNI coordinates)
newCube(isnan(newCube)) = 0;
diff --git a/toolbox/anatomy/mri_skullstrip.m b/toolbox/anatomy/mri_skullstrip.m
new file mode 100644
index 000000000..ec7282ed7
--- /dev/null
+++ b/toolbox/anatomy/mri_skullstrip.m
@@ -0,0 +1,215 @@
+function [MriFileMask, errMsg, fileTag, binBrainMask] = mri_skullstrip(MriFileSrc, MriFileRef, Method)
+% MRI_SKULLSTRIP: Skull stripping on 'MriFileSrc' using 'MriFileRef' as reference MRI.
+% Both volumes must have the same Cube and Voxel size
+%
+% USAGE: [MriFileMask, errMsg, fileTag, binBrainMask] = mri_skullstrip(MriFileSrc, MriFileRef, Method)
+% [sMriMask, errMsg, fileTag, binBrainMask] = mri_skullstrip(sMriSrc, sMriRef, Method)
+%
+% INPUTS:
+% - MriFileSrc : MRI structure or MRI file to apply skull stripping on
+% - MriFileRef : MRI structure or MRI file to find brain masking for skull stripping
+% If empty, the Default MRI for that Subject with 'MriFileSrc' is used
+% - Method : If 'BrainSuite', use BrainSuite's Brain Surface Extractor (BSE)
+% If 'SPM', use SPM Tissue Segmentation
+%
+% OUTPUTS:
+% - MriFileMask : MRI structure or MRI file after skull stripping
+% - errMsg : Error message. Empty if no error
+% - fileTag : Tag added to the comment and filename
+% - binBrainMask : Volumetric binary mask of the skull stripped 'MriFileRef' reference MRI
+%
+% @=============================================================================
+% 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: Raymundo Cassani, 2024
+% Chinmay Chinara, 2024
+
+% ===== PARSE INPUTS =====
+% Parse inputs
+if (nargin < 3)
+ Method = [];
+end
+
+% Initialize outputs
+MriFileMask = [];
+errMsg = '';
+fileTag = '';
+binBrainMask = [];
+
+% Return if invalid Method
+if isempty(Method) || strcmpi(Method, 'Skip')
+ return;
+end
+
+% Progress bar
+isProgress = bst_progress('isVisible');
+if ~isProgress
+ bst_progress('start', 'MRI skull stripping', 'Loading input volumes...');
+end
+% USAGE: mri_reslice(sMriSrc, sMriRef)
+if isstruct(MriFileSrc)
+ sMriSrc = MriFileSrc;
+ sMriRef = MriFileRef;
+ MriFileSrc = [];
+ MriFileRef = [];
+% USAGE: mri_reslice(MriFileSrc, MriFileRef)
+elseif ischar(MriFileSrc)
+ % Get the default MRI for this subject
+ if isempty(MriFileRef)
+ sSubject = bst_get('MriFile', MriFileSrc);
+ MriFileRef = sSubject.Anatomy(sSubject.iAnatomy).FileName;
+ end
+ % Load MRI volumes
+ sMriSrc = in_mri_bst(MriFileSrc);
+ sMriRef = in_mri_bst(MriFileRef);
+else
+ error('Invalid call.');
+end
+
+% Check that same size
+refSize = size(sMriRef.Cube(:,:,:,1));
+srcSize = size(sMriSrc.Cube(:,:,:,1));
+if ~all(refSize == srcSize) || ~all(round(sMriRef.Voxsize(1:3) .* 1000) == round(sMriSrc.Voxsize(1:3) .* 1000))
+ errMsg = 'Skull stripping cannot be performed if the reference MRI has different size';
+ return
+end
+
+% === SKULL STRIPPING ===
+% Reset any previous logo
+bst_plugin('SetProgressLogo', []);
+switch lower(Method)
+ case 'brainsuite'
+ % Check for BrainSuite Installation
+ [~, errMsg] = process_dwi2dti('CheckBrainSuiteInstall');
+ if ~isempty(errMsg)
+ bst_progress('text', 'Skipping skull stripping. BrainSuite not installed.');
+ return
+ end
+ % Set the BrainSuite logo
+ bst_progress('setimage', bst_fullfile(bst_get('BrainstormDocDir'), 'plugins', 'brainsuite_logo.png'));
+ % Get temporary folder
+ TmpDir = bst_get('BrainstormTmpDir', 0, 'brainsuite');
+ % Save reference MRI in .nii format
+ NiiRefFile = bst_fullfile(TmpDir, 'mri_ref.nii');
+ out_mri_nii(sMriRef, NiiRefFile);
+ % Perform skull stripping using Brain Surface Extractor (BSE)
+ bst_progress('text', 'Skull Stripping: BrainSuite Brain Surface Extractor...');
+ strCall = [...
+ 'bse -i "' NiiRefFile '" --auto' ...
+ ' -o "' fullfile(TmpDir, 'skull_stripped_mri.nii.gz"') ...
+ ' --trim --mask "' fullfile(TmpDir, 'bse_smooth_brain.mask.nii.gz"') ...
+ ' --hires "' fullfile(TmpDir, 'bse_detailled_brain.mask.nii.gz"') ...
+ ' --cortex "' fullfile(TmpDir, 'bse_cortex_file.nii.gz"')];
+ disp(['BST> System call: ' strCall]);
+ status = system(strCall);
+ % Error handling
+ if (status ~= 0)
+ errMsg = ['BrainSuite failed at step BSE.', 10, 'Check the Matlab command window for more information.'];
+ return
+ end
+ % Get the brain mask
+ NiiBrainMaskFile = bst_fullfile(TmpDir, 'bse_smooth_brain.mask.nii.gz');
+ sMriBrainMask = in_mri(NiiBrainMaskFile, 'ALL', 0, 0);
+ % Make it a binary mask
+ sMriBrainMask.Cube = sMriBrainMask.Cube/255;
+ % Some erosion to reduce any artefacts
+ sMriBrainMask.Cube = sMriBrainMask.Cube & ~mri_dilate(~sMriBrainMask.Cube, 3);
+ % Logic brain mask cube
+ binBrainMask = sMriBrainMask.Cube > 0;
+ % Temporary files to delete
+ filesDel = TmpDir;
+
+ case 'spm'
+ % Check for SPM12 installation
+ [isInstalledSpm, errMsg] = bst_plugin('Install', 'spm12');
+ if ~isInstalledSpm
+ bst_progress('text', 'Skipping skull stripping. SPM not installed.');
+ return;
+ end
+ % Set the SPM logo
+ bst_plugin('SetProgressLogo', 'spm12');
+ % Perform skull stripping using SPM Tissue Segmentation
+ bst_progress('text', 'Skull Stripping: SPM Segment...');
+ % Reset matlabbatch to start fresh
+ clear matlabbatch;
+ % Get the TPM atlas
+ TpmFile = bst_get('SpmTpmAtlas', 'SPM');
+ % Get the SPM tissue segments
+ [~, TpmFiles] = mri_normalize_segment(sMriRef, TpmFile);
+ % Compute brain mask: union(GM, WM, CSF)
+ sGm = in_mri_nii(TpmFiles{2}, 0, 0, 0);
+ sWm = in_mri_nii(TpmFiles{1}, 0, 0, 0);
+ sCsf = in_mri_nii(TpmFiles{3}, 0, 0, 0);
+ binBrainMask = (sGm.Cube + sWm.Cube + sCsf.Cube) > 0;
+ % Temporary files to delete
+ filesDel = bst_fileparts(TpmFiles{1});
+
+ otherwise
+ errMsg = ['Invalid skull stripping method: ' Method];
+ return
+end
+% Reset logo
+bst_progress('removeimage');
+
+% Apply brain mask
+sMriMask = sMriSrc;
+sMriMask.Cube(~binBrainMask) = 0;
+% File tag
+fileTag = sprintf('_masked_%s', lower(Method));
+
+% ===== SAVE NEW FILE =====
+% Add file tag
+sMriMask.Comment = [sMriSrc.Comment, fileTag];
+% Save output
+if ~isempty(MriFileSrc)
+ bst_progress('text', 'Saving new file...');
+ % Get subject
+ [sSubject, iSubject] = bst_get('MriFile', MriFileSrc);
+ % Update comment
+ sMriMask.Comment = file_unique(sMriMask.Comment, {sSubject.Anatomy.Comment});
+ % Add history entry
+ sMriMask = bst_history('add', sMriMask, 'resample', ['Skull stripping with "' Method '" using on default file: ' MriFileRef]);
+ % Save new file
+ MriFileMaskFull = file_unique(strrep(file_fullpath(MriFileSrc), '.mat', [fileTag '.mat']));
+ MriFileMask = file_short(MriFileMaskFull);
+ % Save new MRI in Brainstorm format
+ sMriMask = out_mri_bst(sMriMask, MriFileMaskFull);
+
+ % Register new MRI
+ iAnatomy = length(sSubject.Anatomy) + 1;
+ sSubject.Anatomy(iAnatomy) = db_template('Anatomy');
+ sSubject.Anatomy(iAnatomy).FileName = MriFileMask;
+ sSubject.Anatomy(iAnatomy).Comment = sMriMask.Comment;
+ % Update subject structure
+ bst_set('Subject', iSubject, sSubject);
+ % Refresh tree
+ panel_protocols('UpdateNode', 'Subject', iSubject);
+ panel_protocols('SelectNode', [], 'anatomy', iSubject, iAnatomy);
+ % Save database
+ db_save();
+else
+ % Return output structure
+ MriFileMask = sMriMask;
+end
+
+% Delete the temporary files
+file_delete(filesDel, 1, 1);
+% Close progress bar
+if ~isProgress
+ bst_progress('stop');
+end
\ No newline at end of file
diff --git a/toolbox/anatomy/tess_check.m b/toolbox/anatomy/tess_check.m
new file mode 100644
index 000000000..a3bc191a1
--- /dev/null
+++ b/toolbox/anatomy/tess_check.m
@@ -0,0 +1,201 @@
+function [isOk, Info] = tess_check(Vertices, Faces, isVerbose, isOpenOk, isShow)
+% TESS_CHECK: Check the integrity of a tesselation.
+%
+% USAGE: [isOk, Details] = tess_check(Vertices, Faces, Verbose)
+%
+% DESCRIPTION:
+% Check if a surface mesh is simple, closed, non self-intersecting, well oriented, duplicate
+% vertices or faces, etc. There are some custom checks, and if available, use the Matlab Lidar
+% toolbox. Could add meshcheckrepair from iso2mesh toolbox, and possibly others.
+%
+% INPUTS:
+% - Vertices : Mx3 double matrix
+% - Faces : Nx3 double matrix
+% - isOpenOk : An open surface is considered ok, otherwise flag non-closed as an issue.
+% - isVerbose : Write details to command window if any unexpected features.
+% - isShow : Display surface in new figure
+% OUTPUTS:
+% - isOk : All checks look good
+% - Details : Structure with all check statuses
+
+% @=============================================================================
+% 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, 2025
+
+% Parse inputs
+if nargin < 4 || isempty(isOpenOk)
+ isOpenOk = false;
+end
+if nargin < 3 || isempty(isVerbose)
+ isVerbose = true;
+end
+% Check matrices orientation
+if (size(Vertices, 2) ~= 3) || (size(Faces, 2) ~= 3)
+ error('Faces and Vertices must have 3 columns (X,Y,Z).');
+end
+
+isOk = true;
+Info = [];
+
+% Some custom checks first based on face edge connectivity.
+% First check duplicate faces (based on vertex indices, not coordinates)
+[~, FaceConn3] = tess_faceconn(Faces, 3); % 3 vertices in common = duplicate
+nAdjFaces = sum(FaceConn3, 2) - 1;
+Info.nDuplicate = sum(nAdjFaces > 0) / 2;
+% Then check for other issues
+[~, FaceConn] = tess_faceconn(Faces, 2);
+nAdjFaces = sum(FaceConn, 2) - 1;
+Info.nEdgeDisconnectedFaces = sum(nAdjFaces == 0);
+Info.nEdgeOverConnectedFaces = sum(nAdjFaces > 3); % 5, 7, 9 found after reducepatch
+% If there is an even number of adjacent faces > 3, there's something strange. An odd number could
+% be two lobes of the same surface just touching on an edge or face, not necessarily intersecting.
+Info.nEvenEdgeOverConnectedFaces = sum(nAdjFaces > 3 & ~mod(nAdjFaces,2));
+Info.nBoundaryFaces = sum(nAdjFaces == 1) + sum(nAdjFaces == 2);
+
+if Info.nDuplicate > 0
+ isOk = false;
+ if isVerbose
+ fprintf('BST>Surface has %d duplicate faces.\n', Info.nDuplicate);
+ end
+end
+if Info.nEdgeDisconnectedFaces > 0 % yes many after reducepatch
+ isOk = false;
+ if isVerbose
+ fprintf('BST>Surface has %d disconnected faces (no shared edges, but possibly one shared vertex).\n', Info.nEdgeDisconnectedFaces);
+ end
+end
+if Info.nEdgeOverConnectedFaces > 0
+ isOk = false;
+ if isVerbose
+ fprintf('BST>Surface intersects or has touching/duplicate edges or faces (%d).\n', Info.nEdgeOverConnectedFaces);
+ if Info.nEvenEdgeOverConnectedFaces > 0
+ fprintf('BST>Surface has edges shared by 3,5,7,... faces, indicating strange topology (%d).\n', Info.nEvenEdgeOverConnectedFaces);
+ end
+ end
+end
+% Note openness if ok so far
+if isOk
+ if Info.nBoundaryFaces > 0
+ Info.isOpen = true;
+ else
+ Info.isOpen = false;
+ end
+else % don't bother defining if surface is weird
+ Info.isOpen = [];
+end
+% Check for boundary faces either way if we want closed
+if Info.nBoundaryFaces > 0 && ~isOpenOk
+ isOk = false;
+ if isVerbose
+ fprintf('BST>Surface has %d boundary edges.\n', Info.nBoundaryFaces);
+ end
+end
+
+% Check orientation if ok so far; so we should have each edge shared by 2 faces, or 1 if open.
+Info.isOriented = [];
+if isOk
+ Edges = [Faces(:), [Faces(:, 2); Faces(:, 3); Faces(:, 1)]];
+ isEdgeFlip = Edges(:, 1) > Edges(:, 2);
+ [~, ~, iE] = unique(sort(Edges, 2), 'rows');
+ % Look for boundaries of open surface.
+ isBoundE = false(size(Edges, 1), 1);
+ [iE, iSort] = sort(iE);
+ % Add one more row for the loop to also evaluate the last real edge.
+ iE(end+1,:) = 0;
+ n = 1;
+ for i = 2:numel(iE)
+ if iE(i) ~= iE(i-1)
+ % Evaluate previous edge
+ if n == 1
+ % Only one copy, boundary edge.
+ isBoundE(iE(i-1)) = true;
+ elseif n == 2
+ % Two faces, were the orientations different?
+ if sum(isEdgeFlip(iSort([i-1,i-2]))) ~= 1 % should be sum([0,1])
+ isOk = false;
+ if isVerbose
+ fprintf('BST>Surface not well oriented (face normals are mixed pointing in and out).\n');
+ end
+ break;
+ end
+ % Reset for new edge
+ n = 1;
+ else
+ % Previously undetected issue with edge shared among more than 2 faces.
+ isOk = false;
+ if isVerbose
+ fprintf('BST>Surface has edge shared among more than 2 faces.\n');
+ end
+ break;
+ end
+ else
+ n = n + 1;
+ end
+ end
+end
+
+if isShow
+ FigureId = db_template('FigureId');
+ FigureId.Type = '3DViz';
+ hFig = figure_3d('CreateFigure', FigureId);
+ figure_3d('PlotSurface', hFig, Faces, Vertices, [1,1,1], 0); % color, transparency required
+ figure_3d('ViewAxis', hFig, true); % isVisible
+ hFig.Visible = "on";
+end
+
+
+% -------------------------------------------
+% Check if Lidar Toolbox is installed (requires image processing + computer vision)
+isLidarToolbox = exist('surfaceMesh', 'file') == 2;
+if ~isLidarToolbox
+ fprintf('BST>tess_downsize method "simplify" requires Matlab''s Lidar Toolbox, which was not found.\n');
+ return;
+end
+% Create mesh object
+oMesh = surfaceMesh(Vertices, Faces);
+
+% Check all mesh features Lidar Toolbox offers
+Info.isVertexManifold = isVertexManifold(oMesh); % Check if surface mesh is vertex-manifold
+Info.isEdgeManifold = isEdgeManifold(oMesh, true); % allow boundary edges
+Info.isClosedManifold = isEdgeManifold(oMesh, false); % allow boundary edges
+Info.isOrientable = isOrientable(oMesh); % Check if surface mesh is orientable
+% Self-intersecting test is slow (not sure how long, didn't wait more than 15 minutes, uses one core only)
+%Info.isSelfIntersecting = isSelfIntersecting(oMesh); % Check if surface mesh is self-intersecting
+Info.isWatertight = isWatertight(oMesh); % Check if surface mesh is watertight
+% removeDefects could be used in tess_clean
+
+if isVerbose
+ if (isOpenOk && ~Info.isEdgeManifold) || (~isOpenOk && ~Info.isClosedManifold)
+ fprintf('BST>Surface not "edge manifold" (each edge has at most one face on each side).\n');
+ end
+ if ~Info.isVertexManifold
+ fprintf('BST>Surface not "vertex manifold" (like a "fan" at each vertex).\n');
+ end
+ if ~Info.isOrientable
+ fprintf('BST>Surface not well oriented (face normals are mixed pointing in and out).\n');
+ end
+ % if Info.isSelfIntersecting
+ % fprintf('BST>Surface self-intersects.\n');
+ % end
+end
+
+end
+
+
+
diff --git a/toolbox/anatomy/tess_deface.m b/toolbox/anatomy/tess_deface.m
new file mode 100644
index 000000000..e16c0ce4c
--- /dev/null
+++ b/toolbox/anatomy/tess_deface.m
@@ -0,0 +1,62 @@
+function [head_surface] = tess_deface(head_surface)
+% TESS_DEFACE: Removing non-essential vertices (bottom half of the subject's face in mesh) to deface the 3D mesh
+%
+% USAGE: [head_surface] = tess_deface(head_surface);
+%
+% INPUT:
+% - head_surface: Brainstorm tesselation structure with fields:
+% |- Vertices : {[nVertices x 3] double}, in millimeters
+% |- Faces : {[nFaces x 3] double}
+% |- Color : {[nColors x 3] double}, normalized between 0-1 (optional)
+%
+% OUTPUT:
+% - head_surface: Brainstorm tesselation structure with fields:
+% |- Vertices : {[nVertices x 3] double}, in millimeters
+% |- Faces : {[nFaces x 3] double}
+% |- Color : {[nColors x 3] double}, normalized between 0-1 (optional)
+%
+% @=============================================================================
+% 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: Yash Shashank Vakilna, 2024
+% Chinmay Chinara, 2024
+
+% Identify vertices to remove from the surface mesh
+% Spherical coordinates
+[TH,PHI,R] = cart2sph(head_surface.Vertices(:,1), head_surface.Vertices(:,2), head_surface.Vertices(:,3));
+% Flat projection
+R = 1 - PHI ./ pi*2;
+
+% Remove the identified vertices from the surface mesh
+iRemoveVert = find(R > 1.1);
+if ~isempty(iRemoveVert)
+ [head_surface.Vertices, head_surface.Faces] = tess_remove_vert(head_surface.Vertices, head_surface.Faces, iRemoveVert);
+ if isfield(head_surface, 'Color')
+ head_surface.Color(iRemoveVert, :) = [];
+ end
+end
+
+head_surface.VertConn = tess_vertconn(head_surface.Vertices, head_surface.Faces);
+head_surface.VertNormals = tess_normals(head_surface.Vertices, head_surface.Faces, head_surface.VertConn);
+head_surface.Curvature = tess_curvature(head_surface.Vertices, head_surface.VertConn, head_surface.VertNormals, .1);
+[~, head_surface.VertArea] = tess_area(head_surface.Vertices, head_surface.Faces);
+head_surface.SulciMap = tess_sulcimap(head_surface);
+head_surface.Comment = [head_surface.Comment '_defaced'];
+head_surface = bst_history('add', head_surface, 'deface_mesh', 'mesh defaced');
+
+end
\ No newline at end of file
diff --git a/toolbox/anatomy/tess_downsize.m b/toolbox/anatomy/tess_downsize.m
index 972c7ac15..5d4b57df1 100644
--- a/toolbox/anatomy/tess_downsize.m
+++ b/toolbox/anatomy/tess_downsize.m
@@ -2,9 +2,12 @@
% TESS_DOWNSIZE: Reduces the number of vertices in a surface file.
%
% USAGE: [NewTessFile, iSurface, I, J] = tess_downsize(TessFile, newNbVertices=[ask], Method=[ask]);
+% [NewTessMat, iSurface, I, J] = tess_downsize(TessMat, newNbVertices=[ask], Method=[ask]);
%
% INPUT:
% - TessFile : Full path to surface file to decimate
+% - TessMat : Already loaded surface structure, a structure is returned in that case and no
+% file is saved.
% - newNbVertices : Desired number of vertices
% - Method : {'reducepatch', 'reducepatch_subdiv', 'iso2mesh', 'iso2mesh_project'}
% OUTPUT:
@@ -56,11 +59,17 @@
I = [];
J = [];
+% Surface structure now accepted as input
+isTessInput = isstruct(TessFile);
%% ===== ASK FOR MISSING OPTIONS =====
% Get the number of vertices
-VarInfo = whos('-file',file_fullpath(TessFile),'Vertices');
-oldNbVertices = VarInfo.size(1);
+if isTessInput
+ oldNbVertices = size(TessFile.Vertices, 1);
+else
+ VarInfo = whos('-file',file_fullpath(TessFile),'Vertices');
+ oldNbVertices = VarInfo.size(1);
+end
% If new number of vertices was not provided: ask user
if isempty(newNbVertices)
% Ask user the new number of vertices
@@ -82,23 +91,39 @@
return;
end
+% Check if Lidar Toolbox is installed (requires image processing + computer vision)
+isLidarToolbox = exist('surfaceMesh', 'file') == 2;
+
% Ask for resampling method
if isempty(Method)
+ % Downsize methods strings
+ methods_str = {['Matlab''s reducepatch: ' ...
+ ' | - Inhomogeneous mesh: large faces at the top of the gyri ' ...
+ ' | - Keeps the atlases and the subjects co-registration'], ...
+ ['Matlab''s reducepatch + subdivide large faces: ' ...
+ ' | - The large faces at the top of the gyri are subdivided in three ' ...
+ ' | - Deletes the atlases and the subjects co-registration'], ...
+ ['iso2mesh/CGAL library: ' ...
+ ' | - Homogeneous mesh: all the faces have similar sizes ' ...
+ ' | - Deletes the atlases and the subject co-registration ' ...
+ ' | - If the downsample looks dark, right-click > Swap faces']};
+ % ['iso2mesh/CGAL + project on the original surface: ' ...
+ % ' | - Homogeneous mesh but possible topological problems ' ...
+ % ' | - Damages the atlases and the subject co-registration']},
+
+ if isLidarToolbox
+ methods_str{end+1} = ...
+ ['Matlab''s simplify, from Lidar Toolbox: ' ...
+ ' | - Possibly better at preserving shape topology than reducepatch ' ...
+ ' | - Deletes the atlases and the subjects co-registration'];
+ end
+ % Identify textured surfaces (color info is present) and show available methods for them
+ VarInfo = whos('-file',file_fullpath(TessFile), 'Color');
+ if all(VarInfo.size ~= 0)
+ methods_str = methods_str(1); % Inhomogeneous mesh
+ end
% Ask method
- ind = java_dialog('radio', 'Select the resampling method:', 'Resample surface', [], ...
- {['Matlab''s reducepatch: ' ...
- ' | - Inhomogeneous mesh: large faces at the top of the gyri ' ...
- ' | - Keeps the atlases and the subjects co-registration'], ...
- ['Matlab''s reducepatch + subdivide large faces: ' ...
- ' | - The large faces at the top of the gyri are subdivided in three ' ...
- ' | - Deletes the atlases and the subjects co-registration'], ...
- ['iso2mesh/CGAL library: ' ...
- ' | - Homogeneous mesh: all the faces have similar sizes ' ...
- ' | - Deletes the atlases and the subject co-registration ' ...
- ' | - If the downsample looks dark, right-click > Swap faces']}, 1);
-% ['iso2mesh/CGAL + project on the original surface: ' ...
-% ' | - Homogeneous mesh but possible topological problems ' ...
-% ' | - Damages the atlases and the subject co-registration']}, 1);
+ ind = java_dialog('radio', 'Select the resampling method:', 'Resample surface', [], methods_str, 1);
if isempty(ind)
return
end
@@ -107,7 +132,8 @@
case 1, Method = 'reducepatch';
case 2, Method = 'reducepatch_subdiv';
case 3, Method = 'iso2mesh';
- case 4, Method = 'iso2mesh_project';
+ case 4, Method = 'simplify';
+ % case 4, Method = 'iso2mesh_project';
end
end
@@ -121,16 +147,27 @@
end
%% ===== LOAD FILE =====
-% Progress bar
-bst_progress('start', 'Resample surface', 'Loading file...');
-% Load file
-TessMat = in_tess_bst(TessFile);
-% Prepare variables
+if isTessInput
+ TessMat = TessFile;
+ if ~isfield(TessMat, 'Comment')
+ TessMat.Comment = 'iso head';
+ end
+else
+ % Progress bar
+ bst_progress('start', 'Resample surface', 'Loading file...');
+ % Load file
+ TessMat = in_tess_bst(TessFile);
+ % Prepare variables
+end
TessMat.Faces = double(TessMat.Faces);
TessMat.Vertices = double(TessMat.Vertices);
+if isfield(TessMat, 'Color')
+ TessMat.Color = double(TessMat.Color);
+else
+ TessMat.Color = [];
+end
dsFactor = newNbVertices / size(TessMat.Vertices, 1);
-
%% ===== RESAMPLE =====
bst_progress('start', 'Resample surface', ['Resampling surface: ' TessMat.Comment '...']);
% Resampling methods
@@ -145,6 +182,9 @@
% Re-order the vertices so that they are in the same order in the output surface
[I, iSort] = sort(I);
NewTessMat.Vertices = TessMat.Vertices(I,:);
+ if ~isempty(TessMat.Color)
+ NewTessMat.Color = TessMat.Color(I,:);
+ end
J = J(iSort);
% Re-order the vertices in the faces
iSortFaces(J) = 1:length(J);
@@ -411,8 +451,29 @@
NewTessMat.Faces = Faces;
NewTessMat.Vertices = Vertices;
MethodTag = '_iso2mesh_proj';
+
+ % ===== SIMPLIFY =====
+ % Matlab's Lidar Toolbox simplify (since R2022b)
+ case 'simplify'
+ if ~isLidarToolbox
+ fprintf('BST>tess_downsize method "simplify" requires Matlab''s Lidar Toolbox, which was not found.\n');
+ end
+ % Create mesh object
+ oMesh = surfaceMesh(TessMat.Vertices, TessMat.Faces);
+
+ % Reduce number of vertices
+ simplify(oMesh, 'TargetNumFaces', (newNbVertices - 2) * 2); % no output variable for this toolbox!
+ NewTessMat.Faces = oMesh.Faces;
+ NewTessMat.Vertices = oMesh.Vertices;
+
end
+if isTessInput
+ % This should go after "remove folded faces" if that step was desired... skipping for now for
+ % tess_isohead as it does its own checks and corrections.
+ NewTessFile = NewTessMat;
+ return;
+end
%% ===== REMOVE FOLDED FACES =====
% Find equal faces
@@ -529,15 +590,18 @@
%% ===== UPDATE DATABASE =====
-% Save downsized surface file
-bst_save(NewTessFile, NewTessMat, 'v7');
-% Make output filename relative
-NewTessFile = file_short(NewTessFile);
-% Get subject
-[sSubject, iSubject] = bst_get('SurfaceFile', TessFile);
-% Register this file in Brainstorm database
-iSurface = db_add_surface(iSubject, NewTessFile, NewComment);
-
+% Save downsized surface file, or return structure
+if isTessInput
+ NewTessFile = NewTessMat;
+else
+ bst_save(NewTessFile, NewTessMat, 'v7');
+ % Make output filename relative
+ NewTessFile = file_short(NewTessFile);
+ % Get subject
+ [~, iSubject] = bst_get('SurfaceFile', TessFile);
+ % Register this file in Brainstorm database
+ iSurface = db_add_surface(iSubject, NewTessFile, NewComment);
+end
% Close progress bar
bst_progress('stop');
@@ -548,17 +612,11 @@
% Resample a surface using iso2mesh/CGAL library
% Author: Qianqian Fang (fangq nmr.mgh.harvard.edu)
function NewTessMat = iso2mesh_resample(TessMat, dsFactor)
- % Check if iso2mesh is installed
- if ~exist('meshresample', 'file')
- bst_error(['Please install iso2mesh on your computer:
' ...
- ' 1) Visit the website: http://iso2mesh.sourceforge.net ' ...
- ' 2) Download the iso2mesh package for your operating system. ' ...
- ' 3) Unzip it on your local hard drive. ' ...
- ' 4) Add the iso2mesh folder to your Matlab path. ' ...
- ' 5) Try again downsampling the surface.
'], 'Install iso2mesh', 0);
- web('http://sourceforge.net/projects/iso2mesh/files/iso2mesh/1.5.0%20%28iso2mesh%202013%29/', '-browser');
- NewTessMat = [];
- return;
+ % Install/load iso2mesh plugin
+ isInteractive = 1;
+ [isInstalled, errInstall] = bst_plugin('Install', 'iso2mesh', isInteractive);
+ if ~isInstalled
+ error('Plugin "iso2mesh" not available.');
end
% Running iso2mesh routine
[Vertices,Faces] = meshresample(TessMat.Vertices, TessMat.Faces, dsFactor);
diff --git a/toolbox/anatomy/tess_faceconn.m b/toolbox/anatomy/tess_faceconn.m
index 98b999ad5..d22f6d26b 100644
--- a/toolbox/anatomy/tess_faceconn.m
+++ b/toolbox/anatomy/tess_faceconn.m
@@ -1,12 +1,15 @@
-function [VertFacesConn, FaceConn] = tess_faceconn(Faces)
+function [VertFacesConn, FaceConn] = tess_faceconn(Faces, nVert)
% TESS_FACECONN: Computes faces connectivity.
%
-% USAGE: [VertFacesConn, FaceConn] = tess_faceconn(Faces);
+% USAGE: [VertFacesConn, FaceConn] = tess_faceconn(Faces, nVert=1);
%
% INPUT:
% - Faces : Nx3 double matrix
+% - nVert : Number of vertices that a pair of faces need to share to be considered connected.
+% nVert=1 by default, but for finding only edge-adjacent faces, use 2.
% OUTPUT:
-% - FacesConn : sparse matrix [nVertices x nFaces]
+% - VertFacesConn : sparse matrix [nVertices x nFaces]
+% - FacesConn : sparse matrix [nFaces x nFaces]
% @=============================================================================
% This function is part of the Brainstorm software:
@@ -28,6 +31,11 @@
%
% Authors: Anand Joshi, Dimitrios Pantazis, November 2007
% Francois Tadel, 2008-2010
+% Marc Lalancette, 2025
+
+if nargin < 2 || isempty(nVert)
+ nVert = 1;
+end
% Check matrices orientation
if (size(Faces, 2) ~= 3)
@@ -43,7 +51,7 @@
% Build FacesConn
if (nargout > 1)
- FaceConn = (VertFacesConn' * VertFacesConn) > 0;
+ FaceConn = (VertFacesConn' * VertFacesConn) >= nVert;
end
diff --git a/toolbox/anatomy/tess_interp_tess2tess.m b/toolbox/anatomy/tess_interp_tess2tess.m
index 5ac2f6114..04f590af9 100644
--- a/toolbox/anatomy/tess_interp_tess2tess.m
+++ b/toolbox/anatomy/tess_interp_tess2tess.m
@@ -318,24 +318,24 @@
% ===== USE FREESURFER SPHERES =====
% Interpolate using the sphere and the Shepard's algorithm
if isCortexL && isFreeSurfer
- Wmat(sScoutDest.Vertices, sScoutSrc.Vertices) = bst_shepards(vertSphLdest, vertSphLsrc, nbNeighbors, 0);
+ Wmat(sScoutDest.Vertices, sScoutSrc.Vertices) = bst_shepards(vertSphLdest, vertSphLsrc, nbNeighbors, 0, [], isInteractive);
elseif isCortexR && isFreeSurfer
- Wmat(sScoutDest.Vertices, sScoutSrc.Vertices) = bst_shepards(vertSphRdest, vertSphRsrc, nbNeighbors, 0);
+ Wmat(sScoutDest.Vertices, sScoutSrc.Vertices) = bst_shepards(vertSphRdest, vertSphRsrc, nbNeighbors, 0, [], isInteractive);
% ===== USE BRAINSUITE SQUARES =====
% Interpolate using the Brainsuite squares and the Shepard's algorithm
elseif isCortexL && isBrainSuite
% Interpolation: Subject => BrainSuiteAtlas1
- Wsrc2atlas = bst_shepards(vertAtlasLsrc, vertSquareLsrc, nbNeighbors, 0);
+ Wsrc2atlas = bst_shepards(vertAtlasLsrc, vertSquareLsrc, nbNeighbors, 0, [], isInteractive);
% Interpolation: BrainSuiteAtlas1 => Default anatomy
- Watlas2dest = bst_shepards(vertSquareLdest, vertAtlasLdest, nbNeighbors, 0);
+ Watlas2dest = bst_shepards(vertSquareLdest, vertAtlasLdest, nbNeighbors, 0, [], isInteractive);
% Combined: Subject => Default anatomy
Wmat(sScoutDest.Vertices, sScoutSrc.Vertices) = Watlas2dest * Wsrc2atlas;
elseif isCortexR && isBrainSuite
% Interpolation: Subject => BrainSuiteAtlas1
- Wsrc2atlas = bst_shepards(vertAtlasRsrc, vertSquareRsrc, nbNeighbors, 0);
+ Wsrc2atlas = bst_shepards(vertAtlasRsrc, vertSquareRsrc, nbNeighbors, 0, [], isInteractive);
% Interpolation: BrainSuiteAtlas1 => Default anatomy
- Watlas2dest = bst_shepards(vertSquareRdest, vertAtlasRdest, nbNeighbors, 0);
+ Watlas2dest = bst_shepards(vertSquareRdest, vertAtlasRdest, nbNeighbors, 0, [], isInteractive);
% Combined: Subject => Default anatomy
Wmat(sScoutDest.Vertices, sScoutSrc.Vertices) = Watlas2dest * Wsrc2atlas;
@@ -386,7 +386,7 @@
% === INTERPOLATION ===
% Compute Shepard's interpolation
- Wmat(sScoutDest.Vertices, sScoutSrc.Vertices) = bst_shepards(vertDest, vertSrcFit, nbNeighbors, 0);
+ Wmat(sScoutDest.Vertices, sScoutSrc.Vertices) = bst_shepards(vertDest, vertSrcFit, nbNeighbors, 0, [], isInteractive);
% === DISPLAY ALIGNMENT ===
if isInteractive && ~isSameSubject
diff --git a/toolbox/anatomy/tess_isohead.m b/toolbox/anatomy/tess_isohead.m
index ed1c25137..df44a1fd4 100644
--- a/toolbox/anatomy/tess_isohead.m
+++ b/toolbox/anatomy/tess_isohead.m
@@ -1,205 +1,710 @@
-function [HeadFile, iSurface] = tess_isohead(iSubject, nVertices, erodeFactor, fillFactor, Comment)
-% 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)
-% [HeadFile, iSurface] = tess_isohead(MriFile, nVertices=10000, erodeFactor=0, fillFactor=2, Comment)
-% [Vertices, Faces] = tess_isohead(sMri, nVertices=10000, erodeFactor=0, fillFactor=2)
-%
-% If input is loaded MRI structure, no surface file is created and the surface vertices and faces are returned instead.
-
-% @=============================================================================
-% 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: Francois Tadel, 2012-2022
-
-%% ===== PARSE INPUTS =====
-% Initialize returned variables
-HeadFile = [];
-iSurface = [];
-isSave = true;
-% Parse inputs
-if (nargin < 5) || isempty(Comment)
- Comment = [];
-end
-% MriFile instead of subject index
-sMri = [];
-if ischar(iSubject)
- MriFile = iSubject;
- [sSubject, iSubject] = bst_get('MriFile', MriFile);
-elseif isnumeric(iSubject)
- % Get subject
- sSubject = bst_get('Subject', iSubject);
- MriFile = sSubject.Anatomy(sSubject.iAnatomy).FileName;
-elseif isstruct(iSubject)
- sMri = iSubject;
- MriFile = sMri.FileName;
- [sSubject, iSubject] = bst_get('MriFile', MriFile);
- % Don't save a surface file, instead return surface directly.
- isSave = false;
-else
- error('Wrong input type.');
-end
+function [HeadFile, iSurface] = tess_isohead(iSubject, nVertices, erodeFactor, fillFactor, bgLevel, Comment, isGradient, Method)
+ % 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, bgLevel=GuessFromHistorgram, Comment)
+ % [HeadFile, iSurface] = tess_isohead(MriFile, nVertices=10000, erodeFactor=0, fillFactor=2, bgLevel=GuessFromHistorgram, Comment)
+ % [Vertices, Faces] = tess_isohead(sMri, nVertices=10000, erodeFactor=0, fillFactor=2)
+ %
+ % If input is loaded MRI structure, no surface file is created and the surface vertices and faces are returned instead.
-%% ===== LOAD MRI =====
-isProgress = ~bst_progress('isVisible');
-if isempty(sMri)
- % Load MRI
- bst_progress('start', 'Generate head surface', 'Loading MRI...');
- sMri = bst_memory('LoadMri', MriFile);
- if isProgress
- bst_progress('stop');
+ % @=============================================================================
+ % 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: Francois Tadel, 2012-2022
+ % Marc Lalancette, 2022-2025
+
+ % To visualize steps for debugging.
+ isDebugVis = false;
+ nDebugVisSlices = 9; %#ok
+
+ %% ===== PARSE INPUTS =====
+ % Initialize returned variables
+ HeadFile = [];
+ iSurface = [];
+ isSave = true;
+ % Parse inputs
+ if (nargin < 8) || isempty(Method)
+ Method = 'simplify';
+ end
+ if strcmpi(Method, 'simplify')
+ % Check if Lidar Toolbox is installed (requires image processing + computer vision)
+ isLidarToolbox = exist('surfaceMesh', 'file') == 2;
+ if ~isLidarToolbox
+ bst_error('Lidar toolbox required for method ''simplify''.');
+ end
+ end
+ if (nargin < 7) || isempty(isGradient)
+ isGradient = false;
+ end
+ if (nargin < 6)
+ if nargin == 5
+ % Handle legacy call: tess_isohead(iSubject, nVertices, erodeFactor, fillFactor, Comment)
+ if ischar(bgLevel)
+ Comment = bgLevel;
+ bgLevel = [];
+ % Parameter 'bgLevel' is provided: tess_isohead(iSubject, nVertices, erodeFactor, fillFactor, bgLevel)
+ else
+ Comment = [];
+ end
+ % Call tess_isohead(iSubject, nVertices, erodeFactor, fillFactor)
+ else
+ bgLevel = [];
+ Comment = [];
+ end
+ end
+ % MriFile instead of subject index
+ sMri = [];
+ if ischar(iSubject)
+ MriFile = file_short(iSubject);
+ [sSubject, iSubject] = bst_get('MriFile', MriFile);
+ elseif isnumeric(iSubject)
+ % Get subject
+ sSubject = bst_get('Subject', iSubject);
+ MriFile = sSubject.Anatomy(sSubject.iAnatomy).FileName;
+ elseif isstruct(iSubject)
+ sMri = iSubject;
+ MriFile = sMri.FileName;
+ [sSubject, iSubject] = bst_get('MriFile', MriFile);
+ % Don't save a surface file, instead return surface directly.
+ isSave = false;
+ else
+ error('Wrong input type.');
end
-end
-% Save current scouts modifications
-panel_scout('SaveModifications');
-% If subject is using the default anatomy: use the default subject instead
-if sSubject.UseDefaultAnat
- iSubject = 0;
-end
-% Check layers
-if isempty(sSubject.iAnatomy) || isempty(sSubject.Anatomy)
- bst_error('The generate of the head surface requires at least the MRI of the subject.', 'Head surface', 0);
- return
-end
-% Check that everything is there
-if ~isfield(sMri, 'Histogram') || isempty(sMri.Histogram) || isempty(sMri.SCS) || isempty(sMri.SCS.NAS) || isempty(sMri.SCS.LPA) || isempty(sMri.SCS.RPA)
- bst_error('You need to set the fiducial points in the MRI first.', 'Head surface', 0);
- return
-end
-%% ===== 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)});
- % If user cancelled: return
- if isempty(res)
+ %% ===== LOAD MRI =====
+ isProgress = ~bst_progress('isVisible');
+ if isempty(sMri)
+ % Load MRI
+ bst_progress('start', 'Generate head surface', 'Loading MRI...');
+ sMri = bst_memory('LoadMri', MriFile);
+ if isProgress
+ bst_progress('stop');
+ end
+ end
+ % Save current scouts modifications
+ panel_scout('SaveModifications');
+ % If subject is using the default anatomy: use the default subject instead
+ if sSubject.UseDefaultAnat
+ iSubject = 0;
+ end
+ % Check layers
+ if isempty(sSubject.iAnatomy) || isempty(sSubject.Anatomy)
+ bst_error('The generate of the head surface requires at least the MRI of the subject.', 'Head surface', 0);
+ return
+ end
+ % Check that everything is there
+ if ~isfield(sMri, 'Histogram') || isempty(sMri.Histogram) || isempty(sMri.SCS) || isempty(sMri.SCS.NAS) || isempty(sMri.SCS.LPA) || isempty(sMri.SCS.RPA)
+ bst_error('You need to set the fiducial points in the MRI first.', 'Head surface', 0);
return
end
- % Get new values
- nVertices = str2num(res{1});
- erodeFactor = str2num(res{2});
- fillFactor = str2num(res{3});
- bgLevel = str2num(res{4});
- if isempty(bgLevel)
+ % Guess background level
+ if isempty(bgLevel) && ~isGradient
bgLevel = sMri.Histogram.bgLevel;
end
-else
- bgLevel = sMri.Histogram.bgLevel;
-end
-% Check parameters values
-if isempty(nVertices) || (nVertices < 50) || (nVertices ~= round(nVertices)) || isempty(erodeFactor) || ~ismember(erodeFactor,[0,1,2,3]) || isempty(fillFactor) || ~ismember(fillFactor,[0,1,2,3])
- bst_error('Invalid parameters.', 'Head surface', 0);
- return
-end
+ %% ===== 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', [], {'15000', '0', '0', num2str(bgLevel)});
+ % If user cancelled: return
+ if isempty(res)
+ return
+ end
+ % Get new values
+ nVertices = str2double(res{1});
+ erodeFactor = str2double(res{2});
+ fillFactor = str2double(res{3});
+ bgLevel = str2double(res{4});
+ if isempty(bgLevel)
+ bgLevel = sMri.Histogram.bgLevel;
+ end
+ end
+ % Check parameters values
+ if isempty(nVertices) || (nVertices < 50) || (nVertices ~= round(nVertices)) || isempty(erodeFactor) || ~ismember(erodeFactor,[0,1,2,3]) || isempty(fillFactor) || ~ismember(fillFactor,[0,1,2,3])
+ bst_error('Invalid parameters.', 'Head surface', 0);
+ return
+ end
-%% ===== CREATE HEAD MASK =====
-% 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);
-% 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);
-% Erode + dilate, to remove small components
-if (erodeFactor > 0)
- headmask = headmask & ~mri_dilate(~headmask, erodeFactor);
- headmask = mri_dilate(headmask, erodeFactor);
-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);
-
-% view_mri_slices(headmask, 'x', 20)
-
-
-%% ===== CREATE SURFACE =====
-% Compute isosurface
-bst_progress('text', 'Creating isosurface...');
-[sHead.Faces, sHead.Vertices] = mri_isosurface(headmask, 0.5);
-bst_progress('inc', 10);
-% 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));
+
+ %% ===== CREATE HEAD MASK =====
+ % Progress bar
+ bst_progress('start', 'Generate head surface', 'Creating head mask...', 0, 100);
+ % Threshold mri to the level estimated in the histogram
+ 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)); %#ok
+ 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(end,:,:) = 0;
+ headmask(:,1,:) = 0;
+ headmask(:,end,:) = 0;
+ headmask(:,:,1) = 0;
+ headmask(:,:,end) = 0;
+ if isDebugVis
+ view_mri_slices(headmask, 'x', nDebugVisSlices); %#ok<*UNRCH>
+ end
+
+ % Fill neck holes (bones, etc.) where it is cut at edge of volume.
+ bst_progress('text', 'Filling holes and removing disconnected parts...');
+ % Brainstorm reorients MRI so voxels are in RAS. But do all faces in case the bounding box was too
+ % small and another part is cut (e.g. nose).
+
+ % Number of slices to average to smooth out noise in low SNR regions (e.g. around neck and chin).
+ % 4,3 worked ok in noisy scan, but probably best to denoise entire scan first.
+ nSlices = 1; % 1 = no averaging.
+ FillThresh = 1; %min(nSlices, floor(nSlices/2)+1);
+ if FillThresh > nSlices || FillThresh < floor(nSlices/2)
+ error('Bad hard-coded FillThresh.');
+ end
+ for iDim = 1:3
+ % Swap slice dimension into first position. For a single swap, the permutation is it's own inverse.
+ 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. Flip the array to reuse code with same indices.
+ for isFlip = [false, true]
+ if isFlip
+ TempMask = flip(TempMask, 1);
+ end
+ % Skip if just background (e.g. above or behind head)
+ if ~any(any(squeeze(TempMask(2, :, :))))
+ % Flip back and move on
+ if isFlip
+ TempMask = flip(TempMask, 1);
+ end
+ continue;
+ end
+ if isDebugVis
+ figure; imagesc(squeeze(TempMask(2,:,:))); colormap('gray'); axis equal; title(sprintf('Dim %d, Flip %d', iDim, isFlip));
+ end
+ Slice = sum(TempMask(2:2+nSlices-1, :, :), 1);
+ if isDebugVis && nSlices > 1
+ figure; imagesc(squeeze(Slice)); colormap('gray'); axis equal; title(sprintf('Dim %d, Flip %d, Avg %d slices', iDim, isFlip, nSlices));
+ end
+ Slice = Slice >= FillThresh;
+ % Skip if just background (previous check had just some noise)
+ if ~any(any(squeeze(Slice)))
+ if isFlip
+ TempMask = flip(TempMask, 1);
+ end
+ continue;
+ end
+ Slice = FillConcaveVolume(Slice, true); % isClean
+ % Slice = Slice | (Fill(Slice, 2) & Fill(Slice, 3));
+ if isDebugVis
+ figure; imagesc(squeeze(Slice)); colormap('gray'); axis equal; title(sprintf('Dim %d, Flip %d, Filled', iDim, isFlip));
+ end
+ [Slice, isFail] = CenterSpread(Slice);
+ % Avoid warnings for slices other than neck.
+ if isFail && iDim == 3 && isFlip == false % inferior slice
+ warning('CenterSpread failed for filling "neck" slice. Resulting head surface may be problematic.');
+ % Keep original.
+ else
+ % Keep filled in slice.
+ TempMask(2, :, :) = Slice;
+ end
+ if isDebugVis
+ figure; imagesc(squeeze(Slice)); colormap('gray'); axis equal; title(sprintf('Dim %d, Flip %d, Center spread', iDim, isFlip));
+ end
+ % Flip back this dimension
+ if isFlip
+ TempMask = flip(TempMask, 1);
+ end
+ end
+ % Permute back dimensions to original order.
+ headmask = permute(TempMask, Perm);
+ end
+ % Fill holes
+ headmask = FillConcaveVolume(headmask, true); % clean, which may remove a few more original 1-voxel-wide or noise bits
+ if isDebugVis
+ view_mri_slices(headmask, 'x', nDebugVisSlices); title('Filled');
+ end
+ % Keep only central connected volume (trim "beard" or bubbles)
+ headmask = CenterSpread(headmask);
+ bst_progress('inc', 15);
+
+ if isDebugVis
+ view_mri_slices(headmask, 'x', nDebugVisSlices); title('Center spread');
+ end
+
+
+ %% ===== CREATE SURFACE =====
+ % Compute isosurface
+ bst_progress('text', 'Creating isosurface...');
+
+ switch Method
+ case 'iso2mesh'
+ method = 'cgalsurf';
+ opt.radbound = 4; % max radius of the Delaunay sphere - adjust to get desired vertex numbers
+ opt.distbound = 1; % max distance from isosurface
+ dofix = 1; % don't know if needed
+
+ [sHead.Vertices, sHead.Faces, regions, holes] = vol2surf(headmask, ...
+ 1:size(headmask,1), 1:size(headmask,2), 1:size(headmask,3), opt, dofix, method); % ,isovalues
+ if size(regions, 1) > 1
+ bst_error('Multiple regions returned.\n');
+ return;
+ elseif ~isempty(holes)
+ bst_error('Holes present.\n');
+ return;
+ end
+ % Remove region label
+ sHead.Faces(:,4) = [];
+ if isDebugVis
+ fprintf('iso2mesh surface\n');
+ [isOk, Info] = tess_check(sHead.Vertices, sHead.Faces, true, false, true); % verbose, not open, show
+ title('iso2mesh surface', 'color', 'white');
+ end
+ bst_progress('inc', 45);
+
+ case {'reducepatch', 'simplify'}
+ % Could have avoided x-y flip by specifying XYZ in isosurface...
+ [sHead.Faces, sHead.Vertices] = mri_isosurface(headmask, 0.5);
+ % Flip x-y back to our voxel coordinates
+ sHead.Vertices = sHead.Vertices(:, [2, 1, 3]);
+ % Flip to have desired face orientations (seems inconsistent if needed or not).
+ % sHead.Faces = sHead.Faces(:, [2, 1, 3]);
+ if isDebugVis
+ fprintf('mri_isohead surface\n');
+ [isOk, Info] = tess_check(sHead.Vertices, sHead.Faces, true, false, true); % verbose, not open, show
+ title('isosurface', 'color', 'white');
+ end
+ bst_progress('inc', 10);
+
+ % Remove small objects
+ bst_progress('text', 'Removing small patches...');
+ nVertTemp = size(sHead.Vertices, 1);
+ [sHead.Vertices, sHead.Faces] = tess_remove_small(sHead.Vertices, sHead.Faces);
+ if isDebugVis && nVertTemp > size(sHead.Vertices, 1) % only if something was removed in previous step
+ fprintf('BST>Some disconnected small patches removed (%d vertices).\n', nVertTemp - size(sHead.Vertices, 1));
+ [isOk, Info] = tess_check(sHead.Vertices, sHead.Faces, true, false, true); %#ok % verbose, not open, show
+ title('isosurface & small removed', 'color', 'white');
+ end
+ bst_progress('inc', 15);
+
+ % TODO: No existing functions, including from iso2mesh, correctly fix topology issues, which
+ % are present after downsampling with all methods tested (including again iso2mesh).
+ % tess_clean is very strange, it doesn't look at face locations, only the normals. And after
+ % isosurface, many faces are parallel.
+
+ % 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.
+ % 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
+ if isDebugVis
+ fprintf('mildly smoothed isosurface\n');
+ [isOk, Info] = tess_check(sHead.Vertices, sHead.Faces, true, false, true); % verbose, not open, show
+ title('mildly smoother isosurface', 'color', 'white');
+ end
+ bst_progress('inc', 20);
+ end
+
+ % Downsampling surface
+ if (length(sHead.Vertices) > 1.5* nVertices)
+ bst_progress('text', 'Downsampling surface...');
+ % Modified tess_downsize to accept sHead
+ sHead = tess_downsize(sHead, nVertices, Method);
+ if isDebugVis
+ fprintf('reduced surface (%s)\n', Method);
+ [isOk, Info] = tess_check(sHead.Vertices, sHead.Faces, true, false, true); % verbose, not open, show
+ title('reduced', 'color', 'white')
+ end
+ % Fix this patch
+ if ~strcmpi(Method, 'iso2mesh') % I don't think iso2mesh returns multiple disconnected regions.
+ nVertTemp = size(sHead.Vertices, 1);
+ [sHead.Vertices, sHead.Faces] = tess_remove_small(sHead.Vertices, sHead.Faces);
+ if isDebugVis && nVertTemp > size(sHead.Vertices, 1) % only if something was removed in previous step
+ fprintf('BST>Some disconnected small patches removed (%d vertices).\n', nVertTemp - size(sHead.Vertices, 1));
+ fprintf('reduced surface (small disconnected parts removed)\n');
+ [isOk, Info] = tess_check(sHead.Vertices, sHead.Faces, true, false, true); %#ok % verbose, not open, show
+ title('reduced & small removed', 'color', 'white')
+ end
+ end
+ end
+ bst_progress('inc', 15);
+
+ bst_progress('text', 'Smoothing...');
+ sHead.Vertices = SurfaceSmooth(sHead.Vertices, sHead.Faces, 1.5, [], 45, 0, false); % voxel/smoothing size, iterations, freedom (normal), verbose
+ if isDebugVis
+ fprintf('final smoothed surface\n');
+ [isOk, Info] = tess_check(sHead.Vertices, sHead.Faces, true, false, true); % verbose, not open, show
+ end
bst_progress('inc', 10);
+
+ % Convert to SCS
+ sHead.Vertices = cs_convert(sMri, 'voxel', 'scs', sHead.Vertices);
+ % Flip face order to Brainstorm convention
+ sHead.Faces = sHead.Faces(:,[2,1,3]);
+
+
+ %% ===== SAVE FILES =====
+ if isSave
+ bst_progress('text', 'Saving new file...');
+ % Create output filenames
+ ProtocolInfo = bst_get('ProtocolInfo');
+ SurfaceDir = bst_fullfile(ProtocolInfo.SUBJECTS, bst_fileparts(MriFile));
+ HeadFile = file_unique(bst_fullfile(SurfaceDir, 'tess_head_mask.mat'));
+ % Save head
+ if ~isempty(Comment)
+ sHead.Comment = Comment;
+ else
+ sHead.Comment = sprintf('head mask (%d,%d,%d,%d)', nVertices, erodeFactor, fillFactor, round(bgLevel));
+ end
+ sHead = bst_history('add', sHead, 'bem', 'Head surface generated with Brainstorm');
+ bst_save(HeadFile, sHead, 'v7');
+ iSurface = db_add_surface( iSubject, HeadFile, sHead.Comment);
+ else
+ % Return surface
+ HeadFile = sHead.Vertices;
+ iSurface = sHead.Faces;
+ end
+
+ % Close, success
+ if isProgress
+ bst_progress('stop');
+ end
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);
-
-% 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);
+
+%% ===== Subfunctions =====
+function mask = FillConcaveVolume(mask, isClean)
+ % Try to fill the interior of a concave volume. For a head, expects the neck "cut" to be
+ % previously filled. This method still depends on the orientation of the object vs the volume
+ % dimensions (x,y,z axes). But for a head shape, not too important. Mostly noticeable behind
+ % ears or in nostrils for example, depending on their angles.
+
+ if nargin < 2 || isempty(isClean)
+ % Default to not remove any of the original mask. But called with true in this file.
+ isClean = false;
+ end
+
+ % First, fill thin holes with boolean kernel convolution
+ % This fills voxels surrounded by 1s with specific patterns.
+ mask = KernelClean(mask, true);
+
+ % Main filling step
+ % Fill from first to last 1-voxels along each direction and keep intersection across 3 dimensions.
+ % This can result in very narrow deep "tunnels", which we fix with the "surround" fill again next.
+ mask = (Fill(mask, 1, true) & Fill(mask, 2, true) & Fill(mask, 3, true));
+
+ % "Surround" and "sandwich" fills again
+ mask = KernelClean(mask, true);
+ % Repeat for filling intersections. Twice ok for 2d or 3d.
+ mask = KernelClean(mask, true);
+
+ if isClean
+ % Apply inverse "surround" to remove noise and small protrusions.
+ % Erase voxel if surrounded by 0 in a plane. Could do before "first to last" above to clean
+ % noise first, but could also erase more parts in low SNR areas. We later keep only
+ % connected central part so no need to iterate this step.
+ mask = KernelClean(mask, false);
+ end
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);
+
+function mask = KernelClean(mask, Value)
+ % Boolean convolution, with predetermined kernels, to fill in thin strands or cracks.
+ % If Value = false, inverts the mask before and after filling, essentially eroding away thin
+ % structures instead of filling thin holes. Works for 3d volume or 2d slice, with a different
+ % set of kernel patterns for each.
+ if nargin < 2 || isempty(Value)
+ Value = true;
+ end
+ % Flip the mask if we're looking for false.
+ if ~Value
+ mask = ~mask;
+ end
+
+ % Dimensions that have thickness, enough for convolution with 3-wide kernel.
+ isThk = size(mask, [1,2,3]) > 2;
+ nD = sum(isThk);
+ if nD == 1
+ error('Unexpected 1d "volume".');
+ end
+
+ % All kernels should have mirror symmetry on one plane through the middle, since they're only
+ % applied in one orientation for each dimension. For now only 3x3x3 kernels, but should work
+ % with larger "square" ones as well.
+ switch nD
+ case 3
+ % Kernels for 3d
+ % "Surround": to get rid of thin tunnels, "hairs"
+ % 4 adjacent voxels in a plane (not diagonals) are 1s.
+ Kernels{1} = zeros(3,3,3); Kernels{1}(:,:,2) = [0,1,0;1,0,1;0,1,0];
+ % "Sandwich"/"Tie fighter": to remove thin cracks, wedges
+ % both side planes (3x3) are 1s
+ Kernels{2} = ones(3,3,3); Kernels{2}(:,:,2) = zeros(3,3);
+ case 2
+ % Kernels for 2d
+ % "Surround"/"sandwich" (same for 2d)
+ Kernels{1} = zeros(3,3); Kernels{1}(2,:) = [1,0,1];
+ otherwise
+ error('Unexpected multi-dim (>3) mask.');
+ end
+ nK = numel(Kernels);
+
+ switch nD
+ case 3
+ for iK = 1:nK
+ % To avoid cumulative effects that would depend on the order in which we orient the
+ % kernel, we apply all dimensions on the original mask before "adding" them (with "or").
+ FilledMask = mask;
+ for iD = 1:3
+ % Re-orient kernel along each dimension
+ K = permute(Kernels{iK}, circshift(1:3, iD-1));
+ N = sum(K(:));
+ FilledMask = FilledMask | convn(mask, K, 'same') == N;
+ end
+ % Because of the shapes of our kernels, we don't have to enforce keeping "zero" on
+ % our mask volume boundary slices.
+ % mask(2:end-1,2:end-1,2:end-1) = FilledMask(2:end-1,2:end-1,2:end-1);
+ mask = FilledMask;
+ end
+ case 2
+ % Rotate mask to have flat dimension last
+ iD = 1:3;
+ Perm = [iD(isThk), iD(~isThk)]; % This is not always a single swap, so we need the inverse.
+ [~, PermInv] = sort(Perm);
+ mask = permute(mask, Perm);
+ for iK = 1:nK
+ FilledMask = mask;
+ for iD = 1:2
+ % Re-orient kernel along each dimension
+ if iD == 1
+ K = Kernels{iK};
+ else % iD == 2 permute
+ K = Kernels{iK}';
+ end
+ N = sum(K(:));
+ FilledMask = FilledMask | convn(mask, K, 'same') == N;
+ end
+ mask = FilledMask;
+ end
+ mask = permute(mask, PermInv);
+ end
+ % Inverse mask back if needed.
+ if ~Value
+ mask = ~mask;
+ end
+end
+
+
+% function mask = Surrounded(mask, Value)
+% % Find voxels that are surrounded by a value and add them (if Value=true) or remove them (false).
+% % 4 adjacent voxels in a plane (not diagonals) for 3d, 2 adjacent voxels in a line for 2d.
+%
+% if nargin < 2 || isempty(Value)
+% Value = true;
+% end
+% % Flip the mask if we're looking for false.
+% if ~Value
+% mask = ~mask;
+% end
+%
+% % Indices for dimensions excluding ends (2:end-1), except if thin dimension, then it's just 1 or [1 2].
+% nVox = size(mask, [1,2,3]);
+% iVox = {min(2, nVox(1)):max(nVox(1)-1, 1), min(2, nVox(2)):max(nVox(2)-1, 1), min(2, nVox(3)):max(nVox(3)-1, 1)};
+% S = zeros(nVox - (nVox > 2)*2);
+%
+% % Loop so it works on 2d or 3d
+% nDim = 0;
+% for iDim = 1:3
+% if size(mask, iDim) > 2 % Skip singleton dimensions
+% nDim = nDim + 1;
+% switch iDim
+% case 1
+% S = S + (mask(1:end-2,iVox{2},iVox{3}) & mask(3:end,iVox{2},iVox{3}));
+% case 2
+% S = S + (mask(iVox{1},1:end-2,iVox{3}) & mask(iVox{1},3:end,iVox{3}));
+% case 3
+% S = S + (mask(iVox{1},iVox{2},1:end-2) & mask(iVox{1},iVox{2},3:end));
+% end
+% end
+% end
+%
+% % Modify original mask by adding (true) or removing (false)
+% mask(iVox{1},iVox{2},iVox{3}) = mask(iVox{1},iVox{2},iVox{3}) | S >= nDim - 1;
+% if ~Value
+% mask = ~mask;
+% end
% end
-%% ===== SAVE FILES =====
-if isSave
- bst_progress('text', 'Saving new file...');
- % Create output filenames
- ProtocolInfo = bst_get('ProtocolInfo');
- SurfaceDir = bst_fullfile(ProtocolInfo.SUBJECTS, bst_fileparts(MriFile));
- HeadFile = file_unique(bst_fullfile(SurfaceDir, 'tess_head_mask.mat'));
- % Save head
- if ~isempty(Comment)
- sHead.Comment = Comment;
- else
- sHead.Comment = sprintf('head mask (%d,%d,%d,%d)', nVertices, erodeFactor, fillFactor, round(bgLevel));
- end
- sHead = bst_history('add', sHead, 'bem', 'Head surface generated with Brainstorm');
- bst_save(HeadFile, sHead, 'v7');
- iSurface = db_add_surface( iSubject, HeadFile, sHead.Comment);
-else
- % Return surface
- HeadFile = sHead.Vertices;
- iSurface = sHead.Faces;
+function mask = Fill(mask, dim, isFullSingl)
+ % Modified to exclude boundaries, so we can get rid of external junk as well as
+ % internal holes easily.
+ if nargin < 3 || isempty(isFullSingl)
+ % Return original mask for singleton dim by default.
+ isFullSingl = false;
+ end
+
+ % Initialize two accumulators, for the two directions
+ acc1 = mask;
+ acc2 = mask;
+ n = size(mask,dim);
+ % Skip singleton dimensions
+ if n == 1
+ if isFullSingl
+ % Return all true, e.g. to combine with Fill in other directions.
+ mask = true(size(mask));
+ end
+ return;
+ end
+
+ % Process in required direction
+ switch dim
+ case 1
+ for i = 2:n
+ acc1(i,:,:) = acc1(i,:,:) | acc1(i-1,:,:);
+ end
+ for i = n-1:-1:1
+ acc2(i,:,:) = acc2(i,:,:) | acc2(i+1,:,:);
+ end
+ case 2
+ for i = 2:n
+ acc1(:,i,:) = acc1(:,i,:) | acc1(:,i-1,:);
+ end
+ for i = n-1:-1:1
+ acc2(:,i,:) = acc2(:,i,:) | acc2(:,i+1,:);
+ end
+ case 3
+ for i = 2:n
+ acc1(:,:,i) = acc1(:,:,i) | acc1(:,:,i-1);
+ end
+ for i = n-1:-1:1
+ acc2(:,:,i) = acc2(:,:,i) | acc2(:,:,i+1);
+ end
+ end
+ % Combine two accumulators
+ mask = acc1 & acc2;
end
-% Close, success
-if isProgress
- bst_progress('stop');
+
+% function mask = Dilate(mask)
+% % Dilate by 1 voxel in 6 directions, except at volume edges
+% % Indices for dimensions excluding ends (2:end-1), except if thin dimension, then it's just 1 or [1 2].
+% nVox = size(mask, [1,2,3]);
+% iVox = {min(2, nVox(1)):max(nVox(1)-1, 1), min(2, nVox(2)):max(nVox(2)-1, 1), min(2, nVox(3)):max(nVox(3)-1, 1)};
+%
+% % Loop so it works on 2d or 3d
+% for iDim = 1:3
+% if nVox(iDim) > 2 % Skip thin dimensions (size 1 or 2)
+% switch iDim
+% case 1
+% DilateMask = mask(1:end-2,iVox{2},iVox{3}) | mask(3:end,iVox{2},iVox{3});
+% case 2
+% DilateMask = mask(iVox{1},1:end-2,iVox{3}) | mask(iVox{1},3:end,iVox{3});
+% case 3
+% DilateMask = mask(iVox{1},iVox{2},1:end-2) | mask(iVox{1},iVox{2},3:end);
+% end
+% mask(iVox{1},iVox{2},iVox{3}) = mask(iVox{1},iVox{2},iVox{3}) | DilateMask;
+% end
+% end
+% end
+
+
+function [OutMask, isFail] = CenterSpread(InMask)
+ % Similar to Fill, but from a central starting point and intersecting with the input "reference" mask.
+ % This should work on slices as well as volumes.
+ isFail = false;
+ OutMask = false(size(InMask));
+ iStart = max(1,round(size(OutMask)/2));
+ nVox = size(OutMask);
+ % Force starting center point to be 1, and spread from there. But this will still fail if it's fully
+ % surrounded by 0s.
+ 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);
+ % Instead, propagate as far as possible in each direction (3 dim, forward & back) at each step
+ % of the main loop.
+ 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
+ if nOut == 1
+ % Remove "forced" initial vertex, everything else is now gone.
+ OutMask(iStart(1), iStart(2), iStart(3)) = false;
+ isFail = true;
+ % warning('CenterSpread failed: starting center point is not part of the mask.');
+ 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/anatomy/tess_isosurface.m b/toolbox/anatomy/tess_isosurface.m
new file mode 100644
index 000000000..03eef30e3
--- /dev/null
+++ b/toolbox/anatomy/tess_isosurface.m
@@ -0,0 +1,188 @@
+function [MeshFile, iSurface] = tess_isosurface(iSubject, isoValue, Comment)
+% TESS_ISOSURFACE: Reconstruct a thresholded surface mesh from a CT
+%
+% USAGE: [MeshFile, iSurface] = tess_isosurface(iSubject, isoValue, Comment)
+% [MeshFile, iSurface] = tess_isosurface(iSubject)
+% [MeshFile, iSurface] = tess_isosurface(CtFile, isoValue, Comment)
+% [MeshFile, iSurface] = tess_isosurface(CtFile)
+% [Vertices, Faces] = tess_isosurface(sMri, isoValue)
+% [Vertices, Faces] = tess_isosurface(sMri)
+%
+% INPUT:
+% - iSubject : Indice of the subject where to add the surface
+% - isoValue : The value in Housefield Unit to set for thresholding the CT. If this parameter is empty, then a GUI pops up asking the user for the desired value
+% - Comment : Surface description
+% OUTPUT:
+% - MeshFile : indice of the surface that was created in the sSubject structure
+% - iSurface : indice of the surface that was created in the sSubject structure
+% - Vertices : The vertices of the mesh
+% - Faces : The faces of the mesh
+%
+% If input is loaded CT structure, no surface file is created and the surface vertices and faces are returned instead.
+%
+% @=============================================================================
+% 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.
+% =============================================================================@
+%
+% Inspired by tess_isohead.m
+%
+% Authors: Chinmay Chinara, 2023-2024
+
+%% ===== PARSE INPUTS =====
+% Initialize returned variables
+MeshFile = [];
+iSurface = [];
+isSave = true;
+
+% Parse inputs
+if (nargin < 3) || isempty(Comment)
+ Comment = [];
+end
+% CtFile instead of subject index
+sMri = [];
+if ischar(iSubject)
+ CtFile = iSubject;
+ [sSubject, iSubject] = bst_get('MriFile', CtFile);
+elseif isnumeric(iSubject)
+ % Get subject
+ sSubject = bst_get('Subject', iSubject);
+ CtFile = sSubject.Anatomy(sSubject.iAnatomy).FileName;
+elseif isstruct(iSubject)
+ sMri = iSubject;
+ CtFile = sMri.FileName;
+ [sSubject, iSubject] = bst_get('MriFile', CtFile);
+ % Don't save a surface file, instead return surface directly.
+ isSave = false;
+else
+ error('Wrong input type.');
+end
+
+%% ===== LOAD CT =====
+isProgress = ~bst_progress('isVisible');
+if isempty(sMri)
+ % Load CT
+ bst_progress('start', 'Generate thresholded isosurface from CT', 'Loading CT...');
+ sMri = bst_memory('LoadMri', CtFile);
+ if isProgress
+ bst_progress('stop');
+ end
+end
+% Save current scouts modifications
+panel_scout('SaveModifications');
+% If subject is using the default anatomy: use the default subject instead
+if sSubject.UseDefaultAnat
+ iSubject = 0;
+end
+% Check layers
+if isempty(sSubject.iAnatomy) || isempty(sSubject.Anatomy)
+ bst_error('The surface generation requires at least the CT of the subject.', 'Generate isosurface', 0);
+ return
+end
+% Check that everything is there
+if ~isfield(sMri, 'Histogram') || isempty(sMri.Histogram) || isempty(sMri.SCS) || isempty(sMri.SCS.NAS) || isempty(sMri.SCS.LPA) || isempty(sMri.SCS.RPA)
+ bst_error('You need to set the fiducial points in the MRI first.', 'Generate isosurface', 0);
+ return
+end
+
+%% ===== ASK PARAMETERS =====
+% Ask user to set the parameters if they are not set
+if (nargin < 2) || isempty(isoValue)
+ res = java_dialog('input', ['Background level guessed from MRI histogram (HU): ', num2str(round(sMri.Histogram.bgLevel)), ...
+ ' White level guessed from MRI histogram (HU): ', num2str(round(sMri.Histogram.whiteLevel)), ...
+ ' Max intensity level guessed from MRI histogram (HU): ', num2str(round(sMri.Histogram.intensityMax)), ...
+ '
Set isoValue for thresholding (HU):' ...
+ ' (estimate below is mean of whitelevel and max intensity)'], ...
+ 'Generate isosurface', [], num2str(round((sMri.Histogram.whiteLevel+sMri.Histogram.intensityMax)/2)));
+
+ % If user cancelled: return
+ if isempty(res)
+ return
+ end
+ % Get new value isoValue
+ isoValue = round(str2double(res));
+end
+
+% Check parameters values
+% isoValue cannot be < 0 as there cannot be negative intensity in the CT
+% isoValue=0 does not makes sense as it means we do not want to do any thresholding
+% isoValue cannot be > the maximum intensity of the CT as it means there is nothing to generate or threshold on
+if isempty(isoValue) || isoValue <= 0 || isoValue > round(sMri.Histogram.intensityMax)
+ bst_error('Invalid ''isoValue''. Enter proper values.', 'Mesh surface', 0);
+ return
+end
+
+
+%% ===== CREATE SURFACE =====
+% Compute isosurface
+bst_progress('start', 'Generate thresholded isosurface from CT', 'Creating isosurface...');
+[sMesh.Faces, sMesh.Vertices] = mri_isosurface(sMri.Cube, isoValue);
+bst_progress('inc', 10);
+% Downsample to a maximum number of vertices
+maxIsoVert = 60000;
+if (length(sMesh.Vertices) > maxIsoVert)
+ bst_progress('text', 'Downsampling isosurface...');
+ [sMesh.Faces, sMesh.Vertices] = reducepatch(sMesh.Faces, sMesh.Vertices, maxIsoVert./length(sMesh.Vertices));
+ bst_progress('inc', 10);
+end
+
+% Convert to millimeters
+sMesh.Vertices = sMesh.Vertices(:,[2,1,3]);
+sMesh.Faces = sMesh.Faces(:,[2,1,3]);
+sMesh.Vertices = bst_bsxfun(@times, sMesh.Vertices, sMri.Voxsize);
+% Convert to SCS
+sMesh.Vertices = cs_convert(sMri, 'mri', 'scs', sMesh.Vertices ./ 1000);
+
+%% ===== SAVE FILES =====
+if isSave
+ bst_progress('text', 'Saving new file...');
+ % Create output filenames
+ ProtocolInfo = bst_get('ProtocolInfo');
+ SurfaceDir = bst_fullfile(ProtocolInfo.SUBJECTS, bst_fileparts(CtFile));
+ % Get the mesh file
+ MeshFile = bst_fullfile(SurfaceDir, 'tess_isosurface.mat');
+
+ % Replace existing isoSurface surface (tess_isosurface.mat)
+ [sSubjectTmp, iSubjectTmp, iSurfaceTmp] = bst_get('SurfaceFile', MeshFile);
+ if ~isempty(iSurfaceTmp)
+ file_delete(file_fullpath(MeshFile), 1);
+ sSubjectTmp.Surface(iSurfaceTmp) = [];
+ bst_set('Subject', iSubjectTmp, sSubjectTmp);
+ end
+
+ % Save isosurface
+ sMesh.Comment = sprintf('isoSurface (ISO_%d)', isoValue);
+ sMesh = bst_history('add', sMesh, 'threshold_ct', ['Thresholded CT: ' sMri.FileName ' threshold = ' num2str(isoValue)]);
+ bst_save(MeshFile, sMesh, 'v7');
+ iSurface = db_add_surface(iSubject, MeshFile, sMesh.Comment);
+ % Display mesh with 3D orthogonal slices of the default MRI
+ MriFile = sSubject.Anatomy(1).FileName;
+ hFig = bst_figures('GetFiguresByType', '3DViz');
+ if isempty(hFig)
+ hFig = view_mri_3d(MriFile, [], 0.3, []);
+ end
+ view_surface(MeshFile, 0.6, [], hFig, []);
+ panel_surface('SetIsoValue', isoValue);
+else
+ % Return surface
+ MeshFile = sMesh.Vertices;
+ iSurface = sMesh.Faces;
+end
+
+% Close, success
+if isProgress
+ bst_progress('stop');
+end
\ No newline at end of file
diff --git a/toolbox/anatomy/tess_smooth_sources.m b/toolbox/anatomy/tess_smooth_sources.m
index 3342bdf66..d025261fb 100644
--- a/toolbox/anatomy/tess_smooth_sources.m
+++ b/toolbox/anatomy/tess_smooth_sources.m
@@ -1,21 +1,16 @@
-function W = tess_smooth_sources(Vertices, Faces, VertConn, FWHM, Method)
+function W = tess_smooth_sources(SurfaceMat, FWHM, Method)
% TESS_SMOOTH_SOURCES: Gaussian smoothing matrix over a mesh.
%
-% USAGE: W = tess_smooth_sources(Vertices, Faces, VertConn=[], FWHM=0.010, Method='average')
+% USAGE: W = tess_smooth_sources(SurfaceMat, FWHM=0.010, Method='geodesic_dist')
%
% INPUT:
-% - Vertices : Vertices positions ([X(:) Y(:) Z(:)])
-% - Faces : Triangles matrix
-% - VertConn : Vertices connectivity, logical sparse matrix [Nvert,Nvert]
-% - FWHM : Full width at half maximum, in meters (default=0.010)
-% - Method : {'euclidian', 'path', 'average', 'surface'}
+% - SurfaceMat : Cortical surface matrix
+% - FWHM : Full Width at Half Maximum, in m (default = 0.010m = 10mm)
+% - Method : {'euclidian', 'geodesic_edge', 'geodesic_dist' (default)}
% OUPUT:
-% - W: smoothing matrix (sparse)
+% - W : Smoothing matrix (sparse)
%
% DESCRIPTION:
-% - The distance between two points is an average of:
-% - the direct euclidian between the two points and
-% - the number of edges between the two points * the average length of an edge
% - Gaussian smoothing function on the euclidian distance:
% f(r) = 1 / sqrt(2*pi*sigma^2) * exp(-(r.^2/(2*sigma^2)))
% - Full Width at Half Maximum (FWHM) is related to sigma by:
@@ -40,159 +35,47 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2010-2013
+% Edouard Delaire, 2023
% ===== PARSE INPUTS =====
-if (nargin < 5) || isempty(Method)
- Method = 'average';
+if (nargin < 3) || isempty(Method)
+ Method = 'geodesic_dist';
end
-if (nargin < 4) || isempty(FWHM)
+if (nargin < 2) || isempty(FWHM)
FWHM = 0.010;
end
-if (nargin < 3) || isempty(VertConn)
- VertConn = tess_vertconn(Vertices, Faces);
-end
-if ~islogical(VertConn)
- error('Invalid vertices connectivity matrix.');
-end
-nv = size(Vertices,1);
-
-
-% ===== ANALYZE INPUT =====
-% Calculate Gaussian kernel properties
-Sigma = FWHM / (2 * sqrt(2*log2(2)));
-% FWTM = 2 * sqrt(2*log2(10)) * Sigma;
-% Get the average edge length
-[vi,vj] = find(VertConn);
-meanDist = mean(sqrt((Vertices(vi,1) - Vertices(vj,1)).^2 + (Vertices(vi,2) - Vertices(vj,2)).^2 + (Vertices(vi,3) - Vertices(vj,3)).^2));
-% Guess the number of iterations
-nIter = min(10, ceil(FWHM / meanDist));
-
-% ===== COMPUTE DISTANCE =====
-switch lower(Method)
- % === METHOD 1: USE EUCLIDIAN DISTANCE ===
- case 'euclidian'
- % Get the neighborhood around each vertex
- VertConn = mpower(VertConn, nIter);
- [vi,vj] = find(VertConn);
- % Use Euclidean distance
- d = sqrt((Vertices(vi,1) - Vertices(vj,1)).^2 + (Vertices(vi,2) - Vertices(vj,2)).^2 + (Vertices(vi,3) - Vertices(vj,3)).^2);
- Dist = sparse(vi, vj, d, nv, nv);
- % === METHOD 2: USE NUMBER OF CONNECTIONS =====
- % === METHOD 3: AVERAGE METHOD 1+2 ===
- case {'path', 'average'}
- % Initialize loop variables
- VertConnGrow = speye(nv);
- VertIter = sparse(nv,nv);
- vall = [];
+Method = lower(Method);
+Vertices = SurfaceMat.Vertices;
+VertConn = SurfaceMat.VertConn;
+Faces = SurfaceMat.Faces;
+Dist = SurfaceMat.VertDist;
+nVertices = size(Vertices,1);
- % Grow and keep track of the layers
- for iter = 1:nIter
- disp(sprintf('SMOOTH> Iteration %d/%d', iter, nIter));
- % Grow selection of vertices
- VertConnPrev = VertConnGrow;
- VertConnGrow = double(VertConnGrow * VertConn > 0);
- % Find all the new connections
- vind = find(VertConnGrow - VertConnPrev > 0);
- [vi,vj] = ind2sub([nv,nv], vind);
- VertIter = VertIter + iter * sparse(vi, vj, ones(size(vi)), nv, nv);
- end
- % Use distance in number of connections
- Dist = VertIter * meanDist;
- Dist(1:nv+1:nv*nv) = 0;
-
- % == AVERAGE WITH METHOD 1 ==
- if strcmpi(Method, 'average')
- % Calculate Euclidean distance
- [vi,vj] = find(VertConnGrow);
- d = sqrt((Vertices(vi,1) - Vertices(vj,1)).^2 + (Vertices(vi,2) - Vertices(vj,2)).^2 + (Vertices(vi,3) - Vertices(vj,3)).^2);
- % Average with results of method #2
- Dist = (0.5 .* Dist + 0.5 .* sparse(vi, vj, d, nv, nv));
- end
-
- % ===== METHOD 4: CALCULATE SURFACE DISTANCE =====
- % WARNING: NOT FINISHED!!!!
- case 'surface'
- % Initialize loop variables
- VertConnGrow = speye(nv);
- Dist = sparse([], [], [], nv, nv, 3*nnz(VertConn));
- vall = [];
- nIter = 2;
- % Grow until we reach an accepteable distance
- for iter = 1:nIter
- disp(sprintf('Iteration %d', iter));
- % Get neighbors
- VertConnGrow = VertConnGrow * VertConn;
- % Get all the existing edges in the surface
- vind = find(VertConnGrow);
- % Remove all the previously processed connections
- vind = setdiff(vind, vall);
- [vi,vj] = ind2sub([nv,nv], vind);
- % Remove diagonal
- iDel = (vi == vj);
- vi(iDel) = [];
- vj(iDel) = [];
- % Calculate the distance to the neighbor nodes
- if (iter == 1)
- % Calculate all the distances for all the pairs of edges
- d = sqrt((Vertices(vi,1) - Vertices(vj,1)).^2 + (Vertices(vi,2) - Vertices(vj,2)).^2 + (Vertices(vi,3) - Vertices(vj,3)).^2);
- else
- % Initialize d matrix
- d = zeros(size(vi));
- % Process each new connection separately
- for i = 1:length(vi)
- % Find nodes that are connected to both nodes
- iMid = find((Dist(vi(i),:) & VertConn(vj(i),:)) | (VertConn(vi(i),:) & Dist(vj(i),:)));
- % Find nodes for which we know one connection at least
- iMid0 = (Dist(vi(i),iMid) & Dist(vj(i),iMid));
- iMid1 = (Dist(vi(i),iMid) & ~iMid0);
- iMid2 = (Dist(vj(i),iMid) & ~iMid0);
- % Compute distances
- dMid = 0*iMid;
- dMid(iMid0) = Dist(vi(i),iMid0) + Dist(vj(i),iMid0);
- dMid(iMid1) = Dist(vi(i),iMid1) + sqrt((Vertices(vj(i),1) - Vertices(iMid1,1)).^2 + (Vertices(vj(i),2) - Vertices(iMid1,2)).^2 + (Vertices(vj(i),3) - Vertices(iMid1,3)).^2)';
- dMid(iMid2) = Dist(vj(i),iMid2) + sqrt((Vertices(vi(i),1) - Vertices(iMid2,1)).^2 + (Vertices(vi(i),2) - Vertices(iMid2,2)).^2 + (Vertices(vi(i),3) - Vertices(iMid2,3)).^2)';
- dMid(dMid == 0) = Inf;
- % Find the shortest path
- d(i) = min(dMid);
- if isinf(d(i))
- error('???');
- end
- end
- end
- % Add to processed vertices
- vall = union(vind, vall);
- % Create a sparse distance matrix
- Dist = Dist + sparse(vi, vj, d, nv, nv);
- end
+% Calculate Gaussian kernel properties
+if strcmp(Method,'geodesic_edge') % Sigma given in (integer) number of edges
+ [vi, vj] = find(VertConn);
+ meanDist = mean(sqrt((Vertices(vi,1) - Vertices(vj,1)).^2 + (Vertices(vi,2) - Vertices(vj,2)).^2 + (Vertices(vi,3) - Vertices(vj,3)).^2));
+ Sigma = ceil(ceil(FWHM./ meanDist) / (2 * sqrt(2*log2(2))));
+else % Sigma given in meters
+ Sigma = FWHM / (2 * sqrt(2*log2(2)));
end
-
-% ===== APPLY GAUSSIAN FUNCTION =====
-% Gaussian function
-fun = inline('1 / sqrt(2*pi*sigma2) * exp(-(x.^2/(2*sigma2)))', 'x', 'sigma2');
+% Ignore long distances
+Dist(Dist > 10 * Sigma) = 0;
% Calculate interpolation as a function of distance
-[vi,vj] = find(Dist>0);
-vind = sub2ind([nv,nv], vi, vj);
-w = fun(Dist(vind), Sigma^2);
-% Build final symmetric matrix
-%W = sparse([vi;vj], [vj;vi], [w;w], nv, nv);
-W = sparse(vi, vj, w, nv, nv);
-% Add the diagonal
-W = W + fun(0,Sigma^2) * speye(nv);
+[vi, vj, x] = find(Dist);
+W = sparse(vi, vj, GaussianKernel(x,Sigma^2), nVertices, nVertices) + ...
+ speye (nVertices) .* GaussianKernel(0,Sigma^2);
% Normalize columns
-W = bst_bsxfun(@rdivide, W, sum(W,1));
-% Remove insignificant values
-[vi,vj] = find(W>0.005);
-vind = sub2ind([nv,nv], vi, vj);
-W = sparse(vi, vj, W(vind), nv, nv);
-
+W = bst_bsxfun(@rdivide, W, sum(W,1));
% ===== FIX BAD TRIANGLES =====
% Only for methods including neighbor distance
-if ismember(lower(Method), {'path', 'average'})
+% Todo: check what this is doing :)
+if contains(Method, 'geodesic')
% Configurations to detect:
% - One face divided in 3 with a point in the middle of the face
% - Square divided into 4 triangles with one point in the middle
@@ -208,5 +91,11 @@
W(:,iVert) = AvgConn;
end
-
+% ===== APPLY GAUSSIAN FUNCTION =====
+% Gaussian function
+function y = GaussianKernel(x,sigma2)
+ y = 1 / sqrt(2*pi*sigma2);
+ y = y .* exp(-(x.^2/(2*sigma2)));
+end
+end
diff --git a/toolbox/connectivity/bst_connectivity.m b/toolbox/connectivity/bst_connectivity.m
index 8b079b680..a977d9fde 100644
--- a/toolbox/connectivity/bst_connectivity.m
+++ b/toolbox/connectivity/bst_connectivity.m
@@ -153,9 +153,10 @@
% Load kernel-based results as kernel+data for coherence and phase metrics only.
% This is always for 1xN, i.e. the B side has all sources and the A side has only one signal.
% Kernel on the A side is not implemented.
+methodsKernelBased = {'plv', 'ciplv', 'wpli', 'dwpli', 'pli', 'cohere'};
LoadOptionsA.LoadFull = 1; % ~isempty(OPTIONS.TargetA) || ~ismember(OPTIONS.Method, {'cohere','plv','ciplv','wpli'});
LoadOptionsB = LoadOptionsA;
-LoadOptionsB.LoadFull = ~isempty(OPTIONS.TargetB) || ~ismember(OPTIONS.Method, {'cohere','plv','ciplv','wpli'});
+LoadOptionsB.LoadFull = ~isempty(OPTIONS.TargetB) || ~ismember(OPTIONS.Method, methodsKernelBased);
% Use the signal processing toolbox?
if bst_get('UseSigProcToolbox')
hilbert_fcn = @hilbert;
@@ -404,7 +405,7 @@
nWinLenSamples = [];
% Loop over input files
-for iFile = 1:nFiles
+for iFile = 1 : length(FilesA)
% Increments here, and in LoadAll above. 100 points are assigned per process (in bst_process('run'))
bst_progress('set', round(startValue + (iFile-1) / nFiles * 100));
%% ===== LOAD SIGNALS =====
@@ -472,6 +473,10 @@
sfreq = round(sfreq * 1e6) * 1e-6;
nA = size(sInputA.Data,1);
nB = size(sInputB.Data,1);
+ % Number of sources if B is kernel-based
+ if ~isempty(sInputB.ImagingKernel) && ismember(OPTIONS.Method, methodsKernelBased)
+ nB = size(sInputB.ImagingKernel, 1);
+ end
% ===== CHECK UNCONSTRAINED SOURCES =====
% Unconstrained models?
@@ -541,8 +546,35 @@
DisplayUnits = 'Correlation';
bst_progress('text', sprintf('Calculating: Correlation [%dx%d]...', nA, nB));
Comment = 'Corr';
- % All the correlations with one call
- R = bst_corrn(sInputA.Data, sInputB.Data, OPTIONS.RemoveMean);
+ % Verify WinLen argument for windowed metric
+ if strcmpi(OPTIONS.TimeRes, 'windowed')
+ % Window length and overlap in samples
+ nWinLenSamples = round(OPTIONS.WinLen * sfreq);
+ nWinOvelapSamples = round(OPTIONS.WinOverlap * nWinLenSamples);
+ if nWinLenSamples >= nTime
+ Message = 'File time duration too short wrt requested window length. Only computing one estimate across all time.';
+ bst_report('Warning', OPTIONS.ProcessName, unique({FilesA{iFile}, FilesB{iFile}}), Message);
+ % Avoid further checks and error messages.
+ OPTIONS.TimeRes = 'none';
+ end
+ end
+ % Compute correlation
+ if strcmpi(OPTIONS.TimeRes, 'windowed')
+ Comment = [Comment '-time'];
+ % Get [start, end] indices for windows
+ [~, ixs] = bst_epoching(1 : length(sInputA.Time), nWinLenSamples, nWinOvelapSamples);
+ nTimeOut = size(ixs,1);
+ % Center of the time window (sample 1 = 0 s)
+ Time = reshape((mean(ixs, 2)-1) ./ sfreq, 1, []);
+ % Initialize R
+ R = zeros(nA, nB, nTimeOut);
+ for iWin = 1 : size(ixs, 1)
+ R(:,:,iWin) = bst_corrn(sInputA.Data(:, ixs(iWin,1) : ixs(iWin,2)), sInputB.Data(:, ixs(iWin,1): ixs(iWin,2)), OPTIONS.RemoveMean);
+ end
+ else
+ % All the correlations with one call
+ R = bst_corrn(sInputA.Data, sInputB.Data, OPTIONS.RemoveMean);
+ end
% ==== GRANGER ====
case 'granger'
@@ -838,6 +870,10 @@
HB = morlet_transform(sInputB.Data, sInputB.Time, OPTIONS.Freqs(iBand), OPTIONS.MorletFc, OPTIONS.MorletFwhmTc, 'n');
end
end
+ % Apply kernel if needed
+ if ~isConnNN && ~isempty(sInputB.ImagingKernel)
+ HB = sInputB.ImagingKernel * HB;
+ end
% PLV: Normalize first, keep only phase info.
if ismember(OPTIONS.Method, {'plv', 'ciplv'})
HA = HA ./ abs(HA);
@@ -923,6 +959,14 @@
end
% Add the number of averaged windows & files to the report
nWinLenSamples = nWinLenAvg;
+ elseif strcmp(OPTIONS.TimeRes, 'none')
+ % Add time dimension
+ for f = 1:numel(Terms)
+ % Insert a singleton second-to-last dimension
+ order = 1 : (length(size(S.(Terms{f}))) + 1);
+ newOrder = [order(1:end-2), order(end:-1:end-1)];
+ S.(Terms{f}) = permute(S.(Terms{f}), newOrder);
+ end
end
% Initial R or Accumulate R
if isempty(R) || strcmpi(OPTIONS.OutputMode, 'input')
@@ -932,12 +976,15 @@
end
nWin = 0;
% Add the number of averaged windows & files to the report (only once per output file)
- if TimeRes == 0
- nAvgLen = nWinFile;
- else
- nAvgLen = nWinLenAvg;
+ switch OPTIONS.TimeRes
+ case 'full'
+ nAvgLen = 1;
+ case 'windowed'
+ nAvgLen = nWinLenAvg;
+ case 'none'
+ nAvgLen = nWinFile;
end
- Message = sprintf('Estimating across %d windows of %d samples each', nAvgLen, round(OPTIONS.WinLen * sfreq));
+ Message = sprintf('Estimating across %d windows of %d samples each', nAvgLen, round(OPTIONS.StftWinLen * sfreq));
if ~strcmpi(OPTIONS.OutputMode, 'input') && nFiles > 1
Message = [Message sprintf(' per file, across %d files', nFiles)];
end
@@ -1054,7 +1101,7 @@
end
OutputFiles{iFile} = Finalize(OrigFilesB{iFile});
R = [];
- else
+ elseif strcmpi(OPTIONS.OutputMode, 'avg')
% Sum terms and continue file loop.
if isnumeric(R)
if isempty(Ravg)
@@ -1067,7 +1114,8 @@
end
% Else R is a struct and terms are already being summed into its fields directly.
end
-
+ else % case 'concat'
+ Ravg = R;
end
end
@@ -1100,11 +1148,11 @@
end
+%% ===== ASSEMBLE CONNECTIVITY METRIC FROM ACCUMULATED TERMS =====
function NewFile = Finalize(DataFile)
if nargin < 1
DataFile = [];
end
- %% ===== ASSEMBLE CONNECTIVITY METRIC FROM ACCUMULATED TERMS =====
if isstruct(R)
switch OPTIONS.Method
case 'plv'
@@ -1152,11 +1200,6 @@
R(isnan(R(:))) = 0;
end
end
- % Static measures may need to be reshaped to add singleton time dimension.
- if ndims(R) == 3
- % Push freq to 4th dim.
- R = permute(R, [1,2,4,3]);
- end
end
%% ===== APPLY FINAL MEASURE =====
diff --git a/toolbox/connectivity/bst_xspectrum.m b/toolbox/connectivity/bst_xspectrum.m
index db71b010c..3a26a3577 100644
--- a/toolbox/connectivity/bst_xspectrum.m
+++ b/toolbox/connectivity/bst_xspectrum.m
@@ -44,12 +44,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
diff --git a/toolbox/connectivity/private/direct_pac_mex.mexmaca64 b/toolbox/connectivity/private/direct_pac_mex.mexmaca64
new file mode 100755
index 000000000..4688651ec
Binary files /dev/null and b/toolbox/connectivity/private/direct_pac_mex.mexmaca64 differ
diff --git a/toolbox/connectivity/private/direct_pac_mex.mexmaci64 b/toolbox/connectivity/private/direct_pac_mex.mexmaci64
old mode 100644
new mode 100755
index adc2ec53d..3071d68df
Binary files a/toolbox/connectivity/private/direct_pac_mex.mexmaci64 and b/toolbox/connectivity/private/direct_pac_mex.mexmaci64 differ
diff --git a/toolbox/core/bst_colormaps.m b/toolbox/core/bst_colormaps.m
index 13fb57e56..4a7917e69 100644
--- a/toolbox/core/bst_colormaps.m
+++ b/toolbox/core/bst_colormaps.m
@@ -376,18 +376,30 @@ function SetMaxCustom(ColormapType, DisplayUnits, newMin, newMax)
case {'3DViz', 'MriViewer'}
% Get surfaces defined in this figure
TessInfo = getappdata(sFigure.hFigure, 'Surface');
- % Find 1st surface that match this ColormapType
- iTess = find(strcmpi({TessInfo.ColormapType}, ColormapType), 1);
+ % Find surfaces that match this ColormapType
+ iSurfaces = find(strcmpi({TessInfo.ColormapType}, ColormapType));
DataFig = [];
- if ~isempty(iTess) && ~isempty(TessInfo(iTess).DataSource.Type)
- DataFig = TessInfo(iTess).DataMinMax;
- DataType = TessInfo(iTess).DataSource.Type;
- % 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';
+ for i = 1:length(iSurfaces)
+ iTess = iSurfaces(i);
+ 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;
+ % 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')
+ if ~isempty(strfind(lower(TessInfo(iTess).DataSource.FileName), 'sloreta'))
+ DataType = 'sLORETA';
+ else
+ [~, iResult] = bst_memory('LoadResultsFile', TessInfo(iTess).DataSource.FileName, 0);
+ if ~isempty(strfind(lower(GlobalData.DataSet(iDS).Results(iResult).Function), 'sloreta'));
+ DataType = 'sLORETA';
+ end
+ end
+ end
end
end
if isempty(DataFig)
@@ -1329,7 +1341,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 = [];
@@ -1340,7 +1352,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);
@@ -1442,7 +1454,10 @@ function ConfigureColorbar(hFig, ColormapType, DataType, DisplayUnits) %#ok= 706)
+ % [RamTotal_MiB, RamAvailable_MiB] = bst_get('SystemMemory')
+ RamTotal_MiB = [];
+ RamAvailable_MiB = [];
+ tmp = regexp(bst_get('OsType'), '^[a-z]+', 'match', 'ignorecase');
+ if ~isempty(tmp)
+ osFamily = tmp{1};
+ end
+ if strcmpi(osFamily, 'win') && (bst_get('MatlabVersion') >= 706)
try
% Get memory info
- usermem = memory();
- maxvar = round(usermem.MaxPossibleArrayBytes / 1024 / 1024);
- totalmem = round(usermem.MemAvailableAllArrays / 1024 / 1024);
+ [usermem, systemmem] = memory();
+ RamTotal_MiB = round(systemmem.PhysicalMemory.Total / 1024 / 1024);
+ RamAvailable_MiB = round(usermem.MemAvailableAllArrays / 1024 / 1024);
+ catch
+ % Whatever...
+ end
+
+ elseif strcmpi(osFamily, 'linux')
+ try
+ meminfoRes = fileread('/proc/meminfo');
+ ramTotalkB = regexp(meminfoRes, '(?<=MemTotal:)(.*?)(?=kB)', 'match');
+ ramAvailablekB = regexp(meminfoRes, '(?<=MemAvailable:)(.*?)(?=kB)', 'match');
+ if ~isempty(ramAvailablekB) && ~isempty(ramTotalkB)
+ ramTotalkB = str2double(strtrim(ramTotalkB{1}));
+ RamTotal_MiB = round(ramTotalkB /1024);
+ ramAvailablekB = str2double(strtrim(ramAvailablekB{1}));
+ RamAvailable_MiB = round(ramAvailablekB /1024);
+ end
+ catch
+ % Whatever...
+ end
+ elseif strcmpi(osFamily, 'mac')
+ try
+ [~, mem_pressure] = system('memory_pressure');
+ if ~isempty(mem_pressure)
+ ramTotalB = regexp(mem_pressure, '(?<=The system has)(.*?)(?= )', 'match');
+ prcFree = regexp(mem_pressure, '(?<=System-wide memory free percentage:)(.*?)(?=%)', 'match');
+ if ~isempty(ramTotalB) && ~isempty(prcFree)
+ ramTotalB = str2double(ramTotalB{1});
+ RamTotal_MiB = round(ramTotalB / 1024 / 1024);
+ ramAvailableB = ramTotalB * str2double(prcFree{1}) / 100;
+ RamAvailable_MiB = round(ramAvailableB / 1024 / 1024);
+ end
+ end
catch
% Whatever...
end
end
- argout1 = maxvar;
- argout2 = totalmem;
+ argout1 = RamTotal_MiB;
+ argout2 = RamAvailable_MiB;
case 'BrainstormHomeDir'
argout1 = GlobalData.Program.BrainstormHomeDir;
@@ -501,6 +538,69 @@
case 'BrainstormDbFile'
argout1 = bst_fullfile(bst_get('BrainstormUserDir'), 'brainstorm.mat');
+ case 'Pipelines'
+ argout1 = GlobalData.Processes.Pipelines;
+
+ case 'OsType'
+ switch (mexext)
+ case 'mexglx', argout1 = 'linux32';
+ case 'mexa64', argout1 = 'linux64';
+ case 'mexmaci', argout1 = 'mac32';
+ case 'mexmaci64', argout1 = 'mac64';
+ case 'mexmaca64', argout1 = 'mac64arm';
+ case 'mexs64', argout1 = 'sol64';
+ case 'mexw32', argout1 = 'win32';
+ case 'mexw64', argout1 = 'win64';
+ otherwise, error('Unsupported extension.');
+ end
+ % CALL: bst_get('OsType', isMatlab=0)
+ if (nargin >= 2) && isequal(varargin{2}, 0)
+ if strcmpi(argout1, 'win32') && (~isempty(strfind(java.lang.System.getProperty('java.home'), '(x86)')) || ~isempty(strfind(java.lang.System.getenv('ProgramFiles(x86)'), '(x86)')))
+ argout1 = 'win64';
+ end
+ end
+
+ case 'OsName'
+ argout1 = '';
+ osFamily = [];
+ tmp = regexp(bst_get('OsType'), '^[a-z]+', 'match', 'ignorecase');
+ if ~isempty(tmp)
+ osFamily = tmp{1};
+ end
+ switch osFamily
+ case 'win'
+ [~, system_info] = system('ver');
+ argout1 = strtrim(system_info);
+
+ case 'linux'
+ os_release = fileread('/etc/os-release');
+ osName = regexp(os_release, '(?<=PRETTY_NAME=")(.*?)(?=")', 'match');
+ if ~isempty(osName)
+ osName = strtrim(osName{1});
+ else
+ osName = regexp(os_release, '(?<=NAME=")(.*?)(?=")', 'match');
+ if ~isempty(osName)
+ osName = strtrim(osName{1});
+ else
+ osName = 'Linux unknow distribution';
+ end
+ end
+ [~, kernelVer] = system('uname -r');
+ kernelVer = strtrim(kernelVer);
+ argout1 = [osName, ' (' kernelVer, ')'];
+
+ case 'mac'
+ [~, sw_vers] = system('sw_vers');
+ osName = regexp(sw_vers, '(?<=ProductName:)(.*?)(?=\n)', 'match');
+ osName = strtrim(osName{1});
+ osVer = regexp(sw_vers, '(?<=ProductVersion:)(.*?)(?=\n)', 'match');
+ osVer = strtrim(osVer{1});
+ [~, osHw] = system('uname -m');
+ osHw = strtrim(osHw);
+ argout1 = [osName, ' ' osVer, ' (', osHw, ')'];
+ end
+
+
%% ==== PROTOCOL ====
case 'iProtocol'
if isempty(GlobalData.DataBase.iProtocol)
@@ -868,7 +968,7 @@
% Usage: [sAnalStudy, iAnalStudy] = bst_get('AnalysisIntraStudy', iSubject)
case 'AnalysisIntraStudy'
% Parse inputs
- if (nargin == 2) && isnumeric(varargin{2})
+ if (nargin == 2)
iSubject = varargin{2};
else
error('Invalid call to bst_get()');
@@ -2284,6 +2384,10 @@
sTemplates(end+1).FilePath = 'http://neuroimage.usc.edu/bst/getupdate.php?t=Colin27_2016';
sTemplates(end).Name = 'Colin27_2016';
end
+ if ~ismember('colin27_4nirs_2024', lower({sTemplates.Name}))
+ sTemplates(end+1).FilePath = 'http://neuroimage.usc.edu/bst/getupdate.php?t=Colin27_4NIRS_2024';
+ sTemplates(end).Name = 'Colin27_4NIRS_2024';
+ end
if ~ismember('colin27_brainsuite_2016', lower({sTemplates.Name}))
sTemplates(end+1).FilePath = 'http://neuroimage.usc.edu/bst/getupdate.php?t=Colin27_BrainSuite_2016';
sTemplates(end).Name = 'Colin27_BrainSuite_2016';
@@ -2397,6 +2501,11 @@
end
% Get defaults from internet
+ if ~ismember('aal1', lower({sTemplates.Name}))
+ sTemplates(end+1).FilePath = 'http://neuroimage.usc.edu/bst/getupdate.php?t=mni_AAL1';
+ sTemplates(end).Name = 'AAL1';
+ sTemplates(end).Info = 'https://www.gin.cnrs.fr/en/tools/aal/';
+ end
if ~ismember('aal2', lower({sTemplates.Name}))
sTemplates(end+1).FilePath = 'http://neuroimage.usc.edu/bst/getupdate.php?t=mni_AAL2';
sTemplates(end).Name = 'AAL2';
@@ -2804,16 +2913,22 @@
end
case 'SpmTpmAtlas'
+ preferSpm = 0;
+ % CALL: bst_get('SpmTpmAtlas', 'SPM')
+ if (nargin >= 2) && strcmpi(varargin{2}, 'SPM')
+ preferSpm = 1;
+ end
+
% Get template file
tpmUser = bst_fullfile(bst_get('BrainstormUserDir'), 'defaults', 'spm', 'TPM.nii');
- if file_exist(tpmUser)
+ if file_exist(tpmUser) && ~preferSpm
argout1 = tpmUser;
disp(['BST> SPM12 template found: ' tpmUser]);
return;
end
% If it does not exist: check in brainstorm3 folder
tpmDistrib = bst_fullfile(bst_get('BrainstormHomeDir'), 'defaults', 'spm', 'TPM.nii');
- if file_exist(tpmDistrib)
+ if file_exist(tpmDistrib) && ~preferSpm
argout1 = tpmDistrib;
disp(['BST> SPM12 template found: ' tpmDistrib]);
return;
@@ -2826,6 +2941,9 @@
argout1 = tpmSpm;
disp(['BST> SPM12 template found: ' tpmSpm]);
return;
+ elseif preferSpm
+ argout1 = bst_get('SpmTpmAtlas');
+ return
end
else
tpmSpm = '';
@@ -2952,6 +3070,13 @@
else
argout1 = [.33 .0042 .33 .88 .93];
end
+
+ case 'ShowHiddenFiles'
+ if isfield(GlobalData, 'Preferences') && isfield(GlobalData.Preferences, 'ShowHiddenFiles')
+ argout1 = GlobalData.Preferences.ShowHiddenFiles;
+ else
+ argout1 = 0;
+ end
case 'LastUsedDirs'
defPref = struct(...
@@ -3008,25 +3133,6 @@
'MontageOut', '', ...
'FibersIn', '');
argout1 = FillMissingFields(contextName, defPref);
-
- case 'OsType'
- switch (mexext)
- case 'mexglx', argout1 = 'linux32';
- case 'mexa64', argout1 = 'linux64';
- case 'mexmaci', argout1 = 'mac32';
- case 'mexmaci64', argout1 = 'mac64';
- case 'mexmaca64', argout1 = 'mac64arm';
- case 'mexs64', argout1 = 'sol64';
- case 'mexw32', argout1 = 'win32';
- case 'mexw64', argout1 = 'win64';
- otherwise, error('Unsupported extension.');
- end
- % CALL: bst_get('OsType', isMatlab=0)
- if (nargin >= 2) && isequal(varargin{2}, 0)
- if strcmpi(argout1, 'win32') && (~isempty(strfind(java.lang.System.getProperty('java.home'), '(x86)')) || ~isempty(strfind(java.lang.System.getenv('ProgramFiles(x86)'), '(x86)')))
- argout1 = 'win64';
- end
- end
case 'ImportDataOptions'
defPref = db_template('ImportOptions');
@@ -3095,6 +3201,7 @@
case 'TopoLayoutOptions'
defPref = struct(...
'TimeWindow', [], ...
+ 'FreqWindow', [], ...
'WhiteBackground', 0, ...
'ShowRefLines', 1, ...
'ShowLegend', 1, ...
@@ -3127,9 +3234,9 @@
case 'ProcessOptions'
defPref = struct(...
- 'SavedParam', struct(), ...
- 'MaxBlockSize', 100 / 8 * 1024 * 1024, ... % 100Mb
- 'LastMaxBlockSize', 100 / 8 * 1024 * 1024); % 100Mb
+ 'SavedParam', struct(), ...
+ 'MaxBlockSize', 100 * 1024 * 1024 / 8, ... % 100MiB == 13,107,200 doubles
+ 'LastMaxBlockSize', 100 * 1024 * 1024 / 8); % 100MiB == 13,107,200 doubles
argout1 = FillMissingFields(contextName, defPref);
case 'ImportEegRawOptions'
@@ -3357,21 +3464,27 @@
case 'DigitizeOptions'
defPref = struct(...
+ 'PatientId', 'S001', ...
'ComPort', 'COM1', ...
'ComRate', 9600, ...
'ComByteCount', 94, ... % 47 bytes * 2 receivers
'UnitType', 'fastrak', ...
- 'PatientId', 'S001', ...
+ 'ConfigCommands', [], ... % setup-specific device configuration commands, e.g. hemisphere of operation
'nFidSets', 2, ...
+ 'Fids', {{'NAS', 'LPA', 'RPA'}}, ... % 3 anat points (required) and any other, e.g. MEG coils, in desired digitization order
+ 'DistThresh', 0.005, ... % 5 mm distance threshold between repeated measures of fid positions
'isBeep', 1, ...
'isMEG', 1, ...
'isSimulate', 0, ...
'Montages', [...
struct('Name', 'No EEG', ...
- 'Labels', []), ...
+ 'Labels', [], ...
+ 'ChannelFile', []), ...
struct('Name', 'Default', ...
- 'Labels', [])], ...
- 'iMontage', 1);
+ 'Labels', [], ...
+ 'ChannelFile', [])], ...
+ 'iMontage', 1, ...
+ 'Version', '2024'); % Version of the Digitize panel: 'legacy' or '2024'
argout1 = FillMissingFields(contextName, defPref);
case 'PcaOptions'
@@ -3526,6 +3639,7 @@
{'.gii'}, 'GIfTI / World coordinates (*.gii)', 'GII-WORLD'; ...
{'.fif'}, 'MNE (*.fif)', 'FIF'; ...
{'.obj'}, 'MNI OBJ (*.obj)', 'MNIOBJ'; ...
+ {'.obj'}, 'Wavefront OBJ (*.obj)', 'WFTOBJ'; ...
{'.msh'}, 'SimNIBS3/headreco Gmsh4 (*.msh)', 'SIMNIBS3'; ...
{'.msh'}, 'SimNIBS4/charm Gmsh4 (*.msh)', 'SIMNIBS4'; ...
{'.tri'}, 'TRI (*.tri)', 'TRI'; ...
@@ -3539,7 +3653,8 @@
argout1 = {...
{'.mesh'}, 'BrainVISA (*.mesh)', 'MESH'; ...
{'.dfs'}, 'BrainSuite (*.dfs)', 'DFS'; ...
- {'.fs'}, 'FreeSurfer (*.fs)', 'FS'
+ {'.fs'}, 'FreeSurfer (*.fs)', 'FS'; ...
+ {'.obj'}, 'Wavefront OBJ (*.obj)', 'OBJ'; ...
{'.off'}, 'Geomview OFF (*.off)', 'OFF'; ...
{'.gii'}, 'GIfTI (*.gii)', 'GII'; ...
{'.tri'}, 'TRI (*.tri)', 'TRI'; ...
@@ -3577,6 +3692,7 @@
{'.rda'}, 'EEG: Compumedics ProFusion Sleep (*.rda)', 'EEG-COMPUMEDICS-PFS'; ...
{'.bin'}, 'EEG: Deltamed Coherence-Neurofile (*.bin)', 'EEG-DELTAMED'; ...
{'.edf','.rec'}, 'EEG: EDF / EDF+ (*.rec;*.edf)', 'EEG-EDF'; ...
+ {'.edf','.rec'}, 'EEG EDF / EDF+ FieldTrip reader (*.rec;*.edf)', 'EEG-EDF-FT'; ...
{'.set'}, 'EEG: EEGLAB (*.set)', 'EEG-EEGLAB'; ...
{'.raw'}, 'EEG: EGI Netstation RAW (*.raw)', 'EEG-EGI-RAW'; ...
{'.mff','.bin'}, 'EEG: EGI-Philips (*.mff)', 'EEG-EGI-MFF'; ...
@@ -3640,6 +3756,7 @@
{'.dat','.cdt'}, 'EEG: Curry (*.dat;*.cdt)', 'EEG-CURRY'; ...
{'.bin'}, 'EEG: Deltamed Coherence-Neurofile (*.bin)', 'EEG-DELTAMED'; ...
{'.edf','.rec'}, 'EEG: EDF / EDF+ (*.rec;*.edf)', 'EEG-EDF'; ...
+ {'.edf','.rec'}, 'EEG EDF / EDF+ FieldTrip reader (*.rec;*.edf)', 'EEG-EDF-FT'; ...
{'.set'}, 'EEG: EEGLAB (*.set)', 'EEG-EEGLAB'; ...
{'.raw'}, 'EEG: EGI Netstation RAW (*.raw)', 'EEG-EGI-RAW'; ...
{'.mff','.bin'}, 'EEG: EGI-Philips (*.mff)', 'EEG-EGI-MFF'; ...
@@ -3744,6 +3861,7 @@
{'.txt'}, 'Array of samples (*.txt)', 'ARRAY-SAMPLES'; ...
{'.txt','.csv'}, 'CSV text file: label, time, duration (*.txt;*.csv)', 'CSV-TIME'; ...
{'.txt'}, 'CTF Video Times (*.txt)', 'CTFVIDEO'; ...
+ {'.tsv'}, 'BIDS events: onset, duration, trial_type (*.tsv)', 'BIDS'; ...
};
case 'channel'
argout1 = {...
@@ -3767,7 +3885,7 @@
{'.tsv'}, 'EEG: BIDS electrodes.tsv, CapTrak space mm (*.tsv)', 'BIDS-CAPTRAK-MM'; ...
{'.els','.xyz'}, 'EEG: Cartool (*.els;*.xyz)', 'CARTOOL'; ...
{'.eeg'}, 'EEG: MegDraw (*.eeg)', 'MEGDRAW'; ...
- {'.res','.rs3','.pom'}, 'EEG: Curry (*.res;*.rs3;*.pom)', 'CURRY'; ...
+ {'.res','.rs3','.pom'}, 'EEG: Curry, LPS (*.res;*.rs3;*.pom)', 'CURRY'; ...
{'.ced','.xyz','.set'}, 'EEG: EEGLAB (*.ced;*.xyz;*.set)', 'EEGLAB'; ...
{'.elc'}, 'EEG: EETrak (*.elc)', 'EETRAK'; ...
{'.sfp'}, 'EEG: EGI (*.sfp)', 'EGI'; ...
@@ -3823,7 +3941,7 @@
{'.txt'}, 'EEG/NIRS: ASCII: XYZ,Name (*.txt)', 'ASCII_XYZN-EEG'; ...
{'.txt'}, 'EEG/NIRS: ASCII: XYZ_MNI,Name (*.txt)', 'ASCII_XYZN_MNI-EEG'; ...
{'.txt'}, 'EEG/NIRS: ASCII: XYZ_World,Name (*.txt)', 'ASCII_XYZN_WORLD-EEG'; ...
- {'.txt'}, 'NIRS: Brainsight (*.txt)', 'BRAINSIGHT-TXT'; ...
+ {'.txt'}, 'EEG/NIRS: Brainsight (*.txt)', 'BRAINSIGHT-TXT'; ...
{'.tsv'}, 'NIRS: BIDS optrodes.tsv, subject space mm (*.tsv)', 'BIDS-NIRS-SCANRAS-MM'; ...
{'.tsv'}, 'NIRS: BIDS optrodes.tsv, MNI space mm (*.tsv)', 'BIDS-NIRS-MNI-MM'; ...
{'.tsv'}, 'NIRS: BIDS optrodes.tsv, ALS/SCS/CTF space mm (*.tsv)', 'BIDS-NIRS-ALS-MM'; ...
diff --git a/toolbox/core/bst_memory.m b/toolbox/core/bst_memory.m
index b2071e459..e99fd8d8e 100644
--- a/toolbox/core/bst_memory.m
+++ b/toolbox/core/bst_memory.m
@@ -304,6 +304,7 @@
sSurf.Comment = surfMat.Comment;
sSurf.Faces = double(surfMat.Faces);
sSurf.Vertices = double(surfMat.Vertices);
+ sSurf.Color = double(surfMat.Color);
sSurf.VertConn = surfMat.VertConn;
sSurf.VertNormals = surfMat.VertNormals;
[tmp, sSurf.VertArea] = tess_area(surfMat.Vertices, surfMat.Faces);
@@ -795,9 +796,6 @@ function LoadChannelFile(iDS, ChannelFile)
GlobalData.DataSet(iDS).DataFile = file_short(DataFile);
GlobalData.DataSet(iDS).Measures = Measures;
- % ===== LOAD CHANNEL FILE =====
- LoadChannelFile(iDS, ChannelFile);
-
% ===== Check time window consistency with previously loaded data =====
if isTimeCheck
% Update time window
@@ -814,25 +812,18 @@ function LoadChannelFile(iDS, ChannelFile)
return;
% Otherwise: unload all the other datasets
else
- % Save newly created dataset
- bakDS = GlobalData.DataSet(iDS);
- % Unload everything
- UnloadAll('Forced');
- % If not everything was unloaded correctly (eg. the user cancelled half way when asked to save the modifications)
- if ~isempty(GlobalData.DataSet)
- % Unload the new dataset
- UnloadDataSets(iDS);
- iDS = [];
- return;
+ iDS = UnloadOtherDs(iDS);
+ if isempty(iDS)
+ return
end
- % Restore new dataset
- GlobalData.DataSet = bakDS;
- iDS = 1;
% Update time window
isTimeCoherent = CheckTimeWindows();
end
end
end
+
+ % ===== LOAD CHANNEL FILE =====
+ LoadChannelFile(iDS, ChannelFile);
% ===== UPDATE TOOL TABS =====
if ~isempty(iDS) && strcmpi(GlobalData.DataSet(iDS).Measures.DataType, 'raw')
@@ -1141,7 +1132,7 @@ function ReloadStatDataSets() %#ok
SamplingRate = [];
if any(strcmpi('ImageGridAmp', {File_whos.name}))
% Load results .Mat
- ResultsMat = in_bst_results(ResultsFullFile, 0, 'Comment', 'Time', 'ChannelFlag', 'SurfaceFile', 'HeadModelType', 'ColormapType', 'DisplayUnits', 'GoodChannel', 'Atlas');
+ ResultsMat = in_bst_results(ResultsFullFile, 0, 'Comment', 'Time', 'ChannelFlag', 'SurfaceFile', 'HeadModelType', 'ColormapType', 'DisplayUnits', 'GoodChannel', 'Atlas', 'Function');
% Raw file: Use only the loaded time window
if ~isempty(DataFile) && strcmpi(GlobalData.DataSet(iDS).Measures.DataType, 'raw') && ~isempty(strfind(ResultsFullFile, '_KERNEL_'))
Time = GlobalData.DataSet(iDS).Measures.Time;
@@ -1226,7 +1217,11 @@ function ReloadStatDataSets() %#ok
else
Results.GoodChannel = ResultsMat.GoodChannel;
end
-
+ % If Results structure has Function field
+ if isfield(ResultsMat, 'Function')
+ Results.Function = ResultsMat.Function;
+ end
+
% Store new Results structure in GlobalData
iResult = length(GlobalData.DataSet(iDS).Results) + 1;
GlobalData.DataSet(iDS).Results(iResult) = Results;
@@ -1247,14 +1242,24 @@ function ReloadStatDataSets() %#ok
isTimeCoherent = CheckTimeWindows();
% If loaded results are not coherent with previous data
if ~isTimeCoherent
- % Remove it
- GlobalData.DataSet(iDS).Results(iResult) = [];
- iDS = [];
- iResult = [];
- bst_error(['Time definition for this file is not compatible with the other files' 10 ...
- 'already loaded in Brainstorm.' 10 10 ...
- 'Close existing windows before opening this file, or use the Navigator.'], 'Load results', 0);
- return
+ res = java_dialog('question', [...
+ 'The time definition is not compatible with previously loaded files.' 10 ...
+ 'Unload all the other files first?' 10 10], 'Load results', [], {'Unload other files', 'Cancel'});
+ % Cancel: Unload the new dataset
+ if isempty(res) || strcmpi(res, 'Cancel')
+ UnloadDataSets(iDS);
+ iDS = [];
+ return;
+ % Otherwise: unload all the other datasets
+ else
+ iDS = UnloadOtherDs(iDS);
+ if isempty(iDS)
+ iResult = [];
+ return
+ end
+ % Update time window
+ isTimeCoherent = CheckTimeWindows();
+ end
end
end
% Update TimeWindow panel, if it exists
@@ -1978,13 +1983,31 @@ function LoadResultsMatrix(iDS, iResult)
if isempty(iDS) && isempty(Mat.Events)
iDS = GetDataSetStudy(sStudy.FileName);
end
- % Create dataset
- if isempty(iDS)
+ % Check time against existing DS
+ isTimeOkDs = 1;
+ if ~isempty(iDS) && (length(Mat.Time) >= 2)
+ % Save measures information if no DataFile is available
+ if isempty(GlobalData.DataSet(iDS).Measures) || isempty(GlobalData.DataSet(iDS).Measures.Time)
+ GlobalData.DataSet(iDS).Measures.Time = double(Mat.Time([1, end]));
+ GlobalData.DataSet(iDS).Measures.SamplingRate = double(Mat.Time(2) - Mat.Time(1));
+ GlobalData.DataSet(iDS).Measures.NumberOfSamples = length(Mat.Time);
+ elseif (abs(Mat.Time(1) - GlobalData.DataSet(iDS).Measures.Time(1)) > 1e-5) || ...
+ (abs(Mat.Time(end) - GlobalData.DataSet(iDS).Measures.Time(2)) > 1e-5) || ...
+ ~isequal(length(Mat.Time), GlobalData.DataSet(iDS).Measures.NumberOfSamples)
+ isTimeOkDs = 0;
+ end
+ end
+ % Create dataset if not existent or different time definition
+ if isempty(iDS) || ~isTimeOkDs
% Create a new DataSet only for results
iDS = length(GlobalData.DataSet) + 1;
GlobalData.DataSet(iDS) = db_template('DataSet');
GlobalData.DataSet(iDS).SubjectFile = file_short(sStudy.BrainStormSubject);
GlobalData.DataSet(iDS).StudyFile = file_short(sStudy.FileName);
+ % Save measures information
+ GlobalData.DataSet(iDS).Measures.Time = double(Mat.Time([1, end]));
+ GlobalData.DataSet(iDS).Measures.SamplingRate = double(Mat.Time(2) - Mat.Time(1));
+ GlobalData.DataSet(iDS).Measures.NumberOfSamples = length(Mat.Time);
end
% Make sure that there is only one dataset selected
iDS = iDS(1);
@@ -1992,26 +2015,28 @@ function LoadResultsMatrix(iDS, iResult)
% ===== CHECK TIME =====
% If there time in this file
if (length(Mat.Time) >= 2)
- isTimeOkDs = 1;
- % Save measures information if no DataFile is available
- if isempty(GlobalData.DataSet(iDS).Measures) || isempty(GlobalData.DataSet(iDS).Measures.Time)
- GlobalData.DataSet(iDS).Measures.Time = double(Mat.Time([1, end]));
- GlobalData.DataSet(iDS).Measures.SamplingRate = double(Mat.Time(2) - Mat.Time(1));
- GlobalData.DataSet(iDS).Measures.NumberOfSamples = length(Mat.Time);
- elseif (abs(Mat.Time(1) - GlobalData.DataSet(iDS).Measures.Time(1)) > 1e-5) || ...
- (abs(Mat.Time(end) - GlobalData.DataSet(iDS).Measures.Time(2)) > 1e-5) || ...
- ~isequal(length(Mat.Time), GlobalData.DataSet(iDS).Measures.NumberOfSamples)
- isTimeOkDs = 0;
- end
% Update time window
isTimeCoherent = CheckTimeWindows();
% If loaded file are not coherent with previous data
if ~isTimeCoherent || ~isTimeOkDs
- iDS = [];
- bst_error(['Time definition for this file is not compatible with the other files' 10 ...
- 'already loaded in Brainstorm.' 10 10 ...
- 'Close existing windows before opening this file, or use the Navigator.'], 'Load matrix', 0);
- return
+ res = java_dialog('question', [...
+ 'The time definition is not compatible with previously loaded files.' 10 ...
+ 'Unload all the other files first?' 10 10], 'Load matrix', [], {'Unload other files', 'Cancel'});
+ % Cancel: Unload the new dataset
+ if isempty(res) || strcmpi(res, 'Cancel')
+ UnloadDataSets(iDS);
+ iDS = [];
+ return;
+ % Otherwise: unload all the other datasets
+ else
+ iDS = UnloadOtherDs(iDS);
+ if isempty(iDS)
+ iMatrix = [];
+ return
+ end
+ % Update time window
+ isTimeCoherent = CheckTimeWindows();
+ end
end
% Update TimeWindow panel
panel_time('UpdatePanel');
@@ -3258,6 +3283,7 @@ function CheckFrequencies()
GlobalData.Program.ProcessMenuCache = struct();
% Clear some display options
GlobalData.Preferences.TopoLayoutOptions.TimeWindow = [];
+ GlobalData.Preferences.TopoLayoutOptions.FreqWindow = [];
end
% Close all unecessary tabs when forced, or when no data left
if isForced || isempty(GlobalData.DataSet)
@@ -3551,5 +3577,24 @@ function SaveChannelFile(iDS)
end
end
+%% ===== UNLOAD OTHER DS =====
+function iDS = UnloadOtherDs(iDS)
+% Unload Brainstorm datasets except for iDS. It returns the new iDS (iDS=1) for the kept DS
+ global GlobalData;
+ % Save dataset to keep
+ bakDS = GlobalData.DataSet(iDS);
+ % Unload everything
+ UnloadAll('Forced');
+ % If not everything was unloaded correctly (eg. the user cancelled half way when asked to save the modifications)
+ if ~isempty(GlobalData.DataSet)
+ % Unload also dataset to keep
+ UnloadDataSets(iDS);
+ iDS = [];
+ return;
+ end
+ % Restore dataset
+ GlobalData.DataSet = bakDS;
+ iDS = 1;
+end
diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m
index a33a43c2f..b69da8b1e 100644
--- a/toolbox/core/bst_plugin.m
+++ b/toolbox/core/bst_plugin.m
@@ -11,11 +11,13 @@
% ReadmeFile = bst_plugin('GetReadmeFile', PlugDesc) % Get full path to plugin readme file
% LogoFile = bst_plugin('GetLogoFile', PlugDesc) % Get full path to plugin logo file
% Version = bst_plugin('CompareVersions', v1, v2) % Compare two version strings
+% [isOk, errMsg] = bst_plugin('AddUserDefDesc', RegMethod, jsonLocation=[]) % Register user-defined plugin definition
+% [isOk, errMsg] = bst_plugin('RemoveUserDefDesc' PlugName) % Remove user-defined plugin definition
% [isOk, errMsg, PlugDesc] = bst_plugin('Load', PlugName/PlugDesc, isVerbose=1)
% [isOk, errMsg, PlugDesc] = bst_plugin('LoadInteractive', PlugName/PlugDesc)
% [isOk, errMsg, PlugDesc] = bst_plugin('Unload', PlugName/PlugDesc, isVerbose=1)
% [isOk, errMsg, PlugDesc] = bst_plugin('UnloadInteractive', PlugName/PlugDesc)
-% [isOk, errMsg, PlugDesc] = bst_plugin('Install', PlugName, isInteractive=0, minVersion=[])
+% [isOk, errMsg, PlugDesc] = bst_plugin('Install', PlugName, isInteractive=0, minVersion=[]) % Install and Load a plugin and its dependencies
% [isOk, errMsg, PlugDesc] = bst_plugin('InstallMultipleChoice',PlugNames, isInteractive=0) % Install at least one of the input plugins
% [isOk, errMsg, PlugDesc] = bst_plugin('InstallInteractive', PlugName)
% [isOk, errMsg] = bst_plugin('Uninstall', PlugName, isInteractive=0, isDependencies=1)
@@ -27,6 +29,7 @@
% bst_plugin('MenuCreate', jMenu)
% bst_plugin('MenuUpdate', jMenu)
% bst_plugin('LinkCatSpm', Action) % 0=Delete/1=Create/2=Check a symbolic link for CAT12 in SPM12 toolbox folder
+% bst_plugin('UpdateDescription', PlugDesc, doDelete=0) % Update plugin description after load
%
%
% PLUGIN DEFINITION
@@ -105,7 +108,7 @@
% For more information type "brainstorm license" at command prompt.
% =============================================================================@
%
-% Authors: Francois Tadel 2021-2023
+% Authors: Francois Tadel, 2021-2023
eval(macro_method);
end
@@ -114,8 +117,12 @@
%% ===== GET SUPPORTED PLUGINS =====
% USAGE: PlugDesc = bst_plugin('GetSupported') % List all the plugins supported by Brainstorm
% PlugDesc = bst_plugin('GetSupported', PlugName/PlugDesc) % Get only one specific supported plugin
-function PlugDesc = GetSupported(SelPlug)
+% PlugDesc = bst_plugin('GetSupported', ..., UserDefVerbose) % Print info on user-defined plugins
+function PlugDesc = GetSupported(SelPlug, UserDefVerbose)
% Parse inputs
+ if (nargin < 2) || isempty(UserDefVerbose)
+ UserDefVerbose = 0;
+ end
if (nargin < 1) || isempty(SelPlug)
SelPlug = [];
end
@@ -124,6 +131,7 @@
% Get OS
OsType = bst_get('OsType', 0);
+ % Add new curated plugins by 'CATEGORY:' and alphabetic order
% ================================================================================================================
% === ANATOMY: BRAIN2MESH ===
PlugDesc(end+1) = GetStruct('brain2mesh');
@@ -153,31 +161,45 @@
PlugDesc(end).UninstalledFcn = 'LinkCatSpm(0);';
PlugDesc(end).LoadedFcn = 'LinkCatSpm(2);';
PlugDesc(end).ExtraMenus = {'Online tutorial', 'web(''https://neuroimage.usc.edu/brainstorm/Tutorials/SegCAT12'', ''-browser'')'};
+
+ % === ANATOMY: CT2MRIREG ===
+ PlugDesc(end+1) = GetStruct('ct2mrireg');
+ PlugDesc(end).Version = 'github-master';
+ PlugDesc(end).Category = 'Anatomy';
+ PlugDesc(end).AutoUpdate = 1;
+ PlugDesc(end).URLzip = 'https://github.com/ajoshiusc/USCCleveland/archive/master.zip';
+ PlugDesc(end).URLinfo = 'https://github.com/ajoshiusc/USCCleveland/tree/master/ct2mrireg';
+ PlugDesc(end).TestFile = 'ct2mrireg.m';
+ PlugDesc(end).ReadmeFile = 'ct2mrireg/README.md';
+ PlugDesc(end).CompiledStatus = 2;
+ PlugDesc(end).LoadFolders = {'ct2mrireg'};
+ PlugDesc(end).DeleteFiles = {'fmri_analysis', 'for_clio', 'mixed_atlas', 'process_script', 'reg_prepost', 'visualize_channels', '.gitignore', 'README.md'};
% === ANATOMY: ISO2MESH ===
PlugDesc(end+1) = GetStruct('iso2mesh');
- PlugDesc(end).Version = '1.9.6';
+ PlugDesc(end).Version = '1.9.8';
PlugDesc(end).Category = 'Anatomy';
PlugDesc(end).AutoUpdate = 1;
- PlugDesc(end).URLzip = 'https://github.com/fangq/iso2mesh/releases/download/v1.9.6/iso2mesh-1.9.6-allinone.zip';
+ PlugDesc(end).URLzip = 'https://github.com/fangq/iso2mesh/archive/refs/tags/v1.9.8.zip';
PlugDesc(end).URLinfo = 'http://iso2mesh.sourceforge.net';
PlugDesc(end).TestFile = 'iso2meshver.m';
PlugDesc(end).ReadmeFile = 'README.txt';
PlugDesc(end).CompiledStatus = 2;
PlugDesc(end).LoadedFcn = 'assignin(''base'', ''ISO2MESH_TEMP'', bst_get(''BrainstormTmpDir''));';
- PlugDesc(end).DeleteFiles = {'doc', 'tools', '.git_filters', 'sample', ...
- 'bin/cgalmesh.exe', 'bin/cgalmesh.mexglx', 'bin/cgalmesh.mexmaci', ...
- 'bin/cgalpoly.exe', 'bin/cgalpoly.mexglx', 'bin/cgalpoly.mexmaci', 'bin/cgalpoly.mexa64', 'bin/cgalpoly.mexmaci64', 'bin/cgalpoly_x86-64.exe', ... % Removing cgalpoly completely (not used)
- 'bin/cgalsimp2.exe', 'bin/cgalsimp2.mexglx', 'bin/cgalsimp2.mexmaci', 'bin/cgalsimp2.mexmac', ...
- 'bin/cgalsurf.exe', 'bin/cgalsurf.mexglx', 'bin/cgalsurf.mexmaci', ...
- 'bin/cork.exe', ...
- 'bin/gtsrefine.mexglx', 'bin/gtsrefine.mexmaci', 'bin/gtsrefine.mexarmhf', 'bin/gtsrefine.exe', 'bin/gtsrefine.mexmaci64', ... % Removing gtsrefine completely (not used)
- 'bin/jmeshlib.exe', 'bin/jmeshlib.mexglx', 'bin/jmeshlib.mexmaci', 'bin/jmeshlib.mexmac', 'bin/jmeshlib.mexarmhf', ...
- 'bin/meshfix.exe', 'bin/meshfix.mexglx', 'bin/meshfix.mexmaci', 'bin/meshfix.mexarmhf', ...
- 'bin/tetgen.mexglx', 'bin/tetgen.mexmac', 'bin/tetgen.mexarmhf', ...
- 'bin/tetgen1.5.mexglx'};
- PlugDesc(end).DeleteFilesBin = {'bin/tetgen.exe', 'bin/tetgen.mexa64', 'bin/tetgen.mexmaci', 'bin/tetgen.mexmaci64', 'bin/tetgen_x86-64.exe', ... % Removing older tetgen completely (very sparsely used)
- 'bin/tetgen1.5.exe'};
+ PlugDesc(end).UnloadPlugs = {'easyh5','jsnirfy'};
+
+ % === ANATOMY: NEUROMAPS ===
+ PlugDesc(end+1) = GetStruct('neuromaps');
+ PlugDesc(end).Version = 'github-main';
+ PlugDesc(end).Category = 'Anatomy';
+ PlugDesc(end).AutoUpdate = 0;
+ PlugDesc(end).AutoLoad = 0;
+ PlugDesc(end).CompiledStatus = 2;
+ PlugDesc(end).URLzip = 'https://github.com/thuy-n/bst-neuromaps/archive/refs/heads/main.zip';
+ PlugDesc(end).URLinfo = 'https://github.com/thuy-n/bst-neuromaps';
+ PlugDesc(end).ReadmeFile = 'README.md';
+ PlugDesc(end).LoadFolders = {'*'};
+ PlugDesc(end).TestFile = 'process_nmp_fetch_maps.m';
% === ANATOMY: ROAST ===
PlugDesc(end+1) = GetStruct('roast');
@@ -192,6 +214,20 @@
PlugDesc(end).UnloadPlugs = {'spm12', 'iso2mesh'};
PlugDesc(end).LoadFolders = {'lib/spm12', 'lib/iso2mesh', 'lib/cvx', 'lib/ncs2daprox', 'lib/NIFTI_20110921'};
+ % === ANATOMY: ZEFFIRO ===
+ PlugDesc(end+1) = GetStruct('zeffiro');
+ PlugDesc(end).Version = 'github-main_development_branch';
+ PlugDesc(end).Category = 'Anatomy';
+ PlugDesc(end).AutoUpdate = 1;
+ PlugDesc(end).URLzip = 'https://github.com/sampsapursiainen/zeffiro_interface/archive/main_development_branch.zip';
+ PlugDesc(end).URLinfo = 'https://github.com/sampsapursiainen/zeffiro_interface';
+ PlugDesc(end).TestFile = 'zeffiro_downloader.m';
+ PlugDesc(end).ReadmeFile = 'README.md';
+ PlugDesc(end).CompiledStatus = 0;
+ PlugDesc(end).LoadFolders = {'*'};
+ PlugDesc(end).DeleteFiles = {'.gitignore'};
+
+
% === FORWARD: OPENMEEG ===
PlugDesc(end+1) = GetStruct('openmeeg');
PlugDesc(end).Version = '2.4.1';
@@ -204,6 +240,10 @@
case 'mac64'
PlugDesc(end).URLzip = 'https://files.inria.fr/OpenMEEG/download/OpenMEEG-2.4.1-MacOSX.tar.gz';
PlugDesc(end).TestFile = 'libOpenMEEG.1.1.0.dylib';
+ case 'mac64arm'
+ PlugDesc(end).Version = '2.5.8';
+ PlugDesc(end).URLzip = ['https://github.com/openmeeg/openmeeg/releases/download/', PlugDesc(end).Version, '/OpenMEEG-', PlugDesc(end).Version, '-', 'macOS_M1.tar.gz'];
+ PlugDesc(end).TestFile = 'libOpenMEEG.1.1.0.dylib';
case 'win32'
PlugDesc(end).URLzip = 'https://files.inria.fr/OpenMEEG/download/release-2.2/OpenMEEG-2.2.0-win32-x86-cl-OpenMP-shared.tar.gz';
PlugDesc(end).TestFile = 'om_assemble.exe';
@@ -290,6 +330,33 @@
'NPMK/Dependent Functions/.svn', 'NPMK/Dependent Functions/.DS_Store', 'NPMK/Dependent Functions/bnsx.dat', 'NPMK/Dependent Functions/syncPatternDetectNEV.m', ...
'NPMK/Dependent Functions/syncPatternDetectNSx.m', 'NPMK/Dependent Functions/syncPatternFinderNSx.m'};
+ % === I/O: EASYH5 ===
+ PlugDesc(end+1) = GetStruct('easyh5');
+ PlugDesc(end).Version = 'github-master';
+ PlugDesc(end).Category = 'I/O';
+ PlugDesc(end).URLzip = 'https://github.com/NeuroJSON/easyh5/archive/master.zip';
+ PlugDesc(end).URLinfo = 'https://github.com/NeuroJSON/easyh5';
+ PlugDesc(end).TestFile = 'loadh5.m';
+ PlugDesc(end).CompiledStatus = 2;
+ PlugDesc(end).LoadFolders = {'*'};
+ PlugDesc(end).DeleteFiles = {'examples'};
+ PlugDesc(end).ReadmeFile = 'README.md';
+ PlugDesc(end).UnloadPlugs = {'iso2mesh'};
+
+ % === I/O: JSNIRF ===
+ PlugDesc(end+1) = GetStruct('jsnirfy');
+ PlugDesc(end).Version = 'github-master';
+ PlugDesc(end).Category = 'I/O';
+ PlugDesc(end).URLzip = 'https://github.com/NeuroJSON/jsnirfy/archive/master.zip';
+ PlugDesc(end).URLinfo = 'https://github.com/NeuroJSON/jsnirfy';
+ PlugDesc(end).TestFile = 'loadsnirf.m';
+ PlugDesc(end).CompiledStatus = 2;
+ PlugDesc(end).LoadFolders = {'*'};
+ PlugDesc(end).DeleteFiles = {'loadjsnirf.m', 'savejsnirf.m'};
+ PlugDesc(end).ReadmeFile = 'README.md';
+ PlugDesc(end).RequiredPlugs = {'easyh5'};
+ PlugDesc(end).UnloadPlugs = {'iso2mesh'};
+
% === I/O: MFF ===
PlugDesc(end+1) = GetStruct('mff');
PlugDesc(end).Version = 'github-master';
@@ -319,6 +386,17 @@
'f=fopen(''private' filesep 'eeg_checkset.m'',''wt''); fprintf(f,''function EEG=eeg_checkset(EEG)''); fclose(f);' ...
'cd(d);'];
+ % === I/O: npy-matlab ===
+ PlugDesc(end+1) = GetStruct('npy-matlab');
+ PlugDesc(end).Version = 'github-master';
+ PlugDesc(end).Category = 'I/O';
+ PlugDesc(end).URLzip = 'https://github.com/kwikteam/npy-matlab/archive/refs/heads/master.zip';
+ PlugDesc(end).URLinfo = 'https://github.com/kwikteam/npy-matlab';
+ PlugDesc(end).TestFile = 'constructNPYheader.m';
+ PlugDesc(end).LoadFolders = {'*'};
+ PlugDesc(end).ReadmeFile = 'README.md';
+ PlugDesc(end).CompiledStatus = 0;
+
% === I/O: NWB ===
PlugDesc(end+1) = GetStruct('nwb');
PlugDesc(end).Version = 'github-master';
@@ -393,6 +471,17 @@
PlugDesc(end).CompiledStatus = 0;
PlugDesc(end).RequiredPlugs = {'fieldtrip', '20200911'};
+
+ % === STATISTICS: FASTICA ===
+ PlugDesc(end+1) = GetStruct('fastica');
+ PlugDesc(end).Version = '2.5';
+ PlugDesc(end).Category = 'Statistics';
+ PlugDesc(end).URLzip = 'https://research.ics.aalto.fi/ica/fastica/code/FastICA_2.5.zip';
+ PlugDesc(end).URLinfo = 'https://research.ics.aalto.fi/ica/fastica/';
+ PlugDesc(end).TestFile = 'fastica.m';
+ PlugDesc(end).ReadmeFile = 'Contents.m';
+ PlugDesc(end).CompiledStatus = 2;
+
% === STATISTICS: LIBSVM ===
PlugDesc(end+1) = GetStruct('libsvm');
PlugDesc(end).Version = 'github-master';
@@ -406,15 +495,17 @@
PlugDesc(end).LoadFolders = {'*'};
PlugDesc(end).InstalledFcn = 'd=pwd; cd(fileparts(which(''make''))); make; cd(d);';
- % === STATISTICS: FASTICA ===
- PlugDesc(end+1) = GetStruct('fastica');
- PlugDesc(end).Version = '2.5';
+ % === STATISTICS: mTRF ===
+ PlugDesc(end+1) = GetStruct('mtrf');
+ PlugDesc(end).Version = '2.4';
PlugDesc(end).Category = 'Statistics';
- PlugDesc(end).URLzip = 'https://research.ics.aalto.fi/ica/fastica/code/FastICA_2.5.zip';
- PlugDesc(end).URLinfo = 'https://research.ics.aalto.fi/ica/fastica/';
- PlugDesc(end).TestFile = 'fastica.m';
- PlugDesc(end).ReadmeFile = 'Contents.m';
- PlugDesc(end).CompiledStatus = 2;
+ PlugDesc(end).URLzip = 'https://github.com/mickcrosse/mTRF-Toolbox/archive/refs/tags/v2.4.zip';
+ PlugDesc(end).URLinfo = 'https://github.com/mickcrosse/mTRF-Toolbox';
+ PlugDesc(end).TestFile = 'mTRFtrain.m';
+ PlugDesc(end).ReadmeFile = 'README.md';
+ PlugDesc(end).CompiledStatus = 0;
+ PlugDesc(end).LoadFolders = {'mtrf'};
+ PlugDesc(end).DeleteFiles = {'.gitattributes', '.github/ISSUE_TEMPLATE', 'data', 'doc', 'examples', 'img'};
% === STATISTICS: PICARD ===
PlugDesc(end+1) = GetStruct('picard');
@@ -477,17 +568,6 @@
PlugDesc(end).CompiledStatus = 0;
PlugDesc(end).RequiredPlugs = {'npy-matlab'};
- % === ELECTROPHYSIOLOGY: npy-matlab ===
- PlugDesc(end+1) = GetStruct('npy-matlab');
- PlugDesc(end).Version = 'github-master';
- PlugDesc(end).Category = 'e-phys';
- PlugDesc(end).URLzip = 'https://github.com/kwikteam/npy-matlab/archive/refs/heads/master.zip';
- PlugDesc(end).URLinfo = 'https://github.com/kwikteam/npy-matlab';
- PlugDesc(end).TestFile = 'constructNPYheader.m';
- PlugDesc(end).LoadFolders = {'*'};
- PlugDesc(end).ReadmeFile = 'README.md';
- PlugDesc(end).CompiledStatus = 0;
-
% === ELECTROPHYSIOLOGY: ultramegasort2000 ===
PlugDesc(end+1) = GetStruct('ultramegasort2000');
PlugDesc(end).Version = 'github-master';
@@ -510,7 +590,7 @@
PlugDesc(end).ReadmeFile = 'README.md';
PlugDesc(end).CompiledStatus = 0;
- % === NIRSTORM ===
+ % === fNIRS: NIRSTORM ===
PlugDesc(end+1) = GetStruct('nirstorm');
PlugDesc(end).Version = 'github-master';
PlugDesc(end).Category = 'fNIRS';
@@ -527,31 +607,31 @@
PlugDesc(end).MinMatlabVer = 803; % 2014a
PlugDesc(end).DeleteFiles = {'scripts', 'test', 'run_tests.m', 'test_suite_bak.m', '.gitignore'};
- % === MCXLAB CUDA ===
+ % === fNIRS: MCXLAB CUDA ===
PlugDesc(end+1) = GetStruct('mcxlab-cuda');
- PlugDesc(end).Version = '2021.12.04';
+ PlugDesc(end).Version = '2024.07.23';
PlugDesc(end).Category = 'fNIRS';
PlugDesc(end).AutoUpdate = 1;
- PlugDesc(end).URLzip = 'http://mcx.space/nightly/release/v2020/lite/mcxlab-allinone-x86_64-v2020.zip';
+ PlugDesc(end).URLzip = 'https://mcx.space/nightly/release/git20240723/mcxlab-allinone-git20240723.zip';
PlugDesc(end).TestFile = 'mcxlab.m';
- PlugDesc(end).URLinfo = 'http://mcx.space/wiki/';
+ PlugDesc(end).URLinfo = 'https://mcx.space/wiki/';
PlugDesc(end).CompiledStatus = 0;
PlugDesc(end).LoadFolders = {'*'};
PlugDesc(end).UnloadPlugs = {'mcxlab-cl'};
- % === MCXLAB CL ===
+ % === fNIRS: MCXLAB CL ===
PlugDesc(end+1) = GetStruct('mcxlab-cl');
- PlugDesc(end).Version = '2020';
+ PlugDesc(end).Version = '2024.07.23';
PlugDesc(end).Category = 'fNIRS';
PlugDesc(end).AutoUpdate = 0;
- PlugDesc(end).URLzip = 'http://mcx.space/nightly/release/v2020/lite/mcxlabcl-allinone-x86_64-v2020.zip';
+ PlugDesc(end).URLzip = 'https://mcx.space/nightly/release/git20240723/mcxlabcl-allinone-git20240723.zip';
PlugDesc(end).TestFile = 'mcxlabcl.m';
- PlugDesc(end).URLinfo = 'http://mcx.space/wiki/';
+ PlugDesc(end).URLinfo = 'https://mcx.space/wiki/';
PlugDesc(end).CompiledStatus = 2;
PlugDesc(end).LoadFolders = {'*'};
PlugDesc(end).UnloadPlugs = {'mcxlab-cuda'};
- % === MIA ===
+ % === sEEG: MIA ===
PlugDesc(end+1) = GetStruct('mia');
PlugDesc(end).Version = 'github-master';
PlugDesc(end).Category = 'sEEG';
@@ -570,7 +650,7 @@
PlugDesc(end+1) = GetStruct('fieldtrip');
PlugDesc(end).Version = 'latest';
PlugDesc(end).AutoUpdate = 0;
- PlugDesc(end).URLzip = 'https://download.fieldtriptoolbox.org/fieldtrip-lite-20220228.zip';
+ PlugDesc(end).URLzip = 'https://download.fieldtriptoolbox.org/fieldtrip-lite-20240405.zip';
PlugDesc(end).URLinfo = 'http://www.fieldtriptoolbox.org';
PlugDesc(end).TestFile = 'ft_defaults.m';
PlugDesc(end).ReadmeFile = 'README';
@@ -580,6 +660,7 @@
PlugDesc(end).GetVersionFcn = 'ft_version';
PlugDesc(end).LoadedFcn = ['global ft_default; ' ...
'ft_default = []; ' ...
+ 'clear ft_defaults; ' ...
'if exist(''filtfilt'', ''file''), ft_default.toolbox.signal=''matlab''; end; ' ...
'if exist(''nansum'', ''file''), ft_default.toolbox.stats=''matlab''; end; ' ...
'if exist(''rgb2hsv'', ''file''), ft_default.toolbox.images=''matlab''; end; ' ...
@@ -589,7 +670,14 @@
PlugDesc(end+1) = GetStruct('spm12');
PlugDesc(end).Version = 'latest';
PlugDesc(end).AutoUpdate = 0;
- PlugDesc(end).URLzip = 'https://www.fil.ion.ucl.ac.uk/spm/download/restricted/eldorado/spm12.zip';
+ switch(OsType)
+ case 'mac64arm'
+ PlugDesc(end).URLzip = 'https://github.com/spm/spm12/archive/refs/heads/maint.zip';
+ PlugDesc(end).Version = 'github-maint';
+ otherwise
+ PlugDesc(end).Version = 'latest';
+ PlugDesc(end).URLzip = 'https://www.fil.ion.ucl.ac.uk/spm/download/restricted/eldorado/spm12.zip';
+ end
PlugDesc(end).URLinfo = 'https://www.fil.ion.ucl.ac.uk/spm/';
PlugDesc(end).TestFile = 'spm.m';
PlugDesc(end).ReadmeFile = 'README.md';
@@ -598,6 +686,43 @@
PlugDesc(end).LoadFolders = {'matlabbatch'};
PlugDesc(end).GetVersionFcn = 'bst_getoutvar(2, @spm, ''Ver'')';
PlugDesc(end).LoadedFcn = 'spm(''defaults'',''EEG'');';
+
+ % === USER DEFINED PLUGINS ===
+ plugJsonFiles = dir(fullfile(bst_get('UserPluginsDir'), 'plugin_*.json'));
+ badJsonFiles = {};
+ plugUserDefNames = {};
+ for ix = 1:length(plugJsonFiles)
+ plugJsonText = fileread(fullfile(plugJsonFiles(ix).folder, plugJsonFiles(ix).name));
+ try
+ PlugUserDesc = bst_jsondecode(plugJsonText);
+ catch
+ badJsonFiles{end+1} = plugJsonFiles(ix).name;
+ continue
+ end
+ % Reshape fields "ExtraMenus"
+ if isfield(PlugUserDesc, 'ExtraMenus') && ~isempty(PlugUserDesc.ExtraMenus) && iscell(PlugUserDesc.ExtraMenus{1})
+ PlugUserDesc.ExtraMenus = cat(2, PlugUserDesc.ExtraMenus{:})';
+ end
+ % Reshape fields "RequiredPlugs"
+ if isfield(PlugUserDesc, 'RequiredPlugs') && ~isempty(PlugUserDesc.RequiredPlugs) && iscell(PlugUserDesc.RequiredPlugs{1})
+ PlugUserDesc.RequiredPlugs = cat(2, PlugUserDesc.RequiredPlugs{:})';
+ end
+ % Check for uniqueness for user-defined plugin
+ if ~ismember(PlugUserDesc.Name, {PlugDesc.Name})
+ plugUserDefNames{end+1} = PlugUserDesc.Name;
+ PlugDesc(end+1) = struct_copy_fields(GetStruct(PlugUserDesc.Name), PlugUserDesc);
+ end
+ end
+ % Print info on user-defined plugins
+ if UserDefVerbose
+ if ~isempty(plugUserDefNames)
+ fprintf(['BST> User-defined plugins... ' strjoin(plugUserDefNames, ' ') '\n']);
+ end
+ for iBad = 1 : length(badJsonFiles)
+ fprintf(['BST> User-defined plugins, error reading .json file... ' badJsonFiles{iBad} '\n']);
+ end
+ end
+
% ================================================================================================================
% Select only one plugin
@@ -626,6 +751,142 @@
end
+%% ===== ADD USER DEFINED PLUGIN DESCRIPTION =====
+function [isOk, errMsg] = AddUserDefDesc(RegMethod, jsonLocation)
+ isOk = 1;
+ errMsg = '';
+ isInteractive = strcmp(RegMethod, 'manual') || nargin < 2 || isempty(jsonLocation);
+
+ % Get json file location from user
+ if ismember(RegMethod, {'file', 'url'}) && isInteractive
+ if strcmp(RegMethod, 'file')
+ jsonLocation = java_getfile('open', 'Plugin description JSON file...', '', 'single', 'files', {{'.json'}, 'Brainstorm plugin description (*.json)', 'JSON'}, 1);
+ elseif strcmp(RegMethod, 'url')
+ jsonLocation = java_dialog('input', 'Enter the URL the plugin description file (.json)', 'Plugin description JSON file...', [], '');
+ end
+ if isempty(jsonLocation)
+ return
+ end
+ res = java_dialog('question', ['Warning: This plugin has not been verified.' 10 ...
+ 'Malicious plugins can alter your database, proceed with caution and only install plugins from trusted sources.' 10 ...
+ 'If any unusual behavior occurs after installation, start by uninstalling the plugins.' 10 ...
+ 'Are you sure you want to proceed?'], ...
+ 'Warning', [], {'yes', 'no'});
+ if strcmp(res, 'no')
+ return
+ end
+ end
+
+ % Get plugin description
+ switch RegMethod
+ case 'file'
+ jsonText = fileread(jsonLocation);
+ try
+ PlugDesc = bst_jsondecode(jsonText);
+ catch
+ errMsg = sprintf(['Could not parse JSON file:' 10 '%s'], jsonLocation);
+ end
+
+ case 'url'
+ % Handle GitHub links, convert the link to load the raw content
+ if strcmp(jsonLocation(1:4),'http') && strcmp(jsonLocation(end-4:end),'.json')
+ if ~isempty(regexp(jsonLocation, '^http[s]*://github.com', 'once'))
+ jsonLocation = strrep(jsonLocation, 'github.com','raw.githubusercontent.com');
+ jsonLocation = strrep(jsonLocation, 'blob/', '');
+ end
+ end
+ jsonText = bst_webread(jsonLocation);
+ try
+ PlugDesc = bst_jsondecode(jsonText);
+ catch
+ errMsg = sprintf(['Could not parse JSON file at:' 10 '%s'], jsonLocation);
+ end
+
+ case 'manual'
+ % Get info for user-defined plugin description from user
+ res = java_dialog('input', { ['Provide the mandatory fields for a user defined Brainstorm plugin ' ...
+ 'See this page for further details: ' ...
+ 'https://neuroimage.usc.edu/brainstorm/Tutorials/Plugins' ...
+ '
' ...
+ 'Plugin name ' ...
+ 'EXAMPLE: bst-users'], ...
+ ['Version ' ...
+ 'EXAMPLE: github-main or 3.1.4'], ...
+ ['URL for zip ' ...
+ 'EXAMPLE: https://github.com/brainstorm-tools/bst-users/archive/refs/heads/master.zip'], ...
+ ['URL for information ' ...
+ 'EXAMPLE: https://github.com/brainstorm-tools/bst-users']}, ...
+ 'User defined plugin', [], {'', '', '', ''});
+ if isempty(res) || any(cellfun(@isempty,res))
+ return
+ end
+ PlugDesc.Name = lower(res{1});
+ PlugDesc.Version = res{2};
+ PlugDesc.URLzip = res{3};
+ PlugDesc.URLinfo = res{4};
+ end
+ if ~isempty(errMsg)
+ bst_error(errMsg);
+ isOk = 0;
+ return;
+ end
+
+ % Validate retrieved plugin description
+ if length(PlugDesc) > 1
+ errMsg = 'JSON file should contain only one plugin description';
+ elseif ~all(ismember({'Name', 'Version', 'URLzip', 'URLinfo'}, fieldnames(PlugDesc)))
+ errMsg = 'Plugin description must contain the fields ''Name'', ''Version'', ''URLzip'' and ''URLinfo''';
+ else
+ PlugDesc.Name = lower(PlugDesc.Name);
+ PlugDescs = GetSupported();
+ if ismember(PlugDesc.Name, {PlugDescs.Name})
+ errMsg = sprintf('Plugin ''%s'' already exist in Brainstorm', PlugDesc.Name);
+ end
+ end
+ if ~isempty(errMsg)
+ bst_error(errMsg);
+ isOk = 0;
+ return;
+ end
+ % Override category
+ PlugDesc.Category = 'User defined';
+
+ % Write validated JSON file
+ pluginJsonFileOut = fullfile(bst_get('UserPluginsDir'), sprintf('plugin_%s.json', file_standardize(PlugDesc.Name)));
+ fid = fopen(pluginJsonFileOut, 'wt');
+ jsonText = bst_jsonencode(PlugDesc, 0);
+ fprintf(fid, jsonText);
+ fclose(fid);
+
+ fprintf(1, 'BST> Plugin ''%s'' was added to ''User defined'' plugins\n', PlugDesc.Name);
+end
+
+
+%% ===== REMOVE USER DEFINED PLUGIN DESCRIPTION =====
+function [isOk, errMsg] = RemoveUserDefDesc(PlugName)
+ isOk = 1;
+ errMsg = '';
+ if nargin < 1 || isempty(PlugName)
+ PlugDescs = GetSupported();
+ PlugDescs = PlugDescs(ismember({PlugDescs.Category}, 'User defined'));
+ PlugName = java_dialog('combo', 'Indicate the name of the plugin to remove:', 'Remove plugin from ''User defined'' list', [], {PlugDescs.Name});
+ end
+ if isempty(PlugName)
+ return
+ end
+ PlugDesc = GetSupported(PlugName);
+ if ~isempty(PlugDesc.Path) || file_exist(bst_fullfile(bst_get('UserPluginsDir'), PlugDesc.Name))
+ [isOk, errMsg] = Uninstall(PlugDesc.Name, 0);
+ end
+ % Delete json file
+ if isOk
+ isOk = file_delete(fullfile(bst_get('UserPluginsDir'), sprintf('plugin_%s.json', file_standardize(PlugDesc.Name))), 1);
+ end
+
+ fprintf(1, 'BST> Plugin ''%s'' was removed from ''User defined'' plugins\n', PlugDesc.Name);
+end
+
+
%% ===== CONFIGURE PLUGIN =====
function Configure(PlugDesc)
switch (PlugDesc.Name)
@@ -766,8 +1027,11 @@ function Configure(PlugDesc)
%% ===== GET GITHUB COMMIT =====
% Get SHA of the GitHub HEAD commit
function sha = GetGithubCommit(URLzip)
+ zipUri = matlab.net.URI(URLzip);
+ % Primary branch name: master or main
+ [~, primaryBranch] = bst_fileparts(char(zipUri.Path(end)));
% Default result
- sha = 'github-master';
+ sha = ['github-', primaryBranch];
% Only available after Matlab 2016b (because of matlab.net.http.RequestMessage)
if (bst_get('MatlabVersion') < 901)
return;
@@ -779,7 +1043,7 @@ function Configure(PlugDesc)
gitUser = char(zipUri.Path(2));
gitRepo = char(zipUri.Path(3));
% Request last commit SHA with GitHub API
- apiUri = matlab.net.URI(['https://api.github.com/repos/' gitUser '/' gitRepo '/commits/master']);
+ apiUri = matlab.net.URI(['https://api.github.com/repos/' gitUser '/' gitRepo '/commits/' primaryBranch]);
request = matlab.net.http.RequestMessage;
request = request.addFields(matlab.net.http.HeaderField('Accept', 'application/vnd.github.VERSION.sha'));
r = send(request, apiUri);
@@ -1100,10 +1364,11 @@ function Configure(PlugDesc)
if ~isempty(p) && ~isempty(strfind(TestFilePath, bst_fileparts(p)))
TestFilePath = [];
end
- % SPM12: Ignore if found embedded in ROAST
+ % SPM12: Ignore if found embedded in ROAST or in FieldTrip
elseif strcmpi(PlugDesc.Name, 'spm12')
p = which('roast.m');
- if ~isempty(p) && ~isempty(strfind(TestFilePath, bst_fileparts(p)))
+ q = which('ft_defaults.m');
+ if (~isempty(p) && ~isempty(strfind(TestFilePath, bst_fileparts(p)))) || (~isempty(q) && ~isempty(strfind(TestFilePath, bst_fileparts(q))))
TestFilePath = [];
end
% Iso2mesh: Ignore if found embedded in ROAST
@@ -1112,6 +1377,12 @@ function Configure(PlugDesc)
if ~isempty(p) && ~isempty(strfind(TestFilePath, bst_fileparts(p)))
TestFilePath = [];
end
+ % easyh5 and jsnirfy: Ignore if found embedded in iso2mesh
+ elseif strcmpi(PlugDesc.Name, 'easyh5') || strcmpi(PlugDesc.Name, 'jsnirfy')
+ p = which('iso2meshver.m');
+ if ~isempty(p) && ~isempty(strfind(TestFilePath, bst_fileparts(p)))
+ TestFilePath = [];
+ end
end
else
TestFilePath = [];
@@ -1216,9 +1487,16 @@ function Configure(PlugDesc)
if ~isempty(errMsg)
return;
end
+ % Check if plugin is supported on Apple silicon
+ OsType = bst_get('OsType', 0);
+ if strcmpi(OsType, 'mac64arm') && ismember(PlugName, PluginsNotSupportAppleSilicon())
+ errMsg = ['Plugin ', PlugName ' is not supported on Apple silicon yet.'];
+ PlugDesc = [];
+ return;
+ end
% Check if there is a URL to download
if isempty(PlugDesc.URLzip)
- errMsg = ['No download URL for ', bst_get('OsType', 0), ': ', PlugName ''];
+ errMsg = ['No download URL for ', OsType, ': ', PlugName ''];
return;
end
% Compiled version
@@ -1275,7 +1553,7 @@ function Configure(PlugDesc)
'
Brainstorm will now install these plugins.' 10 10], 'Plugin manager');
end
for iPlug = 1:length(installPlugs)
- [isInstalled, errMsg] = Install(installPlugs{iPlug}, isInteractive, installPlugs{iPlug});
+ [isInstalled, errMsg] = Install(installPlugs{iPlug}, isInteractive, installVer{iPlug});
if ~isInstalled
errMsg = ['Error processing dependency: ' PlugDesc.RequiredPlugs{iPlug,1} 10 errMsg];
return;
@@ -1460,9 +1738,69 @@ function Configure(PlugDesc)
bst_progress('removeimage');
return;
end
+ % Update plugin description after first load, and delete unwanted files
+ [isOk, errMsg, PlugDesc] = UpdateDescription(PlugDesc, 1);
+ if ~isOk
+ return;
+ end
+ % === SHOW PLUGIN INFO ===
+ % Log install
+ bst_webread(['http://neuroimage.usc.edu/bst/pluglog.php?c=K8Yda7B&plugname=' PlugDesc.Name '&action=install']);
+ % Show plugin information (interactive mode only)
+ if isInteractive
+ % Hide progress bar
+ isProgress = bst_progress('isVisible');
+ if isProgress
+ bst_progress('hide');
+ end
+ % Message box: aknowledgements
+ java_dialog('msgbox', ['Plugin ' PlugName ' was sucessfully installed.
' ...
+ 'This software is not distributed by the Brainstorm developers. ' ...
+ 'Please take a few minutes to read the license information, ' ...
+ 'check the authors'' website and register online if recommended.
' ...
+ 'Cite the authors in your publications if you are using their software.
'], 'Plugin manager');
+ % Show the readme file
+ if ~isempty(PlugDesc.ReadmeFile)
+ view_text(PlugDesc.ReadmeFile, ['Installed plugin: ' PlugName], 1, 1);
+ end
+ % Open the website
+ if ~isempty(PlugDesc.URLinfo)
+ web(PlugDesc.URLinfo, '-browser')
+ end
+ % Restore progress bar
+ if isProgress
+ bst_progress('show');
+ end
+ end
+ % Remove logo
+ bst_progress('removeimage');
+ % Return success
+ isOk = 1;
+end
+
+
+%% ===== UPDATE DESCRIPTION =====
+% USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('UpdateDescription', PlugDesc, doDelete=0)
+function [isOk, errMsg, PlugDesc] = UpdateDescription(PlugDesc, doDelete)
+ isOk = 1;
+ errMsg = '';
+ PlugPath = PlugDesc.Path;
+ PlugName = PlugDesc.Name;
+
+ if nargin < 2
+ doDelete = 0;
+ end
+
+ % Plug in needs to be installed
+ if isempty(bst_plugin('GetInstalled', PlugDesc.Name))
+ isOk = 0;
+ errMsg = ['Cannot update description, plugin ''' PlugDesc.Name ''' needs to be installed'];
+ return
+ end
+
% === DELETE UNWANTED FILES ===
- if ~isempty(PlugDesc.DeleteFiles) && iscell(PlugDesc.DeleteFiles)
+ if doDelete && ~isempty(PlugDesc.DeleteFiles) && iscell(PlugDesc.DeleteFiles)
warning('off', 'MATLAB:RMDIR:RemovedFromPath');
for iDel = 1:length(PlugDesc.DeleteFiles)
if ~isempty(PlugDesc.SubFolder)
@@ -1498,8 +1836,10 @@ function Configure(PlugDesc)
% Get readme and logo
PlugDesc.ReadmeFile = GetReadmeFile(PlugDesc);
PlugDesc.LogoFile = GetLogoFile(PlugDesc);
- % Update plugin.mat after loading
+ % Update plugin.mat
+ excludedFields = {'LoadedFcn', 'UnloadedFcn', 'DownloadedFcn', 'InstalledFcn', 'UninstalledFcn', 'Path', 'isLoaded', 'isManaged'};
PlugDescSave = rmfield(PlugDesc, excludedFields);
+ PlugMatFile = bst_fullfile(PlugDesc.Path, 'plugin.mat');
bst_save(PlugMatFile, PlugDescSave, 'v6');
% === CALLBACK: POST-INSTALL ===
@@ -1528,43 +1868,8 @@ function Configure(PlugDesc)
bst_save(PlugMatFile, PlugDescSave, 'v6');
end
end
-
- % === SHOW PLUGIN INFO ===
- % Log install
- bst_webread(['http://neuroimage.usc.edu/bst/pluglog.php?c=K8Yda7B&plugname=' PlugDesc.Name '&action=install']);
- % Show plugin information (interactive mode only)
- if isInteractive
- % Hide progress bar
- isProgress = bst_progress('isVisible');
- if isProgress
- bst_progress('hide');
- end
- % Message box: aknowledgements
- java_dialog('msgbox', ['Plugin ' PlugName ' was sucessfully installed.
' ...
- 'This software is not distributed by the Brainstorm developers. ' ...
- 'Please take a few minutes to read the license information, ' ...
- 'check the authors'' website and register online if recommended.
' ...
- 'Cite the authors in your publications if you are using their software.
'], 'Plugin manager');
- % Show the readme file
- if ~isempty(PlugDesc.ReadmeFile)
- view_text(PlugDesc.ReadmeFile, ['Installed plugin: ' PlugName], 1, 1);
- end
- % Open the website
- if ~isempty(PlugDesc.URLinfo)
- web(PlugDesc.URLinfo, '-browser')
- end
- % Restore progress bar
- if isProgress
- bst_progress('show');
- end
- end
- % Remove logo
- bst_progress('removeimage');
- % Return success
- isOk = 1;
end
-
%% ===== INSTALL INTERACTIVE =====
% USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('InstallInteractive', PlugName)
function [isOk, errMsg, PlugDesc] = InstallInteractive(PlugName)
@@ -1815,6 +2120,12 @@ function Configure(PlugDesc)
if ~isempty(errMsg)
return;
end
+ % Check if plugin is supported on Apple silicon
+ OsType = bst_get('OsType', 0);
+ if strcmpi(OsType, 'mac64arm') && ismember(PlugDesc.Name, PluginsNotSupportAppleSilicon())
+ errMsg = ['Plugin ', PlugDesc.Name ' is not supported on Apple silicon yet.'];
+ return;
+ end
% Minimum Matlab version
if ~isempty(PlugDesc.MinMatlabVer) && (PlugDesc.MinMatlabVer > 0) && (bst_get('MatlabVersion') < PlugDesc.MinMatlabVer)
strMinVer = sprintf('%d.%d', ceil(PlugDesc.MinMatlabVer / 100), mod(PlugDesc.MinMatlabVer, 100));
@@ -1822,6 +2133,15 @@ function Configure(PlugDesc)
return;
end
+ % === PROCESS DEPENDENCIES ===
+ % Unload incompatible plugins
+ if ~isempty(PlugDesc.UnloadPlugs)
+ for iPlug = 1:length(PlugDesc.UnloadPlugs)
+ % disp(['BST> Unloading incompatible plugin: ' PlugDesc.UnloadPlugs{iPlug}]);
+ Unload(PlugDesc.UnloadPlugs{iPlug}, isVerbose);
+ end
+ end
+
% === ALREADY LOADED ===
% If plugin is already full loaded
if isequal(PlugDesc.isLoaded, 1) && ~isempty(PlugDesc.Path)
@@ -1894,14 +2214,6 @@ function Configure(PlugDesc)
bst_progress('setimage', LogoFile);
end
- % === PROCESS DEPENDENCIES ===
- % Unload incompatible plugins
- if ~isempty(PlugDesc.UnloadPlugs)
- for iPlug = 1:length(PlugDesc.UnloadPlugs)
- % disp(['BST> Unloading incompatible plugin: ' PlugDesc.UnloadPlugs{iPlug}]);
- Unload(PlugDesc.UnloadPlugs{iPlug}, isVerbose);
- end
- end
% Load required plugins
if ~isempty(PlugDesc.RequiredPlugs)
for iPlug = 1:size(PlugDesc.RequiredPlugs,1)
@@ -1944,11 +2256,16 @@ function Configure(PlugDesc)
if isequal(filesep, '\')
subDir = strrep(subDir, '/', '\');
end
- if isdir([PlugHomeDir, filesep, subDir])
+ if ~isempty(dir([PlugHomeDir, filesep, subDir]))
if isVerbose
disp(['BST> Adding plugin ' PlugDesc.Name ' to path: ', PlugHomeDir, filesep, subDir]);
end
- addpath([PlugHomeDir, filesep, subDir]);
+ if regexp(subDir, '\*[/\\]*$')
+ subDir = regexprep(subDir, '\*[/\\]*$', '');
+ addpath(genpath([PlugHomeDir, filesep, subDir]));
+ else
+ addpath([PlugHomeDir, filesep, subDir]);
+ end
end
end
end
@@ -2261,17 +2578,34 @@ function Configure(PlugDesc)
%% ===== MENUS: CREATE =====
-function j = MenuCreate(jMenu, fontSize)
+function j = MenuCreate(jMenu, jPlugsPrev, PlugDesc, fontSize)
import org.brainstorm.icon.*;
% Get all the supported plugins
- PlugDesc = GetSupported();
+ if isempty(PlugDesc)
+ PlugDesc = GetSupported();
+ end
% Get Matlab version
MatlabVersion = bst_get('MatlabVersion');
isCompiled = bst_iscompiled();
- % Submenus
+ % Submenus array
jSub = {};
+ % Generate submenus array from existing menu
+ if ~isCompiled && jMenu.getMenuComponentCount > 0
+ for iItem = 0 : jMenu.getItemCount-1
+ if ~isempty(regexp(jMenu.getMenuComponent(iItem).class, 'JMenu$', 'once'))
+ jSub(end+1,1:2) = {char(jMenu.getMenuComponent(iItem).getText), jMenu.getMenuComponent(iItem)};
+ end
+ end
+ end
+ % Editing an existing menu?
+ if isempty(jPlugsPrev)
+ isNewMenu = 1;
+ j = repmat(struct(), 0);
+ else
+ isNewMenu = 0;
+ j = repmat(jPlugsPrev(1), 0);
+ end
% Process each plugin
- j = repmat(struct(), 0);
for iPlug = 1:length(PlugDesc)
Plug = PlugDesc(iPlug);
% Skip if Matlab is too old
@@ -2282,6 +2616,18 @@ function Configure(PlugDesc)
if isCompiled && (Plug.CompiledStatus == 0)
continue;
end
+ % === Add menus for each plugin ===
+ % One menu per plugin
+ ij = length(j) + 1;
+ j(ij).name = Plug.Name;
+ % Skip if it is already a menu item
+ if ~isNewMenu
+ iPlugPrev = ismember({jPlugsPrev.name}, Plug.Name);
+ if any(iPlugPrev)
+ j(ij) = jPlugsPrev(iPlugPrev);
+ continue
+ end
+ end
% Category=submenu
if ~isempty(Plug.Category)
if isempty(jSub) || ~ismember(Plug.Category, jSub(:,1))
@@ -2294,9 +2640,6 @@ function Configure(PlugDesc)
else
jParent = jMenu;
end
- % One menu per plugin
- ij = length(j) + 1;
- j(ij).name = Plug.Name;
% Compiled and included: Simple static menu
if isCompiled && (Plug.CompiledStatus == 2)
j(ij).menu = gui_component('MenuItem', jParent, [], Plug.Name, [], [], [], fontSize);
@@ -2339,8 +2682,51 @@ function Configure(PlugDesc)
end
end
end
+ % === Remove menus for plugins with description ===
+ if ~isempty(jPlugsPrev)
+ [~, iOld] = setdiff({jPlugsPrev.name}, {PlugDesc.Name});
+ for ix = 1 : length(iOld)
+ % Find category menu component
+ jMenuCat = jPlugsPrev(iOld(ix)).menu.getParent.getInvoker;
+ % Find index in parent
+ iDel = [];
+ for ic = 0 : jMenuCat.getMenuComponentCount-1
+ if jPlugsPrev(iOld(ix)).menu == jMenuCat.getMenuComponent(ic)
+ iDel = ic;
+ break
+ end
+ end
+ % Remove from parent
+ if ~isempty(iDel)
+ jMenuCat.remove(iDel);
+ end
+ end
+ end
+ % Create options for adding user-defined plugins
+ if ~isCompiled && isNewMenu
+ menuCategory = 'User defined';
+ jMenuUserDef = [];
+ for iMenuItem = 0 : jMenu.getItemCount-1
+ if ~isempty(regexp(jMenu.getMenuComponent(iMenuItem).class, 'JMenu$', 'once')) && strcmp(char(jMenu.getMenuComponent(iMenuItem).getText), menuCategory)
+ jMenuUserDef = jMenu.getMenuComponent(iMenuItem);
+ end
+ end
+ if isempty(jMenuUserDef)
+ jMenuUserDef = gui_component('Menu', jMenu, [], menuCategory, IconLoader.ICON_FOLDER_OPEN, [], [], fontSize);
+ end
+ jAddUserDefMan = gui_component('MenuItem', [], [], 'Add manually', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('manual'), fontSize);
+ jAddUserDefFile = gui_component('MenuItem', [], [], 'Add from file', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('file'), fontSize);
+ jAddUserDefUrl = gui_component('MenuItem', [], [], 'Add from URL', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('url'), fontSize);
+ jRmvUserDefMan = gui_component('MenuItem', [], [], 'Remove plugin', IconLoader.ICON_DELETE, [], @(h,ev)RemoveUserDefDesc, fontSize);
+ % Insert "Add" options at the begining of the 'User defined' menu
+ jMenuUserDef.insert(jAddUserDefMan, 0);
+ jMenuUserDef.insert(jAddUserDefFile, 1);
+ jMenuUserDef.insert(jAddUserDefUrl, 2);
+ jMenuUserDef.insert(jRmvUserDefMan, 3);
+ jMenuUserDef.insertSeparator(4);
+ end
% List
- if ~isCompiled
+ if ~isCompiled && isNewMenu
jMenu.addSeparator();
gui_component('MenuItem', jMenu, [], 'List', IconLoader.ICON_EDIT, [], @(h,ev)List('Installed', 1), fontSize);
end
@@ -2348,8 +2734,17 @@ function Configure(PlugDesc)
%% ===== MENUS: UPDATE =====
-function MenuUpdate(jPlugs)
+function MenuUpdate(jMenu, fontSize)
import org.brainstorm.icon.*;
+ global GlobalData
+ % Get installed and supported plugins
+ [PlugsInstalled, PlugsSupported]= GetInstalled();
+ % Get previous menu entries
+ jPlugs = GlobalData.Program.GUI.pluginMenus;
+ % Regenerate plugin menu to look for new plugins
+ jPlugs = MenuCreate(jMenu, jPlugs, PlugsSupported, fontSize);
+ % Update menu entries
+ GlobalData.Program.GUI.pluginMenus = jPlugs;
% If compiled: disable most menus
isCompiled = bst_iscompiled();
% Interface scaling
@@ -2358,9 +2753,9 @@ function MenuUpdate(jPlugs)
for iPlug = 1:length(jPlugs)
j = jPlugs(iPlug);
PlugName = j.name;
+ Plug = PlugsInstalled(ismember({PlugsInstalled.Name}, PlugName));
+ PlugRef = PlugsSupported(ismember({PlugsSupported.Name}, PlugName));
% Is installed?
- PlugRef = GetSupported(PlugName);
- Plug = GetInstalled(PlugName);
if ~isempty(Plug)
isInstalled = 1;
elseif ~isempty(PlugRef)
@@ -2644,7 +3039,18 @@ function Archive(OutputFile)
envPlug = bst_fullfile(envPlugins, PlugDesc(iPlug).Name);
isOk = file_copy(PlugDesc(iPlug).Path, envPlug);
if ~isOk
- error(['Cannot copy folder: "' userProc '" into "' envProc '"']);
+ error(['Cannot copy folder: "' PlugDesc(iPlug).Path '" into "' envProc '"']);
+ end
+ end
+ % Copy user-defined JSON files
+ PlugJson = dir(fullfile(bst_get('UserPluginsDir'), 'plugin_*.json'));
+ for iPlugJson = 1:length(PlugJson)
+ bst_progress('text', ['Copying use-defined plugin JSON file: ' PlugJson(iPlugJson).name '...']);
+ plugJsonFile = bst_fullfile(PlugJson(iPlugJson).folder, PlugJson(iPlugJson).name);
+ envPlugJson = bst_fullfile(envPlugins, PlugJson(iPlugJson).name);
+ isOk = file_copy(plugJsonFile, envPlugJson);
+ if ~isOk
+ error(['Cannot copy file: "' plugJsonFile '" into "' envProc '"']);
end
end
@@ -2739,6 +3145,10 @@ function LinkCatSpm(Action)
if isempty(PlugCat) || ~PlugCat.isLoaded
error('Plugin CAT12 is not loaded.');
end
+ % Return if installation is not complete yet (first load before installation ends)
+ if isempty(PlugCat.InstallDate)
+ return
+ end
% Define source and target for the link
if ~isempty(PlugCat.SubFolder)
linkTarget = bst_fullfile(PlugCat.Path, PlugCat.SubFolder);
@@ -2789,3 +3199,9 @@ function SetProgressLogo(PlugDesc)
end
end
+
+%% ===== NOT SUPPORTED APPLE SILICON =====
+% Return list of plugins not supported on Apple silicon
+function pluginNames = PluginsNotSupportAppleSilicon()
+ pluginNames = { 'duneuro', 'mcxlab-cuda'};
+end
diff --git a/toolbox/core/bst_set.m b/toolbox/core/bst_set.m
index a27e257df..831cb2a55 100644
--- a/toolbox/core/bst_set.m
+++ b/toolbox/core/bst_set.m
@@ -85,6 +85,7 @@ function bst_set( varargin )
% - bst_set('PlotlyCredentials', Username, ApiKey, Domain)
% - bst_set('KlustersExecutable', ExecutablePath)
% - bst_set('ExportBidsOptions'), ExportBidsOptions)
+% - bst_set('Pipelines') Saved Pipelines stored
%
% SEE ALSO bst_get
@@ -134,6 +135,8 @@ function bst_set( varargin )
GlobalData.DataBase.BrainstormDbDir = contextValue;
case 'BrainstormTmpDir'
GlobalData.Preferences.BrainstormTmpDir = contextValue;
+ case 'Pipelines'
+ GlobalData.Processes.Pipelines = contextValue;
%% ==== PROTOCOL ====
case 'iProtocol'
@@ -274,7 +277,7 @@ function bst_set( varargin )
'MagneticExtrapOptions', 'MriOptions', 'ConnectGraphOptions', 'NodelistOptions', 'IgnoreMemoryWarnings', 'SystemCopy', ...
'TimefreqOptions_morlet', 'TimefreqOptions_hilbert', 'TimefreqOptions_fft', 'TimefreqOptions_psd', 'TimefreqOptions_stft', 'TimefreqOptions_plv', ...
'OpenMEEGOptions', 'DuneuroOptions', 'DigitizeOptions', 'PcaOptions', 'CustomColormaps', 'PluginCustomPath', 'BrainSuiteDir', 'PythonExe', ...
- 'GridOptions_headmodel', 'GridOptions_dipfit', 'LastPsdDisplayFunction', 'KlustersExecutable', 'ExportBidsOptions'}
+ 'GridOptions_headmodel', 'GridOptions_dipfit', 'LastPsdDisplayFunction', 'KlustersExecutable', 'ExportBidsOptions', 'ShowHiddenFiles'}
GlobalData.Preferences.(contextName) = contextValue;
case 'ReadOnly'
diff --git a/toolbox/core/bst_startup.m b/toolbox/core/bst_startup.m
index b727b3120..0a4cacf17 100644
--- a/toolbox/core/bst_startup.m
+++ b/toolbox/core/bst_startup.m
@@ -1,4 +1,4 @@
-function bst_startup(BrainstormHomeDir, GuiLevel, BrainstormDbDir)
+function bst_startup(BrainstormHomeDir, GuiLevel, BrainstormDbDir, TemplateName)
% BST_STARTUP: Start a new Brainstorm Session.
%
% USAGE: bst_startup(BrainstormHomeDir, GuiLevel=1, BrainstormDbDir=[])
@@ -7,6 +7,7 @@ function bst_startup(BrainstormHomeDir, GuiLevel, BrainstormDbDir)
% - BrainstormHomeDir : Path to the brainstorm3 folder
% - GuiLevel : -1=server, 0=nogui, 1=normal, 2=autopilot
% - BrainstormDbDir : Database folder to use by default in this session
+% - TemplateName : Default anatomy template
% @=============================================================================
% This function is part of the Brainstorm software:
@@ -35,6 +36,9 @@ function bst_startup(BrainstormHomeDir, GuiLevel, BrainstormDbDir)
if (nargin < 3) || isempty(BrainstormDbDir)
BrainstormDbDir = [];
end
+if (nargin < 4) || isempty(TemplateName)
+ TemplateName = '';
+end
% If version is too old
MatlabVersion = bst_get('MatlabVersion');
if (MatlabVersion < 701)
@@ -148,6 +152,17 @@ function bst_startup(BrainstormHomeDir, GuiLevel, BrainstormDbDir)
disp('BST> Warning: For better graphics, use Matlab >= 2014b');
end
+% Check for New Matlab Desktop (started with R2023a)
+if (MatlabVersion >= 914) && panel_options('isJSDesktop')
+ disp('BST> Warning: Brainstorm is not fully tested and supported on the New Matlab Desktop.');
+end
+
+% Check for Apple silicon (started with R2023b)
+if (MatlabVersion >= 2302) && strcmp(bst_get('OsType', 0), 'mac64arm')
+ disp(['BST> Warning: Running on Apple silicon, some functions and plugins are not supported yet:' 10 ...
+ ' Use Matlab < 2023b or Matlab for Intel processor for full support']);
+end
+
%% ===== FORCE COMPILATION OF SOME INTERFACE FILES =====
if (GuiLevel == 1)
@@ -300,22 +315,28 @@ function bst_startup(BrainstormHomeDir, GuiLevel, BrainstormDbDir)
end
+%% ===== INTERNET CONNECTION =====
+% Check internet connection
+fprintf(1, 'BST> Checking internet connectivity... ');
+[GlobalData.Program.isInternet, onlineRel] = bst_check_internet();
+if GlobalData.Program.isInternet
+ disp('ok');
+else
+ disp('failed');
+end
+
+
%% ===== AUTOMATIC UPDATES =====
-% Automatic updates disabled: do not check for internet connection
+% Automatic updates disabled
if ~bst_get('AutoUpdates')
disp('BST> Warning: Automatic updates are disabled.');
disp('BST> Warning: Make sure your version of Brainstorm is up to date.');
% Matlab is running: check for updates
elseif ~isCompiled && (GuiLevel == 1)
- % Check internect connection
- fprintf(1, 'BST> Checking internet connectivity... ');
- [GlobalData.Program.isInternet, onlineRel] = bst_check_internet();
% If no internet connection
if ~GlobalData.Program.isInternet
- disp('failed');
disp('BST> Could not check for Brainstorm updates.')
else
- disp('ok');
% Determine if release is old (local version > 30 days older than online version)
daysOnline = onlineRel.year*365 + onlineRel.month*30 + onlineRel.day;
daysLocal = localRel.year*365 + localRel.month*30 + localRel.day;
@@ -418,6 +439,11 @@ function bst_startup(BrainstormHomeDir, GuiLevel, BrainstormDbDir)
end
+%% ===== USER-DEFINED PLUGINS =====
+% Print information about user-defined plugins
+bst_plugin('GetSupported', [], 1);
+
+
%% ===== LOAD PLUGINS =====
% Get installed plugins
[InstPlugs, AllPlugs] = bst_plugin('GetInstalled');
@@ -453,10 +479,9 @@ function bst_startup(BrainstormHomeDir, GuiLevel, BrainstormDbDir)
%% ===== INSTALL ANATOMY TEMPLATE =====
-% Download ICBM152 template if missing (e.g. when cloning from GitHub)
+% Download ICBM152 template if missing
TemplateDir = fullfile(BrainstormHomeDir, 'defaults', 'anatomy', 'ICBM152');
-if ~isCompiled && ~exist(TemplateDir, 'file')
- TemplateName = 'ICBM152_2023b';
+if ~isCompiled && ~exist(TemplateDir, 'file') && ~isempty(TemplateName)
isSkipTemplate = 0;
% Template file
ZipFile = bst_fullfile(bst_get('UserDefaultsDir'), 'anatomy', [TemplateName '.zip']);
diff --git a/toolbox/core/bst_systeminfo.m b/toolbox/core/bst_systeminfo.m
new file mode 100644
index 000000000..df6dbd7bc
--- /dev/null
+++ b/toolbox/core/bst_systeminfo.m
@@ -0,0 +1,102 @@
+function systemInfoText = bst_systeminfo(showInfo)
+% BST_SYSTEMNINFO: Get general information about Brainstorm, Matlab and the Computer
+%
+% USAGE: bst_systeminfo(showInfo)
+% INPUTS:
+% - showInfo : 0 (default) only return system info text
+% 1 with GUI, open window with system info
+% without GUI, print information in console
+%
+% @=============================================================================
+% 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: Raymundo Cassani, 2024
+
+% Parse inputs
+if (nargin < 1) || isempty(showInfo)
+ showInfo = 0;
+end
+
+summaryPairs = cell(0,2);
+% == Brainstorm
+summaryPairs = [summaryPairs; {'=== Brainstorm ===', ''}];
+% Version
+bst_version = bst_get('Version');
+summaryPairs = [summaryPairs; {'Version ', bst_version.Version}];
+summaryPairs = [summaryPairs; {'Release ', bst_version.Release}];
+bst_variant = 'source';
+if bst_iscompiled()
+ bst_variant = 'standalone';
+end
+summaryPairs = [summaryPairs; {'Variant ', bst_variant}];
+% Plugins
+pluginTextPairs = cell(0,2);
+pluginTextPairs = [pluginTextPairs, {'Plugins ', 'No installed plugins.'}];
+InstPlugs = bst_plugin('GetInstalled');
+nInstPlugs = length(InstPlugs);
+iPluginRow = 0;
+for ix = 1 : nInstPlugs
+ plugName = InstPlugs(ix).Name;
+ if InstPlugs(ix).isLoaded
+ plugName = [plugName, '*'];
+ end
+ if mod(ix-1, 8) == 0
+ iPluginRow = iPluginRow + 1;
+ pluginTextPairs{iPluginRow, 2} = '';
+ end
+ pluginTextPairs{iPluginRow, 2} = strtrim([pluginTextPairs{iPluginRow, 2}, ' ', plugName]);
+end
+summaryPairs = [summaryPairs; pluginTextPairs];
+summaryPairs = [summaryPairs; {'', ''}];
+
+% == Directories
+summaryPairs = [summaryPairs; {'=== Brainstorm directories ===', ''}];
+summaryPairs = [summaryPairs; {'*** Directory paths may contain sensitive information, check before sharing ***', ''}];
+summaryPairs = [summaryPairs; {'Brainstorm ', bst_get('BrainstormHomeDir')}];
+summaryPairs = [summaryPairs; {'DataBase ', bst_get('BrainstormDbDir')}];
+summaryPairs = [summaryPairs; {'Bst_User ', bst_get('BrainstormUserDir')}];
+summaryPairs = [summaryPairs; {'Temporary ', bst_get('BrainstormTmpDir')}];
+summaryPairs = [summaryPairs; {'', ''}];
+
+% === Matlab and Java
+summaryPairs = [summaryPairs; {'=== Matlab ===', ''}];
+summaryPairs = [summaryPairs; {'Matlab version ', [bst_get('MatlabReleaseName') ' (' num2str(bst_get('MatlabVersion')/100) ')']}];
+summaryPairs = [summaryPairs; {'Java version ', num2str(bst_get('JavaVersion'))}];
+summaryPairs = [summaryPairs; {'', ''}];
+
+% == System
+summaryPairs = [summaryPairs; {'=== System ===', ''}];
+summaryPairs = [summaryPairs; {'OS name ', bst_get('OsName')}];
+summaryPairs = [summaryPairs; {'OS type ', bst_get('OsType')}];
+[memTotal, memAvail] = bst_get('SystemMemory');
+summaryPairs = [summaryPairs; {'Mem total ', [num2str(memTotal) ' MiB']}];
+summaryPairs = [summaryPairs; {'Mem avail ', [num2str(memAvail) ' MiB']}];
+
+% Format string
+iFields = find(~cellfun(@isempty, summaryPairs(:,2)));
+maxField = max([cellfun(@length, summaryPairs(iFields,1))]);
+summaryPairs(iFields,1) = cellfun(@(x) [' ', x, ':', repmat(' ', 1, maxField-length(x))], summaryPairs(iFields,1), 'UniformOutput', 0);
+summaryRows = cellfun(@(x,y) strjoin({x,y}, ' '), summaryPairs(:,1), summaryPairs(:,2), 'UniformOutput', 0);
+systemInfoText = strjoin(summaryRows, char(10));
+if showInfo
+ if bst_get('isGUI')
+ view_text(systemInfoText, 'System info');
+ else
+ fprintf([systemInfoText, '\n']);
+ end
+end
diff --git a/toolbox/core/bst_userstat.m b/toolbox/core/bst_userstat.m
index bc39790af..7d7d4d90c 100644
--- a/toolbox/core/bst_userstat.m
+++ b/toolbox/core/bst_userstat.m
@@ -77,6 +77,9 @@ function bst_userstat(isSave, PlugName)
if isempty(PlugName)
% Read list of users
str = bst_webread('http://neuroimage.usc.edu/bst/get_logs.php?c=J7rTwq');
+ % Replace actions ['i' and 'j'] with 'x' so it is not read as imaginary in textscan
+ str = strrep(str, 'i', 'x');
+ str = strrep(str, 'j', 'x');
% Extract values
c = textscan(str, '%02d%02d%c');
dates = double([c{1}, c{2}]);
@@ -85,19 +88,21 @@ function bst_userstat(isSave, PlugName)
% Create histograms
iUpdate = find((action == 'A') | (action == 'L') | (action == 'D'));
[nUpdate,xUpdate] = hist(dates(iUpdate), length(unique(dates(iUpdate))));
- % Look for all dates in the current year (exclude current month)
- year = 2023;
- iAvg = find((xUpdate >= year) & (xUpdate < year+1));
+ % Look for all dates in last 12 months (exclude current month)
+ t = datetime('today');
+ finRollAvg = t.Year + ((t.Month -1) ./12);
+ iniRollAvg = finRollAvg - 1;
+ iAvg = find((xUpdate >= iniRollAvg) & (xUpdate < finRollAvg));
% Remove invalid data
iBad = ((nUpdate < 100) | (nUpdate > 4000));
nUpdate(iBad) = interp1(xUpdate(~iBad), nUpdate(~iBad), xUpdate(iBad), 'pchip');
% Plot number of downloads
[hFig(end+1), hAxes] = fig_report(xUpdate(1:end-1), nUpdate(1:end-1), 0, ...
[2005, max(xUpdate(1:end-1))], [], ...
- sprintf('Downloads per month: Avg(%d)=%d', year, round(mean(nUpdate(iAvg)))), [], 'Downloads per month', ...
+ sprintf('Downloads per month: 12-month Avg=%d', round(mean(nUpdate(iAvg)))), [], 'Downloads per month', ...
[100, Hs(2) - (length(hFig)+1)*hf], isSave, bst_fullfile(ImgDir, 'download.png'));
% String for MoinMoin website
- fprintf('Number of software downloads per month: (average %d = %s%d/month%s)\r', year, boldMoinMoin, round(mean(nUpdate(iAvg))), boldMoinMoin);
+ fprintf('Number of software downloads per month: (12-month average = %s%d/month%s)\r', boldMoinMoin, round(mean(nUpdate(iAvg))), boldMoinMoin);
end
@@ -123,11 +128,42 @@ function bst_userstat(isSave, PlugName)
% ===== PUBLICATIONS =====
if isempty(PlugName)
- % Hard coded list of publications
- year = [2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022];
- nPubli = [ 2 2 1 1 3 5 5 11 10 20 20 32 38 55 78 94 133 214 224 290 382 393 478];
- nPubliCurYear = 118; % Updated March 2023
- strPubDate = 'Up to March 2023';
+ % Up to December 2022, citation count was manually curated
+ year_man = [2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022];
+ nPubli_man = [ 2 2 1 1 3 5 5 11 10 20 20 32 38 55 78 94 133 214 224 290 382 393 478];
+ % nPubliCurYear = 118; % January to March 2023
+ % strPubDate = 'Up to March 2023';
+
+ % From January 2023 onwards, citation count is obtained from Google Scholar, and posted in:
+ % https://neuroimage.usc.edu/bst/citations_count.html
+ % Read list of users
+ str = bst_webread('https://neuroimage.usc.edu/bst/citations_count.html');
+ % Extract values YYYY#nPubli
+ year_Npubli_gs = regexp(str, '[0-9]+#[0-9]+', 'match');
+ year_Npubli_gs = sort(year_Npubli_gs);
+ year_gs = [];
+ nPubli_gs = [];
+ for iRow = 1 : length(year_Npubli_gs)
+ C = textscan(year_Npubli_gs{iRow}, '%d#%d');
+ if C{1} >= 2023
+ year_gs(end+1) = C{1};
+ nPubli_gs(end+1) = C{2};
+ end
+ end
+ % Publications current year (last year in array)
+ nPubliCurYear = nPubli_gs(end);
+ % Remove current year from graph
+ nPubli_gs(end) = [];
+ year_gs(end) = [];
+
+ % Get month of last update
+ dateCount = regexp(str, 'UpdatedOn#([^<]*)', 'tokens', 'once');
+ C = str_split(dateCount{1}, '-');
+ strPubDate = sprintf('Up to %s %s', C{2}, C{3});
+
+ % Aggregate manual and automatic citation counts
+ year = [year_man, year_gs];
+ nPubli = [nPubli_man, nPubli_gs];
% Plot figure
hFig(end+1) = fig_report(year, nPubli, 1, ...
[2000 max(year)], [], ...
@@ -143,6 +179,11 @@ function bst_userstat(isSave, PlugName)
% Download statistics
url = sprintf('https://neuroimage.usc.edu/bst/pluglog.php?c=K8Yda7B&plugname=%s&action=install&list=1', PlugName);
str = bst_webread(url);
+
+ if isempty(str)
+ bst_progress('stop');
+ return;
+ end
% Process report
str = str_split(str, char(10));
nTotal = length(str);
diff --git a/toolbox/db/db_add.m b/toolbox/db/db_add.m
index 2751860c6..b877f083c 100644
--- a/toolbox/db/db_add.m
+++ b/toolbox/db/db_add.m
@@ -91,8 +91,8 @@
end
% Surfaces subtypes
if ismember(fileType, {'fibers', 'fem'})
- fileType = 'tess';
fileSubType = [fileType, '_'];
+ fileType = 'tess';
end
isAnatomy = ismember(fileType, {'subjectimage', 'tess'});
% Spikes: file tag is data
diff --git a/toolbox/db/db_group_conditions.m b/toolbox/db/db_group_conditions.m
index 87dc67004..4183b0104 100644
--- a/toolbox/db/db_group_conditions.m
+++ b/toolbox/db/db_group_conditions.m
@@ -107,6 +107,7 @@ function db_group_conditions( ConditionsPaths, newConditionName )
% === PROCESS ALL STUDIES ===
isChanCopied = 0;
oldCondPath = {};
+ badDataFiles = {};
for iStud = 1:length(iCondStudies)
% Increment progressbar
bst_progress('inc', 1);
@@ -123,6 +124,8 @@ function db_group_conditions( ConditionsPaths, newConditionName )
dirStudy = bst_fullfile(ProtocolInfo.STUDIES, bst_fileparts(sStudy.FileName));
studyFiles = dir(dirStudy);
iNonTrial = 1;
+ % Bad trials
+ badDataFiles = [badDataFiles, {sStudy.Data(find([sStudy.Data.BadTrial])).FileName}];
% Copy all files in the target study
for iFile = 1:length(studyFiles)
% Ignore if directory
@@ -157,6 +160,11 @@ function db_group_conditions( ConditionsPaths, newConditionName )
srcFilename = bst_fullfile(dirStudy, studyFiles(iFile).name);
% Move file physically
file_move(srcFilename, destFilename);
+ % Get new FileName for bad trial
+ iBadDataFile = find(ismember(badDataFiles, file_short(srcFilename)));
+ if ~isempty(iBadDataFile)
+ badDataFiles{iBadDataFile} = file_short(destFilename);
+ end
end
end
end
@@ -182,6 +190,8 @@ function db_group_conditions( ConditionsPaths, newConditionName )
end
% Reload modified studies
db_reload_studies(iAllDestStudies);
+% Update bad trials info
+process_detectbad('SetTrialStatus', badDataFiles, 1);
% Repaint node
panel_protocols('UpdateNode', 'Study', iAllDestStudies);
% Save database
diff --git a/toolbox/db/db_set_channel.m b/toolbox/db/db_set_channel.m
index d5d0764bc..d866171e4 100644
--- a/toolbox/db/db_set_channel.m
+++ b/toolbox/db/db_set_channel.m
@@ -183,12 +183,10 @@
[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
+ if isUserCancel || isempty(ChannelMat)
ChannelAlign = 0;
- elseif ~isempty(ChannelMat)
+ elseif ChannelAlign < 2
ChannelAlign = 2;
- else
- ChannelAlign = 0;
end
end
end
diff --git a/toolbox/db/db_template.m b/toolbox/db/db_template.m
index 058bccb71..2166c7774 100644
--- a/toolbox/db/db_template.m
+++ b/toolbox/db/db_template.m
@@ -102,6 +102,7 @@
'Comment', '', ...
'Vertices', [], ...
'Faces', [], ...
+ 'Color', [], ...
'VertConn', [], ...
'VertNormals', [], ...
'Curvature', [], ...
@@ -272,7 +273,8 @@
'Components', [], ...
'CompMask', [], ...
'Status', 0, ... % 0: not applied; 1: applied on the fly; 2: saved in the file, not revertible : ADDITIONAL VALUES = EEG REFERENCES
- 'SingVal', []);
+ 'SingVal', [], ...
+ 'Method', '');
case 'matrixmat'
template = struct(...
@@ -605,7 +607,8 @@
'Atlas', [], ...
'StatClusters', [], ...
'StatThreshUnder', [], ...
- 'StatThreshOver', []);
+ 'StatThreshOver', [], ...
+ 'Function', '');
case 'loadeddipoles'
template = struct(...
@@ -671,6 +674,7 @@
'Comment', '', ...
'Vertices', [], ...
'Faces', [], ...
+ 'Color', [], ...
'VertConn', [], ...
'VertNormals', [], ...
'VertArea', [], ...
@@ -734,7 +738,12 @@
'ElecDiameter', [], ...
'ElecLength', [], ...
'Visible', 1);
-
+
+ case 'intracontact'
+ template = struct(...
+ 'Name', '', ... % Identification)
+ 'Loc', []); % [3x1] position for contact
+
case 'dataset'
template = struct(...
'DataFile', '', ...
@@ -1048,11 +1057,12 @@
'SizeThreshold', 1, ... % Threshold to apply to color coding of data values
'DataLimitValue', [], ... % Relative limits for colormapping
'CutsPosition', [0 0 0], ... % Position of the three orthogonal MRI slices
- 'Resect', 'none', ... % Either [x,y,z] resect values, or {'left', 'right', 'none'}
+ 'Resect', [], ... % 2 cells: Resect values [x,y,z] and resect sections {'left', 'right', 'struct', 'none'}
'MipAnatomy', [], ... % 3 cells: Maximum intensity power in each direction (MRI amplitudes)
'MipFunctional', [], ... % 3 cells: Maximum intensity power in each direction (sources amplitudes)
'StatThreshOver', [], ...
'StatThreshUnder', []);
+ template.Resect = {[0,0,0], 'none'};
template.MipAnatomy = cell(3,1);
template.MipFunctional = cell(3,1);
diff --git a/toolbox/db/private/db_parse_subject.m b/toolbox/db/private/db_parse_subject.m
index 7a41b17ce..8dce8f8db 100644
--- a/toolbox/db/private/db_parse_subject.m
+++ b/toolbox/db/private/db_parse_subject.m
@@ -202,7 +202,9 @@
% ==== ANATOMY ====
% By default : use the first anatomy in list (which is not a volume atlas)
- if ~isempty(sSubject(1).Anatomy)
+ if ~isempty(sSubject(1).Anatomy) && isempty(strfind(sSubject(1).Anatomy(1).FileName, '_volatlas')) ...
+ && isempty(strfind(sSubject(1).Anatomy(1).FileName, '_tissues')) ...
+ && isempty(strfind(sSubject(1).Anatomy(1).FileName, '_volct'))
sSubject(1).iAnatomy = 1;
else
sSubject(1).iAnatomy = [];
diff --git a/toolbox/forward/panel_headmodel.m b/toolbox/forward/panel_headmodel.m
index 8682b018b..611466f45 100644
--- a/toolbox/forward/panel_headmodel.m
+++ b/toolbox/forward/panel_headmodel.m
@@ -29,7 +29,7 @@
%% ===== CREATE PANEL =====
-function [bstPanelNew, panelName] = CreatePanel(isMeg, isEeg, isEcog, isSeeg, isMixed) %#ok
+function [bstPanelNew, panelName] = CreatePanel(isMeg, isEeg, isEcog, isSeeg, isMixed, isNirs) %#ok
panelName = 'HeadmodelOptions';
% Java initializations
import java.awt.*;
@@ -41,6 +41,10 @@
isSeeg = 0;
isMixed = 0;
end
+ % Call without 'isNIRS'
+ if (nargin < 6)
+ isNirs = 0;
+ end
% Create main main panel
jPanelNew = gui_river([8,10], [0,15,20,15]);
@@ -124,6 +128,17 @@
jCheckMethodSEEG = [];
jComboMethodSEEG = [];
end
+ % === NIRS ===
+ if isNirs
+ % Checkbox
+ jCheckMethodNIRS = gui_component('CheckBox', jPanelMethod, 'br', 'NIRS: ');
+ jCheckMethodNIRS.setSelected(0);
+ jCheckMethodNIRS.setEnabled(0)
+ % Label
+ gui_component('Label', jPanelMethod, 'tab hfill', ['To compute head model for NIRS, use process: ' ...
+ 'NIRS > Sources > Compute head model from fluence ' ...
+ '(NIRSTORM plugin is required)']);
+ end
% Attach sub panel to NewPanel
jPanelNew.add('br hfill', jPanelMethod);
@@ -329,15 +344,16 @@ function UpdateComment(varargin)
isEeg = any(strcmpi(sStudies(i).Channel.DisplayableSensorTypes, 'EEG'));
isEcog = any(strcmpi(sStudies(i).Channel.DisplayableSensorTypes, 'ECOG'));
isSeeg = any(strcmpi(sStudies(i).Channel.DisplayableSensorTypes, 'SEEG'));
- isNIRS = any(strcmpi(sStudies(i).Channel.DisplayableSensorTypes, 'NIRS'));
+ isNirs = any(strcmpi(sStudies(i).Channel.DisplayableSensorTypes, 'NIRS'));
end
end
% Check that at least one modality is available
- if isNIRS
- errMessage = ['To compute head model for NIRS, use process:' 10 'NIRS > Sources > Compute head model from fluence' 10 'NIRSTORM plugin is required'];
- return;
- elseif ~isMeg && ~isEeg && ~isEcog && ~isSeeg
- errMessage = 'No valid sensor types to estimate a head model.';
+ if ~isMeg && ~isEeg && ~isEcog && ~isSeeg
+ if isNirs
+ errMessage = ['To compute head model for NIRS, use process:' 10 'NIRS > Sources > Compute head model from fluence' 10 'NIRSTORM plugin is required'];
+ else
+ errMessage = 'No valid sensor types to estimate a head model.';
+ end
return;
end
% Check if the first subject has a "Source model" atlas
@@ -356,7 +372,7 @@ function UpdateComment(varargin)
end
% Display options panel
if (nargin < 2) || isempty(sMethod)
- sMethod = gui_show_dialog('Compute head model', @panel_headmodel, 1, [], isMeg, isEeg, isEcog, isSeeg, isMixed);
+ sMethod = gui_show_dialog('Compute head model', @panel_headmodel, 1, [], isMeg, isEeg, isEcog, isSeeg, isMixed, isNirs);
if isempty(sMethod)
return;
end
diff --git a/toolbox/gui/figure_3d.m b/toolbox/gui/figure_3d.m
index 176571088..a07e434ad 100644
--- a/toolbox/gui/figure_3d.m
+++ b/toolbox/gui/figure_3d.m
@@ -514,6 +514,8 @@ function FigureMouseMoveCallback(hFig, varargin)
posXYZ = [NaN, NaN, NaN];
posXYZ(moveAxis) = newPos;
panel_surface('PlotMri', hFig, posXYZ, 1);
+ % Update sliders in surface panel
+ panel_surface('UpdateSurfaceProperties');
end
end
end
@@ -609,9 +611,24 @@ function FigureMouseUpCallback(hFig, varargin)
% === SELECTING POINT ===
elseif isSelectingCoordinates
- % Selecting from Coordinates panel
- if gui_brainstorm('isTabVisible', 'Coordinates')
- panel_coordinates('SelectPoint', hFig);
+ % Selecting from Coordinates or iEEG panels
+ if gui_brainstorm('isTabVisible', 'Coordinates') || gui_brainstorm('isTabVisible', 'iEEG')
+ if gui_brainstorm('isTabVisible', 'iEEG')
+ % For SEEG, making sure centroid calculation for plotting contacts is active
+ [iTess, TessInfo, hFig, sSurf] = panel_surface('GetSurface', hFig, [], 'Other');
+ if ~isempty(sSurf)
+ iIsoSurf = find(cellfun(@(x) ~isempty(regexp(x, '_isosurface', 'match')), {sSurf.FileName}));
+ if ~isempty(iIsoSurf)
+ panel_coordinates('SelectPoint', hFig, 0, 1);
+ else
+ panel_coordinates('SelectPoint', hFig);
+ end
+ else
+ panel_coordinates('SelectPoint', hFig);
+ end
+ else
+ panel_coordinates('SelectPoint', hFig);
+ end
% Selecting fiducials linked with MRI viewer
else
hView3DHeadFig = findobj(0, 'Type', 'Figure', 'Tag', 'View3DHeadFig', '-depth', 1);
@@ -820,7 +837,9 @@ function FigureMouseUpCallback(hFig, varargin)
% If there are intra electrodes defined, and if the channels are SEEG/ECOG: try to select the electrode in panel_ieeg
if ~isempty(GlobalData.DataSet(iDS).IntraElectrodes) && all(~cellfun(@isempty, {GlobalData.DataSet(iDS).Channel(iSelChan).Group}))
selGroup = unique({GlobalData.DataSet(iDS).Channel(iSelChan).Group});
+ % Highlight the electrode and contacts
panel_ieeg('SetSelectedElectrodes', selGroup);
+ panel_ieeg('SetSelectedContacts', SelChan);
end
end
end
@@ -954,7 +973,7 @@ function FigureMouseWheelCallback(hFig, event, target)
%% ===== KEYBOARD CALLBACK =====
function FigureKeyPressedCallback(hFig, keyEvent)
- global GlobalData TimeSliderMutex;
+ global GlobalData TimeSliderMutex Digitize;
% Prevent multiple executions
hAxes = findobj(hFig, '-depth', 1, 'Tag', 'Axes3D');
set([hFig hAxes], 'BusyAction', 'cancel');
@@ -1075,7 +1094,19 @@ function FigureKeyPressedCallback(hFig, keyEvent)
case 'a'
if ismember('control', keyEvent.Modifier)
ViewAxis(hFig);
- end
+ end
+ % C : Collect point
+ case 'c'
+ % for 3DScanner
+ if gui_brainstorm('isTabVisible', 'Digitize') && strcmpi(Digitize.Type, '3DScanner')
+ % Get Digitize options
+ DigitizeOptions = bst_get('DigitizeOptions');
+ panel_fun = @panel_digitize;
+ if isfield(DigitizeOptions, 'Version') && strcmpi(DigitizeOptions.Version, '2024')
+ panel_fun = @panel_digitize_2024;
+ end
+ panel_fun('ManualCollect_Callback');
+ end
% CTRL+D : Dock figure
case 'd'
if ismember('control', keyEvent.Modifier)
@@ -1168,6 +1199,20 @@ function FigureKeyPressedCallback(hFig, keyEvent)
% M : Jump to maximum
case 'm'
JumpMaximum(hFig);
+ % CTRL+P : Toggle point selection mode
+ case 'p'
+ if ismember('control', keyEvent.Modifier)
+ tmp = bst_get('PanelControls', 'Coordinates');
+ if isempty(tmp)
+ gui_brainstorm('ShowToolTab', 'Coordinates');
+ end
+ tmp = bst_get('PanelControls', 'iEEG');
+ if ~isempty(tmp)
+ gui_brainstorm('ShowToolTab', 'iEEG');
+ end
+ pause(0.01);
+ panel_coordinates('SetSelectionState', ~panel_coordinates('GetSelectionState'));
+ end
% CTRL+R : Recordings time series
case 'r'
if ismember('control', keyEvent.Modifier) && ~isempty(GlobalData.DataSet(iDS).DataFile) && ~strcmpi(FigureId.Modality, 'MEG GRADNORM')
@@ -1282,7 +1327,11 @@ function ResetView(hFig)
% 2D LAYOUT: separate function
if strcmpi(GlobalData.DataSet(iDS).Figure(iFig).Id.SubType, '2DLayout')
GlobalData.DataSet(iDS).Figure(iFig).Handles.DisplayFactor = 1;
- GlobalData.Preferences.TopoLayoutOptions.TimeWindow = abs(GlobalData.UserTimeWindow.Time(2) - GlobalData.UserTimeWindow.Time(1)) .* [-1, 1];
+ if ~getappdata(hFig, 'isStaticFreq')
+ GlobalData.Preferences.TopoLayoutOptions.FreqWindow = [GlobalData.UserFrequencies.Freqs(1), GlobalData.UserFrequencies.Freqs(end)];
+ else
+ GlobalData.Preferences.TopoLayoutOptions.TimeWindow = abs(GlobalData.UserTimeWindow.Time(2) - GlobalData.UserTimeWindow.Time(1)) .* [-1, 1];
+ end
figure_topo('UpdateTopo2dLayout', iDS, iFig);
return
% 3D figures
@@ -1411,13 +1460,12 @@ function SetStandardView(hFig, viewNames)
end
end
-
%% ===== GET COORDINATES =====
function GetCoordinates(varargin)
% Show Coordinates panel
gui_show('panel_coordinates', 'JavaWindow', 'Get coordinates', [], 0, 1, 0);
- % Start point selection
- panel_coordinates('SetSelectionState', 1);
+ % Toggle point selection mode
+ panel_coordinates('SetSelectionState', ~panel_coordinates('GetSelectionState'));
end
@@ -1605,6 +1653,10 @@ function DisplayFigurePopup(hFig)
jItem = gui_component('MenuItem', jPopup, [], 'View sources', IconLoader.ICON_RESULTS, [], @(h,ev)bst_figures('ViewResults',hFig));
jItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_MASK));
end
+ % === VIEW SPECTRUM ===
+ if strcmpi(FigureType, 'Topography') && strcmpi(GlobalData.DataSet(iDS).Figure(iFig).Id.SubType, '2DLayout') && getappdata(hFig, 'isStatic')
+ jItem = gui_component('MenuItem', jPopup, [], [Modality ' Spectrum'], IconLoader.ICON_SPECTRUM, [], @(h,ev)view_spectrum(TfFile, 'Spectrum'));
+ end
% === VIEW PAC/TIME-FREQ ===
if strcmpi(FigureType, 'Topography') && ~isempty(SelChan) && ~isempty(Modality) && (Modality(1) ~= '$')
if ~isempty(strfind(TfFile, '_pac_fullmaps'))
@@ -1629,7 +1681,11 @@ function DisplayFigurePopup(hFig)
TopoLayoutOptions = bst_get('TopoLayoutOptions');
% Create menu
jMenu = gui_component('Menu', jPopup, [], '2DLayout options', IconLoader.ICON_2DLAYOUT);
- gui_component('MenuItem', jMenu, [], 'Set time window...', [], [], @(h,ev)figure_topo('SetTopoLayoutOptions', 'TimeWindow'));
+ if ~getappdata(hFig, 'isStatic')
+ gui_component('MenuItem', jMenu, [], 'Set time window...', [], [], @(h,ev)figure_topo('SetTopoLayoutOptions', 'TimeWindow'));
+ else
+ gui_component('MenuItem', jMenu, [], 'Set frequency window...', [], [], @(h,ev)figure_topo('SetTopoLayoutOptions', 'FreqWindow'));
+ end
jItem = gui_component('CheckBoxMenuItem', jMenu, [], 'White background', [], [], @(h,ev)figure_topo('SetTopoLayoutOptions', 'WhiteBackground', ~TopoLayoutOptions.WhiteBackground));
jItem.setSelected(TopoLayoutOptions.WhiteBackground);
jItem = gui_component('CheckBoxMenuItem', jMenu, [], 'Show reference lines', [], [], @(h,ev)figure_topo('SetTopoLayoutOptions', 'ShowRefLines', ~TopoLayoutOptions.ShowRefLines));
@@ -1939,7 +1995,9 @@ function DisplayFigurePopup(hFig)
% ==== MENU: GET COORDINATES ====
if ~strcmpi(FigureType, 'Topography')
- gui_component('MenuItem', jPopup, [], 'Get coordinates...', IconLoader.ICON_SCOUT_NEW, [], @GetCoordinates);
+ jItem = gui_component('checkboxmenuitem', jPopup, [], 'Get coordinates...', IconLoader.ICON_SCOUT_NEW, [], @GetCoordinates);
+ jItem.setSelected(panel_coordinates('GetSelectionState'));
+ jItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_MASK));
end
% ==== MENU: SNAPSHOT ====
@@ -1949,8 +2007,8 @@ function DisplayFigurePopup(hFig)
DefaultOutputDir = LastUsedDirs.ExportImage;
% Is there a time window defined
isTime = ~isempty(GlobalData) && ~isempty(GlobalData.UserTimeWindow.CurrentTime) && ~isempty(GlobalData.UserTimeWindow.Time) ...
- && (~isempty(DataFile) || ~isempty(ResultsFile) || ~isempty(Dipoles) || ~isempty(TfFile));
- isFreq = ~isempty(GlobalData) && ~isempty(GlobalData.UserFrequencies.iCurrentFreq) && ~isempty(TfFile);
+ && (~isempty(DataFile) || ~isempty(ResultsFile) || ~isempty(Dipoles) || ~isempty(TfFile)) && ~getappdata(hFig, 'isStatic');
+ isFreq = ~isempty(GlobalData) && ~isempty(GlobalData.UserFrequencies.iCurrentFreq) && ~isempty(TfFile) && ~getappdata(hFig, 'isStaticFreq');
% === SAVE AS IMAGE ===
jItem = gui_component('MenuItem', jMenuSave, [], 'Save as image', IconLoader.ICON_SAVE, [], @(h,ev)out_figure_image(hFig));
jItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, KeyEvent.CTRL_MASK));
@@ -2024,6 +2082,12 @@ function DisplayFigurePopup(hFig)
gui_component('MenuItem', jMenuSave, [], 'Time contact sheet: Sagittal', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'time', 'x', DefaultOutputDir));
gui_component('MenuItem', jMenuSave, [], 'Time contact sheet: Axial', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'time', 'z', DefaultOutputDir));
end
+ if isFreq
+ jMenuSave.addSeparator();
+ gui_component('MenuItem', jMenuSave, [], 'Frequency contact sheet: Coronal', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'freq', 'y', DefaultOutputDir));
+ gui_component('MenuItem', jMenuSave, [], 'Frequency contact sheet: Sagittal', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'freq', 'x', DefaultOutputDir));
+ gui_component('MenuItem', jMenuSave, [], 'Frequency contact sheet: Axial', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'freq', 'z', DefaultOutputDir));
+ end
jMenuSave.addSeparator();
gui_component('MenuItem', jMenuSave, [], 'Volume contact sheet: Coronal', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'volume', 'y', DefaultOutputDir));
gui_component('MenuItem', jMenuSave, [], 'Volume contact sheet: Sagittal', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'volume', 'x', DefaultOutputDir));
@@ -2756,12 +2820,17 @@ function UpdateSurfaceColor(hFig, iTess)
SulciMap = zeros(TessInfo(iTess).nVertices, 1);
end
% Compute RGB values
- FaceVertexCdata = BlendAnatomyData(SulciMap, ... % Anatomy: Sulci map
- TessInfo(iTess).AnatomyColor([1,end], :), ... % Anatomy: color
- DataSurf, ... % Data: values map
- TessInfo(iTess).DataLimitValue, ... % Data: limit value
- TessInfo(iTess).DataAlpha,... % Data: transparency
- sColormap); % Colormap
+ if ~isempty(regexp(TessInfo(iTess).SurfaceFile, 'tess_textured', 'match'))
+ FaceVertexCdata = TessInfo(iTess).AnatomyColor(TessInfo(iTess).nVertices+1:end, :);
+ else
+ FaceVertexCdata = BlendAnatomyData(SulciMap, ... % Anatomy: Sulci map
+ TessInfo(iTess).AnatomyColor([1,end], :), ... % Anatomy: color
+ DataSurf, ... % Data: values map
+ TessInfo(iTess).DataLimitValue, ... % Data: limit value
+ TessInfo(iTess).DataAlpha,... % Data: transparency
+ sColormap); % Colormap
+ end
+
% Edge display : on/off
if ~TessInfo(iTess).SurfShowEdges
EdgeColor = 'none';
@@ -2861,7 +2930,6 @@ function UpdateSurfaceColor(hFig, iTess)
end
end
-
%% ===== PLOT 3D ELECTRODES =====
function [hElectrodeGrid, ChanLoc] = PlotSensors3D(iDS, iFig, Channel, ChanLoc, TopoType) %#ok
global GlobalData;
@@ -3339,7 +3407,7 @@ function UpdateSurfaceAlpha(hFig, iTess)
% Apply current smoothing
SmoothSurface(hFig, iTess, Surface.SurfSmoothValue);
% Apply structures selection
- if isequal(Surface.Resect, 'struct')
+ if isequal(Surface.Resect{2}, 'struct')
SetStructLayout(hFig, iTess);
end
% Get surfaces vertices
@@ -3355,7 +3423,7 @@ function UpdateSurfaceAlpha(hFig, iTess)
FaceVertexAlphaData = ones(length(sSurf.Faces),1) * (1-Surface.SurfAlpha);
% ===== HEMISPHERE SELECTION (CHAR) =====
- if ischar(Surface.Resect) && ~strcmpi(Surface.Resect, 'none')
+ if ischar(Surface.Resect{2}) && ~strcmpi(Surface.Resect{2}, 'none')
% Detect hemispheres
if strcmpi(Surface.Name, 'FEM')
isConnected = 1;
@@ -3365,13 +3433,15 @@ function UpdateSurfaceAlpha(hFig, iTess)
% If there is no separation between left and right: use the numeric split
if isConnected
iHideVert = [];
- switch (Surface.Resect)
- case 'right', Surface.Resect = [0 0.0000001 0];
- case 'left', Surface.Resect = [0 -0.0000001 0];
+ switch (Surface.Resect{2})
+ case 'right'
+ Surface.Resect{1}(2) = max( 0.0000001, Surface.Resect{1}(2));
+ case 'left'
+ Surface.Resect{1}(2) = min(-0.0000001, Surface.Resect{1}(2));
end
% If there is a structural separation between left and right: usr
else
- switch (Surface.Resect)
+ switch (Surface.Resect{2})
case 'right', iHideVert = lH;
case 'left', iHideVert = rH;
otherwise, iHideVert = [];
@@ -3385,7 +3455,7 @@ function UpdateSurfaceAlpha(hFig, iTess)
end
% ===== RESECT (DOUBLE) =====
- if isnumeric(Surface.Resect) && (length(Surface.Resect) == 3) && (~all(Surface.Resect == 0) || strcmpi(Surface.Name, 'FEM'))
+ if isnumeric(Surface.Resect{1}) && (length(Surface.Resect{1}) == 3) && (~all(Surface.Resect{1} == 0) || strcmpi(Surface.Name, 'FEM'))
% Regular triangular surface
if ~strcmpi(Surface.Name, 'FEM')
iNoModif = [];
@@ -3393,12 +3463,12 @@ function UpdateSurfaceAlpha(hFig, iTess)
meanVertx = mean(Vertices, 1);
maxVertx = max(abs(Vertices), [], 1);
% Limit values
- resectVal = Surface.Resect .* maxVertx + meanVertx;
+ resectVal = Surface.Resect{1} .* maxVertx + meanVertx;
% Get vertices that are kept in all the cuts
for iCoord = 1:3
- if Surface.Resect(iCoord) > 0
+ if Surface.Resect{1}(iCoord) > 0
iNoModif = union(iNoModif, find(Vertices(:,iCoord) < resectVal(iCoord)));
- elseif Surface.Resect(iCoord) < 0
+ elseif Surface.Resect{1}(iCoord) < 0
iNoModif = union(iNoModif, find(Vertices(:,iCoord) > resectVal(iCoord)));
end
end
@@ -3423,7 +3493,7 @@ function UpdateSurfaceAlpha(hFig, iTess)
% For the projected vertices: get the distance from each cut
distToCut = abs(Vertices(iVerticesToProject, :) - repmat(resectVal, [length(iVerticesToProject), 1]));
% Set the distance to the cuts that are not required to infinite
- distToCut(:,(Surface.Resect == 0)) = Inf;
+ distToCut(:,(Surface.Resect{1} == 0)) = Inf;
% Get the closest cut
[minDist, closestCut] = min(distToCut, [], 2);
@@ -3452,7 +3522,7 @@ function UpdateSurfaceAlpha(hFig, iTess)
else
% Create a surface for the outside surface of this tissue
Elements = get(Surface.hPatch, 'UserData');
- Faces = tess_voledge(Vertices, Elements, Surface.Resect);
+ Faces = tess_voledge(Vertices, Elements, Surface.Resect{1});
% Update patch
set(Surface.hPatch, 'Faces', Faces);
end
@@ -3993,18 +4063,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/gui/figure_connect.m b/toolbox/gui/figure_connect.m
index 211953aa4..008003fa3 100644
--- a/toolbox/gui/figure_connect.m
+++ b/toolbox/gui/figure_connect.m
@@ -52,6 +52,26 @@
'WindowButtonUpFcn', @FigureMouseUpCallback, ...
'WindowScrollWheelFcn', @(h, ev)FigureMouseWheelCallback(h, ev), ...
bst_get('ResizeFunction'), @(h, ev)ResizeCallback(h, ev));
+
+ % === CREATE AXES ===
+ hAxeT = axes('Parent', hFig, ...
+ 'Units', 'normalized', ...
+ 'Position', [0 .95 1 .05], ...
+ 'Tag', 'AxesTitle', ...
+ 'FontName', 'Default', ...
+ 'Visible', 'off', ...
+ 'BusyAction', 'queue', ...
+ 'Interruptible', 'off');
+
+ hTxtT = text(0.5, 0.5, '', ...
+ 'Parent', hAxeT, ...
+ 'color', [1,1,1], ...
+ 'Units', 'normalized', ...
+ 'FontSize', 1.1 * bst_get('FigFont') , ... % title() is 1.1 FontSize
+ 'FontWeight', 'bold', ...
+ 'HorizontalAlignment', 'center', ...
+ 'Tag', 'TextTitle', ...
+ 'ButtonDownFcn', @TitleButtonDownFcn);
% === CREATE AXES ===
hAxes = axes('Parent', hFig, ...
@@ -62,6 +82,7 @@
'BusyAction', 'queue', ...
'Interruptible', 'off');
+
% === APPDATA STRUCTURE ===
setappdata(hFig, 'FigureId', FigureId);
setappdata(hFig, 'hasMoved', 0);
@@ -1410,6 +1431,8 @@ function LoadFigurePlot(hFig) %#ok
RefreshTextDisplay(hFig);
% Display region and hem lobes?
SetHierarchyNodeIsVisible(hFig, DispOptions.HierarchyNodeIsVisible);
+ % Set title
+ SetTitle(hFig, TfInfo.DisplayUnits);
% Position camera
DefaultCamera(hFig);
end
@@ -2471,7 +2494,7 @@ function UpdateColormap(hFig)
end
% Get figure colormap
ColormapInfo = getappdata(hFig, 'Colormap');
- ColormapInfo.DisplayUnits = TfInfo.DisplayUnits;
+ ColormapInfo.DisplayUnits = 'No units';
sColormap = bst_colormaps('GetColormap', ColormapInfo.Type);
% Set figure colormap
set(hFig, 'Colormap', sColormap.CMap);
@@ -3976,4 +3999,19 @@ function DeleteAllNodes(hFig)
bst_figures('SetFigureHandleField', hFig, 'NumberOfLevels', NumberOfLevels);
%
bst_figures('SetFigureHandleField', hFig, 'Levels', Levels);
-end
\ No newline at end of file
+end
+
+%% ===== SET TITLE =====
+function SetTitle(hFig, title)
+ hTxtT = findobj(hFig, '-depth', 2, 'Tag', 'TextTitle');
+ set(hTxtT, 'String', title);
+ TitleButtonDownFcn(hFig, 0);
+end
+
+%% Callbacks for Title
+% Set current axes to AxesConnect
+function TitleButtonDownFcn(src, ~)
+ hFig = ancestor(src, 'figure');
+ hAxes = findobj(hFig, '-depth', 1, 'Tag', 'AxesConnect');
+ axes(hAxes);
+end
diff --git a/toolbox/gui/figure_mri.m b/toolbox/gui/figure_mri.m
index 873264af0..0d4fecc3e 100644
--- a/toolbox/gui/figure_mri.m
+++ b/toolbox/gui/figure_mri.m
@@ -84,7 +84,7 @@
'Renderer', rendererName, ...
'BusyAction', 'cancel', ...
'Interruptible', 'off', ...
- 'CloseRequestFcn', @(h,ev)ButtonCancel_Callback(h,ev), ...
+ 'CloseRequestFcn', @(h,ev)bst_figures('DeleteFigure',h,ev), ...
'KeyPressFcn', @FigureKeyPress_Callback, ...
'WindowButtonDownFcn', [], ...
'WindowButtonMotionFcn', [], ...
@@ -874,7 +874,7 @@ function ButtonView3DHead_Callback(hFig)
sMri.SCS.RPA = [1, 0.5, 0.5] .* size(sMri.Cube) .* sMri.Voxsize;
[Transf, sMri] = cs_compute(sMri, 'scs');
% Generate head surface
- [Vertices, Faces] = tess_isohead(sMri, 20000, 0, 0, '');
+ [Vertices, Faces] = tess_isohead(sMri, 20000, 0, 0, [], '');
% Convert coordinates back to MRI (mm) so that the head surface doesn't have
% to be updated when we move fiducials
Vertices = cs_convert(sMri, 'scs', 'mri', Vertices) * 1000;
@@ -1061,9 +1061,9 @@ function DisplayFigurePopup(hFig)
jItem = gui_component('MenuItem', jMenuElec, [], 'Set electrode position', IconLoader.ICON_CHANNEL, [], @(h,ev)SetElectrodePosition(hFig));
jItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_MASK));
elseif isequal(GlobalData.DataSet(iDS).Figure(iFig).Id.Modality, 'SEEG')
- gui_component('MenuItem', jMenuElec, [], 'SEEG contacts', IconLoader.ICON_CHANNEL, [], @(h,ev)LoadElectrodes(hFig, GlobalData.DataSet(iDS).ChannelFile, 'SEEG'));
+ gui_component('MenuItem', jMenuElec, [], 'SEEG contacts', IconLoader.ICON_CHANNEL, [], @(h,ev)panel_ieeg('LoadElectrodes', hFig, GlobalData.DataSet(iDS).ChannelFile, 'SEEG'));
elseif isequal(GlobalData.DataSet(iDS).Figure(iFig).Id.Modality, 'ECOG')
- gui_component('MenuItem', jMenuElec, [], 'ECOG contacts', IconLoader.ICON_CHANNEL, [], @(h,ev)LoadElectrodes(hFig, GlobalData.DataSet(iDS).ChannelFile, 'ECOG'));
+ gui_component('MenuItem', jMenuElec, [], 'ECOG contacts', IconLoader.ICON_CHANNEL, [], @(h,ev)panel_ieeg('LoadElectrodes', hFig, GlobalData.DataSet(iDS).ChannelFile, 'ECOG'));
end
end
@@ -1099,6 +1099,13 @@ function DisplayFigurePopup(hFig)
gui_component('MenuItem', jMenuSave, [], 'Time contact sheet: Sagittal', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'time', 'x', DefaultOutputDir));
gui_component('MenuItem', jMenuSave, [], 'Time contact sheet: Axial', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'time', 'z', DefaultOutputDir));
end
+ if ~getappdata(hFig, 'isStaticFreq')
+ % Separator
+ jMenuSave.addSeparator();
+ gui_component('MenuItem', jMenuSave, [], 'Frequency contact sheet: Coronal', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'freq', 'y', DefaultOutputDir));
+ gui_component('MenuItem', jMenuSave, [], 'Frequency contact sheet: Sagittal', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'freq', 'x', DefaultOutputDir));
+ gui_component('MenuItem', jMenuSave, [], 'Frequency contact sheet: Axial', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'freq', 'z', DefaultOutputDir));
+ end
jMenuSave.addSeparator();
gui_component('MenuItem', jMenuSave, [], 'Volume contact sheet: Coronal', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'volume', 'y', DefaultOutputDir));
gui_component('MenuItem', jMenuSave, [], 'Volume contact sheet: Sagittal', IconLoader.ICON_CONTACTSHEET, [], @(h,ev)view_contactsheet(hFig, 'volume', 'x', DefaultOutputDir));
@@ -1959,42 +1966,6 @@ function MouseMovePoint(hAxes, sMri, Handles, iChannel)
end
end
-
-%% ===== LOAD ELECTRODES =====
-function LoadElectrodes(hFig, ChannelFile, Modality) %#ok
- global GlobalData;
- % Get figure and dataset
- [hFig,iFig,iDS] = bst_figures('GetFigure', hFig);
- if isempty(iDS)
- return;
- end
- % Check that the channel is not already defined
- if ~isempty(GlobalData.DataSet(iDS).ChannelFile) && ~file_compare(GlobalData.DataSet(iDS).ChannelFile, ChannelFile)
- error('There is already another channel file loaded for this MRI. Close the existing figures.');
- end
- % Load channel file in the dataset
- bst_memory('LoadChannelFile', iDS, ChannelFile);
- % If iEEG channels: load both SEEG and ECOG
- if ismember(Modality, {'SEEG', 'ECOG', 'ECOG+SEEG'})
- iChannels = channel_find(GlobalData.DataSet(iDS).Channel, 'SEEG, ECOG');
- else
- iChannels = channel_find(GlobalData.DataSet(iDS).Channel, Modality);
- end
- % Set the list of selected sensors
- GlobalData.DataSet(iDS).Figure(iFig).SelectedChannels = iChannels;
- GlobalData.DataSet(iDS).Figure(iFig).Id.Modality = Modality;
- % Plot electrodes
- if ~isempty(iChannels)
- GlobalData.DataSet(iDS).Figure(iFig).Handles = PlotElectrodes(iDS, iFig, GlobalData.DataSet(iDS).Figure(iFig).Handles);
- PlotSensors3D(iDS, iFig);
- end
- % Set EEG flag
- SetFigureStatus(hFig, [], [], [], 1, 1);
- % Update figure name
- bst_figures('UpdateFigureName', hFig);
-end
-
-
%% ===== PLOT 3D ELECTRODES =====
function PlotSensors3D(iDS, iFig, Channel, ChanLoc)
global GlobalData;
@@ -2305,7 +2276,7 @@ function showPt(hPoint, locPoint)
if Handles.isEeg
% Hide all the points that we will never show
iEegHide = Handles.HiddenChannels;
- iEegShow = setdiff(1:length(Handles.hPointEEG), iEegHide);
+ iEegShow = setdiff(1:size(Handles.hPointEEG,1), iEegHide);
if ~isempty(iEegHide)
set(Handles.hPointEEG(iEegHide(:),:), 'Visible', 'off');
end
@@ -2557,10 +2528,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
@@ -2578,21 +2561,26 @@ function ButtonSave_Callback(hFig, varargin)
warning('off', 'MATLAB:load:variableNotFound');
sMriOld = load(MriFileFull, 'SCS');
warning('on', 'MATLAB:load:variableNotFound');
- % If the fiducials were modified
+ % Check if the fiducials were modified (> 1um), to realign surfaces below
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
% === 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/gui/figure_timefreq.m b/toolbox/gui/figure_timefreq.m
index 4f7c5c3b5..ee4e227ac 100644
--- a/toolbox/gui/figure_timefreq.m
+++ b/toolbox/gui/figure_timefreq.m
@@ -970,18 +970,18 @@ function DisplayFigurePopup(hFig)
% Keep only the selected (good) channels
% This won't apply when displaying a single channel (e.g. FOOOF overlay mode)
% or when displaying connectivity matrices, or any PSD not computed directly on sensor data
- [hFig, iFig] = bst_figures('GetFigure', hFig);
- if ~isempty(GlobalData.DataSet(iDS).Figure(iFig).SelectedChannels) && ...
- numel(GlobalData.DataSet(iDS).Figure(iFig).SelectedChannels) < numel(iRow) && ...
+ [hFig, iFig, iDSFig] = bst_figures('GetFigure', hFig);
+ if ~isempty(GlobalData.DataSet(iDSFig).Figure(iFig).SelectedChannels) && ...
+ numel(GlobalData.DataSet(iDSFig).Figure(iFig).SelectedChannels) < numel(iRow) && ...
strcmpi(DataType, 'data') && isempty(GlobalData.DataSet(iDS).Timefreq(iTimefreq).RefRowNames)
% Grad norm: need to return both gradiometers
- if strcmpi(GlobalData.DataSet(iDS).Figure(iFig).Id.Modality, 'MEG GRADNORM')
- selBaseName = cellfun(@(c)cat(2, {[c(1:end-1) '2']}, {[c(1:end-1) '3']}), {GlobalData.DataSet(iDS).Channel(GlobalData.DataSet(iDS).Figure(iFig).SelectedChannels).Name}, 'UniformOutput', false);
+ if strcmpi(GlobalData.DataSet(iDSFig).Figure(iFig).Id.Modality, 'MEG GRADNORM')
+ selBaseName = cellfun(@(c)cat(2, {[c(1:end-1) '2']}, {[c(1:end-1) '3']}), {GlobalData.DataSet(iDS).Channel(GlobalData.DataSet(iDSFig).Figure(iFig).SelectedChannels).Name}, 'UniformOutput', false);
selBaseName = [selBaseName{:}];
iSelected = ismember(RowNames, selBaseName);
% Regular sensor selection
else
- iSelected = ismember(RowNames, {GlobalData.DataSet(iDS).Channel(GlobalData.DataSet(iDS).Figure(iFig).SelectedChannels).Name});
+ iSelected = ismember(RowNames, {GlobalData.DataSet(iDS).Channel(GlobalData.DataSet(iDSFig).Figure(iFig).SelectedChannels).Name});
end
% Return only selected sensors
TF = TF(iSelected,:,:);
@@ -1063,9 +1063,20 @@ function UpdateFigurePlot(hFig, isForced)
% Get figure colormap
ColormapInfo = getappdata(hFig, 'Colormap');
sColormap = bst_colormaps('GetColormap', ColormapInfo.Type);
- % Displaying LOG values: always use the "RealMin" display
- if strcmpi(TfInfo.Function, 'log')
- sColormap.isRealMin = 1;
+ % Displaying LOG values : always use the "RealMin" display and not absolutes values
+ % Displaying Power values : always use absolutes values
+ if ~isempty(TfInfo) && strcmpi(ColormapInfo.Type, 'timefreq')
+ isAbsoluteValues = sColormap.isAbsoluteValues;
+ if strcmpi(TfInfo.Function, 'log')
+ sColormap.isRealMin = 1;
+ isAbsoluteValues = 0;
+ elseif strcmpi(TfInfo.Function, 'power')
+ isAbsoluteValues = 1;
+ end
+ if isAbsoluteValues ~= sColormap.isAbsoluteValues
+ sColormap.isAbsoluteValues = isAbsoluteValues;
+ bst_colormaps('SetColormap', ColormapInfo.Type, sColormap);
+ end
end
% Get figure maximum
MinMaxVal = bst_colormaps('GetMinMax', sColormap, TF, TopoHandles.DataMinMax);
diff --git a/toolbox/gui/figure_timeseries.m b/toolbox/gui/figure_timeseries.m
index 446b4876b..3b92f9eca 100644
--- a/toolbox/gui/figure_timeseries.m
+++ b/toolbox/gui/figure_timeseries.m
@@ -3250,6 +3250,15 @@ function SetTimeVisible(hFig, isVisible) %#ok<*DEFNU>
if (TsInfo.ShowLegend)
if isempty(LinesColor)
ColorOrder = panel_scout('GetScoutsColorTable');
+ iHbO = find(cellfun(@(x)~isempty(x), strfind(LinesLabels, 'HbO')));
+ iHbR = find(cellfun(@(x)~isempty(x), strfind(LinesLabels, 'HbR')));
+ iHbT = find(cellfun(@(x)~isempty(x), strfind(LinesLabels, 'HbT')));
+ if (~isempty(iHbO) && ~isempty(iHbR)) || (~isempty(iHbO) && ~isempty(iHbT)) || (~isempty(iHbT) && ~isempty(iHbR))
+ ColorsGRB = ColorOrder(1:3,:);
+ ColorOrder(iHbO,:) = repmat(ColorsGRB(2,:), length(iHbO),1); % Red
+ ColorOrder(iHbR,:) = repmat(ColorsGRB(3,:), length(iHbR),1); % Blue
+ ColorOrder(iHbT,:) = repmat(ColorsGRB(1,:), length(iHbT),1); % Green
+ end
else
ColorOrder = [];
end
@@ -3364,10 +3373,11 @@ function SetTimeVisible(hFig, isVisible) %#ok<*DEFNU>
iLinesGroup = find(strcmpi(LinesLabels{1}, LinesLabels));
% Get montage
sMontage = panel_montage('GetMontage', TsInfo.MontageName);
+ [~, ~, iMatrixDisp] = panel_montage('GetMontageChannels', sMontage, {GlobalData.DataSet(iDS).Channel.Name}, GlobalData.DataSet(iDS).Measures.sFile.channelflag);
% Get groups in which each sensor belongs, to map group and color
GroupNames = {};
for i = 1:length(iLinesGroup)
- ChanName = sMontage.ChanNames{find(sMontage.Matrix(iLinesGroup(i),:), 1)};
+ ChanName = sMontage.ChanNames{find(sMontage.Matrix(iMatrixDisp(iLinesGroup(i)),:), 1)};
GroupNames{i} = GlobalData.DataSet(iDS).Channel(strcmpi(ChanName, {GlobalData.DataSet(iDS).Channel.Name})).Group;
end
% Create legend
diff --git a/toolbox/gui/figure_topo.m b/toolbox/gui/figure_topo.m
index f92a3d0aa..00f1a9ef1 100644
--- a/toolbox/gui/figure_topo.m
+++ b/toolbox/gui/figure_topo.m
@@ -42,13 +42,16 @@ function CurrentFreqChangedCallback(iDS, iFig) %#ok
global GlobalData;
% Get figure appdata
hFig = GlobalData.DataSet(iDS).Figure(iFig).hFigure;
+ % Get topography type requested (3DSensorCap, 2DDisc, 2DSensorCap, 2DLayout)
+ TopoType = GlobalData.DataSet(iDS).Figure(iFig).Id.SubType;
+ % Get TimeFreq info
TfInfo = getappdata(hFig, 'Timefreq');
- % If no frequencies in this figure
+ % If no frequencies (time series) in this figure
if getappdata(hFig, 'isStaticFreq')
return;
end
% Update frequency to display
- if ~isempty(TfInfo)
+ if ~isempty(TfInfo) && ~(strcmpi(TopoType, '2DLayout') && getappdata(hFig, 'isStatic'))
TfInfo.iFreqs = GlobalData.UserFrequencies.iCurrentFreq;
setappdata(hFig, 'Timefreq', TfInfo);
end
@@ -147,9 +150,20 @@ function UpdateTopoPlot(iDS, iFig)
% Get figure colormap
ColormapInfo = getappdata(hFig, 'Colormap');
sColormap = bst_colormaps('GetColormap', ColormapInfo.Type);
- % Displaying LOG values: always use the "RealMin" display
- if ~isempty(TfInfo) && strcmpi(TfInfo.Function, 'log')
- sColormap.isRealMin = 1;
+ % Displaying LOG values : always use the "RealMin" display and not absolutes values
+ % Displaying Power values : always use absolutes values
+ if ~isempty(TfInfo) && strcmpi(ColormapInfo.Type, 'timefreq')
+ isAbsoluteValues = sColormap.isAbsoluteValues;
+ if strcmpi(TfInfo.Function, 'log')
+ sColormap.isRealMin = 1;
+ isAbsoluteValues = 0;
+ elseif strcmpi(TfInfo.Function, 'power')
+ isAbsoluteValues = 1;
+ end
+ if isAbsoluteValues ~= sColormap.isAbsoluteValues
+ sColormap.isAbsoluteValues = isAbsoluteValues;
+ bst_colormaps('SetColormap', ColormapInfo.Type, sColormap);
+ end
end
% Get figure maximum
CLim = bst_colormaps('GetMinMax', sColormap, DataToPlot, TopoHandles.DataMinMax);
@@ -237,12 +251,14 @@ function UpdateTopoPlot(iDS, iFig)
%% ===== GET FIGURE DATA =====
-% Warning: Time output is only defined for the time-frequency plots
-function [F, Time, selChan, overlayLabels, dispNames, StatThreshUnder, StatThreshOver] = GetFigureData(iDS, iFig, isAllTime, isMultiOutput)
+% Warning: xAxis output is only defined for the timefreq plots
+% xAxis = 'Time' for TF maps
+% xAxis = 'Freqs' for Spectra
+function [F, xAxis, selChan, overlayLabels, dispNames, StatThreshUnder, StatThreshOver] = GetFigureData(iDS, iFig, isAllTime, isMultiOutput)
global GlobalData;
% Initialize returned values
F = [];
- Time = [];
+ xAxis = [];
selChan = [];
overlayLabels = {};
dispNames = {};
@@ -332,10 +348,23 @@ function UpdateTopoPlot(iDS, iFig)
StatThreshUnder = GlobalData.DataSet(iDS).Measures.StatThreshUnder;
end
case 'timefreq'
- % Get timefreq values
+ [sStudy, iStudy, iTimefreq] = bst_get('TimefreqFile', ReadFiles{iFile});
+ if ~isempty(sStudy)
+ overlayLabels{iFile} = sStudy.Timefreq(iTimefreq).Comment;
+ end
+ % Get loaded timefreq values (only first file DS is the same as Fig)
+ TfInfo = getappdata(hFig, 'Timefreq');
+ TfInfo.FileName = file_short(ReadFiles{iFile});
+ setappdata(hFig, 'Timefreq', TfInfo);
[Time, Freqs, TfInfo, TF, RowNames] = figure_timefreq('GetFigureData', hFig, TimeDef);
+ xAxis = Time; % TF map
+ isStatic = getappdata(hFig, 'isStatic');
+ if isStatic
+ xAxis = Freqs; % Spectrum
+ end
% Initialize returned matrix
- F{iFile} = zeros(length(selChan), size(TF, 2));
+ F{iFile} = zeros(length(selChan), length(xAxis));
+
% Re-order channels
for i = 1:length(selChan)
selrow = GlobalData.DataSet(iDS).Channel(selChan(i)).Name;
@@ -353,16 +382,25 @@ function UpdateTopoPlot(iDS, iFig)
iRow = find(strcmpi(selrow, RowNames));
% If channel was found (if there is time-freq decomposition available for it)
if ~isempty(iRow)
- F{iFile}(i,:) = TF(iRow(1),:);
+ if isStatic
+ F{iFile}(i,:) = TF(iRow(1),1,:); % Spectrum
+ else
+ F{iFile}(i,:) = TF(iRow(1),:,1); % Freq slice in TF
+ end
end
end
end
end
end
+ % Reset TfInfo with first TF file
+ if strcmpi(TopoInfo.FileType, 'timefreq')
+ TfInfo.FileName = file_short(ReadFiles{1});
+ setappdata(hFig, 'Timefreq', TfInfo);
+ end
end
% Get time if required and not defined yet
- if (nargout >= 2) && isempty(Time)
- Time = bst_memory('GetTimeVector', iDS, [], TimeDef);
+ if (nargout >= 2) && isempty(xAxis) && ismember(lower(TopoInfo.FileType), {'data', 'pdata'})
+ xAxis = bst_memory('GetTimeVector', iDS, [], TimeDef);
end
% ===== APPLY MONTAGE =====
@@ -758,66 +796,90 @@ function CreateTopo2dLayout(iDS, iFig, hAxes, Channel, Vertices, modChan)
% ===== GET ALL DATA =====
% Get data
- [F, Time, selChanGlobal, overlayLabels, dispNames] = GetFigureData(iDS, iFig, 1, 1);
+ [F, xAxis, selChanGlobal, overlayLabels, dispNames] = GetFigureData(iDS, iFig, 1, 1);
selChan = bst_closest(selChanGlobal, modChan);
if isempty(selChan)
disp('2DLAYOUT> No good sensor to display...');
return;
end
- % Convert time bands in time vector
- if iscell(Time)
- nBands = size(Time,1);
- TimeVector = zeros(1,nBands);
- for i = 1:nBands
- % Take the middle of each time band
- TimeVector(i) = (Time{i,2} + Time{i,3}) / 2;
- end
+ % Convert x axis (time or frequency) bands in time vector
+ if iscell(xAxis)
+ % Take the middle of each time band
+ xAxisVector = zeros(1, size(xAxis,1));
+ xAxisVector(:) = mean(process_tf_bands('GetBounds', xAxis), 2);
else
- TimeVector = Time;
+ xAxisVector = xAxis;
end
+
% Get 2DLayout display options
TopoLayoutOptions = bst_get('TopoLayoutOptions');
+
+ hFig = GlobalData.DataSet(iDS).Figure(iFig).hFigure;
+ isStatic = getappdata(hFig, 'isStatic'); % Time static
+ isStaticFreq = getappdata(hFig, 'isStaticFreq'); % Freq static
+
% Flip Y axis if needed
- if TopoLayoutOptions.FlipYAxis
+ if TopoLayoutOptions.FlipYAxis && isStaticFreq
F = cellfun(@(c)times(c,-1), F, 'UniformOutput', 0);
end
- % Default time window: all the window
- if isempty(TopoLayoutOptions.TimeWindow)
- TopoLayoutOptions.TimeWindow = GlobalData.UserTimeWindow.Time;
- % Otherwise, center the time window around the current time
+
+ % Handle xAxis as Time
+ if ~isStatic
+ % Default time window: all the window
+ if isempty(TopoLayoutOptions.TimeWindow)
+ TopoLayoutOptions.TimeWindow = GlobalData.UserTimeWindow.Time;
+ % Otherwise, center the time window around the current time
+ else
+ winLen = (TopoLayoutOptions.TimeWindow(2) - TopoLayoutOptions.TimeWindow(1));
+ TopoLayoutOptions.TimeWindow = bst_saturate(GlobalData.UserTimeWindow.CurrentTime + winLen ./ 2 .* [-1, 1] , GlobalData.UserTimeWindow.Time, 1);
+ end
+ xWindow = TopoLayoutOptions.TimeWindow;
else
- winLen = (TopoLayoutOptions.TimeWindow(2) - TopoLayoutOptions.TimeWindow(1));
- TopoLayoutOptions.TimeWindow = bst_saturate(GlobalData.UserTimeWindow.CurrentTime + winLen ./ 2 .* [-1, 1] , GlobalData.UserTimeWindow.Time, 1);
+ % Default freq window: all spectrum
+ if isempty(TopoLayoutOptions.FreqWindow)
+ TopoLayoutOptions.FreqWindow = [xAxisVector(1), xAxisVector(end)];
+ end
+ xWindow = TopoLayoutOptions.FreqWindow;
end
- % Get only the 2DLayout time window
- iTime = find((TimeVector >= TopoLayoutOptions.TimeWindow(1)) & (TimeVector <= TopoLayoutOptions.TimeWindow(2)));
+
+ % Get only requested x axis window
+ ixAxis = find((xAxisVector >= xWindow(1)) & (xAxisVector <= xWindow(2)));
% Check for errors
- if isempty(iTime)
- error('Invalid time window.');
- elseif (length(iTime) < 2)
- if (iTime + 1 <= length(TimeVector))
- iTime = [iTime, iTime + 1];
- elseif (iTime >= 2)
- iTime = [iTime - 1, iTime];
+ if isempty(ixAxis)
+ error('Invalid x-axis window.');
+ elseif (length(ixAxis) < 2)
+ if (ixAxis + 1 <= length(xAxisVector))
+ ixAxis = [ixAxis, ixAxis + 1];
+ elseif (ixAxis >= 2)
+ ixAxis = [ixAxis - 1, ixAxis];
else
- error('Invalid time window.');
+ error('Invalid x-axis window.');
end
end
% Keep only the selected time indices
- TimeVector = TimeVector(iTime);
- % Flip Time vector (it's the way the data will be represented too)
- TimeVector = fliplr(TimeVector);
- % Look for current time in TimeVector
- %iCurrentTime = bst_closest(0, TimeVector);
- iCurrentTime = bst_closest(GlobalData.UserTimeWindow.CurrentTime, TimeVector);
- if isempty(iCurrentTime)
- iCurrentTime = 1;
- end
- % Normalize time between 0 and 1
- TimeVector = (TimeVector - TimeVector(1)) ./ (TimeVector(end) - TimeVector(1));
+ xAxisVector = xAxisVector(ixAxis);
+ % Flip x axis vector (it's the way the data will be represented too)
+ xAxisVector = fliplr(xAxisVector);
+ if ~isStatic
+ % Look for current time in TimeVector
+ iCurrentX = bst_closest(GlobalData.UserTimeWindow.CurrentTime, xAxisVector);
+ else
+ if iscell(GlobalData.UserFrequencies.Freqs)
+ bands = mean(process_tf_bands('GetBounds', xAxis), 2);
+ currentX = bands(GlobalData.UserFrequencies.iCurrentFreq);
+ else
+ currentX = GlobalData.UserFrequencies.Freqs(GlobalData.UserFrequencies.iCurrentFreq);
+ end
+ iCurrentX = bst_closest(currentX, xAxisVector);
+ end
+ % Current position
+ if isempty(iCurrentX)
+ iCurrentX = 1;
+ end
+ % Normalize xAxis between 0 and 1
+ xAxisVector = (xAxisVector - xAxisVector(1)) ./ (xAxisVector(end) - xAxisVector(1));
% Get graphic objects handles
PlotHandles = GlobalData.DataSet(iDS).Figure(iFig).Handles;
- hFig = GlobalData.DataSet(iDS).Figure(iFig).hFigure;
isDrawZeroLines = isempty(PlotHandles.hZeroLines) || any(~ishandle(PlotHandles.hZeroLines));
isDrawLines = isempty(PlotHandles.hLines) || any(~ishandle(PlotHandles.hLines{1}));
isDrawLegend = isempty(PlotHandles.hLabelLegend);
@@ -894,13 +956,28 @@ function CreateTopo2dLayout(iDS, iFig, hAxes, Channel, Vertices, modChan)
DispFactor = PlotHandles.DisplayFactor; % * figure_timeseries('GetDefaultFactor', GlobalData.DataSet(iDS).Figure(iFig).Id.Modality);
% Loop on multiple files
+ MinMaxs = zeros(2, length(F));
for iFile = 1:length(F)
% Keep only selected time points
- F{iFile} = F{iFile}(:, iTime);
- % Normalize data
- M = double(max(abs(F{iFile}(:))));
- F{iFile} = F{iFile} ./ M;
+ F{iFile} = F{iFile}(:, ixAxis);
+ % Find minimum and maximum
+ MinMaxs(1, iFile) = double(min(F{iFile}(:)));
+ MinMaxs(2, iFile) = double(max(F{iFile}(:)));
end
+ % Get scale and offset to normalize data
+ TfInfo = getappdata(hFig, 'Timefreq');
+ if isStatic && isfield(TfInfo, 'Function') && strcmpi(TfInfo.Function, 'log') && max(MinMaxs(:)) < 0
+ % Data is dB
+ offset = max(MinMaxs(2,:));
+ % Remove offset
+ M = max(abs(MinMaxs(1,:) - offset));
+ F = cellfun(@(c)minus(c, offset), F, 'UniformOutput', 0);
+ else
+ offset = 0;
+ M = max(abs(MinMaxs(2,:)));
+ end
+ % Normalize data
+ F = cellfun(@(c)rdivide(c, M), F, 'UniformOutput', 0);
% Draw each sensor
displayedLabels = {};
@@ -918,7 +995,8 @@ function CreateTopo2dLayout(iDS, iFig, hAxes, Channel, Vertices, modChan)
% Define lines to trace
XData = plotSize(1) * dat(end:-1:1) * DispFactor + Xi;
Xrange = plotSize(1) * [min(0,datMin), max(0,datMax)] * DispFactor + Xi;
- YData = plotSize(2) * (TimeVector - 0.5) + Yi;
+ Xrange = Xrange + 0.2.*[-1, 1].*(abs(diff(Xrange)));
+ YData = plotSize(2) * (xAxisVector - 0.5) + Yi;
ZData = 0;
% === DATA LINE ===
@@ -955,13 +1033,13 @@ function CreateTopo2dLayout(iDS, iFig, hAxes, Channel, Vertices, modChan)
PlotHandles.hZeroLines(i) = line([Xi, Xi], [YData(1), YData(end)], [ZData, ZData], ...
'Tag', '2DLayoutZeroLines', ...
'Parent', hAxes);
- % Time cursor
- PlotHandles.hCursors(i) = line([Xrange(1), Xrange(2)], [YData(iCurrentTime), YData(iCurrentTime)], [ZData, ZData], ...
+ % X axis cursor
+ PlotHandles.hCursors(i) = line([Xrange(1), Xrange(2)], [YData(iCurrentX), YData(iCurrentX)], [ZData, ZData], ...
'Tag', '2DLayoutTimeCursor', ...
'Parent', hAxes);
else
set(PlotHandles.hZeroLines(i), 'XData', [Xi, Xi], 'YData', [YData(1), YData(end)]);
- set(PlotHandles.hCursors(i), 'XData', [Xrange(1), Xrange(2)], 'YData', [YData(iCurrentTime), YData(iCurrentTime)]);
+ set(PlotHandles.hCursors(i), 'XData', [Xrange(1), Xrange(2)], 'YData', [YData(iCurrentX), YData(iCurrentX)]);
end
else
if ~isempty(PlotHandles.hZeroLines)
@@ -1059,7 +1137,7 @@ function CreateTopo2dLayout(iDS, iFig, hAxes, Channel, Vertices, modChan)
'BusyAction', 'queue');
% Create label
PlotHandles.hLabelLegend = text(...
- 10 / figPos(3), .5, '', ...
+ 10 / figPos(3), .6, '', ...
'FontUnits', 'points', ...
'FontWeight', 'bold', ...
'FontSize', 8 .* Scaling, ...
@@ -1100,19 +1178,29 @@ function CreateTopo2dLayout(iDS, iFig, hAxes, Channel, Vertices, modChan)
end
end
% Get data type
- if isappdata(hFig, 'Timefreq')
- DataType = 'timefreq';
+ if isappdata(hFig, 'Timefreq') && ~isStatic
+ DataType = 'Timefreq';
else
DataType = GlobalData.DataSet(iDS).Figure(iFig).Id.Modality;
end
% Get data units and time window
[fScaled, fFactor, fUnits] = bst_getunits( M, DataType );
- msTime = round(TopoLayoutOptions.TimeWindow * 1000);
fUnits = strrep(fUnits, 'x10^{', 'e');
fUnits = strrep(fUnits, '10^{', 'e');
fUnits = strrep(fUnits, '}', '');
fUnits = strrep(fUnits, '\mu', 'u');
fUnits = strrep(fUnits, '\Delta', 'd');
+ % Handle units for PSD
+ if isappdata(hFig, 'Timefreq') && isStatic
+ TfInfo = getappdata(hFig, 'Timefreq');
+ if isempty(TfInfo.Normalized) && (~isfield(TfInfo, 'FreqUnits') || isempty(TfInfo.FreqUnits))
+ switch lower(TfInfo.Function)
+ case 'power', fUnits = [fUnits '^2/Hz']; fScaled = fScaled * fFactor;
+ case 'magnitude', fUnits = [fUnits '/sqrt(Hz)'];
+ case 'log', fUnits = 'dB';
+ end
+ end
+ end
% Round values if large values
if (fScaled > 5)
strAmp = sprintf('%d', round(fScaled));
@@ -1122,9 +1210,22 @@ function CreateTopo2dLayout(iDS, iFig, hAxes, Channel, Vertices, modChan)
strAmp = sprintf('%g', fScaled);
end
% Create legend text
- strLegend = sprintf(['Max amplitude: %s %s\n' ...
- 'Time window: [%d, %d] ms'], ...
- strAmp, fUnits, msTime(1), msTime(2));
+ strLegend = '';
+ if offset ~= 0
+ % Offset legend
+ strLegend = [sprintf('Offset level: %d %s', round(offset), fUnits)];
+ end
+ % Amplitude legend
+ strLegend = [strLegend 10 sprintf('Max amplitude: %s %s', strAmp, fUnits)];
+ % Time legend
+ if ~isStatic
+ msTime = round(TopoLayoutOptions.TimeWindow * 1000);
+ strLegend = [strLegend 10 sprintf('Time window: [%d, %d] ms', msTime(1), msTime(2))];
+ % Frequency legend
+ else
+ hzFreq = round(TopoLayoutOptions.FreqWindow * 100) / 100;
+ strLegend = [strLegend 10 sprintf('Frequency range: [%s, %s] Hz', num2str(hzFreq(1)), num2str(hzFreq(2)))];
+ end
% Update legend
set(PlotHandles.hLabelLegend, 'String', strLegend, 'Visible', 'on');
set(PlotHandles.hOverlayLegend, 'Visible', 'on');
@@ -1264,6 +1365,16 @@ function CreateButtons2dLayout(iDS, iFig)
global GlobalData;
% Get figure
hFig = GlobalData.DataSet(iDS).Figure(iFig).hFigure;
+ % Callbacks to adjsut x axis
+ if ~getappdata(hFig, 'isStatic')
+ xAxisName = 'Time';
+ xAxisOption = 'TimeWindow';
+ UpdateTopoXWindow = @UpdateTopoTimeWindow;
+ else
+ xAxisName = 'Frequency';
+ xAxisOption = 'FreqWindow';
+ UpdateTopoXWindow = @UpdateTopoFreqWindow;
+ end
% Create scale buttons
h1 = bst_javacomponent(hFig, 'button', [], [], IconLoader.ICON_SCROLL_UP, ...
'
Increase gain
Shortcuts: [+] [SHIFT + Mouse wheel]
', ...
@@ -1272,14 +1383,14 @@ function CreateButtons2dLayout(iDS, iFig)
'
'], ['Import ', volType], [], cellOptions, 'Reg+reslice');
+
% In non-interactive mode: ignore if possible, or use the first option available
else
RegMethod = 'Ignore';
@@ -281,17 +309,47 @@
strSizeWarn = [];
end
% Ask to reslice
- isReslice = java_dialog('confirm', [...
+ [isReslice, isCancel]= java_dialog('confirm', [...
'Reslice the volume?
' ...
- 'This operation rewrites the new MRI to match the alignment, size and resolution of the original volume.' ...
+ ['This operation rewrites the new ', volType, ' to match the alignment, size and resolution of the original volume.'] ...
strSizeWarn ...
- '
'], 'Import MRI');
+ '
'], ['Import ', volType]);
+ % User aborted the process
+ if isCancel
+ bst_progress('stop');
+ return;
+ end
% In non-interactive mode: never reslice
else
isReslice = 0;
end
+ % Check that reference volume has set fiducials for reslicing
+ if isReslice && (~isfield(sMriRef, 'SCS') || ~isfield(sMriRef.SCS, 'R') || ~isfield(sMriRef.SCS, 'T') || isempty(sMriRef.SCS.R) || isempty(sMriRef.SCS.T))
+ errMsg = 'Reslice: No SCS transformation available for the reference volume. Set the fiducials first.';
+ RegMethod = ''; % Registration will not be performed
+ end
+
+ % === ASK SKULL STRIPPING ===
+ if isInteractive && isCt && (strcmpi(RegMethod, 'SPM') || strcmpi(RegMethod, 'CT2MRI'))
+ % Ask if the user wants to mask out region outside skull in CT
+ [MaskMethod, isCancel] = java_dialog('question', ['Perform skull stripping on the CT volume? ' ...
+ 'This removes non-brain tissues (skull, scalp, fat, and other head tissues) from the CT volume.
' ...
+ 'Which method do you want to proceed with?
'], ...
+ 'Import CT', [], {'SPM', 'BrainSuite', 'Skip'}, '');
+ % User aborted the process
+ if isCancel
+ bst_progress('stop');
+ return;
+ end
+ else
+ % In non-interactive mode: never do skull stripping
+ MaskMethod = 'Skip';
+ end
- % === REGISTRATION ===
+ % === REGISTRATION AND RESLICING ===
switch (RegMethod)
case 'MNI'
% Register the new MRI on the existing one using the MNI transformation (+ RESLICE)
@@ -299,6 +357,9 @@
case 'SPM'
% Register the new MRI on the existing one using SPM + RESLICE
[sMri, errMsg, fileTag] = mri_coregister(sMri, sMriRef, 'spm', isReslice, isAtlas);
+ case 'CT2MRI'
+ % Register the CT to existing MRI using USC's CT2MRI plugin + RESLICE
+ [sMri, errMsg, fileTag] = mri_coregister(sMri, sMriRef, 'ct2mri', isReslice, isAtlas);
case 'Ignore'
if isReslice
% Register the new MRI on the existing one using the transformation in the input files (files already registered)
@@ -317,18 +378,57 @@
sMri.SCS = sMriRef.SCS;
%sMri.NCS = sMriRef.NCS;
end
+ otherwise
+ % Do nothing
end
- end
- % Stop in case of error
- if ~isempty(errMsg)
- if isInteractive
- bst_error(errMsg, [RegMethod ' MRI'], 0);
- sMri = [];
- bst_progress('stop');
- return;
- else
- error(errMsg);
+ % Stop in case of error
+ if ~isempty(errMsg)
+ if isInteractive
+ bst_error(errMsg, [RegMethod ' MRI'], 0);
+ sMri = [];
+ bst_progress('stop');
+ return;
+ else
+ error(errMsg);
+ end
+ end
+ % === SKULL STRIPPING ===
+ switch lower(MaskMethod)
+ case 'spm'
+ [sMri, errMsg, maskFileTag] = mri_skullstrip(sMri, sMriRef, 'spm');
+ case 'brainsuite'
+ [sMri, errMsg, maskFileTag] = mri_skullstrip(sMri, sMriRef, 'brainsuite');
+ case 'skip'
+ % Do nothing
+ maskFileTag = '';
+ end
+ fileTag = [fileTag, maskFileTag];
+ % Stop in case of error
+ if ~isempty(errMsg)
+ if isInteractive
+ bst_error(errMsg, [MaskMethod ' brain mask MRI'], 0);
+ sMri = [];
+ bst_progress('stop');
+ return;
+ else
+ error(errMsg);
+ end
+ end
+ % Add history entry (co-registration)
+ if ~isempty(RegMethod) && ~strcmpi(RegMethod, 'Ignore')
+ % Co-registration
+ sMri = bst_history('add', sMri, 'resample', ['MRI co-registered on default file (' RegMethod '): ' refMriFile]);
+ end
+ % Add history entry (reslice)
+ if isReslice || isMni
+ sMri = bst_history('add', sMri, 'resample', ['MRI resliced to default file: ' refMriFile]);
+ end
+ % Add history entry (skull stripping)
+ if ~isempty(maskFileTag)
+ sMri = bst_history('add', sMri, 'resample', ['Skull stripping with "' MaskMethod '" using on default file: ' refMriFile]);
end
+ % Add back history entry (import)
+ sMri.History = [tmpHistory.History; sMri.History];
end
end
@@ -376,7 +476,7 @@
% Get subject subdirectory
subjectSubDir = bst_fileparts(sSubject.FileName);
% Produce a default anatomy filename
-BstMriFile = bst_fullfile(ProtocolInfo.SUBJECTS, subjectSubDir, ['subjectimage_' importedBaseName fileTag tagAtlas '.mat']);
+BstMriFile = bst_fullfile(ProtocolInfo.SUBJECTS, subjectSubDir, ['subjectimage_' importedBaseName fileTag tagVolType '.mat']);
% Make this filename unique
BstMriFile = file_unique(BstMriFile);
% Save new MRI in Brainstorm format
@@ -388,7 +488,7 @@
sSubject.Anatomy(iAnatomy).FileName = file_short(BstMriFile);
sSubject.Anatomy(iAnatomy).Comment = sMri.Comment;
% Default anatomy: do not change
-if isempty(sSubject.iAnatomy)
+if isempty(sSubject.iAnatomy) && ~isCt && ~isAtlas
sSubject.iAnatomy = iAnatomy;
end
% Default subject
@@ -400,7 +500,7 @@
end
bst_set('ProtocolSubjects', ProtocolSubjects);
% Save first MRI as permanent default
-if (iAnatomy == 1)
+if (iAnatomy == 1) && ~isCt && ~isAtlas
db_surface_default(iSubject, 'Anatomy', iAnatomy, 0);
end
diff --git a/toolbox/io/import_sources.m b/toolbox/io/import_sources.m
index ac1cd6abe..b618c2a11 100644
--- a/toolbox/io/import_sources.m
+++ b/toolbox/io/import_sources.m
@@ -92,6 +92,7 @@
{'*.w'}, 'FreeSurfer weight files (*.w)', 'FS-WFILE'; ...
{'*'}, 'CIVET maps (*.*)', 'CIVET'; ...
{'.gii'}, 'GIfTI texture (*.gii)', 'GII'; ...
+ {'_sources.mat'}, 'Brainstorm sources (*sources*.mat)', 'BST'; ...
{'.mri', '.fif', '.img', '.ima', '.nii', '.mgh', '.mgz', '.mnc', '.mni', '.gz', '_subjectimage'}, 'Volume grids (subject space)', 'ALLMRI'; ...
{'.mri', '.fif', '.img', '.ima', '.nii', '.mgh', '.mgz', '.mnc', '.mni', '.gz', '_subjectimage'}, 'Volume grids (MNI space)', 'ALLMRI-MNI'; ...
}, DefaultFormats.ResultsIn);
@@ -230,6 +231,10 @@
end
Comment = strrep(Comment, 'results_', '');
Comment = strrep(Comment, '_results', '');
+ if strcmp(FileFormat, 'BST')
+ Comment = strrep(Comment, 'surface_', '');
+ Comment = strrep(Comment, 'volume_', '');
+ end
% If the two files are imported: remove .lh and .rh
if ~isempty(SourceFiles2)
Comment = strrep(Comment, 'rh.', '');
@@ -317,12 +322,16 @@
else
% New results structure
ResultsMat = db_template('resultsmat');
- ResultsMat.ImageGridAmp = [map, map];
+ if size(map, 2) > 1
+ ResultsMat.ImageGridAmp = map;
+ else
+ ResultsMat.ImageGridAmp = [map, map];
+ end
ResultsMat.ImagingKernel = [];
FileType = 'results';
% Time vector
if isempty(TimeVector) || (length(TimeVector) ~= size(ResultsMat.ImageGridAmp,2))
- ResultsMat.Time = 0:(size(map,2)-1);
+ ResultsMat.Time = 0:(size(ResultsMat.ImageGridAmp,2)-1);
else
ResultsMat.Time = TimeVector;
end
@@ -434,6 +443,9 @@
end
case 'ALLMRI-MNI'
error('Not supported yet.');
+ case 'BST'
+ sResultsMat = load(SourceFile);
+ map = sResultsMat.ImageGridAmp;
otherwise
error('Unsupported file format.');
end
diff --git a/toolbox/io/import_subject.m b/toolbox/io/import_subject.m
index 042feb035..f5f318a7e 100644
--- a/toolbox/io/import_subject.m
+++ b/toolbox/io/import_subject.m
@@ -108,8 +108,8 @@ function import_subject(ZipFile)
end
% Check the subject configuration
SubjectMat = load(SubjectFile);
- if (SubjectMat.UseDefaultAnat == 1) || (SubjectMat.UseDefaultChannel == 1)
- errMsg = [errMsg, 'Subjects using a default anatomy or channel file cannot be imported in an existing protocol.' 10 ...
+ if (SubjectMat.UseDefaultAnat == 1) || (SubjectMat.UseDefaultChannel == 2)
+ errMsg = [errMsg, 'Subjects using "Default anatomy" or "global channel file" cannot be imported in an existing protocol.' 10 ...
'Use the menu: File > Load protocol > Load from zip file.'];
continue;
end
diff --git a/toolbox/io/import_surfaces.m b/toolbox/io/import_surfaces.m
index 428e0228d..f36e7b66c 100644
--- a/toolbox/io/import_surfaces.m
+++ b/toolbox/io/import_surfaces.m
@@ -172,6 +172,9 @@
if isfield(Tess, 'Faces') % Volume meshes do not have Faces field
NewTess.Faces = Tess(1).Faces;
end
+ if isfield(Tess, 'Color') % Not all meshes have color
+ NewTess.Color = Tess(1).Color;
+ end
% Volume FEM mesh
else
NewTess = Tess;
@@ -204,7 +207,11 @@
NewTess = bst_history('add', NewTess, 'import', ['Import from: ' TessFile]);
% Produce a default surface filename (surface of volume mesh)
if isfield(NewTess, 'Faces')
- BstTessFile = bst_fullfile(ProtocolInfo.SUBJECTS, subjectSubDir, ['tess_' importedBaseName '.mat']);
+ BaseTessFile = ['tess_' importedBaseName '.mat'];
+ if ~isempty(NewTess.Color)
+ BaseTessFile = regexprep(BaseTessFile, '^tess_', 'tess_textured_');
+ end
+ BstTessFile = bst_fullfile(ProtocolInfo.SUBJECTS, subjectSubDir, BaseTessFile);
else
BstTessFile = bst_fullfile(ProtocolInfo.SUBJECTS, subjectSubDir, ['tess_fem_' importedBaseName '.mat']);
end
diff --git a/toolbox/io/import_video.m b/toolbox/io/import_video.m
index 54fb0f7b9..3b4c09304 100644
--- a/toolbox/io/import_video.m
+++ b/toolbox/io/import_video.m
@@ -1,7 +1,7 @@
-function iNewFiles = import_video(iStudy, VideoFiles)
+function [iNewFiles, OutputVideoFiles] = import_video(iStudy, VideoFiles)
% IMPORT_VIDEO Link video files to the database.
%
-% USAGE: iNewFiles = import_dipoles(iStudy, VideoFiles=[ask])
+% USAGE: [iNewFiles, OutputVideoFiles] = import_dipoles(iStudy, VideoFiles=[ask])
%
% INPUT:
% - iStudy : Index of the study where to import the DipolesFiles
@@ -35,6 +35,7 @@
end
% Returned variables
iNewFiles = [];
+OutputVideoFiles = {};
%% ===== SELECT FILES =====
@@ -145,6 +146,7 @@
iImage = length(sStudy.Image) + 1;
sStudy.Image(iImage) = sImage;
iNewFiles = [iNewFiles, iImage];
+ OutputVideoFiles{end+1} = OutputFile;
end
% Save study
diff --git a/toolbox/io/in_bst_channel.m b/toolbox/io/in_bst_channel.m
index 78826e4a8..2849daf78 100644
--- a/toolbox/io/in_bst_channel.m
+++ b/toolbox/io/in_bst_channel.m
@@ -74,6 +74,12 @@
% Old format (I-UUt) => Convert to new format
if ~isstruct(sMat.Projector)
sMat.Projector = process_ssp2('ConvertOldFormat', sMat.Projector);
+ elseif ~isfield(sMat.Projector, 'Method') || any(cellfun(@isempty, {sMat.Projector.Method}))
+ tmpProjector = repmat(db_template('projector'), 1, length(sMat.Projector));
+ for ix = 1 : length(sMat.Projector)
+ tmpProjector(ix) = process_ssp2('ConvertOldFormat', sMat.Projector(ix));
+ end
+ sMat.Projector = tmpProjector;
end
% Field does not exist
else
diff --git a/toolbox/io/in_bst_data.m b/toolbox/io/in_bst_data.m
index 1d304dc70..8f2b9c09c 100644
--- a/toolbox/io/in_bst_data.m
+++ b/toolbox/io/in_bst_data.m
@@ -128,6 +128,55 @@
studyPath = bst_fileparts(DataFile);
[rawPath, rawBase, rawExt] = bst_fileparts(DataMat.F.filename);
newRaw = bst_fullfile(studyPath, [rawBase, rawExt]);
+ % If not found, try to look for the file in the same file system
+ if ~file_exist(newRaw)
+ % Identify the OS for the raw link path
+ waspc = isempty(regexp(DataMat.F.filename, '/', 'once')); % '/' is forbidden char in Windows paths
+ % Linux/MacOS -> Linux/MacOS
+ if ~ispc()
+ % Get mountpoint
+ [status, dfResult] = system(['df ' DataFile]);
+ if ~status % no error
+ dfLines = str_split(dfResult, 10);
+ iChar = regexp(dfLines{1}, 'Mounted on');
+ mountpoint = dfLines{2}(iChar:end);
+ % Update raw link path
+ if ~waspc
+ % Replace mountpoint
+ [mountDir, mountLabel] = bst_fileparts(mountpoint);
+ iCharRelMount = regexp(DataMat.F.filename, mountLabel);
+ newRaw = bst_fullfile(mountDir, DataMat.F.filename(iCharRelMount:end));
+ else
+ % Replace drive letter with mountpoint
+ pathTmp = file_win2unix(DataMat.F.filename);
+ pathTmp = regexprep(pathTmp, '^[A-Z]:/', '');
+ newRaw = bst_fullfile(mountpoint, pathTmp);
+ end
+ end
+ else
+ % Get drive letter
+ tmp = DataFile;
+ driverLetter = '';
+ while ~strcmp(tmp, driverLetter)
+ driverLetter = tmp;
+ tmp = bst_fileparts(tmp);
+ end
+ % Update raw link path
+ if ~waspc
+ tmp = DataMat.F.filename;
+ % Remove common mountpoints
+ tmp2 = regexprep(tmp, '^/Volumes/.*?/', '');
+ if strcmp(tmp2, tmp)
+ tmp2 = regexprep(tmp, '^.*/media/.*?/.*?/', '');
+ end
+ % Add driverletter
+ newRaw = bst_fullfile(driverLetter, tmp2);
+ else
+ % Replace old drive letter
+ newRaw = regexprep(DataMat.F.filename, '^[A-Z]:/', driverLetter);
+ end
+ end
+ end
% If the corrected file exists
if file_exist(newRaw)
% Update the file in the returned structure
@@ -143,6 +192,11 @@
DataMat.Time = DataMat.Time';
end
+% ===== FIX TRANSPOSED CHANNEL FLAG =====
+if isfield(DataMat, 'ChannelFlag') && (size(DataMat.ChannelFlag,2) > 1)
+ DataMat.ChannelFlag = DataMat.ChannelFlag';
+end
+
% ===== FIX EVENTS STRUCTURES =====
% Imported file
if isfield(DataMat, 'Events') && ~isempty(DataMat.Events)
diff --git a/toolbox/io/in_bst_results.m b/toolbox/io/in_bst_results.m
index 149907b04..8bbc69253 100644
--- a/toolbox/io/in_bst_results.m
+++ b/toolbox/io/in_bst_results.m
@@ -260,6 +260,14 @@
Results.Leff = DataMat.Leff;
end
end
+% If full results are saved as factor decomposition
+elseif isfield(Results,'ImageGridAmp') && iscell(Results.ImageGridAmp)
+ % ImageGridAmp = ImageGridAmp{1} * ImageGridAmp{2} * ... * ImageGridAmp{N}
+ tmp = Results.ImageGridAmp{1};
+ for iDecomposition = 2 : length(Results.ImageGridAmp)
+ tmp = tmp * Results.ImageGridAmp{iDecomposition};
+ end
+ Results.ImageGridAmp = full(tmp);
end
diff --git a/toolbox/io/in_channel_ascii.m b/toolbox/io/in_channel_ascii.m
index 36a9411bd..598f3e7f9 100644
--- a/toolbox/io/in_channel_ascii.m
+++ b/toolbox/io/in_channel_ascii.m
@@ -91,6 +91,9 @@
case 'name'
iName = i;
strScanFormat = [strScanFormat, '%[^,; \t\b\n\r] '];
+ case 'namewithspace'
+ iName = i;
+ strScanFormat = [strScanFormat, '%[^,;\t\b\n\r] '];
case 'indice'
iIndice = i;
strScanFormat = [strScanFormat, '%d '];
diff --git a/toolbox/io/in_channel_curry_pom.m b/toolbox/io/in_channel_curry_pom.m
index f4966b817..19dae2e75 100644
--- a/toolbox/io/in_channel_curry_pom.m
+++ b/toolbox/io/in_channel_curry_pom.m
@@ -1,5 +1,5 @@
function ChannelMat = in_channel_curry_pom(ChannelFile)
-% IN_CHANNEL_CURRY_RS3: Read 3D cartesian positions for a set of points from a Curry .pom file.
+% IN_CHANNEL_CURRY_POM: Read 3D cartesian positions for a set of points from a Curry .pom file.
%
% USAGE: ChannelMat = in_channel_curry_pom(ChannelFile)
%
diff --git a/toolbox/io/in_channel_pos.m b/toolbox/io/in_channel_pos.m
index 00441f657..c9fbd7636 100644
--- a/toolbox/io/in_channel_pos.m
+++ b/toolbox/io/in_channel_pos.m
@@ -1,5 +1,10 @@
function ChannelMat = in_channel_pos(ChannelFile)
% IN_CHANNEL_POS: Read 3D positions from a Polhemus .pos CTF-compatible file.
+%
+% Coordinates are transformed to either "Native" CTF head-coil-based coordinates if digitized HPI
+% are present, or to SCS if HPI are not found but anatomical fiducials are present. If neither sets
+% of fiducials are found, raw coordinates are kept and Brainstorm assumes they are in "Native"
+% coordinates, as was previously done.
% @=============================================================================
% This function is part of the Brainstorm software:
@@ -19,7 +24,7 @@
% For more information type "brainstorm license" at command prompt.
% =============================================================================@
%
-% Authors: Francois Tadel, Elizabeth Bock, 2012-2013
+% Authors: Francois Tadel, Elizabeth Bock, 2012-2013, Marc Lalancette 2024
% Initialize output structure
ChannelMat = db_template('channelmat');
@@ -89,7 +94,7 @@
Loc = ChannelMat.HeadPoints.Loc;
iRemove = [];
for i = 1:length(iFid)
- iRemove = [iRemove, find((abs(Loc(1,iFid(i))-Loc(1,iExtra))<1e-5) & (abs(Loc(2,iFid(i))-Loc(2,iExtra))<1e-5) & (abs(Loc(2,iFid(i))-Loc(2,iExtra)<1e-5)))];
+ iRemove = [iRemove, find((abs(Loc(1,iFid(i))-Loc(1,iExtra))<1e-5) & (abs(Loc(2,iFid(i))-Loc(2,iExtra))<1e-5) & (abs(Loc(2,iFid(i))-Loc(2,iExtra))<1e-5))];
end
if ~isempty(iRemove)
ChannelMat.HeadPoints.Loc(:,iExtra(iRemove)) = [];
@@ -98,6 +103,66 @@
end
end
-
-
+% Transform coordinates to head-coil-based system if possible, or anatomical-based system. This is
+% done during digitization if it's done with Brainstorm, but doing it here will make this compatible
+% with other files, and importantly allow simple manual fixes (e.g. swapping mislabeled coils)
+% before importing.
+% Keep backup in case we get an error, so it can still at least work as before.
+ChannelBackup = ChannelMat;
+try
+ if ~isempty(iFid)
+ % Are the MEG head coils present?
+ % Get the three fiducials in the head points
+ iNas = find(strcmpi(ChannelMat.HeadPoints.Label, 'HPI-N'));
+ iLpa = find(strcmpi(ChannelMat.HeadPoints.Label, 'HPI-L'));
+ iRpa = find(strcmpi(ChannelMat.HeadPoints.Label, 'HPI-R'));
+ iCardinal = find(strcmpi(ChannelMat.HeadPoints.Type, 'CARDINAL'));
+ if ~isempty(iNas) && ~isempty(iLpa) && ~isempty(iRpa)
+ % Hack: rename anat fids, rename HPI to anat fids to reuse ususal realign functions. Then
+ % restore names.
+ RealPointLabels = ChannelMat.HeadPoints.Label;
+ for iP = iCardinal
+ ChannelMat.HeadPoints.Label{iP} = ['Tmp-' ChannelMat.HeadPoints.Label(iCardinal)];
+ end
+ for iP = iNas
+ ChannelMat.HeadPoints.Label{iP} = 'NAS';
+ end
+ for iP = iLpa
+ ChannelMat.HeadPoints.Label{iP} = 'LPA';
+ end
+ for iP = iRpa
+ ChannelMat.HeadPoints.Label{iP} = 'RPA';
+ end
+ % Transform to coil-based "Native" CTF coordinates
+ ChannelMat = channel_detect_type(ChannelMat, 1);
+ % Restore labels
+ if numel(RealPointLabels) ~= numel(ChannelMat.HeadPoints.Label)
+ % channel_detect_type did something unexpected and changed number of points.
+ error('Unexpected change in number of head points.');
+ end
+ ChannelMat.HeadPoints.Label = RealPointLabels;
+ % Correct misleading transformation name.
+ iTrans = find(strcmpi(ChannelMat.TransfMegLabels, 'Native=>Brainstorm/CTF'));
+ if numel(iTrans) ~= 1
+ error('Unexpected transformation(s).')
+ end
+ % And for EEG
+ ChannelMat.TransfMegLabels{iTrans} = 'RawPoints=>Native';
+ iTrans = find(strcmpi(ChannelMat.TransfEegLabels, 'Native=>Brainstorm/CTF'));
+ if numel(iTrans) ~= 1
+ error('Unexpected transformation(s).')
+ end
+ ChannelMat.TransfEegLabels{iTrans} = 'RawPoints=>Native';
+ elseif numel(iCardinal) >= 3
+ % Transform to SCS coordinates
+ ChannelMat = channel_detect_type(ChannelMat, 1);
+ % Native=>Brainstorm/CTF already added.
+ end
+ end
+catch ME
+ disp('BST> Warning: Unable to ensure head points are in "native" coordinates.');
+ % rethrow(ME);
+ disp([' ' ME.message]);
+ ChannelMat = ChannelBackup;
+end
diff --git a/toolbox/io/in_channel_tvb.m b/toolbox/io/in_channel_tvb.m
index 65e0d08ba..cd2cb847c 100644
--- a/toolbox/io/in_channel_tvb.m
+++ b/toolbox/io/in_channel_tvb.m
@@ -23,6 +23,14 @@
%
% Authors: Francois Tadel, 2020
+% Install/load EasyH5 Toolbox (https://github.com/NeuroJSON/easyh5) as plugin
+if ~exist('loadh5', 'file')
+ [isInstalled, errMsg] = bst_plugin('Install', 'easyh5');
+ if ~isInstalled
+ error(errMsg);
+ end
+end
+
% Read data from .h5
h5 = loadh5(ChannelFile);
% Check data format
diff --git a/toolbox/io/in_data_edf_ft.m b/toolbox/io/in_data_edf_ft.m
new file mode 100644
index 000000000..97a729eb7
--- /dev/null
+++ b/toolbox/io/in_data_edf_ft.m
@@ -0,0 +1,44 @@
+function [DataMat, ChannelMat] = in_data_edf_ft(DataFile)
+% in_data_edf_ft: Read entire EDF/EDF+ file using FieldTrip import function
+% data is upsampled to the highest sampling rate
+%
+% USAGE: [DataMat, ChannelMat] = in_data_edf_ft(DataFile)
+
+% @=============================================================================
+% 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
+
+%% ===== INSTALL PLUGIN FIELDTRIP =====
+if ~exist('edf2fieldtrip', 'file')
+ [isInstalled, errMsg] = bst_plugin('Install', 'fieldtrip');
+ if ~isInstalled
+ error(errMsg);
+ end
+end
+
+% Temporary FieldTrip file
+[~, filename] = bst_fileparts(DataFile);
+tmpfilename = bst_fullfile(bst_get('BrainstormTmpDir', 0, 'fieldtrip'), [filename '.mat']);
+% Read EDF using FieldTrip
+data = edf2fieldtrip(DataFile);
+% Save read data
+save(tmpfilename, 'data');
+% Import in Brainstorm
+[DataMat, ChannelMat] = in_data_fieldtrip(tmpfilename);
diff --git a/toolbox/io/in_data_snirf.m b/toolbox/io/in_data_snirf.m
index 4d09fd6fe..a01d8363b 100644
--- a/toolbox/io/in_data_snirf.m
+++ b/toolbox/io/in_data_snirf.m
@@ -25,7 +25,15 @@
%
% Authors: Edouard Delaire, Francois Tadel, 2020
-% Load file header with the JSNIRF Toolbox (https://github.com/fangq/jsnirfy)
+% Install/load JSNIRF Toolbox (https://github.com/NeuroJSON/jsnirfy) as plugin
+if ~exist('loadsnirf', 'file')
+ [isInstalled, errMsg] = bst_plugin('Install', 'jsnirfy');
+ if ~isInstalled
+ error(errMsg);
+ end
+end
+
+% Load file header
jnirs = loadsnirf(DataFile);
if isempty(jnirs) || ~isfield(jnirs, 'nirs')
@@ -85,13 +93,22 @@
% Create channel file structure
ChannelMat = db_template('channelmat');
ChannelMat.Comment = 'NIRS-BRS channels';
-ChannelMat.Nirs.Wavelengths = jnirs.nirs.probe.wavelengths;
+ChannelMat.Nirs.Wavelengths = round(jnirs.nirs.probe.wavelengths);
% NIRS channels
for iChan = 1:nChannels
% This assume measure are raw; need to change for Hbo,HbR,HbT
channel = jnirs.nirs.data.measurementList(iChan);
- [ChannelMat.Channel(iChan).Name, ChannelMat.Channel(iChan).Group] = nst_format_channel(channel.sourceIndex, channel.detectorIndex, jnirs.nirs.probe.wavelengths(channel.wavelengthIndex));
+ if isempty(jnirs.nirs.probe.sourceLabels) || isempty(jnirs.nirs.probe.detectorLabels)
+ [ChannelMat.Channel(iChan).Name, ChannelMat.Channel(iChan).Group] = nst_format_channel(channel.sourceIndex, channel.detectorIndex, jnirs.nirs.probe.wavelengths(channel.wavelengthIndex));
+ else
+
+ ChannelMat.Channel(iChan).Name = sprintf('%s%sWL%d', jnirs.nirs.probe.sourceLabels(channel.sourceIndex), ...
+ jnirs.nirs.probe.detectorLabels(channel.detectorIndex), ...
+ round(jnirs.nirs.probe.wavelengths(channel.wavelengthIndex)));
+ ChannelMat.Channel(iChan).Group = sprintf('WL%d', round(jnirs.nirs.probe.wavelengths(channel.wavelengthIndex)));
+
+ end
ChannelMat.Channel(iChan).Type = 'NIRS';
ChannelMat.Channel(iChan).Weight = 1;
if ~isempty(src_pos) && ~isempty(det_pos)
@@ -148,11 +165,11 @@
for iLandmark = 1:size(jnirs.nirs.probe.landmarkPos3D, 1)
name = strtrim(str_remove_spec_chars(toLine(jnirs.nirs.probe.landmarkLabels{iLandmark})));
- coord = scale .* jnirs.nirs.probe.landmarkPos3D(iLandmark, :);
+ coord = scale .* jnirs.nirs.probe.landmarkPos3D(iLandmark, 1:3);
% Fiducials NAS/LPA/RPA
switch lower(name)
- case {'nasion','nas'}
+ case {'nasion','nas','nz'}
ChannelMat.SCS.NAS = coord;
ltype = 'CARDINAL';
case {'leftear', 'lpa'}
@@ -222,24 +239,31 @@
DataMat.Events = repmat(db_template('event'), 1, length(jnirs.nirs.stim));
for iEvt = 1:length(jnirs.nirs.stim)
- DataMat.Events(iEvt).label = strtrim(str_remove_spec_chars(toLine(jnirs.nirs.stim(iEvt).name)));
- if ~isfield(jnirs.nirs.stim(iEvt), 'data')
+ if iscell(jnirs.nirs.stim(iEvt))
+ DataMat.Events(iEvt).label = strtrim(str_remove_spec_chars(toLine(jnirs.nirs.stim{iEvt}.name)));
+ else
+ DataMat.Events(iEvt).label = strtrim(str_remove_spec_chars(toLine(jnirs.nirs.stim(iEvt).name)));
+ end
+ if ~isfield(jnirs.nirs.stim(iEvt), 'data') || isempty(jnirs.nirs.stim(iEvt).data)
% Events structure
warning(sprintf('No data found for event: %s',DataMat.Events(iEvt).label))
continue
end
% Get timing
-
- if size(jnirs.nirs.stim(iEvt).data,1) > size(jnirs.nirs.stim(iEvt).data,1)
+ nStimDataCols = 3; % [starttime duration value]
+ if isfield(jnirs.nirs.stim(iEvt), 'dataLabels')
+ nStimDataCols = length(jnirs.nirs.stim(iEvt).dataLabels);
+ end
+ % Transpose to match number of columns
+ if size(jnirs.nirs.stim(iEvt).data, 1) == nStimDataCols && diff(size(jnirs.nirs.stim(iEvt).data)) ~= 0
jnirs.nirs.stim(iEvt).data = jnirs.nirs.stim(iEvt).data';
end
-
- isExtended = (size(jnirs.nirs.stim(iEvt).data,1) >= 2) && ~all(jnirs.nirs.stim(iEvt).data(2,:) == 0);
+ isExtended = ~all(jnirs.nirs.stim(iEvt).data(:,2) == 0);
if isExtended
- evtTime = [jnirs.nirs.stim(iEvt).data(1,:); ...
- jnirs.nirs.stim(iEvt).data(1,:) + jnirs.nirs.stim(iEvt).data(2,:)];
+ evtTime = [jnirs.nirs.stim(iEvt).data(:,1) , ...
+ jnirs.nirs.stim(iEvt).data(:,1) + jnirs.nirs.stim(iEvt).data(:,2)]';
else
- evtTime = jnirs.nirs.stim(iEvt).data(1,:)';
+ evtTime = jnirs.nirs.stim(iEvt).data(:,1)';
end
DataMat.Events(iEvt).times = evtTime;
diff --git a/toolbox/io/in_data_tvb.m b/toolbox/io/in_data_tvb.m
index d76f3e890..1984192be 100644
--- a/toolbox/io/in_data_tvb.m
+++ b/toolbox/io/in_data_tvb.m
@@ -32,6 +32,14 @@
ChannelFile = [];
end
+% Install/load EasyH5 Toolbox (https://github.com/NeuroJSON/easyh5) as plugin
+if ~exist('loadh5', 'file')
+ [isInstalled, errMsg] = bst_plugin('Install', 'easyh5');
+ if ~isInstalled
+ error(errMsg);
+ end
+end
+
% Read data from .h5
h5ts = loadh5(DataFile);
% Check data format
diff --git a/toolbox/io/in_events_brainamp.m b/toolbox/io/in_events_brainamp.m
index 06a9aeeaa..469e2ae75 100644
--- a/toolbox/io/in_events_brainamp.m
+++ b/toolbox/io/in_events_brainamp.m
@@ -1,4 +1,4 @@
-function events = in_events_brainamp(sFile, EventFile)
+function events = in_events_brainamp(sFile, ChannelMat, EventFile)
% IN_EVENTS_BRAINAMP: Open a BrainVision BrainAmp .vmrk file.
%
% OUTPUT:
@@ -27,6 +27,7 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2012
+% Raymundo Cassani, 2024
% Open and read file
@@ -44,7 +45,7 @@
% Lines to skip
if isempty(newLine)
continue;
- elseif ~isempty(strfind(newLine, '[Marker Infos]'))
+ elseif ~isempty(strfind(lower(newLine), '[marker infos]'))
isMarkerSection = 1;
elseif ~isMarkerSection || ismember(newLine(1), {'[', ';', char(10), char(13)}) || ~any(newLine == '=')
continue;
@@ -65,8 +66,14 @@
else
mlabel = 'Mk';
end
+ % Marker channels
+ if str2num(argLine{5}) == 0
+ channels = {[]};
+ else
+ channels = {{ChannelMat.Channel(str2num(argLine{5})).Name}};
+ end
% Add markers entry: {name, type, start, length}
- Markers(end+1,:) = {mlabel, argLine{1}, str2num(argLine{3}), str2num(argLine{4})};
+ Markers(end+1,:) = {mlabel, argLine{1}, str2num(argLine{3}), str2num(argLine{4}), channels};
end
% Close file
fclose(fid);
@@ -93,9 +100,40 @@
events(iEvt).times = samples ./ sFile.prop.sfreq;
events(iEvt).reactTimes = [];
events(iEvt).select = 1;
- events(iEvt).channels = [];
events(iEvt).notes = [];
+ if all(cellfun(@isempty, [Markers{iMrk,5}]))
+ events(iEvt).channels = [];
+ else
+ % Handle channel-wise events
+ events(iEvt).channels = [Markers{iMrk,5}];
+ end
end
+% Merge channel-wise events with same times
+for iEvt = 1:length(events)
+ if ~isempty(events(iEvt).channels)
+ % Find occurrences with channel
+ iwChannel = find(~cellfun(@isempty, [events(iEvt).channels]));
+ iwDelete = [];
+ for iw = 1 : length(iwChannel)
+ if ~ismember(iw, iwDelete)
+ % Find others channel-wise events with the same time, and not to be deleted yet
+ iwOthers = find(all(bsxfun(@eq, events(iEvt).times(:,iw), events(iEvt).times), 1));
+ % Exclude own
+ iwOthers = setdiff(iwOthers, iw);
+ % Only consider ones with channel
+ iwOthers = intersect(iwOthers, iwChannel);
+ for ix = 1 : length(iwOthers)
+ % Append the channel names
+ events(iEvt).channels{iw} = [events(iEvt).channels{iw}, events(iEvt).channels{iwOthers(ix)}];
+ iwDelete = [iwDelete, iwOthers(ix)];
+ end
+ end
+ end
+ events(iEvt).epochs(iwDelete) = [];
+ events(iEvt).times(:,iwDelete) = [];
+ events(iEvt).channels(:,iwDelete) = [];
+ end
+end
diff --git a/toolbox/io/in_events_oebin.m b/toolbox/io/in_events_oebin.m
index c7e54641d..0764e93da 100644
--- a/toolbox/io/in_events_oebin.m
+++ b/toolbox/io/in_events_oebin.m
@@ -1,5 +1,5 @@
function events = in_events_oebin(sFile, EventFile)
-% IN_EVENTS_OEBIN: Import events from a Open Ephys flat binary event file (timestamps.npy)
+% IN_EVENTS_OEBIN: Import events from a Open Ephys flat binary event sample_numbers file
%
% USAGE: events = in_events_oebin(sFile, EventFile)
@@ -22,10 +22,19 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2020
+% Raymundo Cassani, 2024
-% Read time stamps
-evtTime = reshape(readNPY(EventFile), 1, []);
-if isempty(evtTime)
+% Install/load npy-matlab Toolbox (https://github.com/kwikteam/npy-matlab) as plugin
+if ~exist('readNPY', 'file')
+ [isInstalled, errMsg] = bst_plugin('Install', 'npy-matlab');
+ if ~isInstalled
+ error(errMsg);
+ end
+end
+
+% Read event sample number
+evtIndices = reshape(readNPY(EventFile), 1, []);
+if isempty(evtIndices)
events = [];
return
end
@@ -40,7 +49,7 @@
if file_exist(TextFile)
disp('EOBIN> ERROR: Text events are not supported yet...');
evtGroupLabel = {'TEXT'};
- evtGroupInd = {1:length(evtTime)};
+ evtGroupInd = {1:length(evtIndices)};
evtChan = [];
% evtLabels = readNPY(TextFile);
@@ -70,7 +79,7 @@
evtChan = [];
else
evtGroupLabel = {'Unknown'};
- evtGroupInd = {1:length(evtTime)};
+ evtGroupInd = {1:length(evtIndices)};
evtChan = [];
end
@@ -79,7 +88,7 @@
% Get occurrences for each event
for iEvt = 1:length(evtGroupLabel)
events(iEvt).label = evtGroupLabel{iEvt};
- events(iEvt).times = double(evtTime(evtGroupInd{iEvt})) ./ sFile.prop.sfreq;
+ events(iEvt).times = double(evtIndices(evtGroupInd{iEvt})) ./ sFile.prop.sfreq;
events(iEvt).epochs = ones(1, length(events(iEvt).times)); % Epoch: set as 1 for all the occurrences
events(iEvt).reactTimes = [];
events(iEvt).select = 1;
diff --git a/toolbox/io/in_fopen.m b/toolbox/io/in_fopen.m
index 3d87ff10e..d6d8ac1f1 100644
--- a/toolbox/io/in_fopen.m
+++ b/toolbox/io/in_fopen.m
@@ -193,6 +193,8 @@
DataMat = in_data_ascii(DataFile);
case 'EEG-CARTOOL'
DataMat = in_data_cartool(DataFile);
+ case {'EEG-EDF-FT'}
+ [DataMat, ChannelMat] = in_data_edf_ft(DataFile);
case 'EEG-ERPCENTER'
DataMat = in_data_erpcenter(DataFile);
case 'EEG-ERPLAB'
diff --git a/toolbox/io/in_fopen_brainamp.m b/toolbox/io/in_fopen_brainamp.m
index f5d733cd3..6c86d0040 100644
--- a/toolbox/io/in_fopen_brainamp.m
+++ b/toolbox/io/in_fopen_brainamp.m
@@ -224,7 +224,7 @@
%% ===== READ EVENTS =====
if ~isempty(VmrkFile)
- sFile = import_events(sFile, [], VmrkFile, 'BRAINAMP', [], 0);
+ sFile = import_events(sFile, ChannelMat, VmrkFile, 'BRAINAMP', [], 0);
end
diff --git a/toolbox/io/in_fopen_bst.m b/toolbox/io/in_fopen_bst.m
index df3621fde..2b6415947 100644
--- a/toolbox/io/in_fopen_bst.m
+++ b/toolbox/io/in_fopen_bst.m
@@ -51,7 +51,7 @@
hdr.nchannels = double(fread(fid, [1 1], 'uint32')); % UINT32(1) : Number of channels
% ===== CHECK WHETHER VERSION IS SUPPORTED =====
-if (hdr.version > 52)
+if (hdr.version > 53)
error(['The selected version of the BST format is currently not supported.' ...
10 'Please update Brainstorm.']);
end
@@ -108,6 +108,12 @@
end
end
ChannelMat.Projector(i).Status = double(fread(fid, [1 1], 'int8')); % INT8(1) : Status
+ % September 2024: Added char array for projector method
+ if hdr.version >= 53
+ ChannelMat.Projector(i).Method = str_read(fid, 20); % CHAR(20) : Projector method
+ end
+ % Complete projector method if necesary
+ ChannelMat.Projector(i) = process_ssp2('ConvertOldFormat', ChannelMat.Projector(i));
end
% ===== HEAD POINTS =====
diff --git a/toolbox/io/in_fopen_bstmatrix.m b/toolbox/io/in_fopen_bstmatrix.m
index 4e773343b..930e6aa97 100644
--- a/toolbox/io/in_fopen_bstmatrix.m
+++ b/toolbox/io/in_fopen_bstmatrix.m
@@ -45,7 +45,7 @@
DataMat.Comment = MatrixMat.Comment;
DataMat.Time = MatrixMat.Time;
DataMat.Events = MatrixMat.Events;
-DataMat.ChannelFlag = ones(1, nSignals);
+DataMat.ChannelFlag = ones(nSignals, 1);
% Generate ChannelMat structure
ChannelMat = db_template('channelmat');
ChannelMat.Channel = repmat(db_template('channeldesc'), 1, nSignals);
diff --git a/toolbox/io/in_fopen_ctf.m b/toolbox/io/in_fopen_ctf.m
index da6639678..c7325ed6e 100644
--- a/toolbox/io/in_fopen_ctf.m
+++ b/toolbox/io/in_fopen_ctf.m
@@ -188,8 +188,19 @@
HeadMat = in_channel_pos(pos_file);
% Copy head points
ChannelMat.HeadPoints = HeadMat.HeadPoints;
+ isAlign = true;
+ % Warn if unsure about coordinate system. Should now be converted to "Native" CTF coil-based
+ % when HPI points are present when loading the pos file.
+ if ~isfield(HeadMat, 'TransfMegLabels') || ~iscell(HeadMat.TransfMegLabels) || isempty(HeadMat.TransfMegLabels)
+ disp('BST> Warning: Unable to confirm coordinate system of head points. Assuming "Native" CTF head-coil-based system.');
+ elseif ismember('Native=>Brainstorm/CTF', HeadMat.TransfMegLabels)
+ disp('BST> Warning: head point coordinates appear to already be in SCS, presumably because of missing digitized head coils.');
+ isAlign = false;
+ elseif ~ismember('RawPoints=>Native', HeadMat.TransfMegLabels)
+ disp('BST> Warning: Unable to confirm coordinate system of head points. Assuming "Native" CTF head-coil-based system.');
+ end
% Force re-alignment on the new set of NAS/LPA/RPA (switch from CTF coil-based to SCS anatomical-based coordinate system)
- ChannelMat = channel_detect_type(ChannelMat, 1, 0);
+ ChannelMat = channel_detect_type(ChannelMat, isAlign, 0);
end
%% ===== READ HC FILE =====
diff --git a/toolbox/io/in_fopen_edf.m b/toolbox/io/in_fopen_edf.m
index 9c24ca5a4..09c1896c0 100644
--- a/toolbox/io/in_fopen_edf.m
+++ b/toolbox/io/in_fopen_edf.m
@@ -120,9 +120,7 @@
hdr.signal(i).physical_max = hdr.signal(i).digital_max;
end
if (hdr.signal(i).physical_min >= hdr.signal(i).physical_max)
- disp(['EDF> Warning: Physical maximum larger than minimum for channel "' hdr.signal(i).label '".']);
- hdr.signal(i).physical_min = hdr.signal(i).digital_min;
- hdr.signal(i).physical_max = hdr.signal(i).digital_max;
+ disp(['EDF> Warning: Physical minimum larger than physical maximum for channel "' hdr.signal(i).label '".']);
end
% Calculate and save channel gain
hdr.signal(i).gain = unit_gain ./ (hdr.signal(i).physical_max - hdr.signal(i).physical_min) .* (hdr.signal(i).digital_max - hdr.signal(i).digital_min);
@@ -343,6 +341,7 @@
if ~isempty(iChanWrongRate)
sFile.channelflag(iChanWrongRate) = -1;
disp([sprintf('EDF> WARNING: Excluding channels with sampling rates other than %.3f Hz : ', hdr.signal(iChanFreqRef).sfreq), sprintf('%s ', ChannelMat.Channel(iChanWrongRate).Name)]);
+ disp( ' To uniform sampling rates import EDF file as "EEG EDF / EDF+ FieldTrip reader".');
end
% Consider that the sampling rate of the file is the sampling rate of the first signal
diff --git a/toolbox/io/in_fopen_fif.m b/toolbox/io/in_fopen_fif.m
index b32ba2aa2..3a1bb031c 100644
--- a/toolbox/io/in_fopen_fif.m
+++ b/toolbox/io/in_fopen_fif.m
@@ -53,6 +53,13 @@
end
%% ===== OPEN FIF FILE =====
+% Use MNE functions in brainstorm3/external/mne/matlab
+bst_plugin('Unload', 'fieldtrip');
+bst_plugin('Unload', 'spm12');
+% Reset FIFF
+global FIFF;
+FIFF = fiff_define_constants();
+
% Open file
[ fid, tree ] = fiff_open(DataFile);
if (fid < 0)
diff --git a/toolbox/io/in_fopen_intan.m b/toolbox/io/in_fopen_intan.m
index e61272b9a..b04524a33 100644
--- a/toolbox/io/in_fopen_intan.m
+++ b/toolbox/io/in_fopen_intan.m
@@ -38,7 +38,7 @@
%% ===== GET FILES =====
% Get base dataset folder
[hdr.BaseFolder, isItInfo, hdr.FileExt] = bst_fileparts(DataFile);
-[filePath, fileComment] = bst_fileparts(hdr.BaseFolder);
+[filePath, fileComment] = bst_fileparts(DataFile);
% Check the type of file
if strcmp(isItInfo,'info')
diff --git a/toolbox/io/in_fopen_itab.m b/toolbox/io/in_fopen_itab.m
index 091244505..85c8c7d38 100644
--- a/toolbox/io/in_fopen_itab.m
+++ b/toolbox/io/in_fopen_itab.m
@@ -34,9 +34,9 @@
hdr = read_itab_mhd(HeaderFile);
% Get endianness
switch (hdr.data_type)
- case {0,1,2}, byteorder = 'b';
- case {3,4,5}, byteorder = 'l';
- otherwise, error('Data type not supported.');
+ case {0,1,2, 6}, byteorder = 'b';
+ case {3,4,5}, byteorder = 'l';
+ otherwise, error('Data type not supported.');
end
@@ -52,7 +52,7 @@
% Position
for iCoil = 1:hdr.ch(iChannels(i)).ncoils
ChannelMat.Channel(i).Loc(:,iCoil) = hdr.ch(iChannels(i)).position(iCoil).r_s' ./ 1000;
- ChannelMat.Channel(i).Orient(:,iCoil) = hdr.ch(iChannels(i)).position(iCoil).u_s' ./ 1000;
+ ChannelMat.Channel(i).Orient(:,iCoil) = hdr.ch(iChannels(i)).position(iCoil).u_s';
ChannelMat.Channel(i).Weight(1,iCoil) = hdr.ch(iChannels(i)).wgt(iCoil);
end
% Type
@@ -112,7 +112,7 @@
%% ===== EXTRA HEAD POINTS =====
% Get extra head points
-HpLoc = hdr.marker;
+HpLoc = hdr.marker(1:3, :); % Keep only marker positions
iGood = find(~all(HpLoc == 0, 1));
HpLoc = HpLoc(:,iGood) ./ 1000;
nPoints = size(HpLoc,2);
diff --git a/toolbox/io/in_fopen_neuralynx.m b/toolbox/io/in_fopen_neuralynx.m
index 0fd8f7d14..c9d15b339 100644
--- a/toolbox/io/in_fopen_neuralynx.m
+++ b/toolbox/io/in_fopen_neuralynx.m
@@ -10,21 +10,31 @@
% http://www.fieldtriptoolbox.org/getting_started/neuralynx
%
% NCS files structure:
-% |- Header ASCII: 16*1044 bytes
+% |- Header ASCII: 16*1024 bytes
% |- Records: nRecords x 1044 bytes
% |- TimeStamp : uint64
% |- ChanNumber : int32
% |- SampFreq : int32
% |- NumValidSamp : int32
-% |- Data : 512 x int16
+% |- Data : 512 samples x int16
+%
% NSE files structure:
-% |- Header ASCII: 16*1044 bytes
+% |- Header ASCII: 16*1024 bytes
% |- Records: nRecords x 112 bytes
% |- TimeStamp : uint64
% |- ScNumber : int32
% |- CellNumber : int32
% |- Param : 8 x int32
-% |- Data : NumSamples x int16
+% |- Data : 32 samples x int16
+%
+% NTT files structure:
+% |- Header ASCII: 16*1024 bytes
+% |- Records: nRecords x 304 bytes
+% |- TimeStamp : uint64
+% |- ScNumber : int32
+% |- CellNumber : int32
+% |- Param : 8 x int32
+% |- Data : 4 channels x 32 samples x int16
% @=============================================================================
% This function is part of the Brainstorm software:
@@ -45,13 +55,14 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2015-2019
+% Raymundo Cassani, 2024
%% ===== GET FILES =====
% Get base dataset folder
if isdir(DataFile)
hdr.BaseFolder = DataFile;
-elseif strcmpi(DataFile(end-3:end), '.ncs') || strcmpi(DataFile(end-3:end), '.nse') || strcmpi(DataFile(end-3:end), '.nev')
+elseif ismember(lower(DataFile(end-3:end)), {'.ncs', '.nse', '.ntt', '.nev'})
hdr.BaseFolder = bst_fileparts(DataFile);
else
error('Invalid Neuralynx folder.');
@@ -62,13 +73,14 @@
disp(['BST> Warning: Events file not found in folder: ' 10 hdr.BaseFolder]);
EventFile = [];
end
-% Recordings (*.ncs; *.nse)
+% Recordings (*.ncs; *.nse; *.ntt)
NcsFiles = dir(bst_fullfile(hdr.BaseFolder, '*.ncs'));
NseFiles = dir(bst_fullfile(hdr.BaseFolder, '*.nse'));
+NttFiles = dir(bst_fullfile(hdr.BaseFolder, '*.ntt'));
if isempty(NcsFiles)
error(['Could not find any .ncs recordings in folder: ' 10 hdr.BaseFolder]);
end
-ChanFiles = sort({NcsFiles.name, NseFiles.name});
+ChanFiles = sort({NcsFiles.name, NseFiles.name, NttFiles.name});
%% ===== FILE COMMENT =====
@@ -90,7 +102,12 @@
error(['Missing fields in the file header of file: ' ChanFiles{i}]);
end
% Compute number of records saved in the file
- nRecordsFile = round((newHeader.FileSize - newHeader.HeaderSize) / newHeader.RecordSize);
+ switch newHeader.FileExtension
+ case {'NCS', 'NSE'}
+ nRecordsFile = round((newHeader.FileSize - newHeader.HeaderSize) / newHeader.RecordSize);
+ case {'NTT'}
+ nRecordsFile = round((newHeader.FileSize - newHeader.HeaderSize) / newHeader.RecordSize);
+ end
% Check if there are missing timestamps in the file
if isfield(newHeader, 'LastTimeStamp') && ~isempty(newHeader.LastTimeStamp)
nRecordsTime = round(double(newHeader.LastTimeStamp - newHeader.FirstTimeStamp) / 1e6 * newHeader.SamplingFrequency / 512) + 1;
@@ -100,11 +117,12 @@
nRecordsFile = nRecordsTime;
end
end
+
% Extract information needed for opening the file
if (i == 1)
hdr.FirstTimeStamp = newHeader.FirstTimeStamp;
hdr.LastTimeStamp = newHeader.LastTimeStamp;
- hdr.NumSamples = nRecordsFile * 512;
+ hdr.NumSamples = nRecordsFile * 512; % 512 samples per recording in NCS file
hdr.SamplingFrequency = newHeader.SamplingFrequency;
if isfield(newHeader, 'HardwareSubSystemType')
hdr.HardwareSubSystemType = newHeader.HardwareSubSystemType;
@@ -116,14 +134,31 @@
elseif isfield(newHeader, 'LastTimeStamp') && ~isempty(newHeader.LastTimeStamp) && ((hdr.FirstTimeStamp ~= newHeader.FirstTimeStamp) || (hdr.LastTimeStamp ~= newHeader.LastTimeStamp))
disp(['BST> Warning: Timestamps in "' ChanFiles{i} '" do not match "' ChanFiles{1} '". Skipping file...']);
continue;
- % For .nse files: Compute the spike times
- elseif strcmpi(newHeader.FileExtension, 'NSE')
+ end
+
+ % For spike files: Compute the spike times
+ if strcmpi(newHeader.FileType, 'Spike')
newHeader.SpikeTimes = double(newHeader.SpikeTimeStamps - hdr.FirstTimeStamp) / 1e6;
end
-
- % Save all file names
- hdr.chan_headers{end+1} = newHeader;
- hdr.chan_files{end+1} = ChanFiles{i};
+ % For .ntt files: Repeat the header 4 times, as there are four electrodes per NTT file
+ if strcmpi(newHeader.FileExtension, 'NTT')
+ nChannelsNtt = newHeader.NumADChannels;
+ newHeaders = repmat(newHeader, 1, nChannelsNtt);
+ for iChannelNtt = 1 : nChannelsNtt
+ newHeaders(iChannelNtt).NttIndexCh = iChannelNtt;
+ newHeaders(iChannelNtt).AcqEntName = sprintf('%s_%d', newHeader.AcqEntName, iChannelNtt);
+ newHeaders(iChannelNtt).ADBitVolts = newHeader.ADBitVolts(iChannelNtt);
+ newHeaders(iChannelNtt).ADChannel = newHeader.ADChannel(iChannelNtt);
+ newHeaders(iChannelNtt).InputRange = newHeader.InputRange(iChannelNtt);
+ newHeaders(iChannelNtt).ThreshVal = newHeader.ThreshVal(iChannelNtt);
+ end
+ newHeader = newHeaders;
+ end
+ % Save all headers for each channel file
+ for iHeader = 1 : length(newHeader)
+ hdr.chan_headers{end+1} = newHeader(iHeader);
+ hdr.chan_files{end+1} = ChanFiles{i};
+ end
end
hdr.NumChannels = length(hdr.chan_files);
@@ -156,8 +191,12 @@
ChannelMat.Channel = repmat(db_template('channeldesc'), [1, hdr.NumChannels]);
% For each channel
for i = 1:hdr.NumChannels
- [fPath,fName,fExt] = bst_fileparts(hdr.chan_files{i});
- ChannelMat.Channel(i).Name = fName;
+ if isfield(hdr.chan_headers{i}, 'AcqEntName') && ~isempty(hdr.chan_headers{i}.AcqEntName)
+ channelName = hdr.chan_headers{i}.AcqEntName;
+ else
+ [~, channelName] = bst_fileparts(hdr.chan_files{i});
+ end
+ ChannelMat.Channel(i).Name = channelName;
ChannelMat.Channel(i).Loc = [0; 0; 0];
ChannelMat.Channel(i).Type = 'EEG';
ChannelMat.Channel(i).Orient = [];
diff --git a/toolbox/io/in_fopen_nirs_brs.m b/toolbox/io/in_fopen_nirs_brs.m
index 73b1bf43a..dd3f3c481 100644
--- a/toolbox/io/in_fopen_nirs_brs.m
+++ b/toolbox/io/in_fopen_nirs_brs.m
@@ -252,13 +252,13 @@
measure_type = 'Hb';
ChannelMat.Nirs.Hb = nirs.SD.Lambda;
else
-
measure_type = 'WL';
if( size(nirs.SD.Lambda,1) > 1) % Wavelengths have to be stored as a line vector
ChannelMat.Nirs.Wavelengths = nirs.SD.Lambda';
else
ChannelMat.Nirs.Wavelengths = nirs.SD.Lambda;
end
+ ChannelMat.Nirs.Wavelengths = round(ChannelMat.Nirs.Wavelengths);
end
%% Channel information
@@ -276,7 +276,7 @@
if strcmp(measure_type, 'Hb')
measure_tag = ChannelMat.Nirs.Hb{idx_measure};
else
- measure_tag = sprintf('WL%d', round(ChannelMat.Nirs.Wavelengths(idx_measure)));
+ measure_tag = sprintf('WL%d', ChannelMat.Nirs.Wavelengths(idx_measure));
end
Channel(iChan).Name = sprintf('S%dD%d%s', idx_src, idx_det, ...
measure_tag);
diff --git a/toolbox/io/in_fopen_oebin.m b/toolbox/io/in_fopen_oebin.m
index 11405c05f..9b8af339b 100644
--- a/toolbox/io/in_fopen_oebin.m
+++ b/toolbox/io/in_fopen_oebin.m
@@ -25,8 +25,16 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2020
+% Raymundo Cassani, 2024
%% ===== GET FILES =====
+% Install/load npy-matlab Toolbox (https://github.com/kwikteam/npy-matlab) as plugin
+if ~exist('readNPY', 'file')
+ [isInstalled, errMsg] = bst_plugin('Install', 'npy-matlab');
+ if ~isInstalled
+ error(errMsg);
+ end
+end
% Build header and markers files names
procDir = bst_fileparts(DataFile);
[contDir, procName] = bst_fileparts(procDir);
@@ -34,23 +42,29 @@
expDir = bst_fileparts(recDir);
[parentDir, expName] = bst_fileparts(expDir);
[tmp, parentName] = bst_fileparts(parentDir);
-% Timestamp file
-TimeFile = bst_fullfile(procDir, 'timestamps.npy');
-if ~file_exist(TimeFile)
- error(['Could not find timestamp file: ' TimeFile]);
-end
% OEBIN JSON header
OebinFile = bst_fullfile(recDir, 'structure.oebin');
if ~file_exist(OebinFile)
error(['Could not find header file: ' OebinFile]);
end
-% Event files
-EvtFiles = file_find(bst_fullfile(recDir, 'events'), 'timestamps.npy', 3, 0);
-
-
-%% ===== READ HEADER =====
% Read JSON file
hdr = bst_jsondecode(OebinFile);
+% Identify file with sample indices depending on the Open Ephys GUI version
+if bst_plugin('CompareVersions', hdr.GUIVersion, '0.6.0') == -1
+ SampleIndicesFileName = 'timestamps.npy';
+else
+ SampleIndicesFileName = 'sample_numbers.npy';
+end
+% Sample indices file
+SampleIndicesFile = file_find(procDir, SampleIndicesFileName, 1, 1);
+if ~file_exist(SampleIndicesFile)
+ error(['Could not find file with sample indices: ' SampleIndicesFileName]);
+end
+% Event indices files
+EvtIndicesFiles = file_find(bst_fullfile(recDir, 'events'), SampleIndicesFileName, 3, 0);
+
+
+%% ===== PARSE HEADER =====
% If there are multiple processors in the same recording: find the one corresponding to this .dat file
if (length(hdr.continuous) > 1)
iRec = find(strcmpi({hdr.continuous.folder_name}, [procName, '/']));
@@ -68,9 +82,11 @@
error('Recordings do not contain continuous data.');
end
hdr.continuous = hdr.continuous(iRec);
-% Read time stamps
-TimeStamps = readNPY(TimeFile);
-
+% Read sample indices
+SampleIndices = readNPY(SampleIndicesFile);
+% Add first and last sample indices to header structure
+hdr.first_samp = double(SampleIndices(1));
+hdr.last_samp = double(SampleIndices(end));
%% ===== CREATE BRAINSTORM SFILE STRUCTURE =====
% Initialize returned file structure
@@ -85,7 +101,7 @@
sFile.condition = parentName;
% Consider that the sampling rate of the file is the sampling rate of the first signal
sFile.prop.sfreq = hdr.continuous.sample_rate;
-sFile.prop.times = double([TimeStamps(1), TimeStamps(1) + length(TimeStamps) - 1]) ./ sFile.prop.sfreq;
+sFile.prop.times = double([SampleIndices(1), SampleIndices(1) + length(SampleIndices) - 1]) ./ sFile.prop.sfreq;
sFile.prop.nAvg = 1;
% No info on bad channels
sFile.channelflag = ones(hdr.continuous.num_channels, 1);
@@ -124,8 +140,8 @@
%% ===== READ EVENTS =====
-for iFile = 1:length(EvtFiles)
- sFile = import_events(sFile, [], EvtFiles{iFile}, 'OEBIN', [], 0);
+for iFile = 1:length(EvtIndicesFiles)
+ sFile = import_events(sFile, [], EvtIndicesFiles{iFile}, 'OEBIN', [], 0);
end
diff --git a/toolbox/io/in_fread_edf.m b/toolbox/io/in_fread_edf.m
index 1c54bc8c4..2e40a25a3 100644
--- a/toolbox/io/in_fread_edf.m
+++ b/toolbox/io/in_fread_edf.m
@@ -219,11 +219,12 @@
F = double(F);
% Apply gains
F = bst_bsxfun(@rdivide, F, [sFile.header.signal(iChannels).gain]');
-% IN THEORY: THIS OFFSET SECTION IS USEFUL, BUT IN PRACTICE IT LOOKS LIKE THE VALUES IN ALL THE FILES ARE CENTERED ON ZERO
-% % Add offset
-% if isfield(sFile.header.signal, 'offset') && ~isempty(sFile.header.signal(1).offset)
-% % ...
-% end
+ % Add offset
+ if isfield(sFile.header.signal, 'offset') && ~isempty(sFile.header.signal(1).offset)
+ offsets = [sFile.header.signal(iChannels).offset];
+ offsets(isnan(offsets)) = 0;
+ F = bst_bsxfun(@plus, F, offsets');
+ end
end
end
diff --git a/toolbox/io/in_fread_itab.m b/toolbox/io/in_fread_itab.m
index fc295eeb9..280312b3e 100644
--- a/toolbox/io/in_fread_itab.m
+++ b/toolbox/io/in_fread_itab.m
@@ -36,7 +36,7 @@
% ===== READ DATA =====
% Get data type
switch (sFile.header.data_type)
- case {0,3}
+ case {0,3,6}
bytesPerVal = 2;
dataClass = 'int16';
case {1,4}
diff --git a/toolbox/io/in_fread_neuralynx.m b/toolbox/io/in_fread_neuralynx.m
index e4b04aa5d..5d5ac5b77 100644
--- a/toolbox/io/in_fread_neuralynx.m
+++ b/toolbox/io/in_fread_neuralynx.m
@@ -22,6 +22,7 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2015-2021
+% Raymundo Cassani, 2024
% Parse inputs
if (nargin < 3) || isempty(iChannels)
@@ -68,27 +69,38 @@
% Copy values to final matrix
F(iChan, :) = Ftmp(iSamples);
- % NSE: Spike files, just set the values wherever they are defined
- case 'NSE'
+ % NSE and NTT: Spike files, just set the values wherever they are defined
+ case {'NSE', 'NTT'}
+ isNtt = strcmpi(hdr.FileExtension, 'NTT');
+ % Number of channels in file
+ if isNtt
+ nChannels = 4;
+ else
+ nChannels = 1;
+ end
% Compute the spikes samples
SpikeSamples = round(hdr.SpikeTimes .* sFile.prop.sfreq);
% Get the spikes happening during the selected segment
iSpikes = find((SpikeSamples + hdr.NumSamples >= SamplesBounds(1)) & (SpikeSamples <= SamplesBounds(2)));
- % Size of one record in the file
- sizeRecHdr = 48 + hdr.NumSamples * 2;
+ % Size the header in each record
+ sizeRecHdr = hdr.RecordSize - hdr.NumSamples * 2 * nChannels;
% Loop on the spikes that were found
for i = 1:length(iSpikes)
% Seek at the beginning of the spike data
offsetStart = hdr.HeaderSize + (iSpikes(i)-1) * hdr.RecordSize + sizeRecHdr;
fseek(sfid, offsetStart, 'bof');
% Read the spike data
- dat = fread(sfid, hdr.NumSamples, 'int16');
+ dat = fread(sfid, [nChannels, hdr.NumSamples], 'int16');
% Find the samples of this spike in the read segment
iSmpSpike = 1:hdr.NumSamples;
iSmpFile = SpikeSamples(iSpikes(i)) - SamplesBounds(1) + iSmpSpike;
iGoodSmp = find((iSmpFile >= 1) & (iSmpFile <= nReadSamples));
% Set the data in the file
- F(iChan, iSmpFile(iGoodSmp)) = dat(iSmpSpike(iGoodSmp));
+ if isNtt
+ F(iChan, iSmpFile(iGoodSmp)) = dat(hdr.NttIndexCh, iSmpSpike(iGoodSmp));
+ else
+ F(iChan, iSmpFile(iGoodSmp)) = dat(iSmpSpike(iGoodSmp));
+ end
end
end
% Close file
diff --git a/toolbox/io/in_fread_oebin.m b/toolbox/io/in_fread_oebin.m
index 7c22281d3..75e64c1a8 100644
--- a/toolbox/io/in_fread_oebin.m
+++ b/toolbox/io/in_fread_oebin.m
@@ -22,14 +22,20 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2020
+% Raymundo Cassani, 2024
% Parse inputs
if (nargin < 4) || isempty(ChannelsRange)
ChannelsRange = [1, sFile.header.continuous.num_channels];
end
if (nargin < 3) || isempty(SamplesBounds)
- SamplesBounds = round(sFile.prop.times .* sFile.prop.sfreq);
+ SamplesBounds = [sFile.header.first_samp, sFile.header.last_samp];
end
+% Clip sample limits
+SamplesBounds(1) = max(SamplesBounds(1), sFile.header.first_samp);
+SamplesBounds(2) = min(SamplesBounds(2), sFile.header.last_samp);
+% Remove first sample offset
+SamplesBounds = SamplesBounds - sFile.header.first_samp;
% ===== COMPUTE OFFSETS =====
nChannels = double(sFile.header.continuous.num_channels);
diff --git a/toolbox/io/in_mri.m b/toolbox/io/in_mri.m
index 5098ba1b1..b4e816e40 100644
--- a/toolbox/io/in_mri.m
+++ b/toolbox/io/in_mri.m
@@ -133,7 +133,13 @@
if isInteractive
[MRI, vox2ras, tReorient] = in_mri_mgh(MriFile, [], []);
else
- [MRI, vox2ras, tReorient] = in_mri_mgh(MriFile, 1, 0);
+ mriDir = bst_fileparts(MriFile);
+ isReconAllClinical = ~isempty(file_find(mriDir, 'synthSR.mgz', 2));
+ if isReconAllClinical
+ [MRI, vox2ras, tReorient] = in_mri_mgh(MriFile, 0, 1);
+ else
+ [MRI, vox2ras, tReorient] = in_mri_mgh(MriFile, 1, 0);
+ end
end
case 'KIT'
error('Not supported yet');
diff --git a/toolbox/io/in_mri_mgh.m b/toolbox/io/in_mri_mgh.m
index 1882db0f9..e01fda753 100644
--- a/toolbox/io/in_mri_mgh.m
+++ b/toolbox/io/in_mri_mgh.m
@@ -7,7 +7,7 @@
% - MriFile : full path to a MRI file, WITH EXTENSION
% - isApplyBst : If 1, apply best orientation found to match Brainstorm convention
% considering that the volume is aligned as the standard T1.mgz in the
-% FreeSurfer output folder.
+% FreeSurfer output folder from 'recon-all'.
% - isApplyVox2ras : Apply additional transformation to the volume
% OUTPUT:
% - sMri : Standard brainstorm structure for MRI volumes
@@ -142,7 +142,7 @@
if isempty(isApplyBst)
isApplyBst = java_dialog('confirm', ['Apply the standard transformation FreeSurfer=>Brainstorm?' 10 10 ...
'Answer "yes" if importing transformed volumes such as T1.mgz in the' 10 ...
- 'FreeSurfer output folder, or other volumes in the same folder.' 10 10], 'MRI orientation');
+ 'FreeSurfer output folder from ''recon-call'', or other volumes in the same folder.' 10 10], 'MRI orientation');
end
% Apply transformation
diff --git a/toolbox/io/in_tess.m b/toolbox/io/in_tess.m
index 911f68a9c..7dc222b8b 100644
--- a/toolbox/io/in_tess.m
+++ b/toolbox/io/in_tess.m
@@ -13,7 +13,8 @@
% OUTPUT:
% - TessMat: Brainstorm tesselation structure with fields:
% |- Vertices : {[3 x nbVertices] double}, in millimeters
-% |- Faces : {[nbFaces x 3] double}
+% |- Faces : {[nbFaces x 3] double} (optional, volume meshes do not have 'Faces')
+% |- Color : {[nColors x 3] double}, normalized between 0-1 (optional, not all surfaces have color info)
% |- Comment : {information string}
% @=============================================================================
@@ -178,6 +179,10 @@
TessMat.Faces = TessMat.Faces(:,[2 1 3]);
case 'OFF'
TessMat = in_tess_off(TessFile);
+ % Vertices: convert to meters
+ TessMat.Vertices = TessMat.Vertices ./ 1000;
+ % Swap faces
+ TessMat.Faces = TessMat.Faces(:,[2 1 3]);
case 'TRI'
TessMat = in_tess_tri(TessFile);
case 'DSGL'
@@ -213,7 +218,11 @@
T = sMri.Header.info.mat(1:3,4)' - 1;
TessMat.Vertices = bst_bsxfun(@minus, TessMat.Vertices, T / 1000);
end
-
+
+ case 'WFTOBJ'
+ TessMat = in_tess_wftobj(TessFile);
+ isConvertScs = 0;
+
case 'MRI-MASK'
[TessMat, Labels] = in_tess_mrimask(TessFile, 0, SelLabels);
@@ -268,6 +277,14 @@
%% ===== COMMENT =====
% Add a comment field to the TessMat structure.
+
+if ~isempty(sMri)
+ % Get the current subject
+ sSubject = bst_get('MriFile', sMri.FileName);
+ % Unique comment
+ fileBase = file_unique(fileBase, {sSubject.Surface.Comment});
+end
+
% If various tesselations were loaded from one file
if (length(TessMat) > 1)
for iTess = 1:length(TessMat)
diff --git a/toolbox/io/in_tess_bst.m b/toolbox/io/in_tess_bst.m
index 3c8efe1fc..7188ee4eb 100644
--- a/toolbox/io/in_tess_bst.m
+++ b/toolbox/io/in_tess_bst.m
@@ -52,6 +52,7 @@
% - Remove cells: Old Brainstorm surface files contained more than one tesselation, now one tesselation = file
% - Check matrix orientations
% - Convert to double
+% - Add Color field
UpdateFile = 0;
if isfield(TessMat, 'Faces')
TessMat.Faces = double(TessMat.Faces);
@@ -87,6 +88,10 @@
TessMat.Curvature = TessMat.Curvature{1};
UpdateFile = 1;
end
+if ~isfield(TessMat, 'Color')
+ TessMat.Color = [];
+ UpdateFile = 1;
+end
% ===== ATLASES =====
diff --git a/toolbox/io/in_tess_off.m b/toolbox/io/in_tess_off.m
index e3deec5d6..8cbae2579 100644
--- a/toolbox/io/in_tess_off.m
+++ b/toolbox/io/in_tess_off.m
@@ -59,7 +59,7 @@
Vertices = double(fscanf(fid, '%f', [3 nVertices]));
% Go to next line
fgetl(fid);
-% Read faces
+% Read faces, add 1 (convert to 1-based indices)
Faces = double(fscanf(fid, '%f',[4 nFaces]) + 1);
Faces = Faces(2:4,:);
% Close file
diff --git a/toolbox/io/in_tess_wftobj.m b/toolbox/io/in_tess_wftobj.m
new file mode 100644
index 000000000..f4bdd82ab
--- /dev/null
+++ b/toolbox/io/in_tess_wftobj.m
@@ -0,0 +1,200 @@
+function TessMat = in_tess_wftobj(TessFile)
+% IN_TESS_WFTOBJ: Load a WAVEFRONT OBJ mesh file.
+%
+% USAGE: TessMat = in_tess_wftobj(TessFile, FileType);
+%
+% INPUT:
+% - TessFile : full path to a tesselation file (*.obj)
+%
+% OUTPUT:
+% - TessMat: Brainstorm tesselation structure with fields:
+% |- Vertices : {[nVertices x 3] double}, in millimeters
+% |- Faces : {[nFaces x 3] double}
+% |- Color : {[nColors x 3] double}, normalized between 0-1
+% |- Comment : {information string}
+%
+% @=============================================================================
+% 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: Yash Shashank Vakilna, 2024
+% Chinmay Chinara, 2024
+% Raymundo Cassani, 2024
+
+%% ===== PARSE INPUTS =====
+% Check inputs
+if (nargin < 1)
+ bst_error('Invalid call. Please specify the mesh file to be loaded.', 'Importing tesselation', 1);
+end
+
+%% ===== PARSE THE OBJ FILE: SET UP IMPORT OPTIONS AND IMPORT THE DATA =====
+if bst_get('MatlabVersion') < 901
+ % MATLAB < R2016b
+ % Read entire .wobj file
+ fid = fopen(TessFile, 'r');
+ txtStr = fread(fid, '*char')';
+ fclose(fid);
+ % Keep relevant lines from file content
+ allData = regexp(txtStr, '(\w)+ ([^\n])*\n', 'tokens'); % (\w) ignores comments (#)
+ allData = cat(1,allData{:});
+ % Read data for each element type
+ elementTags = {'v', 'vt', 'f'}; % Vertices, Texture, Faces
+ elementData = cell(1, length(elementTags));
+ % Parse element data
+ for iElement = 1: length(elementTags)
+ iLines = strcmp(elementTags{iElement}, allData(:,1));
+ elementTmp = regexp(allData(iLines, 2), '([e|\-|\.|\d])*', 'match')';
+ elementTmp = cat(1, elementTmp{:});
+ elementSize = size(elementTmp);
+ elementTmp = sscanf(sprintf(' %s', elementTmp{:}), '%f'); % Faster than str2double
+ elementData{iElement} = reshape(elementTmp, elementSize);
+ end
+ vertices = elementData{1, 1}(:, 1:3); % Use only the first 3
+ texture = elementData{2};
+ faces = elementData{1, 3}(:, [3,6,9]);
+ textureIdx = elementData{1, 3}(:, [2,5,8]);
+else
+ % MATLAB R2016b to R2018a had 'DelimitedTextImportOptions'
+ if(bst_get('MatlabVersion') >= 901) && (bst_get('MatlabVersion') <= 904)
+ opts = matlab.io.text.DelimitedTextImportOptions();
+ else
+ opts = delimitedTextImportOptions('NumVariables', 10);
+ end
+
+ opts.Delimiter = {' ', '/'};
+ opts.VariableNames = {'type', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9'};
+ opts.VariableTypes = {'categorical', 'double', 'double', 'double', 'double', 'double', 'double', 'double', 'double', 'double'};
+
+ % Specify file level properties
+ opts.ExtraColumnsRule = 'ignore';
+ opts.EmptyLineRule = 'read';
+
+ % Import the data
+ objtbl = readtable(TessFile, opts);
+
+ obj = struct;
+ obj.Vertices = objtbl{objtbl.type=='v', 2:4};
+ obj.VertexNormals = objtbl{objtbl.type=='vn', 2:4};
+ obj.Faces = objtbl{objtbl.type=='f', [2,5,8]};
+ obj.TextCoords = objtbl{objtbl.type=='vt', 2:3};
+ obj.TextIndices = objtbl{objtbl.type=='f', [3,6,9]};
+ % For some OBJ's exported from 3D softwares like Maya and Blender, when parsed using
+ % 'readtable', the vertex coordinates start from the 3rd column
+ if isnan(obj.Vertices(:,1))
+ obj.Vertices = objtbl{objtbl.type=='v', 3:5};
+ end
+ vertices = obj.Vertices;
+ faces = obj.Faces;
+ texture = obj.TextCoords;
+ textureIdx = obj.TextIndices;
+end
+
+%% ===== REFINE FACES, MESH AND GENERATE COLOR MATRIX =====
+% Check if there exists a .jpg file of 'TessFile'
+[pathstr, name] = fileparts(TessFile);
+if exist(fullfile(pathstr, [name, '.jpg']), 'file')
+ image = fullfile(pathstr, [name, '.jpg']);
+ hasimage = true;
+elseif exist(fullfile(pathstr,[name,'.png']), 'file')
+ image = fullfile(pathstr,[name,'.png']);
+ hasimage = true;
+else
+ hasimage = false;
+end
+
+% Check if the texture is defined per vertex, in which case the texture can be refined below
+if size(texture, 1)==size(vertices, 1)
+ texture_per_vert = true;
+else
+ texture_per_vert = false;
+end
+
+% Remove the faces with 0's first
+allzeros = sum(faces==0,2)==3;
+faces(allzeros, :) = [];
+textureIdx(allzeros, :) = [];
+
+% Check whether all vertices belong to a face. If not, prune the vertices and keep the faces consistent.
+ufacesIdx = unique(faces(:));
+remove = setdiff((1:size(vertices, 1))', ufacesIdx);
+if ~isempty(remove)
+ [vertices, faces] = tess_remove_vert(vertices, faces, remove);
+ if texture_per_vert
+ % Also remove the removed vertices from the texture
+ texture(remove, :) = [];
+ end
+end
+
+color = [];
+if hasimage
+ % If true then there is an image/texture with color information
+ if texture_per_vert
+ picture = imread(image);
+ color = zeros(size(vertices, 1), 3);
+ for i = 1:size(vertices, 1)
+ color(i,1:3) = picture(floor((1-texture(i,2))*length(picture)),1+floor(texture(i,1)*length(picture)),1:3);
+ end
+ else
+ % Do the texture to color mapping in a different way, without additional refinement
+ picture = flip(imread(image),1);
+ [sy, sx, sz] = size(picture);
+ picture = reshape(picture, sy*sx, sz);
+
+ % Make image 3D if grayscale
+ if sz == 1
+ picture = repmat(picture, 1, 3);
+ end
+ [~, ix] = unique(faces);
+ textureIdx = textureIdx(ix);
+
+ % Get the indices into the image
+ x = abs(round(texture(:,1)*(sx-1)))+1;
+ y = abs(round(texture(:,2)*(sy-1)))+1;
+
+ % Eliminates points out of bounds
+ if any(x > sx)
+ texture(x > sx,:) = 1;
+ x(x > sx) = sx;
+ end
+
+ if any(find(y > sy))
+ texture(y > sy,:) = 1;
+ y(y > sy) = sy;
+ end
+
+ xy = sub2ind([sy sx], y, x);
+ sel = xy(textureIdx);
+ color = double(picture(sel,:))/255;
+ end
+
+ % If color is specified as 0-255 rather than 0-1 correct by dividing by 255
+ if range(color(:)) > 1
+ color = color./255;
+ end
+end
+
+% Centering vertices
+vertices = vertices - repmat(mean(vertices,1), [size(vertices, 1),1]);
+
+% Convert vertices' unit as locations in Brainstorm are saved in 'meters'
+vertices = channel_fixunits(vertices, 'mm', 1, 1);
+
+%% ===== BRAINSTORM SURFACE STRUCTURE =====
+TessMat = struct('Faces', faces, ...
+ 'Vertices', vertices, ...
+ 'Color', color, ...
+ 'Comment', '');
diff --git a/toolbox/io/out_channel_nirs_brainsight.m b/toolbox/io/out_channel_brainsight.m
old mode 100644
new mode 100755
similarity index 86%
rename from toolbox/io/out_channel_nirs_brainsight.m
rename to toolbox/io/out_channel_brainsight.m
index 6eda3f38d..8f141342b
--- a/toolbox/io/out_channel_nirs_brainsight.m
+++ b/toolbox/io/out_channel_brainsight.m
@@ -1,135 +1,140 @@
-function out_channel_nirs_brainsight(BstFile, OutputFile, Factor, Transf)
-% OUT_CHANNEL_NIRS_BRAINSIGHT: Export a Brainstorm channel file in
-% brainsight coordinate files.
-%
-% USAGE: out_channel_nirs_brainsight( BstFile, OutputFile, Factor, Transf)
-%
-% INPUT:
-% - BstFile : full path to Brainstorm file to export
-% - OutputFile : full path to output file
-% - Factor : Factor to convert the positions values in meters.
-% - Transf : 4x4 transformation matrix to apply to the 3D positions before saving
-% or entire MRI structure. Optodes are
-% exported using world coordinates.
-%
-% @=============================================================================
-% 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: Thomas Vincent 2017, Edouard Delaire 2023
-
-if (nargin < 3) || isempty(Factor)
- Factor = .001;
-end
-if (nargin < 4) || isempty(Transf)
- Transf = [];
-end
-
-
-% Load brainstorm channel file
-if ischar(BstFile)
- ChannelMat = in_bst_channel(BstFile);
-else
- ChannelMat = BstFile;
-end
-
-if ~isfield(ChannelMat, 'Nirs')
- bst_error('Channel file does not correspond to NIRS data.');
- return;
-end
-
-Loc = zeros(3,0);
-Label = {};
-Type = {};
-
-for i = 1:length(ChannelMat.Channel)
- if ~isempty(ChannelMat.Channel(i).Loc) && ~all(ChannelMat.Channel(i).Loc(:) == 0)
- CHAN_RE = '^S([0-9]+)D([0-9]+)(WL\d+|HbO|HbR|HbT)$';
- toks = regexp(strrep(ChannelMat.Channel(i).Name, ' ', '_'), CHAN_RE, 'tokens');
-
- Loc(:,end+1) = ChannelMat.Channel(i).Loc(:,1);
- Label{end+1} = sprintf('S%s',toks{1}{1} );
- Type{end+1} = 'source';
-
- Loc(:,end+1) = ChannelMat.Channel(i).Loc(:,2);
- Label{end+1} = sprintf('D%s',toks{1}{2} );
- Type{end+1} = 'detector';
- end
-end
-
-% Remove duplicate and sort Sources / Detectors
-[Label, I] = unique(Label, 'stable');
-Loc = Loc(:,I);
-Type = Type(I);
-
-[Type, I] = sort(Type);
-Label = Label(I);
-Loc = Loc(:,I);
-
-
-if isfield(ChannelMat, 'HeadPoints') && ~isempty(ChannelMat.HeadPoints) && ~isempty(ChannelMat.HeadPoints.Loc)
- % Find fiducials in the head points
- iCardinal = find(strcmpi(ChannelMat.HeadPoints.Type, 'CARDINAL'));
- fidu_coords = ChannelMat.HeadPoints.Loc(:,iCardinal);
- fidu_labels = ChannelMat.HeadPoints.Label(iCardinal);
-
- Label = [Label, fidu_labels];
- Loc = [Loc , fidu_coords ];
-end
-
-% Apply transformation
-if ~isempty(Transf)
- if isstruct(Transf)
- Loc = cs_convert(Transf, 'scs', 'world', Loc')';
- % World coordinates
- else
- R = Transf(1:3,1:3);
- T = Transf(1:3,4);
- Loc = R * Loc + T * ones(1, size(Loc,2));
- end
-end
-% Apply factor
-Loc = Loc ./ Factor;
-
-
-% Format header
-header = sprintf(['# Version: 5\n# Coordinate system: NIftI-Aligned\n# Created by: Brainstorm (nirstorm plugin)\n' ...
- '# units: millimetres, degrees, milliseconds, and microvolts\n# Encoding: UTF-8\n' ...
- '# Notes: Each column is delimited by a tab. Each value within a column is delimited by a semicolon.\n' ...
- '# Sample Name Index Loc. X Loc. Y Loc. Z Offset\n']);
-
-
-% Open output file
-fid = fopen(OutputFile, 'w');
-if (fid < 0)
- error('Cannot open file');
-end
-
-fprintf(fid, '%s', header);
-% Write file: one line per location
-for i = 1:length(Label)
- fprintf(fid,'%s\t%d\t%f\t%f\t%f\t0.0\n',Label{i}, i, Loc(1, i), Loc(2, i), Loc(3, i));
-end
-
-% Close file
-fclose(fid);
-
-end
-
-
-
-
+function out_channel_brainsight(BstFile, OutputFile, Factor, Transf)
+% OUT_CHANNEL_BRAINSIGHT: Export a Brainstorm channel file in
+% brainsight coordinate files.
+%
+% USAGE: out_channel_brainsight( BstFile, OutputFile, Factor, Transf)
+%
+% INPUT:
+% - BstFile : full path to Brainstorm file to export
+% - OutputFile : full path to output file
+% - Factor : Factor to convert the positions values in meters.
+% - Transf : 4x4 transformation matrix to apply to the 3D positions before saving
+% or entire MRI structure. Optodes are
+% exported using world coordinates.
+%
+% @=============================================================================
+% 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: Thomas Vincent 2017, Edouard Delaire 2023
+
+if (nargin < 3) || isempty(Factor)
+ Factor = .001;
+end
+if (nargin < 4) || isempty(Transf)
+ Transf = [];
+end
+
+
+% Load brainstorm channel file
+if ischar(BstFile)
+ ChannelMat = in_bst_channel(BstFile);
+else
+ ChannelMat = BstFile;
+end
+
+
+Loc = zeros(3,0);
+Label = {};
+Type = {};
+
+for i = 1:length(ChannelMat.Channel)
+ if strcmpi(ChannelMat.Channel(i).Type,'NIRS')
+ CHAN_RE = '^S([0-9]+)D([0-9]+)(WL\d+|HbO|HbR|HbT)$';
+ toks = regexp(strrep(ChannelMat.Channel(i).Name, ' ', '_'), CHAN_RE, 'tokens');
+
+ Loc(:,end+1) = ChannelMat.Channel(i).Loc(:,1);
+ Label{end+1} = sprintf('S%s',toks{1}{1} );
+ Type{end+1} = 'source';
+
+ Loc(:,end+1) = ChannelMat.Channel(i).Loc(:,2);
+ Label{end+1} = sprintf('D%s',toks{1}{2} );
+ Type{end+1} = 'detector';
+ elseif strcmpi(ChannelMat.Channel(i).Type,'EEG')
+ Loc(:,end+1) = ChannelMat.Channel(i).Loc;
+ Label{end+1} = ChannelMat.Channel(i).Name;
+ Type{end+1} = 'EEG';
+ end
+end
+
+if isempty(Label)
+ bst_error('Channel file does not contain EEG nor NIRS channels.');
+ return;
+end
+
+% Remove duplicate and sort Sources / Detectors
+[Label, I] = unique(Label, 'stable');
+Loc = Loc(:,I);
+Type = Type(I);
+
+[Type, I] = sort(Type);
+Label = Label(I);
+Loc = Loc(:,I);
+
+
+if isfield(ChannelMat, 'HeadPoints') && ~isempty(ChannelMat.HeadPoints) && ~isempty(ChannelMat.HeadPoints.Loc)
+ % Find fiducials in the head points
+ iCardinal = find(strcmpi(ChannelMat.HeadPoints.Type, 'CARDINAL'));
+ fidu_coords = ChannelMat.HeadPoints.Loc(:,iCardinal);
+ fidu_labels = ChannelMat.HeadPoints.Label(iCardinal);
+
+ Label = [Label, fidu_labels];
+ Loc = [Loc , fidu_coords ];
+end
+
+% Apply transformation
+if ~isempty(Transf)
+ if isstruct(Transf)
+ Loc = cs_convert(Transf, 'scs', 'world', Loc')';
+ % World coordinates
+ else
+ R = Transf(1:3,1:3);
+ T = Transf(1:3,4);
+ Loc = R * Loc + T * ones(1, size(Loc,2));
+ end
+end
+% Apply factor
+Loc = Loc ./ Factor;
+
+
+% Format header
+header = sprintf(['# Version: 5\n# Coordinate system: NIftI-Aligned\n# Created by: Brainstorm (nirstorm plugin)\n' ...
+ '# units: millimetres, degrees, milliseconds, and microvolts\n# Encoding: UTF-8\n' ...
+ '# Notes: Each column is delimited by a tab. Each value within a column is delimited by a semicolon.\n' ...
+ '# Sample Name Index Loc. X Loc. Y Loc. Z Offset\n']);
+
+
+% Open output file
+fid = fopen(OutputFile, 'w');
+if (fid < 0)
+ error('Cannot open file');
+end
+
+fprintf(fid, '%s', header);
+% Write file: one line per location
+for i = 1:length(Label)
+ fprintf(fid,'%s\t%d\t%f\t%f\t%f\t0.0\n',Label{i}, i, Loc(1, i), Loc(2, i), Loc(3, i));
+end
+
+% Close file
+fclose(fid);
+
+end
+
+
+
+
diff --git a/toolbox/io/out_data_snirf.m b/toolbox/io/out_data_snirf.m
index f71531ad8..d3bfd8e1b 100644
--- a/toolbox/io/out_data_snirf.m
+++ b/toolbox/io/out_data_snirf.m
@@ -25,6 +25,14 @@ function out_data_snirf(ExportFile, DataMat, ChannelMatOut)
%
% Authors: Edouard Delaire, Francois Tadel, 2020
+% Install/load JSNIRF Toolbox (https://github.com/NeuroJSON/jsnirfy) as plugin
+if ~exist('jsnirfcreate', 'file')
+ [isInstalled, errMsg] = bst_plugin('Install', 'jsnirfy');
+ if ~isInstalled
+ error(errMsg);
+ end
+end
+
% Create an empty snirf data structure
snirfdata = jsnirfcreate();
@@ -47,20 +55,9 @@ function out_data_snirf(ExportFile, DataMat, ChannelMatOut)
snirfdata.SNIRFData.aux(i_aux).name=ChannelMatOut.Channel(aux_channel(i_aux)).Name;
snirfdata.SNIRFData.aux(i_aux).dataTimeSeries=DataMat.F(aux_channel(i_aux),:)';
snirfdata.SNIRFData.aux(i_aux).time=DataMat.Time';
+ snirfdata.SNIRFData.aux(i_aux).timeOffset = 0;
end
-% Set Probe; maybe can be simplified with the export of the measurment list
-[isrcs, idets, chan_measures, measure_type] = nst_unformat_channels({ChannelMatOut.Channel(nirs_channels).Name});
-
-src_pos= zeros(length(unique(isrcs)),3);
-det_pos= zeros(length(unique(idets)),3);
-
-
-% Todo : export detectorLabels and sourceLabels (string array)
-snirfdata.SNIRFData.probe.wavelengths=ChannelMatOut.Nirs.Wavelengths;
-snirfdata.SNIRFData.probe.sourcePos=src_pos;
-snirfdata.SNIRFData.probe.detectorPos=det_pos;
-
% Set landmark position (eg fiducials)
n_landmark=length(ChannelMatOut.HeadPoints.Label);
snirfdata.SNIRFData.probe.landmarkPos=zeros(n_landmark,3);
@@ -69,39 +66,72 @@ function out_data_snirf(ExportFile, DataMat, ChannelMatOut)
snirfdata.SNIRFData.probe.landmarkLabels(i_landmark)=string(ChannelMatOut.HeadPoints.Label{i_landmark});
end
+% Set Probe; maybe can be simplified with the export of the measurment list
+[isrcs, idets, chan_measures, measure_type] = nst_unformat_channels({ChannelMatOut.Channel(nirs_channels).Name});
+
+src_pos = zeros(length(unique(isrcs)),3);
+src_label = repmat( "", 1,length(unique(isrcs)));
+src_Index = zeros(length(unique(isrcs)),1);
+det_pos = zeros(length(unique(idets)),3);
+det_label = repmat( "", 1,length(unique(idets)));
+det_Index = zeros(length(unique(isrcs)),1);
+
+% Set Measurment list
+nSrc = 1;
+nDet = 1;
+for ichan=1:n_channel
+ [isrc, idet, chan_measures, measure_type] = nst_unformat_channels({ChannelMatOut.Channel(ichan).Name});
+
+ if ~any(cellfun(@(x)strcmp(x, sprintf('S%d',isrc )), src_label))
+ src_label(nSrc) = sprintf("S%d",isrc );
+ src_Index(nSrc) = isrc;
+ src_pos(nSrc,:)=ChannelMatOut.Channel(ichan).Loc(:,1)';
+
+ nSrc = nSrc + 1;
+ end
+
+ if ~any(cellfun(@(x)strcmp(x, sprintf('D%d',idet )), det_label))
+ det_label(nDet) = sprintf("D%d",idet );
+ det_Index(nDet) = idet;
+ det_pos(nDet,:)=ChannelMatOut.Channel(ichan).Loc(:,2)';
+
+ nDet = nDet + 1;
+ end
+
+end
+
% Set Measurment list
for ichan=1:n_channel
measurement=struct('sourceIndex',[],'detectorIndex',[],...
'wavelengthIndex',[],'dataType',1,'dataTypeIndex',1);
- [isrcs, idets, chan_measures, measure_type] = nst_unformat_channels({ChannelMatOut.Channel(ichan).Name});
+ [isrc, idet, chan_measures, measure_type] = nst_unformat_channels({ChannelMatOut.Channel(ichan).Name});
- src_pos(isrcs,:)=ChannelMatOut.Channel(ichan).Loc(:,1)';
- det_pos(idets,:)=ChannelMatOut.Channel(ichan).Loc(:,2)';
-
- measurement.sourceIndex=isrcs;
- measurement.detectorIndex=idets;
- measurement.wavelengthIndex=find(ChannelMatOut.Nirs.Wavelengths==chan_measures);
+ measurement.sourceIndex = find(src_Index == isrc);
+ measurement.detectorIndex = find(det_Index == idet);
+ measurement.wavelengthIndex = find(ChannelMatOut.Nirs.Wavelengths==chan_measures);
snirfdata.SNIRFData.data.measurementList(ichan)=measurement;
end
-% Todo : export detectorLabels and sourceLabels (string array)
snirfdata.SNIRFData.probe.wavelengths=ChannelMatOut.Nirs.Wavelengths;
-snirfdata.SNIRFData.probe.sourcePos=src_pos;
-snirfdata.SNIRFData.probe.sourcePos(:,3)=0; % set z to 0
+snirfdata.SNIRFData.probe.sourcePos2D=src_pos(:,[1,2]);
snirfdata.SNIRFData.probe.sourcePos3D=src_pos;
+snirfdata.SNIRFData.probe.sourceLabels = src_label;
-snirfdata.SNIRFData.probe.detectorPos=det_pos;
-snirfdata.SNIRFData.probe.detectorPos(:,3)=0; % set z to 0
+snirfdata.SNIRFData.probe.detectorPos2D=det_pos(:,[1,2]);
snirfdata.SNIRFData.probe.detectorPos3D=det_pos;
+snirfdata.SNIRFData.probe.detectorLabels=det_label;
+
% Set Stim
nEvt = length(DataMat.Events);
+evt_include = true(1,length(DataMat.Events));
for iEvt = 1:nEvt
% Skip empty events
if isempty(DataMat.Events(iEvt).times)
+ evt_include(iEvt) = false;
continue;
end
% Event structure
@@ -124,7 +154,11 @@ function out_data_snirf(ExportFile, DataMat, ChannelMatOut)
stim.data = data;
snirfdata.SNIRFData.stim(iEvt) = stim;
end
-
+if any(evt_include)
+ snirfdata.SNIRFData.stim = snirfdata.SNIRFData.stim(evt_include);
+else
+ snirfdata.SNIRFData = rmfield(snirfdata.SNIRFData,'stim');
+end
% Save snirf file.
savesnirf(snirfdata, ExportFile);
diff --git a/toolbox/io/out_events_bids.m b/toolbox/io/out_events_bids.m
new file mode 100644
index 000000000..e87afc1e2
--- /dev/null
+++ b/toolbox/io/out_events_bids.m
@@ -0,0 +1,72 @@
+function out_events_bids( sFile, EventsFile )
+% OUT_EVENTS_BIDS: export a BIDS _events.tsv file (columns "onset", "duration", "trial_type").
+%
+% USAGE: out_events_bids( sFile, EventsFile )
+
+% @=============================================================================
+% 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
+
+% Concatenate all the events together
+allTime = zeros(2,0);
+allInd = [];
+for i = 1:length(sFile.events)
+ % Simple events
+ if (size(sFile.events(i).times, 1) == 1)
+ allTime = [allTime, [sFile.events(i).times; 0*sFile.events(i).times]];
+ % Extented events
+ elseif (size(sFile.events(i).times, 1) == 2)
+ allTime = [allTime, sFile.events(i).times];
+ end
+ allInd = [allInd, repmat(i, 1, size(sFile.events(i).times,2))];
+end
+% Sort based on time
+[tmp, iSort] = sort(allTime(1,:));
+% Apply sorting to both arrays
+allTime = allTime(:,iSort);
+allInd = allInd(iSort);
+
+% Save file (ascii)
+fout = fopen(EventsFile, 'w');
+if (fout < 0)
+ warning('Cannot open file.');
+ return
+end
+
+% Write header
+fprintf(fout,'%s\t%s\t%s\n', 'onset', 'duration', 'trial_type');
+% Write all the events, one by line
+for i = 1:length(allInd)
+ % Get event structure
+ sEvt = sFile.events(allInd(i));
+ % Simple events
+ if (size(sEvt.times, 1) == 1)
+ fprintf(fout, '%g\t%g\t%s\n', allTime(1,i), 0, sEvt.label);
+ % Extended events
+ elseif (size(sEvt.times, 1) == 2)
+ fprintf(fout, '%g\t%g\t%s\n', allTime(1,i), allTime(2,i) - allTime(1,i), sEvt.label);
+ end
+end
+% Close file
+fclose(fout);
+
+
+
+
+
diff --git a/toolbox/io/out_fopen_bst.m b/toolbox/io/out_fopen_bst.m
index 330661fde..71696e6cd 100644
--- a/toolbox/io/out_fopen_bst.m
+++ b/toolbox/io/out_fopen_bst.m
@@ -37,7 +37,8 @@
sFileOut.header.starttime = sFileOut.prop.times(1);
sFileOut.header.navg = sFileOut.prop.nAvg;
% sFileOut.header.version = 51; % April 2019
-sFileOut.header.version = 52; % March 2023
+% sFileOut.header.version = 52; % March 2023
+sFileOut.header.version = 53; % September 2024
sFileOut.header.nsamples = round((sFileOut.prop.times(2) - sFileOut.prop.times(1)) .* sFileOut.prop.sfreq) + 1;
sFileOut.header.epochsize = EpochSize;
sFileOut.header.nchannels = length(ChannelMat.Channel);
@@ -108,7 +109,8 @@
if ~isempty(ChannelMat.Projector(i).SingVal)
fwrite(fid, ChannelMat.Projector(i).SingVal, 'float32'); % FLOAT32(N) : SingVal matrix
end
- fwrite(fid, ChannelMat.Projector(i).Status, 'int8'); % INT8(1) : Status
+ fwrite(fid, ChannelMat.Projector(i).Status, 'int8'); % INT8(1) : Status
+ fwrite(fid, str_zeros(ChannelMat.Projector(i).Method, 20), 'char'); % CHAR(20) : Projector method
end
% ===== HEAD POINTS =====
diff --git a/toolbox/io/out_fopen_edf.m b/toolbox/io/out_fopen_edf.m
index 864e2762a..2d10a1cc9 100644
--- a/toolbox/io/out_fopen_edf.m
+++ b/toolbox/io/out_fopen_edf.m
@@ -83,6 +83,7 @@
sFileOut.format = 'EEG-EDF';
sFileOut.byteorder = 'l';
sFileOut.comment = fBase;
+sFileOut.encoding = 'UTF-8';
date = datetime;
% Create a new header structure
@@ -187,7 +188,7 @@
if i == header.annotchan
header.signal(i).label = 'EDF Annotations';
eventsPerRecord = ceil(numel(header.annotations) / header.nrec);
- header.signal(i).nsamples = eventsPerRecord * maxAnnotLength + 15; % For first annotation of each record
+ header.signal(i).nsamples = eventsPerRecord * maxAnnotLength + 18; % For first annotation of each record
% Convert chars (1-byte) to 2-byte integers, the size of a sample
header.signal(i).nsamples = int64((header.signal(i).nsamples + 1) / 2);
else
@@ -227,7 +228,7 @@
end
% Open file
-fid = fopen(OutputFile, 'w+', sFileOut.byteorder);
+fid = fopen(OutputFile, 'w+', sFileOut.byteorder, sFileOut.encoding);
if (fid == -1)
error('Could not open output file.');
end
diff --git a/toolbox/io/out_fwrite.m b/toolbox/io/out_fwrite.m
index 2172a9043..f77055f91 100644
--- a/toolbox/io/out_fwrite.m
+++ b/toolbox/io/out_fwrite.m
@@ -63,8 +63,12 @@
end
fclose(sfid);
end
- % Open file
- sfid = fopen(sFile.filename, 'r+', sFile.byteorder);
+ % Open file (using requested encoding)
+ if ~isfield(sFile, 'encoding') || isempty(sFile.encoding)
+ sfid = fopen(sFile.filename, 'r+', sFile.byteorder);
+ else
+ sfid = fopen(sFile.filename, 'r+', sFile.byteorder, sFile.encoding);
+ end
if (sfid == -1)
error(['Could not open output file: "' sFile.filename '".']);
end
diff --git a/toolbox/io/out_matrix_ascii.m b/toolbox/io/out_matrix_ascii.m
index c12b0e11c..980a8eb24 100644
--- a/toolbox/io/out_matrix_ascii.m
+++ b/toolbox/io/out_matrix_ascii.m
@@ -182,27 +182,48 @@ function out_matrix_ascii( OutputFile, Data, FileFormat, Label1, Label2, Label3,
end
% Save headers
if ~isempty(Label1) && ~isempty(Label2)
- xlswrite(OutputFile, {Title2}, SheetName, 'A1');
- xlswrite(OutputFile, Label1(:), SheetName, 'A2');
- xlswrite(OutputFile, Label2(:)', SheetName, 'B1');
+ xls_write(OutputFile, {Title2}, SheetName, 'A1');
+ xls_write(OutputFile, Label1(:), SheetName, 'A2');
+ xls_write(OutputFile, Label2(:)', SheetName, 'B1');
StartCell = 'B2';
elseif ~isempty(Label1)
- xlswrite(OutputFile, Label1(:), SheetName, 'A1');
+ xls_write(OutputFile, Label1(:), SheetName, 'A1');
StartCell = 'B1';
elseif ~isempty(Label2)
- xlswrite(OutputFile, Label2(:)', SheetName, 'A1');
+ xls_write(OutputFile, Label2(:)', SheetName, 'A1');
StartCell = 'A2';
else
StartCell = 'A1';
end
% Save data as a new sheet
- [res,errMsg] = xlswrite(OutputFile, Data(:,:,i3), SheetName, StartCell);
+ [res,errMsg] = xls_write(OutputFile, Data(:,:,i3), SheetName, StartCell);
if ~res
error(['Could not export file to Excel: ' 10 errMsg]);
end
end
end
+function [res,errMsg] = xls_write(varargin)
+ % Wrapper to write a EXCEL .xls file for different Matlab versions)
+ if bst_get('MatlabVersion') < 906 % R2019a
+ [res,errMsg] = xlswrite(varargin{:});
+ else
+ res = 1; % Success
+ errMsg = '';
+ vars = varargin;
+ try
+ if iscell(vars{2})
+ writecell(vars{2}, vars{1}, 'Sheet', vars{3}, 'Range', vars{4});
+ elseif isnumeric(vars{2})
+ writematrix(vars{2}, vars{1}, 'Sheet', vars{3}, 'Range', vars{4});
+ end
+ catch me
+ res = 0; % Fail
+ errMsg = me.message;
+ end
+ end
+end
+end
\ No newline at end of file
diff --git a/toolbox/io/out_mri_bst.m b/toolbox/io/out_mri_bst.m
index ca0bc1270..c8f65033a 100644
--- a/toolbox/io/out_mri_bst.m
+++ b/toolbox/io/out_mri_bst.m
@@ -1,4 +1,4 @@
-function MRI = out_mri_bst( MRI, MriFile )
+function MRI = out_mri_bst( MRI, MriFile, Version)
% OUT_MRI_BST: Save a Brainstorm MRI structure.
%
% USAGE: MRI = out_mri_bst( MRI, MriFile )
@@ -6,8 +6,11 @@
% INPUT:
% - MRI : Brainstorm MRI structure
% - MriFile : full path to file where to save the MRI in brainstorm format
+% - Version : 'v6', fastest option, bigger files, no files >2Gb
+% 'v7', slower option, compressed, no files >2Gb (default)
+% 'v7.3', much slower, compressed, allows files >2Gb
% OUTPUT:
-% - MRI : Modificed MRI structure
+% - MRI : Modified MRI structure
%
% NOTES:
% - MRI structure:
@@ -46,6 +49,10 @@
%
% Authors: Francois Tadel, 2008-2012
+if nargin < 3
+ Version = 'v7';
+end
+
% ===== Clean-up MRI structure =====
% Remove (useless or old fieldnames)
Fields2BDeleted = {'Origin','sag','ax','cor','hFiducials','header','filename'};
@@ -98,10 +105,10 @@
% SAVE .mat file
try
- bst_save(MriFile, MRI, 'v7');
+ bst_save(MriFile, MRI, Version);
catch
end
-
+end
diff --git a/toolbox/io/out_mri_nii.m b/toolbox/io/out_mri_nii.m
index f01aec313..c4172dd3f 100644
--- a/toolbox/io/out_mri_nii.m
+++ b/toolbox/io/out_mri_nii.m
@@ -69,9 +69,9 @@
typeMatlab = 'float32';
elseif (MaxVal <= 255)
typeMatlab = 'uint8';
- elseif all(MaxVal < 32767)
+ elseif all(MaxVal <= 32767)
typeMatlab = 'int16';
- elseif all(MaxVal < 2147483647)
+ elseif all(MaxVal <= 2147483647)
typeMatlab = 'int32';
else
typeMatlab = 'float32';
diff --git a/toolbox/io/out_tess.m b/toolbox/io/out_tess.m
index 2d44969eb..c2034495c 100644
--- a/toolbox/io/out_tess.m
+++ b/toolbox/io/out_tess.m
@@ -86,7 +86,15 @@ function out_tess(BstFile, OutputFile, FileFormat, sMri)
end
% Export file
out_tess_fs(TessMat, OutputFile);
+ case 'OBJ'
+ % Swap faces
+ TessMat.Faces = TessMat.Faces(:,[2 1 3]);
+ out_tess_obj(TessMat, OutputFile);
case 'OFF'
+ % Vertices: convert to millimeters
+ TessMat.Vertices = TessMat.Vertices .* 1000;
+ % Swap faces
+ TessMat.Faces = TessMat.Faces(:,[2 1 3]);
out_tess_off(TessMat, OutputFile);
case 'TRI'
out_tess_tri(TessMat, OutputFile);
diff --git a/toolbox/io/out_tess_obj.m b/toolbox/io/out_tess_obj.m
new file mode 100644
index 000000000..dbebfb212
--- /dev/null
+++ b/toolbox/io/out_tess_obj.m
@@ -0,0 +1,48 @@
+function out_tess_obj( TessMat, OutputFile )
+% OUT_TESS_OBJ: Exports a surface to a .OBJ file.
+%
+% USAGE: out_tess_obj( TessMat, OutputFile )
+%
+% INPUT:
+% - TessMat : surface structure
+% - OutputFile : full path to output file (with '.obj' extension)
+
+% @=============================================================================
+% 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: Étienne Léger, 2023
+
+% ===== PREPARE VALUES ======
+Faces = TessMat.Faces;
+% Vertices: convert to millimeters
+Vertices = TessMat.Vertices .* 1000;
+
+% ===== SAVE FILE =====
+% Open file
+[fid, message] = fopen(OutputFile, 'wt');
+if (fid < 0)
+ error(['Could not create file : ' message]);
+end
+% Write vertices
+fprintf(fid, 'v %f\t%f\t%f\n', Vertices');
+% Write faces
+fprintf(fid, 'f %d\t%d\t%d\n', Faces');
+% Close file
+fclose(fid);
+
+
diff --git a/toolbox/io/out_tess_off.m b/toolbox/io/out_tess_off.m
index 6dfbb92e7..933bfb3f9 100644
--- a/toolbox/io/out_tess_off.m
+++ b/toolbox/io/out_tess_off.m
@@ -30,7 +30,6 @@ function out_tess_off( TessMat, OutputFile )
% ===== PREPARE VALUES ======
% Faces : remove 1 (convert to 0-based indices)
Faces = TessMat.Faces - 1;
-% Vertices: convert to millimeters
Vertices = TessMat.Vertices;
% ===== SAVE FILE =====
diff --git a/toolbox/io/private/fif_setup_raw.m b/toolbox/io/private/fif_setup_raw.m
index ff8c26df3..907aa6d90 100644
--- a/toolbox/io/private/fif_setup_raw.m
+++ b/toolbox/io/private/fif_setup_raw.m
@@ -51,7 +51,10 @@
FIFFT_SHORT=2;
FIFFT_INT=3;
FIFFT_FLOAT=4;
+FIFFT_DOUBLE=5;
FIFFT_DAU_PACK16=16;
+FIFFT_COMPLEX_FLOAT=20;
+FIFFT_COMPLEX_DOUBLE=21;
me = 'MNE-BST:fif_setup_raw';
% Arguments
if (nargin < 3)
@@ -137,6 +140,12 @@
nsamp = ent.size/(4*info.nchan);
case FIFFT_INT
nsamp = ent.size/(4*info.nchan);
+ case FIFFT_DOUBLE
+ nsamp = ent.size/(8*info.nchan);
+ case FIFFT_COMPLEX_FLOAT
+ nsamp = ent.size/(8*info.nchan);
+ case FIFFT_COMPLEX_DOUBLE
+ nsamp = ent.size/(16*info.nchan);
otherwise
fclose(fid);
error(me,'Cannot handle data buffers of type %d',ent.type);
diff --git a/toolbox/io/private/neuralynx_getheader.m b/toolbox/io/private/neuralynx_getheader.m
index d6055bc48..430169114 100644
--- a/toolbox/io/private/neuralynx_getheader.m
+++ b/toolbox/io/private/neuralynx_getheader.m
@@ -21,6 +21,7 @@
%
% Authors: Robert Oostenveld, 2007, as part of the FieldTrip toolbox
% Francois Tadel, 2015, for the Brainstorm integration
+% Raymundo Cassani, 2024
% ===== CONSTANTS =====
% Get file extension
@@ -55,34 +56,41 @@
% Get file size
fseek(fid, 0, 'eof');
hdr.FileSize = ftell(fid);
-% NCS: Read first and last timestamps
-if strcmpi(fExt, '.ncs')
- % Read first time stamp
- fseek(fid, hdr.HeaderSize, 'bof');
- hdr.FirstTimeStamp = fread(fid, 1, '*uint64');
- % Read last time stamp
- fseek(fid, -hdr.RecordSize, 'eof');
- hdr.LastTimeStamp = fread(fid, 1, '*uint64');
-% NSE: Read all the time samples
-elseif strcmpi(fExt, '.nse')
- % Compute number of records
- hdr.NumSamples = (hdr.RecordSize - 48) / 2;
- hdr.NumRecords = floor((hdr.FileSize - hdr.HeaderSize) / hdr.RecordSize);
- % Initialize the variables to read from the header
- hdr.SpikeTimeStamps = zeros(1, hdr.NumRecords, 'uint64');
- SpikeScNumber = zeros(1, hdr.NumRecords, 'int32');
- SpikeCellNumber = zeros(1, hdr.NumRecords, 'int32');
- hdr.SpikeParam = zeros(8, hdr.NumRecords, 'int32');
- % Loop on the records to get all the timestamps
- for iRec = 1:hdr.NumRecords
- % Seek at the beginning of the record
- fseek(fid, hdr.HeaderSize + (iRec-1) * hdr.RecordSize, 'bof');
- % Read header of the record
- hdr.SpikeTimeStamps(iRec) = fread(fid, 1, '*uint64');
- SpikeScNumber(iRec) = fread(fid, 1, 'int32'); % Do not save in the header
- SpikeCellNumber(iRec) = fread(fid, 1, 'int32'); % Do not save in the header
- hdr.SpikeParam(:,iRec) = fread(fid, 8, 'int32');
- end
+switch hdr.FileExtension
+ % NCS: Read first and last timestamps
+ case 'NCS'
+ % Read first time stamp
+ fseek(fid, hdr.HeaderSize, 'bof');
+ hdr.FirstTimeStamp = fread(fid, 1, '*uint64');
+ % Read last time stamp
+ fseek(fid, -hdr.RecordSize, 'eof');
+ hdr.LastTimeStamp = fread(fid, 1, '*uint64');
+
+ % NSE and NTT: Read all the time samples
+ case {'NSE', 'NTT'}
+ % Samples are 2 bytes
+ hdr.NumSamples = (hdr.RecordSize - 48) / 2;
+ % At each time sample there are four samples (one for each channel) in the NTT file
+ if strcmpi(hdr.FileExtension, 'NTT')
+ hdr.NumSamples = hdr.NumSamples / 4;
+ end
+ % Compute number of records
+ hdr.NumRecords = floor((hdr.FileSize - hdr.HeaderSize) / hdr.RecordSize);
+ % Initialize the variables to read from the header
+ hdr.SpikeTimeStamps = zeros(1, hdr.NumRecords, 'uint64');
+ SpikeScNumber = zeros(1, hdr.NumRecords, 'int32');
+ SpikeCellNumber = zeros(1, hdr.NumRecords, 'int32');
+ hdr.SpikeParam = zeros(8, hdr.NumRecords, 'int32');
+ % Loop on the records to get all the timestamps
+ for iRec = 1:hdr.NumRecords
+ % Seek at the beginning of the record
+ fseek(fid, hdr.HeaderSize + (iRec-1) * hdr.RecordSize, 'bof');
+ % Read header of the record
+ hdr.SpikeTimeStamps(iRec) = fread(fid, 1, '*uint64');
+ SpikeScNumber(iRec) = fread(fid, 1, 'int32'); % Do not save in the header
+ SpikeCellNumber(iRec) = fread(fid, 1, 'int32'); % Do not save in the header
+ hdr.SpikeParam(:,iRec) = fread(fid, 8, 'int32');
+ end
end
% Close file
fclose(fid);
@@ -105,12 +113,8 @@
elseif (hdrlines{i}(1) == '#')
continue;
end
- % Strip the '-' sign
- while (hdrlines{i}(1) == '-')
- hdrlines{i} = hdrlines{i}(2:end);
- end
- % Cut into pieces
- item = textscan(hdrlines{i}, '%s');
+ % Get item ('-Key Value')
+ [~, item] = regexp(hdrlines{i}, '-(\w+) (.*$)', 'match', 'tokens');
% Ignore line if there are less or more than two items
if (length(item) ~= 1) || (length(item{1}) ~= 2)
continue;
@@ -118,13 +122,11 @@
% Item1=key, Item2=value
key = item{1}{1};
val = item{1}{2};
- if any(val(1) == '-01234567989')
- % Try to convert to number
- val = str2num(val);
- % Revert to the original text
- if isempty(val)
- val = item{1}{2};
- end
+ % Try to convert to number
+ val = str2num(val);
+ % Revert to the original text
+ if isempty(val)
+ val = item{1}{2};
end
% Remove unuseable characters from the variable name (key)
key = key(key ~= ':');
diff --git a/toolbox/io/private/read_fieldtrip_chaninfo.m b/toolbox/io/private/read_fieldtrip_chaninfo.m
index 2cbf5f0a7..c07c6bdf4 100644
--- a/toolbox/io/private/read_fieldtrip_chaninfo.m
+++ b/toolbox/io/private/read_fieldtrip_chaninfo.m
@@ -190,7 +190,7 @@
end
% Apply units
if isfield(grad, 'unit') && ~isempty(grad.unit)
- ChannelMat.Channel(i).Loc(:,1) = bst_units_ui(grad.unit, ChannelMat.Channel(i).Loc(:,1));
+ ChannelMat.Channel(i).Loc = bst_units_ui(grad.unit, ChannelMat.Channel(i).Loc);
end
% Get type
if isempty(ChannelMat.Channel(i).Type)
diff --git a/toolbox/math/bst_epoching.m b/toolbox/math/bst_epoching.m
index 0a002245b..c74159ee9 100644
--- a/toolbox/math/bst_epoching.m
+++ b/toolbox/math/bst_epoching.m
@@ -38,7 +38,7 @@
% This function is part of the Brainstorm software:
% https://neuroimage.usc.edu/brainstorm
%
-% Copyright (c)2000-2020 University of Southern California & McGill University
+% 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.
diff --git a/toolbox/math/bst_meanvar.mexmaca64 b/toolbox/math/bst_meanvar.mexmaca64
new file mode 100755
index 000000000..2d66bdec4
Binary files /dev/null and b/toolbox/math/bst_meanvar.mexmaca64 differ
diff --git a/toolbox/math/bst_meshfit.m b/toolbox/math/bst_meshfit.m
index 5ad40a256..5a1eaf364 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,64 @@
%
% Authors: Qianqian Fang, 2008
% Francois Tadel, 2013-2021
+% Marc Lalancette, 2022
+
+% Coordinates are in m.
+PenalizeInside = true;
+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 = cross(EdgesV(:,:,1), EdgesV(:,:,2));
+FaceNormals = bsxfun(@rdivide, FaceNormals, sqrt(sum(FaceNormals.^2, 2))); % normr
+%FaceArea = sqrt(sum(FaceNormals.^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.02 mm displacement.
+OptimOptions = optimoptions(@fminunc, 'MaxFunctionEvaluations', 1000, 'MaxIterations', 200, ...
+ 'FiniteDifferenceStepSize', 1e-3, ...
+ 'FunctionTolerance', 1e-4, 'StepTolerance', 2e-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 +106,103 @@
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);
+ 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
+ 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
+%% 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
- 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 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
- % 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);
+ % 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
-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/math/bst_pca.m b/toolbox/math/bst_pca.m
index ed230cd9b..c8abfc419 100644
--- a/toolbox/math/bst_pca.m
+++ b/toolbox/math/bst_pca.m
@@ -54,12 +54,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
diff --git a/toolbox/math/bst_permtest.m b/toolbox/math/bst_permtest.m
index cd3224113..147ab804f 100644
--- a/toolbox/math/bst_permtest.m
+++ b/toolbox/math/bst_permtest.m
@@ -215,6 +215,9 @@
S = zeros(sizeData);
% Save statistics for all the permutations
if (nargout >= 5)
+ % Args to index PS
+ ixP = cell(size(sizeData));
+ ixP(:) = {':'};
PS = zeros([nPerm, sizeData, 1],'single');
end
% Count all good and bad channels for each set
@@ -236,7 +239,7 @@
end
% Save statistics for all the permutations
if (nargout >= 5)
- PS(i,:) = Z;
+ PS(i,ixP{:}) = Z;
end
end
end
diff --git a/toolbox/math/bst_project_channel.m b/toolbox/math/bst_project_channel.m
index 138b6e39e..fe373e3aa 100644
--- a/toolbox/math/bst_project_channel.m
+++ b/toolbox/math/bst_project_channel.m
@@ -152,6 +152,13 @@
end
% Copy clusters
ChannelMatDest.Clusters = ChannelMatSrc.Clusters;
+% Copy NIRS information
+if isfield(ChannelMatSrc,'Nirs')
+ ChannelMatDest.Nirs = ChannelMatSrc.Nirs;
+end
+% Copy history
+ChannelMatDest.History = ChannelMatSrc.History;
+ChannelMatDest = bst_history('add', ChannelMatDest, 'project', ['Project channel file: ' sSubjectSrc.Name ' => ' sSubjectDest.Name]);
% ===== SAVE NEW FILE =====
bst_progress('text', 'Saving results...');
diff --git a/toolbox/math/bst_project_sources.m b/toolbox/math/bst_project_sources.m
index 43a076ee1..8f7584a06 100644
--- a/toolbox/math/bst_project_sources.m
+++ b/toolbox/math/bst_project_sources.m
@@ -189,8 +189,8 @@
ResultsFile = ResultsGroups{iGroup}{iFile};
if isInteractive
bst_progress('inc', 1);
+ bst_progress('text', sprintf('Processing file #%d/%d: %s', iFile, nFile, ResultsFile));
end
- bst_progress('text', sprintf('Processing file #%d/%d: %s', iFile, nFile, ResultsFile));
% ===== OUTPUT STUDY =====
% Get source study
@@ -254,8 +254,8 @@
end
% Remove link with original file
ResultsMat.DataFile = [];
- % Check if the file was reprojected on an atlas
- if isfield(ResultsMat, 'Atlas') && ~isempty(ResultsMat.Atlas)
+ % Check if the file was reprojected on an atlas (only for results files)
+ if isfield(ResultsMat, 'Atlas') && ~isempty(ResultsMat.Atlas) && ~isTimefreq
wrnMsg = ['Cannot process atlas-based source files: Skipping file "' ResultsFile '"...'];
if isInteractive
disp(wrnMsg);
diff --git a/toolbox/math/bst_scout_channels.m b/toolbox/math/bst_scout_channels.m
new file mode 100644
index 000000000..91eb214d6
--- /dev/null
+++ b/toolbox/math/bst_scout_channels.m
@@ -0,0 +1,225 @@
+function OutputFile = bst_scout_channels(ChannelFile, SurfaceFile, Modality, Radius)
+% BST_SCOUT_CHANNELS: Create a scout with vertices within a radius around sensors on a surface.
+%
+% USAGE: OutputFile = bst_scout_channels(ChannelFile, SurfaceFile, Modality, Radius)
+% OutputFile = bst_scout_channels(ChannelFiles, ...)
+%
+% INPUT:
+% - ChannelFile : Path to channel file with sensors
+% - SurfaceFile : Path to surface file to create the scouts, or
+% Type of surface, it will use the default surface for tha type
+% - Modality : Modality to indicate the sensors to be used in scout creation
+% - Radius : Radius around sensor to create scout on surface (in mm)
+% If Radius == 0, the closest vertex is selected
+
+% @=============================================================================
+% 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
+
+% ===== PARSE INPUTS ======
+if (nargin < 4)
+ Radius = [];
+ if (nargin < 3)
+ Modality = [];
+ if (nargin < 2)
+ SurfaceFile = [];
+ end
+ end
+end
+errMsg = [];
+OutputFile = SurfaceFile;
+
+% Get Subject info
+sStudy = bst_get('ChannelFile', file_short(ChannelFile));
+[sSubject, iSubject] = bst_get('Subject', sStudy.BrainStormSubject);
+% Subjects must be either the default anatomy, or must have individual anatomy
+if (sSubject.UseDefaultChannel && (iSubject ~= 0))
+ errMsg = 'Subject is using the default anatomy.';
+end
+
+% Modality options (options with Location)
+[~, modalityOptions] = bst_get('ChannelModalities', ChannelFile);
+% Surface options
+surfaceOptions = {};
+if ~isempty(sSubject.iScalp)
+ surfaceOptions{end+1} = 'Scalp';
+end
+if ~isempty(sSubject.iOuterSkull)
+ surfaceOptions{end+1} = 'OuterSkull';
+end
+if ~isempty(sSubject.iInnerSkull)
+ surfaceOptions{end+1} = 'InnerSkull';
+end
+if ~isempty(sSubject.iCortex)
+ surfaceOptions{end+1} = 'Cortex';
+end
+
+% Get and validate Modality, SurfaceFile and Radius
+surfaceTarget = [];
+modalityTarget = [];
+radiusTarget = [];
+
+% Modality
+if isempty(errMsg) && ~isempty(Modality)
+ if ~iscell(Modality)
+ Modality = {Modality};
+ end
+ if any(ismember(Modality, modalityOptions))
+ modalityTarget = Modality;
+ else
+ modalityMiss = setdiff(Modality, modalityOptions);
+ errMsg = ['No channel for modality: "', strjoin(modalityMiss, ', '), '" was found in Channel file.'];
+ end
+elseif isempty(errMsg) && isempty(Modality)
+ [modalityTarget, isCancel] = java_dialog('checkbox', 'Which sensor modality or modalities will be used to create surface scouts?', ...
+ 'Scouts from sensors', [], modalityOptions);
+ if isempty(modalityTarget) || isCancel
+ return
+ end
+end
+
+% Surface
+if isempty(errMsg) && ~isempty(SurfaceFile)
+ % Surface is a filename
+ if ~strcmpi(file_gettype(SurfaceFile), 'unknown') && file_exist(file_fullpath(SurfaceFile))
+ [~, iSubjectSurf] = bst_get('SurfaceFile', SurfaceFile);
+ if iSubject == iSubjectSurf
+ surfaceTarget = SurfaceFile;
+ surfaceType = file_gettype(SurfaceFile);
+ else
+ errMsg = 'Subjects for channel File and for Surface files are not the same.';
+ end
+ % Surface is Type
+ else
+ if ismember(SurfaceFile, surfaceOptions)
+ surfaceType = SurfaceFile;
+ surfaceTarget = sSubject.Surface(sSubject.(['i' surfaceType])).FileName;
+ else
+ errMsg = ['Subject does not have default surface of type ' SurfaceFile '.'];
+ end
+ end
+elseif isempty(errMsg) && isempty(SurfaceFile)
+ [surfaceType, isCancel] = java_dialog('question', sprintf('Surface to create scouts from [%s] sensors:', strjoin(modalityTarget, ', ')), ...
+ 'Scouts from sensors', [], surfaceOptions);
+ if isempty(surfaceType) || isCancel
+ return
+ end
+ surfaceTarget = sSubject.Surface(sSubject.(['i' surfaceType])).FileName;
+end
+
+% Radius
+if isempty(errMsg) && ~isempty(Radius)
+ radiusTarget = Radius;
+elseif isempty(errMsg) && isempty(Radius)
+ [res, isCancel] = java_dialog('input', sprintf('Radius (in mm) for scouts on [%s] surface for sensors [%s]:', ...
+ surfaceType, strjoin(modalityTarget, ', ')), ...
+ 'Scouts from sensors', [], '5');
+ if isempty(res) || isCancel
+ return
+ end
+ radiusTarget = str2double(res);
+end
+if isempty(errMsg) && (isnan(radiusTarget) || radiusTarget < 0)
+ errMsg = 'Radius must be a number larger than 0 mm.';
+end
+
+% Error handling
+if ~isempty(errMsg)
+ bst_error(errMsg);
+ return;
+end
+
+% ===== GET INPUT DATA =====
+% Progress bar
+isProgress = bst_progress('isVisible');
+bst_progress('start', 'Scouts from sensors', 'Loading surface file...');
+
+% Load surface
+sSurf = in_tess_bst(surfaceTarget);
+surfVertices = sSurf.Vertices;
+
+% ===== SCOUTS FROM CHANNEL FILE =====
+bst_progress('start', 'Scouts from sensors', 'Loading surface file...');
+ChannelMat = in_bst_channel(ChannelFile);
+iChannels = channel_find(ChannelMat.Channel, modalityTarget);
+
+ChanLocOrg = [ChannelMat.Channel(iChannels).Loc]';
+ChanLocPrj = channel_project_scalp(surfVertices, ChanLocOrg);
+if any(sqrt(sum( (ChanLocPrj - ChanLocOrg).^2, 2)) > 0.001)
+ bst_error(['One or more sensors are not placed on the surface.' 10 ...
+ 'Project the sensors to the target surface before generating the scouts.']);
+ return
+end
+
+% Project sensors
+scoutVertices = [];
+for ix = 1 : length(iChannels)
+ if isempty(ChannelMat.Channel(iChannels(ix)).Loc)
+ continue
+ end
+ for iLoc = 1 : size(ChannelMat.Channel(iChannels(ix)).Loc, 2)
+ distances = sqrt(sum((surfVertices - ChannelMat.Channel(iChannels(ix)).Loc(:, iLoc)').^2, 2));
+ if radiusTarget == 0
+ [~, iMinDist] = min(distances);
+ scoutVertices = [scoutVertices, iMinDist];
+ else
+ scoutVertices = [scoutVertices, find(distances < radiusTarget./1000)'];
+ end
+ end
+end
+
+if isempty(scoutVertices)
+ bst_error(['No vertex found on the surface. '...
+ 'Check that the sensors are projected on the target surface']);
+ return;
+end
+% Create scout
+scout_channel = db_template('Scout');
+scout_channel.Label = sprintf('%s | %s (%d mm)', sStudy.Condition{1}, strjoin(modalityTarget, ' '), radiusTarget);
+scout_channel.Vertices = unique(scoutVertices);
+scout_channel.Seed = scoutVertices(1);
+scout_channel.Handles = [];
+scout_channel.Color = [1 0 0];
+
+% ===== SAVE SCOUT =====
+bst_progress('text', 'Saving scouts...');
+atlasName = 'Scout from sensors';
+s.Atlas = sSurf.Atlas;
+if ~isempty(s.Atlas) && ismember(atlasName, {s.Atlas.Name})
+ [~, iAtlas] = ismember(atlasName, {s.Atlas.Name});
+else
+ s.Atlas(end+1).Name = 'Scout from sensors';
+ iAtlas = length(s.Atlas);
+end
+if ~isempty(s.Atlas(iAtlas).Scouts) && ismember(scout_channel.Label, {s.Atlas(iAtlas).Scouts.Label})
+ [~, iScout] = ismember(scout_channel.Label, {s.Atlas(iAtlas).Scouts.Label});
+else
+ s.Atlas(iAtlas).Scouts(end+1) = scout_channel;
+ iScout = length(s.Atlas(iAtlas).Scouts);
+end
+s.Atlas(iAtlas).Scouts(iScout) = scout_channel;
+bst_save(file_fullpath(surfaceTarget), s, [], 1);
+OutputFile = surfaceTarget;
+% Close progress bar
+if ~isProgress
+ bst_progress('stop');
+end
+
+end
diff --git a/toolbox/math/bst_scout_value.m b/toolbox/math/bst_scout_value.m
index 522eea5f8..4cf28604d 100644
--- a/toolbox/math/bst_scout_value.m
+++ b/toolbox/math/bst_scout_value.m
@@ -189,14 +189,21 @@
% STD : Standard deviation of the patch activity at each time instant
case 'std'
Fs = std(F,[],1);
+
% STDERR : Standard error
case 'stderr'
%% This formula was incorrect for standard error (fixed 2023-04). Is it used anywhere?
Fs = std(F,[],1) ./ sqrt(nRow);
- % RMS
+
+ % RMS : Root mean square, square root of the average of the square of the all the signals
case 'rms'
- Fs = sqrt(sum(F.^2,1));
-
+ if (nComponents == 1)
+ Fs = mean(F.^2, 1);
+ else
+ Fs = mean(sum(F.^2, 3), 1);
+ end
+ Fs = sqrt(Fs);
+
% MEAN_NORM : Average of the norms of all the vertices each time instant
% If only one components: computes mean(abs(F)) => Compatibility with older versions
case 'mean_norm'
diff --git a/toolbox/math/bst_shepards.m b/toolbox/math/bst_shepards.m
index 1c2d8a439..18fc1b9f7 100644
--- a/toolbox/math/bst_shepards.m
+++ b/toolbox/math/bst_shepards.m
@@ -1,7 +1,7 @@
-function Wmat = bst_shepards(destLoc, srcLoc, nbNeighbors, excludeParam, expDistance)
+function Wmat = bst_shepards(destLoc, srcLoc, nbNeighbors, excludeParam, expDistance, isInteractive)
% BST_SHEPARDS: 3D nearest-neighbor interpolation using Shepard's weighting.
%
-% USAGE: Wmat = bst_shepards(destLoc, srcLoc, nbNeighbors=8, excludeParam=0, expDistance=2)
+% USAGE: Wmat = bst_shepards(destLoc, srcLoc, nbNeighbors=8, excludeParam=0, expDistance=2, isInteractive=1)
%
% INPUT:
% - srcLoc : Nx3 array of original locations, or tesselation structure (Faces,Vertices,VertConn)
@@ -12,6 +12,7 @@
% where minDist represents the minimal distance between each source point and the destination surface
% If < 0, exclude the vertices that are further from the absolute distance excludeParam (in millimeters)
% - expDistance : Distance exponent (if higher, influence of a value decreases faster)
+% - isInteractive: If 1, show a progress bar
%
% OUTPUT:
% - Wmat : Interpolation matrix
@@ -57,6 +58,11 @@
if (nargin < 5) || isempty(expDistance)
expDistance = 2;
end
+% Argument: isInteractive
+if (nargin < 6) || isempty(isInteractive)
+ isInteractive = 1;
+end
+
%% ===== SHEPARDS INTERPOLATION =====
% Allocate interpolation matrix
@@ -68,7 +74,7 @@
end
% Find nearest neighbors
-[I,dist] = bst_nearest(srcLoc, destLoc, nbNeighbors, 1);
+[I,dist] = bst_nearest(srcLoc, destLoc, nbNeighbors, isInteractive);
% Square the distance matrix
dist = dist .^ 2;
% Eliminate zeros in distance matrix for stability
diff --git a/toolbox/math/bst_tess_distance.m b/toolbox/math/bst_tess_distance.m
new file mode 100644
index 000000000..0993496a5
--- /dev/null
+++ b/toolbox/math/bst_tess_distance.m
@@ -0,0 +1,60 @@
+function Dist = bst_tess_distance(SurfaceMat, VerticesA, VerticesB, metric)
+% bst_tess_distance: Distance computation between two set of vertices (A and B)
+%
+% USAGE: W = bst_tess_distance(SurfaceMat, VerticesA, VerticesB, metric)
+%
+% INPUT:
+% - SurfaceMat : Cortical surface matrix
+% - VerticesA : Vertices from region A
+% - VerticesB : Vertices from region B
+% - Method : Metric used to compute the distance {'euclidean', 'geodesic_edge', 'geodesic_dist'}
+% OUPUT:
+% - Dist: distance matrix. D(i,j) is the distance between vertex VerticesA(i), and
+% VerticesB(j).
+
+%
+% @=============================================================================
+% 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 2023
+
+ Vertices = SurfaceMat.Vertices;
+
+ if strcmp(metric,'euclidean')
+ Dist = zeros(length(VerticesA),length(VerticesB));
+ x = Vertices(VerticesA,:)';
+ for i = 1:length(VerticesB)
+ y = Vertices(VerticesB(i),:)';
+ Dist(:,i) = sum((x-y).^2).^0.5; % m
+ end
+
+ elseif ~isempty(strfind(metric,'geodesic'))
+ if strcmp(metric,'geodesic_dist')
+ [vi,vj] = find(SurfaceMat.VertConn);
+ nv = size(Vertices,1);
+ x = Vertices(vi,:)';
+ y = Vertices(vj,:)';
+ D = sparse(vi, vj, sum((x-y).^2).^0.5, nv, nv); % m
+ else
+ D = SurfaceMat.VertConn; % edges
+ end
+
+ G = graph(D);
+ Dist = distances(G, VerticesA, VerticesB);
+ end
+end
diff --git a/toolbox/misc/struct_copy_fields.m b/toolbox/misc/struct_copy_fields.m
index 6ae5a4cbc..96a70b6d5 100644
--- a/toolbox/misc/struct_copy_fields.m
+++ b/toolbox/misc/struct_copy_fields.m
@@ -1,6 +1,7 @@
-function sDest = struct_copy_fields(sDest, sSrc, override)
+function sDest = struct_copy_fields(sDest, sSrc, override, deepest)
% STRUCT_COPY_FIELDS: Copy the fields from sSrc structure to sDest structure
-
+% If override, override the field sDest by the field of sSrc
+% If deepest, try to apply the copy at the deepest level
% @=============================================================================
% This function is part of the Brainstorm software:
% https://neuroimage.usc.edu/brainstorm
@@ -21,10 +22,15 @@
%
% Authors: Sylvain Baillet, March 2002
+if (nargin < 4) || isempty(deepest)
+ deepest = 0;
+end
+
if (nargin < 3) || isempty(override)
override = 1;
end
+
% No fields to add
if isempty(sSrc)
return
@@ -35,6 +41,12 @@
else
namesSrc = fieldnames(sSrc);
for i = 1:length(namesSrc)
+ % if subfield are both structure, we do a "deep copy"
+ if deepest && isfield(sDest, namesSrc{i}) && isstruct(sDest.(namesSrc{i})) && isfield(sSrc, namesSrc{i}) && isstruct(sSrc.(namesSrc{i}))
+ sDest.(namesSrc{i}) = struct_copy_fields(sDest.(namesSrc{i}), sSrc.(namesSrc{i}), override);
+ continue;
+ end
+
if override || ~isfield(sDest, namesSrc{i})
sDest.(namesSrc{i}) = sSrc.(namesSrc{i});
end
diff --git a/toolbox/process/bst_process.m b/toolbox/process/bst_process.m
index 143324ec4..406f4d867 100644
--- a/toolbox/process/bst_process.m
+++ b/toolbox/process/bst_process.m
@@ -401,7 +401,7 @@
sInput.Measure = [];
end
% Do not allow Time Bands
- if isfield(sMat, 'TimeBands') && ~isempty(sMat.TimeBands) && ismember(func2str(sProcess.Function), {'process_average_time', 'process_baseline_norm', 'process_extract_time'})
+ if isfield(sMat, 'TimeBands') && ~isempty(sMat.TimeBands) && ~strcmpi(sMat.Method, 'mtmconvol') && ismember(func2str(sProcess.Function), {'process_average_time', 'process_baseline_norm', 'process_extract_time'})
bst_report('Error', sProcess, sInput, 'Cannot process values averaged by time bands.');
return;
end
@@ -608,14 +608,14 @@
else
[rawPathIn, rawBaseIn] = bst_fileparts(sFileIn.filename);
end
- % Make sure that there are not weird characters in the folder names
- rawBaseIn = file_standardize(rawBaseIn);
% New folder name
if isfield(sFileIn, 'condition') && ~isempty(sFileIn.condition)
newCondition = ['@raw', sFileIn.condition, fileTag];
else
newCondition = ['@raw', rawBaseIn, fileTag];
end
+ % Make sure that there are not weird characters in the folder names
+ newCondition = file_standardize(newCondition);
% Get new condition name
newStudyPath = file_unique(bst_fullfile(ProtocolInfo.STUDIES, sInput.SubjectName, newCondition));
% Output file name derives from the condition name
@@ -963,7 +963,17 @@
end
% Output time vector
if isfield(sMat, 'TimeBands') && ~isempty(sMat.TimeBands)
- % Time bands: Do not update time vector
+ if isTimeChange
+ % Find time bands related to new time vector (obtained from time bands)
+ timeVectorBands = mean(process_tf_bands('GetBounds', sMat.TimeBands), 2);
+ ixTimeBandKeep = (timeVectorBands >= OutTime(1)) & (timeVectorBands <= OutTime(end));
+ sMat.TimeBands = sMat.TimeBands(ixTimeBandKeep, :);
+ % Update original time vector to match new time vector range
+ ixTimeDel = (sMat.Time < OutTime(1)) | (sMat.Time > OutTime(end));
+ sMat.Time(ixTimeDel) = [];
+ else
+ % Time bands: Do not update time vector
+ end
else
sMat.Time = OutTime;
end
@@ -997,7 +1007,7 @@
if isfield(sProcess.options, 'Comment') && isfield(sProcess.options.Comment, 'Value') && ~isempty(sProcess.options.Comment.Value)
sMat.Comment = sProcess.options.Comment.Value;
% Modify comment based on modifications in function Run
- elseif ~isRaw && isfield(sInput, 'Comment') && ~isempty(sInput.Comment) && ~isequal(sMat.Comment, sInput.Comment)
+ elseif ~isRaw && isfield(sInput, 'Comment') && ~isempty(sInput.Comment) && ~isequal(sMat.Comment, sInput.Comment) && ~isAbsolute
sMat.Comment = sInput.Comment;
% Add file tag (defined in process Run function)
elseif isfield(sInput, 'CommentTag') && ~isempty(sInput.CommentTag)
@@ -1145,7 +1155,7 @@
sInputB.Measure = [];
end
% Do not allow TimeBands
- if ((isfield(sMatA, 'TimeBands') && ~isempty(sMatA.TimeBands)) || (isfield(sMatB, 'TimeBands') && ~isempty(sMatB.TimeBands))) ...
+ if ((isfield(sMatA, 'TimeBands') && ~isempty(sMatA.TimeBands)) && ~strcmpi(sMatA.Method, 'mtmconvol') || (isfield(sMatB, 'TimeBands') && ~isempty(sMatB.TimeBands))) && ~strcmpi(sMatB.Method, 'mtmconvol') ...
&& ismember(func2str(sProcess.Function), {'process_baseline_ab', 'process_zscore_ab', 'process_baseline_norm2'})
bst_report('Error', sProcess, [sInputA, sInputB], 'Cannot process values averaged by time bands.');
OutputFile = [];
@@ -1904,7 +1914,9 @@
isflip = ismember(sInput.DataType, {'link','results'}) && ...
isempty(strfind(FileName, '_norm')) && ...
isempty(strfind(FileName, 'NIRS')) && ...
- isempty(strfind(FileName, 'Summed_sensitivities'));
+ isempty(strfind(FileName, 'Summed_sensitivities')) && ...
+ isempty(strfind(FileName, 'bold'));
+
% Call process
sMat = CallProcess('process_extract_scout', FileName, [], ...
'timewindow', TimeWindow, ...
diff --git a/toolbox/process/bst_report.m b/toolbox/process/bst_report.m
index de2232cdb..82eb2cbf5 100644
--- a/toolbox/process/bst_report.m
+++ b/toolbox/process/bst_report.m
@@ -12,6 +12,7 @@
% bst_report('Save', sInputs, ReportFile=[])
% bst_report('Save', FileNames, ReportFile=[])
% bst_report('Open', ReportFile=[ask], isFullReport=1)
+% bst_report('Reset')
% bst_report('Export', ReportFile, HtmlFile=[ask])
% bst_report('Export', ReportFile, HtmlDir)
% bst_report('Email', ReportFile, username, to, subject, isFullReport=1)
@@ -66,13 +67,12 @@
%% ===== START =====
function Start(sInputs)
- global GlobalData;
% If there were no inputs
if (nargin < 1) || isempty(sInputs)
sInputs = [];
end
% Reset current report
- GlobalData.ProcessReports.Reports = {};
+ Reset();
% Get current protocol description
ProtocolInfo = bst_get('ProtocolInfo');
% Add start entry
@@ -89,7 +89,7 @@ function Add(strType, sProcess, sInputs, strMsg)
return;
end
if isempty(GlobalData.ProcessReports.Reports) || ~iscell(GlobalData.ProcessReports.Reports) || (size(GlobalData.ProcessReports.Reports,2) ~= 5)
- GlobalData.ProcessReports.Reports = {};
+ Reset();
end
% No input
if isempty(strType)
@@ -189,8 +189,6 @@ function Info(sProcess, sInputs, strMsg)
end
return;
end
- % Use short file name
- FileName = file_short(FileName);
% Get current window layout
curLayout = bst_get('Layout', 'WindowManager');
if ~isempty(curLayout)
@@ -226,6 +224,10 @@ function Info(sProcess, sInputs, strMsg)
if ~isempty(ScoutsOptions) && ~strcmpi(ScoutsOptions.showSelection, 'none')
panel_scout('SetScoutShowSelection', 'none');
end
+ % Use short file name
+ if ~isempty(FileName)
+ FileName = file_short(FileName);
+ end
% Show figures
try
@@ -680,7 +682,7 @@ function Info(sProcess, sInputs, strMsg)
strErr = bst_error();
disp(['BST_REPORT> ERROR:' 10 strErr]);
% Log error message
- Error('process_snapshot', FileName, strErr);
+ Error('process_snapshot', ['"' SnapType '" "' FileName '"'], strErr);
hFig = [];
end
% Output images
@@ -775,7 +777,7 @@ function Info(sProcess, sInputs, strMsg)
ReportMat.Reports = GlobalData.ProcessReports.Reports;
bst_save(ReportFile, ReportMat, 'v7');
% Reset
- GlobalData.ProcessReports.Reports = {};
+ Reset();
end
@@ -931,21 +933,10 @@ function Info(sProcess, sInputs, strMsg)
'Brainstorm process report' 10];
% Elapsed time
if ~isempty(iStart) && ~isempty(iStop)
- % Get time elapsed between start and stop
- eTime = datevec(datenum(Reports{iStop,5}) - datenum(Reports{iStart,5}));
- % Format elapsed time
- strElapsed = [];
- if (eTime(3) > 0)
- strElapsed = [strElapsed num2str(eTime(3)) 'd '];
- end
- if (eTime(4) > 0)
- strElapsed = [strElapsed num2str(eTime(4)) 'h '];
- end
- if (eTime(5) > 0)
- strElapsed = [strElapsed num2str(eTime(5)) 'm '];
- end
+ % Get elapsed time string 'Xd Xh Xm Xs'
+ strElapsed = GetElapsedStr(Reports, iStart, iStop);
% Time line
- strElapsed = [strElapsed num2str(eTime(6)) 's' 10];
+ strElapsed = [strElapsed '' 10];
html = [html 'Start: ' Reports{iStart,5} ' Elapsed: ' strElapsed];
end
@@ -1344,6 +1335,9 @@ function ClearHistory(isUserConfirm)
end
% Get all the available reports
ProtocolInfo = bst_get('ProtocolInfo');
+ if isempty(ProtocolInfo)
+ return
+ end
ProtocolName = file_standardize(ProtocolInfo.Comment);
reportsDir = bst_get('UserReportsDir');
% If directory exists
@@ -1631,9 +1625,21 @@ function Recall(target)
% Compact report: prepare and send
if ~isFullReport
html = '';
+ iStart = find(strcmpi(Reports(:,1), 'start'), 1);
+ iStop = find(strcmpi(Reports(:,1), 'stop'), 1);
+ iErrors = find(strcmpi(Reports(:,1), 'error'));
+ iWarnings = find(strcmpi(Reports(:,1), 'warning'));
+ % Elapsed time
+ if ~isempty(iStart) && ~isempty(iStop)
+ % Get elapsed time string 'Xd Xh Xm Xs'
+ strElapsed = GetElapsedStr(Reports, iStart, iStop);
+ html = [html 'Start: ' Reports{iStart,5} ' Elapsed: ' strElapsed 10 10];
+ end
+ % Errors and warnings
+ html = [html sprintf('%d errors and %d warnings', length(iErrors), length(iWarnings)) 10 10];
for iEntry = 1:size(Reports,1)
if ~isempty(Reports{iEntry,1}) && ~isempty(Reports{iEntry,5})
- html = [html, Reports{iEntry,5}, ' : ', Reports{iEntry,1}];
+ html = [html, Reports{iEntry,5}, ' : ', Reports{iEntry,1}, repmat(' ', 1, 7-length(Reports{iEntry,1}))];
if ~isempty(Reports{iEntry,2})
if isstruct(Reports{iEntry,2})
html = [html, 9, ' - ' func2str(Reports{iEntry,2}.Function)];
@@ -1655,4 +1661,33 @@ function Recall(target)
isOk = isequal(resp, 'ok');
end
+%% == GET ELAPSED TIME ===
+function strElapsed = GetElapsedStr(Reports, iStart, iStop)
+ strElapsed = '';
+ if nargin < 3 || isempty(Reports) || isempty(iStart) || isempty(iStop)
+ return
+ end
+ % Get time elapsed between start and stop
+ eTime = datevec(datenum(Reports{iStop,5}) - datenum(Reports{iStart,5}));
+ % Format elapsed time
+ strElapsed = [];
+ if (eTime(3) > 0)
+ strElapsed = [strElapsed num2str(eTime(3)) 'd '];
+ end
+ if (eTime(4) > 0)
+ strElapsed = [strElapsed num2str(eTime(4)) 'h '];
+ end
+ if (eTime(5) > 0)
+ strElapsed = [strElapsed num2str(eTime(5)) 'm '];
+ end
+ % Time line
+ strElapsed = [strElapsed num2str(eTime(6)) 's'];
+end
+
+%% ===== RESET CURRENT REPORT =====
+% USAGE: bst_report('Reset')
+function Reset()
+ global GlobalData;
+ GlobalData.ProcessReports.Reports = {};
+end
diff --git a/toolbox/process/functions/process_corr1n_time.m b/toolbox/process/deprecated/process_corr1n_time.m
similarity index 100%
rename from toolbox/process/functions/process_corr1n_time.m
rename to toolbox/process/deprecated/process_corr1n_time.m
diff --git a/toolbox/process/functions/process_add_tag.m b/toolbox/process/functions/process_add_tag.m
index 8cfaf5710..60ee92865 100644
--- a/toolbox/process/functions/process_add_tag.m
+++ b/toolbox/process/functions/process_add_tag.m
@@ -186,6 +186,8 @@
OldFileName = file_fullpath(sInputs(i).FileName);
[fPath, fBase, fExt] = bst_fileparts(OldFileName);
NewFileName = bst_fullfile(fPath, [fBase, fileTag, fExt]);
+ % Ensure uniqueness
+ NewFileName = file_unique(NewFileName);
OutputFiles{i} = file_short(NewFileName);
end
% Rename file
diff --git a/toolbox/process/functions/process_adjust_coordinates.m b/toolbox/process/functions/process_adjust_coordinates.m
index bf861a247..ec135d658 100644
--- a/toolbox/process/functions/process_adjust_coordinates.m
+++ b/toolbox/process/functions/process_adjust_coordinates.m
@@ -1,9 +1,10 @@
function varargout = process_adjust_coordinates(varargin)
-% PROCESS_ADJUST_COORDINATES: Adjust, recompute, or remove various coordinate transformations.
+% PROCESS_ADJUST_COORDINATES: Adjust, recompute, or remove various coordinate transformations, primarily for CTF MEG datasets.
%
-% 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. 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:
@@ -23,14 +24,13 @@
% 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';
@@ -42,7 +42,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,10 +60,18 @@
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;
+ 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 = 'Replace MRI nasion and ear points with digitized landmarks (cannot undo).';
+ sProcess.options.scs.Value = 0;
sProcess.options.remove.Type = 'checkbox';
sProcess.options.remove.Comment = 'Remove selected adjustments (if present) instead of adding them.';
sProcess.options.remove.Value = 0;
@@ -82,7 +90,7 @@
function OutputFiles = Run(sProcess, sInputs)
-
+ OutputFiles = {};
isDisplay = sProcess.options.display.Value;
nInFiles = length(sInputs);
@@ -93,25 +101,58 @@
bst_memory('UnloadAll', 'Forced'); % Close all the existing figures.
end
- [UniqueChan, iUniqFiles, iUniqInputs] = unique({sInputs.ChannelFile});
+ [~, iUniqFiles, iUniqInputs] = unique({sInputs.ChannelFile});
nFiles = numel(iUniqFiles);
- if ~sProcess.options.remove.Value && sProcess.options.head.Value && ...
- nFiles < nInFiles
+ % Special cases when replacing MRI fids: don't allow resetting or refining with head points,
+ % probably a user mistake. Still possible if done in two separate calls. Adjusting head position
+ % ignored with a warning only (would be lost anyway).
+ % Also only a single channel file per subject is allowed.
+ if ~sProcess.options.remove.Value && sProcess.options.scs.Value
+ % TODO: how to go back to process panel, like for missing TF options in some processes?
+ if sProcess.options.reset.Value
+ bst_report('Error', sProcess, sInputs, ...
+ ['Incompatible options: "Reset coordinates" would be applied first and remove coregistration adjustments.' 10, ...
+ '"Replace MRI landmarks" automatically resets all channel files for selected subject(s) after MRI update.']);
+ return;
+ end
+ if sProcess.options.points.Value
+ bst_report('Error', sProcess, sInputs, ...
+ ['Incompatible options: "Refine with head points" not currently allowed in same call as' 10, ...
+ '"Replace MRI landmarks" to avoid user error and possibly loosing manual coregistration adjustments.' 10, ...
+ 'If you really want to do this, run this process separately for each operation.']);
+ return;
+ end
+ if sProcess.options.head.Value
+ bst_report('Warning', sProcess, sInputs, ...
+ ['Incompatible options: "Adjust head position" ignored since it would be lost after MRI update.' 10, ...
+ '"Replace MRI landmarks" automatically resets all channel files for selected subject(s) after MRI update.']);
+ sProcess.options.head.Value = 0;
+ end
+
+ % Check for multiple channel files per subject.
+ UniqueSubs = unique({sInputs(iUniqFiles).SubjectFile});
+ if numel(UniqueSubs) < nFiles
+ bst_report('Error', sProcess, sInputs, ...
+ '"Replace MRI landmarks" can only be run with a single channel file per subject.');
+ return;
+ end
+ end
+
+ if ~sProcess.options.remove.Value && sProcess.options.head.Value && nFiles < nInFiles
bst_report('Info', sProcess, sInputs, ...
'Multiple inputs were found for a single channel file. They will be concatenated for adjusting the head position.');
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.
+
+ 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.
NewChannelFiles = cell(0, 2);
for iFile = iUniqFiles(:)' % no need to repeat on same channel file.
ChannelMat = in_bst_channel(sInputs(iFile).ChannelFile);
% Get the leading modality
- [tmp, DispMod] = channel_get_modalities(ChannelMat.Channel);
+ [~, DispMod] = channel_get_modalities(ChannelMat.Channel);
% 'MEG' is added when 'MEG GRAD' or 'MEG MAG'.
ModPriority = {'MEG', 'EEG', 'SEEG', 'ECOG', 'NIRS'};
iMod = find(ismember(ModPriority, DispMod), 1, 'first');
@@ -132,52 +173,69 @@
% ----------------------------------------------------------------
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 original goal of this option was to fix data affected by a previous bug while
+ % keeping as much pre-processing that was previously done. We re-import the channel
+ % file, and copy the projectors (and history) from the old one.
- [ChannelMat, NewChannelFiles, Failed] = ...
- ResetChannelFile(ChannelMat, NewChannelFiles, sInputs(iFile), sProcess);
- if Failed
+ [ChannelMat, NewChannelFiles, isError] = ResetChannelFile(ChannelMat, ...
+ NewChannelFiles, sInputs(iFile), sProcess);
+ if isError
continue;
end
% ----------------------------------------------------------------
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
+
+ % We cannot change back the MRI fiducials, but in order to be able to update it again
+ % 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;
+ 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'));
+ 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(MriFile), sMri, 'v7');
+ catch
+ bst_report('Error', sProcess, sInputs(iFile), ...
+ sprintf('Unable to save MRI file %s.', MriFile));
+ continue;
+ end
+ end
+ end
end % reset channel file or remove transformations
% ----------------------------------------------------------------
if ~sProcess.options.remove.Value && sProcess.options.head.Value
% Complex indexing to get all inputs for this same channel file.
- [ChannelMat, Failed] = AdjustHeadPosition(ChannelMat, ...
+ [ChannelMat, isError] = AdjustHeadPosition(ChannelMat, ...
sInputs(iUniqInputs == iUniqInputs(iFile)), sProcess);
- if Failed
+ if isError
continue;
end
end % adjust head position
@@ -187,23 +245,57 @@
% 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
- % 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 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, ~, ~, isSkip, isUserCancel, strReport] = channel_align_auto(sInputs(iFile).ChannelFile, ...
+ 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)
+ 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;
+ elseif isUserCancel
+ bst_report('Info', sProcess, sInputs(iFile), 'User cancelled registration with head points.');
+ continue
end
end % refine registration with head points
% ----------------------------------------------------------------
- % Save channel file.
+ % Save channel file.
+ % Before potiential MRI update since that function takes ChannelFile, not ChannelMat.
bst_save(file_fullpath(sInputs(iFile).ChannelFile), ChannelMat, 'v7');
- isFileOk(iFile) = true;
+ % ----------------------------------------------------------------
+ if ~sProcess.options.remove.Value && sProcess.options.scs.Value
+ % This is now a subfunction in this process
+ [~, isCancel, isError] = channel_align_scs(sInputs(iFile).ChannelFile, eye(4), ...
+ true, false, sInputs(iFile), sProcess); % interactive warnings but no confirmation
+ % Head surface was unloaded from memory, in case we want to display "after" figure.
+ % TODO Maybe modify like channel_align_auto to take in and return ChannelMat.
+ if isError
+ return;
+ elseif isCancel
+ continue;
+ end
+ % If something happened and channel files were not reset, there was a pop-up error and
+ % we'll just let the user deal with it for now (i.e. try again to reset the channel file).
+
+ % TODO Verify
+ end
+
+ % ----------------------------------------------------------------
+ isFileOk(iFile) = true;
+
if isDisplay && ~isempty(Modality)
% Display "after" results, besides the "before" figure.
hFigAfter = channel_align_manual(sInputs(iFile).ChannelFile, Modality, 0);
@@ -212,10 +304,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,21 +357,32 @@
% end
-function [ChannelMat, NewChannelFiles, Failed] = ...
- ResetChannelFile(ChannelMat, NewChannelFiles, sInput, sProcess)
- if nargin < 4
+function [ChannelMat, NewChannelFiles, isError] = ResetChannelFile(ChannelMat, NewChannelFiles, sInput, sProcess)
+ % Reload a channel file, but keep projectors and history. First look for original file from
+ % history, and if it's no longer there, user will be prompted. User selections are noted as
+ % pairs {old, new} in NewChannelFiles for potential reuse (e.g. same original data at multiple
+ % pre-processing steps).
+ % This function does not save the file, but only returns the updated structure.
+ if nargin < 4 || isempty(sProcess)
sProcess = [];
+ isReport = false;
+ else
+ isReport = true;
+ end
+ if nargin < 3
+ sInput = [];
end
- Failed = false;
+ if nargin < 2 || isempty(NewChannelFiles)
+ NewChannelFiles = cell(0,2);
+ end
+ isError = false;
bst_progress('text', 'Importing channel file...');
% Extract original data file from channel file history.
- if any(size(ChannelMat.History) < [1, 3]) || ...
- ~strcmp(ChannelMat.History{1, 2}, 'import')
+ if any(size(ChannelMat.History) < [1, 3]) || ~strcmp(ChannelMat.History{1, 2}, 'import')
NotFound = true;
ChannelFile = '';
else
- ChannelFile = regexp(ChannelMat.History{1, 3}, ...
- '(?<=: )(.*)(?= \()', 'match');
+ ChannelFile = regexp(ChannelMat.History{1, 3}, '(?<=: )(.*)(?= \()', 'match');
if isempty(ChannelFile)
NotFound = true;
else
@@ -298,19 +400,26 @@
if NewFound
ChannelFile = NewChannelFiles{iNew, 2};
NotFound = false;
- bst_report('Info', sProcess, sInput, ...
- sprintf('Using channel file in new location: %s.', ChannelFile));
+ if isReport
+ bst_report('Info', sProcess, sInput, ...
+ sprintf('Using channel file in new location: %s.', ChannelFile));
+ end
end
end
- FileFormatsChan = bst_get('FileFilters', 'channel');
- FileFormat = FileFormatsChan{sProcess.options.format.Value{1}, 3};
+ if isfield(sProcess, 'options') && isfield(sProcess.options, 'format')
+ FileFormatsChan = bst_get('FileFilters', 'channel');
+ FileFormat = FileFormatsChan{sProcess.options.format.Value{1}, 3};
+ else
+ FileFormat = [];
+ end
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).
+ if isReport
+ bst_report('Info', sProcess, sInput, ...
+ sprintf('Could not find original channel file: %s.', ChannelFile));
+ end
+ % import_channel will prompt the user, but they would 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). So add another pop-up with the needed info.
[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), ...
@@ -318,17 +427,17 @@
movegui(MsgFig, 'north');
figure(MsgFig); % bring it to front.
% Adjust default format to the one selected.
- DefaultFormats = bst_get('DefaultFormats');
- DefaultFormats.ChannelIn = FileFormat;
- bst_set('DefaultFormats', DefaultFormats);
+ if ~isempty(FileFormat)
+ DefaultFormats = bst_get('DefaultFormats');
+ DefaultFormats.ChannelIn = FileFormat;
+ bst_set('DefaultFormats', DefaultFormats);
+ end
- [NewChannelMat, NewChannelFile] = import_channel(...
- sInput.iStudy, '', FileFormat, 0, 0, 0, [], []);
+ [NewChannelMat, NewChannelFile] = import_channel(sInput.iStudy, '', FileFormat, 0, 0, 0, [], []);
else
% Import from original file.
- [NewChannelMat, NewChannelFile] = import_channel(...
- sInput.iStudy, ChannelFile, FileFormat, 0, 0, 0, [], []);
+ [NewChannelMat, NewChannelFile] = import_channel(sInput.iStudy, ChannelFile, FileFormat, 0, 0, 0, [], []);
% iStudies, ChannelFile, FileFormat, ChannelReplace, ChannelAlign, isSave, isFixUnits, isApplyVox2ras)
% iStudy index is needed to avoid error for noise recordings with missing SCS transform.
% ChannelReplace is for replacing the file, only if isSave.
@@ -337,31 +446,42 @@
% See if it worked.
if isempty(NewChannelFile)
- bst_report('Error', sProcess, sInput, ...
- 'No file channel file selected.');
- Failed = true;
+ if isReport
+ bst_report('Error', sProcess, sInput, 'No channel file selected.');
+ else
+ bst_error('No channel file selected.');
+ end
+ isError = true;
return;
elseif isempty(NewChannelMat)
- bst_report('Error', sProcess, sInput, ...
- sprintf('Unable to import channel file: %s', NewChannelFile));
- Failed = true;
+ if isReport
+ bst_report('Error', sProcess, sInput, sprintf('Unable to import channel file: %s', NewChannelFile));
+ else
+ bst_error(sprintf('Unable to import channel file: %s', NewChannelFile));
+ end
+ isError = true;
return;
elseif numel(NewChannelMat.Channel) ~= numel(ChannelMat.Channel)
- bst_report('Error', sProcess, sInput, ...
- 'Original channel file has different channels than current one, aborting.');
- Failed = true;
+ if isReport
+ bst_report('Error', sProcess, sInput, ...
+ 'Original channel file has different channels than current one, aborting.');
+ else
+ bst_error('Original channel file has different channels than current one, aborting.');
+ end
+ isError = true;
return;
elseif NotFound && ~isempty(ChannelFile)
% Save the selected new location.
NewChannelFiles(end+1, :) = {ChannelFile, NewChannelFile};
end
- % Copy the new old projectors and history to the new structure.
+ % Copy the old projectors and history to the new structure.
NewChannelMat.Projector = ChannelMat.Projector;
NewChannelMat.History = ChannelMat.History;
ChannelMat = NewChannelMat;
- % clear NewChannelMat
+
% Add number of channels to comment, like in db_set_channel.
ChannelMat.Comment = [ChannelMat.Comment, sprintf(' (%d)', length(ChannelMat.Channel))];
+ % Add history
ChannelMat = bst_history('add', ChannelMat, 'import', ...
['Reset from: ' NewChannelFile ' (Format: ' FileFormat ')']);
end % ResetChannelFile
@@ -380,9 +500,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)
@@ -441,8 +560,8 @@
end % RemoveTransformation
-function [ChannelMat, Failed] = AdjustHeadPosition(ChannelMat, sInputs, sProcess)
- Failed = false;
+function [ChannelMat, isError] = AdjustHeadPosition(ChannelMat, sInputs, sProcess)
+ isError = false;
% Check the input is CTF.
isRaw = (length(sInputs(1).FileName) > 9) && ~isempty(strfind(sInputs(1).FileName, 'data_0raw'));
if isRaw
@@ -453,13 +572,12 @@
if ~strcmp(DataMat.Device, 'CTF')
bst_report('Error', sProcess, sInputs, ...
'Adjust head position is currently only available for CTF data.');
- Failed = true;
+ isError = true;
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, ...
@@ -477,7 +595,7 @@
[Locs, HeadSamplePeriod] = process_evt_head_motion('LoadHLU', sInputs(iIn), [], false);
if isempty(Locs)
% No HLU channels. Error already reported. Skip this file.
- Failed = true;
+ isError = true;
return;
end
% Exclude all bad segments.
@@ -513,7 +631,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,55 +641,46 @@
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.
- Failed = true;
+ isError = true;
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)
% There was an error, already reported. Skip this file.
- Failed = true;
+ isError = true;
return;
end
% 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);
@@ -591,24 +700,22 @@
AfterRefLoc = ReferenceHeadLocation(ChannelMat, sInputs);
if isempty(AfterRefLoc)
% There was an error, already reported. Skip this file.
- Failed = true;
+ isError = true;
return;
end
DistanceAdjusted = process_evt_head_motion('RigidDistances', AfterRefLoc, InitRefLoc);
fprintf('Head position adjusted by %1.1f mm.\n', DistanceAdjusted * 1e3);
bst_report('Info', sProcess, sInputs, ...
- sprintf('Head position adjusted by %1.1f mm.\n', DistanceAdjusted * 1e3));
+ sprintf('Head position adjusted by %1.1f mm.', DistanceAdjusted * 1e3));
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 +723,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 +751,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 +761,25 @@
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.
+ if nargin < 2
+ sInputs = [];
+ end
+ % 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 +830,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 +840,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 +863,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 +898,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).
- %
- %
- % (c) Copyright 2018 Marc Lalancette
- % The Hospital for Sick Children, Toronto, Canada
+ % 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.
%
- % 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.
+ % 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 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 +937,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 +953,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 +967,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 +987,516 @@
end % GeoMedian
+function [AlignType, isMriUpdated, isMriMatch, isSessionMatch, ChannelMat] = CheckPrevAdjustments(ChannelMat, sMri)
+ % Flag if auto or manual registration performed, and if MRI fids updated. Print to command
+ % window for now, if no output arguments.
+ AlignType = [];
+ isMriUpdated = [];
+ isMriMatch = [];
+ isSessionMatch = [];
+ isPrint = nargout == 0;
+ if any(~isfield(ChannelMat, {'History', 'HeadPoints'}))
+ % Nothing to check.
+ return;
+ end
+ if nargin < 2 || isempty(sMri) || ~isfield(sMri, 'History') || isempty(sMri.History)
+ iMriHist = [];
+ else
+ % History string is set in figure_mri SaveMri.
+ 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'));
+ iAlign = find(strcmpi(ChannelMat.History(:,2), 'align'));
+ 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))
+ case 'remov' % ['Removed transform: ' TransfLabel]
+ % Removed a previous step. Ignore corresponding adjustment and look again.
+ iAlign(end) = [];
+ 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
+ if isempty(iAlignRemoved)
+ bst_error('Missing removed transformation in history.');
+ else
+ iAlign(iAlignRemoved) = [];
+ end
+ 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' % 'Refining the registration using the head points:'
+ % Automatic MRI-points alignment
+ AlignType = 'auto';
+ break;
+ 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
+ disp(['BST> Previous registration adjustment: ' AlignType]);
+ end
+ if ~isempty(iMriHist)
+ isMriUpdated = true;
+ % 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
+ ChannelMat = UpdateChannelMatScs(ChannelMat);
+ % Check if coordinates differ by more than 1 um.
+ 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;
+ % Check if just different alignment, or if different set of fiducials (different
+ % session), using inter-fid distances.
+ DiffMri = [sMri.SCS.NAS - sMri.SCS.LPA, sMri.SCS.LPA - sMri.SCS.RPA, sMri.SCS.RPA - sMri.SCS.NAS];
+ DiffChannel = [ChannelMat.SCS.NAS - ChannelMat.SCS.LPA, ChannelMat.SCS.LPA - ChannelMat.SCS.RPA, ChannelMat.SCS.RPA - ChannelMat.SCS.NAS];
+ if any(abs(DiffMri - DiffChannel) > 1e-3)
+ isSessionMatch = false;
+ if isPrint
+ disp('BST> MRI fiducials previously updated, but different session than current digitized fiducials.');
+ end
+ else
+ isSessionMatch = true;
+ if isPrint
+ disp('BST> MRI fiducials previously updated, same session but not aligned with current digitized fiducials.');
+ end
+ end
+ else
+ isMriMatch = true;
+ isSessionMatch = true;
+ if isPrint
+ disp('BST> MRI fiducials previously updated, and match current digitized fiducials.');
+ end
+ end
+ else
+ isMriUpdated = false;
+ isMriMatch = false;
+ isSessionMatch = false;
+ end
+
+end
+
+
+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;
+ if nargin < 2 || isempty(ChannelMatRef)
+ ChannelMatRef = [];
+ end
+
+ % Update SCS from head points if present.
+ ChannelMat = UpdateChannelMatScs(ChannelMat);
+
+ if ~isempty(ChannelMatRef)
+ ChannelMatRef = UpdateChannelMatScs(ChannelMatRef);
+ % For head displacement, we use the "rigid distance" from the head motion code, basically
+ % the max distance of any point on a simplified spherical head.
+ DistHead = process_evt_head_motion('RigidDistances', ...
+ [ChannelMat.SCS.NAS(:); ChannelMat.SCS.LPA(:); ChannelMat.SCS.RPA(:)], ...
+ [ChannelMatRef.SCS.NAS(:); ChannelMatRef.SCS.LPA(:); ChannelMatRef.SCS.RPA(:)]);
+ DistSens = max(sqrt(sum(([ChannelMat.Channel.Loc] - [ChannelMatRef.Channel.Loc]).^2)));
+ else
+ % 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. 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(Message);
+ end
+
+end
+
+
+function ChannelMat = UpdateChannelMatScs(ChannelMat)
+ if ~isfield(ChannelMat, 'HeadPoints')
+ return;
+ end
+ % 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); %#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)
+ % Temporarily put the head coils there to calculate transform.
+ 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);
+ % 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;
+ % Now apply the transform to the digitized anat fiducials. These are not used anywhere yet,
+ % only the transform, but might as well be consistent and save the same points as in .SCS.
+ % But still in meters, not cm.
+ ChannelMat.Native.NAS(:) = [ChannelMat.Native.R, ChannelMat.Native.T] * [ChannelMat.SCS.NAS'; 1];
+ ChannelMat.Native.LPA(:) = [ChannelMat.Native.R, ChannelMat.Native.T] * [ChannelMat.SCS.LPA'; 1];
+ ChannelMat.Native.RPA(:) = [ChannelMat.Native.R, ChannelMat.Native.T] * [ChannelMat.SCS.RPA'; 1];
+ else
+ % Missing digitized MEG head coils, probably the anatomical points are actually coils.
+ disp('BST> Missing digitized MEG head coils, NAS/LPA/RPA are likely head coils.');
+ ChannelMat.Native = ChannelMat.SCS;
+ end
+end
+
+
+% Decided to bring this back as subfunction of this process, as it is the only place to run it from for now.
+function [Transform, isCancel, isError] = channel_align_scs(ChannelFile, Transform, isInteractive, isConfirm, sInput, sProcess)
+% CHANNEL_ALIGN_SCS: Saves new MRI anatomical points after manual or auto registration adjustment.
+%
+% USAGE: Transform = channel_align_scs(ChannelFile, Transform=eye(4), isInteractive=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). 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 Brainstorm 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 isInteractive and isConfirm are false). In that case, 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.
+% 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. (This will typically be empty or identity, it
+% was intended for calling from manual alignment panel, but now done after.)
+% - isInteractive : If true, display dialog in case of errors, or if this was already done
+% previously for this MRI.
+% - isConfirm : If true, 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 the MRI was not updated, the input Transform is
+% returned. The idea is that the returned Transform applied to the *reset*
+% channels would maintain the registration. If channel files were not reset
+% (error or cancellation in this function), this will no longer be true and the
+% user should verify the registration of all affected studies.
+% - isCancel : If true, nothing was changed nor saved.
+% - isError : An error occurred that can affect registration of MRI and functional studies,
+% e.g. the MRI was updated, but some channel files of that subject were not
+% reset.
+
+% The Transform output is currently unused. It was changed (below is the previous behavior) since it
+% depended on CTF MEG specific transform labels.
+% - Transform : If the MRI fiducial points and coordinate system are updated, and the channel
+% file is reset, the transform becomes the identity. If the channel file is not
+% reset, Transform will be the inverse of all previous manual or automatic
+% adjustments. If the MRI was not updated, the input Transform is returned. The
+% idea is that the returned Transform applied to the channels would maintain the
+% registration.
+
+% Authors: Marc Lalancette 2022-2025
+
+if nargin < 6 || isempty(sProcess)
+ isReport = false;
+else
+ isReport = true;
+end
+if nargin < 4 || isempty(isConfirm)
+ isConfirm = true;
+end
+if nargin < 3 || isempty(isInteractive)
+ isInteractive = true;
+end
+if nargin < 2 || isempty(Transform)
+ Transform = eye(4);
+end
+if nargin < 1 || isempty(ChannelFile)
+ bst_error('ChannelFile argument required.');
+end
+
+isError = false;
+isCancel = false;
+% Get study
+sStudy = bst_get('ChannelFile', ChannelFile);
+% Get subject
+sSubject = 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 isReport
+ bst_report('Error', sProcess, sInput, Message);
+ elseif isInteractive
+ bst_error(Message, 'Apply digitized anatomical fiducials to MRI', 0);
+ else
+ disp(['BST> ' Message]);
+ end
+ isCancel = true;
+ return;
+end
+% 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)
+ Message = 'Digitized nasion and ear points not found.';
+ if isReport
+ bst_report('Error', sProcess, sInput, Message);
+ elseif isInteractive
+ bst_error(Message, 'Apply digitized anatomical fiducials to MRI', 0);
+ else
+ disp(['BST> ' Message]);
+ end
+ isCancel = true;
+ return;
+end
+
+% Check if already adjusted
+sMriOld = in_mri_bst(sSubject.Anatomy(sSubject.iAnatomy).FileName);
+% This Check function also updates ChannelMat.SCS with the saved (possibly previously adjusted) head
+% points. (We don't consider isMriMatch here because we still have to apply the provided
+% Transformation.)
+[~, isMriUpdated, ~, ~, ChannelMat] = CheckPrevAdjustments(ChannelMat, sMriOld);
+% Get user confirmation
+if isMriUpdated
+ % Already done previously.
+ if isInteractive || 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.
+ Message = 'Digitized nasion and ear points previously applied to this MRI. Not applying again.';
+ if isReport
+ bst_report('Warning', sProcess, sInput, Message);
+ else
+ disp(['BST> ' Message]);
+ end
+ isCancel = true;
+ return;
+ end
+elseif isConfirm
+ % Request confirmation.
+ [Proceed, isCancel] = java_dialog('confirm', ['Updating the MRI fiducial points NAS/LPA/RPA to match a set of' 10 ...
+ 'aligned digitized points is mainly used for exporting registration to a BIDS dataset.' 10 ...
+ 'It will break any previous alignment of this subject with all other functional datasets!' 10 10 ...
+ 'Proceed and update MRI now?' 10], 'Head points/anatomy registration');
+ if ~Proceed || isCancel
+ isCancel = true;
+ 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 (possibly identity).
+% Apply the transformation provided.
+sMri = sMriOld;
+% Intermediate step, these are not valid coordinates for sMri.
+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.
+% 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;
+% Re-compute transformation in this struct, which goes from MRI to SCS (but the fids stay in MRI coords in this struct).
+[~, 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);
+% At minimum, we must unload surfaces that have been modified, but we want to avoid closing figures
+% for when we show "before" and "after" figures.
+bst_memory('UnloadAll'); % not 'forced' so won't close figures, but won't unload what we want most likely.
+bst_memory('UnloadSurface'); % this unloads the head surface as we want.
+% Now that MRI is saved, update Transform to identity.
+Transform = eye(4);
+
+% MRI SCS now matches digitized-points-defined SCS (defined from same points), but registration is
+% now broken with all channel files that were adjusted! Reset channel file, and all others for this
+% anatomy.
+isError = ResetChannelFiles(ChannelMat, sSubject, isConfirm, sInput, sProcess);
+
+% Removed this output Transform for now as GetTransform only works on CTF MEG data and the rest of
+% this function can work more generally.
+% if isError
+% % Get the equivalent overall registration adjustment transformation previously saved.
+% [~, ~, TransfAfter] = GetTransforms(ChannelMat);
+% % Return its inverse as it's now part of the MRI and should be removed from the channel file.
+% Transform = inverse(TransfAfter);
+% else
+% Transform = eye(4);
+% end
+
+end % main function
+
+% (This function was based on channel_align_manual CopyToOtherFolders).
+function [isError, Message] = ResetChannelFiles(ChannelMatSrc, sSubject, isConfirm, sInput, sProcess)
+ if nargin < 5 || isempty(sProcess)
+ sProcess = [];
+ isReport = false;
+ else
+ isReport = true;
+ end
+ % Confirmation: ask the first time
+ if nargin < 3 || isempty(isConfirm)
+ isConfirm = true;
+ end
+
+ NewChannelFiles = cell(0,2);
+ % First, always reset the "source" channel file.
+ [ChannelMatSrc, NewChannelFiles, isError] = ResetChannelFile(ChannelMatSrc, NewChannelFiles, sInput, sProcess);
+ if isError
+ Message = sprintf(['Unable to reset channel file for subject: %s\n' ...
+ 'MRI registration for all their functional studies should be verified!'], sSubject.Name);
+ if isReport
+ bst_report('Error', sProcess, sInput, Message);
+ end
+ % This is very important so always show it interactively.
+ java_dialog('msgbox', Message);
+ return;
+ end
+ bst_save(file_fullpath(sInput.ChannelFile), ChannelMatSrc, 'v7');
+
+ % If the subject is configured to share its channel files, nothing to do
+ if (sSubject.UseDefaultChannel >= 1)
+ return;
+ end
+ % Get all the dependent studies
+ sStudies = bst_get('StudyWithSubject', sSubject.FileName);
+ % List of channel files to update
+ ChannelFiles = {};
+ % Loop on the other folders
+ for i = 1:length(sStudies)
+ % Skip studies without channel files
+ if isempty(sStudies(i).Channel) || isempty(sStudies(i).Channel(1).FileName)
+ continue;
+ end
+ % Add channel file to list of files to process
+ ChannelFiles{end+1} = sStudies(i).Channel(1).FileName; %#ok
+ end
+ % Unique files and skip "source".
+ ChannelFiles = setdiff(unique(ChannelFiles), sInput.ChannelFile);
+ if ~isempty(ChannelFiles)
+ % Ask confirmation to the user
+ if isConfirm
+ Proceed = java_dialog('confirm', ...
+ sprintf('Reset all %d other channel files for this subject (typically recommended)?', numel(ChannelFiles)), 'Reset channel files');
+ if ~Proceed
+ Message = sprintf(['User cancelled resetting %d other channel files for subject: %s\n' ...
+ 'MRI registration for all functional studies should be verified!'], numel(ChannelFiles), sSubject.Name);
+ if isReport
+ bst_report('Warning', sProcess, sInput, Message);
+ else
+ java_dialog('msgbox', Message);
+ end
+ return;
+ end
+ end
+ % Progress bar
+ bst_progress('start', 'Reset channel files', 'Updating other studies...');
+ strMsg = '';
+ strErr = '';
+ for iChan = 1:numel(ChannelFiles)
+ ChannelFile = ChannelFiles{iChan};
+ % Load channel file
+ ChannelMat = in_bst_channel(ChannelFile);
+ % Reset & save
+ [ChannelMat, NewChannelFiles, isError] = ResetChannelFile(ChannelMat, NewChannelFiles, sInput, sProcess);
+ if isError
+ strErr = [strErr, ChannelFile, 10]; %#ok
+ else
+ strMsg = [strMsg, ChannelFile, 10]; %#ok
+ bst_save(file_fullpath(ChannelFile), ChannelMat, 'v7');
+ end
+ end
+ bst_progress('stop');
+ % Give report to the user
+ if ~isempty(strErr)
+ Message = sprintf(['Unable to reset channel file(s) for subject %s:\n%s\n' ...
+ 'MRI registration should be verified for these studies!'], sSubject.Name, strErr);
+ if isReport
+ bst_report('Error', sProcess, sInput, Message);
+ end
+ % This is very important so always show it interactively.
+ java_dialog('msgbox', Message);
+ return;
+ end
+ Message = sprintf('%d channel files reset for subject %s:\n%s', numel(ChannelFiles)+1, sSubject.Name, strMsg);
+ if isReport
+ bst_report('Info', sProcess, sInput, Message);
+ elseif isConfirm
+ java_dialog('msgbox', Message);
+ else
+ disp(Message);
+ end
+ end
+end
+
diff --git a/toolbox/process/functions/process_channel_addloc.m b/toolbox/process/functions/process_channel_addloc.m
index 288622abd..6cda64695 100644
--- a/toolbox/process/functions/process_channel_addloc.m
+++ b/toolbox/process/functions/process_channel_addloc.m
@@ -202,8 +202,8 @@
bst_report('Error', sProcess, [], 'No channel file selected.');
return
end
- % Load channel file
- ChannelMat = in_bst_channel(ChannelFile);
+ % Channel file to be loaded in channel_add_loc()
+ ChannelMat = ChannelFile;
else
isMni = 0;
end
diff --git a/toolbox/process/functions/process_channel_biosemi.m b/toolbox/process/functions/process_channel_biosemi.m
index 3907b9963..6bbf3e16f 100644
--- a/toolbox/process/functions/process_channel_biosemi.m
+++ b/toolbox/process/functions/process_channel_biosemi.m
@@ -161,7 +161,7 @@ function ComputeInteractive(ChannelFile)
'C4', 'C6', 'T8', 'TP7', 'CP5', 'CP3', 'CP1', 'CPz', ...
'CP2', 'CP4', 'CP6', 'TP8', 'P9', 'P7', 'P5', 'P3', ...
'P1', 'Pz', 'P2', 'P4', 'P6', 'P8', 'P10', 'PO7', ...
- 'PO3', 'POZ', 'PO4', 'PO8', 'O1', 'OZ', 'O2', 'Iz'};
+ 'PO3', 'POz', 'PO4', 'PO8', 'O1', 'Oz', 'O2', 'Iz'};
otherwise
error('BioSemi cap size %d is not supported.', capSize);
diff --git a/toolbox/process/functions/process_cohere1.m b/toolbox/process/functions/process_cohere1.m
index 31075db97..06c41dd66 100644
--- a/toolbox/process/functions/process_cohere1.m
+++ b/toolbox/process/functions/process_cohere1.m
@@ -52,9 +52,9 @@
sProcess.options.label1.Comment = 'Connectivity Metric:';
sProcess.options.label1.Type = 'label';
sProcess.options.cohmeasure.Comment = {...
- 'Magnitude-squared coherence: |C|^2 = |Cxy|^2/(Cxx*Cyy)', ...
- 'Imaginary coherence: IC = |imag(C)|', ...
- 'Lagged coherence / Corrected imaginary coherence: LC = |imag(C)|/sqrt(1-real(C)^2)'; ...
+ 'Magnitude-squared coherence', ...
+ 'Imaginary coherence', ...
+ 'Lagged coherence / Corrected imaginary coherence'; ...
'mscohere', 'icohere2019','lcohere2019'}; % , 'icohere'
% ' Squared Lagged Coherence ("imaginary coherence" before 2019)' ...
sProcess.options.cohmeasure.Type = 'radio_label';
@@ -98,16 +98,7 @@
%% ===== FORMAT COMMENT =====
function Comment = FormatComment(sProcess)
- if ~isempty(sProcess.options.cohmeasure.Value)
- iMethod = find(strcmpi(sProcess.options.cohmeasure.Comment(2,:), sProcess.options.cohmeasure.Value));
- if ~isempty(iMethod)
- Comment = str_striptag(sProcess.options.cohmeasure.Comment{1,iMethod});
- else
- Comment = sProcess.Comment;
- end
- else
- Comment = sProcess.Comment;
- end
+ Comment = sProcess.Comment;
end
diff --git a/toolbox/process/functions/process_cohere1n.m b/toolbox/process/functions/process_cohere1n.m
index f3faa491f..294c18820 100644
--- a/toolbox/process/functions/process_cohere1n.m
+++ b/toolbox/process/functions/process_cohere1n.m
@@ -53,9 +53,9 @@
sProcess.options.label1.Comment = 'Connectivity Metric:';
sProcess.options.label1.Type = 'label';
sProcess.options.cohmeasure.Comment = {...
- 'Magnitude-squared coherence: |C|^2 = |Cxy|^2/(Cxx*Cyy)', ...
- 'Imaginary coherence: IC = |imag(C)|', ...
- 'Lagged coherence / Corrected imaginary coherence: LC = |imag(C)|/sqrt(1-real(C)^2)'; ...
+ 'Magnitude-squared coherence', ...
+ 'Imaginary coherence', ...
+ 'Lagged coherence / Corrected imaginary coherence'; ...
'mscohere', 'icohere2019','lcohere2019'}; % , 'icohere'
% ' Squared Lagged Coherence ("imaginary coherence" before 2019)' ...
sProcess.options.cohmeasure.Type = 'radio_label';
@@ -99,16 +99,7 @@
%% ===== FORMAT COMMENT =====
function Comment = FormatComment(sProcess)
- if ~isempty(sProcess.options.cohmeasure.Value)
- iMethod = find(strcmpi(sProcess.options.cohmeasure.Comment(2,:), sProcess.options.cohmeasure.Value));
- if ~isempty(iMethod)
- Comment = str_striptag(sProcess.options.cohmeasure.Comment{1,iMethod});
- else
- Comment = sProcess.Comment;
- end
- else
- Comment = sProcess.Comment;
- end
+ Comment = sProcess.Comment;
end
diff --git a/toolbox/process/functions/process_cohere2.m b/toolbox/process/functions/process_cohere2.m
index c3383b7c7..6ab1df5e6 100644
--- a/toolbox/process/functions/process_cohere2.m
+++ b/toolbox/process/functions/process_cohere2.m
@@ -99,16 +99,7 @@
%% ===== FORMAT COMMENT =====
function Comment = FormatComment(sProcess)
- if ~isempty(sProcess.options.cohmeasure.Value)
- iMethod = find(strcmpi(sProcess.options.cohmeasure.Comment(2,:), sProcess.options.cohmeasure.Value));
- if ~isempty(iMethod)
- Comment = str_striptag(sProcess.options.cohmeasure.Comment{1,iMethod});
- else
- Comment = sProcess.Comment;
- end
- else
- Comment = sProcess.Comment;
- end
+ Comment = sProcess.Comment;
end
diff --git a/toolbox/process/functions/process_combine_recordings.m b/toolbox/process/functions/process_combine_recordings.m
new file mode 100644
index 000000000..d47e4cd9a
--- /dev/null
+++ b/toolbox/process/functions/process_combine_recordings.m
@@ -0,0 +1,298 @@
+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;
+
+ % Option: Condition
+ sProcess.options.condition.Comment = 'Condition name:';
+ sProcess.options.condition.Type = 'text';
+ sProcess.options.condition.Value = 'Combined';
+end
+
+
+%% ===== FORMAT COMMENT =====
+function Comment = FormatComment(sProcess) %#ok
+ Comment = sProcess.Comment;
+end
+
+
+%% ===== RUN =====
+function OutputFiles = Run(sProcess, sInputs)
+ OutputFiles = {};
+
+ nInputs = length(sInputs);
+ sIdxChNew = cell(1, nInputs); % Channel indices for each input in new channel file
+ sProjNew = cell(1, nInputs); % Projectors for each input in new channel file
+
+ % Check for same Subject
+ if length(unique({sInputs.SubjectFile})) > 1
+ bst_report('Error', sProcess, sInputs, 'All raw recordings must belong to the same Subject.');
+ return
+ end
+ % Get unique comment for new condition
+ NewComment = sProcess.options.condition.Value;
+ if isempty(NewComment)
+ bst_report('Error', sProcess, sInputs, 'Condition name was not defined.');
+ return
+ end
+ NewCondition = ['@raw', file_standardize(NewComment)];
+ sStudies = bst_get('StudyWithSubject', sInputs(1).SubjectFile);
+ NewCondition = file_unique(NewCondition, [sStudies.Condition]);
+
+
+ % ===== GET METADATA FOR RECORDINGS =====
+ bst_progress('start', 'Combining recordings', 'Loading metadata...', 0, 3 * nInputs); % 3 steps per input file
+ % Get Time and F structure
+ for iInput = 1 : nInputs
+ sMetaData(iInput) = in_bst_data(sInputs(iInput).FileName, 'Time', 'F');
+ bst_progress('inc', 1);
+ end
+ % Check that the limits are close enough, same duration, and same start
+ all_times = [arrayfun(@(x) x.F.prop.times(1), sMetaData)', arrayfun(@(x) x.F.prop.times(2), sMetaData)'];
+ max_diffs = max(all_times,[], 1) - min(all_times,[], 1);
+ if any(max_diffs > 1) % Tolerance of 1 second for maximum differences
+ bst_report('Error', sProcess, sInputs, 'Recordings to merge must have the same start and end time.');
+ return
+ end
+
+
+ % ===== COMBINE METADATA =====
+ bst_progress('text', 'Combining metadata...');
+ % Recordings file with higher sampling frequency is used as seed for time
+ [~, iRefRec] = max(arrayfun(@(x) x.F.prop.sfreq, sMetaData));
+ % New sampling frequency
+ NewFs = sMetaData(iRefRec).F.prop.sfreq;
+ % Study for combined recordings
+ iNewStudy = db_add_condition(sInputs(iRefRec).SubjectName, NewCondition);
+ sNewStudy = bst_get('Study', iNewStudy);
+ % New time vector
+ NewTime = sMetaData(iRefRec).Time;
+ % New channel definition
+ NewChannelsN = 0;
+ NewChannelMat = db_template('ChannelMat');
+ NewChannelMat.Channel = repmat(db_template('channeldesc'), NewChannelsN);
+ % New channel flag
+ NewChannelFlag = [];
+ % New events
+ NewEvents = repmat(db_template('event'), 0);
+
+ % Events to be merged in combined raw file
+ poolEvents = NewEvents; % Empty sEvents
+ poolEventsIx = []; % Index of file associated with the event
+ poolEventsFx = []; % Fixing channel-wise data is needed
+ for iInput = 1 : nInputs
+ poolEvents = [poolEvents, sMetaData(iInput).F.events];
+ poolEventsIx = [poolEventsIx, repmat(iInput, 1, length(sMetaData(iInput).F.events))];
+ poolEventsFx = [poolEventsFx, ones(1, length(sMetaData(iInput).F.events))];
+ end
+ % Handle duplicated names and identify events do not need their channel field updated
+ [uniqueLabels, ~, iUnique] = unique({poolEvents.label});
+ iDel = [];
+ for iu = 1 : length(uniqueLabels)
+ % Instances of unique events
+ ixUnique = find(iUnique == iu)';
+ % Do nothing if event is not duplicated
+ if length(ixUnique) == 1
+ continue
+ % Keep only one copy, set to not be channel-wise fixed
+ elseif all(cellfun(@isempty, {poolEvents(ixUnique).channels})) && isequal(poolEvents(ixUnique).times)
+ poolEventsFx(ixUnique) = 0;
+ iDel = [iDel, ixUnique(2:end)];
+ % Create unique names
+ else
+ baseLabel = poolEvents(ixUnique(1)).label;
+ for id = 1 : length(ixUnique)
+ % Update their names to make unique
+ poolEvents(ixUnique(id)).label = sprintf([baseLabel '_%02d'], id);
+ end
+ end
+ end
+ poolEvents(iDel) = [];
+ poolEventsIx(iDel) = [];
+ poolEventsFx(iDel) = [];
+
+ for iInput = 1 : nInputs
+ % Get channel file
+ tmpChannelMat = in_bst_channel(sInputs(iInput).ChannelFile);
+ tmpChannelNames = {tmpChannelMat.Channel.Name};
+ % Concatenate channels
+ % Ensure unique names for channels
+ ixChannelDup = find(ismember({tmpChannelMat.Channel.Name}, {NewChannelMat.Channel.Name}));
+ for iDup = 1 : length(ixChannelDup)
+ tmpChannelMat.Channel(ixChannelDup(iDup)).Name = file_unique(tmpChannelMat.Channel(ixChannelDup(iDup)).Name, {NewChannelMat.Channel.Name});
+ end
+ sIdxChNew{iInput} = NewChannelsN + [1 : length(tmpChannelMat.Channel)];
+ NewChannelMat.Channel = [NewChannelMat.Channel, tmpChannelMat.Channel];
+
+ % Concatenate channel flag
+ NewChannelFlag = [NewChannelFlag; sMetaData(iInput).ChannelFlag];
+
+ % Store projectors to concatenate later
+ sProjNew{iInput} = tmpChannelMat.Projector;
+
+ % Add channel information if needed
+ tmpEvents = poolEvents(poolEventsIx == iInput);
+ for iEvent = 1 : length(tmpEvents)
+ tmpEvent = tmpEvents(iEvent);
+ if poolEventsFx(iEvent)
+ % Add channel info
+ addedChannelNames = {NewChannelMat.Channel(sIdxChNew{iInput}).Name};
+ nOccurences = size(tmpEvent.times, 2);
+ % Make a channel-wise event with all channels in Input file
+ if isempty(tmpEvent.channels)
+ tmpEvent.channels = repmat({addedChannelNames}, 1, nOccurences);
+ else
+ for iOccurence = 1 : nOccurences
+ % Make a channel-wise event with all channels in Input file
+ if isempty(tmpEvent.channels{iOccurence})
+ tmpEvent.channels{iOccurence} = addedChannelNames;
+ % Update channel names to names that were added in combined file
+ else
+ [~, iLoc] = ismember(tmpEvent.channels{iOccurence}, tmpChannelNames);
+ tmpEvent.channels{iOccurence} = addedChannelNames(iLoc);
+ end
+ end
+ end
+ end
+ NewEvents = [NewEvents, tmpEvent];
+ end
+
+ % Copy videos
+ tmpStudy = bst_get('Study', sInputs(iInput).iStudy);
+ if isfield(tmpStudy,'Image') && ~isempty(tmpStudy.Image)
+ for iTmpVideo = 1 : length(tmpStudy.Image)
+ sTmpVideo = load(file_fullpath(tmpStudy.Image(iTmpVideo).FileName));
+ if isempty(sTmpVideo.VideoStart)
+ sTmpVideo.VideoStart = 0;
+ end
+ [~, outVideoFile] = import_video(iNewStudy, sTmpVideo.LinkTo);
+ figure_video('SetVideoStart', outVideoFile{1}, sprintf('%.3f', sTmpVideo.VideoStart));
+ end
+ end
+
+ % Concat NIRS wavelengths
+ if isfield(tmpChannelMat, 'Nirs')
+ if ~isfield(NewChannelMat, 'Nirs')
+ NewChannelMat.Nirs = tmpChannelMat.Nirs;
+ else
+ NewChannelMat.Nirs = sort(union(tmpChannelMat.Nirs, NewChannelMat.Nirs));
+ end
+ end
+
+ % New channel count
+ NewChannelsN = length(NewChannelMat.Channel);
+ % Progress
+ bst_progress('inc', 1);
+ end
+
+ % Concatenate Projectors
+ for iInput = 1 : nInputs
+ for iProj = 1 : length(sProjNew{iInput})
+ % Adjust Nchan in Projector
+ sizeComp = size(sProjNew{iInput}(iProj).Components);
+ newSizeComp = sizeComp;
+ ixs = cell(1, length(newSizeComp));
+ for iDim = 1 : length(sizeComp)
+ if sizeComp(iDim) == length(sIdxChNew{iInput})
+ newSizeComp(iDim) = NewChannelsN;
+ ixs{iDim} = sIdxChNew{iInput};
+ else
+ newSizeComp(iDim) = sizeComp(iDim);
+ ixs{iDim} = 1:sizeComp(iDim);
+ end
+ end
+ newComponents = zeros(newSizeComp);
+ newComponents(ixs{1}, ixs{2}) = sProjNew{iInput}(iProj).Components;
+ sProjNew{iInput}(iProj).Components = newComponents;
+ end
+ end
+ NewChannelMat.Projector = [sProjNew{:}];
+
+ % Save channel file
+ db_set_channel(iNewStudy, NewChannelMat, 0, 0);
+
+
+ % ===== COMBINE DATA =====
+ bst_progress('text', 'Combining data...');
+ % Link to combined raw file
+ OutputFile = bst_process('GetNewFilename', bst_fileparts(sNewStudy.FileName), 'data_0raw_combined');
+ % Combined raw file
+ [rawDirOut, rawBaseOut] = bst_fileparts(OutputFile);
+ rawBaseOut = regexprep(rawBaseOut, '^data_0raw_', '');
+ RawFileOut = bst_fullfile(rawDirOut, [rawBaseOut, '.bst']);
+
+ % Create a header structure for combined recordings
+ sFileIn = db_template('sfile');
+ sFileIn.header.nsamples = length(NewTime);
+ sFileIn.prop.times = [NewTime(1), NewTime(end)];
+ sFileIn.prop.sfreq = NewFs;
+ sFileIn.events = NewEvents;
+ sFileIn.channelflag = NewChannelFlag;
+ sFileOut = out_fopen(RawFileOut, 'BST-BIN', sFileIn, NewChannelMat);
+
+ % Build output structure for combined recordings
+ sOutMat = db_template('DataMat');
+ sOutMat.Comment = 'Link to raw file | Combined';
+ sOutMat.F = sFileOut;
+ sOutMat.format = 'BST-BIN';
+ sOutMat.DataType = 'raw';
+ sOutMat.ChannelFlag = NewChannelFlag;
+ sOutMat.Time = sFileIn.prop.times;
+ sOutMat.Device = 'Brainstorm';
+ bst_save(OutputFile, sOutMat, 'v6');
+
+ % Save all data to combined file
+ for iInput = 1 : nInputs
+ % Load raw data
+ sDataToCombine = in_bst(sInputs(iInput).FileName, [], 1, 1, 'no', 0);
+ % Update raw data to new time vector
+ if iInput ~= iRefRec
+ sDataToCombine.F = interp1(sDataToCombine.Time, sDataToCombine.F', NewTime)';
+ end
+ % Write these channels
+ out_fwrite(sFileOut, NewChannelMat, 1, [], sIdxChNew{iInput}, sDataToCombine.F);
+ bst_progress('inc', 1);
+ end
+
+ % Register in BST database
+ db_add_data(iNewStudy, OutputFile, sOutMat);
+ OutputFiles{iInput} = OutputFile;
+ bst_progress('stop');
+end
diff --git a/toolbox/process/functions/process_convert_raw_to_lfp.m b/toolbox/process/functions/process_convert_raw_to_lfp.m
index 20ed8351e..8a63bce81 100644
--- a/toolbox/process/functions/process_convert_raw_to_lfp.m
+++ b/toolbox/process/functions/process_convert_raw_to_lfp.m
@@ -168,7 +168,7 @@
RawFileFormat = 'BST-BIN';
% Number of time points in output file
- newTimeVector = downsample(DataMat.Time, round(Fs/LFP_fs));
+ newTimeVector = process_resample('Compute', DataMat.Time, linspace(0, length(DataMat.Time)/Fs, length(DataMat.Time)), LFP_fs);
nTimeOut = length(newTimeVector);
% Template structure for the creation of the output raw file
sFileTemplate = sFileIn;
diff --git a/toolbox/process/functions/process_corr1.m b/toolbox/process/functions/process_corr1.m
index 6eec2891f..0b0a9c093 100644
--- a/toolbox/process/functions/process_corr1.m
+++ b/toolbox/process/functions/process_corr1.m
@@ -22,6 +22,8 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2012-2020
+% Raymundo Cassani, 2023
+
eval(macro_method);
end
@@ -43,6 +45,22 @@
% === CONNECT INPUT
sProcess = process_corr1n('DefineConnectOptions', sProcess, 0);
+ % === TIME RESOLUTION
+ sProcess.options.timeres.Comment = {'Windowed', 'None', 'Time resolution:'; ...
+ 'windowed', 'none', ''};
+ sProcess.options.timeres.Type = 'radio_linelabel';
+ sProcess.options.timeres.Value = 'none';
+ sProcess.options.timeres.Controller = struct('windowed', 'windowed', 'none', 'nowindowed');
+ % === WINDOW LENGTH
+ sProcess.options.avgwinlength.Comment = ' Time window length:';
+ sProcess.options.avgwinlength.Type = 'value';
+ sProcess.options.avgwinlength.Value = {1, 's', []};
+ sProcess.options.avgwinlength.Class = 'windowed';
+ % === WINDOW OVERLAP
+ sProcess.options.avgwinoverlap.Comment = ' Time window overlap:';
+ sProcess.options.avgwinoverlap.Type = 'value';
+ sProcess.options.avgwinoverlap.Value = {50, '%', []};
+ sProcess.options.avgwinoverlap.Class = 'windowed';
% === SCALAR PRODUCT
sProcess.options.scalarprod.Comment = 'Compute scalar product instead of correlation (do not remove average of the signal)';
sProcess.options.scalarprod.Type = 'checkbox';
@@ -75,6 +93,12 @@
OPTIONS.Method = 'corr';
OPTIONS.pThresh = 0.05;
OPTIONS.RemoveMean = ~sProcess.options.scalarprod.Value;
+ if strcmpi(sProcess.options.timeres.Value, 'windowed')
+ OPTIONS.WinLen = sProcess.options.avgwinlength.Value{1};
+ OPTIONS.WinOverlap = sProcess.options.avgwinoverlap.Value{1}/100;
+ end
+ % Time-resolved; now option, no longer separate process
+ OPTIONS.TimeRes = sProcess.options.timeres.Value;
% Compute metric
OutputFiles = bst_connectivity(sInputA, sInputA, OPTIONS);
diff --git a/toolbox/process/functions/process_corr1n.m b/toolbox/process/functions/process_corr1n.m
index b7f0c103f..4070d9d2c 100644
--- a/toolbox/process/functions/process_corr1n.m
+++ b/toolbox/process/functions/process_corr1n.m
@@ -22,6 +22,7 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2012-2020
+% Raymundo Cassani, 2023
eval(macro_method);
end
@@ -40,9 +41,26 @@
sProcess.OutputTypes = {'timefreq', 'timefreq', 'timefreq'};
sProcess.nInputs = 1;
sProcess.nMinFiles = 1;
+ sProcess.isSeparator = 1;
% === CONNECT INPUT
sProcess = process_corr1n('DefineConnectOptions', sProcess, 1);
+ % === TIME RESOLUTION
+ sProcess.options.timeres.Comment = {'Windowed', 'None', 'Time resolution:'; ...
+ 'windowed', 'none', ''};
+ sProcess.options.timeres.Type = 'radio_linelabel';
+ sProcess.options.timeres.Value = 'none';
+ sProcess.options.timeres.Controller = struct('windowed', 'windowed', 'none', 'nowindowed');
+ % === WINDOW LENGTH
+ sProcess.options.avgwinlength.Comment = ' Time window length:';
+ sProcess.options.avgwinlength.Type = 'value';
+ sProcess.options.avgwinlength.Value = {1, 's', []};
+ sProcess.options.avgwinlength.Class = 'windowed';
+ % === WINDOW OVERLAP
+ sProcess.options.avgwinoverlap.Comment = ' Time window overlap:';
+ sProcess.options.avgwinoverlap.Type = 'value';
+ sProcess.options.avgwinoverlap.Value = {50, '%', []};
+ sProcess.options.avgwinoverlap.Class = 'windowed';
% === SCALAR PRODUCT
sProcess.options.scalarprod.Comment = 'Compute scalar product instead of correlation (do not remove average of the signal)';
sProcess.options.scalarprod.Type = 'checkbox';
@@ -74,6 +92,12 @@
OPTIONS.Method = 'corr';
OPTIONS.pThresh = 0.05;
OPTIONS.RemoveMean = ~sProcess.options.scalarprod.Value;
+ if strcmpi(sProcess.options.timeres.Value, 'windowed')
+ OPTIONS.WinLen = sProcess.options.avgwinlength.Value{1};
+ OPTIONS.WinOverlap = sProcess.options.avgwinoverlap.Value{1}/100;
+ end
+ % Time-resolved; now option, no longer separate process
+ OPTIONS.TimeRes = sProcess.options.timeres.Value;
% Compute metric
OutputFiles = bst_connectivity(sInputA, [], OPTIONS);
diff --git a/toolbox/process/functions/process_corr2.m b/toolbox/process/functions/process_corr2.m
index 89057673d..2d62957f4 100644
--- a/toolbox/process/functions/process_corr2.m
+++ b/toolbox/process/functions/process_corr2.m
@@ -22,6 +22,7 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2012-2020
+% Raymundo Cassani, 2023
eval(macro_method);
end
@@ -44,6 +45,22 @@
% === CONNECT INPUT
sProcess = process_corr2('DefineConnectOptions', sProcess);
+ % === TIME RESOLUTION
+ sProcess.options.timeres.Comment = {'Windowed', 'None', 'Time resolution:'; ...
+ 'windowed', 'none', ''};
+ sProcess.options.timeres.Type = 'radio_linelabel';
+ sProcess.options.timeres.Value = 'none';
+ sProcess.options.timeres.Controller = struct('windowed', 'windowed', 'none', 'nowindowed');
+ % === WINDOW LENGTH
+ sProcess.options.avgwinlength.Comment = ' Time window length:';
+ sProcess.options.avgwinlength.Type = 'value';
+ sProcess.options.avgwinlength.Value = {1, 's', []};
+ sProcess.options.avgwinlength.Class = 'windowed';
+ % === WINDOW OVERLAP
+ sProcess.options.avgwinoverlap.Comment = ' Time window overlap:';
+ sProcess.options.avgwinoverlap.Type = 'value';
+ sProcess.options.avgwinoverlap.Value = {50, '%', []};
+ sProcess.options.avgwinoverlap.Class = 'windowed';
% === SCALAR PRODUCT
sProcess.options.scalarprod.Comment = 'Compute scalar product instead of correlation (do not remove average of the signal)';
sProcess.options.scalarprod.Type = 'checkbox';
@@ -76,6 +93,12 @@
OPTIONS.Method = 'corr';
OPTIONS.pThresh = 0.05;
OPTIONS.RemoveMean = ~sProcess.options.scalarprod.Value;
+ if strcmpi(sProcess.options.timeres.Value, 'windowed')
+ OPTIONS.WinLen = sProcess.options.avgwinlength.Value{1};
+ OPTIONS.WinOverlap = sProcess.options.avgwinoverlap.Value{1}/100;
+ end
+ % Time-resolved; now option, no longer separate process
+ OPTIONS.TimeRes = sProcess.options.timeres.Value;
% Compute metric
OutputFiles = bst_connectivity(sInputA, sInputB, OPTIONS);
diff --git a/toolbox/process/functions/process_decoding_maxcorr.m b/toolbox/process/functions/process_decoding_maxcorr.m
index d2c892321..472d18162 100644
--- a/toolbox/process/functions/process_decoding_maxcorr.m
+++ b/toolbox/process/functions/process_decoding_maxcorr.m
@@ -30,7 +30,7 @@
sProcess.Comment = 'Max-correlation decoding';
sProcess.Category = 'Custom';
sProcess.SubGroup = 'Decoding';
- sProcess.Index = 702;
+ sProcess.Index = 712;
sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/Decoding';
% Definition of the input accepted by this process
sProcess.InputTypes = {'data'};
diff --git a/toolbox/process/functions/process_decoding_svm.m b/toolbox/process/functions/process_decoding_svm.m
index ead868c61..ac6cc4a28 100644
--- a/toolbox/process/functions/process_decoding_svm.m
+++ b/toolbox/process/functions/process_decoding_svm.m
@@ -30,7 +30,7 @@
sProcess.Comment = 'SVM decoding';
sProcess.Category = 'Custom';
sProcess.SubGroup = 'Decoding';
- sProcess.Index = 702;
+ sProcess.Index = 712;
sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/Decoding';
% Definition of the input accepted by this process
sProcess.InputTypes = {'data'};
@@ -46,6 +46,11 @@
sProcess.options.sensortypes.Comment = 'Sensor types or names (empty=all): ';
sProcess.options.sensortypes.Type = 'text';
sProcess.options.sensortypes.Value = 'MEG';
+ % === ignore bad channels
+ sProcess.options.ignorebad.Comment = 'Ignore bad channels';
+ sProcess.options.ignorebad.Type = 'checkbox';
+ sProcess.options.ignorebad.Value = 0;
+ sProcess.options.ignorebad.InputTypes = {'data'};
% === lowpass filtering
sProcess.options.lowpass.Comment = 'Low-pass cutoff frequency (0=disabled): ';
sProcess.options.lowpass.Type = 'value';
@@ -94,6 +99,11 @@
model = sProcess.options.model.Value;
methods = {'pairwise', 'temporalgen', 'multiclass'};
method = methods{sProcess.options.method.Value};
+ if isfield(sProcess.options, 'ignorebad') && isfield(sProcess.options.ignorebad, 'Value') && ~isempty(sProcess.options.ignorebad.Value)
+ IgnoreBad = sProcess.options.ignorebad.Value;
+ else
+ IgnoreBad = 0;
+ end
% Ensure we are including the LibSVM folder in the Matlab path
if strcmpi(model, 'svm')
@@ -141,6 +151,20 @@
bst_report('Error', sProcess, [], ['Decoding using the ' method ' method is not yet supported.']);
return;
end
+ % Channel files for all inputs must have the same list of channels
+ uniqueChannelFiles = unique({sInputs.ChannelFile});
+ if length(uniqueChannelFiles) > 1
+ channelMatRef = in_bst_channel(uniqueChannelFiles{1});
+ channelNamesRef = {channelMatRef.Channel.Name};
+ for iChannelFile = 2 : length(uniqueChannelFiles)
+ channelMatTest = in_bst_channel(uniqueChannelFiles{iChannelFile});
+ channelNamesCond = {channelMatTest.Channel.Name};
+ if ~isequal(channelNamesRef, channelNamesCond)
+ bst_report('Error', sProcess, [], 'Input files have different lists of channels.');
+ return
+ end
+ end
+ end
% Summarize trials and conditions to process
fprintf('BST> Found %d different conditions across %d trials:%c', numConditions, length(sInputs), char(10));
@@ -150,7 +174,15 @@
end
% Load trials
- [trial,Time] = load_trials_bs(sInputs, LowPass, SensorTypes);
+ [trial, Time, iChannels, errMsg] = load_trials_bs(sInputs, LowPass, SensorTypes, IgnoreBad);
+ if ~isempty(errMsg)
+ bst_report('Error', sProcess, [], errMsg);
+ return
+ end
+ varNames = {channelMatRef.Channel(iChannels).Name};
+ strVars = cellfun(@(x) ['"', x, '", '], varNames, 'UniformOutput', false);
+ strVars = [strVars{:}];
+ strVars = regexprep(strVars, ', $', '');
% Run SVM decoding
if strcmpi(model, 'maxcorr')
% Run max-correlation decoding
@@ -185,6 +217,7 @@
FileMat.Description = Description;
FileMat.Time = Time;
FileMat.CondLabels = uniqueConditions;
+ FileMat = bst_history('add', FileMat, 'SVM', ['Channels: ' strVars]);
% ===== OUTPUT CONDITION =====
% Default condition name
@@ -215,8 +248,10 @@
% - sInputs : Trial files to load
% - SensorTypes : List of channel types or names separated with commas
% - LowPass : Low pass frequency for data filtering
+% - IgnoreBad : 1 = bad channels are ignored, 0 = use all channels
% Authors: Dimitrios Pantazis, Seyed-Mahdi Khaligh-Razavi, Martin Cousineau
-function [trial, Time] = load_trials_bs(sInputs, LowPass, SensorTypes)
+function [trial, Time, iChannels, errMsg] = load_trials_bs(sInputs, LowPass, SensorTypes, IgnoreBad)
+ errMsg = [];
% Load channel file
ChannelMat = in_bst_channel(sInputs(1).ChannelFile);
% Parse inputs
@@ -232,16 +267,25 @@
% Make sure channels are unique and sorted
iChannels = unique(iChannels);
end
+ if nargin < 4 || isempty(IgnoreBad)
+ % Use all channels
+ IgnoreBad = 0;
+ end
% Initialize output matrix (numChannels x numSamples x numObservations)
nInputs = length(sInputs);
DataMat = in_bst_data(sInputs(1).FileName);
+ if IgnoreBad
+ channelFlagRef = (DataMat.ChannelFlag == 1);
+ iChannels = iChannels(channelFlagRef);
+ end
trial = zeros(length(iChannels), length(DataMat.Time), nInputs);
+ Time = DataMat.Time;
% Low-pass filtering
if LowPass > 0
% Design low pass filter
- order = max(100,round(size(DataMat.F(iChannels,:),2)/10)); %keep one 10th of the timepoints as model order
+ order = max(100,round(size(DataMat.F,2)/10)); %keep one 10th of the timepoints as model order
Fs = 1 ./ (DataMat.Time(2) - DataMat.Time(1));
h = filter_design('lowpass', LowPass, order, Fs, 0);
end
@@ -251,6 +295,14 @@
for f = 1:nInputs
bst_progress('inc',1);
DataMat = in_bst_data(sInputs(f).FileName);
+ % Check for same channel number
+ if IgnoreBad && (f > 1)
+ % Check channel flag for this data
+ if ~isequal(channelFlagRef, (DataMat.ChannelFlag == 1))
+ errMsg = 'Input files have different lists of bad channels.';
+ return
+ end
+ end
if LowPass > 0 % do low-pass filtering
trial(:,:,f) = filter_apply(DataMat.F(iChannels,:),h); %smooth over time
@@ -259,7 +311,6 @@
end
end
bst_progress('stop');
- Time = DataMat.Time;
end
diff --git a/toolbox/process/functions/process_detectbad.m b/toolbox/process/functions/process_detectbad.m
index e23a60c55..9076e790f 100644
--- a/toolbox/process/functions/process_detectbad.m
+++ b/toolbox/process/functions/process_detectbad.m
@@ -20,6 +20,7 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2010-2021
+% Raymundo Cassani, 2024
eval(macro_method);
end
@@ -32,10 +33,10 @@
sProcess.Category = 'Custom';
sProcess.SubGroup = 'Artifacts';
sProcess.Index = 115;
- sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/MedianNerveCtf#Review_the_individual_trials';
+ sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/ArtifactsDetect#Other_detection_processes';
% Definition of the input accepted by this process
- sProcess.InputTypes = {'data'};
- sProcess.OutputTypes = {'data'};
+ sProcess.InputTypes = {'raw', 'data'};
+ sProcess.OutputTypes = {'raw', 'data'};
sProcess.nInputs = 1;
sProcess.nMinFiles = 1;
@@ -79,9 +80,15 @@
sProcess.options.comment1.Comment = [' - First column: Signal detection threshold (peak-to-peak) ' 10 ...
' - Second column: Signal rejection threshold (peak-to-peak)'];
sProcess.options.comment1.Type = 'label';
- % Reject entire trial
+ % Separator
sProcess.options.sep2.Type = 'separator';
- sProcess.options.rejectmode.Comment = {'Reject only the bad channels', 'Reject the entire trial'};
+ % Option: Window length
+ sProcess.options.win_length.Comment = 'Length of analysis window: ';
+ sProcess.options.win_length.Type = 'value';
+ sProcess.options.win_length.Value = {1, 's', []};
+ sProcess.options.win_length.InputTypes = {'raw'};
+ % Reject entire trial
+ sProcess.options.rejectmode.Comment = {'Reject only the bad channels', 'Reject the entire segments/trials (all channels)'};
sProcess.options.rejectmode.Type = 'radio';
sProcess.options.rejectmode.Value = 2;
end
@@ -102,7 +109,7 @@
if (sProcess.options.rejectmode.Value == 1)
Comment = 'Detect bad channels: Peak-to-peak ';
else
- Comment = 'Detect bad trials: Peak-to-peak ';
+ Comment = 'Detect bad segments/trials: Peak-to-peak ';
end
% What are the criteria
for critName = {'meggrad', 'megmag', 'eeg', 'eog', 'ecg'}
@@ -142,36 +149,39 @@
OutputFiles = [];
return;
end
+ % Check: Same FileType for all files
+ is_raw = strcmp({sInputs.FileType},'raw');
+ if ~all(is_raw) && ~all(~is_raw)
+ bst_error('Please do not mix continous (raw) and imported data', 'Detect bad segments', 0);
+ return;
+ end
% Reject entire trial
isRejectTrial = (sProcess.options.rejectmode.Value == 2);
-
+ % If raw file, get window length
+ if is_raw(1) && isfield(sProcess.options, 'win_length') && ~isempty(sProcess.options.win_length) && ~isempty(sProcess.options.win_length.Value) && iscell(sProcess.options.win_length.Value)
+ winLength = sProcess.options.win_length.Value{1};
+ end
+
% Initializations
- iBadTrials = [];
+ iBadTrials = []; % Bad trials for imported files
progressPos = bst_progress('get');
prevChannelFile = '';
% ===== LOOP ON FILES =====
for iFile = 1:length(sInputs)
- % === LOAD ALL DATA ===
+ % === LOAD FILE ===
% Progress bar
bst_progress('set', progressPos + round(iFile / length(sInputs) * 100));
- % Get file in database
- [sStudy, iStudy, iData] = bst_get('DataFile', sInputs(iFile).FileName);
% Load channel file (if not already loaded
ChannelFile = sInputs(iFile).ChannelFile;
if isempty(prevChannelFile) || ~strcmpi(ChannelFile, prevChannelFile)
prevChannelFile = ChannelFile;
ChannelMat = in_bst_channel(ChannelFile);
end
- % Get modalities
- Modalities = unique({ChannelMat.Channel.Type});
% Load file
DataMat = in_bst_data(sInputs(iFile).FileName, 'F', 'ChannelFlag', 'History', 'Time');
- % Scale gradiometers / magnetometers:
- % - Neuromag: Apply axial factor to MEG GRAD sensors, to convert in fT/cm
- % - CTF: Apply factor to MEG REF gradiometers
- DataMat.F = bst_scale_gradmag(DataMat.F, ChannelMat.Channel);
- % Get time window
+ nChannels = length(DataMat.ChannelFlag);
+ % Get sample bounds for time window
if ~isempty(TimeWindow)
iTime = bst_closest(sProcess.options.timewindow.Value{1}, DataMat.Time);
if (iTime(1) == iTime(2)) && any(iTime(1) == DataMat.Time)
@@ -183,74 +193,141 @@
else
iTime = 1:length(DataMat.Time);
end
- % List of bad channels for this file
- iBadChan = [];
- % === LOOP ON MODALITIES ===
- for iMod = 1:length(Modalities)
- % === GET REJECTION CRITERIA ===
- % Get threshold according to the modality
- if ismember(Modalities{iMod}, {'MEG', 'MEG GRAD'})
- Threshold = Criteria.meggrad;
- elseif strcmpi(Modalities{iMod}, 'MEG MAG')
- Threshold = Criteria.megmag;
- elseif strcmpi(Modalities{iMod}, 'EEG')
- Threshold = Criteria.eeg;
- elseif ismember(Modalities{iMod}, {'SEEG', 'EOCG'})
- Threshold = Criteria.ieeg;
- elseif ~isempty(strfind(lower(Modalities{iMod}), 'eog'))
- Threshold = Criteria.eog;
- elseif ~isempty(strfind(lower(Modalities{iMod}), 'ecg')) || ~isempty(strfind(lower(Modalities{iMod}), 'ekg'))
- Threshold = Criteria.ecg;
- else
- continue;
- end
- % If threshold is [0 0]: nothing to do
- if isequal(Threshold, [0 0])
- continue;
+ % ===== DETECT BAD SEGMENTS =====
+ % Process raw (continuous) data file in blocks
+ if strcmp(sInputs(iFile).FileType, 'raw')
+ % Get maximum size of a data block
+ ProcessOptions = bst_get('ProcessOptions');
+ blockLengthSamples = max(floor(ProcessOptions.MaxBlockSize / nChannels), 1);
+ % Sampling frequency
+ fs = 1 ./ (DataMat.Time(2) - DataMat.Time(1));
+ % Length of the analysis window in samples
+ winLengthSamples = round(fs*winLength);
+ % Block length as multiple of the length of the analysis window
+ blockLengthSamples = winLengthSamples * floor(blockLengthSamples / winLengthSamples);
+ % List of bad events for this file
+ sBadEvents = repmat(db_template('event'), 0);
+ % Indices for each block
+ [~, iTimesBlocks, R] = bst_epoching(iTime, blockLengthSamples);
+ if ~isempty(R)
+ if ~isempty(iTimesBlocks)
+ lastTime = iTimesBlocks(end, 2);
+ else
+ lastTime = 0;
+ end
+ % Add the times for the remaining block
+ iTimesBlocks = [iTimesBlocks; lastTime+1, lastTime+size(R,2)];
end
-
- % === DETECT BAD CHANNELS ===
- % Get channels for this modality
- iChan = good_channel(ChannelMat.Channel, DataMat.ChannelFlag, Modalities{iMod});
- % Get data to test
- DataToTest = DataMat.F(iChan, iTime);
- % Compute peak-to-peak values for all the sensors
- p2p = (max(DataToTest,[],2) - min(DataToTest,[],2));
- % Get bad channels
- iBadChanMod = find((p2p < Threshold(1)) | (p2p > Threshold(2)));
- % If some bad channels were detected
- if ~isempty(iBadChanMod)
- % Convert indices back into the intial Channels structure
- iBadChan = [iBadChan, iChan(iBadChanMod)];
+ for iBlock = 1 : size(iTimesBlocks, 1)
+ blockTimeBounds = DataMat.Time(iTimesBlocks(iBlock, :));
+ % Load data from link to raw data
+ RawDataMat = in_bst(sInputs(iFile).FileName, blockTimeBounds, 1, 0, 'no');
+ % Scale gradiometers / magnetometers:
+ % - Neuromag: Apply axial factor to MEG GRAD sensors, to convert in fT/cm
+ % - CTF: Apply factor to MEG REF gradiometers
+ RawDataMat.F = bst_scale_gradmag(RawDataMat.F, ChannelMat.Channel);
+ [~, iTimesSegments, R] = bst_epoching(RawDataMat.F, winLengthSamples);
+ if ~isempty(R)
+ if ~isempty(iTimesSegments)
+ lastTime = iTimesSegments(end, 2);
+ else
+ lastTime = 0;
+ end
+ % Add the times for the remaining
+ iTimesSegments = [iTimesSegments; lastTime+1, lastTime+size(R,2)];
+ end
+ % Detect bad segments
+ for iSegment = 1 : size(iTimesSegments, 1)
+ iTimesSegment = iTimesSegments(iSegment, :);
+ [iBadChannels, criteriaModalities] = Thresholding(RawDataMat.F(:,iTimesSegment(1):iTimesSegment(2)), RawDataMat.ChannelFlag, ChannelMat, Criteria);
+ % Create one bad event for each channel-segment
+ for ix = 1 : length(iBadChannels)
+ % Create bad event
+ sBadEvent = db_template('event');
+ sBadEvent.label = sprintf('BAD_detectbad_%s_block_%04d_segment_%04d_channel_%04d', criteriaModalities{ix}, iBlock, iSegment, iBadChannels(ix));
+ sBadEvent.times = RawDataMat.Time(iTimesSegment)';
+ sBadEvent.epochs = 1;
+ sBadEvent.channels = {{ChannelMat.Channel(iBadChannels(ix)).Name}};
+ sBadEvent.notes = [];
+ % Add to events structure
+ sBadEvents(end+1) = sBadEvent;
+ end
+ end
end
- end
-
- % === TAG FILE ===
- if ~isempty(iBadChan)
- % Reject entire trial
+ % If reject trial, ignore 'channels' field
if isRejectTrial
- % Mark trial as bad
- iBadTrials(end+1) = iFile;
- % Report
- bst_report('Info', sProcess, sInputs(iFile), 'Marked as bad trial.');
- % Update study
- sStudy.Data(iData).BadTrial = 1;
- bst_set('Study', iStudy, sStudy);
- % Reject channels only
- else
- % Add detected channels to list of file bad channels
- s.ChannelFlag = DataMat.ChannelFlag;
- s.ChannelFlag(iBadChan) = -1;
- % History
- DataMat = bst_history('add', DataMat, 'detect', [FormatComment(sProcess) ' => Detected bad channels:' sprintf(' %d', iBadChan)]);
- s.History = DataMat.History;
- bst_report('Info', sProcess, sInputs(iFile), ['Bad channels: ' sprintf(' %d', iBadChan)]);
- % Save file
- bst_save(file_fullpath(sInputs(iFile).FileName), s, 'v6', 1);
+ % Remove channel information
+ [sBadEvents.channels] = deal([]);
+ end
+ % Merge all bad events by modality
+ badEventModNames = cellfun(@(x) regexp(x, '^BAD_detectbad_[^\W_]+', 'match'), {sBadEvents.label});
+ [badEventModNamesUnique, ~, ic] = unique(badEventModNames);
+ for iMod = 1 : length(badEventModNamesUnique)
+ % If only one event for modality
+ if sum(ic == iMod) == 1
+ sBadEvents = [sBadEvents, sBadEvents(ic == iMod)];
+ sBadEvents(end).label = badEventModNamesUnique{iMod};
+ else
+ sBadEvents = process_evt_merge('Compute', '', sBadEvents, {sBadEvents(ic == iMod).label}, badEventModNamesUnique{iMod}, 0);
+ end
+ end
+ % Remove bad events that were merged
+ sBadEvents(1:length(ic)) = [];
+ % Combine bad channels for same bad segment
+ if ~isRejectTrial
+ for iBadEvent = 1 : length(sBadEvents)
+ % Combine channels for each unique window
+ [~, ics, ias] = unique(bst_round(sBadEvents(iBadEvent).times', 9), 'rows', 'stable');
+ for iw = 1 : length(ics)
+ % Combine channels
+ sBadEvents(iBadEvent).channels{ics(iw)} = [sBadEvents(iBadEvent).channels{ias == iw}];
+ end
+ % Delete all but unique windows
+ sBadEvents(iBadEvent).times = sBadEvents(iBadEvent).times(:, ics);
+ sBadEvents(iBadEvent).epochs = sBadEvents(iBadEvent).epochs(:, ics);
+ sBadEvents(iBadEvent).channels = sBadEvents(iBadEvent).channels(:, ics);
+ end
+ end
+ % Append bad events to original events in file
+ DataMat.F.events = [DataMat.F.events, sBadEvents];
+ % Save bad events
+ bst_save(file_fullpath(sInputs(iFile).FileName), DataMat, 'v6', 1);
+
+ % Process imported data file
+ else
+ % File is already loaded
+ % Scale gradiometers / magnetometers:
+ % - Neuromag: Apply axial factor to MEG GRAD sensors, to convert in fT/cm
+ % - CTF: Apply factor to MEG REF gradiometers
+ DataMat.F = bst_scale_gradmag(DataMat.F, ChannelMat.Channel);
+ % List of bad channels for this file
+ iBadChan = Thresholding(DataMat.F(:,iTime), DataMat.ChannelFlag, ChannelMat, Criteria);
+
+ % === TAG FILE ===
+ if ~isempty(iBadChan)
+ % Reject entire trial
+ if isRejectTrial
+ % Mark trial as bad
+ iBadTrials(end+1) = iFile;
+ % Report
+ bst_report('Info', sProcess, sInputs(iFile), 'Marked as bad trial.');
+ % Reject channels only
+ else
+ % Add detected channels to list of file bad channels
+ s.ChannelFlag = DataMat.ChannelFlag;
+ s.ChannelFlag(iBadChan) = -1;
+ % History
+ DataMat = bst_history('add', DataMat, 'detect', [FormatComment(sProcess) ' => Detected bad channels:' sprintf(' %d', iBadChan)]);
+ s.History = DataMat.History;
+ bst_report('Info', sProcess, sInputs(iFile), ['Bad channels: ' sprintf(' %d', iBadChan)]);
+ % Save file
+ bst_save(file_fullpath(sInputs(iFile).FileName), s, 'v6', 1);
+ end
end
end
end
+
% Record bad trials in study
if ~isempty(iBadTrials)
SetTrialStatus({sInputs(iBadTrials).FileName}, 1);
@@ -262,6 +339,69 @@
end
+%% ===== THRESHOLDING =====
+% USAGE: iBadChannel = Thresholding(Data, ChannelFile, Criteria)
+
+function [iBadChannel, criteriaModalities] = Thresholding(F, ChannelFlag, ChannelFile, Criteria)
+ % CALL: Thresholding(FileName, ChannelFile, ...)
+ if ischar(ChannelFile)
+ ChannelMat = in_bst_channel(ChannelFile);
+ % CALL: Thresholding(FileName, ChannelMat, ...)
+ else
+ ChannelMat = ChannelFile;
+ end
+ % Get modalities
+ Modalities = unique({ChannelMat.Channel.Type});
+
+ % List of bad channels
+ iBadChannel = [];
+ % List of modality criteria used for each bad channel
+ criteriaModalities = {};
+
+ % === LOOP ON MODALITIES ===
+ for iMod = 1:length(Modalities)
+ % === GET REJECTION CRITERIA ===
+ % Get threshold according to the modality
+ if ismember(Modalities{iMod}, {'MEG', 'MEG GRAD'})
+ criteriaField = 'meggrad';
+ elseif strcmpi(Modalities{iMod}, 'MEG MAG')
+ criteriaField = 'megmag';
+ elseif strcmpi(Modalities{iMod}, 'EEG')
+ criteriaField = 'eeg';
+ elseif ismember(Modalities{iMod}, {'SEEG', 'EOCG'})
+ criteriaField = 'ieeg';
+ elseif ~isempty(strfind(lower(Modalities{iMod}), 'eog'))
+ criteriaField = 'eog';
+ elseif ~isempty(strfind(lower(Modalities{iMod}), 'ecg')) || ~isempty(strfind(lower(Modalities{iMod}), 'ekg'))
+ criteriaField = 'ecg';
+ else
+ return;
+ end
+ Threshold = Criteria.(criteriaField);
+ % If threshold is [0 0]: nothing to do
+ if isequal(Threshold, [0 0])
+ return;
+ end
+
+ % === DETECT BAD CHANNELS ===
+ % Get channels for this modality
+ iChan = good_channel(ChannelMat.Channel, ChannelFlag, Modalities{iMod});
+ % Get data to test
+ DataToTest = F(iChan, :);
+ % Compute peak-to-peak values for all the sensors
+ p2p = (max(DataToTest,[],2) - min(DataToTest,[],2));
+ % Get bad channels
+ iBadChanMod = find((p2p < Threshold(1)) | (p2p > Threshold(2)));
+ % If some bad channels were detected
+ if ~isempty(iBadChanMod)
+ % Convert indices back into the intial Channels structure
+ iBadChannel = [iBadChannel, iChan(iBadChanMod)];
+ criteriaModalities = [criteriaModalities, repmat({criteriaField}, 1, length(iBadChanMod))];
+ end
+ end
+end
+
+
%% ===== SET STUDY BAD TRIALS =====
% USAGE: SetTrialStatus(FileNames, isBad)
% SetTrialStatus(FileName, isBad)
diff --git a/toolbox/process/functions/process_detectbad_mad.m b/toolbox/process/functions/process_detectbad_mad.m
new file mode 100644
index 000000000..556871e22
--- /dev/null
+++ b/toolbox/process/functions/process_detectbad_mad.m
@@ -0,0 +1,419 @@
+function varargout = process_detectbad_mad( varargin )
+% PROCESS_DETECTBAD_MAD: Detection based on +-n median absolute deviation from amplitude and gradient medians
+
+% @=============================================================================
+% 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: Raymundo Cassani, 2024
+% Alex Wiesman, 2024
+%
+% Process based on the AUTO and MANUAL methods from Trial Exclusion in ArtifactScanTool
+% https://github.com/nichrishayes/ArtifactScanTool
+
+eval(macro_method);
+end
+
+
+%% ===== GET DESCRIPTION =====
+function sProcess = GetDescription() %#ok
+ % Description the process
+ sProcess.Comment = 'Detect bad: amplitude and gradient thresholds';
+ sProcess.Category = 'Custom';
+ sProcess.SubGroup = 'Artifacts';
+ sProcess.Index = 116;
+ sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/ArtifactsDetect#Other_detection_processes';
+ % Definition of the input accepted by this process
+ sProcess.InputTypes = {'raw', 'data'};
+ sProcess.OutputTypes = {'raw', 'data'};
+ sProcess.nInputs = 1;
+ sProcess.nMinFiles = 1;
+
+ % Extra info
+ sProcess.options.info.Comment = ['Reject bad segments/trials on MEG recordings, based on: '...
+ '1. peak-to-peak amplitude, and/or ' ...
+ '2. numerical gradient values ' ...
+ 'outside of specified thresholds. ' ...
+ ' ', ...
+ ' '];
+ sProcess.options.info.Type = 'label';
+ % Time window
+ sProcess.options.timewindow.Comment = 'Time window:';
+ sProcess.options.timewindow.Type = 'timewindow';
+ sProcess.options.timewindow.Value = [];
+ % Option: Window length
+ sProcess.options.win_length.Comment = 'Length of analysis window: ';
+ sProcess.options.win_length.Type = 'value';
+ sProcess.options.win_length.Value = {1, 's', []};
+ sProcess.options.win_length.InputTypes = {'raw'};
+ % Warning on complete windows
+ sProcess.options.warning_raw.Comment = 'Only will complete windows be analyzed';
+ sProcess.options.warning_raw.Type = 'label';
+ sProcess.options.warning_raw.InputTypes = {'raw'};
+ % Separator
+ sProcess.options.sep1.Type = 'separator';
+ % Threshold method: Auto or Manual
+ sProcess.options.threshold_method.Comment = {'auto', 'manual', 'Threshold method: '; ...
+ 'auto', 'manual', ''};
+ sProcess.options.threshold_method.Type = 'radio_linelabel';
+ sProcess.options.threshold_method.Value = 'auto';
+ sProcess.options.threshold_method.Controller = struct('auto', 'auto', 'manual', 'manual');
+ % AUTO
+ % Option: Number of mad for amplitude and grandient
+ sProcess.options.n_mad.Comment = 'Number of median absolute deviations for thresholds:';
+ sProcess.options.n_mad.Type = 'value';
+ sProcess.options.n_mad.Value = {3, 'mad', []};
+ sProcess.options.n_mad.Class = 'auto';
+ % MANUAL
+ % Option: Threshold p2p amplitude
+ sProcess.options.threshold_p2p.Comment = 'Threshold peak-to-peak amplitude: ';
+ sProcess.options.threshold_p2p.Type = 'value';
+ sProcess.options.threshold_p2p.Value = {0, 'fT', 2};
+ sProcess.options.threshold_p2p.Class = 'manual';
+ % Option: Threshold gradiente
+ sProcess.options.threshold_grad.Comment = 'Threshold gradient: ';
+ sProcess.options.threshold_grad.Type = 'value';
+ sProcess.options.threshold_grad.Value = {0, 'fT/s', 2};
+ sProcess.options.threshold_grad.Class = 'manual';
+ % Separator
+ sProcess.options.sep2.Type = 'separator';
+ % Option: Ignore sign for gradient
+ sProcess.options.abs_gradient.Comment = 'Use absolute gradient: ';
+ sProcess.options.abs_gradient.Type = 'checkbox';
+ sProcess.options.abs_gradient.Value = 1;
+end
+
+
+%% ===== FORMAT COMMENT =====
+function Comment = FormatComment(sProcess)
+ Comment = sProcess.Comment;
+end
+
+
+%% ===== RUN =====
+function OutputFiles = Run(sProcess, sInputsAll) %#ok
+ % ===== GET OPTIONS =====
+ % Get time window
+ if isfield(sProcess.options, 'timewindow') && isfield(sProcess.options.timewindow, 'Value') && iscell(sProcess.options.timewindow.Value) && ~isempty(sProcess.options.timewindow.Value)
+ TimeWindow = sProcess.options.timewindow.Value{1};
+ else
+ TimeWindow = [];
+ end
+ % Check: Same FileType for all files
+ is_raw = strcmp({sInputsAll.FileType},'raw');
+ if ~all(is_raw) && ~all(~is_raw)
+ bst_error('Please do not mix continous (raw) and imported data', 'Detect bad segments', 0);
+ return;
+ end
+ % If raw file, get window length
+ if is_raw(1) && isfield(sProcess.options, 'win_length') && ~isempty(sProcess.options.win_length) && ~isempty(sProcess.options.win_length.Value) && iscell(sProcess.options.win_length.Value)
+ winLength = sProcess.options.win_length.Value{1};
+ end
+ % Number of mad
+ nMad = sProcess.options.n_mad.Value{1};
+ if nMad <= 0
+ bst_error('Number of MAD must be greater than 0', 'Detect bad segments', 0);
+ return;
+ end
+ abs_gradient = sProcess.options.abs_gradient.Value;
+
+ % Threshold method
+ thMethod = sProcess.options.threshold_method.Value;
+ switch thMethod
+ case 'auto'
+ isThAuto = 1;
+
+ case 'manual'
+ isThAuto = 0;
+ threshold_p2p = sProcess.options.threshold_p2p.Value{1} * 1e-15;
+ threshold_gradient = sProcess.options.threshold_grad.Value{1} * 1e-15;
+ if threshold_p2p <= 0 || threshold_gradient <=0
+ bst_error('Thresholds cannot be smaller than zero', 'Detect bad segments', 0);
+ return;
+ end
+
+ otherwise
+ bst_error(sprintf('Threshold method "%s" is not supported', thMethod), 'Detect bad segments', 0);
+ return;
+ end
+
+ % Group files by Study
+ [iGroups, ~, StudyPaths] = process_average('SortFiles', sInputsAll, 3);
+
+ % Get current progressbar position
+ progressPos = bst_progress('get');
+
+ OutputFiles = {};
+ % ===== LOOP ON STUDIES =====
+ for ix = 1 : length(StudyPaths)
+ bst_progress('set', progressPos + round(ix / length(StudyPaths) * 100));
+ % Find Study in database
+ [sStudy, iStudy] = bst_get('StudyWithCondition', StudyPaths{ix});
+ % Find Subject with Study
+ sSubject = bst_get('Subject', sStudy.BrainStormSubject);
+ sInputs = sInputsAll(iGroups{ix});
+ % Load channel file
+ sChannel = bst_get('ChannelForStudy', iStudy);
+ ChannelMat = in_bst_channel(sChannel.FileName);
+ % Get MEG channels
+ Modalities = channel_get_modalities(ChannelMat.Channel);
+ if ~any(ismember(Modalities, {'MEG', 'MEG GRAD', 'MEG MAG'}))
+ bst_error(sprintf('Channel files "%s" does not contain MEG sensors', sChannel.FileName), 'Detect bad segments', 0);
+ return;
+ end
+
+ % === PROCESS RAW DATA ===
+ if is_raw
+ if length(sInputs) > 1
+ bst_error(sprintf('Study "%s" has more than one continous (raw) data', sStudy.Condition), 'Detect bad segments', 0);
+ return;
+ end
+ % === LOAD FILE ===
+ % Load data file
+ DataMat = in_bst_data(sInputs(1).FileName, 'F', 'ChannelFlag', 'History', 'Time');
+ % Get sample bounds for time window
+ if ~isempty(TimeWindow)
+ iTime = bst_closest(sProcess.options.timewindow.Value{1}, DataMat.Time);
+ if (iTime(1) == iTime(2)) && any(iTime(1) == DataMat.Time)
+ bst_report('Error', sProcess, sInputs(1), 'Invalid time definition.');
+ OutputFiles = [];
+ return;
+ end
+ iTime = iTime(1):iTime(2);
+ else
+ iTime = 1:length(DataMat.Time);
+ end
+ % Sampling frequency
+ fs = 1 ./ (DataMat.Time(2) - DataMat.Time(1));
+ % MEG channels (includes 'MEG GRAD' and 'MEG MAG')
+ iMegChannels = good_channel(ChannelMat.Channel, DataMat.ChannelFlag, 'MEG');
+ nChannels = length(iMegChannels);
+ % Get maximum size of a data block
+ ProcessOptions = bst_get('ProcessOptions');
+ blockLengthSamples = max(floor(ProcessOptions.MaxBlockSize / nChannels), 1);
+ % Length of window of analysis in samples
+ winLengthSamples = round(fs*winLength);
+ % Block length as multiple of the length of the analysis window
+ blockLengthSamples = winLengthSamples * floor(blockLengthSamples / winLengthSamples);
+ if blockLengthSamples == 0
+ return
+ end
+ % List of bad events for this file
+ sBadEvents = repmat(db_template('event'), 0);
+ % Indices for each block
+ [~, iTimesBlocks, R] = bst_epoching(iTime, blockLengthSamples);
+ nWindows = size(iTimesBlocks, 1) * floor(blockLengthSamples / winLengthSamples);
+ if ~isempty(R)
+ if ~isempty(iTimesBlocks)
+ lastTime = iTimesBlocks(end, 2);
+ else
+ lastTime = 0;
+ end
+ % Add the times for the remaining block
+ iTimesBlocks = [iTimesBlocks; lastTime+1, lastTime+size(R,2)];
+ nWindows = nWindows + floor(size(R,2) / winLengthSamples);
+ end
+ % Maximum Peak-2-peak per window
+ max_p2p = zeros(nWindows, 1);
+ % Maximum Gradient per window
+ max_gradient = zeros(nWindows, 1);
+ iWindow = 1;
+ iTimesWindows = [];
+ for iBlock = 1 : size(iTimesBlocks, 1)
+ blockTimeBounds = DataMat.Time(iTimesBlocks(iBlock, :));
+ % Load data from link to raw data
+ RawDataMat = in_bst(sInputs(1).FileName, blockTimeBounds, 1, 0, 'no');
+ % Scale gradiometers / magnetometers:
+ % - Neuromag: Apply axial factor to MEG GRAD sensors, to convert in fT/cm
+ % - CTF: Apply factor to MEG REF gradiometers
+ RawDataMat.F = bst_scale_gradmag(RawDataMat.F, ChannelMat.Channel);
+ [~, iTimesSegments] = bst_epoching(RawDataMat.F, winLengthSamples);
+ iTimesWindows = [iTimesWindows; iTimesSegments + (iBlock-1)*blockLengthSamples];
+ % Process each segment
+ for iSegment = 1 : size(iTimesSegments, 1)
+ iTimesSegment = iTimesSegments(iSegment, :);
+ % Compute metrics
+ max_p2p(iWindow) = max(max(RawDataMat.F(iMegChannels, iTimesSegment(1):iTimesSegment(2)), [], 2) - ...
+ min(RawDataMat.F(iMegChannels, iTimesSegment(1):iTimesSegment(2)), [], 2), [], 1);
+ if abs_gradient
+ max_gradient(iWindow) = max(max(abs(gradient(RawDataMat.F(iMegChannels, iTimesSegment(1):iTimesSegment(2)), 1./fs)), [], 2), [], 1);
+ else
+ max_gradient(iWindow) = max(max(gradient(RawDataMat.F(iMegChannels, iTimesSegment(1):iTimesSegment(2)), 1./fs), [], 2), [], 1);
+ end
+ iWindow = iWindow + 1;
+ end
+ end
+ if isThAuto
+ % Compute thresholds
+ threshold_p2p = median(max_p2p) + (nMad * mad(max_p2p,1));
+ threshold_gradient = median(max_gradient) + (nMad * mad(max_gradient,1));
+ end
+ % Create one bad event for each window over any threshold
+ iBadWindows = [];
+ for iWindow = 1 : nWindows
+ % Criteria
+ if max_p2p(iWindow) > threshold_p2p && max_gradient(iWindow) > threshold_gradient
+ criteria_str = 'p2p_gradient';
+ elseif max_p2p(iWindow) > threshold_p2p
+ criteria_str = 'p2p';
+ elseif max_gradient(iWindow) > threshold_gradient
+ criteria_str = 'gradient';
+ else
+ continue
+ end
+ iBadWindows(end+1) = iWindow;
+ % Create bad event
+ sBadEvent = db_template('event');
+ sBadEvent.label = sprintf('BAD_detectbad_mad_%s_window_%d', criteria_str, iWindow);
+ sBadEvent.times = DataMat.Time(iTimesWindows(iWindow,:))';
+ sBadEvent.epochs = 1;
+ sBadEvent.channels = [];
+ sBadEvent.notes = [];
+ % Add to events structure
+ sBadEvents(end+1) = sBadEvent;
+ end
+ % Merge all bad events by criteria
+ badEventCriteriaNames = cellfun(@(x) regexp(x, '^BAD_detectbad_mad_.*(?=_window)', 'match'), {sBadEvents.label});
+ [badEventCriteriaNamesUnique, ~, ic] = unique(badEventCriteriaNames);
+ for iCriteria = 1 : length(badEventCriteriaNamesUnique)
+ % If only one event for criteria
+ if sum(ic == iCriteria) == 1
+ sBadEvents = [sBadEvents, sBadEvents(ic == iCriteria)];
+ sBadEvents(end).label = badEventCriteriaNamesUnique{iCriteria};
+ else
+ sBadEvents = process_evt_merge('Compute', '', sBadEvents, {sBadEvents(ic == iCriteria).label}, badEventCriteriaNamesUnique{iCriteria}, 0);
+ end
+ end
+ % Remove bad events that were merged
+ sBadEvents(1:length(ic)) = [];
+ % Append bad events to original events in file
+ DataMat.F.events = [DataMat.F.events, sBadEvents];
+ % Save bad events
+ bst_save(file_fullpath(sInputs(1).FileName), DataMat, 'v6', 1);
+ % Report by Study
+ GenerateReportEntry(sProcess, sInputs(1), sSubject, sStudy, is_raw, ...
+ {max_p2p, max_gradient}, {threshold_p2p, threshold_gradient}, ...
+ nWindows, nWindows-length(iBadWindows));
+ % Return input file
+ OutputFiles = [OutputFiles, sInputs(1).FileName];
+
+
+ % === PROCESS IMPORTED DATA FILES ===
+ else
+ nFiles = length(sInputs);
+ % Maximum Peak-2-peak per trial
+ max_p2p = zeros(nFiles, 1);
+ % Maximum Gradient per trial
+ max_gradient = zeros(nFiles, 1);
+ for iFile = 1:length(sInputs)
+ % === LOAD FILE ===
+ % Load data file
+ DataMat = in_bst_data(sInputs(iFile).FileName, 'F', 'ChannelFlag', 'History', 'Time');
+ % Get sample bounds for time window
+ if ~isempty(TimeWindow)
+ iTime = bst_closest(sProcess.options.timewindow.Value{1}, DataMat.Time);
+ if (iTime(1) == iTime(2)) && any(iTime(1) == DataMat.Time)
+ bst_report('Error', sProcess, sInputs(iFile), 'Invalid time definition.');
+ OutputFiles = [];
+ return;
+ end
+ iTime = iTime(1):iTime(2);
+ else
+ iTime = 1:length(DataMat.Time);
+ end
+ % Sampling frequency
+ fs = 1 ./ (DataMat.Time(2) - DataMat.Time(1));
+ % MEG channels (includes 'MEG GRAD' and 'MEG MAG')
+ iMegChannels = good_channel(ChannelMat.Channel, DataMat.ChannelFlag, 'MEG');
+ % Scale gradiometers / magnetometers:
+ % - Neuromag: Apply axial factor to MEG GRAD sensors, to convert in fT/cm
+ % - CTF: Apply factor to MEG REF gradiometers
+ DataMat.F = bst_scale_gradmag(DataMat.F, ChannelMat.Channel);
+ % Compute metrics
+ max_p2p(iFile) = max(max(DataMat.F(iMegChannels, iTime), [], 2) - ...
+ min(DataMat.F(iMegChannels, iTime), [], 2), [], 1);
+ if abs_gradient
+ max_gradient(iFile) = max(max(abs(gradient(DataMat.F(iMegChannels, iTime), 1./fs)), [], 2), [], 1);
+ else
+ max_gradient(iFile) = max(max(gradient(DataMat.F(iMegChannels, iTime), 1./fs), [], 2), [], 1);
+ end
+ end
+ if isThAuto
+ % Compute thresholds
+ threshold_p2p = median(max_p2p) + (nMad * mad(max_p2p,1));
+ threshold_gradient = median(max_gradient) + (nMad * mad(max_gradient,1));
+ end
+ % Set as bad trials over any threshold
+ iBadTrials = [];
+ % === TAG FILE ===
+ for iFile = 1 : nFiles
+ % Criteria
+ if max_p2p(iFile) > threshold_p2p && max_gradient(iFile) > threshold_gradient
+ criteria_str = 'p2p_gradient';
+ elseif max_p2p(iFile) > threshold_p2p
+ criteria_str = 'p2p';
+ elseif max_gradient(iFile) > threshold_gradient
+ criteria_str = 'gradient';
+ else
+ continue
+ end
+ iBadTrials(end+1) = iFile;
+ % Add rejection criteria to history of file
+ DataMat = bst_history('add', DataMat, 'detect', [FormatComment(sProcess) ' => Detected bad with MAP due to: ' criteria_str]);
+ s.History = DataMat.History;
+ bst_save(file_fullpath(sInputs(iFile).FileName), s, 'v6', 1);
+ % Set bad in Study
+ process_detectbad('SetTrialStatus', {sInputs(iFile).FileName}, 1);
+ end
+ % Report by study
+ GenerateReportEntry(sProcess, sInputs(1), sSubject, sStudy, is_raw, ...
+ {max_p2p, max_gradient}, {threshold_p2p, threshold_gradient}, ...
+ nFiles, nFiles-length(iBadTrials));
+ % Return only good files
+ iGoodTrials = setdiff(1:length(sInputs), iBadTrials);
+ OutputFiles = [OutputFiles, sInputs(iGoodTrials).FileName];
+ end
+ end
+end
+
+
+function GenerateReportEntry(sProcess, sInput, sSubject, sStudy, is_raw, maxValues, thresholdValues, nTotal, nAccepted)
+ histLegends = {'Max P2P range', 'Max Gradient range'};
+ histColors = {'b', 'r'};
+ histUnits = {'fT', 'fT/s'};
+ itemLabel = 'windows';
+ if ~is_raw
+ itemLabel = 'files';
+ end
+ % Report thresholds and number of windows/files
+ bst_report('Info', sProcess, sInput, sprintf('Subject = %s, Study = %s, P2P threshold = %E %s, Gradient threshold %E %s, Total %s = %d, Acepted %s = %d', ...
+ sSubject.Name, sStudy.Name, ...
+ thresholdValues{1}*1e15, histUnits{1}, thresholdValues{2}*1e15, histUnits{2}, ...
+ itemLabel, nTotal, itemLabel, nAccepted));
+ % Report histograms
+ hFig = figure();
+ for iHist = 1 : length(maxValues)
+ ax = subplot(2,1,iHist);
+ histogram(ax, maxValues{iHist}*1e15,'BinWidth',(max(maxValues{iHist}) - min(maxValues{iHist}))*1e15/10, 'FaceColor', histColors{iHist});
+ line(ax, [thresholdValues{iHist}, thresholdValues{iHist}]*1e15, ylim, 'Color','black','LineStyle','--');
+ legend(ax, histLegends{iHist});
+ xlabel(ax, histUnits{iHist});
+ ylabel(ax, [itemLabel, ' count'])
+ end
+ bst_report('Snapshot', hFig, sInput, [sProcess.Comment, ':: Distributions P2P and Gradient']);
+ close(hFig);
+end
diff --git a/toolbox/process/functions/process_dwi2dti.m b/toolbox/process/functions/process_dwi2dti.m
index 419f64e40..7ff6bfecf 100644
--- a/toolbox/process/functions/process_dwi2dti.m
+++ b/toolbox/process/functions/process_dwi2dti.m
@@ -147,16 +147,47 @@
OutputFiles = {'import'};
end
-
-%% ===== COMPUTE DTI =====
-function [DtiFile, errMsg] = Compute(iSubject, T1BstFile, DwiFile, BvalFile, BvecFile)
- DtiFile = [];
- errMsg = '';
+%% ===== CHECK FOR BRAINSUITE INSTALLATION =====
+function [bdp_exe, errMsg] = CheckBrainSuiteInstall()
+ errMsg = [];
if ~ispc
bdp_exe = 'bdp.sh';
else
bdp_exe = 'bdp';
end
+
+ % ===== INSTALL BRAINSUITE =====
+ bst_progress('text', 'Testing BrainSuite installation...');
+ % Check BrainSuite installation
+ status = system([bdp_exe ' --version']);
+ if (status ~= 0)
+ % Get BrainSuite path from Brainstorm preferences
+ BsDir = bst_get('BrainSuiteDir');
+ BsBinDir = bst_fullfile(BsDir, 'bin');
+ BsBdpDir = bst_fullfile(BsDir, 'bdp');
+ % Add BrainSuite path to system path
+ if ~isempty(BsDir) && file_exist(BsBinDir) && file_exist(BsBdpDir)
+ disp(['BST> Adding to system path: ' BsBinDir]);
+ disp(['BST> Adding to system path: ' BsBdpDir]);
+ setenv('PATH', [getenv('PATH'), pathsep, BsBinDir, pathsep, BsBdpDir]);
+ % Check again
+ status = system([bdp_exe ' --version']);
+ end
+ % Brainsuite is not installed
+ if (status ~= 0)
+ errMsg = ['BrainSuite is not installed on your computer.' 10 ...
+ 'Download it from http://brainsuite.org and install it.' 10 ...
+ 'Then set its installation folder in the Brainstorm options (File > Edit preferences)'];
+ return
+ end
+ end
+end
+
+%% ===== COMPUTE DTI =====
+function [DtiFile, errMsg] = Compute(iSubject, T1BstFile, DwiFile, BvalFile, BvecFile)
+ DtiFile = [];
+ errMsg = '';
+
% ===== INPUTS =====
% Try to find the bval/bvec files in the same folder
[fPath, fBase, fExt] = bst_fileparts(DwiFile);
@@ -195,31 +226,8 @@
T1BstFile = sSubject.Anatomy(sSubject.iAnatomy).FileName;
end
- % ===== INSTALL BRAINSUITE =====
- bst_progress('text', 'Testing BrainSuite installation...');
- % Check BrainSuite installation
- status = system([bdp_exe ' --version']);
- if (status ~= 0)
- % Get BrainSuite path from Brainstorm preferences
- BsDir = bst_get('BrainSuiteDir');
- BsBinDir = bst_fullfile(BsDir, 'bin');
- BsBdpDir = bst_fullfile(BsDir, 'bdp');
- % Add BrainSuite path to system path
- if ~isempty(BsDir) && file_exist(BsBinDir) && file_exist(BsBdpDir)
- disp(['BST> Adding to system path: ' BsBinDir]);
- disp(['BST> Adding to system path: ' BsBdpDir]);
- setenv('PATH', [getenv('PATH'), pathsep, BsBinDir, pathsep, BsBdpDir]);
- % Check again
- status = system([bdp_exe ' --version']);
- end
- % Brainsuite is not installed
- if (status ~= 0)
- errMsg = ['BrainSuite is not installed on your computer.' 10 ...
- 'Download it from http://brainsuite.org and install it.' 10 ...
- 'Then set its installation folder in the Brainstorm options (File > Edit preferences)'];
- return
- end
- end
+ % ===== CHECK FOR BRAINSUITE INSTALLATION =====
+ [bdp_exe, errMsg] = CheckBrainSuiteInstall();
% ===== TEMPORARY FOLDER =====
bst_progress('text', 'Preparing temporary folder...');
diff --git a/toolbox/process/functions/process_eegref.m b/toolbox/process/functions/process_eegref.m
index 6bf7c733d..6b1d3e55c 100644
--- a/toolbox/process/functions/process_eegref.m
+++ b/toolbox/process/functions/process_eegref.m
@@ -153,11 +153,12 @@
proj.Components = W;
proj.CompMask = [];
proj.Status = 1;
- proj.SingVal = 'REF';
+ proj.SingVal = [];
+ proj.Method = 'REF';
% === SAVE PROJECTOR ===
% Check for existing re-referencing projector
- if ~isempty(ChannelMat.Projector) && any(cellfun(@(c)isequal(c,'REF'), {ChannelMat.Projector.SingVal}))
+ if ~isempty(ChannelMat.Projector) && any(cellfun(@(c)isequal(c,'REF'), {ChannelMat.Projector.Method}))
%bst_report('Warning', sProcess, [], 'There was already a re-referencing projector.');
disp('BST> EEGREF: There was already a re-referencing projector.');
end
diff --git a/toolbox/process/functions/process_evt_extended.m b/toolbox/process/functions/process_evt_extended.m
index 9118d5efe..e39dbc0f5 100644
--- a/toolbox/process/functions/process_evt_extended.m
+++ b/toolbox/process/functions/process_evt_extended.m
@@ -80,10 +80,12 @@
DataMat = in_bst_data(sInput.FileName, 'F');
sEvents = DataMat.F.events;
sFreq = DataMat.F.prop.sfreq;
+ FullTimeWindow = DataMat.F.prop.times;
else
DataMat = in_bst_data(sInput.FileName, 'Events', 'Time');
sEvents = DataMat.Events;
sFreq = 1 ./ (DataMat.Time(2) - DataMat.Time(1));
+ FullTimeWindow = [DataMat.Time(1), DataMat.Time(end)];
end
% If no markers are present in this file
if isempty(sEvents)
@@ -111,11 +113,9 @@
end
% ===== PROCESS EVENTS =====
- for i = 1:length(iEvtList)
- sEvents(iEvtList(i)).times = round([...
- sEvents(iEvtList(i)).times + TimeWindow(1);
- sEvents(iEvtList(i)).times + TimeWindow(2)] .* sFreq) ./ sFreq;
- end
+ sEventsMod = sEvents(iEvtList);
+ sEventsMod = Compute(sEventsMod, TimeWindow, FullTimeWindow, sFreq);
+ sEvents(iEvtList) = sEventsMod;
% ===== SAVE RESULT =====
% Report results
@@ -129,5 +129,11 @@
end
-
-
+%% ===== COMPUTE =====
+function sEvents = Compute(sEvents, EventTimeWindow, FullTimeWindow, sFreq)
+ for i = 1:length(sEvents)
+ sEvents(i).times = round([...
+ max(FullTimeWindow(1), sEvents(i).times(1,:) + EventTimeWindow(1));
+ min(FullTimeWindow(2), sEvents(i).times(1,:) + EventTimeWindow(2))] .* sFreq) ./ sFreq;
+ end
+end
\ No newline at end of file
diff --git a/toolbox/process/functions/process_evt_head_motion.m b/toolbox/process/functions/process_evt_head_motion.m
index 43a7d659c..137f7db44 100644
--- a/toolbox/process/functions/process_evt_head_motion.m
+++ b/toolbox/process/functions/process_evt_head_motion.m
@@ -323,9 +323,9 @@
nSamples = SamplesBounds(2) - SamplesBounds(1) + 1;
- iHLU = find(strcmp({ChannelMat.Channel.Type}, 'HLU'));
+ iHLU = find(strcmpi({ChannelMat.Channel.Type}, 'HLU'));
[Unused, iSortHlu] = sort({ChannelMat.Channel(iHLU).Name});
- iFitErr = find(strcmp({ChannelMat.Channel.Type}, 'FitErr'));
+ iFitErr = find(strcmpi({ChannelMat.Channel.Type}, 'FitErr'));
[Unused, iSortFitErr] = sort({ChannelMat.Channel(iFitErr).Name});
nChannels = numel(iHLU);
if nChannels == 0
@@ -467,8 +467,7 @@
if nargin < 3 || isempty(StopThreshold)
StopThreshold = false;
end
-
- if size(Locations, 1) ~= 9 || size(Reference, 1) ~= 9
+ if size(Locations, 1) ~= 9 %|| size(Reference, 1) ~= 9
bst_error('Expecting 9 HLU channels in first dimension.');
end
nS = size(Locations, 2);
@@ -476,15 +475,24 @@
% Calculate distances.
- Reference = reshape(Reference, [3, 3]);
- % Reference "head origin" and inverse "orientation matrix".
- [YO, YR] = RigidCoordinates(Reference);
- % Sphere radius.
- r = max( sqrt(sum((Reference - YO(:, [1, 1, 1])).^2, 1)) );
- if any(YR(:)) % any ignores NaN and returns false for empty.
- YI = inv(YR); % Faster to calculate inverse once here than "/" in loop.
+ if nargin < 2 || isempty(Reference)
+ % Assume reference defines coordinate system.
+ YO = zeros(3, 1);
+ YI = eye(3);
+ % Use first location to estimate sphere radius.
+ r = max(sqrt(sum(bsxfun(@minus, reshape(Locations(1:9), [3,3]), ...
+ (Locations(4:6) + Locations(7:9))/2).^2, 1)));
else
- YI = YR;
+ Reference = reshape(Reference, [3, 3]);
+ % Reference "head origin" and inverse "orientation matrix".
+ [YO, YR] = RigidCoordinates(Reference);
+ % Sphere radius, estimate from reference.
+ r = max( sqrt(sum((Reference - YO(:, [1, 1, 1])).^2, 1)) );
+ if any(YR(:)) % any ignores NaN and returns false for empty.
+ YI = inv(YR); % Faster to calculate inverse once here than "/" in loop.
+ else
+ YI = YR;
+ end
end
% SinHalf = zeros([nS, 1, nT]);
@@ -499,21 +507,8 @@
% it is a rotation around an axis through the real origin).
R = XR * YI; % %#ok
- % Sine of half the rotation angle.
- % SinHalf = sqrt(3 - trace(R)) / 2;
- % For very small angles, this formula is not accurate compared to
- % w, since diagonal elements are around 1, and eps(1) = 2.2e-16.
- % This will be the order of magnitude of non-diag. elements due to
- % errors. So we should get SinHalf from w.
- % Rotation axis with amplitude = SinHalf (like in rotation quaternions).
- w = [R(3, 2) - R(2, 3); R(1, 3) - R(3, 1); R(2, 1) - R(1, 2)] / ...
- (2 * sqrt(1 + R(1, 1) + R(2, 2) + R(3, 3)));
- SinHalf = sqrt(sum(w.^2));
- TNormSq = sum(T.^2);
- % Maximum sphere distance for translation + rotation, as described
- % above.
- D(s, t) = sqrt( TNormSq + (2 * r * SinHalf)^2 + ...
- 4 * r * sqrt(TNormSq * SinHalf^2 - (T' * w)^2) );
+ % Maximum sphere distance for translation + rotation, as described above.
+ D(s, t) = RigidDistTransform(R, T, r);
% CHECK should be comparable AND >= to max coil movement.
% Option to interrupt when past a distance threshold.
@@ -527,6 +522,28 @@
+function D = RigidDistTransform(R, T, rad)
+ % Maximum sphere distance for translation + rotation, as described above.
+ if isempty(T) && size(R,1) == 4
+ T = R(1:3, 4);
+ R = R(1:3, 1:3);
+ end
+ % Sine of half the rotation angle.
+ % SinHalf = sqrt(3 - trace(R)) / 2;
+ % For very small angles, this formula is not accurate compared to w, since diagonal
+ % elements are around 1, and eps(1) = 2.2e-16. This will be the order of magnitude of
+ % non-diag. elements due to errors. So we should get SinHalf from w.
+ % Rotation axis with amplitude = SinHalf (like in rotation quaternions).
+ w = [R(3, 2) - R(2, 3); R(1, 3) - R(3, 1); R(2, 1) - R(1, 2)] / ...
+ (2 * sqrt(1 + R(1, 1) + R(2, 2) + R(3, 3)));
+ SinHalf = sqrt(sum(w.^2));
+ TNormSq = sum(T.^2);
+
+ D = sqrt( TNormSq + (2 * rad * SinHalf)^2 + 4 * rad * sqrt(TNormSq * SinHalf^2 - (T' * w)^2) );
+end % RigidDistTransform
+
+
+
function [O, R] = RigidCoordinates(FidsColumns)
% Convert head coil locations to origin position and rotation matrix.
% Works with 9x1 or 3x3 (columns) input.
diff --git a/toolbox/process/functions/process_evt_merge.m b/toolbox/process/functions/process_evt_merge.m
index 084cfe51f..9036377f7 100644
--- a/toolbox/process/functions/process_evt_merge.m
+++ b/toolbox/process/functions/process_evt_merge.m
@@ -124,6 +124,9 @@
%% ===== MERGE EVENTS =====
function [events, isModified] = Compute(sInput, events, EvtNames, NewName, isDelete)
+ if isempty(sInput)
+ sInput = '';
+ end
% No modification
isModified = 0;
@@ -149,6 +152,13 @@
bst_report('Error', 'process_evt_merge', sInput, 'You must enter at least one event name to copy.');
return;
end
+ % Make sure selected events are all of same type
+ try
+ [events(iEvents).times];
+ catch
+ bst_report('Error', 'process_evt_merge', sInput, 'You cannot merge simple and extended events together.');
+ return;
+ end
% Inialize new event group
newEvent = events(iEvents(1));
@@ -156,23 +166,58 @@
newEvent.times = [events(iEvents).times];
newEvent.epochs = [events(iEvents).epochs];
% Reaction time, channels, notes: only if all the events have them
- if all(~cellfun(@isempty, {events(iEvents).channels}))
- newEvent.channels = [events(iEvents).channels];
- else
+ if all(cellfun(@isempty, {events(iEvents).channels}))
newEvent.channels = [];
- end
- if all(~cellfun(@isempty, {events(iEvents).notes}))
- newEvent.notes = [events(iEvents).notes];
else
- newEvent.notes = [];
+ % Expand empty channels if needed
+ for ie = 1 : length(iEvents)
+ if isempty(events(iEvents(ie)).channels)
+ events(iEvents(ie)).channels = cell(1, size(events(iEvents(ie)).times, 2));
+ end
+ end
+ newEvent.channels = [events(iEvents).channels];
end
- if all(~cellfun(@isempty, {events(iEvents).reactTimes}))
- newEvent.reactTimes = [events(iEvents).reactTimes];
+ if all(cellfun(@isempty, {events(iEvents).notes}))
+ newEvent.notes = [];
else
+ % Expand empty notes if needed
+ for ie = 1 : length(iEvents)
+ if isempty(events(iEvents(ie)).notes)
+ events(iEvents(ie)).notes = cell(1, size(events(iEvents(ie)).notes, 2));
+ end
+ end
+ newEvent.notes = [events(iEvents).notes];
+ end
+ if all(cellfun(@isempty, {events(iEvents).reactTimes}))
newEvent.reactTimes = [];
+ else
+ % Expand empty reactTimes if needed
+ for ie = 1 : length(iEvents)
+ if isempty(events(iEvents(ie)).reactTimes)
+ events(iEvents(ie)).reactTimes = zeros(1, size(events(iEvents(ie)).reactTimes, 2));
+ end
+ end
+ newEvent.reactTimes = [events(iEvents).reactTimes];
+ end
+ % Find duplicated events
+ iRemoveDuplicate = [];
+ [~, ics, ias] = unique(bst_round(newEvent.times', 9), 'rows', 'stable');
+ % Check if duplicated times are really duplicated events
+ for ix = 1 : length(ics)
+ ids = find(ias == ix);
+ for iy = 2 : length(ids)
+ id = ids(iy);
+ if (isempty(newEvent.channels) || isequal(newEvent.channels{ids(1)}, newEvent.channels{id})) && ...
+ (isempty(newEvent.notes) || isequal(newEvent.notes{ids(1)}, newEvent.notes{id})) && ...
+ (isempty(newEvent.reactTimes) || isequal(newEvent.reactTimes(ids(1)), newEvent(id).reactTimes))
+ iRemoveDuplicate = [iRemoveDuplicate, id];
+ end
+ end
end
- % Sort by samples indices, and remove redundant values
- [tmp__, iSort] = unique(bst_round(newEvent.times(1,:), 9));
+ % Sort by samples indices
+ [~, iSort] = sort(bst_round(newEvent.times(1,:), 9));
+ % Remove indices of duplicated events
+ iSort = iSort(~ismember(iSort, iRemoveDuplicate));
newEvent.times = newEvent.times(:,iSort);
newEvent.epochs = newEvent.epochs(iSort);
if ~isempty(newEvent.channels)
diff --git a/toolbox/process/functions/process_evt_read.m b/toolbox/process/functions/process_evt_read.m
index dfbde5517..745002994 100644
--- a/toolbox/process/functions/process_evt_read.m
+++ b/toolbox/process/functions/process_evt_read.m
@@ -48,9 +48,22 @@
sProcess.options.trackmode.Comment = {'Value: detect the changes of channel value', ...
'Bit: detect the changes for each bit independently', ...
'TTL: detect peaks of 5V/12V on an analog channel (baseline=0V)', ...
- 'RTTL: detect peaks of 0V on an analog channel (baseline!=0V)'};
- sProcess.options.trackmode.Type = 'radio';
- sProcess.options.trackmode.Value = 1;
+ 'RTTL: detect peaks of 0V on an analog channel (baseline!=0V)'; ...
+ 'value', 'bit', 'ttl', 'rttl'};
+ sProcess.options.trackmode.Type = 'radio_label';
+ sProcess.options.trackmode.Value = 'value';
+ sProcess.options.trackmode.Controller = struct('value', 'mask_check');
+ % Option: Mask boolean
+ sProcess.options.maskcheck.Comment = 'Apply digital mask to events (only for "Value" option)';
+ sProcess.options.maskcheck.Type = 'checkbox';
+ sProcess.options.maskcheck.Value = 0;
+ sProcess.options.maskcheck.Class = 'mask_check';
+ sProcess.options.maskcheck.Controller = 'mask_value';
+ % Option: Mask
+ sProcess.options.mask.Comment = 'Mask value (integer or 0xHEX): ';
+ sProcess.options.mask.Type = 'text';
+ sProcess.options.mask.Value = '0';
+ sProcess.options.mask.Class = 'mask_value';
% Option: Accept zeros
sProcess.options.zero.Comment = 'Accept zeros as trigger values';
sProcess.options.zero.Type = 'checkbox';
@@ -82,15 +95,31 @@
end
% Event type
switch (sProcess.options.trackmode.Value)
- case 1, EventsTrackMode = 'value';
- case 2, EventsTrackMode = 'bit';
- case 3, EventsTrackMode = 'ttl';
- case 4, EventsTrackMode = 'rttl';
+ case {1, 'value'}, EventsTrackMode = 'value';
+ case {2, 'bit'}, EventsTrackMode = 'bit';
+ case {3, 'ttl'}, EventsTrackMode = 'ttl';
+ case {4, 'rttl'}, EventsTrackMode = 'rttl';
end
+ sProcess.options.trackmode.Value = EventsTrackMode;
% Other options
isAcceptZero = sProcess.options.zero.Value;
MinDuration = sProcess.options.min_duration.Value{1};
-
+ % Get mask
+ MaskValue = 0;
+ if strcmpi(EventsTrackMode, 'value') && sProcess.options.maskcheck.Value
+ strMask = regexprep(sProcess.options.mask.Value, '^0[x|X]', '');
+ % Hex value was provided
+ if ~strcmpi(strMask, sProcess.options.mask.Value)
+ MaskValue = hex2dec(strMask);
+ % Integer string (only 0-9 characters)
+ elseif isempty(regexp(strMask, '[^0-9]', 'once'))
+ MaskValue = str2double(strMask);
+ % Invalid string
+ else
+ bst_report('Error', sProcess, sInput, ['String "' strMask '" is not a valid mask.']);
+ return
+ end
+ end
% ===== GET FILE DESCRIPTOR =====
% Load the raw file descriptor
isRaw = strcmpi(sInput.FileType, 'raw');
@@ -113,8 +142,8 @@
% CTF: Read separately upper and lower bytes
if ismember(sFile.format, {'CTF', 'CTF-CONTINUOUS'})
% Detect separately events on the upper and lower bytes of the STIM channel
- eventsU = Compute(sFile, ChannelMat, [StimChan '__U'], EventsTrackMode, isAcceptZero, MinDuration);
- eventsL = Compute(sFile, ChannelMat, [StimChan '__L'], EventsTrackMode, isAcceptZero, MinDuration);
+ eventsU = Compute(sFile, ChannelMat, [StimChan '__U'], EventsTrackMode, isAcceptZero, MinDuration, MaskValue);
+ eventsL = Compute(sFile, ChannelMat, [StimChan '__L'], EventsTrackMode, isAcceptZero, MinDuration, MaskValue);
% If there are events on both: add marker U/L
if ~isempty(eventsU) && ~isempty(eventsL)
for iEvt = 1:length(eventsU)
@@ -126,7 +155,7 @@
end
events = [eventsL, eventsU];
else
- events = Compute(sFile, ChannelMat, StimChan, EventsTrackMode, isAcceptZero, MinDuration);
+ events = Compute(sFile, ChannelMat, StimChan, EventsTrackMode, isAcceptZero, MinDuration, MaskValue);
end
% ===== SAVE RESULT =====
@@ -152,8 +181,11 @@
%% ===== COMPUTE =====
-function [events, EventsTrackMode, StimChan] = Compute(sFile, ChannelMat, StimChan, EventsTrackMode, isAcceptZero, MinDuration)
+function [events, EventsTrackMode, StimChan] = Compute(sFile, ChannelMat, StimChan, EventsTrackMode, isAcceptZero, MinDuration, MaskValue)
% Parse inputs
+ if (nargin < 7)
+ MaskValue = 0;
+ end
if (nargin < 6)
MinDuration = 0;
end
@@ -366,6 +398,10 @@
% === PROCESS EACH TRACK SEPARATELY ===
nTooShort = 0;
for iTrack = 1:length(tracks_name)
+ % Apply mask
+ if strcmpi(EventsTrackMode, 'value') && MaskValue > 0
+ tracks_vals{iTrack} = bitand(tracks_vals{iTrack}, MaskValue, 'uint32');
+ end
% Get the indices where something happens
if strcmpi(EventsTrackMode, 'rttl')
ixs = find(tracks_vals{iTrack} == 0);
diff --git a/toolbox/process/functions/process_evt_simple.m b/toolbox/process/functions/process_evt_simple.m
index bcf85ce32..c285352ba 100644
--- a/toolbox/process/functions/process_evt_simple.m
+++ b/toolbox/process/functions/process_evt_simple.m
@@ -44,10 +44,12 @@
sProcess.options.eventname.Comment = 'Event names: ';
sProcess.options.eventname.Type = 'text';
sProcess.options.eventname.Value = '';
- % Time offset
- sProcess.options.method.Comment = {'Keep the start of the events', 'Keep the middle of the events', 'Keep the end of the events'};
- sProcess.options.method.Type = 'radio';
- sProcess.options.method.Value = 1;
+ % Method from extended to simple
+ sProcess.options.method.Comment = {'Keep the start of the events', 'Keep the middle of the events', 'Keep the end of the events', 'Keep all samples of the events'; ...
+ 'start', 'middle', 'end', 'all'};
+ sProcess.options.method.Type = 'radio_label';
+ sProcess.options.method.Value = 'start';
+
end
@@ -65,9 +67,10 @@
% ===== GET OPTIONS =====
% Time offset
switch (sProcess.options.method.Value)
- case 1, Method = 'start';
- case 2, Method = 'middle';
- case 3, Method = 'end';
+ case {1, 'start'}, Method = 'start';
+ case {2, 'middle'}, Method = 'middle';
+ case {3, 'end'}, Method = 'end';
+ case {4, 'all'}, Method = 'every_sample';
end
% Event names
EvtNames = strtrim(str_split(sProcess.options.eventname.Value, ',;'));
@@ -115,18 +118,10 @@
end
% ===== PROCESS EVENTS =====
- for i = 1:length(iEvtList)
- switch Method
- case 'start'
- sEvents(iEvtList(i)).times = sEvents(iEvtList(i)).times(1,:);
- case 'middle'
- sEvents(iEvtList(i)).times = mean(sEvents(iEvtList(i)).times,1);
- case 'end'
- sEvents(iEvtList(i)).times = sEvents(iEvtList(i)).times(2,:);
- end
- sEvents(iEvtList(i)).times = round(sEvents(iEvtList(i)).times .* sFreq) / sFreq;
- end
-
+ sEventsMod = sEvents(iEvtList);
+ sEventsMod = Compute(sEventsMod, Method, sFreq);
+ sEvents(iEvtList) = sEventsMod;
+
% ===== SAVE RESULT =====
% Report results
if isRaw
@@ -139,5 +134,38 @@
end
-
-
+%% ===== COMPUTE =====
+function sEvents = Compute(sEvents, modification, sFreq)
+ % Apply modificiation to each event type
+ for i = 1:length(sEvents)
+ switch (modification)
+ case 'start'
+ sEvents(i).times = sEvents(i).times(1,:);
+ case 'middle'
+ sEvents(i).times = mean(sEvents(i).times, 1);
+ case 'end'
+ sEvents(i).times = sEvents(i).times(2,:);
+ case 'every_sample'
+ % Create an event instance for every sample in limits of the extendend event
+ sEventAllOcc = repmat(db_template('Event'), 0);
+ for iOccurExt = 1 : size(sEvents(i).times,2)
+ nSamples = round(diff(sEvents(i).times(:, iOccurExt)) * sFreq) + 1;
+ sEventOcc = sEvents(i);
+ sEventOcc.epochs = repmat(sEvents(i).epochs(iOccurExt), 1, nSamples);
+ sEventOcc.times = ([0:nSamples-1] / sFreq) + sEvents(i).times(1,iOccurExt);
+ if ~isempty(sEvents(i).channels)
+ sEventOcc.channels = repmat(sEvents(i).channels(iOccurExt), 1, nSamples);
+ end
+ if ~isempty(sEvents(i).notes)
+ sEventOcc.notes = repmat(sEvents(i).notes(iOccurExt), 1, nSamples);
+ end
+ % Events for one occurence
+ sEventOcc.label = sprintf('%s_%05d', sEvents(i).label, iOccurExt);
+ sEventAllOcc(end+1) = sEventOcc;
+ end
+ % Merge events from all ocurrences
+ sEvents(i) = process_evt_merge('Compute', '', sEventAllOcc, {sEventAllOcc.label}, sEvents(i).label, 1);
+ end
+ end
+ sEvents(i).times = round(sEvents(i).times .* sFreq) / sFreq;
+end
diff --git a/toolbox/process/functions/process_export_file.m b/toolbox/process/functions/process_export_file.m
new file mode 100644
index 000000000..bc45dd514
--- /dev/null
+++ b/toolbox/process/functions/process_export_file.m
@@ -0,0 +1,205 @@
+function varargout = process_export_file( varargin )
+% PROCESS_FILE_EXPORT: Exports RawData, Data, Results, TimeFreq or Matrix files
+%
+% USAGE: sProcess = process_export_file('GetDescription')
+% process_export_file('Run', sProcess, sInputs)
+
+% @=============================================================================
+% 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: Raymundo Cassani, 2023
+
+eval(macro_method);
+end
+
+
+%% ===== GET DESCRIPTION =====
+function sProcess = GetDescription() %#ok
+ % Description the process
+ sProcess.Comment = 'Export to file';
+ sProcess.Category = 'Custom';
+ sProcess.SubGroup = 'File';
+ sProcess.Index = 982;
+ sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/Scripting';
+ % Definition of the input accepted by this process
+ sProcess.InputTypes = {'raw', 'data', 'results', 'timefreq', 'matrix'};
+ sProcess.OutputTypes = {'raw', 'data', 'results', 'timefreq', 'matrix'};
+ sProcess.nInputs = 1;
+ sProcess.nMinFiles = 1;
+ % Instructions label
+ sProcess.options.label1.Comment = ['One file: Provide filename and format
' ...
+ 'Multiple files: Provide directory and format ' ...
+ '(filenames are obtained from database)
'];
+ sProcess.options.label1.Type = 'label';
+ % File selection options
+ SelectOptions = {...
+ '', ... % Filename
+ '', ... % FileFormat
+ 'save', ... % Dialog type: {open,save}
+ '', ... % Window title
+ '', ... % DefaultFile
+ 'single', ... % Selection mode: {single,multiple}
+ 'files', ... % Selection mode: {files,dirs,files_and_dirs}
+ '', ... % Available file formats
+ ''}; % DefaultFormats: {ChannelIn,DataIn,DipolesIn,EventsIn,AnatIn,MriIn,NoiseCovIn,ResultsIn,SspIn,SurfaceIn,TimefreqIn}
+ exportComment = 'Output file:';
+ windowTitle = 'Export: %s...';
+ % === TARGET RAW DATA FILENAME
+ SelectOptions{4} = sprintf(windowTitle, 'raw data');
+ SelectOptions{8} = bst_get('FileFilters', 'rawout');
+ SelectOptions{9} = 'DataOut';
+ sProcess.options.exportraw.Comment = exportComment;
+ sProcess.options.exportraw.Type = 'filename';
+ sProcess.options.exportraw.Value = SelectOptions;
+ sProcess.options.exportraw.InputTypes = {'raw'};
+
+ % === TARGET DATA FILENAME
+ SelectOptions{4} = sprintf(windowTitle, 'data');
+ SelectOptions{8} = bst_get('FileFilters', 'dataout');
+ SelectOptions{9} = 'DataOut';
+ sProcess.options.exportdata.Comment = exportComment;
+ sProcess.options.exportdata.Type = 'filename';
+ sProcess.options.exportdata.Value = SelectOptions;
+ sProcess.options.exportdata.InputTypes = {'data'};
+
+ % === TARGET RESULTS FILENAME
+ SelectOptions{4} = sprintf(windowTitle, 'sources');
+ SelectOptions{8} = bst_get('FileFilters', 'resultsout');
+ SelectOptions{9} = 'ResultsOut';
+ sProcess.options.exportresults.Comment = exportComment;
+ sProcess.options.exportresults.Type = 'filename';
+ sProcess.options.exportresults.Value = SelectOptions;
+ sProcess.options.exportresults.InputTypes = {'results'};
+
+ % === TARGET TIMEFREQ FILENAME
+ SelectOptions{4} = sprintf(windowTitle, 'time-freq');
+ SelectOptions{8} = bst_get('FileFilters', 'timefreqout');
+ SelectOptions{9} = 'TimefreqOut';
+ sProcess.options.exporttimefreq.Comment = exportComment;
+ sProcess.options.exporttimefreq.Type = 'filename';
+ sProcess.options.exporttimefreq.Value = SelectOptions;
+ sProcess.options.exporttimefreq.InputTypes = {'timefreq'};
+
+ % === TARGET MATRIX FILENAME
+ SelectOptions{4} = sprintf(windowTitle, 'matrix');
+ SelectOptions{8} = bst_get('FileFilters', 'matrixout');
+ SelectOptions{9} = 'MatrixOut';
+ sProcess.options.exportmatrix.Comment = exportComment;
+ sProcess.options.exportmatrix.Type = 'filename';
+ sProcess.options.exportmatrix.Value = SelectOptions;
+ sProcess.options.exportmatrix.InputTypes = {'matrix'};
+end
+
+%% ===== FORMAT COMMENT =====
+function Comment = FormatComment(sProcess) %#ok
+ inputType = InputTypeFromFields(sProcess);
+ if ~isempty(inputType)
+ inputType(1) = upper(inputType(1));
+ end
+ Comment = ['Export to file: ' inputType];
+end
+
+%% ===== RUN =====
+function OutputFiles = Run(sProcess, sInputs) %#ok
+ % Returned files: same as input
+ OutputFiles = {sInputs.FileName};
+ % Get options
+ inputType = InputTypeFromFields(sProcess);
+ outFileOptions = sProcess.options.(['export' inputType]).Value;
+ isExportDir = isdir(outFileOptions{1});
+ % If dir (export multiple files), find extension for filter
+ if isExportDir
+ % Get dir path
+ fPath = outFileOptions{1};
+ % Get extension for filter
+ Filters = bst_get('FileFilters', [inputType, 'out']);
+ iFilter = find(ismember(Filters(:,3), outFileOptions{2}), 1, 'first');
+ if isempty(iFilter)
+ bst_error(sprintf('Format "%s" is not valid for files of type "%s"', outFileOptions{2}, inputType), 'Export to file');
+ return
+ end
+ fExt = Filters{iFilter, 1}{1};
+ end
+ % Export files
+ for iInput = 1 : length(sInputs)
+ % Name for one file is already clean of tags
+ if isExportDir
+ % Base name from Brainstorm database
+ fileName = sInputs(iInput).FileName;
+ fileType = file_gettype(fileName);
+ if strcmp(fileType, 'link')
+ [kernelFile, dataFile] = file_resolve_link(sInputs(iInput).FileName);
+ [~, kBase] = bst_fileparts(kernelFile);
+ [~, fBase] = bst_fileparts(dataFile);
+ fBase = [kBase, '_' ,fBase];
+ else
+ [~, fBase] = bst_fileparts(fileName);
+ end
+ % Remove fit type tagsn from fBase
+ switch(fileType)
+ case 'data' % includes 'raw' files
+ tagsToRemove = {'_data', 'data_', '0raw_'};
+ case {'results', 'link'}
+ tagsToRemove = {'_results', 'results_'};
+ case 'timefreq'
+ tagsToRemove = {'_timefreq', 'timefreq_'};
+ case 'matrix'
+ tagsToRemove = {'_matrix', 'matrix_'};
+ end
+ for iTag = 1 : length(tagsToRemove)
+ fBase = strrep(fBase, tagsToRemove{iTag}, '');
+ end
+ outFileOptions{1} = bst_fullfile(fPath, [fBase, fExt]);
+ % Verify that extension for BST format ends in '.ext'
+ if strcmp(outFileOptions{2}, 'BST') && isempty(regexp(outFileOptions{1}, '\.\w+$', 'once'))
+ % Add prefix for Brainstorm files
+ prefix = '';
+ if strcmp(fExt(1), '_')
+ prefix = [fExt(2:end), '_'];
+ end
+ outFileOptions{1} = bst_fullfile(fPath, [prefix, fBase, '.mat']);
+ end
+ end
+ % Export files according their input type
+ switch inputType
+ case {'data', 'raw'}
+ export_data(sInputs(iInput).FileName, [], outFileOptions{1}, outFileOptions{2});
+ case 'results'
+ export_result(sInputs(iInput).FileName, outFileOptions{1}, outFileOptions{2});
+ case 'timefreq'
+ export_timefreq(sInputs(iInput).FileName, outFileOptions{1}, outFileOptions{2});
+ case 'matrix'
+ export_matrix(sInputs(iInput).FileName, outFileOptions{1}, outFileOptions{2});
+ end
+ % Info of where the file was saved (console and report)
+ bst_report('Info', sProcess, sInputs(iInput), sprintf('File exported as %s', outFileOptions{1}));
+ fprintf(['BST: File "%s" exported as "%s"' 10], sInputs(iInput).FileName, outFileOptions{1});
+ end
+end
+
+function inputType = InputTypeFromFields(sProcess)
+ % Find InputType from first option field named 'exportINPUTTYPE'
+ % FileTypes: 'raw', 'data', 'results', 'timefreq' or 'matrix'
+ optFields = fieldnames(sProcess.options);
+ iField = find(~cellfun(@isempty, regexp(optFields, '^export')), 1, 'first');
+ if ~isempty(iField)
+ inputType = regexprep(optFields{iField}, '^export', '');
+ else
+ inputType = '';
+ end
+end
diff --git a/toolbox/process/functions/process_export_spmvol.m b/toolbox/process/functions/process_export_spmvol.m
index 3237576f9..4f8f580a4 100644
--- a/toolbox/process/functions/process_export_spmvol.m
+++ b/toolbox/process/functions/process_export_spmvol.m
@@ -194,6 +194,15 @@
bst_progress('inc',1);
% ===== LOAD RESULTS =====
+ % Check the data type: timefreq must be source/surface based
+ if strcmpi(file_gettype(sInputs(iFile).FileName), 'timefreq')
+ ResMat = in_bst_timefreq(sInputs(iFile).FileName, 0, 'DataType');
+ if ~strcmpi(ResMat.DataType, 'results')
+ errMsg = 'Only surface or volume maps can be exported.';
+ bst_report('Error', func2str(sProcess.Function), sInputs(iFile).FileName, errMsg);
+ continue;
+ end
+ end
% Load results
sInput = bst_process('LoadInputFile', sInputs(iFile).FileName, [], TimeWindow, LoadOptions);
if isempty(sInput.Data)
@@ -250,8 +259,8 @@
end
% Unconstrained sources: combine orientations (norm of all dimensions)
sInput.Data = bst_source_orient([], sInput.nComponents, ResultsMat.GridAtlas, sInput.Data, 'rms');
- % If an atlas exists
- if strcmpi(ResultsMat.HeadModelType, 'surface') && isfield(ResultsMat, 'Atlas') && ~isempty(ResultsMat.Atlas) && ~isempty(ResultsMat.Atlas.Scouts)
+ % If an atlas exists and it is not from a 1xN connectivity file (1 Scout x N Sources)
+ if strcmpi(ResultsMat.HeadModelType, 'surface') && isfield(ResultsMat, 'Atlas') && ~isempty(ResultsMat.Atlas) && ~isempty(ResultsMat.Atlas.Scouts) && isempty(strfind(sInputs(iFile).FileName, '_connect1'))
Atlas = ResultsMat.Atlas;
else
Atlas = [];
@@ -292,13 +301,40 @@
% Else: Compute interpolation matrix grid points => MRI voxels
else
sMri.FileName = MriFile;
- GridSmooth = isempty(ResultsMat.DisplayUnits) || ~ismember(ResultsMat.DisplayUnits, {'s','ms','t'});
- MriInterp = grid_interp_mri(ResultsMat.GridLoc, sMri, ResultsMat.SurfaceFile, 0, [], [], GridSmooth);
+ GridSmooth = isempty(ResultsMat.DisplayUnits) || ~ismember(ResudltsMat.DisplayUnits, {'s','ms','t'});
+ switch (ResultsMat.HeadModelType)
+ case 'volume'
+ % Compute interpolation
+ MriInterp = grid_interp_mri(ResultsMat.GridLoc, sMri, ResultsMat.SurfaceFile, 0, [], [], GridSmooth);
+
+ case 'mixed'
+ % Compute the surface interpolation
+ tess2mri_interp = tess_interp_mri(ResultsMat.SurfaceFile, sMri);
+ % Initialize returned interpolation matrix
+ MriInterp = sparse(numel(sMri.Cube(:,:,:,1)), size(ResultsMat.GridAtlas.Grid2Source, 1));
+ % Process each region separately
+ ind = 1;
+ sScouts = ResultsMat.GridAtlas.Scouts;
+ for i = 1:length(sScouts)
+ % Indices in the interpolation matrix
+ iGrid = ind + (0:length(sScouts(i).GridRows) - 1);
+ ind = ind + length(sScouts(i).GridRows);
+ % Interpolation depends on the type of region (volume or surface)
+ switch (sScouts(i).Region(2))
+ case 'V'
+ GridLoc = ResultsMat.GridLoc(sScouts(i).GridRows,:);
+ MriInterp(:,iGrid) = grid_interp_mri(GridLoc, sMri, ResultsMat.SurfaceFile, 1, [], [], GridSmooth);
+ case 'S'
+ MriInterp(:,iGrid) = tess2mri_interp(:, sScouts(i).Vertices);
+ end
+ end
+ end
% Save values for next iteration
prevInterp = MriInterp;
prevGridLoc = ResultsMat.GridLoc;
end
end
+
% Export surface-based files
else
MriInterp = [];
diff --git a/toolbox/process/functions/process_extract_headdist.m b/toolbox/process/functions/process_extract_headdist.m
index 73e2c6a82..d1ded4a68 100644
--- a/toolbox/process/functions/process_extract_headdist.m
+++ b/toolbox/process/functions/process_extract_headdist.m
@@ -68,7 +68,7 @@
DataMat = in_bst_data(sInputs(iInput).FileName);
% Check for CTF.
if ~strcmp(DataMat.Device, 'CTF')
- bst_report('Error', sProcess, sInputs(iFile), 'Extract head distance is currently only available for CTF data.');
+ bst_report('Error', sProcess, sInputs(iInput), 'Extract head distance is currently only available for CTF data.');
end
% Channel file for Study
ChannelMat = in_bst_channel(sInputs.ChannelFile);
diff --git a/toolbox/process/functions/process_extract_scout.m b/toolbox/process/functions/process_extract_scout.m
index f7cfe962f..db72fded1 100644
--- a/toolbox/process/functions/process_extract_scout.m
+++ b/toolbox/process/functions/process_extract_scout.m
@@ -6,12 +6,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
@@ -55,11 +55,11 @@
sProcess.options.flatten.Value = 1;
sProcess.options.flatten.InputTypes = {'results'};
% === SCOUT FUNCTION ===
- sProcess.options.scoutfunc.Comment = {'Mean', 'Power', 'Max', 'PCA', 'Std', 'All', 'Scout function:'; ...
- 'mean', 'power', 'max', 'pca', 'std', 'all', ''};
+ sProcess.options.scoutfunc.Comment = {'Mean', 'Power', 'Max', 'PCA', 'Std', 'RMS', 'All', 'Scout function:'; ...
+ 'mean', 'power', 'max', 'pca', 'std', 'rms', 'all', ''};
sProcess.options.scoutfunc.Type = 'radio_linelabel';
sProcess.options.scoutfunc.Value = 'pca';
- sProcess.options.scoutfunc.Controller = struct('pca', 'pca', 'mean', 'notpca', 'power', 'notpca', 'max', 'notpca', 'std', 'notpca', 'all', 'notpca');
+ sProcess.options.scoutfunc.Controller = struct('pca', 'pca', 'mean', 'notpca', 'power', 'notpca', 'max', 'notpca', 'std', 'notpca', 'rms', 'notpca', 'all', 'notpca');
% === PCA Options
sProcess.options.pcaedit.Comment = {'panel_pca', ' PCA options: '};
sProcess.options.pcaedit.Type = 'editpref';
@@ -151,6 +151,7 @@
case {4, 'std'}, ScoutFunc = 'std';
case {5, 'all'}, ScoutFunc = 'all';
case {6, 'power'}, ScoutFunc = 'power';
+ case {7, 'rms'}, ScoutFunc = 'rms';
otherwise, bst_report('Error', sProcess, [], 'Invalid scout function.'); return;
end
else
@@ -185,18 +186,15 @@
end
% Unconstrained orientations
- % Only allow norm when called without new pca flattening option.
- if isfield(sProcess.options, 'flatten') && isfield(sProcess.options.flatten, 'Value')
- if isequal(sProcess.options.flatten.Value, 1)
- UnconstrFunc = 'pca';
- else
- UnconstrFunc = 'none';
- end
- elseif isfield(sProcess.options, 'isnorm') && isfield(sProcess.options.isnorm, 'Value') && isequal(sProcess.options.isnorm.Value, 1)
+ UnconstrFunc = 'none';
+ % Perform norm if requested ('isnorm' may be set to '1' by calling process)
+ if isfield(sProcess.options, 'isnorm') && isfield(sProcess.options.isnorm, 'Value') && isequal(sProcess.options.isnorm.Value, 1)
UnconstrFunc = 'norm';
- else
- UnconstrFunc = 'none';
+ % If norm=0, then check if PCA fattening was requested
+ elseif isfield(sProcess.options, 'flatten') && isfield(sProcess.options.flatten, 'Value') && isequal(sProcess.options.flatten.Value, 1)
+ UnconstrFunc = 'pca';
end
+
% Check if there actually are sources with unconstrained orientations
if ~strcmpi(UnconstrFunc, 'none')
% Check if there are unconstrained sources. The function only checks the first file. Other files
@@ -300,7 +298,10 @@
bst_progress('set', round(100*(iInput-1)/length(sInputs)));
end
end
- isAbs = ~isempty(strfind(sInputs(iInput).FileName, '_abs'));
+ % Get meaningful tags in the results file name (without folders)
+ TestResFile = file_resolve_link(sInputs(iInput).FileName);
+ [~, TestTags] = bst_fileparts(TestResFile);
+ isAbs = ~isempty(strfind(TestTags, '_abs'));
% === READ FILES ===
[sResults, matSourceValues, matDataValues, fileComment] = LoadFile(sProcess, sInputs(iInput), TimeWindow);
@@ -357,6 +358,10 @@
OutputFiles = {};
CleanExit; return; % Error already reported.
end
+ % If norm, orientations will be aggregated, keep only one name per scout
+ if strcmpi(UnconstrFunc, 'norm')
+ RowNames = {ScoutName};
+ end
if AddFileComment && ~isempty(fileComment)
RowNames = cellfun(@(c) [c ' @ ' fileComment], RowNames, 'UniformOutput', false);
end
@@ -463,15 +468,17 @@
scoutStd = cat(1, scoutStd, tmpScoutStd);
end
% Add frequency to row descriptions.
- if (nFreq > 1) && AddFileComment
+ if isfield(sResults, 'Freqs') && ~isempty(sResults.Freqs) && ((iscell(sResults.Freqs) && size(sResults.Freqs,1) == nFreq) || (~iscell(sResults.Freqs) && length(sResults.Freqs) == nFreq))
if iscell(sResults.Freqs)
freqComment = [' ' sResults.Freqs{iFreq,1}];
else
freqComment = [' ' num2str(sResults.Freqs(iFreq)), 'Hz'];
end
- RowNames = cellfun(@(c) [c freqComment], RowNames, 'UniformOutput', false);
+ RowNamesFreq = cellfun(@(c) [c freqComment], RowNames, 'UniformOutput', false);
+ else
+ RowNamesFreq = RowNames;
end
- Description = cat(1, Description, RowNames);
+ Description = cat(1, Description, RowNamesFreq);
end
end
% If nothing was found
diff --git a/toolbox/process/functions/process_extract_time.m b/toolbox/process/functions/process_extract_time.m
index 57ae02e81..7226a05e0 100644
--- a/toolbox/process/functions/process_extract_time.m
+++ b/toolbox/process/functions/process_extract_time.m
@@ -35,8 +35,8 @@
sProcess.Index = 353;
sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/ExploreRecordings?highlight=%28Extract+time%29#Time_selection';
% Definition of the input accepted by this process
- sProcess.InputTypes = {'data', 'results', 'timefreq', 'matrix'};
- sProcess.OutputTypes = {'data', 'results', 'timefreq', 'matrix'};
+ sProcess.InputTypes = {'raw', 'data', 'results', 'timefreq', 'matrix'};
+ sProcess.OutputTypes = {'raw', 'data', 'results', 'timefreq', 'matrix'};
sProcess.nInputs = 1;
sProcess.nMinFiles = 1;
sProcess.isSeparator = 1;
diff --git a/toolbox/process/functions/process_extract_values.m b/toolbox/process/functions/process_extract_values.m
index 8cbc5163b..9cb7ca95e 100644
--- a/toolbox/process/functions/process_extract_values.m
+++ b/toolbox/process/functions/process_extract_values.m
@@ -91,7 +91,8 @@
sProcess.options.scoutfunc.InputTypes = {'results'};
% === NORM XYZ
- sProcess.options.isnorm.Comment = 'Compute absolute values (or norm for unconstrained sources)';
+ sProcess.options.isnorm.Comment = ['Compute absolute values (or norm for unconstrained sources). ' ...
+ 'Applied after scout function.'];
sProcess.options.isnorm.Type = 'checkbox';
sProcess.options.isnorm.Value = 0;
sProcess.options.isnorm.InputTypes = {'results'};
@@ -395,14 +396,6 @@
sLoaded.Data = sLoaded.Data(:,1,:,:);
sLoaded.Time = OPTIONS.TimeWindow(1);
end
- % Add components labels
- if (sLoaded.nComponents == 3)
- sLoaded.RowNames = sLoaded.RowNames(:)';
- sLoaded.RowNames = [cellfun(@(c)cat(2,c,'.1'), sLoaded.RowNames, 'UniformOutput', 0); ...
- cellfun(@(c)cat(2,c,'.2'), sLoaded.RowNames, 'UniformOutput', 0); ...
- cellfun(@(c)cat(2,c,'.3'), sLoaded.RowNames, 'UniformOutput', 0)];
- sLoaded.RowNames = sLoaded.RowNames(:);
- end
% Convert to matrix structure
FileMat = db_template('matrixmat');
FileMat.Value = sLoaded.Data;
@@ -681,8 +674,18 @@
tstart = FileMat.Time(1);
end
elseif isfield(FileMat, 'TimeBands') && ~isempty(FileMat.TimeBands)
- sfreq = 1;
- tstart = 1;
+ timeBounds = process_tf_bands('GetBounds', FileMat.TimeBands);
+ durationBands = diff(timeBounds, 1, 2);
+ stepBands = diff(timeBounds(:,1)); % Empty if only one timeband
+ % Generate time vector if time bands are periodic
+ if (~isempty(stepBands)) && (std(stepBands) / mean(stepBands) < 0.01) && (std(durationBands) / mean(durationBands) < 0.01)
+ tmpTime = mean(timeBounds, 2);
+ sfreq = 1/(tmpTime(2) - tmpTime(1));
+ tstart = tmpTime(1);
+ else
+ sfreq = 1;
+ tstart = 1 ;
+ end
FileMat.TimeBands = [];
else
sfreq = 1/(FileMat.Time(2) - FileMat.Time(1));
diff --git a/toolbox/process/functions/process_fem_mesh.m b/toolbox/process/functions/process_fem_mesh.m
index f4f7bde35..d38b76e4d 100644
--- a/toolbox/process/functions/process_fem_mesh.m
+++ b/toolbox/process/functions/process_fem_mesh.m
@@ -56,8 +56,9 @@
'Brain2mesh: Segment the T1 (and T2) MRI with SPM12, mesh with Brain2mesh ', ...
'SimNIBS 3.x: Call SimNIBS/headreco to segment and mesh the T1 (and T2) MRI.', ...
'SimNIBS 4.x: Call SimNIBS/charm to segment and mesh the T1 (and T2) MRI.', ...
- 'FieldTrip: Call FieldTrip to create hexahedral mesh of the T1 MRI.'; ...
- 'iso2mesh-2021', 'iso2mesh', 'brain2mesh', 'simnibs3', 'simnibs4', 'fieldtrip'};
+ 'FieldTrip: Call FieldTrip to create hexahedral mesh of the T1 MRI.', ...
+ 'Zeffiro: Call Zeffiro to create a tetrahedral mesh from the BEM surfaces '; ...
+ 'iso2mesh-2021', 'iso2mesh', 'brain2mesh', 'simnibs3', 'simnibs4', 'fieldtrip', 'zeffiro'};
sProcess.options.method.Type = 'radio_label';
sProcess.options.method.Value = 'iso2mesh';
% Iso2mesh options:
@@ -108,6 +109,22 @@
sProcess.options.zneck.Comment = 'Cut neck below MNI Z coordinate (0=disable): ';
sProcess.options.zneck.Type = 'value';
sProcess.options.zneck.Value = {OPTIONS.Zneck, '', 0};
+ % Zeffiro options:
+ sProcess.options.opt4.Comment = ' Zeffiro options: ';
+ sProcess.options.opt4.Type = 'label';
+ % Zeffiro options: Mesh resolution [Add more Zef option TODO by Zef team]
+ sProcess.options.zefMeshResolution.Comment = 'Mesh resolution (edge mm): ';
+ sProcess.options.zefMeshResolution.Type = 'value';
+ sProcess.options.zefMeshResolution.Value = {OPTIONS.ZefMeshResolution, '', 3};
+ % Zeffiro options:Use GPU
+ sProcess.options.zefUseGPU.Comment = 'Use GPU for Zeffiro mesh generation';
+ sProcess.options.zefUseGPU.Type = 'checkbox';
+ sProcess.options.zefUseGPU.Value = OPTIONS.ZefUseGPU;
+ % Zeffiro options:Use GPU
+ sProcess.options.zefAdvancedInterface.Comment = 'Use Zeffiro advanced interface';
+ sProcess.options.zefAdvancedInterface.Type = 'checkbox';
+ sProcess.options.zefAdvancedInterface.Value = OPTIONS.ZefAdvancedInterface;
+
end
@@ -141,7 +158,7 @@
end
% Method
OPTIONS.Method = sProcess.options.method.Value;
- if isempty(OPTIONS.Method) || ~ischar(OPTIONS.Method) || ~ismember(OPTIONS.Method, {'iso2mesh-2021','iso2mesh','brain2mesh','simnibs3','simnibs4','fieldtrip'})
+ if isempty(OPTIONS.Method) || ~ischar(OPTIONS.Method) || ~ismember(OPTIONS.Method, {'iso2mesh-2021','iso2mesh','brain2mesh','simnibs3','simnibs4','fieldtrip','zeffiro'})
bst_report('Error', sProcess, [], 'Invalid method.');
return
end
@@ -190,6 +207,16 @@
bst_report('Error', sProcess, [], 'Invalid downsampling factor.');
return
end
+ % Zeffiro: Mesh resolution -Edge Length-
+ OPTIONS.ZefMeshResolution = sProcess.options.zefMeshResolution.Value{1};
+ if isempty(OPTIONS.ZefMeshResolution) || (OPTIONS.ZefMeshResolution < 1) || (OPTIONS.ZefMeshResolution > 4.5)
+ bst_report('Error', sProcess, [], 'Invalid Mesh resolution value, please use value [1 - 4.5] mm.');
+ return
+ end
+ % Zeffiro: Use the GPU -Edge Length-
+ OPTIONS.ZefUseGPU = sProcess.options.zefUseGPU.Value;
+ % Zeffiro: Use the zef Advanced Interface
+ OPTIONS.ZefAdvancedInterface = sProcess.options.zefAdvancedInterface.Value;
% Call processing function
[isOk, errMsg] = Compute(iSubject, [], 0, OPTIONS);
@@ -207,18 +234,21 @@
%% ===== DEFAULT OPTIONS =====
function OPTIONS = GetDefaultOptions()
OPTIONS = struct(...
- 'Method', 'iso2mesh-2021', ... % {'iso2mesh-2021', 'iso2mesh', 'brain2mesh', 'simnibs3', 'simnibs4', 'roast', 'fieldtrip'}
- 'MeshType', 'tetrahedral', ... % iso2mesh: 'tetrahedral'; simnibs: 'tetrahedral'; roast:'hexahedral'/'tetrahedral'; fieldtrip:'hexahedral'/'tetrahedral'
- 'MaxVol', 0.1, ... % iso2mesh: Max tetrahedral volume (10=coarse, 0.0001=fine)
- 'KeepRatio', 100, ... % iso2mesh: Percentage of elements kept (1-100%)
- 'BemFiles', [], ... % iso2mesh: List of layers to use for meshing (if not specified, use the files selected in the database
- 'MergeMethod', 'mergemesh', ... % iso2mesh: {'mergemesh', 'mergesurf'} Function used to merge the meshes
- 'VertexDensity', 0.5, ... % SimNIBS: [0.1 - X] setting the vertex density (nodes per mm2) of the surface meshes
- 'NbVertices', 15000, ... % SimNIBS: Number of vertices for the cortex surface
- 'isEegCaps', 0, ... % SimNIBS: If 1, import the default EEG caps generated by SimNIBS
- 'NodeShift', 0.3, ... % FieldTrip: [0 - 0.49] Improves the geometrical properties of the mesh
- 'Downsample', 3, ... % FieldTrip: Integer, Downsampling factor to apply to the volumes before meshing
- 'Zneck', -115); % Input T1/T2: Cut volumes below neck (MNI Z-coordinate)
+ 'Method', 'iso2mesh-2021', ... % {'iso2mesh-2021', 'iso2mesh', 'brain2mesh', 'simnibs3', 'simnibs4', 'roast', 'fieldtrip'}
+ 'MeshType', 'tetrahedral', ... % iso2mesh: 'tetrahedral'; simnibs: 'tetrahedral'; roast:'hexahedral'/'tetrahedral'; fieldtrip:'hexahedral'/'tetrahedral'
+ 'MaxVol', 0.1, ... % iso2mesh: Max tetrahedral volume (10=coarse, 0.0001=fine)
+ 'KeepRatio', 100, ... % iso2mesh: Percentage of elements kept (1-100%)
+ 'BemFiles', [], ... % iso2mesh: List of layers to use for meshing (if not specified, use the files selected in the database
+ 'MergeMethod', 'mergemesh', ... % iso2mesh: {'mergemesh', 'mergesurf'} Function used to merge the meshes
+ 'VertexDensity', 0.5, ... % SimNIBS: [0.1 - X] setting the vertex density (nodes per mm2) of the surface meshes
+ 'NbVertices', 15000, ... % SimNIBS: Number of vertices for the cortex surface
+ 'isEegCaps', 0, ... % SimNIBS: If 1, import the default EEG caps generated by SimNIBS
+ 'NodeShift', 0.3, ... % FieldTrip: [0 - 0.49] Improves the geometrical properties of the mesh
+ 'Downsample', 3, ... % FieldTrip: Integer, Downsampling factor to apply to the volumes before meshing
+ 'Zneck', -115, ... % Input T1/T2: Cut volumes below neck (MNI Z-coordinate)
+ 'ZefMeshResolution', 3, ... % Zeffiro: mesh resolution: size of the element edge in mm
+ 'ZefUseGPU', 0, ... % Zeffiro: If 1, use GPU for mesh generation
+ 'ZefAdvancedInterface', 0); % Zeffireo: If 1, open the BST-Zeffiro advance interface
end
@@ -499,6 +529,10 @@
iSort = [iSort, iOther(~isnan(iOther))];
OPTIONS.BemFiles = OPTIONS.BemFiles(iSort);
TissueLabels = TissueLabels(iSort);
+ % If there is a CSF layer but nothing inside: rename into BRAIN
+ if ismember('csf', TissueLabels) && ~ismember('white', TissueLabels) && ~ismember('gray', TissueLabels)
+ TissueLabels{ismember(TissueLabels, 'csf')} = 'brain';
+ end
end
% Load surfaces
bst_progress('text', 'Loading surfaces...');
@@ -937,7 +971,181 @@
newelem = meshreorient(no, el(:,1:4));
elem = [newelem elem(:,5)];
node = no; % need to updates the new list
-
+
+ case 'zeffiro'
+ % Notes:
+ % This case follows mainly the same steps as Iso2mesh cases,
+ % with adaptation of the data with Zef Interface
+ % Some advantage compare to the other methods:
+ % - Source code in matlab, and no external binary dependencies
+ % - Ability to use the GPU and Parallel toolboxes
+ % - Can support intersected meshes and avoid errors observed
+ % with other methods [example: defaced head + inner +...]
+ % - Check the https://github.com/sampsapursiainen/zeffiro_interface/wiki
+ % - Possible issues: very rare instabilities due to unknow issues
+ % : when using low resolution >4.5mm hole in the meshes
+
+ % Install/load iso2mesh plugin
+ [isInstalled, errInstall] = bst_plugin('Install', 'zeffiro', isInteractive);
+ if ~isInstalled
+ errMsg = [errMsg, errInstall];
+ return;
+ end
+ % Get the Zeffiro folder
+ PlugDesc = bst_plugin('GetInstalled', 'zeffiro');
+ bst_plugin('SetProgressLogo', 'zeffiro');
+ % Use the BST Zef Interface or the advanced Zef Interface
+ if ~OPTIONS.ZefAdvancedInterface
+ % If surfaces are not passed in input: get default surfaces
+ % Zeffiro require inverse order ... from outer to inner (not as the other methods)
+ bst_progress('text', 'Loading data...');
+ if isempty(OPTIONS.BemFiles)
+ if ~isempty(sSubject.iScalp) && ~isempty(sSubject.iOuterSkull) && ~isempty(sSubject.iInnerSkull)
+ OPTIONS.BemFiles = {...
+ sSubject.Surface(sSubject.iScalp).FileName, ...
+ sSubject.Surface(sSubject.iOuterSkull).FileName, ...
+ sSubject.Surface(sSubject.iInnerSkull).FileName};
+ TissueLabels = {'scalp', 'skull', 'brain'};
+ else
+ errMsg = [errMsg, 'Method "' OPTIONS.Method '" requires three surfaces: head, inner skull and outer skull.' 10 ...
+ 'Create them with process "Generate BEM surfaces" first.'];
+ return;
+ end
+ % If surfaces are given: get their labels and sort from inner to outer
+ else
+ % Get tissue label
+ for iBem = 1:length(OPTIONS.BemFiles)
+ [sSubject, iSubject, iSurface] = bst_get('SurfaceFile', OPTIONS.BemFiles{iBem});
+ if ~strcmpi(sSubject.Surface(iSurface).SurfaceType, 'Other')
+ TissueLabels{iBem} = GetFemLabel(sSubject.Surface(iSurface).SurfaceType);
+ else
+ TissueLabels{iBem} = GetFemLabel(sSubject.Surface(iSurface).Comment);
+ end
+ end
+ % Sort from inner to outer [For Zeffiro this order will be reverted]
+ iSort = [];
+ iOther = 1:length(OPTIONS.BemFiles);
+ for label = {'white', 'gray', 'csf', 'skull', 'scalp'}
+ iLabel = find(strcmpi(label{1}, TissueLabels));
+ iSort = [iSort, iLabel];
+ iOther(iLabel) = NaN;
+ end
+ % flip will reverse the order of the surfaces
+ iSort = flip([iSort, iOther(~isnan(iOther))]);
+ OPTIONS.BemFiles = OPTIONS.BemFiles(iSort);
+ TissueLabels = TissueLabels(iSort);
+ % If there is a CSF layer but nothing inside: rename into BRAIN
+ if ismember('csf', TissueLabels) && ~ismember('white', TissueLabels) && ~ismember('gray', TissueLabels)
+ TissueLabels{ismember(TissueLabels, 'csf')} = 'brain';
+ end
+ end
+ % Load surfaces file and assign the names to Zef
+ bst_progress('text', 'Loading surfaces...');
+ bemMerge = {}; bemComment = {};
+ disp(' ');
+ nBem = length(OPTIONS.BemFiles);
+ for iBem = 1:nBem
+ disp(sprintf('FEM> %d. %5s: %s', iBem, TissueLabels{iBem}, OPTIONS.BemFiles{iBem}));
+ BemMat = in_tess_bst(OPTIONS.BemFiles{iBem});
+ bemComment{iBem} = BemMat.Comment;
+ end
+
+ % ===== CALL ZEFFIRO FROM HERE =====
+ bst_progress('text', 'Calling Zeffiro FEM Mesh Generation...');
+ disp('Now Calling Zeffiro FEM Mesh Generation ...');
+ % basic options ==> that can be used from Brainstorm
+ % ===== WRITE ZEF SETTING FILE =====
+ % documentation : https://github.com/sampsapursiainen/zeffiro_interface/wiki/Finite-Element-Mesh-generation
+ % Get the Zef folder path for brainstorm utilities
+ bst2zefInterface = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, '+utilities', '+brainstorm2zef');
+ % General setting :
+ % Open the setting file file
+ settingFile = fullfile(bst2zefInterface, '+m', 'zef_bst_import_settings.m');
+ fid = fopen(settingFile, 'wt+');
+ fprintf(fid, 'zef = zef_add_bounding_box(zef);\n');
+ % zef.exclude_box: include the bounding box in the mesh => default no ==>1
+ fprintf(fid, 'zef.exclude_box = %d;\n', 1);
+ % max_surface_face_count : default 1: fit to the input mesh, if <1:coraser, if >1 finer
+ % can be tuned from the advanced parameters (recomended is 0.5)
+ fprintf(fid, 'zef.max_surface_face_count = %d;\n', 0.5);
+ fprintf(fid, 'zef.mesh_smoothing_on = %d;\n', 1);
+ % this option smooth all the volum using the Taubin algo
+ % for advanced users.
+ fprintf(fid, 'zef.mesh_resolution = %d;\n', OPTIONS.ZefMeshResolution);
+ % unit is mm; the coarsest value is 4.5mm. value [minValue = 1.3 maxValue = 4.5]mm
+ % doc: https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0290715
+ % IT MAY GO TO 1mm, Fernando will check that, depends on the zef.ini parameters
+ % see here: plugins\zeffiro\zeffiro_interface-main_development_branch\profile\zeffiro_interface.ini
+ % CHANGE THE LINE 6: 100 TO 20 if any issue
+ % Level of parallelization in mesh labeling,100,parallel_vectors,number
+ % Level of parallelization in mesh labeling,20,parallel_vectors,number
+ fprintf(fid, 'zef.use_gpu = %d;\n', OPTIONS.ZefUseGPU);
+ % default is 0, require the GPU hardware and the toolbox
+ % To change at the zef.ini : in line 11: when GPU is off, CPU is used,
+ % if GPU is set to 0, then a CPU is used, require parallel toolbox
+ % it uses 4 cpu per default ==> advanced options
+ % Parallel threads in CPU forward computing,4,parallel_processes,number
+ % Check zeffiro_interface.ini
+ % Refinement
+ fprintf(fid, 'zef.refinement_on = %d;\n', 1); %
+ % boolean value to refine the mesh ==> default 1, => set to 1 for BST users
+ % Other function to check
+ % edit zef_init_forward_and_inverse_options.m
+ % edit zef_open_forward_and_inverse_options.m
+ fprintf(fid, 'zef.refinement_surface_on = %d;\n', 1);
+ % Activat the refinement of the surface, default 1, set to 1 in for BST users
+ % wiki page of the refienement:https://github.com/sampsapursiainen/zeffiro_interface/wiki/Finite-Element-Mesh-generation
+ fprintf(fid, 'import_compartment_list_aux = zef_get_active_compartments(zef);\n'); % list of the compartement
+ fprintf(fid, 'import_compartment_list_aux = import_compartment_list_aux(end-%d:end-1);\n', nBem);
+ % Take the outer most from end-%d:end-1, end is the bounding box, do not include for BST users
+ % List of the active compartment
+ fprintf(fid, 'import_compartment_list_aux = import_compartment_list_aux(:)'';\n');
+ fprintf(fid, 'zef.refinement_surface_compartments = [-1 import_compartment_list_aux];\n');
+ % -1 : refine all the compartement that have the sources [Not used here]
+ % in this case it will also refine the labeld tissue in import_compartment_list_aux indexes
+ fprintf(fid, 'zef_mesh_tool;\n'); % set the other options to default
+ fclose(fid);
+ % ===== WRITE BrainStorm2Zeffiro IMPORT FILE (BST to Zef Interface) =====
+ % Get temp folder
+ TmpDir = bst_get('BrainstormTmpDir', 1, 'zeffiro');
+ % writeZefFile() ==> Need to discuss with Sampsa
+ ZefFile = fullfile(TmpDir, 'BrainStorm2Zeffiro_import.zef');
+ fid = fopen(ZefFile, 'wt+');
+ bemComment = bemComment(:)'; % from outer to inner
+ for iBem = 1:nBem
+ fprintf(fid, ...
+ 'type,segmentation,name,%s,database,bst,tag,%s,parameter_name,sigma,parameter_value,0.33,invert,1 \n',...
+ bemComment{iBem}, bemComment{iBem});
+ end
+ fprintf(fid,'type,script,filename,utilities.brainstorm2zef.m.zef_bst_import_settings \n');
+ fclose(fid);
+
+ % ===== Run Zeffiro Mesh =====
+ % ==> Check with Sampsa
+ % https://github.com/sampsapursiainen/zeffiro_interface/blob/master/%2Bexamples/zef_meshing_example.m
+ zef = zeffiro_interface('start_mode','nodisplay','import_to_new_project',...
+ ZefFile, 'run_script','zef = zef_create_finite_element_mesh(zef);','exit_zeffiro', 1);
+ % outpts conversion:
+ node = zef.nodes;
+ elem = [zef.tetra zef.domain_labels];
+ TissueLabels = zef.name_tags(1:end-1);
+ else % use advanced Zeffiro Interface
+ bst_progress('text', 'Opening Zeffiro Advanced Panel...');
+ pause(2); % some time to display the progress bar.
+ % Advanced option ==> that will be used from Zef side
+ % Zef team is developing this interface within the Zef repo
+ % Get the Zef folder path for brainstorm utilities
+ zefPath = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder);
+ utilities.brainstorm2zef.m.zef_bst_plugin_start(zefPath)
+ % Sampsa team : export back the output to brainstorm db
+ % import the data from the Zef outputs and ad to bst database
+ % add some information to the History field from the Zef codes
+
+ % Return success
+ isOk = 1;
+ return
+ end
+
otherwise
errMsg = [errMsg, 'Invalid method "' OPTIONS.Method '".'];
return;
@@ -1096,9 +1304,9 @@ function ComputeInteractive(iSubject, iMris, BemFiles) %#ok
% Get default options
OPTIONS = GetDefaultOptions();
OPTIONS.BemFiles = BemFiles;
- % If BEM surfaces are selected, the only possible method is "iso2mesh"
+ % If BEM surfaces are selected, the possible methods are "iso2mesh" or "zeffiro"
if ~isempty(BemFiles) && iscell(BemFiles)
- FemMethods = {'Iso2mesh-2021','Iso2mesh'};
+ FemMethods = {'Iso2mesh-2021','Iso2mesh', 'Zeffiro'};
DefMethod = 'Iso2mesh-2021';
% More than 2 MRI selected: error
elseif (length(iMris) > 2)
@@ -1114,7 +1322,7 @@ function ComputeInteractive(iSubject, iMris, BemFiles) %#ok
DefMethod = 'SimNIBS4';
% Otherwise: Use the defaults from the folder: Ask for method to use
else
- FemMethods = {'Iso2mesh-2021','Iso2mesh','Brain2mesh','SimNIBS3','SimNIBS4','ROAST','FieldTrip'};
+ FemMethods = {'Iso2mesh-2021','Iso2mesh','Brain2mesh','SimNIBS3','SimNIBS4','ROAST','FieldTrip', 'Zeffiro'};
DefMethod = 'Iso2mesh-2021';
end
@@ -1157,7 +1365,10 @@ function ComputeInteractive(iSubject, iMris, BemFiles) %#ok
strQuestion = [strQuestion, ...
'FieldTrip: Call FieldTrip to segment and mesh the T1 MRI. ' ...
'FieldTrip is downloaded automatically as a plugin.
'];
- end
+ case 'Zeffiro'
+ strQuestion = [strQuestion, ...
+ 'Zeffiro: Call Zeffiro to create a tetrahedral mesh from the BEM surfaces. ' ...
+ 'Zeffiro is downloaded automatically as a plugin.
']; end
end
% Ask the user to select a method
res = java_dialog('question', strQuestion, 'FEM mesh generation method', [], FemMethods, DefMethod);
@@ -1274,6 +1485,43 @@ function ComputeInteractive(iSubject, iMris, BemFiles) %#ok
case 'roast'
% No extra options for now
OPTIONS.MeshType = 'tetrahedral';
+
+ case 'zeffiro'
+ % Ask user for the Zeffiro Option here
+ advancedZefMsg = 'Would you like to use the advanced interface for Zeffiro mesh generation?';
+ [res, isCancel] = java_dialog('question', [advancedZefMsg 10 ' This will open the advanced Zeffiro panel '], 'Zeffiro FEM Mesh Interface');
+ if isCancel || isempty(res)
+ return
+ end
+ if strcmpi(res, 'yes')
+ OPTIONS.ZefAdvancedInterface = 1;
+
+ else % use 'Brainstorm Basic Options'
+ OPTIONS.ZefAdvancedInterface = 0;
+ % Get mesh resolution
+ [res, isCancel] = java_dialog('input', 'Mesh Resolution (edge length) in [mm]:', 'Zeffiro FEM mesh', [], num2str(OPTIONS.ZefMeshResolution));
+ if isCancel || isempty(res)
+ return
+ end
+ OPTIONS.ZefMeshResolution = str2num(res);
+ % Check the values
+ if isempty(OPTIONS.ZefMeshResolution) || (OPTIONS.ZefMeshResolution < 1) || (OPTIONS.ZefMeshResolution > 4.5)
+ errMsg = ['Invalid Mesh resolution value.' 10 'Please use value from this interval [1 - 4.5] mm.'];
+ java_dialog('msgbox', ['Warning: ' errMsg]);
+ return;
+ end
+ % Use GPU?
+ [res, isCancel] = java_dialog('question', 'Use GPU for mesh computation?', 'Zeffiro FEM mesh');
+ if isCancel || isempty(res)
+ return
+ end
+ if strcmpi(res, 'yes')
+ OPTIONS.ZefUseGPU = 1;
+ else
+ OPTIONS.ZefUseGPU = 0;
+ end
+ end
+ OPTIONS.MeshType = 'tetrahedral';
end
% Open progress bar
diff --git a/toolbox/process/functions/process_fooof.m b/toolbox/process/functions/process_fooof.m
index 32740fc0f..d6c207b7e 100644
--- a/toolbox/process/functions/process_fooof.m
+++ b/toolbox/process/functions/process_fooof.m
@@ -25,7 +25,7 @@
% For more information type "brainstorm license" at command prompt.
% =============================================================================@
%
-% Authors: Luc Wilson, Francois Tadel, 2020-2022
+% Authors: Luc Wilson, Francois Tadel, 2020-2024
eval(macro_method);
end
@@ -53,18 +53,18 @@
sProcess.options.implementation.Controller.python = 'Python';
% === FREQUENCY RANGE
sProcess.options.freqrange.Comment = 'Frequency range for analysis: ';
- sProcess.options.freqrange.Type = 'freqrange_static'; % 'freqrange'
+ sProcess.options.freqrange.Type = 'freqrange_static';
sProcess.options.freqrange.Value = {[1 40], 'Hz', 1};
% === POWER LINE
sProcess.options.powerline.Comment = {'None', '50 Hz', '60 Hz', 'Ignore power line frequencies:'; '-5', '50', '60', ''};
sProcess.options.powerline.Type = 'radio_linelabel';
- sProcess.options.powerline.Value = '60';
+ sProcess.options.powerline.Value = 'None';
sProcess.options.powerline.Class = 'Matlab';
- % === PEAK TYPE
- sProcess.options.peaktype.Comment = {'Gaussian', 'Cauchy*', 'Best of both* (* experimental)', 'Peak model:'; 'gaussian', 'cauchy', 'best', ''};
- sProcess.options.peaktype.Type = 'radio_linelabel';
- sProcess.options.peaktype.Value = 'gaussian';
- sProcess.options.peaktype.Class = 'Matlab';
+ % === MODEL SELECTION
+ sProcess.options.method.Comment = {'Default', 'Model selection (experimental)', 'Optimization method:'; 'leastsquare', 'negloglike', ''};
+ sProcess.options.method.Type = 'radio_linelabel';
+ sProcess.options.method.Value = 'leastsquare';
+ sProcess.options.method.Class = 'Matlab';
% === PEAK WIDTH LIMITS
sProcess.options.peakwidth.Comment = 'Peak width limits (default=[0.5-12]): ';
sProcess.options.peakwidth.Type = 'freqrange_static';
@@ -80,7 +80,7 @@
% === PROXIMITY THRESHOLD
sProcess.options.proxthresh.Comment = 'Proximity threshold (default=2): ';
sProcess.options.proxthresh.Type = 'value';
- sProcess.options.proxthresh.Value = {2, 'stdev of peak model', 1};
+ sProcess.options.proxthresh.Value = {2, 'stdev of peak model', 2};
sProcess.options.proxthresh.Class = 'Matlab';
% === APERIODIC MODE
sProcess.options.apermode.Comment = {'Fixed', 'Knee', 'Aperiodic mode (default=fixed):'; 'fixed', 'knee', ''};
@@ -138,8 +138,9 @@
opt.return_spectrum = 0; % SPM/FT: set to 1
% Matlab-only options
opt.power_line = sProcess.options.powerline.Value;
- opt.peak_type = sProcess.options.peaktype.Value;
opt.proximity_threshold = sProcess.options.proxthresh.Value{1};
+ opt.optim_obj = sProcess.options.method.Value; % negloglike or leastsquare
+ opt.peak_type = 'gaussian'; % 'cauchy', for interface simplification
opt.guess_weight = sProcess.options.guessweight.Value;
opt.thresh_after = true; % Threshold after fitting always selected for Matlab (mirrors the Python FOOOF closest by removing peaks that do not satisfy a user's predetermined conditions)
% Python-only options
@@ -167,6 +168,11 @@
bst_progress('text',['Standby: FOOOFing spectrum ' num2str(iFile) ' of ' num2str(length(sInputs))]);
% Load input file
PsdMat = in_bst_timefreq(sInputs(iFile).FileName);
+ % Check for frequency definition
+ if iscell(PsdMat.Freqs)
+ bst_report('Error', sProcess, sInputs, 'FOOOF cannot be computed with PSD using frequency bands.');
+ return;
+ end
% Exclude 0Hz from the computation
if (opt.freq_range(1) == 0) && (PsdMat.Freqs(1) == 0) && (length(PsdMat.Freqs) >= 2)
opt.freq_range(1) = PsdMat.Freqs(2);
@@ -176,14 +182,25 @@
% Switch between implementations
switch (implementation)
case 'matlab' % Matlab standalone FOOOF
- [FOOOF_freqs, FOOOF_data] = FOOOF_matlab(PsdMat.TF, PsdMat.Freqs, opt, hasOptimTools);
+ switch (opt.optim_obj)
+ case 'leastsquare'
+ [FOOOF_freqs, FOOOF_data, errMsg] = FOOOF_matlab(PsdMat.TF, PsdMat.Freqs, opt, hasOptimTools);
+ case 'negloglike'
+ [FOOOF_freqs, FOOOF_data, errMsg] = FOOOF_matlab_nll(PsdMat.TF, PsdMat.Freqs, opt, hasOptimTools);
+ end
case 'python'
opt.peak_type = 'gaussian';
- [FOOOF_freqs, FOOOF_data] = process_fooof_py('FOOOF_python', PsdMat.TF, PsdMat.Freqs, opt);
+ opt.optim_obj = 'leastsquare';
+ [FOOOF_freqs, FOOOF_data, errMsg] = process_fooof_py('FOOOF_python', PsdMat.TF, PsdMat.Freqs, opt);
% Remove unnecessary structure level, allowing easy concatenation across channels, e.g. for display.
- FOOOF_data = FOOOF_data.FOOOF;
+ FOOOF_data = [FOOOF_data.FOOOF];
otherwise
- error('Invalid implentation.');
+ errMsg = ['Invalid FOOOF implentation: ' implementation];
+ end
+ % Return if error
+ if ~isempty(errMsg)
+ bst_report('Error', sProcess, sInputs(iFile), errMsg);
+ return;
end
% === FOOOF ANALYSIS ===
@@ -199,11 +216,15 @@
'peaks', ePeaks, ...
'aperiodics', eAperiodics, ...
'stats', eStats);
+ mstag = '';
+ if ~isempty(strfind(opt.optim_obj, 'negloglike'))
+ mstag = 'ms-';
+ end
% Comment: Add FOOOF
if ~isempty(strfind(PsdMat.Comment, 'PSD:'))
- PsdMat.Comment = strrep(PsdMat.Comment, 'PSD:', 'specparam:');
+ PsdMat.Comment = strrep(PsdMat.Comment, 'PSD:', [mstag 'specparam:']);
else
- PsdMat.Comment = strcat(PsdMat.Comment, ' | specparam');
+ PsdMat.Comment = strcat(PsdMat.Comment, [' | ' mstag 'specparam']);
end
% History: Computation
PsdMat = bst_history('add', PsdMat, 'compute', 'specparam');
@@ -225,9 +246,141 @@
%% ===================================================================================
% ===== MATLAB FOOOF ================================================================
% ===================================================================================
+function [fs, fg, errMsg] = FOOOF_matlab_nll(TF, Freqs, opt, hOT)
+ errMsg = '';
+ % Find all frequency values within user limits
+ fMask = (round(Freqs.*10)./10 >= opt.freq_range(1)) & (round(Freqs.*10)./10 <= opt.freq_range(2)) & ~mod(sum(abs(round(Freqs.*10)./10-[1;2;3].*str2double(opt.power_line)) >= 2),3);
+ fs = Freqs(fMask);
+ spec = log10(squeeze(TF(:,1,fMask))); % extract log spectra
+ nChan = size(TF,1);
+ if nChan == 1, spec = spec'; end
+ % Initalize FOOOF structs
+ fg(nChan) = struct(...
+ 'aperiodic_params', [],...
+ 'peak_params', [],...
+ 'peak_types', '',...
+ 'ap_fit', [],...
+ 'fooofed_spectrum', [],...
+ 'peak_fit', [],...
+ 'error', [],...
+ 'r_squared', []);
+ % Iterate across channels
+ bst_progress('text','Standby: ms-specparam is running in parallel');
+ try
+ parfor chan = 1:nChan
+ bst_progress('set', bst_round(chan / nChan,2) * 100);
+ % Fit aperiodic
+ aperiodic_pars = robust_ap_fit(fs, spec(chan,:), opt.aperiodic_mode);
+ % Remove aperiodic
+ flat_spec = flatten_spectrum(fs, spec(chan,:), aperiodic_pars, opt.aperiodic_mode);
+ % estimate valid peaks (and determine max n)
+ [est_pars, peak_function] = est_peaks(fs, flat_spec, opt.max_peaks, opt.peak_threshold, opt.min_peak_height, ...
+ opt.peak_width_limits/2, opt.proximity_threshold, opt.border_threshold, opt.peak_type);
+ model = struct();
+ for pk = 0:size(est_pars,1)
+ aperiodic_pars_tmp = [];
+ peak_pars_tmp = [];
+
+ peak_pars = est_fit(est_pars(1:pk,:), fs, flat_spec, opt.peak_width_limits/2, opt.peak_type, opt.guess_weight,hOT);
+ % Refit aperiodic
+ aperiodic = spec(chan,:);
+ for peak = 1:size(peak_pars,1)
+ aperiodic = aperiodic - peak_function(fs,peak_pars(peak,1), peak_pars(peak,2), peak_pars(peak,3));
+ end
+ aperiodic_pars = simple_ap_fit(fs, aperiodic, opt.aperiodic_mode);
+ guess = peak_pars;
+ if ~isempty(guess)
+ lb = [max([ones(size(guess(1:pk,:),1),1).*fs(1) guess(1:pk,1)-guess(1:pk,3)*2],[],2),zeros(size(guess(1:pk,2))),ones(size(guess(1:pk,3)))*opt.peak_width_limits(1)/2]';
+ ub = [min([ones(size(guess(1:pk,:),1),1).*fs(end) guess(1:pk,1)+guess(1:pk,3)*2],[],2),inf(size(guess(1:pk,2))),ones(size(guess(1:pk,3)))*opt.peak_width_limits(2)/2]';
+ else
+ lb = [];
+ ub = [];
+ end
+ switch opt.aperiodic_mode
+ case 'fixed'
+ lb = [-inf; 0; lb(:)];
+ ub = [inf; inf; ub(:)];
+ case 'knee'
+ lb = [-inf; 0; 0; lb(:)];
+ ub = [inf; 100; inf; ub(:)];
+ end
+ if opt.return_spectrum
+ fg(chan).power_spectrum = spec(chan,:);
+ end
+ guess = guess(1:pk,:)';
+ guess = [aperiodic_pars'; guess(:)];
+ options = optimset('Display', 'off', 'TolX', 1e-7, 'TolFun', 1e-9, ...
+ 'MaxFunEvals', 5000, 'MaxIter', 5000); % Tuned options
+ params = fmincon(@err_fm_constr,guess,[],[],[],[], ...
+ lb,ub,[],options,fs,spec(chan,:),opt.aperiodic_mode,opt.peak_type);
+ switch opt.aperiodic_mode
+ case 'fixed'
+ aperiodic_pars_tmp = params(1:2);
+ if length(params) > 3
+ peak_pars_tmp = reshape(params(3:end),[3 length(params(3:end))./3])';
+ end
+ case 'knee'
+ aperiodic_pars_tmp = params(1:3);
+ if length(params) > 3
+ peak_pars_tmp = reshape(params(4:end),[3 length(params(4:end))./3])';
+ end
+ end
+ % Generate model fit
+ ap_fit = gen_aperiodic(fs, aperiodic_pars_tmp, opt.aperiodic_mode);
+ model_fit = ap_fit;
+ if length(params) > 3
+ for peak = 1:size(peak_pars_tmp,1)
+ model_fit = model_fit + peak_function(fs,peak_pars_tmp(peak,1),...
+ peak_pars_tmp(peak,2),peak_pars_tmp(peak,3));
+ end
+ else
+ peak_pars_tmp = [0 0 0];
+ end
+ % Calculate model error
+ MSE = sum((spec(chan,:) - model_fit).^2)/length(model_fit);
+ rsq_tmp = corrcoef(spec(chan,:),model_fit).^2;
+ loglik = -length(model_fit)/2.*(1+log(MSE)+log(2*pi));
+ AIC = 2.*(length(params)-loglik);
+ BIC = length(params).*log(length(model_fit))-2.*loglik;
+ model(pk+1).aperiodic_params = aperiodic_pars_tmp;
+ model(pk+1).peak_params = peak_pars_tmp;
+ model(pk+1).MSE = MSE;
+ model(pk+1).r_squared = rsq_tmp(2);
+ model(pk+1).loglik = loglik;
+ model(pk+1).AIC = AIC;
+ model(pk+1).BIC = BIC;
+ model(pk+1).BF = exp((BIC-model(1).BIC)./2);
+ end
+ % insert data from best model
+
+ [~,mi] = min([model.BIC]);
+
+ aperiodic_pars = model(mi).aperiodic_params;
+ peak_pars = model(mi).peak_params;
+ % Return FOOOF results
+ aperiodic_pars(2) = abs(aperiodic_pars(2));
+ fg(chan).aperiodic_params = aperiodic_pars;
+ fg(chan).peak_params = peak_pars;
+ fg(chan).peak_types = func2str(peak_function);
+ fg(chan).ap_fit = 10.^gen_aperiodic(fs, aperiodic_pars, opt.aperiodic_mode);
+ fg(chan).fooofed_spectrum = 10.^build_model(fs, aperiodic_pars, opt.aperiodic_mode, peak_pars, peak_function);
+ fg(chan).peak_fit = fg(chan).fooofed_spectrum ./ fg(chan).ap_fit;
+ fg(chan).error = model(mi).MSE;
+ fg(chan).r_squared = model(mi).r_squared;
+ fg(chan).loglik = model(mi).loglik; % log-likelihood
+ fg(chan).AIC = model(mi).AIC;
+ fg(chan).BIC = model(mi).BIC;
+ fg(chan).models = model;
+ end
+ catch err
+ errMsg = err.message;
+ end
+end
+
%% ===== MATLAB STANDALONE FOOOF =====
-function [fs, fg] = FOOOF_matlab(TF, Freqs, opt, hOT)
+function [fs, fg, errMsg] = FOOOF_matlab(TF, Freqs, opt, hOT)
+ errMsg = '';
% Find all frequency values within user limits
fMask = (round(Freqs.*10)./10 >= opt.freq_range(1)) & (round(Freqs.*10)./10 <= opt.freq_range(2)) & ~mod(sum(abs(round(Freqs.*10)./10-[1;2;3].*str2double(opt.power_line)) >= 2),3);
fs = Freqs(fMask);
@@ -253,7 +406,7 @@
flat_spec = flatten_spectrum(fs, spec(chan,:), aperiodic_pars, opt.aperiodic_mode);
% Fit peaks
[peak_pars, peak_function] = fit_peaks(fs, flat_spec, opt.max_peaks, opt.peak_threshold, opt.min_peak_height, ...
- opt.peak_width_limits/2, opt.proximity_threshold, opt.border_threshold, opt.peak_type, opt.guess_weight,hOT);
+ opt.peak_width_limits/2, opt.proximity_threshold, opt.border_threshold, opt.peak_type, opt.guess_weight, hOT);
if opt.thresh_after && ~hOT % Check thresholding requirements are met for unbounded optimization
peak_pars(peak_pars(:,2) < opt.min_peak_height,:) = []; % remove peaks shorter than limit
peak_pars(peak_pars(:,3) < opt.peak_width_limits(1)/2,:) = []; % remove peaks narrower than limit
@@ -405,7 +558,7 @@
end
-function ys = expo_fl_function(freqs, params)
+function ys = expo_fl_function(f, params)
ys = log10(f.^(params(1)) * 10^(params(2)) + params(3));
@@ -432,7 +585,7 @@
% Set guess params for lorentzian aperiodic fit, guess params set at init
options = optimset('Display', 'off', 'TolX', 1e-4, 'TolFun', 1e-6, ...
- 'MaxFunEvals', 5000, 'MaxIter', 5000);
+ 'MaxFunEvals', 10000, 'MaxIter', 10000);
switch (aperiodic_mode)
case 'fixed' % no knee
@@ -483,7 +636,7 @@
% Second aperiodic fit - using results of first fit as guess parameters
options = optimset('Display', 'off', 'TolX', 1e-4, 'TolFun', 1e-6, ...
- 'MaxFunEvals', 5000, 'MaxIter', 5000);
+ 'MaxFunEvals', 10000, 'MaxIter', 10000);
guess_vec = popt;
switch (aperiodic_mode)
@@ -779,6 +932,209 @@
end
+function [guess_params,peak_function] = est_peaks(freqs, flat_iter, max_n_peaks, peak_threshold, min_peak_height, gauss_std_limits, proxThresh, bordThresh, peakType)
+% Iteratively fit peaks to flattened spectrum.
+%
+% Parameters
+% ----------
+% freqs : 1xn array
+% Frequency values for the power spectrum, in linear scale.
+% flat_iter : 1xn array
+% Flattened (aperiodic removed) power spectrum.
+% max_n_peaks : double
+% Maximum number of gaussians to fit within the spectrum.
+% peak_threshold : double
+% Threshold (in standard deviations of noise floor) to detect a peak.
+% min_peak_height : double
+% Minimum height of a peak (in log10).
+% gauss_std_limits : 1x2 double
+% Limits to gaussian (cauchy) standard deviation (gamma) when detecting a peak.
+% proxThresh : double
+% Minimum distance between two peaks, in st. dev. (gamma) of peaks.
+% peakType : {'gaussian', 'cauchy', 'both'}
+% Which types of peaks are being fitted
+% guess_weight : {'none', 'weak', 'strong'}
+% Parameter to weigh initial estimates during optimization (None, Weak, or Strong)
+% hOT : 0 or 1
+% Defines whether to use constrained optimization, fmincon, or
+% basic simplex, fminsearch.
+%
+% Returns
+% -------
+% guess_params : mx3 array, where m = No. of peaks.
+% Parameters that define the peak fit(s). Each row is a peak, as [mean, height, st. dev. (gamma)].
+
+ switch peakType
+ case 'gaussian' % gaussian only
+ peak_function = @gaussian; % Identify peaks as gaussian
+ % Initialize matrix of guess parameters for gaussian fitting.
+ guess_params = zeros(max_n_peaks, 3);
+ % Find peak: Loop through, finding a candidate peak, and fitting with a guess gaussian.
+ % Stopping procedure based on either the limit on # of peaks,
+ % or the relative or absolute height thresholds.
+ for guess = 1:max_n_peaks
+ % Find candidate peak - the maximum point of the flattened spectrum.
+ max_ind = find(flat_iter == max(flat_iter));
+ max_height = flat_iter(max_ind);
+
+ % Stop searching for peaks once max_height drops below height threshold.
+ if max_height <= peak_threshold * std(flat_iter)
+ break
+ end
+
+ % Set the guess parameters for gaussian fitting - mean and height.
+ guess_freq = freqs(max_ind);
+ guess_height = max_height;
+
+ % Halt fitting process if candidate peak drops below minimum height.
+ if guess_height <= min_peak_height
+ break
+ end
+
+ % Data-driven first guess at standard deviation
+ % Find half height index on each side of the center frequency.
+ half_height = 0.5 * max_height;
+
+ le_ind = sum(flat_iter(1:max_ind) <= half_height);
+ ri_ind = length(flat_iter) - sum(flat_iter(max_ind:end) <= half_height)+1;
+
+ % Keep bandwidth estimation from the shortest side.
+ % We grab shortest to avoid estimating very large std from overalapping peaks.
+ % Grab the shortest side, ignoring a side if the half max was not found.
+ % Note: will fail if both le & ri ind's end up as None (probably shouldn't happen).
+ short_side = min(abs([le_ind,ri_ind]-max_ind));
+
+ % Estimate std from FWHM. Calculate FWHM, converting to Hz, get guess std from FWHM
+ fwhm = short_side * 2 * (freqs(2)-freqs(1));
+ guess_std = fwhm / (2 * sqrt(2 * log(2)));
+
+ % Check that guess std isn't outside preset std limits; restrict if so.
+ % Note: without this, curve_fitting fails if given guess > or < bounds.
+ if guess_std < gauss_std_limits(1)
+ guess_std = gauss_std_limits(1);
+ end
+ if guess_std > gauss_std_limits(2)
+ guess_std = gauss_std_limits(2);
+ end
+
+ % Collect guess parameters.
+ guess_params(guess,:) = [guess_freq, guess_height, guess_std];
+
+ % Subtract best-guess gaussian.
+ peak_gauss = gaussian(freqs, guess_freq, guess_height, guess_std);
+ flat_iter = flat_iter - peak_gauss;
+
+ end
+ % Remove unused guesses
+ guess_params(guess_params(:,1) == 0,:) = [];
+
+ % Check peaks based on edges, and on overlap
+ % Drop any that violate requirements.
+ guess_params = drop_peak_cf(guess_params, bordThresh, [min(freqs) max(freqs)]);
+ guess_params = drop_peak_overlap(guess_params, proxThresh);
+
+ case 'cauchy' % cauchy only
+ peak_function = @cauchy; % Identify peaks as cauchy
+ guess_params = zeros(max_n_peaks, 3);
+ for guess = 1:max_n_peaks
+ max_ind = find(flat_iter == max(flat_iter));
+ max_height = flat_iter(max_ind);
+ if max_height <= peak_threshold * std(flat_iter)
+ break
+ end
+ guess_freq = freqs(max_ind);
+ guess_height = max_height;
+ if guess_height <= min_peak_height
+ break
+ end
+ half_height = 0.5 * max_height;
+ le_ind = sum(flat_iter(1:max_ind) <= half_height);
+ ri_ind = length(flat_iter) - sum(flat_iter(max_ind:end) <= half_height);
+ short_side = min(abs([le_ind,ri_ind]-max_ind));
+
+ % Estimate gamma from FWHM. Calculate FWHM, converting to Hz, get guess gamma from FWHM
+ fwhm = short_side * 2 * (freqs(2)-freqs(1));
+ guess_gamma = fwhm/2;
+ % Check that guess gamma isn't outside preset limits; restrict if so.
+ % Note: without this, curve_fitting fails if given guess > or < bounds.
+ if guess_gamma < gauss_std_limits(1)
+ guess_gamma = gauss_std_limits(1);
+ end
+ if guess_gamma > gauss_std_limits(2)
+ guess_gamma = gauss_std_limits(2);
+ end
+
+ % Collect guess parameters.
+ guess_params(guess,:) = [guess_freq(1), guess_height, guess_gamma];
+
+ % Subtract best-guess cauchy.
+ peak_cauchy = cauchy(freqs, guess_freq(1), guess_height, guess_gamma);
+ flat_iter = flat_iter - peak_cauchy;
+
+ end
+ guess_params(guess_params(:,1) == 0,:) = [];
+ guess_params = drop_peak_cf(guess_params, proxThresh, [min(freqs) max(freqs)]);
+ guess_params = drop_peak_overlap(guess_params, proxThresh);
+
+ end
+end
+
+function model_params = est_fit(guess_params, freqs, flat_spec, gauss_std_limits, peakType, guess_weight,hOT)
+% Iteratively fit peaks to flattened spectrum.
+%
+% Parameters
+% ----------
+% freqs : 1xn array
+% Frequency values for the power spectrum, in linear scale.
+% flat_iter : 1xn array
+% Flattened (aperiodic removed) power spectrum.
+% max_n_peaks : double
+% Maximum number of gaussians to fit within the spectrum.
+% peak_threshold : double
+% Threshold (in standard deviations of noise floor) to detect a peak.
+% min_peak_height : double
+% Minimum height of a peak (in log10).
+% gauss_std_limits : 1x2 double
+% Limits to gaussian (cauchy) standard deviation (gamma) when detecting a peak.
+% proxThresh : double
+% Minimum distance between two peaks, in st. dev. (gamma) of peaks.
+% peakType : {'gaussian', 'cauchy', 'both'}
+% Which types of peaks are being fitted
+% guess_weight : {'none', 'weak', 'strong'}
+% Parameter to weigh initial estimates during optimization (None, Weak, or Strong)
+% hOT : 0 or 1
+% Defines whether to use constrained optimization, fmincon, or
+% basic simplex, fminsearch.
+%
+% Returns
+% -------
+% guess_params : mx3 array, where m = No. of peaks.
+% Parameters that define the peak fit(s). Each row is a peak, as [mean, height, st. dev. (gamma)].
+
+ switch peakType
+ case 'gaussian' % gaussian only
+
+ % If there are peak guesses, fit the peaks, and sort results.
+ if ~isempty(guess_params)
+ model_params = fit_peak_guess(guess_params, freqs, flat_spec, 1, guess_weight, gauss_std_limits,hOT);
+ else
+ model_params = [];
+ end
+
+ case 'cauchy' % cauchy only
+
+ % If there are peak guesses, fit the peaks, and sort results.
+ if ~isempty(guess_params)
+ model_params = fit_peak_guess(guess_params, freqs, flat_spec, 2, guess_weight, gauss_std_limits,hOT);
+ else
+ model_params = [];
+ end
+
+ end
+end
+
+
+
function guess = drop_peak_cf(guess, bw_std_edge, freq_range)
% Check whether to drop peaks based on center's proximity to the edge of the spectrum.
%
@@ -850,6 +1206,9 @@
end
% Drop any peaks guesses that overlap too much, based on threshold.
guess(drop_inds,:) = [];
+
+ % Readjust order by amplitude
+ guess = sortrows(guess,2,'descend');
end
function peak_params = fit_peak_guess(guess, freqs, flat_spec, peak_type, guess_weight, std_limits, hOT)
@@ -880,20 +1239,48 @@
if hOT % Use OptimToolbox for fmincon
- options = optimset('Display', 'off', 'TolX', 1e-3, 'TolFun', 1e-5, ...
- 'MaxFunEvals', 3000, 'MaxIter', 3000); % Tuned options
lb = [max([ones(size(guess,1),1).*freqs(1) guess(:,1)-guess(:,3)*2],[],2),zeros(size(guess(:,2))),ones(size(guess(:,3)))*std_limits(1)];
ub = [min([ones(size(guess,1),1).*freqs(end) guess(:,1)+guess(:,3)*2],[],2),inf(size(guess(:,2))),ones(size(guess(:,3)))*std_limits(2)];
+ options = optimset('Display', 'off', 'TolX', 1e-3, 'TolFun', 1e-5, ...
+ 'MaxFunEvals', 5000, 'MaxIter', 5000); % Tuned options
peak_params = fmincon(@error_model_constr,guess,[],[],[],[], ...
lb,ub,[],options,freqs,flat_spec, peak_type);
else % Use basic simplex approach, fminsearch, with guess_weight
- options = optimset('Display', 'off', 'TolX', 1e-4, 'TolFun', 1e-5, ...
+ options = optimset('Display', 'off', 'TolX', 1e-5, 'TolFun', 1e-7, ...
'MaxFunEvals', 5000, 'MaxIter', 5000);
peak_params = fminsearch(@error_model,...
guess, options, freqs, flat_spec, peak_type, guess, guess_weight);
end
end
+function model_fit = build_model(freqs, ap_pars, ap_type, pk_pars, peak_function)
+% Builds a full spectral model from parameters.
+%
+% Parameters
+% ----------
+% freqs : 1xn array
+% Frequency values for the power spectrum, in linear scale.
+% ap_pars : 1xm array
+% Parameter estimates for aperiodic fit.
+% pk_pars : kx3 array, where k = No. of peaks.
+% Guess parameters for peak fits.
+% pk_type : {'gaussian', 'cauchy', 'best'}
+% Which types of peaks are being fitted.
+%
+% Returns
+% -------
+% model_fit : 1xn array
+% Model power spectrum, in log10-space
+
+ ap_fit = gen_aperiodic(freqs, ap_pars, ap_type);
+ model_fit = ap_fit;
+ if length(pk_pars) > 1
+ for peak = 1:size(pk_pars,1)
+ model_fit = model_fit + peak_function(freqs,pk_pars(peak,1),...
+ pk_pars(peak,2),pk_pars(peak,3));
+ end
+ end
+end
%% ===== ERROR FUNCTIONS =====
function err = error_expo_nk_function(params,xs,ys)
@@ -945,6 +1332,25 @@
err = sum((yVals - fitted_vals).^2);
end
+function err = err_fm_constr(params, xVals, yVals, aperiodic_mode, peak_type)
+ switch (aperiodic_mode)
+ case 'fixed' % no knee
+ npk = (length(params)-2)/3;
+ fitted_vals = -log10(xVals.^params(2)) + params(1);
+ case 'knee'
+ npk = (length(params)-3)/3;
+ fitted_vals = params(1) - log10(abs(params(2)) +xVals.^params(3));
+ end
+ for set = 1:npk
+ switch peak_type
+ case 'gaussian' % gaussian only
+ fitted_vals = fitted_vals + gaussian(xVals, params(3.*set), params(3*set+1), params(3*set+2));
+ case 'cauchy' % Cauchy
+ fitted_vals = fitted_vals + cauchy(xVals, params(3.*set), params(3*set+1), params(3*set+2));
+ end
+ end
+ err = sum((yVals - fitted_vals).^2);
+end
%% ===================================================================================
@@ -954,7 +1360,6 @@
% ===== EXTRACT PEAKS =====
% Organize/extract peak components from FOOOF models
nChan = numel(ChanNames);
- maxEnt = nChan * max_peaks;
switch sort_type
case 'param'
% Initialize output struct
@@ -1042,4 +1447,3 @@
eStats(chan).frequency_wise_error = abs(spec-fspec);
end
end
-
diff --git a/toolbox/process/functions/process_fooof_py.m b/toolbox/process/functions/process_fooof_py.m
index 76c7f73d0..2b222cbe6 100644
--- a/toolbox/process/functions/process_fooof_py.m
+++ b/toolbox/process/functions/process_fooof_py.m
@@ -26,7 +26,8 @@
%% ===== PYTHON FOOOF =====
-function [fs, fg] = FOOOF_python(TF, Freqs, opt)
+function [fs, fg, errMsg] = FOOOF_python(TF, Freqs, opt)
+ errMsg = '';
% Import python modules
modules = py.sys.modules;
modules = string(cell(py.list(modules.keys())));
diff --git a/toolbox/process/functions/process_ft_mtmconvol.m b/toolbox/process/functions/process_ft_mtmconvol.m
index d4457d494..759cf0b86 100644
--- a/toolbox/process/functions/process_ft_mtmconvol.m
+++ b/toolbox/process/functions/process_ft_mtmconvol.m
@@ -75,6 +75,8 @@
sProcess.options.mt_taper.Type = 'combobox_label';
sProcess.options.mt_taper.Value = {'dpss', {'dpss', 'hanning', 'rectwin', 'sine'; ...
'dpss', 'hanning', 'rectwin', 'sine'}};
+ sProcess.options.mt_taper.Controller.dpss = 'ModFreq';
+ sProcess.options.mt_taper.Controller.sine = 'ModFreq';
% Options: Frequencies
sProcess.options.mt_frequencies.Comment = 'Frequencies (start:step:stop): ';
sProcess.options.mt_frequencies.Type = 'text';
@@ -83,6 +85,7 @@
sProcess.options.mt_freqmod.Comment = 'Modulation factor: ';
sProcess.options.mt_freqmod.Type = 'value';
sProcess.options.mt_freqmod.Value = {10, ' ', 0};
+ sProcess.options.mt_freqmod.Class = 'ModFreq';
% Options: Time resolution
sProcess.options.mt_timeres.Comment = 'Time resolution: ';
sProcess.options.mt_timeres.Type = 'value';
diff --git a/toolbox/process/functions/process_headmodel_exclusionzone.m b/toolbox/process/functions/process_headmodel_exclusionzone.m
new file mode 100644
index 000000000..dbb03df7b
--- /dev/null
+++ b/toolbox/process/functions/process_headmodel_exclusionzone.m
@@ -0,0 +1,231 @@
+function varargout = process_headmodel_exclusionzone( varargin )
+% PROCESS_HEADMODEL_EXCLUSIONZONE: Remove leadfields and sources within the exclusion zone.
+%
+% USAGE: OutputFiles = process_headmodel_exclusionzone('Run', sProcess, sInputs)
+% [newHMMat, errMsg] = process_headmodel_exclusionzone('Compute', HMMat, ChannelMat, Modality, ExclusionRadius)
+% process_headmodel_exclusionzone('ComputeInteractive', HMFile, Modality, iStudy)
+
+% @=============================================================================
+% 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: Takfarinas Medani, Yash Shashank Vakilna, Raymundo Cassani 2024
+
+eval(macro_method);
+end
+
+
+%% ===== GET DESCRIPTION =====
+function sProcess = GetDescription()
+ % Description the process
+ sProcess.Comment = 'Head model exclusion zone';
+ sProcess.Category = 'Custom';
+ sProcess.SubGroup = 'Sources';
+ sProcess.Index = 321;
+ sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/TutVolSource#Exclusion_zone';
+ % Definition of the input accepted by this process
+ sProcess.InputTypes = {'data', 'raw'};
+ sProcess.OutputTypes = {'data', 'raw'};
+ sProcess.nInputs = 1;
+ sProcess.nMinFiles = 0;
+ sProcess.isSeparator = 1;
+ % === Usage label
+ sProcess.options.usage.Comment = GetUsageText();
+ sProcess.options.usage.Type = 'label';
+ sProcess.options.usage.Value = '';
+ % === Modality
+ sProcess.options.modality.Comment = 'Modality of sensors: ';
+ sProcess.options.modality.Type = 'text';
+ sProcess.options.modality.Value = 'SEEG';
+ sProcess.options.modality.InputTypes = {'data', 'raw'};
+ % === Exclusion radius
+ sProcess.options.exclusionradius.Comment = 'Exclusion distance: ';
+ sProcess.options.exclusionradius.Type = 'value';
+ sProcess.options.exclusionradius.Value = {3,'mm',2};
+end
+
+
+%% ===== RUN =====
+function OutputFiles = Run(sProcess, sInputs) %#ok
+ OutputFiles = {};
+ % ===== GET OPTIONS =====
+ % Modality
+ Modality = [];
+ if isfield(sProcess.options, 'modality') && ~isempty(sProcess.options.modality) && ~isempty(sProcess.options.modality.Value)
+ Modality = sProcess.options.modality.Value;
+ end
+ % Exclusion radius
+ ExclusionRadius = sProcess.options.exclusionradius.Value{1} ./ 1000;
+ if ExclusionRadius <= 0
+ bst_report('Error', sProcess, [], 'You must define an exclusion distance greater than zero.');
+ return;
+ end
+ OutputFiles = sInputs;
+ % Get unique channel files
+ [sChannels, iChanStudies] = bst_get('ChannelForStudy', unique([sInputs.iStudy]));
+ % Check if there are channel files everywhere
+ if (length(sChannels) ~= length(iChanStudies))
+ bst_report('Error', sProcess, sInputs, ['Some of the input files are not associated with a channel file.' 10 'Please import the channel files first.']);
+ return;
+ end
+ % Keep only once each channel file
+ iChanStudies = unique(iChanStudies);
+ newDefaultHM = {}; % Pairs [iStudy, newHeadmodelFileName]
+
+ % ===== COMPUTE EXCLUSION ZONE =====
+ % Start the progress bar
+ bst_progress('start', 'Leadfield exclusion zone', 'Computing leadfield exclusion zone...', 0, 100);
+ barStep = 100 ./ length(iChanStudies);
+ for ix = 1 : length(iChanStudies)
+ bst_progress('set', barStep * ix);
+ iStudy = iChanStudies(ix);
+ % Get default headmodel for study
+ sHeadmodel = bst_get('HeadModelForStudy', iStudy);
+ HeadmodelMat = in_bst_headmodel(sHeadmodel.FileName);
+ % Check that is volumetric
+ if ~strcmpi(HeadmodelMat.HeadModelType, 'volume')
+ bst_report('Error', sProcess, [], 'Head model must be volumetric');
+ continue
+ end
+ % Load Channel file
+ sChannel = bst_get('ChannelForStudy', iStudy);
+ ChannelMat = in_bst_channel(sChannel.FileName, 'Channel');
+ % Compute exclusion zone
+ [newHeadmodelMat, errMsg] = Compute(HeadmodelMat, ChannelMat, Modality, ExclusionRadius);
+ if ~isempty(errMsg)
+ bst_report('Error', sProcess, [], errMsg);
+ continue;
+ end
+ % Add to database
+ newHeadmodelFileName = db_add(iStudy, newHeadmodelMat);
+ newDefaultHM{end+1, 1} = iStudy;
+ newDefaultHM{end, 2} = newHeadmodelFileName;
+ end
+ % Set exclusion-zone headmodel as default in study
+ for ix = 1 : size(newDefaultHM, 1)
+ iStudy = newDefaultHM{ix, 1};
+ newHeadmodelFileName = newDefaultHM{ix, 2};
+ sStudy = bst_get('Study', iStudy);
+ iHeadModel = find(strcmpi({sStudy.HeadModel.FileName}, newHeadmodelFileName));
+ sStudy.iHeadModel = iHeadModel;
+ bst_set('Study', iStudy, sStudy);
+ end
+ % Close progress bar
+ bst_progress('stop');
+ panel_protocols('UpdateTree');
+end
+
+
+%% ===== COMPUTE =====
+function [newHeadmodelMat, errMsg] = Compute(HeadmodelMat, ChannelMat, Modality, ExclusionRadius)
+ % Computes the exclusion zone in the HeadmodelMat based on the sensor locations, and ExclusionRadius. Only for volumetric grids
+
+ newHeadmodelMat = [];
+ errMsg = '';
+ % Check that is volumetric
+ if ~strcmpi(HeadmodelMat.HeadModelType, 'volume')
+ errMsg = 'Head model must be volumetric';
+ return
+ end
+ % Find selected channels
+ iChannels = channel_find(ChannelMat.Channel, Modality);
+ if isempty(iChannels)
+ errMsg = ['Could not load any sensor for modality: ' Modality];
+ return;
+ end
+ % Get channel locations
+ channelLocs = [ChannelMat.Channel(iChannels).Loc]';
+ % Indices of grid points in exclusion zone
+ iBadVertices = [];
+ for iLocation = 1 : size(channelLocs, 1)
+ iBadVerticesLoc = find(sqrt(sum(bst_bsxfun(@minus, HeadmodelMat.GridLoc, channelLocs(iLocation, :)) .^ 2, 2)) <= ExclusionRadius);
+ iBadVertices = [iBadVertices, iBadVerticesLoc'];
+ end
+ if isempty(iBadVertices)
+ errMsg = 'There is no grid points to remove in the exclusion zone.';
+ return
+ end
+ iBadVertices = unique(iBadVertices);
+ % Indices to gain indices
+ iBadGains = sort([3*iBadVertices, 3*iBadVertices - 1, 3*iBadVertices - 2]);
+ % New head model with exclusion zone
+ newHeadmodelMat = HeadmodelMat;
+ newHeadmodelMat.GridLoc(iBadVertices, :) = [];
+ newHeadmodelMat.Gain(:, iBadGains) = [];
+ exclusionZoneComment = sprintf('exclusion_zone %.2f mm', ExclusionRadius * 1000);
+ newHeadmodelMat.Comment = [HeadmodelMat.Comment, ' | ' exclusionZoneComment];
+ newHeadmodelMat = bst_history('add', newHeadmodelMat, ['Apply ' exclusionZoneComment]);
+end
+
+
+%% ===== COMPUTE/INTERACTIVE =====
+function ComputeInteractive(HeadmodelFileName, Modality, iStudy)
+ windowTitle = 'Leadfield exclusion zone';
+ HeadmodelMat = in_bst_headmodel(HeadmodelFileName);
+ % Check that is volumetric
+ if ~strcmpi(HeadmodelMat.HeadModelType, 'volume')
+ bst_error('Head model must be volumetric', windowTitle, 0);
+ return
+ end
+ % Ask user the distance of the exclusion zone
+ [res, isCancel] = java_dialog('input', ['' GetUsageText(Modality) '
' ...
+ 'Exclusion distance (mm):'], ...
+ windowTitle, [], sprintf('%.2f', 1));
+ if isCancel || isempty(res)
+ return
+ end
+ % Exclusion radius in m
+ ExclusionRadius = str2double(res) ./ 1000;
+ if ExclusionRadius <= 0
+ bst_error('You must define an exclusion distance greater than zero.', 'Leadfield exclusion zone', 0);
+ return;
+ end
+ % Start the progress bar
+ bst_progress('start', windowTitle, 'Computing leadfield exclusion zone...');
+ % Load Channel file
+ sChannel = bst_get('ChannelForStudy', iStudy);
+ ChannelMat = in_bst_channel(sChannel.FileName, 'Channel');
+ % Compute exclusion zone
+ [newHeadmodelMat, errMsg] = Compute(HeadmodelMat, ChannelMat, Modality, ExclusionRadius);
+ if ~isempty(errMsg)
+ bst_progress('stop');
+ bst_error(errMsg, windowTitle, 0);
+ return;
+ end
+ % Add to database
+ newHeadmodelFileName = db_add(iStudy, newHeadmodelMat);
+ % Set as default headmodel
+ sStudy = bst_get('Study', iStudy);
+ iHeadModel = find(strcmpi({sStudy.HeadModel.FileName}, newHeadmodelFileName));
+ sStudy.iHeadModel = iHeadModel;
+ bst_set('Study', iStudy, sStudy);
+ panel_protocols('UpdateTree');
+ % Close progress bar
+ bst_progress('stop');
+end
+
+
+%% ===== GET USAGE TEXT =====
+function usageHtmlText = GetUsageText(Modality)
+ if nargin < 1 || isempty(Modality)
+ Modality = '';
+ end
+ usageHtmlText = ['Define the exclusion zone around the ' Modality ' sensors.
' ...
+ 'Warning This approach will remove the leadfield vectors located near the sensors. ' ...
+ 'This method also remove the sources located in the exlusion zone '...
+ 'The exclusion zone is defined by the distance from the sensors to the sources.'];
+end
\ No newline at end of file
diff --git a/toolbox/process/functions/process_headpoints_refine.m b/toolbox/process/functions/process_headpoints_refine.m
index 9cf995a6f..f6e4f6db9 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):';
@@ -61,13 +61,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);
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
diff --git a/toolbox/process/functions/process_import_bids.m b/toolbox/process/functions/process_import_bids.m
index 5997290c5..551ff0df2 100644
--- a/toolbox/process/functions/process_import_bids.m
+++ b/toolbox/process/functions/process_import_bids.m
@@ -473,7 +473,7 @@
end
OPTIONS.nVertices = str2double(OPTIONS.nVertices);
end
- % If there are fiducials define: record these, to use them when importing FreeSurfer (or other) segmentations
+ % If there are fiducials defined: record these, to use them when importing FreeSurfer (or other) segmentations
if ~isempty(SubjectFidMriFile{iSubj})
sMriFid = in_mri(SubjectFidMriFile{iSubj}, 'ALL', 0);
else
@@ -533,7 +533,7 @@
errorMsg = [errorMsg, 10, errMsg];
end
% Generate head surface
- tess_isohead(iSubject, 10000, 0, 2);
+ tess_isohead(iSubject, 15000, 0, 0);
else
MrisToRegister{end+1} = BstMriFile;
end
@@ -751,6 +751,10 @@
end
% Coordinates can be linked to the scanner/world coordinates of a specific volume in the dataset
if isfield(sCoordsystem, 'IntendedFor') && ~isempty(sCoordsystem.IntendedFor)
+ % Quick bug fix: Only check first file for now if array
+ if iscell(sCoordsystem.IntendedFor)
+ sCoordsystem.IntendedFor = sCoordsystem.IntendedFor{1};
+ end
if file_exist(bst_fullfile(BidsDir, sCoordsystem.IntendedFor))
% Check whether the IntendedFor files is already imported as a volume
if ~isempty(MriMatchOrigImport)
diff --git a/toolbox/process/functions/process_import_channel.m b/toolbox/process/functions/process_import_channel.m
index 8ad96fa86..b2a727576 100644
--- a/toolbox/process/functions/process_import_channel.m
+++ b/toolbox/process/functions/process_import_channel.m
@@ -66,8 +66,8 @@
'ChannelIn'}; % DefaultFormats
% Option: Default channel files
sProcess.options.usedefault.Comment = 'Or use default:';
- sProcess.options.usedefault.Type = 'combobox';
- sProcess.options.usedefault.Value = {1, strList};
+ sProcess.options.usedefault.Type = 'combobox_label';
+ sProcess.options.usedefault.Value = {'', cat(1, strList, strList)};
% Separator
sProcess.options.separator.Type = 'separator';
sProcess.options.separator.Comment = ' ';
@@ -100,14 +100,31 @@
% Get filename to import
ChannelFile = sProcess.options.channelfile.Value{1};
FileFormat = sProcess.options.channelfile.Value{2};
+
+ % HANDLING ISSUE #591: https://github.com/brainstorm-tools/brainstorm3/issues/591
+ % The list of default caps is changing between versions of Brainstorm, therefore the index of a "combobox" option can't be considered a reliable information.
+ % On 6-Jan-2022: The option "usedefault" was changed from type "combobox" to "combobox_label" and the use of previous syntax is now an error.
+ % Users with existing scripts will get an error and will be requested to update their scripts.
+ if isempty(ChannelFile) && ~ischar(sProcess.options.usedefault.Value{1})
+ bst_report('Error', sProcess, [], [...
+ 'On 6-Jan-2023, the option "usedefault" of process_channel_add loc was changed from type "combobox" to "combobox_label".' 10 ...
+ 'This parameter was previously an integer, indicating an index in a list that unfortunately changes across versions of Brainstorm.' 10 ...
+ 'The value now must be a string, which points at a specific default EEG cap with no amibiguity.' 10 10 ...
+ 'Scripts generated before 30-Jun-2022 and executed with a version of Brainstorm posterior to 30-Jun-2022' 10 ...
+ 'might have been selecting the wrong EEG cap, and should be fixed and executed again.' 10 10 ...
+ 'If you get this error, you must edit your processing script:' 10 ...
+ 'Use the pipeline editor to generate a new script to call process_channel_add.' 10 10 ...
+ 'More information in GitHub issue #591: ' 10 ...
+ 'https://github.com/brainstorm-tools/brainstorm3/issues/591']);
+ return
+ end
% ===== GET DEFAULT =====
if isempty(ChannelFile)
% Get registered Brainstorm EEG defaults
bstDefaults = bst_get('EegDefaults');
% Get default channel file
- iSel = sProcess.options.usedefault.Value{1};
- strDef = sProcess.options.usedefault.Value{2}{iSel};
+ strDef = sProcess.options.usedefault.Value{1};
% If there is something selected
if ~isempty(strDef)
% Format: "group: name"
diff --git a/toolbox/process/functions/process_inverse.m b/toolbox/process/functions/process_inverse.m
index 2d2442866..cd24be804 100644
--- a/toolbox/process/functions/process_inverse.m
+++ b/toolbox/process/functions/process_inverse.m
@@ -694,7 +694,10 @@
OPTIONS.NoiseCovRaw = NoiseCov;
% Call the mem solver
[Results, OPTIONS] = be_main(HeadModel, OPTIONS);
- Results.nComponents = round(max(size(Results.ImageGridAmp,1),size(Results.ImagingKernel,1)) / nSources);
+ if ~isfield(Results, 'nComponents') || isempty(Results.nComponents)
+ Results.nComponents = round(max(size(Results.ImageGridAmp,1),size(Results.ImagingKernel,1)) / nSources);
+ end
+
% Get outputs
DataFile = OPTIONS.DataFile;
Time = OPTIONS.DataTime;
diff --git a/toolbox/process/functions/process_inverse_2018.m b/toolbox/process/functions/process_inverse_2018.m
index 1da40f777..233b99826 100644
--- a/toolbox/process/functions/process_inverse_2018.m
+++ b/toolbox/process/functions/process_inverse_2018.m
@@ -498,6 +498,12 @@
errMessage = [errMessage 'The noise covariance contains NaN values. Please re-calculate it after tagging correctly the bad channels in the recordings.' 10];
break;
end
+ % Check that bad channels in noise covariance are the same as bad channels in recordings
+ badChNoiseCov_goodChRecs = intersect(find(and(~any(NoiseCovMat.NoiseCov,1), ~any(NoiseCovMat.NoiseCov,2)')), GoodChannel);
+ if ~isempty(badChNoiseCov_goodChRecs)
+ errMessage = [errMessage 'Bad channels in noise covariance are different from bad channels in recordings.' 10 'Please re-calculate it after tagging correctly the bad channels in the recordings.' 10];
+ break;
+ end
% % Divide noise covariance by number of trials (DEPRECATED IN THIS VERSION)
% if ~isempty(nAvg) && (nAvg > 1)
% NoiseCovMat.NoiseCov = NoiseCovMat.NoiseCov ./ nAvg;
@@ -512,6 +518,11 @@
errMessage = [errMessage 'The data covariance contains NaN values. Please re-calculate it after tagging correctly the bad channels in the recordings.' 10];
break;
end
+ badChDataCov_goodChRecs = intersect(find(and(~any(DataCovMat.NoiseCov,1), ~any(DataCovMat.NoiseCov,2)')), GoodChannel);
+ if ~isempty(badChDataCov_goodChRecs)
+ errMessage = [errMessage 'Bad channels in data covariance are different from bad channels in recordings.' 10 'Please re-calculate it after tagging correctly the bad channels in the recordings.' 10];
+ break;
+ end
% % Divide data covariance by number of trials
% if isempty(nAvg) && (nAvg > 1)
% DataCovMat.NoiseCov = DataCovMat.NoiseCov ./ nAvg;
@@ -525,7 +536,7 @@
break;
end
% Shrinkage: Require the FourthMoment matrix
- if strcmpi(OPTIONS.NoiseMethod, 'shrink') && ...
+ if ~strcmpi(OPTIONS.InverseMethod, 'mem') && strcmpi(OPTIONS.NoiseMethod, 'shrink') && ...
((~isempty(DataCovMat) && (~isfield(DataCovMat, 'FourthMoment') || isempty(DataCovMat.FourthMoment))) || ...
(~isempty(NoiseCovMat) && (~isfield(NoiseCovMat, 'FourthMoment') || isempty(NoiseCovMat.FourthMoment))))
errMessage = [errMessage 'Please recalculate the noise and data covariance matrices for using the "automatic shrinkage" option.' 10];
@@ -683,7 +694,10 @@
OPTIONS.FunctionName = 'mem';
% Call the mem solver
[Results, OPTIONS] = be_main(HeadModel, OPTIONS);
- Results.nComponents = round(max(size(Results.ImageGridAmp,1),size(Results.ImagingKernel,1)) / nSources);
+ if ~isfield(Results, 'nComponents') || isempty(Results.nComponents)
+ Results.nComponents = round(max(size(Results.ImageGridAmp,1),size(Results.ImagingKernel,1)) / nSources);
+ end
+
% Get outputs
DataFile = OPTIONS.DataFile;
Time = OPTIONS.DataTime;
diff --git a/toolbox/process/functions/process_movefile.m b/toolbox/process/functions/process_movefile.m
index b3cd3c134..a7789e8d2 100644
--- a/toolbox/process/functions/process_movefile.m
+++ b/toolbox/process/functions/process_movefile.m
@@ -37,8 +37,8 @@
sProcess.Index = 1024;
sProcess.Description = '';
% Definition of the input accepted by this process
- sProcess.InputTypes = {'raw', 'data', 'results', 'timefreq', 'matrix'};
- sProcess.OutputTypes = {'raw', 'data', 'results', 'timefreq', 'matrix'};
+ sProcess.InputTypes = {'data', 'results', 'timefreq', 'matrix'};
+ sProcess.OutputTypes = {'data', 'results', 'timefreq', 'matrix'};
sProcess.nInputs = 1;
sProcess.nMinFiles = 1;
sProcess.isSeparator = 1;
diff --git a/toolbox/process/functions/process_mri_deface.m b/toolbox/process/functions/process_mri_deface.m
index 268107649..6d74d62e0 100644
--- a/toolbox/process/functions/process_mri_deface.m
+++ b/toolbox/process/functions/process_mri_deface.m
@@ -185,6 +185,9 @@
MriHead = [];
for iFile = 1:length(MriFiles)
bst_progress('text', 'Loading input MRI...');
+ % Check if volume is CT
+ tagFileCt = '_volct';
+ isCt = ~isempty(strfind(MriFiles{iFile}, tagFileCt));
% Get subject index
[sSubject, iSubject, iAnatomy] = bst_get('MriFile', MriFiles{iFile});
% If MRI was already defaced: skip
@@ -306,7 +309,16 @@
bst_memory('UnloadMri', MriFile);
% Add new file
else
- DefacedFiles{end+1} = db_add(iSubject, sMri, 0);
+ tmp = db_add(iSubject, sMri, 0);
+ % Add file tag for CT volume
+ if isCt
+ [fPath, fBase, fExt] = bst_fileparts(file_fullpath(tmp));
+ fBase = [fBase, tagFileCt, fExt];
+ tmpNew = bst_fullfile(fPath, fBase);
+ file_move(file_fullpath(tmp), tmpNew);
+ tmp = file_short(tmpNew);
+ end
+ DefacedFiles{end+1} = tmp;
iAnatomy = length(sSubject.Anatomy) + 1;
end
% Update database registration
@@ -347,7 +359,7 @@
bst_set('Subject', iSubject, sSubject);
% Compute new head surface
sSubject.Anatomy(sSubject.iAnatomy).FileName;
- tess_isohead(MriHead, 10000, 0, 2, HeadComment);
+ tess_isohead(MriHead, 10000, 0, 2, [], HeadComment);
end
end
diff --git a/toolbox/process/functions/process_mtrf_train.m b/toolbox/process/functions/process_mtrf_train.m
new file mode 100644
index 000000000..ec091aee0
--- /dev/null
+++ b/toolbox/process/functions/process_mtrf_train.m
@@ -0,0 +1,173 @@
+function varargout = process_mtrf_train( varargin )
+% process_mtrf_train: Fits an encoding/decoding model using mTRF-Toolbox
+
+% @=============================================================================
+% 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: Anna Zaidi, 2024
+% Raymundo Cassani, 2024
+
+eval(macro_method);
+end
+
+%% ===== GET DESCRIPTION =====
+function sProcess = GetDescription()
+ % Description of the process
+ sProcess.Comment = 'Temporal Response Function Analyis';
+ sProcess.Category = 'Custom';
+ sProcess.SubGroup = 'Encoding';
+ sProcess.Index = 702;
+ sProcess.InputTypes = {'data'};
+ sProcess.OutputTypes = {'matrix'};
+ sProcess.nInputs = 1;
+ sProcess.nMinFiles = 1;
+ sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/MultivariateTemporalResponse';
+ % === Sensor types
+ sProcess.options.sensortypes.Comment = 'Sensor types or names (empty=all): ';
+ sProcess.options.sensortypes.Type = 'text';
+ sProcess.options.sensortypes.Value = 'MEG, EEG';
+ sProcess.options.sensortypes.InputTypes = {'data', 'raw'};
+ % Event name
+ sProcess.options.labelevt.Comment = 'For multiple events: separate them with commas';
+ sProcess.options.labelevt.Type = 'label';
+ sProcess.options.eventname.Comment = 'Event names: ';
+ sProcess.options.eventname.Type = 'text';
+ sProcess.options.eventname.Value = '';
+ % Minimum time lag
+ sProcess.options.tmin.Comment = 'Minimun time lag:';
+ sProcess.options.tmin.Type = 'value';
+ sProcess.options.tmin.Value = {-100, 'ms', 0};
+ % Maximum time lag
+ sProcess.options.tmax.Comment = 'Maximum time lag:';
+ sProcess.options.tmax.Type = 'value';
+ sProcess.options.tmax.Value = {100, 'ms', 0};
+end
+
+%% ===== FORMAT COMMENT =====
+function Comment = FormatComment(sProcess)
+ Comment = sProcess.Comment;
+end
+
+%% ===== RUN =====
+function OutputFiles = Run(sProcess, sInput)
+ % Initialize output file list
+ OutputFiles = {};
+
+ % Install/load mTRF-Toolbox as plugin
+ if ~exist('mTRFtrain', 'file')
+ [isInstalled, errMsg] = bst_plugin('Install', 'mtrf');
+ if ~isInstalled
+ error(errMsg);
+ end
+ end
+
+ % ===== GET OPTIONS =====
+ % Sensor types
+ sensorTypes = [];
+ if isfield(sProcess.options, 'sensortypes') && ~isempty(sProcess.options.sensortypes) && ~isempty(sProcess.options.sensortypes.Value)
+ sensorTypes = sProcess.options.sensortypes.Value;
+ end
+ % Get event names
+ evtNames = strtrim(str_split(sProcess.options.eventname.Value, ',;'));
+ if isempty(evtNames)
+ bst_report('Error', sProcess, [], 'No events were provided.');
+ return;
+ end
+ % Get minimum time lag (ms)
+ tmin = sProcess.options.tmin.Value{1};
+ if isempty(tmin) || ~isnumeric(tmin) || isnan(tmin)
+ bst_report('Error', sProcess, sInput, 'Invalid tmin.');
+ return;
+ end
+ tmin = tmin * 1000;
+ % Get maximum time lag (ms)
+ tmax = sProcess.options.tmax.Value{1};
+ if isempty(tmax) || ~isnumeric(tmax) || isnan(tmax)
+ bst_report('Error', sProcess, sInput, 'Invalid tmax.');
+ return;
+ end
+ tmax = tmax * 1000;
+ % Check for exactly one input file
+ if length(sInput) ~= 1
+ bst_report('Error', sProcess, sInput, 'This process requires exactly one input file.');
+ return;
+ end
+
+ % Load file
+ DataMat = in_bst_data(sInput.FileName);
+ if isempty(DataMat) || ~isfield(DataMat, 'F') || isempty(DataMat.F) || ~isnumeric(DataMat.F)
+ bst_report('Error', sProcess, sInput, 'EEG data is empty or not a numeric matrix.');
+ return;
+ end
+ % Sampling frequency (Hz)
+ fs = 1 ./ (DataMat.Time(2) - DataMat.Time(1));
+ nSamples = size(DataMat.F,2);
+ % Load channel file
+ ChannelFile = sInput.ChannelFile;
+ ChannelMat = in_bst_channel(ChannelFile);
+
+ % Select sensors
+ if ~isempty(sensorTypes)
+ % Find selected channels
+ iChannels = channel_find(ChannelMat.Channel, sensorTypes);
+ if isempty(iChannels)
+ bst_report('Error', sProcess, sInput, 'Could not load any sensor from the input file. Check the sensor selection.');
+ return;
+ end
+ % Keep only selected channels
+ F = DataMat.F(iChannels, :);
+ channelNames = {ChannelMat.Channel(iChannels).Name}';
+ else
+ F = DataMat.F;
+ channelNames = {ChannelMat.Channel.Name}';
+ end
+
+ % mTRF train for each event
+ for iEvent = 1 : length(evtNames)
+ stim = zeros(nSamples, 1);
+ iEvt = find(strcmpi({DataMat.Events.label}, evtNames{iEvent}));
+ if isempty(iEvt)
+ continue
+ end
+ % Event must be simple event
+ if size(DataMat.Events(iEvt).times, 1) ~= 1
+ bst_report('Warning', sProcess, sInputs, ['Events must be simple. Skipping event: "' evtNames{iEvent} '"' ]);
+ continue;
+ end
+ % Event occurrences (in samples)
+ iEvtOccur = bst_closest(DataMat.Events(iEvt).times, DataMat.Time);
+ stim(iEvtOccur) = 1;
+
+ % mTRF train
+ lambda = 0.1;
+ model = mTRFtrain(stim, F', fs, 1, tmin, tmax, lambda);
+
+ % Store weights of the mTRF model in a matrix file
+ OutputMat = db_template('matrixmat');
+ OutputMat.Comment = ['TRF Model Weights: ' evtNames{iEvent}];
+ OutputMat.Time = squeeze(model.t);
+ OutputMat.Value = squeeze(model.w(1,:,:))';
+ OutputMat.Description = channelNames;
+ % Save and add to database
+ OutputFile = bst_process('GetNewFilename', bst_fileparts(sInput.FileName), 'matrix_trf_weights');
+ bst_save(OutputFile, OutputMat, 'v6');
+ db_add_data(sInput.iStudy, OutputFile, OutputMat);
+
+ OutputFiles{end+1} = OutputFile;
+ end
+end
diff --git a/toolbox/process/functions/process_pac_dynamic.m b/toolbox/process/functions/process_pac_dynamic.m
index 0349fd28d..2a500326b 100644
--- a/toolbox/process/functions/process_pac_dynamic.m
+++ b/toolbox/process/functions/process_pac_dynamic.m
@@ -269,8 +269,8 @@
return;
end
- % Set time window of first file if none specified in parameters and average across trials is requested
- if isempty(OPTIONS.TimeWindow) && OPTIONS.isAvgOutput
+ % If not specified, set time window value for each file. If average across trials is requested, use first file
+ if isempty(OPTIONS.TimeWindow) && (~OPTIONS.isAvgOutput || iFile == 1)
OPTIONS.TimeWindow = sInput.Time([1, end]);
end
diff --git a/toolbox/process/functions/process_pac_dynamic_sur2.m b/toolbox/process/functions/process_pac_dynamic_sur2.m
index 62c80af04..8d476ea08 100644
--- a/toolbox/process/functions/process_pac_dynamic_sur2.m
+++ b/toolbox/process/functions/process_pac_dynamic_sur2.m
@@ -220,6 +220,10 @@
return;
end
+ % If not specified, set time window value
+ if isempty(OPTIONS.TimeWindow)
+ OPTIONS.TimeWindow = sInput.Time([1, end]);
+ end
% Get sampling frequency
sRate = 1 / (sInput.Time(2) - sInput.Time(1));
diff --git a/toolbox/process/functions/process_psd.m b/toolbox/process/functions/process_psd.m
index fec97bcc8..aa97aaf1b 100644
--- a/toolbox/process/functions/process_psd.m
+++ b/toolbox/process/functions/process_psd.m
@@ -38,7 +38,6 @@
sProcess.OutputTypes = {'timefreq', 'timefreq', 'timefreq', 'timefreq'};
sProcess.nInputs = 1;
sProcess.nMinFiles = 1;
- sProcess.isSeparator = 1;
% Options: Time window
sProcess.options.timewindow.Comment = 'Time window:';
sProcess.options.timewindow.Type = 'timewindow';
diff --git a/toolbox/process/functions/process_psd_features.m b/toolbox/process/functions/process_psd_features.m
new file mode 100644
index 000000000..0b27ccb95
--- /dev/null
+++ b/toolbox/process/functions/process_psd_features.m
@@ -0,0 +1,201 @@
+function varargout = process_psd_features( varargin )
+% PROCESS_PSD_FEATURES: Extract features from the power spectrum
+
+% @=============================================================================
+% 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: Pauline Amrouche, Raymundo Cassani, 2024
+
+eval(macro_method);
+end
+
+
+%% ===== GET DESCRIPTION =====
+function sProcess = GetDescription() %#ok
+ % Description the process
+ sProcess.Comment = 'Compute PSD features';
+ sProcess.Category = 'File';
+ sProcess.SubGroup = 'Frequency';
+ sProcess.Index = 482;
+ sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/DeviationMaps';
+ % Definition of the input accepted by this process
+ sProcess.InputTypes = {'raw', 'data', 'results', 'matrix'};
+ sProcess.OutputTypes = {'timefreq', 'timefreq', 'timefreq', 'timefreq'};
+ sProcess.nInputs = 1;
+ sProcess.nMinFiles = 1;
+ sProcess.isSeparator = 1;
+ % Options: Time window
+ sProcess.options.timewindow.Comment = 'Time window:';
+ sProcess.options.timewindow.Type = 'timewindow';
+ sProcess.options.timewindow.Value = [];
+ % Option: Window (Length)
+ sProcess.options.win_length.Comment = 'Window length: ';
+ sProcess.options.win_length.Type = 'value';
+ sProcess.options.win_length.Value = {1, 's', []};
+ % Option: Window (Overlapping ratio)
+ sProcess.options.win_overlap.Comment = 'Window overlap ratio: ';
+ sProcess.options.win_overlap.Type = 'value';
+ sProcess.options.win_overlap.Value = {50, '%', 1};
+ % Options: Units / scaling
+ sProcess.options.units.Comment = {'Physical: U2/Hz', 'Normalized: U2/Hz/s', ...
+ 'Before Nov 2020', 'Units:'; ...
+ 'physical', 'normalized', 'old', ''};
+ sProcess.options.units.Type = 'radio_linelabel';
+ sProcess.options.units.Value = 'physical';
+ % Options: CLUSTERS
+ sProcess.options.clusters.Comment = '';
+ sProcess.options.clusters.Type = 'scout_confirm';
+ sProcess.options.clusters.Value = {};
+ sProcess.options.clusters.InputTypes = {'results'};
+ % Options: Scout function
+ sProcess.options.scoutfunc.Comment = {'Mean', 'Max', 'PCA', 'Std', 'All', 'Scout function:'};
+ sProcess.options.scoutfunc.Type = 'radio_line';
+ sProcess.options.scoutfunc.Value = 1;
+ sProcess.options.scoutfunc.InputTypes = {'results'};
+ % Options: Sensor types
+ sProcess.options.sensortypes.Comment = 'Sensor types or names (empty=all): ';
+ sProcess.options.sensortypes.Type = 'text';
+ sProcess.options.sensortypes.Value = 'MEG, EEG';
+ sProcess.options.sensortypes.InputTypes = {'raw','data'};
+ % Options: Extract mean
+ sProcess.options.mean.Comment = 'Extract mean';
+ sProcess.options.mean.Type = 'checkbox';
+ sProcess.options.mean.Value = 1;
+ % Options: Extract std
+ sProcess.options.std.Comment = 'Extract std';
+ sProcess.options.std.Type = 'checkbox';
+ sProcess.options.std.Value = 1;
+ % Options: Extract coefficient of variation
+ sProcess.options.cv.Comment = 'Extract cv';
+ sProcess.options.cv.Type = 'checkbox';
+ sProcess.options.cv.Value = 1;
+ % Options: Compute relative power
+ sProcess.options.relative.Comment = 'Use relative power';
+ sProcess.options.relative.Type = 'checkbox';
+ sProcess.options.relative.Value = 0;
+ % Separator
+ sProcess.options.sep.Type = 'label';
+ sProcess.options.sep.Comment = ' ';
+ % Options: Time-freq
+ sProcess.options.edit.Comment = {'panel_timefreq_options', ' PSD options: '};
+ sProcess.options.edit.Type = 'editpref';
+ sProcess.options.edit.Value = [];
+end
+
+
+%% ===== FORMAT COMMENT =====
+function Comment = FormatComment(sProcess) %#ok
+ Comment = sProcess.Comment;
+end
+
+
+%% ===== RUN =====
+function OutputFiles = Run(sProcess, sInput) %#ok
+ OutputFiles = {};
+ % Process options
+ if (sProcess.options.mean.Value && sProcess.options.std.Value) || sProcess.options.cv.Value
+ sProcess.options.win_std.Value = 'mean+std'; % One PSD file with mean and std across windows
+ elseif sProcess.options.mean.Value
+ sProcess.options.win_std.Value = 'mean'; % One PSD file with mean (Welch)
+ elseif sProcess.options.std.Value
+ sProcess.options.win_std.Value = 'std'; % One PSD file with std
+ else
+ bst_report('Error', sProcess, [], 'Must choose at least one feature.'); return;
+ end
+
+ % Call TIME-FREQ process
+ OutputFile = process_timefreq('Run', sProcess, sInput);
+
+ % Extract std and/or cv from one PSD (mean+std) file
+ if strcmpi(sProcess.options.win_std.Value, 'mean+std')
+ OutputFiles = ExtractStdCv(sProcess, OutputFile{1}, sInput);
+ else
+ OutputFiles = OutputFile;
+ end
+end
+
+%% ===== EXTRACT PSD FEATURES =====
+function OutputFiles = ExtractStdCv(sProcess, tfMeanStdFile, sInput)
+ OutputFiles = {tfMeanStdFile};
+ % Get options
+ extractMean = sProcess.options.mean.Value;
+ extractStd = sProcess.options.std.Value;
+ extractCv = sProcess.options.cv.Value;
+ % Load timefreq file with mean+std
+ timefreqtMat = in_bst_timefreq(tfMeanStdFile);
+ if isempty(timefreqtMat.Std)
+ bst_report('Error', sProcess, [], 'Input file must contain Std matrix.');
+ return;
+ end
+
+ % Extract std from mean+std file, and save in new timefreq file
+ if extractStd
+ newTF = timefreqtMat.Std;
+ OutputFile = saveMat(timefreqtMat, tfMeanStdFile, newTF, 'std', sInput);
+ OutputFiles = [OutputFiles, OutputFile];
+ end
+
+ % Extract cv (std/mean) from mean+std file, and save in new timefreq file
+ if extractCv
+ newTF = timefreqtMat.Std ./ timefreqtMat.TF;
+ OutputFile = saveMat(timefreqtMat, tfMeanStdFile, newTF, 'cv', sInput);
+ OutputFiles = [OutputFiles, OutputFile];
+ end
+
+ % Modify or delete initial mean+std file
+ if extractMean
+ % Update content of original mean+std file
+ % Remove std
+ newMat.Std = [];
+ % Update the function name
+ newMat.Options = timefreqtMat.Options;
+ newMat.Options.WindowFunction = 'mean';
+ % Add extraction in history
+ newMat.History = timefreqtMat.History;
+ newMat = bst_history('add', newMat, 'extract_std_cv', sprintf('mean matrix extracted from %s', tfMeanStdFile));
+ fileName = file_fullpath(tfMeanStdFile);
+ bst_save(fileName, newMat, [], 1);
+ else
+ % Delete mean+std file
+ bst_process('CallProcess', 'process_delete', tfMeanStdFile, [], 'target', 1);
+ OutputFiles(1) = [];
+ end
+end
+
+%% ===== UPDATE AND SAVE TIMEFREQ =====
+function OutputFile = saveMat(timefreqtMat, tfMeanStdFile, newTF, function_name, sInput)
+ % Update TF field
+ newMat = timefreqtMat;
+ newMat.TF = newTF;
+ newMat.Std = [];
+ % Update across-windows function name
+ newMat.Options.WindowFunction = function_name;
+ % Update Comment, append function name
+ newMat.Comment = [timefreqtMat.Comment ' ' function_name];
+ % Add extraction in history
+ newMat = bst_history('add', newMat, 'extract_std_cv', sprintf('%s matrix extracted from %s', function_name, tfMeanStdFile));
+ % New file name
+ [tfMeanStdFilePath, tfMeanStdFileBase, tfMeanStdFileExt] = bst_fileparts(tfMeanStdFile);
+ output = bst_fullfile(tfMeanStdFilePath, [tfMeanStdFileBase, '_' function_name, tfMeanStdFileExt]);
+ output = file_unique(output);
+ % Save the file
+ bst_save(output, newMat, 'v6');
+ db_add_data(sInput.iStudy, output, newMat);
+ OutputFile = {output};
+end
+
diff --git a/toolbox/process/functions/process_remove_evoked.m b/toolbox/process/functions/process_remove_evoked.m
index 38497d64b..a55e2bc82 100644
--- a/toolbox/process/functions/process_remove_evoked.m
+++ b/toolbox/process/functions/process_remove_evoked.m
@@ -9,12 +9,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
diff --git a/toolbox/process/functions/process_segment_cat12.m b/toolbox/process/functions/process_segment_cat12.m
index f7ea79a35..bf8f8c3a6 100644
--- a/toolbox/process/functions/process_segment_cat12.m
+++ b/toolbox/process/functions/process_segment_cat12.m
@@ -117,11 +117,11 @@
else
isCerebellum = 1;
end
- % TPM atlas
+ % % TPM atlas, preferably from SPM plugin
if isfield(sProcess.options, 'tpmnii') && isfield(sProcess.options.tpmnii, 'Value') && ~isempty(sProcess.options.tpmnii.Value) && ~isempty(sProcess.options.tpmnii.Value{1})
TpmNii = sProcess.options.tpmnii.Value{1};
else
- TpmNii = bst_get('SpmTpmAtlas');
+ TpmNii = bst_get('SpmTpmAtlas', 'SPM');
end
% Thickness maps
if isfield(sProcess.options, 'extramaps') && isfield(sProcess.options.extramaps, 'Value') && ~isempty(sProcess.options.extramaps.Value)
@@ -191,7 +191,8 @@
end
% Check provided TPM.nii
if isempty(TpmNii)
- TpmNii = bst_get('SpmTpmAtlas');
+ % TPM atlas, preferably from SPM plugin
+ TpmNii = bst_get('SpmTpmAtlas', 'SPM');
end
% ===== GET SUBJECT =====
@@ -430,8 +431,9 @@ function ComputeInteractive(iSubject, iAnatomy) %#ok
end
% Open progress bar
bst_progress('start', 'CAT12', 'CAT12 MRI segmentation...');
+ % TPM atlas, preferably from SPM plugin
+ TpmNii = bst_get('SpmTpmAtlas', 'SPM');
% Run CAT12
- TpmNii = bst_get('SpmTpmAtlas');
isInteractive = 1;
isSphReg = 1;
isCerebellum = 0;
diff --git a/toolbox/process/functions/process_select_files_data.m b/toolbox/process/functions/process_select_files_data.m
index 0795d291b..92164a658 100644
--- a/toolbox/process/functions/process_select_files_data.m
+++ b/toolbox/process/functions/process_select_files_data.m
@@ -67,6 +67,11 @@
sProcess.options.includecommon.Comment = 'Include the folder "Common files"';
sProcess.options.includecommon.Type = 'checkbox';
sProcess.options.includecommon.Value = 0;
+ % USE FOUND FILES IN PROCESS TABS
+ sProcess.options.outprocesstab.Comment = 'Use found files in Process tab';
+ sProcess.options.outprocesstab.Type = 'combobox_label';
+ sProcess.options.outprocesstab.Value = {'no', {'No', 'Process1', 'Process2A', 'Process2B'; ...
+ 'no', 'process1', 'process2a', 'process2b'}};
end
@@ -160,6 +165,11 @@
end
% Get files
OutputFiles = SelectFiles([SubjectName '/' Condition], FileType, IncludeBad, IncludeIntra, IncludeCommon, CommentTag);
+ % Add files to process tab
+ if isfield(sProcess.options, 'outprocesstab') && isfield(sProcess.options.outprocesstab, 'Value') && ~isempty(sProcess.options.outprocesstab.Value) && ~strcmpi('no', sProcess.options.outprocesstab.Value{1})
+ panel_nodelist('ResetList', sProcess.options.outprocesstab.Value{1})
+ panel_nodelist('AddFiles', sProcess.options.outprocesstab.Value{1}, OutputFiles);
+ end
% Build process comment
strInfo = [FormatComment(sProcess) ' '];
diff --git a/toolbox/process/functions/process_select_files_matrix.m b/toolbox/process/functions/process_select_files_matrix.m
index 314eb8ed8..029a66651 100644
--- a/toolbox/process/functions/process_select_files_matrix.m
+++ b/toolbox/process/functions/process_select_files_matrix.m
@@ -63,6 +63,11 @@
sProcess.options.includecommon.Comment = 'Include the folder "Common files"';
sProcess.options.includecommon.Type = 'checkbox';
sProcess.options.includecommon.Value = 0;
+ % USE FOUND FILES IN PROCESS TABS
+ sProcess.options.outprocesstab.Comment = 'Use found files in Process tab';
+ sProcess.options.outprocesstab.Type = 'combobox_label';
+ sProcess.options.outprocesstab.Value = {'no', {'No', 'Process1', 'Process2A', 'Process2B'; ...
+ 'no', 'process1', 'process2a', 'process2b'}};
end
diff --git a/toolbox/process/functions/process_select_files_results.m b/toolbox/process/functions/process_select_files_results.m
index 8474cca83..3de1fc11c 100644
--- a/toolbox/process/functions/process_select_files_results.m
+++ b/toolbox/process/functions/process_select_files_results.m
@@ -63,6 +63,11 @@
sProcess.options.includecommon.Comment = 'Include the folder "Common files"';
sProcess.options.includecommon.Type = 'checkbox';
sProcess.options.includecommon.Value = 0;
+ % USE FOUND FILES IN PROCESS TABS
+ sProcess.options.outprocesstab.Comment = 'Use found files in Process tab';
+ sProcess.options.outprocesstab.Type = 'combobox_label';
+ sProcess.options.outprocesstab.Value = {'no', {'No', 'Process1', 'Process2A', 'Process2B'; ...
+ 'no', 'process1', 'process2a', 'process2b'}};
end
diff --git a/toolbox/process/functions/process_select_files_timefreq.m b/toolbox/process/functions/process_select_files_timefreq.m
index 53db90e07..c3f98237c 100644
--- a/toolbox/process/functions/process_select_files_timefreq.m
+++ b/toolbox/process/functions/process_select_files_timefreq.m
@@ -63,6 +63,11 @@
sProcess.options.includecommon.Comment = 'Include the folder "Common files"';
sProcess.options.includecommon.Type = 'checkbox';
sProcess.options.includecommon.Value = 0;
+ % USE FOUND FILES IN PROCESS TABS
+ sProcess.options.outprocesstab.Comment = 'Use found files in Process tab';
+ sProcess.options.outprocesstab.Type = 'combobox_label';
+ sProcess.options.outprocesstab.Value = {'no', {'No', 'Process1', 'Process2A', 'Process2B'; ...
+ 'no', 'process1', 'process2a', 'process2b'}};
end
diff --git a/toolbox/process/functions/process_select_search.m b/toolbox/process/functions/process_select_search.m
index c4ce17a70..bd631dcdd 100644
--- a/toolbox/process/functions/process_select_search.m
+++ b/toolbox/process/functions/process_select_search.m
@@ -51,6 +51,11 @@
sProcess.options.includebad.Comment = 'Include the bad trials';
sProcess.options.includebad.Type = 'checkbox';
sProcess.options.includebad.Value = 1;
+ % USE FOUND FILES IN PROCESS TABS
+ sProcess.options.outprocesstab.Comment = 'Use found files in Process tab';
+ sProcess.options.outprocesstab.Type = 'combobox_label';
+ sProcess.options.outprocesstab.Value = {'no', {'No', 'Process1', 'Process2A', 'Process2B'; ...
+ 'no', 'process1', 'process2a', 'process2b'}};
end
@@ -111,6 +116,11 @@
bst_report('Info', sProcess, [], strReport);
% Return only the filenames that passed the search
OutputFiles = {sInputs(iFiles).FileName};
+ % Use files in process tab
+ if isfield(sProcess.options, 'outprocesstab') && isfield(sProcess.options.outprocesstab, 'Value') && ~isempty(sProcess.options.outprocesstab.Value) && ~strcmpi('no', sProcess.options.outprocesstab.Value{1})
+ panel_nodelist('ResetList', sProcess.options.outprocesstab.Value{1})
+ panel_nodelist('AddFiles', sProcess.options.outprocesstab.Value{1}, OutputFiles);
+ end
end
function sOutputs = GetAllProtocolFiles(FileTypes, IncludeBad)
diff --git a/toolbox/process/functions/process_source_atlas.m b/toolbox/process/functions/process_source_atlas.m
index 15ed5bcfe..7dd1d5987 100644
--- a/toolbox/process/functions/process_source_atlas.m
+++ b/toolbox/process/functions/process_source_atlas.m
@@ -197,7 +197,8 @@
isempty(strfind(TestTags, '_abs')) && ...
isempty(strfind(TestTags, '_norm')) && ...
isempty(strfind(TestTags, 'NIRS')) && ...
- isempty(strfind(TestTags, 'Summed_sensitivities'));
+ isempty(strfind(TestTags, 'Summed_sensitivities')) && ...
+ isempty(strfind(TestTags, 'bold'));
ImageGridAmp(iScout,:) = bst_scout_value(Fscout, sScouts(iScout).Function, ScoutOrient, ResultsMat.nComponents, [], isFlipSign);
elseif isNorm
ImageGridAmp(iScout,:) = bst_scout_value(Fscout, sScouts(iScout).Function, ScoutOrient, ResultsMat.nComponents, 'norm');
diff --git a/toolbox/process/functions/process_sprint.m b/toolbox/process/functions/process_sprint.m
index 3092b5135..b31a45b71 100644
--- a/toolbox/process/functions/process_sprint.m
+++ b/toolbox/process/functions/process_sprint.m
@@ -91,10 +91,10 @@
sProcess.options.freqrange.Comment = 'Frequency range for analysis: ';
sProcess.options.freqrange.Type = 'freqrange_static';
sProcess.options.freqrange.Value = {[1 40], 'Hz', 1};
- % Option: Peak type
- sProcess.options.peaktype.Comment = {'Gaussian', 'Cauchy (experimental)', 'Peak model:'; 'gaussian', 'cauchy', ''};
- sProcess.options.peaktype.Type = 'radio_linelabel';
- sProcess.options.peaktype.Value = 'gaussian';
+ % Option: Optimization type
+ sProcess.options.optimobj.Comment = {'Default', 'Model selection (experimental)', 'Optimization type:'; 'leastsquare', 'negloglike', ''};
+ sProcess.options.optimobj.Type = 'radio_linelabel';
+ sProcess.options.optimobj.Value = 'leastsquare';
% Option: Peak width limits
sProcess.options.peakwidth.Comment = 'Peak width limits (default=[0.5-12]): ';
sProcess.options.peakwidth.Type = 'freqrange_static';
diff --git a/toolbox/process/deprecated/process_ssmooth.m b/toolbox/process/functions/process_ssmooth.m
similarity index 50%
rename from toolbox/process/deprecated/process_ssmooth.m
rename to toolbox/process/functions/process_ssmooth.m
index 0ddaeac62..87c3f1c1e 100644
--- a/toolbox/process/deprecated/process_ssmooth.m
+++ b/toolbox/process/functions/process_ssmooth.m
@@ -1,5 +1,5 @@
function varargout = process_ssmooth( varargin )
-% PROCESS_SSMOOTH: Spatial smoothing of the sources (DEPRECATED)
+% PROCESS_SSMOOTH: Spatial smoothing of the sources
% @=============================================================================
% This function is part of the Brainstorm software:
@@ -20,6 +20,8 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2010-2016
+% Edouard Delaire, Raymundo Cassani, 2023
+
eval(macro_method);
end
@@ -28,11 +30,11 @@
%% ===== GET DESCRIPTION =====
function sProcess = GetDescription() %#ok
% Description the process
- sProcess.Comment = 'Spatial smoothing (DEPRECATED)';
+ sProcess.Comment = 'Spatial smoothing [2024]';
sProcess.FileTag = 'ssmooth';
sProcess.Category = 'Filter';
sProcess.SubGroup = 'Sources';
- sProcess.Index = 0;
+ sProcess.Index = 336;
sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/VisualGroup';
% Definition of the input accepted by this process
sProcess.InputTypes = {'results', 'timefreq'};
@@ -50,13 +52,15 @@
sProcess.options.fwhm.Type = 'value';
sProcess.options.fwhm.Value = {10, 'mm', 0};
% === METHOD
- sProcess.options.label2.Comment = ' Distance between vertices (v1,v2):';
+ sProcess.options.label2.Comment = 'Distance between a pair of vertices:';
sProcess.options.label2.Type = 'label';
- sProcess.options.method.Comment = {'Euclidean distance: norm(v1-v2)', ...
- 'Path length: number of edges between v1 and v2', ...
- 'Average: (euclidian distance + path length) / 2'};
- sProcess.options.method.Type = 'radio';
- sProcess.options.method.Value = 3;
+ sProcess.options.method.Comment = {[' Geodesic (mm) ', ...
+ '(recommended)'], ...
+ [' Path length (edges) ', ...
+ 'FWHM is converted to edges for each connected surface']; ...
+ 'geodesic_dist', 'geodesic_edge'};
+ sProcess.options.method.Type = 'radio_label';
+ sProcess.options.method.Value = 'geodesic_dist';
end
@@ -68,29 +72,16 @@
else
strAbs = '';
end
- % Method
- switch (sProcess.options.method.Value)
- case 1, Method = 'euclidian';
- case 2, Method = 'path';
- case 3, Method = 'average';
- otherwise, error(['Unknown method: ' sProcess.options.method.Value]);
- end
% Final comment
- Comment = sprintf('%s (%d%c%s)', sProcess.Comment, sProcess.options.fwhm.Value{1}, Method(1), strAbs);
+ Comment = sprintf('%s (%1.2f mm%s)', sProcess.Comment, sProcess.options.fwhm.Value{1}, strAbs);
end
%% ===== RUN =====
function sInput = Run(sProcess, sInput) %#ok
- global GlobalData;
% Get options
- FWHM = sProcess.options.fwhm.Value{1} / 1000;
- switch (sProcess.options.method.Value)
- case 1, Method = 'euclidian';
- case 2, Method = 'path';
- case 3, Method = 'average';
- otherwise, error(['Unknown method: ' sProcess.options.method.Value]);
- end
+ FWHM = sProcess.options.fwhm.Value{1} / 1000; % meters
+ Method = sProcess.options.method.Value;
% ===== LOAD SURFACE =====
% Load the surface filename from results file
@@ -121,9 +112,41 @@
return;
end
- % ===== COMPUTE SMOOTHING OPERATOR =====
+ % Perform smoothing
+ [sInput.A, msgInfo, errInfo] = compute(FileMat.SurfaceFile, sInput.A, FWHM, Method);
+ % Error handling
+ if ~isempty(errInfo)
+ bst_report('Error', sProcess, sInputs, errInfo);
+ return;
+ end
+
+ % Force the output comment
+ sInput.CommentTag = FormatComment(sProcess);
+ sInput.HistoryComment = msgInfo;
+
+ % Do not keep the Std field in the output
+ if isfield(sInput, 'Std') && ~isempty(sInput.Std)
+ sInput.Std = [];
+ end
+end
+
+function [sData, msgInfo, errInfo] = compute(SurfaceFile, sData, FWHM, Method)
+ global GlobalData;
+
+ msgInfo = '';
+ errInfo = '';
+
+ SurfaceMat = in_tess_bst(SurfaceFile);
+
+ switch Method
+ case 'geodesic_dist'
+ msgInfo = sprintf('Spatial smoothing using %1.2f mm kernel calculating distance using geodesic distance', FWHM*1000);
+ case 'geodesic_edge'
+ msgInfo = sprintf('Spatial smoothing using %1.2f mm kernel calculating distance using edge path length distance', FWHM*1000);
+ end
+
% Get existing interpolation for this surface
- Signature = sprintf('ssmooth(%d,%s):%s', round(FWHM*1000), Method, FileMat.SurfaceFile);
+ Signature = sprintf('ssmooth(%1.2f,%s):%s', round(FWHM*1000), Method, SurfaceFile);
WInterp = [];
if isfield(GlobalData, 'Interpolations') && ~isempty(GlobalData.Interpolations) && isfield(GlobalData.Interpolations, 'Signature')
iInterp = find(cellfun(@(c)isequal(c,Signature), {GlobalData.Interpolations.Signature}), 1);
@@ -131,18 +154,37 @@
WInterp = GlobalData.Interpolations(iInterp).WInterp;
end
end
+
% Calculate new interpolation matrix
if isempty(WInterp)
% Load surface file
- SurfaceMat = in_tess_bst(FileMat.SurfaceFile);
- % Compute the smoothing operator
- WInterp = tess_smooth_sources(SurfaceMat.Vertices, SurfaceMat.Faces, SurfaceMat.VertConn, FWHM, Method);
- % Check for errors
- if isempty(WInterp)
- sInput = [];
- return;
+ nVertices = size(SurfaceMat.Vertices,1);
+ switch Method
+ case 'geodesic_dist'
+ Dist = bst_tess_distance(SurfaceMat, 1:nVertices, 1:nVertices, 'geodesic_dist'); % in meter
+ % One region
+ subRegions(1) = SurfaceMat;
+ subRegions(1).Indices = (1 : nVertices)';
+ subRegions(1).VertDist = Dist;
+ case 'geodesic_edge'
+ Dist = bst_tess_distance(SurfaceMat, 1:nVertices, 1:nVertices, 'geodesic_edge'); % in edges
+ % Connected regions
+ subRegions = GetConnectedRegions(SurfaceMat,Dist);
end
- % Save interpolation in memory for future calls
+
+ % Full smooth operator
+ WInterp = sparse(nVertices, nVertices);
+ for iSubRegion = 1:length(subRegions)
+ % Subregion smoothing operator
+ WInterpTmp = tess_smooth_sources(subRegions(iSubRegion), FWHM, Method);
+ % Check for errors
+ if isempty(WInterpTmp)
+ errInfo = sprintf('Cannot compute the smoothig %s.', Signature);
+ return;
+ end
+ WInterp(subRegions(iSubRegion).Indices, subRegions(iSubRegion).Indices) = WInterpTmp(:,:);
+ end
+
sInterp = db_template('interpolation');
sInterp.WInterp = WInterp;
sInterp.Signature = Signature;
@@ -151,18 +193,56 @@
else
GlobalData.Interpolations(end+1) = sInterp;
end
+
end
- % ===== APPLY TO THE DATA =====
% Apply smoothing operator
- for iFreq = 1:size(sInput.A,3)
- sInput.A(:,:,iFreq) = WInterp * sInput.A(:,:,iFreq);
+ for iFreq = 1:size(sData,3)
+ sData(:,:,iFreq) = WInterp * sData(:,:,iFreq);
end
- % Force the output comment
- sInput.CommentTag = [sProcess.FileTag, num2str(FWHM*1000), Method(1)];
- % Do not keep the Std field in the output
- if isfield(sInput, 'Std') && ~isempty(sInput.Std)
- sInput.Std = [];
+end
+
+function sSubRegions = GetConnectedRegions(SurfaceMat, Dist)
+ % Find connenected regions (subregions) of the surface
+ sSubRegion = struct('Vertices', [], ...
+ 'VertConn', [], ...
+ 'Faces', [], ...
+ 'Indices', [], ...
+ 'VertDist', []);
+ sSubRegions = repmat(sSubRegion, 0, 0);
+ for k=1:size(Dist,1)
+ nn_in = find(~isinf(Dist(k,:)));
+ found = 0;
+ for i=1:length(sSubRegions)
+ if length(nn_in) == length(sSubRegions(i).Indices) && all(nn_in == sSubRegions(i).Indices)
+ found = 1;
+ break;
+ end
+ end
+ if ~found
+ sSubRegions(end+1).Indices = nn_in;
+ end
+ end
+
+ % Subregion elements
+ for i = 1:length(sSubRegions)
+ % Vertices
+ sSubRegions(i).Vertices = SurfaceMat.Vertices(sSubRegions(i).Indices, :);
+ iRemoveVert = setdiff(1:size(SurfaceMat.Vertices,1), sSubRegions(i).Indices);
+ iKeptVert = sSubRegions(i).Indices;
+ iVertMap = zeros(1, size(SurfaceMat.Vertices, 1));
+ iVertMap(iKeptVert) = 1:length(iKeptVert);
+ % Faces
+ sSubRegions(i).Faces = SurfaceMat.Faces;
+ iRemoveFace = find(sum(ismember(SurfaceMat.Faces, iRemoveVert), 2));
+ sSubRegions(i).Faces(iRemoveFace, :) = [];
+ % Renumber indices for faces
+ sSubRegions(i).Faces = iVertMap(sSubRegions(i).Faces);
+ % VertConn
+ sSubRegions(i).VertConn = SurfaceMat.VertConn(sSubRegions(i).Indices, sSubRegions(i).Indices);
+ % Distances
+ sSubRegions(i).VertDist = Dist(sSubRegions(i).Indices, sSubRegions(i).Indices);
+
end
end
diff --git a/toolbox/process/functions/process_ssmooth_surfstat.m b/toolbox/process/functions/process_ssmooth_surfstat.m
index 7829d080c..3ab969b22 100644
--- a/toolbox/process/functions/process_ssmooth_surfstat.m
+++ b/toolbox/process/functions/process_ssmooth_surfstat.m
@@ -20,6 +20,7 @@
% =============================================================================@
%
% Authors: Peter Donhauser, Francois Tadel, 2015-2016
+% Edouard Delaire, 2023
eval(macro_method);
end
@@ -32,7 +33,7 @@
sProcess.FileTag = 'ssmooth';
sProcess.Category = 'Filter';
sProcess.SubGroup = 'Sources';
- sProcess.Index = 336;
+ sProcess.Index = 336.1;
% Definition of the input accepted by this process
sProcess.InputTypes = {'results', 'timefreq'};
sProcess.OutputTypes = {'results', 'timefreq'};
@@ -54,6 +55,15 @@
sProcess.options.fwhm.Comment = 'FWHM (Full width at half maximum): ';
sProcess.options.fwhm.Type = 'value';
sProcess.options.fwhm.Value = {3, 'mm', 0};
+ % === METHOD
+ sProcess.options.label.Comment = 'Method:';
+ sProcess.options.label.Type = 'label';
+ sProcess.options.method.Comment = {'Before 2023 (not recommended)', ...
+ 'Fixed FWHM for all surfaces', ...
+ 'Adjust FWHM for each disconnected surface (slower)'; ...
+ 'before_2023', 'fixed_fwhm', 'adaptive_fwhm'};
+ sProcess.options.method.Type = 'radio_label';
+ sProcess.options.method.Value = 'before_2023';
end
@@ -66,13 +76,18 @@
strAbs = '';
end
% Final comment
- Comment = sprintf('%s (%1.2f%s)', sProcess.Comment, sProcess.options.fwhm.Value{1}, strAbs);
+ Comment = sprintf('%s (%1.2f mm%s)', sProcess.Comment, sProcess.options.fwhm.Value{1}, strAbs);
end
%% ===== RUN =====
function sInput = Run(sProcess, sInput) %#ok
% Get options
+ if ~isfield(sProcess.options, 'method') || isempty(sProcess.options.method.Value)
+ method = 'before_2023';
+ else
+ method = sProcess.options.method.Value;
+ end
FWHM = sProcess.options.fwhm.Value{1} / 1000;
% ===== LOAD DATA =====
@@ -82,7 +97,13 @@
FileMat = in_bst_results(sInput.FileName, 0, 'SurfaceFile', 'GridLoc', 'Atlas', 'nComponents', 'HeadModelType');
nComponents = FileMat.nComponents;
case 'timefreq'
- FileMat = in_bst_timefreq(sInput.FileName, 0, 'SurfaceFile', 'GridLoc', 'Atlas', 'HeadModelType');
+ FileMat = in_bst_timefreq(sInput.FileName, 0, 'DataType', 'SurfaceFile', 'GridLoc', 'Atlas', 'HeadModelType');
+ % Check the data type: timefreq must be source/surface based, and no kernel-based file
+ if ~strcmpi(FileMat.DataType, 'results')
+ errMsg = 'Only cortical maps can be smoothed.';
+ bst_report('Error', 'process_ssmooth_surfstat', sInput.FileName, errMsg);
+ return;
+ end
nComponents = 1;
otherwise
error('Unsupported file format.');
@@ -93,7 +114,7 @@
sInput = [];
return;
% Error: cannot smooth results that are already based on atlases
- elseif ~isempty(FileMat.Atlas)
+ elseif ~isempty(FileMat.Atlas) && isempty(strfind(sInput.FileName, '_connect1'))
bst_report('Error', sProcess, sInput, 'Spatial smoothing is not supported for sources based on atlases.');
sInput = [];
return;
@@ -103,36 +124,99 @@
sInput = [];
return;
end
- % Load surface
- SurfaceMat = in_tess_bst(FileMat.SurfaceFile);
-
-
+
% ===== PROCESS =====
+ % Smooth surface
+ [sInput.A, msgInfo, warmInfo] = compute(FileMat.SurfaceFile, sInput.A, FWHM, method);
+ if iscell(msgInfo)
+ tmp = sprintf('Smoothing %d independent regions \n', length(msgInfo));
+ for iRegion = 1:length(msgInfo)
+ tmp = [tmp, sprintf('Region %d: %s \n', iRegion, msgInfo{iRegion})];
+ end
+ msgInfo = tmp;
+ end
+ bst_report('Info', sProcess, sInput, msgInfo);
+
+ if ~isempty(warmInfo)
+ bst_report('Warning', sProcess, sInput, warmInfo);
+ end
+
+ % Force the output comment
+ sInput.CommentTag = FormatComment(sProcess);
+ % Format the output history
+ tmp = strsplit(msgInfo,'\n');
+ HistoryComment = [sprintf('%s %.1f mm',sProcess.FileTag,FWHM*1000) newline];
+ for iLine = 1:length(tmp)
+ if ~isempty(tmp{iLine})
+ HistoryComment = [HistoryComment ...
+ '|-------- ' strrep(tmp{iLine},'=',':') newline ];
+ end
+ end
+ if ~isempty(warmInfo)
+ HistoryComment = [HistoryComment ...
+ '|-------- ' warmInfo];
+ end
+
+ sInput.HistoryComment = HistoryComment;
+end
+
+function [sData, msgInfo, warmInfo] = compute(SurfaceFile, sData, FWHM, version)
+ warmInfo = '';
+
+ % Get surface
+ if ischar(SurfaceFile)
+ SurfaceMat = in_tess_bst(SurfaceFile);
+ else
+ SurfaceMat = SurfaceFile;
+ end
+
+ if strcmp(version,'adaptive_fwhm')
+ % Smooth each connenected part of the surface separately
+ % first estimate the connected regions
+ nVertices = size(SurfaceMat.Vertices,1);
+ Dist = bst_tess_distance(SurfaceMat, 1:nVertices, 1:nVertices, 'geodesic_edge');
+ subRegions = process_ssmooth('GetConnectedRegions', SurfaceMat, Dist);
+ % Smooth each region separately
+ msgInfo = cell(1,length(subRegions));
+ for i = 1:length(subRegions)
+ [sData(subRegions(i).Indices,:,:), msgInfo{i}] = compute(subRegions(i), sData(subRegions(i).Indices,:,:), FWHM, 'fixed_fwhm');
+ end
+ return;
+ end
% Convert surface to SurfStat format
cortS.tri = SurfaceMat.Faces;
cortS.coord = SurfaceMat.Vertices';
% Get the average edge length
[vi,vj] = find(SurfaceMat.VertConn);
- Vertices = SurfaceMat.VertConn;
+ if strcmp(version,'before_2023')
+ Vertices = SurfaceMat.VertConn;
+ elseif strcmp(version,'fixed_fwhm')
+ Vertices = SurfaceMat.Vertices;
+ end
+
meanDist = mean(sqrt((Vertices(vi,1) - Vertices(vj,1)).^2 + (Vertices(vi,2) - Vertices(vj,2)).^2 + (Vertices(vi,3) - Vertices(vj,3)).^2));
+
% FWHM in surfstat is in mesh units: Convert from millimeters to "edges"
FWHMedge = FWHM ./ meanDist;
-
+
% Display the result of this conversion
msgInfo = ['Average distance between two vertices: ' num2str(round(meanDist*10000)/10) ' mm' 10 ...
'SurfStatSmooth called with FWHM=' num2str(round(FWHMedge * 1000)/1000) ' edges'];
- bst_report('Info', sProcess, sInput, msgInfo);
- disp(['SMOOTH> ' strrep(msgInfo, char(10), [10 'SMOOTH> '])]);
-
- % Smooth surface
- for iFreq = 1:size(sInput.A,3)
- sInput.A(:,:,iFreq) = SurfStatSmooth(sInput.A(:,:,iFreq)', cortS, FWHMedge)';
+ disp(['SMOOTH> ' strrep(msgInfo, char(10), [10 'SMOOTH> '])]);
+
+ if strcmp(version,'before_2023')
+ Vertices = SurfaceMat.Vertices;
+ true_meanDist = mean(sqrt((Vertices(vi,1) - Vertices(vj,1)).^2 + (Vertices(vi,2) - Vertices(vj,2)).^2 + (Vertices(vi,3) - Vertices(vj,3)).^2));
+ used_FWHM = FWHMedge * true_meanDist;
+
+ warmInfo = sprintf('This process is using a FWHM of %.2f mm instead of %.2f mm. Please consult https://github.com/brainstorm-tools/brainstorm3/pull/645 for more information.',used_FWHM*1000,FWHM*1000);
+ end
+
+ for iFreq = 1:size(sData,3)
+ sData(:,:,iFreq) = SurfStatSmooth(sData(:,:,iFreq)', cortS, FWHMedge)';
end
-
- % Force the output comment
- sInput.CommentTag = [sProcess.FileTag, num2str(FWHM*1000)];
-end
+end
diff --git a/toolbox/process/functions/process_ssp2.m b/toolbox/process/functions/process_ssp2.m
index 2fb1d908c..7edceb5af 100644
--- a/toolbox/process/functions/process_ssp2.m
+++ b/toolbox/process/functions/process_ssp2.m
@@ -638,6 +638,7 @@
chanmask(iChannels) = 1;
% Call computation function
proj = Compute(F, chanmask);
+ proj.Method = Method;
% % Select the components with a singular value > threshold
% if isempty(ForceSelect)
% singThresh = 0.12;
@@ -676,6 +677,7 @@
proj.CompMask = 1;
proj.Status = 1;
proj.SingVal = [];
+ proj.Method = Method;
% === ICA: JADE ===
@@ -698,6 +700,18 @@
bst_progress('text', 'Calling external function: EEGLAB''s runica()...');
% Remove the mean
F = bst_bsxfun(@minus, F, mean(F,2));
+ rankF = rank(F);
+ isLowRank = rank(F) < size(F,1);
+ if isLowRank
+ % Warning message
+ strWarning = [sprintf('INFOMAX: The rank of your data (%d) is lower than the number of channels (%d) in it.', rankF, size(F,1)), 10 ...
+ ' This could be caused because a refrence channel is included, or AVERAGE re-referencing is applied to this data.', 10 ...
+ ' Please consider limiting the "Number of ICA components" to the rank of the data.'];
+ % Add to the report
+ bst_report('Warning', sProcess, [], strWarning);
+ % Display on the console
+ disp([10 'WARNING: ' strWarning]);
+ end
% Run EEGLAB ICA function
if ~isempty(nIcaComp) && (nIcaComp ~= 0)
[icaweights, icasphere] = runica(F, 'pca', nIcaComp, 'lrate', 0.001, 'extended', 1, 'interupt', 'off');
@@ -759,7 +773,8 @@
proj.Components = Wall;
proj.CompMask = zeros(size(Wall,2), 1); % No component selected by default
proj.Status = 1;
- proj.SingVal = 'ICA';
+ proj.Method = Method;
+
% Apply component selection (if set explicitly)
if ~isempty(SelectComp)
proj.CompMask(SelectComp) = 1;
@@ -768,22 +783,33 @@
if isempty(Y)
Y = W * F;
end
- % Sort ICA components
+ % Compute mixing matrix
+ if diff(size(W)) == 0
+ M = inv(W);
+ else
+ M = pinv(W);
+ end
+ % Variance in recovered data explained by each component
+ varIcs = sum(M.^2, 1) .* sum(Y.^2, 2)';
+ varIcs = varIcs ./ sum(varIcs);
+ % Find sorting order for ICA components
+ iSort = [];
if ~isempty(icaSort)
% By correlation with reference channel
C = bst_corrn(Fref, Y);
- [corrs, iSort] = sort(max(abs(C),[],1), 'descend');
- proj.Components = proj.Components(:,iSort);
+ [~, iSort] = sort(max(abs(C),[],1), 'descend');
elseif ismember(Method, {'ICA_picard', 'ICA_fastica'})
- % By explained variance
- if diff(size(W)) == 0
- M = inv(W);
- else
- M = pinv(W);
- end
- var = sum(M.^2, 1) .* sum(Y.^2, 2)';
- [var, iSort] = sort(var, 'descend');
+ [~, iSort] = sort(varIcs, 'descend');
+ end
+ % Explained variance ratio
+ Fdiff = (F - M * Y);
+ rVarExp = 1 - (sum(sum(Fdiff.^2, 2)) ./ sum(sum(F.^2, 2)));
+ % Variance in original data by each component
+ proj.SingVal = rVarExp * varIcs;
+ % Sort components and their variances
+ if ~isempty(iSort)
proj.Components = proj.Components(:,iSort);
+ proj.SingVal = proj.SingVal(iSort);
end
end
@@ -945,15 +971,17 @@
%
% COMMENTS:
% There are 5 categories of projectors:
-% - SSP_pca: CompMask=[Ncomp x 1], SingVal=[Ncomp x 1], Components=[Nchan x Ncomp]=U
-% - SSP_mean: CompMask=1, SingVal=[], Components=[Nchan x 1]=U
-% - ICA: CompMask=[Ncomp x 1], SingVal='ICA', Components=[Nchan x Ncomp]=W'
-% - REF: CompMask=[], SingVal='REF', Components=[Nchan x Ncomp]=Wmontage
-% - Other: CompMask=[], SingVal=[], Components=[Nchan x Nchan]=Projector=I-UUt
+% - SSP_pca: Method = 'SSP_pca' CompMask=[Ncomp x 1], SingVal=[Ncomp x 1], Components=[Nchan x Ncomp]=U
+% - SSP_mean: Method = 'SSP_pca' CompMask=1, SingVal=[], Components=[Nchan x 1]=U
+% - ICA: Method = 'ICA_variant' CompMask=[Ncomp x 1], SingVal=[Ncomp x 1], Components=[Nchan x Ncomp]=W'
+% - REF: Method = 'REF' CompMask=[], SingVal=[], Components=[Nchan x Ncomp]=Wmontage
+% - Other: Method = 'Other' CompMask=[], SingVal=[], Components=[Nchan x Nchan]=Projector=I-UUt
+%
+% For ICA projectors, 'SingVal' contains the fraction explained variance with respect to the original signal
%
% Description of the notations used here:
-% - W: Unmixing matrix [Nelectrodes x Ncomponents]
-% - Winv = pinv(W) = [Ncomponents x Nelectrodes]
+% - W: Unmixing matrix [Ncomponents x Nelectrodes]
+% - Winv = pinv(W) = [Nelectrodes x Ncomponents]
% - In EEGLAB: W = icaweights * icasphere;
% - Activations_IC = W * Data
% - CleanData = Winv(:,iComp) * Activations(iComp,:)
@@ -974,11 +1002,12 @@
iProjSsp = [];
U = [];
for i = 1:length(ListProj)
+ ListProj(i) = ConvertOldFormat(ListProj(i));
% Is entry not selected: skip
if ~ismember(ListProj(i).Status, ProjStatus) || (~isempty(ListProj(i).CompMask) && all(ListProj(i).CompMask == 0))
iProjDel(end+1) = i;
% New SSP: Stack selected vectors all together
- elseif ~isempty(ListProj(i).CompMask) && ~isequal(ListProj(i).SingVal, 'ICA') && ~isequal(ListProj(i).SingVal, 'REF')
+ elseif ~isempty(ListProj(i).CompMask) && ~ismember(ListProj(i).Method(1:3), {'ICA', 'REF'})
iProjSsp(end+1) = i;
U = [U, ListProj(i).Components(:,ListProj(i).CompMask == 1)];
end
@@ -1030,7 +1059,7 @@
% Add the projectors in the order of appearance
for i = 1:length(ListProj)
% ICA
- if isequal(ListProj(i).SingVal, 'ICA')
+ if isequal(ListProj(i).Method(1:3), 'ICA')
% Get selected channels (find the non-zero channels)
iChan = find(any(ListProj(i).Components ~= 0, 2));
% Get selected components
@@ -1068,9 +1097,34 @@
proj = [];
elseif ~isstruct(OldProj)
proj = db_template('projector');
- proj.Components = OldProj;
- proj.Comment = 'Unnamed';
- proj.Status = 1;
+ proj.Components = OldProj;
+ proj.Comment = 'Unnamed';
+ proj.Status = 1;
+ proj.Method = 'Other';
+ elseif ~isfield(OldProj, 'Method') || isempty(OldProj.Method)
+ proj = db_template('projector');
+ proj = struct_copy_fields(proj, OldProj, 1);
+ % Add projector method
+ if isnumeric(proj.SingVal) && (length(proj.SingVal) == length(proj.CompMask))
+ proj.Method = 'SSP_pca';
+ elseif isempty(proj.SingVal) && length(proj.CompMask) == 1 && proj.CompMask == 1
+ proj.Method = 'SSP_mean';
+ elseif ischar(proj.SingVal) && strcmpi(proj.SingVal, 'ICA')
+ proj.Method = 'ICA';
+ proj.SingVal = [];
+ elseif ischar(proj.SingVal) && strcmpi(proj.SingVal, 'REF')
+ proj.Method = 'REF';
+ proj.SingVal = [];
+ elseif isempty(proj.SingVal) && isempty(proj.CompMask)
+ proj.Method = 'Other';
+ end
+ % Try to get ICA method from comment
+ if strcmp(proj.Method, 'ICA')
+ tmp = regexp(proj.Comment, 'ICA_\w*', 'match');
+ if ~isempty(tmp)
+ proj.Method = tmp{1};
+ end
+ end
else
proj = OldProj;
end
diff --git a/toolbox/process/functions/process_sync_recordings.m b/toolbox/process/functions/process_sync_recordings.m
new file mode 100644
index 000000000..333b00df9
--- /dev/null
+++ b/toolbox/process/functions/process_sync_recordings.m
@@ -0,0 +1,310 @@
+function varargout = process_sync_recordings(varargin)
+% process_sync_recordings: Synchronize multiple signals based on common event
+
+% @=============================================================================
+% 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, 2021-2023
+% Raymundo Cassani, 2024
+
+eval(macro_method);
+end
+
+
+%% ===== GET DESCRIPTION =====
+function sProcess = GetDescription()
+ % Description the process
+ sProcess.Comment = 'Synchronyze files';
+ sProcess.Category = 'Custom';
+ sProcess.SubGroup = 'Synchronize';
+ sProcess.Index = 681;
+ % Definition of the input accepted by this process
+ sProcess.InputTypes = {'data', 'raw'};
+ sProcess.OutputTypes = {'data', 'raw'};
+ sProcess.nInputs = 1;
+ sProcess.nMinFiles = 2;
+
+ %Description of options
+ sProcess.options.inputs.Comment = ['For synchronization, please choose an event type ' ...
+ 'which is available in all datasets.
'];
+ sProcess.options.inputs.Type = 'label';
+
+ % Source Event name for synchronization
+ sProcess.options.src.Comment = 'Sync event name: ';
+ sProcess.options.src.Type = 'text';
+ sProcess.options.src.Value = '';
+ % Sync method
+ sProcess.options.method_title.Comment = ' Method for event synchronization::';
+ sProcess.options.method_title.Type = 'label';
+ sProcess.options.method.Comment = {'xcorr : maximize the correlation between the event timing of the two files', ...
+ 'mean : minimize the mean difference between the event timing of the two files'; ...
+ 'xcorr', 'mean'};
+ sProcess.options.method.Type = 'radio_label';
+ sProcess.options.method.Value = 'xcorr';
+end
+
+
+%% ===== FORMAT COMMENT =====
+function Comment = FormatComment(sProcess) %#ok
+ Comment = sProcess.Comment;
+end
+
+
+%% ===== RUN =====
+function OutputFiles = Run(sProcess, sInputs)
+ OutputFiles = {};
+
+ % === Sync event management === %
+ syncEventName = sProcess.options.src.Value;
+ nInputs = length(sInputs);
+ sEvtSync = repmat(db_template('event'), 1, nInputs);
+ sOldTiming = cell(1, nInputs);
+ fs = zeros(1, nInputs);
+
+ % Check: Same FileType for all files
+ is_raw = strcmp({sInputs.FileType},'raw');
+ if ~all(is_raw) && ~all(~is_raw)
+ bst_error('Please do not mix continous (raw) and imported data', 'Synchronize signal', 0);
+ return;
+ end
+ is_raw = is_raw(1);
+
+ bst_progress('start', 'Synchronizing files', 'Loading data...', 0, 3*nInputs);
+
+ % Get Time vector, events and sampling frequency for each file
+ for iInput = 1:nInputs
+ if strcmp(sInputs(iInput).FileType, 'data') % Imported data structure
+ sData = in_bst_data(sInputs(iInput).FileName, 'Time', 'Events');
+ sOldTiming{iInput}.Time = sData.Time;
+ sOldTiming{iInput}.Events = sData.Events;
+ elseif strcmp(sInputs(iInput).FileType, 'raw') % Continuous data file
+ sDataRaw = in_bst_data(sInputs(iInput).FileName, 'Time', 'F');
+ sOldTiming{iInput}.Time = sDataRaw.Time;
+ sOldTiming{iInput}.Events = sDataRaw.F.events;
+ end
+ fs(iInput) = 1/(sOldTiming{iInput}.Time(2) - sOldTiming{iInput}.Time(1)); % in Hz
+ iSyncEvt = strcmp({sOldTiming{iInput}.Events.label}, syncEventName);
+ if any(iSyncEvt)
+ sEvtSync(iInput) = sOldTiming{iInput}.Events(iSyncEvt);
+ end
+ end
+
+ % Check: Sync event must be present in all files
+ if any(~(strcmp({sEvtSync.label}, syncEventName)))
+ bst_error(['Sync event ("' syncEventName '") must be present in all files'], 'Synchronize signal', 0);
+ return;
+ end
+
+ % Check: Sync event must be simple event
+ if any(cellfun(@(x) size(x,1), {sEvtSync.times}) ~= 1)
+ bst_error(['Sync event ("' syncEventName '") must be simple event in all the files'], 'Synchronize signal', 0);
+ return;
+ 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]);
+ sEvtSync([1, im]) = sEvtSync([im, 1]);
+ sOldTiming([1, im]) = sOldTiming([im, 1]);
+ fs([1, im]) = fs([im, 1]);
+
+ % Compute shifiting between file i and first file
+ new_times = cell(1,nInputs);
+ new_times{1} = sOldTiming{1}.Time;
+ mean_shifting = zeros(1, nInputs);
+ for iInput = 2:nInputs
+ isSameNumberEvts = size(sEvtSync(iInput).times, 2) == size(sEvtSync(1).times, 2);
+ % Require same number of events
+ if ~isSameNumberEvts
+ bst_report('Error', sProcess, sInputs, 'Files doesnt have the same number of sync events.');
+ return
+ end
+ % Sync
+ if strcmpi(sProcess.options.method.Value, 'mean')
+ % Mean difference
+ shifting = sEvtSync(iInput).times - sEvtSync(1).times;
+ mean_shifting(iInput) = mean(shifting);
+ offsetStd = std(shifting);
+ elseif strcmpi(sProcess.options.method.Value, 'xcorr')
+ % Cross-correlate trigger signals; need to be at the same sampling frequency
+ tmp_fs = max(fs(iInput), fs(1));
+ tmp_time_a = sOldTiming{iInput}.Time(1):1/tmp_fs:sOldTiming{iInput}.Time(end);
+ tmp_time_b = sOldTiming{1}.Time(1):1/tmp_fs:sOldTiming{1}.Time(end);
+
+ blocA = zeros(1 , length(tmp_time_a));
+ for i_event = 1:size(sEvtSync(iInput).times,2)
+ i_intra_event = panel_time('GetTimeIndices', tmp_time_a, sEvtSync(iInput).times(i_event) + [0 1]');
+ blocA(1,i_intra_event) = 1;
+ end
+
+ blocB = zeros(1 , length(tmp_time_b));
+ for i_event = 1:size(sEvtSync(1).times,2)
+ i_intra_event = panel_time('GetTimeIndices', tmp_time_b, sEvtSync(1).times(i_event) + [0 1]');
+ blocB(1,i_intra_event) = 1;
+ end
+
+ [c,lags] = xcorr(blocA,blocB);
+ [~,colum] = max(c);
+
+ mean_shifting(iInput) = lags(colum) / tmp_fs;
+ offsetStd = 0;
+ end
+ new_times{iInput} = sOldTiming{iInput}.Time - mean_shifting(iInput);
+
+ fprintf('Lag difference between %s and %s : %.2f ms (std: %.2f ms) \n', ...
+ sInputs(1).Condition, sInputs(iInput).Condition, mean_shifting(iInput)*1000, offsetStd*1000);
+ end
+
+ % New start and new end
+ new_start = max(cellfun(@(x)min(x), new_times));
+ new_end = min(cellfun(@(x)max(x), new_times));
+
+ % Compute new time vectors, and new events times
+ sNewTiming = sOldTiming;
+ pool_events = [];
+ for iInput = 1:nInputs
+ index = panel_time('GetTimeIndices', new_times{iInput}, [new_start, new_end]);
+ sNewTiming{iInput}.Time = new_times{iInput}(index) - new_times{iInput}(index(1));
+ tmp_events = sNewTiming{iInput}.Events;
+ for i_event = 1:length(tmp_events)
+ % Update event times
+ tmp_events(i_event).times = tmp_events(i_event).times - mean_shifting(iInput) - new_times{iInput}(index(1));
+ % Remove events outside new time range
+ timeRange = [sNewTiming{iInput}.Time(1), sNewTiming{iInput}.Time(end)];
+ iEventTimesDel = all(or(tmp_events(i_event).times < timeRange(1), tmp_events(i_event).times > timeRange(2)), 1);
+ tmp_events(i_event).times(:,iEventTimesDel) = [];
+ tmp_events(i_event).epochs(iEventTimesDel) = [];
+ if ~isempty(tmp_events(i_event).channels)
+ tmp_events(i_event).channels(iEventTimesDel) = [];
+ end
+ if ~isempty(tmp_events(i_event).notes)
+ tmp_events(i_event).notes(iEventTimesDel) = [];
+ end
+ if ~isempty(tmp_events(i_event).reactTimes)
+ tmp_events(i_event).reactTimes(iEventTimesDel) = [];
+ end
+ % Clip values to time range
+ tmp_events(i_event).times(tmp_events(i_event).times < timeRange(1)) = timeRange(1);
+ tmp_events(i_event).times(tmp_events(i_event).times > timeRange(2)) = timeRange(2);
+ % Aggregate eventes across files
+ if isempty(pool_events)
+ pool_events = tmp_events(i_event);
+ elseif ~strcmp(tmp_events(i_event).label,syncEventName) || (strcmp(tmp_events(i_event).label,syncEventName) && ~any(strcmp({pool_events.label},syncEventName)))
+ pool_events = [pool_events tmp_events(i_event)];
+ end
+ end
+ end
+ % Update polled events
+ for iInput = 1:nInputs
+ sNewTiming{iInput}.Events = pool_events;
+ end
+
+ bst_progress('inc', nInputs);
+ bst_progress('text', 'Saving files...');
+
+ % Save sync data to file
+ for iInput = 1:nInputs
+ if ~is_raw
+ % Load original data
+ sDataSync = in_bst_data(sInputs(iInput).FileName);
+ % Set new time and events
+ sDataSync.Comment = [sDataSync.Comment ' | Synchronized '];
+ sDataSync.Time = sNewTiming{iInput}.Time;
+ sDataSync.Events = sNewTiming{iInput}.Events;
+ % Update data
+ index = panel_time('GetTimeIndices', new_times{iInput}, [new_start, new_end]);
+ sDataSync.F = sDataSync.F(:,index);
+ % History: List of sync files
+ sDataSync = bst_history('add', sDataSync, 'sync', ['List of synchronized files (event = "', syncEventName , '"):']);
+ for ix = 1:nInputs
+ sDataSync = bst_history('add', sDataSync, 'sync', [' - ' sInputs(ix).FileName]);
+ end
+ % Save data
+ OutputFile = bst_process('GetNewFilename', bst_fileparts(sInputs(iInput).FileName), 'data_sync');
+ sDataSync.FileName = file_short(OutputFile);
+ bst_save(OutputFile, sDataSync, 'v7');
+ % Register in database
+ db_add_data(sInputs(iInput).iStudy, OutputFile, sDataSync);
+ else
+ % New raw condition
+ newCondition = [sInputs(iInput).Condition '_synced'];
+ iNewStudy = db_add_condition(sInputs(iInput).SubjectName, newCondition);
+ sNewStudy = bst_get('Study', iNewStudy);
+ % 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 - mean_shifting(iInput) - new_start));
+ end
+ end
+ newStudyPath = bst_fileparts(file_fullpath(sNewStudy.FileName));
+ % Save channel definition
+ ChannelMat = in_bst_channel(sInputs(iInput).ChannelFile);
+ [~, iChannelStudy] = bst_get('ChannelForStudy', iNewStudy);
+ db_set_channel(iChannelStudy, ChannelMat, 0, 0);
+ % Link to raw file
+ OutputFile = bst_process('GetNewFilename', bst_fileparts(sNewStudy.FileName), 'data_0raw_sync');
+ % Raw file
+ [~, rawBaseOut, rawBaseExt] = bst_fileparts(newStudyPath);
+ rawBaseOut = strrep([rawBaseOut rawBaseExt], '@raw', '');
+ RawFileOut = bst_fullfile(newStudyPath, [rawBaseOut '.bst']);
+ % Load original link to raw data
+ sDataRawSync = in_bst_data(sInputs(iInput).FileName, 'F');
+ sFileIn = sDataRawSync.F;
+ % Set new time and events
+ sFileIn.events = sNewTiming{iInput}.Events;
+ sFileIn.header.nsamples = length( sNewTiming{iInput}.Time);
+ sFileIn.prop.times = [ sNewTiming{iInput}.Time(1), sNewTiming{iInput}.Time(end)];
+ sFileOut = out_fopen(RawFileOut, 'BST-BIN', sFileIn, ChannelMat);
+ % Set Output sFile structure
+ sDataSync = in_bst(sInputs(iInput).FileName, [], 1, 1, 'no');
+ sOutMat = rmfield(sDataSync, 'F');
+ sOutMat.format = 'BST-BIN';
+ sOutMat.DataType = 'raw';
+ sOutMat.F = sFileOut;
+ sOutMat.Comment = [sDataSync.Comment ' | Synchronized'];
+ % History: List of sync files
+ sOutMat = bst_history('add', sOutMat, 'sync', ['List of synchronized files (event = "', syncEventName , '"):']);
+ for ix = 1:nInputs
+ sOutMat = bst_history('add', sOutMat, 'sync', [' - ' sInputs(ix).FileName]);
+ end
+ % Update raw data
+ index = panel_time('GetTimeIndices', new_times{iInput}, [new_start, new_end]);
+ sDataSync.F = sDataSync.F(:,index);
+ % Save new link to raw .mat file
+ bst_save(OutputFile, sOutMat, 'v6');
+ % Write block
+ out_fwrite(sFileOut, ChannelMat, 1, [], [], sDataSync.F);
+ % Register in BST database
+ db_add_data(iNewStudy, OutputFile, sOutMat);
+ end
+ OutputFiles{iInput} = OutputFile;
+ bst_progress('inc', 1);
+ end
+ bst_progress('stop');
+end
+
diff --git a/toolbox/process/functions/process_test_normative.m b/toolbox/process/functions/process_test_normative.m
new file mode 100644
index 000000000..4002c4910
--- /dev/null
+++ b/toolbox/process/functions/process_test_normative.m
@@ -0,0 +1,419 @@
+function varargout = process_test_normative( varargin )
+% PROCESS_TEST_NORMATIVE: Compare PSDs of each file A with the distribution of PSDs from files B
+% For testing the normality of the residuals, the Shapiro-Wilk test is used.
+% Careful, the test is less appropriate for large sample sizes (n>=50) [1] and may be too conservative.
+%
+% References:
+% [1] Mishra, Prabhaker, Chandra M Pandey, Uttam Singh, Anshul Gupta, Chinmoy Sahu, and Amit Keshri,
+% Descriptive Statistics and Normality Tests for Statistical Data,
+% Annals of Cardiac Anaesthesia 22, no. 1 (2019): 67–72. https://doi.org/10.4103/aca.ACA_157_18.
+
+% @=============================================================================
+% 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: Pauline Amrouche, 2024
+% Raymundo Cassani, 2024
+
+eval(macro_method);
+end
+
+
+%% ===== GET DESCRIPTION =====
+function sProcess = GetDescription() %#ok
+ % Description the process
+ sProcess.Comment = 'Compare (A) to normative PSDs (B)';
+ sProcess.FileTag = '';
+ sProcess.Category = 'Custom';
+ sProcess.SubGroup = 'Test';
+ sProcess.Index = 110;
+ sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/Tutorials/DeviationMaps';
+ % Definition of the input accepted by this process
+ sProcess.InputTypes = {'timefreq'};
+ sProcess.OutputTypes = {'timefreq'};
+ sProcess.nInputs = 2;
+ sProcess.nMinFiles = 1;
+ sProcess.isSeparator = 1;
+ % Options: Log values
+ sProcess.options.islog.Comment = 'Use log10 values';
+ sProcess.options.islog.Type = 'checkbox';
+ sProcess.options.islog.Value = 1;
+ % Options : Select deviation level
+ sProcess.options.devlevel.Comment = 'Deviation level (range 0-1):';
+ sProcess.options.devlevel.Type = 'value';
+ sProcess.options.devlevel.Value = {0.05, '', 2};
+ % Options: Normal distribution
+ sProcess.options.isnormal.Comment = 'Assume normal distribution of residuals';
+ sProcess.options.isnormal.Type = 'checkbox';
+ sProcess.options.isnormal.Value = 0;
+ sProcess.options.isnormal.Controller = 'Normal';
+ % Options : Shapiro-Wilk test for normality
+ sProcess.options.shapiro.Comment = 'Test for normality of residuals (Shapiro-Wilk)';
+ sProcess.options.shapiro.Type = 'checkbox';
+ sProcess.options.shapiro.Value = 1;
+ sProcess.options.shapiro.Class = 'Normal';
+ % Options: Frequency definition
+ % === Frequency output
+ sProcess.options.freqout.Comment = {'Same as input', 'Frequency range', 'Frequency bands', 'Frequency definition:'; ...
+ 'input', 'range', 'bands', ''};
+ sProcess.options.freqout.Type = 'radio_linelabel';
+ sProcess.options.freqout.Value = 'input';
+ sProcess.options.freqout.Controller.range = 'range';
+ sProcess.options.freqout.Controller.bands = 'bands';
+ % === Individual frequency range
+ sProcess.options.freqrange.Comment = 'Frequency range: ';
+ sProcess.options.freqrange.Type = 'freqrange_static'; % 'freqrange'
+ sProcess.options.freqrange.Value = {[1 150], 'Hz', 1};
+ sProcess.options.freqrange.Class = 'range';
+ % === Frequency bands
+ sProcess.options.freqbands.Comment = '';
+ sProcess.options.freqbands.Type = 'groupbands';
+ sProcess.options.freqbands.Value = bst_get('DefaultFreqBands');
+ sProcess.options.freqbands.Class = 'bands';
+end
+
+
+%% ===== FORMAT COMMENT =====
+function Comment = FormatComment(sProcess) %#ok
+ Comment = sProcess.Comment;
+end
+
+
+%% ===== RUN =====
+function sOutput = Run(sProcess, sInputsA, sInputsB) %#ok
+ % Initialize output
+ sOutput = cell(1, length(sInputsA));
+
+ % Fetch user options
+ OPTIONS.IsLog = sProcess.options.islog.Value;
+ OPTIONS.FreqOut = sProcess.options.freqout.Value;
+ OPTIONS.FreqRange = sProcess.options.freqrange.Value{1};
+ OPTIONS.FreqBands = sProcess.options.freqbands.Value;
+ OPTIONS.DevLevel = sProcess.options.devlevel.Value{1};
+ OPTIONS.IsNormal = sProcess.options.isnormal.Value;
+ OPTIONS.TestNormality = sProcess.options.shapiro.Value;
+
+ % FilesA in FilesB
+ Lia = ismember({sInputsA.FileName}, {sInputsB.FileName});
+ sInputsUnique = [sInputsB, sInputsA(~Lia)];
+
+ % Validate inputs
+ [errMsg, iInputErr, nRows, Freqs] = CheckInputs(sInputsUnique);
+ if ~isempty(errMsg) && ~isempty(iInputErr)
+ bst_report('Error', sProcess, sInputsUnique(iInputErr), errMsg);
+ return
+ end
+
+ % Frequency options
+ errMsg = '';
+ switch OPTIONS.FreqOut
+ case 'bands'
+ if iscell(Freqs)
+ requestedBands = cellfun(@(x,y,z) strjoin({x,y,z}, ';'), OPTIONS.FreqBands(:,1), OPTIONS.FreqBands(:,2), OPTIONS.FreqBands(:,3), 'UniformOutput', 0);
+ availableBands = cellfun(@(x,y,z) strjoin({x,y,z}, ';'), Freqs(:,1), Freqs(:,2), Freqs(:,3), 'UniformOutput', 0);
+ [Lia,Locb] = ismember(requestedBands, availableBands);
+ if ~all(Lia)
+ errMsg = 'Not all requested bands are defined in the frequency bands of the input files.';
+ end
+ freqMask = false(1, size(Freqs,1));
+ freqMask(Locb) = true;
+ OPTIONS.FreqBands = [];
+ else
+ tmp = str2num(strjoin(OPTIONS.FreqBands(:,2), ','));
+ requestedBandsLimits(1) = min(tmp);
+ requestedBandsLimits(2) = max(tmp);
+ if requestedBandsLimits(1) < Freqs(1) || requestedBandsLimits(2) > Freqs(end)
+ errMsg = 'Frequency bands must be defined inside the frequency range of the input files.';
+ end
+ freqMask = true(1, size(OPTIONS.FreqBands,1));
+ end
+
+ case 'range'
+ OPTIONS.FreqBands = [];
+ if iscell(Freqs)
+ errMsg = 'Cannot use frequency range ouput if input files are defined in frequency bands.';
+ else
+ if OPTIONS.FreqRange(1) < Freqs(1) || OPTIONS.FreqRange(2) > Freqs(end)
+ errMsg = 'Frequency bands must be defined inside the frequency range of the input files.';
+ end
+ % find closest
+ iFreqRange1 = bst_closest(OPTIONS.FreqRange(1), Freqs);
+ iFreqRange2 = bst_closest(OPTIONS.FreqRange(2), Freqs);
+ freqMask = false(1, length(Freqs));
+ freqMask(iFreqRange1:iFreqRange2) = true;
+ end
+
+ case 'input'
+ % Keep all frequencies
+ OPTIONS.FreqBands = [];
+ freqMask = true(1, length(Freqs));
+
+ end
+ if ~isempty(errMsg)
+ bst_report('Error', sProcess, sInputsB(1), errMsg);
+ return
+ end
+
+ % Initialize array to hold distributions
+ tfDataB = zeros(nRows, sum(freqMask), length(sInputsB));
+ % Load normative values
+ for iInputB = 1 : length(sInputsB)
+ % Load and preprocess timefreq file
+ timefreqMat = PreProcessInput(sInputsB(iInputB), freqMask, OPTIONS);
+ tfDataB(:,:,iInputB) = squeeze(timefreqMat.TF);
+ end
+ % Compute normative distribution
+ normDistrib = ComputeNormDistrib(sProcess, tfDataB, OPTIONS);
+ clear tfDataB
+ % Compare each SubjectA PSD to the normative distribution
+ for iSubA = 1:length(sInputsA)
+ % Load and preprocess timefreq file
+ timefreqMat = PreProcessInput(sInputsA(iSubA), freqMask, OPTIONS);
+ % Compute significance deviation map, replace TF field
+ timefreqMat = CompareToNormDistrib(timefreqMat, normDistrib, OPTIONS);
+ % Create the output file
+ sOutput{iSubA} = SaveData(sInputsA(iSubA), sInputsB, timefreqMat, OPTIONS);
+ end
+end
+
+
+%% ===== FORMAT FILE COMMENT =====
+function comment = GetComment(tfMat, options)
+ % Initialize suffix for file comment
+ comment_suffix = '';
+ if options.IsLog
+ comment_suffix = [comment_suffix, ' log'];
+ end
+ if options.IsNormal
+ comment_suffix = [comment_suffix, ' normal'];
+ end
+ % Check that the options are valid and update the comment suffix
+ switch options.FreqOut
+ case 'bands'
+ comment_suffix = [comment_suffix, ' bands'];
+
+ case 'range'
+ comment_suffix = [comment_suffix, sprintf(' %d-%dHz', options.FreqRange(1), options.FreqRange(2))];
+
+ case 'input'
+ if iscell(tfMat.Freqs)
+ comment_suffix = [comment_suffix, ' bands'];
+ end
+ end
+ % Format the comment suffix
+ if ~isempty(comment_suffix)
+ comment_suffix = ['| ' comment_suffix];
+ end
+ comment = sprintf('comp. to norm: devLevel (%.2f) %s', options.DevLevel, comment_suffix);
+end
+
+
+%% ===== PREPROCESS INPUT DATA =====
+% Load and preprocess timefreq file
+function timefreqMat = PreProcessInput(sInput, freqMask, options)
+ % Load PSD file
+ timefreqMat = in_bst_timefreq(sInput.FileName, 1);
+ % Extract frequency bands (input has Frequency vector, output resquested in Bands)
+ if ~isempty(options.FreqBands)
+ timefreqMat = process_tf_bands('Compute', timefreqMat, options.FreqBands, []);
+ end
+ % Keep requested frequencies
+ timefreqMat.TF = timefreqMat.TF(:, 1, freqMask);
+ if iscell(timefreqMat.Freqs)
+ timefreqMat.Freqs = timefreqMat.Freqs(freqMask, :);
+ else
+ timefreqMat.Freqs = timefreqMat.Freqs(freqMask);
+ end
+ % If log values, apply log
+ if options.IsLog
+ timefreqMat.TF = log10(timefreqMat.TF);
+ end
+end
+
+
+%% ===== VALIDATE INPUT FILES =====
+% Check that all files are PSD files, have same DataType, have same space, and have same frequency definition
+function [errMsg, iInputErr, nRows, Freqs] = CheckInputs(sInputs)
+ errMsg = '';
+ nRows = [];
+ Freqs = [];
+ % Files must: be PSD, have same DataType, have same space, and have same frequency definition
+ for iInput = 1 : length(sInputs)
+ iInputErr = iInput;
+ timefreqMat = in_bst_timefreq(sInputs(iInput).FileName, 0, 'Method', 'RowNames', 'DataType', 'HeadModelType', 'SurfaceFile', 'HeadModelFile', 'Freqs');
+ if ~strcmpi(timefreqMat.Method, 'psd')
+ errMsg = 'Input files must be PSD files.';
+ return
+ end
+ % Verify files that must be common
+ if iInput == 1
+ % Get reference fields
+ refDataType = timefreqMat.DataType;
+ switch refDataType
+ case {'data', 'matrix'}
+ refCommonSpaceFile = timefreqMat.RowNames;
+ case 'results'
+ refHeadModelType = timefreqMat.HeadModelType;
+ switch refHeadModelType
+ case 'surface'
+ refCommonSpaceFile = timefreqMat.SurfaceFile;
+ case 'volume'
+ refCommonSpaceFile = timefreqMat.HeadModelFile;
+ otherwise
+ errMsg = ['HeadModel of type ' refHeadModelType ' is not supported.'];
+ return
+ end
+ end
+ refRowNames = timefreqMat.RowNames;
+ nRows = length(refRowNames);
+ refFreqs = timefreqMat.Freqs;
+
+ else
+ % Check against reference DataType
+ if ~strcmpi(timefreqMat.DataType, refDataType)
+ errMsg = 'All PSD files must share the same "DataType"';
+ return
+ end
+ % PSD files must be computed in the same modality and space
+ switch timefreqMat.DataType
+ case {'data', 'matrix'}
+ if ~isequal(refCommonSpaceFile, timefreqMat.RowNames)
+ errMsg = 'PSD files from sensors (or matrices) must share the same channel names.';
+ return
+ end
+ case 'results'
+ switch timefreqMat.HeadModelType
+ case 'surface'
+ if ~isequal(refCommonSpaceFile, timefreqMat.SurfaceFile)
+ errMsg = 'PSD files from surface sources must share the same surface file.';
+ return
+ end
+ case 'volume'
+ if ~isequal(refCommonSpaceFile, timefreqMat.RowNames)
+ errMsg = 'PSD files from volume sources must share the head model (volume grid) file.';
+ return
+ end
+ otherwise
+ errMsg = ['HeadModel of type ' timefreqMat.HeadModelType ' is not supported.'];
+ return
+ end
+ end
+ % Check frequency definition
+ if isequal(size(timefreqMat.Freqs), size(refFreqs))
+ if iscell(refFreqs)
+ for iBand = 1 : size(timefreqMat.Freqs, 1)
+ if ~strcmpi(strjoin(timefreqMat.Freqs(iBand, :)), strjoin(refFreqs(iBand, :)))
+ errMsg = 'PSD files have different frequency band definition.';
+ return
+ end
+ end
+ else
+ if abs(sum(timefreqMat.Freqs - refFreqs)) > 1e-6
+ errMsg = 'PSD files have different frequency axes.';
+ return
+ end
+ end
+ else
+ errMsg = 'PSD files must have the same frequency definition.';
+ return
+ end
+ end
+ end
+ Freqs = refFreqs;
+ iInputErr = [];
+end
+
+
+%% ===== COMPUTE NORMATIVE DISTRIBUTION =====
+% Compute statistics for normative distribution
+function normDistrib = ComputeNormDistrib(sProcess, norm_values, options)
+ [nRows, nFreqs, ~] = size(norm_values);
+ % Compute the mean and std of the normative values, across subjects
+ normDistrib.norm_means = mean(norm_values, 3);
+ normDistrib.norm_stds = std(norm_values, 0, 3);
+
+ % Compute the residuals (z-scores) for normative values
+ residuals = (norm_values - normDistrib.norm_means) ./ normDistrib.norm_stds;
+
+ if options.IsNormal && options.TestNormality
+ % Assuming that the residuals are normally distributed
+ % We can test the normality of the residuals using the Shapiro-Wilk test
+ % Results are displayed in the report
+ p_shapiro = zeros(nRows, nFreqs);
+ % Compute the Shapiro-Wilk test for normality
+ for iRow = 1:nRows
+ for iFreq = 1:nFreqs
+ [~, p] = swtest(residuals(iRow, iFreq, :));
+ p_shapiro(iRow, iFreq) = p;
+ end
+ end
+ % Report the results of normality test
+ bst_report('Info', sProcess, [], ['Shapiro-Wilk test for normality of residuals:', 10, ...
+ sprintf('Significant at p < 0.05 : %d/%d', sum(p_shapiro < 0.05, "all"), nRows*nFreqs), 10,...
+ sprintf('Significant at p < 0.1 : %d/%d', sum(p_shapiro < 0.1, "all"), nRows*nFreqs)]);
+ end
+
+ % Compute the percentiles of the distribution of residuals
+ if options.IsNormal
+ normDistrib.norm_percentile = norminv(1-options.DevLevel/2);
+ else
+ normDistrib.percentiles = prctile(residuals, [(options.DevLevel/2)*100, (1-(options.DevLevel/2))*100], 3);
+ end
+end
+
+
+%% ===== COMPARE TO NORMATIVE DISTRIBUTION =====
+% Compute the deviation map wrt the normative distribution
+function timefreqMat = CompareToNormDistrib(timefreqMat, normDistrib, options)
+ % Compute the z-scores
+ z_scores = (squeeze(timefreqMat.TF) - normDistrib.norm_means) ./ normDistrib.norm_stds;
+ % Identify z_scores that are significantly different from the normative distribution
+ if options.IsNormal
+ signif = abs(z_scores) > normDistrib.norm_percentile;
+ else
+ signif = (z_scores < normDistrib.percentiles(:, :, 1)) | (z_scores > normDistrib.percentiles(:, :, 2));
+ end
+ % Restore time dimenstion
+ signif = permute(signif, [1,3,2]);
+ timefreqMat.TF = signif;
+end
+
+
+%% ===== SAVE TEST RESULTS =====
+function output = SaveData(sInputA, sInputsB, tfMat, options)
+ % Add comment, change filename and save
+ tfMat.ColormapType = 'stat2';
+
+ % Get file comment from options
+ tfMat.Comment = GetComment(tfMat, options);
+
+ % History
+ tfMat = bst_history('add', tfMat, 'comp2norm', sprintf('File compared to normative: %s', sInputA.FileName));
+ tfMat = bst_history('add', tfMat, 'comp2norm', sprintf('devLevel = %.2f, isNorm = %d, isLog10 = %d', options.DevLevel, options.IsNormal, options.IsLog));
+ % History: List files used for normative
+ tfMat = bst_history('add', tfMat, 'comp2norm', 'List of files used for normative distribution:');
+ for i = 1:length(sInputsB)
+ tfMat = bst_history('add', tfMat, 'comp2norm', [' - ' sInputsB(i).FileName]);
+ end
+ % Output filename
+ [originalPath, originalBase, originalExt] = bst_fileparts(file_fullpath(sInputA.FileName));
+ output = bst_fullfile(originalPath, [originalBase, '_' 'comp2norm', originalExt]);
+ output = file_unique(output);
+ % Save the file
+ bst_save(output, tfMat, 'v6');
+ db_add_data(sInputA.iStudy, output, tfMat);
+end
\ No newline at end of file
diff --git a/toolbox/process/functions/process_timefreq.m b/toolbox/process/functions/process_timefreq.m
index 9c0b86a8e..643387c85 100644
--- a/toolbox/process/functions/process_timefreq.m
+++ b/toolbox/process/functions/process_timefreq.m
@@ -93,6 +93,7 @@
case 'process_hilbert', strProcess = 'hilbert';
case 'process_fft', strProcess = 'fft';
case 'process_psd', strProcess = 'psd';
+ case 'process_psd_features', strProcess = 'psd';
case 'process_sprint', strProcess = 'sprint';
case 'process_ft_mtmconvol', strProcess = 'mtmconvol';
otherwise, error('Unsupported process.');
@@ -163,16 +164,33 @@
tfOPTIONS.WinLength = sProcess.options.win_length.Value{1};
tfOPTIONS.WinOverlap = sProcess.options.win_overlap.Value{1};
end
- if isfield(sProcess.options, 'win_std') && ~isempty(sProcess.options.win_std) && ~isempty(sProcess.options.win_std.Value)
- tfOPTIONS.WinStd = sProcess.options.win_std.Value;
- if tfOPTIONS.WinStd
- tfOPTIONS.Comment = [tfOPTIONS.Comment ' std'];
+ % Aggregating function across windows (PSD)
+ if isfield(sProcess.options, 'win_std') && isfield(sProcess.options.win_std, 'Value') && ~isempty(sProcess.options.win_std.Value)
+ switch lower(sProcess.options.win_std.Value)
+ case {0, 'mean'}
+ tfOPTIONS.WinFunc = 'mean';
+ case {1, 'std'}
+ tfOPTIONS.WinFunc = 'std';
+ tfOPTIONS.Comment = [tfOPTIONS.Comment, ' ', tfOPTIONS.WinFunc];
+ case {2, 'mean+std'}
+ tfOPTIONS.WinFunc = 'mean+std';
+ otherwise
+ bst_report('Error', sProcess, [], ['Invalid "' num2str(lower(sProcess.options.win_std.Value)) '" window aggregating function.']);
+ return;
end
end
% If units specified (PSD)
if isfield(sProcess.options, 'units') && ~isempty(sProcess.options.units) && ~isempty(sProcess.options.units.Value)
tfOPTIONS.PowerUnits = sProcess.options.units.Value;
- end
+ end
+ % Compute relative power (PSD)
+ if isfield(sProcess.options, 'relative') && ~isempty(sProcess.options.relative) && ~isempty(sProcess.options.relative.Value)
+ tfOPTIONS.IsRelative = sProcess.options.relative.Value;
+ if tfOPTIONS.IsRelative
+ % Add relative to comment
+ tfOPTIONS.Comment = [tfOPTIONS.Comment, ' relative'];
+ end
+ end
% Multitaper options
if isfield(sProcess.options, 'mt_taper') && ~isempty(sProcess.options.mt_taper) && ~isempty(sProcess.options.mt_taper.Value)
if iscell(sProcess.options.mt_taper.Value)
@@ -250,6 +268,7 @@
'timewindow', tfOPTIONS.TimeWindow, ...
'scouts', tfOPTIONS.Clusters, ...
'scoutfunc', ExtractScoutFunc, ... % If ScoutFunc is not defined, use the scout function available in each scout
+ 'flatten', 0, ...
'isflip', isflip, ...
'isnorm', 0, ...
'concatenate', 0, ...
diff --git a/toolbox/process/functions/process_timeoffset.m b/toolbox/process/functions/process_timeoffset.m
index 7c2f72dbb..592e2dfac 100644
--- a/toolbox/process/functions/process_timeoffset.m
+++ b/toolbox/process/functions/process_timeoffset.m
@@ -20,6 +20,7 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2010-2016
+% Raymundo Cassani, 2024
eval(macro_method);
end
@@ -30,13 +31,13 @@
% Description the process
sProcess.Comment = 'Add time offset';
sProcess.FileTag = 'timeoffset';
- sProcess.Category = 'Filter';
+ sProcess.Category = 'File';
sProcess.SubGroup = 'Pre-process';
sProcess.Index = 76;
sProcess.Description = '';
% Definition of the input accepted by this process
- sProcess.InputTypes = {'data', 'results', 'matrix', 'timefreq'};
- sProcess.OutputTypes = {'data', 'results', 'matrix', 'timefreq'};
+ sProcess.InputTypes = {'data', 'raw', 'matrix'};
+ sProcess.OutputTypes = {'data', 'raw', 'matrix'};
sProcess.nInputs = 1;
sProcess.nMinFiles = 1;
@@ -52,6 +53,11 @@
sProcess.options.offset.Comment = 'Time offset:';
sProcess.options.offset.Type = 'value';
sProcess.options.offset.Value = {0, 'ms', []};
+ % === Overwrite
+ sProcess.options.overwrite.Comment = 'Overwrite input files';
+ sProcess.options.overwrite.Type = 'checkbox';
+ sProcess.options.overwrite.Value = 0;
+ sProcess.options.overwrite.Group = 'output';
end
@@ -62,11 +68,81 @@
%% ===== RUN =====
-function sInput = Run(sProcess, sInput) %#ok
+function OutputFile = Run(sProcess, sInput) %#ok
+ OutputFile = {};
+
% Get inputs
- TimeOffset = sProcess.options.offset.Value{1};
- % Apply offset
- sInput.TimeVector = sInput.TimeVector + TimeOffset;
+ OffsetTime = sProcess.options.offset.Value{1};
+ isOverwrite = sProcess.options.overwrite.Value;
+
+ % ===== LOAD FILE =====
+ % Get file descriptor
+ isRaw = strcmpi(sInput.FileType, 'raw');
+ % Load file
+ DataMat = in_bst_data(sInput.FileName);
+ if isRaw
+ sEvents = DataMat.F.events;
+ sFreq = DataMat.F.prop.sfreq;
+ DataMat.Time = [DataMat.Time(1), DataMat.Time(end)];
+ else
+ sEvents = DataMat.Events;
+ sFreq = 1 ./ (DataMat.Time(2) - DataMat.Time(1));
+ end
+
+ % ===== PROCESS =====
+ % Apply offset to time
+ DataMat.Time = DataMat.Time + OffsetTime;
+ if isRaw
+ DataMat.F.prop.times = DataMat.Time;
+ if isfield(DataMat.F, 'epochs') && ~isempty(DataMat.F.epochs)
+ [DataMat.F.epochs(:).times] = deal(DataMat.Time);
+ end
+ end
+
+ % Add offset to all events
+ for iEvt = 1:length(sEvents)
+ sEvents(iEvt).times = round((sEvents(iEvt).times + OffsetTime) .* sFreq) ./ sFreq;
+ end
+ if isRaw
+ DataMat.F.events = sEvents;
+ else
+ DataMat.Events = sEvents;
+ end
+
+ % ===== SAVE FILE =====
+ % Add history entry
+ DataMat = bst_history('add', DataMat, 'timeoffset', sprintf('Added time offset %1.4fs', OffsetTime));
+ DataMat.Comment = [DataMat.Comment ' | ' sProcess.FileTag];
+
+ % Overwrite the input file
+ if isOverwrite
+ OutputFile = file_fullpath(sInput.FileName);
+ bst_save(OutputFile, DataMat, 'v6');
+ % Reload study
+ db_reload_studies(sInput.iStudy);
+ % Save new file
+ else
+ % Create new raw condition
+ if isRaw
+ ChannelFile = sInput.ChannelFile;
+ newCondition = [sInput.Condition '_' sProcess.FileTag];
+ iStudy = db_add_condition(sInput.SubjectName, newCondition);
+ sNewStudy = bst_get('Study', iStudy);
+ db_set_channel(iStudy, ChannelFile, 0, 0);
+ newStudyPath = bst_fileparts(file_fullpath(sNewStudy.FileName));
+ [~, base, ext] = bst_fileparts(sInput.FileName);
+ OutputFile = bst_fullfile(newStudyPath, [base '.' ext]);
+ else
+ OutputFile = file_fullpath(sInput.FileName);
+ iStudy = sInput.iStudy;
+ end
+ % Unique output filename
+ OutputFile = file_unique(strrep(OutputFile, '.mat', ['_' sProcess.FileTag '.mat']));
+ % Save file
+ bst_save(OutputFile, DataMat, 'v6');
+ % Add file to database structure
+ db_add_data(iStudy, OutputFile, DataMat);
+ end
end
diff --git a/toolbox/process/panel_process2.m b/toolbox/process/panel_process2.m
index 8b37cf096..c1bcc9aa0 100644
--- a/toolbox/process/panel_process2.m
+++ b/toolbox/process/panel_process2.m
@@ -139,7 +139,7 @@
'process_plv1', 'process_plv1n', 'process_plv2'}) && ...
(~isfield(sProcesses(iProc).options.tfedit, 'Value') || isempty(sProcesses(iProc).options.tfedit.Value))) % check 'tfedit' field
bst_error(['Please check the advanced options of the process "', sProcesses(iProc).Comment, '" before running the pipeline.'], 'Pipeline editor', 0);
- panel_process_select('ShowPanel', {sFilesA.FileName}, {sFilesB.FileName}, sProcesses);
+ panel_process_select('ShowPanel', [{sFilesA.FileName}, {sFilesB.FileName}], sProcesses);
return
end
end
diff --git a/toolbox/process/panel_process_select.m b/toolbox/process/panel_process_select.m
index fda548c8e..7c3a6f778 100644
--- a/toolbox/process/panel_process_select.m
+++ b/toolbox/process/panel_process_select.m
@@ -1107,7 +1107,15 @@ function UpdateProcessOptions()
end
% Set validation callbacks
java_setcb(jCombo, 'ActionPerformedCallback', @(h,ev)SetOptionValue(iProcess, optNames{iOpt}, {cellValues{2,ev.getSource().getSelectedIndex()+1}, option.Value{2}}));
-
+ % If class controller not selected, toggle off class
+ if isfield(option, 'Controller') && ~isempty(option.Controller) && isstruct(option.Controller)
+ for f = fieldnames(option.Controller)'
+ if ~strcmpi(f{1}, option.Value{1}) && ~isempty(option.Controller.(f{1})) && ~(isfield(option.Controller, option.Value{1}) && isequal(option.Controller.(option.Value{1}), option.Controller.(f{1})))
+ ClassesToToggleOff{end+1} = option.Controller.(f{1});
+ end
+ end
+ end
+
case 'freqsel'
% Load Freq field from the input file
if strcmpi(sFiles(1).FileType, 'timefreq')
@@ -1384,13 +1392,35 @@ function UpdateProcessOptions()
end
end
% Create controls
- gui_component('label', jPanelOpt, [], ['', option.Comment, ' ']);
+ jLabel = gui_component('label', jPanelOpt, [], ['', option.Comment, ' ']);
jText = gui_component('text', jPanelOpt, [], strFiles);
jText.setEditable(0);
jText.setPreferredSize(java_scaled('dimension', 210, 20));
isUpdateTime = strcmpi(option.Type, 'datafile');
- gui_component('button', jPanelOpt, '', '...', [],[], @(h,ev)PickFile_Callback(iProcess, optNames{iOpt}, jText, isUpdateTime));
-
+ if strcmp(strFunction, 'process_export_file')
+ if length(sFiles) > 1
+ % Export multiple files, suggest dir name to export files (filenames from Brainstorm DB)
+ jLabel.setText('Output dir');
+ GlobalData.Processes.Current(iProcess).options.(optNames{iOpt}).Value{7} = 'dirs';
+ LastUsedDirs = bst_get('LastUsedDirs');
+ GlobalData.Processes.Current(iProcess).options.(optNames{iOpt}).Value{1} = LastUsedDirs.ExportData;
+ jText.setText(LastUsedDirs.ExportData);
+ else
+ % Export one file, suggest filename for new file from Input file
+ jLabel.setText('Output file');
+ GlobalData.Processes.Current(iProcess).options.(optNames{iOpt}).Value{7} = 'files';
+ if isempty(GlobalData.Processes.Current(iProcess).options.(optNames{iOpt}).Value{1}) || strcmp(option.Value{7}, 'dirs')
+ % Used in SaveFile_Callback() to suggeste name of export file
+ GlobalData.Processes.Current(iProcess).options.(optNames{iOpt}).Value{1} = sFiles(1).FileName;
+ end
+ jText.setText(GlobalData.Processes.Current(iProcess).options.(optNames{iOpt}).Value{1});
+ end
+ gui_component('button', jPanelOpt, '', '...', [],[], @(h,ev)SaveFile_Callback(iProcess, optNames{iOpt}, jText));
+ else
+ % Pick file or dir (Open File or Select Dir to Save)
+ gui_component('button', jPanelOpt, '', '...', [],[], @(h,ev)PickFile_Callback(iProcess, optNames{iOpt}, jText, isUpdateTime));
+ end
+
case 'editpref'
gui_component('label', jPanelOpt, [], ['', option.Comment{2}, ' ']);
gui_component('button', jPanelOpt, [], 'Edit...', [],[], @(h,ev)EditProperties_Callback(iProcess, optNames{iOpt}));
@@ -1516,6 +1546,44 @@ function UpdateProcessOptions()
eventList.add('br', jScroll);
optionPanel.add(eventList);
jPanelOpt.add(optionPanel);
+
+ case {'list_vertical', 'list_horizontal'}
+ % List items
+ listModel = javax.swing.DefaultListModel();
+ for iItem = 1 : length(option.Comment)-1
+ listModel.addElement(option.Comment{iItem});
+ end
+ % Create list
+ jList = java_create('javax.swing.JList');
+ % Orientation
+ if strcmpi(option.Type, 'list_vertical')
+ jList.setLayoutOrientation(jList.HORIZONTAL_WRAP);
+ else
+ jList.setLayoutOrientation(jList.VERTICAL_WRAP);
+ end
+ jList.setModel(listModel);
+ jList.setVisibleRowCount(-1);
+ jList.setCellRenderer(BstStringListRenderer(fontSize));
+ jList.setEnabled(1);
+ % Last item in list is the list comment
+ gui_component('label', jPanelOpt, [], option.Comment{end});
+ % Restore previous selected items
+ gui_component('label', jPanelOpt, 'hfill', ' ', [],[],[],[]);
+ if ~isempty(sProcess.options.(optNames{iOpt}).Value)
+ [~, iSelItems] = ismember(sProcess.options.(optNames{iOpt}).Value, option.Comment);
+ iSelItems(iSelItems==0) = [];
+ if length(iSelItems) == length(sProcess.options.(optNames{iOpt}).Value)
+ jList.setSelectedIndices(iSelItems-1);
+ end
+ end
+ java_setcb(jList, 'ValueChangedCallback', @(h,ev)ItemSelection_Callback(iProcess, optNames{iOpt}, jList));
+ % Create scroll panel
+ jScroll = javax.swing.JScrollPane(jList);
+ % Horizontal glue
+ jPanelOpt.add('br hfill vfill', jScroll);
+ % Set preferred size for the container
+ prefPanelSize = java_scaled('dimension', 250,180);
+
end
jPanelOpt.setPreferredSize(prefPanelSize);
end
@@ -1796,6 +1864,141 @@ function PickFile_Callback(iProcess, optName, jText, isUpdateTime)
end
+ %% ===== OPTIONS: SAVE FILE CALLBACK =====
+ function SaveFile_Callback(iProcess, optName, jText)
+ % Get default import directory and formats
+ LastUsedDirs = bst_get('LastUsedDirs');
+ DefaultFormats = bst_get('DefaultFormats');
+ % Get file selection options
+ selectOptions = GlobalData.Processes.Current(iProcess).options.(optName).Value;
+ if (length(selectOptions) == 9)
+ DialogType = selectOptions{3};
+ WindowTitle = selectOptions{4};
+ DefaultOutFile = selectOptions{5};
+ SelectionMode = selectOptions{6};
+ FilesOrDir = selectOptions{7};
+ Filters = selectOptions{8};
+ DefaultFormat = selectOptions{9};
+ if isfield(DefaultFormats, DefaultFormat) && isempty(selectOptions{2})
+ defaultFilter = DefaultFormats.(DefaultFormat);
+ else
+ defaultFilter = selectOptions{2};
+ end
+ else
+ DialogType = 'save';
+ WindowTitle = 'Export file';
+ DefaultOutFile = '';
+ SelectionMode = 'single';
+ FilesOrDir = 'files';
+ Filters = {{'*'}, 'All files (*.*)', 'ALL'};
+ defaultFilter = [];
+ end
+
+ % First input file
+ inBstFile = selectOptions{1};
+ % Filters and extension according to file type
+ fileType = file_gettype(inBstFile);
+ isRaw = 0;
+ if strcmp(fileType, 'data') && ~isempty(strfind(inBstFile, '_0raw'))
+ isRaw = 1;
+ fileType = 'raw';
+ end
+ if isempty(Filters)
+ Filters = bst_get('FileFilters', [fileType, 'out']);
+ end
+ % Select only Filter if not provided
+ if isempty(defaultFilter)
+ switch fileType
+ case 'raw'
+ defaultFilter = 'BST-BIN';
+ case {'data', 'results', 'timefreq', 'matrix'}
+ defaultFilter = 'BST';
+ end
+ end
+ % Get extension for filter
+ iFilter = find(ismember(Filters(:,3), defaultFilter), 1, 'first');
+ if isempty(iFilter)
+ iFilter = 1;
+ end
+ fExt = Filters{iFilter, 1}{1};
+ % Verify that extension for BST format ends in '.ext' (no 'BST' format for raw data)
+ if strcmp(defaultFilter, 'BST') && isempty(regexp(DefaultOutFile, '\.\w+$', 'once')) && ~(strcmp(fileType, 'data') && isRaw)
+ fExt = '.mat';
+ end
+
+ % Suggest filename or dir
+ switch FilesOrDir
+ % Suggest filename
+ case 'files'
+ switch(fileType)
+ case 'data'
+ [~, fBase] = bst_fileparts(inBstFile);
+ fBase = strrep(fBase, '_data', '');
+ fBase = strrep(fBase, 'data_', '');
+ fBase = strrep(fBase, '0raw_', '');
+
+ case {'results', 'link'}
+ if strcmp(fileType, 'link')
+ [kernelFile, dataFile] = file_resolve_link(inBstFile);
+ [~, kBase] = bst_fileparts(kernelFile);
+ [~, fBase] = bst_fileparts(dataFile);
+ fBase = [kBase, '_' ,fBase];
+ else
+ [~, fBase] = bst_fileparts(inBstFile);
+ end
+ fBase = strrep(fBase, '_results', '');
+ fBase = strrep(fBase, 'results_', '');
+
+ case 'timefreq'
+ [~, fBase] = bst_fileparts(inBstFile);
+ fBase = strrep(fBase, '_timefreq', '');
+ fBase = strrep(fBase, 'timefreq_', '');
+
+ case 'matrix'
+ [~, fBase] = bst_fileparts(inBstFile);
+ fBase = strrep(fBase, '_matrix', '');
+ fBase = strrep(fBase, 'matrix_', '');
+
+ otherwise
+ % e.g., user set outfile more than once
+ [~, fBase] = bst_fileparts(inBstFile);
+
+ end
+ DefaultOutFile = bst_fullfile(LastUsedDirs.ExportData, [fBase, fExt]);
+
+ % Suggest directory
+ case 'dirs'
+ DefaultOutFile = bst_fullfile(LastUsedDirs.ExportData);
+ end
+
+ % Pick a file
+ [OutputFile, FileFormat] = java_getfile(DialogType, WindowTitle, DefaultOutFile, SelectionMode, FilesOrDir, Filters, defaultFilter);
+ % If nothing selected
+ if isempty(OutputFile)
+ return
+ end
+ % Update ExportData path
+ if strcmp(FilesOrDir, 'dirs')
+ % Remove extension (introduced by the Filters)
+ [fPath, fBase] = bst_fileparts(OutputFile);
+ OutputFile = bst_fullfile(fPath, fBase);
+ LastUsedDirs.ExportData = OutputFile;
+ elseif strcmp(FilesOrDir, 'files')
+ fPath = bst_fileparts(OutputFile);
+ LastUsedDirs.ExportData = fPath;
+ end
+ bst_set('LastUsedDirs', LastUsedDirs);
+
+ % Update the values
+ selectOptions{1} = OutputFile;
+ selectOptions{2} = FileFormat;
+ % Save the new values
+ SetOptionValue(iProcess, optName, selectOptions);
+ % Update the text field
+ jText.setText(OutputFile);
+ end
+
+
%% ===== OPTIONS: EDIT PROPERTIES CALLBACK =====
function EditProperties_Callback(iProcess, optName)
% Get current value: {@panel, sOptions}
@@ -2080,6 +2283,20 @@ function ScoutSelection_Callback(iProcess, optName, AtlasList, jCombo, jList, jC
end
+ %% ===== OPTIONS: SELECT ITEM CALLBACK =====
+ function ItemSelection_Callback(iProcess, optName, jList)
+ listModel = jList.getModel();
+ iSels = jList.getSelectedIndices();
+ elems = {};
+
+ % Update saved selected list
+ for iSel = 1:length(iSels)
+ elems{end + 1} = listModel.elementAt(iSels(iSel));
+ end
+ SetOptionValue(iProcess, optName, elems);
+ end
+
+
%% ===== OPTIONS: GET EVENT LIST =====
function EventList = GetEventList(varargin)
excludeSpikes = 0;
@@ -2119,7 +2336,10 @@ function SetOptionValue(iProcess, optName, value)
UpdateProcessesList();
% Save option value for future uses
optType = GlobalData.Processes.Current(iProcess).options.(optName).Type;
- if ismember(optType, {'value', 'range', 'freqrange', 'freqrange_static', 'checkbox', 'radio', 'radio_line', 'radio_label', 'radio_linelabel', 'combobox', 'combobox_label', 'text', 'textarea', 'channelname', 'subjectname', 'atlas', 'groupbands', 'montage', 'freqsel', 'scout', 'scout_confirm'}) ...
+ if ismember(optType, {'value', 'range', 'freqrange', 'freqrange_static', 'checkbox', ...
+ 'radio', 'radio_line', 'radio_label', 'radio_linelabel', 'combobox', 'combobox_label', ...
+ 'text', 'textarea', 'channelname', 'subjectname', 'atlas', 'groupbands', 'montage', ...
+ 'freqsel', 'scout', 'scout_confirm', 'list_vertical', 'list_horizontal'}) ...
|| (strcmpi(optType, 'filename') && (length(value)>=7) && strcmpi(value{7},'dirs') && strcmpi(value{3},'save'))
% Get processing options
ProcessOptions = bst_get('ProcessOptions');
@@ -2134,7 +2354,10 @@ function SetOptionValue(iProcess, optName, value)
opt = GlobalData.Processes.Current(iProcess).options.(optName);
if strcmp(optType, 'checkbox') && ~isempty(opt.Controller)
ToggleClass(opt.Controller, value);
- elseif ismember(optType, {'radio_label', 'radio_linelabel'}) && ~isempty(opt.Controller) && isstruct(opt.Controller)
+ elseif ismember(optType, {'radio_label', 'radio_linelabel', 'combobox_label'}) && ~isempty(opt.Controller) && isstruct(opt.Controller)
+ if strcmpi(optType, 'combobox_label')
+ value = value{1};
+ end
for cl = fieldnames(opt.Controller)'
% Ignore a disabled class that is associated with 2 options, one selected and one not selected
if ~strcmp(cl{1}, value) && isfield(opt.Controller, value) && isequal(opt.Controller.(cl{1}), opt.Controller.(value))
@@ -2634,9 +2857,13 @@ function ParseProcessFolder(isForced) %#ok
bstFunc = union(usrFunc, bstFunc);
end
- % Get processes from installed plugins ($HOME/.brainstorm/plugins/*)
+ % Get processes from installed (a supported) plugins ($HOME/.brainstorm/plugins/*)
plugFunc = {};
- PlugAll = bst_plugin('GetInstalled');
+ plugList = [];
+ PlugSupported = bst_plugin('GetSupported');
+ PlugInstalled = bst_plugin('GetInstalled');
+ [~, iPlug] = intersect({PlugInstalled.Name}, {PlugSupported.Name});
+ PlugAll = PlugInstalled(iPlug);
for iPlug = 1:length(PlugAll)
if ~isempty(PlugAll(iPlug).Processes)
% Keep only the processes with function names that are not already defined in Brainstorm
@@ -2656,7 +2883,9 @@ function ParseProcessFolder(isForced) %#ok
end
% Add plugin processes to list of processes
if ~isempty(plugFunc)
- bstFunc = union(plugFunc, bstFunc);
+ iFunc = cellfun(@(x)exist(x,'file') > 0 , plugFunc);
+ plugList = cellfun(@dir, plugFunc(iFunc));
+ bstFunc = union(plugFunc, bstFunc);
end
% ===== CHECK FOR MODIFICATIONS =====
@@ -2668,8 +2897,8 @@ function ParseProcessFolder(isForced) %#ok
for i = 1:length(usrList)
sig = [sig, usrList(i).name, usrList(i).date, num2str(usrList(i).bytes)];
end
- for i = 1:length(plugFunc)
- sig = [sig, plugFunc{i}];
+ for i = 1:length(plugList)
+ sig = [sig, plugList(i).name, plugList(i).date, num2str(plugList(i).bytes)];
end
% If signature is same as previously: do not reload all the files
if ~isForced
@@ -2720,7 +2949,16 @@ function ParseProcessFolder(isForced) %#ok
try
desc = Function('GetDescription');
catch
- disp(['BST> Invalid plug-in function: "' bstFunc{iFile} '"']);
+ if ismember(bstFunc{iFile}, usrFunc)
+ processType = 'User';
+ elseif ismember(bstFunc{iFile}, {bstList.name})
+ processType = 'Brainstorm';
+ elseif ismember(bstFunc{iFile}, plugFunc)
+ processType = 'Plug-in';
+ else
+ processType = char(8); % backspace
+ end
+ disp(['BST> Invalid ' processType ' function: "' bstFunc{iFile} '"']);
continue;
end
% Copy fields to returned structure
@@ -2850,7 +3088,7 @@ function ParseProcessFolder(isForced) %#ok
% Radio button: check the index of the selection
if ismember(option.Type, {'radio','radio_line'}) && (savedOpt > length(option.Comment))
% Error: ignoring previous option
- elseif strcmpi(option.Type, 'radio_label') && ~ismember(savedOpt, option.Comment(2,:))
+ elseif strcmpi(option.Type, 'radio_label') && ~isnumeric(savedOpt) && ~ismember(savedOpt, option.Comment(2,:))
% Error: ignoring previous option
elseif strcmpi(option.Type, 'radio_linelabel') && ~ismember(savedOpt, option.Comment(2,1:end-1))
% Error: ignoring previous option
diff --git a/toolbox/script/README.md b/toolbox/script/README.md
new file mode 100644
index 000000000..506c6fa05
--- /dev/null
+++ b/toolbox/script/README.md
@@ -0,0 +1,107 @@
+# Brainstorm scripts
+This directory contains the scripts to replicate most [Brainstorm tutorials](https://neuroimage.usc.edu/brainstorm/Tutorials) and to test different parts of Brainstorm.
+Tutorials can be executed individually with the respective `tutorial_TUTORIALNAME.m` script, or by calling the script `test-tutorial.m`.
+These scripts can be run on Brainstorm releases (**source** and **binary**).
+
+## Tutorial scripts
+
+| Tutorial name | Info | Report | Locally source | Locally binary | :octocat: runner | :octocat: exec time |
+|---------------------------|-------|--------|-------------------|-------------------|---------------------|-------------------------|
+| tutorial_introduction | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/AllIntroduction) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialIntroduction.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 70 min |
+| tutorial_connectivity | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/Connectivity) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialConnectivity.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 05 min |
+| tutorial_coherence | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/CorticomuscularCoherence) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialCMC.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 100 min |
+| tutorial_ephys | [Link](https://neuroimage.usc.edu/brainstorm/e-phys/Introduction) | [Report](https://neuroimage.usc.edu/bst/examples/report_Tutorial_e-Phys.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 25 min |
+| tutorial_dba | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/DeepAtlas) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialDba.html) | 🐧🪟🍎 | 🐧🪟🍎 | N/A | N/A |
+| tutorial_epilepsy | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/Epilepsy) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialEpilepsy.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 15 min |
+| tutorial_epileptogenicity | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/Epileptogenicity) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialEpimap.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 15 min |
+| tutorial_fem_charm | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/FemMedianNerveCharm) | [Report](https://neuroimage.usc.edu/brainstorm/Tutorials/FemMedianNerveCharm) | 🐧🪟🍎 | 🐧🪟🍎 | N/A | N/A |
+| tutorial_fem_tensors | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/FemTensors) | [Report](https://neuroimage.usc.edu/brainstorm/Tutorials/FemTensors) | 🐧🪟🍎 | 🐧🪟🍎 | N/A | N/A |
+| tutorial_frontiers2018 | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/VisualSingle) | [Report](https://neuroimage.usc.edu/brainstorm/Tutorials/VisualSingle) | 🐧🪟🍎 | 🐧🪟🍎 | N/A | N/A |
+| tutorial_visual | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/VisualSingle) | [Report](https://neuroimage.usc.edu/brainstorm/Tutorials/VisualSingle) | 🐧🪟🍎 | 🐧🪟🍎 | N/A | N/A |
+| tutorial_hcp | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/HCP-MEG) | [Report](https://neuroimage.usc.edu/brainstorm/Tutorials/HCP-MEG) | 🐧🪟🍎 | 🐧🪟🍎 | N/A | N/A |
+| tutorial_neuromag | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/TutMindNeuromag) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialNeuromag.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 20 min |
+| tutorial_omega | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/RestingOmega) | [Report](https://neuroimage.usc.edu/brainstorm/Tutorials/RestingOmega) | 🐧🪟🍎 | 🐧🪟🍎 | N/A | N/A |
+| tutorial_phantom_ctf | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/PhantomCtf) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialPhantom.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 20 min |
+| tutorial_phantom_elekta | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/PhantomElekta) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialPhantomElekta.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 10 min |
+| tutorial_practicalmeeg | [Link](https://neuroimage.usc.edu/brainstorm/WorkshopParis2019) | [Report](https://neuroimage.usc.edu/bst/examples/report_PracticalMEEG.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 30 min |
+| tutorial_raw | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/MedianNerveCtf) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialRaw.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 10 min |
+| tutorial_resting | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/Resting) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialResting.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 85 min |
+| tutorial_simulations | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/Simulations) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialSimulation.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 40 min |
+| tutorial_yokogawa | [Link](https://neuroimage.usc.edu/brainstorm/Tutorials/Yokogawa) | [Report](https://neuroimage.usc.edu/bst/examples/report_TutorialYokogawa.html) | 🐧🪟🍎 | 🐧🪟🍎 | 🐧🪟🍎 | 50 min |
+
+\* `N\A` indicates that this tutorial is not run on GitHub runners, [see below](#1-on-github-runners)
+
+# Function `test_tutorial.m`
+This function allows to run one or more tutorial scripts. This function handles fetching the required data and sending reports by email.
+Source code can be found in:
+
+The `test_tutorial.m` can be run:
+1. On GitHub-hosted runners (only for source distribution)
+2. Locally (source and compiled distributions)
+
+## 1. On GitHub-hosted runners
+The function `test_tutorial.m` can be run on [GitHub-hosted runners](https://docs.github.com/en/actions/using-github-hosted-runners) for a specific **tutorial script** by using the **Test tutorial (source)** (`run_tutorial.yaml`) GitHub workflow in the `master` branch of the `brainstorm-tools/brainstorm3`. This workflow makes use of the [Matlab GitHub actions](https://github.com/matlab-actions) to setup a Matlab environment and run code. The workflow starts 3 runners (Linux, Windows and macOS) then, for each runner Matlab is installed, and Brainstorm is started in server mode to run `test_tutorial.m` with the **tutorial script** is indicated in the workflow **Tutorial name** droplist. The three runners run simultaneously.
+
+:bulb: Some tutorial scripts are not available on `Test tutorial (source)` GitHub workflow because these tutorial scripts:
+* are not supported on the server mode (`tutorial_dba`), or
+* require large datasets (`tutorial_frontiers2018`, `tutorial_visual` , `tutorial_hcp`, `tutorial_omega`), or
+* require additional software such as SimNIBS or BrainSuite (`tutorial_fem_charm`, `tutorial_fem_tensors`)
+As such these are not shown on the `Test tutorial (source)` GitHub action, and indicated in the [Table above](#tutorial-scripts), with the legend `N/A`.
+
+## 2. Locally
+It is also possible to run the tutorial scripts locally using `test_tutorial.m`, to tests the source and compiled Brainstorm distributions.
+When run locally, the `test_tutorial.m` scripts requires this parameters:
+
+**tutorialNames** : Tutorial or {Tutorials} to run
+**dataDir** : (optional) Directory wtih tutorial data files (default = 'pwd'/tmpdir)
+**reportDir** : (optional) Directory to save reports (default = reports are not saved)
+**bstUser** : (optional) BST user to receive email with report (default = no email)
+**bstPwd** : (optional) Password for BST user to download data if needed (default = empty)
+
+### Source distribution
+A local installation of Matlab and a local copy of Brainstorm source are required.
+It can be run directly from inside the **Matlab IDE** or from the OS **Terminal** making use of Brainstorm capability of executing scripts when in server mode.
+The following sections show the commands to run `test_tutorial.m`, assuming that the current directory is `brainstorm3/`
+
+#### Matlab IDE
+This execution is the same regardless of the OS
+```
+brainstorm ./toolbox/script/test_tutorial.m TUTORIALNAME DATADIR REPORTDIR BSTUSER BSTPWD
+brainstorm ./toolbox/script/test_tutorial.m {'TUTORIALNAME1','TUTORIALNAME2'} DATADIR REPORTDIR BSTUSER BSTPWD
+```
+
+#### Terminal
+The parameters given to Matlab differ a bit among OS.
+If an argument needs to be set to empty, it can be replaced with two single quotes ``''``
+
+**Linux and macOS**
+```
+matlab -nodisplay -r "brainstorm ./toolbox/script/test_tutorial.m TUTORIALNAME DATADIR REPORTDIR BSTUSER BSTPWD"
+matlab -nodisplay -r "brainstorm ./toolbox/script/test_tutorial.m {'TUTORIALNAME1','TUTORIALNAME2'} DATADIR REPORTDIR BSTUSER BSTPWD"
+```
+
+**Windows**
+```
+matlab.exe -batch "brainstorm .\toolbox\script\test_tutorial.m TUTORIALNAME DATADIR REPORTDIR BSTUSER BSTPWD"
+matlab.exe -batch "brainstorm .\toolbox\script\test_tutorial.m {'TUTORIALNAME1','TUTORIALNAME2'} DATADIR REPORTDIR BSTUSER BSTPWD"
+```
+
+### Compiled distribution
+You need a physical (or a virtual) machine with the OS to test, a copy of compiled Brainstorm and the installed [Matlab Runtime](https://www.mathworks.com/products/compiler/matlab-runtime.html) that matches the OS and Runtime required by the compiled Brainstorm. The following sections show the commands to run `test_tutorial.m`, assuming that the current directory is `brainstorm3/bin/R2023a`
+
+#### Terminal
+This execution differs a bit among OS.
+:bulb: Be careful if `BSTUSER` or `BSTPWD` contain special characters are [escaping](https://en.wikipedia.org/wiki/Escape_character) them will be needed
+
+**Linux and macOS**
+The parameter `MATLABROOT` corresponds to the Matlab Runtime full path, e.g `/usr/local/MATLAB/MATLAB_Runtime/R2023a` or `/Applications/MATLAB/MATLAB_Runtime/R2023a`
+```
+./brainstorm3.command MATLABROOT ../../toolbox/script/test_tutorial.m TUTORIALNAME DATADIR REPORTDIR BSTUSER BSTPWD
+./brainstorm3.command MATLABROOT ../../toolbox/script/test_tutorial.m "{'TUTORIALNAME1','TUTORIALNAME2'}" DATADIR REPORTDIR BSTUSER BSTPWD
+```
+
+**Windows**
+```
+./brainstorm3.bat ..\..\toolbox\script\test_tutorial.m TUTORIALNAME DATADIR REPORTDIR BSTUSER BSTPWD
+./brainstorm3.bat ..\..\toolbox\script\test_tutorial.m "{'TUTORIALNAME1','TUTORIALNAME2'}" DATADIR REPORTDIR BSTUSER BSTPWD
+```
diff --git a/toolbox/script/generate_phantom_ctf.m b/toolbox/script/generate_phantom_ctf.m
index fcf869bde..209a0ebff 100644
--- a/toolbox/script/generate_phantom_ctf.m
+++ b/toolbox/script/generate_phantom_ctf.m
@@ -113,7 +113,7 @@ function generate_phantom_ctf(SubjectName)
% ===== GENERATE HEAD SURFACE =====
% Create surface
-[HeadFile, iSurface] = tess_isohead(iSubject, 10000, 0, 0, 'Phantom head (mask)');
+[HeadFile, iSurface] = tess_isohead(iSubject, 10000, 0, 0, [], 'Phantom head (mask)');
diff --git a/toolbox/script/generate_phantom_elekta.m b/toolbox/script/generate_phantom_elekta.m
index 2272e26fa..3a1167084 100644
--- a/toolbox/script/generate_phantom_elekta.m
+++ b/toolbox/script/generate_phantom_elekta.m
@@ -140,7 +140,7 @@
% ===== GENERATE HEAD SURFACE =====
% Create surface
-[HeadFile, iSurface] = tess_isohead(iSubject, 10000, 0, 0, 'Phantom head (mask)');
+[HeadFile, iSurface] = tess_isohead(iSubject, 10000, 0, 0, [], 'Phantom head (mask)');
diff --git a/toolbox/script/get_tutorial_data.m b/toolbox/script/get_tutorial_data.m
new file mode 100644
index 000000000..87b3bbf2a
--- /dev/null
+++ b/toolbox/script/get_tutorial_data.m
@@ -0,0 +1,69 @@
+function dataFullFile = get_tutorial_data(dataDir, dataFile, bstUser, bstPwd)
+% GET_TUTORIAL_DATA check if the 'dataFile' needed for tutorial scripts is in 'dataDir'
+% otherwise, if BST user and password are provided, data is downloaded from server
+%
+% USAGE: get_tutorial_data(dataDir, dataFile, bstUser, bstPwd)
+%
+% INPUTS:
+% - dataDir : Directory to search (or save) dataFile
+% - dataFile : File to search (or download)
+% - bstUser : (opt) BST user (default = empty)
+% - bstPwd : (opt) Password for BST user (default = empty)
+%
+% @=============================================================================
+% 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: Raymundo Cassani, 2024
+
+
+%% ===== PARAMETERS =====
+if nargin < 2
+ error('At least two parameters are needed');
+end
+if nargin <= 3 || isempty(bstUser) || isempty(bstPwd)
+ bstUser = '';
+ bstPwd = '';
+end
+
+
+%% ===== GET FILE =====
+% Search or download dataFile for tutorial
+dataFullFile = bst_fullfile(dataDir, dataFile);
+% Try to download if file does not exist
+if ~exist(dataFullFile, 'file')
+ if ~isempty(bstUser) && ~isempty(bstPwd)
+ dwnUrl = sprintf('http://neuroimage.usc.edu/bst/download.php?file=%s&user=%s&mdp=%s', ...
+ urlencode(dataFile), urlencode(bstUser), urlencode(bstPwd));
+ dataFullFile = bst_fullfile(dataDir, dataFile);
+ errMsg = bst_websave(dataFullFile, dwnUrl);
+ % Return if error
+ if ~isempty(errMsg) || ~exist(dataFullFile, 'file')
+ dataFullFile = '';
+ return
+ end
+ else
+ dataFullFile = '';
+ return
+ end
+end
+% Check size, if less than 50 bytes, error with the downloaded file
+d = dir(dataFullFile);
+if d.bytes < 50
+ dataFullFile = '';
+ return
+end
diff --git a/toolbox/script/script_view_sources.m b/toolbox/script/script_view_sources.m
index 969f87f26..6841b18b3 100644
--- a/toolbox/script/script_view_sources.m
+++ b/toolbox/script/script_view_sources.m
@@ -1,5 +1,5 @@
function [hFig, iDS, iFig] = script_view_sources(ResultsFile, DisplayMode)
-% SCRIPT_VIEW_SOURCES: Display the sources in a brainstorm figure.
+% SCRIPT_VIEW_SOURCES: Display the sources or timefreq from sources in a brainstorm figure.
%
% USAGE: [hFig, iDS, iFig] = script_view_sources(ResultsFile, DisplayMode)
%
@@ -26,9 +26,20 @@
% =============================================================================@
%
% Author: Francois Tadel, 2009-2010
+% Raymundo Cassani, 2024
-% Find results file in database
-[sStudy, iStudy, iResult] = bst_get('ResultsFile', ResultsFile);
+% Find Study for input file in database
+[sStudy, ~, ~, fileType, sItem] = bst_get('AnyFile', ResultsFile);
+% Check file type
+if ~ismember(fileType, {'results', 'timefreq'})
+ error('Input file must contain sources, or be a timefreq file from sources.');
+end
+% TimeFreq must be from sources
+if strcmpi(fileType, 'timefreq')
+ if ~strcmpi(sItem.DataType, 'results')
+ error('Input file must be a timefreq file from sources.');
+ end
+end
% Find subject in database
sSubject = bst_get('Subject', sStudy.BrainStormSubject);
diff --git a/toolbox/script/test_tutorial.m b/toolbox/script/test_tutorial.m
new file mode 100644
index 000000000..6b28233b9
--- /dev/null
+++ b/toolbox/script/test_tutorial.m
@@ -0,0 +1,315 @@
+function test_tutorial(tutorialNames, dataDir, reportDir, bstUser, bstPwd)
+% TEST_TUTORIAL Test Brainstorm by running tutorial scripts
+% If Brainstorm is not running, it is started without GUI and with 'local' database
+%
+% USAGE: test_brainstorm(tutorialNames, dataDir, reportDir, bstUser, bstPwd)
+%
+% INPUTS:
+% - tutorialNames : Tutorial or {Tutorials} to run, usually scripts in "./toolbox/scripts"
+% - dataDir : (opt) Directory wtih tutorial data files (default = 'pwd'/tmpdir)
+% - reportDir : (opt) Directory to save reports (default = reports are not saved)
+% - bstUser : (opt) BST user to receive email with report (default = no email)
+% - bstPwd : (opt) Password for BST user to download data if needed (default = empty)
+%
+% For a given tutorial in 'tutorialNames', the script does:
+% 1. Find/get the tutorial data (download if bstUser and bstPwd are available)
+% 2. Run tutorial script
+% 3. Send report by email to bstUser
+%
+% @=============================================================================
+% 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: Raymundo Cassani, 2023-2024
+
+
+%% ===== PARAMETERS =====
+if nargin < 2 || isempty(dataDir)
+ dataDir = bst_fullfile(pwd, 'tmpdir');
+end
+if nargin < 3 || isempty(reportDir)
+ reportDir = '';
+end
+if nargin < 4 || isempty(bstUser)
+ bstUser = '';
+end
+if nargin < 5 || isempty(bstPwd)
+ bstPwd = '';
+end
+
+% All tutorials
+if ischar(tutorialNames)
+ if strcmpi(tutorialNames, 'all')
+ tutorialNames = {'tutorial_introduction', ...
+ 'tutorial_connectivity', ...
+ 'tutorial_coherence', ...
+ 'tutorial_ephys', ...
+ 'tutorial_dba', ...
+ 'tutorial_epilepsy', ...
+ 'tutorial_epileptogenicity', ...
+ 'tutorial_fem_charm', ...
+ 'tutorial_fem_tensors', ...
+ 'tutorial_frontiers2018', ...
+ 'tutorial_visual', ...
+ 'tutorial_hcp', ...
+ 'tutorial_neuromag', ...
+ 'tutorial_omega', ...
+ 'tutorial_phantom_ctf', ...
+ 'tutorial_phantom_elekta', ...
+ 'tutorial_practicalmeeg', ...
+ 'tutorial_raw', ...
+ 'tutorial_resting', ...
+ 'tutorial_simulations', ...
+ 'tutorial_yokogawa', ...
+ };
+ else
+ tutorialNames = {tutorialNames};
+ end
+end
+
+
+%% ===== START BRAINSTORM =====
+% Check that Brainstorm is in the Matlab path
+res = exist('brainstorm.m', 'file');
+if res ~=2
+ error('Could not find "brainstorm.m" in Matlab path.');
+end
+% Start Brainstorm without GUI and with local database
+stopBstAtEnd = 0;
+if ~brainstorm('status')
+ brainstorm nogui local
+ stopBstAtEnd = 1;
+end
+
+
+%% ===== DATA AND REPORT DIRS =====
+% Data directory
+if ~exist(dataDir, 'dir')
+ mkdir(dataDir);
+end
+% Report dir
+if ~isempty(reportDir) && ~exist(reportDir, 'dir')
+ mkdir(reportDir)
+end
+
+
+%% ===== RUN TUTORIALS, SAVE REPORTS AND SEND EMAIL =====
+for iTutorial = 1 : length(tutorialNames)
+ tutoriallName = tutorialNames{iTutorial};
+ % Clean report history
+ bst_report('ClearHistory', 0);
+ infoStr = 'Error preparing file for tutorial';
+ % === Run tutorial
+ switch tutoriallName
+ case 'tutorial_introduction'
+ dataFile = get_tutorial_data(dataDir, 'sample_introduction.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_introduction(dataDir);
+ end
+
+ case 'tutorial_connectivity'
+ tutorial_connectivity();
+
+ case 'tutorial_coherence'
+ dataFile = bst_fullfile(dataDir, 'SubjectCMC.zip');
+ if ~exist(dataFile, 'file')
+ bst_websave(dataFile, 'https://download.fieldtriptoolbox.org/tutorial/SubjectCMC.zip');
+ end
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, bst_fullfile(dataDir, 'SubjectCMC'));
+ tutorial_coherence(dataDir);
+ end
+
+ case 'tutorial_dba'
+ dataFile = get_tutorial_data(dataDir, 'TutorialDba.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ tutorial_dba(dataFile);
+ end
+
+ case 'tutorial_ephys'
+ dataFile = get_tutorial_data(dataDir, 'sample_ephys.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_ephys(dataDir);
+ end
+
+ case 'tutorial_epilepsy'
+ dataFile = get_tutorial_data(dataDir, 'sample_epilepsy.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_epilepsy(dataDir);
+ end
+
+ case 'tutorial_epileptogenicity'
+ dataFile = get_tutorial_data(dataDir, 'tutorial_epimap_bids.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_epileptogenicity(dataDir);
+ end
+
+ case 'tutorial_fem_charm'
+ infoStr = 'REQUIRES TO INSTALL SimNIBS';
+% tmpDwnFile = mget(bstFtp, '/pub/tutorials/sample_fem.zip', tmpDir);
+% bst_unzip(tmpDwnFile{:}, tmpDir);
+% tutorial_fem_charm(tmpDir);
+
+ case 'tutorial_fem_tensors'
+ infoStr = 'REQUIRES TO BrainSuite';
+% dataFile1 = bst_fullfile(dataDir, 'BrainSuiteTutorialSVReg.zip');
+% if ~exist(dataFile1, 'file')
+% bst_websave(dataFile, 'http://brainsuite.org/WebTutorialData/BrainSuiteTutorialSVReg_Sept16.zip');
+% end
+% dataFile2 = bst_fullfile(dataDir, 'DWI.zip');
+% if ~exist(dataFile2, 'file')
+% bst_websave(dataFile2, 'http://brainsuite.org/WebTutorialData/DWI_Feb15.zip');
+% end
+% if exist(dataFile1, 'file') && exist(dataFile2, 'file')
+% bst_unzip(dataFile1, dataDir);
+% bst_unzip(dataFile2, dataDir);
+% tutorial_fem_tensors(dataDir);
+% end
+
+ case 'tutorial_frontiers2018'
+ infoStr = 'REQUIRES TO DOWNLOAD ~100GB https://openneuro.org/datasets/ds000117';
+% tutorial_frontiers2018(tmpDir);
+
+ case 'tutorial_visual'
+ infoStr = 'REQUIRES TO DOWNLOAD ~100GB https://openneuro.org/datasets/ds000117';
+% tutorial_visual(tmpDir);
+
+ case 'tutorial_hcp'
+ infoStr = 'REQUIRES TO DOWNLOAD ~20GB HCP-MEG2 distribution: subject #175237';
+% tutorial_hcp(tmpDir);
+
+ case 'tutorial_neuromag'
+ dataFile = get_tutorial_data(dataDir, 'sample_neuromag.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_neuromag(dataDir);
+ end
+
+ case 'tutorial_omega'
+ infoStr = 'REQUIRES TO DOWNLOAD ~12GB https://openneuro.org/datasets/ds000247';
+ % tutorial_omega(tmpDir);
+
+ case 'tutorial_phantom_ctf'
+ dataFile = get_tutorial_data(dataDir, 'sample_phantom_ctf.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_phantom_ctf(dataDir);
+ end
+
+ case 'tutorial_phantom_elekta'
+ dataFile = get_tutorial_data(dataDir, 'sample_phantom_elekta.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_phantom_elekta(dataDir);
+ end
+
+ case 'tutorial_practicalmeeg'
+ dataFile = get_tutorial_data(dataDir, 'tutorial_practicalmeeg.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_practicalmeeg(bst_fullfile(dataDir, 'tutorial_practicalmeeg'));
+ end
+
+ case 'tutorial_raw'
+ dataFile = get_tutorial_data(dataDir, 'sample_raw.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_raw(dataDir);
+ end
+
+ case 'tutorial_resting'
+ dataFile = get_tutorial_data(dataDir, 'sample_resting.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_resting(dataDir);
+ end
+
+ case 'tutorial_simulations'
+ tutorial_simulations();
+
+ case 'tutorial_yokogawa'
+ dataFile = get_tutorial_data(dataDir, 'sample_yokogawa.zip', bstUser, bstPwd);
+ if exist(dataFile, 'file')
+ bst_unzip(dataFile, dataDir);
+ tutorial_yokogawa(dataDir);
+ end
+ end
+
+ % Get report (available if tutorial was run)
+ [~, ReportFile] = bst_report('GetReport', 'last');
+ % Was the tutorial run?
+ wasRun = ~isempty(ReportFile);
+ % Generate not-executed report
+ if ~wasRun
+ tmp = struct('Comment', tutoriallName); % Dummy struct to give name to report
+ bst_report('Reset');
+ bst_report('Add', 'start', [], [], tmp);
+ bst_report('Info', '', '', sprintf('"%s" was not executed', tutoriallName));
+ bst_report('Info', '', '', infoStr);
+ ReportFile = bst_report('Save');
+ end
+
+ % === Save report file
+ if ~isempty(reportDir) && ~isempty(ReportFile)
+ [~, baseName] = bst_fileparts(ReportFile);
+ bst_report('Export', ReportFile, bst_fullfile(reportDir, [baseName, '.html']));
+ end
+
+ % === Report email
+ if ~isempty(bstUser)
+ % Tutorial info
+ tutorialInfo = sprintf('Tutorial: "%s"', tutoriallName);
+ % Hostname and OS
+ [~, hostName] = system('hostname');
+ hostInfo = sprintf('Host: "%s" with %s', strtrim(hostName), bst_get('OsName'));
+ % Matlab info
+ matlabInfo = sprintf('Matlab: %s', bst_get('MatlabReleaseName'));
+ % Brainstorm info
+ bstVersion = bst_get('Version');
+ bstVariant = 'source';
+ if bst_iscompiled()
+ bstVariant = 'standalone';
+ end
+ bstInfo = sprintf('BST: %s (%s)', bstVersion.Version, bstVariant);
+ % Status
+ if wasRun
+ statusInfo = '[Complete]';
+ else
+ statusInfo = '[Not exec]';
+ end
+ % Subject string
+ strSubject = [statusInfo, ' ' tutorialInfo, ', ' hostInfo, ', ' matlabInfo, ', ', bstInfo];
+
+ % Process: Send report by email
+ bst_process('CallProcess', 'process_report_email', [], [], ...
+ 'username', bstUser, ...
+ 'cc', '', ...
+ 'subject', strSubject, ...
+ 'reportfile', ReportFile, ...
+ 'full', 1);
+ end
+end
+
+% Stop Brainstorm
+if stopBstAtEnd
+ brainstorm stop
+end
+end
diff --git a/toolbox/script/tutorial_BEst.m b/toolbox/script/tutorial_BEst.m
new file mode 100644
index 000000000..74b927255
--- /dev/null
+++ b/toolbox/script/tutorial_BEst.m
@@ -0,0 +1,209 @@
+function tutorial_BEst(tutorial_dir, reports_dir)
+% TUTORIAL_BEST: Script that runs all the Brain Entropy in space and time introduction tutorials.
+%
+% CORRESPONDING ONLINE TUTORIAL:
+% https://neuroimage.usc.edu/brainstorm/Tutorials/TutBEst
+%
+% INPUTS:
+% - tutorial_dir : Directory where the sample_introduction.zip file has been unzipped
+% - reports_dir : Directory where to save the execution report (instead of displaying it)
+
+% @=============================================================================
+% 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.
+% =============================================================================@
+%
+% Author: Edouard Delaire, 2024
+% Raymundo Cassani, 2024
+
+
+% ===== FILES TO IMPORT =====
+% Output folder for reports
+if (nargin < 2) || isempty(reports_dir) || ~isdir(reports_dir)
+ reports_dir = [];
+end
+% Folder in which the Introduction tutorial dataset is unzipped (if needed)
+if (nargin == 0) || isempty(tutorial_dir) || ~file_exist(tutorial_dir)
+ tutorial_dir = [];
+end
+
+% Re-inialize random number generator
+if (bst_get('MatlabVersion') >= 712)
+ rng('default');
+end
+
+
+%% ===== VERIFY REQUIRED PROTOCOL =====
+ProtocolName = 'TutorialIntroduction';
+SubjectName = 'Subject01';
+
+iProtocolIntroduction = bst_get('Protocol', ProtocolName);
+if isempty(iProtocolIntroduction)
+ % Produce the Introduction protocol
+ tutorial_introduction(tutorial_dir, reports_dir)
+else
+ % Select input protocol
+ gui_brainstorm('SetCurrentProtocol', iProtocolIntroduction);
+end
+
+
+%% ===== REQUIRED PLUGIN =====
+% Install and Load Brain Entropy plugin
+[isInstalled, errMsg] = bst_plugin('Install', 'brainentropy');
+if ~isInstalled
+ error(errMsg);
+end
+
+
+%% ===== FIND FILES =====
+% Process: Select recordings in: Subject01/S01_AEF_20131218_01_600Hz_notch
+sFiles01 = bst_process('CallProcess', 'process_select_files_data', [], [], ...
+ 'subjectname', SubjectName, ...
+ 'condition', 'S01_AEF_20131218_01_600Hz_notch', ...
+ 'includebad', 0);
+% Process: Select file comments with tag: deviant
+sFilesAvgDeviant01 = bst_process('CallProcess', 'process_select_tag', sFiles01, [], ...
+ 'tag', 'Avg: deviant', ...
+ 'search', 2, ... % Search the file comments
+ 'select', 1); % Select only the files with the tag
+
+
+%% ===== HEAD MODEL =====
+disp([10 'BST> Head model' 10]);
+
+% Process: Generate BEM surfaces
+bst_process('CallProcess', 'process_generate_bem', [], [], ...
+ 'subjectname', SubjectName, ...
+ 'nscalp', 1922, ...
+ 'nouter', 1922, ...
+ 'ninner', 1922, ...
+ 'thickness', 4, ...
+ 'method', 'brainstorm');
+% Process: Compute head model
+bst_process('CallProcess', 'process_headmodel', sFilesAvgDeviant01, [], ...
+ 'sourcespace', 1, ... % Cortex surface
+ 'meg', 4, ... % OpenMEEG BEM
+ 'openmeeg', struct(...
+ 'BemSelect', [0, 0, 1], ...
+ 'BemCond', [0.33, 0.0165, 0.33], ...
+ 'BemNames', {{'Scalp', 'Skull', 'Brain'}}, ...
+ 'BemFiles', {{}}, ...
+ 'isAdjoint', 1, ...
+ 'isAdaptative', 1, ...
+ 'isSplit', 0, ...
+ 'SplitLength', 4000));
+
+
+%% ===== SOURCE ESTIMATION =====
+% coherent Maximum Entropy on the Mean (cMEM)
+disp([10 'BST> Source estimation using cMEM' 10]);
+
+% Process: Compute sources: BEst
+mem_option = be_pipelineoptions(be_main, 'cMEM');
+mem_option.optional = struct_copy_fields(mem_option.optional, ...
+ struct(...
+ 'TimeSegment', [0.05, 0.15], ...
+ 'BaselineType', {{'within-data'}}, ...
+ 'Baseline', [], ...
+ 'BaselineHistory', {{'within'}}, ...
+ 'BaselineSegment', [-0.1, 0], ...
+ 'groupAnalysis', 0, ...
+ 'display', 0));
+sAvgSrcMEM = bst_process('CallProcess', 'process_inverse_mem', sFilesAvgDeviant01, [], ...
+ 'comment', 'MEM', ...
+ 'mem', struct('MEMpaneloptions', mem_option), ...
+ 'sensortypes', 'MEG');
+% Process: Snapshot: Sources (one time)
+bst_process('CallProcess', 'process_snapshot', sAvgSrcMEM, [], ...
+ 'target', 8, ... % Sources (one time)
+ 'modality', 1, ... % MEG (All)
+ 'orient', 1, ... % left
+ 'time', 83.3*1e-3, ...
+ 'threshold', 0, ...
+ 'Comment', 'Average Deviant (cMEM)');
+
+
+% wavelet Maximum Entropy on the Mean (wMEM)
+disp([10 'BST> Source estimation using WMEM' 10]);
+
+% Process: Compute sources: BEst
+wMEM_options = be_pipelineoptions(be_main, 'wMEM');
+wMEM_options.optional = struct_copy_fields(wMEM_options.optional, ...
+ struct(...
+ 'TimeSegment', [0.05, 0.15], ...
+ 'BaselineType', {{'within-data'}}, ...
+ 'Baseline', [], ...
+ 'BaselineHistory', {{'within'}}, ...
+ 'BaselineSegment', [-0.1, 0], ...
+ 'groupAnalysis', 0, ...
+ 'display', 0));
+
+% 1. Localizing only scale 4:
+wMEM_options.wavelet.selected_scales = [4];
+sAvgSrwMEM_scale4 = bst_process('CallProcess', 'process_inverse_mem', sFilesAvgDeviant01, [], ...
+ 'comment', 'MEM', ...
+ 'mem', struct( 'MEMpaneloptions', wMEM_options), ...
+ 'sensortypes', 'MEG');
+% Process: Snapshot: Sources (one time)
+bst_process('CallProcess', 'process_snapshot', sAvgSrwMEM_scale4, [], ...
+ 'target', 8, ... % Sources (one time)
+ 'modality', 1, ... % MEG (All)
+ 'orient', 1, ... % left
+ 'time', 83.3*1e-3, ...
+ 'threshold', 0, ...
+ 'Comment', 'Average Deviant (wMEM - scale 4)');
+
+% 2. Localizing only scale 5:
+wMEM_options.wavelet.selected_scales = [5];
+sAvgSrwMEM_scale5 = bst_process('CallProcess', 'process_inverse_mem', sFilesAvgDeviant01, [], ...
+ 'comment', 'MEM', ...
+ 'mem', struct( 'MEMpaneloptions', wMEM_options), ...
+ 'sensortypes', 'MEG');
+% Process: Snapshot: Sources (one time)
+bst_process('CallProcess', 'process_snapshot', sAvgSrwMEM_scale5, [], ...
+ 'target', 8, ... % Sources (one time)
+ 'modality', 1, ... % MEG (All)
+ 'orient', 1, ... % left
+ 'time', 83.3*1e-3, ...
+ 'threshold', 0, ...
+ 'Comment', 'Average Deviant (wMEM - scale 5 )');
+
+% 3. Localizing all scales:
+wMEM_options.wavelet.selected_scales = [1,2,3,4,5];
+sAvgSrwMEM_scaleAll = bst_process('CallProcess', 'process_inverse_mem', sFilesAvgDeviant01, [], ...
+ 'comment', 'MEM', ...
+ 'mem', struct( 'MEMpaneloptions', wMEM_options), ...
+ 'sensortypes', 'MEG');
+% Process: Snapshot: Sources (one time)
+bst_process('CallProcess', 'process_snapshot', sAvgSrwMEM_scaleAll, [], ...
+ 'target', 8, ... % Sources (one time)
+ 'modality', 1, ... % MEG (All)
+ 'orient', 1, ... % left
+ 'time', 83.3*1e-3, ...
+ 'threshold', 0, ...
+ 'Comment', 'Average Deviant (wMEM - all scale)');
+
+
+%% ===== SAVE REPORT =====
+% Save and display report
+ReportFile = bst_report('Save', []);
+if ~isempty(reports_dir) && ~isempty(ReportFile)
+ bst_report('Export', ReportFile, reports_dir);
+else
+ bst_report('Open', ReportFile);
+end
+
+disp([10 'BST> tutorial_BEst: Done.' 10]);
diff --git a/toolbox/script/tutorial_brain_fingerprint.m b/toolbox/script/tutorial_brain_fingerprint.m
new file mode 100644
index 000000000..42c560d63
--- /dev/null
+++ b/toolbox/script/tutorial_brain_fingerprint.m
@@ -0,0 +1,339 @@
+function tutorial_brain_fingerprint(ProtocolNameOmega, reports_dir)
+% TUTORIAL_BRAIN_FINGERPRINT: Script that reproduces the results of the online tutorial "Brain-fingerprint".
+%
+% CORRESPONDING ONLINE TUTORIAL:
+% https://neuroimage.usc.edu/brainstorm/Tutorials/BrainFingerprint
+%
+% INPUTS:
+% - ProtocolNameOmega : Name of the protocol created with all the data imported (TutorialOmega)
+% - reports_dir : Directory where to save the execution report (instead of displaying it)
+%
+% @=============================================================================
+% 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.
+% =============================================================================@
+%
+% Author: Jason da Silva Castanheira, Raymundo Cassani, 2024
+
+
+%% ===== CHECK PROTOCOL =====
+% Start brainstorm without the GUI
+if ~brainstorm('status')
+ brainstorm nogui
+end
+% Output folder for reports
+if (nargin < 2) || isempty(reports_dir) || ~isdir(reports_dir)
+ reports_dir = [];
+end
+% You have to specify the folder in which the tutorial dataset is unzipped
+if (nargin < 1) || isempty(ProtocolNameOmega)
+ ProtocolNameOmega = 'TutorialOmega';
+end
+
+
+%% ===== BRAIN-FINGERPRINT PARAMETERS =====
+% Subjects
+SubjectNames = {'sub-0002', 'sub-0003', 'sub-0004', 'sub-0006', 'sub-0007'};
+nSubjects = length(SubjectNames);
+% Frequency range
+LowerFreq = 4; % Hz, inclusive
+UpperFreq = 150; % Hz, exclusive
+% Cortical parcellation (atlas)
+ % Note we downsample the cortical surface using an atlas for computation efficiency
+AtlasName = 'Destrieux';
+AtlasScouts = {'G_Ins_lg_and_S_cent_ins L', 'G_Ins_lg_and_S_cent_ins R', 'G_and_S_cingul-Ant L', 'G_and_S_cingul-Ant R', 'G_and_S_cingul-Mid-Ant L', 'G_and_S_cingul-Mid-Ant R', 'G_and_S_cingul-Mid-Post L', 'G_and_S_cingul-Mid-Post R', 'G_and_S_frontomargin L', 'G_and_S_frontomargin R', 'G_and_S_occipital_inf L', 'G_and_S_occipital_inf R', 'G_and_S_paracentral L', 'G_and_S_paracentral R', 'G_and_S_subcentral L', 'G_and_S_subcentral R', 'G_and_S_transv_frontopol L', 'G_and_S_transv_frontopol R', 'G_cingul-Post-dorsal L', 'G_cingul-Post-dorsal R', 'G_cingul-Post-ventral L', 'G_cingul-Post-ventral R', 'G_cuneus L', 'G_cuneus R', 'G_front_inf-Opercular L', 'G_front_inf-Opercular R', 'G_front_inf-Orbital L', 'G_front_inf-Orbital R', 'G_front_inf-Triangul L', 'G_front_inf-Triangul R', 'G_front_middle L', 'G_front_middle R', 'G_front_sup L', 'G_front_sup R', 'G_insular_short L', 'G_insular_short R', 'G_oc-temp_lat-fusifor L', 'G_oc-temp_lat-fusifor R', 'G_oc-temp_med-Lingual L', 'G_oc-temp_med-Lingual R', 'G_oc-temp_med-Parahip L', 'G_oc-temp_med-Parahip R', 'G_occipital_middle L', 'G_occipital_middle R', 'G_occipital_sup L', 'G_occipital_sup R', 'G_orbital L', 'G_orbital R', 'G_pariet_inf-Angular L', 'G_pariet_inf-Angular R', 'G_pariet_inf-Supramar L', 'G_pariet_inf-Supramar R', 'G_parietal_sup L', 'G_parietal_sup R', 'G_postcentral L', 'G_postcentral R', 'G_precentral L', 'G_precentral R', 'G_precuneus L', 'G_precuneus R', 'G_rectus L', 'G_rectus R', 'G_subcallosal L', 'G_subcallosal R', 'G_temp_sup-G_T_transv L', 'G_temp_sup-G_T_transv R', 'G_temp_sup-Lateral L', 'G_temp_sup-Lateral R', 'G_temp_sup-Plan_polar L', 'G_temp_sup-Plan_polar R', 'G_temp_sup-Plan_tempo L', 'G_temp_sup-Plan_tempo R', 'G_temporal_inf L', 'G_temporal_inf R', 'G_temporal_middle L', 'G_temporal_middle R', 'Lat_Fis-ant-Horizont L', 'Lat_Fis-ant-Horizont R', 'Lat_Fis-ant-Vertical L', 'Lat_Fis-ant-Vertical R', 'Lat_Fis-post L', 'Lat_Fis-post R', 'Pole_occipital L', 'Pole_occipital R', 'Pole_temporal L', 'Pole_temporal R', 'S_calcarine L', 'S_calcarine R', 'S_central L', 'S_central R', 'S_cingul-Marginalis L', 'S_cingul-Marginalis R', 'S_circular_insula_ant L', 'S_circular_insula_ant R', 'S_circular_insula_inf L', 'S_circular_insula_inf R', 'S_circular_insula_sup L', 'S_circular_insula_sup R', 'S_collat_transv_ant L', 'S_collat_transv_ant R', 'S_collat_transv_post L', 'S_collat_transv_post R', 'S_front_inf L', 'S_front_inf R', 'S_front_middle L', 'S_front_middle R', 'S_front_sup L', 'S_front_sup R', 'S_interm_prim-Jensen L', 'S_interm_prim-Jensen R', 'S_intrapariet_and_P_trans L', 'S_intrapariet_and_P_trans R', 'S_oc-temp_lat L', 'S_oc-temp_lat R', 'S_oc-temp_med_and_Lingual L', 'S_oc-temp_med_and_Lingual R', 'S_oc_middle_and_Lunatus L', 'S_oc_middle_and_Lunatus R', 'S_oc_sup_and_transversal L', 'S_oc_sup_and_transversal R', 'S_occipital_ant L', 'S_occipital_ant R', 'S_orbital-H_Shaped L', 'S_orbital-H_Shaped R', 'S_orbital_lateral L', 'S_orbital_lateral R', 'S_orbital_med-olfact L', 'S_orbital_med-olfact R', 'S_parieto_occipital L', 'S_parieto_occipital R', 'S_pericallosal L', 'S_pericallosal R', 'S_postcentral L', 'S_postcentral R', 'S_precentral-inf-part L', 'S_precentral-inf-part R', 'S_precentral-sup-part L', 'S_precentral-sup-part R', 'S_suborbital L', 'S_suborbital R', 'S_subparietal L', 'S_subparietal R', 'S_temporal_inf L', 'S_temporal_inf R', 'S_temporal_sup L', 'S_temporal_sup R', 'S_temporal_transverse L', 'S_temporal_transverse R'};
+% Bands for ICC cortex plot
+BandNames = {'theta', 'alpha', 'beta', 'gamma', 'highgamma'};
+BandLowerFreqs = [ 4, 8, 13, 30, 50]; % Hz, inclusive
+BandUpperFreqs = [ 8, 13, 30, 50, 150]; % Hz, exclusive
+
+%% ===== VERIFY REQUIRED PROTOCOL =====
+% Check Protocol that it exists
+iProtocolOmega = bst_get('Protocol', ProtocolNameOmega);
+if isempty(iProtocolOmega)
+ error(['Unknown protocol: ' ProtocolNameOmega]);
+end
+% Select input protocol
+gui_brainstorm('SetCurrentProtocol', iProtocolOmega);
+
+% Verify the Subjects
+ProtocolSubjects = bst_get('ProtocolSubjects');
+ProtocolSubjectNames = {ProtocolSubjects.Subject.Name};
+if ~all(ismember(SubjectNames, ProtocolSubjectNames))
+ error(['All requested subjects must be present in the ' ProtocolNameOmega ' protocol.']);
+end
+
+
+%% ===== FIND FILES =====
+bst_report('Start');
+
+% Get raw and source files for each Subject
+sRawDataFiles = [];
+sSourcesFiles = [];
+
+for iSubject = 1 : nSubjects
+ % Process: Select data files in: sub-000*/*
+ sFiles = bst_process('CallProcess', 'process_select_files_data', [], [], ...
+ 'subjectname', SubjectNames{iSubject}, ...
+ 'condition', '', ...
+ 'tag', '', ...
+ 'includebad', 0, ...
+ 'includeintra', 0, ...
+ 'includecommon', 0);
+ sRawDataFiles = [sRawDataFiles, sFiles];
+
+ % Process: Select results files in: sub-000*/*
+ sFiles = bst_process('CallProcess', 'process_select_files_results', [], [], ...
+ 'subjectname', SubjectNames{iSubject}, ...
+ 'condition', '', ...
+ 'tag', '', ...
+ 'includebad', 0, ...
+ 'includeintra', 0, ...
+ 'includecommon', 0);
+ sSourcesFiles = [sSourcesFiles, sFiles];
+end
+
+
+%% Compute PSD for all ROIs of an atlas
+% For each subject, their recordings are split in two parts (data segments)
+% these two data segments will be used to create PSDs
+% These two PSDs are the features which will define the brain-fingerprint
+
+nSegments = 2; % Training and Validation
+timeIni = zeros(nSubjects, nSegments);
+timeFin = zeros(nSubjects, nSegments);
+sPsdFiles = repmat(db_template('processfile'), nSubjects, nSegments);
+for iSubject = 1 : nSubjects
+ sData = load(file_fullpath(sRawDataFiles(iSubject).FileName), 'Time');
+ halfTime = diff(sData.Time) / 2;
+ % First segment
+ timeIni(iSubject, 1) = sData.Time(1) + 30;
+ timeFin(iSubject, 1) = halfTime - 30;
+ % Second segment
+ timeIni(iSubject, 2) = halfTime + 30;
+ timeFin(iSubject, 2) = sData.Time(2) - 30;
+ % Compute PSD
+ for iSegment = 1 : size(timeIni, 2)
+ % Process: Power spectrum density (Welch)
+ sPsdFiles(iSubject, iSegment) = bst_process('CallProcess', 'process_psd', sSourcesFiles(iSubject).FileName, [], ...
+ 'timewindow', [timeIni(iSubject, iSegment), timeFin(iSubject, iSegment)], ...
+ 'win_length', 2, ...
+ 'win_overlap', 50, ...
+ 'units', 'physical', ... % Physical: U2/Hz
+ 'clusters', {AtlasName, AtlasScouts}, ...
+ 'scoutfunc', 1, ... % Mean
+ 'win_std', 0, ...
+ 'edit', struct(...
+ 'Comment', 'Scouts,Power', ...
+ 'TimeBands', [], ...
+ 'Freqs', [], ...
+ 'ClusterFuncTime', 'before', ...
+ 'Measure', 'power', ...
+ 'Output', 'all', ...
+ 'SaveKernel', 0));
+ end
+end
+
+
+%% ===== VECTORIZE PSD DATA FOR FINGERPRINTING =====
+% Find size of requested PSD data
+sPsdMat = in_bst_timefreq(sPsdFiles(1,1).FileName, 0, 'RowNames', 'Freqs');
+Freqs = sPsdMat.Freqs;
+ixLowerFreq = find(Freqs >= LowerFreq, 1, 'first');
+ixUpperFreq = find(Freqs < UpperFreq, 1, 'last');
+Freqs = sPsdMat.Freqs(ixLowerFreq:ixUpperFreq);
+nFreqs = (ixUpperFreq - ixLowerFreq + 1);
+ScoutNames = sPsdMat.RowNames;
+nScouts = length(ScoutNames);
+
+% Subject vectors
+trainingVectors = zeros(iSubject, nScouts * nFreqs);
+validationVectors = zeros(iSubject, nScouts * nFreqs);
+
+% Vectorize Scout PSDs
+for iSubject = 1 : nSubjects
+ for iSegment = 1 : nSegments
+ sPsdMat = in_bst_timefreq(sPsdFiles(iSubject, iSegment).FileName, 0, 'TF');
+ if iSegment == 1
+ trainingVectors(iSubject,:) = reshape(squeeze(sPsdMat.TF(:, :, ixLowerFreq:ixUpperFreq)), 1, []);
+ elseif iSegment == 2
+ validationVectors(iSubject,:) = reshape(squeeze(sPsdMat.TF(:, :, ixLowerFreq:ixUpperFreq)), 1, []);
+ end
+ end
+end
+
+
+%% ==== SIMILARITY AND DIFFERENTIABILITY =====
+% Subject similarity matrix
+% It is generally symmetric although it does not necessarily have to be!
+SubjectCorrMatrix= corr(trainingVectors', validationVectors');
+% Differentiability
+diff_1= (diag(SubjectCorrMatrix)-mean(SubjectCorrMatrix,1) )/ std(SubjectCorrMatrix,0,1); % along columns
+diff_2= (diag(SubjectCorrMatrix)-mean(SubjectCorrMatrix,2)')/ std(SubjectCorrMatrix,0,2)'; % along rows
+% Differentiability across rows and columns are generally strongly correlated
+% Take the mean for a summary statistic per participant
+Differentiability = (diff_1 + diff_2)/2; % Mean differentiability derived from rows and columns
+
+% IntrClass Correlations (ICC)
+zTraining = (trainingVectors - mean(trainingVectors, 2)) ./ std(trainingVectors, 0, 2);
+zValidation= (validationVectors - mean(validationVectors, 2)) ./ std(validationVectors, 0, 2);
+df_b = nSubjects-1; % degrees of freedom for s2
+df_w = nSubjects*(nSegments-1); % degrees of freedom for r
+nFeatures = nScouts * nFreqs;
+icc = zeros(1, nFeatures);
+for iFeature = 1 : nFeatures
+ x = [zTraining(:, iFeature), zValidation(:, iFeature)];
+ x_w_mean = mean(x, 2);
+ x_g_mean = mean(x(:));
+ ss_t = sum((x - x_g_mean).^2, 'all');
+ ss_w = sum((x - x_w_mean).^2, 'all');
+ ss_b = ss_t - ss_w;
+ ms_b = ss_b / df_b;
+ ms_w = ss_w / df_w;
+ icc(iFeature) = (ms_b - ms_w) ./ (ms_b + ((nSegments-1).*ms_w));
+end
+iccFreqsScouts = reshape(icc, [nFreqs, nScouts]);
+% Compute Scout ICC for frequency bands
+nBands = length(BandNames);
+iccBands = zeros(nScouts, nBands);
+for iBand = 1 : nBands
+ ixLowerFreq = find(Freqs >= BandLowerFreqs(iBand), 1, 'first');
+ ixUpperFreq = find(Freqs < BandUpperFreqs(iBand), 1, 'last');
+ iccBands(: ,iBand) = mean(iccFreqsScouts(ixLowerFreq:ixUpperFreq, :), 1)';
+end
+
+%% ===== SAVE OUTCOME IN BRAINSTORM DATABASE =====
+% Retrieve 'Group_analysis' subject
+sNormSubj = bst_get('NormalizedSubject');
+% Study to save files
+[sOutputStudy, iOutputStudy] = bst_get('StudyWithCondition', bst_fullfile(sNormSubj.Name, bst_get('DirAnalysisIntra')));
+
+% Similarity matrix
+sSimilarityMat = db_template('timefreqmat');
+% Reshape: [nA x nB x nTime x nFreq] => [nA*nB x nTime x nFreq]
+sSimilarityMat.TF = reshape(SubjectCorrMatrix, [], 1, 1);
+sSimilarityMat.Comment = 'Similarity matrix';
+sSimilarityMat.DataType = 'matrix';
+sSimilarityMat.Time = [0, 1];
+sSimilarityMat.RefRowNames = cellfun(@(x) ['Train ', x], SubjectNames, 'UniformOutput', false);
+sSimilarityMat.RowNames = cellfun(@(x) ['Validation ', x], SubjectNames, 'UniformOutput', false);
+% Output filename
+SimilarityFile = bst_process('GetNewFilename', bst_fileparts(sOutputStudy.FileName), 'timefreq_connectn_corr');
+% Save file
+bst_save(SimilarityFile, sSimilarityMat, 'v6');
+% Add file to database structure
+db_add_data(iOutputStudy, SimilarityFile, sSimilarityMat);
+
+% Differentiability matrix
+sDiffMat = db_template('matrixmat');
+sDiffMat.Value = Differentiability;
+sDiffMat.Comment = 'Differentiability';
+sDiffMat.Time = [0, 1];
+sDiffMat.Description = SubjectNames;
+% Output filename
+DiffFile = bst_process('GetNewFilename', bst_fileparts(sOutputStudy.FileName), 'matrix');
+% Save file
+bst_save(DiffFile, sDiffMat, 'v6');
+% Add file to database structure
+db_add_data(iOutputStudy, DiffFile, sDiffMat);
+
+% Save band ICC values (scout level)
+for iBand = 1 : nBands
+ sIccBandMat = db_template('matrixmat');
+ sIccBandMat.Value = iccBands(:, iBand);
+ sIccBandMat.Comment = ['ICC_' BandNames{iBand}];
+ sIccBandMat.Time = [0, 1];
+ sIccBandMat.Description = ScoutNames;
+ % Output filename
+ IccBandFile = bst_process('GetNewFilename', bst_fileparts(sOutputStudy.FileName), 'matrix');
+ % Save file
+ bst_save(IccBandFile, sIccBandMat, 'v6');
+ % Add file to database structure
+ db_add_data(iOutputStudy, IccBandFile, sIccBandMat);
+end
+
+% Cortex surface display for band ICC values
+% Verify destination surface
+sSubjectGroup = bst_get('Subject', sOutputStudy.BrainStormSubject);
+sSurfFile = sSubjectGroup.Surface(sSubjectGroup.iCortex);
+sSurfMat = in_tess_bst(sSurfFile.FileName);
+iAtlas = find(ismember({sSurfMat.Atlas.Name}, AtlasName));
+if isempty(iAtlas)
+ error('Default surface in "%s" does not contain "%s" atlas', bst_get('NormalizedSubjectName'), AtlasName);
+end
+sAtlas = sSurfMat.Atlas(iAtlas);
+if ~all(ismember({sAtlas.Scouts.Label}, ScoutNames))
+ error('Atlas "%s" in "%s" does not contain the same scouts as ICC', AtlasName, bst_get('NormalizedSubjectName'));
+end
+% Create one full results file per band ICC values
+IccBandFiles = {};
+for iBand = 1 : nBands
+ sIccBandMat = db_template('resultsmat');
+ sIccBandMat.SurfaceFile = sSurfFile.FileName;
+ sIccBandMat.ImageGridAmp = nan(size(sSurfMat.Vertices, 1), 1);
+ sIccBandMat.Comment = ['ICC_' BandNames{iBand}];
+ sIccBandMat.Time = [0, 1];
+ % Assign ICC values to vertices in Scout
+ for ix = 1 : nScouts
+ iScout = find(ismember({sAtlas.Scouts.Label}, ScoutNames(ix)));
+ sIccBandMat.ImageGridAmp(sAtlas.Scouts(iScout).Vertices) = deal(iccBands(ix, iBand));
+ end
+ % Output filename
+ IccBandFile = bst_process('GetNewFilename', bst_fileparts(sOutputStudy.FileName), 'results');
+ IccBandFiles = [IccBandFiles, IccBandFile];
+ % Save file
+ bst_save(IccBandFile, sIccBandMat, 'v6');
+ % Add file to database structure
+ db_add_data(iOutputStudy, IccBandFile, sIccBandMat);
+end
+
+% Reload database
+db_reload_studies(iOutputStudy)
+
+
+%% ===== SNAPSHOTS =====
+% Process: Snapshot: Similarity matrix
+bst_process('CallProcess', 'process_snapshot', SimilarityFile, [], ...
+ 'type', 'connectimage', ... % Connectivity matrix
+ 'Comment', 'Similarity matrix');
+
+% Process: Snapshot: Recordings time series
+bst_process('CallProcess', 'process_snapshot', DiffFile, [], ...
+ 'type', 'data', ... % Recordings time series
+ 'time', 0, ...
+ 'Comment', 'Differentiability');
+
+for iBand = 1 : nBands
+ % Process: Snapshot: Sources (one time)
+ bst_process('CallProcess', 'process_snapshot', IccBandFiles{iBand}, [], ...
+ 'type', 'sources', ... % Sources (one time)
+ 'orient', 1, ... % left
+ 'time', 0, ...
+ 'contact_time', [0, 0.1], ...
+ 'contact_nimage', 12, ...
+ 'threshold', 0, ...
+ 'surfsmooth', 30, ...
+ 'mni', [0, 0, 0], ...
+ 'Comment', ['ICC_' BandNames{iBand}]);
+end
+
+% Save report
+ReportFile = bst_report('Save', []);
+if ~isempty(reports_dir) && ~isempty(ReportFile)
+ bst_report('Export', ReportFile, reports_dir);
+else
+ bst_report('Open', ReportFile);
+end
diff --git a/toolbox/script/tutorial_coherence.m b/toolbox/script/tutorial_coherence.m
index 1df191627..f52dcc9f3 100644
--- a/toolbox/script/tutorial_coherence.m
+++ b/toolbox/script/tutorial_coherence.m
@@ -40,10 +40,10 @@ function tutorial_coherence(tutorial_dir, reports_dir)
% Subject name
SubjectName = 'Subject01';
% Channel selection
-emg_channel = 'EMGlft'; % Name of EMG channel
-meg_sensor = 'MRC21'; % MEG sensor over the left motor-cortex (MRC21)
+emg_channel = 'EMGlft'; % Name of EMG channel
+meg_sensor = 'MRC21'; % MEG sensor over the left motor-cortex (MRC21)
% Coherence parameters
-cohmeasure = 'mscohere'; % Magnitude-squared Coherence|C|^2 = |Cxy|^2/(Cxx*Cyy)
+cohmeasure = 'mscohere'; % Magnitude-squared Coherence |C|^2 = |Cxy|^2/(Cxx*Cyy)
win_length = 0.5; % 500ms
overlap = 50; % 50%
maxfreq = 80; % 80Hz
@@ -229,6 +229,10 @@ function tutorial_coherence(tutorial_dir, reports_dir)
'freq', [], ...
'baseline', 'all', ...
'blsensortypes', 'MEG');
+% Process: Uniform epoch time
+sFilesEpochs = bst_process('CallProcess', 'process_stdtime', sFilesEpochs, [], ...
+ 'method', 'spline', ... % spline
+ 'overwrite', 1);
% Process: Snapshot: Recordings time series
bst_process('CallProcess', 'process_snapshot', sFilesEpochs(1), [], ...
'type', 'data', ... % Recordings time series
@@ -244,14 +248,14 @@ function tutorial_coherence(tutorial_dir, reports_dir)
%% ===== COHERENCE: EMG x MEG =====
-% Process: Magnitude-squared coherence: |C|^2 = |Cxy|^2/(Cxx*Cyy)
+% Process: Coherence NxN [2023]
sFileCoh1N = bst_process('CallProcess', 'process_cohere1', {sFilesEpochs.FileName}, [], ...
'timewindow', [], ...
'src_channel', emg_channel, ...
'dest_sensors', 'MEG', ...
'includebad', 0, ...
'removeevoked', 0, ...
- 'cohmeasure', cohmeasure, ... % Magnitude-squared coherence: |C|^2 = |Cxy|^2/(Cxx*Cyy)
+ 'cohmeasure', cohmeasure, ... % Magnitude-squared coherence
'tfmeasure', 'stft', ... % Fourier transform
'tfedit', struct(...
'Comment', 'Complex', ...
@@ -429,7 +433,7 @@ function tutorial_coherence(tutorial_dir, reports_dir)
'includebad', 0, ...
'includeintra', 0, ...
'includecommon', 0);
- % Process: Magnitude-squared coherence
+ % Process: Coherence NxN [2023]
sFileCoh1N = bst_process('CallProcess', 'process_cohere2', sFilesRecEmg, sFilesResMeg, ...
'timewindow', [], ...
'src_channel', emg_channel, ...
@@ -460,7 +464,7 @@ function tutorial_coherence(tutorial_dir, reports_dir)
'timeres', 'none', ... % None
'avgwinlength', 1, ...
'avgwinoverlap', 50, ...
- 'outputmode', 'avg'); % separately for each file
+ 'outputmode', 'avg'); % across combined files/epochs
% Process: Add tag
sFileCoh1N = bst_process('CallProcess', 'process_add_tag', sFileCoh1N, [], ...
'tag', sourceType, ...
@@ -499,51 +503,51 @@ function tutorial_coherence(tutorial_dir, reports_dir)
'includecommon', 0);
% Only performed for (surface)(Constrained)
sourceType = '(surface)(Constr)';
-sFilesResSrfUnc = bst_process('CallProcess', 'process_select_files_results', [], [], ...
+sFilesResSrfCon = bst_process('CallProcess', 'process_select_files_results', [], [], ...
'subjectname', SubjectName, ...
'condition', '', ...
'tag', sourceType, ...
'includebad', 0, ...
'includeintra', 0, ...
'includecommon', 0);
- % Process: Magnitude-squared coherence
- sFileCoh1N = bst_process('CallProcess', 'process_cohere2', sFilesRecEmg, sFilesResSrfUnc, ...
- 'timewindow', [], ...
- 'src_channel', emg_channel, ...
- 'dest_scouts', {'Schaefer_100_17net', {'Background+FreeSurfer_Defined_Medial_Wall L', 'Background+FreeSurfer_Defined_Medial_Wall R', 'ContA_IPS_1 L', 'ContA_IPS_1 R', 'ContA_PFCl_1 L', 'ContA_PFCl_1 R', 'ContA_PFCl_2 L', 'ContA_PFCl_2 R', 'ContB_IPL_1 R', 'ContB_PFCld_1 R', 'ContB_PFClv_1 L', 'ContB_PFClv_1 R', 'ContB_Temp_1 R', 'ContC_Cingp_1 L', 'ContC_Cingp_1 R', 'ContC_pCun_1 L', 'ContC_pCun_1 R', 'ContC_pCun_2 L', 'DefaultA_IPL_1 R', 'DefaultA_PFCd_1 L', 'DefaultA_PFCd_1 R', 'DefaultA_PFCm_1 L', 'DefaultA_PFCm_1 R', 'DefaultA_pCunPCC_1 L', 'DefaultA_pCunPCC_1 R', 'DefaultB_IPL_1 L', 'DefaultB_PFCd_1 L', 'DefaultB_PFCd_1 R', 'DefaultB_PFCl_1 L', 'DefaultB_PFCv_1 L', 'DefaultB_PFCv_1 R', 'DefaultB_PFCv_2 L', 'DefaultB_PFCv_2 R', 'DefaultB_Temp_1 L', 'DefaultB_Temp_2 L', 'DefaultC_PHC_1 L', 'DefaultC_PHC_1 R', 'DefaultC_Rsp_1 L', 'DefaultC_Rsp_1 R', 'DorsAttnA_ParOcc_1 L', 'DorsAttnA_ParOcc_1 R', 'DorsAttnA_SPL_1 L', 'DorsAttnA_SPL_1 R', 'DorsAttnA_TempOcc_1 L', 'DorsAttnA_TempOcc_1 R', 'DorsAttnB_FEF_1 L', 'DorsAttnB_FEF_1 R', 'DorsAttnB_PostC_1 L', 'DorsAttnB_PostC_1 R', 'DorsAttnB_PostC_2 L', 'DorsAttnB_PostC_2 R', 'DorsAttnB_PostC_3 L', 'LimbicA_TempPole_1 L', 'LimbicA_TempPole_1 R', 'LimbicA_TempPole_2 L', 'LimbicB_OFC_1 L', 'LimbicB_OFC_1 R', 'SalVentAttnA_FrMed_1 L', 'SalVentAttnA_FrMed_1 R', 'SalVentAttnA_Ins_1 L', 'SalVentAttnA_Ins_1 R', 'SalVentAttnA_Ins_2 L', 'SalVentAttnA_ParMed_1 L', 'SalVentAttnA_ParMed_1 R', 'SalVentAttnA_ParOper_1 L', 'SalVentAttnA_ParOper_1 R', 'SalVentAttnB_IPL_1 R', 'SalVentAttnB_PFCl_1 L', 'SalVentAttnB_PFCl_1 R', 'SalVentAttnB_PFCmp_1 L', 'SalVentAttnB_PFCmp_1 R', 'SomMotA_1 L', 'SomMotA_1 R', 'SomMotA_2 L', 'SomMotA_2 R', 'SomMotA_3 R', 'SomMotA_4 R', 'SomMotB_Aud_1 L', 'SomMotB_Aud_1 R', 'SomMotB_Cent_1 L', 'SomMotB_Cent_1 R', 'SomMotB_S2_1 L', 'SomMotB_S2_1 R', 'SomMotB_S2_2 L', 'SomMotB_S2_2 R', 'TempPar_1 L', 'TempPar_1 R', 'TempPar_2 R', 'TempPar_3 R', 'VisCent_ExStr_1 L', 'VisCent_ExStr_1 R', 'VisCent_ExStr_2 L', 'VisCent_ExStr_2 R', 'VisCent_ExStr_3 L', 'VisCent_ExStr_3 R', 'VisCent_Striate_1 L', 'VisPeri_ExStrInf_1 L', 'VisPeri_ExStrInf_1 R', 'VisPeri_ExStrSup_1 L', 'VisPeri_ExStrSup_1 R', 'VisPeri_StriCal_1 L', 'VisPeri_StriCal_1 R'}}, ...
- 'flatten', 0, ...
- 'scouttime', 'before', ... % before connectivity metric
- 'scoutfunc', 'mean', ... % Mean
- 'scoutfuncaft', 'mean', ... % Mean
- 'pcaedit', struct(...
- 'Method', 'pcaa', ...
- 'Baseline', [], ...
- 'DataTimeWindow', [], ...
- 'RemoveDcOffset', 'file'), ...
- 'removeevoked', 0, ...
- 'cohmeasure', cohmeasure, ... % Magnitude-squared coherence
- 'tfmeasure', 'stft', ... % Fourier transform
- 'tfedit', struct(...
- 'Comment', 'Complex', ...
- 'TimeBands', [], ...
- 'Freqs', [], ...
- 'StftWinLen', win_length, ...
- 'StftWinOvr', overlap, ...
- 'StftFrqMax', maxfreq, ...
- 'ClusterFuncTime', 'none', ...
- 'Measure', 'none', ...
- 'Output', 'all', ...
- 'SaveKernel', 0), ...
- 'timeres', 'none', ... % None
- 'avgwinlength', 1, ...
- 'avgwinoverlap', 50, ...
- 'outputmode', 'avg'); % separately for each file
+% Process: Coherence NxN [2023]
+sFileCoh1N = bst_process('CallProcess', 'process_cohere2', sFilesRecEmg, sFilesResSrfCon, ...
+ 'timewindow', [], ...
+ 'src_channel', emg_channel, ...
+ 'dest_scouts', {'Schaefer_100_17net', {'Background+FreeSurfer_Defined_Medial_Wall L', 'Background+FreeSurfer_Defined_Medial_Wall R', 'ContA_IPS_1 L', 'ContA_IPS_1 R', 'ContA_PFCl_1 L', 'ContA_PFCl_1 R', 'ContA_PFCl_2 L', 'ContA_PFCl_2 R', 'ContB_IPL_1 R', 'ContB_PFCld_1 R', 'ContB_PFClv_1 L', 'ContB_PFClv_1 R', 'ContB_Temp_1 R', 'ContC_Cingp_1 L', 'ContC_Cingp_1 R', 'ContC_pCun_1 L', 'ContC_pCun_1 R', 'ContC_pCun_2 L', 'DefaultA_IPL_1 R', 'DefaultA_PFCd_1 L', 'DefaultA_PFCd_1 R', 'DefaultA_PFCm_1 L', 'DefaultA_PFCm_1 R', 'DefaultA_pCunPCC_1 L', 'DefaultA_pCunPCC_1 R', 'DefaultB_IPL_1 L', 'DefaultB_PFCd_1 L', 'DefaultB_PFCd_1 R', 'DefaultB_PFCl_1 L', 'DefaultB_PFCv_1 L', 'DefaultB_PFCv_1 R', 'DefaultB_PFCv_2 L', 'DefaultB_PFCv_2 R', 'DefaultB_Temp_1 L', 'DefaultB_Temp_2 L', 'DefaultC_PHC_1 L', 'DefaultC_PHC_1 R', 'DefaultC_Rsp_1 L', 'DefaultC_Rsp_1 R', 'DorsAttnA_ParOcc_1 L', 'DorsAttnA_ParOcc_1 R', 'DorsAttnA_SPL_1 L', 'DorsAttnA_SPL_1 R', 'DorsAttnA_TempOcc_1 L', 'DorsAttnA_TempOcc_1 R', 'DorsAttnB_FEF_1 L', 'DorsAttnB_FEF_1 R', 'DorsAttnB_PostC_1 L', 'DorsAttnB_PostC_1 R', 'DorsAttnB_PostC_2 L', 'DorsAttnB_PostC_2 R', 'DorsAttnB_PostC_3 L', 'LimbicA_TempPole_1 L', 'LimbicA_TempPole_1 R', 'LimbicA_TempPole_2 L', 'LimbicB_OFC_1 L', 'LimbicB_OFC_1 R', 'SalVentAttnA_FrMed_1 L', 'SalVentAttnA_FrMed_1 R', 'SalVentAttnA_Ins_1 L', 'SalVentAttnA_Ins_1 R', 'SalVentAttnA_Ins_2 L', 'SalVentAttnA_ParMed_1 L', 'SalVentAttnA_ParMed_1 R', 'SalVentAttnA_ParOper_1 L', 'SalVentAttnA_ParOper_1 R', 'SalVentAttnB_IPL_1 R', 'SalVentAttnB_PFCl_1 L', 'SalVentAttnB_PFCl_1 R', 'SalVentAttnB_PFCmp_1 L', 'SalVentAttnB_PFCmp_1 R', 'SomMotA_1 L', 'SomMotA_1 R', 'SomMotA_2 L', 'SomMotA_2 R', 'SomMotA_3 R', 'SomMotA_4 R', 'SomMotB_Aud_1 L', 'SomMotB_Aud_1 R', 'SomMotB_Cent_1 L', 'SomMotB_Cent_1 R', 'SomMotB_S2_1 L', 'SomMotB_S2_1 R', 'SomMotB_S2_2 L', 'SomMotB_S2_2 R', 'TempPar_1 L', 'TempPar_1 R', 'TempPar_2 R', 'TempPar_3 R', 'VisCent_ExStr_1 L', 'VisCent_ExStr_1 R', 'VisCent_ExStr_2 L', 'VisCent_ExStr_2 R', 'VisCent_ExStr_3 L', 'VisCent_ExStr_3 R', 'VisCent_Striate_1 L', 'VisPeri_ExStrInf_1 L', 'VisPeri_ExStrInf_1 R', 'VisPeri_ExStrSup_1 L', 'VisPeri_ExStrSup_1 R', 'VisPeri_StriCal_1 L', 'VisPeri_StriCal_1 R'}}, ...
+ 'flatten', 0, ...
+ 'scouttime', 'before', ... % before connectivity metric
+ 'scoutfunc', 'mean', ... % Mean
+ 'scoutfuncaft', 'mean', ... % Mean
+ 'pcaedit', struct(...
+ 'Method', 'pcaa', ...
+ 'Baseline', [], ...
+ 'DataTimeWindow', [], ...
+ 'RemoveDcOffset', 'file'), ...
+ 'removeevoked', 0, ...
+ 'cohmeasure', cohmeasure, ... % Magnitude-squared coherence
+ 'tfmeasure', 'stft', ... % Fourier transform
+ 'tfedit', struct(...
+ 'Comment', 'Complex', ...
+ 'TimeBands', [], ...
+ 'Freqs', [], ...
+ 'StftWinLen', win_length, ...
+ 'StftWinOvr', overlap, ...
+ 'StftFrqMax', maxfreq, ...
+ 'ClusterFuncTime', 'none', ...
+ 'Measure', 'none', ...
+ 'Output', 'all', ...
+ 'SaveKernel', 0), ...
+ 'timeres', 'none', ... % None
+ 'avgwinlength', 1, ...
+ 'avgwinoverlap', 50, ...
+ 'outputmode', 'avg'); % across combined files/epochs
% Process: Add tag
sFileCoh1N = bst_process('CallProcess', 'process_add_tag', sFileCoh1N, [], ...
'tag', sourceType, ...
'output', 1); % Add to file name
% Highlight scout of interest
-bst_figures('SetSelectedRows', {'DorsAttnB_FEF_1 R', 'SomMotA_2 R', 'SomMotA_4 R'});
+bst_figures('SetSelectedRows', {'SomMotA_2 R', 'SomMotA_4 R'});
% Process: Snapshot: Frequency spectrum
bst_process('CallProcess', 'process_snapshot', sFileCoh1N, [], ...
'target', 10, ... % Frequency spectrum
@@ -556,6 +560,71 @@ function tutorial_coherence(tutorial_dir, reports_dir)
'Comment', ['MSC 14.65Hz,' sourceType, ' Before']);
+%% ===== COHERENCE: SCOUTS x SCOUTS (BEFORE) =====
+% Only performed for (surface)(Constrained)
+sourceType = '(surface)(Constr)';
+sFilesResSrfCon = bst_process('CallProcess', 'process_select_files_results', [], [], ...
+ 'subjectname', SubjectName, ...
+ 'condition', '', ...
+ 'tag', sourceType, ...
+ 'includebad', 0, ...
+ 'includeintra', 0, ...
+ 'includecommon', 0);
+% Process: Coherence NxN [2023]
+sFileCohNN = bst_process('CallProcess', 'process_cohere1n', sFilesResSrfCon, [], ...
+ 'timewindow', [], ...
+ 'scouts', {'Schaefer_100_17net', {'SomMotA_2 R', 'SomMotA_4 R', 'VisCent_ExStr_2 L'}}, ...
+ 'flatten', 0, ...
+ 'scouttime', 'before', ... % before
+ 'scoutfunc', 'mean', ... % Mean
+ 'scoutfuncaft', 'mean', ... % Mean
+ 'pcaedit', struct(...
+ 'Method', 'pcaa', ...
+ 'Baseline', [], ...
+ 'DataTimeWindow', [], ...
+ 'RemoveDcOffset', 'file'), ...
+ 'removeevoked', 0, ...
+ 'cohmeasure', 'mscohere', ... % Magnitude-squared coherence
+ 'tfmeasure', 'stft', ... % Fourier transform
+ 'tfedit', struct(...
+ 'Comment', 'Complex', ...
+ 'TimeBands', [], ...
+ 'Freqs', [], ...
+ 'StftWinLen', 0.5, ...
+ 'StftWinOvr', 50, ...
+ 'StftFrqMax', 80, ...
+ 'ClusterFuncTime', 'none', ...
+ 'Measure', 'none', ...
+ 'Output', 'all', ...
+ 'SaveKernel', 0), ...
+ 'timeres', 'none', ... % None
+ 'avgwinlength', 1, ...
+ 'avgwinoverlap', 50, ...
+ 'outputmode', 'avg'); % across combined files/epochs
+% Process: Add tag
+sFileCohNN = bst_process('CallProcess', 'process_add_tag', sFileCohNN, [], ...
+ 'tag', sourceType, ...
+ 'output', 1); % Add to file name
+% Highlight scout of interest
+bst_figures('SetSelectedRows', {'SomMotA_2 R', 'SomMotA_4 R'});
+% Process: Snapshot: Frequency spectrum
+bst_process('CallProcess', 'process_snapshot', sFileCohNN, [], ...
+ 'target', 10, ... % Frequency spectrum
+ 'freq', 14.65, ...
+ 'Comment', ['MSC ,' sourceType, ' Before']);
+% Process: Snapshot: Connectivity matrix
+bst_process('CallProcess', 'process_snapshot', sFileCohNN, [], ...
+ 'type', 'connectimage', ... % Connectivity matrix
+ 'freq', 14.65, ...
+ 'Comment', ['MSC 14.65Hz,' sourceType, ' Before']);
+% Process: Snapshot: Connectivity graph
+bst_process('CallProcess', 'process_snapshot', sFileCohNN, [], ...
+ 'type', 'connectgraph', ... % Connectivity graph
+ 'freq', 14.65, ...
+ 'threshold', 0, ...
+ 'Comment', ['MSC 14.65Hz,' sourceType, ' Before']);
+
+
%% ===== SAVE REPORT =====
% Save and display report
ReportFile = bst_report('Save', []);
diff --git a/toolbox/script/tutorial_connectivity.m b/toolbox/script/tutorial_connectivity.m
index 81eb09e85..d7abc4065 100644
--- a/toolbox/script/tutorial_connectivity.m
+++ b/toolbox/script/tutorial_connectivity.m
@@ -267,30 +267,34 @@ function tutorial_connectivity(reports_dir)
%% ===== PHASE LOCKING VALUE =====
-% Process: Phase locking value
-sFiles = bst_process('CallProcess', 'process_plv1n', sFileSim, [], ...
- 'timewindow', [], ...
- 'plvmethod', 'plv', ... % Phase locking value
- 'plvmeasure', 2, ... % Magnitude
- 'tfmeasure', 'hilbert', ... % Hilbert transform
- 'tfedit', struct(...
- 'Comment', 'Complex', ...
- 'TimeBands', [], ...
- 'Freqs', {{'delta', '2, 4', 'mean'; 'theta', '5, 7', 'mean'; 'alpha', '8, 12', 'mean'; 'beta', '15, 29', 'mean'; 'gamma1', '30, 59', 'mean'}}, ...
- 'ClusterFuncTime', 'none', ...
- 'Measure', 'none', ...
- 'Output', 'all', ...
- 'SaveKernel', 0), ...
- 'timeres', 'none', ... % None
- 'avgwinlength', 1, ...
- 'avgwinoverlap', 50, ...
- 'outputmode', 'input'); % separately for each file
-
-% Process: Snapshot: Frequency spectrum
-bst_process('CallProcess', 'process_snapshot', sFiles, [], ...
- 'type', 'spectrum', ... % Frequency spectrum
- 'Comment', 'Phase locking value NxN');
-
+plv_variants = {'plv', ... % Phase locking value
+ 'ciplv', ... % Lagged phase synchronization / Corrected imaginary PLV
+ 'wpli'}; % Weighted phase lag index
+for ix = 1 : length(plv_variants)
+ % Process: Phase locking value
+ sFiles = bst_process('CallProcess', 'process_plv1n', sFileSim, [], ...
+ 'timewindow', [], ...
+ 'plvmethod', plv_variants{ix}, ...
+ 'plvmeasure', 2, ... % Magnitude
+ 'tfmeasure', 'hilbert', ... % Hilbert transform
+ 'tfedit', struct(...
+ 'Comment', 'Complex', ...
+ 'TimeBands', [], ...
+ 'Freqs', {{'delta', '2, 4', 'mean'; 'theta', '5, 7', 'mean'; 'alpha', '8, 12', 'mean'; 'beta', '15, 29', 'mean'; 'gamma1', '30, 59', 'mean'}}, ...
+ 'ClusterFuncTime', 'none', ...
+ 'Measure', 'none', ...
+ 'Output', 'all', ...
+ 'SaveKernel', 0), ...
+ 'timeres', 'none', ... % None
+ 'avgwinlength', 1, ...
+ 'avgwinoverlap', 50, ...
+ 'outputmode', 'input'); % separately for each file
+
+ % Process: Snapshot: Frequency spectrum
+ bst_process('CallProcess', 'process_snapshot', sFiles, [], ...
+ 'type', 'spectrum', ... % Frequency spectrum
+ 'Comment', ['Phase locking value (' plv_variants{ix} ') NxN']);
+end
%% ===== PHASE TRANSFER ENTROPY =====
% Process: Phase Transfer Entropy NxN
diff --git a/toolbox/script/tutorial_dba.m b/toolbox/script/tutorial_dba.m
new file mode 100644
index 000000000..9921f3578
--- /dev/null
+++ b/toolbox/script/tutorial_dba.m
@@ -0,0 +1,294 @@
+function tutorial_dba(zip_file, reports_dir)
+% TUTORIAL_DBA: script that runs the Brainstorm deep brain activity (DBA) tutorial.
+% https://neuroimage.usc.edu/brainstorm/Tutorials/DeepAtlas
+%
+% INPUTS:
+% - zip_file : Full path to the TutorialDba.zip file
+% - reports_dir : Directory where to save the execution report (instead of displaying it)
+%
+% @=============================================================================
+% 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.
+% =============================================================================@
+%
+% Author: Raymundo Cassani, 2024
+
+% Output folder for reports
+if (nargin < 2) || isempty(reports_dir) || ~exist(reports_dir, 'dir')
+ reports_dir = [];
+end
+% You have to specify the folder in which the tutorial dataset is unzipped
+if (nargin == 0) || isempty(zip_file) || ~file_exist(zip_file)
+ error('The first argument must be the full path to the dataset zip file.');
+end
+
+%% ===== MIXED MODEL PARAMETERS =====
+% Cortex name
+cortexComment = 'cortex_15002V';
+% Surface with subcortical segmentation (ASEG)
+subCortexComment = 'aseg atlas';
+% Subcortical structures to keep from ASEG
+subCortexKeep = {'Amygdala L', 'Amygdala R', 'Hippocampus L', 'Hippocampus R', 'Thalamus L', 'Thalamus R'};
+% Mixed cortex name
+mixedComment = 'cortex_mixed' ;
+
+% Mixed model structures: Cortex + Subcortical structures
+% Table with structures, and their locations and orientations constraints
+% Location constraint : 'Surface', 'Volume', 'Deep brain'*, 'Exclude'
+% Orientation constraint: 'Constrained', 'Unconstrained', 'Loose'
+% * If 'Deep brain' is used as location constraint the location and orientation constraints
+% will be set automatically according to the scout name.
+%
+% 'ScoutName' , 'Location' , 'Orientation'
+mixedStructs = {'Amygdala L' , 'Deep brain', ''; ... % Deep brain --> Volume , Unconstrained
+ 'Amygdala R' , 'Deep brain', ''; ... % Deep brain --> Volume , Unconstrained
+ 'Hippocampus L', 'Deep brain', ''; ... % Deep brain --> Surface, Constrained
+ 'Hippocampus R', 'Deep brain', ''; ... % Deep brain --> Surface, Constrained
+ 'Thalamus L' , 'Deep brain', ''; ... % Deep brain --> Volume, Unconstrained
+ 'Thalamus R' , 'Deep brain', ''; ... % Deep brain --> Volume, Unconstrained
+ 'Cortex L' , 'Deep brain', ''; ... % Deep brain --> Surface, Constrained
+ 'Cortex R' , 'Deep brain', ''}; % Deep brain --> Surface, Constrained
+
+
+%% ===== LOAD PROTOCOL =====
+% Start brainstorm without the GUI
+if ~brainstorm('status')
+ brainstorm nogui
+end
+% Check Brainstorm mode
+if bst_get('GuiLevel') < 0
+ error('For the moment the tutorial "tutorial_dba" is not supported on Brainstorm server mode.');
+end
+ProtocolName = 'TutorialDba';
+[~, fBase] = bst_fileparts(zip_file);
+if ~strcmpi(fBase, ProtocolName)
+ error('Incorrect .zip file.');
+end
+% Delete existing protocol
+gui_brainstorm('DeleteProtocol', ProtocolName);
+% Import protocol from zip file
+import_protocol(zip_file);
+% Start a new report
+bst_report('Start');
+
+
+%% ===== SELECT DEEP STRUCTURES =====
+% Get Subject for Default Anatomy
+sSubject = bst_get('Subject', bst_get('DirDefaultSubject'));
+% Find subcortical atlas, and downsize it
+iAseg = find(strcmpi(subCortexComment, {sSubject.Surface.Comment}));
+newAsegFile = tess_downsize(sSubject.Surface(iAseg).FileName, 15000, 'reducepatch');
+% Create atlas with only selected structures
+panel_scout('SetCurrentSurface', newAsegFile);
+sScouts = panel_scout('GetScouts');
+[~, iScouts] = ismember(subCortexKeep, {sScouts.Label});
+panel_scout('SetSelectedScouts', iScouts);
+newAsegFile = panel_scout('NewSurface', 1);
+% Find cortex
+sSubject = bst_get('Subject', bst_get('DirDefaultSubject'));
+iCortex = find(strcmpi(cortexComment, {sSubject.Surface.Comment}));
+% Merge cortex with selected DBA structures
+mixedFile = tess_concatenate({sSubject.Surface(iCortex).FileName, newAsegFile}, mixedComment, 'Cortex');
+% Atlas with structures
+atlasName = 'Structures';
+% Display mixed cortex
+hFigMix = view_surface(mixedFile);
+[~, sSurf] = panel_scout('GetScouts');
+iAtlas = find(strcmpi(atlasName, {sSurf.Atlas.Name}));
+panel_scout('SetCurrentAtlas', iAtlas, 1);
+panel_surface('SelectHemispheres', 'struct');
+bst_report('Snapshot', hFigMix, mixedFile, 'Mix cortex: Cortex + Subcortical structures');
+pause(1);
+% Unload everything
+bst_memory('UnloadAll', 'Forced');
+
+
+%% ===== LOCATIONS AND ORIENTATIONS CONSTRAINTS =====
+% Select atlas with structures
+panel_scout('SetCurrentSurface', mixedFile);
+[~, sSurf] = panel_scout('GetScouts');
+iAtlas = find(strcmpi(atlasName, {sSurf.Atlas.Name}));
+panel_scout('SetCurrentAtlas', iAtlas, 1);
+% Create source model atlas
+panel_scout('CreateAtlasInverse');
+% Set modeling options
+sScouts = panel_scout('GetScouts');
+% Set location and orientation constraints
+for iScout = 1 : length(sScouts)
+ % Select this scout
+ iRow = find(ismember(sScouts(iScout).Label, mixedStructs(:,1)));
+ % Set location constraint
+ panel_scout('SetLocationConstraint', iScout, mixedStructs{iRow,2});
+ % Set orientation constraint
+ panel_scout('SetOrientationConstraint', iScout, mixedStructs{iRow,3});
+end
+% Unload everything
+bst_memory('UnloadAll', 'Forced');
+
+
+%% ===== SOURCE ESTIMATION =====
+% Find all recordings files for all Subjects, except 'Empty_Subject'
+% Process: Select data files in: */*/Trial (#1)
+sRecFiles = bst_process('CallProcess', 'process_select_files_data', [], [], ...
+ 'subjectname', 'All', ...
+ 'condition', '', ...
+ 'tag', '', ...
+ 'includebad', 0, ...
+ 'includeintra', 0, ...
+ 'includecommon', 0);
+iDelete = strcmpi('Empty_Subject', {sRecFiles.SubjectName});
+sRecFiles(iDelete) = [];
+
+% Process: Compute head model
+bst_process('CallProcess', 'process_headmodel', sRecFiles, [], ...
+ 'comment', '', ...
+ 'sourcespace', 3, ...
+ 'meg', 3); % Overlapping spheres
+
+% Display surface and volume grids
+sStudy = bst_get('AnyFile', sRecFiles(1).FileName);
+headmodelFile = sStudy.HeadModel.FileName;
+hFigSrfGrid = view_gridloc(file_fullpath(headmodelFile), 'S');
+hFigVolGrid = view_gridloc(file_fullpath(headmodelFile), 'V');
+figure_3d('SetStandardView', hFigSrfGrid, 'top');
+figure_3d('SetStandardView', hFigVolGrid, 'top');
+bst_report('Snapshot', hFigSrfGrid, headmodelFile, 'Mix head model, surface grid');
+bst_report('Snapshot', hFigVolGrid, headmodelFile, 'Mix head model, volume grid');
+pause(1);
+close([hFigSrfGrid, hFigVolGrid]);
+
+% Minimum norm options
+InverseOptions = struct(...
+ 'Comment', 'MN: MEG', ...
+ 'InverseMethod', 'minnorm', ...
+ 'InverseMeasure', 'amplitude', ...
+ 'SourceOrient', [], ...
+ 'Loose', 0.2, ...
+ 'UseDepth', 1, ...
+ 'WeightExp', 0.5, ...
+ 'WeightLimit', 10, ...
+ 'NoiseMethod', 'reg', ...
+ 'NoiseReg', 0.1, ...
+ 'SnrMethod', 'fixed', ...
+ 'SnrRms', 1e-6, ...
+ 'SnrFixed', 3, ...
+ 'ComputeKernel', 1, ...
+ 'DataTypes', {{'MEG'}});
+
+% Process: Compute sources [2018]
+sSrcFiles = bst_process('CallProcess', 'process_inverse_2018', sRecFiles, [], ...
+ 'output', 1, ... % Kernel only: one per file
+ 'inverse', InverseOptions);
+
+% Display sources
+hSrcFig = view_surface_data([], sSrcFiles(1).FileName);
+panel_time('SetCurrentTime', 4.897);
+bst_report('Snapshot', hSrcFig, sSrcFiles(1).FileName);
+panel_surface('SelectHemispheres', 'struct');
+bst_report('Snapshot', hSrcFig, sSrcFiles(1).FileName);
+pause(1);
+
+% Unload everything
+bst_memory('UnloadAll', 'Forced');
+
+
+%% ===== COMPUTE STATISTICS =====
+% Process: MEAN: [all], abs
+sSrcAvgFiles = bst_process('CallProcess', 'process_average_time', sSrcFiles, [], ...
+ 'timewindow', [], ...
+ 'avg_func', 'mean', ... % Arithmetic average: mean(x)
+ 'overwrite', 0, ...
+ 'source_abs', 1);
+% Split average source files by condition
+% YF = 'Eyes closed'
+iYF = strcmpi('YF', {sSrcAvgFiles.Condition});
+sYfSrcAvgFiles = sSrcAvgFiles(iYF);
+iYO = strcmpi('YO', {sSrcAvgFiles.Condition});
+sYoSrcAvgFiles = sSrcAvgFiles(iYO);
+% Process: Perm t-test equal [all] H0:(A=B), H1:(A<>B)
+sTestFile = bst_process('CallProcess', 'process_test_permutation2', sYfSrcAvgFiles, sYoSrcAvgFiles, ...
+ 'timewindow', [], ...
+ 'scoutsel', {}, ...
+ 'scoutfunc', 1, ... % Mean
+ 'isnorm', 0, ...
+ 'avgtime', 0, ...
+ 'iszerobad', 0, ...
+ 'Comment', '', ...
+ 'test_type', 'ttest_equal', ... % Student's t-test (equal variance) t = (mean(A)-mean(B)) / (Sx * sqrt(1/nA + 1/nB))Sx = sqrt(((nA-1)*var(A) + (nB-1)*var(B)) / (nA+nB-2))
+ 'randomizations', 1000, ...
+ 'tail', 'two'); % Two-tailed
+
+% Set display properties
+StatThreshOptions = bst_get('StatThreshOptions');
+StatThreshOptions.pThreshold = 0.01;
+StatThreshOptions.Correction = 'fdr';
+StatThreshOptions.Control = [1 2 3];
+bst_set('StatThreshOptions', StatThreshOptions);
+% Display test result
+hSrcFig = view_surface_data([], sTestFile.FileName);
+panel_surface('SelectHemispheres', 'left');
+panel_stat('UpdatePanel');
+bst_report('Snapshot', hSrcFig, sTestFile.FileName);
+pause(1);
+% Unload everything
+bst_memory('UnloadAll', 'Forced');
+
+
+%% ===== VOLUME SCOUTS =====
+volumeScouts = {'Amygdala L', 'Amygdala R', 'Thalamus L', 'Thalamus R'};
+% Load one source file
+resultsMat = in_bst_results(sSrcFiles(1).FileName);
+% Get headmodel, needed to retrieve information about the source grid
+headmodelMat = in_bst_headmodel(resultsMat.HeadModelFile);
+% Create volume atlas
+sAtlas = db_template('atlas');
+sAtlas.Name = sprintf('Volume %d', size(headmodelMat.GridLoc, 1));
+panel_scout('SetCurrentSurface', headmodelMat.SurfaceFile);
+panel_scout('SetAtlas', [], 'Add', sAtlas);
+% Create a volume scout for each volume structure
+iNewScouts = zeros(1,length(volumeScouts));
+for ix = 1 : length(volumeScouts)
+ % Index of the structure in the Grid Atlas (in headmodel data)
+ iGridScout = find(strcmpi(volumeScouts{ix}, {headmodelMat.GridAtlas.Scouts.Label}));
+ % New scout
+ sNewScout = db_template('Scout');
+ sNewScout.Label = headmodelMat.GridAtlas.Scouts(iGridScout).Label;
+ sNewScout.Vertices = headmodelMat.GridAtlas.Scouts(iGridScout).GridRows;
+ sNewScout.Region = headmodelMat.GridAtlas.Scouts(iGridScout).Region(1);
+ sNewScout = panel_scout('SetScoutsSeed', sNewScout, headmodelMat.GridLoc);
+ % Register new scout
+ iNewScout = panel_scout('SetScouts', [], 'Add', sNewScout);
+ iNewScouts(ix) = iNewScout;
+end
+% Configure scouts display
+panel_scout('SetScoutsOptions', 1, 1, 1, 'all', 0.7, 1, 1, 0);
+hFigScouts = view_scouts({sSrcFiles(1).FileName}, iNewScouts);
+bst_report('Snapshot', hFigScouts, sSrcFiles(1).FileName, 'Volume scouts');
+pause(1);
+% Unload everything
+bst_memory('UnloadAll', 'Forced');
+
+
+%% ===== SAVE REPORT =====
+% Save and display report
+ReportFile = bst_report('Save', []);
+if ~isempty(reports_dir) && ~isempty(ReportFile)
+ bst_report('Export', ReportFile, reports_dir);
+else
+ bst_report('Open', ReportFile);
+end
+
+
diff --git a/toolbox/script/tutorial_ephys.m b/toolbox/script/tutorial_ephys.m
index 612ebb02b..b1be3288a 100644
--- a/toolbox/script/tutorial_ephys.m
+++ b/toolbox/script/tutorial_ephys.m
@@ -109,7 +109,8 @@ function tutorial_ephys(tutorial_dir)
hFig = view_surface(CortexFile{1}, 0.8, [1 0 0], hFig);
figure_3d('SetStandardView', hFig, 'right');
bst_report('Snapshot', hFig, [], 'Anatomy');
-close(hFig);
+% Unload everything
+bst_memory('UnloadAll', 'Forced');
%% ===== SPIKE SORTING =====
@@ -176,6 +177,7 @@ function tutorial_ephys(tutorial_dir)
'eventsel', {'Stim On 1', 'Stim On 2', 'Stim On 3', 'Stim On 4', 'Stim On 5', 'Stim On 6', 'Stim On 7', 'Stim On 8', 'Stim On 9'}, ...
'spikesel', {'Spikes Channel AD06', 'Spikes Channel AD08 |1|'}, ...
'timewindow', [0.05, 0.12]);
+close(findobj('Type','figure'));
%% ===== NOISE CORRELATION =====
diff --git a/toolbox/script/tutorial_epilepsy.m b/toolbox/script/tutorial_epilepsy.m
index 49967b675..44671272d 100644
--- a/toolbox/script/tutorial_epilepsy.m
+++ b/toolbox/script/tutorial_epilepsy.m
@@ -283,6 +283,38 @@ function tutorial_epilepsy(tutorial_dir, reports_dir)
'Comment', 'Average spike');
+% Install and Load Brain Entropy plugin
+[isInstalled, errMsg] = bst_plugin('Install', 'brainentropy');
+if isInstalled
+ % Use default options of cMEM
+ mem_option = be_pipelineoptions(be_main, 'cMEM');
+ mem_option.optional = struct_copy_fields(mem_option.optional, ...
+ struct(...
+ 'TimeSegment', [-0.30078, 0.5], ...
+ 'BaselineType', {{'within-data'}}, ...
+ 'Baseline', [], ...
+ 'BaselineHistory', {{'within'}}, ...
+ 'BaselineSegment', [-0.30078, -0.10156], ...
+ 'groupAnalysis', 0, ...
+ 'display', 0));
+ % Process: Compute sources: BEst
+ sAvgSrcMEM = bst_process('CallProcess', 'process_inverse_mem', sFilesAvg, [], ...
+ 'comment', 'MEM', ...
+ 'mem', struct('MEMpaneloptions', mem_option), ...
+ 'sensortypes', 'EEG');
+ % Process: Snapshot: Sources (one time)
+ bst_process('CallProcess', 'process_snapshot', sAvgSrcMEM, [], ...
+ 'target', 8, ... % Sources (one time)
+ 'modality', 1, ... % MEG (All)
+ 'orient', 3, ... % top
+ 'time', 0, ...
+ 'threshold', 30, ...
+ 'Comment', 'Average spike (cMEM)');
+else
+ bst_report('Error', [], [], errMsg);
+end
+
+
% ===== SOURCE ANALYSIS: VOLUME =====
% Process: Compute head model
bst_process('CallProcess', 'process_headmodel', sFilesAvg, [], ...
diff --git a/toolbox/sensors/channel_align_auto.m b/toolbox/sensors/channel_align_auto.m
index 86671ea89..71d8fcd48 100644
--- a/toolbox/sensors/channel_align_auto.m
+++ b/toolbox/sensors/channel_align_auto.m
@@ -14,7 +14,7 @@
%
% 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
@@ -101,7 +101,7 @@
sSubject = bst_get('Subject', sStudy.BrainStormSubject);
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
@@ -162,19 +162,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');
@@ -190,24 +190,32 @@
' | 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;
+
%% ===== 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 = {};
@@ -221,13 +229,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';
diff --git a/toolbox/sensors/channel_align_manual.m b/toolbox/sensors/channel_align_manual.m
index 8d847b538..77f57e157 100644
--- a/toolbox/sensors/channel_align_manual.m
+++ b/toolbox/sensors/channel_align_manual.m
@@ -191,7 +191,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');
@@ -204,7 +204,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
@@ -232,7 +234,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); ...
@@ -406,6 +408,8 @@
bst_progress('stop');
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 =====
@@ -742,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)
@@ -851,41 +856,47 @@ function AlignKeyPress_Callback(hFig, keyEvent)
function AlignClose_Callback(varargin)
global gChanAlign;
if gChanAlign.isChanged
+ isCancel = false;
+ % Get new positions
+ [ChannelMat, Transf, iChannels] = GetCurrentChannelMat();
+ % Load original channel file
+ ChannelMatOrig = in_bst_channel(gChanAlign.ChannelFile);
+
% Ask user to save changes (only if called as a callback)
if (nargin == 3)
- SaveChanged = 1;
+ SaveChanges = 1;
else
- SaveChanged = java_dialog('confirm', ['The sensors locations changed.' 10 10 ...
- 'Would you like to save changes? ' 10 10], 'Align sensors');
+ [SaveChanges, isCancel] = 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
+ % Report (in command window) max head and sensor displacements from changes.
+ if SaveChanges || gChanAlign.isHeadPoints
+ process_adjust_coordinates('CheckCurrentAdjustments', ChannelMat, ChannelMatOrig);
end
% Save changes to channel file and close figure
- if SaveChanged
+ if SaveChanges
% 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;
- % Get new positions
- [ChannelMat, Transf, iChannels] = GetCurrentChannelMat();
- % Load original channel file
- ChannelMatOrig = in_bst_channel(gChanAlign.ChannelFile);
% Save new electrodes positions in ChannelFile
bst_save(gChanAlign.ChannelFile, ChannelMat, 'v7');
% Get study associated with channel file
[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
- SaveChanged = 0;
end
% Only close figure
gChanAlign.Figure3DCloseRequest_Bak(varargin{1:2});
- % Apply to other recordings with same sensor locations in the same subject
- if SaveChanged
- CopyToOtherFolders(ChannelMatOrig, iStudy, Transf, iChannels);
- end
end
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
diff --git a/toolbox/sensors/channel_detect_eegcap_auto.m b/toolbox/sensors/channel_detect_eegcap_auto.m
new file mode 100644
index 000000000..9f15d99f0
--- /dev/null
+++ b/toolbox/sensors/channel_detect_eegcap_auto.m
@@ -0,0 +1,241 @@
+function varargout = channel_detect_eegcap_auto(varargin)
+% CHANNEL_DETECT_EEGCAP_AUTO: Automatic electrode detection and labelling of 3D Scanner acquired mesh
+%
+% USAGE: [capCenters2d, capImg2d, surface3dscannerUv] = channel_detect_eegcap_auto('FindElectrodesEegCap', surface3dscanner, isWhiteCap)
+% channel_detect_eegcap_auto('WarpLayout2Mesh', capCenters2d, capImg2d, surface3dscannerUv, channelRef, eegPoints)
+% eegCapLandmarkLabels = channel_detect_eegcap_auto('GetEegCapLandmarkLabels', eegCapName)
+%
+% PARAMETERS:
+% - surface3dscanner : The 3D mesh surface obtained from the 3d Scanner loaded into brainstorm
+% - isWhiteCap : Set if the 3D mesh surface correspongs to a white EEG cap
+% - surface3dscannerUv : 'surface3dscanner' above along with the UV texture information of the surface
+% - capImg2d : Flattend 2D grayscale image of the mesh
+% - capCenters2d : The ceters of the various electrodes detected in the flattened 2D image of the mesh
+% - channelRef : The channel file containing all the layout information of the cap
+% - eegCapName : Name of the EEG cap
+% - eegCapLandmarkLabels : The manually chosen list of labels of the electrodes to be used as initilization for automation
+% - nLandmarkLabels : The count for the number of chosen electrode labels above
+%
+% @=============================================================================
+% 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: Anand A. Joshi, 2024
+% Chinmay Chinara, 2024
+% Raymundo Cassani, 2024
+
+eval(macro_method);
+end
+
+%% ===== FIND ELECTRODES ON THE EEG CAP =====
+function [capCenters2d, capImg2d, surface3dscannerUv] = FindElectrodesEegCap(surface3dscanner, isWhiteCap)
+ % Hyperparameters for circle detection
+ % NOTE: these values can vary for new caps
+ minRadius = 1;
+ maxRadius = 25;
+
+ % create a copy of the input mesh to add UV texture information to it as well
+ surface3dscannerUv = surface3dscanner;
+
+ % Flatten the 3D mesh to 2D space
+ [surface3dscannerUv.u, surface3dscannerUv.v] = bst_project_2d(surface3dscanner.Vertices(:,1), surface3dscanner.Vertices(:,2), surface3dscanner.Vertices(:,3), '2dcap');
+
+ % Perform image processing to detect the electrode locations
+ % Convert to grayscale
+ grayness = surface3dscanner.Color*[1;1;1]/sqrt(3);
+
+ % Interpolate and fit flattended mesh image to a 512x512 grid
+ % NOTE: Should work with any flattened cap mesh but needs more testing
+ ll=linspace(-1,1,512);
+ [X,Y]=meshgrid(ll,ll);
+ capImg2d = 0*X;
+ warning('off','MATLAB:scatteredInterpolant:DupPtsAvValuesWarnId');
+ capImg2d(:) = griddata(surface3dscannerUv.u(1:end),surface3dscannerUv.v(1:end),grayness,X(:),Y(:),'linear');
+ warning('on','MATLAB:scatteredInterpolant:DupPtsAvValuesWarnId');
+
+ % For white caps
+ if isWhiteCap
+ capImg2d = imcomplement(capImg2d);
+ end
+
+ % Detect the centers of the electrodes which appear as circles in the flattened image whose radii are in the range below
+ warning('off','images:imfindcircles:warnForSmallRadius');
+ warning('off','images:imfindcircles:warnForLargeRadiusRange');
+ capCenters2d = imfindcircles(capImg2d, [minRadius maxRadius]);
+ warning('on','images:imfindcircles:warnForSmallRadius');
+ warning('on','images:imfindcircles:warnForLargeRadiusRange');
+
+end
+
+%% ===== WARP ELECTRODE LOCATIONS FROM EEG CAP MANUFACTURER LAYOUT AVAILABLE IN BRAINSTORM TO THE MESH =====
+function capPoints = WarpLayout2Mesh(capCenters2d, capImg2d, surface3dscannerUv, channelRef, eegPoints)
+ capPoints = struct();
+ % Hyperparameters for warping and interpolation
+ % NOTE: these values can vary for new caps
+ % Number of iterations to run warp-interpolation on
+ numIters = 1000;
+ % Defines the rigidity of the warping (check the 'tpsGetWarp' function for more details)
+ lambda = 100000;
+ % Dimension of the flattened cap from mesh
+ capImgDim = length(capImg2d);
+ % Threshold for ignoring some border pixels that might be bad detections
+ ignorePix = 15;
+
+ % Get current montage
+ DigitizeOptions = bst_get('DigitizeOptions');
+ panel_fun = @panel_digitize;
+ eegPointsLabel = eegPoints.Label;
+ if isfield(DigitizeOptions, 'Version') && strcmpi(DigitizeOptions.Version, '2024')
+ panel_fun = @panel_digitize_2024;
+ eegPointsLabel = {eegPoints.Label};
+ end
+ curMontage = panel_fun('GetCurrentMontage');
+ % Get EEG cap landmark labels used for initialization
+ capLandmarkLabels = GetEegCapLandmarkLabels(curMontage.Name);
+
+ % Check that all landmarks are acquired
+ if ~all(ismember([capLandmarkLabels], eegPointsLabel))
+ bst_error('Not all EEG landmarks are provided', 'Auto electrode location', 1);
+ return
+ end
+
+ % Convert EEG cap manufacturer layout from 3D to 2D
+ capLayoutPts3d = [channelRef.Loc]';
+ [X1, Y1] = bst_project_2d(capLayoutPts3d(:,1), capLayoutPts3d(:,2), capLayoutPts3d(:,3), '2dcap');
+ capLayoutPts2d = [X1 Y1];
+ capLayoutNames = {channelRef.Name};
+
+ % Indices for capLayoutPts2dSorted for points to compute warp
+ [~, iwarp] = ismember(eegPointsLabel, capLayoutNames);
+
+ % Warping EEG cap layout electrodes to mesh
+ % Get 2D projected landmark points to be used for initialization
+ capLayoutPts2dInit = capLayoutPts2d(iwarp, :);
+ % Get 2D projected points of the 3D points selected by the user on the mesh
+ % Check if using new version
+ if isfield(DigitizeOptions, 'Version') && strcmpi(DigitizeOptions.Version, '2024')
+ eegPointsLoc = cat(1, eegPoints.Loc);
+ else
+ eegPointsLoc = cat(1, eegPoints.EEG);
+ end
+ [x2, y2] = bst_project_2d(eegPointsLoc(:,1), eegPointsLoc(:,2), eegPointsLoc(:,3), '2dcap');
+ % Reprojection into the space of the flattened mesh dimensions
+ capUserSelectPts2d = ([x2 y2]+1) * capImgDim/2;
+
+ % Delete the manual electrodes selected in figure to update it with the automatic detected ones
+ for i=1 : length(eegPoints)
+ panel_fun('DeletePoint_Callback');
+ end
+
+ % Do the warping and interpolation
+ warp = tpsGetWarp(10, capLayoutPts2dInit(:,1)', capLayoutPts2dInit(:,2)', capUserSelectPts2d(:,1)', capUserSelectPts2d(:,2)' );
+ [xsR,ysR] = tpsInterpolate(warp, capLayoutPts2d(:,1)', capLayoutPts2d(:,2)', 0);
+ capLayoutPts2d(:,1) = xsR;
+ capLayoutPts2d(:,2) = ysR;
+ % 'ignorePix' is just a hyperparameter. It is because if some point is detected near the border then it is
+ % too close to the border; it moves it inside. It leaves a margin of 'ignorePix' pixels around the border
+ capLayoutPts2d = max(min(capLayoutPts2d,capImgDim-ignorePix),ignorePix);
+
+ bst_progress('start', '3Dscanner', 'Automatic labelling of EEG sensors...', 0, 100);
+ % Warp and interpolate to get the best point fitting
+ for numIter=1:numIters
+ % Show progress
+ progressPrc = round(100 .* numIter ./ numIters);
+ if progressPrc > 0 && ~mod(progressPrc, 5)
+ bst_progress('set', progressPrc);
+ end
+ % Nearest point search between the layout and detected circle centers from the 2D flattened mesh
+ % 'k' is an index into points from the available layout
+ k = dsearchn(capLayoutPts2d, capCenters2d);
+ [vecLayoutPts,ind] = unique(k);
+
+ % distance between the layout and detected circle centers from the 2D flattened mesh
+ vecLayout2Mesh = capCenters2d(ind,:)-capLayoutPts2d(vecLayoutPts,:);
+ dist = sqrt(vecLayout2Mesh(:,1).^2+vecLayout2Mesh(:,2).^2);
+
+ % Identify outliers with 3*scaled_MAD from median and remove them
+ % Use 'rmoutliers' for Matlab >= R2018b
+ if bst_get('MatlabVersion') >= 905
+ [~, isoutlier] = rmoutliers(dist);
+ % Implementation
+ else
+ mad = median(abs(dist-median(dist)));
+ c = -1/(sqrt(2) * erfcinv(3/2)) * 2;
+ scaled_mad = c * mad;
+ isoutlier = find(abs(dist-median(dist)) > 3*scaled_mad);
+ end
+ ind(isoutlier) = [];
+ vecLayoutPts(isoutlier) = [];
+
+ % Perform warping and interpolation to fit the points
+ warp = tpsGetWarp(lambda, capLayoutPts2d(vecLayoutPts,1)', capLayoutPts2d(vecLayoutPts,2)', capCenters2d(ind,1)', capCenters2d(ind,2)' );
+ [xsR,ysR] = tpsInterpolate(warp, capLayoutPts2d(:,1)', capLayoutPts2d(:,2)', 0);
+
+ % Perform gradual warping for half the iterations and fast warping for the rest of the iterations
+ if numIter 'vertices', 'channel' -> 'surface'
+%
+% OUTPUT:
+% - ChannelMat : Same type as ChannelMat input
% @=============================================================================
% This function is part of the Brainstorm software:
@@ -28,25 +33,32 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2017
+% Raymundo Cassani, 2024
% Parse inputs
+if (nargin < 4) || isempty(isVertices)
+ isVertices = 0;
+end
if (nargin < 3) || isempty(isConfirmFix)
isConfirmFix = 1;
end
-% Get EEG channels
-iEEG = good_channel(ChannelMat.Channel, [], {'EEG','SEEG','ECOG','Fiducial'});
-% If not enough channels: nothing to do
-if (length(iEEG) <= 8) && (length(iEEG) ~= length(ChannelMat.Channel))
- return;
-end
-
-% Get all EEG locations
-eegLoc = [];
-for k = 1:length(iEEG)
- if ~isempty(ChannelMat.Channel(iEEG(k)).Loc) && ~isequal(ChannelMat.Channel(iEEG(k)).Loc, [0;0;0])
- eegLoc = [eegLoc, ChannelMat.Channel(iEEG(k)).Loc(:,1)];
+if isstruct(ChannelMat)
+ % Get EEG channels
+ iEEG = good_channel(ChannelMat.Channel, [], {'EEG','SEEG','ECOG','Fiducial'});
+ % If not enough channels: nothing to do
+ if (length(iEEG) <= 8) && (length(iEEG) ~= length(ChannelMat.Channel))
+ return;
end
+ % Get all EEG locations
+ eegLoc = [];
+ for k = 1:length(iEEG)
+ if ~isempty(ChannelMat.Channel(iEEG(k)).Loc) && ~isequal(ChannelMat.Channel(iEEG(k)).Loc, [0;0;0])
+ eegLoc = [eegLoc, ChannelMat.Channel(iEEG(k)).Loc(:,1)];
+ end
+ end
+else
+ eegLoc = ChannelMat';
end
if isempty(eegLoc)
return;
@@ -64,22 +76,33 @@
strFactor = num2str(FactorTest(iFactor));
% Ask user if we should scale the distances
if isConfirmFix
+ % Strings for GUI message
+ locType = 'EEG electrodes';
+ fileType = 'channel';
+ if isVertices
+ locType = 'vertices';
+ fileType = 'surface';
+ end
strFactor = java_dialog('question', ...
- ['Warning: The EEG electrodes locations might not be in the expected units (' FileUnits ').' 10 ...
- 'Please select a scaling factor for the units (suggested: ' strFactor '):' 10 10], 'Import channel file', ...
+ ['Warning: The ' locType ' locations might not be in the expected units (' FileUnits ').' 10 ...
+ 'Please select a scaling factor for the units (suggested: ' strFactor '):' 10 10], ['Import ' fileType ' file'], ...
[], {'0.001', '0.01', '0.1', '1', '10', '100' '1000'}, strFactor);
end
% If user accepted to scale
if ~isempty(strFactor) && ~isequal(strFactor, '1')
Factor = str2num(strFactor);
% Apply correction to location values
- for k = 1:length(iEEG)
- ChannelMat.Channel(iEEG(k)).Loc = ChannelMat.Channel(iEEG(k)).Loc .* Factor;
- end
- % Apply correction to head points
- isHeadPoints = isfield(ChannelMat, 'HeadPoints') && ~isempty(ChannelMat.HeadPoints.Loc);
- if isHeadPoints
- ChannelMat.HeadPoints.Loc = ChannelMat.HeadPoints.Loc .* Factor;
+ if isstruct(ChannelMat)
+ for k = 1:length(iEEG)
+ ChannelMat.Channel(iEEG(k)).Loc = ChannelMat.Channel(iEEG(k)).Loc .* Factor;
+ end
+ % Apply correction to head points
+ isHeadPoints = isfield(ChannelMat, 'HeadPoints') && ~isempty(ChannelMat.HeadPoints.Loc);
+ if isHeadPoints
+ ChannelMat.HeadPoints.Loc = ChannelMat.HeadPoints.Loc .* Factor;
+ end
+ else
+ ChannelMat = eegLoc' .* Factor;
end
end
end
diff --git a/toolbox/sensors/panel_digitize.m b/toolbox/sensors/panel_digitize.m
index dd9ee9dbb..47dc31186 100644
--- a/toolbox/sensors/panel_digitize.m
+++ b/toolbox/sensors/panel_digitize.m
@@ -26,6 +26,7 @@
% =============================================================================@
%
% Authors: Elizabeth Bock & Francois Tadel, 2012-2017
+% Chinmay Chinara, 2024
eval(macro_method);
end
@@ -39,51 +40,22 @@
%#function icinterface
%% ===== START =====
-function Start() %#ok
- global Digitize;
- % ===== PREPARE DATABASE =====
- % If no protocol: exit
- if (bst_get('iProtocol') <= 0)
- bst_error('Please create a protocol first.', 'Digitize', 0);
- return;
- end
- % Get subject
- SubjectName = 'Digitize';
- [sSubject, iSubject] = bst_get('Subject', SubjectName);
- % Create if subject doesnt exist
- if isempty(iSubject)
- % Default anat / one channel file per subject
- UseDefaultAnat = 1;
- UseDefaultChannel = 0;
- [sSubject, iSubject] = db_add_subject(SubjectName, iSubject, UseDefaultAnat, UseDefaultChannel);
- % Update tree
- panel_protocols('UpdateTree');
- end
-
- % ===== PATIENT ID =====
- % Get Digitize options
- DigitizeOptions = bst_get('DigitizeOptions');
- % Ask for subject id
- PatientId = java_dialog('input', 'Please, enter subject name or id:', 'Digitize', [], DigitizeOptions.PatientId);
- if isempty(PatientId)
- return;
- end
- % Save the new default patient id
- DigitizeOptions.PatientId = PatientId;
- bst_set('DigitizeOptions', DigitizeOptions);
-
+function Start(varargin) %#ok
+ global Digitize
% ===== INITIALIZE CONNECTION =====
% Intialize global variable
Digitize = struct(...
+ 'Type' , [], ...
'SerialConnection', [], ...
'Mode', 0, ...
'hFig', [], ...
'iDS', [], ...
'FidSets', 2, ...
'EEGlabels', [], ...
- 'SubjectName', SubjectName, ...
+ 'SubjectName', [], ...
'ConditionName', [], ...
'BeepWav', [], ...
+ 'isEditPts', 0, ...
'Points', struct(...
'nasion', [], ...
'LPA', [], ...
@@ -92,10 +64,104 @@ function Start() %#ok
'hpiL', [], ...
'hpiR', [], ...
'EEG', [], ...
+ 'Label', [], ...
'headshape',[], ...
'trans', []));
+
+ % ===== PARSE INPUT =====
+ DigitizerType = 'Digitize';
+ sSubject = [];
+ iSubject = [];
+ surfaceFile = [];
+ if nargin > 0 && ~isempty(varargin{1})
+ DigitizerType = varargin{1};
+ end
+ if nargin > 1 && ~isempty(varargin{2})
+ sSubject = varargin{2};
+ end
+ if nargin > 2 && ~isempty(varargin{3})
+ iSubject = varargin{3};
+ end
+ if nargin > 3 && ~isempty(varargin{4})
+ surfaceFile = varargin{4};
+ end
+ Digitize.Type = DigitizerType;
+ switch DigitizerType
+ case 'Digitize'
+ % Do nothing
+ case '3DScanner'
+ % Simulate
+ SetSimulate(1);
+ otherwise
+ bst_error(sprintf('DigitizerType : "%s" is not supported', DigitizerType));
+ return
+ end
+
+ % Get Digitize options
+ DigitizeOptions = bst_get('DigitizeOptions');
+ % Check if using new version
+ if isfield(DigitizeOptions, 'Version') && strcmpi(DigitizeOptions.Version, '2024')
+ if strcmpi(Digitize.Type, '3DScanner')
+ bst_call(@panel_digitize_2024, 'Start', varargin{:});
+ else
+ bst_call(@panel_digitize_2024, 'Start');
+ end
+ return;
+ end
+
+ % ===== PREPARE DATABASE =====
+ % If no protocol: exit
+ if (bst_get('iProtocol') <= 0)
+ bst_error('Please create a protocol first.', Digitize.Type, 0);
+ return;
+ end
+
+ % ===== PATIENT ID =====
+ if isempty(sSubject)
+ % Get Digitize options
+ DigitizeOptions = bst_get('DigitizeOptions');
+ % Ask for subject id
+ PatientId = java_dialog('input', 'Please, enter subject ID:', Digitize.Type, [], DigitizeOptions.PatientId);
+ if isempty(PatientId)
+ return;
+ end
+ % Save the new default patient id
+ DigitizeOptions.PatientId = PatientId;
+ bst_set('DigitizeOptions', DigitizeOptions);
+
+ % ===== GET SUBJECT =====
+ if strcmpi(Digitize.Type, '3DScanner')
+ SubjectName = [Digitize.Type, '_', PatientId];
+ else
+ SubjectName = Digitize.Type;
+ end
+
+ [sSubject, iSubject] = bst_get('Subject', SubjectName);
+ else
+ SubjectName = sSubject.Name;
+ end
+
+ % Save the new SubjectName
+ Digitize.SubjectName = SubjectName;
+
+ % Create if subject doesnt exist
+ if isempty(iSubject)
+ % Default anat / one channel file per subject
+ if strcmpi(Digitize.Type, '3DScanner')
+ [sSubject, iSubject] = db_add_subject(SubjectName, iSubject);
+ sTemplates = bst_get('AnatomyDefaults');
+ db_set_template(iSubject, sTemplates(1), 1);
+ else
+ UseDefaultAnat = 1;
+ UseDefaultChannel = 0;
+ [sSubject, iSubject] = db_add_subject(SubjectName, iSubject, UseDefaultAnat, UseDefaultChannel);
+ end
+ % Update tree
+ panel_protocols('UpdateTree');
+ end
+
% Start Serial Connection
- if ~CreateSerialConnection();
+ if ~CreateSerialConnection()
return;
end
@@ -107,7 +173,7 @@ function Start() %#ok
% Generate new condition name
ConditionName = sprintf('%s_%02d%02d%02d_%02d', DigitizeOptions.PatientId, c(1), c(2), c(3), i);
% Get condition
- [sStudy, iStudy] = bst_get('StudyWithCondition', [SubjectName '/' ConditionName]);
+ sStudy = bst_get('StudyWithCondition', [SubjectName '/' ConditionName]);
% If condition doesn't exist: ok, keep this one
if isempty(sStudy)
break;
@@ -126,10 +192,58 @@ function Start() %#ok
db_reload_studies(iStudy);
% Save condition name
Digitize.ConditionName = ConditionName;
+
+ if strcmpi(Digitize.Type, '3DScanner')
+ if isempty(surfaceFile)
+ % Import surface
+ iSurface = find(cellfun(@(x)~isempty(regexp(x, 'tess_textured', 'match')), {sSubject.Surface.FileName}));
+ if isempty(iSurface)
+ [~, surfaceFiles] = import_surfaces(iSubject);
+ if isempty(surfaceFiles)
+ return
+ end
+ surfaceFile = file_short(surfaceFiles{end});
+ else
+ [res, isCancel] = java_dialog('question', ['There is already scanned mesh available for this subject.' 10 10 ...
+ 'What do you want to do?'], ...
+ 'Import surface', [], {'Use existing', 'Add new', 'Cancel'}, 'Use existing');
+ if strcmpi(res, 'cancel') || isCancel
+ return
+ elseif strcmpi(res, 'use existing')
+ % If more than one surface present, user can choose
+ if length(iSurface) > 1
+ texSurfComment = java_dialog('combo', 'Select the textured surface:
', 'Choose textured surface', [], {sSubject.Surface(iSurface).Comment});
+ texSurfComment = strrep(texSurfComment, '_defaced', '');
+ if isempty(texSurfComment)
+ return
+ end
+ iSurfFile = find(cellfun(@(x)~isempty(regexp(x, [texSurfComment '.mat'], 'match')), {sSubject.Surface.FileName}));
+ surfaceFile = sSubject.Surface(iSurfFile).FileName;
+ % If only one surface is present, then load it directly
+ else
+ surfaceFile = sSubject.Surface(iSurface(end)).FileName;
+ end
+ elseif strcmpi(res, 'add new')
+ % Import a new textured mesh and append it to the list
+ [~, surfaceFiles] = import_surfaces(iSubject);
+ if isempty(surfaceFiles)
+ return
+ end
+ surfaceFile = file_short(surfaceFiles{end});
+ end
+ end
+ end
+ bst_progress('start', Digitize.Type, 'Loading surface file...');
+ Digitize.surfaceFile = surfaceFile;
+ sSurf = bst_memory('LoadSurface', Digitize.surfaceFile);
+ % Display surface
+ view_surface_matrix(sSurf.Vertices, sSurf.Faces, [], sSurf.Color, [], [], Digitize.surfaceFile);
+ end
% ===== DISPLAY DIGITIZE WINDOW =====
% Display panel
- panelContainer = gui_show('panel_digitize', 'JavaWindow', 'Digitize', [], [], [], []);
+ % Set window title to Digitize.Type
+ panelContainer = gui_show('panel_digitize', 'JavaWindow', Digitize.Type, [], [], [], []);
% Hide Brainstorm window
jBstFrame = bst_get('BstFrame');
jBstFrame.setVisible(0);
@@ -142,11 +256,8 @@ function Start() %#ok
ResetDataCollection();
% Load beep sound
- if bst_iscompiled()
- wavfile = bst_fullfile(bst_get('BrainstormHomeDir'), 'toolbox', 'sensors', 'private', 'bst_beep_wav.mat');
- filemat = load(wavfile, 'wav');
- Digitize.BeepWav = filemat.wav;
- end
+ wavfile = bst_fullfile(bst_get('BrainstormHomeDir'), 'toolbox', 'sensors', 'private', 'bst_beep.wav');
+ [Digitize.BeepWav.data, Digitize.BeepWav.fs] = audioread(wavfile);
end
@@ -156,6 +267,7 @@ function Start() %#ok
%% ===== CREATE PANEL =====
function [bstPanelNew, panelName] = CreatePanel() %#ok
+ global Digitize
% Constants
panelName = 'Digitize';
% Java initializations
@@ -171,32 +283,49 @@ function Start() %#ok
fontSize = round(11 * bst_get('InterfaceScaling') / 100);
% ===== MENU BAR =====
+ jPanelMenu = gui_component('panel');
jMenuBar = java_create('javax.swing.JMenuBar');
- jPanelNew.add(jMenuBar, BorderLayout.NORTH);
- % File menu
+ jPanelMenu.add(jMenuBar, BorderLayout.NORTH);
+ jLabelNews = gui_component('label', jPanelMenu, BorderLayout.CENTER, ...
+ ['
Digitize version: "legacy"
' ...
+ '&bull Try the new Digitize version: File > Switch to Digitize "2024" ' ...
+ '&bull More details: Help > Digitize tutorial'], [], [], [], fontSize);
+ jLabelNews.setHorizontalAlignment(SwingConstants.CENTER);
+ jLabelNews.setOpaque(true);
+ jLabelNews.setBackground(java.awt.Color.yellow);
+
+ % ===== FILE MENU =====
jMenu = gui_component('Menu', jMenuBar, [], 'File', [], [], [], []);
gui_component('MenuItem', jMenu, [], 'Start over', IconLoader.ICON_RELOAD, [], @(h,ev)bst_call(@ResetDataCollection, 1), []);
gui_component('MenuItem', jMenu, [], 'Save as...', IconLoader.ICON_SAVE, [], @(h,ev)bst_call(@Save_Callback), []);
jMenu.addSeparator();
gui_component('MenuItem', jMenu, [], 'Edit settings...', IconLoader.ICON_EDIT, [], @(h,ev)bst_call(@EditSettings), []);
- gui_component('MenuItem', jMenu, [], 'Reset serial connection', IconLoader.ICON_FLIP, [], @(h,ev)bst_call(@CreateSerialConnection), []);
+ gui_component('MenuItem', jMenu, [], 'Switch to Digitize "2024"', [], [], @(h,ev)bst_call(@SwitchVersion), []);
+ if ~strcmpi(Digitize.Type, '3DScanner')
+ gui_component('MenuItem', jMenu, [], 'Reset serial connection', IconLoader.ICON_FLIP, [], @(h,ev)bst_call(@CreateSerialConnection), []);
+ end
jMenu.addSeparator();
- if exist('bst_headtracking')
+ if exist('bst_headtracking', 'file') && ~strcmpi(Digitize.Type, '3DScanner')
gui_component('MenuItem', jMenu, [], 'Start head tracking', IconLoader.ICON_ALIGN_CHANNELS, [], @(h,ev)bst_call(@(h,ev)bst_headtracking([],1,1)), []);
jMenu.addSeparator();
end
gui_component('MenuItem', jMenu, [], 'Save and exit', IconLoader.ICON_RESET, [], @(h,ev)bst_call(@Close_Callback), []);
- % EEG Montage menu
+ % ===== EEG MONTAGE MENU =====
jMenuEeg = gui_component('Menu', jMenuBar, [], 'EEG montage', [], [], [], []);
CreateMontageMenu(jMenuEeg);
+ % ===== HELP MENU =====
+ jMenuHelp = gui_component('Menu', jMenuBar, [], 'Help', [], [], [], []);
+ gui_component('MenuItem', jMenuHelp, [], 'Digitize tutorial', [], [], @(h,ev)web('https://neuroimage.usc.edu/brainstorm/Tutorials/TutDigitizeLegacy', '-browser'), []);
- % ===== Control Panel =====
+ jPanelNew.add(jPanelMenu, BorderLayout.NORTH);
+
+ % ===== CONTROL PANEL =====
jPanelControl = java_create('javax.swing.JPanel');
jPanelControl.setLayout(BoxLayout(jPanelControl, BoxLayout.Y_AXIS));
jPanelControl.setBorder(BorderFactory.createEmptyBorder(7,7,7,7));
modeButtonGroup = javax.swing.ButtonGroup();
- % ===== Coils panel =====
+ % ===== COILS PANEL =====
jPanelCoils = gui_river([5,4], [10,10,10,10], 'Head Localization Coils');
% Fiducials
jButtonhpiN = gui_component('toggle', jPanelCoils, [], 'HPI-N', {modeButtonGroup}, 'Center Coil', @(h,ev)SwitchToNewMode(1), largeFontSize);
@@ -218,7 +347,7 @@ function Start() %#ok
jPanelControl.add(jPanelCoils);
jPanelControl.add(Box.createVerticalStrut(20));
- % ===== Fiducials panel =====
+ % ===== FIDUCIALS PANEL =====
jPanelHeadCoord = gui_river([5,4], [10,10,10,10], 'Anatomical fiducials');
% Fiducials
jButtonNasion = gui_component('toggle', jPanelHeadCoord, [], 'Nasion', {modeButtonGroup}, 'Nasion', @(h,ev)SwitchToNewMode(4), largeFontSize);
@@ -243,15 +372,23 @@ function Start() %#ok
jPanelControl.add(jPanelHeadCoord);
jPanelControl.add(Box.createVerticalStrut(20));
- % ===== EEG panel =====
+ % ===== EEG PANEL =====
jPanelEEG = gui_river([5,4], [10,10,10,10], 'EEG electrodes coordinates');
% Start EEG coord collection
jButtonEEGStart = gui_component('toggle', jPanelEEG, [], 'EEG', {modeButtonGroup}, 'Start/Restart EEG digitization', @(h,ev)SwitchToNewMode(7), largeFontSize);
newButtonSize = Dimension(initButtonSize.getWidth()*1.5, initButtonSize.getHeight()*1.5);
jButtonEEGStart.setPreferredSize(newButtonSize);
jButtonEEGStart.setFocusable(0);
- % Separator
- gui_component('label', jPanelEEG, 'hfill', '');
+
+ if strcmpi(Digitize.Type, '3DScanner')
+ % Auto EEG cap electrodes detection button
+ jButtonEEGAutoDetectElectrodes = gui_component('button', jPanelEEG, [], 'Auto', [], GenerateTooltipTextAuto(), @EEGAutoDetectElectrodes, largeFontSize);
+ jButtonEEGAutoDetectElectrodes.setPreferredSize(newButtonSize);
+ else
+ % Separator
+ jButtonEEGAutoDetectElectrodes = gui_component('label', jPanelEEG, 'hfill', '');
+ end
+
% Number
jTextFieldEEG = gui_component('text',jPanelEEG, [], '1', [], 'EEG Sensor # to be digitized', @EEGChangePoint_Callback, largeFontSize);
jTextFieldEEG.setPreferredSize(newButtonSize)
@@ -259,14 +396,22 @@ function Start() %#ok
jPanelControl.add(jPanelEEG);
jPanelControl.add(Box.createVerticalStrut(20));
- % ===== Extra points panel =====
+ % ===== EXTRA POINTS PANEL =====
jPanelExtra = gui_river([5,4], [10,10,10,10], 'Head shape coordinates');
% Start Extra coord collection
jButtonExtraStart = gui_component('toggle',jPanelExtra, [], 'Shape', {modeButtonGroup}, 'Start/Restart head shape digitization', @(h,ev)SwitchToNewMode(8), largeFontSize);
jButtonExtraStart.setPreferredSize(newButtonSize);
jButtonExtraStart.setFocusable(0);
- % Separator
- gui_component('label', jPanelExtra, 'hfill', '');
+
+ if strcmpi(Digitize.Type, '3DScanner')
+ % Add 150 random head shape points generation button
+ jButtonRandomHeadPts = gui_component('button', jPanelExtra, [], 'Random', [], 'Collect 150 head shape points from mesh', @CollectRandomHeadPts_Callback, largeFontSize);
+ jButtonRandomHeadPts.setPreferredSize(newButtonSize);
+ else
+ % Separator
+ jButtonRandomHeadPts = gui_component('label', jPanelExtra, 'hfill', '');
+ end
+
% Number
jTextFieldExtra = gui_component('text',jPanelExtra, [], '1',[], 'Head shape point to be digitized', @ExtraChangePoint_Callback, largeFontSize);
jTextFieldExtra.setPreferredSize(newButtonSize)
@@ -274,23 +419,28 @@ function Start() %#ok
jPanelControl.add(jPanelExtra);
jPanelControl.add(Box.createVerticalStrut(20));
- % ===== Extra buttons =====
+ % ===== EXTRA BUTTONS =====
jPanelMisc = gui_river([5,4], [2,4,4,0]);
- gui_component('button', jPanelMisc, [], 'Collect point', [], [], @ManualCollect_Callback);
+ if ~strcmpi(Digitize.Type, '3DScanner')
+ gui_component('button', jPanelMisc, [], 'Collect point', [], [], @(h,ev)bst_call(@ManualCollect_Callback));
+ end
jButtonDeletePoint = gui_component('button', jPanelMisc, 'hfill', 'Delete last point', [], [], @DeletePoint_Callback);
gui_component('Button', jPanelMisc, [], 'Save as...', [], [], @Save_Callback);
jPanelControl.add(jPanelMisc);
jPanelControl.add(Box.createVerticalStrut(20));
jPanelNew.add(jPanelControl, BorderLayout.WEST);
- % ===== Coordinate Display Panel =====
+ % ===== COORDINATE DISPLAY PANEL =====
jPanelDisplay = gui_component('Panel');
jPanelDisplay.setBorder(java_scaled('titledborder', 'Coordinates (cm)'));
% List of coordinates
jListCoord = JList(largeFontSize);
jListCoord.setCellRenderer(BstStringListRenderer(fontSize));
+ java_setcb(jListCoord, ...
+ 'KeyTypedCallback', @(h,ev)bst_call(@CoordListKeyTyped_Callback,h,ev), ...
+ 'MouseClickedCallback', @(h,ev)bst_call(@CoordListClick_Callback,h,ev));
% Size
- jPanelScrollList = JScrollPane();
+ jPanelScrollList = JScrollPane(jListCoord);
jPanelScrollList.getLayout.getViewport.setView(jListCoord);
jPanelScrollList.setHorizontalScrollBarPolicy(jPanelScrollList.HORIZONTAL_SCROLLBAR_NEVER);
jPanelScrollList.setVerticalScrollBarPolicy(jPanelScrollList.VERTICAL_SCROLLBAR_ALWAYS);
@@ -298,25 +448,102 @@ function Start() %#ok
jPanelDisplay.add(jPanelScrollList, BorderLayout.CENTER);
jPanelNew.add(jPanelDisplay, BorderLayout.CENTER);
- % create the controls structure
- ctrl = struct('jMenuEeg', jMenuEeg, ...
- 'jButtonNasion', jButtonNasion, ...
- 'jButtonLPA', jButtonLPA, ...
- 'jButtonRPA', jButtonRPA, ...
- 'jLabelCoilMessage', jLabelCoilMessage, ...
- 'jLabelFidMessage', jLabelFidMessage, ...
- 'jButtonhpiN', jButtonhpiN, ...
- 'jButtonhpiL', jButtonhpiL, ...
- 'jButtonhpiR', jButtonhpiR, ...
- 'jListCoord', jListCoord, ...
- 'jButtonEEGStart', jButtonEEGStart, ...
- 'jTextFieldEEG', jTextFieldEEG, ...
- 'jButtonExtraStart', jButtonExtraStart, ...
- 'jTextFieldExtra', jTextFieldExtra, ...
- 'jButtonDeletePoint', jButtonDeletePoint);
+ % Create the controls structure
+ ctrl = struct('jMenuEeg', jMenuEeg, ...
+ 'jButtonNasion', jButtonNasion, ...
+ 'jButtonLPA', jButtonLPA, ...
+ 'jButtonRPA', jButtonRPA, ...
+ 'jLabelCoilMessage', jLabelCoilMessage, ...
+ 'jLabelFidMessage', jLabelFidMessage, ...
+ 'jButtonhpiN', jButtonhpiN, ...
+ 'jButtonhpiL', jButtonhpiL, ...
+ 'jButtonhpiR', jButtonhpiR, ...
+ 'jListCoord', jListCoord, ...
+ 'jButtonEEGStart', jButtonEEGStart, ...
+ 'jButtonEEGAutoDetectElectrodes', jButtonEEGAutoDetectElectrodes, ...
+ 'jTextFieldEEG', jTextFieldEEG, ...
+ 'jButtonExtraStart', jButtonExtraStart, ...
+ 'jButtonRandomHeadPts', jButtonRandomHeadPts, ...
+ 'jTextFieldExtra', jTextFieldExtra, ...
+ 'jButtonDeletePoint', jButtonDeletePoint);
bstPanelNew = BstPanel(panelName, jPanelNew, ctrl);
+
+ %% =================================================================================
+ % === INTERNAL CALLBACKS =========================================================
+ % =================================================================================
+ %% ===== COORDINATE LIST KEY TYPED CALLBACK =====
+ function CoordListKeyTyped_Callback(h, ev)
+ switch(uint8(ev.getKeyChar()))
+ % Delete
+ case {ev.VK_DELETE, ev.VK_BACK_SPACE}
+ ctrl = bst_get('PanelControls', 'Digitize');
+ % If contact list rendering is blank in panel then dont't proceed
+ if ctrl.jListCoord.isSelectionEmpty()
+ return;
+ end
+
+ [sCoordName, iSelCoord] = GetSelectedCoord();
+ spl = regexp(sCoordName,'\s+','split');
+ nameFinal = spl{1};
+ if (~strcmpi(nameFinal, 'Nasion') &&...
+ ~strcmpi(nameFinal, 'LPA') &&...
+ ~strcmpi(nameFinal, 'RPA'))
+ listModel = ctrl.jListCoord.getModel();
+ listModel.setElementAt(nameFinal, iSelCoord-1);
+ RemoveCoordinates('EEG', iSelCoord-3);
+ Digitize.isEditPts = 1;
+ SwitchToNewMode(7);
+ end
+ end
+ end
+
+ %% ===== COORDINATE LIST CLICK CALLBACK =====
+ function CoordListClick_Callback(h, ev)
+ % If single click
+ if (ev.getClickCount() == 1)
+ ctrl = bst_get('PanelControls', 'Digitize');
+ % If contact list rendering is blank in panel then dont't proceed
+ if ctrl.jListCoord.isSelectionEmpty()
+ return;
+ end
+
+ [sCoordName, ~] = GetSelectedCoord();
+ spl = regexp(sCoordName,'\s+','split');
+ nameFinal = spl{1};
+ bst_figures('SetSelectedRows', nameFinal);
+ end
+ end
+end
+
+%% ===== GET SELECTED ELECTRODE =====
+function [sCoordName, iSelCoord] = GetSelectedCoord()
+ % Get panel handles
+ ctrl = bst_get('PanelControls', 'Digitize');
+ if isempty(ctrl)
+ return;
+ end
+
+ % Get JList selected indices
+ iSelCoord = uint16(ctrl.jListCoord.getSelectedIndices())' + 1;
+ listModel = ctrl.jListCoord.getModel();
+ sCoordName = listModel.getElementAt(iSelCoord-1);
end
+%% ===== SWITCH TO 2024 VERSION =====
+function SwitchVersion()
+ % Always confirm this switch.
+ if ~java_dialog('confirm', ['Switch to new (2024) version of the Digitize panel? ', ...
+ 'See Digitize tutorial (Digitize panel > Help menu). ', ...
+ 'This will close the window. Any unsaved points will be lost.'], 'Digitize version')
+ return;
+ end
+ % Close this panel
+ Close_Callback();
+ % Save the preferred version. Must be after closing
+ DigitizeOptions = bst_get('DigitizeOptions');
+ DigitizeOptions.Version = '2024';
+ bst_set('DigitizeOptions', DigitizeOptions);
+end
%% ===== CLOSE =====
function Close_Callback()
@@ -325,7 +552,8 @@ function Close_Callback()
%% ===== HIDING CALLBACK =====
function isAccepted = PanelHidingCallback() %#ok
- global Digitize;
+ global Digitize
+
% If Brainstorm window was hidden before showing the Digitizer
if bst_get('isGUI')
% Get Brainstorm frame
@@ -346,6 +574,17 @@ function Close_Callback()
else
db_reload_studies(iStudy);
end
+ % Close serial connection, to allow switching Digitize version, and to avoid further callbacks if stylus is pressed.
+ if ~isempty(Digitize.SerialConnection)
+ fclose(Digitize.SerialConnection);
+ delete(Digitize.SerialConnection);
+ end
+ % Check for any remaining open connection
+ s = instrfind('status','open');
+ if ~isempty(s)
+ fclose(s);
+ delete(s);
+ end
% Unload everything
bst_memory('UnloadAll', 'Forced');
isAccepted = 1;
@@ -354,51 +593,89 @@ function Close_Callback()
%% ===== EDIT SETTINGS =====
function isOk = EditSettings()
+ global Digitize
+
isOk = 0;
% Get options
DigitizeOptions = bst_get('DigitizeOptions');
+
% Ask for new options
- [res, isCancel] = java_dialog('input', ...
- {'Serial connection settings
Serial port name (COM1):', ...
- 'Unit Type (Fastrak or Patriot):', ...
- ' Collection settings
Digitize MEG HPI coils (0=no, 1=yes):', ...
- 'How many times do you want to collect the three fiducials (NAS,LPA,RPA):', ...
- 'Beep when collecting point (0=no, 1=yes):'}, ...
- 'Digitizer configuration', [], ...
- {DigitizeOptions.ComPort, ...
- DigitizeOptions.UnitType, ...
- num2str(DigitizeOptions.isMEG), ...
- num2str(DigitizeOptions.nFidSets), ...
- num2str(DigitizeOptions.isBeep)});
- if isempty(res) || isCancel
+ options_all = {'Serial connection settings
Serial port name (COM1):', ...
+ DigitizeOptions.ComPort;
+ 'Unit Type (Fastrak or Patriot):', ...
+ DigitizeOptions.UnitType;
+ ' Collection settings
Digitize MEG HPI coils (0=no, 1=yes):', ...
+ num2str(DigitizeOptions.isMEG);
+ 'How many times do you want to collect the three fiducials (NAS,LPA,RPA):', ...
+ num2str(DigitizeOptions.nFidSets);
+ 'Beep when collecting point (0=no, 1=yes):', ...
+ num2str(DigitizeOptions.isBeep)};
+
+ % Options to show for each type
+ switch lower(Digitize.Type)
+ case 'digitize'
+ iOptionsType = [1:5];
+ case '3dscanner'
+ iOptionsType = [3:5];
+ end
+
+ % Ask options
+ [resType, isCancel] = java_dialog('input', options_all(iOptionsType,1), [Digitize.Type ' configuration'], [], options_all(iOptionsType,2));
+ if isempty(resType) || isCancel
return
end
+
+ % Results from options asked to user
+ options_all(iOptionsType, 3) = resType;
+ % Results from options not asked to user
+ iOptionsKeep = setdiff([1:length(options_all)], iOptionsType);
+ options_all(iOptionsKeep, 3) = options_all(iOptionsKeep,2);
+ res = options_all(:,3);
+
% Check values
- if (length(res) < 5) || isempty(res{1}) || isempty(res{2}) || ~ismember(str2double(res{3}), [0 1]) || isnan(str2double(res{4})) || ~ismember(str2double(res{5}), [0 1])
- bst_error('Invalid values.', 'Digitize', 0);
+ if length(resType) < length(iOptionsType) || (ismember(1, iOptionsType) && isempty(res{1})) || ...
+ (ismember(2, iOptionsType) && isempty(res{2})) || ...
+ (ismember(3, iOptionsType) && ~ismember(str2double(res{3}), [0 1])) || ...
+ (ismember(4, iOptionsType) && isnan(str2double(res{4}))) || ...
+ (ismember(5, iOptionsType) && ~ismember(str2double(res{5}), [0 1]))
+ bst_error('Invalid values.', 'Digitize', 0);
return;
end
- % Get entered values
- DigitizeOptions.ComPort = res{1};
- DigitizeOptions.UnitType = lower(res{2});
- DigitizeOptions.isMEG = str2double(res{3});
- DigitizeOptions.nFidSets = str2double(res{4});
- DigitizeOptions.isBeep = str2double(res{5});
-
- if strcmp(DigitizeOptions.UnitType,'fastrak')
+ % Digitizer: COM port
+ DigitizeOptions.ComPort = res{1};
+ % Digitizer: Type
+ DigitizeOptions.UnitType = lower(res{2});
+ % Digitizer: COM properties
+ if isempty(DigitizeOptions.UnitType)
+ % Do nothing
+ elseif strcmp(DigitizeOptions.UnitType,'fastrak')
DigitizeOptions.ComRate = 9600;
DigitizeOptions.ComByteCount = 94;
elseif strcmp(DigitizeOptions.UnitType,'patriot')
DigitizeOptions.ComRate = 115200;
DigitizeOptions.ComByteCount = 120;
else
- bst_error('Incorrect unit type.', 'Digitize', 0);
+ bst_error('Incorrect unit type.', Digitize.Type, 0);
return;
end
+
+ % Common: Digitize MEG HPI coils
+ if ~isempty(res{3})
+ DigitizeOptions.isMEG = str2double(res{3});
+ end
+ % Common: Number of fiducial sets
+ if ~isempty(res{4})
+ DigitizeOptions.nFidSets = str2double(res{4});
+ end
+ % Common: Beep
+ if ~isempty(res{5})
+ DigitizeOptions.isBeep = str2double(res{5});
+ end
% Save values
bst_set('DigitizeOptions', DigitizeOptions);
- %ResetDataCollection();
+ % ResetDataCollection();
+ UpdateList();
isOk = 1;
end
@@ -422,7 +699,8 @@ function SetSimulate(isSimulate) %#ok
%% ===== RESET DATA COLLECTION =====
function ResetDataCollection(isResetSerial)
global Digitize
- bst_progress('start', 'Digitize', 'Initializing...');
+
+ bst_progress('start', Digitize.Type, 'Initializing...');
% Reset serial?
if (nargin == 1) && isequal(isResetSerial, 1)
CreateSerialConnection();
@@ -434,10 +712,11 @@ function ResetDataCollection(isResetSerial)
'nasion', [], ...
'LPA', [], ...
'RPA', [], ...
- 'hpiN', [], ...
- 'hpiL', [], ...
- 'hpiR', [], ...
+ 'hpiN', [], ...
+ 'hpiL', [], ...
+ 'hpiR', [], ...
'EEG', [], ...
+ 'Label', [], ...
'headshape', [], ...
'trans', []);
% Reset counters
@@ -445,10 +724,17 @@ function ResetDataCollection(isResetSerial)
ctrl.jTextFieldEEG.setText(java.lang.String.valueOf(int16(1)));
ctrl.jTextFieldExtra.setText(java.lang.String.valueOf(int16(1)));
end
- % Reset figure
- if isfield(Digitize, 'hFig') && ~isempty(Digitize.hFig) && ishandle(Digitize.hFig)
- %close(Digitize.hFig);
+ % Reset figure (also unloads in global data)
+ if ~isempty(Digitize.hFig) && ishandle(Digitize.hFig)
bst_figures('DeleteFigure', Digitize.hFig, []);
+
+ % For 3D Scanner reload the surface
+ if strcmpi(Digitize.Type, '3DScanner')
+ % load the surface
+ sSurf = bst_memory('LoadSurface', Digitize.surfaceFile);
+ % Display surface
+ view_surface_matrix(sSurf.Vertices, sSurf.Faces, [], sSurf.Color, [], [], Digitize.surfaceFile);
+ end
end
Digitize.iDS = [];
@@ -474,6 +760,7 @@ function ResetDataCollection(isResetSerial)
% - Mode 8 = Headshape
function SwitchToNewMode(mode)
global Digitize
+
% Get controls
ctrl = bst_get('PanelControls', 'Digitize');
% Get options
@@ -494,7 +781,8 @@ function SwitchToNewMode(mode)
ctrl.jButtonLPA.setEnabled(0);
ctrl.jButtonRPA.setEnabled(0);
ctrl.jButtonDeletePoint.setEnabled(0);
- % always switch to next mode to start with the nasion
+ ctrl.jButtonEEGAutoDetectElectrodes.setEnabled(0);
+ % Always switch to next mode to start with the nasion
SwitchToNewMode(1);
else
ctrl.jButtonhpiN.setEnabled(0);
@@ -504,13 +792,15 @@ function SwitchToNewMode(mode)
ctrl.jButtonLPA.setEnabled(1);
ctrl.jButtonRPA.setEnabled(1);
ctrl.jButtonDeletePoint.setEnabled(0);
- % always switch to next mode to start with the nasion
+ ctrl.jButtonEEGAutoDetectElectrodes.setEnabled(0);
+ % Always switch to next mode to start with the nasion
SwitchToNewMode(4);
end
ctrl.jButtonEEGStart.setEnabled(0);
ctrl.jTextFieldEEG.setEnabled(0);
ctrl.jButtonExtraStart.setEnabled(0);
+ ctrl.jButtonRandomHeadPts.setEnabled(0);
ctrl.jTextFieldExtra.setEnabled(0);
@@ -572,6 +862,10 @@ function SwitchToNewMode(mode)
% Shape
case 8
ctrl.jButtonExtraStart.setEnabled(1);
+ if strcmpi(Digitize.Type, '3DScanner')
+ ctrl.jButtonEEGAutoDetectElectrodes.setEnabled(0);
+ ctrl.jButtonRandomHeadPts.setEnabled(1);
+ end
ctrl.jTextFieldExtra.setEnabled(1);
SetSelectedButton(8);
Digitize.Mode = 8;
@@ -581,7 +875,8 @@ function SwitchToNewMode(mode)
%% ===== UPDATE LIST =====
function UpdateList()
- global Digitize;
+ global Digitize
+
% Get controls
ctrl = bst_get('PanelControls', 'Digitize');
% Define the model
@@ -684,12 +979,18 @@ function UpdateList()
ctrl.jListCoord.setModel(listModel);
ctrl.jListCoord.repaint();
drawnow;
- % Scroll down
+ % Scroll to last collected point (non-empty Loc), +1 if more points listed.
lastIndex = min(listModel.getSize(), 12 + nRecEEG + nHeadShape);
- selRect = ctrl.jListCoord.getCellBounds(lastIndex-1, lastIndex-1);
- %ctrl.jListCoord.scrollRectToVisible(selRect);
- ctrl.jListCoord.repaint();
- ctrl.jListCoord.getParent().getParent().repaint();
+ if listModel.getSize() > lastIndex
+ ctrl.jListCoord.ensureIndexIsVisible(lastIndex); % 0-indexed
+ else
+ ctrl.jListCoord.ensureIndexIsVisible(lastIndex-1); % 0-indexed, -1 works even if 0
+ end
+
+ % Update tooltip text for 'Auto' button
+ if strcmpi(Digitize.Type, '3DScanner')
+ ctrl.jButtonEEGAutoDetectElectrodes.setToolTipText(GenerateTooltipTextAuto());
+ end
end
@@ -724,15 +1025,89 @@ function SetSelectedButton(iButton)
end
end
+%% 3DSCANNER: AUTOMATICALLY DETECT AND LABEL EEG CAP ELECTRODES
+function EEGAutoDetectElectrodes(h, ev)
+ global Digitize
+
+ % Add disclaimer to users that 'Auto' feature is experimental
+ if ~java_dialog('confirm', [' Automatic detection of EEG sensors is an experimental feature. ' ...
+ 'Please verify the results carefully.
' ...
+ 'Do you want to continue?'], 'Auto detect EEG electrodes')
+ return
+ end
+ % Get controls
+ ctrl = bst_get('PanelControls', 'Digitize');
+ % Disable Auto button
+ ctrl.jButtonEEGAutoDetectElectrodes.setEnabled(0);
+ % Progress bar
+ bst_progress('start', Digitize.Type, 'Automatic labelling of EEG sensors...');
+
+ % Get current montage
+ curMontage = GetCurrentMontage();
+ isWhiteCap = 0;
+ % For white caps change the color space by inverting the colors
+ % NOTE: only 'Acticap' is the tested white cap (needs work on finding a better aprrooach)
+ if ~isempty(regexp(curMontage.Name, 'ActiCap', 'match'))
+ isWhiteCap = 1;
+ end
+
+ % Get the cap surface from 3D scanner
+ hFig = bst_figures('GetCurrentFigure','3D');
+ TessInfo = getappdata(hFig, 'Surface');
+ sSurf = bst_memory('LoadSurface', TessInfo.SurfaceFile);
+
+ % Automatically find electrodes locations on EEG cap
+ [capCenters2d, capImg2d, surface3dscannerUv] = channel_detect_eegcap_auto('FindElectrodesEegCap', sSurf, isWhiteCap);
+ DigitizeOptions = bst_get('DigitizeOptions');
+ if isempty(DigitizeOptions.Montages(DigitizeOptions.iMontage).ChannelFile)
+ bst_error('EEG cap layout not selected. Go to EEG', Digitize.Type, 1);
+ bst_progress('stop');
+ return;
+ else
+ ChannelMat = in_bst_channel(DigitizeOptions.Montages(DigitizeOptions.iMontage).ChannelFile);
+ end
+
+
+ % Warp points from layout to mesh
+ capPoints3d = channel_detect_eegcap_auto('WarpLayout2Mesh', capCenters2d, capImg2d, surface3dscannerUv, ChannelMat.Channel, Digitize.Points);
+
+ % Plot the electrodes and their labels
+ for i= 1:size(capPoints3d.EEG, 1)
+ % Get current montage
+ [curMontage, nEEG] = GetCurrentMontage();
+ % Find found point in current montage
+ [~, iPoint] = ismember(capPoints3d.Label{i}, [curMontage.Labels]);
+ % Transform coordinate
+ Digitize.Points.EEG(iPoint,:) = capPoints3d.EEG(i, :);
+ % Add the point to the display
+ Digitize.Points.Label{iPoint} = cell2mat(capPoints3d.Label{i});
+ PlotCoordinate(Digitize.Points.EEG(iPoint,:), Digitize.Points.Label{iPoint}, 'EEG', iPoint)
+ % Update text field counter to the next point in the list
+ nextPoint = max(size(Digitize.Points.EEG,1)+1, 1);
+ if nextPoint > nEEG
+ % All EEG points have been collected, switch to next mode
+ ctrl.jTextFieldEEG.setText(java.lang.String.valueOf(int16(nEEG)));
+ SwitchToNewMode(8);
+ else
+ ctrl.jTextFieldEEG.setText(java.lang.String.valueOf(int16(nextPoint)));
+ end
+ end
+
+ UpdateList();
+ % Enable Random button
+ ctrl.jButtonRandomHeadPts.setEnabled(1);
+ bst_progress('stop');
+end
%% ===== MANUAL COLLECT CALLBACK ======
-function ManualCollect_Callback(h, ev)
+function ManualCollect_Callback()
global Digitize
+
% Get Digitize options
DigitizeOptions = bst_get('DigitizeOptions');
% Simulation: call the callback directly
if DigitizeOptions.isSimulate
- BytesAvailable_Callback(h, ev);
+ BytesAvailable_Callback([], []);
% Else: Send a collection request to the Polhemus
else
% User clicked the button, collect a point
@@ -741,14 +1116,97 @@ function ManualCollect_Callback(h, ev)
end
end
+%% ===== COLLECT RANDOM HEADPOINTS =====
+function CollectRandomHeadPts_Callback(h, ev)
+ global Digitize;
+ % Get controls
+ ctrl = bst_get('PanelControls', 'Digitize');
+ % Disable Random button
+ ctrl.jButtonRandomHeadPts.setEnabled(0);
+ % Progress bar
+ bst_progress('start', Digitize.Type, 'Plotting 150 random head shape points...');
+
+ hFig = bst_figures('GetCurrentFigure','3D');
+ TessInfo = getappdata(hFig, 'Surface');
+ TessMat = bst_memory('LoadSurface', TessInfo.SurfaceFile);
+
+ % Brainstorm recommends to collect approximately 100-150 points from the head
+ % 5-10 points from the boney part of the nose
+ PlotHeadShapePoints(TessMat.Vertices, 'nose', 10);
+ % 10-20 points across the left eyebrow
+ PlotHeadShapePoints(TessMat.Vertices, 'leyebrow', 20);
+ % 10-20 points across the right eyebrow
+ PlotHeadShapePoints(TessMat.Vertices, 'reyebrow', 20);
+ % 100 points on the scalp
+ PlotHeadShapePoints(TessMat.Vertices, 'scalp', 100);
+
+ UpdateList();
+ bst_progress('stop');
+end
+
+%% ===== PLOT HEAD SHAPE POINTS =====
+function PlotHeadShapePoints(Vertices, plotRegion, nPoints)
+ global Digitize
+ % Get controls
+ ctrl = bst_get('PanelControls', 'Digitize');
+
+ % Get the plotting parameters based on the region in the head
+ switch plotRegion
+ case 'nose'
+ nosePoint = Digitize.Points.nasion;
+ % Get 600 nearest points to the 'nosePoint' and choose 'nPoints' from it
+ nearPointsIdx = bst_nearest(Vertices, nosePoint, 600, 0, []);
+ range = length(nearPointsIdx);
+ stepFactor = range/nPoints;
+ case 'leyebrow'
+ lEyebrowPoint = (1.25 * Digitize.Points.nasion) + (0.5 * Digitize.Points.LPA);
+ % Get 400 nearest points to the 'lEyebrowPoint' and choose 'nPoints' from it
+ nearPointsIdx = bst_nearest(Vertices, lEyebrowPoint, 400, 0, []);
+ range = length(nearPointsIdx);
+ stepFactor = range/nPoints;
+ case 'reyebrow'
+ rEyebrowPoint = (1.25 * Digitize.Points.nasion) + (0.5 * Digitize.Points.RPA);
+ % Get 400 nearest points to the 'rEyebrowPoint' and choose 'nPoints' from it
+ nearPointsIdx = bst_nearest(Vertices, rEyebrowPoint, 400, 0, []);
+ range = length(nearPointsIdx);
+ stepFactor = range/nPoints;
+ case 'scalp'
+ range = length(Vertices);
+ stepFactor = ceil(range/nPoints);
+ otherwise
+ bst_error([plotRegion 'is invalid'], 'Plot Head Shape Points', 0);
+ bst_progress(stop);
+ return
+ end
+
+ % Plot the head shape points
+ for i= 1:stepFactor:range
+ if strcmpi(plotRegion, 'scalp')
+ pointCoord = Vertices(i, :);
+ else
+ pointCoord = Vertices(nearPointsIdx(i), :);
+ end
+ % Find the index for the current point in the headshape points
+ iPoint = str2double(ctrl.jTextFieldExtra.getText());
+ % Transformed points_pen from original points_pen
+ Digitize.Points.headshape(iPoint,:) = pointCoord;
+ % Add the point to the display (in cm)
+ PlotCoordinate(Digitize.Points.headshape(iPoint,:), 'EXTRA', 'EXTRA', iPoint)
+ % Update text field counter to the next point in the list
+ nextPoint = iPoint+1;
+ ctrl.jTextFieldExtra.setText(java.lang.String.valueOf(int16(nextPoint)));
+ end
+end
+
%% ===== DELETE POINT CALLBACK =====
function DeletePoint_Callback(h, ev) %#ok
global Digitize
+
% Get controls
ctrl = bst_get('PanelControls', 'Digitize');
DigitizeOptions = bst_get('DigitizeOptions');
- % only remove cardinal points when MEG coils are used for the
+ % Only remove cardinal points when MEG coils are used for the
% transformation.
if ismember(Digitize.Mode, [4 5 6]) && DigitizeOptions.isMEG
% Remove the last cardinal point that was collected
@@ -761,30 +1219,30 @@ function DeletePoint_Callback(h, ev) %#ok
end
if Digitize.Mode == 7
- % find the last EEG point collected
+ % Find the last EEG point collected
iPoint = str2double(ctrl.jTextFieldEEG.getText()) - 1;
if iPoint == 0
- % if no EEG points are collected, delete the last cardinal point
+ % If no EEG points are collected, delete the last cardinal point
iPoint = size(Digitize.Points.nasion,1);
coordInd = (iPoint-1)*3;
point_type = 'cardinal';
else
- % delete last EEG point
+ % Delete last EEG point
point_type = 'eeg';
end
elseif Digitize.Mode == 8
- % headshape point
+ % Headshape point
iPoint = str2double(ctrl.jTextFieldExtra.getText()) - 1;
if iPoint == 0
% If no headpoints are collected:
[tmp, nEEG] = GetCurrentMontage();
if nEEG > 0
- % check for EEG, then delete the last point
+ % Check for EEG, then delete the last point
iPoint = str2double(ctrl.jTextFieldEEG.getText());
point_type = 'eeg';
else
- % delete the last cardinal point
+ % Delete the last cardinal point
iPoint = size(Digitize.Points.nasion,1);
coordInd = (iPoint-1)*3;
point_type = 'cardinal';
@@ -818,6 +1276,7 @@ function DeletePoint_Callback(h, ev) %#ok
end
case 'eeg'
Digitize.Points.EEG(iPoint,:) = [];
+ Digitize.Points.Label{iPoint} = {};
RemoveCoordinates('EEG', iPoint);
ctrl.jTextFieldEEG.setText(java.lang.String.valueOf(int16(iPoint)));
SwitchToNewMode(7)
@@ -828,25 +1287,23 @@ function DeletePoint_Callback(h, ev) %#ok
SwitchToNewMode(8)
end
-
-
% Update coordinates list
UpdateList();
end
-
%% ===== COMPUTE TRANFORMATION =====
function ComputeTransform()
global Digitize
+
% Get controls
ctrl = bst_get('PanelControls', 'Digitize');
% Get options
DigitizeOptions = bst_get('DigitizeOptions');
- % if MEG coils are used, these will determine the coodinate system
+ % If MEG coils are used, these will determine the coodinate system
if DigitizeOptions.isMEG
- % find the difference between the first two collections to determine error
+ % Find the difference between the first two collections to determine error
if (size(Digitize.Points.hpiN, 1) > 1)
diffPoint = Digitize.Points.hpiN(1,:) - Digitize.Points.hpiN(2,:);
normPoint(1) = sum(sqrt(diffPoint(1)^2+diffPoint(2)^2+diffPoint(3)^2));
@@ -858,14 +1315,13 @@ function ComputeTransform()
ctrl.jLabelCoilMessage.setText('Difference error exceeds 5 mm');
end
end
- % find the average across collections to compute the transformation
+ % Find the average across collections to compute the transformation
na = mean(Digitize.Points.hpiN,1); % these values are in meters
la = mean(Digitize.Points.hpiL,1);
ra = mean(Digitize.Points.hpiR,1);
else
- % if only EEG is used, the cardinal points will determine the
- % coordinate system
+ % If only EEG is used, the cardinal points will determine the coordinate system
% find the difference between the first two collections to determine error
if (size(Digitize.Points.nasion, 1) > 1)
@@ -949,7 +1405,8 @@ function ComputeTransform()
%% ===== CREATE FIGURE =====
function CreateHeadpointsFigure()
- global Digitize
+ global Digitize
+
if isempty(Digitize.hFig) || ~ishandle(Digitize.hFig) || isempty(Digitize.iDS)
% Get study
sStudy = bst_get('StudyWithCondition', [Digitize.SubjectName '/' Digitize.ConditionName]);
@@ -958,10 +1415,10 @@ function CreateHeadpointsFigure()
% Hide head surface
panel_surface('SetSurfaceTransparency', hFig, 1, 0.8);
% Get Digitizer JFrame
- bstContainer = get(bst_get('Panel','Digitize'), 'container');
+ bstContainer = get(bst_get('Panel', 'Digitize'), 'container');
% Get maximum figure position
decorationSize = bst_get('DecorationSize');
- [jBstArea, FigArea] = gui_layout('GetScreenBrainstormAreas', bstContainer.handle{1});
+ [~, FigArea] = gui_layout('GetScreenBrainstormAreas', bstContainer.handle{1});
FigPos = FigArea(1,:) + [decorationSize(1), decorationSize(4), - decorationSize(1) - decorationSize(3), - decorationSize(2) - decorationSize(4)];
if (FigPos(3) > 0) && (FigPos(4) > 0)
set(hFig, 'Position', FigPos);
@@ -971,12 +1428,56 @@ function CreateHeadpointsFigure()
% Save handles in global variable
Digitize.hFig = hFig;
Digitize.iDS = iDS;
- end
+ else
+ % Hide figure
+ set(Digitize.hFig, 'Visible', 'off');
+ % Get study
+ sStudy = bst_get('StudyWithCondition', [Digitize.SubjectName '/' Digitize.ConditionName]);
+ % Plot head points
+ [hFig, iDS] = view_headpoints(file_fullpath(sStudy.Channel.FileName));
+ % Get the surface
+ sSurf = bst_memory('LoadSurface', Digitize.surfaceFile);
+ % Apply the transformation
+ sSurf.Vertices = (Digitize.Points.trans * [sSurf.Vertices ones(size(sSurf.Vertices, 1),1)]')';
+ % Remove the surface
+ panel_surface('RemoveSurface', hFig, 1);
+ % Deface the surface
+ if isempty(regexp(sSurf.Comment, 'defaced', 'match'))
+ sSurf = tess_deface(sSurf);
+ end
+ % Save the surface and update the node
+ ProtocolInfo = bst_get('ProtocolInfo');
+ surfaceFile = bst_fullfile(ProtocolInfo.SUBJECTS, Digitize.surfaceFile);
+ bst_save(surfaceFile, sSurf, 'v7');
+ [~, iSubject] = bst_get('Subject', Digitize.SubjectName);
+ db_reload_subjects(iSubject);
+ % Hide head surface
+ if ~strcmpi(Digitize.Type, '3DScanner')
+ panel_surface('SetSurfaceTransparency', hFig, 1, 0.8);
+ end
+ % Get Digitizer JFrame
+ bstContainer = get(bst_get('Panel', 'Digitize'), 'container');
+ % Get maximum figure position
+ decorationSize = bst_get('DecorationSize');
+ [~, FigArea] = gui_layout('GetScreenBrainstormAreas', bstContainer.handle{1});
+ FigPos = FigArea(1,:) + [decorationSize(1), decorationSize(4), - decorationSize(1) - decorationSize(3), - decorationSize(2) - decorationSize(4)];
+ % Display updated surface
+ view_surface_matrix(sSurf.Vertices, sSurf.Faces, [], sSurf.Color, hFig, [], Digitize.surfaceFile);
+ if (FigPos(3) > 0) && (FigPos(4) > 0)
+ set(hFig, 'Position', FigPos);
+ end
+ % Remove the close handle function
+ set(hFig, 'CloseRequestFcn', []);
+ % Save handles in global variable
+ Digitize.hFig = hFig;
+ Digitize.iDS = iDS;
+ end
end
%% ===== PLOT POINTS =====
function PlotCoordinate(Loc, Label, Type, iPoint)
global Digitize GlobalData
+
sStudy = bst_get('StudyWithCondition', [Digitize.SubjectName '/' Digitize.ConditionName]);
ChannelFile = file_fullpath(sStudy.Channel.FileName);
ChannelMat = load(ChannelFile);
@@ -984,7 +1485,7 @@ function PlotCoordinate(Loc, Label, Type, iPoint)
% Add EEG sensor locations to channel stucture
if strcmp(Type, 'EEG')
if isempty(ChannelMat.Channel)
- % first point in the list
+ % First point in the list
ChannelMat.Channel = db_template('channeldesc');
end
ChannelMat.Channel(iPoint).Name = Label;
@@ -1010,19 +1511,22 @@ function PlotCoordinate(Loc, Label, Type, iPoint)
figure_3d('ViewHeadPoints', Digitize.hFig, 1);
figure_3d('ViewSensors',Digitize.hFig, 1, 1, 0,'EEG');
% Hide head surface
- panel_surface('SetSurfaceTransparency', Digitize.hFig, 1, 1);
+ if ~strcmpi(Digitize.Type, '3DScanner')
+ panel_surface('SetSurfaceTransparency', Digitize.hFig, 1, 1);
+ end
end
%% ===== EEG CHANGE POINT CALLBACK =====
function EEGChangePoint_Callback(h, ev) %#ok
global Digitize
+
% Get controls
ctrl = bst_get('PanelControls', 'Digitize');
% Get digitize options
DigitizeOptions = bst_get('DigitizeOptions');
initPoint = str2num(ctrl.jTextFieldEEG.getText());
- % restrict to a maximum of points collected or defined max points and minimum of '1'
+ % Restrict to a maximum of points collected or defined max points and minimum of '1'
[curMontage, nEEG] = GetCurrentMontage();
newPoint = max(min(initPoint, min(length(Digitize.Points.EEG)+1, nEEG)), 1);
ctrl.jTextFieldEEG.setText(java.lang.String.valueOf(int16(newPoint)));
@@ -1031,11 +1535,12 @@ function EEGChangePoint_Callback(h, ev) %#ok
%% ===== EXTRA CHANGE POINT CALLBACK =====
function ExtraChangePoint_Callback(h, ev) %#ok
global Digitize
+
% Get controls
ctrl = bst_get('PanelControls', 'Digitize');
initPoint = str2num(ctrl.jTextFieldExtra.getText()); %#ok<*ST2NM>
- % restrict to a maximum of points collected and minimum of '1'
+ % Restrict to a maximum of points collected and minimum of '1'
newPoint = max(min(initPoint, length(Digitize.Points.headshape)+1), 1);
ctrl.jTextFieldExtra.setText(java.lang.String.valueOf(int16(newPoint)));
end
@@ -1043,6 +1548,7 @@ function ExtraChangePoint_Callback(h, ev) %#ok
%% ===== SAVE CALLBACK =====
function Save_Callback(h, ev) %#ok
global Digitize
+
sStudy = bst_get('StudyWithCondition', [Digitize.SubjectName '/' Digitize.ConditionName]);
ChannelFile = file_fullpath(sStudy.Channel.FileName);
export_channel( ChannelFile );
@@ -1050,6 +1556,9 @@ function Save_Callback(h, ev) %#ok
%% ===== CREATE MONTAGE MENU =====
function CreateMontageMenu(jMenu)
+ import org.brainstorm.icon.*;
+ global Digitize
+
% Get menu pointer if not in argument
if (nargin < 1) || isempty(jMenu)
ctrl = bst_get('PanelControls', 'Digitize');
@@ -1073,13 +1582,24 @@ function CreateMontageMenu(jMenu)
end
% Add new montage / reset list
jMenu.addSeparator();
- gui_component('MenuItem', jMenu, [], 'Add EEG montage...', [], [], @(h,ev)bst_call(@AddMontage), []);
+
+ if strcmpi(Digitize.Type, '3DScanner')
+ jMenuAddMontage = gui_component('Menu', jMenu, [], 'Add EEG montage...', [], [], [], []);
+ gui_component('MenuItem', jMenuAddMontage, [], 'From file...', [], [], @(h,ev)bst_call(@AddMontage), []);
+ % Creating montages from EEG cap layout mat files (only for 3DScanner)
+ jMenuEegCaps = gui_component('Menu', jMenuAddMontage, [], 'From default EEG cap', IconLoader.ICON_CHANNEL, [], [], []);
+ % Use default channel file
+ menu_default_eegcaps(jMenuEegCaps);
+ else % if not 3DScanner
+ gui_component('MenuItem', jMenu, [], 'Add EEG montage...', [], [], @(h,ev)bst_call(@AddMontage), []);
+ end
gui_component('MenuItem', jMenu, [], 'Unload all montages', [], [], @(h,ev)bst_call(@UnloadAllMontages), []);
end
-
%% ===== SELECT MONTAGE =====
function SelectMontage(iMontage)
+ global Digitize
+
% Get Digitize options
DigitizeOptions = bst_get('DigitizeOptions');
% Default montage: ask for number of channels
@@ -1118,6 +1638,20 @@ function SelectMontage(iMontage)
ResetDataCollection();
end
+%% ===== TOOLTIP TEXT FOR AUTO BUTTON =====
+function autoButtonTooltip = GenerateTooltipTextAuto()
+ % Get current montage
+ curMontage = GetCurrentMontage();
+ % Get cap landmark labels for selected montage
+ eegCapLandmarkLabels = channel_detect_eegcap_auto('GetEegCapLandmarkLabels', curMontage.Name);
+ autoButtonTooltip = 'Auto localization of EEG sensor is not suported for this cap.';
+ if ~isempty(eegCapLandmarkLabels)
+ strSensors = sprintf('%s, ',eegCapLandmarkLabels{:});
+ strSensors = strSensors(1:end-2);
+ autoButtonTooltip = ['Set at least sensors: [' strSensors '] to enable.'];
+ end
+end
+
%% ===== GET CURRENT MONTAGE =====
function [curMontage, nEEG] = GetCurrentMontage()
% Get Digitize options
@@ -1128,46 +1662,69 @@ function SelectMontage(iMontage)
end
%% ===== ADD EEG MONTAGE =====
-function AddMontage()
- % Get recently used folders
- LastUsedDirs = bst_get('LastUsedDirs');
- % Open file
- MontageFile = java_getfile('open', 'Select montage file...', LastUsedDirs.ImportChannel, 'single', 'files', ...
- {{'*.txt'}, 'Text files', 'TXT'}, 0);
- if isempty(MontageFile)
- return;
- end
- % Get filename
- [MontageDir, MontageName] = bst_fileparts(MontageFile);
- % Intialize new montage
- newMontage.Name = MontageName;
- newMontage.Labels = {};
-
- % Open file
- fid = fopen(MontageFile,'r');
- if (fid == -1)
- error('Cannot open file.');
- end
- % Read file
- while (1)
- tline = fgetl(fid);
- if ~ischar(tline)
- break;
+function AddMontage(ChannelFile)
+ global Digitize
+
+ % Add Montage from text file
+ if nargin<1
+ % Get recently used folders
+ LastUsedDirs = bst_get('LastUsedDirs');
+ % Open file
+ MontageFile = java_getfile('open', 'Select montage file...', LastUsedDirs.ImportChannel, 'single', 'files', ...
+ {{'*.txt'}, 'Text files', 'TXT'}, 0);
+ if isempty(MontageFile)
+ return;
end
- spl = regexp(tline,'\s+','split');
- if (length(spl) >= 2)
- newMontage.Labels{end+1} = spl{2};
+ % Get filename
+ [MontageDir, MontageName] = bst_fileparts(MontageFile);
+ % Intialize new montage
+ newMontage.Name = MontageName;
+ newMontage.Labels = {};
+
+ % Open file
+ fid = fopen(MontageFile,'r');
+ if (fid == -1)
+ error('Cannot open file.');
end
+ % Read file
+ while (1)
+ tline = fgetl(fid);
+ if ~ischar(tline)
+ break;
+ end
+ spl = regexp(tline,'\s+','split');
+ if (length(spl) >= 2)
+ newMontage.Labels{end+1} = spl{2};
+ end
+ end
+ % Close file
+ fclose(fid);
+ % If no labels were read: exit
+ if isempty(newMontage.Labels)
+ return
+ end
+ % Save last dir
+ LastUsedDirs.ImportChannel = MontageDir;
+ bst_set('LastUsedDirs', LastUsedDirs);
+ % Add Montage from mat file of EEG caps
+ else
+ % Load existing file
+ ChannelMat = in_bst_channel(ChannelFile);
+
+ % Intialize new montage
+ newMontage.Name = ChannelMat.Comment;
+ newMontage.Labels = {};
+ newMontage.ChannelFile = ChannelFile;
+
+ % Get cap landmark labels
+ eegCapLandmarkLabels = channel_detect_eegcap_auto('GetEegCapLandmarkLabels', newMontage.Name);
+
+ % Sort as per the initialization landmark labels of EEG Cap
+ nonLandmarkLabelsIdx = find(~ismember({ChannelMat.Channel.Name},eegCapLandmarkLabels));
+ allLabels = {ChannelMat.Channel.Name};
+ newMontage.Labels = cat(2, eegCapLandmarkLabels, allLabels(nonLandmarkLabelsIdx));
end
- % Close file
- fclose(fid);
- % If no labels were read: exit
- if isempty(newMontage.Labels)
- return
- end
- % Save last dir
- LastUsedDirs.ImportChannel = MontageDir;
- bst_set('LastUsedDirs', LastUsedDirs);
+
% Get Digitize options
DigitizeOptions = bst_get('DigitizeOptions');
@@ -1187,8 +1744,13 @@ function AddMontage()
bst_set('DigitizeOptions', DigitizeOptions);
% Reload Menu
CreateMontageMenu();
- % Restart acquisition
- ResetDataCollection();
+ if nargin<1
+ % Restart acquisition
+ ResetDataCollection();
+ else
+ % Update List
+ UpdateList();
+ end
end
%% ===== UNLOAD ALL MONTAGES =====
@@ -1198,15 +1760,19 @@ function UnloadAllMontages()
% Remove all montages
DigitizeOptions.Montages = [...
struct('Name', 'No EEG', ...
- 'Labels', []), ...
+ 'Labels', [], ...
+ 'ChannelFile', []), ...
struct('Name', 'Default', ...
- 'Labels', [])];
+ 'Labels', [], ...
+ 'ChannelFile', [])];
% Reset to "No EEG"
DigitizeOptions.iMontage = 1;
% Save Digitize options
bst_set('DigitizeOptions', DigitizeOptions);
% Reload menu bar
CreateMontageMenu();
+ % Update List
+ UpdateList();
end
@@ -1221,37 +1787,42 @@ function RemoveCoordinates(type, iPoint)
ChannelFile = file_fullpath(sStudy.Channel.FileName);
ChannelMat = load(ChannelFile);
if isempty(type)
- % remove all points
+ % Remove all points
ChannelMat.HeadPoints = [];
ChannelMat.Channel = [];
% Save file back
save(ChannelFile, '-struct', 'ChannelMat');
- % close the figure
+ % Close the figure
if ishandle(Digitize.hFig)
- %close(Digitize.hFig);
+ % close(Digitize.hFig);
bst_figures('DeleteFigure', Digitize.hFig, []);
end
else
% find group and remove selected point
if isempty(iPoint)
- % create a mask of points to keep that exclude the specified
+ % Create a mask of points to keep that exclude the specified
% type
mask = cellfun(@isempty,regexp([ChannelMat.HeadPoints.Type], type));
ChannelMat.HeadPoints.Loc = ChannelMat.HeadPoints.Loc(:,mask);
ChannelMat.HeadPoints.Label = ChannelMat.HeadPoints.Label(mask);
ChannelMat.HeadPoints.Type = ChannelMat.HeadPoints.Type(mask);
if strcmp(type, 'EEG')
- % remove all EEG channels from channel struct
+ % Remove all EEG channels from channel struct
ChannelMat.Channel = [];
end
else
- % find the point in the type and create a mask of points to keep
+ % Find the point in the type and create a mask of points to keep
% that excludes the specified point
if strcmp(type, 'EEG')
- % remove specific EEG channel
- ChannelMat.Channel(iPoint) = [];
+ if strcmpi(Digitize.Type, '3DScanner')
+ % Remove only location
+ ChannelMat.Channel(iPoint).Loc = [];
+ else
+ % Remove specific EEG channel
+ ChannelMat.Channel(iPoint) = [];
+ end
else
- % all other types
+ % All other types
iType = find(~cellfun(@isempty,regexp([ChannelMat.HeadPoints.Type], type)));
mask = true(1,size(ChannelMat.HeadPoints.Type,2));
iDelete = iType(iPoint);
@@ -1262,7 +1833,7 @@ function RemoveCoordinates(type, iPoint)
end
end
- % save changes
+ % Save changes
save(ChannelFile, '-struct', 'ChannelMat');
GlobalData.DataSet(Digitize.iDS).HeadPoints = ChannelMat.HeadPoints;
GlobalData.DataSet(Digitize.iDS).Channel = ChannelMat.Channel;
@@ -1277,7 +1848,7 @@ function RemoveCoordinates(type, iPoint)
% View headpoints
figure_3d('ViewHeadPoints', Digitize.hFig, 1);
- % manually remove any remaining EEG markers if the channel file is
+ % Manually remove any remaining EEG markers if the channel file is
% empty.
if isempty(ChannelMat.Channel)
hSensorMarkers = findobj(hAxes, 'Tag', 'SensorsMarkers');
@@ -1286,7 +1857,7 @@ function RemoveCoordinates(type, iPoint)
delete(hSensorLabels);
end
- % view EEG sensors
+ % View EEG sensors
figure_3d('ViewSensors',Digitize.hFig, 1, 1, 0,'EEG');
end
end
@@ -1299,6 +1870,7 @@ function RemoveCoordinates(type, iPoint)
%% ===== CREATE SERIAL COLLECTION =====
function isOk = CreateSerialConnection(h, ev) %#ok
global Digitize
+
isOk = 0;
while ~isOk
% Get COM port options
@@ -1318,7 +1890,7 @@ function RemoveCoordinates(type, iPoint)
if strcmp(DigitizeOptions.UnitType,'patriot')
SerialConnection.terminator = 'CR';
end
- % set up the Bytes Available function and open the connection (if needed)
+ % Set up the Bytes Available function and open the connection (if needed)
SerialConnection.BytesAvailableFcnCount = DigitizeOptions.ComByteCount;
SerialConnection.BytesAvailableFcnMode = 'byte';
SerialConnection.BytesAvailableFcn = @BytesAvailable_Callback;
@@ -1340,7 +1912,7 @@ function RemoveCoordinates(type, iPoint)
pause(0.2);
catch %#ok
% If the connection cannot be established: error message
- bst_error(['Cannot open serial connection.' 10 10 'Please check the serial port configuration.' 10], 'Digitize', 0);
+ bst_error(['Cannot open serial connection.' 10 10 'Please check the serial port configuration.' 10], Digitize.Type, 0);
% Ask user to edit the port options
isChanged = EditSettings();
% If edit was canceled: exit
@@ -1361,8 +1933,9 @@ function RemoveCoordinates(type, iPoint)
%% ===== BYTES AVAILABLE CALLBACK =====
-function BytesAvailable_Callback(h, ev) %#ok
+function BytesAvailable_Callback(h, ev)
global Digitize rawpoints
+
% Get controls
ctrl = bst_get('PanelControls', 'Digitize');
% Get digitizer options
@@ -1370,14 +1943,34 @@ function BytesAvailable_Callback(h, ev) %#ok
% Simulate: Generate random points
if DigitizeOptions.isSimulate
- switch (Digitize.Mode)
- case 1, pointCoord = [.08 0 -.01];
- case 2, pointCoord = [-.01 .07 0];
- case 3, pointCoord = [-.01 -.07 0];
- case 4, pointCoord = [.08 0 0];
- case 5, pointCoord = [0 .07 0];
- case 6, pointCoord = [0 -.07 0];
- otherwise, pointCoord = rand(1,3) * .15 - .075;
+ if strcmpi(Digitize.Type, '3DScanner')
+ % Get current 3D figure
+ [hFig,~,iDS] = bst_figures('GetCurrentFigure', '3D');
+ if isempty(hFig)
+ return
+ end
+ % Get current selected point
+ CoordinatesSelector = getappdata(hFig, 'CoordinatesSelector');
+ isSelectingCoordinates = getappdata(hFig, 'isSelectingCoordinates');
+ if isempty(CoordinatesSelector) || isempty(CoordinatesSelector.MRI)
+ return;
+ else
+ if isSelectingCoordinates
+ pointCoord = CoordinatesSelector.SCS;
+ end
+ end
+ Digitize.hFig = hFig;
+ Digitize.iDS = iDS;
+ else
+ switch (Digitize.Mode)
+ case 1, pointCoord = [.08 0 -.01];
+ case 2, pointCoord = [-.01 .07 0];
+ case 3, pointCoord = [-.01 -.07 0];
+ case 4, pointCoord = [.08 0 0];
+ case 5, pointCoord = [0 .07 0];
+ case 6, pointCoord = [0 -.07 0];
+ otherwise, pointCoord = rand(1,3) * .15 - .075;
+ end
end
% Else: Get digitized point coordinates
else
@@ -1424,16 +2017,10 @@ function BytesAvailable_Callback(h, ev) %#ok
end
% Beep at each click AND not for headshape points
if DigitizeOptions.isBeep
- % Beep not working in compiled version, replacing with this:
- if bst_iscompiled() && (Digitize.Mode ~= 8)
- sound(Digitize.BeepWav(6000:2:16000,1), 22000);
- else
- beep on;
- beep();
- end
+ sound(Digitize.BeepWav.data, Digitize.BeepWav.fs);
end
- % check for change in Mode (click at least 1 meter away from transmitter)
- if any(abs(pointCoord) > 1.5)
+ % Check for change in Mode (click at least 1 meter away from transmitter)
+ if ~strcmpi(Digitize.Type, '3DScanner') && any(abs(pointCoord) > 1.5)
newMode = Digitize.Mode +1;
SwitchToNewMode(newMode);
return;
@@ -1452,7 +2039,7 @@ function BytesAvailable_Callback(h, ev) %#ok
Digitize.Points.hpiN(iPoint,:) = (Digitize.Points.trans * [pointCoord 1]')';
PlotCoordinate(Digitize.Points.hpiN(iPoint,:), 'HPI-N', 'HPI', iPoint);
else
- % used to compute transform
+ % Used to compute transform
Digitize.Points.hpiN(iPoint,:) = pointCoord;
end
SwitchToNewMode(2);
@@ -1464,7 +2051,7 @@ function BytesAvailable_Callback(h, ev) %#ok
Digitize.Points.hpiL(iPoint,:) = (Digitize.Points.trans * [pointCoord 1]')';
PlotCoordinate(Digitize.Points.hpiL(iPoint,:), 'HPI-L', 'HPI', iPoint);
else
- % used to compute transform
+ % Used to compute transform
Digitize.Points.hpiL(iPoint,:) = pointCoord;
end
SwitchToNewMode(3);
@@ -1476,7 +2063,7 @@ function BytesAvailable_Callback(h, ev) %#ok
Digitize.Points.hpiR(iPoint,:) = (Digitize.Points.trans * [pointCoord 1]')';
PlotCoordinate(Digitize.Points.hpiR(iPoint,:), 'HPI-R', 'HPI', iPoint);
else
- % used to compute transform
+ % Used to compute transform
Digitize.Points.hpiR(iPoint,:) = pointCoord;
end
% Check for multiple fiducial measurements
@@ -1547,43 +2134,75 @@ function BytesAvailable_Callback(h, ev) %#ok
% === EEG ===
case 7
- % find the index for the current point
- iPoint = str2double(ctrl.jTextFieldEEG.getText());
- % Transform coordinate
- Digitize.Points.EEG(iPoint,:) = (Digitize.Points.trans * [pointCoord 1]')';
+ % Find the index for the current point
+ % ADD A CONDITION HERE THAT EDITS EXISTING EEG POINTS
+ if Digitize.isEditPts
+ [~, iSelCoord] = GetSelectedCoord();
+ iPoint = iSelCoord - 3;
+ else
+ % ELSE DOES THE BELOW OF ADDING NEW POINTS
+ iPoint = str2double(ctrl.jTextFieldEEG.getText());
+ end
+ if strcmpi(Digitize.Type, '3DScanner')
+ Digitize.Points.EEG(iPoint,:) = pointCoord;
+ else
+ % Transform coordinate
+ Digitize.Points.EEG(iPoint,:) = (Digitize.Points.trans * [pointCoord 1]')';
+ end
% Add the point to the display
% Get current montage
[curMontage, nEEG] = GetCurrentMontage();
- PlotCoordinate(Digitize.Points.EEG(iPoint,:), curMontage.Labels{iPoint}, 'EEG', iPoint)
- % update text field counter to the next point in the list
- nextPoint = max(size(Digitize.Points.EEG,1)+1, 1);
- if nextPoint > nEEG
- % all EEG points have been collected, switch to next mode
- ctrl.jTextFieldEEG.setText(java.lang.String.valueOf(int16(nEEG)));
- SwitchToNewMode(8);
+ Digitize.Points.Label{iPoint} = curMontage.Labels{iPoint};
+ PlotCoordinate(Digitize.Points.EEG(iPoint,:), Digitize.Points.Label{iPoint}, 'EEG', iPoint)
+
+ if Digitize.isEditPts
+ Digitize.isEditPts = 0;
else
- ctrl.jTextFieldEEG.setText(java.lang.String.valueOf(int16(nextPoint)));
+ % Update text field counter to the next point in the list
+ nextPoint = max(size(Digitize.Points.EEG,1)+1, 1);
+ if nextPoint > nEEG
+ % All EEG points have been collected, switch to next mode
+ ctrl.jTextFieldEEG.setText(java.lang.String.valueOf(int16(nEEG)));
+ SwitchToNewMode(8);
+ else
+ ctrl.jTextFieldEEG.setText(java.lang.String.valueOf(int16(nextPoint)));
+ end
end
% === EXTRA ===
case 8
- % find the index for the current point in the headshape points
+ % Find the index for the current point in the headshape points
iPoint = str2double(ctrl.jTextFieldExtra.getText());
- % Transformed points_pen from original points_pen
- Digitize.Points.headshape(iPoint,:) = (Digitize.Points.trans * [pointCoord 1]')';
- % add the point to the display (in cm)
+ if strcmpi(Digitize.Type, '3DScanner')
+ Digitize.Points.headshape(iPoint,:) = pointCoord;
+ else
+ % Transformed points_pen from original points_pen
+ Digitize.Points.headshape(iPoint,:) = (Digitize.Points.trans * [pointCoord 1]')';
+ end
+ % Add the point to the display (in cm)
PlotCoordinate(Digitize.Points.headshape(iPoint,:), 'EXTRA', 'EXTRA', iPoint)
- % update text field counter to the next point in the list
+ % Update text field counter to the next point in the list
nextPoint = iPoint+1;
ctrl.jTextFieldExtra.setText(java.lang.String.valueOf(int16(nextPoint)));
end
% Update coordinates list
UpdateList();
+ % Enable 'Auto' button IFF all landmark fiducials have been acquired
+ if strcmpi(Digitize.Type, '3DScanner') && (Digitize.Mode ~= 8)
+ curMontage = GetCurrentMontage();
+ eegCapLandmarkLabels = channel_detect_eegcap_auto('GetEegCapLandmarkLabels', curMontage.Name);
+ if ~isempty(eegCapLandmarkLabels) && ~isempty(Digitize.Points.EEG)
+ acqPointLabels = Digitize.Points.Label(1 : size(Digitize.Points.EEG, 1));
+ if all(ismember([eegCapLandmarkLabels], acqPointLabels))
+ ctrl.jButtonEEGAutoDetectElectrodes.setEnabled(1);
+ end
+ end
+ end
end
%% ===== MOTION COMPENSATION =====
function newPT = DoMotionCompensation(sensors)
- % use sensor one and its orientation vectors as the new coordinate system
+ % Use sensor one and its orientation vectors as the new coordinate system
% Define the origin as the position of sensor attached to the glasses.
WAND = 1;
REMOTE1 = 2;
@@ -1624,7 +2243,7 @@ function BytesAvailable_Callback(h, ev) %#ok
rotMat(4, 1:4) = 0;
- %Translate and rotate the WAND into new coordinate system
+ % Translate and rotate the WAND into new coordinate system
pt(1) = sensors(WAND,2) - C(1);
pt(2) = sensors(WAND,3) - C(2);
pt(3) = sensors(WAND,4) - C(3);
@@ -1634,6 +2253,3 @@ function BytesAvailable_Callback(h, ev) %#ok
newPT(3) = pt(1) * rotMat(3, 1) + pt(2) * rotMat(3, 2) + pt(3) * rotMat(3, 3)'+ rotMat(3, 4);
end
-
-
-
diff --git a/toolbox/sensors/panel_digitize_2024.m b/toolbox/sensors/panel_digitize_2024.m
new file mode 100644
index 000000000..26db389b0
--- /dev/null
+++ b/toolbox/sensors/panel_digitize_2024.m
@@ -0,0 +1,1735 @@
+function varargout = panel_digitize_2024(varargin)
+% PANEL_DIGITIZE_2024: Digitize EEG sensors and head shape.
+%
+% USAGE: panel_digitize_2024('Start')
+% panel_digitize_2024('CreateSerialConnection')
+% panel_digitize_2024('ResetDataCollection')
+% bstPanelNew = panel_digitize_2024('CreatePanel')
+% panel_digitize_2024('SetSimulate', isSimulate) Run this from command window BEFORE opening the Digitize panel.
+
+% @=============================================================================
+% 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: Elizabeth Bock & Francois Tadel, 2012-2017
+% Marc Lalancette, 2024
+% Chinmay Chinara, 2024
+
+eval(macro_method);
+end
+
+
+%% ========================================================================
+% ======= INITIALIZE =====================================================
+% ========================================================================
+
+%% ===== START =====
+function Start(varargin)
+ global Digitize
+ % Intialize global variable
+ Digitize = struct(...
+ 'Options', bst_get('DigitizeOptions'), ...
+ 'Type' , [], ...
+ 'SerialConnection', [], ...
+ 'Mode', 0, ...
+ 'hFig', [], ...
+ 'iDS', [], ...
+ 'SubjectName', [], ...
+ 'ConditionName', [], ...
+ 'iStudy', [], ...
+ 'BeepWav', [], ...
+ 'isEditPts', 0, ... % for correcting a wrongly detected point manually
+ 'Points', struct(...
+ 'Label', [], ...
+ 'Type', [], ...
+ 'Loc', []), ...
+ 'iPoint', 0, ...
+ 'Transf', []);
+
+ % Fix old structure (bef 2024) for Digitize.Options.Montages
+ if length(Digitize.Options.Montages) > 1 && ~isfield(Digitize.Options.Montages, 'ChannelFile')
+ Digitize.Options.Montages(end).ChannelFile = [];
+ end
+
+ % ===== PARSE INPUT =====
+ DigitizerType = 'Digitize';
+ sSubject = [];
+ iSubject = [];
+ surfaceFile = [];
+ if nargin > 0 && ~isempty(varargin{1})
+ DigitizerType = varargin{1};
+ end
+ if nargin > 1 && ~isempty(varargin{2})
+ sSubject = varargin{2};
+ end
+ if nargin > 2 && ~isempty(varargin{3})
+ iSubject = varargin{3};
+ end
+ if nargin > 3 && ~isempty(varargin{4})
+ surfaceFile = varargin{4};
+ end
+ Digitize.Type = DigitizerType;
+ switch DigitizerType
+ case 'Digitize'
+ % Do nothing
+ case '3DScanner'
+ % Simulate
+ SetSimulate(1);
+ otherwise
+ bst_error(sprintf('DigitizerType : "%s" is not supported', DigitizerType));
+ return
+ end
+
+ % ===== PREPARE DATABASE =====
+ % If no protocol: exit
+ if (bst_get('iProtocol') <= 0)
+ bst_error('Please create a protocol first.', Digitize.Type, 0);
+ return;
+ end
+
+ % Ask for subject id
+ if isempty(sSubject)
+ Digitize.Options.PatientId = java_dialog('input', 'Please, enter subject ID:', Digitize.Type, [], Digitize.Options.PatientId);
+ if isempty(Digitize.Options.PatientId)
+ return;
+ end
+ % Save new ID
+ bst_set('DigitizeOptions', Digitize.Options);
+
+ % ===== GET SUBJECT =====
+ % Save the new SubjectName
+ if strcmpi(Digitize.Type, '3DScanner')
+ Digitize.SubjectName = [Digitize.Type, '_', Digitize.Options.PatientId];
+ else
+ Digitize.SubjectName = Digitize.Type;
+ end
+
+ [sSubject, iSubject] = bst_get('Subject', Digitize.SubjectName);
+ else
+ Digitize.SubjectName = sSubject.Name;
+ end
+
+ % Create if subject doesnt exist
+ if isempty(iSubject)
+ % Default anat / one channel file per subject
+ if strcmpi(Digitize.Type, '3DScanner')
+ [sSubject, iSubject] = db_add_subject(Digitize.SubjectName, iSubject);
+ sTemplates = bst_get('AnatomyDefaults');
+ db_set_template(iSubject, sTemplates(1), 1);
+ else
+ UseDefaultAnat = 1;
+ UseDefaultChannel = 0;
+ [sSubject, iSubject] = db_add_subject(Digitize.SubjectName, iSubject, UseDefaultAnat, UseDefaultChannel);
+ end
+ % Update tree
+ panel_protocols('UpdateTree');
+ end
+
+ % ===== INITIALIZE CONNECTION =====
+ % Start Serial Connection
+ if ~CreateSerialConnection()
+ return;
+ end
+
+ % ===== CREATE CONDITION =====
+ % Get current date/time
+% CurrentDate = char(datetime('now'), 'yyyyMMdd');
+ c = clock;
+ % Condition name: PatientId_Date_Run
+ for i = 1:99
+ % Generate new condition name
+ Digitize.ConditionName = sprintf('%s_%02d%02d%02d_%02d', Digitize.Options.PatientId, c(1), c(2), c(3), i);
+ % Get condition
+ sStudy = bst_get('StudyWithCondition', [Digitize.SubjectName '/' Digitize.ConditionName]);
+ % If condition doesn't exist: ok, keep this one
+ if isempty(sStudy)
+ break;
+ end
+ end
+ % Create condition
+ Digitize.iStudy = db_add_condition(Digitize.SubjectName, Digitize.ConditionName);
+ sStudy = bst_get('Study', Digitize.iStudy);
+ % Create an empty channel file in there
+ ChannelMat = db_template('channelmat');
+ ChannelMat.Comment = Digitize.ConditionName;
+ % Save new channel file
+ ChannelFile = bst_fullfile(bst_fileparts(file_fullpath(sStudy.FileName)), ['channel_' Digitize.ConditionName '.mat']);
+ bst_save(ChannelFile, ChannelMat, 'v7');
+ % Reload condition to update the functional nodes
+ db_reload_studies(Digitize.iStudy);
+
+ if strcmpi(Digitize.Type, '3DScanner')
+ if isempty(surfaceFile)
+ % Import surface
+ iSurface = find(cellfun(@(x)~isempty(regexp(x, 'tess_textured', 'match')), {sSubject.Surface.FileName}));
+ if isempty(iSurface)
+ [~, surfaceFiles] = import_surfaces(iSubject);
+ if isempty(surfaceFiles)
+ return
+ end
+ surfaceFile = file_short(surfaceFiles{end});
+ else
+ [res, isCancel] = java_dialog('question', ['There is already scanned mesh available for this subject.' 10 10 ...
+ 'What do you want to do?'], ...
+ 'Import surface', [], {'Use existing', 'Add new', 'Cancel'}, 'Use existing');
+ if strcmpi(res, 'cancel') || isCancel
+ return
+ elseif strcmpi(res, 'use existing')
+ % If more than one surface present, user can choose
+ if length(iSurface) > 1
+ texSurfComment = java_dialog('combo', 'Select the textured surface:
', 'Choose textured surface', [], {sSubject.Surface(iSurface).Comment});
+ texSurfComment = strrep(texSurfComment, '_defaced', '');
+ if isempty(texSurfComment)
+ return
+ end
+ iSurfFile = find(cellfun(@(x)~isempty(regexp(x, [texSurfComment '.mat'], 'match')), {sSubject.Surface.FileName}));
+ surfaceFile = sSubject.Surface(iSurfFile).FileName;
+ % If only one surface is present, then load it directly
+ else
+ surfaceFile = sSubject.Surface(iSurface(end)).FileName;
+ end
+ elseif strcmpi(res, 'add new')
+ % Import a new textured mesh and append it to the list
+ [~, surfaceFiles] = import_surfaces(iSubject);
+ if isempty(surfaceFiles)
+ return
+ end
+ surfaceFile = file_short(surfaceFiles{end});
+ end
+ end
+ end
+ bst_progress('start', Digitize.Type, 'Loading surface file...');
+ Digitize.surfaceFile = surfaceFile;
+ sSurf = bst_memory('LoadSurface', Digitize.surfaceFile);
+ % Display surface
+ view_surface_matrix(sSurf.Vertices, sSurf.Faces, [], sSurf.Color, [], [], Digitize.surfaceFile);
+ end
+
+ % ===== DISPLAY DIGITIZE WINDOW =====
+ % Display panel
+ % Set the window to the position of the main Bst window, which is then hidden
+ % Set window title to Digitize.Type
+ panelContainer = gui_show('panel_digitize_2024', 'JavaWindow', Digitize.Type, [], [], [], [], [0,0]);
+
+ % Hide Brainstorm window
+ jBstFrame = bst_get('BstFrame');
+ jBstFrame.setVisible(0);
+ drawnow;
+
+ % Hard-coded window size for now
+ panelContainer.handle{1}.setSize(600, 600);
+ % Set the window to the left of the screen
+ loc = panelContainer.handle{1}.getLocation();
+ loc.x = 0;
+ panelContainer.handle{1}.setLocation(loc);
+
+ % Load beep sound
+ wavfile = bst_fullfile(bst_get('BrainstormHomeDir'), 'toolbox', 'sensors', 'private', 'bst_beep.wav');
+ [Digitize.BeepWav.data, Digitize.BeepWav.fs] = audioread(wavfile);
+
+ % Reset collection
+ ResetDataCollection();
+end
+
+
+%% ========================================================================
+% ======= PANEL FUNCTIONS ================================================
+% ========================================================================
+
+%% ===== CREATE PANEL =====
+function [bstPanelNew, panelName] = CreatePanel()
+ global Digitize
+
+ % Constants
+ panelName = 'Digitize';
+ % Java initializations
+ import java.awt.*;
+ import javax.swing.*;
+ import java.awt.event.KeyEvent;
+ import org.brainstorm.list.*;
+ import org.brainstorm.icon.*;
+ % Create new panel
+ jPanelNew = gui_component('Panel');
+ % Font size for the lists
+ veryLargeFontSize = round(100 * bst_get('InterfaceScaling') / 100);
+ largeFontSize = round(20 * bst_get('InterfaceScaling') / 100);
+ fontSize = round(11 * bst_get('InterfaceScaling') / 100);
+
+ % ===== MENU BAR =====
+ jPanelMenu = gui_component('panel');
+ jMenuBar = java_create('javax.swing.JMenuBar');
+ jPanelMenu.add(jMenuBar, BorderLayout.NORTH);
+ jLabelNews = gui_component('label', jPanelMenu, BorderLayout.CENTER, ...
+ ['
Digitize version: "2024"
' ...
+ '&bull To go back to previous version: File > Switch to Digitize "legacy" ' ...
+ '&bull More details: Help > Digitize tutorial'], [], [], [], fontSize);
+ jLabelNews.setHorizontalAlignment(SwingConstants.CENTER);
+ jLabelNews.setOpaque(true);
+ jLabelNews.setBackground(java.awt.Color.yellow);
+
+ % ===== FILE MENU =====
+ jMenu = gui_component('Menu', jMenuBar, [], 'File', [], [], [], []);
+ gui_component('MenuItem', jMenu, [], 'Start over', IconLoader.ICON_RELOAD, [], @(h,ev)bst_call(@ResetDataCollection, 1), []);
+ gui_component('MenuItem', jMenu, [], 'Edit settings...', IconLoader.ICON_EDIT, [], @(h,ev)bst_call(@EditSettings), []);
+ gui_component('MenuItem', jMenu, [], 'Switch to Digitize "legacy"', [], [], @(h,ev)bst_call(@SwitchVersion), []);
+ if ~strcmpi(Digitize.Type, '3DScanner')
+ gui_component('MenuItem', jMenu, [], 'Reset serial connection', IconLoader.ICON_FLIP, [], @(h,ev)bst_call(@CreateSerialConnection), []);
+ end
+ jMenu.addSeparator();
+ if exist('bst_headtracking', 'file') && ~strcmpi(Digitize.Type, '3DScanner')
+ gui_component('MenuItem', jMenu, [], 'Start head tracking', IconLoader.ICON_ALIGN_CHANNELS, [], @(h,ev)bst_call(@(h,ev)bst_headtracking([],1,1)), []);
+ jMenu.addSeparator();
+ end
+ gui_component('MenuItem', jMenu, [], 'Save as...', IconLoader.ICON_SAVE, [], @(h,ev)bst_call(@Save_Callback), []);
+ gui_component('MenuItem', jMenu, [], 'Save in database and exit', IconLoader.ICON_RESET, [], @(h,ev)bst_call(@Close_Callback), []);
+ % ===== EEG MONTAGE MENU =====
+ jMenuEeg = gui_component('Menu', jMenuBar, [], 'EEG montage', [], [], [], []);
+ CreateMontageMenu(jMenuEeg);
+ % ===== HELP MENU =====
+ jMenuHelp = gui_component('Menu', jMenuBar, [], 'Help', [], [], [], []);
+ gui_component('MenuItem', jMenuHelp, [], 'Digitize tutorial', [], [], @(h,ev)web('https://neuroimage.usc.edu/brainstorm/Tutorials/TutDigitize', '-browser'), []);
+
+ jPanelNew.add(jPanelMenu, BorderLayout.NORTH);
+
+ % ===== CONTROL PANEL =====
+ jPanelControl = gui_component('panel');
+ jPanelControl.setBorder(BorderFactory.createEmptyBorder(0,0,7,0));
+
+ % ===== NEXT POINT PANEL =====
+ jPanelNext = gui_river([5,4], [4,4,4,4], 'Next point');
+ % Next point label
+ jLabelNextPoint = gui_component('label', jPanelNext, [], '', [], [], [], veryLargeFontSize);
+ jButtonFids = gui_component('button', jPanelNext, 'br', 'Add fiducials', [], 'Add set of fiducials to digitize', @(h,ev)bst_call(@Fiducials_Callback));
+ jButtonFids.setEnabled(0);
+ if strcmpi(Digitize.Type, '3DScanner')
+ jButtonEEGAutoDetectElectrodes = gui_component('button', jPanelNext, [], 'Auto', [], GenerateTooltipTextAuto(), @(h,ev)bst_call(@EEGAutoDetectElectrodes));
+ else
+ % Separator
+ jButtonEEGAutoDetectElectrodes = gui_component('label', jPanelNext, 'hfill', '');
+ end
+ jButtonEEGAutoDetectElectrodes.setEnabled(0);
+ jPanelControl.add(jPanelNext, BorderLayout.NORTH);
+
+ % ===== INFO PANEL =====
+ jPanelInfo = gui_river([5,4], [10,10,10,10], '');
+ % Message label
+ jLabelWarning = gui_component('label', jPanelInfo, 'br', ' ', [], [], [], largeFontSize);
+ gui_component('label', jPanelInfo, 'br', ''); % spacing
+ % Number of head points collected
+ gui_component('label', jPanelInfo, 'br', 'Head shape points');
+ jTextFieldExtra = gui_component('text', jPanelInfo, [], '0', [], 'Head shape points digitized', @(h,ev)bst_call(@ExtraChangePoint_Callback), largeFontSize);
+ initSize = jTextFieldExtra.getPreferredSize();
+ jTextFieldExtra.setPreferredSize(Dimension(initSize.getWidth()*1.5, initSize.getHeight()*1.5))
+ if strcmpi(Digitize.Type, '3DScanner')
+ % Add 150 random head shape points generation button
+ jButtonRandomHeadPts = gui_component('button', jPanelInfo, [], 'Random', [], 'Collect 150 head shape points from mesh', @(h,ev)bst_call(@CollectRandomHeadPts_Callback), largeFontSize);
+ jButtonRandomHeadPts.setPreferredSize(Dimension(initSize.getWidth()*2.2, initSize.getHeight()*1.7));
+ else
+ % Separator
+ jButtonRandomHeadPts = gui_component('label', jPanelInfo, 'hfill', '');
+ end
+ jButtonRandomHeadPts.setEnabled(0);
+ jPanelControl.add(jPanelInfo, BorderLayout.CENTER);
+
+ % ===== OTHER BUTTONS =====
+ jPanelMisc = gui_river([5,4], [10,4,4,4]);
+ if ~strcmpi(Digitize.Type, '3DScanner')
+ jButtonCollectPoint = gui_component('button', jPanelMisc, 'br', 'Collect point', [], [], @(h,ev)bst_call(@ManualCollect_Callback));
+ else
+ jButtonCollectPoint = gui_component('label', jPanelMisc, 'hfill', ''); % spacing
+ end
+ % Until initial fids are collected and figure displayed, "delete" button is used to "restart".
+ jButtonDeletePoint = gui_component('button', jPanelMisc, [], 'Start over', [], [], @(h,ev)bst_call(@ResetDataCollection, 1));
+ gui_component('label', jPanelMisc, 'hfill', ''); % spacing
+ gui_component('button', jPanelMisc, [], 'Save as...', [], [], @(h,ev)bst_call(@Save_Callback));
+ jPanelControl.add(jPanelMisc, BorderLayout.SOUTH);
+ jPanelNew.add(jPanelControl, BorderLayout.WEST);
+
+ % ===== COORDINATE DISPLAY PANEL =====
+ jPanelDisplay = gui_component('Panel');
+ jPanelDisplay.setBorder(java_scaled('titledborder', 'Coordinates (cm)'));
+ % List of coordinates
+ jListCoord = JList(fontSize);
+ jListCoord.setCellRenderer(BstStringListRenderer(fontSize));
+ java_setcb(jListCoord, ...
+ 'KeyTypedCallback', @(h,ev)bst_call(@CoordListKeyTyped_Callback,h,ev), ...
+ 'MouseClickedCallback', @(h,ev)bst_call(@CoordListClick_Callback,h,ev));
+ jPanelScrollList = JScrollPane(jListCoord);
+ jPanelScrollList.setViewportView(jListCoord);
+ jPanelScrollList.setHorizontalScrollBarPolicy(jPanelScrollList.HORIZONTAL_SCROLLBAR_NEVER);
+ jPanelScrollList.setVerticalScrollBarPolicy(jPanelScrollList.VERTICAL_SCROLLBAR_ALWAYS);
+ jPanelScrollList.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
+ jPanelDisplay.add(jPanelScrollList, BorderLayout.CENTER);
+ jPanelNew.add(jPanelDisplay, BorderLayout.CENTER);
+
+ % Create the controls structure
+ ctrl = struct('jMenuEeg', jMenuEeg, ...
+ 'jButtonFids', jButtonFids, ...
+ 'jLabelNextPoint', jLabelNextPoint, ...
+ 'jLabelWarning', jLabelWarning, ...
+ 'jListCoord', jListCoord, ...
+ 'jButtonEEGAutoDetectElectrodes', jButtonEEGAutoDetectElectrodes, ...
+ 'jButtonRandomHeadPts', jButtonRandomHeadPts, ...
+ 'jTextFieldExtra', jTextFieldExtra, ...
+ 'jButtonCollectPoint', jButtonCollectPoint, ...
+ 'jButtonDeletePoint', jButtonDeletePoint);
+ bstPanelNew = BstPanel(panelName, jPanelNew, ctrl);
+
+ %% =================================================================================
+ % === INTERNAL CALLBACKS =========================================================
+ % =================================================================================
+ %% ===== COORDINATE LIST KEY TYPED CALLBACK =====
+ function CoordListKeyTyped_Callback(h, ev)
+ switch(uint8(ev.getKeyChar()))
+ % Delete
+ case {ev.VK_DELETE, ev.VK_BACK_SPACE}
+ ctrl = bst_get('PanelControls', 'Digitize');
+ % If contact list rendering is blank in panel then dont't proceed
+ if ctrl.jListCoord.isSelectionEmpty()
+ return;
+ end
+
+ [sCoordName, iSelCoord] = GetSelectedCoord();
+ spl = regexp(sCoordName,'\s+','split');
+ nameFinal = spl{1};
+ if (~strcmpi(nameFinal, 'NAS') &&...
+ ~strcmpi(nameFinal, 'LPA') &&...
+ ~strcmpi(nameFinal, 'RPA'))
+ listModel = ctrl.jListCoord.getModel();
+ listModel.setElementAt(nameFinal, iSelCoord-1);
+ Digitize.iPoint = iSelCoord;
+ Digitize.isEditPts = 1;
+ DeletePoint_Callback();
+ end
+ end
+ end
+
+ %% ===== COORDINATE LIST CLICK CALLBACK =====
+ function CoordListClick_Callback(h, ev)
+ % If single click
+ if (ev.getClickCount() == 1)
+ ctrl = bst_get('PanelControls', 'Digitize');
+ % If contact list rendering is blank in panel then dont't proceed
+ if ctrl.jListCoord.isSelectionEmpty()
+ return;
+ end
+
+ [sCoordName, ~] = GetSelectedCoord();
+ spl = regexp(sCoordName,'\s+','split');
+ nameFinal = spl{1};
+ bst_figures('SetSelectedRows', nameFinal);
+ end
+ end
+end
+
+%% ===== GET SELECTED ELECTRODE =====
+function [sCoordName, iSelCoord] = GetSelectedCoord()
+ global Digitize
+ % Get panel handles
+ ctrl = bst_get('PanelControls', 'Digitize');
+ if isempty(ctrl)
+ return;
+ end
+
+ % Get JList selected indices
+ iSelCoord = uint16(ctrl.jListCoord.getSelectedIndices())' + 1;
+ listModel = ctrl.jListCoord.getModel();
+ sCoordName = listModel.getElementAt(iSelCoord-1);
+end
+
+%% ===== SWITCH TO OLD VERSION =====
+function SwitchVersion()
+ % Always confirm this switch.
+ if ~java_dialog('confirm', ['Switch to legacy version of the Digitize panel? ', ...
+ 'See Digitize tutorial (Digitize panel > Help menu). ', ...
+ 'This will close the window. Any unsaved points will be lost.'], 'Digitize version')
+ return;
+ end
+ % Close this panel
+ Close_Callback();
+ % Save the preferred version. Must be after closing
+ DigitizeOptions = bst_get('DigitizeOptions');
+ DigitizeOptions.Version = 'legacy';
+ bst_set('DigitizeOptions', DigitizeOptions);
+end
+
+%% ===== CLOSE =====
+function Close_Callback()
+ % Save channel file
+ SaveDigitizeChannelFile();
+ % Close panel
+ gui_hide('Digitize');
+end
+
+%% ===== HIDING CALLBACK =====
+function isAccepted = PanelHidingCallback()
+ global Digitize;
+ % If Brainstorm window was hidden before showing the Digitizer
+ if bst_get('isGUI')
+ % Get Brainstorm frame
+ jBstFrame = bst_get('BstFrame');
+ % Hide Brainstorm window
+ jBstFrame.setVisible(1);
+ end
+ % Get study
+ [sStudy, iStudy] = bst_get('StudyWithCondition', [Digitize.SubjectName '/' Digitize.ConditionName]);
+ % If nothing was clicked: delete the condition that was just created
+ if isempty(Digitize.Transf)
+ % Delete study
+ if ~isempty(iStudy)
+ db_delete_studies(iStudy);
+ panel_protocols('UpdateTree');
+ end
+ % Else: reload to get access to the EEG type of sensors
+ else
+ db_reload_studies(iStudy);
+ end
+ % Close serial connection, in particular to avoid further callbacks if stylus is pressed.
+ if ~isempty(Digitize.SerialConnection)
+ delete(Digitize.SerialConnection);
+ Digitize.SerialConnection = []; % cleaner than "handle to deleted serialport".
+ end
+ % Could also: clear Digitize;
+ % Unload everything
+ bst_memory('UnloadAll', 'Forced');
+ isAccepted = 1;
+end
+
+
+%% ===== EDIT SETTINGS =====
+function isOk = EditSettings()
+ global Digitize
+
+ isOk = 0;
+ % Ask for new options
+ if isfield(Digitize.Options, 'Fids') && iscell(Digitize.Options.Fids)
+ FidsString = sprintf('%s, ', Digitize.Options.Fids{:});
+ FidsString(end-1:end) = '';
+ else
+ FidsString = 'NAS, LPA, RPA';
+ end
+ if isfield(Digitize.Options, 'ConfigCommands') && iscell(Digitize.Options.ConfigCommands) && ...
+ ~isempty(Digitize.Options.ConfigCommands)
+ ConfigString = sprintf('%s; ', Digitize.Options.ConfigCommands{:});
+ ConfigString(end-1:end) = '';
+ else
+ ConfigString = '';
+ end
+
+ % GUI all potential options: {Description, default values}
+ options_all = {'Serial connection settings
Serial port name (COM1):', ...
+ Digitize.Options.ComPort;
+ 'Unit Type (Fastrak or Patriot):', ...
+ Digitize.Options.UnitType; ...
+ 'Additional device configuration commands, separated by ";" (see device documentation, e.g. H1,0,0,-1;H2,0,0,-1):', ...
+ ConfigString; ...
+ ' Collection settings
List anatomy and possibly MEG fiducials, in desired order (NAS, LPA, RPA, HPI-N, HPI-L, HPI-R, HPI-X):', ...
+ FidsString; ...
+ 'How many times do you want to localize these fiducials at the start:', ...
+ num2str(Digitize.Options.nFidSets); ...
+ 'Distance threshold for repeated measure of fiducial locations (mm):', ...
+ num2str(Digitize.Options.DistThresh * 1000); ... % m to mm
+ 'Beep when collecting point (0=no, 1=yes):', ...;
+ num2str(Digitize.Options.isBeep)};
+
+ % Options to show for each type
+ switch lower(Digitize.Type)
+ case 'digitize'
+ iOptionsType = [1:7];
+ case '3dscanner'
+ iOptionsType = [4:7];
+ end
+
+ % Ask options
+ [resType, isCancel] = java_dialog('input', options_all(iOptionsType,1), [Digitize.Type ' configuration'], [], options_all(iOptionsType,2));
+ if isempty(resType) || isCancel
+ return
+ end
+
+ % Results from options asked to user
+ options_all(iOptionsType, 3) = resType;
+ % Results from options not asked to user
+ iOptionsKeep = setdiff([1:length(options_all)], iOptionsType);
+ options_all(iOptionsKeep, 3) = options_all(iOptionsKeep,2);
+ res = options_all(:,3);
+
+ % Check values
+ if length(resType) < length(iOptionsType) || (ismember(1, iOptionsType) && isempty(res{1})) || ...
+ (ismember(2, iOptionsType) && isempty(res{2})) || ...
+ (ismember(5, iOptionsType) && isnan(str2double(res{5}))) || ...
+ (ismember(6, iOptionsType) && isnan(str2double(res{6}))) || ...
+ (ismember(7, iOptionsType) && ~ismember(str2double(res{7}), [0 1]))
+ bst_error('Invalid values.', 'Digitize', 0);
+ return;
+ end
+ % Digitizer: COM port
+ Digitize.Options.ComPort = res{1};
+ % Digitizer: Type
+ Digitize.Options.UnitType = lower(res{2});
+ % Digitizer: COM properties
+ if isempty(Digitize.Options.UnitType)
+ % Do nothing
+ elseif strcmp(Digitize.Options.UnitType,'fastrak')
+ Digitize.Options.ComRate = 9600;
+ Digitize.Options.ComByteCount = 94;
+ elseif strcmp(Digitize.Options.UnitType,'patriot')
+ Digitize.Options.ComRate = 115200;
+ Digitize.Options.ComByteCount = 120;
+ else
+ bst_error('Incorrect unit type.', Digitize.Type, 0);
+ return;
+ end
+ % Digitizer: Parse device configuration commands. Remove all spaces, and split at ";"
+ Digitize.Options.ConfigCommands = str_split(strrep(res{3}, ' ', ''), ';', true); % remove empty
+ % Common: Parse fiducials.
+ Digitize.Options.Fids = str_split(res{4}, '()[],;"'' ', true); % remove empty
+ if isempty(Digitize.Options.Fids) || ~iscell(Digitize.Options.Fids) || numel(Digitize.Options.Fids) < 3
+ bst_error('At least 3 anatomy fiducials are required, e.g. NAS, LPA, RPA.', Digitize.Type, 0);
+ Digitize.Options.Fids = {'NAS', 'LPA', 'RPA'};
+ return;
+ end
+ % Common: Number of fiducial sets
+ if ~isempty(res{5})
+ Digitize.Options.nFidSets = str2double(res{5});
+ end
+ % Common: Threshold for distance in fiducial sets
+ if ~isempty(res{6})
+ Digitize.Options.DistThresh = str2double(res{6}) / 1000; % mm to m
+ end
+ % Common: Beep
+ if ~isempty(res{7})
+ Digitize.Options.isBeep = str2double(res{7});
+ end
+
+ for iFid = 1:numel(Digitize.Options.Fids)
+ switch lower(Digitize.Options.Fids{iFid})
+ % Possible names copied from channel_detect_type
+ case {'nas', 'nasion', 'nz', 'fidnas', 'fidnz', 'n', 'na'}
+ Digitize.Options.Fids{iFid} = 'NAS';
+ case {'lpa', 'pal', 'og', 'left', 'fidt9', 'leftear', 'l'}
+ Digitize.Options.Fids{iFid} = 'LPA';
+ case {'rpa', 'par', 'od', 'right', 'fidt10', 'rightear', 'r'}
+ Digitize.Options.Fids{iFid} = 'RPA';
+ otherwise
+ if ~strfind(lower(Digitize.Options.Fids{iFid}), 'hpi')
+ bst_error(sprintf('Unrecognized fiducial: %s', Digitize.Options.Fids{iFid}), Digitize.Type, 0);
+ return;
+ end
+ Digitize.Options.Fids{iFid} = upper(Digitize.Options.Fids{iFid});
+ end
+ end
+
+ % Save values
+ bst_set('DigitizeOptions', Digitize.Options);
+ isOk = 1;
+
+ % If no points collected, reset.
+ if isempty(Digitize.Points) || ~isfield(Digitize.Points, 'Loc') || isempty(Digitize.Points(1).Loc)
+ ResetDataCollection(1);
+ end
+end
+
+
+%% ===== SET SIMULATION MODE =====
+% USAGE: panel_digitize_2024('SetSimulate', isSimulate)
+% Run this from command line BEFORE opening the Digitize panel.
+function SetSimulate(isSimulate)
+ DigitizeOptions = bst_get('DigitizeOptions');
+ % Change value
+ DigitizeOptions.isSimulate = isSimulate;
+ bst_set('DigitizeOptions', DigitizeOptions);
+end
+
+
+%% ========================================================================
+% ======= ACQUISITION FUNCTIONS ==========================================
+% ========================================================================
+
+%% ===== RESET DATA COLLECTION =====
+function ResetDataCollection(isResetSerial)
+ global Digitize
+ bst_progress('start', Digitize.Type, 'Initializing...');
+ % Reset serial?
+ if (nargin == 1) && isequal(isResetSerial, 1)
+ CreateSerialConnection();
+ end
+ % Reset points structure
+ Digitize.Points = struct(...
+ 'Label', [], ...
+ 'Type', [], ...
+ 'Loc', []);
+ Digitize.iPoint = 0;
+ Digitize.Transf = [];
+ % Reset figure (also unloads in global data)
+ if ~isempty(Digitize.hFig) && ishandle(Digitize.hFig)
+ bst_figures('DeleteFigure', Digitize.hFig, []);
+
+ % For 3D Scanner reload the surface
+ if strcmpi(Digitize.Type, '3DScanner')
+ % Load the surface
+ sSurf = bst_memory('LoadSurface', Digitize.surfaceFile);
+ % Display surface
+ view_surface_matrix(sSurf.Vertices, sSurf.Faces, [], sSurf.Color, [], [], Digitize.surfaceFile);
+ end
+ end
+ Digitize.iDS = [];
+
+ % Get controls
+ ctrl = bst_get('PanelControls', 'Digitize');
+ % Reset counters
+ ctrl.jTextFieldExtra.setText(num2str(0));
+ ctrl.jTextFieldExtra.setEnabled(0);
+ java_setcb(ctrl.jButtonDeletePoint, 'ActionPerformedCallback', @(h,ev)bst_call(@ResetDataCollection, 1));
+ ctrl.jButtonDeletePoint.setText('Start over');
+ ctrl.jLabelWarning.setText('');
+ ctrl.jLabelWarning.setBackground([]);
+
+ % Generate list of labeled points
+ % Initial fiducials
+ for iP = 1:numel(Digitize.Options.Fids)
+ Digitize.Points(iP).Label = Digitize.Options.Fids{iP};
+ Digitize.Points(iP).Type = 'CARDINAL';
+ end
+ if Digitize.Options.nFidSets > 1
+ Digitize.Points = repmat(Digitize.Points, 1, Digitize.Options.nFidSets);
+ end
+ % EEG
+ [curMontage, nEEG] = GetCurrentMontage();
+ for iEEG = 1:nEEG
+ Digitize.Points(end+1).Label = curMontage.Labels{iEEG};
+ Digitize.Points(end).Type = 'EEG';
+ end
+
+ % Display list in text box
+ UpdateList();
+
+ % Close progress bar
+ bst_progress('stop');
+end
+
+
+%% ===== UPDATE LIST OF POINTS IN TEXT BOX =====
+function UpdateList()
+ global Digitize;
+ % Get controls
+ ctrl = bst_get('PanelControls', 'Digitize');
+ % Define the model
+ listModel = javax.swing.DefaultListModel();
+ % Add points to list
+ iHeadPoints = 0;
+ lastIndex = 0;
+ for iP = 1:numel(Digitize.Points)
+ if ~isempty(Digitize.Points(iP).Label)
+ listModel.addElement(sprintf('%s %3.3f %3.3f %3.3f', Digitize.Points(iP).Label, Digitize.Points(iP).Loc .* 100));
+ else % Head points
+ iHeadPoints = iHeadPoints + 1;
+ listModel.addElement(sprintf('%03d %3.3f %3.3f %3.3f', iHeadPoints, Digitize.Points(iP).Loc .* 100));
+ end
+ if ~isempty(Digitize.Points(iP).Loc)
+ lastIndex = iP;
+ end
+ end
+ % Set this list
+ ctrl.jListCoord.setModel(listModel);
+ % Scroll to last collected point (non-empty Loc), +1 if more points listed.
+ if listModel.getSize() > lastIndex
+ ctrl.jListCoord.ensureIndexIsVisible(lastIndex); % 0-indexed
+ else
+ ctrl.jListCoord.ensureIndexIsVisible(lastIndex-1); % 0-indexed, -1 works even if 0
+ end
+ % Also update label of next point to digitize.
+ if numel(Digitize.Points) >= Digitize.iPoint + 1 && ~isempty(Digitize.Points(Digitize.iPoint + 1).Label)
+ ctrl.jLabelNextPoint.setText(Digitize.Points(Digitize.iPoint + 1).Label);
+ else
+ ctrl.jLabelNextPoint.setText(num2str(iHeadPoints + 1));
+ end
+
+ % Update tooltip text for 'Auto' button
+ if strcmpi(Digitize.Type, '3DScanner')
+ ctrl.jButtonEEGAutoDetectElectrodes.setToolTipText(GenerateTooltipTextAuto());
+ end
+end
+
+%% ===== 3DSCANNER: AUTOMATICALLY DETECT AND LABEL EEG CAP ELECTRODES =====
+function EEGAutoDetectElectrodes()
+ global Digitize GlobalData
+
+ % Add disclaimer to users that 'Auto' feature is experimental
+ if ~java_dialog('confirm', [' Automatic detection of EEG sensors is an experimental feature. ' ...
+ 'Please verify the results carefully.
' ...
+ 'Do you want to continue?'], 'Auto detect EEG electrodes')
+ return
+ end
+ % Get controls
+ ctrl = bst_get('PanelControls', 'Digitize');
+ % Disable Auto button
+ ctrl.jButtonEEGAutoDetectElectrodes.setEnabled(0);
+ % Progress bar
+ bst_progress('start', Digitize.Type, 'Automatic labelling of EEG sensors...');
+
+ % Get current montage
+ curMontage = GetCurrentMontage();
+ isWhiteCap = 0;
+ % For white caps change the color space by inverting the colors
+ % NOTE: only 'Acticap' is the tested white cap (needs work on finding a better aprrooach)
+ if ~isempty(regexp(curMontage.Name, 'ActiCap', 'match'))
+ isWhiteCap = 1;
+ end
+
+ % Get the cap surface from 3D scanner
+ hFig = bst_figures('GetCurrentFigure','3D');
+ TessInfo = getappdata(hFig, 'Surface');
+ sSurf = bst_memory('LoadSurface', TessInfo.SurfaceFile);
+
+ % Automatically find electrodes locations on EEG cap
+ [capCenters2d, capImg2d, surface3dscannerUv] = channel_detect_eegcap_auto('FindElectrodesEegCap', sSurf, isWhiteCap);
+ if isempty(Digitize.Options.Montages(Digitize.Options.iMontage).ChannelFile)
+ bst_error('EEG cap layout not selected. Go to EEG', Digitize.Type, 1);
+ bst_progress('stop');
+ return;
+ else
+ ChannelMat = in_bst_channel(Digitize.Options.Montages(Digitize.Options.iMontage).ChannelFile);
+ end
+
+ % Get acquired EEG points
+ iEeg = and(cellfun(@(x) ~isempty(regexp(x, 'EEG', 'match')), {Digitize.Points.Type}), ~cellfun(@isempty, {Digitize.Points.Loc}));
+ pointsEEG = Digitize.Points(iEeg);
+
+ % Warp points from layout to mesh
+ capPoints3d = channel_detect_eegcap_auto('WarpLayout2Mesh', capCenters2d, capImg2d, surface3dscannerUv, ChannelMat.Channel, pointsEEG);
+
+ % Plot the electrodes and their labels
+ for iPoint= 1:length(capPoints3d)
+ % Find found point in current montage and set it in global
+ [~, Digitize.iPoint] = ismember(capPoints3d(iPoint).Label, {Digitize.Points.Label});
+ Digitize.Points(Digitize.iPoint).Loc = capPoints3d(iPoint).Loc;
+ Digitize.Points(Digitize.iPoint).Type = 'EEG';
+ % Add the point to the display (in cm)
+ PlotCoordinate();
+ end
+
+ UpdateList();
+ % Enable Random button
+ ctrl.jButtonRandomHeadPts.setEnabled(1);
+ bst_progress('stop');
+
+end
+
+%% ===== MANUAL COLLECT CALLBACK ======
+function ManualCollect_Callback()
+ global Digitize
+ ctrl = bst_get('PanelControls', 'Digitize');
+ ctrl.jButtonCollectPoint.setEnabled(0);
+ % Simulation: call the callback directly
+ if Digitize.Options.isSimulate
+ BytesAvailable_Callback([], []);
+ % Else: Send a collection request to the Polhemus
+ else
+ % User clicked the button, collect a point
+ writeline(Digitize.SerialConnection,'P');
+ pause(0.2);
+ end
+ ctrl.jButtonCollectPoint.setEnabled(1);
+end
+
+%% ===== COLLECT RANDOM HEADPOINTS =====
+function CollectRandomHeadPts_Callback()
+ global Digitize
+ % Get controls
+ ctrl = bst_get('PanelControls', 'Digitize');
+ % Disable Random button
+ ctrl.jButtonRandomHeadPts.setEnabled(0);
+ % Progress bar
+ bst_progress('start', Digitize.Type, 'Plotting 150 random head shape points...');
+
+ hFig = bst_figures('GetCurrentFigure','3D');
+ TessInfo = getappdata(hFig, 'Surface');
+ TessMat = bst_memory('LoadSurface', TessInfo.SurfaceFile);
+
+ % Brainstorm recommends to collect approximately 100-150 points from the head
+ % 5-10 points from the boney part of the nose
+ PlotHeadShapePoints(TessMat.Vertices, 'nose', 10);
+ % 10-20 points across the left eyebrow
+ PlotHeadShapePoints(TessMat.Vertices, 'leyebrow', 20);
+ % 10-20 points across the right eyebrow
+ PlotHeadShapePoints(TessMat.Vertices, 'reyebrow', 20);
+ % 100 points on the scalp
+ PlotHeadShapePoints(TessMat.Vertices, 'scalp', 100);
+
+ UpdateList();
+ bst_progress('stop');
+end
+
+%% ===== PLOT HEAD SHAPE POINTS =====
+function PlotHeadShapePoints(Vertices, plotRegion, nPoints)
+ global Digitize
+ % Get controls
+ ctrl = bst_get('PanelControls', 'Digitize');
+
+ % Get the plotting parameters based on the region in the head
+ switch plotRegion
+ case 'nose'
+ nosePoint = Digitize.Points(1).Loc;
+ % Get 600 nearest points to the 'nosePoint' and choose 'nPoints' from it
+ nearPointsIdx = bst_nearest(Vertices, nosePoint, 600, 0, []);
+ range = length(nearPointsIdx);
+ stepFactor = range/nPoints;
+ case 'leyebrow'
+ lEyebrowPoint = (1.25 * Digitize.Points(1).Loc) + (0.5 * Digitize.Points(2).Loc);
+ % Get 400 nearest points to the 'lEyebrowPoint' and choose 'nPoints' from it
+ nearPointsIdx = bst_nearest(Vertices, lEyebrowPoint, 400, 0, []);
+ range = length(nearPointsIdx);
+ stepFactor = range/nPoints;
+ case 'reyebrow'
+ rEyebrowPoint = (1.25 * Digitize.Points(1).Loc) + (0.5 * Digitize.Points(3).Loc);
+ % Get 400 nearest points to the 'rEyebrowPoint' and choose 'nPoints' from it
+ nearPointsIdx = bst_nearest(Vertices, rEyebrowPoint, 400, 0, []);
+ range = length(nearPointsIdx);
+ stepFactor = range/nPoints;
+ case 'scalp'
+ range = length(Vertices);
+ stepFactor = ceil(range/nPoints);
+ otherwise
+ bst_error([plotRegion 'is invalid for plotting head shape'], Digitize.Type, 0);
+ bst_progress('stop');
+ return
+ end
+
+ % Plot the head shape points
+ for i= 1:stepFactor:range
+ % Increment current point index
+ Digitize.iPoint = Digitize.iPoint + 1;
+ % Update the coordinate and Type
+ if strcmpi(plotRegion, 'scalp')
+ Digitize.Points(Digitize.iPoint).Loc = Vertices(i, :);
+ else
+ Digitize.Points(Digitize.iPoint).Loc = Vertices(nearPointsIdx(i), :);
+ end
+ Digitize.Points(Digitize.iPoint).Type = 'EXTRA';
+ % Add the point to the display (in cm)
+ PlotCoordinate();
+ % Update text field counter to the next point in the list
+ iCount = str2double(ctrl.jTextFieldExtra.getText());
+ ctrl.jTextFieldExtra.setText(num2str(iCount + 1));
+ end
+end
+
+%% ===== DELETE POINT CALLBACK =====
+function DeletePoint_Callback()
+ global Digitize
+ % Get controls
+ ctrl = bst_get('PanelControls', 'Digitize');
+
+ % If we're down to initial fids only, change delete button label and callback to "restart" instead of delete.
+ if Digitize.iPoint <= numel(Digitize.Options.Fids) * Digitize.Options.nFidSets + 1
+ java_setcb(ctrl.jButtonDeletePoint, 'ActionPerformedCallback', @(h,ev)bst_call(@ResetDataCollection, 1));
+ ctrl.jButtonDeletePoint.setText('Start over');
+ % Safety check, but this should not happen.
+ if Digitize.iPoint <= numel(Digitize.Options.Fids) * Digitize.Options.nFidSets
+ error('Cannot delete initial fiducials.');
+ end
+ end
+
+ % Remove last point from figure. It must still be in the list.
+ PlotCoordinate(false); % isAdd = false: remove last point instead of adding one
+
+ % Decrement head shape point count
+ if strcmpi(Digitize.Points(Digitize.iPoint).Type, 'EXTRA')
+ nShapePts = str2num(ctrl.jTextFieldExtra.getText());
+ ctrl.jTextFieldExtra.setText(num2str(max(0, nShapePts - 1)));
+ end
+
+ % Remove last point in list
+ if ~isempty(Digitize.Points(Digitize.iPoint).Label)
+ % Keep point in list, but remove location and decrease index to collect again
+ Digitize.Points(Digitize.iPoint).Loc = [];
+ else
+ % Delete the point from the list entirely
+ Digitize.Points(Digitize.iPoint) = [];
+ end
+ Digitize.iPoint = Digitize.iPoint - 1;
+
+ % Update coordinates list
+ UpdateList();
+end
+
+%% ===== CHECK FIDUCIALS: ADD SET TO DIGITIZE NOW =====
+function Fiducials_Callback()
+ global Digitize
+ nRemaining = numel(Digitize.Points) - Digitize.iPoint;
+ nFids = numel(Digitize.Options.Fids);
+ if nRemaining > 0
+ % Add space in points array.
+ Digitize.Points(Digitize.iPoint + nFids + (1:nRemaining)) = Digitize.Points(Digitize.iPoint + (1:nRemaining));
+ end
+ for iP = 1:nFids
+ Digitize.Points(Digitize.iPoint + iP).Label = Digitize.Options.Fids{iP};
+ Digitize.Points(Digitize.iPoint + iP).Type = 'CARDINAL';
+ end
+ UpdateList();
+end
+
+%% ===== CREATE FIGURE =====
+function CreateHeadpointsFigure()
+ global Digitize
+ if isempty(Digitize.hFig) || ~ishandle(Digitize.hFig) || isempty(Digitize.iDS)
+ % Get study
+ sStudy = bst_get('StudyWithCondition', [Digitize.SubjectName '/' Digitize.ConditionName]);
+ % Plot head points and save handles in global variable
+ [Digitize.hFig, Digitize.iDS] = view_headpoints(file_fullpath(sStudy.Channel.FileName));
+ % Hide head surface
+ panel_surface('SetSurfaceTransparency', Digitize.hFig, 1, 0.8);
+ % Get Digitizer JFrame
+ bstContainer = get(bst_get('Panel', 'Digitize'), 'container');
+ % Get maximum figure position
+ decorationSize = bst_get('DecorationSize');
+ [~, FigArea] = gui_layout('GetScreenBrainstormAreas', bstContainer.handle{1});
+ FigPos = FigArea(1,:) + [decorationSize(1), decorationSize(4), - decorationSize(1) - decorationSize(3), - decorationSize(2) - decorationSize(4)];
+ if (FigPos(3) > 0) && (FigPos(4) > 0)
+ set(Digitize.hFig, 'Position', FigPos);
+ end
+ % Remove the close handle function
+ set(Digitize.hFig, 'CloseRequestFcn', []);
+ else
+ % Hide figure
+ set(Digitize.hFig, 'Visible', 'off');
+ % Get study
+ sStudy = bst_get('StudyWithCondition', [Digitize.SubjectName '/' Digitize.ConditionName]);
+ % Plot head points and save handles in global variable
+ [Digitize.hFig, Digitize.iDS] = view_headpoints(file_fullpath(sStudy.Channel.FileName));
+ % Get the surface
+ sSurf = bst_memory('LoadSurface', Digitize.surfaceFile);
+ % Apply the transformation
+ sSurf.Vertices = [sSurf.Vertices ones(size(sSurf.Vertices,1),1)] * Digitize.Transf';
+ % Remove the surface
+ panel_surface('RemoveSurface', Digitize.hFig, 1);
+ % Deface the surface
+ if isempty(regexp(sSurf.Comment, 'defaced', 'match'))
+ sSurf = tess_deface(sSurf);
+ end
+ % Save the surface and update the node
+ ProtocolInfo = bst_get('ProtocolInfo');
+ surfaceFile = bst_fullfile(ProtocolInfo.SUBJECTS, Digitize.surfaceFile);
+ bst_save(surfaceFile, sSurf, 'v7');
+ [~, iSubject] = bst_get('Subject', Digitize.SubjectName);
+ db_reload_subjects(iSubject);
+ % Get Digitizer JFrame
+ bstContainer = get(bst_get('Panel', 'Digitize'), 'container');
+ % Get maximum figure position
+ decorationSize = bst_get('DecorationSize');
+ [~, FigArea] = gui_layout('GetScreenBrainstormAreas', bstContainer.handle{1});
+ FigPos = FigArea(1,:) + [decorationSize(1), decorationSize(4), - decorationSize(1) - decorationSize(3), - decorationSize(2) - decorationSize(4)];
+ % Display updated surface
+ view_surface_matrix(sSurf.Vertices, sSurf.Faces, [], sSurf.Color, Digitize.hFig, [], Digitize.surfaceFile);
+ if (FigPos(3) > 0) && (FigPos(4) > 0)
+ set(Digitize.hFig, 'Position', FigPos);
+ end
+ % Remove the close handle function
+ set(Digitize.hFig, 'CloseRequestFcn', []);
+ end
+end
+
+%% ===== PLOT NEXT POINT, OR REMOVE LAST OR REMOVE SELECTED POINT =====
+function PlotCoordinate(isAdd)
+ if nargin < 1 || isempty(isAdd)
+ isAdd = true;
+ end
+ global Digitize GlobalData
+
+ % Add EEG sensor locations to channel stucture
+ if strcmpi(Digitize.Points(Digitize.iPoint).Type, 'EEG')
+ if ~isstruct(GlobalData.DataSet(Digitize.iDS).Channel) || ~isfield(GlobalData.DataSet(Digitize.iDS).Channel, 'Name')
+ % First point in the list. This creates one channel, with empty fields.
+ GlobalData.DataSet(Digitize.iDS).Channel = db_template('channeldesc');
+ end
+ if numel(GlobalData.DataSet(Digitize.iDS).Channel) == 1 && isempty(GlobalData.DataSet(Digitize.iDS).Channel(1).Name)
+ % Overwrite empty channel created by template.
+ iP = 1;
+ else
+ if Digitize.isEditPts
+ % 'iP' points to the 'GlobalData's Channel' which just contains
+ % EEG data and not the fiducials so an offset is required
+ % from 'Digitize.iPoint' to exclude the fiducials
+ if isAdd
+ iP = Digitize.iPoint - 3;
+ else
+ iP = Digitize.iPoint - 2;
+ end
+ else
+ iP = numel(GlobalData.DataSet(Digitize.iDS).Channel) + 1;
+ end
+ end
+
+ if isAdd
+ GlobalData.DataSet(Digitize.iDS).Channel(iP).Name = Digitize.Points(Digitize.iPoint).Label;
+ GlobalData.DataSet(Digitize.iDS).Channel(iP).Type = Digitize.Points(Digitize.iPoint).Type; % 'EEG'
+ GlobalData.DataSet(Digitize.iDS).Channel(iP).Loc = Digitize.Points(Digitize.iPoint).Loc';
+ else % Remove last point or a selected point
+ iP = iP - 1;
+ if iP > 0
+ if Digitize.isEditPts % remove selected point
+ % Keep point in list, but remove location
+ GlobalData.DataSet(Digitize.iDS).Channel(iP).Loc = [];
+ else % Remove last point
+ GlobalData.DataSet(Digitize.iDS).Channel(iP) = [];
+ end
+ end
+ end
+ else % FIDs or head points
+ iP = size(GlobalData.DataSet(Digitize.iDS).HeadPoints.Loc, 2) + 1;
+ if isAdd
+ GlobalData.DataSet(Digitize.iDS).HeadPoints.Label{iP} = Digitize.Points(Digitize.iPoint).Label;
+ GlobalData.DataSet(Digitize.iDS).HeadPoints.Type{iP} = Digitize.Points(Digitize.iPoint).Type; % 'CARDINAL' or 'EXTRA'
+ GlobalData.DataSet(Digitize.iDS).HeadPoints.Loc(:,iP) = Digitize.Points(Digitize.iPoint).Loc';
+ else
+ iP = iP - 1;
+ if iP > 0
+ GlobalData.DataSet(Digitize.iDS).HeadPoints.Label(iP) = [];
+ GlobalData.DataSet(Digitize.iDS).HeadPoints.Type(iP) = [];
+ GlobalData.DataSet(Digitize.iDS).HeadPoints.Loc(:,iP) = [];
+ end
+ end
+ end
+
+ % Remove old HeadPoints
+ hAxes = findobj(Digitize.hFig, '-depth', 1, 'Tag', 'Axes3D');
+ hHeadPointsMarkers = findobj(hAxes, 'Tag', 'HeadPointsMarkers');
+ hHeadPointsLabels = findobj(hAxes, 'Tag', 'HeadPointsLabels');
+ delete(hHeadPointsMarkers);
+ delete(hHeadPointsLabels);
+ % If all EEG were removed, ViewSensors won't remove the last remaining (first) EEG from the figure, so do it manually.
+ if isempty(GlobalData.DataSet(Digitize.iDS).Channel)
+ hSensorMarkers = findobj(hAxes, 'Tag', 'SensorsMarkers');
+ hSensorLabels = findobj(hAxes, 'Tag', 'SensorsLabels');
+ delete(hSensorMarkers);
+ delete(hSensorLabels);
+ end
+ % View all points in the channel file
+ figure_3d('ViewHeadPoints', Digitize.hFig, 1);
+ % This would give error if the channel structure is not truely empty: db_template creates effectively 1 channel with empty fields.
+ if ~isempty(GlobalData.DataSet(Digitize.iDS).Channel) && ~isempty(GlobalData.DataSet(Digitize.iDS).Channel(1).Name)
+ figure_3d('ViewSensors', Digitize.hFig, 1, 1, 0, 'EEG');
+ end
+ % Hide template head surface
+ if ~strcmpi(Digitize.Type, '3DScanner')
+ panel_surface('SetSurfaceTransparency', Digitize.hFig, 1, 1);
+ end
+end
+
+%% ===== SAVE CALLBACK =====
+% This saves a .pos file, which requires first saving the channel file.
+function Save_Callback(OutFile)
+ global Digitize
+ % Do nothing if no points to save
+ if isempty(Digitize.Points) || ~isfield(Digitize.Points, 'Loc') || isempty(Digitize.Points(1).Loc)
+ java_dialog('msgbox', 'No points yet collected. Nothing to save.', 'Save as...', []);
+ return;
+ end
+ sStudy = bst_get('StudyWithCondition', [Digitize.SubjectName '/' Digitize.ConditionName]);
+ ChannelFile = file_fullpath(sStudy.Channel.FileName);
+ SaveDigitizeChannelFile();
+ % Export
+ if nargin > 0 && ~isempty(OutFile)
+ export_channel(ChannelFile, OutFile, 'POLHEMUS', 0);
+ else
+ export_channel(ChannelFile);
+ end
+end
+
+%% ===== SAVE CHANNEL FILE WITH CONTENTS OF POINTS LIST =====
+function SaveDigitizeChannelFile()
+ global Digitize
+ sStudy = bst_get('StudyWithCondition', [Digitize.SubjectName '/' Digitize.ConditionName]);
+ ChannelFile = file_fullpath(sStudy.Channel.FileName);
+ ChannelMat = load(ChannelFile);
+ % GlobalData may not exist here: before 3d figure is created or after it is closed.
+ % So fill in ChannelMat from Digitize.Points.
+ iHead = 0;
+ iChan = 0;
+ % Reset points
+ ChannelMat.Channel = db_template('channeldesc');
+ ChannelMat.HeadPoints.Loc = [];
+ ChannelMat.HeadPoints.Label = [];
+ ChannelMat.HeadPoints.Type = [];
+ for iP = 1:numel(Digitize.Points)
+ % Skip uncollected points
+ if isempty(Digitize.Points(iP).Loc)
+ continue;
+ end
+ if ~isempty(Digitize.Points(iP).Label) && strcmpi(Digitize.Points(iP).Type, 'EEG')
+ % Add EEG sensor locations to channel stucture
+ iChan = iChan + 1;
+ ChannelMat.Channel(iChan).Name = Digitize.Points(iP).Label;
+ ChannelMat.Channel(iChan).Type = Digitize.Points(iP).Type;
+ ChannelMat.Channel(:,iChan).Loc = Digitize.Points(iP).Loc';
+ else % Head points, including fiducials
+ iHead = iHead + 1;
+ ChannelMat.HeadPoints.Loc(:,iHead) = Digitize.Points(iP).Loc';
+ ChannelMat.HeadPoints.Label{iHead} = Digitize.Points(iP).Label;
+ ChannelMat.HeadPoints.Type{iHead} = Digitize.Points(iP).Type;
+ end
+ end
+ bst_save(ChannelFile, ChannelMat, 'v7');
+end
+
+%% ===== CREATE MONTAGE MENU =====
+function CreateMontageMenu(jMenu)
+ import org.brainstorm.icon.*;
+ global Digitize
+
+ % Get menu pointer if not in argument
+ if (nargin < 1) || isempty(jMenu)
+ ctrl = bst_get('PanelControls', 'Digitize');
+ jMenu = ctrl.jMenuEeg;
+ end
+ % Empty menu
+ jMenu.removeAll();
+ % Button group
+ buttonGroup = javax.swing.ButtonGroup();
+ % Display all the montages
+ for i = 1:length(Digitize.Options.Montages)
+ jMenuMontage = gui_component('RadioMenuItem', jMenu, [], Digitize.Options.Montages(i).Name, buttonGroup, [], @(h,ev)bst_call(@SelectMontage, i), []);
+ if (i == 2) && (length(Digitize.Options.Montages) > 2)
+ jMenu.addSeparator();
+ end
+ if (i == Digitize.Options.iMontage)
+ jMenuMontage.setSelected(1);
+ end
+ end
+ % Add new montage / reset list
+ jMenu.addSeparator();
+
+ if strcmpi(Digitize.Type, '3DScanner')
+ jMenuAddMontage = gui_component('Menu', jMenu, [], 'Add EEG montage...', [], [], [], []);
+ gui_component('MenuItem', jMenuAddMontage, [], 'From file...', [], [], @(h,ev)bst_call(@AddMontage), []);
+ % Creating montages from EEG cap layout mat files (only for 3DScanner)
+ jMenuEegCaps = gui_component('Menu', jMenuAddMontage, [], 'From default EEG cap', IconLoader.ICON_CHANNEL, [], [], []);
+ % Use default channel file
+ menu_default_eegcaps(jMenuEegCaps);
+ else % If not 3DScanner
+ gui_component('MenuItem', jMenu, [], 'Add EEG montage...', [], [], @(h,ev)bst_call(@AddMontage), []);
+ end
+ gui_component('MenuItem', jMenu, [], 'Unload all montages', [], [], @(h,ev)bst_call(@UnloadAllMontages), []);
+end
+
+%% ===== SELECT MONTAGE =====
+function SelectMontage(iMontage)
+ global Digitize
+ % Default montage: ask for number of channels
+ if (iMontage == 2)
+ % Get previous number of electrodes
+ nEEG = length(Digitize.Options.Montages(iMontage).Labels);
+ if (nEEG == 0)
+ nEEG = 56;
+ end
+ % Ask user for the number of electrodes
+ res = java_dialog('input', 'Number of EEG channels in your montage:', 'Default EEG montage', [], num2str(nEEG));
+ if isempty(res) || isnan(str2double(res))
+ CreateMontageMenu();
+ return;
+ end
+ nEEG = str2double(res);
+ % Create default montage
+ Digitize.Options.Montages(iMontage).Name = sprintf('Default (%d)', nEEG);
+ Digitize.Options.Montages(iMontage).Labels = {};
+ for i = 1:nEEG
+ if (nEEG > 99)
+ strFormat = 'EEG%03d';
+ else
+ strFormat = 'EEG%02d';
+ end
+ Digitize.Options.Montages(iMontage).Labels{i} = sprintf(strFormat, i);
+ end
+ end
+ % Save currently selected montage
+ Digitize.Options.iMontage = iMontage;
+ % Save Digitize options
+ bst_set('DigitizeOptions', Digitize.Options);
+ % Update menu
+ CreateMontageMenu();
+ % Restart acquisition
+ ResetDataCollection();
+end
+
+%% ===== TOOLTIP TEXT FOR AUTO BUTTON =====
+function autoButtonTooltip = GenerateTooltipTextAuto()
+ global Digitize
+ % Get cap landmark labels for selected montage
+ eegCapLandmarkLabels = channel_detect_eegcap_auto('GetEegCapLandmarkLabels', Digitize.Options.Montages(Digitize.Options.iMontage).Name);
+ autoButtonTooltip = 'Auto localization of EEG sensor is not suported for this cap.';
+ if ~isempty(eegCapLandmarkLabels)
+ strSensors = sprintf('%s, ',eegCapLandmarkLabels{:});
+ strSensors = strSensors(1:end-2);
+ autoButtonTooltip = ['Set at least sensors: [' strSensors '] to enable.'];
+ end
+end
+
+%% ===== GET CURRENT MONTAGE =====
+function [curMontage, nEEG] = GetCurrentMontage()
+ global Digitize
+ % Return current montage
+ curMontage = Digitize.Options.Montages(Digitize.Options.iMontage);
+ nEEG = length(curMontage.Labels);
+end
+
+%% ===== ADD EEG MONTAGE =====
+function AddMontage(ChannelFile)
+ global Digitize
+ % Add Montage from text file
+ if nargin<1
+ % Get recently used folders
+ LastUsedDirs = bst_get('LastUsedDirs');
+ % Open file
+ MontageFile = java_getfile('open', 'Select montage file...', LastUsedDirs.ImportChannel, 'single', 'files', ...
+ {{'*.txt'}, 'Text files', 'TXT'}, 0);
+ if isempty(MontageFile)
+ return;
+ end
+ % Get filename
+ [MontageDir, MontageName] = bst_fileparts(MontageFile);
+ % Intialize new montage
+ newMontage.Name = MontageName;
+ newMontage.Labels = {};
+
+ % Open file
+ fid = fopen(MontageFile,'r');
+ if (fid == -1)
+ error('Cannot open file.');
+ end
+ % Read file
+ while (1)
+ tline = fgetl(fid);
+ if ~ischar(tline)
+ break;
+ end
+ spl = regexp(tline,'\s+','split');
+ if (length(spl) >= 2)
+ newMontage.Labels{end+1} = spl{2};
+ end
+ end
+ % Close file
+ fclose(fid);
+ % If no labels were read: exit
+ if isempty(newMontage.Labels)
+ return
+ end
+ % Save last dir
+ LastUsedDirs.ImportChannel = MontageDir;
+ bst_set('LastUsedDirs', LastUsedDirs);
+ else % Add Montage from mat file of EEG caps
+ % Load existing file
+ ChannelMat = in_bst_channel(ChannelFile);
+
+ % Intialize new montage
+ newMontage.Name = ChannelMat.Comment;
+ newMontage.Labels = {};
+ newMontage.ChannelFile = ChannelFile;
+
+ % Get cap landmark labels
+ eegCapLandmarkLabels = channel_detect_eegcap_auto('GetEegCapLandmarkLabels', newMontage.Name);
+
+ % Sort as per the initialization landmark labels of EEG Cap
+ nonLandmarkLabelsIdx = find(~ismember({ChannelMat.Channel.Name},eegCapLandmarkLabels));
+ allLabels = {ChannelMat.Channel.Name};
+ newMontage.Labels = cat(2, eegCapLandmarkLabels, allLabels(nonLandmarkLabelsIdx));
+ end
+
+ % Get existing montage with the same name
+ iMontage = find(strcmpi({Digitize.Options.Montages.Name}, newMontage.Name));
+ % If not found: create new montage entry
+ if isempty(iMontage)
+ iMontage = length(Digitize.Options.Montages) + 1;
+ else
+ iMontage = iMontage(1);
+ disp('DIGITIZER> Warning: Montage name already exists. Overwriting...');
+ end
+ % Add new montage to registered montages
+ Digitize.Options.Montages(iMontage) = newMontage;
+ Digitize.Options.iMontage = iMontage;
+ % Save options
+ bst_set('DigitizeOptions', Digitize.Options);
+ % Reload Menu
+ CreateMontageMenu();
+ % Restart acquisition
+ ResetDataCollection();
+end
+
+%% ===== UNLOAD ALL MONTAGES =====
+function UnloadAllMontages()
+ global Digitize
+ % Remove all montages
+ Digitize.Options.Montages = [...
+ struct('Name', 'No EEG', ...
+ 'Labels', [], ...
+ 'ChannelFile', []), ...
+ struct('Name', 'Default', ...
+ 'Labels', [], ...
+ 'ChannelFile', [])];
+ % Reset to "No EEG"
+ Digitize.Options.iMontage = 1;
+ % Save Digitize options
+ bst_set('DigitizeOptions', Digitize.Options);
+ % Reload menu bar
+ CreateMontageMenu();
+ % Reset list
+ ResetDataCollection();
+end
+
+
+%% ========================================================================
+% ======= POLHEMUS COMMUNICATION =========================================
+% ========================================================================
+
+%% ===== CREATE SERIAL COLLECTION =====
+function isOk = CreateSerialConnection()
+ global Digitize
+ isOk = 0;
+ while ~isOk
+ % Simulation: exit
+ if Digitize.Options.isSimulate
+ isOk = 1;
+ return;
+ end
+ try
+ % Delete previous connection.
+ if ~isempty(Digitize.SerialConnection)
+ delete(Digitize.SerialConnection);
+ end
+ % Create the serial port connection and store in global variable.
+ Digitize.SerialConnection = serialport(Digitize.Options.ComPort, Digitize.Options.ComRate);
+ if strcmp(Digitize.Options.UnitType,'patriot')
+ configureTerminator(Digitize.SerialConnection, 'CR');
+ else
+ configureTerminator(Digitize.SerialConnection, 'LF');
+ end
+ if Digitize.SerialConnection.NumBytesAvailable > 0
+ flush(Digitize.SerialConnection);
+ end
+
+ % Set up the Bytes Available function
+ configureCallback(Digitize.SerialConnection, 'byte', Digitize.Options.ComByteCount, @BytesAvailable_Callback);
+ if strcmp(Digitize.Options.UnitType, 'fastrak')
+ %'c' - Disable Continuous Printing
+ % Required for some configuration options.
+ writeline(Digitize.SerialConnection,'c');
+ %'u' - Metric Conversion Units (set units to cm)
+ writeline(Digitize.SerialConnection,'u');
+ %'F' - Enable ASCII Output Format
+ writeline(Digitize.SerialConnection,'F');
+ %'R' - Reset Alignment Reference Frame
+ writeline(Digitize.SerialConnection,'R1');
+ writeline(Digitize.SerialConnection,'R2');
+ %'A' - Alignment Reference Frame
+ %'l' - Active Station State
+ % Could check here if 1 and 2 are active.
+ %'N' - Define Tip Offsets % Always factory default on power-up.
+ % writeline(Digitize.SerialConnection,'N1'); data = readline(Digitize.SerialConnection)
+ % data = '21N 6.344 0.013 0.059
+ %'O' - Output Data List
+ writeline(Digitize.SerialConnection,'O1,2,4,1'); % default precision: position, Euler angles, CRLF
+ writeline(Digitize.SerialConnection,'O2,2,4,1'); % default precision: position, Euler angles, CRLF
+ %writeline(Digitize.SerialConnection,'O1,52,54,51'); % extended precision: position, Euler angles, CRLF
+ %writeline(Digitize.SerialConnection,'O2,52,54,51'); % extended precision: position, Euler angles, CRLF
+ %'x' - Position Filter Parameters
+ % The macro setting used here also applies to attitude filtering.
+ % 1=none, 2=low, 3=medium (default), 4=high
+ writeline(Digitize.SerialConnection,'x3');
+
+ %'e' - Define Stylus Button Function
+ writeline(Digitize.SerialConnection,'e1,1'); % Point mode
+
+ % These should be set through the options panel, since they depend on the geometry of setup.
+ % e.g. 'H1,0,0,-1; H2,0,0,-1; Q1,180,90,180,-180,-90,-180; Q2,180,90,180,-180,-90,-180; V1,100,100,100,-100,-100,-100; V2,100,100,100,-100,-100,-100'
+ %'H' - Hemisphere of Operation
+ %writeline(Digitize.SerialConnection,'H1,0,0,-1'); % -Z hemisphere
+ %writeline(Digitize.SerialConnection,'H2,0,0,-1'); % -Z hemisphere
+ %'Q' - Angular Operational Envelope
+ %writeline(Digitize.SerialConnection,'Q1,180,90,180,-180,-90,-180');
+ %writeline(Digitize.SerialConnection,'Q2,180,90,180,-180,-90,-180');
+ %'V' - Position Operational Envelope
+ % Could use to warn if too far.
+ %writeline(Digitize.SerialConnection,'V1,100,100,100,-100,-100,-100');
+ %writeline(Digitize.SerialConnection,'V2,100,100,100,-100,-100,-100');
+
+ %'^K' - *Save Operational Configuration
+ % 'ctrl+K' = char(11)
+ %'^Y' - *Reinitialize System
+ % 'ctrl+Y' = char(25)
+
+ % Apply commands from options after, so they can overwride.
+ for iCmd = 1:numel(Digitize.Options.ConfigCommands)
+ writeline(Digitize.SerialConnection, Digitize.Options.ConfigCommands{iCmd});
+ end
+ elseif strcmp(Digitize.Options.UnitType,'patriot')
+ % Request input from stylus
+ writeline(Digitize.SerialConnection,'L1,1\r');
+ % Set units to centimeters
+ writeline(Digitize.SerialConnection,'U1\r');
+ end
+ pause(0.2);
+ catch %#ok
+ % If the connection cannot be established: error message
+ bst_error(['Cannot open serial connection.' 10 10 'Please check the serial port configuration.' 10], Digitize.Type, 0);
+ % Ask user to edit the port options
+ isChanged = EditSettings();
+ % If edit was canceled: exit
+ if ~isChanged
+ %Digitize.SerialConnection = [];
+ return;
+ % If not, try again
+ else
+ continue;
+ end
+ end
+ isOk = 1;
+ end
+end
+
+
+%% ===== BYTES AVAILABLE CALLBACK =====
+function BytesAvailable_Callback(h, ev) %#ok
+ global Digitize
+ % Get controls
+ ctrl = bst_get('PanelControls', 'Digitize');
+
+ % Simulate: Generate random points
+ if Digitize.Options.isSimulate
+ % Increment current point index
+ Digitize.iPoint = Digitize.iPoint + 1;
+ if Digitize.iPoint > numel(Digitize.Points)
+ Digitize.Points(Digitize.iPoint).Type = 'EXTRA';
+ end
+ if strcmpi(Digitize.Type, '3DScanner')
+ % Get current 3D figure
+ [Digitize.hFig,~,Digitize.iDS] = bst_figures('GetCurrentFigure', '3D');
+ if isempty(Digitize.hFig)
+ return
+ end
+ % Get current selected point
+ CoordinatesSelector = getappdata(Digitize.hFig, 'CoordinatesSelector');
+ isSelectingCoordinates = getappdata(Digitize.hFig, 'isSelectingCoordinates');
+ if isempty(CoordinatesSelector) || isempty(CoordinatesSelector.MRI)
+ return;
+ else
+ if isSelectingCoordinates
+ Digitize.Points(Digitize.iPoint).Loc = CoordinatesSelector.SCS;
+ end
+ end
+ else
+ Digitize.Points(Digitize.iPoint).Loc = rand(1,3) * .15 - .075;
+ end
+
+ % Else: Get digitized point coordinates
+ else
+ vals = zeros(1,7); % header, x, y, z, azimuth, elevation, roll
+ rawpoints = zeros(2,7); % 2 receivers
+ data = [];
+ try
+ for j=1:2 % 1 point * 2 receivers
+ data = char(readline(Digitize.SerialConnection));
+ if strcmp(Digitize.Options.UnitType, 'fastrak')
+ % This is fastrak
+ % The factory default ASCII output record x-y-z-azimuth-elevation-roll is composed of
+ % 47 bytes (3 status bytes, 6 data words each 7 bytes long, and a CR LF terminator)
+ vals(1) = str2double(data(1:3)); % header is first three char
+ for v=2:7
+ % next 6 values are each 7 char
+ ind=(v-1)*7;
+ vals(v) = str2double(data((ind-6)+3:ind+3));
+ end
+ elseif strcmp(Digitize.Options.UnitType, 'patriot')
+ % This is patriot
+ % The factory default ASCII output record x-y-z-azimuth-elevation-roll is composed of
+ % 60 bytes (4 status bytes, 6 data words each 9 bytes long, and a CR LF terminator)
+ vals(1) = str2double(data(1:4)); % header is first 5 char
+ for v=2:7
+ % next 6 values are each 9 char
+ ind=(v-1)*9;
+ vals(v) = str2double(data((ind-8)+4:ind+4));
+ end
+ end
+ rawpoints(j,:) = vals;
+ end
+ catch
+ disp(['Error reading data point. Try again.' 10, ...
+ 'If the problem persits, reset the serial connnection.' 10, ...
+ data]);
+ return;
+ end
+ % Increment current point index
+ Digitize.iPoint = Digitize.iPoint + 1;
+ if Digitize.iPoint > numel(Digitize.Points)
+ Digitize.Points(Digitize.iPoint).Type = 'EXTRA';
+ end
+ % Motion compensation and conversion to meters
+ % This is not converting to SCS, but to another digitizer-specific head-fixed coordinate system.
+ Digitize.Points(Digitize.iPoint).Loc = DoMotionCompensation(rawpoints) ./100; % cm => meters
+ end
+ % Beep at each click
+ if Digitize.Options.isBeep
+ sound(Digitize.BeepWav.data, Digitize.BeepWav.fs);
+ end
+
+ % Transform coordinates
+ if ~isempty(Digitize.Transf) && ~strcmpi(Digitize.Type, '3DScanner')
+ Digitize.Points(Digitize.iPoint).Loc = [Digitize.Points(Digitize.iPoint).Loc 1] * Digitize.Transf';
+ end
+ % Update coordinates list only when there is no updating of selected point
+ % for which the updating happens at the end
+ if ~Digitize.isEditPts
+ UpdateList();
+ end
+
+ % Update counters
+ switch upper(Digitize.Points(Digitize.iPoint).Type)
+ case 'EXTRA'
+ iCount = str2double(ctrl.jTextFieldExtra.getText());
+ ctrl.jTextFieldExtra.setText(num2str(iCount + 1));
+ end
+
+ if ~isempty(Digitize.hFig) && ishandle(Digitize.hFig) && ~strcmpi(Digitize.Points(Digitize.iPoint).Type, 'CARDINAL')
+ % Add this point to the figure
+ % Saves in GlobalData, but NOT in actual channel file
+ PlotCoordinate();
+ end
+
+ % Check distance for fiducials and warn if greater than threshold.
+ if strcmpi(Digitize.Points(Digitize.iPoint).Type, 'CARDINAL') && ...
+ Digitize.iPoint > numel(Digitize.Options.Fids)
+ iSameFid = find(strcmpi({Digitize.Points(1:(Digitize.iPoint-1)).Label}, Digitize.Points(Digitize.iPoint).Label));
+ % Average location of this fiducial point initially, averaging those collected at start only.
+ InitLoc = mean(cat(1, Digitize.Points(iSameFid(1:min(numel(iSameFid),max(1,Digitize.Options.nFidSets)))).Loc), 1);
+ Distance = norm((InitLoc - Digitize.Points(Digitize.iPoint).Loc));
+ if Distance > Digitize.Options.DistThresh
+ ctrl.jLabelWarning.setText(sprintf('%s distance exceeds %1.0f mm', Digitize.Points(Digitize.iPoint).Label, Digitize.Options.DistThresh * 1000));
+ fprintf('%s distance %1.1f mm\n', Digitize.Points(Digitize.iPoint).Label, Distance * 1000);
+ ctrl.jLabelWarning.setOpaque(true);
+ ctrl.jLabelWarning.setBackground(java.awt.Color.red);
+ % Extra beep for large distances
+ pause(0.25);
+ sound(Digitize.BeepWav.data, Digitize.BeepWav.fs);
+ end
+ end
+
+ % When initial fids are all collected
+ if Digitize.iPoint == numel(Digitize.Options.Fids) * Digitize.Options.nFidSets
+ % Save temp pos file
+ TmpDir = bst_get('BrainstormTmpDir');
+ TmpPosFile = bst_fullfile(TmpDir, [Digitize.SubjectName '_' matlab.lang.makeValidName(Digitize.ConditionName) '.pos']);
+ Save_Callback(TmpPosFile);
+ % Re-import that .pos file. This converts to "Native" CTF coil-based coordinates.
+ HeadPointsMat = in_channel_pos(TmpPosFile);
+ % Delete temp file
+ file_delete(TmpPosFile, 1);
+ % Check for coordinate system transformation. There should be only 1, either to Native CTF or to SCS.
+ if ~isfield(HeadPointsMat, 'TransfMegLabels') || ~iscell(HeadPointsMat.TransfMegLabels) || numel(HeadPointsMat.TransfMegLabels) ~= 1
+ error('Missing coordinate transformation');
+ end
+ Digitize.Transf = HeadPointsMat.TransfMeg{1}(1:3,:); % 3x4 transform matrix
+ % Update coordinates in our list
+ for iP = 1:Digitize.iPoint % there could be EEG after, with empty Loc
+ Digitize.Points(iP).Loc = [Digitize.Points(iP).Loc, 1] * Digitize.Transf';
+ end
+ UpdateList();
+ % Update the channel file to save these essential points, and possibly needed for creating figure.
+ SaveDigitizeChannelFile();
+
+ % Create figure, store hFig & iDS
+ CreateHeadpointsFigure();
+ % Enable fids button
+ ctrl.jButtonFids.setEnabled(1);
+ elseif Digitize.iPoint == numel(Digitize.Options.Fids) * Digitize.Options.nFidSets + 1
+ % Change delete button label and callback such that we can delete the last point.
+ java_setcb(ctrl.jButtonDeletePoint, 'ActionPerformedCallback', @(h,ev)bst_call(@DeletePoint_Callback));
+ ctrl.jButtonDeletePoint.setText('Delete last point');
+ end
+
+ % Update coordinate list after the updating the selected point
+ if Digitize.isEditPts
+ % Reset global variable required for updating
+ Digitize.isEditPts = 0;
+ % Update the Digitize.iPoint
+ iNotEmptyLoc = find(cellfun(@(x)~isempty(x), {Digitize.Points.Loc}));
+ Digitize.iPoint = length(iNotEmptyLoc);
+ % Update the coordinate list
+ UpdateList();
+ end
+ % Enable 'Auto' button IFF all landmark fiducials have been acquired
+ if strcmpi(Digitize.Type, '3DScanner') && ~strcmpi(Digitize.Points(Digitize.iPoint).Type, 'EXTRA')
+ eegCapLandmarkLabels = channel_detect_eegcap_auto('GetEegCapLandmarkLabels', Digitize.Options.Montages(Digitize.Options.iMontage).Name);
+ if ~isempty(eegCapLandmarkLabels)
+ acqPoints = Digitize.Points(~cellfun(@isempty, {Digitize.Points.Loc}));
+ if all(ismember([eegCapLandmarkLabels], {acqPoints.Label}))
+ ctrl.jButtonEEGAutoDetectElectrodes.setEnabled(1);
+ end
+ end
+ end
+end
+
+
+%% ===== MOTION COMPENSATION =====
+function newPT = DoMotionCompensation(sensors)
+ % Use sensor one and its orientation vectors as the new coordinate system
+ % Define the origin as the position of sensor attached to the glasses
+ WAND = 1;
+ REMOTE1 = 2;
+
+ C(1) = sensors(REMOTE1,2);
+ C(2) = sensors(REMOTE1,3);
+ C(3) = sensors(REMOTE1,4);
+
+ % Deg2Rad = (angle / 180) * pi
+ % alpha = Deg2Rad(sensors(REMOTE1).o.Azimuth)
+ % beta = Deg2Rad(sensors(REMOTE1).o.Elevation)
+ % gamma = Deg2Rad(sensors(REMOTE1).o.Roll)
+
+ alpha = (sensors(REMOTE1,5)/180) * pi;
+ beta = (sensors(REMOTE1,6)/180) * pi;
+ gamma = (sensors(REMOTE1,7)/180) * pi;
+
+ SA = sin(alpha);
+ SE = sin(beta);
+ SR = sin(gamma);
+ CA = cos(alpha);
+ CE = cos(beta);
+ CR = cos(gamma);
+
+ % Convert Euler angles to directional cosines using formulae in Polhemus manual
+ rotMat(1, 1) = CA * CE;
+ rotMat(1, 2) = SA * CE;
+ rotMat(1, 3) = -SE;
+
+ rotMat(2, 1) = CA * SE * SR - SA * CR;
+ rotMat(2, 2) = CA * CR + SA * SE * SR;
+ rotMat(2, 3) = CE * SR;
+
+ rotMat(3, 1) = CA * SE * CR + SA * SR;
+ rotMat(3, 2) = SA * SE * CR - CA * SR;
+ rotMat(3, 3) = CE * CR;
+
+ rotMat(4, 1:4) = 0;
+
+ % Translate and rotate the WAND into new coordinate system
+ pt(1) = sensors(WAND,2) - C(1);
+ pt(2) = sensors(WAND,3) - C(2);
+ pt(3) = sensors(WAND,4) - C(3);
+
+ newPT(1) = pt(1) * rotMat(1, 1) + pt(2) * rotMat(1, 2) + pt(3) * rotMat(1, 3)'+ rotMat(1, 4);
+ newPT(2) = pt(1) * rotMat(2, 1) + pt(2) * rotMat(2, 2) + pt(3) * rotMat(2, 3)'+ rotMat(2, 4);
+ newPT(3) = pt(1) * rotMat(3, 1) + pt(2) * rotMat(3, 2) + pt(3) * rotMat(3, 3)'+ rotMat(3, 4);
+end
+
diff --git a/toolbox/sensors/private/bst_beep.wav b/toolbox/sensors/private/bst_beep.wav
new file mode 100644
index 000000000..946c0485c
Binary files /dev/null and b/toolbox/sensors/private/bst_beep.wav differ
diff --git a/toolbox/sensors/private/bst_beep_wav.mat b/toolbox/sensors/private/bst_beep_wav.mat
deleted file mode 100644
index d6e8cbe9c..000000000
Binary files a/toolbox/sensors/private/bst_beep_wav.mat and /dev/null differ
diff --git a/toolbox/timefreq/bst_psd.m b/toolbox/timefreq/bst_psd.m
index e5423735a..385e06263 100644
--- a/toolbox/timefreq/bst_psd.m
+++ b/toolbox/timefreq/bst_psd.m
@@ -1,4 +1,4 @@
-function [TF, FreqVector, Nwin, Messages] = bst_psd( F, sfreq, WinLength, WinOverlap, BadSegments, ImagingKernel, isVariance, PowerUnits )
+function [TF, FreqVector, Nwin, Messages, TFbis] = bst_psd( F, sfreq, WinLength, WinOverlap, BadSegments, ImagingKernel, WinFunc, PowerUnits, IsRelative )
% BST_PSD: Compute the PSD of a set of signals using Welch method
% @=============================================================================
@@ -21,13 +21,17 @@
%
% Authors: Francois Tadel, 2012-2017
% Marc Lalancette, 2020
+% Pauline Amrouche, 2024
% Parse inputs
+if (nargin < 9) || isempty(IsRelative)
+ IsRelative = 0;
+end
if (nargin < 8) || isempty(PowerUnits)
PowerUnits = 'physical';
end
-if (nargin < 7) || isempty(isVariance)
- isVariance = 0;
+if (nargin < 7) || isempty(WinFunc)
+ WinFunc = 'mean';
end
if (nargin < 6) || isempty(ImagingKernel)
ImagingKernel = [];
@@ -41,14 +45,26 @@
if (nargin < 3) || isempty(WinLength) || (WinLength == 0)
WinLength = size(F,2) ./ sfreq;
end
+
Messages = '';
% Get sampling frequency
nTime = size(F,2);
% Initialize returned values
TF = [];
+TFbis = [];
+% Initialize frequency and number of windows
FreqVector = [];
Nwin = [];
-Var = [];
+
+% ===== FUNCTION ACROSS WINDOWS =====
+% Backward compatibility with previous versions where winFunc could be 0 (mean) or 1 (std)
+switch lower(WinFunc)
+ case {0, 'mean'}, WinFunc = 'mean';
+ case {1, 'std'}, WinFunc = 'std';
+ case {2, 'mean+std'}, WinFunc = 'mean+std';
+ otherwise, bst_error(['Invalid window aggregating function: ' num2str(lower(WinFunc))]); return
+end
+computeStd = ~isempty(strfind(WinFunc,'std'));
% ===== WINDOWING =====
Lwin = round(WinLength * sfreq);
@@ -75,6 +91,18 @@
% Positive frequency bins spanned by FFT
FreqVector = sfreq / 2 * linspace(0,1,NFFT/2+1);
+% ===== INITIALIZE INTERMEDIATE SUM MATRICES =====
+if ~isempty(ImagingKernel)
+ nChannels = size(ImagingKernel,1);
+else
+ nChannels = size(F,1);
+end
+% Sum of the FFTs for each channel and each frequency bin
+S1 = zeros(nChannels, 1, NFFT/2+1);
+% Sum of the squares of the FFTs for each channel and each frequency bin
+if computeStd
+ S2 = zeros(nChannels, 1, NFFT/2+1);
+end
% ===== CALCULATE FFT FOR EACH WINDOW =====
Nbad = 0;
@@ -133,46 +161,35 @@
TFwin = permute(TFwin, [1 3 2]);
% Convert to power
TFwin = process_tf_measure('Compute', TFwin, 'none', 'power');
-
-
-% %%%%% OLD VERSION: MEAN ONLY %%%%%
-% % Add PSD of the window to the average
-% if isempty(TF)
-% TF = TFwin ./ Nwin;
-% else
-% TF = TF + TFwin ./ Nwin;
-% end
-% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-
- %%%%% NEW VERSION: MEAN AND STD %%%%%
- % If file is first of the list: Initialize returned matrices
- if isempty(TF)
- TF = zeros(size(TFwin));
- if isVariance
- Var = zeros(size(TFwin));
- end
+ % Convert to relative power
+ if IsRelative
+ TFwin = TFwin ./ sum(TFwin, 3);
end
- % Compute mean and standard deviation
- TFwin = TFwin - TF;
- R = TFwin ./ (iWin-Nbad);
- if isVariance
- Var = Var + TFwin .* R .* (iWin-Nbad-1);
+ % Compute sum and sum of squares
+ S1 = S1 + TFwin;
+ if computeStd
+ S2 = S2 + TFwin.^2;
end
- TF = TF + R;
- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-end
-
-% Convert variance to standard deviation
-if isVariance
- Var = Var ./ (Nwin-Nbad - 1);
- TF = sqrt(Var);
end
% Correct the dividing factor if there are bad segments
if (Nbad > 0)
- % TF = TF .* (Nwin ./ (Nwin - Nbad)); % OLD VERSION
Nwin = Nwin - Nbad;
end
+% Compute mean and standard deviation
+TFmean = S1 ./ Nwin;
+if computeStd
+ Var = S2 ./ Nwin - TFmean.^2;
+ TFstd = sqrt(Var);
+end
+
+% Define the matrices to return
+switch WinFunc
+ case 'mean', TF = TFmean; TFbis = [];
+ case 'std', TF = TFstd; TFbis = [];
+ case 'mean+std', TF = TFmean; TFbis = TFstd;
+end
+
% Format message
if isempty(Messages)
Messages = [Messages, sprintf('Using %d windows of %d samples each', Nwin, Lwin)];
diff --git a/toolbox/timefreq/bst_sprint.m b/toolbox/timefreq/bst_sprint.m
index 0da177fcf..01b658c25 100644
--- a/toolbox/timefreq/bst_sprint.m
+++ b/toolbox/timefreq/bst_sprint.m
@@ -1,7 +1,7 @@
function [TF, Messages, OPTIONS] = bst_sprint(F, sfreq, RowNames, OPTIONS)
% BST_SPRiNT: Compute time-resolved specparam models for a set of signals using
% an STFT approach.
-% REFERENCE: Please cite the preprint for the SPRiNT algorithm:
+% REFERENCE: Please cite the article for the SPRiNT algorithm:
% Wilson, L. E., da Silva Castanheira, J., & Baillet, S. (2022).
% Time-resolved parameterization of aperiodic and periodic brain
% activity. eLife, 11, e77348. doi:10.7554/eLife.77348
@@ -25,7 +25,7 @@
% For more information type "brainstorm license" at command prompt.
% =============================================================================@
%
-% Authors: Luc Wilson (2021)
+% Authors: Luc Wilson (2021-2024)
% Fetch user settings
opt = struct();
@@ -38,10 +38,11 @@
opt.min_peak_height = OPTIONS.SPRiNTopts.minpeakheight.Value{1} / 10; % convert from dB to B
opt.aperiodic_mode = OPTIONS.SPRiNTopts.apermode.Value;
opt.peak_threshold = 2; % 2 std dev: parameter for interface simplification
-opt.peak_type = OPTIONS.SPRiNTopts.peaktype.Value;
+opt.peak_type = 'gaussian'; % 'cauchy', for interface simplification
opt.proximity_threshold = OPTIONS.SPRiNTopts.proxthresh.Value{1};
opt.guess_weight = OPTIONS.SPRiNTopts.guessweight.Value;
opt.hOT = 0;
+opt.optim_obj = OPTIONS.SPRiNTopts.optimobj.Value;
opt.thresh_after = true;
opt.rmoutliers = OPTIONS.SPRiNTopts.rmoutliers.Value;
opt.maxfreq = OPTIONS.SPRiNTopts.maxfreq.Value{1};
@@ -60,6 +61,13 @@
disp('Using constrained optimization, Guess Weight ignored.')
end
+dct = 0;
+if size(F,1) > 1 & license('test','distrib_computing_toolbox') % use distributed computing if more than 1 channel
+ dct = 1; % dct = 0; to force disable
+ % to force disable parallel processing, set above to 0;
+ disp('Using parallel processing.')
+end
+
% Get sampling frequency
nTime = size(F,2);
% Initialize returned values
@@ -86,6 +94,19 @@
Lwin = Lwin - mod(Lwin,2); % Make sure the number of samples is even
Nwin = floor((nTime - Loverlap) ./ (Lwin - Loverlap));
end
+% Finally, handle when aggregate window length exceeds recording when
+% considering averaging across sliding window.
+nAvgChanged = 0;
+while (Lwin+Loverlap*(opt.nAverage-1) > nTime)
+ nAvgChanged = 1;
+ Messages = ['Time windows included in average exceed recording length, Reducing number of windows by 1' 10];
+ opt.nAverage = opt.nAverage-1;
+end
+if nAvgChanged
+ disp('Time windows included in average exceed recording length')
+ disp(['Reduced number of windows used in average to: ' num2str(opt.nAverage)])
+end
+
% Next power of 2 from length of signal
% NFFT = 2^nextpow2(Lwin); % Function fft() pads the signal with zeros before computing the FT
NFFT = Lwin; % No zero-padding: Nfft = Ntime
@@ -147,6 +168,16 @@
TF(:,indGood:end,:) = [];
ts(indGood:end) = [];
+switch opt.optim_obj
+ case 'leastsquare' % no knee
+ [TF, OPTIONS] = lse_sprint(TF, FreqVector, ts, opt, OPTIONS, RowNames, dct);
+ case 'negloglike'
+ [TF, OPTIONS] = nll_sprint(TF, FreqVector, ts, opt, OPTIONS, RowNames, dct);
+end
+
+end
+
+function [TF, OPTIONS] = lse_sprint(TF, FreqVector, ts, opt, OPTIONS, RowNames, dct)
% ===== GENERATE SPECPARAM MODELS FOR EACH WINDOW =====
% Find all frequency values within user limits
fMask = (round(FreqVector.*10)./10 >= round(opt.freq_range(1).*10)./10) & (round(FreqVector.*10)./10 <= round(opt.freq_range(2).*10)./10);
@@ -168,110 +199,623 @@
if isa(RowNames,'double')
RowNames = cellstr(num2str(RowNames'));
end
- for chan = 1:nChan
- channel(chan).name = RowNames{chan};
- bst_progress('text',['Standby: SPRiNTing sensor ' num2str(chan) ' of ' num2str(nChan)]);
- channel(chan).data(nTimes) = struct(...
- 'time', [],...
- 'aperiodic_params', [],...
- 'peak_params', [],...
- 'peak_types', '',...
- 'ap_fit', [],...
- 'fooofed_spectrum', [],...
- 'power_spectrum', [],...
- 'peak_fit', [],...
- 'error', [],...
- 'r_squared', []);
- channel(chan).peaks(nTimes*opt.max_peaks) = struct(...
- 'time', [],...
- 'center_frequency', [],...
- 'amplitude', [],...
- 'st_dev', []);
- channel(chan).aperiodics(nTimes) = struct(...
- 'time', [],...
- 'offset', [],...
- 'exponent', []);
- channel(chan).stats(nTimes) = struct(...
- 'MSE', [],...
- 'r_squared', [],...
- 'frequency_wise_error', []);
- spec = log10(squeeze(TF(chan,:,:))); % extract log spectra for a given channel
- % Iterate across time
- i = 1; % For peak extraction
- ag = -(spec(1,end)-spec(1,1))./log10(fs(end)./fs(1)); % aperiodic guess initialization
- for time = 1:nTimes
- bst_progress('set', bst_round(time / nTimes,2).*100);
- % Fit aperiodic
- aperiodic_pars = robust_ap_fit(fs, spec(time,:), opt.aperiodic_mode, ag);
- % Remove aperiodic
- flat_spec = flatten_spectrum(fs, spec(time,:), aperiodic_pars, opt.aperiodic_mode);
- % Fit peaks
- [peak_pars, peak_function] = fit_peaks(fs, flat_spec, opt.max_peaks, opt.peak_threshold, opt.min_peak_height, ...
- opt.peak_width_limits/2, opt.proximity_threshold, opt.peak_type, opt.guess_weight,opt.hOT);
- if opt.thresh_after && ~opt.hOT % Check thresholding requirements are met for unbounded optimization
- peak_pars(peak_pars(:,2) < opt.min_peak_height,:) = []; % remove peaks shorter than limit
- peak_pars(peak_pars(:,3) < opt.peak_width_limits(1)/2,:) = []; % remove peaks narrower than limit
- peak_pars(peak_pars(:,3) > opt.peak_width_limits(2)/2,:) = []; % remove peaks broader than limit
- peak_pars = drop_peak_cf(peak_pars, opt.proximity_threshold, opt.freq_range); % remove peaks outside frequency limits
- peak_pars(peak_pars(:,1) < 0,:) = []; % remove peaks with a centre frequency less than zero (bypass drop_peak_cf)
- peak_pars = drop_peak_overlap(peak_pars, opt.proximity_threshold); % remove smallest of two peaks fit too closely
- end
- % Refit aperiodic
- aperiodic = spec(time,:);
- for peak = 1:size(peak_pars,1)
- aperiodic = aperiodic - peak_function(fs,peak_pars(peak,1), peak_pars(peak,2), peak_pars(peak,3));
+ if ~dct
+ for chan = 1:nChan
+ channel(chan).name = RowNames{chan};
+ bst_progress('text',['Standby: SPRiNTing sensor ' num2str(chan) ' of ' num2str(nChan)]);
+ channel(chan).data(nTimes) = struct(...
+ 'time', [],...
+ 'aperiodic_params', [],...
+ 'peak_params', [],...
+ 'peak_types', '',...
+ 'ap_fit', [],...
+ 'fooofed_spectrum', [],...
+ 'power_spectrum', [],...
+ 'peak_fit', [],...
+ 'error', [],...
+ 'r_squared', []);
+ channel(chan).peaks(nTimes*opt.max_peaks) = struct(...
+ 'time', [],...
+ 'center_frequency', [],...
+ 'amplitude', [],...
+ 'st_dev', []);
+ channel(chan).aperiodics(nTimes) = struct(...
+ 'time', [],...
+ 'offset', [],...
+ 'exponent', []);
+ channel(chan).stats(nTimes) = struct(...
+ 'MSE', [],...
+ 'r_squared', [],...
+ 'frequency_wise_error', []);
+ spec = log10(squeeze(TF(chan,:,:))); % extract log spectra for a given channel
+ % Iterate across time
+ i = 1; % For peak extraction
+ ag = -(spec(1,end)-spec(1,1))./log10(fs(end)./fs(1)); % aperiodic guess initialization
+ for time = 1:nTimes
+ bst_progress('set', bst_round(time / nTimes,2).*100);
+ % Fit aperiodic
+ aperiodic_pars = robust_ap_fit(fs, spec(time,:), opt.aperiodic_mode, ag);
+ % Remove aperiodic
+ flat_spec = flatten_spectrum(fs, spec(time,:), aperiodic_pars, opt.aperiodic_mode);
+ % Fit peaks
+ [peak_pars, pk_function] = fit_peaks(fs, flat_spec, opt.max_peaks, opt.peak_threshold, opt.min_peak_height, ...
+ opt.peak_width_limits/2, opt.proximity_threshold, opt.peak_type, opt.guess_weight,opt.hOT);
+ if opt.thresh_after && ~opt.hOT % Check thresholding requirements are met for unbounded optimization
+ peak_pars(peak_pars(:,2) < opt.min_peak_height,:) = []; % remove peaks shorter than limit
+ peak_pars(peak_pars(:,3) < opt.peak_width_limits(1)/2,:) = []; % remove peaks narrower than limit
+ peak_pars(peak_pars(:,3) > opt.peak_width_limits(2)/2,:) = []; % remove peaks broader than limit
+ peak_pars = drop_peak_cf(peak_pars, opt.proximity_threshold, opt.freq_range); % remove peaks outside frequency limits
+ peak_pars(peak_pars(:,1) < 0,:) = []; % remove peaks with a centre frequency less than zero (bypass drop_peak_cf)
+ peak_pars = drop_peak_overlap(peak_pars, opt.proximity_threshold); % remove smallest of two peaks fit too closely
+ end
+ % Refit aperiodic
+ aperiodic = spec(time,:);
+ for peak = 1:size(peak_pars,1)
+ aperiodic = aperiodic - pk_function(fs,peak_pars(peak,1), peak_pars(peak,2), peak_pars(peak,3));
+ end
+ aperiodic_pars = simple_ap_fit(fs, aperiodic, opt.aperiodic_mode, aperiodic_pars(end));
+ ag = aperiodic_pars(end); % save aperiodic estimate for next iteration
+ % Generate model fit
+ ap_fit = gen_aperiodic(fs, aperiodic_pars, opt.aperiodic_mode);
+ model_fit = ap_fit;
+ for peak = 1:size(peak_pars,1)
+ model_fit = model_fit + pk_function(fs,peak_pars(peak,1),...
+ peak_pars(peak,2),peak_pars(peak,3));
+ end
+ % Calculate model error
+ MSE = sum((spec(time,:) - model_fit).^2)/length(model_fit);
+ rsq_tmp = corrcoef(spec(time,:),model_fit).^2;
+ % Return FOOOF results
+ aperiodic_pars(2) = abs(aperiodic_pars(2));
+ channel(chan).data(time).time = ts(time);
+ channel(chan).data(time).aperiodic_params = aperiodic_pars;
+ channel(chan).data(time).peak_params = peak_pars;
+ channel(chan).data(time).peak_types = func2str(pk_function);
+ channel(chan).data(time).ap_fit = 10.^ap_fit;
+ aperiodic_models(chan,time,:) = 10.^ap_fit;
+ channel(chan).data(time).fooofed_spectrum = 10.^model_fit;
+ SPRiNT_models(chan,time,:) = 10.^model_fit;
+ channel(chan).data(time).power_spectrum = 10.^spec(time,:);
+ channel(chan).data(time).peak_fit = 10.^(model_fit-ap_fit);
+ peak_models(chan,time,:) = 10.^(model_fit-ap_fit);
+ channel(chan).data(time).error = MSE;
+ channel(chan).data(time).r_squared = rsq_tmp(2);
+ % Extract peaks
+ if ~isempty(peak_pars) & any(peak_pars)
+ for p = 1:size(peak_pars,1)
+ channel(chan).peaks(i).time = ts(time);
+ channel(chan).peaks(i).center_frequency = peak_pars(p,1);
+ channel(chan).peaks(i).amplitude = peak_pars(p,2);
+ channel(chan).peaks(i).st_dev = peak_pars(p,3);
+ i = i +1;
+ end
+ end
+ % Extract aperiodic
+ channel(chan).aperiodics(time).time = ts(time);
+ channel(chan).aperiodics(time).offset = aperiodic_pars(1);
+ if length(aperiodic_pars)>2 % Legacy FOOOF alters order of parameters
+ channel(chan).aperiodics(time).exponent = aperiodic_pars(3);
+ channel(chan).aperiodics(time).knee_frequency = aperiodic_pars(2);
+ else
+ channel(chan).aperiodics(time).exponent = aperiodic_pars(2);
+ end
+ channel(chan).stats(time).MSE = MSE;
+ channel(chan).stats(time).r_squared = rsq_tmp(2);
+ channel(chan).stats(time).frequency_wise_error = abs(spec(time,:)-model_fit);
end
- aperiodic_pars = simple_ap_fit(fs, aperiodic, opt.aperiodic_mode, aperiodic_pars(end));
- ag = aperiodic_pars(end); % save aperiodic estimate for next iteration
- % Generate model fit
- ap_fit = gen_aperiodic(fs, aperiodic_pars, opt.aperiodic_mode);
- model_fit = ap_fit;
- for peak = 1:size(peak_pars,1)
- model_fit = model_fit + peak_function(fs,peak_pars(peak,1),...
- peak_pars(peak,2),peak_pars(peak,3));
+ channel(chan).peaks(i:end) = [];
+ end
+ else
+ bst_progress('text','Standby: Parallel SPRiNTing channels');
+ parfor chan = 1:nChan
+ channel(chan).name = RowNames{chan};
+ channel(chan).data(nTimes) = struct(...
+ 'time', [],...
+ 'aperiodic_params', [],...
+ 'peak_params', [],...
+ 'peak_types', '',...
+ 'ap_fit', [],...
+ 'fooofed_spectrum', [],...
+ 'power_spectrum', [],...
+ 'peak_fit', [],...
+ 'error', [],...
+ 'r_squared', []);
+ channel(chan).peaks(nTimes*opt.max_peaks) = struct(...
+ 'time', [],...
+ 'center_frequency', [],...
+ 'amplitude', [],...
+ 'st_dev', []);
+ channel(chan).aperiodics(nTimes) = struct(...
+ 'time', [],...
+ 'offset', [],...
+ 'exponent', []);
+ channel(chan).stats(nTimes) = struct(...
+ 'MSE', [],...
+ 'r_squared', [],...
+ 'frequency_wise_error', []);
+ spec = log10(squeeze(TF(chan,:,:))); % extract log spectra for a given channel
+ % Iterate across time
+ i = 1; % For peak extraction
+ ag = -(spec(1,end)-spec(1,1))./log10(fs(end)./fs(1)); % aperiodic guess initialization
+ for time = 1:nTimes
+ % Fit aperiodic
+ aperiodic_pars = robust_ap_fit(fs, spec(time,:), opt.aperiodic_mode, ag);
+ % Remove aperiodic
+ flat_spec = flatten_spectrum(fs, spec(time,:), aperiodic_pars, opt.aperiodic_mode);
+ % Fit peaks
+ [peak_pars, pk_function] = fit_peaks(fs, flat_spec, opt.max_peaks, opt.peak_threshold, opt.min_peak_height, ...
+ opt.peak_width_limits/2, opt.proximity_threshold, opt.peak_type, opt.guess_weight,opt.hOT);
+ if opt.thresh_after && ~opt.hOT % Check thresholding requirements are met for unbounded optimization
+ peak_pars(peak_pars(:,2) < opt.min_peak_height,:) = []; % remove peaks shorter than limit
+ peak_pars(peak_pars(:,3) < opt.peak_width_limits(1)/2,:) = []; % remove peaks narrower than limit
+ peak_pars(peak_pars(:,3) > opt.peak_width_limits(2)/2,:) = []; % remove peaks broader than limit
+ peak_pars = drop_peak_cf(peak_pars, opt.proximity_threshold, opt.freq_range); % remove peaks outside frequency limits
+ peak_pars(peak_pars(:,1) < 0,:) = []; % remove peaks with a centre frequency less than zero (bypass drop_peak_cf)
+ peak_pars = drop_peak_overlap(peak_pars, opt.proximity_threshold); % remove smallest of two peaks fit too closely
+ end
+ % Refit aperiodic
+ aperiodic = spec(time,:);
+ for peak = 1:size(peak_pars,1)
+ aperiodic = aperiodic - pk_function(fs,peak_pars(peak,1), peak_pars(peak,2), peak_pars(peak,3));
+ end
+ aperiodic_pars = simple_ap_fit(fs, aperiodic, opt.aperiodic_mode, aperiodic_pars(end));
+ ag = aperiodic_pars(end); % save aperiodic estimate for next iteration
+ % Generate model fit
+ ap_fit = gen_aperiodic(fs, aperiodic_pars, opt.aperiodic_mode);
+ model_fit = ap_fit;
+ for peak = 1:size(peak_pars,1)
+ model_fit = model_fit + pk_function(fs,peak_pars(peak,1),...
+ peak_pars(peak,2),peak_pars(peak,3));
+ end
+ % Calculate model error
+ MSE = sum((spec(time,:) - model_fit).^2)/length(model_fit);
+ rsq_tmp = corrcoef(spec(time,:),model_fit).^2;
+ % Return FOOOF results
+ aperiodic_pars(2) = abs(aperiodic_pars(2));
+ channel(chan).data(time).time = ts(time);
+ channel(chan).data(time).aperiodic_params = aperiodic_pars;
+ channel(chan).data(time).peak_params = peak_pars;
+ channel(chan).data(time).peak_types = func2str(pk_function);
+ channel(chan).data(time).ap_fit = 10.^ap_fit;
+ aperiodic_models(chan,time,:) = 10.^ap_fit;
+ channel(chan).data(time).fooofed_spectrum = 10.^model_fit;
+ SPRiNT_models(chan,time,:) = 10.^model_fit;
+ channel(chan).data(time).power_spectrum = 10.^spec(time,:);
+ channel(chan).data(time).peak_fit = 10.^(model_fit-ap_fit);
+ peak_models(chan,time,:) = 10.^(model_fit-ap_fit);
+ channel(chan).data(time).error = MSE;
+ channel(chan).data(time).r_squared = rsq_tmp(2);
+ % Extract peaks
+ if ~isempty(peak_pars) & any(peak_pars)
+ for p = 1:size(peak_pars,1)
+ channel(chan).peaks(i).time = ts(time);
+ channel(chan).peaks(i).center_frequency = peak_pars(p,1);
+ channel(chan).peaks(i).amplitude = peak_pars(p,2);
+ channel(chan).peaks(i).st_dev = peak_pars(p,3);
+ i = i +1;
+ end
+ end
+ % Extract aperiodic
+ channel(chan).aperiodics(time).time = ts(time);
+ channel(chan).aperiodics(time).offset = aperiodic_pars(1);
+ if length(aperiodic_pars)>2 % Legacy FOOOF alters order of parameters
+ channel(chan).aperiodics(time).exponent = aperiodic_pars(3);
+ channel(chan).aperiodics(time).knee_frequency = aperiodic_pars(2);
+ else
+ channel(chan).aperiodics(time).exponent = aperiodic_pars(2);
+ end
+ channel(chan).stats(time).MSE = MSE;
+ channel(chan).stats(time).r_squared = rsq_tmp(2);
+ channel(chan).stats(time).frequency_wise_error = abs(spec(time,:)-model_fit);
end
- % Calculate model error
- MSE = sum((spec(time,:) - model_fit).^2)/length(model_fit);
- rsq_tmp = corrcoef(spec(time,:),model_fit).^2;
- % Return FOOOF results
- aperiodic_pars(2) = abs(aperiodic_pars(2));
- channel(chan).data(time).time = ts(time);
- channel(chan).data(time).aperiodic_params = aperiodic_pars;
- channel(chan).data(time).peak_params = peak_pars;
- channel(chan).data(time).peak_types = func2str(peak_function);
- channel(chan).data(time).ap_fit = 10.^ap_fit;
- aperiodic_models(chan,time,:) = 10.^ap_fit;
- channel(chan).data(time).fooofed_spectrum = 10.^model_fit;
- SPRiNT_models(chan,time,:) = 10.^model_fit;
- channel(chan).data(time).power_spectrum = 10.^spec(time,:);
- channel(chan).data(time).peak_fit = 10.^(model_fit-ap_fit);
- peak_models(chan,time,:) = 10.^(model_fit-ap_fit);
- channel(chan).data(time).error = MSE;
- channel(chan).data(time).r_squared = rsq_tmp(2);
- % Extract peaks
- if ~isempty(peak_pars) & any(peak_pars)
- for p = 1:size(peak_pars,1)
- channel(chan).peaks(i).time = ts(time);
- channel(chan).peaks(i).center_frequency = peak_pars(p,1);
- channel(chan).peaks(i).amplitude = peak_pars(p,2);
- channel(chan).peaks(i).st_dev = peak_pars(p,3);
- i = i +1;
+ channel(chan).peaks(i:end) = [];
+ end
+ end
+ SPRiNT.channel = channel;
+ SPRiNT.aperiodic_models = aperiodic_models;
+ SPRiNT.SPRiNT_models = SPRiNT_models;
+ SPRiNT.peak_models = peak_models;
+ if strcmp(opt.rmoutliers,'yes')
+ bst_progress('text','Standby: Removing outlier peaks');
+ SPRiNT = remove_outliers(SPRiNT,@gaussian,opt);
+ end
+ for chan = 1:nChan
+ tp_exponent(chan,:) = [SPRiNT.channel(chan).aperiodics(:).exponent];
+ tp_offset(chan,:) = [SPRiNT.channel(chan).aperiodics(:).offset];
+ end
+ SPRiNT.topography.exponent = tp_exponent;
+ SPRiNT.topography.offset = tp_offset;
+ bst_progress('text','Standby: Clustering modelled peaks');
+ SPRiNT = cluster_peaks_dynamic(SPRiNT); % Cluster peaks
+ OPTIONS.TimeVector = ts'; % Reassign times by windows used
+ TF = sqrt(TF); % remove power transformation
+ OPTIONS.SPRiNT = SPRiNT;
+
+end
+
+function [TF, OPTIONS] = nll_sprint(TF, FreqVector, ts, opt, OPTIONS, RowNames, dct)
+
+% ===== GENERATE SPECPARAM MODELS FOR EACH WINDOW =====
+% Find all frequency values within user limits
+ fMask = (round(FreqVector.*10)./10 >= round(opt.freq_range(1).*10)./10) & (round(FreqVector.*10)./10 <= round(opt.freq_range(2).*10)./10);
+ fs = FreqVector(fMask);
+ lfdif = log10(fs(end)./fs(1));
+ mp = opt.max_peaks;
+ am = opt.aperiodic_mode;
+ pet = opt.peak_threshold;
+ mph = opt.min_peak_height;
+ pwl = opt.peak_width_limits./2;
+ prt = opt.proximity_threshold;
+ pt = opt.peak_type;
+ gw = opt.guess_weight;
+ hOT = opt.hOT;
+ OPTIONS.Freqs = fs;
+ nChan = size(TF,1);
+ nTimes = size(TF,2);
+ % Adjust TF plots to only include modelled frequencies
+ TF = TF(:,:,fMask);
+ % Initalize FOOOF structs
+ channel(nChan) = struct('name',[]);
+ SPRiNT = struct('options',opt,'freqs',fs,'channel',channel,'SPRiNT_models',nan(size(TF)),'peak_models',nan(size(TF)),'aperiodic_models',nan(size(TF)));
+ % Iterate across channels
+ aperiodic_models = nan(nChan,nTimes,length(fs));
+ peak_models = nan(nChan,nTimes,length(fs));
+ SPRiNT_models = nan(nChan,nTimes,length(fs));
+ tp_exponent = nan(nChan,nTimes);
+ tp_offset = nan(nChan,nTimes);
+ if isa(RowNames,'double')
+ RowNames = cellstr(num2str(RowNames'));
+ end
+ switch opt.peak_type
+ case 'gaussian' % gaussian only
+ peak_function = @gaussian;
+ case 'cauchy'
+ peak_function = @cauchy;
+ end
+ if ~dct
+ for chan = 1:nChan
+ channel(chan).name = RowNames{chan};
+ bst_progress('text',['Standby: ms-SPRiNTing sensor ' num2str(chan) ' of ' num2str(nChan)]);
+ channel(chan).data(nTimes) = struct(...
+ 'time', [],...
+ 'aperiodic_params', [],...
+ 'peak_params', [],...
+ 'peak_types', '',...
+ 'ap_fit', [],...
+ 'fooofed_spectrum', [],...
+ 'power_spectrum', [],...
+ 'peak_fit', [],...
+ 'error', [],...
+ 'r_squared', []);
+ channel(chan).peaks(nTimes*mp) = struct(...
+ 'time', [],...
+ 'center_frequency', [],...
+ 'amplitude', [],...
+ 'st_dev', []);
+ channel(chan).aperiodics(nTimes) = struct(...
+ 'time', [],...
+ 'offset', [],...
+ 'exponent', []);
+ channel(chan).stats(nTimes) = struct(...
+ 'MSE', [],...
+ 'r_squared', [],...
+ 'frequency_wise_error', []);
+ spec = log10(squeeze(TF(chan,:,:))); % extract log spectra for a given channel
+ % Iterate across time
+ i = 1; % For peak extraction
+ ag = -(spec(1,end)-spec(1,1))./lfdif; % aperiodic guess initialization
+ for time = 1:nTimes
+ bst_progress('set', bst_round(time / nTimes,2).*100);
+ % Fit aperiodic
+ aperiodic_pars = robust_ap_fit(fs, spec(time,:), am, ag);
+ % Remove aperiodic
+ flat_spec = flatten_spectrum(fs, spec(time,:), aperiodic_pars, am);
+ try
+ [est_pars, pk_function] = est_peaks(fs, flat_spec, mp, pet, mph, ...
+ pwl, prt, pt);
+ catch
+ error(['Failure fitting peaks: channel ' num2str(chan) ', time index ' num2str(time)])
end
+ model = struct();
+ for pk = 0:size(est_pars,1)
+ peak_pars = est_fit(est_pars(1:pk,:), fs, flat_spec, pwl, pt, gw, hOT);
+ % Refit aperiodic
+ aperiodic = spec(time,:);
+ for peak = 1:size(peak_pars,1)
+ aperiodic = aperiodic - pk_function(fs,peak_pars(peak,1), peak_pars(peak,2), peak_pars(peak,3));
+ end
+ aperiodic_pars = simple_ap_fit(fs, aperiodic, am, aperiodic_pars(2));
+ guess = peak_pars;
+ if ~isempty(guess)
+ lb = [max([ones(size(guess(1:pk,:),1),1).*fs(1) guess(1:pk,1)-guess(1:pk,3)*2],[],2),zeros(size(guess(1:pk,2))),ones(size(guess(1:pk,3)))*pwl(1)]';
+ ub = [min([ones(size(guess(1:pk,:),1),1).*fs(end) guess(1:pk,1)+guess(1:pk,3)*2],[],2),inf(size(guess(1:pk,2))),ones(size(guess(1:pk,3)))*pwl(2)]';
+
+ else
+ lb = [];
+ ub = [];
+ end
+ switch am
+ case 'fixed'
+ lb = [-inf; 0; lb(:)];
+ ub = [inf; inf; ub(:)];
+ case 'knee'
+ lb = [-inf; 0; 0; lb(:)];
+ ub = [inf; 100; inf; ub(:)];
+ end
+
+ guess = guess(1:pk,:)';
+ guess = [aperiodic_pars'; guess(:)];
+ options = optimset('Display', 'off', 'TolX', 1e-7, 'TolFun', 1e-9, ...
+ 'MaxFunEvals', 5000, 'MaxIter', 5000); % Tuned options
+ try
+ params = fmincon(@err_fm_constr, guess, [], [], [], [], ...
+ lb, ub, [], options, fs, spec(time,:), am, pt);
+ catch
+ error(['Optimization failed to converge: channel ' num2str(chan) ', time index ' num2str(time)]);
+ end
+ switch am
+ case 'fixed'
+ aperiodic_pars_tmp = params(1:2);
+ if length(params) > 3
+ peak_pars_tmp = reshape(params(3:end),[3 length(params(3:end))./3])';
+ end
+ case 'knee'
+ aperiodic_pars_tmp = params(1:3);
+ if length(params) > 3
+ peak_pars_tmp = reshape(params(4:end),[3 length(params(4:end))./3])';
+ end
+ end
+ % Generate model fit
+ ap_fit = gen_aperiodic(fs, aperiodic_pars_tmp, am);
+ model_fit = ap_fit;
+ if length(params) > 3
+ for peak = 1:size(peak_pars_tmp,1)
+ model_fit = model_fit + peak_function(fs,peak_pars_tmp(peak,1),...
+ peak_pars_tmp(peak,2),peak_pars_tmp(peak,3));
+ end
+ else
+ peak_pars_tmp = [0 0 0];
+ end
+ % Calculate model error
+ MSE = sum((spec(time,:) - model_fit).^2)/length(model_fit);
+ rsq_tmp = corrcoef(spec(time,:),model_fit).^2;
+ loglik = -length(model_fit)/2.*(1+log(MSE)+log(2*pi));
+ AIC = 2.*(length(params)-loglik);
+ BIC = length(params).*log(length(model_fit))-2.*loglik;
+ model(pk+1).aperiodic_params = aperiodic_pars_tmp;
+ model(pk+1).peak_params = peak_pars_tmp;
+ model(pk+1).MSE = MSE;
+ model(pk+1).r_squared = rsq_tmp(2);
+ model(pk+1).loglik = loglik;
+ model(pk+1).AIC = AIC;
+ model(pk+1).BIC = BIC;
+ model(pk+1).BF = exp((BIC-model(1).BIC)./2);
+ end
+
+ % insert data from best model
+ [~,mi] = min([model.BIC]);
+ aperiodic_pars = model(mi).aperiodic_params;
+ peak_pars = model(mi).peak_params;
+ % Return FOOOF results
+ aperiodic_pars(2) = abs(aperiodic_pars(2));
+ channel(chan).data(time).time = ts(time);
+ channel(chan).data(time).aperiodic_params = aperiodic_pars;
+ channel(chan).data(time).peak_params = peak_pars;
+ channel(chan).data(time).peak_types = func2str(peak_function);
+ channel(chan).data(time).ap_fit = 10.^gen_aperiodic(fs, aperiodic_pars, am);
+ aperiodic_models(chan,time,:) = channel(chan).data(time).ap_fit;
+ channel(chan).data(time).fooofed_spectrum = 10.^build_model(fs, aperiodic_pars, opt.aperiodic_mode, peak_pars, peak_function);
+ SPRiNT_models(chan,time,:) = channel(chan).data(time).fooofed_spectrum;
+ channel(chan).data(time).power_spectrum = 10.^spec(time,:);
+ channel(chan).data(time).peak_fit = 10.^(SPRiNT_models(chan,time,:)-aperiodic_models(chan,time,:));
+ peak_models(chan,time,:) = 10.^(SPRiNT_models(chan,time,:)-aperiodic_models(chan,time,:));
+ channel(chan).data(time).error = model(mi).MSE;
+ channel(chan).data(time).r_squared = model(mi).r_squared;
+ channel(chan).data(time).loglik = model(mi).loglik; % log-likelihood
+ channel(chan).data(time).AIC = model(mi).AIC;
+ channel(chan).data(time).BIC = model(mi).BIC;
+ channel(chan).data(time).models = model;
+ % Extract peaks
+ if ~isempty(peak_pars) & any(peak_pars)
+ for p = 1:size(peak_pars,1)
+ channel(chan).peaks(i).time = ts(time);
+ channel(chan).peaks(i).center_frequency = peak_pars(p,1);
+ channel(chan).peaks(i).amplitude = peak_pars(p,2);
+ channel(chan).peaks(i).st_dev = peak_pars(p,3);
+ i = i +1;
+ end
+ end
+ % Extract aperiodic
+ channel(chan).aperiodics(time).time = ts(time);
+ channel(chan).aperiodics(time).offset = aperiodic_pars(1);
+ if length(aperiodic_pars)>2 % Legacy FOOOF alters order of parameters
+ channel(chan).aperiodics(time).exponent = aperiodic_pars(3);
+ channel(chan).aperiodics(time).knee_frequency = aperiodic_pars(2);
+ else
+ channel(chan).aperiodics(time).exponent = aperiodic_pars(2);
+ end
+ channel(chan).stats(time).MSE = MSE;
+ channel(chan).stats(time).r_squared = rsq_tmp(2);
+ channel(chan).stats(time).frequency_wise_error = abs(spec(time,:)-log10(channel(chan).data(time).fooofed_spectrum));
end
- % Extract aperiodic
- channel(chan).aperiodics(time).time = ts(time);
- channel(chan).aperiodics(time).offset = aperiodic_pars(1);
- if length(aperiodic_pars)>2 % Legacy FOOOF alters order of parameters
- channel(chan).aperiodics(time).exponent = aperiodic_pars(3);
- channel(chan).aperiodics(time).knee_frequency = aperiodic_pars(2);
- else
- channel(chan).aperiodics(time).exponent = aperiodic_pars(2);
+ channel(chan).peaks(i:end) = [];
+ end
+ else
+ bst_progress('text','Standby: Parallel ms-SPRiNTing channels');
+ parfor chan = 1:nChan
+ channel(chan).name = RowNames{chan};
+ bst_progress('text',['Standby: ms-SPRiNTing sensor ' num2str(chan) ' of ' num2str(nChan)]);
+ channel(chan).data(nTimes) = struct(...
+ 'time', [],...
+ 'aperiodic_params', [],...
+ 'peak_params', [],...
+ 'peak_types', '',...
+ 'ap_fit', [],...
+ 'fooofed_spectrum', [],...
+ 'power_spectrum', [],...
+ 'peak_fit', [],...
+ 'error', [],...
+ 'r_squared', []);
+ channel(chan).peaks(nTimes*mp) = struct(...
+ 'time', [],...
+ 'center_frequency', [],...
+ 'amplitude', [],...
+ 'st_dev', []);
+ channel(chan).aperiodics(nTimes) = struct(...
+ 'time', [],...
+ 'offset', [],...
+ 'exponent', []);
+ channel(chan).stats(nTimes) = struct(...
+ 'MSE', [],...
+ 'r_squared', [],...
+ 'frequency_wise_error', []);
+ spec = log10(squeeze(TF(chan,:,:))); % extract log spectra for a given channel
+ % Iterate across time
+ i = 1; % For peak extraction
+ ag = -(spec(1,end)-spec(1,1))./lfdif; % aperiodic guess initialization
+ for time = 1:nTimes
+ bst_progress('set', bst_round(time / nTimes,2).*100);
+ est_pars = [];
+ pk_function = [];
+ MSE = [];
+ rsq_tmp = [];
+ % Fit aperiodic
+ aperiodic_pars = robust_ap_fit(fs, spec(time,:), am, ag);
+ % Remove aperiodic
+ flat_spec = flatten_spectrum(fs, spec(time,:), aperiodic_pars, am);
+ try
+ [est_pars, pk_function] = est_peaks(fs, flat_spec, mp, pet, mph, ...
+ pwl, prt, pt);
+ catch
+ error(['Failure fitting peaks: channel ' num2str(chan) ', time index ' num2str(time)])
+ end
+ model = struct();
+ for pk = 0:size(est_pars,1)
+ params = [];
+ aperiodic_pars_tmp = [];
+ peak_pars_tmp = [];
+ peak_pars = est_fit(est_pars(1:pk,:), fs, flat_spec, pwl, pt, gw, hOT);
+ % Refit aperiodic
+ aperiodic = spec(time,:);
+ for peak = 1:size(peak_pars,1)
+ aperiodic = aperiodic - pk_function(fs,peak_pars(peak,1), peak_pars(peak,2), peak_pars(peak,3));
+ end
+ aperiodic_pars = simple_ap_fit(fs, aperiodic, am, aperiodic_pars(2));
+ guess = peak_pars;
+ if ~isempty(guess)
+ lb = [max([ones(size(guess(1:pk,:),1),1).*fs(1) guess(1:pk,1)-guess(1:pk,3)*2],[],2),zeros(size(guess(1:pk,2))),ones(size(guess(1:pk,3)))*pwl(1)]';
+ ub = [min([ones(size(guess(1:pk,:),1),1).*fs(end) guess(1:pk,1)+guess(1:pk,3)*2],[],2),inf(size(guess(1:pk,2))),ones(size(guess(1:pk,3)))*pwl(2)]';
+
+ else
+ lb = [];
+ ub = [];
+ end
+ switch am
+ case 'fixed'
+ lb = [-inf; 0; lb(:)];
+ ub = [inf; inf; ub(:)];
+ case 'knee'
+ lb = [-inf; 0; 0; lb(:)];
+ ub = [inf; 100; inf; ub(:)];
+ end
+
+ guess = guess(1:pk,:)';
+ guess = [aperiodic_pars'; guess(:)];
+ options = optimset('Display', 'off', 'TolX', 1e-7, 'TolFun', 1e-9, ...
+ 'MaxFunEvals', 5000, 'MaxIter', 5000); % Tuned options
+ try
+ params = fmincon(@err_fm_constr, guess, [], [], [], [], ...
+ lb, ub, [], options, fs, spec(time,:), am, pt);
+ catch
+ error(['Optimization failed to converge: channel ' num2str(chan) ', time index ' num2str(time)]);
+ end
+ switch am
+ case 'fixed'
+ aperiodic_pars_tmp = params(1:2);
+ if length(params) > 3
+ peak_pars_tmp = reshape(params(3:end),[3 length(params(3:end))./3])';
+ end
+ case 'knee'
+ aperiodic_pars_tmp = params(1:3);
+ if length(params) > 3
+ peak_pars_tmp = reshape(params(4:end),[3 length(params(4:end))./3])';
+ end
+ end
+ % Generate model fit
+ ap_fit = gen_aperiodic(fs, aperiodic_pars_tmp, am);
+ model_fit = ap_fit;
+ if length(params) > 3
+ for peak = 1:size(peak_pars_tmp,1)
+ model_fit = model_fit + peak_function(fs,peak_pars_tmp(peak,1),...
+ peak_pars_tmp(peak,2),peak_pars_tmp(peak,3));
+ end
+ else
+ peak_pars_tmp = [0 0 0];
+ end
+ % Calculate model error
+ MSE = sum((spec(time,:) - model_fit).^2)/length(model_fit);
+ rsq_tmp = corrcoef(spec(time,:),model_fit).^2;
+ loglik = -length(model_fit)/2.*(1+log(MSE)+log(2*pi));
+ AIC = 2.*(length(params)-loglik);
+ BIC = length(params).*log(length(model_fit))-2.*loglik;
+ model(pk+1).aperiodic_params = aperiodic_pars_tmp;
+ model(pk+1).peak_params = peak_pars_tmp;
+ model(pk+1).MSE = MSE;
+ model(pk+1).r_squared = rsq_tmp(2);
+ model(pk+1).loglik = loglik;
+ model(pk+1).AIC = AIC;
+ model(pk+1).BIC = BIC;
+ model(pk+1).BF = exp((BIC-model(1).BIC)./2);
+ end
+
+ % Insert data from best model
+ [~,mi] = min([model.BIC]);
+ aperiodic_pars = model(mi).aperiodic_params;
+ peak_pars = model(mi).peak_params;
+ % Return FOOOF results
+ aperiodic_pars(2) = abs(aperiodic_pars(2));
+ channel(chan).data(time).time = ts(time);
+ channel(chan).data(time).aperiodic_params = aperiodic_pars;
+ channel(chan).data(time).peak_params = peak_pars;
+ channel(chan).data(time).peak_types = func2str(peak_function);
+ channel(chan).data(time).ap_fit = 10.^gen_aperiodic(fs, aperiodic_pars, am);
+ aperiodic_models(chan,time,:) = channel(chan).data(time).ap_fit;
+ channel(chan).data(time).fooofed_spectrum = 10.^build_model(fs, aperiodic_pars, opt.aperiodic_mode, peak_pars, peak_function);
+ SPRiNT_models(chan,time,:) = channel(chan).data(time).fooofed_spectrum;
+ channel(chan).data(time).power_spectrum = 10.^spec(time,:);
+ channel(chan).data(time).peak_fit = 10.^(SPRiNT_models(chan,time,:)-aperiodic_models(chan,time,:));
+ peak_models(chan,time,:) = 10.^(SPRiNT_models(chan,time,:)-aperiodic_models(chan,time,:));
+ channel(chan).data(time).error = model(mi).MSE;
+ channel(chan).data(time).r_squared = model(mi).r_squared;
+ channel(chan).data(time).loglik = model(mi).loglik; % log-likelihood
+ channel(chan).data(time).AIC = model(mi).AIC;
+ channel(chan).data(time).BIC = model(mi).BIC;
+ channel(chan).data(time).models = model;
+ % Extract peaks
+ if ~isempty(peak_pars) & any(peak_pars)
+ for p = 1:size(peak_pars,1)
+ channel(chan).peaks(i).time = ts(time);
+ channel(chan).peaks(i).center_frequency = peak_pars(p,1);
+ channel(chan).peaks(i).amplitude = peak_pars(p,2);
+ channel(chan).peaks(i).st_dev = peak_pars(p,3);
+ i = i +1;
+ end
+ end
+ % Extract aperiodic
+ channel(chan).aperiodics(time).time = ts(time);
+ channel(chan).aperiodics(time).offset = aperiodic_pars(1);
+ if length(aperiodic_pars)>2 % Legacy FOOOF alters order of parameters
+ channel(chan).aperiodics(time).exponent = aperiodic_pars(3);
+ channel(chan).aperiodics(time).knee_frequency = aperiodic_pars(2);
+ else
+ channel(chan).aperiodics(time).exponent = aperiodic_pars(2);
+ end
+ channel(chan).stats(time).MSE = MSE;
+ channel(chan).stats(time).r_squared = rsq_tmp(2);
+ channel(chan).stats(time).frequency_wise_error = abs(spec(time,:)-log10(channel(chan).data(time).fooofed_spectrum));
end
- channel(chan).stats(time).MSE = MSE;
- channel(chan).stats(time).r_squared = rsq_tmp(2);
- channel(chan).stats(time).frequency_wise_error = abs(spec(time,:)-model_fit);
+ channel(chan).peaks(i:end) = [];
end
- channel(chan).peaks(i:end) = [];
end
SPRiNT.channel = channel;
SPRiNT.aperiodic_models = aperiodic_models;
@@ -282,8 +826,8 @@
SPRiNT = remove_outliers(SPRiNT,peak_function,opt);
end
for chan = 1:nChan
- tp_exponent(chan,:) = [channel(chan).aperiodics(:).exponent];
- tp_offset(chan,:) = [channel(chan).aperiodics(:).offset];
+ tp_exponent(chan,:) = [SPRiNT.channel(chan).aperiodics(:).exponent];
+ tp_offset(chan,:) = [SPRiNT.channel(chan).aperiodics(:).offset];
end
SPRiNT.topography.exponent = tp_exponent;
SPRiNT.topography.offset = tp_offset;
@@ -315,95 +859,105 @@
timeRange = opt.maxtime.*opt.winLen.*(1-opt.Ovrlp./100);
nC = length(SPRiNT.channel);
+ channel = SPRiNT.channel;
+ freqs = SPRiNT.freqs;
+ aperiodic_models = SPRiNT.aperiodic_models;
+ SPRiNT_models = SPRiNT.SPRiNT_models;
+ peak_models = SPRiNT.peak_models;
for c = 1:nC
bst_progress('set', bst_round(c / nC,2).*100);
- ts = [SPRiNT.channel(c).data.time];
+ ts = [channel(c).data.time];
remove = 1;
while any(remove)
- remove = zeros(length([SPRiNT.channel(c).peaks]),1);
- for p = 1:length([SPRiNT.channel(c).peaks])
- if sum((abs([SPRiNT.channel(c).peaks.time] - SPRiNT.channel(c).peaks(p).time) <= timeRange) &...
- (abs([SPRiNT.channel(c).peaks.center_frequency] - SPRiNT.channel(c).peaks(p).center_frequency) <= opt.maxfreq)) < opt.minnear +1 % includes current peak
+ remove = zeros(length([channel(c).peaks]),1);
+ for p = 1:length([channel(c).peaks])
+ if sum((abs([channel(c).peaks.time] - channel(c).peaks(p).time) <= timeRange) &...
+ (abs([channel(c).peaks.center_frequency] - channel(c).peaks(p).center_frequency) <= opt.maxfreq)) < opt.minnear +1 % includes current peak
remove(p) = 1;
end
end
- SPRiNT.channel(c).peaks(logical(remove)) = [];
+ channel(c).peaks(logical(remove)) = [];
end
for t = 1:length(ts)
- if SPRiNT.channel(c).data(t).peak_params(1) == 0
+ if channel(c).data(t).peak_params(1) == 0
continue % never any peaks to begin with
end
- p = [SPRiNT.channel(c).peaks.time] == ts(t);
- if sum(p) == size(SPRiNT.channel(c).data(t).peak_params,1)
+ p = [channel(c).peaks.time] == ts(t);
+ if sum(p) == size(channel(c).data(t).peak_params,1)
continue % number of peaks has not changed
end
- peak_fit = zeros(size(SPRiNT.freqs));
+ peak_fit = zeros(size(freqs));
if any(p)
- SPRiNT.channel(c).data(t).peak_params = [[SPRiNT.channel(c).peaks(p).center_frequency]' [SPRiNT.channel(c).peaks(p).amplitude]' [SPRiNT.channel(c).peaks(p).st_dev]'];
- peak_pars = SPRiNT.channel(c).data(t).peak_params;
+ channel(c).data(t).peak_params = [[channel(c).peaks(p).center_frequency]' [channel(c).peaks(p).amplitude]' [channel(c).peaks(p).st_dev]'];
+ peak_pars = channel(c).data(t).peak_params;
for peak = 1:size(peak_pars,1)
- peak_fit = peak_fit + peak_function(SPRiNT.freqs,peak_pars(peak,1),...
+ peak_fit = peak_fit + peak_function(freqs,peak_pars(peak,1),...
peak_pars(peak,2),peak_pars(peak,3));
end
- ap_spec = log10(SPRiNT.channel(c).data(t).power_spectrum) - peak_fit;
- ap_pars = simple_ap_fit(SPRiNT.freqs, ap_spec, opt.aperiodic_mode, SPRiNT.channel(c).data(t).aperiodic_params(end));
- ap_fit = gen_aperiodic(SPRiNT.freqs, ap_pars, opt.aperiodic_mode);
- MSE = sum((ap_spec - ap_fit).^2)/length(SPRiNT.freqs);
+ ap_spec = log10(channel(c).data(t).power_spectrum) - peak_fit;
+ ap_pars = simple_ap_fit(freqs, ap_spec, opt.aperiodic_mode, channel(c).data(t).aperiodic_params(end));
+ ap_fit = gen_aperiodic(freqs, ap_pars, opt.aperiodic_mode);
+ MSE = sum((ap_spec - ap_fit).^2)/length(freqs);
rsq_tmp = corrcoef(ap_spec+peak_fit,ap_fit+peak_fit).^2;
% Return FOOOF results
ap_pars(2) = abs(ap_pars(2));
- SPRiNT.channel(c).data(t).ap_fit = 10.^(ap_fit);
- SPRiNT.channel(c).data(t).fooofed_spectrum = 10.^(ap_fit+peak_fit);
- SPRiNT.channel(c).data(t).peak_fit = 10.^(peak_fit);
- SPRiNT.channel(c).data(t).error = MSE;
- SPRiNT.channel(c).data(t).r_squared = rsq_tmp(2);
- SPRiNT.aperiodic_models(c,t,:) = SPRiNT.channel(c).data(t).ap_fit;
- SPRiNT.SPRiNT_models(c,t,:) = SPRiNT.channel(c).data(t).fooofed_spectrum;
- SPRiNT.peak_models(c,t,:) = SPRiNT.channel(c).data(t).peak_fit;
- SPRiNT.channel(c).aperiodics(t).offset = ap_pars(1);
+ channel(c).data(t).ap_fit = 10.^(ap_fit);
+ channel(c).data(t).fooofed_spectrum = 10.^(ap_fit+peak_fit);
+ channel(c).data(t).peak_fit = 10.^(peak_fit);
+ channel(c).data(t).error = MSE;
+ channel(c).data(t).r_squared = rsq_tmp(2);
+ aperiodic_models(c,t,:) = channel(c).data(t).ap_fit;
+ SPRiNT_models(c,t,:) = channel(c).data(t).fooofed_spectrum;
+ peak_models(c,t,:) = channel(c).data(t).peak_fit;
+ channel(c).aperiodics(t).offset = ap_pars(1);
+ channel(c).data(t).aperiodic_params = ap_pars;
if length(ap_pars)>2 % Legacy FOOOF alters order of parameters
- SPRiNT.channel(c).aperiodics(t).exponent = ap_pars(3);
- SPRiNT.channel(c).aperiodics(t).knee_frequency = ap_pars(2);
+ channel(c).aperiodics(t).exponent = ap_pars(3);
+ channel(c).aperiodics(t).knee_frequency = ap_pars(2);
else
- SPRiNT.channel(c).aperiodics(t).exponent = ap_pars(2);
+ channel(c).aperiodics(t).exponent = ap_pars(2);
end
- SPRiNT.channel(c).stats(t).MSE = MSE;
- SPRiNT.channel(c).stats(t).r_squared = rsq_tmp(2);
- SPRiNT.channel(c).stats(t).frequency_wise_error = abs(ap_spec-ap_fit);
+ channel(c).stats(t).MSE = MSE;
+ channel(c).stats(t).r_squared = rsq_tmp(2);
+ channel(c).stats(t).frequency_wise_error = abs(ap_spec-ap_fit);
else
- SPRiNT.channel(c).data(t).peak_params = [0 0 0];
- ap_spec = log10(SPRiNT.channel(c).data(t).power_spectrum) - peak_fit;
- ap_pars = simple_ap_fit(SPRiNT.freqs, ap_spec, opt.aperiodic_mode, SPRiNT.channel(c).data(t).aperiodic_params(end));
- ap_fit = gen_aperiodic(SPRiNT.freqs, ap_pars, opt.aperiodic_mode);
- MSE = sum((ap_spec - ap_fit).^2)/length(SPRiNT.freqs);
+ channel(c).data(t).peak_params = [0 0 0];
+ ap_spec = log10(channel(c).data(t).power_spectrum) - peak_fit;
+ ap_pars = simple_ap_fit(freqs, ap_spec, opt.aperiodic_mode, channel(c).data(t).aperiodic_params(end));
+ ap_fit = gen_aperiodic(freqs, ap_pars, opt.aperiodic_mode);
+ MSE = sum((ap_spec - ap_fit).^2)/length(freqs);
rsq_tmp = corrcoef(ap_spec+peak_fit,ap_fit+peak_fit).^2;
% Return FOOOF results
ap_pars(2) = abs(ap_pars(2));
- SPRiNT.channel(c).data(t).ap_fit = 10.^(ap_fit);
- SPRiNT.channel(c).data(t).fooofed_spectrum = 10.^(ap_fit+peak_fit);
- SPRiNT.channel(c).data(t).peak_fit = 10.^(peak_fit);
- SPRiNT.aperiodic_models(c,t,:) = SPRiNT.channel(c).data(t).ap_fit;
- SPRiNT.SPRiNT_models(c,t,:) = SPRiNT.channel(c).data(t).fooofed_spectrum;
- SPRiNT.peak_models(c,t,:) = SPRiNT.channel(c).data(t).peak_fit;
- SPRiNT.channel(c).aperiodics(t).offset = ap_pars(1);
+ channel(c).data(t).ap_fit = 10.^(ap_fit);
+ channel(c).data(t).fooofed_spectrum = 10.^(ap_fit+peak_fit);
+ channel(c).data(t).peak_fit = 10.^(peak_fit);
+ aperiodic_models(c,t,:) = channel(c).data(t).ap_fit;
+ SPRiNT_models(c,t,:) = channel(c).data(t).fooofed_spectrum;
+ peak_models(c,t,:) = channel(c).data(t).peak_fit;
+ channel(c).aperiodics(t).offset = ap_pars(1);
if length(ap_pars)>2 % Legacy FOOOF alters order of parameters
- SPRiNT.channel(c).aperiodics(t).exponent = ap_pars(3);
- SPRiNT.channel(c).aperiodics(t).knee_frequency = ap_pars(2);
+ channel(c).aperiodics(t).exponent = ap_pars(3);
+ channel(c).aperiodics(t).knee_frequency = ap_pars(2);
else
- SPRiNT.channel(c).aperiodics(t).exponent = ap_pars(2);
+ channel(c).aperiodics(t).exponent = ap_pars(2);
end
- SPRiNT.channel(c).stats(t).MSE = MSE;
- SPRiNT.channel(c).stats(t).r_squared = rsq_tmp(2);
- SPRiNT.channel(c).stats(t).frequency_wise_error = abs(ap_spec-ap_fit);
+ channel(c).stats(t).MSE = MSE;
+ channel(c).stats(t).r_squared = rsq_tmp(2);
+ channel(c).stats(t).frequency_wise_error = abs(ap_spec-ap_fit);
end
end
end
+ SPRiNT.channel = channel;
+ SPRiNT.aperiodic_models = aperiodic_models;
+ SPRiNT.SPRiNT_models = SPRiNT_models;
+ SPRiNT.peak_models = peak_models;
end
-function oS = cluster_peaks_dynamic(oS)
+function SPRiNT = cluster_peaks_dynamic(SPRiNT)
% Helper function to cluster peaks within sensors across time.
%
% Parameters
@@ -418,21 +972,22 @@
%
% Author: Luc Wilson
- pthr = oS.options.proximity_threshold;
- for chan = 1:length(oS.channel)
+ pthr = SPRiNT.options.proximity_threshold;
+ channel = SPRiNT.channel;
+ for chan = 1:length(channel)
clustLead = [];
nCl = 0;
- oS.channel(chan).clustered_peaks = struct();
- times = unique([oS.channel(chan).peaks.time]);
- all_peaks = oS.channel(chan).peaks;
+ channel(chan).clustered_peaks = struct();
+ times = unique([channel(chan).peaks.time]);
+ all_peaks = channel(chan).peaks;
for time = 1:length(times)
time_peaks = all_peaks([all_peaks.time] == times(time));
% Initialize first clusters
if time == 1
nCl = length(time_peaks);
for Cl = 1:nCl
- oS.channel(chan).clustered_peaks(Cl).cluster = Cl;
- oS.channel(chan).clustered_peaks(Cl).peaks(Cl) = time_peaks(Cl);
+ channel(chan).clustered_peaks(Cl).cluster = Cl;
+ channel(chan).clustered_peaks(Cl).peaks(Cl) = time_peaks(Cl);
clustLead(Cl,1) = time_peaks(Cl).time;
clustLead(Cl,2) = time_peaks(Cl).center_frequency;
clustLead(Cl,3) = time_peaks(Cl).amplitude;
@@ -453,7 +1008,7 @@
[tmp,idx] = min(([time_peaks(match).center_frequency] - clustLead(Cl,2)).^2 +...
([time_peaks(match).amplitude] - clustLead(Cl,3)).^2 +...
([time_peaks(match).st_dev] - clustLead(Cl,4)).^2);
- oS.channel(chan).clustered_peaks(clustLead(Cl,5)).peaks(length(oS.channel(chan).clustered_peaks(clustLead(Cl,5)).peaks)+1) = time_peaks(idx_tmp(idx));
+ channel(chan).clustered_peaks(clustLead(Cl,5)).peaks(length(channel(chan).clustered_peaks(clustLead(Cl,5)).peaks)+1) = time_peaks(idx_tmp(idx));
clustLead(Cl,1) = time_peaks(idx_tmp(idx)).time;
clustLead(Cl,2) = time_peaks(idx_tmp(idx)).center_frequency;
clustLead(Cl,3) = time_peaks(idx_tmp(idx)).amplitude;
@@ -472,14 +1027,15 @@
clustLead(Cl,3) = time_peaks(peak).amplitude;
clustLead(Cl,4) = time_peaks(peak).st_dev;
clustLead(Cl,5) = Cl;
- oS.channel(chan).clustered_peaks(Cl).cluster = Cl;
- oS.channel(chan).clustered_peaks(Cl).peaks(length(oS.channel(chan).clustered_peaks(clustLead(Cl,5)).peaks)+1) = time_peaks(peak);
+ channel(chan).clustered_peaks(Cl).cluster = Cl;
+ channel(chan).clustered_peaks(Cl).peaks(length(channel(chan).clustered_peaks(clustLead(Cl,5)).peaks)+1) = time_peaks(peak);
end
end
% Sort clusters based on most recent
clustLead = sortrows(clustLead,1,'descend');
end
end
+ SPRiNT.channel = channel;
end
%% ===== GENERATE APERIODIC =====
@@ -594,10 +1150,38 @@
function ys = expo_fl_function(freqs, params)
- ys = log10(f.^(params(1)) * 10^(params(2)) + params(3));
+ ys = log10(freqs.^(params(1)) * 10^(params(2)) + params(3));
end
+function model_fit = build_model(freqs, ap_pars, ap_type, pk_pars, peak_function)
+% Builds a full spectral model from parameters.
+%
+% Parameters
+% ----------
+% freqs : 1xn array
+% Frequency values for the power spectrum, in linear scale.
+% ap_pars : 1xm array
+% Parameter estimates for aperiodic fit.
+% pk_pars : kx3 array, where k = No. of peaks.
+% Guess parameters for peak fits.
+% pk_type : {'gaussian', 'cauchy', 'best'}
+% Which types of peaks are being fitted.
+%
+% Returns
+% -------
+% model_fit : 1xn array
+% Model power spectrum, in log10-space
+
+ ap_fit = gen_aperiodic(freqs, ap_pars, ap_type);
+ model_fit = ap_fit;
+ if length(pk_pars) > 1
+ for peak = 1:size(pk_pars,1)
+ model_fit = model_fit + peak_function(freqs,pk_pars(peak,1),...
+ pk_pars(peak,2),pk_pars(peak,3));
+ end
+ end
+end
%% ===== FITTING ALGORITHM =====
function aperiodic_params = simple_ap_fit(freqs, power_spectrum, aperiodic_mode, aperiodic_guess)
@@ -709,6 +1293,208 @@
end
+function [guess_params,peak_function] = est_peaks(freqs, flat_iter, max_n_peaks, peak_threshold, min_peak_height, gauss_std_limits, proxThresh, peakType)
+% Iteratively fit peaks to flattened spectrum.
+%
+% Parameters
+% ----------
+% freqs : 1xn array
+% Frequency values for the power spectrum, in linear scale.
+% flat_iter : 1xn array
+% Flattened (aperiodic removed) power spectrum.
+% max_n_peaks : double
+% Maximum number of gaussians to fit within the spectrum.
+% peak_threshold : double
+% Threshold (in standard deviations of noise floor) to detect a peak.
+% min_peak_height : double
+% Minimum height of a peak (in log10).
+% gauss_std_limits : 1x2 double
+% Limits to gaussian (cauchy) standard deviation (gamma) when detecting a peak.
+% proxThresh : double
+% Minimum distance between two peaks, in st. dev. (gamma) of peaks.
+% peakType : {'gaussian', 'cauchy', 'both'}
+% Which types of peaks are being fitted
+% guess_weight : {'none', 'weak', 'strong'}
+% Parameter to weigh initial estimates during optimization (None, Weak, or Strong)
+% hOT : 0 or 1
+% Defines whether to use constrained optimization, fmincon, or
+% basic simplex, fminsearch.
+%
+% Returns
+% -------
+% guess_params : mx3 array, where m = No. of peaks.
+% Parameters that define the peak fit(s). Each row is a peak, as [mean, height, st. dev. (gamma)].
+
+ switch peakType
+ case 'gaussian' % gaussian only
+ peak_function = @gaussian; % Identify peaks as gaussian
+ % Initialize matrix of guess parameters for gaussian fitting.
+ guess_params = zeros(max_n_peaks, 3);
+ % Find peak: Loop through, finding a candidate peak, and fitting with a guess gaussian.
+ % Stopping procedure based on either the limit on # of peaks,
+ % or the relative or absolute height thresholds.
+ for guess = 1:max_n_peaks
+ % Find candidate peak - the maximum point of the flattened spectrum.
+ max_ind = find(flat_iter == max(flat_iter));
+ max_height = flat_iter(max_ind);
+
+ % Stop searching for peaks once max_height drops below height threshold.
+ if max_height <= peak_threshold * std(flat_iter)
+ break
+ end
+
+ % Set the guess parameters for gaussian fitting - mean and height.
+ guess_freq = freqs(max_ind);
+ guess_height = max_height;
+
+ % Halt fitting process if candidate peak drops below minimum height.
+ if guess_height <= min_peak_height
+ break
+ end
+
+ % Data-driven first guess at standard deviation
+ % Find half height index on each side of the center frequency.
+ half_height = 0.5 * max_height;
+
+ le_ind = sum(flat_iter(1:max_ind) <= half_height);
+ ri_ind = length(flat_iter) - sum(flat_iter(max_ind:end) <= half_height)+1;
+
+ % Keep bandwidth estimation from the shortest side.
+ % We grab shortest to avoid estimating very large std from overalapping peaks.
+ % Grab the shortest side, ignoring a side if the half max was not found.
+ % Note: will fail if both le & ri ind's end up as None (probably shouldn't happen).
+ short_side = min(abs([le_ind,ri_ind]-max_ind));
+
+ % Estimate std from FWHM. Calculate FWHM, converting to Hz, get guess std from FWHM
+ fwhm = short_side * 2 * (freqs(2)-freqs(1));
+ guess_std = fwhm / (2 * sqrt(2 * log(2)));
+
+ % Check that guess std isn't outside preset std limits; restrict if so.
+ % Note: without this, curve_fitting fails if given guess > or < bounds.
+ if guess_std < gauss_std_limits(1)
+ guess_std = gauss_std_limits(1);
+ end
+ if guess_std > gauss_std_limits(2)
+ guess_std = gauss_std_limits(2);
+ end
+
+ % Collect guess parameters.
+ guess_params(guess,:) = [guess_freq, guess_height, guess_std];
+
+ % Subtract best-guess gaussian.
+ peak_gauss = gaussian(freqs, guess_freq, guess_height, guess_std);
+ flat_iter = flat_iter - peak_gauss;
+
+ end
+ % Remove unused guesses
+ guess_params(guess_params(:,1) == 0,:) = [];
+
+ % Check peaks based on edges, and on overlap
+ % Drop any that violate requirements.
+ guess_params = drop_peak_cf(guess_params, proxThresh, [min(freqs) max(freqs)]);
+ guess_params = drop_peak_overlap(guess_params, proxThresh);
+
+ case 'cauchy' % cauchy only
+ peak_function = @cauchy; % Identify peaks as cauchy
+ guess_params = zeros(max_n_peaks, 3);
+ flat_spec = flat_iter;
+ for guess = 1:max_n_peaks
+ max_ind = find(flat_iter == max(flat_iter));
+ max_height = flat_iter(max_ind);
+ if max_height <= peak_threshold * std(flat_iter)
+ break
+ end
+ guess_freq = freqs(max_ind);
+ guess_height = max_height;
+ if guess_height <= min_peak_height
+ break
+ end
+ half_height = 0.5 * max_height;
+ le_ind = sum(flat_iter(1:max_ind) <= half_height);
+ ri_ind = length(flat_iter) - sum(flat_iter(max_ind:end) <= half_height);
+ short_side = min(abs([le_ind,ri_ind]-max_ind));
+
+ % Estimate gamma from FWHM. Calculate FWHM, converting to Hz, get guess gamma from FWHM
+ fwhm = short_side * 2 * (freqs(2)-freqs(1));
+ guess_gamma = fwhm/2;
+ % Check that guess gamma isn't outside preset limits; restrict if so.
+ % Note: without this, curve_fitting fails if given guess > or < bounds.
+ if guess_gamma < gauss_std_limits(1)
+ guess_gamma = gauss_std_limits(1);
+ end
+ if guess_gamma > gauss_std_limits(2)
+ guess_gamma = gauss_std_limits(2);
+ end
+
+ % Collect guess parameters.
+ guess_params(guess,:) = [guess_freq(1), guess_height, guess_gamma];
+
+ % Subtract best-guess cauchy.
+ peak_cauchy = cauchy(freqs, guess_freq(1), guess_height, guess_gamma);
+ flat_iter = flat_iter - peak_cauchy;
+
+ end
+ guess_params(guess_params(:,1) == 0,:) = [];
+ guess_params = drop_peak_cf(guess_params, proxThresh, [min(freqs) max(freqs)]);
+ guess_params = drop_peak_overlap(guess_params, proxThresh);
+
+ end
+end
+
+function model_params = est_fit(guess_params, freqs, flat_spec, gauss_std_limits, peakType, guess_weight,hOT)
+% Iteratively fit peaks to flattened spectrum.
+%
+% Parameters
+% ----------
+% freqs : 1xn array
+% Frequency values for the power spectrum, in linear scale.
+% flat_iter : 1xn array
+% Flattened (aperiodic removed) power spectrum.
+% max_n_peaks : double
+% Maximum number of gaussians to fit within the spectrum.
+% peak_threshold : double
+% Threshold (in standard deviations of noise floor) to detect a peak.
+% min_peak_height : double
+% Minimum height of a peak (in log10).
+% gauss_std_limits : 1x2 double
+% Limits to gaussian (cauchy) standard deviation (gamma) when detecting a peak.
+% proxThresh : double
+% Minimum distance between two peaks, in st. dev. (gamma) of peaks.
+% peakType : {'gaussian', 'cauchy', 'both'}
+% Which types of peaks are being fitted
+% guess_weight : {'none', 'weak', 'strong'}
+% Parameter to weigh initial estimates during optimization (None, Weak, or Strong)
+% hOT : 0 or 1
+% Defines whether to use constrained optimization, fmincon, or
+% basic simplex, fminsearch.
+%
+% Returns
+% -------
+% guess_params : mx3 array, where m = No. of peaks.
+% Parameters that define the peak fit(s). Each row is a peak, as [mean, height, st. dev. (gamma)].
+
+ switch peakType
+ case 'gaussian' % gaussian only
+
+ % If there are peak guesses, fit the peaks, and sort results.
+ if ~isempty(guess_params)
+ model_params = fit_peak_guess(guess_params, freqs, flat_spec, 1, guess_weight, gauss_std_limits,hOT);
+ else
+ model_params = [];
+ end
+
+ case 'cauchy' % cauchy only
+
+ % If there are peak guesses, fit the peaks, and sort results.
+ if ~isempty(guess_params)
+ model_params = fit_peak_guess(guess_params, freqs, flat_spec, 2, guess_weight, gauss_std_limits,hOT);
+ else
+ model_params = [];
+ end
+
+ end
+end
+
function [model_params,peak_function] = fit_peaks(freqs, flat_iter, max_n_peaks, peak_threshold, min_peak_height, gauss_std_limits, proxThresh, peakType, guess_weight,hOT)
% Iteratively fit peaks to flattened spectrum.
%
@@ -1038,3 +1824,23 @@
end
err = sum((yVals - fitted_vals).^2);
end
+
+function err = err_fm_constr(params, xVals, yVals, aperiodic_mode, peak_type)
+ switch (aperiodic_mode)
+ case 'fixed' % no knee
+ npk = (length(params)-2)/3;
+ fitted_vals = -log10(xVals.^params(2)) + params(1);
+ case 'knee'
+ npk = (length(params)-3)/3;
+ fitted_vals = params(1) - log10(abs(params(2)) +xVals.^params(3));
+ end
+ for set = 1:npk
+ switch peak_type
+ case 'gaussian' % gaussian only
+ fitted_vals = fitted_vals + gaussian(xVals, params(3.*set), params(3*set+1), params(3*set+2));
+ case 'cauchy' % Cauchy
+ fitted_vals = fitted_vals + cauchy(xVals, params(3.*set), params(3*set+1), params(3*set+2));
+ end
+ end
+ err = sum((yVals - fitted_vals).^2);
+end
diff --git a/toolbox/timefreq/bst_timefreq.m b/toolbox/timefreq/bst_timefreq.m
index b8a80f106..792475aad 100644
--- a/toolbox/timefreq/bst_timefreq.m
+++ b/toolbox/timefreq/bst_timefreq.m
@@ -69,7 +69,8 @@
Def_OPTIONS.MorletFwhmTc = 3;
Def_OPTIONS.WinLength = [];
Def_OPTIONS.WinOverlap = 50;
-Def_OPTIONS.WinStd = 0;
+Def_OPTIONS.IsRelative = 0;
+Def_OPTIONS.WinFunc = 'mean';
Def_OPTIONS.isMirror = 0;
Def_OPTIONS.SensorTypes = 'MEG, EEG';
Def_OPTIONS.Clusters = {};
@@ -139,6 +140,12 @@
isError = 1;
return;
end
+% Cannot do average and compute several TF at same time
+if isAverage && contains(OPTIONS.WinFunc, '+')
+ Messages = ['Incompatible options: 1)Use several functions for PSD and 2)average trials.'];
+ isError = 1;
+ return;
+end
% Progress bar
switch(OPTIONS.Method)
@@ -483,6 +490,7 @@
end
% ===== COMPUTE TRANSFORM =====
+ TFbis = [];
isMeasureApplied = 0;
switch (OPTIONS.Method)
% Morlet wavelet transform (Dimitrios Pantazis)
@@ -528,7 +536,7 @@
% PSD: Homemade computation based on Matlab's FFT
case 'psd'
% Calculate PSD/FFT
- [TF, OPTIONS.Freqs, Nwin, Messages] = bst_psd(F, sfreq, OPTIONS.WinLength, OPTIONS.WinOverlap, BadSegments, ImagingKernel, OPTIONS.WinStd, OPTIONS.PowerUnits);
+ [TF, OPTIONS.Freqs, Nwin, Messages, TFbis] = bst_psd(F, sfreq, OPTIONS.WinLength, OPTIONS.WinOverlap, BadSegments, ImagingKernel, OPTIONS.WinFunc, OPTIONS.PowerUnits, OPTIONS.IsRelative);
if isempty(TF)
continue;
end
@@ -596,14 +604,14 @@
end
% Correct the time step to the closest multiple of the sampling interval to keep the time axis uniform
mt.timestep = round(fsample * mt.timestep) / fsample;
- % Time axis
+ % Time-interval of interest
timeoi = (OPTIONS.TimeVector(1) + mt.timeres/2) : mt.timestep : (OPTIONS.TimeVector(end) - mt.timeres/2 - 1/fsample);
% Frequency resolution for each frequency
freqres = mt.frequencies / mt.freqmod;
freqres(find(freqres < 1/mt.timeres)) = 1/mt.timeres;
% Call fieldtrip function
- [TF, ntaper, OPTIONS.Freqs, OPTIONS.TimeVector] = ft_specest_mtmconvol(F, OPTIONS.TimeVector, ...
+ [TF, ntaper, OPTIONS.Freqs, TimeBins] = ft_specest_mtmconvol(F, OPTIONS.TimeVector, ...
'taper', mt.taper, ...
'timeoi', timeoi, ...
'freqoi', mt.frequencies,...
@@ -611,130 +619,32 @@
'tapsmofrq', freqres, ...
'pad', pad, ...
'verbose', 0);
+ % TimeBands
+ OPTIONS.TimeBands = cell(length(TimeBins), 3);
+ for iTimeBin = 1 : length(TimeBins)
+ OPTIONS.TimeBands{iTimeBin, 1} = sprintf('t%d', iTimeBin);
+ OPTIONS.TimeBands{iTimeBin, 2} = sprintf('%1.4f, %1.4f', TimeBins(iTimeBin) + [-mt.timeres/2, mt.timeres/2 - 1/fsample]);
+ OPTIONS.TimeBands{iTimeBin, 3} = 'mean';
+ end
+
% Permute dimensions to get [nChannels x nTime x nFreq x nTapers]
TF = permute(TF, [2 4 3 1]);
end
- bst_progress('inc', 1);
- % Set to zero the bad channels
- if ~isempty(iGoodChannels)
- iBadChannels = setdiff(1:size(F,1), iGoodChannels);
- if ~isempty(iBadChannels)
- TF(iBadChannels, :, :, :) = 0;
- end
- end
+ nChannels = size(F,1);
% Clean memory
clear F;
-
- % ===== REBUILD FULL SOURCES =====
- % Kernel => Full results
- if strcmpi(DataType, 'results') && ~isempty(ImagingKernel) && ~OPTIONS.SaveKernel
- % Initialize full time-frequency matrix
- TF_full = zeros(size(ImagingKernel,1), size(TF,2), size(TF,3), size(TF,4));
- % Loop on the frequencies and tapers
- for itaper = 1:size(TF,4)
- for ifreq = 1:size(TF,3)
- TF_full(:,:,ifreq,itaper) = ImagingKernel * TF(:,:,ifreq,itaper);
- end
- end
- % Replace previous values with new ones
- TF = TF_full;
- clear TF_full;
- end
- % Cannot save kernel when components > 1
- if strcmpi(DataType, 'results') && OPTIONS.SaveKernel && (nComponents ~= 1)
- Messages = ['Cannot keep the inversion kernel when processing unconstrained sources.' 10 ...
- 'Please selection the option "Optimize: No, save full sources."'];
- isError = 1;
- return;
- end
-
- % ===== APPLY MEASURE =====
- if ~isMeasureApplied
- % Multitaper: average power across tapers
- if strcmpi(OPTIONS.Method, 'mtmconvol')
- TF = nanmean(TF .* conj(TF), 4);
- % Power or magnitude
- if strcmpi(OPTIONS.Measure, 'magnitude')
- TF = sqrt(TF);
- end
- % Other measures: Apply the expected measure
- else
- switch lower(OPTIONS.Measure)
- case 'none' % Nothing to do
- case 'power', TF = abs(TF) .^ 2;
- case 'magnitude', TF = abs(TF);
- otherwise, error('Unknown measure.');
- end
- end
- end
-
- % ===== PROCESS UNCONSTRAINED SOURCES =====
- % Unconstrained sources => SUM for each point (only if not complex)
- if ismember(DataType, {'results','scout','matrix'}) && ~isempty(nComponents) && (nComponents ~= 1)
- % This doesn't work for complex values: TODO
- if strcmpi(OPTIONS.Measure, 'none')
- Messages = ['Cannot keep the complex values when processing unconstrained sources.' 10 ...
- 'Please selection the option "Optimize: No, save full sources."'];
- isError = 1;
- return;
- end
- % Apply orientation
- [TF, GridAtlas, RowNames] = bst_source_orient([], nComponents, GridAtlas, TF, 'sum', DataType, RowNames);
- end
-
- % ===== PROCESS POWER FOR SCOUTS =====
- % Get the lists of clusters
- [tmp,I,J] = unique(RowNames);
- ScoutNames = RowNames(sort(I));
- % If processing data blocks and if there are identical row names => Processing clusters / scouts
- if ~isFile && ~isempty(OPTIONS.Clusters) && (length(ScoutNames) ~= length(RowNames))
- % If cluster function should be applied AFTER time-freq: we have now all the time series
- if strcmpi(OPTIONS.ClusterFuncTime, 'after')
- TF_cluster = zeros(length(ScoutNames), size(TF,2), size(TF,3));
- % For each unique row name: compute a measure over the clusters values
- for iScout = 1:length(ScoutNames)
- indClust = find(strcmpi(ScoutNames{iScout}, RowNames));
- % Compute cluster/scout measure
- for iFreq = 1:size(TF,3)
- TF_cluster(iScout,:,iFreq) = bst_scout_value(TF(indClust,:,iFreq), OPTIONS.ScoutFunc);
- end
- end
- % Save only the requested rows
- RowNames = ScoutNames;
- TF = TF_cluster;
- % Just make all RowNames unique
- else
- initRowNames = RowNames;
- RowNames = cell(size(TF,1),1);
- % For each row name: update name with the index of the row
- for iScout = 1:length(ScoutNames)
- indClust = find(strcmpi(ScoutNames{iScout}, initRowNames));
- % Process each cluster element: add an indice
- for i = 1:length(indClust)
- RowNames{indClust(i)} = sprintf('%s.%d', ScoutNames{iScout}, i);
- end
- end
- end
- end
-
- % ===== NORMALIZE VALUES =====
- if ~isempty(OPTIONS.NormalizeFunc) && ismember(OPTIONS.NormalizeFunc, {'multiply', 'multiply2020'})
- % Call normalization function
- [TF, errorMsg] = process_tf_norm('Compute', TF, OPTIONS.Measure, OPTIONS.Freqs, OPTIONS.NormalizeFunc);
- % Error handling
- if ~isempty(errorMsg)
- Messages = errorMsg;
- isError = 1;
- return;
- end
- % Add normalization comment
- if ~isAddedCommentNorm
- isAddedCommentNorm = 1;
- OPTIONS.Comment = [OPTIONS.Comment ' | ' strrep(OPTIONS.NormalizeFunc, '2020', '')];
- end
+ % Set to zero the bad channels
+ TF=SetBadChannels(TF, nChannels, iGoodChannels);
+ % Apply post processing steps
+ [TF, GridAtlas, RowNames, isAddedCommentNorm, OPTIONS] = PostprocessTF(TF, DataType, ImagingKernel, OPTIONS, nComponents, GridAtlas, RowNames, isFile, isMeasureApplied, isAddedCommentNorm);
+ % For PSD_FEATURES, if TFbis (second TF) is returned
+ if ~isempty(TFbis)
+ % Set to zero the bad channels
+ TFbis = SetBadChannels(TFbis, nChannels, iGoodChannels);
+ % Apply post processing steps
+ TFbis = PostprocessTF(TFbis, DataType, ImagingKernel, OPTIONS, nComponents, GridAtlas, RowNames, isFile, isMeasureApplied, isAddedCommentNorm);
end
-
% ===== SAVE FILE / COMPUTE AVERAGE =====
% Only save average
if isAverage
@@ -755,7 +665,7 @@
% Save all the time-frequency maps
else
% Save file
- SaveFile(iTargetStudy, InitFile, DataType, RowNames, TF, OPTIONS, FreqBands, SurfaceFile, GridLoc, GridAtlas, HeadModelType, HeadModelFile, nAvg, Atlas, strHistory);
+ SaveFile(iTargetStudy, InitFile, DataType, RowNames, TF, TFbis, OPTIONS, FreqBands, SurfaceFile, GridLoc, GridAtlas, HeadModelType, HeadModelFile, nAvg, Atlas, strHistory);
end
bst_progress('inc', 1);
end
@@ -787,18 +697,141 @@
InitFile = '';
end
% Save file
- SaveFile(iTargetStudy, InitFile, DataType, RowNames, TF_avg, OPTIONS, FreqBands, SurfaceFile, GridLoc, GridAtlas, HeadModelType, HeadModelFile, nAvgTotal, Atlas, strHistory);
+ SaveFile(iTargetStudy, InitFile, DataType, RowNames, TF_avg, [], OPTIONS, FreqBands, SurfaceFile, GridLoc, GridAtlas, HeadModelType, HeadModelFile, nAvgTotal, Atlas, strHistory);
end
+ %% ===== SET BAD CHANNELS =====
+ function TF=SetBadChannels(TF, nChannels, iGoodChannels)
+ % Set to zero the bad channels
+ if ~isempty(iGoodChannels)
+ iBadChannels = setdiff(1:nChannels, iGoodChannels);
+ if ~isempty(iBadChannels)
+ TF(iBadChannels, :, :, :) = 0;
+ end
+ end
+ end
+
+ %% ===== Post PROCESS TF =====
+ function [TF, GridAtlas, RowNames, isAddedCommentNorm, OPTIONS] = PostprocessTF(TF, DataType, ImagingKernel, OPTIONS, nComponents, GridAtlas, RowNames, isFile, isMeasureApplied, isAddedCommentNorm)
+ % ===== REBUILD FULL SOURCES =====
+ % Kernel => Full results
+ if strcmpi(DataType, 'results') && ~isempty(ImagingKernel) && ~OPTIONS.SaveKernel
+ % Initialize full time-frequency matrix
+ TF_full = zeros(size(ImagingKernel,1), size(TF,2), size(TF,3), size(TF,4));
+ % Loop on the frequencies and tapers
+ for itaper = 1:size(TF,4)
+ for ifreq = 1:size(TF,3)
+ TF_full(:,:,ifreq,itaper) = ImagingKernel * TF(:,:,ifreq,itaper);
+ end
+ end
+ % Replace previous values with new ones
+ TF = TF_full;
+ clear TF_full;
+ end
+ % Cannot save kernel when components > 1
+ if strcmpi(DataType, 'results') && OPTIONS.SaveKernel && (nComponents ~= 1)
+ Messages = ['Cannot keep the inversion kernel when processing unconstrained sources.' 10 ...
+ 'Please selection the option "Optimize: No, save full sources."'];
+ isError = 1;
+ return;
+ end
+
+ % ===== APPLY MEASURE =====
+ if ~isMeasureApplied
+ % Multitaper: average power across tapers
+ if strcmpi(OPTIONS.Method, 'mtmconvol')
+ TF = nanmean(TF .* conj(TF), 4);
+ % Power or magnitude
+ if strcmpi(OPTIONS.Measure, 'magnitude')
+ TF = sqrt(TF);
+ end
+ % Other measures: Apply the expected measure
+ else
+ switch lower(OPTIONS.Measure)
+ case 'none' % Nothing to do
+ case 'power', TF = abs(TF) .^ 2;
+ case 'magnitude', TF = abs(TF);
+ otherwise, error('Unknown measure.');
+ end
+ end
+ end
+
+ % ===== PROCESS UNCONSTRAINED SOURCES =====
+ % Unconstrained sources => SUM for each point (only if not complex)
+ if ismember(DataType, {'results','scout','matrix'}) && ~isempty(nComponents) && (nComponents ~= 1)
+ % This doesn't work for complex values: TODO
+ if strcmpi(OPTIONS.Measure, 'none')
+ Messages = ['Cannot keep the complex values when processing unconstrained sources.' 10 ...
+ 'Please selection the option "Optimize: No, save full sources."'];
+ isError = 1;
+ return;
+ end
+ % Apply orientation
+ [TF, GridAtlas, RowNames] = bst_source_orient([], nComponents, GridAtlas, TF, 'sum', DataType, RowNames);
+ end
+
+ % ===== PROCESS POWER FOR SCOUTS =====
+ % Get the lists of clusters
+ [tmp,I,J] = unique(RowNames);
+ ScoutNames = RowNames(sort(I));
+ % If processing data blocks and if there are identical row names => Processing clusters / scouts
+ if ~isFile && ~isempty(OPTIONS.Clusters) && (length(ScoutNames) ~= length(RowNames))
+ % If cluster function should be applied AFTER time-freq: we have now all the time series
+ if strcmpi(OPTIONS.ClusterFuncTime, 'after')
+ TF_cluster = zeros(length(ScoutNames), size(TF,2), size(TF,3));
+ % For each unique row name: compute a measure over the clusters values
+ for iScout = 1:length(ScoutNames)
+ indClust = find(strcmpi(ScoutNames{iScout}, RowNames));
+ % Compute cluster/scout measure
+ for iFreq = 1:size(TF,3)
+ TF_cluster(iScout,:,iFreq) = bst_scout_value(TF(indClust,:,iFreq), OPTIONS.ScoutFunc);
+ end
+ end
+ % Save only the requested rows
+ RowNames = ScoutNames;
+ TF = TF_cluster;
+ % Just make all RowNames unique
+ else
+ initRowNames = RowNames;
+ RowNames = cell(size(TF,1),1);
+ % For each row name: update name with the index of the row
+ for iScout = 1:length(ScoutNames)
+ indClust = find(strcmpi(ScoutNames{iScout}, initRowNames));
+ % Process each cluster element: add an indice
+ for idx = 1:length(indClust)
+ RowNames{indClust(idx)} = sprintf('%s.%d', ScoutNames{iScout}, idx);
+ end
+ end
+ end
+ end
+
+ % ===== NORMALIZE VALUES =====
+ if ~isempty(OPTIONS.NormalizeFunc) && ismember(OPTIONS.NormalizeFunc, {'multiply', 'multiply2020'})
+ % Call normalization function
+ [TF, errorMsg] = process_tf_norm('Compute', TF, OPTIONS.Measure, OPTIONS.Freqs, OPTIONS.NormalizeFunc);
+ % Error handling
+ if ~isempty(errorMsg)
+ Messages = errorMsg;
+ isError = 1;
+ return;
+ end
+ % Add normalization comment
+ if ~isAddedCommentNorm
+ isAddedCommentNorm = 1;
+ OPTIONS.Comment = [OPTIONS.Comment ' | ' strrep(OPTIONS.NormalizeFunc, '2020', '')];
+ end
+ end
+ end
%% ===== SAVE FILE =====
- function SaveFile(iTargetStudy, DataFile, DataType, RowNames, TF, OPTIONS, FreqBands, SurfaceFile, GridLoc, GridAtlas, HeadModelType, HeadModelFile, nAvgFile, Atlas, strHistory)
+ function SaveFile(iTargetStudy, DataFile, DataType, RowNames, TF, TFbis, OPTIONS, FreqBands, SurfaceFile, GridLoc, GridAtlas, HeadModelType, HeadModelFile, nAvgFile, Atlas, strHistory)
% Create file structure
FileMat = db_template('timefreqmat');
FileMat.Comment = OPTIONS.Comment;
FileMat.DataType = DataType;
FileMat.TF = TF;
+ FileMat.Std = TFbis;
FileMat.Time = OPTIONS.TimeVector;
FileMat.TimeBands = [];
FileMat.Freqs = OPTIONS.Freqs;
@@ -850,8 +883,10 @@ function SaveFile(iTargetStudy, DataFile, DataType, RowNames, TF, OPTIONS, FreqB
if ~isempty(FreqBands) || ~isempty(OPTIONS.TimeBands)
if strcmpi(OPTIONS.Method, 'hilbert') && ~isempty(OPTIONS.TimeBands)
[FileMat, Messages] = process_tf_bands('Compute', FileMat, [], OPTIONS.TimeBands);
- elseif strcmpi(OPTIONS.Method, 'morlet') || strcmpi(OPTIONS.Method, 'psd')
+ elseif strcmpi(OPTIONS.Method, 'morlet') || strcmpi(OPTIONS.Method, 'psd')
[FileMat, Messages] = process_tf_bands('Compute', FileMat, FreqBands, OPTIONS.TimeBands);
+ elseif strcmpi(OPTIONS.Method, 'mtmconvol') && ~isempty(OPTIONS.TimeBands)
+ FileMat.TimeBands = OPTIONS.TimeBands;
end
if isempty(FileMat)
if ~isempty(Messages)
@@ -861,6 +896,21 @@ function SaveFile(iTargetStudy, DataFile, DataType, RowNames, TF, OPTIONS, FreqB
end
end
end
+
+ % Add extra PSD options
+ if strcmpi(OPTIONS.Method, 'psd')
+ FileMat.Options.isRelativePSD = OPTIONS.IsRelative;
+ FileMat.Options.WindowFunction = OPTIONS.WinFunc;
+ % Apply time and frequency bands on TFbis
+ if (~isempty(FreqBands) || ~isempty(OPTIONS.TimeBands)) && ~isempty(FileMat.Std)
+ FileMat2 = FileMat;
+ FileMat2.TF = FileMat.Std;
+ FileMat2.Freqs = OPTIONS.Freqs;
+ [FileMat2, Messages] = process_tf_bands('Compute', FileMat2, FreqBands, OPTIONS.TimeBands);
+ FileMat.Std = FileMat2.TF;
+ clear FileMat2;
+ end
+ end
% Save the file
if ~isempty(iTargetStudy)
diff --git a/toolbox/timefreq/panel_timefreq_options.m b/toolbox/timefreq/panel_timefreq_options.m
index d1aa56771..f181a0838 100644
--- a/toolbox/timefreq/panel_timefreq_options.m
+++ b/toolbox/timefreq/panel_timefreq_options.m
@@ -76,13 +76,14 @@
end
% Determine which function is calling this pannel
- % Used by process_: hilbert, psd, timefreq, and connectivity: henv(1,1n,2), plv(1,1n,2)
+ % Used by process_: hilbert, psd, timefreq, psd_features, and connectivity: henv(1,1n,2), plv(1,1n,2), cohere(1,1n,2)
% Connectivity processes have options.tfmeasure with value 'hilbert' or 'morlet' ('fourier' doesn't use this panel).
isProcConnect = isfield(sProcess.options, 'tfmeasure'); % used multiple times
if isProcConnect
Method = sProcess.options.tfmeasure.Value;
- else % hilbert, psd, timefreq
- Method = strrep(strrep(func2str(sProcess.Function), 'process_', ''), 'timefreq', 'morlet');
+ else % hilbert, psd, timefreq, psd_features
+ tmp = regexp(func2str(sProcess.Function), '(?<=process_)\w*?(?=_|$)', 'match');
+ Method = strrep(tmp{1}, 'timefreq', 'morlet');
end
hFigWavelet = [];
diff --git a/toolbox/tree/node_create_subject.m b/toolbox/tree/node_create_subject.m
index 86355dfa1..5e35e62ac 100644
--- a/toolbox/tree/node_create_subject.m
+++ b/toolbox/tree/node_create_subject.m
@@ -68,6 +68,7 @@
% Create list of anat files (put the default at the top)
iAnatList = 1:length(sSubject.Anatomy);
iAtlas = find(~cellfun(@(c)(isempty(strfind(char(c), '_volatlas')) && isempty(strfind(char(c), '_tissues'))), {sSubject.Anatomy.FileName}));
+ iCt = find(cellfun(@(c)(~isempty(strfind(char(c), '_volct'))), {sSubject.Anatomy.FileName}));
if (length(sSubject.Anatomy) > 1)
iAnatList = [sSubject.iAnatomy, setdiff(iAnatList,[iAtlas,sSubject.iAnatomy]), setdiff(iAtlas,sSubject.iAnatomy)];
end
@@ -75,6 +76,8 @@
for iAnatomy = iAnatList
if ismember(iAnatomy, iAtlas)
nodeType = 'volatlas';
+ elseif ismember(iAnatomy, iCt)
+ nodeType = 'volct';
else
nodeType = 'anatomy';
end
diff --git a/toolbox/tree/node_delete.m b/toolbox/tree/node_delete.m
index 0fcc64d95..d8477b8fd 100644
--- a/toolbox/tree/node_delete.m
+++ b/toolbox/tree/node_delete.m
@@ -133,7 +133,7 @@ function node_delete(bstNodes, isUserConfirm)
%% ===== ANATOMY =====
- case {'anatomy', 'volatlas'}
+ case {'anatomy', 'volatlas', 'volct'}
bst_progress('start', 'Delete nodes', 'Deleting files...');
% Full file names
FullFilesList = cellfun(@(f)fullfile(ProtocolInfo.SUBJECTS,f), FileName', 'UniformOutput',0);
@@ -190,6 +190,16 @@ function node_delete(bstNodes, isUserConfirm)
% Get indices
iSubject = uniqueSubject(i);
iSurfaces = iSubItem(iItem == iSubject);
+ % Current default surfaces
+ saveDefSurf = struct();
+ SurfTypes = {'Scalp', 'Cortex', 'InnerSkull', 'OuterSkull', 'Fibers', 'FEM'};
+ for ix = 1 : length(SurfTypes)
+ if (iSubject == 0)
+ saveDefSurf.(['i' SurfTypes{ix}]) = ['', ProtocolSubjects.DefaultSubject.Surface(ProtocolSubjects.DefaultSubject.(['i' SurfTypes{ix}])).FileName];
+ else
+ saveDefSurf.(['i' SurfTypes{ix}]) = ['', ProtocolSubjects.Subject(iSubject).Surface(ProtocolSubjects.Subject(iSubject).(['i' SurfTypes{ix}])).FileName];
+ end
+ end
% Delete surface
if (iSubject == 0)
ProtocolSubjects.DefaultSubject.Surface(iSurfaces) = [];
@@ -198,8 +208,14 @@ function node_delete(bstNodes, isUserConfirm)
end
% Update default surfaces
bst_set('ProtocolSubjects', ProtocolSubjects);
- for SurfType = {'Scalp', 'Cortex', 'InnerSkull', 'OuterSkull', 'Fibers', 'FEM'}
- db_surface_default(iSubject, SurfType{1}, [], 0);
+ for ix = 1 : length(SurfTypes)
+ if (iSubject == 0)
+ iSurf = find(ismember({ProtocolSubjects.DefaultSubject.Surface.FileName}, saveDefSurf.(['i' SurfTypes{ix}])));
+ else
+ iSurf = find(ismember({ProtocolSubjects.Subject(iSubject).Surface.FileName}, saveDefSurf.(['i' SurfTypes{ix}])));
+ end
+ % Find in current surfaces
+ db_surface_default(iSubject, SurfTypes{ix}, iSurf, 0);
end
drawnow;
ProtocolSubjects = bst_get('ProtocolSubjects');
diff --git a/toolbox/tree/node_rename.m b/toolbox/tree/node_rename.m
index 7a33317c4..c0db84f12 100644
--- a/toolbox/tree/node_rename.m
+++ b/toolbox/tree/node_rename.m
@@ -116,7 +116,7 @@ function node_rename(bstNode, newComment)
%% ===== ANATOMY (Comment) =====
- case {'anatomy', 'volatlas'}
+ case {'anatomy', 'volatlas', 'volct'}
iSubject = iItem;
iAnatomy = iSubItem;
sSubject = bst_get('Subject', iSubject);
diff --git a/toolbox/tree/tree_callbacks.m b/toolbox/tree/tree_callbacks.m
index dcb844960..af704dbc3 100644
--- a/toolbox/tree/tree_callbacks.m
+++ b/toolbox/tree/tree_callbacks.m
@@ -31,6 +31,8 @@
% =============================================================================@
%
% Authors: Francois Tadel, 2008-2023
+% Raymundo Cassani, 2023-2024
+% Chinmay Chinara, 2023-2024
import org.brainstorm.icon.*;
import java.awt.event.KeyEvent;
@@ -66,7 +68,7 @@
filenameRelative = char(bstNodes(1).getFileName());
% Build full filename (depends on the file type)
switch lower(nodeType)
- case {'surface', 'scalp', 'cortex', 'outerskull', 'innerskull', 'fibers', 'fem', 'other', 'subject', 'studysubject', 'anatomy', 'volatlas'}
+ case {'surface', 'scalp', 'cortex', 'outerskull', 'innerskull', 'fibers', 'fem', 'other', 'subject', 'studysubject', 'anatomy', 'volatlas', 'volct'}
filenameFull = bst_fullfile(ProtocolInfo.SUBJECTS, filenameRelative);
case {'study', 'condition', 'rawcondition', 'channel', 'headmodel', 'data','rawdata', 'datalist', 'results', 'kernel', 'pdata', 'presults', 'ptimefreq', 'pspectrum', 'image', 'video', 'videolink', 'noisecov', 'ndatacov', 'dipoles','timefreq', 'spectrum', 'matrix', 'matrixlist', 'pmatrix', 'spike'}
filenameFull = bst_fullfile(ProtocolInfo.STUDIES, filenameRelative);
@@ -164,19 +166,19 @@
% MRI: Display in MRI viewer
view_mri(filenameRelative);
- % ===== VOLUME ATLAS =====
- case 'volatlas'
+ % ===== VOLUME ATLAS AND VOLUME CT=====
+ case {'volatlas', 'volct'}
% Get subject
iSubject = bstNodes(1).getStudyIndex();
iAnatomy = bstNodes(1).getItemIndex();
sSubject = bst_get('Subject', iSubject);
- % Atlas: display as overlay on the default MRI
+ % Atlas/CT: display as overlay on the default MRI
if (iAnatomy ~= sSubject.iAnatomy)
view_mri(sSubject.Anatomy(sSubject.iAnatomy).FileName, filenameRelative);
else
view_mri(filenameRelative);
end
-
+
% ===== SURFACE =====
% Mark/unmark (items selected : 1/category)
case {'scalp', 'outerskull', 'innerskull', 'cortex', 'fibers', 'fem'}
@@ -206,7 +208,18 @@
end
% Other surface: display it
case 'other'
- view_surface(filenameRelative);
+ % Display mesh with 3D orthogonal slices of the default MRI only if it is an isosurface
+ if ~isempty(regexp(filenameRelative, 'isosurface', 'match'))
+ iSubject = bstNodes(1).getStudyIndex();
+ sSubject = bst_get('Subject', iSubject);
+ MriFile = sSubject.Anatomy(1).FileName;
+ hFig = view_mri_3d(MriFile, [], 0.3, []);
+ view_surface(filenameRelative, [], [], hFig, []);
+ elseif ~isempty(regexp(filenameRelative, 'tess_textured', 'match'))
+ ViewTexturedSurface(filenameRelative);
+ else
+ view_surface(filenameRelative);
+ end
% ===== CHANNEL =====
% If one and only one modality available : display sensors
@@ -220,7 +233,7 @@
if strcmpi(DisplayMod{1}, 'ECOG+SEEG') || (length(DisplayMod) >= 2) && all(ismember({'SEEG','ECOG'}, DisplayMod))
DisplayChannels(bstNodes, 'ECOG+SEEG', 'cortex', 1);
elseif strcmpi(DisplayMod{1}, 'SEEG')
- DisplayChannels(bstNodes, DisplayMod{1}, 'anatomy', 1);
+ DisplayChannels(bstNodes, DisplayMod{1}, 'anatomy', 1, 0);
elseif strcmpi(DisplayMod{1}, 'ECOG')
DisplayChannels(bstNodes, DisplayMod{1}, 'cortex', 1);
elseif ismember(DisplayMod{1}, {'MEG','MEG GRAD','MEG MAG'})
@@ -562,6 +575,7 @@
gui_component('MenuItem', jPopup, [], 'Import anatomy folder', IconLoader.ICON_ANATOMY, [], @(h,ev)bst_call(@import_anatomy, iSubject, 0));
gui_component('MenuItem', jPopup, [], 'Import anatomy folder (auto)', IconLoader.ICON_ANATOMY, [], @(h,ev)bst_call(@import_anatomy, iSubject, 1));
gui_component('MenuItem', jPopup, [], 'Import MRI', IconLoader.ICON_ANATOMY, [], @(h,ev)bst_call(@import_mri, iSubject, [], [], 1));
+ gui_component('MenuItem', jPopup, [], 'Import CT', IconLoader.ICON_VOLCT, [], @(h,ev)bst_call(@import_mri, iSubject, [], [], 1, 1, 'Import CT'));
gui_component('MenuItem', jPopup, [], 'Import surfaces', IconLoader.ICON_SURFACE, [], @(h,ev)bst_call(@import_surfaces, iSubject));
gui_component('MenuItem', jPopup, [], 'Import fibers', IconLoader.ICON_FIBERS, [], @(h,ev)bst_call(@import_fibers, iSubject));
gui_component('MenuItem', jPopup, [], 'Convert DWI to DTI', IconLoader.ICON_FIBERS, [], @(h,ev)bst_call(@process_dwi2dti, 'ComputeInteractive', iSubject));
@@ -571,11 +585,12 @@
% Get registered Brainstorm anatomy defaults
sTemplates = bst_get('AnatomyDefaults');
% Create menus
- jMenuDefaults = gui_component('Menu', jPopup, [], 'Use template', IconLoader.ICON_ANATOMY, [], []);
- jMenuDefMni = gui_component('Menu', jMenuDefaults, [], 'MNI', IconLoader.ICON_ANATOMY, [], []);
- jMenuDefUsc = gui_component('Menu', jMenuDefaults, [], 'USC', IconLoader.ICON_ANATOMY, [], []);
- jMenuDefFs = gui_component('Menu', jMenuDefaults, [], 'FsAverage', IconLoader.ICON_ANATOMY, [], []);
+ jMenuDefaults = gui_component('Menu', jPopup, [], 'Use template', IconLoader.ICON_ANATOMY, [], []);
+ jMenuDefMni = gui_component('Menu', jMenuDefaults, [], 'MNI', IconLoader.ICON_ANATOMY, [], []);
+ jMenuDefUsc = gui_component('Menu', jMenuDefaults, [], 'USC', IconLoader.ICON_ANATOMY, [], []);
+ jMenuDefFs = gui_component('Menu', jMenuDefaults, [], 'FsAverage', IconLoader.ICON_ANATOMY, [], []);
jMenuDefInfants = gui_component('Menu', jMenuDefaults, [], 'Infants', IconLoader.ICON_ANATOMY, [], []);
+ jMenuDefOthers = gui_component('Menu', jMenuDefaults, [], 'Others', IconLoader.ICON_ANATOMY, [], []);
% Add an item per Template available
for i = 1:length(sTemplates)
% Local or download?
@@ -593,6 +608,8 @@
jParent = jMenuDefFs;
elseif ~isempty(strfind(lower(sTemplates(i).Name), 'oreilly')) || ~isempty(strfind(lower(sTemplates(i).Name), 'kabdebon')) || ~isempty(strfind(lower(sTemplates(i).Name), 'infant'))
jParent = jMenuDefInfants;
+ else
+ jParent = jMenuDefOthers;
end
% Create item
gui_component('MenuItem', jParent, [], Comment, IconLoader.ICON_ANATOMY, [], @(h,ev)db_set_template(iSubject, sTemplates(i), 1));
@@ -631,7 +648,7 @@
gui_component('MenuItem', jMenuMniVol, [], 'Import from file', IconLoader.ICON_ANATOMY, [], @(h,ev)bst_call(@import_mniatlas, iSubject));
% === MRI SEGMENTATION ===
- fcnMriSegment(jPopup, sSubject, iSubject, [], 0);
+ fcnMriSegment(jPopup, sSubject, iSubject, [], 0, 0);
% Export menu (added later)
if (iSubject ~= 0)
jMenuExport{1} = gui_component('MenuItem', [], [], 'Export subject', IconLoader.ICON_SAVE, [], @(h,ev)export_protocol(bst_get('iProtocol'), iSubject));
@@ -685,6 +702,11 @@
% fcnPopupProjectSources(0);
end
fcnPopupScoutTimeSeries(jPopup);
+ AddSeparator(jPopup);
+ % === SEEG IMPLANTATION ===
+ if ~isempty(sSubject.Anatomy) && ~strcmpi(sSubject.Name, bst_get('NormalizedSubjectName'))
+ gui_component('MenuItem', jPopup, [], 'SEEG/ECOG implantation', IconLoader.ICON_SEEG_DEPTH, [], @(h,ev)bst_call(@panel_ieeg, 'CreateImplantation', sSubject));
+ end
% Export menu (added later)
jMenuExport = gui_component('MenuItem', [], [], 'Export subject', IconLoader.ICON_SAVE, [], @(h,ev)export_protocol(bst_get('iProtocol'), iSubject));
@@ -884,8 +906,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
@@ -896,7 +918,7 @@
gui_component('MenuItem', jPopup, [], 'Edit channel file', IconLoader.ICON_EDIT, [], @(h,ev)gui_edit_channel(filenameRelative));
end
% === RENAME CHANNELS BIOSEMI ===
- if ~isempty(regexp(char(bstNodes(1).getComment()), 'BDF'))
+ if ~isempty(regexp(lower(char(bstNodes(1).getComment())), 'bdf')) || ~isempty(regexp(lower(char(bstNodes(1).getComment())), 'biosemi'))
gui_component('MenuItem', jPopup, [], 'BioSemi channels names to 10-10 system', IconLoader.ICON_EDIT, [], @(h,ev)process_channel_biosemi('ComputeInteractive', filenameRelative));
end
@@ -907,12 +929,12 @@
fcnPopupImportChannel(bstNodes, jPopup, 1);
end
% === SEEG CONTACT LABELLING ===
- if (length(bstNodes) == 1) && any(ismember({'SEEG','ECOG','ECOG+SEEG'}, AllMod))
+ if (length(bstNodes) == 1) && ~isempty(AllMod) && any(ismember({'SEEG','ECOG','ECOG+SEEG'}, AllMod))
gui_component('MenuItem', jPopup, [], 'iEEG atlas labels', IconLoader.ICON_VOLATLAS, [], @(h,ev)bst_call(@export_channel_atlas, filenameRelative, 'ECOG+SEEG'));
end
% === NIRS CHANNEL LABELLING ===
- if (length(bstNodes) == 1) && any(ismember({'NIRS'}, AllMod))
+ if (length(bstNodes) == 1) && ~isempty(AllMod) && any(ismember({'NIRS'}, AllMod))
gui_component('MenuItem', jPopup, [], 'NIRS atlas labels', IconLoader.ICON_VOLATLAS, [], @(h,ev)bst_call(@export_channel_nirs_atlas, filenameRelative));
end
@@ -975,6 +997,10 @@
if ~bst_get('ReadOnly')
gui_component('MenuItem', jMenuAlign, [], 'Refine using head points', IconLoader.ICON_ALIGN_CHANNELS, [], @(h,ev)channel_align_auto(filenameRelative, [], 1, 0));
end
+ if ~bst_get('ReadOnly') && ~isempty(DisplayMod) && ~any(ismember(DisplayMod, {'MEG', 'MEG MAG', 'MEG GRAD'})) && any(ismember(DisplayMod,{'EEG','NIRS'}))
+ AddSeparator(jMenuAlign);
+ gui_component('MenuItem', jMenuAlign, [], 'Scalp scouts from scalp sensors', IconLoader.ICON_PROJECT_ELECTRODES, [], @(h,ev)bst_scout_channels(filenameRelative, 'Scalp', {'EEG', 'NIRS'}));
+ end
% === MENU: EXTRA HEAD POINTS ===
jMenuHeadPoints = gui_component('Menu', jPopup, [], 'Digitized head points', IconLoader.ICON_CHANNEL, [], []);
@@ -988,6 +1014,8 @@
gui_component('MenuItem', jMenuHeadPoints, [], 'Remove all points', IconLoader.ICON_DELETE, [], @(h,ev)ChannelRemoveHeadpoints(filenameRelative));
% Remove points below the nasion
gui_component('MenuItem', jMenuHeadPoints, [], 'Remove points below nasion', IconLoader.ICON_DELETE, [], @(h,ev)ChannelRemoveHeadpoints(filenameRelative, 0));
+ % Remove points manually
+ gui_component('MenuItem', jMenuHeadPoints, [], 'Remove points manually', IconLoader.ICON_DELETE, [], @(h,ev)channel_align_manual(filenameRelative, 'HeadPoints', 1));
% WARP
AddSeparator(jMenuHeadPoints);
jMenuWarp = gui_component('Menu', jMenuHeadPoints, [], 'Warp', IconLoader.ICON_ALIGN_CHANNELS, [], []);
@@ -1003,7 +1031,7 @@
end
% ===== PROJECT SENSORS =====
- if ~bst_get('ReadOnly') && ~any(ismember(DisplayMod, {'MEG', 'MEG MAG', 'MEG GRAD'}))
+ if ~bst_get('ReadOnly') && ~isempty(DisplayMod) && ~any(ismember(DisplayMod, {'MEG', 'MEG MAG', 'MEG GRAD'}))
AddSeparator(jPopup);
if (iSubject == 0) || sSubject.UseDefaultAnat
gui_component('MenuItem', jPopup, [], 'Project to subject...', IconLoader.ICON_PROJECT_ELECTRODES, [], @(h,ev)bst_project_channel(filenameRelative, []));
@@ -1018,7 +1046,7 @@
end
%% ===== POPUP: ANATOMY =====
- case {'anatomy', 'volatlas'}
+ case {'anatomy', 'volatlas', 'volct'}
iSubject = bstNodes(1).getStudyIndex();
sSubject = bst_get('Subject', iSubject);
iAnatomy = [];
@@ -1027,6 +1055,7 @@
end
mriComment = lower(char(bstNodes(1).getComment()));
isAtlas = strcmpi(nodeType, 'volatlas') || ~isempty(strfind(mriComment, 'tissues')) || ~isempty(strfind(mriComment, 'aseg')) || ~isempty(strfind(mriComment, 'atlas'));
+ isCt = strcmpi(nodeType, 'volct');
if (length(bstNodes) == 1)
% MENU : DISPLAY
@@ -1041,7 +1070,7 @@
end
AddSeparator(jMenuDisplay);
% Display as overlay
- if ~bstNodes(1).isMarked()
+ if ~bstNodes(1).isMarked() && ~isempty(sSubject.iAnatomy)
% Get subject structure
sSubject = bst_get('MriFile', filenameRelative);
MriFile = sSubject.Anatomy(sSubject.iAnatomy).FileName;
@@ -1058,11 +1087,11 @@
gui_component('MenuItem', jMenuDisplay, [], 'Histogram', IconLoader.ICON_HISTOGRAM, [], @(h,ev)view_mri_histogram(filenameFull));
end
% === MENU: EDIT MRI ===
- if ~bst_get('ReadOnly') && ~isAtlas
+ if ~bst_get('ReadOnly') && ~isAtlas && ~isCt
gui_component('MenuItem', jPopup, [], 'Edit MRI...', IconLoader.ICON_ANATOMY, [], @(h,ev)view_mri(filenameRelative, 'EditMri'));
end
% === MENU: SET AS DEFAULT ===
- if ~bst_get('ReadOnly') && (~ismember(iAnatomy, sSubject.iAnatomy) || ~bstNodes(1).isMarked()) && ~isAtlas
+ if ~bst_get('ReadOnly') && (~ismember(iAnatomy, sSubject.iAnatomy) || ~bstNodes(1).isMarked()) && ~isAtlas && ~isCt
gui_component('MenuItem', jPopup, [], 'Set as default MRI', IconLoader.ICON_GOOD, [], @(h,ev)SetDefaultSurf(iSubject, 'Anatomy', iAnatomy));
end
% === MENU: CREATE SURFACES ===
@@ -1078,10 +1107,14 @@
AddSeparator(jPopup);
gui_component('MenuItem', jPopup, [], 'MNI normalization', IconLoader.ICON_ANATOMY, [], @(h,ev)process_mni_normalize('ComputeInteractive', filenameRelative));
gui_component('MenuItem', jPopup, [], 'Resample volume...', IconLoader.ICON_ANATOMY, [], @(h,ev)ResampleMri(filenameRelative));
- if ~bstNodes(1).isMarked()
+ if ~bstNodes(1).isMarked() && ~isempty(sSubject.iAnatomy)
jMenuRegister = gui_component('Menu', jPopup, [], 'Register with default MRI', IconLoader.ICON_ANATOMY);
- gui_component('MenuItem', jMenuRegister, [], 'SPM: Register + reslice', IconLoader.ICON_ANATOMY, [], @(h,ev)MriCoregister(filenameRelative, [], 'spm', 1));
- gui_component('MenuItem', jMenuRegister, [], 'SPM: Register only', IconLoader.ICON_ANATOMY, [], @(h,ev)MriCoregister(filenameRelative, [], 'spm', 0));
+ gui_component('MenuItem', jMenuRegister, [], 'SPM: Register + reslice', IconLoader.ICON_ANATOMY, [], @(h,ev)MriCoregister(filenameRelative, [], 'SPM', 1));
+ gui_component('MenuItem', jMenuRegister, [], 'SPM: Register only', IconLoader.ICON_ANATOMY, [], @(h,ev)MriCoregister(filenameRelative, [], 'SPM', 0));
+ if isCt
+ gui_component('MenuItem', jMenuRegister, [], 'CT2MRI: Register + reslice', IconLoader.ICON_ANATOMY, [], @(h,ev)MriCoregister(filenameRelative, [], 'CT2MRI', 1));
+ gui_component('MenuItem', jMenuRegister, [], 'CT2MRI: Register only', IconLoader.ICON_ANATOMY, [], @(h,ev)MriCoregister(filenameRelative, [], 'CT2MRI', 0));
+ end
AddSeparator(jMenuRegister);
gui_component('MenuItem', jMenuRegister, [], 'Reslice / normalized coordinates (MNI)', IconLoader.ICON_ANATOMY, [], @(h,ev)MriReslice(filenameRelative, [], 'ncs', 'ncs'));
gui_component('MenuItem', jMenuRegister, [], 'Reslice / subject coordinates (SCS)', IconLoader.ICON_ANATOMY, [], @(h,ev)MriReslice(filenameRelative, [], 'scs', 'scs'));
@@ -1091,7 +1124,7 @@
end
end
% === MRI SEGMENTATION ===
- fcnMriSegment(jPopup, sSubject, iSubject, iAnatomy, isAtlas);
+ fcnMriSegment(jPopup, sSubject, iSubject, iAnatomy, isAtlas, isCt);
end
% === MENU: EXPORT ===
% Export menu (added later)
@@ -1106,7 +1139,11 @@
sSubject = bst_get('Subject', iSubject);
% === DISPLAY ===
- gui_component('MenuItem', jPopup, [], 'Display', IconLoader.ICON_DISPLAY, [], @(h,ev)view_surface(filenameRelative));
+ if strcmpi(nodeType, 'other') && ~isempty(regexp(filenameRelative, 'tess_textured', 'match'))
+ gui_component('MenuItem', jPopup, [], 'Display', IconLoader.ICON_DISPLAY, [], @(h,ev)ViewTexturedSurface(filenameRelative));
+ else
+ gui_component('MenuItem', jPopup, [], 'Display', IconLoader.ICON_DISPLAY, [], @(h,ev)view_surface(filenameRelative));
+ end
% === SET SURFACE TYPE ===
if ~bst_get('ReadOnly') && (length(bstNodes) == 1)
@@ -1133,7 +1170,7 @@
jItemSetSurfTypeOther.setSelected(1);
end
end
-
+
% SET AS DEFAULT SURFACE
if ~bst_get('ReadOnly') && (length(bstNodes) == 1)
iSurface = bstNodes(1).getItemIndex();
@@ -1150,6 +1187,14 @@
% Separator
AddSeparator(jPopup);
end
+
+ % === DIGITIZE (3D SCANNER) OPTION ===
+ if strcmpi(nodeType, 'other') && ~isempty(regexp(filenameRelative, 'tess_textured', 'match'))
+ gui_component('MenuItem', jPopup, [], 'Digitize (3D scanner)', IconLoader.ICON_SNAPSHOT, [], @(h,ev)bst_call(@panel_digitize, 'Start', '3DScanner', sSubject, iSubject, filenameRelative));
+ % Separator
+ AddSeparator(jPopup);
+ end
+
% NUMBER OF SELECTED FILES
if (length(bstNodes) >= 2)
if ~bst_get('ReadOnly')
@@ -1345,6 +1390,8 @@
end
gui_component('MenuItem', jPopup, [], ['View ' mod{1} ' leadfield sensitivity (MRI 3D)'], IconLoader.ICON_ANATOMY, [], @(h,ev)bst_call(@view_leadfield_sensitivity, filenameRelative, mod{1}, 'Mri3D'));
gui_component('MenuItem', jPopup, [], ['View ' mod{1} ' leadfield sensitivity (MRI Viewer)'], IconLoader.ICON_ANATOMY, [], @(h,ev)bst_call(@view_leadfield_sensitivity, filenameRelative, mod{1}, 'MriViewer'));
+ AddSeparator(jPopup);
+ gui_component('MenuItem', jPopup, [], ['Apply ' mod{1} ' leadfield exclusion zone'], IconLoader.ICON_HEADMODEL, [], @(h,ev)process_headmodel_exclusionzone('ComputeInteractive', filenameRelative, mod{1}, iStudy));
elseif strcmpi(sStudy.HeadModel(iHeadModel).HeadModelType, 'surface')
gui_component('MenuItem', jPopup, [], ['View ' mod{1} ' leadfield sensitivity'], IconLoader.ICON_ANATOMY, [], @(h,ev)bst_call(@view_leadfield_sensitivity, filenameRelative, mod{1}, 'Surface'));
end
@@ -1910,7 +1957,7 @@
% Get data type
isStat = strcmpi(char(bstNodes(1).getType()), 'ptimefreq');
if isStat
- TimefreqMat = in_bst_timefreq(filenameRelative, 0, 'DataType');
+ TimefreqMat = in_bst_timefreq(filenameRelative, 0, 'DataType', 'Time');
if ~isempty(TimefreqMat.DataType)
DataType = TimefreqMat.DataType;
else
@@ -1950,7 +1997,12 @@
if (length(bstNodes) == 1)
% ===== CONNECTIVITY =====
if ~isempty(strfind(filenameRelative, '_connectn')) || ~isempty(strfind(filenameRelative, '_connect1'))
-
+ % Time defined Connectivity file or Stat Connectivity file
+ if isStat
+ cnxTimeDef = length(TimefreqMat.Time) > 1;
+ else
+ cnxTimeDef = ~isempty(strfind(sStudy.Timefreq(iTimefreq).Comment, '-time'));
+ end
% [NxN] only
if ~isempty(strfind(filenameRelative, '_connectn'))
gui_component('MenuItem', jPopup, [], 'Display as graph [NxN]', IconLoader.ICON_CONNECTN, [], @(h,ev)view_connect(filenameRelative, 'GraphFull'));
@@ -1971,7 +2023,8 @@
gui_component('MenuItem', jPopup, [], 'Power spectrum', IconLoader.ICON_SPECTRUM, [], @(h,ev)view_spectrum(filenameRelative, 'Spectrum'));
end
if ~isempty(strfind(filenameRelative, '_plvt')) || ~isempty(strfind(filenameRelative, '_corr_time')) || ~isempty(strfind(filenameRelative, '_cohere_time')) ...
- || ~isempty(strfind(filenameRelative, '_wplit')) || ~isempty(strfind(filenameRelative, '_ciplvt'))
+ || ~isempty(strfind(filenameRelative, '_wplit')) || ~isempty(strfind(filenameRelative, '_ciplvt')) ...
+ || (cnxTimeDef && (~isempty(strfind(filenameRelative, '_corr')) || ~isempty(strfind(filenameRelative, '_cohere'))))
gui_component('MenuItem', jPopup, [], 'Time series', IconLoader.ICON_DATA, [], @(h,ev)view_spectrum(filenameRelative, 'TimeSeries'));
gui_component('MenuItem', jMenuConn1, [], 'One row', IconLoader.ICON_TIMEFREQ, [], @(h,ev)view_timefreq(filenameFull, 'SingleSensor'));
gui_component('MenuItem', jMenuConn1, [], 'All rows', IconLoader.ICON_TIMEFREQ, [], @(h,ev)view_timefreq(filenameFull, 'AllSensors'));
@@ -1989,7 +2042,8 @@
case 'results'
% One channel
if ~isempty(strfind(filenameRelative, '_plvt')) || ~isempty(strfind(filenameRelative, '_corr_time')) || ~isempty(strfind(filenameRelative, '_cohere_time'))...
- || ~isempty(strfind(filenameRelative, '_wplit')) || ~isempty(strfind(filenameRelative, '_ciplvt'))
+ || ~isempty(strfind(filenameRelative, '_wplit')) || ~isempty(strfind(filenameRelative, '_ciplvt')) ...
+ || (cnxTimeDef && (~isempty(strfind(filenameRelative, '_corr')) || ~isempty(strfind(filenameRelative, '_cohere'))))
gui_component('MenuItem', jMenuConn1, [], 'One channel', IconLoader.ICON_TIMEFREQ, [], @(h,ev)view_timefreq(filenameFull, 'SingleSensor'));
AddSeparator(jMenuConn1);
end
@@ -2010,7 +2064,8 @@
gui_component('MenuItem', jPopup, [], 'Power spectrum', IconLoader.ICON_SPECTRUM, [], @(h,ev)view_spectrum(filenameRelative, 'Spectrum'));
end
if ~isempty(strfind(filenameRelative, '_plvt')) || ~isempty(strfind(filenameRelative, '_corr_time')) || ~isempty(strfind(filenameRelative, '_cohere_time')) ...
- || ~isempty(strfind(filenameRelative, '_wplit')) || ~isempty(strfind(filenameRelative, '_ciplvt'))
+ || ~isempty(strfind(filenameRelative, '_wplit')) || ~isempty(strfind(filenameRelative, '_ciplvt')) ...
+ || (cnxTimeDef && (~isempty(strfind(filenameRelative, '_corr')) || ~isempty(strfind(filenameRelative, '_cohere'))))
gui_component('MenuItem', jPopup, [], 'Time series', IconLoader.ICON_DATA, [], @(h,ev)view_spectrum(filenameRelative, 'TimeSeries'));
gui_component('MenuItem', jMenuConn1, [], 'One row', IconLoader.ICON_TIMEFREQ, [], @(h,ev)view_timefreq(filenameFull, 'SingleSensor'));
gui_component('MenuItem', jMenuConn1, [], 'All rows', IconLoader.ICON_TIMEFREQ, [], @(h,ev)view_timefreq(filenameFull, 'AllSensors'));
@@ -2246,22 +2301,24 @@
DataType = sStudy.Timefreq(iTimefreq).DataType;
DataFile = sStudy.Timefreq(iTimefreq).DataFile;
end
+ if strcmpi(DataType, 'data')
+ % Get avaible modalities for this data file
+ DisplayMod = bst_get('TimefreqDisplayModalities', filenameRelative);
+ % Add SEEG+ECOG
+ if ~isempty(DisplayMod) && all(ismember({'SEEG','ECOG'}, DisplayMod))
+ DisplayMod = cat(2, {'ECOG+SEEG'}, DisplayMod);
+ end
+ end
% One file selected
if (length(bstNodes) == 1)
% ===== RECORDINGS =====
if strcmpi(DataType, 'data')
- % Get avaible modalities for this data file
- DisplayMod = bst_get('TimefreqDisplayModalities', filenameRelative);
- % Add SEEG+ECOG
- if all(ismember({'SEEG','ECOG'}, DisplayMod))
- DisplayMod = cat(2, {'ECOG+SEEG'}, DisplayMod);
- end
% Power spectrum
gui_component('MenuItem', jPopup, [], 'Power spectrum', IconLoader.ICON_SPECTRUM, [], @(h,ev)view_spectrum(filenameRelative, 'Spectrum'));
AddSeparator(jPopup);
% Topography
isGradNorm = strcmpi(nodeType, 'spectrum');
- jSubMenus = fcnPopupTopoNoInterp(jPopup, filenameRelative, DisplayMod, 0, isGradNorm, 0);
+ jSubMenus = fcnPopupTopoNoInterp(jPopup, filenameRelative, DisplayMod, 1, isGradNorm, 0);
% Interpolate SEEG/ECOG on the anatomy
for iMod = 1:length(DisplayMod)
% Create submenu if there are multiple modalities
@@ -2279,7 +2336,7 @@
AddSeparator(jPopup);
% EEG: Display on scalp
if strcmpi(DisplayMod{iMod}, 'EEG') && ~isempty(sSubject) && ~isempty(sSubject.iScalp)
- gui_component('MenuItem', jPopup, [], 'Display on scalp', IconLoader.ICON_SURFACE_SCALP, [], @(h,ev)view_surface_data(sSubject.Surface(sSubject.iScalp).FileName, filenameRelative, 'EEG'));
+ gui_component('MenuItem', jMenuModality, [], 'Display on scalp', IconLoader.ICON_SURFACE_SCALP, [], @(h,ev)view_surface_data(sSubject.Surface(sSubject.iScalp).FileName, filenameRelative, 'EEG'));
% SEEG/ECOG: Display on cortex or MRI
elseif ismember(DisplayMod{iMod}, {'SEEG', 'ECOG', 'ECOG+SEEG'}) && ~isempty(sSubject)
if ~isempty(sSubject.iCortex)
@@ -2335,6 +2392,20 @@
else
gui_component('MenuItem', jPopup, [], 'Power spectrum', IconLoader.ICON_SPECTRUM, [], @(h,ev)view_spectrum(filenameRelative, 'Spectrum'));
end
+ else
+ % Display of multiple files
+ if ~isempty(DisplayMod)
+ % === 2DLAYOUT ===
+ mod2D = intersect(DisplayMod, {'EEG', 'MEG', 'MEG MAG', 'MEG GRAD', 'ECOG', 'SEEG', 'ECOG+SEEG', 'NIRS'});
+ if (length(mod2D) == 1)
+ gui_component('MenuItem', jPopup, [], ['2D Layout: ' mod2D{1}], IconLoader.ICON_2DLAYOUT, [], @(h,ev)bst_call(@view_topography, GetAllFilenames(bstNodes), mod2D{1}, '2DLayout'));
+ elseif (length(mod2D) > 1)
+ jMenu2d = gui_component('Menu', jPopup, [], '2D Layout', IconLoader.ICON_2DLAYOUT, [], []);
+ for iMod = 1:length(mod2D)
+ gui_component('MenuItem', jMenu2d, [], mod2D{iMod}, IconLoader.ICON_2DLAYOUT, [], @(h,ev)bst_call(@view_topography, GetAllFilenames(bstNodes), mod2D{iMod}, '2DLayout'));
+ end
+ end
+ end
end
% Project sources
if strcmpi(DataType, 'results') && ~strcmpi(nodeType, 'ptimefreq') && isempty(strfind(filenameRelative, '_KERNEL_'))
@@ -2814,7 +2885,7 @@ function fcnPopupAlign()
jMenuAlignManual = gui_component('Menu', jPopup, [], 'Align manually on...', IconLoader.ICON_ALIGN_SURFACES, [], []);
% ADD ANATOMIES
for iAnat = 1:length(sSubject.Anatomy)
- if isempty(strfind(sSubject.Anatomy(iAnat).FileName, '_volatlas'))
+ if isempty(strfind(sSubject.Anatomy(iAnat).FileName, '_volatlas')) && isempty(strfind(sSubject.Anatomy(iAnat).FileName, '_volct'))
fullAnatFile = bst_fullfile(ProtocolInfo.SUBJECTS, sSubject.Anatomy(iAnat).FileName);
gui_component('MenuItem', jMenuAlignManual, [], sSubject.Anatomy(iAnat).Comment, IconLoader.ICON_ANATOMY, [], @(h,ev)tess_align_manual(fullAnatFile, filenameFull));
end
@@ -2846,6 +2917,36 @@ function fcnPopupImportChannel(bstNodes, jMenu, isAddLoc)
jMenu = gui_component('Menu', jMenu, [], 'Add EEG positions', IconLoader.ICON_CHANNEL, [], []);
% Import from file
gui_component('MenuItem', jMenu, [], 'Import from file', IconLoader.ICON_CHANNEL, [], @(h,ev)channel_add_loc(iAllStudies, [], 1));
+ % From other Studies within same Subject
+ sStudies = bst_get('Study', iAllStudies);
+ % If adding locations to multiple channel files, they must be from the same subject
+ if length(unique({sStudies.BrainStormSubject})) == 1
+ sSubject = bst_get('Subject', sStudies(1).BrainStormSubject);
+ if sSubject.UseDefaultChannel == 0
+ % Only consider Studies with ChannelFile
+ [sStudies, iStudies] = bst_get('StudyWithSubject', sSubject.FileName);
+ iChStudies = ~cellfun(@isempty, {sStudies.Channel});
+ sStudies = sStudies(iChStudies);
+ iStudies = iStudies(iChStudies);
+ [~, ixDiff] = setdiff(iStudies, iAllStudies);
+ if ~isempty(ixDiff)
+ % Create menu and entries
+ AddSeparator(jMenu);
+ jMenuStudy = gui_component('Menu', jMenu, [], 'From other studies', IconLoader.ICON_CHANNEL, [], []);
+ for ix = 1 : length(ixDiff)
+ conditionName = sStudies(ixDiff(ix)).Condition{1};
+ if length(conditionName) > 4 && strcmpi(conditionName(1:4), '@raw')
+ iconLoader = IconLoader.ICON_RAW_FOLDER_CLOSE;
+ conditionName(1:4) = '';
+ else
+ iconLoader = IconLoader.ICON_FOLDER_CLOSE;
+ end
+ % Menu entry
+ gui_component('MenuItem', jMenuStudy, [], conditionName, iconLoader, [], @(h,ev)channel_add_loc(iAllStudies, sStudies(ixDiff(ix)).Channel.FileName, 1));
+ end
+ end
+ end
+ end
% If only SEEG/ECOG, stop here (we do not want to offer the standard EEG caps, it doesn't make sense)
if (isAddLoc < 2)
return;
@@ -2856,72 +2957,8 @@ function fcnPopupImportChannel(bstNodes, jMenu, isAddLoc)
gui_component('MenuItem', jMenu, [], 'Import channel file', IconLoader.ICON_CHANNEL, [], @(h,ev)bst_call(@ImportChannelCheck, iAllStudies));
jMenu = gui_component('Menu', jMenu, [], 'Use default EEG cap', IconLoader.ICON_CHANNEL, [], []);
end
- % === USE DEFAULT CHANNEL FILE ===
- % Get registered Brainstorm EEG defaults
- bstDefaults = bst_get('EegDefaults');
- if ~isempty(bstDefaults)
- % Add a directory per template block available
- for iDir = 1:length(bstDefaults)
- jMenuDir = gui_component('Menu', jMenu, [], bstDefaults(iDir).name, IconLoader.ICON_FOLDER_CLOSE, [], []);
- isMni = strcmpi(bstDefaults(iDir).name, 'ICBM152');
- % Create subfolder for cap manufacturer
- jMenuOther = gui_component('Menu', [], [], 'Generic', IconLoader.ICON_FOLDER_CLOSE, [], []);
- jMenuAnt = gui_component('Menu', [], [], 'ANT', IconLoader.ICON_FOLDER_CLOSE, [], []);
- jMenuBs = gui_component('Menu', [], [], 'BioSemi', IconLoader.ICON_FOLDER_CLOSE, [], []);
- jMenuBp = gui_component('Menu', [], [], 'BrainProducts', IconLoader.ICON_FOLDER_CLOSE, [], []);
- jMenuEgi = gui_component('Menu', [], [], 'EGI', IconLoader.ICON_FOLDER_CLOSE, [], []);
- jMenuNs = gui_component('Menu', [], [], 'NeuroScan', IconLoader.ICON_FOLDER_CLOSE, [], []);
- % Add an item per Template available
- fList = bstDefaults(iDir).contents;
- % Sort in natural order
- [tmp,I] = sort_nat({fList.name});
- fList = fList(I);
- % Create an entry for each default
- for iFile = 1:length(fList)
- % Define callback function
- if isAddLoc
- fcnCallback = @(h,ev)channel_add_loc(iAllStudies, fList(iFile).fullpath, 1, isMni);
- else
- fcnCallback = @(h,ev)db_set_channel(iAllStudies, fList(iFile).fullpath, 1, 0);
- end
- % Find corresponding submenu
- if ~isempty(strfind(fList(iFile).name, 'ANT'))
- jMenuType = jMenuAnt;
- elseif ~isempty(strfind(fList(iFile).name, 'BioSemi'))
- jMenuType = jMenuBs;
- elseif ~isempty(strfind(fList(iFile).name, 'BrainProducts'))
- jMenuType = jMenuBp;
- elseif ~isempty(strfind(fList(iFile).name, 'GSN')) || ~isempty(strfind(fList(iFile).name, 'U562'))
- jMenuType = jMenuEgi;
- elseif ~isempty(strfind(fList(iFile).name, 'Neuroscan'))
- jMenuType = jMenuNs;
- else
- jMenuType = jMenuOther;
- end
- % Create item
- gui_component('MenuItem', jMenuType, [], fList(iFile).name, IconLoader.ICON_CHANNEL, [], fcnCallback);
- end
- % Add if not empty
- if (jMenuOther.getMenuComponentCount() > 0)
- jMenuDir.add(jMenuOther);
- end
- if (jMenuAnt.getMenuComponentCount() > 0)
- jMenuDir.add(jMenuAnt);
- end
- if (jMenuBs.getMenuComponentCount() > 0)
- jMenuDir.add(jMenuBs);
- end
- if (jMenuBp.getMenuComponentCount() > 0)
- jMenuDir.add(jMenuBp);
- end
- if (jMenuEgi.getMenuComponentCount() > 0)
- jMenuDir.add(jMenuEgi);
- end
- if (jMenuNs.getMenuComponentCount() > 0)
- jMenuDir.add(jMenuNs);
- end
- end
- end
+ % Use default channel file
+ menu_default_eegcaps(jMenu, iAllStudies, isAddLoc);
end
%% ===== EDIT NODE =====
@@ -3060,7 +3097,7 @@ function fcnPopupDisplayTopography(jMenu, FileName, AllMod, Modality, isStat)
%% ===== MRI SEGMENTATION =====
-function fcnMriSegment(jPopup, sSubject, iSubject, iAnatomy, isAtlas)
+function fcnMriSegment(jPopup, sSubject, iSubject, iAnatomy, isAtlas, isCt)
import org.brainstorm.icon.*;
% No anatomy: nothing to do
if isempty(sSubject.Anatomy)
@@ -3078,24 +3115,39 @@ function fcnMriSegment(jPopup, sSubject, iSubject, iAnatomy, isAtlas)
else
MriFile = {sSubject.Anatomy(iAnatomy).FileName};
end
+ % Menu label
+ volType = 'MRI';
+ volIcon = 'ICON_ANATOMY';
+ if isCt
+ volType = 'CT';
+ volIcon = 'ICON_VOLCT';
+ end
% Add menu separator
AddSeparator(jPopup);
% === MRI/CT ===
if ~isAtlas
% Create sub-menu
- jMenu = gui_component('Menu', jPopup, [], 'MRI segmentation', IconLoader.ICON_ANATOMY);
+ jMenu = gui_component('Menu', jPopup, [], [volType, ' segmentation'], IconLoader.(volIcon));
+ % === MESH FROM THRESHOLD CT ===
+ if (length(iAnatomy) <= 1) && isCt
+ if ~isempty(sSubject.iAnatomy)
+ gui_component('MenuItem', jMenu, [], 'SPM: Skull stripping', IconLoader.(volIcon), [], @(h,ev)MriSkullStrip(MriFile, sSubject.Anatomy(iAnatomy).FileName, 'SPM'));
+ gui_component('MenuItem', jMenu, [], 'BrainSuite: Skull stripping', IconLoader.(volIcon), [], @(h,ev)MriSkullStrip(MriFile, sSubject.Anatomy(iAnatomy).FileName, 'BrainSuite'));
+ end
+ gui_component('MenuItem', jMenu, [], 'Generate threshold mesh from CT', IconLoader.ICON_SURFACE_SCALP, [], @(h,ev)tess_isosurface(MriFile));
+ end
% === GENERATE HEAD/BEM ===
- if (length(iAnatomy) <= 1)
+ if (length(iAnatomy) <= 1) && ~isCt
gui_component('MenuItem', jMenu, [], 'Generate head surface', IconLoader.ICON_SURFACE_SCALP, [], @(h,ev)tess_isohead(MriFile));
gui_component('MenuItem', jMenu, [], 'Generate BEM surfaces', IconLoader.ICON_FEM, [], @(h,ev)bst_call(@process_generate_bem, 'ComputeInteractive', iSubject, iAnatomy));
end
% === GENERATE FEM ===
- if (length(iAnatomy) <= 2) % T1 + optional T2
+ if (length(iAnatomy) <= 2) && ~isCt % T1 + optional T2
jItemFem = gui_component('MenuItem', jMenu, [], 'Generate FEM mesh', IconLoader.ICON_FEM, [], @(h,ev)bst_call(@process_fem_mesh, 'ComputeInteractive', iSubject, iAnatomy));
end
- % === SEGMENTATION ===
- if (length(iAnatomy) <= 1)
+ % === MRI SEGMENTATION ===
+ if (length(iAnatomy) <= 1) && ~isCt
AddSeparator(jMenu);
% gui_component('MenuItem', jMenu, [], 'SPM12 canonical surfaces', IconLoader.ICON_FEM, [], @(h,ev)bst_call(@process_generate_canonical, 'ComputeInteractive', iSubject, iAnatomy));
gui_component('MenuItem', jMenu, [], 'CAT12: Cortex, atlases, tissues', IconLoader.ICON_FEM, [], @(h,ev)bst_call(@process_segment_cat12, 'ComputeInteractive', iSubject, iAnatomy));
@@ -3119,11 +3171,15 @@ function fcnMriSegment(jPopup, sSubject, iSubject, iAnatomy, isAtlas)
if isempty(iAnatomy)
gui_component('MenuItem', jPopup, [], 'Deface anatomy', IconLoader.ICON_ANATOMY, [], @(h,ev)process_mri_deface('Compute', iSubject, struct('isDefaceHead', 1)));
else
- gui_component('MenuItem', jPopup, [], 'Deface volume', IconLoader.ICON_ANATOMY, [], @(h,ev)process_mri_deface('Compute', MriFile, struct('isDefaceHead', 0)));
+ gui_component('MenuItem', jPopup, [], 'Deface volume', IconLoader.(volIcon), [], @(h,ev)process_mri_deface('Compute', MriFile, struct('isDefaceHead', 0)));
end
% === SEEG/ECOG ===
- if (length(iAnatomy) <= 1)
- gui_component('MenuItem', jPopup, [], 'SEEG/ECOG implantation', IconLoader.ICON_SEEG_DEPTH, [], @(h,ev)bst_call(@panel_ieeg, 'CreateNewImplantation', MriFile));
+ % Right click on the subject only
+ if isempty(iAnatomy) && iSubject ~=0
+ gui_component('MenuItem', jPopup, [], 'SEEG/ECOG implantation', IconLoader.ICON_SEEG_DEPTH, [], @(h,ev)bst_call(@panel_ieeg, 'CreateImplantation', sSubject));
+ % Right click on a desired volume (MRI/CT) in a subject
+ elseif (length(iAnatomy) == 1) && iSubject ~=0
+ gui_component('MenuItem', jPopup, [], 'SEEG/ECOG implantation', IconLoader.ICON_SEEG_DEPTH, [], @(h,ev)bst_call(@panel_ieeg, 'CreateImplantation', MriFile));
end
% === TISSUE SEGMENTATION ===
@@ -3337,7 +3393,7 @@ function SurfaceFillHoles_Callback(TessFile)
sHead = in_tess_bst(TessFile, 0);
% Get subject
[sSubject, iSubject] = bst_get('SurfaceFile', TessFile);
- if isempty(sSubject.Anatomy)
+ if isempty(sSubject.Anatomy) || isempty(sSubject.iAnatomy)
bst_error('No MRI available.', 'Remove surface holes');
return;
end
@@ -3764,5 +3820,16 @@ function MriReslice(MriFileSrc, MriFileRef, TransfSrc, TransfRef)
end
end
+function MriSkullStrip(MriFileSrc, MriFileRef, Method)
+ [MriFileMask, errMsg] = bst_call(@mri_skullstrip, MriFileSrc, MriFileRef, Method);
+ if isempty(MriFileMask) || ~isempty(errMsg)
+ bst_error(['Could not perform skull stripping.', 10, 10, errMsg], 'MRI skull stripping', 0);
+ end
+end
+
-
+%% ===== DISPLAY TEXTURED SURFACE =====
+function ViewTexturedSurface(filenameRelative)
+ sSurf = bst_memory('LoadSurface', filenameRelative);
+ view_surface_matrix(sSurf.Vertices, sSurf.Faces, [], sSurf.Color, [], [], filenameRelative);
+end
diff --git a/toolbox/tree/tree_set_noisecov.m b/toolbox/tree/tree_set_noisecov.m
index 134cbaeba..3de0bf7ab 100644
--- a/toolbox/tree/tree_set_noisecov.m
+++ b/toolbox/tree/tree_set_noisecov.m
@@ -73,16 +73,20 @@ function tree_set_noisecov(bstNodes, NoiseCovFile, isDataCov)
% === IMPORT FROM MATLAB ===
elseif strcmpi(NoiseCovFile, 'MatlabVar')
% Get matlab variable
- NoiseCovMat.Comment = 'Noise covariance (Matlab)';
[NoiseCovMat.NoiseCov, varname] = in_matlab_var();
% Check if import was cancelled
if isempty(NoiseCovMat.NoiseCov)
return
end
- % Check if input was already a structure
+ % Check if input was already a Brainstorm structure
if isstruct(NoiseCovMat.NoiseCov) && isfield(NoiseCovMat.NoiseCov, 'NoiseCov')
- NoiseCovMat.NoiseCov = NoiseCovMat.NoiseCov.NoiseCov;
+ NoiseCovMat = struct_copy_fields(db_template('noisecovmat'), NoiseCovMat.NoiseCov);
+ if ~isempty(NoiseCovMat.History)
+ NoiseCovMat.History = [];
+ end
end
+ % Update comment
+ NoiseCovMat.Comment = 'Noise covariance (Matlab)';
% History: Import from Matlab
NoiseCovMat = bst_history('add', NoiseCovMat, 'import', ['Import from Matlab variable: ' varname]);
% Save in database