-
-
Save skjerns/bc660ef59dca0dbd53f00ed38c42f6be to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*- | |
""" | |
Created on Wed Dec 5 12:56:31 2018 | |
@author: skjerns | |
Gist to save a mne.io.Raw object to an EDF file using pyEDFlib | |
(https://github.com/holgern/pyedflib) | |
Disclaimer: | |
- Saving your data this way will result in slight | |
loss of precision (magnitude +-1e-09). | |
- It is assumed that the data is presented in Volt (V), | |
it will be internally converted to microvolt | |
- BDF or EDF+ is selected based on the filename extension | |
- Annotations preserved | |
Update: Since 2021, MNE also supports exporting EDF via edfio: | |
https://mne.tools/stable/generated/mne.export.export_raw.html | |
""" | |
import pyedflib # pip install pyedflib | |
from pyedflib import highlevel # new high-level interface | |
from pyedflib import FILETYPE_BDF, FILETYPE_BDFPLUS, FILETYPE_EDF, FILETYPE_EDFPLUS | |
from datetime import datetime, timezone, timedelta | |
import mne | |
import os | |
def _stamp_to_dt(utc_stamp): | |
"""Convert timestamp to datetime object in Windows-friendly way.""" | |
if 'datetime' in str(type(utc_stamp)): return utc_stamp | |
# The min on windows is 86400 | |
stamp = [int(s) for s in utc_stamp] | |
if len(stamp) == 1: # In case there is no microseconds information | |
stamp.append(0) | |
return (datetime.fromtimestamp(0, tz=timezone.utc) + | |
timedelta(0, stamp[0], stamp[1])) # day, sec, μs | |
def write_mne_edf(mne_raw, fname, picks=None, tmin=0, tmax=None, | |
overwrite=False): | |
""" | |
Saves the raw content of an MNE.io.Raw and its subclasses to | |
a file using the EDF+/BDF filetype | |
pyEDFlib is used to save the raw contents of the RawArray to disk | |
Parameters | |
update 2021: edf export is now also supported in MNE: | |
https://mne.tools/stable/generated/mne.export.export_raw.html | |
---------- | |
mne_raw : mne.io.Raw | |
An object with super class mne.io.Raw that contains the data | |
to save | |
fname : string | |
File name of the new dataset. This has to be a new filename | |
unless data have been preloaded. Filenames should end with .edf | |
picks : array-like of int | None | |
Indices of channels to include. If None all channels are kept. | |
tmin : float | None | |
Time in seconds of first sample to save. If None first sample | |
is used. | |
tmax : float | None | |
Time in seconds of last sample to save. If None last sample | |
is used. | |
overwrite : bool | |
If True, the destination file (if it exists) will be overwritten. | |
If False (default), an error will be raised if the file exists. | |
""" | |
print('did you know EDF export is now supported in MNE via edfio? have a look at https://mne.tools/stable/generated/mne.export.export_raw.html') | |
if not issubclass(type(mne_raw), mne.io.BaseRaw): | |
raise TypeError('Must be mne.io.Raw type') | |
if not overwrite and os.path.exists(fname): | |
raise OSError('File already exists. No overwrite.') | |
# static settings | |
has_annotations = True if len(mne_raw.annotations)>0 else False | |
if os.path.splitext(fname)[-1] == '.edf': | |
file_type = FILETYPE_EDFPLUS if has_annotations else FILETYPE_EDF | |
dmin, dmax = -32768, 32767 | |
else: | |
file_type = FILETYPE_BDFPLUS if has_annotations else FILETYPE_BDF | |
dmin, dmax = -8388608, 8388607 | |
print('saving to {}, filetype {}'.format(fname, file_type)) | |
sfreq = mne_raw.info['sfreq'] | |
date = _stamp_to_dt(mne_raw.info['meas_date']) | |
if tmin: | |
date += timedelta(seconds=tmin) | |
# no conversion necessary, as pyedflib can handle datetime. | |
#date = date.strftime('%d %b %Y %H:%M:%S') | |
first_sample = int(sfreq*tmin) | |
last_sample = int(sfreq*tmax) if tmax is not None else None | |
# convert data | |
channels = mne_raw.get_data(picks, | |
start = first_sample, | |
stop = last_sample) | |
# convert to microvolts to scale up precision | |
channels *= 1e6 | |
# set conversion parameters | |
n_channels = len(channels) | |
# create channel from this | |
try: | |
f = pyedflib.EdfWriter(fname, | |
n_channels=n_channels, | |
file_type=file_type) | |
channel_info = [] | |
ch_idx = range(n_channels) if picks is None else picks | |
keys = list(mne_raw._orig_units.keys()) | |
for i in ch_idx: | |
try: | |
ch_dict = {'label': mne_raw.ch_names[i], | |
'dimension': mne_raw._orig_units[keys[i]], | |
'sample_rate': mne_raw._raw_extras[0]['n_samps'][i], | |
'physical_min': mne_raw._raw_extras[0]['physical_min'][i], | |
'physical_max': mne_raw._raw_extras[0]['physical_max'][i], | |
'digital_min': mne_raw._raw_extras[0]['digital_min'][i], | |
'digital_max': mne_raw._raw_extras[0]['digital_max'][i], | |
'transducer': '', | |
'prefilter': ''} | |
except: | |
ch_dict = {'label': mne_raw.ch_names[i], | |
'dimension': mne_raw._orig_units[keys[i]], | |
'sample_rate': sfreq, | |
'physical_min': channels.min(), | |
'physical_max': channels.max(), | |
'digital_min': dmin, | |
'digital_max': dmax, | |
'transducer': '', | |
'prefilter': ''} | |
channel_info.append(ch_dict) | |
f.setPatientCode(mne_raw._raw_extras[0]['subject_info'].get('id', '0')) | |
f.setPatientName(mne_raw._raw_extras[0]['subject_info'].get('name', 'noname')) | |
f.setTechnician('mne-gist-save-edf-skjerns') | |
f.setSignalHeaders(channel_info) | |
f.setStartdatetime(date) | |
f.writeSamples(channels) | |
for annotation in mne_raw.annotations: | |
onset = annotation['onset'] | |
duration = annotation['duration'] | |
description = annotation['description'] | |
f.writeAnnotation(onset, duration, description) | |
except Exception as e: | |
raise e | |
finally: | |
f.close() | |
return True |
you can add them as annotations
signals = np.random.rand(5, 256*300)*200 # 5 minutes of random signal
channel_names = ['ch1', 'ch2', 'ch3', 'ch4', 'ch5']
signal_headers = highlevel.make_signal_headers(channel_names, sample_frequency=256)
header = highlevel.make_header(patientname='patient_x', gender='Female')
annotations = [[0, 0, "Wake"], [30, 0, "S2"]] # format [onset, duration, description], I think time is in seconds or ms, I don't remember
header['annotations'] = annotations
highlevel.write_edf('edf_file.edf', signals, signal_headers, header)
likely some wrong values for digital min/max and physical min/max in EDF. Else just a display problem in EDFBrowser, try pressing '+' to increase the signals. However, this is not the place to get support for that - it has little to do with this blob.
Hi
I'm trying to open an edf in mne, edit it, and save result in a new file using your gist. Unfortunately, when I do:
write_mne_edf(EEG_raw, fname, picks=None, tmin=0, tmax=None,
overwrite=False)
console-->
saving to EDF_test, filetype 2
'name'
the resulting file is very small and its content is :
Error! C:\Python\Python38\EDF_test is not UTF-8 encoded
Saving disabled.
See Console for more details.
edf original:
https://drive.google.com/file/d/1uuiZ4hswH2i4CLZq5BjqHAvwhhR9xARJ/view?usp=sharing
Could you help me . Thanks in advance
header:
{'technician': '', 'recording_additional': '', 'patientname': 'RLSR', 'patient_additional': 'righthanded 4 Kg', 'patientcode': '00000', 'equipment': 'EMSA Equipamentos Medicos S.A.', 'admincode': '0000001', 'gender': 'Female', 'startdate': datetime.datetime(2004, 11, 1, 8, 19, 53), 'birthdate': '02 nov 1995', 'annotations': []}
I edited the gist, it should work now. (I also removed some patient-sensitive data from your post, please also remove the file from your google drive as it contains patient specific data that you're probably not allowed to share)
Dear Mr Kern
I updated the gist
original file without sensitive info:
https://drive.google.com/file/d/1kJKHQRE5FslZkm7RkOoZkVPZ1b7P_5bB/view?usp=sharing
but the result doesnot copy the data:
https://drive.google.com/file/d/14Sog6jkKrfLIOwFgNGAGlorYz2KHRhrs/view?usp=sharing
I cannot reproduce the problem.
raw = mne.io.read_raw('0651701_Copy (1).edf')
write_mne_edf(raw, 'test.edf')
I can read and save the file with no problems. Sorry, I will not be able to give individual support for specific problems that are not directly related to the gist.
Sure, I do it for fun and I'll find a way, thank you for your time, have a great day.
PKanda
Brazil
I am getting an os error when trying to save the preprocessed eeg data in EDF format. Earlier it worked. I am now unable to save the epoched eeg data into a folder in EDF format. Could you tell me how to solve this issue? I'm getting the error shown below
"OSError: The filename (/Users/sreelakshmiraveendran/Desktop/Research papersMAC/Python programming/out_data) for file type raw must end with .fif or .fif.gz"
You are not using the script, but probably raw.save()
, which is MNE buitlin. Also I have no idea what you are doing, please provide a context with code that reproduces the issue. This does not seem like an error in the script, but rather a general Python programming mistake.
The original file is edF file, now want to convert to BDF file format, can this library be used successfully?Thank you very much,My email is [email protected]
You can do so easily with pyedflib
. However, you will gain nothing from it, as your precision will be obviously capped by the EDF file.
EDF and BDF are the same file format with the difference of having 16 and 24 bits of precision.
import pyedflib
signals, sheads, header = pyedflib.highlevel.read_edf(filename)
pyedflib.highlevel.write_edf('out.bdf', signals, sheads, header)
you might need to manually alter the physical min/max in the signal headers of some channels. Sorry, cannot give any further help than this :-/
I'm encountering an error with the current gist here and BDF files, using the latest versions of mne and pyedflib (1.3.1 and 0.1.32 respectively)
Using the files directly from BioSemi I mentioned in an earlier message here (https://gist.github.com/skjerns/bc660ef59dca0dbd53f00ed38c42f6be?permalink_comment_id=3187890#gistcomment-3187890), I'm running this script:
import mne, save_edf, os
if __name__ == "__main__":
path = '~~~/Downloads/BDFtestfiles'
files = [x for x in os.listdir(path) if 'mod' not in x]
for file in files:
minus = '.'.join(file.split('.')[:-1])
nneww = '{}/{}_mod.bdf'.format(path, minus)
dat = mne.io.read_raw_bdf('{}/{}'.format(path, file))
save_edf.write_mne_edf(mne_raw=dat, fname=nneww, overwrite=True)
There is another file named save_edf.py that is the gist on this page. I get the following error:
I'm not sure but I suspect this is related to BioSemi's default units--I think their devices save in micro-Volts. The only field I see than mentions that (in the mne.io.Raw
object) is the _orig_units
field; however the "original" in that name makes me hesitant to treat it as a ground truth for the current state of the file. Any suggestions for handling this?
MNE has an mne.io.export_raw now too which might be helpful if that's all handled within that function
I think the only thing that you can try is trial and error: downscale the data and change dimension accordingly and check with EdfBrowser if the signal is loaded correctly. If they save in microvolts I'm surprised that there are values of 1e13, which would be death for most organisms-----------Sent from mobileAm 27.04.2023 um 13:52 schrieb GABowers @.>:Re: @. commented on this gist.I'm encountering an error with the current gist here and BDF files, using the latest versions of mne and pyedflib (1.3.1 and 0.1.32 respectively)Using the files directly from BioSemi I mentioned in an earlier message here (https://gist.github.com/skjerns/bc660ef59dca0dbd53f00ed38c42f6be?permalink_comment_id=3187890#gistcomment-3187890), I'm running this script:import mne, save_edf, os if name == "main": path = '~~~/Downloads/BDFtestfiles' files = [x for x in os.listdir(path) if 'mod' not in x] for file in files: minus = '.'.join(file.split('.')[:-1]) nneww = '{}/{}_mod.bdf'.format(path, minus) dat = mne.io.read_raw_bdf('{}/{}'.format(path, file)) save_edf.write_mne_edf(mne_raw=dat, fname=nneww, overwrite=True) There is another file named save_edf.py that is the gist on this page. I get the following error:I'm not sure but I suspect this is related to BioSemi's default units--I think their devices save in micro-Volts. The only field I see than mentions that (in the mne.io.Raw object) is the _orig_units field; however the "original" in that name makes me hesitant to treat it as a ground truth for the current state of the file. Any suggestions for handling this?—Reply to this email directly, view it on GitHub or unsubscribe.You are receiving this email because you authored the thread.Triage notifications on the go with GitHub Mobile for iOS or Android.
Your comment led me to look a bit deeper. It seems that the mne_raw._raw_extras
dictionary is missing some keys, which causes the code to use values from the data for the various channel infos--the "except" in the following:
try:
ch_dict = {'label': mne_raw.ch_names[i],
'dimension': mne_raw._orig_units[keys[i]],
'sample_rate': mne_raw._raw_extras[0]['n_samps'][i],
'physical_min': mne_raw._raw_extras[0]['physical_min'][i],
'physical_max': mne_raw._raw_extras[0]['physical_max'][i],
'digital_min': mne_raw._raw_extras[0]['digital_min'][i],
'digital_max': mne_raw._raw_extras[0]['digital_max'][i],
'transducer': '',
'prefilter': ''}
except:
ch_dict = {'label': mne_raw.ch_names[i],
'dimension': mne_raw._orig_units[keys[i]],
'sample_rate': sfreq,
'physical_min': channels.min(),
'physical_max': channels.max(),
'digital_min': dmin,
'digital_max': dmax,
'transducer': '',
'prefilter': ''}
And it's getting those values from, surprise surprise, the BioSemi Status channel.
I think the simplest solution is to get that physical_min
and max from all channels except any named Status--I'll write something up for that in a few hours when I have some time.
Edit: See below. Simple change.
except:
physical = channels if 'Status' not in mne_raw.ch_names[-1] else channels[:-1]
ch_dict = {'label': mne_raw.ch_names[i],
'dimension': mne_raw._orig_units[keys[i]],
'sample_rate': sfreq,
'physical_min': physical.min(),
'physical_max': physical.max(),
'digital_min': dmin,
'digital_max': dmax,
'transducer': '',
'prefilter': ''}
Hey, thanks for the great little snippet. Somehow I have the problem that when I write something to .edf using it, the remainder of the last full second is zero padded (at least when I open it using MNE).
Do you have any idea why that might be, or how to prevent it? Thanks.
Hey, thanks for the great little snippet. Somehow I have the problem that when I write something to .edf using it, the remainder of the last full second is zero padded (at least when I open it using MNE). Do you have any idea why that might be, or how to prevent it? Thanks.
That is due to the way EDF is implemented, as it saves data in discrete blocks. By default these blocks are 1 second long. This blocksize (record_size
) is also used to calculate the sampling frequency together with nr of samples in each data record (per channel) -> smp_per_record
. so if you change the record_size
toaccomodate exactly for the number of samples you want to store, you must adapt the smp_per_record
accordingly. e.g. if you have 12.5 seconds of data with 100 Hz, you need to choose a record_size
such that it fits into the 12.5 seconds and set the smp_per_record
accordingly, here record_size=0.5
and smp_per_record=50
would work. However it becomes more difficult if you have a really odd number of seconds or sampling frequency, especially one where the solution for smp_per_record
would not result in a neat integer.
in summary: it is possible to avoid the zero-padding, but it's not as easy as it seems.
PS: MNE now supports exporting EDF via edfio
: https://mne.tools/stable/generated/mne.export.export_raw.html
Thanks for the fast and very helpful reply!
Here is a solution that works for me, though clearly has some downsides to it: https://gist.github.com/rectified-evasion/dce33a21e947623fb2c6c77292bf7bc8 (loss of precision + truncation, if necessary).
Thanks @skjerns , but the labels? How could I introduce them? Thanks again!