159 lines
5.3 KiB
Python
159 lines
5.3 KiB
Python
# Authors: The MNE-Python contributors.
|
|
# License: BSD-3-Clause
|
|
# Copyright the MNE-Python contributors.
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
from mne.channels.channels import _unit2human
|
|
from mne.io.constants import FIFF
|
|
from mne.utils import _check_pybv_installed, warn
|
|
|
|
_check_pybv_installed()
|
|
from pybv import write_brainvision # noqa: E402
|
|
|
|
|
|
def _export_mne_raw(*, raw, fname, events=None, overwrite=False):
|
|
"""Export raw data from MNE-Python.
|
|
|
|
Parameters
|
|
----------
|
|
raw : mne.io.Raw
|
|
The raw data to export.
|
|
fname : str | pathlib.Path
|
|
The name of the file where raw data will be exported to. Must end with
|
|
``".vhdr"``, and accompanying *.vmrk* and *.eeg* files will be written inside
|
|
the same directory.
|
|
events : np.ndarray | None
|
|
Events to be written to the marker file (*.vmrk*). If array, must be in
|
|
`MNE-Python format <https://mne.tools/stable/glossary.html#term-events>`_. If
|
|
``None`` (default), events will be written based on ``raw.annotations``.
|
|
overwrite : bool
|
|
Whether or not to overwrite existing data. Defaults to ``False``.
|
|
|
|
"""
|
|
# prepare file location
|
|
if not str(fname).endswith(".vhdr"):
|
|
raise ValueError("`fname` must have the '.vhdr' extension for BrainVision.")
|
|
fname = Path(fname)
|
|
folder_out = fname.parents[0]
|
|
fname_base = fname.stem
|
|
|
|
# prepare data from raw
|
|
data = raw.get_data() # gets data starting from raw.first_samp
|
|
sfreq = raw.info["sfreq"] # in Hz
|
|
meas_date = raw.info["meas_date"] # datetime.datetime
|
|
ch_names = raw.ch_names
|
|
|
|
# write voltage units as micro-volts and all other units without scaling
|
|
# write units that we don't know as n/a
|
|
unit = []
|
|
for ch in raw.info["chs"]:
|
|
if ch["unit"] == FIFF.FIFF_UNIT_V:
|
|
unit.append("µV")
|
|
elif ch["unit"] == FIFF.FIFF_UNIT_CEL:
|
|
unit.append("°C")
|
|
else:
|
|
unit.append(_unit2human.get(ch["unit"], "n/a"))
|
|
unit = [u if u != "NA" else "n/a" for u in unit]
|
|
|
|
# enforce conversion to float32 format
|
|
# XXX: Could add a feature that checks data and optimizes `unit`, `resolution`, and
|
|
# `format` so that raw.orig_format could be retained if reasonable.
|
|
if raw.orig_format != "single":
|
|
warn(
|
|
f"Encountered data in '{raw.orig_format}' format. Converting to float32.",
|
|
RuntimeWarning,
|
|
)
|
|
|
|
fmt = "binary_float32"
|
|
resolution = 0.1
|
|
|
|
# handle events
|
|
# if we got an ndarray, this is in MNE-Python format
|
|
msg = "`events` must be None or array in MNE-Python format."
|
|
if events is not None:
|
|
# subtract raw.first_samp because brainvision marks events starting from the
|
|
# first available data point and ignores the raw.first_samp
|
|
assert isinstance(events, np.ndarray), msg
|
|
assert events.ndim == 2, msg
|
|
assert events.shape[-1] == 3, msg
|
|
events[:, 0] -= raw.first_samp
|
|
events = events[:, [0, 2]] # reorder for pybv required order
|
|
else: # else, prepare pybv style events from raw.annotations
|
|
events = _mne_annots2pybv_events(raw)
|
|
|
|
# no information about reference channels in mne currently
|
|
ref_ch_names = None
|
|
|
|
# write to BrainVision
|
|
write_brainvision(
|
|
data=data,
|
|
sfreq=sfreq,
|
|
ch_names=ch_names,
|
|
ref_ch_names=ref_ch_names,
|
|
fname_base=fname_base,
|
|
folder_out=folder_out,
|
|
overwrite=overwrite,
|
|
events=events,
|
|
resolution=resolution,
|
|
unit=unit,
|
|
fmt=fmt,
|
|
meas_date=meas_date,
|
|
)
|
|
|
|
|
|
def _mne_annots2pybv_events(raw):
|
|
"""Convert mne Annotations to pybv events."""
|
|
events = []
|
|
for annot in raw.annotations:
|
|
# handle onset and duration: seconds to sample, relative to
|
|
# raw.first_samp / raw.first_time
|
|
onset = annot["onset"] - raw.first_time
|
|
onset = raw.time_as_index(onset).astype(int)[0]
|
|
duration = int(annot["duration"] * raw.info["sfreq"])
|
|
|
|
# triage type and description
|
|
# defaults to type="Comment" and the full description
|
|
etype = "Comment"
|
|
description = annot["description"]
|
|
for start in ["Stimulus/S", "Response/R", "Comment/"]:
|
|
if description.startswith(start):
|
|
etype = start.split("/")[0]
|
|
description = description.replace(start, "")
|
|
break
|
|
|
|
if etype in ["Stimulus", "Response"] and description.strip().isdigit():
|
|
description = int(description.strip())
|
|
else:
|
|
# if cannot convert to int, we must use this as "Comment"
|
|
etype = "Comment"
|
|
|
|
event_dict = dict(
|
|
onset=onset, # in samples
|
|
duration=duration, # in samples
|
|
description=description,
|
|
type=etype,
|
|
)
|
|
|
|
if "ch_names" in annot:
|
|
# handle channels
|
|
channels = list(annot["ch_names"])
|
|
event_dict["channels"] = channels
|
|
|
|
# add a "pybv" event
|
|
events += [event_dict]
|
|
|
|
return events
|
|
|
|
|
|
def _export_raw(fname, raw, overwrite):
|
|
"""Export Raw object to BrainVision via pybv."""
|
|
fname = str(fname)
|
|
ext = os.path.splitext(fname)[-1]
|
|
if ext != ".vhdr":
|
|
fname = fname.replace(ext, ".vhdr")
|
|
_export_mne_raw(raw=raw, fname=fname, overwrite=overwrite)
|