def _find_peak(siginfo): """ Obtains an estimation of the peak situation of a QRS complex, from the energy interval that forms the base evidence, a fragment of signal evidence, a reference time point, and the interval of valid points for the peak. """ dist = lambda p : 1.0 + 2.0 * abs(p - C.QRS_BANN_DMAX)/ms2sp(150) dist = np.vectorize(dist) peak = None #For each lead, the peak will be the maximum deviation point wrt the #baseline, and applying the distance function just defined. We give more #importance to the first leads, as they supposedly have more quality. for _, sig, points, baseline, _ in siginfo: if len(points) < 3: continue peaks = points[sig_meas.get_peaks(sig[points])] if len(peaks) == 0: continue peakscore = abs(sig[peaks]-baseline)/dist(peaks) lpeak = peaks[peakscore.argmax()] if peak is None: peak = lpeak elif abs(peak-lpeak) <= C.TMARGIN: peak = lpeak if lpeak < peak else peak return peak
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 _find_spike(signal, points): """ Looks for a pacemaker spike in a signal fragment, applying fixed thresholds on wave duration, angles and amplitude. These thresholds are the following: - The duration of the spike must be shorter than 30ms. - The ascent and descent angles of the spike must be higher than 75º in common ECG scale. - The amplitude of the spike must be at least 0.2 mV (2mm) in the edge with lower amplitude. - The falling edge must be of lower amplitude than the rising edge. Parameters ---------- signal: Numpy array containing the signal information referenced by the wave object. points: Relevant points detected on the signal. Returns ------- out: Tuple with three integer values, which are the begin, peak, and end of the detected spike. If no spikes were detected, returns None. """ #Angle between two points angle = lambda a, b : math.atan(dg2mm(abs(signal[b]-signal[a])/sp2mm(b-a))) #First we search for the left edge of the spike. spike = [] for i in xrange(1, len(points)-3): for j in xrange(i+1, len(points)-2): pts = points[i:j+1] llim = pts[-1] #There can be no peaks inside the left edge. if (llim-pts[0] > C.SPIKE_DUR or (len(pts) >= 3 and len(get_peaks(signal[pts])) > 0)): break #The end of the left edge must be a peak. if len(get_peaks(signal[llim-1:llim+2])) < 1: continue #Left edge candidate ledge = abs(signal[pts[0]] - signal[llim]) if (ledge >= C.SPIKE_EDGE_AMP and angle(pts[0], llim) >= math.radians(85)): #Right edge delineation. ulim = min(int(pts[0]+C.SPIKE_DUR), points[-1]) rsig = signal[llim:ulim+1] if len(rsig) < 3: break rpks = get_peaks(rsig) if np.any(rpks): ulim = llim + rpks[0] ulim = ulim-1 if ulim-1 in points else ulim ulim = ulim+1 if ulim+1 in points else ulim while ulim > llim: redge = abs(signal[ulim] - signal[llim]) if redge < C.SPIKE_EDGE_AMP: break if (redge-ledge < C.SPIKE_ECGE_DIFF and angle(llim, ulim) >= math.radians(75)): #Spike candidate detected spike.append((pts[0], llim, ulim)) break ulim -= 1 if not spike or max(sp[0] for sp in spike) >= min(sp[-1] for sp in spike): return None #We get the spike with highest energy. return max(spike, key = lambda spk: np.sum(np.diff(signal[spk[0]:spk[-1]+1])**2))
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