Skip to content

Commit

Permalink
IO: Support for Neuroelectrics EEG with EEGLAB plugin (.easy, .nedf)
Browse files Browse the repository at this point in the history
  • Loading branch information
ftadel committed Jan 9, 2022
1 parent f402ed3 commit 12fadd2
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 33 deletions.
2 changes: 1 addition & 1 deletion doc/license.html
Expand Up @@ -5,7 +5,7 @@
<body alink="#fff000" link="#fff000" vlink="#fff000">
<h4><span style="font-family: Arial Black; color: #ffffff;"><strong>THERE IS NO UNDO BUTTON - SET UP A BACKUP OF YOUR DATABASE</strong></span></h4>
<HR>
<!-- LICENCE_START -->Version: 3.220108 (08-Jan-2022)<br>
<!-- LICENCE_START -->Version: 3.220109 (09-Jan-2022)<br>
<span style="font-style: italic;">COPYRIGHT &copy; 2000-2022
USC &amp; McGill University.<br>
</span>
Expand Down
4 changes: 3 additions & 1 deletion doc/updates.txt
@@ -1,10 +1,12 @@
--------------------------------------------------------------
January 2022
- Plugins: Archive software environment (brainstorm + plugins + defaults)
- IO: Added support for Neuroelectrics EEG with EEGLAB plugin (.easy, .nedf)
--------------------------------------------------------------
December 2021
- IO: Added support for Tobii Pro Glasses export .tsv
- IO: Add support for FieldTrip trialinfo field
- IO: Added support for FieldTrip trialinfo field
- IO: Updated York Instrument MEGSCAN reader (.meghdf5)
- Anatomy: Add support for InfantFS / infant_recon_all
- SimMEEG: Video tutorials
- Distrib: Removed JAVA3D/JOGL support + older connectivity graph display
Expand Down
2 changes: 1 addition & 1 deletion doc/version.txt
@@ -1,2 +1,2 @@
% Brainstorm
% v. 3.220108 (08-Jan-2022)
% v. 3.220109 (09-Jan-2022)
3 changes: 3 additions & 0 deletions toolbox/core/bst_get.m
Expand Up @@ -3488,6 +3488,8 @@
{'.mat'}, 'EEG: Matlab matrix (*.mat)', 'EEG-MAT'; ...
{'.csv'}, 'EEG: Muse (*.csv)', 'EEG-MUSE-CSV'; ...
{'.ncs'}, 'EEG: Neuralynx (*.ncs)', 'EEG-NEURALYNX'; ...
{'.nwb'}, 'EEG: Neurodata Without Borders (*.nwb)','NWB'; ...
{'.nedf','.easy'}, 'EEG: Neuroelectrics (*.nedf;*.easy)', 'EEG-NEUROELECTRICS'; ...
{'.bin'}, 'EEG: NeurOne session folder', 'EEG-NEURONE'; ...
{'.cnt','.avg','.eeg','.dat'}, 'EEG: Neuroscan (*.cnt;*.eeg;*.avg;*.dat)', 'EEG-NEUROSCAN'; ...
{'.eeg','.dat'}, 'EEG: NeuroScope (*.eeg;*.dat)', 'EEG-NEUROSCOPE'; ...
Expand Down Expand Up @@ -3545,6 +3547,7 @@
{'.trc'}, 'EEG: Micromed (*.trc)', 'EEG-MICROMED'; ...
{'.ncs'}, 'EEG: Neuralynx (*.ncs)', 'EEG-NEURALYNX'; ...
{'.nwb'}, 'EEG: Neurodata Without Borders (*.nwb)','NWB'; ...
{'.nedf','.easy'}, 'EEG: Neuroelectrics (*.nedf;*.neasy)', 'EEG-NEUROELECTRICS'; ...
{'.bin'}, 'EEG: NeurOne session folder', 'EEG-NEURONE'; ...
{'.cnt','.avg','.eeg','.dat'}, 'EEG: Neuroscan (*.cnt;*.eeg;*.avg;*.dat)', 'EEG-NEUROSCAN'; ...
{'.eeg','.dat'}, 'EEG: NeuroScope (*.eeg;*.dat)', 'EEG-NEUROSCOPE'; ...
Expand Down
15 changes: 15 additions & 0 deletions toolbox/core/bst_plugin.m
Expand Up @@ -294,6 +294,21 @@
PlugDesc(end).LoadedFcn = @Configure;
% Stable version: http://neuroimage.usc.edu/bst/getupdate.php?d='mffmatlabio-3.5.zip'

