def challenge(record, alarm_type): """Evaluates the presence of a given alarm in a given record""" assert alarm_type in (ASYST, BRAD, TACH, VFLUT, VTACH), ('Unknown alarm type {0}'.format(alarm_type)) annots = MIT.read_annotations(INTERP_DIR + str(record) + '.igqrs') rec = None if alarm_type == VTACH: rec = MIT.load_MIT_record(DB_DIR + str(record)) rec.leads = [VALID_LEAD_NAMES[l] for l in rec.leads] return EVAL_ALARM_F[alarm_type](annots, rec)
def eval_asyst(annotations, _): """Evaluates the asystole presence""" def check_vf(start, end): """Obtains the flutter waves present in a given interval""" return [ a for a in annotations if start < a.time < end and a.code is ECGCodes.FLWAV ] lth, uth, dth = ms2sp( (4 * 60 + 45) * 1000), ms2sp(5 * 60 * 1000), ms2sp(3500) beats = np.array([ b.time for b in annotations if MIT.is_qrs_annotation(b) and lth <= b.time <= uth ]) if len(beats) < 2: return not check_vf(lth, uth) if uth - beats[-1] > dth: return not check_vf(beats[-1], uth) rrs = np.diff(beats) for i in range(len(rrs)): if rrs[i] > dth: if not check_vf(beats[i], beats[i + 1]): return True return False
def eval_vtach(anns, rec): """Evaluates the ventricular tachycardia presence""" lth, uth = ms2sp((4 * 60 + 45) * 1000), ms2sp(5 * 60 * 1000) #First we perform clustering on all beats qrsdur = {} clusters = [] for ann in anns: if MIT.is_qrs_annotation(ann): delin = json.loads(ann.aux) qrs = {} for lead in delin: sidx = rec.leads.index(lead) qon = ann.time + delin[lead][0] qoff = ann.time + delin[lead][-1] qrs[lead] = SIG(sig=rec.signal[sidx][qon:qoff + 1]) qrsdur[ann] = max(len(s.sig) for s in qrs.values()) clustered = False for cluster in clusters: if _same_cluster(cluster[0], qrs): cluster[1].add(ann) clustered = True break if not clustered: clusters.append((qrs, set([ann]), qrsdur[ann])) if not clusters: return False #We take as normal beats the cluster with highest number of annotations. nclust = max(clusters, key=lambda cl: len(cl[1])) beats = [ ann for ann in anns if MIT.is_qrs_annotation(ann) and lth <= ann.time <= uth ] if len(beats) < 5: return False for i in range(len(beats) - 4): tach = ms2bpm(sp2ms(beats[i + 4].time - beats[i].time) / 4.0) >= 100 bset = set(beats[i:i + 5]) ventr = (np.min([qrsdur[b] for b in bset]) > ms2sp(110) or any([bset.issubset(cl[1]) for cl in clusters])) if (tach and ventr and all([b not in nclust[1] for b in bset])): return True return False
def eval_tach(annotations, _): """Evaluates the tachycardia presence""" lth, uth = ms2sp((4 * 60 + 30) * 1000), ms2sp(5 * 60 * 1000) beats = np.array([ b.time for b in annotations if MIT.is_qrs_annotation(b) and lth <= b.time <= uth ]) for i in range(len(beats) - 16): if ms2bpm(sp2ms(beats[i + 16] - beats[i]) / 16.0) > 120: return True return False
def eval_brad(annotations, _): """Evaluates the bradycardia presence""" lth, uth = ms2sp((4 * 60 + 45) * 1000), ms2sp(5 * 60 * 1000) beats = np.array([b.time for b in annotations if MIT.is_qrs_annotation(b)]) variability = np.std(np.diff(beats)) #The default threshold is 40 bpm, but if the rhythm shows high variability, #we relax such threshold to 45 bpm. thres = 45 if variability > ms2sp(200) else 40 lidx = bisect.bisect_left(beats, lth) uidx = bisect.bisect_right(beats, uth) for i in range(lidx, uidx - 4): bpm = int(ms2bpm(sp2ms(beats[i + 4] - beats[i]) / 4.0)) if bpm <= thres: return True return False
metavar='ann', required=True, help=('Annotations resulting from the abductive ' 'interpretation of the ECG signal')) parser.add_argument('-c', metavar='cluster', required=True, help=('Extension of the file containing the clustering' ' information.')) parser.add_argument('-o', metavar='oann', default='cls', help=('Save annotations with classified QRS complexes' ' as annotator oann (default: cls)')) args = parser.parse_args() rec = MIT.load_MIT_record(args.r) set_sampling_freq(rec.frequency) print('Classifying record {0}'.format(args.r)) #Reconstruction of the abductive interpretation annots = MIT.read_annotations('{0}.{1}'.format(args.r, args.a)) interp = interp2annots.ann2interp(rec, annots) #Cluster information clusters = load_clustering(args.r, args.c, interp.observations) #QRS feature extraction features = get_features(interp) #Cluster feature extraction for c in clusters: clusters[c] = Cluster(clusters[c], get_cluster_features(clusters[c], features)) #Key function to compare clusters: First, we check clusters with more than #30 beats; then, the clusters with more REGULAR or AFIB beats, and finally
ANN = '.iqrs' RHNAMES = { b'(N': 'Normal rhythm', b'(SVTA': 'Tachycardia', b'(SBR': 'Bradycardia', b'(AFIB': 'Atrial Fibrillation', b'(T': 'Trigeminy', b'(B': 'Bigeminy', b'(VFL': 'Ventricular Flutter', b'P': 'Absence of rhythm' } for rec in RECORDS: rhctr = Counter() anns = MIT.read_annotations(PATH + rec + ANN) rhythms = (a for a in anns if a.code in (ECGCodes.RHYTHM, ECGCodes.VFON)) try: start = next(rhythms) except StopIteration: continue print('Interpretation results for record {0}:'.format(rec)) print('Rhythm analysis:') nect = len([a for a in anns if a.aux == b'(EXT']) nbk = len([a for a in anns if a.aux == b'(BK']) ncpt = len([a for a in anns if a.aux == b'(CPT']) while True: end = next(rhythms, anns[-1]) if start.aux in RHNAMES: rhctr[start.aux] += end.time - start.time elif start.code == ECGCodes.VFON:
'2015_BMI/validation/training_dataset/') #DB = '/home/tomas/Escritorio/afdb/' #DB = '/tmp/mit/' ANN = '.iqrs' #ANN = '.ibatr' OANN = '.rhy' RECORDS = [l.strip() for l in open(DB + 'RECORDS')] for rec in RECORDS: if os.path.isfile(DB+rec+OANN): print('Annotator "{0}" already exists. Skipping record {1}'.format( OANN, rec)) continue print('Converting record {0}'.format(rec)) anns = MIT.read_annotations(DB+rec+ANN) record = MIT.load_MIT_record(DB+rec) assert record.frequency == SAMPLING_FREQ record.leads = [VALID_LEAD_NAMES[l] for l in record.leads] interp = i2a.ann2interp(record, anns) afibs = list(interp.get_observations(o.Atrial_Fibrillation)) i = 0 while i < len(afibs): print('{0}/{1}'.format(i,len(afibs))) afib = afibs[i] beats = list(interp.get_observations(o.QRS, filt=lambda q, af=afib: af.earlystart <= q.time.start <= af.lateend)) rpks = np.array([qrs.time.start for qrs in beats]) #We obtain the shape representing the AFIB morphology as the qrs #matching the shape with more other qrss within the rhythm. ctr = collections.Counter()
def ann2interp(record, anns, fmt=False): """ Returns an interpretation containing the observations represented in a list of annotations associated to a loaded MIT record. Note that only the *observations* field is properly set. The optional parameter *fmt* allows to specify if the specific Construe format for annotation files can be assumed. This parameter is also inferred from the first annotation in the list. """ fmt = (fmt or len(anns) > 0 and anns[0].code is C.NOTE and anns[0].aux == FMT_STRING) interp = Interpretation() observations = [] RH_VALS = set(C.RHYTHM_AUX.values()) for i in range(len(anns)): ann = anns[i] if ann.code in (C.PWAVE, C.TWAVE): obs = o.PWave() if ann.code == C.PWAVE else o.TWave() if fmt: beg = next(a for a in reversed(anns[:i]) if a.time < ann.time and a.code == C.WFON and a.subtype == ann.code).time end = next(a for a in anns[i:] if a.time > ann.time and a.code == C.WFOFF and a.subtype == ann.code).time else: beg = next(a for a in reversed(anns[:i]) if a.time < ann.time and a.code == C.WFON).time end = next(a for a in anns[i:] if a.time > ann.time and a.code == C.WFOFF).time obs.start.set(beg, beg) obs.end.set(end, end) if fmt: amp = json.loads(ann.aux) for l in amp.keys(): if l not in record.leads: compatible = next((l2 for l2 in VALID_LEAD_NAMES if VALID_LEAD_NAMES[l2] == l), None) if compatible is None: raise ValueError('Unrecognized lead {0}'.format(l)) obs.amplitude[compatible] = amp.pop(l) else: leads = (record.leads if ann.code is C.TWAVE else set(K.PWAVE_LEADS) & set(record.leads)) leads = record.leads for lead in leads: sidx = record.leads.index(lead) s = record.signal[sidx][beg:end+1] mx, mn = np.amax(s), np.amin(s) pol = (1.0 if max(mx-s[0], mx-s[-1]) >= -min(mn-s[0],mn-s[1]) else -1.0) obs.amplitude[lead] = pol * np.ptp(s) observations.append(obs) elif MIT.is_qrs_annotation(ann): obs = o.QRS() obs.time.set(ann.time, ann.time) obs.tag = ann.code delin = json.loads(ann.aux) #QRS start and end is first tried to set according to delineation #info. If not present, it is done according to delineation #annotations. if delin: for l in delin.keys(): if l not in record.leads: compatible = next((l2 for l2 in VALID_LEAD_NAMES if VALID_LEAD_NAMES[l2] == l), None) if compatible is None: raise ValueError('Unrecognized lead {0}'.format(l)) delin[compatible] = delin.pop(l) beg = ann.time + min(d[0] for d in delin.values()) end = ann.time + max(d[-1] for d in delin.values()) else: def extra_cond(a): return a.subtype == C.SYSTOLE if fmt else True beg = next(a for a in reversed(anns[:i]) if a.code==C.WFON and extra_cond(a)).time end = next(a for a in anns[i:] if a.code == C.WFOFF and extra_cond(a)).time #Endpoints set obs.start.set(beg, beg) obs.end.set(end, end) for lead in delin: assert len(delin[lead]) % 3 == 0, 'Unrecognized delineation' sidx = record.leads.index(lead) beg = ann.time + delin[lead][0] end = ann.time + delin[lead][-1] obs.shape[lead] = o.QRSShape() sig = record.signal[sidx][beg:end+1] obs.shape[lead].sig = sig-sig[0] obs.shape[lead].amplitude = np.ptp(sig) obs.shape[lead].energy = np.sum(np.diff(sig)**2) obs.shape[lead].maxslope = np.max(np.abs(np.diff(sig))) waves = [] for i in range(0, len(delin[lead]), 3): wav = Wave() wav.pts = tuple(delin[lead][i:i+3]) wav.move(-delin[lead][0]) if wav.r >= len(sig): warnings.warn('Found delineation information after ' 'the end of the signal in annotation {0}'.format(ann)) break wav.amp = (np.sign(sig[wav.m]-sig[wav.l]) * np.ptp(sig[wav.l:wav.r+1])) wav.e = np.sum(np.diff(sig[wav.l:wav.r+1])**2) wav.move(delin[lead][0]) wav.move(ann.time-obs.earlystart) waves.append(wav) if not waves: obs.shape.pop(lead) else: obs.shape[lead].waves = tuple(waves) obs.shape[lead].tag = _tag_qrs(waves) observations.append(obs) elif ann.code is C.RHYTHM and ann.aux in RH_VALS: rhclazz = next(rh for rh in C.RHYTHM_AUX if C.RHYTHM_AUX[rh] == ann.aux) obs = rhclazz() obs.start.set(ann.time, ann.time) end = next((a.time for a in anns[i+1:] if a.code is C.RHYTHM), anns[-1].time) obs.end.set(end, end) observations.append(obs) elif ann.code is C.ARFCT: obs = o.RDeflection() obs.time.set(ann.time, ann.time) observations.append(obs) interp.observations = sortedcontainers.SortedList(observations) return interp
""" import construe.utils.MIT as MIT import construe.utils.MIT.ECGCodes as ECGCodes from construe.utils.units_helper import samples2msec as sp2ms import numpy as np import matplotlib.pyplot as plt PATH = '/home/tomas/Dropbox/Investigacion/tese/estadias/2015_BMI/data/' RECORDS = [l.strip() for l in open(PATH + 'RECORDS')] ANN = '.iqrs' plt.ioff() for rec in RECORDS: try: annots = MIT.read_annotations(PATH + rec + ANN) except IOError: print('No results found for record ' + rec) continue rpeaks = sp2ms( np.array([a.time for a in annots if MIT.is_qrs_annotation(a)])) if len(rpeaks) < 2: print('No hearbeats found for record ' + rec) continue pwaves = [a for a in annots if a.code == ECGCodes.PWAVE] #Plot creation fig, host = plt.subplots() par1 = host.twinx() rrstd = [] pwf = [] #We create one point by minute.
if __name__ == "__main__": #Config variables PATH = '/tmp/mit/' REF = '.atr' TEST = '.cls' MWIN = ms2sp(150.0) #Records to be interpreted can be selected from command line SLC_STR = '0:48' if len(sys.argv) < 2 else sys.argv[1] #We get a slice from the input string SLC = slice(*[{True: lambda n: None, False: int}[x == ''](x) for x in (SLC_STR.split(':') + ['', '', ''])[:3]]) CMATS = {} for REC in [l.strip() for l in open(PATH + 'RECORDS')][SLC]: print('Record {}'.format(REC)) tp = fn = fp = 0 ref = [a for a in MIT.read_annotations(PATH + REC + REF) if MIT.is_qrs_annotation(a)] for a in ref: a.code = C.aami_to_mit(C.mit_to_aami(a.code)) test = [a for a in MIT.read_annotations(PATH + REC + TEST) if MIT.is_qrs_annotation(a)] for a in test: a.code = C.aami_to_mit(C.mit_to_aami(a.code)) tags = sorted(set(a.code for a in test).union(a.code for a in ref)) #The -1 tag is used for false positives and false negatives. tags.insert(0, -1) cmat = np.zeros((len(tags), len(tags))) i = j = 0 while i < len(ref) and j < len(test): if abs(ref[i].time - test[j].time) <= MWIN: #True positive, introduced in the corresponding matrix cell
tree = ET.parse(DB_DIR + devid + '_episodes.xml') ep_seq = tree.find('ns:return/mg_di:diResponse/mg_di:additionalInfo', NS) for episode in ep_seq.findall('ns2:observationResult', NS): rhythm = Rhythm() rhythm.code = RHTAG tp = episode.find('ns2:observationEventTime', NS) start = dateutil.parser.parse(tp.attrib['low']) end = dateutil.parser.parse(tp.attrib['high']) #FIXME we need to ignore timezone for the moment rhythm.start = start.replace(tzinfo=None) rhythm.end = end.replace(tzinfo=None) mbg.add(rhythm) abd = sortedcontainers.SortedList() for f in glob.glob(DB_DIR + devid + '*' + ANN): reftime = MIT.get_datetime(f[:-len(ANN)]) annots = MIT.read_annotations(f) if not annots: continue sig_episodes.add( Iv(reftime, reftime + dt.timedelta(milliseconds=s2m(annots[-1].time)))) for r in (a for a in annots if a.code == ECGCodes.RHYTHM and a.aux == RHTAG): rhythm = Rhythm() rhythm.code = r.aux rhythm.start = reftime + dt.timedelta(milliseconds=s2m(r.time)) end = next((a.time for a in annots if a.time > r.time and a.code == ECGCodes.RHYTHM), annots[-1].time) rhythm.end = reftime + dt.timedelta(milliseconds=s2m(end))
return outstr if __name__ == "__main__": DB = '/tmp/mit/' 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] REF_ANN = '.atr' TEST_ANN = '.rhy' epicmpres = {tag : np.zeros(8) for tag in EXCLUSION_TAGS} results = {} for rec in RECORDS: results[rec] = defaultdict(int) ref = MIT.read_annotations(DB + str(rec) + REF_ANN) test = MIT.read_annotations(DB + str(rec) + TEST_ANN) reclen = ref[-1].time #Rhythm loading in form of intervals ref_rhythms = [] test_rhythms = [] for alst in (ref, test): rlst = ref_rhythms if alst is ref else test_rhythms for ann in (a for a in alst if a.code is ECGCodes.RHYTHM): if rlst: rlst[-1].end = ann.time newrhythm = Rhythm() newrhythm.code = ann.aux.strip('\x00') newrhythm.start = ann.time rlst.append(newrhythm) rlst[-1].end = alst[-1].time