Beispiel #1
0
def _find_peak(rdef, siginfo, beg, interv):
    """
    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.
    """
    llim, ulim = interv.start - beg, interv.end - beg
    dist = lambda p: 1.0 + 2.0 * abs(beg + p - rdef.earlystart) / 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])]
        peaks = peaks[np.logical_and(llim <= peaks, peaks <= ulim)]
        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
Beispiel #2
0
def _qrs_gconst(pattern, rdef):
    """
    Checks the general constraints of the QRS pattern transition.
    """
    # We ensure that the abstracted evidence has been observed.
    if rdef.earlystart != rdef.lateend:
        return
    # The energy level of the observed interval must be low
    hyp = pattern.hypothesis
    # First we try a guided QRS observation
    _guided_qrs_observation(hyp)
    if hyp.shape:
        hyp.freeze()
        return
    # Hypothesis initial limits
    beg = int(hyp.earlystart)
    if beg < 0:
        beg = 0
    end = int(hyp.lateend)
    # 1. Signal characterization.
    siginfo = _characterize_signal(beg, end)
    verify(siginfo is not None)
    # 2. Peak point estimation.
    peak = _find_peak(rdef, siginfo, beg, hyp.time)
    verify(peak is not None)
    # 3. 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)
    # 4. 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(beg + start + p - rdef.earlystart) / 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
        hyp.shape[lead] = shape
    # There must be a recognizable QRS waveform in at least one lead.
    verify(hyp.shape)
    # 5. The detected shapes may constrain the delineation area.
    llim = min(hyp.shape[lead].waves[0].l for lead in hyp.shape)
    if llim > 0:
        start = start + llim
        for lead in hyp.shape:
            hyp.shape[lead].move(-llim)
    ulim = max(hyp.shape[lead].waves[-1].r for lead in hyp.shape)
    if ulim < end - start:
        end = start + ulim
    # 6. 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 hyp.shape.itervalues())
    # 7. Segmentation points set
    hyp.paced = any(v[0] for v in limits.itervalues())
    hyp.time.value = Iv(beg + peak, beg + peak)
    hyp.start.value = Iv(beg + start, beg + start)
    hyp.end.value = Iv(beg + end, beg + end)
    ###################################################################
    # Amplitude conditions (between 0.5mV and 6.5 mV in at least one
    # lead or an identified pattern in most leads).
    ###################################################################
    verify(
        len(hyp.shape) > len(sig_buf.get_available_leads()) / 2.0
        or ph2dg(0.5) <= max(s.amplitude for s in hyp.shape.itervalues()) <= ph2dg(6.5)
    )
    hyp.freeze()
Beispiel #3
0
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 range(1, len(points) - 3):
        for j in range(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))
Beispiel #4
0
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 range(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
Beispiel #5
0
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 range(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