% === I/O: NEUROELECTRICS ===
PlugDesc(end+1) = GetStruct('neuroelectrics');
PlugDesc(end).Version = '1.8';
PlugDesc(end).Category = 'I/O';
PlugDesc(end).AutoUpdate = 0;
PlugDesc(end).URLzip = 'https://sccn.ucsd.edu/eeglab/plugins/Neuroelectrics1.8.zip';
PlugDesc(end).URLinfo = 'https://www.neuroelectrics.com/wiki/index.php/EEGLAB';
PlugDesc(end).TestFile = 'pop_nedf.m';
PlugDesc(end).ReadmeFile = 'README.txt';
PlugDesc(end).CompiledStatus = 2;
PlugDesc(end).InstalledFcn = ['d=pwd; cd(fileparts(which(''pop_nedf''))); mkdir(''private''); ' ...
'f=fopen(''private' filesep 'eeg_emptyset.m'',''wt''); fprintf(f,''function EEG=eeg_emptyset()\nEEG=struct();''); fclose(f);' ...
'f=fopen(''private' filesep 'eeg_checkset.m'',''wt''); fprintf(f,''function EEG=eeg_checkset(EEG)''); fclose(f);' ...
'cd(d);'];

% === I/O: NWB ===
PlugDesc(end+1) = GetStruct('nwb');
PlugDesc(end).Version = 'github-master';
Expand Down
61 changes: 32 additions & 29 deletions toolbox/io/in_channel_eeglab_set.m
Expand Up @@ -44,16 +44,19 @@
ChannelFile = '';
end
% Convert Channel locations to Brainstorm format
if isfield(SetFileMat.EEG, 'chanlocs') && ~isempty(SetFileMat.EEG.chanlocs) && isfield(SetFileMat.EEG.chanlocs(1), 'X')
if isfield(SetFileMat.EEG, 'chanlocs') && ~isempty(SetFileMat.EEG.chanlocs)
% Check coordinates sytem
isNormalizedCs = (max([SetFileMat.EEG.chanlocs.X]) <= 1);
if isempty(isNormalizedCs)
isNormalizedCs = 0;
elseif isNormalizedCs && isempty(isFixUnits)
isFixUnits = java_dialog('confirm', ['The EEGLAB file you selected contains 3D electrodes positions, but they' 10 ...
'seem to be spherical projections in a normalized coordinates system.' 10 ...
'Brainstorm needs real 3D positions, you may need to import them separately.' 10 10 ...
'Would you like Brainstorm to try to convert these positions ?'], 'EEGLAB electrodes positions');
isLoc = isfield(SetFileMat.EEG.chanlocs(1), 'X');
if isLoc
isNormalizedCs = (max([SetFileMat.EEG.chanlocs.X]) <= 1);
if isempty(isNormalizedCs)
isNormalizedCs = 0;
elseif isNormalizedCs && isempty(isFixUnits)
isFixUnits = java_dialog('confirm', ['The EEGLAB file you selected contains 3D electrodes positions, but they' 10 ...
'seem to be spherical projections in a normalized coordinates system.' 10 ...
'Brainstorm needs real 3D positions, you may need to import them separately.' 10 10 ...
'Would you like Brainstorm to try to convert these positions ?'], 'EEGLAB electrodes positions');
end
end
% Initialize returned structure
nbChannels = SetFileMat.EEG.nbchan;
Expand All @@ -70,35 +73,35 @@
end
ChannelMat.Channel(iChan).Name = SetFileMat.EEG.chanlocs(iChan).labels;
% Electrode location
if isempty(SetFileMat.EEG.chanlocs(iChan).X) || isempty(SetFileMat.EEG.chanlocs(iChan).Y) || isempty(SetFileMat.EEG.chanlocs(iChan).Z)
ChannelMat.Channel(iChan).Loc = [];
if strcmpi(ChannelMat.Channel(iChan).Type, 'EEG')
ChannelMat.Channel(iChan).Type = 'Misc';
if isLoc
if isempty(SetFileMat.EEG.chanlocs(iChan).X) || isempty(SetFileMat.EEG.chanlocs(iChan).Y) || isempty(SetFileMat.EEG.chanlocs(iChan).Z)
ChannelMat.Channel(iChan).Loc = [];
if strcmpi(ChannelMat.Channel(iChan).Type, 'EEG')
ChannelMat.Channel(iChan).Type = 'Misc';
end
elseif ~isNormalizedCs
ChannelMat.Channel(iChan).Loc = [SetFileMat.EEG.chanlocs(iChan).X; ...
SetFileMat.EEG.chanlocs(iChan).Y; ...
SetFileMat.EEG.chanlocs(iChan).Z] ./ 1000;
elseif isFixUnits
ChannelMat.Channel(iChan).Loc = [SetFileMat.EEG.chanlocs(iChan).X / 9.4 + 0.007; ...
SetFileMat.EEG.chanlocs(iChan).Y / 11.5; ...
SetFileMat.EEG.chanlocs(iChan).Z / 8.7 + 0.042];
else
ChannelMat.Channel(iChan).Loc = [SetFileMat.EEG.chanlocs(iChan).X; ...
SetFileMat.EEG.chanlocs(iChan).Y; ...
SetFileMat.EEG.chanlocs(iChan).Z] / 10;
end
elseif ~isNormalizedCs
ChannelMat.Channel(iChan).Loc = [SetFileMat.EEG.chanlocs(iChan).X; ...
SetFileMat.EEG.chanlocs(iChan).Y; ...
SetFileMat.EEG.chanlocs(iChan).Z] ./ 1000;
% FT 14-Jun-2017: Removed this weird translation... check why this was in there in the first place
% ChannelMat.Channel(iChan).Loc = [SetFileMat.EEG.chanlocs(iChan).X + 22; ...
% SetFileMat.EEG.chanlocs(iChan).Y; ...
% SetFileMat.EEG.chanlocs(iChan).Z + 57] ./ 1000;
elseif isFixUnits
ChannelMat.Channel(iChan).Loc = [SetFileMat.EEG.chanlocs(iChan).X / 9.4 + 0.007; ...
SetFileMat.EEG.chanlocs(iChan).Y / 11.5; ...
SetFileMat.EEG.chanlocs(iChan).Z / 8.7 + 0.042];
else
ChannelMat.Channel(iChan).Loc = [SetFileMat.EEG.chanlocs(iChan).X; ...
SetFileMat.EEG.chanlocs(iChan).Y; ...
SetFileMat.EEG.chanlocs(iChan).Z] / 10;
ChannelMat.Channel(iChan).Loc = [];
end
ChannelMat.Channel(iChan).Orient = [];
ChannelMat.Channel(iChan).Comment = '';
ChannelMat.Channel(iChan).Weight = 1;
end

