Skip to content

Commit dc57e39

Browse files
committed
Adds rdedfann function; cleans up edf2mit function
1 parent aec8752 commit dc57e39

File tree

5 files changed

+188
-11
lines changed

5 files changed

+188
-11
lines changed

sample-data/test_edfann.edf

60.5 KB
Binary file not shown.

wfdb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
wrsamp, dl_database, edf2mit, mit2edf, wav2mit, mit2wav,
33
wfdb2mat, csv2mit, sampfreq, signame, wfdbdesc, wfdbtime)
44
from wfdb.io.annotation import (Annotation, rdann, wrann, show_ann_labels,
5-
show_ann_classes, ann2rr, rr2ann, csv2ann)
5+
show_ann_classes, ann2rr, rr2ann, csv2ann, rdedfann)
66
from wfdb.io.download import get_dbs, get_record_list, dl_files, set_db_index_url
77
from wfdb.plot.plot import plot_items, plot_wfdb, plot_all_records
88

wfdb/io/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
csv2mit, sampfreq, signame, wfdbdesc, wfdbtime, SIGNAL_CLASSES)
44
from wfdb.io._signal import est_res, wr_dat_file
55
from wfdb.io.annotation import (Annotation, rdann, wrann, show_ann_labels,
6-
show_ann_classes, ann2rr, rr2ann, csv2ann)
6+
show_ann_classes, ann2rr, rr2ann, csv2ann, rdedfann)
77
from wfdb.io.download import get_dbs, get_record_list, dl_files, set_db_index_url
88
from wfdb.io.tff import rdtff

wfdb/io/annotation.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66
import posixpath
77
import pdb
8+
import struct
89

