Convert EDF, EDF+ or BDF to MEF 3.0¶
Download
examples/12_edf_to_mef.py
from the mef3io repository (pip does not install the examples), then:
pip install mef3io pyedflib # pyedflib reads EDF; from mef3io 1.3: pip install "mef3io[edf]"
python 12_edf_to_mef.py recording.edf recording.mefd --timezone Europe/Prague
Run it without arguments to convert a small demo file first. It prints each
channel and ends with verified once it has read the new session back and
checked it against the values it reads from the EDF file:
EEG Fp1: 256 Hz, uV, stored to 0.01 uV
ECG: 64 Hz, mV, stored to 0.0001 mV
2 annotation(s); verified /tmp/.../demo.mefd
From your own code, save the script next to your code as edf2mef.py, then:
from edf2mef import convert, verify
result = convert("recording.edf", "recording.mefd", timezone="Europe/Prague")
verify("recording.mefd", result)
The script is short and meant to be read and adapted. The rest of this page explains each choice it makes.
The time zone: you must say it¶
An EDF file stores its start as a local clock time, such as
05.03.24 10.20.30, with no time zone. MEF 3.0 stores every time in UTC. The
same file recorded in Prague and in New York would be six hours apart, and
nothing in the file says which. So the script refuses to guess: pass
--timezone with the zone of the recording site.
- An IANA name, such as
Europe/PragueorAmerica/Chicago. Daylight saving is applied for the recording date. A start time in the hour skipped or repeated by a daylight-saving change is refused; pass a fixed offset instead. On Windows this needspip install tzdata. - A fixed offset, such as
+01:00, orUTC. Use this when you know the offset the recording system used.
The offset is also stored in the session (Subject.gmt_offset), so a viewer
can show local clock times again.
Channels, sampling rates and units¶
Each EDF signal becomes one MEF 3.0 channel with its own sampling rate: a
256 Hz EEG channel and a 64 Hz ECG channel stay 256 Hz and 64 Hz. Nothing is
resampled. The EDF label becomes the channel name, with characters that are
not safe in a folder name replaced by _ (EEG Fp1/Ref becomes
EEG Fp1_Ref). The physical dimension (uV, mV) becomes the channel's
units. The transducer and prefilter text go into the channel description.
Values: how precise the copy is¶
EDF stores whole numbers per sample plus a linear scale to physical units. MEF 3.0 also stores whole numbers ("counts") with a scale, but the scale is a power of ten. The script picks, per channel, the number of decimal places so that one count is no larger than one EDF step. For the demo EEG channel, an EDF step is about 0.098 uV and a count is 0.01 uV.
Every sample therefore comes back within half a count of the EDF value. The counts are 32-bit numbers; if a channel's values are too large for that at full resolution (rare: large values measured very finely), the script uses fewer decimal places and prints a warning naming the channel. More on the scaling: Python guide, writing.
Annotations¶
EDF+ and BDF+ annotations become session-level records at the same absolute
time. An annotation with a duration becomes an EDFA record that keeps it;
one without becomes a Note. Read them with Reader.records().
Discontinuous files (EDF+D) become gaps¶
In an EDF+D file each data record carries its own start time, and time can
jump between records, for example when acquisition was paused. The script
writes each unbroken run of records at its real time. The time between runs
is stored as a gap and reads back as NaN, at the right place, as for any
recording with gaps.
pyedflib refuses EDF+D and BDF+D files, so for those the script reads the
file itself (open_discontinuous): the EDF header has a fixed layout and the
record times are the first annotation of each record.
Large files¶
The script converts one channel at a time, in pieces of ten minutes
(CHUNK_SECONDS), so a recording of days needs no more memory than a short
one. The check at the end reads the same way. EDF stores all channels
interleaved, so the file is read once per channel (and again by the check);
it is fastest when the EDF file fits in the computer's memory cache.
Each piece is written as its own block of data, and MEF 3.0 marks the first
block of every write with a "possible break" flag. That flag appears every ten
minutes in Reader.toc() even in a continuous recording; the block times show
there is no gap (more).
Patient identity is not copied¶
An EDF header holds the patient name, code, birth date and sex in plain text.
The script does not copy them unless you pass --copy-patient, which
copies the name and code of an EDF+ or BDF+ file into the subject fields.
(A plain EDF or BDF file has only a free-text patient field; it is not
copied, and the script says so.) In a session without passwords those fields
are readable by anyone with the files. To protect them, encrypt the session:
add --encrypt, and the script asks for the two passwords (they are not
typed on the command line, where others could see them). From Python, use
convert(..., password1="...", password2="...") and then
verify(..., password="<the level-2 password>")
(what encryption protects).
Leaving the name out does not make a recording anonymous: the start date, annotation text and file names can identify a person too. See De-identification.
Checking the result¶
verify() reads the EDF file again and the new session, piece by piece, and
checks per channel that every sample is within half a count of the EDF value
at the same time, that each count is no coarser than the EDF step (unless a
warning said so), that no extra samples fill a gap, and that every annotation
was written with the same time, type, text and duration. It uses the
script's own EDF reading, so it confirms the conversion, not the EDF reader.
With mef3io 1.3 or later you can also look at the session without reading
any samples:
python -m mef3io inspect recording.mefd
Why pyedflib¶
The script reads EDF with pyedflib, a
small, maintained wrapper around the EDFlib C library. mef3io never installs
it by itself: pip install pyedflib, or the [edf] extra from mef3io 1.3. It reads EDF, EDF+ and
BDF, keeps each channel at its own sampling rate, and gives the stored whole
numbers, so the conversion controls the scaling itself.
MNE-Python also reads EDF and is the better choice for
analysis, but it is a much larger install and brings channels with different
sampling rates to one common rate, which a lossless conversion must not do.
Related¶
- Read MEF 3.0 in Python and in MATLAB
- Examples: the other runnable scripts