% Check distance units
if ~isNormalizedCs && ~isequal(isFixUnits, 0)
if isLoc && ~isNormalizedCs && ~isequal(isFixUnits, 0)
if isempty(isFixUnits)
isConfirmFix = 1;
else
Expand Down
93 changes: 93 additions & 0 deletions toolbox/io/in_data_neuroelectrics.m
@@ -0,0 +1,93 @@
function [DataMat, ChannelMat] = in_data_neuroelectrics(DataFile)
% IN_DATA_MUSE_CSV: Imports a Neuroelectrics .easy/.info or .nedf file, using EEGLAB plugin.
%
% File formats specification:
% https://www.neuroelectrics.com/wiki/index.php/Files_%26_Formats
%
% EEGLAB plugin:
% https://sccn.ucsd.edu/eeglab/plugin_uploader/plugin_list_all.php
%
% USAGE: [DataMat, ChannelMat] = in_data_neuroelectrics(DataFile, sfreq=[ask], isInteractive=0);

% @=============================================================================
% This function is part of the Brainstorm software:
% https://neuroimage.usc.edu/brainstorm
%
% Copyright (c) University of Southern California & McGill University
% This software is distributed under the terms of the GNU General Public License
% as published by the Free Software Foundation. Further details on the GPLv3
% license can be found at http://www.gnu.org/copyleft/gpl.html.
%
% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE
% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY
% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY
% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE.
%
% For more information type "brainstorm license" at command prompt.
% =============================================================================@
%
% Authors: Francois Tadel, 2022


