1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
|
"""Conversion tool from Brain Vision EEG to FIF"""
# Authors: Teon Brooks <teon@nyu.edu>
# Christian Brodbeck <christianbrodbeck@nyu.edu>
#
# License: BSD (3-clause)
import os
import time
import re
import warnings
import numpy as np
from ...coreg import get_ras_to_neuromag_trans, read_elp
from ...transforms import als_ras_trans, apply_trans
from ...utils import verbose, logger
from ..constants import FIFF
from ..meas_info import Info
from ..base import _BaseRaw
from ...externals.six import StringIO, u
from ...externals.six.moves import configparser
class RawBrainVision(_BaseRaw):
"""Raw object from Brain Vision EEG file
Parameters
----------
vdhr_fname : str
Path to the EEG header file.
elp_fname : str | None
Path to the elp file containing electrode positions.
If None, sensor locations are (0,0,0).
elp_names : list | None
A list of channel names in the same order as the points in the elp
file. Electrode positions should be specified with the same names as
in the vhdr file, and fiducials should be specified as "lpa" "nasion",
"rpa". ELP positions with other names are ignored. If elp_names is not
None and channels are missing, a KeyError is raised.
preload : bool
If True, all data are loaded at initialization.
If False, data are not read until save.
reference : None | str
Name of the electrode which served as the reference in the recording.
If a name is provided, a corresponding channel is added and its data
is set to 0. This is useful for later re-referencing. The name should
correspond to a name in elp_names.
eog : list of str
Names of channels that should be designated EOG channels. Names should
correspond to the vhdr file (default: ['HEOGL', 'HEOGR', 'VEOGb']).
verbose : bool, str, int, or None
If not None, override default verbose level (see mne.verbose).
See Also
--------
mne.io.Raw : Documentation of attribute and methods.
"""
@verbose
def __init__(self, vhdr_fname, elp_fname=None, elp_names=None,
preload=False, reference=None,
eog=['HEOGL', 'HEOGR', 'VEOGb'], ch_names=None, verbose=None):
# backwards compatibility
if ch_names is not None:
if elp_names is not None:
err = ("ch_names is a deprecated parameter, don't specify "
"ch_names if elp_names are specified.")
raise TypeError(err)
msg = "The ch_names parameter is deprecated. Use elp_names."
warnings.warn(msg, DeprecationWarning)
elp_names = ['nasion', 'lpa', 'rpa', None, None, None, None,
None] + list(ch_names)
# Preliminary Raw attributes
self._events = np.empty((0, 3))
self.preload = False
# Channel info and events
logger.info('Extracting eeg Parameters from %s...' % vhdr_fname)
vhdr_fname = os.path.abspath(vhdr_fname)
self.info, self._eeg_info, events = _get_eeg_info(vhdr_fname,
elp_fname, elp_names,
reference, eog)
self.set_brainvision_events(events)
logger.info('Creating Raw.info structure...')
# Raw attributes
self.verbose = verbose
self._filenames = list()
self._projector = None
self.comp = None # no compensation for EEG
self.proj = False
self.first_samp = 0
with open(self.info['file_id'], 'rb') as f:
f.seek(0, os.SEEK_END)
n_samples = f.tell()
dtype = int(self._eeg_info['dtype'][-1])
n_chan = self.info['nchan']
self.last_samp = (n_samples // (dtype * (n_chan - 1))) - 1
self._reference = reference
if preload:
self.preload = preload
logger.info('Reading raw data from %s...' % vhdr_fname)
self._data, _ = self._read_segment()
assert len(self._data) == self.info['nchan']
# Add time info
self._times = np.arange(self.first_samp, self.last_samp + 1,
dtype=np.float64)
self._times /= self.info['sfreq']
logger.info(' Range : %d ... %d = %9.3f ... %9.3f secs'
% (self.first_samp, self.last_samp,
float(self.first_samp) / self.info['sfreq'],
float(self.last_samp) / self.info['sfreq']))
logger.info('Ready.')
def __repr__(self):
n_chan = self.info['nchan']
data_range = self.last_samp - self.first_samp + 1
s = ('%r' % os.path.basename(self.info['file_id']),
"n_channels x n_times : %s x %s" % (n_chan, data_range))
return "<RawEEG | %s>" % ', '.join(s)
def _read_segment(self, start=0, stop=None, sel=None, verbose=None,
projector=None):
"""Read a chunk of raw data
Parameters
----------
start : int, (optional)
first sample to include (first is 0). If omitted, defaults to the
first sample in data.
stop : int, (optional)
First sample to not include.
If omitted, data is included to the end.
sel : array, optional
Indices of channels to select.
projector : array
SSP operator to apply to the data.
verbose : bool, str, int, or None
If not None, override default verbose level (see mne.verbose).
Returns
-------
data : array, shape (n_channels, n_samples)
The data.
times : array, shape (n_samples,)
returns the time values corresponding to the samples.
"""
if sel is not None:
if len(sel) == 1 and sel[0] == 0 and start == 0 and stop == 1:
return (666, 666)
if projector is not None:
raise NotImplementedError('Currently does not handle projections.')
if stop is None:
stop = self.last_samp + 1
elif stop > self.last_samp + 1:
stop = self.last_samp + 1
# Initial checks
start = int(start)
stop = int(stop)
if start >= stop:
raise ValueError('No data in this range')
# assemble channel information
eeg_info = self._eeg_info
sfreq = self.info['sfreq']
chs = self.info['chs']
if self._reference:
chs = chs[:-1]
if len(self._events):
chs = chs[:-1]
n_eeg = len(chs)
cals = np.atleast_2d([chan_info['cal'] for chan_info in chs])
mults = np.atleast_2d([chan_info['unit_mul'] for chan_info in chs])
logger.info('Reading %d ... %d = %9.3f ... %9.3f secs...' %
(start, stop - 1, start / float(sfreq),
(stop - 1) / float(sfreq)))
# read data
dtype = np.dtype(eeg_info['dtype'])
buffer_size = (stop - start)
pointer = start * n_eeg * dtype.itemsize
with open(self.info['file_id'], 'rb') as f:
f.seek(pointer)
# extract data
data = np.fromfile(f, dtype=dtype, count=buffer_size * n_eeg)
if eeg_info['data_orientation'] == 'MULTIPLEXED':
data = data.reshape((n_eeg, -1), order='F')
elif eeg_info['data_orientation'] == 'VECTORIZED':
data = data.reshape((n_eeg, -1), order='C')
gains = cals * mults
data = data * gains.T
# add reference channel and stim channel (if applicable)
data_segments = [data]
if self._reference:
shape = (1, data.shape[1])
ref_channel = np.zeros(shape)
data_segments.append(ref_channel)
if len(self._events):
stim_channel = _synthesize_stim_channel(self._events, start, stop)
data_segments.append(stim_channel)
if len(data_segments) > 1:
data = np.vstack(data_segments)
if sel is not None:
data = data[sel]
logger.info('[done]')
times = np.arange(start, stop, dtype=float) / sfreq
return data, times
def get_brainvision_events(self):
"""Retrieve the events associated with the Brain Vision Raw object
Returns
-------
events : array, shape (n_events, 3)
Events, each row consisting of an (onset, duration, trigger)
sequence.
"""
return self._events.copy()
def set_brainvision_events(self, events):
"""Set the events (automatically updates the synthesized stim channel)
Parameters
----------
events : array, shape (n_events, 3)
Events, each row consisting of an (onset, duration, trigger)
sequence.
"""
events = np.copy(events)
if not events.ndim == 2 and events.shape[1] == 3:
raise ValueError("[n_events x 3] shaped array required")
# update info based on presence of stim channel
had_events = bool(len(self._events))
has_events = bool(len(events))
if had_events and not has_events: # remove stim channel
if self.info['ch_names'][-1] != 'STI 014':
err = "Last channel is not stim channel; info was modified"
raise RuntimeError(err)
self.info['nchan'] -= 1
del self.info['ch_names'][-1]
del self.info['chs'][-1]
if self.preload:
self._data = self._data[:-1]
elif has_events and not had_events: # add stim channel
idx = len(self.info['chs']) + 1
chan_info = {'ch_name': 'STI 014',
'kind': FIFF.FIFFV_STIM_CH,
'coil_type': FIFF.FIFFV_COIL_NONE,
'logno': idx,
'scanno': idx,
'cal': 1,
'range': 1,
'unit_mul': 0,
'unit': FIFF.FIFF_UNIT_NONE,
'eeg_loc': np.zeros(3),
'loc': np.zeros(12)}
self.info['nchan'] += 1
self.info['ch_names'].append(chan_info['ch_name'])
self.info['chs'].append(chan_info)
if self.preload:
shape = (1, self._data.shape[1])
self._data = np.vstack((self._data, np.empty(shape)))
# update events
self._events = events
if has_events and self.preload:
start = self.first_samp
stop = self.last_samp + 1
self._data[-1] = _synthesize_stim_channel(events, start, stop)
def _read_vmrk_events(fname):
"""Read events from a vmrk file
Parameters
----------
fname : str
vmrk file to be read.
Returns
-------
events : array, shape (n_events, 3)
An array containing the whole recording's events, each row representing
an event as (onset, duration, trigger) sequence.
"""
# read vmrk file
with open(fname) as fid:
txt = fid.read()
start_tag = 'Brain Vision Data Exchange Marker File, Version 1.0'
if not txt.startswith(start_tag):
raise ValueError("vmrk file should start with %r" % start_tag)
# extract Marker Infos block
m = re.search("\[Marker Infos\]", txt)
if not m:
return np.zeros(0)
mk_txt = txt[m.end():]
m = re.search("\[.*\]", mk_txt)
if m:
mk_txt = mk_txt[:m.start()]
# extract event information
items = re.findall("^Mk\d+=(.*)", mk_txt, re.MULTILINE)
events = []
for info in items:
mtype, mdesc, onset, duration = info.split(',')[:4]
if mtype == 'Stimulus':
trigger = int(re.findall('S\s*?(\d+)', mdesc)[0])
onset = int(onset)
duration = int(duration)
events.append((onset, duration, trigger))
events = np.array(events)
return events
def _synthesize_stim_channel(events, start, stop):
"""Synthesize a stim channel from events read from a vmrk file
Parameters
----------
events : array, shape (n_events, 3)
Each row representing an event as (onset, duration, trigger) sequence
(the format returned by _read_vmrk_events).
start : int
First sample to return.
stop : int
Last sample to return.
Returns
-------
stim_channel : array, shape (n_samples,)
An array containing the whole recording's event marking
"""
# select events overlapping buffer
onset = events[:, 0]
offset = onset + events[:, 1]
idx = np.logical_and(onset < stop, offset > start)
events = events[idx]
# make onset relative to buffer
events[:, 0] -= start
# fix onsets before buffer start
idx = events[:, 0] < 0
events[idx, 0] = 0
# create output buffer
stim_channel = np.zeros(stop - start)
for onset, duration, trigger in events:
stim_channel[onset:onset + duration] = trigger
return stim_channel
def _get_elp_locs(elp_fname, elp_names):
"""Read a Polhemus ascii file
Parameters
----------
elp_fname : str
Path to head shape file acquired from Polhemus system and saved in
ascii format.
elp_names : list
A list in order of EEG electrodes found in the Polhemus digitizer file.
Returns
-------
ch_locs : dict
Dictionary whose keys are the names from elp_names and whose values
are the coordinates from the elp file transformed to Neuromag space.
"""
coords_orig = read_elp(elp_fname)
coords_ras = apply_trans(als_ras_trans, coords_orig)
chs_ras = dict(zip(elp_names, coords_ras))
nasion = chs_ras['nasion']
lpa = chs_ras['lpa']
rpa = chs_ras['rpa']
trans = get_ras_to_neuromag_trans(nasion, lpa, rpa)
coords_neuromag = apply_trans(trans, coords_ras)
chs_neuromag = dict(zip(elp_names, coords_neuromag))
return chs_neuromag
def _get_eeg_info(vhdr_fname, elp_fname, elp_names, reference, eog):
"""Extracts all the information from the header file.
Parameters
----------
vhdr_fname : str
Raw EEG header to be read.
elp_fname : str | None
Path to the elp file containing electrode positions.
If None, sensor locations are (0, 0, 0).
elp_names : list | None
A list of channel names in the same order as the points in the elp
file. Electrode positions should be specified with the same names as
in the vhdr file, and fiducials should be specified as "lpa" "nasion",
"rpa". ELP positions with other names are ignored. If elp_names is not
None and channels are missing, a KeyError is raised.
reference : None | str
Name of the electrode which served as the reference in the recording.
If a name is provided, a corresponding channel is added and its data
is set to 0. This is useful for later re-referencing. The name should
correspond to a name in elp_names.
eog : list of str
Names of channels that should be designated EOG channels. Names should
correspond to the vhdr file.
Returns
-------
info : Info
The measurement info.
edf_info : dict
A dict containing Brain Vision specific parameters.
events : array, shape (n_events, 3)
Events from the corresponding vmrk file.
"""
info = Info()
# Some keys to be consistent with FIF measurement info
info['meas_id'] = None
info['projs'] = []
info['comps'] = []
info['bads'] = []
info['acq_pars'], info['acq_stim'] = None, None
info['filename'] = vhdr_fname
info['ctf_head_t'] = None
info['dev_ctf_t'] = []
info['dig'] = None
info['dev_head_t'] = None
info['proj_id'] = None
info['proj_name'] = None
info['experimenter'] = None
info['description'] = None
info['buffer_size_sec'] = 10.
info['orig_blocks'] = None
info['line_freq'] = None
info['subject_info'] = None
eeg_info = {}
with open(vhdr_fname, 'r') as f:
# extract the first section to resemble a cfg
l = f.readline().strip()
assert l == 'Brain Vision Data Exchange Header File Version 1.0'
settings = f.read()
params, settings = settings.split('[Comment]')
cfg = configparser.ConfigParser()
if hasattr(cfg, 'read_file'): # newer API
cfg.read_file(StringIO(params))
else:
cfg.readfp(StringIO(params))
# get sampling info
# Sampling interval is given in microsec
sfreq = 1e6 / cfg.getfloat('Common Infos', 'SamplingInterval')
sfreq = int(sfreq)
n_data_chan = cfg.getint('Common Infos', 'NumberOfChannels')
n_eeg_chan = n_data_chan + bool(reference)
# check binary format
assert cfg.get('Common Infos', 'DataFormat') == 'BINARY'
eeg_info['data_orientation'] = cfg.get('Common Infos', 'DataOrientation')
if not (eeg_info['data_orientation'] == 'MULTIPLEXED' or
eeg_info['data_orientation'] == 'VECTORIZED'):
raise NotImplementedError('Data Orientation %s is not supported'
% eeg_info['data_orientation'])
binary_format = cfg.get('Binary Infos', 'BinaryFormat')
if binary_format == 'INT_16':
eeg_info['dtype'] = '<i2'
elif binary_format == 'INT_32':
eeg_info['dtype'] = '<i4'
elif binary_format == 'IEEE_FLOAT_32':
eeg_info['dtype'] = '<f4'
else:
raise NotImplementedError('Datatype %s is not supported'
% binary_format)
# load channel labels
ch_names = ['UNKNOWN'] * n_eeg_chan
cals = np.empty(n_eeg_chan)
cals[:] = np.nan
units = ['UNKNOWN'] * n_eeg_chan
for chan, props in cfg.items('Channel Infos'):
n = int(re.findall(r'ch(\d+)', chan)[0])
name, _, resolution, unit = props.split(',')[:4]
ch_names[n - 1] = name
cals[n - 1] = float(resolution)
unit = unit.replace('\xc2', '') # Remove unwanted control characters
if u(unit) == u('\xb5V'):
units[n - 1] = 1e-6
elif unit == 'V':
units[n - 1] = 0
else:
units[n - 1] = unit
# add reference channel info
if reference:
ch_names[-1] = reference
cals[-1] = cals[-2]
units[-1] = units[-2]
# Attempts to extract filtering info from header. If not found, both are
# set to zero.
settings = settings.splitlines()
idx = None
if 'Channels' in settings:
idx = settings.index('Channels')
settings = settings[idx + 1:]
for idx, setting in enumerate(settings):
if re.match('#\s+Name', setting):
break
else:
idx = None
if idx:
lowpass = []
highpass = []
for i, ch in enumerate(ch_names, 1):
if ch == reference:
continue
line = settings[idx + i].split()
assert ch in line
highpass.append(line[5])
lowpass.append(line[6])
if len(highpass) == 0:
info['highpass'] = None
elif all(highpass):
if highpass[0] == 'NaN':
info['highpass'] = None
elif highpass[0] == 'DC':
info['highpass'] = 0
else:
info['highpass'] = int(highpass[0])
else:
info['highpass'] = np.min(highpass)
warnings.warn('%s' % ('Channels contain different highpass '
'filters. Highest filter setting will '
'be stored.'))
if len(lowpass) == 0:
info['lowpass'] = None
elif all(lowpass):
if lowpass[0] == 'NaN':
info['lowpass'] = None
else:
info['lowpass'] = int(lowpass[0])
else:
info['lowpass'] = np.min(lowpass)
warnings.warn('%s' % ('Channels contain different lowpass filters.'
' Lowest filter setting will be stored.'))
else:
info['highpass'] = None
info['lowpass'] = None
# locate EEG and marker files
path = os.path.dirname(vhdr_fname)
info['file_id'] = os.path.join(path, cfg.get('Common Infos', 'DataFile'))
eeg_info['marker_id'] = os.path.join(path, cfg.get('Common Infos',
'MarkerFile'))
info['meas_date'] = int(time.time())
# Creates a list of dicts of eeg channels for raw.info
logger.info('Setting channel info structure...')
info['chs'] = []
info['nchan'] = n_eeg_chan
info['ch_names'] = ch_names
info['sfreq'] = sfreq
if elp_fname and elp_names:
ch_locs = _get_elp_locs(elp_fname, elp_names)
info['dig'] = [{'r': ch_locs['nasion'],
'ident': FIFF.FIFFV_POINT_NASION,
'kind': FIFF.FIFFV_POINT_CARDINAL,
'coord_frame': FIFF.FIFFV_COORD_HEAD},
{'r': ch_locs['lpa'], 'ident': FIFF.FIFFV_POINT_LPA,
'kind': FIFF.FIFFV_POINT_CARDINAL,
'coord_frame': FIFF.FIFFV_COORD_HEAD},
{'r': ch_locs['rpa'], 'ident': FIFF.FIFFV_POINT_RPA,
'kind': FIFF.FIFFV_POINT_CARDINAL,
'coord_frame': FIFF.FIFFV_COORD_HEAD}]
else:
ch_locs = None
missing_positions = []
idxs = range(1, len(ch_names) + 1)
for idx, ch_name, cal, unit_mul in zip(idxs, ch_names, cals, units):
is_eog = ch_name in eog
if ch_locs is None:
loc = np.zeros(3)
elif ch_name in ch_locs:
loc = ch_locs[ch_name]
else:
loc = np.zeros(3)
if not is_eog:
missing_positions.append(ch_name)
if is_eog:
kind = FIFF.FIFFV_EOG_CH
else:
kind = FIFF.FIFFV_EEG_CH
chan_info = {'ch_name': ch_name,
'coil_type': FIFF.FIFFV_COIL_EEG,
'kind': kind,
'logno': idx,
'scanno': idx,
'cal': cal,
'range': 1.,
'unit_mul': unit_mul,
'unit': FIFF.FIFF_UNIT_V,
'coord_frame': FIFF.FIFFV_COORD_HEAD,
'eeg_loc': loc,
'loc': np.hstack((loc, np.zeros(9)))}
info['chs'].append(chan_info)
# raise error if positions are missing
if missing_positions:
err = ("The following positions are missing from the ELP "
"definitions: %s. If those channels lack positions because "
"they are EOG channels use the eog "
"parameter" % str(missing_positions))
raise KeyError(err)
# for stim channel
events = _read_vmrk_events(eeg_info['marker_id'])
return info, eeg_info, events
def read_raw_brainvision(vhdr_fname, elp_fname=None, elp_names=None,
preload=False, reference=None,
eog=['HEOGL', 'HEOGR', 'VEOGb'], ch_names=None,
verbose=None):
"""Reader for Brain Vision EEG file
Parameters
----------
vhdr_fname : str
Path to the EEG header file.
elp_fname : str | None
Path to the elp file containing electrode positions.
If None, sensor locations are (0,0,0).
elp_names : list | None
A list of channel names in the same order as the points in the elp
file. Electrode positions should be specified with the same names as
in the vhdr file, and fiducials should be specified as "lpa" "nasion",
"rpa". ELP positions with other names are ignored. If elp_names is not
None and channels are missing, a KeyError is raised.
preload : bool
If True, all data are loaded at initialization.
If False, data are not read until save.
reference : None | str
Name of the electrode which served as the reference in the recording.
If a name is provided, a corresponding channel is added and its data
is set to 0. This is useful for later re-referencing. The name should
correspond to a name in elp_names.
eog : list of str
Names of channels that should be designated EOG channels. Names should
correspond to the vhdr file (default: ['HEOGL', 'HEOGR', 'VEOGb']).
verbose : bool, str, int, or None
If not None, override default verbose level (see mne.verbose).
See Also
--------
mne.io.Raw : Documentation of attribute and methods.
"""
raw = RawBrainVision(vhdr_fname, elp_fname, elp_names, preload,
reference, eog, ch_names, verbose)
return raw
|