|
5 | 5 | import re
|
6 | 6 | import posixpath
|
7 | 7 | import pdb
|
| 8 | +import struct |
8 | 9 |
|
9 | 10 | from wfdb.io import download
|
10 | 11 | from wfdb.io import _header
|
@@ -2546,6 +2547,156 @@ def csv2ann(file_name, extension='atr', fs=None, record_only=False,
|
2546 | 2547 | print('Finished writing Annotation file')
|
2547 | 2548 |
|
2548 | 2549 |
|
| 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 | + |
2549 | 2700 | ## ------------- Annotation Field Specifications ------------- ##
|
2550 | 2701 |
|
2551 | 2702 |
|
|
0 commit comments