910
from wfdb.io import download
1011
from wfdb.io import _header
@@ -2546,6 +2547,156 @@ def csv2ann(file_name, extension='atr', fs=None, record_only=False,
25462547
print('Finished writing Annotation file')
25472548

25482549

2550+
def rdedfann(record_name, pn_dir=None, delete_file=True, info_only=True,
2551+
record_only=False, verbose=False):
2552+
"""
2553+
This program returns the annotation information from an EDF+ file
2554+
containing annotations (with the signal name given as 'EDF Annotations').
2555+
The information that is returned if `info_only` is set to True is:
2556+
{
2557+
'onset_time': list of %H:%M:%S.fff strings denoting the annotation
2558+
onset times,
2559+
'sample_num': list of integers denoting the annotation onset
2560+
sample numbers,
2561+
'comment': list of comments (`aux_note`) for the annotations,
2562+
'duration': list of floats denoting the duration of the event
2563+
}
2564+
Else, this function will return either the WFDB Annotation format of the
2565+
information of the file if `record_only` is set to True, or nothing if
2566+
neither are set to True though a WFDB Annotation file will be created.
2567+
2568+
Parameters
2569+
----------
2570+
record_name : str
2571+
The name of the input EDF record to be read.
2572+
pn_dir : str, optional
2573+
Option used to stream data from Physionet. The Physionet
2574+
database directory from which to find the required record files.
2575+
eg. For record '100' in 'http://physionet.org/content/mitdb'
2576+
pn_dir='mitdb'.
2577+
delete_file : bool, optional
2578+
Whether to delete the saved EDF file (False) or not (True)
2579+
after being imported.
2580+
info_only : bool, optional
2581+
Return, strictly, the information contained in the file as formatted
2582+
by the original WFDB package. Must not be True if `record_only` is
2583+
True.
2584+
record_only : bool, optional
2585+
Whether to only return the record information (True) or not (False).
2586+
If False, this function will generate both a .dat and .hea file. Must
2587+
not be True if `info_only` is True.
2588+
verbose : bool, optional
2589+
Whether to print all the information read about the file (True) or
2590+
not (False).
2591+
2592+
Returns
2593+
-------
2594+
record : dict, optional
2595+
All of the record information needed to generate MIT formatted files.
2596+
Only returns if 'record_only' is set to True, else generates the
2597+
corresponding .dat and .hea files. This record file will not match the
2598+
`rdrecord` output since it will only give us the digital signal for now.
2599+
2600+
Notes
2601+
-----
2602+
The entire file is composed of (seen here:
2603+
https://www.edfplus.info/specs/edfplus.html#edfplusannotations):
2604+
2605+
HEADER RECORD (we suggest to also adopt the 12 simple additional EDF+ specs)
2606+
8 ascii : version of this data format (0)
2607+
80 ascii : local patient identification (mind item 3 of the additional EDF+ specs)
2608+
80 ascii : local recording identification (mind item 4 of the additional EDF+ specs)
2609+
8 ascii : startdate of recording (dd.mm.yy) (mind item 2 of the additional EDF+ specs)
2610+
8 ascii : starttime of recording (hh.mm.ss)
2611+
8 ascii : number of bytes in header record
2612+
44 ascii : reserved
2613+
8 ascii : number of data records (-1 if unknown, obey item 10 of the additional EDF+ specs)
2614+
8 ascii : duration of a data record, in seconds
2615+
4 ascii : number of signals (ns) in data record
2616+
ns * 16 ascii : ns * label (must be 'EDF Annotations')
2617+
ns * 80 ascii : ns * transducer type (must be whitespace)
2618+
ns * 8 ascii : ns * physical dimension (must be whitespace)
2619+
ns * 8 ascii : ns * physical minimum (e.g. -500 or 34, different than physical maximum)
2620+
ns * 8 ascii : ns * physical maximum (e.g. 500 or 40, different than physical minimum)
2621+
ns * 8 ascii : ns * digital minimum (must be -32768)
2622+
ns * 8 ascii : ns * digital maximum (must be 32767)
2623+
ns * 80 ascii : ns * prefiltering (must be whitespace)
2624+
ns * 8 ascii : ns * nr of samples in each data record
2625+
ns * 32 ascii : ns * reserved
2626+
2627+
ANNOTATION RECORD
2628+
2629+
Examples
2630+
--------
2631+
>>> ann_info = wfdb.rdedfann('sample-data/test_edfann.edf')
2632+
2633+
"""
2634+
# Some preliminary checks
2635+
if info_only and record_only:
2636+
raise Exception('Both `info_only` and `record_only` are set. Only one '
2637+
'can be set at a time.')
2638+
2639+
# According to the EDF+ docs:
2640+
# "The coding is EDF compatible in the sense that old EDF software would
2641+
# simply treat this 'EDF Annotations' signal as if it were a (strange-
2642+
# looking) ordinary signal"
2643+
rec = record.edf2mit(record_name, pn_dir=pn_dir, delete_file=delete_file,
2644+
record_only=True, rdedfann_flag=True)
2645+
2646+
# Convert from array of integers to ASCII strings
2647+
annotation_string = ''
2648+
for chunk in rec.p_signal.flatten().astype(np.int64):
2649+
if chunk+1 == 0:
2650+
continue
2651+
else:
2652+
adjusted_hex = hex(struct.unpack('<H', struct.pack('>H',chunk+1))[0])
2653+
annotation_string += bytes.fromhex(adjusted_hex[2:]).decode('ascii')
2654+
# Remove all of the whitespace
2655+
for rep in ['\x00','\x14','\x15']:
2656+
annotation_string = annotation_string.replace(rep,' ')
2657+
2658+
# Parse the resulting annotation string
2659+
onset_times = []
2660+
sample_nums = []
2661+
comments = []
2662+
durations = []
2663+
all_anns = annotation_string.split('+')
2664+
for ann in all_anns:
2665+
if ann == '':
2666+
continue
2667+
try:
2668+
ann_split = ann.strip().split(' ')
2669+
onset = float(ann_split[0])
2670+
hours, rem = divmod(onset, 3600)
2671+
minutes, seconds = divmod(rem, 60)
2672+
onset_time = f'{hours:02.0f}:{minutes:02.0f}:{seconds:06.3f}'
2673+
sample_num = int(onset*rec.sig_len)
2674+
duration = float(ann_split[1])
2675+
comment = ' '.join(ann_split[2:])
2676+
if verbose:
2677+
print(f'{onset_time}\t{sample_num}\t{comment}\t\tduration: {duration}')
2678+
onset_times.append(onset_time)
2679+
sample_nums.append(sample_num)
2680+
comments.append(comment)
2681+
durations.append(duration)
2682+
except IndexError:
2683+
continue
2684+
2685+
if info_only:
2686+
return {
2687+
'onset_time': onset_times,
2688+
'sample_num': sample_nums,
2689+
'comment': comments,
2690+
'duration': durations
2691+
}
2692+
elif record_only:
2693+
# TODO: return WFDB-formatted annotation object
2694+
pass
2695+
else:
2696+
# TODO: Create the WFDB annotation file and don't return the object
2697+
pass
2698+
2699+
25492700
## ------------- Annotation Field Specifications ------------- ##
25502701

25512702

wfdb/io/record.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,7 +1349,7 @@ def check_np_array(item, field_name, ndim, parent_class, channel_num=None):
13491349

13501350

13511351
def edf2mit(record_name, pn_dir=None, delete_file=True, record_only=True,
1352-
header_only=False, verbose=False):
1352+
header_only=False, verbose=False, rdedfann_flag=False):
13531353
"""
13541354
Convert EDF formatted files to MIT format.
13551355
@@ -1391,6 +1391,11 @@ def edf2mit(record_name, pn_dir=None, delete_file=True, record_only=True,
13911391
verbose : bool, optional
13921392
Whether to print all the information read about the file (True) or
13931393
not (False).
1394+
rdedfann_flag : bool, optional
1395+
Whether the function is being called by `rdedfann` or the user. If it
1396+
is being called by the user and the file has annotations, then warn
1397+
them that the EDF file has annotations and that they should use
1398+
`rdedfann` instead.
13941399
13951400
Returns
13961401
-------
@@ -1521,7 +1526,10 @@ def edf2mit(record_name, pn_dir=None, delete_file=True, record_only=True,
15211526
reserved_notes = struct.unpack('<44s', edf_file.read(44))[0].decode().strip()
15221527
if reserved_notes[:5] == 'EDF+C':
15231528
# The file is EDF compatible and will work without issue
1524-
# See: https://www.sciencedirect.com/science/article/pii/S1388245703001238?via%3Dihub
1529+
# See: Bob Kemp, Jesus Olivan, European data format ‘plus’ (EDF+), an
1530+
# EDF alike standard format for the exchange of physiological
1531+
# data, Clinical Neurophysiology, Volume 114, Issue 9, 2003,
1532+
# Pages 1755-1761, ISSN 1388-2457
15251533
pass
15261534
elif reserved_notes[:5] == 'EDF+D':
15271535
raise Exception('EDF+ File: interrupted data records (not currently supported)')
@@ -1553,7 +1561,11 @@ def edf2mit(record_name, pn_dir=None, delete_file=True, record_only=True,
15531561
# Label (e.g., EEG FpzCz or Body temp) (16 bytes each)
15541562
sig_name = []
15551563
for _ in range(n_sig):
1556-
sig_name.append(struct.unpack('<16s', edf_file.read(16))[0].decode().strip())
1564+
temp_sig = struct.unpack('<16s', edf_file.read(16))[0].decode().strip()
1565+
if temp_sig == 'EDF Annotations' and not rdedfann_flag:
1566+
print('*** This may be an EDF+ Annotation file instead, please see '
1567+
'the `rdedfann` function. ***')
1568+
sig_name.append(temp_sig)
15571569
if verbose:
15581570
print('Signal Labels: {}'.format(sig_name))
15591571

@@ -1653,6 +1665,24 @@ def edf2mit(record_name, pn_dir=None, delete_file=True, record_only=True,
16531665
block_size = n_sig * [0]
16541666
base_datetime = datetime.datetime(start_year, start_month, start_day,
16551667
start_hour, start_minute, start_second)
1668+
base_time = datetime.time(base_datetime.hour,
1669+
base_datetime.minute,
1670+
base_datetime.second)
1671+
base_date = datetime.date(base_datetime.year,
1672+
base_datetime.month,
1673+
base_datetime.day)
1674+
1675+
if header_only:
1676+
return {
1677+
'fs': fs,
1678+
'sig_len': sig_len,
1679+
'n_sig': n_sig,
1680+
'base_date': base_date,
1681+
'base_time': base_time,
1682+
'units': units,
1683+
'sig_name': sig_name,
1684+
'comments': []
1685+
}
16561686

16571687
sig_data = np.empty((sig_len, n_sig))
16581688
temp_sig_data = np.fromfile(edf_file, dtype=np.int16)
@@ -1695,12 +1725,8 @@ def edf2mit(record_name, pn_dir=None, delete_file=True, record_only=True,
16951725
counter_freq = None,
16961726
base_counter = None,
16971727
sig_len = sig_len,
1698-
base_time = datetime.time(base_datetime.hour,
1699-
base_datetime.minute,
1700-
base_datetime.second),
1701-
base_date = datetime.date(base_datetime.year,
1702-
base_datetime.month,
1703-
base_datetime.day),
1728+
base_time = base_time,
1729+
base_date = base_date,
17041730
comments = [],
17051731
sig_name = sig_name, # Remove whitespace to make compatible later?
17061732
p_signal = sig_data,

0 commit comments

Comments
 (0)