Esempio n. 1
0
def _characterize_signal(beg, end):
    """
    Characterizes the available signal in a specific time interval.

    Parameters
    ----------
    beg:
        Starting time point of the interval.
    end:
        Last time point of the interval.

    Returns
    -------
    out:
        sortedlist with one entry by lead. Each entry is a 5-size tuple with
        the lead, the signal samples, the relevant points to represent the
        samples, the baseline level estimation for the fragment, and the
        quality of the fragment in that lead.
    """
    siginfo = sortedcontainers.SortedList(key=lambda v: -v[4])
    for lead in sig_buf.get_available_leads():
        baseline, quality = characterize_baseline(lead, beg, end)
        sig = sig_buf.get_signal_fragment(beg, end, lead=lead)[0]
        if len(sig) == 0:
            return None
        #We build a signal simplification taking at most 9 points, and with
        #a minimum relevant deviation of 50 uV.
        points = DP.arrayRDP(sig, ph2dg(0.05), 9)
        siginfo.add((lead, sig, points, baseline, quality))
    return siginfo
Esempio n. 2
0
def _delimit_p(signal, lead, es_lim, ls_lim, ee_lim):
    """
    Performs the delimitation of a P wave in a signal fragment. If a waveform
    compatible with a P wave cannot be found, returns None, else return an
    Interval within signal length.
    """
    #shape simplification (ignoring the environment signal)
    delta = ph2dg(0.02)
    points = DP.arrayRDP(signal[int(es_lim):], delta, 6) + int(es_lim)
    #If no relevant disturbances are detected, there is no a P wave
    if len(points) == 2:
        return None
    #Now we look for the shorter limits that satisfy the P-Wave classifier.
    cand = None
    i = next(k for k in range(len(points)-1, -1, -1) if points[k] <= ls_lim)
    while i >= 0:
        j = next(k for k in range(i+1, len(points)) if points[k] >= ee_lim)
        while j < len(points):
            sigfr = signal[points[i]:points[j]+1]
            #We consider a good P wave environment if the signal has no
            #amplitude variations
            beg = int(max(0, points[i]-C.PWAVE_ENV))
            plainenv = not np.any(signal[beg:points[i]+1]-signal[beg])
            #The minimum threshold varies with the environment quality
            ampthres = C.PWAVE_MIN_AMP if not plainenv else delta
            if (seems_pwave(sigfr, lead) and np.ptp(sigfr) >= ampthres):
                cand = (points[i], points[j])
                break
            j += 1
        if cand is not None:
            break
        i -= 1
    return None if cand is None else Iv(int(cand[0]-es_lim),
                                        int(cand[1]-es_lim))
Esempio n. 3
0
def get_cluster_features(cluster, features):
    """
    Obtains a BeatInfo object as a summary of the features of a complete
    cluster. It is created by the calculation of the mean value of all the
    relevant features. It also involves the selection of a representant
    from the cluster by the minimization of the distance to the mean.
    """
    cl = [b for b in features if b in cluster]
    if not cl:
        return BeatInfo(o.QRS())
    leads = set.union(*[set(b.shape) for b in cl])
    cl = [b for b in cl if all(l in b.shape for l in leads)]
    if not cl:
        return BeatInfo(o.QRS())
    pwamps = {}
    amplitudes = {}
    qdurs = {}
    for l in leads:
        arr = np.array([features[b].pwave.get(l, 0.0) for b in cl])
        hist = np.histogram(arr, PW_BINS)
        pwamps[l] = dg2ph(hist[1][hist[0].argmax()])
        amplitudes[l] = np.array([b.shape[l].amplitude for b in cl])
        amplitudes[l] = (amplitudes[l] - np.mean(amplitudes[l])) / ph2dg(5.0)
        qdurs[l] = np.array([len(b.shape[l].sig) - 1 for b in cl])
        qdurs[l] = (qdurs[l] - np.mean(qdurs[l])) / ms2sp(120)
    axis = (np.array([features[b].axis for b in cl])
            if Leads.MLII in leads else np.zeros(len(cl)))
    axis = (axis - np.mean(axis)) / 180.0
    #We calculate the euclidean distance of every QRS to the central measures
    eucdist = np.linalg.norm(np.matrix(
        (tuple(qdurs.values()) + tuple(amplitudes.values()) + (axis, ))),
                             axis=0)
    #We select as representative the beat with minimum distance.
    info = BeatInfo(cl[np.argmin(eucdist)])
    info.pwave = np.mean(list(pwamps.values())) > 0.05
    #For the rhythm features, we use all beats
    cl = {b for b in cluster if b in features}
    info.rr = np.mean([features[b].rr for b in cl])
    info.pos = collections.Counter([features[b].pos for b in cl])
    rhpos = max(info.pos, key=lambda v: info.pos[v])
    n = float(sum(info.pos.values()))
    #Factor correction for advanced beats
    if rhpos != ADVANCED and info.pos[ADVANCED] / n > 0.2:
        nadv = ilen(b for b in cl if features[b].pos is REGULAR
                    and features[b].rr < features[b].rh.meas.rr[0])
        nadv -= info.pos[REGULAR] / 2
        if 0 < info.pos[ADVANCED] + nadv > info.pos[REGULAR] - nadv:
            rhpos = ADVANCED
    #Factor correction for delayed beats
    elif rhpos != DELAYED and info.pos[DELAYED] / n > 0.2:
        ndel = ilen(b for b in cl if features[b].pos is REGULAR
                    and features[b].rr < features[b].rh.meas.rr[0])
        ndel -= info.pos[REGULAR] / 2
        if 0 < info.pos[DELAYED] + ndel > info.pos[REGULAR] - ndel:
            rhpos = DELAYED
    info.rh = rhpos
    return info
