Skip to content

Commit 87fc7da

Browse files
committed
enable read custom anntypes
1 parent 12f4bf5 commit 87fc7da

File tree

6 files changed

+170
-51
lines changed

6 files changed

+170
-51
lines changed

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,15 @@ the metadata as a dictionary.
8080

8181
::
8282

83-
sig, fields = rdsamp(recordname, sampfrom=0, sampto=[], channels=[], physical=1,
83+
wrsamp(recordname, sampfrom=0, sampto=[], channels=[], physical=1,
8484
stacksegments=1, pbdl=0, dldir=os.cwd())
8585

8686
Example Usage:
8787

8888
::
8989

9090
import wfdb
91-
sig, fields = wfdb.rdsamp('mitdb/100', sampto=2000, pbdl=1)
91+
sig, fields = wfdb.wrsamp('mitdb/100', sampto=2000, pbdl=1)
9292

9393
Input Arguments:
9494

wfdb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
from .records import Record, MultiRecord, rdheader, rdsamp, srdsamp, wrsamp
44
from .annotations import Annotation, rdann, wrann, showanncodes
5-
from .plots import plotrec
5+
from .plots import plotrec, plotann

wfdb/annotations.py

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -432,12 +432,15 @@ def rdann(recordname, annotator, sampfrom=0, sampto=None):
432432
# Snip the unallocated end of the arrays
433433
annsamp,anntype,num,subtype,chan,aux = snip_arrays(annsamp,anntype,num,subtype,chan,aux,ai)
434434

435+
# Process the fields if there are custom annotation types
436+
allannsyms,annsamp,anntype,num,subtype,chan,aux = proccustomtypes(annsamp,anntype,num,subtype,chan,aux)
437+
435438
# Apply annotation range (from X to Y)
436439
annsamp,anntype,num,subtype,chan,aux = apply_annotation_range(annsamp,
437440
sampfrom,sampto,anntype,num,subtype,chan,aux)
438441

439-
# Set the annotation type to annotation codes
440-
anntype = [annsyms[code] for code in anntype]
442+
# Set the annotation type to the annotation codes
443+
anntype = [allannsyms[code] for code in anntype]
441444

442445
# Store fields in an Annotation object
443446
annotation = Annotation(recordname, annotator, annsamp, anntype,
@@ -539,7 +542,7 @@ def snip_arrays(annsamp,anntype,num,subtype,chan,aux,ai):
539542
aux = aux[0:ai]
540543
return annsamp,anntype,num,subtype,chan,aux
541544

542-
# Keep only the specified annotations
545+
# Keep annotations within a sample range
543546
def apply_annotation_range(annsamp,sampfrom,sampto,anntype,num,subtype,chan,aux):
544547

545548
returnempty = 0
@@ -578,6 +581,43 @@ def apply_annotation_range(annsamp,sampfrom,sampto,anntype,num,subtype,chan,aux)
578581
return annsamp,anntype,num,subtype,chan,aux
579582

580583

584+
# Process the fields if there are custom annotation types
585+
def proccustomtypes(annsamp,anntype,num,subtype,chan,aux):
586+
# Custom anncodes appear as regular annotations in the form:
587+
# sample = 0, anntype = 22 (note annotation '"'), aux = "NUMBER[ \t]CUSTOMANNCODE[ \t]Calibration"
588+
s0 = np.where(annsamp == 0)[0]
589+
t22 = np.where(anntype == 22)[0]
590+
s0t22 = list(set(s0).intersection(t22))
591+
592+
allannsyms = annsyms.copy()
593+
if s0t22 != []:
594+
# The custom anncode indices
595+
custominds = []
596+
# Check aux for custom codes
597+
for i in s0t22:
598+
acceptedstring = re.match('(\d+)[ \t](\w+)[ \t]Calibration', aux[i])
599+
# Found custom annotation code.
600+
if acceptedstring is not None and acceptedstring.string==aux[i]:
601+
# Keep track of index
602+
custominds.append(i)
603+
# Add code to annsym dictionary
604+
codenum, codesym = acceptedstring.group(1, 2)
605+
allannsyms[int(codenum)] = codesym
606+
607+
# Remove the attributes with the custom anncode indices
608+
if custominds != []:
609+
keepinds = [i for i in range(len(annsamp)) if i not in custominds]
610+
611+
annsamp = annsamp[keepinds]
612+
anntype = anntype[keepinds]
613+
num = num[keepinds]
614+
subtype = subtype[keepinds]
615+
chan = chan[keepinds]
616+
aux = [aux[i] for i in keepinds]
617+
618+
return (allannsyms,annsamp,anntype,num,subtype,chan,aux)
619+
620+
581621
## ------------- /Reading Annotations ------------- ##
582622

583623

@@ -599,9 +639,9 @@ def apply_annotation_range(annsamp,sampfrom,sampto,anntype,num,subtype,chan,aux)
599639
12: '/', # paced beat */
600640
13: 'Q', # unclassifiable beat */
601641
14: '~', # signal quality change */
602-
#15: '[15]',
642+
15: '[15]',
603643
16: '|', # isolated QRS-like artifact */
604-
#17: '[17]',
644+
17: '[17]',
605645
18: 's', # ST change */
606646
19: 'T', # T-wave change */
607647
20: '*', # systole */
@@ -624,18 +664,16 @@ def apply_annotation_range(annsamp,sampfrom,sampto,anntype,num,subtype,chan,aux)
624664
37: 'x', # non-conducted P-wave (blocked APB) */
625665
38: 'f', # fusion of paced and normal beat */
626666
39: '(', # waveform onset */
627-
# 39: 'PQ', # PQ junction (beginning of QRS) */
628667
40: ')', # waveform end */
629-
# 40: 'JPT', # J point (end of QRS) */
630668
41: 'r', # R-on-T premature ventricular contraction */
631-
#42: '[42]',
632-
#43: '[43]',
633-
#44: '[44]',
634-
#45: '[45]',
635-
#46: '[46]',
636-
#47: '[47]',
637-
#48: '[48]',
638-
#49: '[49]',
669+
42: '[42]',
670+
43: '[43]',
671+
44: '[44]',
672+
45: '[45]',
673+
46: '[46]',
674+
47: '[47]',
675+
48: '[48]',
676+
49: '[49]',
639677
}
640678
# Reverse ann symbols for mapping symbols back to numbers
641679
revannsyms = {v: k for k, v in annsyms.items()}
@@ -658,7 +696,9 @@ def apply_annotation_range(annsamp,sampfrom,sampto,anntype,num,subtype,chan,aux)
658696
12: 'PACE', # paced beat */
659697
13: 'UNKNOWN', # unclassifiable beat */
660698
14: 'NOISE', # signal quality change */
699+
15: '',
661700
16: 'ARFCT', # isolated QRS-like artifact */
701+
17: '',
662702
18: 'STCH', # ST change */
663703
19: 'TCH', # T-wave change */
664704
20: 'SYSTOLE', # systole */
@@ -681,10 +721,16 @@ def apply_annotation_range(annsamp,sampfrom,sampto,anntype,num,subtype,chan,aux)
681721
37: 'NAPC', # non-conducted P-wave (blocked APB) */
682722
38: 'PFUS', # fusion of paced and normal beat */
683723
39: 'WFON', # waveform onset */
684-
# 39: 'PQ', # PQ junction (beginning of QRS) */
685724
40: 'WFOFF', # waveform end */
686-
# 40: 'JPT', # J point (end of QRS) */
687-
41: 'RONT' # R-on-T premature ventricular contraction */
725+
41: 'RONT', # R-on-T premature ventricular contraction */
726+
42: '',
727+
43: '',
728+
44: '',
729+
45: '',
730+
46: '',
731+
47: '',
732+
48: '',
733+
49: ''
688734
}
689735

