def _get_paced_qrs_shape(signal, points, start, end): """ Obtains the QRSShape object corresponding to a paced QRS complex delimited inside a signal fragment. Parameters ---------- signal: Signal fragment containing a paced QRS complex. The limits of the signal should be the limits determined by the *_paced_qrs_delineation* function. points: Relevant points in the signal fragment. start: Start point of the pace spike wrt the start of the signal. end: Finish point of the paced QRS wrt the start of the signal. Returns ------- out: QRSShape object representing the paced beat. """ try: signal = signal[start:end+1] points = points[np.logical_and(points >= start, points <= end)] - start verify(len(points)>0) if points[0] != 0: points = np.insert(points, 0, 0) if points[-1] != len(signal) - 1: points = np.append(points, len(signal) - 1) verify(len(points) >= 3) #We assume the baseline level is the start signal value of the spike waves = extract_waves(signal, points, signal[points[0]]) verify(waves) total_energ = sum(w.e for w in waves) #We get the longest wave sequence with a valid QRS tag. i = 0 while i < len(waves) and _tag_qrs(waves[:i+1]) in C.QRS_SHAPES: i += 1 tag = _tag_qrs(waves[:i]) verify(tag in C.QRS_SHAPES) shape = QRSShape() shape.waves = waves[:i] shape.energy = sum(w.e for w in shape.waves) shape.tag = tag shape.sig = (signal[shape.waves[0].l:shape.waves[-1].r+1] - signal[shape.waves[0].l]) shape.maxslope = np.max(np.abs(np.diff(shape.sig))) shape.amplitude = np.ptp(shape.sig) shape.move(start) verify(shape.energy/total_energ > 0.5) return shape except (ValueError, InconsistencyError): return None
def _get_qrs_shape(signal, points, peak, baseline): """ Obtains the QRSShape object that best fits a signal fragment, considering the simplification determined by points, and the peak and baseline estimations. The detected QRS shape must collect the majority of the total energy of the waves present in the signal fragment. """ try: waves = extract_waves(signal, points, baseline) verify(waves) total_energ = sum(w.e for w in waves) #We find the longest valid sequence of waves with the highest energy. sequences = [] for i in xrange(len(waves)): #Largest valid sequence starting in the i-th wave. seq = [waves[i]] j = i+1 while j < len(waves) and _is_qrs_complex(waves[i:j+1]): seq.append(waves[j]) j += 1 #We add the valid sequence and the acumulated energy (we require #the peak to actually be inside the sequence.) tag = _tag_qrs(seq) energ = sum(w.e for w in seq) if (tag in C.QRS_SHAPES and energ/total_energ > 0.5 and any(w.l <= peak <= w.r for w in seq)): sequences.append((seq, tag, energ)) #We get the sequence with the maximum value verify(sequences) seq, tag, energ = max(sequences, key= operator.itemgetter(2)) shape = QRSShape() shape.energy = energ shape.tag = tag shape.waves = seq shape.sig = signal[seq[0].l:seq[-1].r+1] - signal[seq[0].l] shape.maxslope = np.max(np.abs(np.diff(shape.sig))) shape.amplitude = np.ptp(shape.sig) return shape except (ValueError, InconsistencyError): return None
def _combine_limits(limits, siginfo, peak): """ Combines the QRS limits detected in a set of leads, applying ad-hoc rules for the situation in which a paced beat is detected. This function raises an *InconsistencyError* exception if the limits cannot be properly combined. Parameters ---------- limits: Dictionary, indexed by lead, with a tuple in each one indicating if a paced beat was detected in that lead, and an Interval instance with the delineation result. siginfo: List with the information about the signal we are dealing with. It is the result of the *_characterize_signal* function. peak: Situation of the QRS peak point. Returns ------- (start, end): Absolute endpoints of the QRS complex obtained from the combination of the limits in all leads. """ start = end = None if any(v[0] for v in limits.itervalues()): #There is a pacing detection, we will check if the information of #all leads is consistent with detection. #First, all spikes must start within a 40ms margin. try: spkstart = [v[1].start for v in limits.itervalues() if v[0]] verify(max(spkstart)-min(spkstart) <= C.TMARGIN) #Second, all non-paced leads must start their QRS complex in the #40 ms after the first spike has appeared. spkstart = min(spkstart) verify(all(-C.TMARGIN <= v[1].start-spkstart <= C.TMARGIN for v in limits.itervalues() if not v[0])) #We have confirmed the beat is a paced beat, we set the limits start = spkstart end = max(v[1].end for v in limits.itervalues() if v[0]) for _, endpoints in limits.itervalues(): if (0 < endpoints.end - end <= C.TMARGIN and endpoints.end-start <= C.QRS_EANN_DMAX): end = endpoints.end except InconsistencyError: #We set the non-paced delineation for previously detected paced #leads. for lead in (k for k, v in limits.iteritems() if v[0]): _, sig, points, _, _ = ([info for info in siginfo if info[0]==lead][0]) endpoints = _qrs_delineation(sig, points, peak) if endpoints is not None: limits[lead] = (False, endpoints) else: limits.pop(lead) #If we have discarded all limits, we raise an exception. verify(limits) #If there is no a paced beat, we join the limits estimation of every #lead, by order of quality. if start is None: start, end = limits.values()[0][1].start, limits.values()[0][1].end for _, endpoints in limits.itervalues(): if (0 < start-endpoints.start <= C.TMARGIN and end-endpoints.start <= C.QRS_EANN_DMAX): start = endpoints.start if (0 < endpoints.end - end <= C.TMARGIN and endpoints.end-start <= C.QRS_EANN_DMAX): end = endpoints.end return (start, end)
def delineate_qrs(siginfo): """ Performs the multi-lead delineation of a QRS complex enclosed in a specific time interval, returning an instance of the QRS class. Parameters ---------- siginfo: List-like structure containing all the necessary information of the ECG signal in the searching time interval. Each entry in this list is assumed to be a tuple of the **LeadInfo** class, and the list is assumed to be ordered by the quality of the signal in each lead. Returns ------- out: QRS object with all the attributes properly set. If the delineation cannot be performed, an InconsistencyError is raised. """ verify(siginfo) qrs = QRS() #Peak point estimation. peak = _find_peak(siginfo) verify(peak is not None) #QRS start and end estimation #For each lead, we first check if it is a paced beat, whose delineation #process is different. In case of failure, we perform common delineation. limits = OrderedDict() for lead, sig, points, baseline, _ in siginfo: endpoints = _paced_qrs_delineation(sig, points, peak, baseline) if endpoints is None: endpoints = _qrs_delineation(sig, points, peak) if endpoints is None: continue limits[lead] = (False, endpoints) else: limits[lead] = (True, endpoints) #Now we combine the limits in all leads. start, end = _combine_limits(limits, siginfo, peak) verify(start is not None and end > start) #QRS waveform extraction for each lead. for lead, sig, points, baseline, _ in siginfo: #We constrain the area delineated so far. sig = sig[start:end+1] points = points[np.logical_and(points >= start, points <= end)] - start if len(points) == 0: continue if points[0] != 0: points = np.insert(points, 0, 0) if points[-1] != len(sig) - 1: points = np.append(points, len(sig) - 1) if len(points) < 3: continue #We define a distance function to evaluate the peaks dist = (lambda p : 1.0 + 2.0 * abs(start + p - C.QRS_BANN_DMAX) /ms2sp(150)) dist = np.vectorize(dist) #We get the peak for this lead pks = points[sig_meas.get_peaks(sig[points])] if len(pks) == 0: continue peakscore = abs(sig[pks]-baseline)/dist(pks) peak = pks[peakscore.argmax()] #Now we get the shape of the QRS complex in this lead. shape = None #If there is a pace detection in this lead if lead in limits and limits[lead][0]: endpoints = limits[lead][1] shape = _get_paced_qrs_shape(sig, points, endpoints.start - start, min(endpoints.end-start,len(sig))) if shape is None: limits[lead] = (False, endpoints) if shape is None: shape = _get_qrs_shape(sig, points, peak, baseline) if shape is None: continue qrs.shape[lead] = shape #There must be a recognizable QRS waveform in at least one lead. verify(qrs.shape) #The detected shapes may constrain the delineation area. llim = min(qrs.shape[lead].waves[0].l for lead in qrs.shape) if llim > 0: start = start + llim for lead in qrs.shape: qrs.shape[lead].move(-llim) ulim = max(qrs.shape[lead].waves[-1].r for lead in qrs.shape) if ulim < end-start: end = start + ulim #The definitive peak is assigned to the first relevant wave #(each QRS shapeform has a specific peak point.) peak = start + min(s.waves[_reference_wave(s)].m for s in qrs.shape.itervalues()) #Segmentation points set qrs.paced = any(v[0] for v in limits.itervalues()) qrs.start, qrs.peak, qrs.end = start, peak, end ################################################################### #Amplitude conditions (between 0.5mV and 6.5 mV in at least one #lead or an identified pattern in most leads). ################################################################### verify(len(qrs.shape) > len(siginfo)/2.0 or C.QRS_MIN_AMP <= max(s.amplitude for s in qrs.shape.itervalues()) <= C.QRS_MAX_AMP) return qrs
def _paced_qrs_delineation(signal, points, peak, baseline): """ Checks if a sequence of waves is a paced heartbeat. The main criteria is the presence of a spike at the beginning of the beat, followed by at least one significant wave. """ try: #Gets the slope between two points. slope = lambda a, b : abs(dg2mm((signal[b]-signal[a])/sp2mm(b-a))) #First we search for the spike. spike = _find_spike(signal, points) verify(spike) if not spike[-1] in points: points = np.insert(points, bisect.bisect(points, spike[-1]), spike[-1]) #Now we get relevant points, checking some related constraints. bpts = points[points <= spike[0]] apts = points[points >= spike[-1]] verify(len(apts) >= 2) #Before and after the spike there must be a significant slope change. verify(slope(spike[0], spike[1]) > 2.0 * slope(bpts[-2], bpts[-1])) verify(slope(spike[1], spike[-1]) > 2.0 * slope(apts[0], apts[1])) #Now we look for the end of the QRS complex, by applying the same #clustering strategy than regular QRS, but only for the end. slopes = (signal[apts][1:]-signal[apts][:-1])/(apts[1:]-apts[:-1]) features = [] for i in xrange(len(slopes)): #The features are the slope in logarithmic scale and the distance to #the peak. features.append([math.log(abs(slopes[i])+1.0), abs(apts[i+1] - peak)]) features = whiten(features) #We initialize the centroids in the extremes (considering what is #interesting of each feature for us) fmin = np.min(features, 0) fmax = np.max(features, 0) valid = np.where(kmeans2(features, np.array([[fmin[0], fmax[1]], [fmax[0], fmin[1]]]), minit = 'matrix')[1])[0] verify(np.any(valid)) end = apts[valid[-1]+1] #The duration of the QRS complex after the spike must be more than 2 #times the duration of the spike. verify((end-apts[0]) > 2.0 * (spike[-1]-spike[0])) #The amplitude of the qrs complex must higher than 0.5 the amplitude #of the spike. sgspike = signal[spike[0]:spike[-1]+1] sgqrs = signal[apts[0]:end+1] verify(np.ptp(sgqrs) > ph2dg(0.5)) verify(np.ptp(sgqrs) > 0.5 * np.ptp(sgspike)) #There must be at least one peak in the QRS fragment. qrspt = signal[apts[apts <= end]] verify(len(qrspt) >= 3) verify(abs(signal[end] - signal[spike[0]]) <= ph2dg(0.3) or len(get_peaks(qrspt)) > 0) #The area of the rest of the QRS complex must be higher than the spike. verify(np.sum(np.abs(sgspike-sgspike[0])) < np.sum(np.abs(sgqrs-sgspike[0]))) #The distance between the beginning of the spike and the baseline #cannot be more than the 30% of the amplitude of the complex. verify(abs(signal[spike[0]]-baseline) < 0.3 * np.ptp(signal[spike[0]:end+1])) #At last, we have found the paced QRS limits. return Iv(spike[0], end) except InconsistencyError: return None
def _qrs_delineation(signal, points, peak): """ Returns the interval points of a possible QRS complex in a signal fragment. Parameters ---------- signal: Array containing a signal fragment with a possible QRS inside its limits points: Representative points candidates to be the limits.. peak: Point of the determined QRS peak. Returns ------- out: The interval of the QRS. """ try: verify(len(points) >= 3) #We get the slope of each segment determined by the relevant points slopes = ((signal[points][1:]-signal[points][:-1])/ (points[1:]-points[:-1])) #We also get the peaks determined by the signal simplification. pks = points[sig_meas.get_peaks(signal[points])] verify(len(pks) > 0) #Now we perform a clustering operation over each slope, with a certain #set of features. features = [] for i in xrange(len(slopes)): #We obtain the midpoint of the segment, and its difference with #respect to the peak, applying a temporal margin. #We get as representative point of the segment the starting point #if the segment is prior to the peak, and the ending point #otherwise. point = points[i] if points[i] < peak else points[i+1] #The features are the slope in logarithmic scale and the distance to #the peak. dist = abs(point - peak) features.append([math.log(abs(slopes[i])+1.0), dist]) #We perform a clustering operation on the extracted features features = whiten(features) #We initialize the centroids in the extremes (considering what is #interesting of each feature for us) fmin = np.min(features, 0) fmax = np.max(features, 0) tags = kmeans2(features, np.array([[fmin[0], fmax[1]], [fmax[0], fmin[1]]]), minit = 'matrix')[1] valid = np.where(tags)[0] verify(np.any(valid)) start = points[valid[0]] end = points[valid[-1]+1] #If the relation between not valid and valid exceeds 0.5, we take the #highest valid interval containing the peak. if _invalidtime_rate(points, valid) > 0.5: #We get the last valid segment before the peak, and the first valid #segment after the peak. We expand them with consecutive valid #segments. try: start = max(v for v in valid if points[v] <= peak) while start-1 in valid: start -= 1 end = min(v for v in valid if points[v+1] >= peak) while end+1 in valid: end += 1 start, end = points[start], points[end+1] except ValueError: return None #We ensure there is a peak between the limits. verify(np.any(np.logical_and(pks > start, pks < end))) #If there are no peaks, we don't accept the delineation return Iv(start, end) except InconsistencyError: return None