def correct_bxc_bxd(): UNITS.set_sampling_freq(360.0) ANNOTS_DIR = ('/home/remoto/tomas.teijeiro/Escritorio/anots_dani/') RECORDS = [ 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 111, 112, 113, 114, 115, 116, 117, 118, 119, 121, 122, 123, 124, 200, 201, 202, 203, 205, 207, 208, 209, 210, 212, 213, 214, 215, 217, 219, 220, 221, 222, 223, 228, 230, 231, 232, 233, 234 ] for rec in RECORDS: REF = ANNOTS_DIR + str(rec) + '.atr' TEST = ANNOTS_DIR + str(rec) + '.bxd' OUT = ANNOTS_DIR + str(rec) + '.bxD' ref = SortedList(MIT.read_annotations(REF)) test = MIT.read_annotations(TEST) for tann in test: dummy = MIT.MITAnnotation() dummy.time = int(tann.time - UNITS.msec2samples(150)) idx = ref.bisect_left(dummy) try: rann = next( a for a in ref[idx:] if MIT.is_qrs_annotation(a) and abs(a.time - tann.time) <= UNITS.msec2samples(150)) tann.code = rann.code except StopIteration: pass MIT.save_annotations(test, OUT) print('Record {0} processed'.format(rec)) print('The full database was successfully processed')
def correct_bxb(): ANNOTS_DIR = ('/home/remoto/tomas.teijeiro/Escritorio/anots_dani/') RECORDS = [ 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 111, 112, 113, 114, 115, 116, 117, 118, 119, 121, 122, 123, 124, 200, 201, 202, 203, 205, 207, 208, 209, 210, 212, 213, 214, 215, 217, 219, 220, 221, 222, 223, 228, 230, 231, 232, 233, 234 ] for rec in RECORDS: IN_FILE = ANNOTS_DIR + str(rec) + '.bxb' OUT_FILE = ANNOTS_DIR + str(rec) + '.bxd' anots = MIT.read_annotations(IN_FILE) out = [] #Corrections according to the -o flag of the bxb utility. for ann in anots: if MIT.is_qrs_annotation(ann): out.append(ann) #Missed beats elif ann.code == CODES.NOTE and ann.aux[0] not in ('O', 'X'): new = MIT.MITAnnotation() new.code = CODES.CHARMAP[ann.aux[0]] new.time = ann.time out.append(new) MIT.save_annotations(out, OUT_FILE) print('Record {0} processed'.format(rec)) print('The full database was successfully processed')
#Now we write the .hea and the .dat files. heafmt = ('{0} 1 {1} {2} {3}\n' '{0}.dat 16 {4} 16 0 0 0 0 MLII\n') with open(PATH + NAME + '.hea', 'w') as hea: hea.write(heafmt.format(NAME, FREQ, len(sig), tp.strftime('%H:%M:%S %d/%m/%Y'), GAIN)) with open(PATH + NAME + '.dat', 'w') as dat: fmt = '<'+'h'*len(sig) dat.write(struct.pack(fmt, *sig)) #And we create the (AFIB annotations according to the loaded episodes. etp = tp + dt.timedelta(milliseconds=len(sig)*4) devid = next((d for d in AF_EPISODES if NAME.startswith(d)), None) annots = [] if devid is not None: afibs = [ep for ep in AF_EPISODES[devid] if tp <= ep.start <= etp] for af in afibs: #Two annotations for each episode bann = MITAnnotation.MITAnnotation() bann.code = ECGCodes.RHYTHM bann.time = int((af.start-tp).total_seconds()*FREQ) bann.aux = b'(AFIB' eann = MITAnnotation.MITAnnotation() eann.code = ECGCodes.RHYTHM eann.time = int((min(etp, af.end)-tp).total_seconds()*FREQ) #The end of AF is encoded as 'back to normality' eann.aux = b'(N' annots.append(bann) annots.append(eann) #Annotations are stored in a file with the '.mbg' extension. MITAnnotation.save_annotations(annots, PATH+NAME+'.mbg')
def interp2ann(interp, btime=0, offset=0, include_format=True): """ Generates a list of annotations representing the observations from an interpretation. The *btime* optional parameter allows to include only the observations after a specific time point, and *offset* allows to define a constant time to be added to the time point of each annotation. An optional format annotation of type NOTE can be included at the beginning. NOTE: A first annotation is included at the beginning of the list, with time=*offset*, to indicate that the annotations are created with the specific format for Construe interpretations. This format includes the following features (for version 17.01): - Beat annotations include the specific delineation information for each lead in a dictionary in JSON format. The keys in this dictionary are the lead names, and the values are a sequence of integer numbers. Each triple in this sequence determines a wave within the QRS complex. - WFON and WFOFF annotations include the type of wave they delimit in the *subtyp* field. QRS complexes are described by the SYSTOLE code, while P and T waves limits have the PWAVE or TWAVE code, respectively. - PWAVE and TWAVE annotations include the amplitude of each lead, in a dictionary in JSON format in the AUX field. """ annots = sortedcontainers.SortedList() if include_format: fmtcode = MITAnnotation.MITAnnotation() fmtcode.code = C.NOTE fmtcode.time = int(offset) fmtcode.aux = FMT_STRING annots.add(fmtcode) beats = list(interp.get_observations(o.QRS, filt=lambda q: q.time.start >= btime)) #We get the beat observations in the best explanation branch. for beat in beats: #We tag all beats as normal, and we include the delineation. The #delineation on each lead is included as a json string in the peak #annotation. beg = MITAnnotation.MITAnnotation() beg.code = C.WFON beg.subtype = C.SYSTOLE beg.time = int(offset + beat.earlystart) peak = MITAnnotation.MITAnnotation() peak.code = beat.tag peak.time = int(offset + beat.time.start) delin = {} for lead in beat.shape: shape = beat.shape[lead] displ = beg.time-peak.time shape.move(displ) waveseq = sum((w.pts for w in shape.waves), tuple()) delin[lead] = tuple(int(w) for w in waveseq) shape.move(-displ) peak.aux = json.dumps(delin) end = MITAnnotation.MITAnnotation() end.code = C.WFOFF end.subtype = C.SYSTOLE end.time = int(offset + beat.lateend) annots.add(beg) annots.add(peak) annots.add(end) #P and T wave annotations pstart = beats[0].earlystart - ms2sp(400) if beats else 0 tend = beats[-1].lateend + ms2sp(400) if beats else 0 for wtype in (o.PWave, o.TWave): for wave in interp.get_observations(wtype, pstart, tend): if wave.earlystart >= btime: code = C.PWAVE if wtype is o.PWave else C.TWAVE beg = MITAnnotation.MITAnnotation() beg.code = C.WFON beg.subtype = code beg.time = int(offset + wave.earlystart) end = MITAnnotation.MITAnnotation() end.code = C.WFOFF end.subtype = code end.time = int(offset + wave.lateend) peak = MITAnnotation.MITAnnotation() peak.code = code peak.time = int((end.time+beg.time)/2.) peak.aux = json.dumps(wave.amplitude) annots.add(beg) annots.add(peak) annots.add(end) #Flutter annotations for flut in interp.get_observations(o.Ventricular_Flutter, btime): vfon = MITAnnotation.MITAnnotation() vfon.code = C.VFON vfon.time = int(offset + flut.earlystart) annots.add(vfon) for vfw in interp.get_observations(o.Deflection, flut.earlystart, flut.lateend): wav = MITAnnotation.MITAnnotation() wav.code = C.FLWAV wav.time = int(offset + vfw.time.start) annots.add(wav) vfoff = MITAnnotation.MITAnnotation() vfoff.code = C.VFOFF vfoff.time = int(offset + flut.lateend) annots.add(vfoff) #All rhythm annotations for rhythm in interp.get_observations(o.Cardiac_Rhythm, btime): if not isinstance(rhythm, o.RhythmStart): rhyon = MITAnnotation.MITAnnotation() rhyon.code = C.RHYTHM rhyon.aux = C.RHYTHM_AUX[type(rhythm)] rhyon.time = int(offset + rhythm.earlystart) annots.add(rhyon) #The end of the last rhythm is also added as an annotation try: rhyoff = MITAnnotation.MITAnnotation() rhyoff.code = C.RHYTHM rhyoff.aux = ')' rhyoff.time = int(offset + rhythm.earlyend) annots.add(rhyoff) except NameError: #If there are no rhythms ('rhythm' variable is undefined), we go on pass #Unintelligible R-Deflections for rdef in interp.get_observations(o.RDeflection, btime, filt=lambda a: a in interp.unintelligible or a in interp.focus): unint = MITAnnotation.MITAnnotation() #We store unintelligible annotations as artifacts unint.code = C.ARFCT unint.time = int(offset + rdef.earlystart) annots.add(unint) return annots
def _standardize_rhythm_annots(annots): """ Standardizes a set of annotations obtained from the interpretation procedure to make them compatible with the criteria applied in the MIT-BIH Arrhythmia database in the labeling of rhythms. """ dest = sortedcontainers.SortedList() for ann in annots: code = ann.code if code in (ECGCodes.RHYTHM, ECGCodes.VFON): #TODO remove this if not necessary if code is ECGCodes.VFON: newann = MITAnnotation.MITAnnotation() newann.code = ECGCodes.RHYTHM newann.aux = b'(VFL' newann.time = ann.time dest.add(newann) ############################################################ #For convention with original annotations, we only admit # #bigeminies with more than two pairs, and trigeminies with # #more than two triplets, # ############################################################ if ann.aux == b'(B': end = next((a for a in annots if a.time > ann.time and a.code in (ECGCodes.RHYTHM, ECGCodes.VFON)), annots[-1]) nbeats = searching.ilen(a for a in annots if a.time >= ann.time and a.time <= end.time and MITAnnotation.is_qrs_annotation(a)) if nbeats < 7: continue if ann.aux == '(T': end = next((a for a in annots if a.time > ann.time and a.code in (ECGCodes.RHYTHM, ECGCodes.VFON)), annots[-1]) nbeats = searching.ilen(a for a in annots if a.time >= ann.time and a.time <= end.time and MITAnnotation.is_qrs_annotation(a)) if nbeats < 7: continue ############################################################# # Pauses and missed beats are replaced by bradycardias (for # # consistency with the reference annotations). # ############################################################# if ann.aux in (b'(BK', b'P'): ann.aux = b'(SBR' if ann.aux not in (b'(EXT', b'(CPT'): prev = next((a for a in reversed(dest) if a.code is ECGCodes.RHYTHM), None) if prev is None or prev.aux != ann.aux: dest.add(ann) else: dest.add(ann) ################################# #Atrial fibrillation correction # ################################# iterator = iter(dest) afibtime = 0 while True: try: start = next(a.time for a in iterator if a.code == ECGCodes.RHYTHM and a.aux == b'(AFIB') end = next((a.time for a in iterator if a.code == ECGCodes.RHYTHM), dest[-1].time) afibtime += end-start except StopIteration: break #If more than 1/20 of the time of atrial fibrillation... if annots and afibtime > (annots[-1].time-annots[0].time)/20.0: iterator = iter(dest) rhythms = ('(N', '(SVTA') start = next((a for a in iterator if a.code == ECGCodes.RHYTHM and a.aux in rhythms), None) while start is not None: end = next((a for a in iterator if a.code == ECGCodes.RHYTHM), dest[-1]) #All normal rhythms that satisfy the Lian method to identify #afib by rhythm are now considered afib. We also check the #method considering alternate RRs to avoid false positives with #bigeminies. fragment = dest[dest.bisect_left(start):dest.bisect_right(end)] rrs = np.diff([a.time for a in fragment if MITAnnotation.is_qrs_annotation(a)]) if (is_afib_rhythm_lian(rrs) and is_afib_rhythm_lian(rrs[0::2]) and is_afib_rhythm_lian(rrs[1::2])): start.aux = b'(AFIB' #Next rhythm start = (end if end.aux in rhythms else next((a for a in iterator if a.code == ECGCodes.RHYTHM and a.aux in rhythms), None)) ############################## #Paced rhythm identification # ############################## #To consider the presence of paced rhythms in a record, we require at #least a mean of one paced beat each 10 seconds. pacedrec = sum(1 for a in dest if a.code == ECGCodes.PACE) > 180 if pacedrec: iterator = iter(dest) rhythms = (b'(AFIB', b'(N', b'(SBR', b'(SVTA') start = next((a for a in iterator if a.code == ECGCodes.RHYTHM and a.aux in rhythms), None) while start is not None: end = next((a for a in iterator if a.code == ECGCodes.RHYTHM), dest[-1]) #If there are paced beats in a rhythm fragment, the full #rhythm is identified as paced. if any([start.time < a.time < end.time and a.code == ECGCodes.PACE for a in dest[dest.index(start):dest.index(end)]]): start.aux = b'(P' #Next rhythm start = (end if end.aux in rhythms else next((a for a in iterator if a.code == ECGCodes.RHYTHM and a.aux in rhythms), None)) ######################################### # Redundant rhythm description removing # ######################################### i = 1 while i < len(dest): if dest[i].code is ECGCodes.RHYTHM: prev = next((a for a in reversed(dest[:i]) if a.code is ECGCodes.RHYTHM), None) if prev is not None and prev.aux == dest[i].aux: dest.pop(i) else: i += 1 else: i += 1 return dest