@@ -19,11 +19,11 @@ class Annotation(object):
19
19
- anntype: The annotation type according the the standard WFDB codes.
20
20
- subtype: The marked class/category of the annotation.
21
21
- chan: The signal channel associated with the annotations.
22
- - num: The labelled annotation number.
22
+ - num: The labelled annotation number.
23
23
- aux: The auxiliary information string for the annotation.
24
24
- fs: The sampling frequency of the record if contained in the annotation file.
25
25
- custom_anntypes: The custom annotation types defined in the annotation file.
26
- A dictionary with {key:value} corresponding to {anntype:description}.
26
+ A dictionary with {key:value} corresponding to {anntype:description}.
27
27
eg. {'#': 'lost connection', 'C': 'reconnected'}
28
28
29
29
Constructor function:
@@ -114,12 +114,13 @@ def checkfields(self):
114
114
def checkfield (self , field ):
115
115
116
116
# Non list/array fields
117
- if field in ['recordname' , 'annotator' , 'fs' ]:
117
+ if field in ['recordname' , 'annotator' , 'fs' , 'custom_anntypes' ]:
118
118
# Check the field type
119
119
if type (getattr (self , field )) not in annfieldtypes [field ]:
120
- print (annfieldtypes [field ])
121
- raise TypeError ('The ' + field + ' field must be one of the above types.' )
122
-
120
+ if len (annfieldtypes [field ]> 1 ):
121
+ raise TypeError ('The ' + field + ' field must be one of the following types:' , annfieldtypes )
122
+ else :
123
+ raise TypeError ('The ' + field + ' field must be the following type:' , annfieldtypes [0 ])
123
124
# Field specific checks
124
125
if field == 'recordname' :
125
126
# Allow letters, digits, hyphens, and underscores.
@@ -134,6 +135,25 @@ def checkfield(self, field):
134
135
elif field == 'fs' :
135
136
if self .fs <= 0 :
136
137
raise ValueError ('The fs field must be a non-negative number' )
138
+ elif field == 'custom_anntypes' :
139
+ # All key/values must be strings
140
+ for key in self .custom_anntypes .keys ():
141
+ if type (key )!= str :
142
+ raise ValueError ('All custom_anntypes keys must be strings' )
143
+ if len (key )> 1 :
144
+ raise ValueError ('All custom_anntypes keys must be single characters' )
145
+ # Discourage (but not prevent) using existing codes
146
+ if key in annsyms .values ():
147
+ print ('It is discouraged to define the custom annotation code: ' + key + ' that already has an entry in the wfdb library.' )
148
+ print ('To see existing annotation codes and their meanings, call: showanncodes(). Continuing...' )
149
+
150
+ for value in self .custom_anntypes .values ():
151
+ if type (key )!= str :
152
+ raise ValueError ('All custom_anntypes dictionary values must be strings' )
153
+ # No pointless characters
154
+ acceptedstring = re .match ('[\w -]+' , value )
155
+ if not acceptedstring or acceptedstring .string != value :
156
+ raise ValueError ('custom_anntypes dictionary values must only contain alphanumerics, spaces, underscores, and dashes' )
137
157
138
158
else :
139
159
fielditem = getattr (self , field )
@@ -147,8 +167,7 @@ def checkfield(self, field):
147
167
if field in ['annsamp' ,'anntype' ]:
148
168
for item in fielditem :
149
169
if type (item ) not in annfieldtypes [field ]:
150
- print ("All elements of the '" , field , "' field must be one of the following types:" )
151
- print (annfieldtypes [field ])
170
+ print ("All elements of the '" + field + "' field must be one of the following types:" , annfieldtypes [field ])
152
171
print ("All elements must be present" )
153
172
raise Exception ()
154
173
else :
@@ -170,11 +189,12 @@ def checkfield(self, field):
170
189
if max (sampdiffs ) > 2147483648 :
171
190
raise ValueError ('WFDB annotation files cannot store sample differences greater than 2**31' )
172
191
elif field == 'anntype' :
173
- # Ensure all fields lie in standard WFDB annotation codes
174
- if set (self .anntype ) - set (annsyms .values ()) != set ():
175
- print ("The 'anntype' field contains items not encoded in the WFDB annotation library." )
192
+ # Ensure all fields lie in standard WFDB annotation codes or custom codes
193
+ if set (self .anntype ) - set (annsyms .values ()). union () != set ():
194
+ print ("The 'anntype' field contains items not encoded in the WFDB library, or in this object's custom defined anntypes ." )
176
195
print ('To see the valid annotation codes call: showanncodes()' )
177
196
print ('To transfer non-encoded anntype items into the aux field call: self.type2aux()' )
197
+ print ("To define custom codes, set the custom_anntypes field as a dictionary with format: {custom anntype character:description}" )
178
198
raise Exception ()
179
199
elif field == 'subtype' :
180
200
# signed character
@@ -204,20 +224,23 @@ def checkfieldcohesion(self):
204
224
# Write an annotation file
205
225
def wrannfile (self ):
206
226
207
- # If there is an fs, write it
227
+ # Calculate the fs bytes to write if present
208
228
if self .fs is not None :
209
229
fsbytes = fs2bytes (self .fs )
210
230
else :
211
- fsbytes = None
231
+ fsbytes = []
232
+
233
+ # Calculate the custom_anntypes bytes to write if present
234
+ if self .custom_anntypes is not None :
235
+ cabytes = ca2bytes (self .custom_anntypes )
236
+ else :
237
+ cabytes = []
212
238
213
239
# Calculate the main bytes to write
214
240
databytes = self .fieldbytes ()
215
241
216
- # Combine all bytes to write including file terminator
217
- if fsbytes is not None :
218
- databytes = np .concatenate ((fsbytes , databytes , np .array ([0 ,0 ]).astype ('u1' )))
219
- else :
220
- databytes = np .concatenate ((databytes , np .array ([0 ,0 ]).astype ('u1' )))
242
+ # Combine all bytes to write: fs (if any), custom annotations(if any), main content, file terminator
243
+ databytes = np .concatenate ((fsbytes , cabytes , databytes , np .array ([0 ,0 ]).astype ('u1' )))
221
244
222
245
# Write the file
223
246
with open (self .recordname + '.' + self .annotator , 'wb' ) as f :
@@ -308,6 +331,51 @@ def fs2bytes(fs):
308
331
309
332
return np .array (databytes ).astype ('u1' )
310
333
334
+ # Calculate the bytes written to the annotation file for the custom_anntypes field
335
+ def ca2bytes (custom_anntypes ):
336
+
337
+ # The start wrapper: '0 NOTE length AUX ## annotation type definitions'
338
+ headbytes = [0 ,88 ,30 ,252 ,35 ,35 ,32 ,97 ,110 ,110 ,111 ,116 ,97 ,116 ,105 ,111 ,110 ,32 ,116
339
+ 121 ,112 ,101 ,32 ,100 ,101 ,102 ,105 ,110 ,105 ,116 ,105 ,111 ,110 ,115 ]
340
+
341
+ # The end wrapper: '0 NOTE length AUX ## end of definitions' followed by SKIP -1, +1
342
+ tailbytes = [0 ,88 ,21 ,252 ,35 ,35 ,32 ,101 ,110 ,100 ,32 ,111 ,102 ,32 ,100 ,101 ,102 ,105 ,110 ,
343
+ 105 ,116 ,105 ,111 ,110 ,115 ,0 ,0 ,236 ,255 ,255 ,255 ,255 ,1 ,0 ]
344
+
345
+ # Annotation codes range from 0-49.
346
+ freenumbers = list (set (range (50 )) - set (annsyms .keys ()))
347
+
348
+ if len (custom_anntypes ) > len (freenumbers ):
349
+ raise Exception ('There can only be a maximum of ' + len (freenumbers )+ ' custom annotation codes.' )
350
+
351
+ # Allocate a number to each custom anntype.
352
+ # List sublists: [number, code, description]
353
+ writecontent = []
354
+ for i in range (len (custom_anntypes )):
355
+ writecontent .append ([i ,custom_anntypes .keys ()[i ],custom_anntypes .values ()[i ]])
356
+
357
+ custombytes = [customcode2bytes (triplet ) for triplet in writecontent ]
358
+ custombytes = [item for sublist in custombytes for item in sublist ]
359
+
360
+ return np .array (headbytes + custombytes + tailbytes ).astype ('u1' )
361
+
362
+ # Convert triplet of [number, codesymbol (character), description] into annotation bytes
363
+ # Helper function to ca2bytes
364
+ def customcode2bytes (c_triplet ):
365
+
366
+ # Structure: 0, NOTE, len(aux), AUX, codenumber, space, codesymbol, space, description, (0 null if necessary)
367
+ # Remember, aux string includes 'number(s)<space><symbol><space><description>''
368
+ annbytes = [0 , 88 , len (c_triplet [2 ]) + 3 + len (str (c_triplet [0 ])), 252 ] + [ord (c ) for c in str (c_triplet [0 ])] \
369
+ + [32 ] + ord (c_triplet [1 ]) + [32 ] + [ord (c ) for c in c_triplet [2 ]]
370
+
371
+ if len (annbytes ) % 2 :
372
+ annbytes .append (0 )
373
+
374
+ return annbytes
375
+
376
+
377
+
378
+
311
379
# Convert an annotation field into bytes to write
312
380
def field2bytes (field , value ):
313
381
@@ -791,6 +859,7 @@ def proc_special_types(annsamp,anntype,num,subtype,chan,aux):
791
859
792
860
# Annotation mnemonic symbols for the 'anntype' field as specified in annot.c
793
861
# from wfdb software library 10.5.24. At this point, several values are blank.
862
+ # Commented out values are present in original file but have no meaning.
794
863
annsyms = {
795
864
0 : ' ' , # not-QRS (not a getann/putann codedict) */
796
865
1 : 'N' , # normal beat */
@@ -807,9 +876,9 @@ def proc_special_types(annsamp,anntype,num,subtype,chan,aux):
807
876
12 : '/' , # paced beat */
808
877
13 : 'Q' , # unclassifiable beat */
809
878
14 : '~' , # signal quality change */
810
- 15 : '[15]' ,
879
+ # 15: '[15]',
811
880
16 : '|' , # isolated QRS-like artifact */
812
- 17 : '[17]' ,
881
+ # 17: '[17]',
813
882
18 : 's' , # ST change */
814
883
19 : 'T' , # T-wave change */
815
884
20 : '*' , # systole */
@@ -834,20 +903,21 @@ def proc_special_types(annsamp,anntype,num,subtype,chan,aux):
834
903
39 : '(' , # waveform onset */
835
904
40 : ')' , # waveform end */
836
905
41 : 'r' , # R-on-T premature ventricular contraction */
837
- 42 : '[42]' ,
838
- 43 : '[43]' ,
839
- 44 : '[44]' ,
840
- 45 : '[45]' ,
841
- 46 : '[46]' ,
842
- 47 : '[47]' ,
843
- 48 : '[48]' ,
844
- 49 : '[49]' ,
906
+ # 42: '[42]',
907
+ # 43: '[43]',
908
+ # 44: '[44]',
909
+ # 45: '[45]',
910
+ # 46: '[46]',
911
+ # 47: '[47]',
912
+ # 48: '[48]',
913
+ # 49: '[49]',
845
914
}
846
915
# Reverse ann symbols for mapping symbols back to numbers
847
916
revannsyms = {v : k for k , v in annsyms .items ()}
848
917
849
918
# Annotation codes for 'anntype' field as specified in ecgcodes.h from
850
- # wfdb software library 10.5.24
919
+ # wfdb software library 10.5.24. Commented out values are present in
920
+ # original file but have no meaning.
851
921
anncodes = {
852
922
0 : 'NOTQRS' , # not-QRS (not a getann/putann codedict) */
853
923
1 : 'NORMAL' , # normal beat */
@@ -864,9 +934,9 @@ def proc_special_types(annsamp,anntype,num,subtype,chan,aux):
864
934
12 : 'PACE' , # paced beat */
865
935
13 : 'UNKNOWN' , # unclassifiable beat */
866
936
14 : 'NOISE' , # signal quality change */
867
- 15 : '' ,
937
+ # 15: '',
868
938
16 : 'ARFCT' , # isolated QRS-like artifact */
869
- 17 : '' ,
939
+ # 17: '',
870
940
18 : 'STCH' , # ST change */
871
941
19 : 'TCH' , # T-wave change */
872
942
20 : 'SYSTOLE' , # systole */
@@ -890,23 +960,24 @@ def proc_special_types(annsamp,anntype,num,subtype,chan,aux):
890
960
38 : 'PFUS' , # fusion of paced and normal beat */
891
961
39 : 'WFON' , # waveform onset */
892
962
40 : 'WFOFF' , # waveform end */
893
- 41 : 'RONT' , # R-on-T premature ventricular contraction */
894
- 42 : '' ,
895
- 43 : '' ,
896
- 44 : '' ,
897
- 45 : '' ,
898
- 46 : '' ,
899
- 47 : '' ,
900
- 48 : '' ,
901
- 49 : ''
963
+ 41 : 'RONT' , # R-on-T premature ventricular contraction */
964
+ # 42: '',
965
+ # 43: '',
966
+ # 44: '',
967
+ # 45: '',
968
+ # 46: '',
969
+ # 47: '',
970
+ # 48: '',
971
+ # 49: ''
902
972
}
903
973
904
974
# Mapping annotation symbols to the annotation codes
905
975
# For printing/user guidance
906
976
symcodes = pd .DataFrame ({'Ann Symbol' : list (annsyms .values ()), 'Ann Code Meaning' : list (anncodes .values ())})
907
977
symcodes = symcodes .set_index ('Ann Symbol' , list (annsyms .values ()))
908
978
909
- annfields = ['recordname' , 'annotator' , 'annsamp' , 'anntype' , 'num' , 'subtype' , 'chan' , 'aux' , 'fs' , 'custom_anntypes' ]
979
+ # All annotation fields. Note: custom_anntypes placed first to check field before anntype
980
+ annfields = ['recordname' , 'annotator' , 'custom_anntypes' , 'annsamp' , 'anntype' , 'num' , 'subtype' , 'chan' , 'aux' , 'fs' ]
910
981
911
982
annfieldtypes = {'recordname' : [str ], 'annotator' : [str ], 'annsamp' : _headers .inttypes ,
912
983
'anntype' : [str ], 'num' :_headers .inttypes , 'subtype' : _headers .inttypes ,
0 commit comments