% ===== INSTALL EEGLAB PLUGIN =====
if ~exist('pop_nedf', 'file')
[isInstalled, errMsg] = bst_plugin('Install', 'neuroelectrics');
if ~isInstalled
error(errMsg);
end
end


% ===== READ FILE =====
bst_progress('text', 'Reading file (EEGLAB plugin)...');
% Get file extension
[fPath, fBase, fExt] = bst_fileparts(DataFile);
switch lower(fExt)
% NEDF: Binary
case '.nedf'
% Change the current directory to the plugin path, because the EEGLAB plugin adds stuff to the Matlab path in its own way...
curDir = pwd;
cd(bst_fileparts(which('pop_easy')));
% Read file: pop_nedf(file,acc,locs,channels_selected)
EEG = pop_nedf(DataFile, 1, 0, []);
% Restore Matlab directory
cd(curDir);
% EASY: Text file + .info header file
case '.easy'
% Read file: pop_easy(file,acc,locs,channels_selected)
EEG = pop_easy(DataFile, 1, 0, []);
end


% ===== CONVERT EEGLAB TO BRAINSTORM =====
bst_progress('text', 'Converting data structures...');
% Convert EEGLAB structure to continuous Brainstorm structure
EEG.filename = DataFile;
EEG.xmin = 0;
[sFile, ChannelMat] = in_fopen_eeglab(EEG);

% Change the type of accelerometers
for iChan = 1:length(ChannelMat.Channel)
if ismember(ChannelMat.Channel(iChan).Name, {'x','y','z'})
ChannelMat.Channel(iChan).Type = 'Accelerometer';
end
end
ChannelMat.Comment = 'Neuroelectrics channels';

% Read data
ImportOptions = db_template('ImportOptions');
ImportOptions.ImportMode = 'Time';
ImportOptions.DisplayMessages = 0;
[F, Time] = in_fread(sFile, ChannelMat, 1, [], [], ImportOptions);

% Convert to imported Brainstorm data structure
DataMat = db_template('DataMat');
DataMat.F = F;
DataMat.Time = Time;
DataMat.Comment = fBase;
DataMat.ChannelFlag = sFile.channelflag;
DataMat.nAvg = 1;
DataMat.Device = 'Neuroelectrics';
DataMat.Events = sFile.events;

2 changes: 2 additions & 0 deletions toolbox/io/in_fopen.m
Expand Up @@ -185,6 +185,8 @@
[DataMat, ChannelMat] = in_data_ws_csv(DataFile);
case 'EEG-MAT'
DataMat = in_data_mat(DataFile);
case 'EEG-NEUROELECTRICS'
[DataMat, ChannelMat] = in_data_neuroelectrics(DataFile);
case 'EEG-NEUROSCAN-DAT'
DataMat = in_data_neuroscan_dat(DataFile);
case 'EEG-TVB'
Expand Down
2 changes: 1 addition & 1 deletion toolbox/io/in_fopen_eeglab.m
Expand Up @@ -53,7 +53,7 @@
end
end
% Add some information
hdr.isRaw = isempty(hdr.EEG.epoch) && ~isempty(hdr.EEG.data);
hdr.isRaw = (~isfield(hdr.EEG, 'epoch') || isempty(hdr.EEG.epoch)) && (isfield(hdr.EEG, 'data') && ~isempty(hdr.EEG.data));
nChannels = hdr.EEG.nbchan;
nTime = hdr.EEG.pnts;
nEpochs = hdr.EEG.trials;
Expand Down

0 comments on commit 12fadd2

Please sign in to comment.