├── .gitattributes ├── .gitignore ├── JSONio ├── LICENSE ├── README.md ├── jsmn.c ├── jsmn.h ├── jsonread.c ├── jsonread.m ├── jsonread.mexa64 ├── jsonread.mexmaci64 ├── jsonread.mexw64 ├── jsonwrite.m └── tests │ ├── test_jsonread.m │ └── test_jsonwrite.m ├── README.md ├── bids_checkfields.m ├── bids_compare.m ├── bids_export.m ├── bids_export_example.m ├── bids_export_example2.m ├── bids_export_example3.m ├── bids_export_example4.m ├── bids_export_example4_eye.m ├── bids_export_example5_ieeg.m ├── bids_export_eye_tracking_example5.m ├── bids_exporter.m ├── bids_formatderivative.m ├── bids_geteventfieldsfromfolder.m ├── bids_getinfofromfolder.m ├── bids_gettaskfromfolder.m ├── bids_importchanlocs.m ├── bids_importcoordsystemfile.m ├── bids_importeventfile.m ├── bids_importjson.m ├── bids_loadfile.m ├── bids_matlab_tools_ver.m ├── bids_metadata_stats.m ├── bids_reexport.m ├── bids_spreadsheet2participants.m ├── bids_template_README.m ├── bids_writebehfile.m ├── bids_writechanfile.m ├── bids_writeelectrodefile.m ├── bids_writeeventfile.m ├── bids_writeieegtinfofile.m ├── bids_writemegtinfofile.m ├── bids_writetinfofile.m ├── eeg_compare_bids.m ├── eeg_getchantype.m ├── eeg_import.m ├── eeg_mergechannels.m ├── eeg_selectsegment.m ├── eegplugin_eegbids.m ├── mergeStructures.m ├── pop_checkdatasetinfo.m ├── pop_eventinfo.m ├── pop_exportbids.m ├── pop_importbids.m ├── pop_participantinfo.m ├── pop_taskinfo.m ├── pop_validatebids.m ├── rename_brainvision_files.m ├── sort_nat.m └── std_tobids.m /.gitattributes: -------------------------------------------------------------------------------- 1 | testing/data/*.set filter=lfs diff=lfs merge=lfs -text 2 | testing/data/*.fdt filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | EEG-BIDS_testcases 2 | *.asv 3 | *~ 4 | *.asv 5 | -------------------------------------------------------------------------------- /JSONio/LICENSE: -------------------------------------------------------------------------------- 1 | JSONio: a MATLAB/Octave JSON library 2 | Copyright (c) 2015-2021 Guillaume Flandin 3 | 4 | The MIT License (MIT) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /JSONio/README.md: -------------------------------------------------------------------------------- 1 | # JSONio: a MATLAB/Octave JSON library 2 | 3 | JSONio is a MATLAB/Octave library to read/write data in the JSON (JavaScript Object Notation) data-interchange format. 4 | 5 | * JSON: https://www.json.org/ 6 | 7 | It relies on the JSON parser jsmn written by [Serge Zaitsev](https://zserge.com/): 8 | 9 | * jsmn: https://zserge.com/jsmn/ 10 | 11 | This library is also part of SPM: 12 | 13 | * SPM: https://www.fil.ion.ucl.ac.uk/spm/ 14 | 15 | ## INSTALLATION 16 | 17 | Simply add the JSONio directory to the MATLAB path: 18 | 19 | ```matlab 20 | addpath /home/login/Documents/MATLAB/JSONio 21 | ``` 22 | 23 | A compiled MEX file is provided for 64-bit MATLAB platforms. It needs to be compiled for Octave with: 24 | ``` 25 | mkoctfile --mex jsonread.c jsmn.c -DJSMN_PARENT_LINKS 26 | ``` 27 | 28 | ## EXAMPLE 29 | 30 | ```matlab 31 | json = jsonread(filename) 32 | 33 | jsonwrite(filename, json) 34 | ``` 35 | -------------------------------------------------------------------------------- /JSONio/jsmn.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Serge A. Zaitsev 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | 24 | #include 25 | 26 | #include "jsmn.h" 27 | 28 | /** 29 | * Allocates a fresh unused token from the token pull. 30 | */ 31 | static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, 32 | jsmntok_t *tokens, size_t num_tokens) { 33 | jsmntok_t *tok; 34 | if (parser->toknext >= num_tokens) { 35 | return NULL; 36 | } 37 | tok = &tokens[parser->toknext++]; 38 | tok->start = tok->end = -1; 39 | tok->size = 0; 40 | #ifdef JSMN_PARENT_LINKS 41 | tok->parent = -1; 42 | #endif 43 | return tok; 44 | } 45 | 46 | /** 47 | * Fills token type and boundaries. 48 | */ 49 | static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, 50 | int start, int end) { 51 | token->type = type; 52 | token->start = start; 53 | token->end = end; 54 | token->size = 0; 55 | } 56 | 57 | /** 58 | * Fills next available token with JSON primitive. 59 | */ 60 | static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, 61 | size_t len, jsmntok_t *tokens, size_t num_tokens) { 62 | jsmntok_t *token; 63 | int start; 64 | 65 | start = parser->pos; 66 | 67 | for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { 68 | switch (js[parser->pos]) { 69 | #ifndef JSMN_STRICT 70 | /* In strict mode primitive must be followed by "," or "}" or "]" */ 71 | case ':': 72 | #endif 73 | case '\t' : case '\r' : case '\n' : case ' ' : 74 | case ',' : case ']' : case '}' : 75 | goto found; 76 | } 77 | if (js[parser->pos] < 32 || js[parser->pos] >= 127) { 78 | parser->pos = start; 79 | return JSMN_ERROR_INVAL; 80 | } 81 | } 82 | #ifdef JSMN_STRICT 83 | /* In strict mode primitive must be followed by a comma/object/array */ 84 | parser->pos = start; 85 | return JSMN_ERROR_PART; 86 | #endif 87 | 88 | found: 89 | if (tokens == NULL) { 90 | parser->pos--; 91 | return 0; 92 | } 93 | token = jsmn_alloc_token(parser, tokens, num_tokens); 94 | if (token == NULL) { 95 | parser->pos = start; 96 | return JSMN_ERROR_NOMEM; 97 | } 98 | jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); 99 | #ifdef JSMN_PARENT_LINKS 100 | token->parent = parser->toksuper; 101 | #endif 102 | parser->pos--; 103 | return 0; 104 | } 105 | 106 | /** 107 | * Filsl next token with JSON string. 108 | */ 109 | static int jsmn_parse_string(jsmn_parser *parser, const char *js, 110 | size_t len, jsmntok_t *tokens, size_t num_tokens) { 111 | jsmntok_t *token; 112 | 113 | int start = parser->pos; 114 | 115 | parser->pos++; 116 | 117 | /* Skip starting quote */ 118 | for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { 119 | char c = js[parser->pos]; 120 | 121 | /* Quote: end of string */ 122 | if (c == '\"') { 123 | if (tokens == NULL) { 124 | return 0; 125 | } 126 | token = jsmn_alloc_token(parser, tokens, num_tokens); 127 | if (token == NULL) { 128 | parser->pos = start; 129 | return JSMN_ERROR_NOMEM; 130 | } 131 | jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); 132 | #ifdef JSMN_PARENT_LINKS 133 | token->parent = parser->toksuper; 134 | #endif 135 | return 0; 136 | } 137 | 138 | /* Backslash: Quoted symbol expected */ 139 | if (c == '\\' && parser->pos + 1 < len) { 140 | int i; 141 | parser->pos++; 142 | switch (js[parser->pos]) { 143 | /* Allowed escaped symbols */ 144 | case '\"': case '/' : case '\\' : case 'b' : 145 | case 'f' : case 'r' : case 'n' : case 't' : 146 | break; 147 | /* Allows escaped symbol \uXXXX */ 148 | case 'u': 149 | parser->pos++; 150 | for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { 151 | /* If it isn't a hex character we have an error */ 152 | if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ 153 | (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ 154 | (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ 155 | parser->pos = start; 156 | return JSMN_ERROR_INVAL; 157 | } 158 | parser->pos++; 159 | } 160 | parser->pos--; 161 | break; 162 | /* Unexpected symbol */ 163 | default: 164 | parser->pos = start; 165 | return JSMN_ERROR_INVAL; 166 | } 167 | } 168 | } 169 | parser->pos = start; 170 | return JSMN_ERROR_PART; 171 | } 172 | 173 | /** 174 | * Parse JSON string and fill tokens. 175 | */ 176 | int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, 177 | jsmntok_t *tokens, unsigned int num_tokens) { 178 | int r; 179 | int i; 180 | jsmntok_t *token; 181 | int count = parser->toknext; 182 | 183 | for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { 184 | char c; 185 | jsmntype_t type; 186 | 187 | c = js[parser->pos]; 188 | switch (c) { 189 | case '{': case '[': 190 | count++; 191 | if (tokens == NULL) { 192 | break; 193 | } 194 | token = jsmn_alloc_token(parser, tokens, num_tokens); 195 | if (token == NULL) 196 | return JSMN_ERROR_NOMEM; 197 | if (parser->toksuper != -1) { 198 | tokens[parser->toksuper].size++; 199 | #ifdef JSMN_PARENT_LINKS 200 | token->parent = parser->toksuper; 201 | #endif 202 | } 203 | token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); 204 | token->start = parser->pos; 205 | parser->toksuper = parser->toknext - 1; 206 | break; 207 | case '}': case ']': 208 | if (tokens == NULL) 209 | break; 210 | type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); 211 | #ifdef JSMN_PARENT_LINKS 212 | if (parser->toknext < 1) { 213 | return JSMN_ERROR_INVAL; 214 | } 215 | token = &tokens[parser->toknext - 1]; 216 | for (;;) { 217 | if (token->start != -1 && token->end == -1) { 218 | if (token->type != type) { 219 | return JSMN_ERROR_INVAL; 220 | } 221 | token->end = parser->pos + 1; 222 | parser->toksuper = token->parent; 223 | break; 224 | } 225 | if (token->parent == -1) { 226 | break; 227 | } 228 | token = &tokens[token->parent]; 229 | } 230 | #else 231 | for (i = parser->toknext - 1; i >= 0; i--) { 232 | token = &tokens[i]; 233 | if (token->start != -1 && token->end == -1) { 234 | if (token->type != type) { 235 | return JSMN_ERROR_INVAL; 236 | } 237 | parser->toksuper = -1; 238 | token->end = parser->pos + 1; 239 | break; 240 | } 241 | } 242 | /* Error if unmatched closing bracket */ 243 | if (i == -1) return JSMN_ERROR_INVAL; 244 | for (; i >= 0; i--) { 245 | token = &tokens[i]; 246 | if (token->start != -1 && token->end == -1) { 247 | parser->toksuper = i; 248 | break; 249 | } 250 | } 251 | #endif 252 | break; 253 | case '\"': 254 | r = jsmn_parse_string(parser, js, len, tokens, num_tokens); 255 | if (r < 0) return r; 256 | count++; 257 | if (parser->toksuper != -1 && tokens != NULL) 258 | tokens[parser->toksuper].size++; 259 | break; 260 | case '\t' : case '\r' : case '\n' : case ' ': 261 | break; 262 | case ':': 263 | parser->toksuper = parser->toknext - 1; 264 | break; 265 | case ',': 266 | if (tokens != NULL && parser->toksuper != -1 && 267 | tokens[parser->toksuper].type != JSMN_ARRAY && 268 | tokens[parser->toksuper].type != JSMN_OBJECT) { 269 | #ifdef JSMN_PARENT_LINKS 270 | parser->toksuper = tokens[parser->toksuper].parent; 271 | #else 272 | for (i = parser->toknext - 1; i >= 0; i--) { 273 | if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { 274 | if (tokens[i].start != -1 && tokens[i].end == -1) { 275 | parser->toksuper = i; 276 | break; 277 | } 278 | } 279 | } 280 | #endif 281 | } 282 | break; 283 | #ifdef JSMN_STRICT 284 | /* In strict mode primitives are: numbers and booleans */ 285 | case '-': case '0': case '1' : case '2': case '3' : case '4': 286 | case '5': case '6': case '7' : case '8': case '9': 287 | case 't': case 'f': case 'n' : 288 | /* And they must not be keys of the object */ 289 | if (tokens != NULL && parser->toksuper != -1) { 290 | jsmntok_t *t = &tokens[parser->toksuper]; 291 | if (t->type == JSMN_OBJECT || 292 | (t->type == JSMN_STRING && t->size != 0)) { 293 | return JSMN_ERROR_INVAL; 294 | } 295 | } 296 | #else 297 | /* In non-strict mode every unquoted value is a primitive */ 298 | default: 299 | #endif 300 | r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); 301 | if (r < 0) return r; 302 | count++; 303 | if (parser->toksuper != -1 && tokens != NULL) 304 | tokens[parser->toksuper].size++; 305 | break; 306 | 307 | #ifdef JSMN_STRICT 308 | /* Unexpected char in strict mode */ 309 | default: 310 | return JSMN_ERROR_INVAL; 311 | #endif 312 | } 313 | } 314 | 315 | if (tokens != NULL) { 316 | for (i = parser->toknext - 1; i >= 0; i--) { 317 | /* Unmatched opened object or array */ 318 | if (tokens[i].start != -1 && tokens[i].end == -1) { 319 | return JSMN_ERROR_PART; 320 | } 321 | } 322 | } 323 | 324 | return count; 325 | } 326 | 327 | /** 328 | * Creates a new parser based over a given buffer with an array of tokens 329 | * available. 330 | */ 331 | void jsmn_init(jsmn_parser *parser) { 332 | parser->pos = 0; 333 | parser->toknext = 0; 334 | parser->toksuper = -1; 335 | } 336 | 337 | -------------------------------------------------------------------------------- /JSONio/jsmn.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Serge A. Zaitsev 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | 24 | #ifndef __JSMN_H_ 25 | #define __JSMN_H_ 26 | 27 | #include 28 | 29 | #ifdef __cplusplus 30 | extern "C" { 31 | #endif 32 | 33 | /** 34 | * JSON type identifier. Basic types are: 35 | * o Object 36 | * o Array 37 | * o String 38 | * o Other primitive: number, boolean (true/false) or null 39 | */ 40 | typedef enum { 41 | JSMN_UNDEFINED = 0, 42 | JSMN_OBJECT = 1, 43 | JSMN_ARRAY = 2, 44 | JSMN_STRING = 3, 45 | JSMN_PRIMITIVE = 4 46 | } jsmntype_t; 47 | 48 | enum jsmnerr { 49 | /* Not enough tokens were provided */ 50 | JSMN_ERROR_NOMEM = -1, 51 | /* Invalid character inside JSON string */ 52 | JSMN_ERROR_INVAL = -2, 53 | /* The string is not a full JSON packet, more bytes expected */ 54 | JSMN_ERROR_PART = -3 55 | }; 56 | 57 | /** 58 | * JSON token description. 59 | * @param type type (object, array, string etc.) 60 | * @param start start position in JSON data string 61 | * @param end end position in JSON data string 62 | */ 63 | typedef struct { 64 | jsmntype_t type; 65 | int start; 66 | int end; 67 | int size; 68 | #ifdef JSMN_PARENT_LINKS 69 | int parent; 70 | #endif 71 | } jsmntok_t; 72 | 73 | /** 74 | * JSON parser. Contains an array of token blocks available. Also stores 75 | * the string being parsed now and current position in that string 76 | */ 77 | typedef struct { 78 | unsigned int pos; /* offset in the JSON string */ 79 | unsigned int toknext; /* next token to allocate */ 80 | int toksuper; /* superior token node, e.g parent object or array */ 81 | } jsmn_parser; 82 | 83 | /** 84 | * Create JSON parser over an array of tokens 85 | */ 86 | void jsmn_init(jsmn_parser *parser); 87 | 88 | /** 89 | * Run JSON parser. It parses a JSON data string into and array of tokens, each describing 90 | * a single JSON object. 91 | */ 92 | int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, 93 | jsmntok_t *tokens, unsigned int num_tokens); 94 | 95 | #ifdef __cplusplus 96 | } 97 | #endif 98 | 99 | #endif /* __JSMN_H_ */ 100 | -------------------------------------------------------------------------------- /JSONio/jsonread.m: -------------------------------------------------------------------------------- 1 | function json = jsonread(filename, opts) 2 | % JSON (JavaScript Object Notation) parser - a compiled routine 3 | % FORMAT json = jsonread(filename, opts) 4 | % filename - name of a JSON file or JSON string 5 | % json - JSON structure 6 | % opts - structure of optional parameters: 7 | % replacementStyle: string to control how non-alphanumeric 8 | % characters are replaced {'underscore','hex','delete','nop'} 9 | % [Default: 'underscore'] 10 | % 11 | % References: 12 | % JSON Standard: http://www.json.org/ 13 | % JSMN C parser: http://zserge.com/jsmn.html 14 | % jsondecode: http://www.mathworks.com/help/matlab/ref/jsondecode.html 15 | 16 | % Guillaume Flandin 17 | % $Id: jsonread.m 7045 2017-03-17 10:41:12Z guillaume $ 18 | 19 | 20 | %-This is merely the help file for the compiled routine 21 | error('jsonread.c not compiled.'); 22 | 23 | % mex jsonread.c jsmn.c -DJSMN_PARENT_LINKS 24 | -------------------------------------------------------------------------------- /JSONio/jsonread.mexa64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sccn/EEG-BIDS/70a3034c77020655b82ba0fd05469d5364162ae0/JSONio/jsonread.mexa64 -------------------------------------------------------------------------------- /JSONio/jsonread.mexmaci64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sccn/EEG-BIDS/70a3034c77020655b82ba0fd05469d5364162ae0/JSONio/jsonread.mexmaci64 -------------------------------------------------------------------------------- /JSONio/jsonread.mexw64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sccn/EEG-BIDS/70a3034c77020655b82ba0fd05469d5364162ae0/JSONio/jsonread.mexw64 -------------------------------------------------------------------------------- /JSONio/jsonwrite.m: -------------------------------------------------------------------------------- 1 | function varargout = jsonwrite(varargin) 2 | % Serialize a JSON (JavaScript Object Notation) structure 3 | % FORMAT jsonwrite(filename,json) 4 | % filename - JSON filename 5 | % json - JSON structure 6 | % 7 | % FORMAT S = jsonwrite(json) 8 | % json - JSON structure 9 | % S - serialized JSON structure (string) 10 | % 11 | % FORMAT [...] = jsonwrite(...,opts) 12 | % opts - structure or list of name/value pairs of optional parameters: 13 | % prettyPrint: indent output [Default: false] 14 | % replacementStyle: string to control how non-alphanumeric 15 | % characters are replaced {'underscore','hex','delete','nop'} 16 | % [Default: 'underscore'] 17 | % convertInfAndNaN: encode NaN, Inf and -Inf as "null" 18 | % [Default: true] 19 | % 20 | % References: 21 | % JSON Standard: https://www.json.org/ 22 | % jsonencode: https://www.mathworks.com/help/matlab/ref/jsonencode.html 23 | 24 | % Guillaume Flandin 25 | % $Id: spm_jsonwrite.m 8031 2020-12-10 13:37:00Z guillaume $ 26 | 27 | 28 | %-Input parameters 29 | %-------------------------------------------------------------------------- 30 | opts = struct(... 31 | 'indent','',... 32 | 'prettyprint',false,... 33 | 'replacementstyle','underscore',... 34 | 'convertinfandnan',true); 35 | opt = {struct([])}; 36 | 37 | if ~nargin 38 | error('Not enough input arguments.'); 39 | elseif nargin == 1 40 | filename = ''; 41 | json = varargin{1}; 42 | else 43 | if ischar(varargin{1}) 44 | filename = varargin{1}; 45 | json = varargin{2}; 46 | opt = varargin(3:end); 47 | else 48 | filename = ''; 49 | json = varargin{1}; 50 | opt = varargin(2:end); 51 | end 52 | end 53 | if numel(opt) == 1 && isstruct(opt{1}) 54 | opt = opt{1}; 55 | elseif mod(numel(opt),2) == 0 56 | opt = cell2struct(opt(2:2:end),opt(1:2:end),2); 57 | else 58 | error('Invalid syntax.'); 59 | end 60 | fn = fieldnames(opt); 61 | for i=1:numel(fn) 62 | if ~isfield(opts,lower(fn{i})), warning('Unknown option "%s".',fn{i}); end 63 | opts.(lower(fn{i})) = opt.(fn{i}); 64 | end 65 | if opts.prettyprint 66 | opts.indent = ' '; 67 | end 68 | optregistry(opts); 69 | 70 | %-JSON serialization 71 | %-------------------------------------------------------------------------- 72 | fmt('init',sprintf(opts.indent)); 73 | S = jsonwrite_var(json,~isempty(opts.indent)); 74 | 75 | %-Output 76 | %-------------------------------------------------------------------------- 77 | if isempty(filename) 78 | varargout = { S }; 79 | else 80 | fid = fopen(filename,'wt'); 81 | if fid == -1 82 | error('Unable to open file "%s" for writing.',filename); 83 | end 84 | fprintf(fid,'%s',S); 85 | fclose(fid); 86 | end 87 | 88 | 89 | %========================================================================== 90 | function S = jsonwrite_var(json,tab) 91 | if nargin < 2, tab = ''; end 92 | if isstruct(json) || isa(json,'containers.Map') 93 | S = jsonwrite_struct(json,tab); 94 | elseif iscell(json) 95 | S = jsonwrite_cell(json,tab); 96 | elseif ischar(json) 97 | if size(json,1) <= 1 98 | S = jsonwrite_char(json); 99 | else 100 | S = jsonwrite_cell(cellstr(json),tab); 101 | end 102 | elseif isnumeric(json) || islogical(json) 103 | S = jsonwrite_numeric(json); 104 | elseif isa(json,'string') 105 | if numel(json) == 1 106 | if ismissing(json) 107 | S = 'null'; 108 | else 109 | S = jsonwrite_char(char(json)); 110 | end 111 | else 112 | json = arrayfun(@(x)x,json,'UniformOutput',false); 113 | json(cellfun(@(x) ismissing(x),json)) = {'null'}; 114 | idx = find(size(json)~=1); 115 | if numel(idx) == 1 % vector 116 | S = jsonwrite_cell(json,tab); 117 | else % array 118 | S = jsonwrite_cell(num2cell(json,setdiff(1:ndims(json),idx(1))),tab); 119 | end 120 | end 121 | elseif isa(json,'datetime') || isa(json,'categorical') 122 | S = jsonwrite_var(string(json)); 123 | elseif isa(json,'table') 124 | S = struct; 125 | s = size(json); 126 | vn = json.Properties.VariableNames; 127 | for i=1:s(1) 128 | for j=1:s(2) 129 | if iscell(json{i,j}) 130 | S(i).(vn{j}) = json{i,j}{1}; 131 | else 132 | S(i).(vn{j}) = json{i,j}; 133 | end 134 | end 135 | end 136 | S = jsonwrite_struct(S,tab); 137 | else 138 | if numel(json) ~= 1 139 | json = arrayfun(@(x)x,json,'UniformOutput',false); 140 | S = jsonwrite_cell(json,tab); 141 | else 142 | p = properties(json); 143 | if isempty(p), p = fieldnames(json); end % for pre-classdef 144 | s = struct; 145 | for i=1:numel(p) 146 | s.(p{i}) = json.(p{i}); 147 | end 148 | S = jsonwrite_struct(s,tab); 149 | %error('Class "%s" is not supported.',class(json)); 150 | end 151 | end 152 | 153 | %========================================================================== 154 | function S = jsonwrite_struct(json,tab) 155 | if numel(json) == 1 156 | if isstruct(json), fn = fieldnames(json); else fn = keys(json); end 157 | S = ['{' fmt('\n',tab)]; 158 | for i=1:numel(fn) 159 | key = fn{i}; 160 | if isequal(key, 'GeneratedBy') 161 | tabin = '['; 162 | tabout = ']'; 163 | else 164 | tabin = ''; 165 | tabout = ''; 166 | end 167 | if strcmp(optregistry('replacementStyle'),'hex') 168 | key = regexprep(key,... 169 | '^x0x([0-9a-fA-F]{2})', '${native2unicode(hex2dec($1))}'); 170 | key = regexprep(key,... 171 | '0x([0-9a-fA-F]{2})', '${native2unicode(hex2dec($1))}'); 172 | end 173 | if isstruct(json), val = json.(fn{i}); else val = json(fn{i}); end 174 | S = [S fmt(tab) jsonwrite_char(key) ':' tabin fmt(' ',tab) ... 175 | jsonwrite_var(val,tab+1) tabout ]; 176 | if i ~= numel(fn), S = [S ',']; end 177 | S = [S fmt('\n',tab)]; 178 | end 179 | S = [S fmt(tab-1) '}']; 180 | else 181 | S = jsonwrite_cell(arrayfun(@(x) {x},json),tab); 182 | end 183 | 184 | %========================================================================== 185 | function S = jsonwrite_cell(json,tab) 186 | if numel(json) == 0 ... 187 | || (numel(json) == 1 && iscellstr(json)) ... 188 | || all(all(cellfun(@isnumeric,json))) ... 189 | || all(all(cellfun(@islogical,json))) 190 | tab = ''; 191 | end 192 | S = ['[' fmt('\n',tab)]; 193 | for i=1:numel(json) 194 | S = [S fmt(tab) jsonwrite_var(json{i},tab+1)]; 195 | if i ~= numel(json), S = [S ',']; end 196 | S = [S fmt('\n',tab)]; 197 | end 198 | S = [S fmt(tab-1) ']']; 199 | 200 | %========================================================================== 201 | function S = jsonwrite_char(json) 202 | % any-Unicode-character-except-"-or-\-or-control-character 203 | % \" \\ \/ \b \f \n \r \t \u four-hex-digits 204 | json = strrep(json,'\','\\'); 205 | json = strrep(json,'"','\"'); 206 | %json = strrep(json,'/','\/'); 207 | json = strrep(json,sprintf('\b'),'\b'); 208 | json = strrep(json,sprintf('\f'),'\f'); 209 | json = strrep(json,sprintf('\n'),'\n'); 210 | json = strrep(json,sprintf('\r'),'\r'); 211 | json = strrep(json,sprintf('\t'),'\t'); 212 | S = ['"' json '"']; 213 | 214 | %========================================================================== 215 | function S = jsonwrite_numeric(json) 216 | if any(imag(json(:))) 217 | error('Complex numbers not supported.'); 218 | end 219 | if numel(json) == 0 220 | S = jsonwrite_cell({}); 221 | return; 222 | elseif numel(json) > 1 223 | idx = find(size(json)~=1); 224 | if numel(idx) == 1 % vector 225 | if any(islogical(json)) || any(~isfinite(json)) 226 | S = jsonwrite_cell(num2cell(json),''); 227 | else 228 | S = ['[' sprintf('%23.16g,',json) ']']; % eq to num2str(json,16) 229 | S(end-1) = ''; % remove last "," 230 | S(S==' ') = []; 231 | end 232 | else % array 233 | S = jsonwrite_cell(num2cell(json,setdiff(1:ndims(json),idx(1))),''); 234 | end 235 | return; 236 | end 237 | if islogical(json) 238 | if json, S = 'true'; else S = 'false'; end 239 | elseif ~isfinite(json) 240 | if optregistry('convertinfandnan') 241 | S = 'null'; 242 | else 243 | if isnan(json) 244 | S = 'NaN'; 245 | elseif json > 0 246 | S = 'Infinity'; 247 | else 248 | S = '-Infinity'; 249 | end 250 | end 251 | else 252 | S = num2str(json,16); 253 | end 254 | 255 | %========================================================================== 256 | function b = fmt(varargin) 257 | persistent tab; 258 | if nargin == 2 && isequal(varargin{1},'init') 259 | tab = varargin{2}; 260 | end 261 | b = ''; 262 | if nargin == 1 263 | if varargin{1} > 0, b = repmat(tab,1,varargin{1}); end 264 | elseif nargin == 2 265 | if ~isempty(tab) && ~isempty(varargin{2}), b = sprintf(varargin{1}); end 266 | end 267 | 268 | %========================================================================== 269 | function val = optregistry(opts) 270 | persistent options 271 | if isstruct(opts) 272 | options = opts; 273 | else 274 | val = options.(lower(opts)); 275 | end 276 | -------------------------------------------------------------------------------- /JSONio/tests/test_jsonread.m: -------------------------------------------------------------------------------- 1 | function tests = test_jsonread 2 | % Unit Tests for jsonread 3 | 4 | % $Id: test_jsonread.m 7077 2017-05-23 09:13:10Z guillaume $ 5 | 6 | tests = functiontests(localfunctions); 7 | 8 | 9 | function test_jsonread_from_string_1(testCase) 10 | json = '["one", "two", "three"]'; 11 | 12 | exp = {'one';'two';'three'}; 13 | act = jsonread(json); 14 | testCase.verifyTrue(isequal(exp, act)); 15 | 16 | function test_jsonread_from_string_2(testCase) 17 | json = '{"Width":800,"Height":600,"Title":"View from the 15th Floor","Animated":false,"IDs":[116,943,234,38793]}'; 18 | 19 | exp = struct('Width',800,'Height',600,'Title','View from the 15th Floor','Animated',false,'IDs',[116;943;234;38793]); 20 | act = jsonread(json); 21 | testCase.verifyTrue(isequal(exp, act)); 22 | 23 | function test_jsonread_all_types(testCase) 24 | 25 | % JSON Data Type | MATLAB Data Type 26 | 27 | % null, in numeric arrays | NaN 28 | json = '[1, 2, null, 4]'; 29 | exp = [1; 2; NaN; 4]; 30 | act = jsonread(json); 31 | %testCase.verifyTrue(isequaln(exp, act)); 32 | 33 | % null, in nonnumeric arrays| empty double [] 34 | json = '{"null": null}'; 35 | exp = struct('null',[]); 36 | act = jsonread(json); 37 | testCase.verifyTrue(isequal(exp, act)); 38 | 39 | % Boolean | scalar logical 40 | json = '{"logical": false}'; 41 | exp = struct('logical',false); 42 | act = jsonread(json); 43 | testCase.verifyTrue(isequal(exp, act)); 44 | 45 | json = '{"logical": true}'; 46 | exp = struct('logical',true); 47 | act = jsonread(json); 48 | testCase.verifyTrue(isequal(exp, act)); 49 | 50 | % Number | scalar double 51 | json = '{"number": 3.14}'; 52 | exp = struct('number',3.14); 53 | act = jsonread(json); 54 | testCase.verifyTrue(isequal(exp, act)); 55 | 56 | % String | character vector 57 | json = '{"string": "string"}'; 58 | exp = struct('string','string'); 59 | act = jsonread(json); 60 | testCase.verifyTrue(isequal(exp, act)); 61 | 62 | % Object (In JSON, object | scalar structure 63 | % means an unordered set | (Names are made 64 | % of name-value pairs.) | valid.) 65 | json = '{"object": {"field1": 1, "field-2": 2, "3field": 3}}'; 66 | exp = struct('object',struct('field1',1,'field_2',2,'x3field',3)); 67 | act = jsonread(json); 68 | testCase.verifyTrue(isequal(exp, act)); 69 | 70 | json = '{"object": {"field 1": 1, "field two": 2, " field Three ": 3}}'; 71 | exp = struct('object',struct('field1',1,'fieldTwo',2,'fieldThree',3)); 72 | act = jsonread(json); 73 | testCase.verifyTrue(isequal(exp, act)); 74 | 75 | json = '{"object": {"": 1}}'; 76 | exp = struct('object',struct('x',1)); 77 | act = jsonread(json); 78 | testCase.verifyTrue(isequal(exp, act)); 79 | 80 | % Array, when elements are | cell array 81 | % of different data types | 82 | json = '{"array": ["a", 1]}'; 83 | exp = struct('array',{{'a';1}}); 84 | act = jsonread(json); 85 | testCase.verifyTrue(isequal(exp, act)); 86 | 87 | % Array of booleans | logical array 88 | json = '{"logical_array": [true, false]}'; 89 | exp = struct('logical_array',[true;false]); 90 | act = jsonread(json); 91 | testCase.verifyTrue(isequal(exp, act)); 92 | 93 | % Array of numbers | double array 94 | json = '{"number_array": [1, 2, 3, 5, 8, 13]}'; 95 | exp = struct('number_array',[1; 2; 3; 5; 8; 13]); 96 | act = jsonread(json); 97 | testCase.verifyTrue(isequal(exp, act)); 98 | 99 | % Array of strings | cellstr 100 | json = '{"cellstr": ["Statistical","Parametric","Mapping"]}'; 101 | exp = struct('cellstr',{{'Statistical';'Parametric';'Mapping'}}); 102 | act = jsonread(json); 103 | testCase.verifyTrue(isequal(exp, act)); 104 | 105 | % Array of objects, when | structure array 106 | % all objects have the | 107 | % same set of names | 108 | json = '{"structarray": [{"a":1,"b":2},{"a":3,"b":4}]}'; 109 | exp = struct('structarray',struct('a',{1;3},'b',{2;4})); 110 | act = jsonread(json); 111 | testCase.verifyTrue(isequal(exp, act)); 112 | 113 | % Array of objects, when | cell array of 114 | % objects have different | scalar structures 115 | % names | 116 | json = '{"cellarray": [{"a":1,"b":2},{"a":3,"c":4}]}'; 117 | exp = struct('cellarray',{{struct('a',1,'b',2);struct('a',3,'c',4)}}); 118 | act = jsonread(json); 119 | testCase.verifyTrue(isequal(exp, act)); 120 | 121 | % Array of objects, when | cell array of 122 | % all objects have the |scalar structures 123 | % same set of names | 124 | % but different order 125 | json = '{"structarray": [{"a":1,"b":2},{"b":3,"a":4}]}'; 126 | exp = struct('structarray',{{struct('a',1,'b',2);struct('b',3,'a',4)}}); 127 | act = jsonread(json); 128 | testCase.verifyTrue(isequal(exp, act)); 129 | 130 | % empty struct 131 | json = '{"a":"aa","b":{},"c":"cc"}'; 132 | exp = struct('a','aa','b',struct(),'c','cc'); 133 | act = jsonread(json); 134 | testCase.verifyTrue(isequal(exp, act)); 135 | 136 | % empty array 137 | json = '{"a":"aa","b":[],"c":"cc"}'; 138 | exp = struct('a','aa','b',[],'c','cc'); 139 | act = jsonread(json); 140 | testCase.verifyTrue(isequal(exp, act)); 141 | 142 | % NaN and Inf 143 | json = '[1,NaN,Inf]'; 144 | exp = [1;NaN;Inf]; 145 | act = jsonread(json); 146 | testCase.verifyTrue(isequaln(exp, act)); 147 | 148 | % numbers and booleans 149 | json = '[1,NaN,Inf,true]'; 150 | exp = {1;NaN;Inf;true}; 151 | act = jsonread(json); 152 | testCase.verifyTrue(isequaln(exp, act)); 153 | 154 | % numbers and arrays of numbers 155 | json = '[1,2,[3,4]]'; 156 | exp = {1;2;[3;4]}; 157 | act = jsonread(json); 158 | testCase.verifyTrue(isequal(exp, act)); 159 | 160 | json = '[[1,2]]'; 161 | exp = [1 2]; 162 | act = jsonread(json); 163 | testCase.verifyTrue(isequal(exp, act)); 164 | 165 | json = '[[1,2],[3,4]]'; 166 | exp = [1 2;3 4]; 167 | act = jsonread(json); 168 | testCase.verifyTrue(isequal(exp, act)); 169 | 170 | json = '[[1,2],[3,4,5]]'; 171 | exp = {[1;2];[3;4;5]}; 172 | act = jsonread(json); 173 | testCase.verifyTrue(isequal(exp, act)); 174 | 175 | json = '[[[1,2],[3,4]],[[5,6],[7,8]]]'; 176 | exp = cat(3,[1,3;5,7],[2,4;6,8]); 177 | act = jsonread(json); 178 | testCase.verifyTrue(isequal(exp, act)); 179 | -------------------------------------------------------------------------------- /JSONio/tests/test_jsonwrite.m: -------------------------------------------------------------------------------- 1 | function tests = test_jsonwrite 2 | % Unit Tests for jsonwrite 3 | 4 | % $Id: test_jsonwrite.m 7526 2019-02-06 14:33:18Z guillaume $ 5 | 6 | tests = functiontests(localfunctions); 7 | 8 | 9 | function test_jsonwrite_array(testCase) 10 | exp = {'one';'two';'three'}; 11 | act = jsonread(jsonwrite(exp)); 12 | testCase.verifyTrue(isequal(exp, act)); 13 | 14 | exp = 2; 15 | act = nnz(jsonwrite(1:3) == ','); 16 | testCase.verifyTrue(isequal(exp, act)); 17 | 18 | function test_jsonwrite_object(testCase) 19 | exp = struct('Width',800,'Height',600,'Title','View from the 15th Floor','Animated',false,'IDs',[116;943;234;38793]); 20 | act = jsonread(jsonwrite(exp)); 21 | testCase.verifyTrue(isequal(exp, act)); 22 | 23 | function test_jsonwrite_all_types(testCase) 24 | exp = []; 25 | act = jsonread(jsonwrite(exp)); 26 | testCase.verifyTrue(isequal(exp, act)); 27 | 28 | exp = [true;false]; 29 | act = jsonread(jsonwrite(exp)); 30 | testCase.verifyTrue(isequal(exp, act)); 31 | 32 | exp = struct('a',''); 33 | act = jsonread(jsonwrite(exp)); 34 | testCase.verifyTrue(isequal(exp, act)); 35 | 36 | str = struct('str',reshape(1:9,3,3)); 37 | exp = jsonread('{"str":[[1,4,7],[2,5,8],[3,6,9]]}'); 38 | act = jsonread(jsonwrite(str)); 39 | testCase.verifyTrue(isequal(act, exp)); 40 | 41 | str = [1,2,NaN,3,Inf]; 42 | exp = jsonread('[1,2,null,3,null]'); 43 | act = jsonread(jsonwrite(str)); 44 | testCase.verifyTrue(isequaln(act, exp)); 45 | 46 | %function test_jsonwrite_chararray(testCase) 47 | %str = char('one','two','three'); 48 | %exp = {'one ';'two ';'three'}; 49 | %act = jsonread(jsonwrite(str)); 50 | %testCase.verifyTrue(isequal(exp, act)); 51 | 52 | function test_options(testCase) 53 | exp = struct('Width',800,'Height',NaN,'Title','View','Bool',true); 54 | jsonwrite(exp,'indent',''); 55 | jsonwrite(exp,'indent',' '); 56 | jsonwrite(exp,'replacementStyle','underscore'); 57 | jsonwrite(exp,'replacementStyle','hex'); 58 | jsonwrite(exp,'convertInfAndNaN',true); 59 | jsonwrite(exp,'convertInfAndNaN',false); 60 | jsonwrite(exp,'indent',' ','replacementStyle','hex','convertInfAndNaN',false); 61 | jsonwrite(exp,struct('indent','\t')); 62 | jsonwrite(exp,struct('indent','\t','convertInfAndNaN',false)); 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![EEG-BIDS](https://github.com/sccn/EEG-BIDS/assets/1872705/47675a55-6573-47d7-abec-48e364d5ad8a) 2 | 3 | # EEG-BIDS 4 | 5 | The EEG-BIDS (formerly known as **BIDS-MATLAB-tools**) repository contains a collection of functions that import and export BIDS-formated experiments. The code is tailored for use as an [EEGLAB](http://eeglab.org) plugin but may also be used independently of EEGLAB. Conversion of data format from non-supported BIDS binary format requires that EEGLAB be installed (supported formats are EEGLAB .set files, EDF files, BDF files, and Brain Vision Exchange Format files). 6 | 7 | # Documentation 8 | 9 | Refer to the [wiki documentation](https://github.com/sccn/EEG-BIDS/wiki) or the submenus of this plugin if you are on the EEGLAB website. 10 | 11 | # EEG-BIDS vs other BIDS software 12 | 13 | [BIDS-MATLAB](https://bids-matlab.readthedocs.io/en/latest/) is a project to import BIDS data. BIDS-MATLAB maps the BIDS directory architectures to MATLAB structures but **does not import or convert data like EEG-BIDS**. In theory, EEG-BIDS could use BIDS-MATLAB to get the BIDS directory architectures into MATLAB and then convert it to an EEGLAB STUDY. However, in 2021, BIDS-MATLAB could not yet import all the relevant EEG, MEG, and iEEG files. 14 | 15 | [EEG2BIDS](https://github.com/aces/EEG2BIDS) is a Python-based executable that formats a collection of EDF files in BIDS format. EEG2BIDS requires users to create JSON files for metadata. It is a tool designed to archive data as part of a lab protocol where JSON files have been prepared in advance and are suited for technicians organizing data. EEG-BIDS export capabilities are more suited for researchers managing their data and are agnostic regarding the original data format. 16 | 17 | [ezBIDS](https://brainlife.io/ezbids/) is a data export tool for MRI and fMRI data. It does not allow exporting EEG data to our knowledge. 18 | 19 | [data2bids.m](https://www.fieldtriptoolbox.org/example/bids/) is a Fieldtrip function to export BIDS data. This includes EEG and maybe fMRI. This function can only be used from the command line. 20 | 21 | [EEG-BIDS](https://github.com/sccn/EEG-BIDS) (this program) is the most popular tool to export EEG data using both a graphical interface and/or command line (more than half of the BIDS datasets on OpenNeuro were exported using EEG-BIDS and it has more than 2200 installs in EEGLAB). A compiled, stand-alone version of EEG-BIDS with a graphic interface wizard is also available when you download the compiled version of EEGLAB. 22 | 23 | # Standalone version 24 | 25 | A standalone version of the plugin is available when [downloading EEGLAB](https://sccn.ucsd.edu/eeglab/download.php). The standalone version of EEG-BIDS is currently bundled with the compiled version of EEGLAB for user convenience; if user demand increases significantly, a separate distribution package will be made available. After downloading EEGLAB, use the EEGBIDS (Mac) or EEGBIDS.bat (Windows) executable. This version of EEG-BIDS does not require a paid MATLAB license. 26 | 27 | # EEG-BIDS export capabilities 28 | 29 | Accepted EEG input formats are all files that EEGLAB can read. 30 | 31 | * EEG export: ✔ (as .set, .edf, .bdf, or .vhmk files) 32 | * iEEG export: ✔ (as .set, .edf or .nwb) 33 | * Eye-tracking export: ✔ (beta) 34 | * HED export: ✔ 35 | * BEH export: ✔ 36 | * MRI export: ✔ (no conversion) 37 | * fMRI export: ✖ 38 | * Motion-cap export: ✖ (upcoming) 39 | * EMG export: ✖ (upcoming) 40 | * MEG export: ✖ (upcoming) 41 | 42 | # EEG-BIDS import capabilities for EEGLAB 43 | 44 | EEG-BIDS allows importing BIDS datasets into EEGLAB. This is the type of information that can be imported. 45 | 46 | * EEG import: ✔ (all formats) 47 | * iEEG import: ✔ (all formats) 48 | * MEG import: ✔ (.ds and .fif files supported, more formats upcoming) 49 | * Eye-tracking import: ✖ (upcoming) 50 | * HED import: ✔ 51 | * BEH import: ✔ 52 | * MRI import: n/a 53 | * fMRI import: n/a 54 | * Motion-cap import: ✔ (beta) 55 | * EMG import: ✖ (upcoming) 56 | 57 | # Cloning 58 | 59 | Make sure you clone with submodules 60 | 61 | ``` 62 | git clone https://github.com/sccn/EEG-BIDS 63 | ``` 64 | 65 | # Testing 66 | 67 | Use the EEG-BIDS_testcases repository for testing 68 | 69 | ``` 70 | git clone https://github.com/sccn/EEG-BIDS_testcases.git 71 | ``` 72 | 73 | # Use with EEGLAB 74 | 75 | Simply place the code in the EEGLAB plugin folder, and EEGLAB will automatically detect it. See documentation at [https://github.com/sccn/EEG-BIDS/wiki](https://github.com/sccn/EEG-BIDS/wiki). 76 | 77 | # Zip command to release plugin 78 | 79 | ``` 80 | zip -r EEG-BIDS8.0.zip EEG-BIDS/* -x /EEG-BIDS/testing/additionaltests/* /EEG-BIDS/testing/ds004117/* /EEG-BIDS/testing/hbn_eye_tracking_data/* /EEG-BIDS/testing/data/* 81 | ``` 82 | 83 | # Version history 84 | 85 | v1.0 - initial version 86 | 87 | v2.0 - add support for external channel location and fix minor bugs 88 | 89 | v3.0 - better export for multiple runs and allowing importing BIDS folder with multiple runs 90 | 91 | v3.1 - fix multiple issues at export time, including subject numbering 92 | 93 | v3.2 - fix menu conflict in EEGLAB with bids validator; check channel types; add option to choose EEG event field; minor bugs 94 | 95 | v3.3 - fix an issue for Windows and work on GUI 96 | 97 | v3.4 - fix the issue with saving datasets in memory. Allowing to anonymize participant ID or not. Fixed issue with looking up channel locations. 98 | 99 | v3.5 - fix issue with choosing event type in graphic interface; various fixes for GUI edit of BIDS info 100 | 101 | v4.0 - fix GUI and many minor export issues 102 | 103 | v4.1 - fix the issue with JSON 104 | 105 | v5.0 - major fixes to import all OpenNeuro EEG datasets 106 | 107 | v5.1 - allow calculating dataset meta-data quality 108 | 109 | v5.2 - fix the issue with history 110 | 111 | v5.3 - adding the capability to export stimuli 112 | 113 | v5.3.1 - update documentation for tInfo.HardwareFilters; fix bug defaults fields not filled for eInfo 114 | 115 | v5.4 - fix the issue with reading BIDS information when importing BIDS data to STUDY 116 | 117 | v6.0 - new examples and fixes for HED 118 | 119 | v6.1 - allow data with no events. Fix HED import/export. Fix history. 120 | 121 | v7.0 - split code into different functions. Support for behavioral data. Various bug fixes. 122 | 123 | v7.2 - fix the issue with the missing file. 124 | 125 | v7.3 - various minor fixes (EEG reference as string; add duration if not present; resave datasets) 126 | 127 | v7.4 - fix version issues for HED and BIDS. Export subjects in order. Remove unused columns in participants.tsv file 128 | 129 | v7.5 - adding support for coordsystem files, for loading specific runs, support for motion files 130 | 131 | v7.6 - adding export to non-EEGLAB formats, refactoring export 132 | 133 | v7.7 - fix importing MEG and MEF files. Better handling of runs. Now tracks tool version in BIDS. 134 | 135 | v8.0 - renamed files, separate file for task info, adding BIDS statistic output, handling EGI & BVA file better, channel types and units, adding eye-tracking and behavioral support 136 | 137 | v9.0 - update json import to conform to BIDS key-level inheritance principle. Support iEEG and MEG export. Support exporting multiple tasks. Fix issues with exporting channel locations. BIDS export wizard. 138 | 139 | v9.1 - better handling of behavioral data. Fix issue with task name and BIDS dataset with no README file. 140 | 141 | v10.0 - adding support for re-exporting datasets. Adding the the "desc" key. Fix some event export issues. 142 | 143 | v10.1 - fix export wizard. 144 | 145 | v10.2 - fix missing task when exporting study, fix bids subject (bug 860), better handling of descriptive tags. 146 | -------------------------------------------------------------------------------- /bids_checkfields.m: -------------------------------------------------------------------------------- 1 | % bids_checkfields() - Check BIDS fields 2 | % 3 | % Usage: 4 | % >> struct = bids_checkfields(struct, fielddefs, fieldname); 5 | % 6 | % Inputs: 7 | % struct - [struct] structure with defined fields 8 | % fielddefs - [fielddefs] field definition 9 | % fieldname - [string] structure name 10 | % 11 | % Outputs: 12 | % struct - checked structure 13 | % 14 | % Authors: Arnaud Delorme, SCCN, INC, UCSD, January, 2023 15 | 16 | % Copyright (C) Arnaud Delorme, 2023 17 | % 18 | % This program is free software; you can redistribute it and/or modify 19 | % it under the terms of the GNU General Public License as published by 20 | % the Free Software Foundation; either version 2 of the License, or 21 | % (at your option) any later version. 22 | % 23 | % This program is distributed in the hope that it will be useful, 24 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 25 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26 | % GNU General Public License for more details. 27 | % 28 | % You should have received a copy of the GNU General Public License 29 | % along with this program; if not, to the Free Software 30 | % Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 31 | 32 | % check the fields for the structures 33 | % ----------------------------------- 34 | function s = bids_checkfields(s, f, structName) 35 | 36 | fields = fieldnames(s); 37 | diffFields = setdiff(fields, f(:,1)'); 38 | if ~isempty(diffFields) 39 | fprintf('Warning: Ignoring invalid field name(s) "%s" for structure %s\n', sprintf('%s ',diffFields{:}), structName); 40 | s = rmfield(s, diffFields); 41 | end 42 | for iRow = 1:size(f,1) 43 | if strcmp(structName,'tInfo') && strcmp(f{iRow,1}, 'EEGReference') && ~isa(s.(f{iRow,1}), 'char') 44 | s.(f{iRow,1}) = char(s.(f{iRow,1})); 45 | end 46 | if isempty(s) || ~isfield(s, f{iRow,1}) 47 | if strcmpi(f{iRow,2}, 'required') % required or optional 48 | if ~iscell(f{iRow,4}) && ~isstruct(f{iRow,4}) 49 | fprintf('Warning: "%s" set to %s\n', f{iRow,1}, num2str(f{iRow,4})); 50 | end 51 | s = setfield(s, {1}, f{iRow,1}, f{iRow,4}); 52 | end 53 | elseif ~isempty(f{iRow,3}) && ~isa(s.(f{iRow,1}), f{iRow,3}) && ~strcmpi(s.(f{iRow,1}), 'n/a') 54 | % if it's HED in eInfoDesc, allow string also 55 | if strcmp(structName,'eInfoDesc') && strcmp(f{iRow,1}, 'HED') && isa(s.(f{iRow,1}), 'char') 56 | return 57 | end 58 | error(sprintf('Parameter %s.%s must be a %s', structName, f{iRow,1}, f{iRow,3})); 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /bids_compare.m: -------------------------------------------------------------------------------- 1 | % BIDS_COMPARE - compare two BIDS folders. 2 | % 3 | % Usage: 4 | % eeg_compare(folder1, folder2, errorFlag); 5 | % 6 | % Input: 7 | % folder1 - [string] first BIDS folder 8 | % folder2 - [string] second BIDS folder 9 | % errorFlag - [true|false] generate error if true (warning if false) 10 | % 11 | % Author: Arnaud Delorme, 2023 12 | 13 | % Copyright (C) 2023 Arnaud Delorme 14 | % 15 | % This file is part of EEGLAB, see http://www.eeglab.org 16 | % for the documentation and details. 17 | % 18 | % Redistribution and use in source and binary forms, with or without 19 | % modification, are permitted provided that the following conditions are met: 20 | % 21 | % 1. Redistributions of source code must retain the above copyright notice, 22 | % this list of conditions and the following disclaimer. 23 | % 24 | % 2. Redistributions in binary form must reproduce the above copyright notice, 25 | % this list of conditions and the following disclaimer in the documentation 26 | % and/or other materials provided with the distribution. 27 | % 28 | % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 29 | % AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 30 | % IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 31 | % ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 32 | % LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 33 | % CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 34 | % SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 35 | % INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 36 | % CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 37 | % ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 38 | % THE POSSIBILITY OF SUCH DAMAGE. 39 | 40 | 41 | function bids_compare(oridir, targetdir, errorFlag) 42 | 43 | if nargin < 2 44 | help bids_compare 45 | return 46 | end 47 | if nargin < 3 48 | errorFlag = false; 49 | end 50 | 51 | dir1 = dir(oridir); 52 | if isempty(dir1) 53 | fprintf(2, 'BIDS compare, directory empty %s\n', oridir) 54 | return 55 | end 56 | dir2 = dir(targetdir); 57 | if isempty(dir1) 58 | fprintf(2, 'BIDS compare, directory empty %s\n', targetdir) 59 | end 60 | 61 | allDir2Folder = { dir2.folder }; 62 | allDir2Name = { dir2.name }; 63 | [allDir2Name,dir2] = generate_variation(allDir2Name, dir2); 64 | listNotFound = {}; 65 | for iDir = 1:length(dir1) 66 | if ~isequal(dir1(iDir).name(1), '.') 67 | indMatch = strmatch(dir1(iDir).name, allDir2Name, 'exact'); 68 | relfolder1 = relativeFolder(dir1(iDir ).folder, oridir); 69 | fullFileName1 = fullfile(relfolder1, dir1(iDir).name); 70 | if length(indMatch) ~= 1 71 | if ~contains(oridir, 'code') && ~contains(oridir, 'derivatives') && ~contains(fullFileName1, '.fdt') && ~contains(fullFileName1, 'derivatives') 72 | listNotFound{end+1} = fullfile(oridir, fullFileName1); 73 | end 74 | else 75 | relfolder2 = relativeFolder(dir2(indMatch).folder, targetdir); 76 | if ~isequal(relfolder1, relfolder2) 77 | listNotFound{end+1} = fullfile(oridir, fullFileName1); 78 | else 79 | if dir1(iDir).isdir 80 | fullFileName2 = fullfile(relfolder2, dir2(indMatch).name); 81 | bids_compare(fullfile(oridir, fullFileName1), fullfile(targetdir, fullFileName2)); 82 | end 83 | end 84 | end 85 | end 86 | end 87 | 88 | if ~isempty(listNotFound) 89 | %for iFile = 1:min(length(listNotFound),10) 90 | for iFile = 1:length(listNotFound) 91 | fprintf('File/folder %s not found in second folder\n', listNotFound{iFile}); 92 | end 93 | if length(listNotFound) > 10 94 | fprintf('... and more\n'); 95 | end 96 | if errorFlag 97 | error('BIDS folder do not match') 98 | end 99 | else 100 | fprintf(' Files found to match\n') 101 | end 102 | 103 | function outstr = relativeFolder( folder, parentFolder ) 104 | 105 | strPos = strfind(folder, parentFolder); 106 | if isempty(strPos) 107 | error('Issue with string searching') 108 | end 109 | outstr = folder(strPos+length(parentFolder):end); 110 | 111 | % generate variation to account for different runs and session formating 112 | % ------- 113 | function [dirstr,dirs] = generate_variation(dirstr, dirs) 114 | 115 | for iDir = 1:length(dirstr) 116 | if length(dirstr{iDir}) > 4 117 | ind = strfind(dirstr{iDir}, 'ses-'); 118 | if ~isempty(ind) 119 | endstr = dirstr{iDir}(ind(1)+4:end); 120 | dirstr{end+1} = [ dirstr{iDir}(1:ind(1)+3) '0' endstr ]; 121 | dirstr{end+1} = [ dirstr{iDir}(1:ind(1)+3) '00' endstr ]; 122 | dirs(end+1) = dirs(iDir); 123 | dirs(end+1) = dirs(iDir); 124 | end 125 | end 126 | end 127 | for iDir = 1:length(dirstr) 128 | if length(dirstr{iDir}) > 4 129 | ind = strfind(dirstr{iDir}, 'run-'); 130 | if ~isempty(ind) 131 | endstr = dirstr{iDir}(ind(1)+4:end); 132 | dirstr{end+1} = [ dirstr{iDir}(1:ind(1)+3) '0' endstr ]; 133 | dirstr{end+1} = [ dirstr{iDir}(1:ind(1)+3) '00' endstr ]; 134 | dirs(end+1) = dirs(iDir); 135 | dirs(end+1) = dirs(iDir); 136 | end 137 | end 138 | end 139 | 140 | 141 | -------------------------------------------------------------------------------- /bids_export_example2.m: -------------------------------------------------------------------------------- 1 | % Matlab script to export to BIDS 2 | % Exported dataset is available at https://openneuro.org/datasets/ds003061 3 | % 4 | % You may not use this script because you do not have the original data 5 | % It is provided as an example to be adapted for your specific need 6 | % 7 | % Arnaud Delorme - Feb 2020 8 | 9 | % export notes 10 | % export event type problem 11 | % export event value problem 12 | return 13 | 14 | clear 15 | 16 | data = []; 17 | data(end+1).file = {'../Ref1_12082011/Ref1_12082011_1_256Hz.bdf' '../Ref1_12082011/Ref1_12082011_2_256Hz.bdf' '../Ref1_12082011/Ref1_12082011_3_256Hz.bdf'}; 18 | data(end ).session = [1 1 1]; 19 | data(end ).run = [1 2 3]; 20 | data(end ).notes = { 'She changed push button hands during the experiment (in the middle of trials)' }; 21 | 22 | data(end+1).file = {'../Ref2_15082011/Ref2_15082011_1_256Hz.bdf' '../Ref2_15082011/Ref2_15082011_2_256Hz.bdf' '../Ref2_15082011/Ref2_15082011_3_256Hz.bdf'}; 23 | data(end ).session = [1 1 1]; 24 | data(end ).run = [1 2 3]; 25 | data(end ).notes = { 'First ~120 seconds did not have push button triggers because the USB cable was not connected' 'She moves around when she s starting to feel sleepy. She said that she was dozing and her head moved from side to side.' 'Had to interrupt the session because she stopped pressing the button during the oddball sounds. She fell asleep in the middle of the experiment.' }; 26 | 27 | data(end+1).file = {'../Ref3_17082011/Ref3_17082011_1_256Hz.bdf' '../Ref3_17082011/Ref3_17082011_2_256Hz.bdf' '../Ref3_17082011/Ref3_17082011_3_256Hz.bdf'}; 28 | data(end ).session = [1 1 1]; 29 | data(end ).run = [1 2 3]; 30 | 31 | data(end+1).file = {'../Ref4_19082011/Ref4_19082011_1_256Hz.bdf' '../Ref4_19082011/Ref4_19082011_2_256Hz.bdf' '../Ref4_19082011/Ref4_19082011_3_256Hz.bdf'}; 32 | data(end ).session = [1 1 1]; 33 | data(end ).run = [1 2 3]; 34 | data(end ).notes = { 'The whole recording was noisy w/ a lot of electrode fluctuations and FP1 and PO4 were noisy. Maybe too much gel' }; 35 | 36 | data(end+1).file = {'../Ref5_20082011/Ref5_20082011_1_256Hz.bdf' '../Ref5_20082011/Ref5_20082011_2_256Hz.bdf' '../Ref5_20082011/Ref5_20082011_3_256Hz.bdf'}; 37 | data(end ).session = [1 1 1]; 38 | data(end ).run = [1 2 3]; 39 | 40 | data(end+1).file = {'../Ref6_20082011/Ref6_20082011_1_256Hz.bdf' '../Ref6_20082011/Ref6_20082011_2_256Hz.bdf' '../Ref6_20082011/Ref6_20082011_3_256Hz.bdf'}; 41 | data(end ).session = [1 1 1]; 42 | data(end ).run = [1 2 3]; 43 | 44 | data(end+1).file = {'../Ref7_21082011/Ref7_21082011_1_256Hz.bdf' '../Ref7_21082011/Ref7_21082011_2_256Hz.bdf' '../Ref7_21082011/Ref7_21082011_3_256Hz.bdf'}; 45 | data(end ).session = [1 1 1]; 46 | data(end ).run = [1 2 3]; 47 | data(end ).notes = { 'Very messy recording, the cap was a bit loose on the back of his head, but fitted elsewhere. The electrodes were unstable and fluctuates randomly. He said that he s not moving, but we don t know.' }; 48 | 49 | data(end+1).file = {'../Ref8_26082011/Ref8_26082011_1_256Hz.bdf' '../Ref8_26082011/Ref8_26082011_2_256Hz.bdf' '../Ref8_26082011/Ref8_26082011_3_256Hz.bdf'}; 50 | data(end ).session = [1 1 1]; 51 | data(end ).run = [1 2 3]; 52 | 53 | data(end+1).file = {'../Ref9_28082011/Ref9_28082011_1_256Hz.bdf' '../Ref9_28082011/Ref9_28082011_2_256Hz.bdf' '../Ref9_28082011/Ref9_28082011_3_256Hz.bdf'}; 54 | data(end ).session = [1 1 1]; 55 | data(end ).run = [1 2 3]; 56 | 57 | data(end+1).file = {'../Ref10_08092011/Ref10_08092011_1_256Hz.bdf' '../Ref10_08092011/Ref10_08092011_2_256Hz.bdf' '../Ref10_08092011/Ref10_08092011_4_256Hz.bdf'}; 58 | data(end ).session = [1 1 1]; 59 | data(end ).run = [1 2 3]; 60 | data(end ).notes = { '' '' 'We did a 4th trial because trial 3 recording was not good. This is the 4th, trial 3 is not included' }; 61 | 62 | data(end+1).file = {'../Ref11_10092011/Ref11_10092011_1_256Hz.bdf' '../Ref11_10092011/Ref11_10092011_2_256Hz.bdf' '../Ref11_10092011/Ref11_10092011_3_256Hz.bdf'}; 63 | data(end ).session = [1 1 1]; 64 | data(end ).run = [1 2 3]; 65 | data(end ).notes = { 'She had a lot of curly hair and her hair felt oily (despite the fact that she said she didn t put oil today). She could have been moving also.' }; 66 | 67 | data(end+1).file = {'../Ref12_10092011/Ref12_10092011_1_256Hz.bdf' '../Ref12_10092011/Ref12_10092011_2_256Hz.bdf' '../Ref12_10092011/Ref12_10092011_3_256Hz.bdf'}; 68 | data(end ).session = [1 1 1]; 69 | data(end ).run = [1 2 3]; 70 | data(end ).notes = { '' 'Fell asleep in the middle. We had to interrupt the session to wake him up.' '' }; 71 | 72 | data(end+1).file = {'../Ref13_11092011/Ref13_11092011_1_256Hz.bdf' '../Ref13_11092011/Ref13_11092011_2_256Hz.bdf' '../Ref13_11092011/Ref13_11092011_3_256Hz.bdf'}; 73 | data(end ).session = [1 1 1]; 74 | data(end ).run = [1 2 3]; 75 | data(end ).notes = { 'His head is moving. He was sleepy so he yawned a lot. He kept on touching his nose to make sure the eletrode is okay.' }; 76 | 77 | %% participant information for participants.tsv file 78 | % ------------------------------------------------- 79 | pInfo = { 'gender' 'age' 'Ethnicity' 'Air_conditioning'; 80 | 'F' 44 'Indian' 'on'; 81 | 'F' 32 'Indian' 'on'; 82 | 'F' 28 'Non_indian' 'off'; 83 | 'M' 35 'Indian' 'off'; 84 | 'F' 49 'Non_indian' 'off'; 85 | 'F' 27 'Non_indian' 'off'; 86 | 'M' 33 'Indian' 'on'; 87 | 'M' 35 'Indian' 'off'; 88 | 'F' 31 'Non_indian' 'on'; 89 | 'M' 24 'Indian' 'on'; 90 | 'F' 58 'Indian' 'on'; 91 | 'M' 27 'Non_indian' 'on'; 92 | 'M' 28 'Indian' 'on' }; 93 | 94 | % data(3:end) = []; 95 | % pInfo(4:end,:) = []; 96 | 97 | %% Code Files used to preprocess and import to BIDS 98 | % -----------------------------------------------------| 99 | codefiles = { fullfile(pwd, mfilename) fullfile(pwd, 'oddball_psychotoolbox.m') }; 100 | 101 | % general information for dataset_description.json file 102 | % ----------------------------------------------------- 103 | generalInfo.Name = 'P300 sound task'; 104 | generalInfo.ReferencesAndLinks = { 'No bibliographic reference other than the DOI for this dataset' }; 105 | generalInfo.BIDSVersion = 'v1.2.1'; 106 | generalInfo.License = 'CC0'; 107 | generalInfo.Authors = {'Arnaud Delorme' }; 108 | 109 | % participant column description for participants.json file 110 | % --------------------------------------------------------- 111 | pInfoDesc.participant_id.LongName = 'Participant identifier'; 112 | pInfoDesc.participant_id.Description = 'Unique participant identifier'; 113 | 114 | pInfoDesc.gender.Description = 'Sex of the participant'; 115 | pInfoDesc.gender.Levels.M = 'male'; 116 | pInfoDesc.gender.Levels.F = 'female'; 117 | 118 | pInfoDesc.age.Description = 'age of the participant'; 119 | pInfoDesc.age.Units = 'years'; 120 | 121 | pInfoDesc.Air_conditioning.Description = 'Ethnicity of participants'; 122 | pInfoDesc.Ethnicity.Levels.Indian = 'Participant of Indian origin'; 123 | pInfoDesc.Ethnicity.Levels.Non_indian = 'Participant of non-Indian origin (Caucasian, etc...)'; 124 | 125 | pInfoDesc.Air_conditioning.Description = 'Air Conditioning - could create interference so noted here'; 126 | pInfoDesc.Air_conditioning.Levels.on = 'Air Conditioning was on - temperature at or below 25C'; 127 | pInfoDesc.Air_conditioning.Levels.off = 'Air Conditioning was off - temperature at or above 25C'; 128 | 129 | % event column description for xxx-events.json file (only one such file) 130 | % ---------------------------------------------------------------------- 131 | eInfo = {'onset' 'latency'; 132 | 'sample' 'latency'; 133 | 'value' 'type' }; % ADD HED HERE 134 | 135 | eInfoDesc.onset.Description = 'Event onset'; 136 | eInfoDesc.onset.Units = 'second'; 137 | 138 | eInfoDesc.response_time.Description = 'Latency of button press after auditory stimulus'; 139 | eInfoDesc.response_time.Levels.Units = 'millisecond'; 140 | 141 | eInfoDesc.trial_type.Description = 'Type of event'; 142 | eInfoDesc.trial_type.Levels.stimulus = 'Auditory stimulus'; 143 | eInfoDesc.trial_type.Levels.responses = 'Behavioral response'; 144 | 145 | eInfoDesc.value.Description = 'Value of event'; 146 | eInfoDesc.value.Levels.response = 'Response of the subject'; 147 | eInfoDesc.value.Levels.standard = 'Standard at 500 hz for 60 ms'; 148 | eInfoDesc.value.Levels.ignore = 'Ignore - not a real event'; 149 | eInfoDesc.value.Levels.oddball = 'Oddball at 1000 hz for 60 ms'; 150 | eInfoDesc.value.Levels.noise = 'White noise for 60 ms'; 151 | 152 | renameTypes = { 'condition 1' 'response'; 153 | 'condition 2' 'standard'; 154 | 'condition 3' 'ignore'; 155 | 'condition 4' 'oddball'; 156 | 'condition 8' 'noise' }; 157 | 158 | trialTypes = { 'condition 1' 'response'; 159 | 'condition 2' 'stimulus'; 160 | 'condition 3' 'n/a'; 161 | 'condition 4' 'stimulus'; 162 | 'condition 8' 'stimulus' }; 163 | 164 | % Content for README file 165 | % ----------------------- 166 | README = [ 'Data collection took place at the Meditation Research Institute (MRI) in Rishikesh, India under the supervision of Arnaud Delorme, PhD. The project was approved by the local MRI Indian ethical committee and the ethical committee of the University of California San Diego (IRB project # 090731).' 10 10 ... 167 | 'Participants sat either on a blanket on the floor or on a chair for both experimental periods depending on their personal preference. They were asked to keep their eyes closed and all lighting in the room was turned off during data collection. An intercom allowed communication between the experimental and the recording room.' 10 10 ... 168 | 'Participants performed three identical sessions of 13 minutes each. 750 stimuli were presented with 70% of them being standard (500 Hz pure tone lasting 60 milliseconds), 15% being oddball (1000 Hz pure tone lasting 60 ms) and 15% being distractors (1000 Hz white noise lasting 60 ms). All sounds took 5 milliseconds to ramp up and 5 milliseconds to ramp down. Sounds were presented at a rate of 1 per second with a random gaussian jitter of standard deviation 25 ms. Participants were instructed to respond to oddball by pressing a key on a keypad that was resting on their lap.' 10 10 ... 169 | ]; 170 | 171 | % Content for CHANGES file 172 | % ------------------------ 173 | CHANGES = sprintf([ 'Version 1.0 - 4 Aug 2020\n' ... 174 | ' - Initial release\n' ]); 175 | 176 | % Task information for xxxx-eeg.json file 177 | % --------------------------------------- 178 | tInfo.InstitutionAddress = ''; 179 | tInfo.InstitutionName = ''; 180 | tInfo.InstitutionalDepartmentName = ''; 181 | tInfo.PowerLineFrequency = 50; 182 | tInfo.ManufacturersModelName = 'Biosemi Active 2'; 183 | 184 | % call to the export function 185 | % --------------------------- 186 | targetFolder = '../BIDS'; 187 | bids_export(data, ... 188 | 'targetdir', targetFolder, ... 189 | 'taskName', 'P300',... 190 | 'gInfo', generalInfo, ... 191 | 'pInfo', pInfo, ... 192 | 'pInfoDesc', pInfoDesc, ... 193 | 'eInfo', eInfo, ... 194 | 'eInfoDesc', eInfoDesc, ... 195 | 'README', README, ... 196 | 'CHANGES', CHANGES, ... 197 | 'codefiles', codefiles, ... 198 | 'trialtype', trialTypes, ... 199 | 'chanlookup', '/data/matlab/eeglab/plugins/dipfit/standard_BEM/elec/standard_1005.elc', ... 200 | 'renametype', renameTypes, ... 201 | 'checkresponse', 'condition 1', ... 202 | 'tInfo', tInfo, ... 203 | 'copydata', 1); 204 | % 205 | % % copy stimuli folder 206 | % % ------------------- 207 | copyfile('../stimuli', fullfile(targetFolder, 'stimuli'), 'f'); 208 | % copyfile(fullfile(tempFolder, 'sourcedata'), fullfile(targetFolder, 'sourcedata'), 'f'); 209 | -------------------------------------------------------------------------------- /bids_export_example3.m: -------------------------------------------------------------------------------- 1 | % Matlab script to export to BIDS 2 | % This is a simple example to export the tutorial EEGLAB dataset 3 | % You can run this example and checks that the data passes the BIDS 4 | % validator, then modify it for your own purpose 5 | % 6 | % Arnaud Delorme - Oct 2021 7 | 8 | data = []; 9 | p =fileparts(which('eeglab')); 10 | data(end+1).file = { fullfile(p, 'sample_data', 'eeglab_data.set') }; 11 | data(end ).session = [1]; 12 | data(end ).run = [1]; 13 | data(end ).task = { 'p300' }; 14 | data(end ).notes = { 'No notes' }; 15 | 16 | %% participant information for participants.tsv file 17 | % ------------------------------------------------- 18 | pInfo = { 'gender' 'age'; 19 | 'M' 22 }; 20 | 21 | %% Code Files used to preprocess and import to BIDS 22 | % -----------------------------------------------------| 23 | codefiles = { fullfile(pwd, mfilename) }; 24 | 25 | %% general information for dataset_description.json file 26 | % ----------------------------------------------------- 27 | generalInfo.Name = 'P300 visual task'; 28 | generalInfo.ReferencesAndLinks = { 'No bibliographic reference other than the DOI for this dataset' }; 29 | generalInfo.BIDSVersion = 'v1.2.1'; 30 | generalInfo.License = 'CC0'; 31 | generalInfo.Authors = { 'Arnaud Delorme' 'Scott Makeig' 'Marissa Westerfield' }; 32 | 33 | %% participant column description for participants.json file 34 | % --------------------------------------------------------- 35 | pInfoDesc.participant_id.LongName = 'Participant identifier'; 36 | pInfoDesc.participant_id.Description = 'Unique participant identifier'; 37 | 38 | pInfoDesc.gender.Description = 'Sex of the participant'; 39 | pInfoDesc.gender.Levels.M = 'male'; 40 | pInfoDesc.gender.Levels.F = 'female'; 41 | 42 | pInfoDesc.age.Description = 'age of the participant'; 43 | pInfoDesc.age.Units = 'years'; 44 | 45 | %% event column description for xxx-events.json file (only one such file) 46 | % ---------------------------------------------------------------------- 47 | eInfo = {'onset' 'latency'; 48 | 'sample' 'latency'; 49 | 'value' 'type' }; % ADD HED HERE 50 | 51 | eInfoDesc.onset.Description = 'Event onset'; 52 | eInfoDesc.onset.Units = 'second'; 53 | 54 | eInfoDesc.response_time.Description = 'Latency of button press after auditory stimulus'; 55 | eInfoDesc.response_time.Levels.Units = 'millisecond'; 56 | 57 | % You do not need to define both trial type and value in this simple 58 | % example, but it is good to know that both exist. There is no definite 59 | % rule regarding the difference between these two fields. As their name 60 | % indicate, "trial_type" contains the type of trial and "value" contains 61 | % more information about a trial of given type. 62 | eInfoDesc.trial_type.Description = 'Type of event'; 63 | eInfoDesc.trial_type.Levels.stimulus = 'Visual stimulus'; 64 | eInfoDesc.trial_type.Levels.response = 'Response of participant'; 65 | 66 | eInfoDesc.value.Description = 'Value of event'; 67 | eInfoDesc.value.Levels.square = 'Square visual stimulus'; 68 | eInfoDesc.value.Levels.rt = 'Behavioral response'; 69 | 70 | % This allow to define trial types based on EEGLAB type - it is optional 71 | trialTypes = { 'rt' 'response'; 72 | 'square' 'stimulus' }; 73 | 74 | %% Content for README file 75 | % ----------------------- 76 | README = [ 'EEGLAB Tutorial Dataset ' 10 ... 77 | '' 10 ... 78 | 'During this selective visual attention experiment, ' 10 ... 79 | 'stimuli appeared briefly in any of five squares ' 10 ... 80 | 'arrayed horizontally above a central fixation cross. ' 10 ... 81 | 'In each experimental block, one (target) box was ' 10 ... 82 | 'differently colored from the rest Whenever a square ' 10 ... 83 | 'appeared in the target box the subject was asked to ' 10 ... 84 | 'respond quickly with a right thumb button press. If ' 10 ... 85 | 'the stimulus was a circular disk, he was asked to ' 10 ... 86 | 'ignore it.' 10 ... 87 | '' 10 ... 88 | 'These data were constructed by concatenating ' 10 ... 89 | 'three-second epochs from one subject, each containing' 10 ... 90 | 'a target square in the attended location (''square'' ' 10 ... 91 | 'events, left-hemifield locations 1 or 2 only) ' 10 ... 92 | 'followed by a button response (''rt'' events). The data' 10 ... 93 | 'were stored in continuous data format to illustrate ' 10 ... 94 | 'the process of epoch extraction from continuous data.' ]; 95 | 96 | %% Content for CHANGES file 97 | % ------------------------ 98 | CHANGES = sprintf([ 'Version 1.0 - 4 Aug 2020\n' ... 99 | ' - Initial release\n' ]); 100 | 101 | %% Task information for xxxx-eeg.json file 102 | % --------------------------------------- 103 | tInfo.InstitutionAddress = '9500 Gilman Drive, La Jolla CA 92093, USA'; 104 | tInfo.InstitutionName = 'University of California, San Diego'; 105 | tInfo.InstitutionalDepartmentName = 'Institute of Neural Computation'; 106 | tInfo.PowerLineFrequency = 60; 107 | tInfo.ManufacturersModelName = 'Snapmaster'; 108 | %tInfo.Reference = 'Delorme A, Westerfield M, Makeig S. Medial prefrontal theta bursts precede rapid motor responses during visual selective attention. J Neurosci. 2007 Oct 31;27(44):11949-59. doi: 10.1523/JNEUROSCI.3477-07.2007. PMID: 17978035; PMCID: PMC6673364.' 109 | % tInfo.Instructions 110 | 111 | 112 | % call to the export function 113 | % --------------------------- 114 | targetFolder = './BIDS_p300'; 115 | bids_export(data, ... 116 | 'targetdir', targetFolder, ... 117 | 'taskName', 'P300',... 118 | 'gInfo', generalInfo, ... 119 | 'pInfo', pInfo, ... 120 | 'pInfoDesc', pInfoDesc, ... 121 | 'eInfo', eInfo, ... 122 | 'eInfoDesc', eInfoDesc, ... 123 | 'README', README, ... 124 | 'CHANGES', CHANGES, ... 125 | 'codefiles', codefiles, ... 126 | 'trialtype', trialTypes, ... 127 | 'renametype', {}, ... 128 | 'checkresponse', 'condition 1', ... 129 | 'tInfo', tInfo, ... 130 | 'copydata', 1); 131 | % 132 | % % copy stimuli and source data folders 133 | % % ----------------------------------- 134 | % copyfile('../stimuli', fullfile(targetFolder, 'stimuli'), 'f'); 135 | % copyfile('../sourcedata', fullfile(targetFolder, 'sourcedata'), 'f'); 136 | 137 | fprintf(2, 'WHAT TO DO NEXT?') 138 | fprintf(2, ' -> upload the %s folder to http://openneuro.org to check it is valid\n', targetFolder); 139 | 140 | -------------------------------------------------------------------------------- /bids_export_example4.m: -------------------------------------------------------------------------------- 1 | % Same as example 3 but uses different sessions and runs 2 | % 3 | % Arnaud Delorme - May 2022 4 | 5 | data = []; 6 | p =fileparts(which('eeglab')); 7 | data(end+1).file = { fullfile(p, 'sample_data', 'eeglab_data.set') fullfile(p, 'sample_data', 'eeglab_data.set') fullfile(p, 'sample_data', 'eeglab_data.set') fullfile(p, 'sample_data', 'eeglab_data.set') }; 8 | data(end ).session = { 's1' 's1' 's2' 's2' }; 9 | data(end ).run = { 'r1' 'r2' 'r1' 'r2' }; 10 | % data(end ).session = [ 1 1 2 2]; % works as well 11 | % data(end ).run = [ 1 2 1 2]; 12 | data(end ).task = { 'p300' 'p300' 'p300' 'p300' }; 13 | data(end ).notes = { 'No notes' 'No notes' 'No notes' 'No notes' }; 14 | 15 | %% participant information for participants.tsv file 16 | % ------------------------------------------------- 17 | pInfo = { 'participant_id' 'gender' 'age'; 18 | 'NDARAB793GL3' 'M' 22 }; 19 | 20 | %% Code Files used to preprocess and import to BIDS 21 | % -----------------------------------------------------| 22 | codefiles = { fullfile(pwd, mfilename) }; 23 | 24 | %% general information for dataset_description.json file 25 | % ----------------------------------------------------- 26 | generalInfo.Name = 'P300 visual task'; 27 | generalInfo.ReferencesAndLinks = { 'No bibliographic reference other than the DOI for this dataset' }; 28 | generalInfo.BIDSVersion = 'v1.2.1'; 29 | generalInfo.License = 'CC0'; 30 | generalInfo.Authors = { 'Arnaud Delorme' 'Scott Makeig' 'Marissa Westerfield' }; 31 | 32 | %% participant column description for participants.json file 33 | % --------------------------------------------------------- 34 | pInfoDesc.participant_id.LongName = 'Participant identifier'; 35 | pInfoDesc.participant_id.Description = 'Unique participant identifier'; 36 | 37 | pInfoDesc.gender.Description = 'Sex of the participant'; 38 | pInfoDesc.gender.Levels.M = 'male'; 39 | pInfoDesc.gender.Levels.F = 'female'; 40 | 41 | pInfoDesc.age.Description = 'age of the participant'; 42 | pInfoDesc.age.Units = 'years'; 43 | 44 | %% event column description for xxx-events.json file (only one such file) 45 | % ---------------------------------------------------------------------- 46 | eInfo = {'onset' 'latency'; 47 | 'sample' 'latency'; 48 | 'value' 'type' }; % ADD HED HERE 49 | 50 | eInfoDesc.onset.Description = 'Event onset'; 51 | eInfoDesc.onset.Units = 'second'; 52 | 53 | eInfoDesc.response_time.Description = 'Latency of button press after auditory stimulus'; 54 | eInfoDesc.response_time.Levels.Units = 'millisecond'; 55 | 56 | % You do not need to define both trial type and value in this simple 57 | % example, but it is good to know that both exist. There is no definite 58 | % rule regarding the difference between these two fields. As their name 59 | % indicate, "trial_type" contains the type of trial and "value" contains 60 | % more information about a trial of given type. 61 | eInfoDesc.trial_type.Description = 'Type of event'; 62 | eInfoDesc.trial_type.Levels.stimulus = 'Visual stimulus'; 63 | eInfoDesc.trial_type.Levels.response = 'Response of participant'; 64 | 65 | eInfoDesc.value.Description = 'Value of event'; 66 | eInfoDesc.value.Levels.square = 'Square visual stimulus'; 67 | eInfoDesc.value.Levels.rt = 'Behavioral response'; 68 | 69 | % This allow to define trial types based on EEGLAB type - it is optional 70 | trialTypes = { 'rt' 'response'; 71 | 'square' 'stimulus' }; 72 | 73 | %% Content for README file 74 | % ----------------------- 75 | README = [ 'EEGLAB Tutorial Dataset ' 10 ... 76 | '' 10 ... 77 | 'During this selective visual attention experiment, ' 10 ... 78 | 'stimuli appeared briefly in any of five squares ' 10 ... 79 | 'arrayed horizontally above a central fixation cross. ' 10 ... 80 | 'In each experimental block, one (target) box was ' 10 ... 81 | 'differently colored from the rest Whenever a square ' 10 ... 82 | 'appeared in the target box the subject was asked to ' 10 ... 83 | 'respond quickly with a right thumb button press. If ' 10 ... 84 | 'the stimulus was a circular disk, he was asked to ' 10 ... 85 | 'ignore it.' 10 ... 86 | '' 10 ... 87 | 'These data were constructed by concatenating ' 10 ... 88 | 'three-second epochs from one subject, each containing' 10 ... 89 | 'a target square in the attended location (''square'' ' 10 ... 90 | 'events, left-hemifield locations 1 or 2 only) ' 10 ... 91 | 'followed by a button response (''rt'' events). The data' 10 ... 92 | 'were stored in continuous data format to illustrate ' 10 ... 93 | 'the process of epoch extraction from continuous data.' ]; 94 | 95 | %% Content for CHANGES file 96 | % ------------------------ 97 | CHANGES = sprintf([ 'Version 1.0 - 4 Aug 2020\n' ... 98 | ' - Initial release\n' ]); 99 | 100 | %% Task information for xxxx-eeg.json file 101 | % --------------------------------------- 102 | tInfo.InstitutionAddress = '9500 Gilman Drive, La Jolla CA 92093, USA'; 103 | tInfo.InstitutionName = 'University of California, San Diego'; 104 | tInfo.InstitutionalDepartmentName = 'Institute of Neural Computation'; 105 | tInfo.PowerLineFrequency = 60; 106 | tInfo.ManufacturersModelName = 'Snapmaster'; 107 | %tInfo.Reference = 'Delorme A, Westerfield M, Makeig S. Medial prefrontal theta bursts precede rapid motor responses during visual selective attention. J Neurosci. 2007 Oct 31;27(44):11949-59. doi: 10.1523/JNEUROSCI.3477-07.2007. PMID: 17978035; PMCID: PMC6673364.' 108 | % tInfo.Instructions 109 | 110 | 111 | % call to the export function 112 | % --------------------------- 113 | targetFolder = './BIDS_p300'; 114 | bids_export(data, ... 115 | 'targetdir', targetFolder, ... 116 | 'gInfo', generalInfo, ... 117 | 'pInfo', pInfo, ... 118 | 'pInfoDesc', pInfoDesc, ... 119 | 'eInfo', eInfo, ... 120 | 'eInfoDesc', eInfoDesc, ... 121 | 'README', README, ... 122 | 'CHANGES', CHANGES, ... 123 | 'codefiles', codefiles, ... 124 | 'trialtype', trialTypes, ... 125 | 'renametype', {}, ... 126 | 'checkresponse', 'condition 1', ... 127 | 'tInfo', tInfo, ... 128 | 'copydata', 1); 129 | % 130 | % % copy stimuli and source data folders 131 | % % ----------------------------------- 132 | % copyfile('../stimuli', fullfile(targetFolder, 'stimuli'), 'f'); 133 | % copyfile('../sourcedata', fullfile(targetFolder, 'sourcedata'), 'f'); 134 | 135 | fprintf(2, 'WHAT TO DO NEXT?') 136 | fprintf(2, ' -> upload the %s folder to http://openneuro.org to check it is valid\n', targetFolder); 137 | 138 | -------------------------------------------------------------------------------- /bids_export_example4_eye.m: -------------------------------------------------------------------------------- 1 | % Example to export joint EEG and Eye-tracking data (child mind) 2 | % The data is not included here because it is too large, but it 3 | % can be downloaded from http://fcon_1000.projects.nitrc.org/indi/cmi_healthy_brain_network/sharing_neuro.html 4 | % 5 | % Arnaud Delorme - May 2023 6 | 7 | clear 8 | cd('/System/Volumes/Data/data/matlab/bids_eyetracking') 9 | data = []; 10 | p =fileparts(which('eeglab')); 11 | data(end+1).file = { 'hbn_files/NDARAA773LUW_SurroundSupp_Block1.set' }; 12 | data(end ).eyefile = { 'hbn_files/NDARAA773LUW_SurrSupp_Block1.tsv' }; 13 | % data(end+1).file = { 'hbn_files/Video1.set' }; 14 | % data(end ).eyefile = { 'hbn_files/NDARAA075AMK_Video1_Samples.txt' }; 15 | 16 | %% participant information for participants.tsv file 17 | % ------------------------------------------------- 18 | pInfo = { 'participantID' 'gender' 'age'; 19 | 'NDARAA773LUW' 'M' 22 }; 20 | % 'NDARAA075AMK' 'M' 22; 21 | 22 | %% Code Files used to preprocess and import to BIDS 23 | % -----------------------------------------------------| 24 | codefiles = { fullfile(pwd, mfilename) }; 25 | 26 | %% general information for dataset_description.json file 27 | % ----------------------------------------------------- 28 | generalInfo.Name = 'Test'; 29 | generalInfo.ReferencesAndLinks = { 'No bibliographic reference other than the DOI for this dataset' }; 30 | generalInfo.BIDSVersion = 'v1.2.1'; 31 | generalInfo.License = 'CC0'; 32 | generalInfo.Authors = { 'Arnaud Delorme' 'Deepa Gupta' }; 33 | 34 | %% participant column description for participants.json file 35 | % --------------------------------------------------------- 36 | pInfoDesc.participant_id.LongName = 'Participant identifier'; 37 | pInfoDesc.participant_id.Description = 'Unique participant identifier'; 38 | 39 | pInfoDesc.gender.Description = 'Sex of the participant'; 40 | pInfoDesc.gender.Levels.M = 'male'; 41 | pInfoDesc.gender.Levels.F = 'female'; 42 | 43 | pInfoDesc.age.Description = 'age of the participant'; 44 | pInfoDesc.age.Units = 'years'; 45 | 46 | % %% event column description for xxx-events.json file (only one such file) 47 | % % ---------------------------------------------------------------------- 48 | % eInfo = {'onset' 'latency'; 49 | % 'sample' 'latency'; 50 | % 'value' 'type' }; % ADD HED HERE 51 | % 52 | % eInfoDesc.onset.Description = 'Event onset'; 53 | % eInfoDesc.onset.Units = 'second'; 54 | % 55 | % eInfoDesc.response_time.Description = 'Latency of button press after auditory stimulus'; 56 | % eInfoDesc.response_time.Levels.Units = 'millisecond'; 57 | 58 | %% Content for README file 59 | % ----------------------- 60 | README = [ 'Test text' ]; 61 | 62 | %% Content for CHANGES file 63 | % ------------------------ 64 | CHANGES = sprintf([ 'Version 1.0 - 3 April 2023\n' ... 65 | ' - Initial release\n' ]); 66 | 67 | %% Task information for xxxx-eeg.json file 68 | % --------------------------------------- 69 | tInfo.InstitutionAddress = '9500 Gilman Drive, La Jolla CA 92093, USA'; 70 | tInfo.InstitutionName = 'University of California, San Diego'; 71 | tInfo.InstitutionalDepartmentName = 'Institute of Neural Computation'; 72 | tInfo.PowerLineFrequency = 60; 73 | tInfo.ManufacturersModelName = 'Snapmaster'; 74 | 75 | eInfo = { 76 | 'onset' 'latency'; 77 | 'sample' 'latency'; 78 | 'value' 'type'; 79 | 'description' 'description' }; 80 | 81 | % call to the export function 82 | % --------------------------- 83 | targetFolder = 'BIDS_eye'; 84 | bids_export(data, ... 85 | 'targetdir', targetFolder, ... 86 | 'taskName', 'HBN-data',... 87 | 'gInfo', generalInfo, ... 88 | 'pInfo', pInfo, ... 89 | 'pInfoDesc', pInfoDesc, ... 90 | 'eInfo', eInfo, ... 91 | 'README', README, ... 92 | 'CHANGES', CHANGES, ... 93 | 'codefiles', codefiles, ... 94 | 'trialtype', {}, ... 95 | 'renametype', {}, ... 96 | 'tInfo', tInfo, ... 97 | 'copydata', 1); 98 | % 99 | % % copy stimuli and source data folders 100 | % % ----------------------------------- 101 | % copyfile('../stimuli', fullfile(targetFolder, 'stimuli'), 'f'); 102 | % copyfile('../sourcedata', fullfile(targetFolder, 'sourcedata'), 'f'); 103 | 104 | fprintf(2, 'WHAT TO DO NEXT?') 105 | fprintf(2, ' -> upload the %s folder to http://openneuro.org to check it is valid\n', targetFolder); 106 | 107 | -------------------------------------------------------------------------------- /bids_export_example5_ieeg.m: -------------------------------------------------------------------------------- 1 | % Example to export NWB-formated iEEG data to BIDS 2 | % The data is not included here because it is too large, but it 3 | % can be downloaded from https://dandiarchive.org/dandiset/000576 4 | % 5 | % Arnaud Delorme - April 2024 6 | 7 | folder = '/System/Volumes/Data/data/data/nwb000576/'; 8 | if ~exist(folder, 'dir') 9 | error([ 'Data folder not found' 10 'Download data at https://dandiarchive.org/dandiset/000576' 10 'and set the folder variable in this script' ]); 10 | end 11 | 12 | % list of files to include (a loop is also possible) 13 | data = []; 14 | data(end+1).file = { fullfile(folder, 'sub-01', 'sub-01_ses-20140828T132700_ecephys+image.nwb') }; 15 | data(end+1).file = { fullfile(folder, 'sub-02', 'sub-02_ses-20160302T164800_ecephys+image.nwb') }; 16 | data(end+1).file = { fullfile(folder, 'sub-03', 'sub-03_ses-20150304T164400_ecephys+image.nwb') }; 17 | data(end+1).file = { fullfile(folder, 'sub-04', 'sub-04_ses-20120829T175200_ecephys+image.nwb') }; 18 | data(end+1).file = { fullfile(folder, 'sub-05', 'sub-05_ses-20121207T173500_ecephys+image.nwb') }; 19 | data(end+1).file = { fullfile(folder, 'sub-06', 'sub-06_ses-20131209T113600_ecephys+image.nwb') }; 20 | data(end+1).file = { fullfile(folder, 'sub-07', 'sub-07_ses-20160212T171400_ecephys+image.nwb') }; 21 | data(end+1).file = { fullfile(folder, 'sub-08', 'sub-08_ses-20161217T120200_ecephys+image.nwb') }; 22 | data(end+1).file = { fullfile(folder, 'sub-09', 'sub-09_ses-20170311T170500_ecephys+image.nwb') }; 23 | 24 | % Content for README file 25 | README = 'Re-exporting Dandiset https://dandiarchive.org/dandiset/000576 as BIDS'; 26 | 27 | % call to the export function 28 | bids_export(data, 'targetdir', 'BIDS000576', 'README', README, 'exportformat', 'same'); 29 | 30 | -------------------------------------------------------------------------------- /bids_export_eye_tracking_example5.m: -------------------------------------------------------------------------------- 1 | % Example to export joint EEG and Eye-tracking data (child mind) 2 | % The data is included in this repository (on Github, maybe not on the 3 | % plugin downloaded from EEGLAB). 4 | % 5 | % THIS PLUGIN REQUIRES THE INSTALLATION OF THE GAZEPOINT AND SMI EEGLAB PLUGINS 6 | % 7 | % Arnaud Delorme - December 2023 8 | 9 | clear 10 | data = []; 11 | p =fileparts(which('bids_export_example5_eye')); 12 | data(end+1).file = { fullfile(p,'testing','hbn_eye_tracking_data','NDARAA773LUW_SurroundSupp_Block1.set') }; 13 | data(end ).eyefile = { fullfile(p,'testing','hbn_eye_tracking_data','NDARAA773LUW_SurrSupp_Block1.tsv') }; % Gazepoint eye tracking format 14 | data(end+1).file = { fullfile(p,'testing','hbn_eye_tracking_data','NDARAA075AMK_Video1.set') }; 15 | data(end ).eyefile = { fullfile(p,'testing','hbn_eye_tracking_data','NDARAA075AMK_Video1_Samples.txt') }; % SMI eye tracking format 16 | 17 | %% participant information for participants.tsv file 18 | % ------------------------------------------------- 19 | pInfo = { 'participantID'; 20 | 'NDARAA773LUW'; 21 | 'NDARAA075AMK' } 22 | 23 | %% Code Files used to preprocess and import to BIDS 24 | % -----------------------------------------------------| 25 | codefiles = { fullfile(pwd, mfilename) }; 26 | 27 | %% general information for dataset_description.json file 28 | % ----------------------------------------------------- 29 | generalInfo.Name = 'Test'; 30 | generalInfo.ReferencesAndLinks = { 'No bibliographic reference other than the DOI for this dataset' }; 31 | generalInfo.BIDSVersion = 'v1.2.1'; 32 | generalInfo.License = 'CC0'; 33 | generalInfo.Authors = { 'Arnaud Delorme' 'Deepa Gupta' }; 34 | 35 | %% participant column description for participants.json file 36 | % --------------------------------------------------------- 37 | pInfoDesc.participant_id.LongName = 'Participant identifier'; 38 | pInfoDesc.participant_id.Description = 'Unique participant identifier'; 39 | 40 | %% Content for README file 41 | % ----------------------- 42 | README = [ 'This is a test export containing joint eye tracking and EEG data' ]; 43 | 44 | %% Content for CHANGES file 45 | % ------------------------ 46 | CHANGES = sprintf([ 'Version 1.0 - 12 December 2023\n' ... 47 | ' - Initial release\n' ]); 48 | 49 | %% Task information for xxxx-eeg.json file 50 | % --------------------------------------- 51 | tInfo.InstitutionAddress = '9500 Gilman Drive, La Jolla CA 92093, USA'; 52 | tInfo.InstitutionName = 'University of California, San Diego'; 53 | tInfo.InstitutionalDepartmentName = 'Institute of Neural Computation'; 54 | tInfo.PowerLineFrequency = 60; 55 | tInfo.ManufacturersModelName = 'Snapmaster'; 56 | 57 | eInfo = { 58 | 'onset' 'latency'; 59 | 'sample' 'latency'; 60 | 'value' 'type'; 61 | 'description' 'description' }; 62 | 63 | % call to the export function 64 | % --------------------------- 65 | targetFolder = 'BIDS_eye'; 66 | bids_export(data, ... 67 | 'targetdir', targetFolder, ... 68 | 'taskName', 'HBNdata',... 69 | 'gInfo', generalInfo, ... 70 | 'pInfo', pInfo, ... 71 | 'pInfoDesc', pInfoDesc, ... 72 | 'eInfo', eInfo, ... 73 | 'README', README, ... 74 | 'CHANGES', CHANGES, ... 75 | 'codefiles', codefiles, ... 76 | 'trialtype', {}, ... 77 | 'renametype', {}, ... 78 | 'tInfo', tInfo, ... 79 | 'copydata', 1); 80 | -------------------------------------------------------------------------------- /bids_exporter.m: -------------------------------------------------------------------------------- 1 | % BIDS_EXPORTER - BIDS export wizard, from raw EEG to BIDS 2 | % 3 | % Usage: 4 | % >> bids_exporter; % create new study interactively 5 | % 6 | % Authors: Dung Truong and Arnaud Delorme, SCCN, INC, UCSD, July 22, 2024 7 | 8 | % Copyright (C) Arnaud Delorme, 2024 9 | % 10 | % This file is part of EEGLAB, see http://www.eeglab.org 11 | % for the documentation and details. 12 | % 13 | % Redistribution and use in source and binary forms, with or without 14 | % modification, are permitted provided that the following conditions are met: 15 | % 16 | % 1. Redistributions of source code must retain the above copyright notice, 17 | % this list of conditions and the following disclaimer. 18 | % 19 | % 2. Redistributions in binary form must reproduce the above copyright notice, 20 | % this list of conditions and the following disclaimer in the documentation 21 | % and/or other materials provided with the distribution. 22 | % 23 | % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | % AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | % IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 26 | % ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 27 | % LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 28 | % CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 29 | % SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 30 | % INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 31 | % CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 32 | % ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 33 | % THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | function bids_exporter(varargin) 36 | if ~exist('pop_studywizard') 37 | error('This tool is not available in this EEGLAB version. Use the latest EEGLAB version.') 38 | end 39 | 40 | if nargin == 0 41 | str = [ 'This tool allows you to select binary EEG files' 10 ... 42 | 'and format them to a BIDS dataset. For more information' 10 ... 43 | 'see the online help at https://eegbids.org']; 44 | 45 | [~, ~, err, ~] = inputgui('geometry', { 1 1 }, 'geomvert', [1 3], 'uilist', ... 46 | { { 'style' 'text' 'string' 'Welcome to the EEG-BIDS exporter tool!' 'fontweight' 'bold' } { 'style' 'text' 'string' str } }, 'okbut', 'continue'); 47 | 48 | if isempty(err), return; end % not ideal but no better solution now 49 | [STUDY, ALLEEG] = pop_studywizard(); 50 | if isempty(ALLEEG), return; end 51 | 52 | % task Name 53 | taskName = ''; 54 | if ~isfield(ALLEEG, 'task') || isempty(ALLEEG(1).task) 55 | res = inputgui('geom', { {2 1 [0 0] [1 1]} {2 1 [1 0] [1 1]} }, 'uilist', ... 56 | { { 'style' 'text' 'string' 'Enter the task name' } ... 57 | { 'style' 'edit' 'string' '' } }); 58 | if ~isempty(res) && ~isempty(res{1}) 59 | taskName = res{1}; 60 | else 61 | errordlg('Operation aborted as a task name is required') 62 | end 63 | end 64 | 65 | STUDY.task = taskName; 66 | pop_exportbids(STUDY, ALLEEG); 67 | elseif nargin == 1 && exist(varargin{1}, 'file') 68 | pop_runscript(varargin{1}); 69 | end 70 | -------------------------------------------------------------------------------- /bids_formatderivative.m: -------------------------------------------------------------------------------- 1 | function bids_add_json_to_dot_set(folder) 2 | 3 | fcontent = dir(folder); 4 | 5 | for iFile = 1:length(fcontent) 6 | if ~isequal(fcontent(iFile).name(1), '.') 7 | if fcontent(iFile).isdir 8 | bids_add_json_to_dot_set(fullfile(folder, fcontent(iFile).name)) 9 | elseif length(fcontent(iFile).name) > 4 && isequal(fcontent(iFile).name(end-3:end), '.set') 10 | filename = fullfile(folder, fcontent(iFile).name); 11 | fprintf('%s\n', filename); 12 | % EEG = pop_loadset(filename); 13 | fid = fopen(filename, 'w'); 14 | fprintf(fid, 'xxx'); 15 | fclose(fid); 16 | 17 | % fid = fopen([ filename(1:end-4) '.json' ], 'w'); 18 | % if fid == -1 19 | % error('Cannot open file') 20 | % end 21 | 22 | % eegChans = sum(cellfun(@(x)isequal(x, 'EEG'), { EEG.chanlocs.type})); 23 | % noneegChans = EEG.nbchan - eegChans; 24 | % 25 | % fprintf(fid, '{\n'); 26 | % fprintf(fid, ' "TaskName": "WorkingMemory",\n'); 27 | % fprintf(fid, ' "EEGReference": "common",\n'); 28 | % fprintf(fid, ' "RecordingType": "continuous",\n'); 29 | % fprintf(fid, ' "RecordingDuration": %1.3f,\n', EEG.xmax); 30 | % fprintf(fid, ' "SamplingFrequency": %1.1f,\n', EEG.srate); 31 | % fprintf(fid, ' "EEGChannelCount": %d,\n', eegChans); 32 | % fprintf(fid, ' "EOGChannelCount": %d,\n', noneegChans); 33 | % fprintf(fid, ' "PowerLineFrequency": 60,\n'); 34 | % fprintf(fid, ' "SoftwareFilters": "n/a"\n'); 35 | % fprintf(fid, '}\n'); 36 | 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /bids_geteventfieldsfromfolder.m: -------------------------------------------------------------------------------- 1 | % bids_geteventfiledsfromfolder() - Scan bids folders recursively to get the list of event fields 2 | % from the first events.tsv file 3 | % 4 | % Usage: 5 | % >> fields = bids_geteventfieldsfromfolder(bidsfolder); 6 | % 7 | % Inputs: 8 | % bidsfolder - a loaded epoched EEG dataset structure. 9 | % 10 | % Outputs: 11 | % fields - [cell array] list of event fields in events.tsv 12 | % 13 | % Authors: Dung Truong, Arnaud Delorme, SCCN, INC, UCSD, March, 2022 14 | 15 | % Copyright (C) Dung Truong, 2022 16 | % 17 | % This program is free software; you can redistribute it and/or modify 18 | % it under the terms of the GNU General Public License as published by 19 | % the Free Software Foundation; either version 2 of the License, or 20 | % (at your option) any later version. 21 | % 22 | % This program is distributed in the hope that it will be useful, 23 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 | % GNU General Public License for more details. 26 | % 27 | % You should have received a copy of the GNU General Public License 28 | % along with this program; if not, to the Free Software 29 | % Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 30 | 31 | function fields = bids_geteventfieldsfromfolder(bidsFolder) 32 | 33 | fields = {}; 34 | files = dir(bidsFolder); 35 | [files(:).folder] = deal(bidsFolder); 36 | %fprintf('Scanning %s\n', bidsFolder); 37 | for iFile = 1:length(files) 38 | if files(iFile).isdir && files(iFile).name(1) ~= '.' 39 | fieldlistTmp = bids_geteventfieldsfromfolder(fullfile(files(iFile).folder, fullfile(files(iFile).name))); 40 | if ~isempty(fieldlistTmp) 41 | fieldsTmp = setdiff(fieldlistTmp, {'onset', 'duration', 'sample', 'stim_file', 'response_time'}); 42 | fields = union(fields, fieldsTmp); 43 | end 44 | else 45 | if ~isempty(strfind(files(iFile).name, 'events.tsv')) 46 | res = loadtxt( fullfile(files(iFile).folder, files(iFile).name), 'verbose', 'off', 'delim', 9, 'convert', 'off'); 47 | fields = union(fields, res(1,:)); 48 | % % other solutions 49 | % res = readtable( fullfile(files(iFile).folder, files(iFile).name), 'filetype', 'delimitedtext', 'delimiter', char(9)); 50 | % fields = union(fields, fieldnames(res)); % has additional fields Properties, Rows and Variables that need to be removeds 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /bids_getinfofromfolder.m: -------------------------------------------------------------------------------- 1 | % bids_gettaskfromfolder() - Scan bids folders to get the list of tasks 2 | % 3 | % Usage: 4 | % >> tasklist = bids_gettaskfromfolder(bidsfolder); 5 | % 6 | % Inputs: 7 | % bidsfolder - a loaded epoched EEG dataset structure. 8 | % 9 | % Outputs: 10 | % tasklist - [cell array] list of tasks 11 | % 12 | % Authors: Arnaud Delorme, SCCN, INC, UCSD, March, 2021 13 | 14 | % Copyright (C) Arnaud Delorme, 2021 15 | % 16 | % This program is free software; you can redistribute it and/or modify 17 | % it under the terms of the GNU General Public License as published by 18 | % the Free Software Foundation; either version 2 of the License, or 19 | % (at your option) any later version. 20 | % 21 | % This program is distributed in the hope that it will be useful, 22 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | % GNU General Public License for more details. 25 | % 26 | % You should have received a copy of the GNU General Public License 27 | % along with this program; if not, to the Free Software 28 | % Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 29 | 30 | function [tasklist,sessions,runs] = bids_getinfofromfolder(bidsFolder) 31 | 32 | tasklist = {}; 33 | sessions = {}; 34 | runs = {}; 35 | files = dir(bidsFolder); 36 | [files(:).folder] = deal(bidsFolder); 37 | %fprintf('Scanning %s\n', bidsFolder); 38 | for iFile = 1:length(files) 39 | if files(iFile).isdir && files(iFile).name(end) ~= '.' && length(files(iFile).name) > 2 && ~isequal(lower(files(iFile).name(end-2:end)), '.ds') 40 | if length(files(iFile).name) > 2 && strcmpi(files(iFile).name(1:3), 'ses') 41 | sessions = union(sessions, { files(iFile).name }); 42 | end 43 | 44 | [tasklistTmp,sessionTmp,runsTmp] = bids_getinfofromfolder(fullfile(files(iFile).folder, fullfile(files(iFile).name))); 45 | tasklist = union(tasklist, tasklistTmp); 46 | sessions = union(sessions, sessionTmp); 47 | runs = union(runs , runsTmp); 48 | else 49 | if (~isempty(strfind(files(iFile).name, 'eeg')) || ~isempty(strfind(files(iFile).name, 'meg'))) && ~isempty(strfind(files(iFile).name, '_task')) 50 | pos = strfind(files(iFile).name, '_task'); 51 | tmpStr = files(iFile).name(pos+6:end); 52 | underS = find(tmpStr == '_'); 53 | newTask = tmpStr(1:underS(1)-1); 54 | tasklist = union( tasklist, { newTask }); 55 | end 56 | if (~isempty(strfind(files(iFile).name, 'eeg')) || ~isempty(strfind(files(iFile).name, 'meg'))) && ~isempty(strfind(files(iFile).name, '_run')) 57 | pos = strfind(files(iFile).name, '_run'); 58 | tmpStr = files(iFile).name(pos+5:end); 59 | underS = find(tmpStr == '_'); 60 | newRun = tmpStr(1:underS(1)-1); 61 | runs = union( runs, { newRun } ); 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /bids_gettaskfromfolder.m: -------------------------------------------------------------------------------- 1 | % bids_gettaskfromfolder() - Scan bids folders to get the list of tasks 2 | % 3 | % Usage: 4 | % >> tasklist = bids_gettaskfromfolder(bidsfolder); 5 | % 6 | % Inputs: 7 | % bidsfolder - a loaded epoched EEG dataset structure. 8 | % 9 | % Outputs: 10 | % tasklist - [cell array] list of tasks 11 | % 12 | % Authors: Arnaud Delorme, SCCN, INC, UCSD, March, 2021 13 | 14 | % Copyright (C) Arnaud Delorme, 2021 15 | % 16 | % This program is free software; you can redistribute it and/or modify 17 | % it under the terms of the GNU General Public License as published by 18 | % the Free Software Foundation; either version 2 of the License, or 19 | % (at your option) any later version. 20 | % 21 | % This program is distributed in the hope that it will be useful, 22 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | % GNU General Public License for more details. 25 | % 26 | % You should have received a copy of the GNU General Public License 27 | % along with this program; if not, to the Free Software 28 | % Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 29 | 30 | function tasklist = bids_gettaskfromfolder(bidsFolder) 31 | 32 | tasklist = {}; 33 | files = dir(bidsFolder); 34 | [files(:).folder] = deal(bidsFolder); 35 | %fprintf('Scanning %s\n', bidsFolder); 36 | for iFile = 1:length(files) 37 | if files(iFile).isdir && files(iFile).name(end) ~= '.' 38 | tasklistTmp = bids_gettaskfromfolder(fullfile(files(iFile).folder, fullfile(files(iFile).name))); 39 | if ~isempty(tasklistTmp) 40 | newTasks = tasklistTmp(~ismember(tasklistTmp, tasklist)); 41 | if ~isempty(newTasks) 42 | tasklist = [ tasklist newTasks ]; 43 | end 44 | end 45 | else 46 | if ~isempty(strfind(files(iFile).name, 'eeg')) && ~isempty(strfind(files(iFile).name, 'task')) 47 | pos = strfind(files(iFile).name, 'task'); 48 | tmpStr = files(iFile).name(pos+5:end); 49 | underS = find(tmpStr == '_'); 50 | newTask = tmpStr(1:underS(1)-1); 51 | if ~any(ismember(tasklist, newTask)) 52 | tasklist = [ tasklist { newTask } ]; 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /bids_importchanlocs.m: -------------------------------------------------------------------------------- 1 | % eeg_importchanlocs - import channel info from channels.tsv and electrodes.tsv% 2 | % Usage: 3 | % [EEG, channelData, elecData] = eeg_importchanlocs(EEG, channelFile, elecFile) 4 | % 5 | % Inputs: 6 | % 'EEG' - [struct] the EEG structure 7 | % 8 | % 'channelFile' - [string] full path to the channels.tsv file 9 | % e.g. 10 | % ~/BIDS_EXPORT/sub-01/ses-01/eeg/sub-01_ses-01_task-GoNogo_channels.tsv 11 | % 'elecFile' - [string] full path to the electrodes.tsv file 12 | % e.g. 13 | % ~/BIDS_EXPORT/sub-01/ses-01/eeg/sub-01_ses-01_task-GoNogo_electrodes.tsv 14 | % 15 | % Outputs: 16 | % 17 | % EEG - [struct] the EEG structure with channel info imported 18 | % 19 | % channelData - [cell array] imported data from channels.tsv 20 | % 21 | % elecData - [cell array] imported data from electrodes.tsv 22 | % 23 | % Authors: Dung Truong, Arnaud Delorme, 2022 24 | 25 | function [EEG, channelData, elecData] = bids_importchanlocs(EEG, channelFile, elecFile) 26 | channelData = bids_loadfile(channelFile, ''); 27 | elecData = bids_loadfile(elecFile, ''); 28 | if isfield(EEG, 'chanlocs') 29 | chanlocs = EEG.chanlocs; 30 | else 31 | chanlocs = []; 32 | end 33 | if isempty(channelData) && isempty(elecData) 34 | return 35 | end 36 | for iChan = 2:size(channelData,1) 37 | if size(channelData,2) == 1 38 | fprintf('Warning: BIDS channel data missing tab characters\n') 39 | [chanlocs(iChan-1).labels ,toktmp] = strtok(channelData{iChan,1}); 40 | [chanlocs(iChan-1).type ,toktmp] = strtok(toktmp); 41 | [chanlocs(iChan-1).unit ,toktmp] = strtok(toktmp); 42 | else 43 | % the fields below are all required 44 | chanlocs(iChan-1).labels = channelData{iChan,1}; 45 | chanlocs(iChan-1).type = channelData{iChan,2}; 46 | chanlocs(iChan-1).unit = channelData{iChan,3}; 47 | if size(channelData,2) > 3 48 | chanlocs(iChan-1).status = channelData{iChan,4}; 49 | end 50 | end 51 | end 52 | for iChan = 2:size(elecData,1) 53 | if ~isempty(elecData) && iChan <= size(elecData,1) 54 | chanlocs(iChan-1).labels = elecData{iChan,1}; 55 | chanlocs(iChan-1).X = elecData{iChan,2}; 56 | chanlocs(iChan-1).Y = elecData{iChan,3}; 57 | chanlocs(iChan-1).Z = elecData{iChan,4}; 58 | end 59 | end 60 | 61 | if length(chanlocs) == EEG.nbchan+1 && isequal(lower(chanlocs(end).labels), 'cz') % EGI 62 | chanlocs(end).type = 'FID'; 63 | [chanlocs(1:end-1).type] = deal('EEG'); 64 | end 65 | [chanlocs,chaninfo] = eeg_checkchanlocs(chanlocs); 66 | 67 | if length(chanlocs) ~= EEG.nbchan 68 | warning('Different number of channels in channel location file and EEG file'); 69 | % check if the difference is due to non EEG channels 70 | % list here https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/03-electroencephalography.html 71 | keep = {'EEG','EOG','HEOG','VEOG'}; % keep all eeg related channels 72 | tsv_eegchannels = arrayfun(@(x) sum(strcmpi(x.type,keep)),chanlocs,'UniformOutput',true); 73 | tmpchanlocs = chanlocs; tmpchanlocs(tsv_eegchannels==0)=[]; % remove non eeg related channels 74 | chanlocs = tmpchanlocs; clear tmpchanlocs 75 | end 76 | 77 | if length(chanlocs) ~= EEG.nbchan 78 | if ~isempty(EEG.chanlocs) 79 | warning('channel location file and EEG file do not have the same number of channels - ignoring channel location BIDS files'); 80 | chanlocs = EEG.chanlocs; 81 | chaninfo = EEG.chaninfo; 82 | else 83 | error('channel location file and EEG file do not have the same number of channels (and no channel location in the EEG file)'); 84 | end 85 | end 86 | EEG.chanlocs = chanlocs; 87 | EEG.chaninfo = chaninfo; 88 | end 89 | -------------------------------------------------------------------------------- /bids_importcoordsystemfile.m: -------------------------------------------------------------------------------- 1 | % BIDS_IMPORTCOORDSYSTEMFILE - import coordinate information 2 | % 3 | % Usage: 4 | % [EEG, bids] = bids_importcoordsystemfile(EEG, coordfile, 'key', value) 5 | % 6 | % Inputs: 7 | % 'EEG' - [struct] the EEG structure to which event information will be imported 8 | % coordfile - [string] path to the coordsystem.json file. 9 | % e.g. ~/BIDS_EXPORT/sub-01/ses-01/eeg/sub-01_ses-01_task-GoNogo_coordsystem.json 10 | % 11 | % Optional inputs: 12 | % 'bids' - [struct] structure that saves imported BIDS information. Default is [] 13 | % 14 | % Outputs: 15 | % EEG - [struct] the EEG structure with event info imported 16 | % bids - [struct] structure that saves BIDS information with event information 17 | % 18 | % Authors: Arnaud Delorme, 2022 19 | 20 | function [EEG, bids] = bids_importcoordsystemfile(EEG, coordfile, varargin) 21 | 22 | if nargin < 2 23 | help bids_importcoordsystemfile; 24 | return; 25 | end 26 | 27 | g = finputcheck(varargin, {'bids' 'struct' [] struct([]) }, 'eeg_importcoordsystemfiles', 'ignore'); 28 | if isstr(g), error(g); end 29 | 30 | bids = g.bids; 31 | 32 | % coordinate information 33 | bids(1).coordsystem = bids_importjson(coordfile, '_coordsystem.json'); %bids_loadfile( coordfile, ''); 34 | if ~isfield(EEG.chaninfo, 'nodatchans') 35 | EEG.chaninfo.nodatchans = []; 36 | end 37 | EEG.chaninfo.BIDS = bids(1).coordsystem; 38 | 39 | % import anatomical landmark 40 | % -------------------------- 41 | if isfield(bids.coordsystem, 'AnatomicalLandmarkCoordinates') && ~isempty(bids.coordsystem.AnatomicalLandmarkCoordinates) 42 | factor = checkunit(EEG.chaninfo, 'AnatomicalLandmarkCoordinateUnits'); 43 | fieldNames = fieldnames(bids.coordsystem.AnatomicalLandmarkCoordinates); 44 | for iField = 1:length(fieldNames) 45 | EEG.chaninfo.nodatchans(end+1).labels = fieldNames{iField}; 46 | EEG.chaninfo.nodatchans(end).type = 'FID'; 47 | EEG.chaninfo.nodatchans(end).X = bids.coordsystem.AnatomicalLandmarkCoordinates.(fieldNames{iField})(1)*factor; 48 | EEG.chaninfo.nodatchans(end).Y = bids.coordsystem.AnatomicalLandmarkCoordinates.(fieldNames{iField})(2)*factor; 49 | EEG.chaninfo.nodatchans(end).Z = bids.coordsystem.AnatomicalLandmarkCoordinates.(fieldNames{iField})(3)*factor; 50 | end 51 | EEG.chaninfo.nodatchans = convertlocs(EEG.chaninfo.nodatchans); 52 | end 53 | 54 | % import head position 55 | % -------------------- 56 | if isfield(bids.coordsystem, 'DigitizedHeadPoints') && ~isempty(bids.coordsystem.DigitizedHeadPoints) 57 | factor = checkunit(EEG.chaninfo, 'DigitizedHeadPointsCoordinateUnits'); 58 | try 59 | headpos = readlocs(bids.coordsystem.DigitizedHeadPoints, 'filetype', 'sfp'); 60 | for iPoint = 1:length(headpos) 61 | EEG.chaninfo.nodatchans(end+1).labels = headpos{iField}; 62 | EEG.chaninfo.nodatchans(end).type = 'HeadPoint'; 63 | EEG.chaninfo.nodatchans(end).X = headpos(iPoint).X*factor; 64 | EEG.chaninfo.nodatchans(end).Y = headpos(iPoint).Y*factor; 65 | EEG.chaninfo.nodatchans(end).Z = headpos(iPoint).Z*factor; 66 | end 67 | EEG.chaninfo.nodatchans = convertlocs(EEG.chaninfo.nodatchans); 68 | catch 69 | if ischar(bids.coordsystem.DigitizedHeadPoints) 70 | fprintf('Could not read head points file %s\n', bids.coordsystem.DigitizedHeadPoints); 71 | end 72 | end 73 | end 74 | 75 | % coordinate transform factor 76 | % --------------------------- 77 | function factor = checkunit(chaninfo, field) 78 | factor = 1; 79 | if isfield(chaninfo, 'BIDS') && isfield(chaninfo.BIDS, field) && isfield(chaninfo, 'unit') 80 | if isequal(chaninfo.BIDS.(field), 'mm') && isequal(chaninfo.unit, 'cm') 81 | factor = 1/10; 82 | elseif isequal(chaninfo.BIDS.(field), 'mm') && isequal(chaninfo.unit, 'm') 83 | factor = 1/1000; 84 | elseif isequal(chaninfo.BIDS.(field), 'cm') && isequal(chaninfo.unit, 'mm') 85 | factor = 10; 86 | elseif isequal(chaninfo.BIDS.(field), 'cm') && isequal(chaninfo.unit, 'm') 87 | factor = 1/10; 88 | elseif isequal(chaninfo.BIDS.(field), 'm') && isequal(chaninfo.unit, 'cm') 89 | factor = 100; 90 | elseif isequal(chaninfo.BIDS.(field), 'm') && isequal(chaninfo.unit, 'mm') 91 | factor = 1000; 92 | elseif isequal(chaninfo.BIDS.(field), chaninfo.unit) 93 | factor = 1; 94 | else 95 | error('Unit not supported') 96 | end 97 | end 98 | 99 | function nameout = cleanvarname(namein) 100 | 101 | % Routine to remove unallowed characters from strings 102 | % nameout can be use as a variable or field in a structure 103 | 104 | % custom change 105 | if strcmp(namein,'#') 106 | namein = 'nb'; 107 | end 108 | 109 | % 1st char must be a letter 110 | for l=1:length(namein) 111 | if isletter(namein(l)) 112 | nameout = namein(l:end); 113 | break % exist for loop 114 | end 115 | end 116 | 117 | % remove usual suspects 118 | stringcheck = [strfind(namein,'('), ... 119 | strfind(namein,')') ... 120 | strfind(namein,'[') ... 121 | strfind(namein,']') ... 122 | strfind(namein,'{') ... 123 | strfind(namein,'}') ... 124 | strfind(namein,'-') ... 125 | strfind(namein,'+') ... 126 | strfind(namein,'*') ... 127 | strfind(namein,'/') ... 128 | strfind(namein,'#') ... 129 | strfind(namein,'%') ... 130 | strfind(namein,'&') ... 131 | strfind(namein,'@') ... 132 | ]; 133 | 134 | if ~isempty(stringcheck) 135 | nameout(stringcheck) = []; 136 | end 137 | 138 | % last check 139 | if ~isvarname(nameout) 140 | error('the variable name to use is still invalid, check chars to remove') 141 | end -------------------------------------------------------------------------------- /bids_importeventfile.m: -------------------------------------------------------------------------------- 1 | % BIDS_IMPORTEVENTFILE - create EEG.event from events.tsv and optionally events.json files 2 | % 3 | % Usage: 4 | % [EEG, ~, ~, ~] = bids_importeventfile(EEG, eventfile, 'key', value) 5 | % 6 | % Inputs: 7 | % 'EEG' - [struct] the EEG structure to which event information will be imported 8 | % 'eventfile' - [string] path to the events.tsv file. 9 | % e.g. ~/BIDS_EXPORT/sub-01/ses-01/eeg/sub-01_ses-01_task-GoNogo_events.tsv 10 | % 11 | % Optional inputs: 12 | % 'bids' - [struct] structure that saves imported BIDS information. Default is [] 13 | % 'eventDescFile' - [string] path to events.json file if applicable. Default is empty 14 | % 'eventtype' - [string] BIDS event column to be used as type in EEG.event. Default is 'value' 15 | % 16 | % 'usevislab' - [string] Whether to use VisLab's functions. Default 'off' 17 | % Outputs: 18 | % EEG - [struct] the EEG structure with event info imported 19 | % bids - [struct] structure that saves BIDS information with event information 20 | % eventData - [cell array] imported data from events.tsv 21 | % eventDesc - [struct] imported data from events.json 22 | % 23 | % Authors: Dung Truong, Arnaud Delorme, 2022 24 | 25 | function [EEG, bids, eventData, eventDesc] = bids_importeventfile(EEG, eventfile, varargin) 26 | g = finputcheck(varargin, {'eventDescFile' 'string' [] ''; 27 | 'bids' 'struct' [] struct([]); 28 | 'eventtype' 'string' [] 'value' ; 29 | 'usevislab' 'string' { 'on' 'off'} 'off' }, 'eeg_importeventsfiles', 'ignore'); 30 | if isstr(g), error(g); end 31 | 32 | % use Kay's old implementation 33 | if strcmpi(g.usevislab, 'on') 34 | [EEG, bids, eventData, eventDesc] = eeg_importeventsfiles(EEG, eventfile, varargin{:}); %change to Kay's function 35 | return; 36 | end 37 | 38 | bids = g.bids; 39 | 40 | % --------- 41 | % load files 42 | eventData = bids_loadfile( eventfile, ''); 43 | eventDesc = bids_importjson(g.eventDescFile, '_events.json'); 44 | 45 | % ---------- 46 | % event data 47 | bids(1).eventInfo = {}; % for eInfo. Default is empty. If replacing EEG.event with events.tsv, match field names accordingly 48 | if isempty(eventData) 49 | warning('No data found in events.tsv'); 50 | else 51 | events = struct([]); 52 | indSample = strmatch('sample', lower(eventData(1,:)), 'exact'); 53 | indType = strmatch('type', lower(eventData(1,:)), 'exact'); 54 | indTrialType = strmatch('trial_type', lower(eventData(1,:)), 'exact'); 55 | if ~isempty(indType) && isempty(indTrialType) 56 | eventData(1,indType) = { 'trial_type' }; % renaming type as trial_type because erased below 57 | end 58 | 59 | for iField = 1:length(eventData(1,:)) 60 | eventData{1,iField} = cleanvarname(eventData{1,iField}); 61 | end 62 | indTrial = strmatch( g.eventtype, lower(eventData(1,:)), 'exact'); 63 | for iEvent = 2:size(eventData,1) 64 | events(end+1).latency = eventData{iEvent,1}*EEG.srate+1; % convert to samples 65 | if EEG.trials > 1 66 | events(end).epoch = floor(events(end).latency/EEG.pnts)+1; 67 | end 68 | events(end).duration = eventData{iEvent,2}*EEG.srate; % convert to samples 69 | bids.eventInfo = {'onset' 'latency'; 'duration' 'duration'}; % order in events.tsv: onset duration 70 | if ~isempty(indSample) 71 | events(end).sample = eventData{iEvent,indSample} + 1; 72 | bids.eventInfo(end+1,:) = {'sample' 'sample'}; 73 | end 74 | for iField = 1:length(eventData(1,:)) 75 | if ~any(strcmpi(eventData{1,iField}, {'onset', 'duration', 'sample', g.eventtype})) 76 | events(end).(eventData{1,iField}) = eventData{iEvent,iField}; 77 | bids.eventInfo(end+1,:) = { eventData{1,iField} eventData{1,iField} }; 78 | end 79 | end 80 | if ~isempty(indTrial) 81 | events(end).type = eventData{iEvent,indTrial}; 82 | bids.eventInfo(end+1,:) = { g.eventtype 'type' }; 83 | end 84 | % if size(eventData,2) > 3 && strcmpi(eventData{1,4}, 'response_time') && ~strcmpi(eventData{iEvent,4}, 'n/a') 85 | % events(end+1).type = 'response'; 86 | % events(end).latency = (eventData{iEvent,1}+eventData{iEvent,4})*EEG.srate+1; % convert to samples 87 | % events(end).duration = 0; 88 | % end 89 | end 90 | EEG.event = events; 91 | EEG = eeg_checkset(EEG, 'makeur'); % add urevent 92 | EEG = eeg_checkset(EEG, 'eventconsistency'); 93 | 94 | % save BIDS information in raw form 95 | bids.eInfoDesc = eventDesc; 96 | EEG.BIDS = bids; 97 | end 98 | 99 | function nameout = cleanvarname(namein) 100 | 101 | % Routine to remove unallowed characters from strings 102 | % nameout can be use as a variable or field in a structure 103 | 104 | % custom change 105 | if strcmp(namein,'#') 106 | namein = 'nb'; 107 | end 108 | 109 | % 1st char must be a letter 110 | for l=1:length(namein) 111 | if isletter(namein(l)) 112 | nameout = namein(l:end); 113 | break % exist for loop 114 | end 115 | end 116 | 117 | % remove usual suspects 118 | stringcheck = [strfind(namein,'('), ... 119 | strfind(namein,')') ... 120 | strfind(namein,'[') ... 121 | strfind(namein,']') ... 122 | strfind(namein,'{') ... 123 | strfind(namein,'}') ... 124 | strfind(namein,'-') ... 125 | strfind(namein,'+') ... 126 | strfind(namein,'*') ... 127 | strfind(namein,'/') ... 128 | strfind(namein,'#') ... 129 | strfind(namein,'%') ... 130 | strfind(namein,'&') ... 131 | strfind(namein,'@') ... 132 | ]; 133 | 134 | if ~isempty(stringcheck) 135 | nameout(stringcheck) = []; 136 | end 137 | 138 | % last check 139 | if ~isvarname(nameout) 140 | nameout(1) = upper(nameout(1)); 141 | if ~isvarname(nameout) 142 | error('the variable name to use is still invalid, check chars to remove') 143 | end 144 | end -------------------------------------------------------------------------------- /bids_importjson.m: -------------------------------------------------------------------------------- 1 | % bids_importjson - Import json file following BIDS key-based inheritance rule 2 | % (https://bids-specification.readthedocs.io/en/stable/common-principles.html#the-inheritance-principle) 3 | % 4 | % Usage: 5 | % curjsondata = bids_importjson(curFile, ext) 6 | % 7 | % Inputs: 8 | % curFile - [string] full path to the current json file 9 | % ext - [string] BIDS post fix extension of the file. 10 | % e.g. _events.json 11 | % 12 | % Outputs: 13 | % curjsondata - [struct] json data imported as matlab structure 14 | % 15 | % Author: Dung Truong, 2024 16 | function curjsondata = bids_importjson(curFile, ext, oriFile) 17 | if nargin < 1 18 | help bids_importjson 19 | return 20 | end 21 | if nargin < 2 22 | % no extension is provided. Default to using the filename 23 | splitted = strsplit(curFile, '_'); 24 | ext = ['_' splitted{end}]; 25 | end 26 | if nargin < 3 27 | % no extension is provided. Default to using the filename 28 | oriFile = curFile; 29 | end 30 | % resolve wildcard if applicable 31 | curFileDir = dir(curFile); 32 | if ~isempty(curFileDir) 33 | curFile = fullfile(curFileDir(1).folder, curFileDir(1).name); 34 | end 35 | if ~exist(curFile, 'file') || isempty(curFile) 36 | curjsondata = struct([]); 37 | else 38 | curjsondata = readjson(curFile); 39 | end 40 | if ~isTopLevel(curFile) 41 | upperFile = fullfile(fileparts(fileparts(curFile)), ['*' ext]); 42 | % resolve wildcard if applicable 43 | upperFileDir = dir(upperFile); 44 | if ~isempty(upperFileDir) 45 | for u=1:numel(upperFileDir) 46 | % check using rule 2.b and 2.c 47 | % of https://bids-specification.readthedocs.io/en/stable/common-principles.html#the-inheritance-principle 48 | upperFileName = upperFileDir(u).name; 49 | upperFileName_parts = strsplit(upperFileName, '_'); 50 | if contains(oriFile, upperFileName_parts) 51 | upperFile = fullfile(upperFileDir(u).folder, upperFileDir(u).name); 52 | break 53 | end 54 | end 55 | end 56 | upperjsondata = bids_importjson(upperFile, ext, oriFile); 57 | 58 | % mergeStructures credit: https://www.mathworks.com/matlabcentral/fileexchange/131718-mergestructures-merge-or-concatenate-nested-structures?s_tid=mwa_osa_a 59 | curjsondata = mergeStructures(curjsondata, upperjsondata); 60 | end 61 | 62 | function res = readjson(file) 63 | if exist('jsondecode.m','file') 64 | res = jsondecode( importalltxt( file )); 65 | else 66 | res = jsonread(file); 67 | end 68 | end 69 | % Import full text file 70 | % --------------------- 71 | function str = importalltxt(fileName) 72 | str = []; 73 | fid =fopen(fileName, 'r'); 74 | while ~feof(fid) 75 | str = [str 10 fgetl(fid) ]; 76 | end 77 | str(1) = []; 78 | end 79 | 80 | function res = isTopLevel(curfile) 81 | res = true; 82 | if ~isempty(curfile) 83 | curpath = fileparts(curfile); 84 | files = dir(curpath); 85 | if ~isempty(files) 86 | for f=1:numel(files) 87 | if ~files(f).isdir && (strcmp(files(f).name, 'README') || strcmp(files(f).name, 'dataset_description.json')) 88 | res = true; 89 | return 90 | end 91 | end 92 | end 93 | res = false; 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /bids_loadfile.m: -------------------------------------------------------------------------------- 1 | % BIDS_LOADFILE - import JSON or TSV file 2 | % 3 | % Usage: 4 | % bids_loadfile(localFile, globalFile) 5 | % 6 | % Input: 7 | % localFile - Name of the localfile 8 | % globalFile - Name of the global file if the local file cannot be 9 | % found 10 | 11 | function data = bids_loadfile(localFile, globalFile, rawFlag) 12 | [~,~,ext] = fileparts(localFile); 13 | data = []; 14 | localFile = dir(localFile); 15 | if nargin > 2 16 | if ischar(localFile) 17 | data = importalltxt( localFile ); 18 | else 19 | data = importalltxt( fullfile(localFile(1).folder, localFile(1).name) ); 20 | end 21 | return 22 | end 23 | 24 | if ~isempty(localFile) 25 | if strcmpi(ext, '.tsv') 26 | data = importtsv( fullfile(localFile(1).folder, localFile(1).name)); 27 | else 28 | if exist('jsondecode.m','file') 29 | data = jsondecode( importalltxt( fullfile(localFile(1).folder, localFile(1).name) )); 30 | else 31 | data = jsonread(fullfile(localFile(1).folder, localFile(1).name)); 32 | end 33 | end 34 | elseif nargin > 1 && ~isempty(globalFile) 35 | if strcmpi(ext, '.tsv') 36 | data = importtsv( fullfile(globalFile(1).folder, globalFile(1).name)); 37 | else 38 | if exist('jsondecode.m','file') 39 | data = jsondecode( importalltxt( fullfile(globalFile(1).folder, globalFile(1).name) )); 40 | else 41 | data = jsonread(fullfile(globalFile(1).folder, globalFile(1).name)); 42 | end 43 | end 44 | end 45 | 46 | 47 | % Import full text file 48 | % --------------------- 49 | function str = importalltxt(fileName) 50 | 51 | str = []; 52 | fid =fopen(fileName, 'r'); 53 | while ~feof(fid) 54 | str = [str 10 fgetl(fid) ]; 55 | end 56 | str(1) = []; 57 | end 58 | 59 | % Import tsv file 60 | % --------------- 61 | function res = importtsv( fileName) 62 | 63 | res = loadtxt( fileName, 'verbose', 'off', 'delim', [ 9 ]); 64 | 65 | for iCol = 1:size(res,2) 66 | % search for NaNs in numerical array 67 | indNaNs = cellfun(@(x)strcmpi('n/a', x), res(:,iCol)); 68 | if ~isempty(indNaNs) 69 | allNonNaNVals = res(find(~indNaNs),iCol); 70 | allNonNaNVals(1) = []; % header 71 | testNumeric = cellfun(@isnumeric, allNonNaNVals); 72 | if all(testNumeric) 73 | res(find(indNaNs),iCol) = { NaN }; 74 | elseif ~all(~testNumeric) 75 | % Convert numerical value back to string 76 | res(:,iCol) = cellfun(@num2str, res(:,iCol), 'uniformoutput', false); 77 | end 78 | end 79 | end 80 | end 81 | end -------------------------------------------------------------------------------- /bids_matlab_tools_ver.m: -------------------------------------------------------------------------------- 1 | % version of BIDS-matlab-tools 2 | 3 | function str = bids_matlab_tools_ver 4 | 5 | str = '10.2'; 6 | -------------------------------------------------------------------------------- /bids_metadata_stats.m: -------------------------------------------------------------------------------- 1 | % BIDS_METADATA_STATS - Check the BIDS metadata content 2 | % 3 | % Usage: 4 | % >> stats = bids_metadata_stats(bids); 5 | % 6 | % Inputs: 7 | % bids - [struct] BIDS structure (for example ALLEEG(1).BIDS) 8 | % 9 | % Outputs: 10 | % stats - BIDS metadata statistics structure 11 | % 12 | % Authors: Arnaud Delorme and Dung Truong, SCCN, INC, UCSD, February, 2024 13 | 14 | % Copyright (C) Arnaud Delorme, 2024 15 | % 16 | % This program is free software; you can redistribute it and/or modify 17 | % it under the terms of the GNU General Public License as published by 18 | % the Free Software Foundation; either version 2 of the License, or 19 | % (at your option) any later version. 20 | % 21 | % This program is distributed in the hope that it will be useful, 22 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | % GNU General Public License for more details. 25 | % 26 | % You should have received a copy of the GNU General Public License 27 | % along with this program; if not, to the Free Software 28 | % Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 29 | 30 | function stats = bids_metadata_stats(bids, inconsistentChannels) 31 | 32 | if nargin < 2 33 | help bids_metadata_stats; 34 | end 35 | inconsistentEvents = 0; 36 | if nargin < 3 37 | inconsistentChannels = 0; 38 | end 39 | 40 | % compute basic statistics 41 | stats.README = 0; 42 | stats.EthicsApprovals = 0; 43 | stats.TaskDescription = 0; 44 | stats.Instructions = 0; 45 | stats.EEGReference = 0; 46 | stats.PowerLineFrequency = 0; 47 | stats.ChannelTypes = 0; 48 | stats.ElectrodePositions = 0; 49 | stats.ParticipantsAgeAndGender = 0; 50 | stats.SubjectArtefactDescription = 0; 51 | stats.EventConsistency = 0; 52 | stats.ChannelConsistency = 0; 53 | stats.EventDescription = 0; 54 | stats.StandardChannelLabels = 0; 55 | if ~isempty(bids.README), stats.README = 1; end 56 | if isfield(bids.dataset_description, 'EthicsApprovals') stats.EthicsApprovals = 1; end 57 | if ismember('age', lower(bids.participants(1,:))) 58 | if ismember('gender', lower(bids.participants(1,:))) || ismember('sex', lower(bids.participants(1,:))) 59 | stats.ParticipantsAgeAndGender = 1; 60 | end 61 | end 62 | if checkBIDSfield(bids, 'TaskDescription'), stats.TaskDescription = 1; end 63 | if checkBIDSfield(bids, 'Instructions'), stats.Instructions = 1; end 64 | if checkBIDSfield(bids, 'EEGReference'), stats.EEGReference = 1; end 65 | if checkBIDSfield(bids, 'PowerLineFrequency'), stats.PowerLineFrequency = 1; end 66 | if isfield(bids.data, 'chaninfo') && ~isempty(bids.data(1).chaninfo) && ~isempty(strmatch('type', lower(bids.data(1).chaninfo(1,:)), 'exact')) 67 | stats.ChannelTypes = 1; 68 | end 69 | if checkBIDSfield(bids, 'elecinfo'), stats.ElectrodePositions = 1; end 70 | if checkBIDSfield(bids, 'eventdesc'), stats.EventDescription = 1; end 71 | if checkBIDSfield(bids, 'SubjectArtefactDescription'), stats.SubjectArtefactDescription = 1; end 72 | 73 | stats.ChannelConsistency = fastif(inconsistentChannels > 0, 0, 1); 74 | stats.EventConsistency = fastif(inconsistentEvents > 0, 0, 1); 75 | if isfield(bids.data, 'chaninfo') && size(bids.data(1).chaninfo, 1) > 1 76 | chanLabels = bids.data(1).chaninfo(2:end,1); 77 | standardLabels = { 'Fp1' 'Fpz' 'Fp2' 'AF9' 'AF7' 'AF5' 'AF3' 'AF1' 'AFz' 'AF2' 'AF4' 'AF6' 'AF8' 'AF10' 'F9' 'F7' 'F5' 'F3' 'F1' 'Fz' 'F2' 'F4' 'F6' 'F8' 'F10' 'FT9' 'FT7' 'FC5' 'FC3' 'FC1' 'FCz' 'FC2' 'FC4' 'FC6' 'FT8' 'FT10' 'T9' 'T7' 'C5' 'C3' 'C1' 'Cz' 'C2' 'C4' 'C6' 'T8' 'T10' 'TP9' 'TP7' 'CP5' 'CP3' 'CP1' 'CPz' 'CP2' 'CP4' 'CP6' 'TP8' 'TP10' 'P9' 'P7' 'P5' 'P3' 'P1' 'Pz' 'P2' 'P4' 'P6' 'P8' 'P10' 'PO9' 'PO7' 'PO5' 'PO3' 'PO1' 'POz' 'PO2' 'PO4' 'PO6' 'PO8' 'PO10' 'O1' 'Oz' 'O2' 'I1' 'Iz' 'I2' 'AFp9h' 'AFp7h' 'AFp5h' 'AFp3h' 'AFp1h' 'AFp2h' 'AFp4h' 'AFp6h' 'AFp8h' 'AFp10h' 'AFF9h' 'AFF7h' 'AFF5h' 'AFF3h' 'AFF1h' 'AFF2h' 'AFF4h' 'AFF6h' 'AFF8h' 'AFF10h' 'FFT9h' 'FFT7h' 'FFC5h' 'FFC3h' 'FFC1h' 'FFC2h' 'FFC4h' 'FFC6h' 'FFT8h' 'FFT10h' 'FTT9h' 'FTT7h' 'FCC5h' 'FCC3h' 'FCC1h' 'FCC2h' 'FCC4h' 'FCC6h' 'FTT8h' 'FTT10h' 'TTP9h' 'TTP7h' 'CCP5h' 'CCP3h' 'CCP1h' 'CCP2h' 'CCP4h' 'CCP6h' 'TTP8h' 'TTP10h' 'TPP9h' 'TPP7h' 'CPP5h' 'CPP3h' 'CPP1h' 'CPP2h' 'CPP4h' 'CPP6h' 'TPP8h' 'TPP10h' 'PPO9h' 'PPO7h' 'PPO5h' 'PPO3h' 'PPO1h' 'PPO2h' 'PPO4h' 'PPO6h' 'PPO8h' 'PPO10h' 'POO9h' 'POO7h' 'POO5h' 'POO3h' 'POO1h' 'POO2h' 'POO4h' 'POO6h' 'POO8h' 'POO10h' 'OI1h' 'OI2h' 'Fp1h' 'Fp2h' 'AF9h' 'AF7h' 'AF5h' 'AF3h' 'AF1h' 'AF2h' 'AF4h' 'AF6h' 'AF8h' 'AF10h' 'F9h' 'F7h' 'F5h' 'F3h' 'F1h' 'F2h' 'F4h' 'F6h' 'F8h' 'F10h' 'FT9h' 'FT7h' 'FC5h' 'FC3h' 'FC1h' 'FC2h' 'FC4h' 'FC6h' 'FT8h' 'FT10h' 'T9h' 'T7h' 'C5h' 'C3h' 'C1h' 'C2h' 'C4h' 'C6h' 'T8h' 'T10h' 'TP9h' 'TP7h' 'CP5h' 'CP3h' 'CP1h' 'CP2h' 'CP4h' 'CP6h' 'TP8h' 'TP10h' 'P9h' 'P7h' 'P5h' 'P3h' 'P1h' 'P2h' 'P4h' 'P6h' 'P8h' 'P10h' 'PO9h' 'PO7h' 'PO5h' 'PO3h' 'PO1h' 'PO2h' 'PO4h' 'PO6h' 'PO8h' 'PO10h' 'O1h' 'O2h' 'I1h' 'I2h' 'AFp9' 'AFp7' 'AFp5' 'AFp3' 'AFp1' 'AFpz' 'AFp2' 'AFp4' 'AFp6' 'AFp8' 'AFp10' 'AFF9' 'AFF7' 'AFF5' 'AFF3' 'AFF1' 'AFFz' 'AFF2' 'AFF4' 'AFF6' 'AFF8' 'AFF10' 'FFT9' 'FFT7' 'FFC5' 'FFC3' 'FFC1' 'FFCz' 'FFC2' 'FFC4' 'FFC6' 'FFT8' 'FFT10' 'FTT9' 'FTT7' 'FCC5' 'FCC3' 'FCC1' 'FCCz' 'FCC2' 'FCC4' 'FCC6' 'FTT8' 'FTT10' 'TTP9' 'TTP7' 'CCP5' 'CCP3' 'CCP1' 'CCPz' 'CCP2' 'CCP4' 'CCP6' 'TTP8' 'TTP10' 'TPP9' 'TPP7' 'CPP5' 'CPP3' 'CPP1' 'CPPz' 'CPP2' 'CPP4' 'CPP6' 'TPP8' 'TPP10' 'PPO9' 'PPO7' 'PPO5' 'PPO3' 'PPO1' 'PPOz' 'PPO2' 'PPO4' 'PPO6' 'PPO8' 'PPO10' 'POO9' 'POO7' 'POO5' 'POO3' 'POO1' 'POOz' 'POO2' 'POO4' 'POO6' 'POO8' 'POO10' 'OI1' 'OIz' 'OI2' 'T3' 'T5' 'T4' 'T6' 'M1' 'M2' 'A1' 'A2' 'O9' 'O10' }; 78 | if ischar(chanLabels{1}) && length(intersect(lower(chanLabels), lower(standardLabels))) > length(chanLabels)/2 79 | stats.StandardChannelLabels = 1; 80 | end 81 | end 82 | 83 | % check BIDS data field present 84 | % ----------------------------- 85 | function res = checkBIDSfield(bids, fieldName) 86 | res = false; 87 | if isfield(bids.data, fieldName) 88 | fieldContent = { bids.data.(fieldName) }; 89 | fieldContent(cellfun(@isempty, fieldContent)) = []; 90 | if ~isempty(fieldContent), res = true; end 91 | end 92 | -------------------------------------------------------------------------------- /bids_template_README.m: -------------------------------------------------------------------------------- 1 | % bids_template_README() - Create template README file 2 | % from https://github.com/bids-standard/bids-starter-kit/tree/main/templates 3 | % and adopted from FieldTrip implementation 4 | % Usage: 5 | % strout = bids_template_README; 6 | % 7 | % Authors: Dung Truong, SCCN, INC, UCSD, April, 2024 8 | 9 | % Copyright (C) Dung Truong, 2024 10 | % 11 | % This program is free software; you can redistribute it and/or modify 12 | % it under the terms of the GNU General Public License as published by 13 | % the Free Software Foundation; either version 2 of the License, or 14 | % (at your option) any later version. 15 | % 16 | % This program is distributed in the hope that it will be useful, 17 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | % GNU General Public License for more details. 20 | % 21 | % You should have received a copy of the GNU General Public License 22 | % along with this program; if not, write to the Free Software 23 | % Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | function strout = bids_template_README() 25 | strout = sprintf(['# README' ... 26 | '\n' ... 27 | '# WARNING' ... 28 | '\n' ... 29 | 'Below is a template to write a README file for this BIDS dataset. If this message is still present, ' ... 30 | 'it means that the person exporting the file has decided not to update the template.' ... 31 | 'If you are the researcher editing this README file, please remove this warning section.' ... 32 | '\n' ... 33 | 'The README is usually the starting point for researchers using your data' ... 34 | 'and serves as a guidepost for users of your data. A clear and informative' ... 35 | 'README makes your data much more usable.' ... 36 | '\n' ... 37 | 'In general you can include information in the README that is not captured by some other' ... 38 | 'files in the BIDS dataset (dataset_description.json, events.tsv, ...).' ... 39 | '\n' ... 40 | 'It can also be useful to also include information that might already be' ... 41 | 'present in another file of the dataset but might be important for users to be aware of' ... 42 | 'before preprocessing or analysing the data.' ... 43 | '\n' ... 44 | 'If the README gets too long you have the possibility to create a `/doc` folder' ... 45 | 'and add it to the `.bidsignore` file to make sure it is ignored by the BIDS validator.' ... 46 | '\n' ... 47 | 'More info here: https://neurostars.org/t/where-in-a-bids-dataset-should-i-put-notes-about-individual-mri-acqusitions/17315/3' ... 48 | '\n' ... 49 | '## Details related to access to the data' ... 50 | '\n' ... 51 | '- [ ] Data user agreement' ... 52 | '\n' ... 53 | 'If the dataset requires a data user agreement, link to the relevant information.' ... 54 | '\n' ... 55 | '- [ ] Contact person' ... 56 | '\n' ... 57 | 'Indicate the name and contact details (email and ORCID) of the person responsible for additional information.' ... 58 | '\n' ... 59 | '- [ ] Practical information to access the data' ... 60 | '\n' ... 61 | 'If there is any special information related to access rights or' ... 62 | 'how to download the data make sure to include it.' ... 63 | 'For example, if the dataset was curated using datalad,' ... 64 | 'make sure to include the relevant section from the datalad handbook:' ... 65 | 'http://handbook.datalad.org/en/latest/basics/101-180-FAQ.html#how-can-i-help-others-get-started-with-a-shared-dataset' ... 66 | '\n' ... 67 | '## Overview' ... 68 | '\n' ... 69 | '- [ ] Project name (if relevant)' ... 70 | '\n' ... 71 | '- [ ] Year(s) that the project ran' ... 72 | '\n' ... 73 | 'If no `scans.tsv` is included, this could at least cover when the data acquisition' ... 74 | 'starter and ended. Local time of day is particularly relevant to subject state.' ... 75 | '\n' ... 76 | '- [ ] Brief overview of the tasks in the experiment' ... 77 | '\n' ... 78 | 'A paragraph giving an overview of the experiment. This should include the' ... 79 | 'goals or purpose and a discussion about how the experiment tries to achieve' ... 80 | 'these goals.' ... 81 | '\n' ... 82 | '- [ ] Description of the contents of the dataset' ... 83 | '\n' ... 84 | 'An easy thing to add is the output of the bids-validator that describes what type of' ... 85 | 'data and the number of subject one can expect to find in the dataset.' ... 86 | '\n' ... 87 | '- [ ] Independent variables' ... 88 | '\n' ... 89 | 'A brief discussion of condition variables (sometimes called contrasts' ... 90 | 'or independent variables) that were varied across the experiment.' ... 91 | '\n' ... 92 | '- [ ] Dependent variables' ... 93 | '\n' ... 94 | 'A brief discussion of the response variables (sometimes called the' ... 95 | 'dependent variables) that were measured and or calculated to assess' ... 96 | 'the effects of varying the condition variables. This might also include' ... 97 | 'questionnaires administered to assess behavioral aspects of the experiment.' ... 98 | '\n' ... 99 | '- [ ] Control variables' ... 100 | '\n' ... 101 | 'A brief discussion of the control variables --- that is what aspects' ... 102 | 'were explicitly controlled in this experiment. The control variables might' ... 103 | 'include subject pool, environmental conditions, set up, or other things' ... 104 | 'that were explicitly controlled.' ... 105 | '\n' ... 106 | '- [ ] Quality assessment of the data' ... 107 | '\n' ... 108 | 'Provide a short summary of the quality of the data ideally with descriptive statistics if relevant' ... 109 | 'and with a link to more comprehensive description (like with MRIQC) if possible.' ... 110 | '\n' ... 111 | '## Methods' ... 112 | '\n' ... 113 | '### Subjects' ... 114 | '\n' ... 115 | 'A brief sentence about the subject pool in this experiment.' ... 116 | '\n' ... 117 | 'Remember that `Control` or `Patient` status should be defined in the `participants.tsv`' ... 118 | 'using a group column.' ... 119 | '\n' ... 120 | '- [ ] Information about the recruitment procedure' ... 121 | '- [ ] Subject inclusion criteria (if relevant)' ... 122 | '- [ ] Subject exclusion criteria (if relevant)' ... 123 | '\n' ... 124 | '### Apparatus' ... 125 | '\n' ... 126 | 'A summary of the equipment and environment setup for the' ... 127 | 'experiment. For example, was the experiment performed in a shielded room' ... 128 | 'with the subject seated in a fixed position.' ... 129 | '\n' ... 130 | '### Initial setup' ... 131 | '\n' ... 132 | 'A summary of what setup was performed when a subject arrived.' ... 133 | '\n' ... 134 | '### Task organization' ... 135 | '\n' ... 136 | 'How the tasks were organized for a session.' ... 137 | 'This is particularly important because BIDS datasets usually have task data' ... 138 | 'separated into different files.)' ... 139 | '\n' ... 140 | '- [ ] Was task order counter-balanced?' ... 141 | '- [ ] What other activities were interspersed between tasks?' ... 142 | '\n' ... 143 | '- [ ] In what order were the tasks and other activities performed?' ... 144 | '\n' ... 145 | '### Task details' ... 146 | '\n' ... 147 | 'As much detail as possible about the task and the events that were recorded.' ... 148 | '\n' ... 149 | '### Additional data acquired' ... 150 | '\n' ... 151 | 'A brief indication of data other than the' ... 152 | 'imaging data that was acquired as part of this experiment. In addition' ... 153 | 'to data from other modalities and behavioral data, this might include' ... 154 | 'questionnaires and surveys, swabs, and clinical information. Indicate' ... 155 | 'the availability of this data.' ... 156 | '\n' ... 157 | 'This is especially relevant if the data are not included in a `phenotype` folder.' ... 158 | 'https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#phenotypic-and-assessment-data' ... 159 | '\n' ... 160 | '### Experimental location' ... 161 | '\n' ... 162 | 'This should include any additional information regarding the' ... 163 | 'the geographical location and facility that cannot be included' ... 164 | 'in the relevant json files.' ... 165 | '\n' ... 166 | '### Missing data' ... 167 | '\n' ... 168 | 'Mention something if some participants are missing some aspects of the data.' ... 169 | 'This can take the form of a processing log and/or abnormalities about the dataset.' ... 170 | '\n' ... 171 | 'Some examples:' ... 172 | '\n' ... 173 | '- A brain lesion or defect only present in one participant' ... 174 | '- Some experimental conditions missing on a given run for a participant because' ... 175 | ' of some technical issue.' ... 176 | '- Any noticeable feature of the data for certain participants' ... 177 | '- Differences (even slight) in protocol for certain participants.' ... 178 | '\n' ... 179 | '### Notes' ... 180 | '\n' ... 181 | 'Any additional information or pointers to information that' ... 182 | 'might be helpful to users of the dataset. Include qualitative information' ... 183 | 'related to how the data acquisition went.' ... 184 | '\n' ... 185 | ]); ... 186 | -------------------------------------------------------------------------------- /bids_writebehfile.m: -------------------------------------------------------------------------------- 1 | % BIDS_WRITEBEHFILE - write behavioral file 2 | % 3 | % Usage: 4 | % bids_writebehfile(beh, fileOut) 5 | % 6 | % 7 | % Inputs: 8 | % beh - [struct] structure containing behavioral data (event 9 | % structure for example) 10 | % behinfo - [struct] column names and description (optional) 11 | % fileOut - [string] filepath of the desired output location with file basename 12 | % e.g. ~/BIDS_EXPORT/sub-01/ses-01/eeg/sub-01_ses-01_task-GoNogo 13 | % 14 | % Authors: Arnaud Delorme, 2022 15 | 16 | function bids_writebehfile(beh, behinfo, fileOut) 17 | 18 | if isempty(beh) 19 | return; 20 | end 21 | 22 | if ~contains(fileOut, '_beh.tsv') 23 | fileOut = [ fileOut '_beh.tsv']; 24 | end 25 | 26 | folder = fileparts(fileOut); 27 | if ~exist(folder) 28 | mkdir(folder); 29 | end 30 | fid = fopen( fileOut, 'w'); 31 | if fid == -1 32 | error('Cannot open behavioral file'); 33 | end 34 | 35 | fields = fieldnames(beh); 36 | for iField = 1:length(fields) 37 | fprintf(fid, '%s', fields{iField} ); 38 | if iField < length(fields), fprintf(fid, '\t'); end 39 | end 40 | fprintf(fid, '\n'); 41 | 42 | for iRow = 1:length(beh) 43 | for iField = 1:length(fields) 44 | if isempty(beh(iRow).(fields{iField})) || any(isnan(beh(iRow).(fields{iField}))) 45 | fprintf(fid, 'n/a' ); 46 | else 47 | if ischar(beh(iRow).(fields{iField})) 48 | fprintf(fid, '%s', beh(iRow).(fields{iField}) ); 49 | else 50 | fprintf(fid, '%1.4f', beh(iRow).(fields{iField}) ); 51 | end 52 | end 53 | if iField < length(fields), fprintf(fid, '\t'); end 54 | end 55 | fprintf(fid, '\n'); 56 | end 57 | fclose(fid); 58 | 59 | if ~isempty(behinfo) 60 | jsonwrite([fileOut(1:end-4) '.json'], behinfo, struct('indent',' ')); 61 | end 62 | 63 | -------------------------------------------------------------------------------- /bids_writechanfile.m: -------------------------------------------------------------------------------- 1 | % eeg_writechanfile - write channels.tsv from single EEG dataset. The 2 | % function also outputs channels count 3 | % 4 | % Usage: 5 | % channelsCount = eeg_writechanfile(EEG, fileOut) 6 | % 7 | % 8 | % 9 | % Inputs: 10 | % 'EEG' - [struct] the EEG structure 11 | % 12 | % 'fileOut' - [string] filepath of the desired output location with file basename 13 | % e.g. ~/BIDS_EXPORT/sub-01/ses-01/eeg/sub-01_ses-01_task-GoNogo 14 | % 15 | % Outputs: 16 | % 17 | % channelsCount - [struct] count of different types of channels 18 | % 19 | % Authors: Dung Truong, Arnaud Delorme, 2022 20 | 21 | function bids_writechanfile(EEG, fileOut) 22 | 23 | fid = fopen( [ fileOut '_channels.tsv' ], 'w'); 24 | if isempty(EEG.chanlocs) 25 | if contains(fileOut, 'ieeg') 26 | fprintf(fid, 'name\ttype\tunits\tlow_cutoff\thigh_cutoff\n'); 27 | for iChan = 1:EEG.nbchan 28 | fprintf(fid, 'E%d\tiEEG\tmicroV\tn/a\tn/a\n', iChan); 29 | end 30 | else 31 | fprintf(fid, 'name\ttype\tunits\n'); 32 | for iChan = 1:EEG.nbchan 33 | fprintf(fid, 'E%d\tEEG\tmicroV\n', iChan); 34 | end 35 | end 36 | channelsCount = struct([]); 37 | else 38 | if contains(fileOut, 'ieeg') 39 | fprintf(fid, 'name\ttype\tunits\tlow_cutoff\thigh_cutoff\n'); 40 | else 41 | fprintf(fid, 'name\ttype\tunits\n'); 42 | end 43 | acceptedChannelTypes = { 'AUDIO' 'EEG' 'EOG' 'ECG' 'EMG' 'EYEGAZE' 'GSR' 'HEOG' 'MISC' 'PUPIL' 'REF' 'RESP' 'SYSCLOCK' 'TEMP' 'TRIG' 'VEOG' }; 44 | for iChan = 1:EEG.nbchan 45 | % Type 46 | if ~isfield(EEG.chanlocs, 'type') || isempty(EEG.chanlocs(iChan).type) 47 | type = 'n/a'; 48 | elseif ismember(upper(EEG.chanlocs(iChan).type), acceptedChannelTypes) 49 | type = upper(EEG.chanlocs(iChan).type); 50 | else 51 | type = 'MISC'; 52 | end 53 | % Unit 54 | if isfield(EEG.chanlocs(iChan), 'unit') 55 | unit = EEG.chanlocs(iChan).unit; 56 | else 57 | if strcmpi(type, 'eeg') 58 | unit = 'uV'; 59 | else 60 | unit = 'n/a'; 61 | end 62 | end 63 | 64 | %Write 65 | if contains(fileOut, 'ieeg') 66 | fprintf(fid, '%s\t%s\t%s\tn/a\tn/a\n', EEG.chanlocs(iChan).labels, type, unit); 67 | else 68 | fprintf(fid, '%s\t%s\t%s\n', EEG.chanlocs(iChan).labels, type, unit); 69 | end 70 | end 71 | end 72 | fclose(fid); 73 | -------------------------------------------------------------------------------- /bids_writeelectrodefile.m: -------------------------------------------------------------------------------- 1 | % BIDS_WRITEELECTRODEFILE - write electrodes.tsv and coordsystem.json from single EEG dataset 2 | % 3 | % Usage: 4 | % bids_writeelectrodefile(EEG, fileOut, flagExport) 5 | % 6 | % Inputs: 7 | % 'EEG' - [struct] the EEG structure 8 | % 'fileOut' - [string] filepath of the desired output location with file basename 9 | % e.g. ~/BIDS_EXPORT/sub-01/ses-01/eeg/sub-01_ses-01_task-GoNogo 10 | % 11 | % Optional inputs: 12 | % 'Export' - ['on'|'off'|'auto'] 13 | % 14 | % Authors: Dung Truong, Arnaud Delorme, 2022 15 | 16 | function bids_writeelectrodefile(EEG, fileOut, varargin) 17 | 18 | if nargin > 2 19 | flagExport = varargin{2}; 20 | else 21 | flagExport = 'auto'; 22 | end 23 | 24 | % remove task because a bug in v1.10.0 validator returns an error (MAYBE REMOVE THAT SECTION LATER) 25 | ind = strfind(fileOut, 'task-'); 26 | if ~isempty(ind) 27 | ind_ = find(fileOut(ind:end) == '_'); 28 | if isempty(ind_) 29 | ind_ = length(fileOut(ind:end))+1; 30 | end 31 | fileOut(ind-1:ind+ind_-2) = []; 32 | end 33 | 34 | % remove desc as well (MAYBE REMOVE THAT SECTION LATER) 35 | ind = strfind(fileOut, 'desc-'); 36 | if ~isempty(ind) 37 | ind_ = find(fileOut(ind:end) == '_'); 38 | if isempty(ind_) 39 | ind_ = length(fileOut(ind:end))+1; 40 | end 41 | fileOut(ind-1:ind+ind_-2) = []; 42 | end 43 | 44 | if isfield(EEG.chaninfo, 'filename') && isequal(flagExport, 'auto') 45 | templates = {'GSN-HydroCel-32.sfp', 'GSN65v2_0.sfp', 'GSN129.sfp', 'GSN-HydroCel-257.sfp', 'standard-10-5-cap385.elp', 'standard_1005.elc', 'standard_1005.ced'}; 46 | if any(contains(EEG.chaninfo.filename, templates)) 47 | flagExport = 'off'; 48 | disp('Template channel location detected, not exporting electrodes.tsv file'); 49 | end 50 | end 51 | 52 | if any(strcmp(flagExport, {'auto', 'on'})) && ~isempty(EEG.chanlocs) && isfield(EEG.chanlocs, 'X') && any(cellfun(@(x)~isempty(x), { EEG.chanlocs.X })) 53 | fid = fopen( [ fileOut '_electrodes.tsv' ], 'w'); 54 | fprintf(fid, 'name\tx\ty\tz\n'); 55 | 56 | for iChan = 1:EEG.nbchan 57 | if isempty(EEG.chanlocs(iChan).X) || isnan(EEG.chanlocs(iChan).X) || contains(fileOut, 'ieeg') 58 | fprintf(fid, '%s\tn/a\tn/a\tn/a\n', EEG.chanlocs(iChan).labels ); 59 | else 60 | fprintf(fid, '%s\t%2.6f\t%2.6f\t%2.6f\n', EEG.chanlocs(iChan).labels, EEG.chanlocs(iChan).X, EEG.chanlocs(iChan).Y, EEG.chanlocs(iChan).Z ); 61 | end 62 | end 63 | fclose(fid); 64 | 65 | % Write coordinate file information (coordsystem.json) 66 | if isfield(EEG.chaninfo, 'BIDS') && isfield(EEG.chaninfo.BIDS, 'EEGCoordinateUnits') 67 | coordsystemStruct.EEGCoordinateUnits = EEG.chaninfo.BIDS.EEGCoordinateUnits; 68 | else 69 | coordsystemStruct.EEGCoordinateUnits = 'mm'; 70 | end 71 | if isfield(EEG.chaninfo, 'BIDS') &&isfield(EEG.chaninfo.BIDS, 'EEGCoordinateSystem') 72 | coordsystemStruct.EEGCoordinateSystem = EEG.chaninfo.BIDS.EEGCoordinateSystem; 73 | else 74 | coordsystemStruct.EEGCoordinateSystem = 'CTF'; 75 | end 76 | if isfield(EEG.chaninfo, 'BIDS') &&isfield(EEG.chaninfo.BIDS, 'EEGCoordinateSystemDescription') 77 | coordsystemStruct.EEGCoordinateSystemDescription = EEG.chaninfo.BIDS.EEGCoordinateSystemDescription; 78 | else 79 | coordsystemStruct.EEGCoordinateSystemDescription = 'EEGLAB'; 80 | end 81 | jsonwrite( [ fileOut '_coordsystem.json' ], coordsystemStruct); 82 | end 83 | -------------------------------------------------------------------------------- /bids_writeieegtinfofile.m: -------------------------------------------------------------------------------- 1 | % BIDS_WRITETIEEGINFOFILE - write tinfo file for iEEG data 2 | % 3 | % Usage: 4 | % bids_writetinfofile(EEG, tinfo, fileOut) 5 | % 6 | % Inputs: 7 | % EEG - [struct] EEGLAB dataset information 8 | % tinfo - [struct] structure containing task information 9 | % notes - [string] notes to store along with the data info 10 | % fileOut - [string] filepath of the desired output location with file basename 11 | % e.g. ~/BIDS_EXPORT/sub-01/ses-01/eeg/sub-01_ses-01_task-GoNogo 12 | % 13 | % Authors: Arnaud Delorme, 2024 14 | 15 | function tInfo = bids_writeieegtinfofile( EEG, tInfo, notes, fileOutRed) 16 | 17 | [~,channelsCount] = eeg_getchantype(EEG); 18 | 19 | % Write task information (ieeg.json) Note: depends on channels 20 | % requiredChannelTypes: 'EEG', 'EOG', 'ECG', 'EMG', 'MISC'. Other channel 21 | % types are currently not valid output for ieeg.json. 22 | nonEmptyChannelTypes = fieldnames(channelsCount); 23 | for i=1:numel(nonEmptyChannelTypes) 24 | if strcmp(nonEmptyChannelTypes{i}, 'MISC') 25 | tInfo.('MiscChannelCount') = channelsCount.('MISC'); 26 | else 27 | tInfo.([nonEmptyChannelTypes{i} 'ChannelCount']) = channelsCount.(nonEmptyChannelTypes{i}); 28 | end 29 | end 30 | 31 | if ~isfield(tInfo, 'EEGReference') 32 | if ~ischar(EEG.ref) && numel(EEG.ref) > 1 % untested for all cases 33 | refChanLocs = EEG.chanlocs(EEG.ref); 34 | ref = join({refChanLocs.labels},','); 35 | ref = ref{1}; 36 | else 37 | ref = EEG.ref; 38 | end 39 | tInfo.iEEGReference = ref; 40 | end 41 | if EEG.trials == 1 42 | tInfo.RecordingType = 'continuous'; 43 | else 44 | tInfo.RecordingType = 'epoched'; 45 | tInfo.EpochLength = EEG.pnts/EEG.srate; 46 | end 47 | tInfo.RecordingDuration = EEG.pnts/EEG.srate; 48 | tInfo.SamplingFrequency = EEG.srate; 49 | if ~isempty(notes) 50 | tInfo.SubjectArtefactDescription = notes; 51 | end 52 | 53 | % https://bids-specification.readthedocs.io/en/stable/modality-specific-files/intracranial-electroencephalography.html 54 | tInfoFields = {... 55 | 'iEEGReference' 'REQUIRED' 'char' 'Unknown'; 56 | 'SamplingFrequency' 'REQUIRED' '' ''; 57 | 'PowerLineFrequency' 'REQUIRED' '' 'n/a'; 58 | 'SoftwareFilters' 'REQUIRED' 'struct' 'n/a'; 59 | ... 60 | 'HardwareFilters' 'RECOMMENDED' 'struct' ''; 61 | 'ElectrodeManufacturer' 'RECOMMENDED' 'struct' ''; 62 | 'ElectrodeManufacturersModelName' 'RECOMMENDED' 'struct' ''; 63 | 'ECOGChannelCount' 'RECOMMENDED' '' ''; 64 | 'SEEGChannelCount' 'RECOMMENDED' '' ''; 65 | 'EEGChannelCount' 'RECOMMENDED' '' ''; 66 | 'EOGChannelCount' 'RECOMMENDED' '' 0; 67 | 'ECGChannelCount' 'RECOMMENDED' '' 0; 68 | 'EMGChannelCount' 'RECOMMENDED' '' 0; 69 | 'MiscChannelCount' ' OPTIONAL' '' ''; 70 | 'TriggerChannelCount' 'RECOMMENDED' '' ''; % double in Bucanl's fork 71 | 'RecordingDuration' 'RECOMMENDED' '' 'n/a'; 72 | 'RecordingType' 'RECOMMENDED' 'char' ''; 73 | 'EpochLength' 'RECOMMENDED' '' 'n/a'; 74 | 'iEEGGround' 'RECOMMENDED ' 'char' ''; 75 | 'iEEGPlacementScheme' 'RECOMMENDED' 'char' ''; 76 | 'iEEGElectrodeGroups' 'RECOMMENDED' 'char' ''; 77 | 'SubjectArtefactDescription' 'OPTIONAL' 'char' ''; 78 | ... 79 | 'ElectricalStimulation' 'OPTIONAL' 'char' ''; 80 | 'ElectricalStimulationParameters' 'OPTIONAL' 'char' ''; 81 | ... 82 | 'Manufacturer' 'RECOMMENDED' 'char' ''; 83 | 'ManufacturersModelName' 'OPTIONAL' 'char' ''; 84 | 'SoftwareVersions' 'RECOMMENDED' 'char' ''; 85 | 'DeviceSerialNumber' 'RECOMMENDED' 'char' ''; 86 | ... 87 | 'TaskName' 'REQUIRED' '' ''; 88 | 'TaskDescription' 'RECOMMENDED' '' ''; 89 | 'Instructions' 'RECOMMENDED' 'char' ''; 90 | 'CogAtlasID' 'RECOMMENDED' 'char' ''; 91 | 'CogPOID' 'RECOMMENDED' 'char' ''; 92 | ... 93 | 'InstitutionName' 'RECOMMENDED' 'char' ''; 94 | 'InstitutionAddress' 'RECOMMENDED' 'char' ''; 95 | 'InstitutionalDepartmentName' ' RECOMMENDED' 'char' ''; 96 | }; 97 | tInfo = bids_checkfields(tInfo, tInfoFields, 'tInfo'); 98 | 99 | jsonwrite([fileOutRed '_ieeg.json' ], tInfo,struct('indent',' ')); -------------------------------------------------------------------------------- /bids_writemegtinfofile.m: -------------------------------------------------------------------------------- 1 | % BIDS_WRITETIEEGINFOFILE - write tinfo file for iEEG data 2 | % 3 | % Usage: 4 | % bids_writetinfofile(EEG, tinfo, fileOut) 5 | % 6 | % Inputs: 7 | % EEG - [struct] EEGLAB dataset information 8 | % tinfo - [struct] structure containing task information 9 | % notes - [string] notes to store along with the data info 10 | % fileOut - [string] filepath of the desired output location with file basename 11 | % e.g. ~/BIDS_EXPORT/sub-01/ses-01/eeg/sub-01_ses-01_task-GoNogo 12 | % 13 | % Authors: Arnaud Delorme, 2024 14 | 15 | function tInfo = bids_writemegtinfofile( EEG, tInfo, notes, fileOutRed) 16 | 17 | [~,channelsCount] = eeg_getchantype(EEG); 18 | 19 | % Write task information (meg.json) Note: depends on channels 20 | % requiredChannelTypes: 'EEG', 'EOG', 'ECG', 'EMG', 'MISC'. Other channel 21 | % types are currently not valid output for ieeg.json. 22 | nonEmptyChannelTypes = fieldnames(channelsCount); 23 | for i=1:numel(nonEmptyChannelTypes) 24 | if strcmp(nonEmptyChannelTypes{i}, 'MISC') 25 | tInfo.('MiscChannelCount') = channelsCount.('MISC'); 26 | else 27 | tInfo.([nonEmptyChannelTypes{i} 'ChannelCount']) = channelsCount.(nonEmptyChannelTypes{i}); 28 | end 29 | end 30 | 31 | if ~isfield(tInfo, 'EEGReference') 32 | if ~ischar(EEG.ref) && numel(EEG.ref) > 1 % untested for all cases 33 | refChanLocs = EEG.chanlocs(EEG.ref); 34 | ref = join({refChanLocs.labels},','); 35 | ref = ref{1}; 36 | else 37 | ref = EEG.ref; 38 | end 39 | tInfo.iEEGReference = ref; 40 | end 41 | if EEG.trials == 1 42 | tInfo.RecordingType = 'continuous'; 43 | else 44 | tInfo.RecordingType = 'epoched'; 45 | tInfo.EpochLength = EEG.pnts/EEG.srate; 46 | end 47 | tInfo.RecordingDuration = EEG.pnts/EEG.srate; 48 | tInfo.SamplingFrequency = EEG.srate; 49 | if ~isempty(notes) 50 | tInfo.SubjectArtefactDescription = notes; 51 | end 52 | 53 | tInfoFields = {... 54 | 'iEEGReference' 'REQUIRED' 'char' 'Unknown'; 55 | 'SamplingFrequency' 'REQUIRED' '' ''; 56 | 'PowerLineFrequency' 'REQUIRED' '' 'n/a'; 57 | 'DewarPosition' 'REQUIRED' '' ''; 58 | 'SoftwareFilters' 'REQUIRED' 'struct' 'n/a'; 59 | 'DigitizedLandmarks', 'REQUIRED', '' ''; 60 | 'DigitizedHeadPoints', 'REQUIRED', '' ''; 61 | ... 62 | 'MEGChannelCount' 'RECOMMENDED' '' ''; 63 | 'MEGREFChannelCount' 'RECOMMENDED' '' ''; 64 | 'EEGChannelCount' 'RECOMMENDED' '' ''; 65 | 'ECOGChannelCount' 'RECOMMENDED' '' ''; 66 | 'SEEGChannelCount' 'RECOMMENDED' '' ''; 67 | 'EOGChannelCount' 'RECOMMENDED' '' 0; 68 | 'ECGChannelCount' 'RECOMMENDED' '' 0; 69 | 'EMGChannelCount' 'RECOMMENDED' '' 0; 70 | 'MiscChannelCount' ' OPTIONAL' '' ''; 71 | 'TriggerChannelCount' 'RECOMMENDED' '' ''; % double in Bucanl's fork 72 | 'RecordingDuration' 'RECOMMENDED' '' 'n/a'; 73 | 'RecordingType' 'RECOMMENDED' 'char' ''; 74 | 'EpochLength' 'RECOMMENDED' '' 'n/a'; 75 | 'ContinuousHeadLocalization' 'RECOMMENDED' '' ''; 76 | 'HeadCoilFrequency' 'RECOMMENDED' '' ''; 77 | 'MaxMovement' 'RECOMMENDED' '' ''; 78 | 'SubjectArtefactDescription' 'RECOMMENDED' 'char' ''; 79 | 'AssociatedEmptyRoom' 'RECOMMENDED' '' ''; 80 | 'HardwareFilters' 'RECOMMENDED' 'struct' ''; 81 | ... 82 | 'ElectricalStimulation' 'OPTIONAL' '' ''; 83 | 'ElectricalStimulationParameters' 'OPTIONAL' 'char' ''; 84 | ... 85 | 'Manufacturer' 'RECOMMENDED' 'char' ''; 86 | 'ManufacturersModelName' 'RECOMMENDED' 'char' ''; 87 | 'SoftwareVersions' 'RECOMMENDED' 'char' ''; 88 | 'DeviceSerialNumber' 'RECOMMENDED' 'char' ''; 89 | ... 90 | 'TaskName' 'REQUIRED' 'char' ''; 91 | 'TaskDescription' 'RECOMMENDED' 'char' ''; 92 | 'Instructions' 'RECOMMENDED' 'char' ''; 93 | 'CogAtlasID' 'RECOMMENDED' 'char' ''; 94 | 'CogPOID' 'RECOMMENDED' 'char' ''; 95 | ... 96 | 'InstitutionName' 'RECOMMENDED' 'char' ''; 97 | 'InstitutionAddress' 'RECOMMENDED' 'char' ''; 98 | 'InstitutionalDepartmentName' 'RECOMMENDED' 'char' ''; 99 | ... 100 | 'EEGPlacementScheme' 'OPTIONAL' 'char' ''; 101 | 'CapManufacturer' ' OPTIONAL' 'char' ''; 102 | 'CapManufacturersModelName' ' OPTIONAL' 'char' ''; 103 | 'EEGReference' ' OPTIONAL' 'char' ''; 104 | }; 105 | tInfo = bids_checkfields(tInfo, tInfoFields, 'tInfo'); 106 | 107 | jsonwrite([fileOutRed '_meg.json' ], tInfo,struct('indent',' ')); -------------------------------------------------------------------------------- /bids_writetinfofile.m: -------------------------------------------------------------------------------- 1 | % BIDS_WRITETINFOFILE - write tinfo file 2 | % 3 | % Usage: 4 | % bids_writetinfofile(EEG, tinfo, fileOut) 5 | % 6 | % Inputs: 7 | % EEG - [struct] EEGLAB dataset information 8 | % tinfo - [struct] structure containing task information 9 | % notes - [string] notes to store along with the data info 10 | % fileOut - [string] filepath of the desired output location with file basename 11 | % e.g. ~/BIDS_EXPORT/sub-01/ses-01/eeg/sub-01_ses-01_task-GoNogo 12 | % 13 | % Authors: Arnaud Delorme, 2023 14 | 15 | function tInfo = bids_writetinfofile( EEG, tInfo, notes, fileOutRed) 16 | 17 | [~,channelsCount] = eeg_getchantype(EEG); 18 | 19 | % Write task information (eeg.json) Note: depends on channels 20 | % requiredChannelTypes: 'EEG', 'EOG', 'ECG', 'EMG', 'MISC'. Other channel 21 | % types are currently not valid output for eeg.json. 22 | nonEmptyChannelTypes = fieldnames(channelsCount); 23 | for i=1:numel(nonEmptyChannelTypes) 24 | if strcmp(nonEmptyChannelTypes{i}, 'MISC') 25 | tInfo.('MiscChannelCount') = channelsCount.('MISC'); 26 | else 27 | tInfo.([nonEmptyChannelTypes{i} 'ChannelCount']) = channelsCount.(nonEmptyChannelTypes{i}); 28 | end 29 | end 30 | 31 | if ~isfield(tInfo, 'EEGReference') 32 | if ~ischar(EEG.ref) && numel(EEG.ref) > 1 % untested for all cases 33 | refChanLocs = EEG.chanlocs(EEG.ref); 34 | ref = join({refChanLocs.labels},','); 35 | ref = ref{1}; 36 | else 37 | ref = EEG.ref; 38 | end 39 | tInfo.EEGReference = ref; 40 | end 41 | if EEG.trials == 1 42 | tInfo.RecordingType = 'continuous'; 43 | tInfo.RecordingDuration = EEG.pnts/EEG.srate; 44 | else 45 | tInfo.RecordingType = 'epoched'; 46 | tInfo.EpochLength = EEG.pnts/EEG.srate; 47 | tInfo.RecordingDuration = (EEG.pnts/EEG.srate)*EEG.trials; 48 | end 49 | tInfo.SamplingFrequency = EEG.srate; 50 | if ~isempty(notes) 51 | tInfo.SubjectArtefactDescription = notes; 52 | end 53 | 54 | tInfoFields = {... 55 | 'TaskName' 'REQUIRED' '' ''; 56 | 'TaskDescription' 'RECOMMENDED' '' ''; 57 | 'Instructions' 'RECOMMENDED' 'char' ''; 58 | 'CogAtlasID' 'RECOMMENDED' 'char' ''; 59 | 'CogPOID' 'RECOMMENDED' 'char' ''; 60 | 'InstitutionName' 'RECOMMENDED' 'char' ''; 61 | 'InstitutionAddress' 'RECOMMENDED' 'char' ''; 62 | 'InstitutionalDepartmentName' ' RECOMMENDED' 'char' ''; 63 | 'DeviceSerialNumber' 'RECOMMENDED' 'char' ''; 64 | 'SamplingFrequency' 'REQUIRED' '' ''; 65 | 'EEGChannelCount' 'RECOMMENDED' '' ''; 66 | 'EOGChannelCount' 'RECOMMENDED' '' 0; 67 | 'ECGChannelCount' 'RECOMMENDED' '' 0; 68 | 'EMGChannelCount' 'RECOMMENDED' '' 0; 69 | 'EEGReference' 'REQUIRED' 'char' 'Unknown'; 70 | 'PowerLineFrequency' 'REQUIRED' '' 'n/a'; 71 | 'EEGGround' 'RECOMMENDED ' 'char' ''; 72 | 'HeadCircumference' 'OPTIONAL ' '' 0; 73 | 'MiscChannelCount' ' OPTIONAL' '' ''; 74 | 'TriggerChannelCount' 'RECOMMENDED' '' ''; % double in Bucanl's fork 75 | 'EEGPlacementScheme' 'RECOMMENDED' 'char' ''; 76 | 'Manufacturer' 'RECOMMENDED' 'char' ''; 77 | 'ManufacturersModelName' 'OPTIONAL' 'char' ''; 78 | 'CapManufacturer' 'RECOMMENDED' 'char' 'Unknown'; 79 | 'CapManufacturersModelName' 'OPTIONAL' 'char' ''; 80 | 'HardwareFilters' 'OPTIONAL' 'struct' 'n/a'; 81 | 'SoftwareFilters' 'REQUIRED' 'struct' 'n/a'; 82 | 'RecordingDuration' 'RECOMMENDED' '' 'n/a'; 83 | 'RecordingType' 'RECOMMENDED' 'char' ''; 84 | 'EpochLength' 'RECOMMENDED' '' 'n/a'; 85 | 'SoftwareVersions' 'RECOMMENDED' 'char' ''; 86 | 'SubjectArtefactDescription' 'OPTIONAL' 'char' '' }; 87 | tInfo = bids_checkfields(tInfo, tInfoFields, 'tInfo'); 88 | if any(contains(tInfo.TaskName, '_')) || any(contains(tInfo.TaskName, ' ')) 89 | error('Task name cannot contain underscore or space character(s)'); 90 | end 91 | 92 | jsonwrite([fileOutRed '_eeg.json' ], tInfo,struct('indent',' ')); -------------------------------------------------------------------------------- /eeg_compare_bids.m: -------------------------------------------------------------------------------- 1 | % eeg_compare - compare EEG structures. Differences are shown on the command 2 | % line and stored in EEG.etc.compare 3 | % Usage: 4 | % [EEG, res] = eeg_compare(EEG1, EEG2); 5 | % 6 | % Input: 7 | % EEG1 - first EEGLAB structure 8 | % EEG2 - second EEGLAB structure 9 | % 10 | % Output: 11 | % EEG - modified EEG1 structure with comparison results 12 | % res - 1 if differences found, 0 if identical 13 | % EEG.etc.compare structure contains fields: 14 | % - data_changed: true if EEG data differs 15 | % - events_changed: true if events differ 16 | % - chanlocs_changed: true if channel locations differ 17 | % - other_changed: true if other fields differ 18 | % 19 | % Author: Seyed Yahya Shirazi, 2024, adapted from eeg_compare in EEGLAB.s 20 | % 21 | % See also: EEGLAB, EEGPLOT, POP_REJEPOCH 22 | 23 | % Copyright (C) 2024 Seyed Yahya Shirazi 24 | % 25 | % This file is part of EEGLAB, see http://www.eeglab.org 26 | % for the documentation and details. 27 | % 28 | % Redistribution and use in source and binary forms, with or without 29 | % modification, are permitted provided that the following conditions are met: 30 | % 31 | % 1. Redistributions of source code must retain the above copyright notice, 32 | % this list of conditions and the following disclaimer. 33 | % 34 | % 2. Redistributions in binary form must reproduce the above copyright notice, 35 | % this list of conditions and the following disclaimer in the documentation 36 | % and/or other materials provided with the distribution. 37 | % 38 | % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 39 | % AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 40 | % IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 41 | % ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 42 | % LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 43 | % CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 44 | % SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 45 | % INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 46 | % CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 47 | % ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 48 | % THE POSSIBILITY OF SUCH DAMAGE. 49 | 50 | function [EEG, res] = eeg_compare(EEG, EEG2) 51 | 52 | res = 1; 53 | 54 | % Initialize comparison structure 55 | EEG.etc.compare.data_changed = false; 56 | EEG.etc.compare.events_changed = false; 57 | EEG.etc.compare.chanlocs_changed = false; 58 | EEG.etc.compare.other_changed = false; 59 | 60 | %% Assess difference between datasets 61 | fields = fieldnames(EEG); 62 | disp('Field analysis:') 63 | for iField = 1:length(fields) 64 | if ~isfield(EEG2, fields{iField}) 65 | fprintf(2, ' Field %s missing in second dataset\n', fields{iField}); 66 | EEG.etc.compare.other_changed = true; 67 | else 68 | if ~isequaln(EEG.(fields{iField}), EEG2.(fields{iField})) 69 | if contains(fields{iField}, { 'filename' 'datfile'}) 70 | fprintf(' Field %s differs (ok, supposed to differ)\n', fields{iField}); 71 | elseif contains(fields{iField}, { 'subject' 'session' 'run' 'task'}) 72 | fprintf(2, ' Field %s differs ("%s" vs "%s")\n', fields{iField}, num2str(EEG.(fields{iField})), num2str(EEG2.(fields{iField}))) 73 | EEG.etc.compare.other_changed = true; 74 | elseif contains(fields{iField}, { 'eventdescription' 'event' }) 75 | fprintf(2, ' Field %s differs (n=%d vs n=%d)\n', fields{iField}, length(EEG.(fields{iField})), length(EEG2.(fields{iField}))) 76 | EEG.etc.compare.events_changed = true; 77 | elseif strcmp(fields{iField}, 'data') 78 | fprintf(2, ' Field data differs\n'); 79 | EEG.etc.compare.data_changed = true; 80 | else 81 | fprintf(2, ' Field %s differs\n', fields{iField}); 82 | EEG.etc.compare.other_changed = true; 83 | end 84 | end 85 | end 86 | end 87 | if ~isequal(EEG.xmin, EEG2.xmin) 88 | fprintf(2, ' Difference between xmin is %1.6f sec\n', EEG.xmin-EEG2.xmin); 89 | EEG.etc.compare.data_changed = true; 90 | end 91 | if ~isequal(EEG.xmax, EEG2.xmax) 92 | fprintf(2, ' Difference between xmax is %1.6f sec\n', EEG.xmax-EEG2.xmax); 93 | EEG.etc.compare.data_changed = true; 94 | end 95 | 96 | % check chanlocs 97 | disp('Chanlocs analysis:') 98 | [~,~,chanlocs1] = eeg_checkchanlocs( EEG.chanlocs, EEG.chaninfo); 99 | [~,~,chanlocs2] = eeg_checkchanlocs( EEG2.chanlocs, EEG2.chaninfo); 100 | if length(chanlocs1) == length(chanlocs2) 101 | differ = 0; 102 | differLabel = 0; 103 | for iChan = 1:length(chanlocs1) 104 | coord1 = [ chanlocs1(iChan).X chanlocs1(iChan).Y chanlocs1(iChan).Z]; 105 | coord2 = [ chanlocs2(iChan).X chanlocs2(iChan).Y chanlocs2(iChan).Z]; 106 | if isempty(coord1) && ~isempty(coord2) 107 | differ = differ+1; 108 | elseif ~isempty(coord1) && isempty(coord2) 109 | differ = differ+1; 110 | elseif ~isempty(coord1) && ~isempty(coord2) && sum(abs( coord1 - coord2 )) > 1e-12 111 | differ = differ+1; 112 | end 113 | if ~isequal(chanlocs1(iChan).labels, chanlocs2(iChan).labels) 114 | differLabel = differLabel+1; 115 | end 116 | end 117 | if differ 118 | fprintf(2, ' %d channel coordinates differ\n', differ); 119 | EEG.etc.compare.chanlocs_changed = true; 120 | else 121 | disp(' All channel coordinates are OK'); 122 | end 123 | if differLabel 124 | fprintf(2, ' %d channel label(s) differ\n', differLabel); 125 | EEG.etc.compare.chanlocs_changed = true; 126 | else 127 | disp(' All channel labels are OK'); 128 | end 129 | else 130 | fprintf(2, ' Different numbers of channels\n'); 131 | EEG.etc.compare.chanlocs_changed = true; 132 | end 133 | 134 | % check events 135 | disp('Event analysis:') 136 | if length(EEG.event) ~= length(EEG2.event) 137 | fprintf(2, ' Different numbers of events\n'); 138 | EEG.etc.compare.events_changed = true; 139 | elseif isempty(EEG.event) 140 | disp(' All events OK (empty)'); 141 | else 142 | fields1 = fieldnames(EEG.event); 143 | fields2 = fieldnames(EEG2.event); 144 | allFieldsOK = true; 145 | 146 | if ~isequal(sort(fields1), sort(fields2)) 147 | fprintf(2, ' Not the same number of event fields\n'); 148 | allFieldsOK = false; 149 | EEG.etc.compare.events_changed = true; 150 | end 151 | 152 | for iField = 1:length(fields1) 153 | if isfield(EEG.event, fields1{iField}) && isfield(EEG2.event, fields1{iField}) 154 | diffVal = zeros(1,length(EEG.event)); 155 | if strcmpi(fields1{iField}, 'latency') 156 | for iEvent = 1:length(EEG.event) 157 | diffVal(iEvent) = EEG.event(iEvent).(fields1{iField}) - EEG2.event(iEvent).(fields1{iField}); 158 | end 159 | else 160 | for iEvent = 1:length(EEG.event) 161 | diffVal(iEvent) = ~isequaln(EEG.event(iEvent).(fields1{iField}), EEG2.event(iEvent).(fields1{iField})); 162 | end 163 | end 164 | if any(diffVal ~= 0) 165 | if strcmpi(fields1{iField}, 'latency') 166 | fprintf(2, ' Event latency (%2.1f %%) are not OK (abs diff of these is %1.4f samples)\n', length(find(diffVal))/length(diffVal)*100, mean( abs(diffVal(diffVal ~=0 ))) ); 167 | fprintf(2, ' ******** (see plot)\n'); 168 | figure; plot(diffVal); 169 | else 170 | fprintf(2, ' Event fields "%s" are NOT OK (%2.1f %% of them)\n', fields1{iField}, length(find(diffVal))/length(diffVal)*100); 171 | end 172 | allFieldsOK = false; 173 | EEG.etc.compare.events_changed = true; 174 | end 175 | end 176 | end 177 | if allFieldsOK 178 | disp(' All other events OK'); 179 | end 180 | end 181 | 182 | % check epochs 183 | if ~isempty(EEG.epoch) 184 | disp('Epoch analysis:') 185 | if length(EEG.epoch) == length(EEG2.epoch) 186 | if ~isempty(EEG.epoch) 187 | fields1 = fieldnames(EEG.epoch); 188 | fields2 = fieldnames(EEG2.epoch); 189 | allFieldsOK = true; 190 | if ~isequal(sort(fields1), sort(fields2)) 191 | fprintf(2, ' Not the same number of event fields\n'); 192 | allFieldsOK = false; 193 | EEG.etc.compare.events_changed = true; 194 | else 195 | diffVal = []; 196 | for iField = 1:length(fields1) 197 | for iEpoch = 1:length(EEG.epoch) 198 | diffVal(iEpoch) = ~isequaln(EEG.epoch(iEpoch).(fields1{iField}), EEG2.epoch(iEpoch).(fields1{iField})); 199 | end 200 | if any(diffVal ~= 0) 201 | fprintf(2, ' Epoch fields "%s" are NOT OK (%2.1f %% of them)\n', fields1{iField}, length(find(diffVal))/length(diffVal)*100); 202 | allFieldsOK = false; 203 | EEG.etc.compare.events_changed = true; 204 | end 205 | end 206 | end 207 | if allFieldsOK 208 | disp(' All epoch and all epoch fields are OK'); 209 | end 210 | end 211 | else 212 | fprintf(2, ' Different numbers of epochs\n'); 213 | EEG.etc.compare.events_changed = true; 214 | end 215 | end 216 | end 217 | 218 | function result = contains(element, cellArray) 219 | % Check if the element is in the cell array 220 | result = any(cellfun(@(x) isequal(x, element), cellArray)); 221 | end 222 | -------------------------------------------------------------------------------- /eeg_getchantype.m: -------------------------------------------------------------------------------- 1 | % EEG_GETCHANTYPE - get the count for each channel type. Also assign 2 | % automatically channel types if they are not yet present. 3 | % 4 | % Usage: 5 | % >> [OUTEEG,chancount] = eeg_getchantype(INEEG); 6 | % 7 | % Inputs: 8 | % INEEG - input EEG dataset structure 9 | % 10 | % Outputs: 11 | % OUTEEG - new EEG dataset structure 12 | % chancount - structure containing channel type information 13 | % 14 | % Author: Arnaud Delorme, SCCN/INC/UCSD, 2023- 15 | % 16 | % see also: EEGLAB 17 | 18 | % Copyright (C) 2023 Arnaud Delorme, SCCN/INC/UCSD 19 | % 20 | % This file is part of EEGLAB, see http://www.eeglab.org 21 | % for the documentation and details. 22 | % 23 | % Redistribution and use in source and binary forms, with or without 24 | % modification, are permitted provided that the following conditions are met: 25 | % 26 | % 1. Redistributions of source code must retain the above copyright notice, 27 | % this list of conditions and the following disclaimer. 28 | % 29 | % 2. Redistributions in binary form must reproduce the above copyright notice, 30 | % this list of conditions and the following disclaimer in the documentation 31 | % and/or other materials provided with the distribution. 32 | % 33 | % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 34 | % AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 35 | % IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 36 | % ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 37 | % LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 38 | % CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 39 | % SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 40 | % INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 41 | % CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 42 | % ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 43 | % THE POSSIBILITY OF SUCH DAMAGE. 44 | 45 | function [EEG,channelsCount] = eeg_getchantype(EEG) 46 | 47 | template_file = fullfile(fileparts(which('eeglab')),'sample_locs/Standard-10-20-Cap81.locs'); 48 | if exist(template_file, 'file') 49 | locs = readtable(template_file,'Delimiter','\t', 'FileType','text'); 50 | eeg_chans = locs.Var4; 51 | else 52 | eeg_chans = ''; 53 | end 54 | 55 | % Assign channel types based on channel name 56 | % types retrived from https://bids-specification.readthedocs.io/en/stable/glossary.html#objects.columns.type__eeg_channels 57 | types = {'EEG', 'MEG', 'MEGREF', 'SEEG', 'EMG', 'EOG', 'ECG', 'EKG', 'EMG', 'TRIG', 'GSR', 'PPG', 'MISC'}; 58 | 59 | for i = 1:length(EEG.chanlocs) 60 | label = EEG.chanlocs(i).labels; 61 | matchIdx = cellfun(@(x) contains(lower(label), lower(x)), types); 62 | 63 | if any(matchIdx) 64 | EEG.chanlocs(i).type = types{matchIdx}; 65 | elseif any(strcmpi(label, eeg_chans)) && ( ~isfield(EEG.chanlocs, 'type') || isempty(EEG.chanlocs(i).type) || all(isnan(EEG.chanlocs(i).type)) || isequal(lower(EEG.chanlocs(i).type), 'n/a')) 66 | EEG.chanlocs(i).type = 'EEG'; 67 | end 68 | end 69 | channelsCount = count_channel(EEG); 70 | 71 | %% ---------------------------- 72 | function channelsCount = count_channel(EEG) 73 | 74 | if isempty(EEG.chanlocs) 75 | channelsCount = struct([]); 76 | else 77 | acceptedChannelTypes = { 'AUDIO' 'EEG' 'MEG' 'MEGREF' 'SEEG' 'EOG' 'ECG' 'EMG' 'EYEGAZE' 'GSR' 'HEOG' 'MISC' 'PUPIL' 'REF' 'RESP' 'SYSCLOCK' 'TEMP' 'TRIG' 'VEOG' }; 78 | channelsCount = []; 79 | channelsCount.EEG = 0; 80 | for iChan = 1:EEG.nbchan 81 | % Type 82 | if ~isfield(EEG.chanlocs, 'type') || isempty(EEG.chanlocs(iChan).type) || isnan(EEG.chanlocs(iChan).type(1)) 83 | type = 'n/a'; 84 | elseif ismember(upper(EEG.chanlocs(iChan).type), acceptedChannelTypes) 85 | type = upper(EEG.chanlocs(iChan).type); 86 | else 87 | type = 'MISC'; 88 | end 89 | % Unit 90 | if strcmpi(type, 'eeg') 91 | unit = 'uV'; 92 | else 93 | unit = 'n/a'; 94 | end 95 | 96 | % Count channels by type (for use later in eeg.json) 97 | if strcmp(type, 'n/a') 98 | channelsCount.('EEG') = channelsCount.('EEG') + 1; 99 | else 100 | if ~isfield(channelsCount, type), channelsCount.(type) = 0; end 101 | if strcmp(type, 'HEOG') || strcmp(type,'VEOG') 102 | if ~isfield(channelsCount, 'EOG') 103 | channelsCount.('EOG') = 1; 104 | else 105 | channelsCount.('EOG') = channelsCount.('EOG') + 1; 106 | end 107 | else 108 | channelsCount.(type) = channelsCount.(type) + 1; 109 | end 110 | end 111 | 112 | end 113 | end 114 | 115 | 116 | -------------------------------------------------------------------------------- /eeg_import.m: -------------------------------------------------------------------------------- 1 | % EEG_IMPORT - Import data files in a variety of supported format 2 | % 3 | % Usage: 4 | % >> EEG = eeg_import(fileIn); 5 | % >> EEG = eeg_import(fileIn, 'key', 'val', ...); 6 | % 7 | % Inputs: 8 | % fileIn - [string] input file name. The function used to import the file is 9 | % based on the file extension. 10 | % '.set' - POP_LOADSET (EEGLAB data format) 11 | % '.edf' - POP_BIOSIG (European Data Format) 12 | % '.bdf' - POP_BIOSIG (BIOSEMI) 13 | % '.vhdr' - POP_LOADBV (Brain Vision Exchange Format) 14 | % '.eeg' - POP_LOADBV (Brain Vision Exchange Format) 15 | % '.cnt' - POP_LOADCNT (Neuroscan) 16 | % '.mff' - POP_MFFIMPORT (EGI MFF data format) 17 | % '.raw' - POP_READEGI (EGI Binary Simple format) 18 | % '.nwb' - POP_NWBIMPORT (Neurodata Without Borders) 19 | % '.mefd' - POP_MEF3 (MEF iEEG data format) 20 | % '.ds' - POP_FILEIO or POP_CTF_READ (see below; CTF MEG data format) 21 | % '.fif' - POP_FILEIO (FIF MEG data format) 22 | % '.fif.gz' - POP_FILEIO (FIF MEG data format) 23 | % 24 | % Optional Inputs: 25 | % 'noevents' - ['on'|'off'] do not save event files. Default is 'off'. 26 | % 'modality' - ['eeg'|'ieeg'|'meg'|'auto'] type of data. 'auto' means the 27 | % format is determined from the file extension. Default is 'auto'. 28 | % 'ctffunc' - ['fileio'|'ctfimport'] function to use to import CTF data. 29 | % Some data is better imported using the POP_CTF_READ 30 | % function and some data using POP_FILEIO. Default 'fileio'. 31 | % 'importfunc' - [function handle or string] function to import raw data to 32 | % EEG format. Default: auto. 33 | % 34 | % Outputs: 35 | % EEG - EEGLAB data structure 36 | % 37 | % Authors: Arnaud Delorme, SCCN, INC, UCSD, 2024 38 | 39 | % Copyright (C) Arnaud Delorme, 2024 40 | % 41 | % This file is part of EEGLAB, see http://www.eeglab.org 42 | % for the documentation and details. 43 | % 44 | % Redistribution and use in source and binary forms, with or without 45 | % modification, are permitted provided that the following conditions are met: 46 | % 47 | % 1. Redistributions of source code must retain the above copyright notice, 48 | % this list of conditions and the following disclaimer. 49 | % 50 | % 2. Redistributions in binary form must reproduce the above copyright notice, 51 | % this list of conditions and the following disclaimer in the documentation 52 | % and/or other materials provided with the distribution. 53 | % 54 | % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 55 | % AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 56 | % IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 57 | % ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 58 | % LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 59 | % CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 60 | % SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 61 | % INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 62 | % CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 63 | % ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 64 | % THE POSSIBILITY OF SUCH DAMAGE. 65 | 66 | function [EEG, modality] = eeg_import(fileIn, varargin) 67 | 68 | if nargin < 1 69 | help eeg_import 70 | return 71 | end 72 | 73 | opt = finputcheck(varargin, { 74 | 'ctffunc' 'string' { 'fileio' 'ctfimport' } 'fileio'; ... 75 | 'importfunc' '' {} ''; 76 | 'importfunc' '' {} ''; 77 | 'modality' 'string' {'ieeg' 'meg' 'eeg' 'auto'} 'auto'; 78 | 'noevents' 'string' {'on' 'off'} 'off' }, 'eeg_import'); 79 | if isstr(opt), error(opt); end 80 | 81 | [~,~,ext] = fileparts(fileIn); 82 | ext = lower(ext); 83 | if ~isempty(opt.importfunc) 84 | if strcmpi(opt.modality, 'auto'), opt.modality = 'eeg'; end 85 | EEG = feval(opt.importfunc, fileIn); 86 | elseif strcmpi(ext, '.bdf') || strcmpi(ext, '.edf') 87 | if strcmpi(opt.modality, 'auto'), opt.modality = 'eeg'; end 88 | EEG = pop_biosig(fileIn); 89 | elseif strcmpi(ext, '.vhdr') 90 | if strcmpi(opt.modality, 'auto'), opt.modality = 'eeg'; end 91 | [fpathin, fname, ext] = fileparts(fileIn); 92 | EEG = pop_loadbv(fpathin, [fname ext]); 93 | elseif strcmpi(ext, '.set') 94 | if strcmpi(opt.modality, 'auto'), opt.modality = 'eeg'; end 95 | EEG = pop_loadset(fileIn); 96 | elseif strcmpi(ext, '.cnt') 97 | if strcmpi(opt.modality, 'auto'), opt.modality = 'eeg'; end 98 | EEG = pop_loadcnt(fileIn, 'dataformat', 'auto'); 99 | datFile = [fileIn(1:end-4) '.dat']; 100 | if exist(datFile,'file') 101 | EEG = pop_importevent(EEG, 'indices',1:length(EEG.event), 'append','no', 'event', datFile,... 102 | 'fields',{'DatTrial','DatResp','DatType','DatCorrect','DatLatency'},'skipline',20,'timeunit',NaN,'align',0); 103 | end 104 | elseif strcmpi(ext, '.mff') 105 | if strcmpi(opt.modality, 'auto'), opt.modality = 'eeg'; end 106 | EEG = pop_mffimport(fileIn,{'code'}); 107 | elseif strcmpi(ext, '.raw') 108 | if strcmpi(opt.modality, 'auto'), opt.modality = 'eeg'; end 109 | EEG = pop_readegi(fileIn); 110 | elseif strcmpi(ext, '.eeg') 111 | if strcmpi(opt.modality, 'auto'), opt.modality = 'eeg'; end 112 | [tmpPath,tmpFileName,~] = fileparts(fileIn); 113 | if exist(fullfile(tmpPath, [tmpFileName '.vhdr']), 'file') 114 | EEG = pop_loadbv( tmpPath, [tmpFileName '.vhdr'] ); 115 | else 116 | error('.eeg files not from BrainVision are currently not supported') 117 | end 118 | elseif strcmpi(ext, '.nwb') 119 | if strcmpi(opt.modality, 'auto'), opt.modality = 'ieeg'; end 120 | if ~exist('pop_nwbimport', 'file') 121 | error('NWB-io plugin not present, please install the plugin first') 122 | end 123 | EEG = pop_nwbimport(fileIn, 'importspikes', 'on', 'typefield', 1); 124 | elseif strcmpi(ext, '.mefd') 125 | if strcmpi(opt.modality, 'auto'), opt.modality = 'ieeg'; end 126 | modality = 'ieeg'; 127 | if ~exist('pop_MEF3', 'file') 128 | error('MEF plugin not present, please install the MEF3 plugin first') 129 | end 130 | EEG = pop_MEF3(eegFileRaw); % MEF folder 131 | elseif strcmpi(ext, '.fif') 132 | if strcmpi(opt.modality, 'auto'), opt.modality = 'meg'; end 133 | EEG = pop_fileio(eegFileRaw); % fif folder 134 | elseif strcmpi(ext, '.gz') 135 | if strcmpi(opt.modality, 'auto'), opt.modality = 'meg'; end 136 | gunzip(eegFileRaw); 137 | EEG = pop_fileio(eegFileRaw(1:end-3)); % fif folder 138 | elseif strcmpi(ext, '.ds') 139 | if strcmpi(opt.modality, 'auto'), opt.modality = 'meg'; end 140 | if strcmpi(opt.ctffunc, 'fileio') 141 | EEG = pop_fileio(eegFileRaw); 142 | else 143 | EEG = pop_ctf_read(eegFileRaw); 144 | end 145 | else 146 | error('Data format not supported'); 147 | end 148 | if strcmpi(opt.noevents, 'on') 149 | EEG.event = []; 150 | end 151 | EEG = eeg_checkset(EEG); 152 | modality = opt.modality; -------------------------------------------------------------------------------- /eeg_selectsegment.m: -------------------------------------------------------------------------------- 1 | % EEG_SELECTSEGMENT - Select a data segment 2 | % 3 | % Usage: 4 | % OUTEEG = eeg_selectsegment(INEEG, 'key', val) 5 | % 6 | % Input: 7 | % INEEG - input EEG dataset structure 8 | % 9 | % Optional inputs: 10 | % 'timeoffset' - [2x float array] beginning and end in seconds. For example 11 | % 'timeoffset' of [0 1800] selects the first 30 minutes of a file. 12 | % Default 'timeoffset' is [0 0]. 13 | % 'eventype' - [cell array of string or integer array] select events of a 14 | % given type for beginning and end of segment. { 'beg' 'end' } 15 | % will select the first 'beg' event for the beginning of the 16 | % segment and the first 'end' event for the end of the 17 | % segment (unless 'eventindex' is specified below). Event 18 | % types may also be numerical (e.g. [1 2]). 19 | % 'eventindex' - [integer array of size 2] event indices for the 20 | % beginning and end of the segment (see example below). 21 | % 22 | % Outputs: 23 | % OUTEEG - EEG dataset structure with the extracted segment 24 | % 25 | % Example: 26 | % % select the first 30 minutes of an EEG dataset 27 | % EEG = eeg_selectsegment(EEG, 'timeoffset', [0 1800]); 28 | % 29 | % % select the segment between the first 'beg' and 'end' event types 30 | % EEG = eeg_selectsegment(EEG, 'eventype', { 'beg' 'end'}); 31 | % 32 | % % select the segment between the second 'beg' and 'end' event types 33 | % EEG = eeg_selectsegment(EEG, 'eventype', { 'beg' 'end'}, 'eventindex', [2 2]); 34 | % 35 | % % select the segment one second after the 'beg' and 'end' event types 36 | % EEG = eeg_selectsegment(EEG, 'eventype', { 'beg' 'end'}, 'timeoffset', [1 1]); 37 | % 38 | % % select the segment from the first 'beg' to 1800 seconds after this event 39 | % EEG = eeg_selectsegment(EEG, 'eventype', { 'beg' 'beg'}, 'timeoffset', [0 1800]); 40 | % 41 | % Author: Arnaud Delorme, UCSD, 2023 42 | % 43 | % see also: EEGLAB 44 | 45 | % Copyright (C) 2023 Arnaud Delorme, UCSD 46 | % 47 | % This file is part of EEGLAB, see http://www.eeglab.org 48 | % for the documentation and details. 49 | % 50 | % Redistribution and use in source and binary forms, with or without 51 | % modification, are permitted provided that the following conditions are met: 52 | % 53 | % 1. Redistributions of source code must retain the above copyright notice, 54 | % this list of conditions and the following disclaimer. 55 | % 56 | % 2. Redistributions in binary form must reproduce the above copyright notice, 57 | % this list of conditions and the following disclaimer in the documentation 58 | % and/or other materials provided with the distribution. 59 | % 60 | % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 61 | % AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 62 | % IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 63 | % ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 64 | % LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 65 | % CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 66 | % SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 67 | % INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 68 | % CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 69 | % ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 70 | % THE POSSIBILITY OF SUCH DAMAGE. 71 | 72 | function EEG = eeg_selectsegment(EEG, varargin) 73 | 74 | if nargin < 2 75 | help eeg_selectsegment; 76 | return 77 | end 78 | 79 | opt = finputcheck(varargin, { 'eventtype' {'cell' 'integer'} {{} {}} []; 80 | 'eventindex' 'integer' {} []; 81 | 'verbose' 'string' {'on' 'off'} 'on'; 82 | 'timeoffset' 'real' {} [0 0]}, 'eeg_selectsegment'); 83 | if ischar(opt), error(opt); end 84 | 85 | if isempty(opt.timeoffset) 86 | opt.timeoffset = [0 0]; 87 | end 88 | if length(opt.timeoffset) ~= 2 89 | error('There must be exactly 2 time offsets, one for the beginning and one for the end') 90 | end 91 | 92 | if isequal(opt.timeoffset, [0 0]) && isempty(opt.eventtype) && isempty(opt.eventindex) 93 | % nothing to do 94 | return; 95 | end 96 | if strcmpi(opt.verbose, 'on') 97 | fprintf('xxx Processing file %s\n', EEG.comments); 98 | end 99 | 100 | % select event types 101 | % ------------------ 102 | if ~isempty(opt.eventtype) 103 | if length(opt.eventtype) < 2 104 | error('You need at least 2 event types, one for the onset and one for the offset'); 105 | end 106 | tmpevents = EEG.event; 107 | if iscell(opt.eventtype) 108 | indEvents1 = strmatch(opt.eventtype{1}, {tmpevents.type}, 'exact' ); 109 | indEvents2 = strmatch(opt.eventtype{2}, {tmpevents.type}, 'exact' ); 110 | if strcmpi(opt.verbose, 'on') 111 | fprintf('xxx %d events of type "%s" found\n', length(indEvents1), opt.eventtype{1}); 112 | fprintf('xxx %d events of type "%s" found\n', length(indEvents2), opt.eventtype{2}); 113 | end 114 | else 115 | indEvents1 = find([tmpevents.type] == opt.eventtype(1)); 116 | indEvents2 = find([tmpevents.type] == opt.eventtype(2)); 117 | end 118 | tmpevents1 = EEG.event(indEvents1); 119 | tmpevents2 = EEG.event(indEvents2); 120 | else 121 | tmpevents1 = EEG.event; 122 | tmpevents2 = EEG.event; 123 | end 124 | 125 | % select event indices 126 | % -------------------- 127 | if ~isempty(opt.eventindex) 128 | if length(opt.eventindex) ~= 2 129 | error('There must be exactly 2 event indices, one for the beginning and one for the end') 130 | end 131 | if ~isnan(opt.eventindex(1)) 132 | if strcmpi(opt.verbose, 'on') 133 | fprintf('xxx Selecting beginning event: the %d type "%s" event at latency %1.0f plus %1.0f seconds\n', opt.eventindex(1), opt.eventtype{1}, tmpevents1(opt.eventindex(1)).latency/EEG.srate, opt.timeoffset(1)); 134 | end 135 | latency1 = tmpevents1(opt.eventindex(1)).latency + opt.timeoffset(1)*EEG.srate; 136 | else latency1 = opt.timeoffset(1)*EEG.srate; 137 | end 138 | if ~isnan(opt.eventindex(2)) 139 | if strcmpi(opt.verbose, 'on') 140 | fprintf('xxx Selecting beginning event: the %d type "%s" event at latency %1.0f plus %1.0f seconds\n', opt.eventindex(2), opt.eventtype{2}, tmpevents1(opt.eventindex(2)).latency/EEG.srate, opt.timeoffset(2)); 141 | end 142 | latency2 = tmpevents2(opt.eventindex(2)).latency + opt.timeoffset(2)*EEG.srate; 143 | else latency2 = opt.timeoffset(2)*EEG.srate; 144 | end 145 | else 146 | latency1 = opt.timeoffset(1)*EEG.srate; 147 | latency2 = opt.timeoffset(2)*EEG.srate; 148 | end 149 | 150 | EEG = pop_select(EEG, 'point', [latency1 latency2]); 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /eegplugin_eegbids.m: -------------------------------------------------------------------------------- 1 | % eegplugin_eegbids() - EEGLAB plugin for importing data saved 2 | % by the finders course (Matlab converted) 3 | % 4 | % Usage: 5 | % >> eegplugin_eegbids(fig, trystrs, catchstrs); 6 | % 7 | % Inputs: 8 | % fig - [integer] EEGLAB figure 9 | % trystrs - [struct] "try" strings for menu callbacks. 10 | % catchstrs - [struct] "catch" strings for menu callbacks. 11 | 12 | function vers = eegplugin_eegbids(fig, trystrs, catchstrs) 13 | 14 | vers = bids_matlab_tools_ver; 15 | if nargin < 3 16 | error('eegplugin_eegbids requires 3 arguments'); 17 | end 18 | 19 | % add folder to path 20 | % ------------------ 21 | p = which('pop_importbids.m'); 22 | p = p(1:findstr(p,'pop_importbids.m')-1); 23 | if ~exist('jsonwrite') 24 | addpath( p ); 25 | addpath( fullfile(p, 'JSONio')); 26 | end 27 | 28 | % find import data menu 29 | % --------------------- 30 | menui1 = findobj(fig, 'tag', 'import data'); 31 | menui2 = findobj(fig, 'tag', 'export'); 32 | menui3 = findobj(fig, 'label', 'File'); 33 | 34 | % menu callbacks 35 | % -------------- 36 | comcnt1 = [ trystrs.no_check '[STUDYTMP, ALLEEGTMP, ~, ~, LASTCOM] = pop_importbids; ' catchstrs.load_study ]; 37 | comcnt2 = [ trystrs.no_check '[~,~,LASTCOM] = pop_exportbids(STUDY, EEG);' catchstrs.add_to_hist ]; 38 | 39 | % create menus 40 | % ------------ 41 | uimenu( menui1, 'label', 'From BIDS folder structure', 'separator', 'on', 'callback', comcnt1); 42 | uimenu( menui2, 'label', 'To BIDS folder structure', 'separator', 'on', 'callback', comcnt2, 'userdata', 'startup:off;study:on'); 43 | set(menui2, 'userdata', 'startup:off;study:on'); 44 | 45 | % create BIDS menus 46 | % ----------------- 47 | comtaskinfo = [trystrs.no_check '[EEG,LASTCOM] = pop_taskinfo(EEG);' catchstrs.store_and_hist ]; 48 | comsubjinfo = [trystrs.no_check '[EEG,STUDY,LASTCOM] = pop_participantinfo(EEG,STUDY);' catchstrs.store_and_hist ]; 49 | comeventinfo = [trystrs.no_check '[EEG,STUDY,LASTCOM] = pop_eventinfo(EEG,STUDY);' catchstrs.store_and_hist ]; 50 | % comvalidatebids = [ trystrs.no_check 'if plugin_askinstall(''bids-validator'',''pop_validatebids'') == 1 pop_validatebids() end' catchstrs.add_to_hist ]; 51 | bids = findobj(fig, 'label', 'BIDS tools'); 52 | if isempty(bids) 53 | bids = uimenu( menui3, 'label', 'BIDS tools', 'separator', 'on', 'position', 5, 'userdata', 'startup:on;study:on'); 54 | end 55 | children = get(bids, 'children'); 56 | if ~isempty(children) 57 | delete(children); 58 | end 59 | 60 | uimenu( bids, 'label', 'BIDS export wizard (from raw EEG to BIDS)', 'callback', 'bids_exporter;'); 61 | uimenu( bids, 'label', 'Import BIDS folder to STUDY', 'separator', 'on', 'callback', comcnt1); 62 | uimenu( bids, 'label', 'Export STUDY to BIDS folder', 'callback', comcnt2, 'userdata', 'startup:off;study:on'); 63 | uimenu( bids, 'label', 'Edit BIDS task info', 'separator', 'on', 'callback', comtaskinfo, 'userdata', 'study:on'); 64 | uimenu( bids, 'label', 'Edit BIDS participant info', 'callback', comsubjinfo, 'userdata', 'study:on'); 65 | uimenu( bids, 'label', 'Edit BIDS event info', 'callback', comeventinfo, 'userdata', 'study:on'); 66 | uimenu( bids, 'label', 'Validate BIDS dataset', 'separator', 'on', 'callback', 'web(''https://bids-standard.github.io/bids-validator/'')', 'userdata', 'startup:on;study:on'); 67 | 68 | function validatebidsCB(src,event) 69 | if plugin_status('bids-validator') == 0 70 | plugin_askinstall('bids-validator'); 71 | else 72 | pop_validatebids() 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /mergeStructures.m: -------------------------------------------------------------------------------- 1 | function ssMerged = mergeStructures( ssDest, ssSource ) 2 | % mergeStructures selectively create and update fields in ssDest with those in ssSource, recursively 3 | 4 | % Merge tree-like structures in much the same way as files are 5 | % copied to destination folders, overwriting values where they exist. 6 | % 7 | % Arrays of structures: 8 | % ssInto and ssFrom may be struct arrays of the same size. 9 | % Each (scalar) element of the source array is 10 | % merged with its corresponding element in the destination. 11 | % 12 | % Empty fields: 13 | % Empty fields in a scalar (size 1-by-1) source overwrite destination. 14 | % Empty fields in an array source DO NOT overwrite destination; 15 | % this makes it easier to update just a few elements of an array. 16 | % credit: https://www.mathworks.com/matlabcentral/fileexchange/131718-mergestructures-merge-or-concatenate-nested-structures?s_tid=mwa_osa_a 17 | 18 | if ~isstruct( ssDest ) || ~isstruct( ssSource ) 19 | error('mergeFieldeRecurse:paramsNotStructures', ... 20 | 'mergeFieldsRecurse() expects structures as inputs') 21 | end 22 | 23 | % modification - Dung Truong 2024 24 | if isempty(ssDest) && ~isempty(ssSource) 25 | ssMerged = ssSource; 26 | elseif isempty(ssSource) && ~isempty(ssDest) 27 | ssMerged = ssDest; 28 | else 29 | 30 | if ~all( size( ssDest ) == size( ssSource ) ) 31 | error('mergeFieldeRecurse:parameterSizesInequal', ... 32 | 'mergeFieldsRecurse(): array structures must have equal sizes') 33 | end 34 | 35 | ssMerged = ssDest; 36 | 37 | % loop over each element in struct arrays 38 | nns = numel( ssMerged ); 39 | for ns = 1:nns 40 | 41 | f = fieldnames(ssSource); 42 | % loop over fields in source 43 | for nf = 1:length(f) 44 | fieldName = f{nf}; 45 | 46 | % copy/merge field from ssSource into ssDest. 47 | % Overwite current values in ssMerged. 48 | % Creates new field in ssMerged as required. 49 | % Recurse if source field is a structure. 50 | if isa( ssSource(ns).(fieldName), 'struct') 51 | % source field is a structure 52 | if ~isfield( ssMerged(ns), fieldName) 53 | % New field in destination. Do a simple copy 54 | ssMerged(ns).(fieldName) = ssSource(ns).(fieldName); 55 | else 56 | % recurse for nested structures 57 | ssM = mergeStructures( ... 58 | ssMerged(ns).(fieldName), ... 59 | ssSource(ns).(fieldName) ); 60 | ssMerged(ns).(fieldName) = ssM; 61 | end 62 | else 63 | % source is a value, not a structure. 64 | if nns > 1 && isempty( ssSource(ns).(fieldName) ) 65 | % don't overwrite values in arrays with [] 66 | else 67 | % assign/overwrite new value to current field 68 | ssMerged(ns).(fieldName) = ssSource(ns).(fieldName); 69 | end 70 | end 71 | end % loop over nf fields in source 72 | end % loop over ns elements in array of structure 73 | end 74 | end % function mergeNestedStructures() 75 | -------------------------------------------------------------------------------- /pop_checkdatasetinfo.m: -------------------------------------------------------------------------------- 1 | % pop_checkdatasetinfo() - Check for consistency between STUDY and ALLEEG 2 | % Fields covered: filename, subject, condition, 3 | % group, session, run. 4 | % 5 | % Usage: 6 | % >> [STUDY, ALLEEG] = pop_checkdatasetinfo(STUDY, ALLEEG); 7 | % 8 | % Inputs: 9 | % STUDY - STUDY structure 10 | % 11 | % ALLEEG - array of all EEG dataset in STUDY 12 | % Outputs: 13 | % STUDY - If there's inconsistency and user select to copy info, 14 | % returned STUDY contains the missing info copied from 15 | % ALLEEG 16 | % 17 | % ALLEEG - Struct array of all EEG dataset in STUDY 18 | % 19 | % Author: Dung Truong, Arnaud Delorme 20 | function [STUDY, ALLEEG] = pop_checkdatasetinfo(STUDY, ALLEEG) 21 | datasetinfo = STUDY.datasetinfo; 22 | different = 0; 23 | for k = 1:length(ALLEEG) 24 | if ~strcmpi(datasetinfo(k).filename, ALLEEG(k).filename), different = 1; break; end 25 | if ~strcmpi(datasetinfo(k).subject, ALLEEG(k).subject), different = 1; break; end 26 | if ~strcmpi(datasetinfo(k).condition, ALLEEG(k).condition), different = 1; break; end 27 | if ~strcmpi(char(datasetinfo(k).group), char(ALLEEG(k).group)), different = 1; break; end 28 | if ~isequal(datasetinfo(k).session, ALLEEG(k).session), different = 1; break; end 29 | if ~isequal(datasetinfo(k).run, ALLEEG(k).run), different = 1; break; end 30 | end 31 | 32 | if different 33 | supergui( 'geomhoriz', { 1 1 1 [1 1] }, 'uilist', { ... 34 | { 'style', 'text', 'string', 'Information between STUDY and single datasets is inconsistent.', 'HorizontalAlignment','center'},... 35 | { 'style', 'text', 'string', 'Would you like to overwrite dataset information with STUDY information and use that for BIDS?','HorizontalAlignment', 'center'}, { }, ... 36 | { 'style', 'pushbutton' , 'string', 'Yes', 'callback', @yesCB}, { 'style', 'pushbutton' , 'string', 'No', 'callback', @noCB } } ); 37 | waitfor(gcf); 38 | end 39 | 40 | function yesCB(src, event) 41 | [STUDY, ALLEEG] = std_editset(STUDY, ALLEEG, 'updatedat', 'on'); 42 | supergui( 'geomhoriz', { 1 1 1 }, 'uilist', { ... 43 | { 'style', 'text', 'string', 'Information updated', 'HorizontalAlignment', 'center'}, { }, ... 44 | { 'style', 'pushbutton', 'string', 'Ok', 'callback', 'close(gcf);'}}); 45 | waitfor(gcf); 46 | close(gcf); 47 | end 48 | function noCB(src,event) 49 | close(gcf); 50 | end 51 | end -------------------------------------------------------------------------------- /pop_exportbids.m: -------------------------------------------------------------------------------- 1 | % pop_exportbids() - Export EEGLAB study into BIDS folder structure 2 | % 3 | % Usage: 4 | % pop_exportbids(STUDY, ALLEEG, 'key', val); 5 | % 6 | % Inputs: 7 | % bidsfolder - a loaded epoched EEG dataset structure. 8 | % 9 | % Note: 'key', val arguments are the same as the one in bids_export() 10 | % 11 | % Authors: Arnaud Delorme, SCCN, INC, UCSD, January, 2019 12 | 13 | % Copyright (C) Arnaud Delorme, 2019 14 | % 15 | % This program is free software; you can redistribute it and/or modify 16 | % it under the terms of the GNU General Public License as published by 17 | % the Free Software Foundation; either version 2 of the License, or 18 | % (at your option) any later version. 19 | % 20 | % This program is distributed in the hope that it will be useful, 21 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | % GNU General Public License for more details. 24 | % 25 | % You should have received a copy of the GNU General Public License 26 | % along with this program; if not, write to the Free Software 27 | % Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 28 | 29 | function [STUDY,EEG,comOut] = pop_exportbids(STUDY, EEG, varargin) 30 | 31 | comOut = ''; 32 | if isempty(STUDY) 33 | error('BIDS export can only export EEGLAB studies'); 34 | end 35 | if nargin < 2 36 | error('This function needs at least 2 parameters'); 37 | end 38 | 39 | if nargin < 3 && ~ischar(STUDY) 40 | com = [ 'bidsFolderxx = uigetdir(''Pick a BIDS output folder'');' ... 41 | 'if ~isequal(bidsFolderxx, 0), set(findobj(gcbf, ''tag'', ''outputfolder''), ''string'', bidsFolderxx); end;' ... 42 | 'clear bidsFolderxx;' ]; 43 | 44 | cb_task = 'pop_exportbids(''edit_task'', gcbf);'; 45 | cb_eeg = 'pop_exportbids(''edit_eeg'', gcbf);'; 46 | cb_participants = 'pop_exportbids(''edit_participants'', gcbf);'; 47 | cb_events = 'pop_exportbids(''edit_events'', gcbf);'; 48 | uilist = { ... 49 | { 'Style', 'text', 'string', 'Export EEGLAB study to BIDS', 'fontweight', 'bold' }, ... 50 | {} ... 51 | { 'Style', 'text', 'string', 'Output folder:' }, ... 52 | { 'Style', 'edit', 'string', fullfile('.', 'BIDS_EXPORT') 'tag' 'outputfolder' }, ... 53 | { 'Style', 'pushbutton', 'string', '...' 'callback' com }, ... 54 | { 'Style', 'text', 'string', 'Licence for distributing:' }, ... 55 | { 'Style', 'edit', 'string', 'Creative Common 0 (CC0)' 'tag' 'license' }, ... 56 | { 'Style', 'text', 'string', 'CHANGES compared to previous releases:' }, ... 57 | { 'Style', 'edit', 'string', '' 'tag' 'changes' 'HorizontalAlignment' 'left' 'max' 3 }, ... 58 | { 'Style', 'pushbutton', 'string', 'Edit task & EEG info' 'tag' 'task' 'callback' cb_task }, ... 59 | { 'Style', 'pushbutton', 'string', 'Edit participants' 'tag' 'participants' 'callback' cb_participants }, ... 60 | { 'Style', 'pushbutton', 'string', 'Edit event info' 'tag' 'events' 'callback' cb_events }, ... 61 | { 'Style', 'checkbox', 'string', 'Do not use participants IDs and create anonymized participants IDs instead' 'tag' 'newids' }, ... 62 | }; 63 | relSize = 0.7; 64 | geometry = { [1] [1] [1-relSize relSize*0.8 relSize*0.2] [1-relSize relSize] [1] [1] [1 1 1] [1]}; 65 | geomvert = [1 0.2 1 1 1 3 1 1]; 66 | userdata.EEG = EEG; 67 | userdata.STUDY = STUDY; 68 | [results,userdata,~,restag] = inputgui( 'geometry', geometry, 'geomvert', geomvert, 'uilist', uilist, 'helpcom', 'pophelp(''pop_exportbids'');', 'title', 'Export EEGLAB STUDY to BIDS -- pop_exportbids()', 'userdata', userdata ); 69 | if length(results) == 0, return; end 70 | STUDY = userdata.STUDY; 71 | EEG = userdata.EEG; 72 | 73 | % decode some outputs 74 | if ~isempty(strfind(restag.license, 'CC0')), restag.license = 'CC0'; end 75 | % if ~isempty(restag.authors) 76 | % authors = textscan(restag.authors, '%s', 'delimiter', ';'); 77 | % authors = authors{1}'; 78 | % else 79 | % authors = { '' }; 80 | % end 81 | 82 | % options 83 | options = { 'targetdir' restag.outputfolder 'License' restag.license 'CHANGES' restag.changes 'createids' fastif(restag.newids, 'on', 'off') 'individualEventsJson' 'off'}; 84 | 85 | if ~isfield(EEG(1), 'BIDS') % none of the edit button was clicked 86 | EEG = pop_eventinfo(EEG, STUDY, 'default'); 87 | EEG = pop_participantinfo(EEG, STUDY, 'default'); 88 | EEG = pop_taskinfo(EEG, 'default'); 89 | end 90 | if isempty(STUDY.task) 91 | uiList = { { 'style' 'text' 'string' 'Task name (no space, dash, underscore, or special chars)' } ... 92 | { 'style' 'edit' 'string' ''} }; 93 | res = inputgui('title', 'Missing required information', 'uilist', uiList, 'geometry', {[ 1 0.4 ]}); 94 | if isempty(res), return; end 95 | if isempty(res{1}) 96 | error('A task name is required') 97 | end 98 | STUDY.task = res{1}; 99 | end 100 | if ~isfield(EEG(1).BIDS.tInfo, 'PowerLineFrequency') || isnan(EEG(1).BIDS.tInfo.PowerLineFrequency) 101 | uiList = { { 'style' 'text' 'string' 'You must specify power line frequency' } ... 102 | { 'style' 'popupmenu' 'string' {'50' '60' }} }; 103 | res = inputgui('title', 'Missing required information', 'uilist', uiList, 'geometry', {[ 1 0.4 ]}); 104 | if isempty(res), return; end 105 | for iEEG = 1:length(EEG) 106 | if res{1} == 1 107 | EEG(iEEG).BIDS.tInfo.PowerLineFrequency = 50; 108 | else 109 | EEG(iEEG).BIDS.tInfo.PowerLineFrequency = 60; 110 | end 111 | EEG(iEEG).saved = 'no'; 112 | end 113 | end 114 | 115 | elseif ischar(STUDY) 116 | command = STUDY; 117 | fig = EEG; 118 | userdata = get(fig, 'userdata'); 119 | switch command 120 | case 'edit_participants' 121 | userdata.EEG = pop_participantinfo(userdata.EEG); 122 | case 'edit_events' 123 | userdata.EEG = pop_eventinfo(userdata.EEG); 124 | case 'edit_task' 125 | userdata.EEG = pop_taskinfo(userdata.EEG); 126 | case 'edit_eeg' 127 | userdata.EEG = pop_eegacqinfo(userdata.EEG); 128 | end 129 | set(fig, 'userdata', userdata); 130 | return 131 | else 132 | options = varargin; 133 | end 134 | 135 | % rearrange information in BIDS structures 136 | if ~isfield(EEG, 'BIDS') 137 | EEG(1).BIDS = struct([]); 138 | end 139 | if isfield(EEG(1).BIDS, 'gInfo') && isfield(EEG(1).BIDS.gInfo,'README') 140 | options = [options 'README' {EEG(1).BIDS.gInfo.README}]; 141 | EEG(1).BIDS.gInfo = rmfield(EEG(1).BIDS.gInfo,'README'); 142 | end 143 | if isfield(EEG(1).BIDS, 'gInfo') && isfield(EEG(1).BIDS.gInfo,'TaskName') 144 | options = [options 'taskName' {EEG(1).BIDS.gInfo.TaskName}]; 145 | EEG(1).BIDS.gInfo = rmfield(EEG(1).BIDS.gInfo,'TaskName'); 146 | end 147 | if ~isempty(STUDY.task) 148 | taskTmp = STUDY.task; 149 | taskTmp(taskTmp == '-') = []; 150 | taskTmp(taskTmp == '_') = []; 151 | taskTmp(taskTmp == ' ') = []; 152 | options = [options { 'taskName' taskTmp }]; 153 | end 154 | 155 | bidsFieldsFromALLEEG = fieldnames(EEG(1).BIDS); % All EEG should share same BIDS info -> using EEG(1) 156 | % tInfo.SubjectArtefactDescription is not shared, arguments passed as `notes` below 157 | for f=1:numel(bidsFieldsFromALLEEG) 158 | if ~isequal(bidsFieldsFromALLEEG{f}, 'behavioral') && ~isequal(bidsFieldsFromALLEEG{f}, 'bidsstats') 159 | options = [options bidsFieldsFromALLEEG{f} {EEG(1).BIDS.(bidsFieldsFromALLEEG{f})}]; 160 | else 161 | warning('Warning: cannot re-export behavioral data yet') 162 | end 163 | end 164 | 165 | % get subjects and sessions 166 | % ------------------------- 167 | if ~isempty(EEG(1).subject) 168 | allSubjects = { EEG.subject }; 169 | elseif ~isempty(STUDY.datasetinfo(1).subject) 170 | allSubjects = { STUDY.datasetinfo.subject }; 171 | else 172 | error('No subject info found in either EEG or STUDY.datasetinfo. Please add using Study > Edit STUDY info'); 173 | end 174 | if ~isempty(STUDY.datasetinfo(1).session) 175 | allSessions = { STUDY.datasetinfo.session }; 176 | else 177 | allSessions = { EEG.session }; 178 | end 179 | [~,inds] = unique(allSubjects); 180 | uniqueSubjects = allSubjects(sort(inds)); 181 | allSessions(cellfun(@isempty, allSessions)) = { 1 }; 182 | allSessions = cellfun(@num2str, allSessions, 'uniformoutput', false); 183 | uniqueSessions = unique(allSessions); 184 | 185 | % export STUDY to BIDS 186 | % -------------------- 187 | pInfo = {}; % each EEG file has its own pInfo --> need to aggregate 188 | if isfield(EEG(1), 'BIDS') && isfield(EEG(1).BIDS,'pInfo') 189 | pInfo = EEG(1).BIDS.pInfo(1,:); 190 | end 191 | subjects = struct('file',{}, 'session', [], 'run', [], 'task', {}); 192 | 193 | % duration field (mandatory) 194 | if ~isempty(EEG(1).event) && ~isfield(EEG(1).event, 'duration') 195 | for iEEG = 1:length(EEG) 196 | EEG(iEEG).event(1).duration = []; 197 | EEG(iEEG).saved = 'no'; 198 | end 199 | end 200 | 201 | % resave dataset 202 | fileAbsent = any(cellfun(@isempty, { EEG.filename })); 203 | if fileAbsent 204 | error('Datasets need to be saved before being exported'); 205 | end 206 | saved = any(cellfun(@(x)isequal(x, 'no'), { EEG.saved })); 207 | 208 | for iSubj = 1:length(uniqueSubjects) 209 | indS = strmatch( uniqueSubjects{iSubj}, allSubjects, 'exact' ); 210 | for iFile = 1:length(indS) 211 | subjects(iSubj).file{iFile} = fullfile( EEG(indS(iFile)).filepath, EEG(indS(iFile)).filename); 212 | 213 | if isfield(EEG(indS(iFile)), 'session') && ~isempty(EEG(indS(iFile)).session) 214 | subjects(iSubj).session(iFile) = EEG(indS(iFile)).session; 215 | else 216 | subjects(iSubj).session(iFile) = iFile; 217 | end 218 | if isfield(EEG(indS(iFile)), 'run') && ~isempty(EEG(indS(iFile)).run) 219 | subjects(iSubj).run(iFile) = EEG(indS(iFile)).run; 220 | else 221 | subjects(iSubj).run(iFile) = 1; % Assume only one run 222 | end 223 | if isfield(EEG(indS(iFile)), 'task') && ~isempty(EEG(indS(iFile)).task) 224 | subjects(iSubj).task{iFile} = EEG(indS(iFile)).task; 225 | % blank task field will be filled in bids_export.m 226 | end 227 | if isfield(EEG(indS(iFile)).BIDS, 'tInfo') && isfield(EEG(indS(iFile)).BIDS.tInfo, 'SubjectArtefactDescription')... 228 | && ~isempty(EEG(indS(iFile)).BIDS.tInfo.SubjectArtefactDescription) 229 | subjects(iSubj).notes{iFile} = EEG(indS(iFile)).BIDS.tInfo.SubjectArtefactDescription; 230 | end 231 | end 232 | if isfield(EEG(indS(1)), 'BIDS') && isfield(EEG(indS(1)).BIDS,'pInfo') 233 | pInfo = [pInfo; EEG(indS(1)).BIDS.pInfo(2,:)]; 234 | end 235 | end 236 | if ~isempty(pInfo) 237 | options = [options 'pInfo' {pInfo}]; 238 | end 239 | if nargin < 3 240 | bids_export(subjects, 'interactive', 'on', options{:}); 241 | else 242 | bids_export(subjects, options{:}); 243 | end 244 | disp('Done'); 245 | 246 | % history 247 | % ------- 248 | if nargin < 3 249 | % Issue: README file and other inserted as plain text 250 | % The history should have the relevant fields 251 | % comOut = sprintf('pop_exportbids(STUDY, %s);', vararg2str(options)); 252 | end 253 | -------------------------------------------------------------------------------- /pop_validatebids.m: -------------------------------------------------------------------------------- 1 | % pop_validatebids() - Validate BIDS dataset. Validation result will be 2 | % printed on command window 3 | % Adopting Openneuro's command-line bids-validator 4 | % https://github.com/bids-standard/bids-validator 5 | % 6 | % Usage: 7 | % >> pop_validatebids(); % open new window to input/select BIDS dataset 8 | % >> pop_validatebids(datasetPath); % validate dataset given provided path 9 | % 10 | function pop_validatebids(varargin) 11 | if ~plugin_status('bids-validator') 12 | plugin_askinstall('bids-validator',[],true); 13 | end 14 | if ismac 15 | validator = 'bids-validator-macos'; 16 | elseif isunix 17 | validator = 'bids-validator-linux'; 18 | elseif ispc 19 | validator = 'bids-validator-win.exe'; 20 | end 21 | filepath = fullfile(fileparts(which('eegplugin_bidsvalidator')), validator); 22 | if ismac || isunix 23 | system(['chmod u+x ' filepath]); 24 | end 25 | if ~exist(filepath,'file') 26 | supergui('geomhoriz', {[1] [1] [1]}, 'geomvert', [1 1 1], 'uilist', {{'Style','text','string','No validator found. Abort'},... 27 | {}, ... 28 | { 'Style', 'pushbutton', 'string', 'Ok' 'callback' 'close(gcf)' }}, 'title','Error -- pop_validatebids()'); 29 | waitfor(gcf); 30 | else 31 | if nargin == 1 && ischar(varargin{1}) && isfolder(varargin{1}) 32 | system([filepath ' ' varargin{1}]); 33 | else 34 | com = [ 'bidsFolderxx = uigetdir(''Pick a BIDS output folder'');' ... 35 | 'if ~isequal(bidsFolderxx, 0), set(findobj(gcbf, ''tag'', ''outputfolder''), ''string'', bidsFolderxx); end;' ... 36 | 'clear bidsFolderxx;' ]; 37 | uilist = { ... 38 | { 'Style', 'text', 'string', 'Validate BIDS dataset', 'fontweight', 'bold' }, ... 39 | { 'Style', 'text', 'string', 'BIDS folder:' }, ... 40 | { 'Style', 'edit', 'string', fullfile('.', 'BIDS_EXPORT') 'tag' 'outputfolder' }, ... 41 | { 'Style', 'pushbutton', 'string', '...' 'callback' com }, ... 42 | { 'Style', 'text', 'string', '' }, ... 43 | { 'Style', 'pushbutton', 'string', 'Validate' 'tag' 'validateBtn' 'callback' @validateCB }, ... 44 | { 'Style', 'text', 'string', ''}, ... 45 | }; 46 | geometry = { [1] [0.2 0.7 0.1] [1 1 1] }; 47 | geomvert = [1 1 1]; 48 | supergui( 'geomhoriz', geometry, 'geomvert', geomvert, 'uilist', uilist, 'title', 'Validate BIDS dataset-- pop_validatebids()'); 49 | end 50 | end 51 | function validateCB(src, event) 52 | obj = findobj('Tag','outputfolder'); 53 | dir = obj.String; 54 | system([filepath ' ' dir]); 55 | close(gcf); 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /rename_brainvision_files.m: -------------------------------------------------------------------------------- 1 | function rename_brainvision_files(varargin) 2 | 3 | % RENAME_BRAINVISION_FILES renames a BrainVision EEG dataset, which consists of a vhdr header 4 | % file, vmrk marker file and a data file that usually has the extension dat, eeg or seg. 5 | % 6 | % Use as 7 | % rename_brainvision_files(oldname, newname, 'rmf', 'on') 8 | % where both the old and the new filename should be strings corresponding to the 9 | % header file, i.e. including the vhdr extension. 10 | % 'rmf' option indicates to remove old files and can be turned 'on' of 'off' (default) 11 | % 12 | % See also http://www.fieldtriptoolbox.org/ and https://sccn.ucsd.edu/wiki/EEGLAB for 13 | % open source software to process BrainVision EEG data. 14 | % 15 | % Robert Oostenveld https://gist.github.com/robertoostenveld/e31637a777c514bf1e86272e1092316e 16 | % Cyril Pernet - fixed few bugs here and there https://gist.github.com/CPernet/e037df46e064ca83a49fb4c595d4566a 17 | 18 | %% deal with inputs 19 | 20 | rmf = 'off'; % by default do not delete files 21 | if nargin >= 3 22 | if strcmpi(varargin{3},'rmf') 23 | if nargin == 3 24 | disp('no value associated to the remove file option, assumning off') 25 | else 26 | rmf = varargin{4}; 27 | end 28 | else 29 | error(['unrecognized option argument ''' varargin{3} '''']) 30 | end 31 | end 32 | 33 | oldheaderfile = varargin{1}; 34 | newheaderfile = varargin{2}; 35 | clear varargin 36 | 37 | % determine whether the file extensions should be in lower or upper case 38 | if ~isempty(regexp(newheaderfile, 'VHDR$', 'once')) 39 | switchcase = @upper; 40 | else 41 | switchcase = @lower; 42 | end 43 | 44 | % determine the filename without extension 45 | [pathIn , baseNameIn, ~] = fileparts(oldheaderfile); 46 | [pathOut, baseNameOut, ~] = fileparts(newheaderfile); 47 | 48 | %% do the renaming 49 | 50 | % deal with the header file 51 | assert(exist(oldheaderfile, 'file')~=0, 'the file %s does not exists', oldheaderfile); 52 | assert(exist(newheaderfile, 'file')==0, 'the file %s already exists', newheaderfile); 53 | fid1 = fopen(oldheaderfile, 'r'); % read old 54 | fid2 = fopen(newheaderfile, 'w'); % write new 55 | 56 | while ~feof(fid1) 57 | line = fgetl(fid1); 58 | if ~isempty(regexp(line, '^MarkerFile', 'once')) 59 | [~, rem] = strtok(line, '='); 60 | oldmarkerfile = rem(2:end); 61 | [~, ~, x] = fileparts(oldmarkerfile); 62 | newmarkerfile = [baseNameOut switchcase(x)]; % use relative path 63 | line = sprintf('MarkerFile=%s', newmarkerfile); 64 | elseif ~isempty(regexp(line, '^DataFile', 'once')) 65 | [~, rem] = strtok(line, '='); 66 | olddatafile = rem(2:end); 67 | [~, ~, x] = fileparts(olddatafile); 68 | newdatafile = [baseNameOut switchcase('.eeg')]; % must be .eeg 69 | line = sprintf('DataFile=%s', newdatafile); 70 | else 71 | end 72 | fprintf(fid2, '%s\r\n', line); 73 | end 74 | fclose(fid1); 75 | fclose(fid2); 76 | 77 | if exist('oldmarkerfile', 'var') 78 | oldmarkerfile = fullfile(pathIn , oldmarkerfile); 79 | newmarkerfile = fullfile(pathOut, newmarkerfile); 80 | assert(exist(newmarkerfile, 'file')==0, 'the file %s already exists', newmarkerfile); 81 | fid1 = fopen(oldmarkerfile, 'r'); 82 | 83 | if fid1 == -1 84 | warning('Marker file specified but not found') 85 | else 86 | fid2 = fopen(newmarkerfile, 'w'); 87 | 88 | while ~feof(fid1) 89 | line = fgetl(fid1); 90 | if ~isempty(regexp(line, '^HeaderFile', 'once')) 91 | [~, rem] = strtok(line, '='); 92 | oldheaderfile = rem(2:end); 93 | [~, ~, x] = fileparts(oldheaderfile); 94 | newheaderfile = [baseNameOut switchcase(x)]; 95 | line = sprintf('HeaderFile=%s', newheaderfile); 96 | elseif ~isempty(regexp(line, '^DataFile', 'once')) 97 | [~, rem] = strtok(line, '='); 98 | olddatafile = rem(2:end); 99 | [~, ~, x] = fileparts(olddatafile); 100 | newdatafile = [baseNameOut switchcase('.eeg')]; 101 | line = sprintf('DataFile=%s', newdatafile); 102 | end 103 | fprintf(fid2, '%s\r\n', line); 104 | end 105 | fclose(fid1); 106 | fclose(fid2); 107 | end 108 | end 109 | 110 | olddatafile = fullfile(pathIn , olddatafile); 111 | newdatafile = fullfile(pathOut, newdatafile); 112 | 113 | % deal with the data file 114 | assert(exist(newdatafile, 'file')==0, 'the file %s already exists', newdatafile); 115 | status = copyfile( olddatafile, newdatafile); 116 | if ~status 117 | error('failed to copy data from %s to %s', olddatafile, newdatafile); 118 | end 119 | 120 | %% delete old files *try* in case of user restriction 121 | if strcmpi(rmf,'on') 122 | try delete(oldheaderfile); end 123 | try delete(oldmarkerfile); end 124 | try delete(olddatafile); end 125 | end 126 | -------------------------------------------------------------------------------- /sort_nat.m: -------------------------------------------------------------------------------- 1 | function [cs,index] = sort_nat(c,mode) 2 | %sort_nat: Natural order sort of cell array of strings. 3 | % usage: [S,INDEX] = sort_nat(C) 4 | % 5 | % where, 6 | % C is a cell array (vector) of strings to be sorted. 7 | % S is C, sorted in natural order. 8 | % INDEX is the sort order such that S = C(INDEX); 9 | % 10 | % Natural order sorting sorts strings containing digits in a way such that 11 | % the numerical value of the digits is taken into account. It is 12 | % especially useful for sorting file names containing index numbers with 13 | % different numbers of digits. Often, people will use leading zeros to get 14 | % the right sort order, but with this function you don't have to do that. 15 | % For example, if C = {'file1.txt','file2.txt','file10.txt'}, a normal sort 16 | % will give you 17 | % 18 | % {'file1.txt' 'file10.txt' 'file2.txt'} 19 | % 20 | % whereas, sort_nat will give you 21 | % 22 | % {'file1.txt' 'file2.txt' 'file10.txt'} 23 | % 24 | % See also: sort 25 | % Version: 1.4, 22 January 2011 26 | % Author: Douglas M. Schwarz 27 | % Email: dmschwarz=ieee*org, dmschwarz=urgrad*rochester*edu 28 | % Real_email = regexprep(Email,{'=','*'},{'@','.'}) 29 | % Set default value for mode if necessary. 30 | if nargin < 2 31 | mode = 'ascend'; 32 | end 33 | % Make sure mode is either 'ascend' or 'descend'. 34 | modes = strcmpi(mode,{'ascend','descend'}); 35 | is_descend = modes(2); 36 | if ~any(modes) 37 | error('sort_nat:sortDirection',... 38 | 'sorting direction must be ''ascend'' or ''descend''.') 39 | end 40 | % Replace runs of digits with '0'. 41 | c2 = regexprep(c,'\d+','0'); 42 | % Compute char version of c2 and locations of zeros. 43 | s1 = char(c2); 44 | z = s1 == '0'; 45 | % Extract the runs of digits and their start and end indices. 46 | [digruns,first,last] = regexp(c,'\d+','match','start','end'); 47 | % Create matrix of numerical values of runs of digits and a matrix of the 48 | % number of digits in each run. 49 | num_str = length(c); 50 | max_len = size(s1,2); 51 | num_val = NaN(num_str,max_len); 52 | num_dig = NaN(num_str,max_len); 53 | for i = 1:num_str 54 | num_val(i,z(i,:)) = sscanf(sprintf('%s ',digruns{i}{:}),'%f'); 55 | num_dig(i,z(i,:)) = last{i} - first{i} + 1; 56 | end 57 | % Find columns that have at least one non-NaN. Make sure activecols is a 58 | % 1-by-n vector even if n = 0. 59 | activecols = reshape(find(~all(isnan(num_val))),1,[]); 60 | n = length(activecols); 61 | % Compute which columns in the composite matrix get the numbers. 62 | numcols = activecols + (1:2:2*n); 63 | % Compute which columns in the composite matrix get the number of digits. 64 | ndigcols = numcols + 1; 65 | % Compute which columns in the composite matrix get chars. 66 | charcols = true(1,max_len + 2*n); 67 | charcols(numcols) = false; 68 | charcols(ndigcols) = false; 69 | % Create and fill composite matrix, comp. 70 | comp = zeros(num_str,max_len + 2*n); 71 | comp(:,charcols) = double(s1); 72 | comp(:,numcols) = num_val(:,activecols); 73 | comp(:,ndigcols) = num_dig(:,activecols); 74 | % Sort rows of composite matrix and use index to sort c in ascending or 75 | % descending order, depending on mode. 76 | [unused,index] = sortrows(comp); 77 | if is_descend 78 | index = index(end:-1:1); 79 | end 80 | index = reshape(index,size(c)); 81 | cs = c(index); --------------------------------------------------------------------------------