Esempio n. 4
0
def _tag_qrs(waves):
    """
    Creates a new string tag for a QRS complex from a sequence of waves. This
    tag matches the name given by cardiologists to the different QRS waveforms.
    """
    #TIP this method consists in a concatenation of heuristic and
    #almost-arbitrary rules.
    result = ''
    waves = list(waves)
    while waves:
        wav = waves.pop(0)
        #If the first wave is negative...
        if not result and wav.sign == -1:
            if not waves:
                result = 'QS' if abs(wav.amp) > ph2dg(0.5) else 'Q'
            else:
                result = 'Q' if abs(wav.amp) > ph2dg(0.2) else 'q'
        else:
            newt = 'r' if wav.sign == 1 else 's'
            if abs(wav.amp) > ph2dg(0.5):
                newt = newt.upper()
            result += newt
    return result
Esempio n. 5
0
import construe.utils.MIT.ECGCodes as C
import construe.utils.MIT.interp2annots as interp2annots
from construe.inference.searching import ilen
from construe.model.interval import Interval as Iv
from construe.acquisition.signal_buffer import Leads
from construe.utils.units_helper import (msec2samples as ms2sp, samples2msec as
                                         sp2ms, phys2digital as ph2dg,
                                         digital2phys as dg2ph, msec2bpm,
                                         set_sampling_freq)
import numpy as np
import collections
import sortedcontainers
import pprint

#Histogram bins for the P-wave characterization
PW_BINS = [0.0, ph2dg(0.05), ph2dg(0.1), ph2dg(0.5), ph2dg(1.0)]

#Features vector
Feat = collections.namedtuple(
    'Feat',
    ["RR", "dRR", "Dur", "dDur", "Axis", "dAxis", "Pw", "Rh", "Sim", "dAmp"])
#Cluster structure
Cluster = collections.namedtuple('Cluster', ['beats', 'info'])

#Codes for the rhythm.
REGULAR, AFIB, ADVANCED, DELAYED = list(range(4))
#Atrial fibrillation beats are tagged as NORMAL in the MIT-BIH Arrhythmia
#database, but during the classification, we marked them with a different code
#not used for other purposes, although semantically related with it
AFTAG = C.SYSTOLE
Esempio n. 6
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()
Esempio n. 7
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 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
Esempio n. 8
0
def _delimit_t(signal, baseline, ls_lim, ee_lim, qrs_shape):
    """
    This function performs the delineation of a possible T Wave present
    in the fragment. To obtain the endpoint of the T Wave, it uses a method
    based on the work by Zhang: 'An algorithm for robust and efficient location
    of T-wave ends in electrocardiograms'. To get the beginning, it uses a
    probabilistic approach with some basic morphology constraints. All the
    processing is made to a simplification of the signal fragment with at most
    7 points.
    """
    try:
        #We exclude the areas in which the slope of the signal exceeds limit.
        maxtslope = qrs_shape.maxslope * C.TQRS_MAX_DIFFR
        lidx, uidx = 0, len(signal)
        if ls_lim > 0:
            idx = np.where(
                np.max(np.abs(np.diff(signal[:ls_lim +
                                             1]))) > maxtslope)[0] + 1
            lidx = max(idx) if len(idx) > 0 else 0
        if ee_lim < len(signal) - 1:
            idx = np.where(
                np.max(np.abs(np.diff(signal[ee_lim:]))) > maxtslope
            )[0] + ee_lim
            uidx = min(idx) if len(idx) > 0 else len(signal) - 1
            if (uidx > 1 and
                    abs(signal[uidx] - baseline) > C.TWEND_BASELINE_MAX_DIFF):
                dfsign = np.sign(np.diff(signal[:uidx + 1]))
                signchange = ((np.roll(dfsign, 1) - dfsign) != 0).astype(int)
                if np.any(signchange):
                    uidx = np.where(signchange)[0][-1]
        verify(uidx >= lidx)
        signal = signal[lidx:uidx + 1]
        ls_lim -= lidx
        ee_lim -= lidx
        #Any T waveform should be representable with at most 7 points.
        points = DP.arrayRDP(signal,
                             max(ph2dg(0.02), qrs_shape.amplitude / 20.0), 7)
        n = len(points)
        verify(n >= 3)
        #1. Endpoint estimation
        epts = points[points >= ee_lim]
        verify(len(epts) > 0)
        Tend, dum = _zhang_tendpoint(signal, epts)
        #2. Onset point estimation.
        bpts = points[np.logical_and(points < Tend, points <= ls_lim)]
        score = {}
        #Range to normalize differences in the signal values
        rang = max(baseline, signal.max()) - min(signal.min(), baseline)
        #There must be between one and 3 peaks in the T Wave.
        for i in xrange(len(bpts)):
            sigpt = signal[points[i:np.where(points == Tend)[0][0] + 1]]
            npks = len(get_peaks(sigpt)) if len(sigpt) >= 3 else 0
            if (npks < 1 or npks > 2 or np.ptp(sigpt) <= ph2dg(0.05)):
                continue
            bl_dist = 1.0 - np.abs(signal[bpts[i]] - baseline) / rang
            tdur = sp2ms(Tend - bpts[i])
            score[bpts[i]] = bl_dist * _check_histogram(_TDUR_HIST, tdur)
        verify(score)
        Tbeg = max(score, key=score.get)
        verify(score[Tbeg] > 0)
        verify(np.max(np.abs(np.diff(signal[Tbeg:Tend + 1]))) <= maxtslope)
        return (Iv(Tbeg + lidx, Tend + lidx), dum)
    except InconsistencyError:
        return None