690736
# Mapping annotation symbols to the annotation codes

wfdb/plots.py

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import numpy as np
2+
from . import records
23
from . import _headers
4+
from . import annotations
35
import matplotlib.pyplot as plt
46

7+
# Plot a WFDB record's signals
58
def plotrec(record=None, signals = None, fields = None, title = None, timeunits='samples', returnfig = False):
69

710
# Figure out which arguments to use to plot
@@ -19,7 +22,7 @@ def plotrec(record=None, signals = None, fields = None, title = None, timeunits=
1922
for ch in range(nsig):
2023
# Plot signal channel
2124
plt.subplot(100*nsig+11+ch)
22-
plt.plot(t, sig[:,ch])
25+
plt.plot(t, signals[:,ch])
2326

2427
if (title is not None) and (ch==0):
2528
plt.title(title)
@@ -30,7 +33,7 @@ def plotrec(record=None, signals = None, fields = None, title = None, timeunits=
3033
else:
3134
plt.xlabel('time/'+timeunits[:-1])
3235

33-
if fields["signame"][ch] is not none:
36+
if fields["signame"][ch] is not None:
3437
chanlabel=fields["signame"][ch]
3538
else:
3639
chanlabel='channel'
@@ -57,7 +60,7 @@ def getplotitems(record, signals, fields):
5760
# Use the record object
5861
if signals is None:
5962
# If it is a MultiRecord, convert it into single
60-
if type(record) == MultiRecord:
63+
if type(record) == records.MultiRecord:
6164
record = record.multi_to_single()
6265

6366
# Need to ensure p_signals is present
@@ -83,23 +86,23 @@ def checkplotitems(signals, fields, title, timeunits):
8386
siglen, nsig = signals.shape
8487

8588
# fs and timeunits
86-
allowedtimes = ['samples, seconds, minutes, hours']
89+
allowedtimes = ['samples', 'seconds', 'minutes', 'hours']
8790
if timeunits not in allowedtimes:
8891
print("The 'timeunits' field must be one of the following: ", allowedtimes)
8992
sys.exit()
9093
# Get x axis values. fs must be valid when plotting time
9194
if timeunits == 'samples':
92-
t = np.linspace(siglen)
95+
t = np.linspace(0, siglen-1, siglen)
9396
else:
9497
if type(fields['fs']) not in _headers.floattypes:
9598
sys.exit("The 'fs' field must be a number")
9699

97100
if timeunits == 'seconds':
98-
t = np.linspace(siglen)/fs
101+
t = np.linspace(0, siglen-1, siglen)/fs
99102
elif timeunits == 'minutes':
100-
t = np.linspace(siglen)/fs/60
103+
t = np.linspace(0, siglen-1, siglen)/fs/60
101104
else:
102-
t = np.linspace(siglen)/fs/3600
105+
t = np.linspace(0, siglen-1, siglen)/fs/3600
103106

104107
# units
105108
if fields['units'] is None:
@@ -124,10 +127,66 @@ def checkplotitems(signals, fields, title, timeunits):
124127
return fields, t
125128

126129

130+
# Plot the sample locations of a WFDB annotation
131+
def plotann(annotation, title = None, timeunits = 'samples', returnfig = False):
132+
133+
# Check the validity of items used to make the plot
134+
# Get the x axis annotation values to plot
135+
plotvals = checkannplotitems(annotation, title, timeunits)
136+
137+
# Create the plot
138+
fig=plt.figure()
139+
140+
plt.plot(plotvals, np.zeros(len(plotvals)), 'r+')
141+
142+
if title is not None:
143+
plt.title(title)
144+
145+
# Axis Labels
146+
if timeunits == 'samples':
147+
plt.xlabel('index/sample')
148+
else:
149+
plt.xlabel('time/'+timeunits[:-1])
150+
151+
plt.show(fig)
152+
153+
# Return the figure if requested
154+
if returnfig:
155+
return fig
127156

157+
# Check the validity of items used to make the annotation plot
158+
def checkannplotitems(annotation, title, timeunits):
159+
160+
# signals
161+
if type(annotation)!= annotations.Annotation:
162+
sys.exit("The 'annotation' field must be a 'wfdb.Annotation' object")
128163

164+
# fs and timeunits
165+
allowedtimes = ['samples', 'seconds', 'minutes', 'hours']
166+
if timeunits not in allowedtimes:
167+
print("The 'timeunits' field must be one of the following: ", allowedtimes)
168+
sys.exit()
129169

170+
# fs must be valid when plotting time
171+
if timeunits != 'samples':
172+
if type(annotation.fs) not in _headers.floattypes:
173+
sys.exit("In order to plot time units, the Annotation object must have a valid 'fs' attribute")
130174

175+
# Get x axis values to plot
176+
if timeunits == 'samples':
177+
plotvals = annotation.annsamp
178+
elif timeunits == 'seconds':
179+
plotvals = annotation.annsamp
180+
elif timeunits == 'minutes':
181+
plotvals = annotation.annsamp
182+
else:
183+
t = np.linspace(0, siglen-1, siglen)/fs/3600
184+
185+
# title
186+
if title is not None and type(title) != str:
187+
sys.exit("The 'title' field must be a string")
188+
189+
return plotvals
131190

132191

133192

@@ -189,7 +248,4 @@ def plotreco(sig, fields, annsamp=None, annch=[0], title=None, plottime=1):
189248
unitlabel='NU'
190249
plt.ylabel(chanlabel+"/"+unitlabel)
191250

192-
plt.show(f1)
193-
194-
def plotann():
195-
print('on it')
251+
plt.show(f1)

wfdb/records.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def checkfield(self, field):
150150
for f in self.segname:
151151
if f == '~':
152152
continue
153-
acceptedstring = re.match('[-\w]+',f)
153+
acceptedstring = re.match('[-\w]+',f).string
154154
if not acceptedstring or acceptedstring.string != f:
155155
sys.exit("Non-null segment names may only contain alphanumerics. Null segment names must be equal to '~'")
156156
elif field == 'seglen':
@@ -225,8 +225,7 @@ class Record(BaseRecord, _headers.HeadersMixin, _signals.SignalsMixin):
225225
def __init__(self, p_signals=None, d_signals=None,
226226
recordname=None, nsig=None,
227227
fs=None, counterfreq=None, basecounter=None,
228-
siglen=N
229-
one, basetime=None, basedate=None,
228+
siglen=None, basetime=None, basedate=None,
230229
filename=None, fmt=None, sampsperframe=None,
231230
skew=None, byteoffset=None, adcgain=None,
232231
baseline=None, units=None, adcres=None,

0 commit comments

Comments
 (0)