def engzee_segmenter(signal=None, sampling_rate=1000., threshold=0.48): # check inputs if signal is None: raise TypeError("Please specify an input signal.") # algorithm parameters changeM = int(0.75 * sampling_rate) Miterate = int(1.75 * sampling_rate) v250ms = int(0.25 * sampling_rate) v1200ms = int(1.2 * sampling_rate) v1500ms = int(1.5 * sampling_rate) v180ms = int(0.18 * sampling_rate) p10ms = int(np.ceil(0.01 * sampling_rate)) p20ms = int(np.ceil(0.02 * sampling_rate)) err_kill = int(0.01 * sampling_rate) inc = 1 mmth = threshold mmp = 0.2 # Differentiator (1) y1 = [signal[i] - signal[i - 4] for i in range(4, len(signal))] # Low pass filter (2) c = [1, 4, 6, 4, 1, -1, -4, -6, -4, -1] y2 = np.array([np.dot(c, y1[n - 9:n + 1]) for n in range(9, len(y1))]) y2_len = len(y2) # vars MM = mmth * max(y2[:Miterate]) * np.ones(3) MMidx = 0 Th = np.mean(MM) NN = mmp * min(y2[:Miterate]) * np.ones(2) NNidx = 0 ThNew = np.mean(NN) update = False nthfpluss = [] rpeaks = [] # Find nthf+ point while True: # If a previous intersection was found, continue the analysis from there if update: if inc * changeM + Miterate < y2_len: a = (inc - 1) * changeM b = inc * changeM + Miterate Mnew = mmth * max(y2[a:b]) Nnew = mmp * min(y2[a:b]) elif y2_len - (inc - 1) * changeM > v1500ms: a = (inc - 1) * changeM Mnew = mmth * max(y2[a:]) Nnew = mmp * min(y2[a:]) if len(y2) - inc * changeM > Miterate: MM[MMidx] = Mnew if Mnew <= 1.5 * MM[MMidx - 1] else 1.1 * MM[ MMidx - 1] NN[NNidx] = Nnew if abs(Nnew) <= 1.5 * abs( NN[NNidx - 1]) else 1.1 * NN[NNidx - 1] MMidx = np.mod(MMidx + 1, len(MM)) NNidx = np.mod(NNidx + 1, len(NN)) Th = np.mean(MM) ThNew = np.mean(NN) inc += 1 update = False if nthfpluss: lastp = nthfpluss[-1] + 1 if lastp < (inc - 1) * changeM: lastp = (inc - 1) * changeM y22 = y2[lastp:inc * changeM + err_kill] # find intersection with Th try: nthfplus = np.intersect1d( np.nonzero(y22 > Th)[0], np.nonzero(y22 < Th)[0] - 1)[0] except IndexError: if inc * changeM > len(y2): break else: update = True continue # adjust index nthfplus += int(lastp) # if a previous R peak was found: if rpeaks: # check if intersection is within the 200-1200 ms interval. Modification: 300 ms -> 200 bpm if nthfplus - rpeaks[-1] > v250ms and nthfplus - rpeaks[ -1] < v1200ms: pass # if new intersection is within the <200ms interval, skip it. Modification: 300 ms -> 200 bpm elif nthfplus - rpeaks[-1] < v250ms: nthfpluss += [nthfplus] continue # no previous intersection, find the first one else: try: aux = np.nonzero( y2[(inc - 1) * changeM:inc * changeM + err_kill] > Th)[0] bux = np.nonzero(y2[ (inc - 1) * changeM:inc * changeM + err_kill] < Th)[0] - 1 nthfplus = int( (inc - 1) * changeM) + np.intersect1d(aux, bux)[0] except IndexError: if inc * changeM > len(y2): break else: update = True continue nthfpluss += [nthfplus] # Define 160ms search region windowW = np.arange(nthfplus, nthfplus + v180ms) # Check if the condition y2[n] < Th holds for a specified # number of consecutive points (experimentally we found this number to be at least 10 points)" i, f = windowW[0], windowW[-1] if windowW[-1] < len(y2) else -1 hold_points = np.diff(np.nonzero(y2[i:f] < ThNew)[0]) cont = 0 for hp in hold_points: if hp == 1: cont += 1 if cont == p10ms - 1: # -1 is because diff eats a sample max_shift = p20ms # looks for X's max a bit to the right if nthfpluss[-1] > max_shift: rpeaks += [ np.argmax(signal[i - max_shift:f]) + i - max_shift ] else: rpeaks += [np.argmax(signal[i:f]) + i] break else: cont = 0 rpeaks = sorted(list(set(rpeaks))) rpeaks = np.array(rpeaks, dtype='int') return utils.ReturnTuple((rpeaks, ), ('rpeaks', ))
def hamilton_segmenter(signal=None, sampling_rate=1000.): if signal is None: raise TypeError("Please specify an input signal.") sampling_rate = float(sampling_rate) length = len(signal) dur = length / sampling_rate # algorithm parameters v1s = int(1. * sampling_rate) v100ms = int(0.1 * sampling_rate) TH_elapsed = np.ceil(0.36 * sampling_rate) sm_size = int(0.08 * sampling_rate) init_ecg = 8 # seconds for initialization if dur < init_ecg: init_ecg = int(dur) # filtering filtered, _, _ = st.filter_signal(signal=signal, ftype='butter', band='lowpass', order=4, frequency=25., sampling_rate=sampling_rate) filtered, _, _ = st.filter_signal(signal=filtered, ftype='butter', band='highpass', order=4, frequency=3., sampling_rate=sampling_rate) # diff dx = np.abs(np.diff(filtered, 1) * sampling_rate) # smoothing dx, _ = st.smoother(signal=dx, kernel='hamming', size=sm_size, mirror=True) # buffers qrspeakbuffer = np.zeros(init_ecg) noisepeakbuffer = np.zeros(init_ecg) peak_idx_test = np.zeros(init_ecg) noise_idx = np.zeros(init_ecg) rrinterval = sampling_rate * np.ones(init_ecg) a, b = 0, v1s all_peaks, _ = st.find_extrema(signal=dx, mode='max') for i in range(init_ecg): peaks, values = st.find_extrema(signal=dx[a:b], mode='max') try: ind = np.argmax(values) except ValueError: pass else: # peak amplitude qrspeakbuffer[i] = values[ind] # peak location peak_idx_test[i] = peaks[ind] + a a += v1s b += v1s # thresholds ANP = np.median(noisepeakbuffer) AQRSP = np.median(qrspeakbuffer) TH = 0.475 DT = ANP + TH * (AQRSP - ANP) DT_vec = [] indexqrs = 0 indexnoise = 0 indexrr = 0 npeaks = 0 offset = 0 beats = [] # detection rules # 1 - ignore all peaks that precede or follow larger peaks by less than 200ms lim = int(np.ceil(0.2 * sampling_rate)) diff_nr = int(np.ceil(0.045 * sampling_rate)) bpsi, bpe = offset, 0 for f in all_peaks: DT_vec += [DT] # 1 - Checking if f-peak is larger than any peak following or preceding it by less than 200 ms peak_cond = np.array( (all_peaks > f - lim) * (all_peaks < f + lim) * (all_peaks != f)) peaks_within = all_peaks[peak_cond] if peaks_within.any() and (max(dx[peaks_within]) > dx[f]): continue # 4 - If the peak is larger than the detection threshold call it a QRS complex, otherwise call it noise if dx[f] > DT: # 2 - look for both positive and negative slopes in raw signal if f < diff_nr: diff_now = np.diff(signal[0:f + diff_nr]) elif f + diff_nr >= len(signal): diff_now = np.diff(signal[f - diff_nr:len(dx)]) else: diff_now = np.diff(signal[f - diff_nr:f + diff_nr]) diff_signer = diff_now[diff_now > 0] if len(diff_signer) == 0 or len(diff_signer) == len(diff_now): continue # RR INTERVALS if npeaks > 0: # 3 - in here we check point 3 of the Hamilton paper # that is, we check whether our current peak is a valid R-peak. prev_rpeak = beats[npeaks - 1] elapsed = f - prev_rpeak # if the previous peak was within 360 ms interval if elapsed < TH_elapsed: # check current and previous slopes if prev_rpeak < diff_nr: diff_prev = np.diff(signal[0:prev_rpeak + diff_nr]) elif prev_rpeak + diff_nr >= len(signal): diff_prev = np.diff(signal[prev_rpeak - diff_nr:len(dx)]) else: diff_prev = np.diff( signal[prev_rpeak - diff_nr:prev_rpeak + diff_nr]) slope_now = max(diff_now) slope_prev = max(diff_prev) if (slope_now < 0.5 * slope_prev): # if current slope is smaller than half the previous one, then it is a T-wave continue if dx[f] < 3. * np.median( qrspeakbuffer): # avoid retarded noise peaks beats += [int(f) + bpsi] else: continue if bpe == 0: rrinterval[indexrr] = beats[npeaks] - beats[npeaks - 1] indexrr += 1 if indexrr == init_ecg: indexrr = 0 else: if beats[npeaks] > beats[bpe - 1] + v100ms: rrinterval[indexrr] = beats[npeaks] - beats[npeaks - 1] indexrr += 1 if indexrr == init_ecg: indexrr = 0 elif dx[f] < 3. * np.median(qrspeakbuffer): beats += [int(f) + bpsi] else: continue npeaks += 1 qrspeakbuffer[indexqrs] = dx[f] peak_idx_test[indexqrs] = f indexqrs += 1 if indexqrs == init_ecg: indexqrs = 0 if dx[f] <= DT: tf = f + bpsi # RR interval median RRM = np.median(rrinterval) # initial values are good? if len(beats) >= 2: elapsed = tf - beats[npeaks - 1] if elapsed >= 1.5 * RRM and elapsed > TH_elapsed: if dx[f] > 0.5 * DT: beats += [int(f) + offset] # RR INTERVALS if npeaks > 0: rrinterval[indexrr] = beats[npeaks] - beats[npeaks - 1] indexrr += 1 if indexrr == init_ecg: indexrr = 0 npeaks += 1 qrspeakbuffer[indexqrs] = dx[f] peak_idx_test[indexqrs] = f indexqrs += 1 if indexqrs == init_ecg: indexqrs = 0 else: noisepeakbuffer[indexnoise] = dx[f] noise_idx[indexnoise] = f indexnoise += 1 if indexnoise == init_ecg: indexnoise = 0 else: noisepeakbuffer[indexnoise] = dx[f] noise_idx[indexnoise] = f indexnoise += 1 if indexnoise == init_ecg: indexnoise = 0 # Update Detection Threshold ANP = np.median(noisepeakbuffer) AQRSP = np.median(qrspeakbuffer) DT = ANP + 0.475 * (AQRSP - ANP) beats = np.array(beats) r_beats = [] thres_ch = 0.85 adjacency = 0.05 * sampling_rate for i in beats: error = [False, False] if i - lim < 0: window = signal[0:i + lim] add = 0 elif i + lim >= length: window = signal[i - lim:length] add = i - lim else: window = signal[i - lim:i + lim] add = i - lim # meanval = np.mean(window) w_peaks, _ = st.find_extrema(signal=window, mode='max') w_negpeaks, _ = st.find_extrema(signal=window, mode='min') zerdiffs = np.where(np.diff(window) == 0)[0] w_peaks = np.concatenate((w_peaks, zerdiffs)) w_negpeaks = np.concatenate((w_negpeaks, zerdiffs)) pospeaks = sorted(zip(window[w_peaks], w_peaks), reverse=True) negpeaks = sorted(zip(window[w_negpeaks], w_negpeaks)) try: twopeaks = [pospeaks[0]] except IndexError: twopeaks = [] try: twonegpeaks = [negpeaks[0]] except IndexError: twonegpeaks = [] # getting positive peaks for i in range(len(pospeaks) - 1): if abs(pospeaks[0][1] - pospeaks[i + 1][1]) > adjacency: twopeaks.append(pospeaks[i + 1]) break try: posdiv = abs(twopeaks[0][0] - twopeaks[1][0]) except IndexError: error[0] = True # getting negative peaks for i in range(len(negpeaks) - 1): if abs(negpeaks[0][1] - negpeaks[i + 1][1]) > adjacency: twonegpeaks.append(negpeaks[i + 1]) break try: negdiv = abs(twonegpeaks[0][0] - twonegpeaks[1][0]) except IndexError: error[1] = True # choosing type of R-peak n_errors = sum(error) try: if not n_errors: if posdiv > thres_ch * negdiv: # pos noerr r_beats.append(twopeaks[0][1] + add) else: # neg noerr r_beats.append(twonegpeaks[0][1] + add) elif n_errors == 2: if abs(twopeaks[0][1]) > abs(twonegpeaks[0][1]): # pos allerr r_beats.append(twopeaks[0][1] + add) else: # neg allerr r_beats.append(twonegpeaks[0][1] + add) elif error[0]: # pos poserr r_beats.append(twopeaks[0][1] + add) else: # neg negerr r_beats.append(twonegpeaks[0][1] + add) except IndexError: continue rpeaks = sorted(list(set(r_beats))) rpeaks = np.array(rpeaks, dtype='int') return utils.ReturnTuple((rpeaks, ), ('rpeaks', ))
def ecg(signal=None, sampling_rate=300., show=True): if signal is None: raise TypeError("Please specify an input signal.") # ensure numpy signal = np.array(signal) sampling_rate = float(sampling_rate) # filter signal order = int(0.3 * sampling_rate) filtered, _, _ = st.filter_signal(signal=signal, ftype='FIR', band='bandpass', order=order, frequency=[3, 45], sampling_rate=sampling_rate) # segment rpeaks, = hamilton_segmenter(signal=filtered, sampling_rate=sampling_rate) # correct R-peak locations rpeaks, = correct_rpeaks(signal=filtered, rpeaks=rpeaks, sampling_rate=sampling_rate, tol=0.05) # extract templates templates, rpeaks = extract_heartbeats(signal=filtered, rpeaks=rpeaks, sampling_rate=sampling_rate, before=0.2, after=0.4) # compute heart rate hr_idx, hr = st.get_heart_rate(beats=rpeaks, sampling_rate=sampling_rate, smooth=True, size=3) # get time vectors length = len(signal) T = (length - 1) / sampling_rate ts = np.linspace(0, T, length, endpoint=False) ts_hr = ts[hr_idx] ts_tmpl = np.linspace(-0.2, 0.4, templates.shape[1], endpoint=False) # plot if show: plotting.plot_ecg(ts=ts, raw=signal, filtered=filtered, rpeaks=rpeaks, templates_ts=ts_tmpl, templates=templates, heart_rate_ts=ts_hr, heart_rate=hr, path=None, show=True) # output args = (ts, filtered, rpeaks, ts_tmpl, templates, ts_hr, hr) names = ('ts', 'filtered', 'rpeaks', 'templates_ts', 'templates', 'heart_rate_ts', 'heart_rate') return utils.ReturnTuple(args, names)
def christov_segmenter(signal=None, sampling_rate=1000.): # check inputs if signal is None: raise TypeError("Please specify an input signal.") length = len(signal) # algorithm parameters v100ms = int(0.1 * sampling_rate) v50ms = int(0.050 * sampling_rate) v300ms = int(0.300 * sampling_rate) v350ms = int(0.350 * sampling_rate) v200ms = int(0.2 * sampling_rate) v1200ms = int(1.2 * sampling_rate) M_th = 0.4 # paper is 0.6 b = np.ones(int(0.02 * sampling_rate)) / 50. a = [1] X = ss.filtfilt(b, a, signal) # 2. Moving averaging of samples in 28 ms interval for electromyogram # noise suppression a filter with first zero at about 35 Hz. b = np.ones(int(sampling_rate / 35.)) / 35. X = ss.filtfilt(b, a, X) X, _, _ = st.filter_signal(signal=X, ftype='butter', band='lowpass', order=7, frequency=40., sampling_rate=sampling_rate) X, _, _ = st.filter_signal(signal=X, ftype='butter', band='highpass', order=7, frequency=9., sampling_rate=sampling_rate) k, Y, L = 1, [], len(X) for n in range(k + 1, L - k): Y.append(X[n]**2 - X[n - k] * X[n + k]) Y = np.array(Y) Y[Y < 0] = 0 b = np.ones(int(sampling_rate / 25.)) / 25. Y = ss.lfilter(b, a, Y) # Init MM = M_th * np.max(Y[:int(5 * sampling_rate)]) * np.ones(5) MMidx = 0 M = np.mean(MM) slope = np.linspace(1.0, 0.6, int(sampling_rate)) Rdec = 0 R = 0 RR = np.zeros(5) RRidx = 0 Rm = 0 QRS = [] Rpeak = [] current_sample = 0 skip = False F = np.mean(Y[:v350ms]) # Go through each sample while current_sample < len(Y): if QRS: # No detection is allowed 200 ms after the current one. In # the interval QRS to QRS+200ms a new value of M5 is calculated: newM5 = 0.6*max(Yi) if current_sample <= QRS[-1] + v200ms: Mnew = M_th * max(Y[QRS[-1]:QRS[-1] + v200ms]) Mnew = Mnew if Mnew <= 1.5 * MM[MMidx - 1] else 1.1 * MM[MMidx - 1] MM[MMidx] = Mnew MMidx = np.mod(MMidx + 1, 5) # M is calculated as an average value of MM. Mtemp = np.mean(MM) M = Mtemp skip = True elif current_sample >= QRS[-1] + v200ms and current_sample < QRS[ -1] + v1200ms: M = Mtemp * slope[current_sample - QRS[-1] - v200ms] # After 1200 ms M remains unchanged. # R = 0 V in the interval from the last detected QRS to 2/3 of the expected Rm. if current_sample >= QRS[-1] and current_sample < QRS[-1] + ( 2 / 3.) * Rm: R = 0 elif current_sample >= QRS[-1] + ( 2 / 3.) * Rm and current_sample < QRS[-1] + Rm: R += Rdec # After QRS + Rm the decrease of R is stopped # MFR = M + F + R MFR = M + F + R # QRS or beat complex is detected if Yi = MFR if not skip and Y[current_sample] >= MFR: QRS += [current_sample] Rpeak += [QRS[-1] + np.argmax(Y[QRS[-1]:QRS[-1] + v300ms])] if len(QRS) >= 2: # A buffer with the 5 last RR intervals is updated at any new QRS detection. RR[RRidx] = QRS[-1] - QRS[-2] RRidx = np.mod(RRidx + 1, 5) skip = False # With every signal sample, F is updated adding the maximum # of Y in the latest 50 ms of the 350 ms interval and # subtracting maxY in the earliest 50 ms of the interval. if current_sample >= v350ms: Y_latest50 = Y[current_sample - v50ms:current_sample] Y_earliest50 = Y[current_sample - v350ms:current_sample - v300ms] F += (max(Y_latest50) - max(Y_earliest50)) / 1000. # Rm is the mean value of the buffer RR. Rm = np.mean(RR) current_sample += 1 rpeaks = [] for i in Rpeak: a, b = i - v100ms, i + v100ms if a < 0: a = 0 if b > length: b = length rpeaks.append(np.argmax(signal[a:b]) + a) rpeaks = sorted(list(set(rpeaks))) rpeaks = np.array(rpeaks, dtype='int') return utils.ReturnTuple((rpeaks, ), ('rpeaks', ))
def smoother(signal=None, kernel='boxzen', size=10, mirror=True, **kwargs): """Smooth a signal using an N-point moving average [MAvg]_ filter. This implementation uses the convolution of a filter kernel with the input signal to compute the smoothed signal [Smit97]_. Availabel kernels: median, boxzen, boxcar, triang, blackman, hamming, hann, bartlett, flattop, parzen, bohman, blackmanharris, nuttall, barthann, kaiser (needs beta), gaussian (needs std), general_gaussian (needs power, width), slepian (needs width), chebwin (needs attenuation). Parameters ---------- signal : array Signal to smooth. kernel : str, array, optional Type of kernel to use; if array, use directly as the kernel. size : int, optional Size of the kernel; ignored if kernel is an array. mirror : bool, optional If True, signal edges are extended to avoid boundary effects. ``**kwargs`` : dict, optional Additional keyword arguments are passed to the underlying scipy.signal.windows function. Returns ------- signal : array Smoothed signal. params : dict Smoother parameters. Notes ----- * When the kernel is 'median', mirror is ignored. References ---------- .. [MAvg] Wikipedia, "Moving Average", http://en.wikipedia.org/wiki/Moving_average .. [Smit97] S. W. Smith, "Moving Average Filters - Implementation by Convolution", http://www.dspguide.com/ch15/1.htm, 1997 """ # check inputs if signal is None: raise TypeError("Please specify a signal to smooth.") length = len(signal) if isinstance(kernel, six.string_types): # check length if size > length: size = length - 1 if size < 1: size = 1 if kernel == 'boxzen': # hybrid method # 1st pass - boxcar kernel aux, _ = smoother(signal, kernel='boxcar', size=size, mirror=mirror) # 2nd pass - parzen kernel smoothed, _ = smoother(aux, kernel='parzen', size=size, mirror=mirror) params = {'kernel': kernel, 'size': size, 'mirror': mirror} args = (smoothed, params) names = ('signal', 'params') return utils.ReturnTuple(args, names) elif kernel == 'median': # median filter if size % 2 == 0: raise ValueError( "When the kernel is 'median', size must be odd.") smoothed = ss.medfilt(signal, kernel_size=size) params = {'kernel': kernel, 'size': size, 'mirror': mirror} args = (smoothed, params) names = ('signal', 'params') return utils.ReturnTuple(args, names) else: win = _get_window(kernel, size, **kwargs) elif isinstance(kernel, np.ndarray): win = kernel size = len(win) # check length if size > length: raise ValueError("Kernel size is bigger than signal length.") if size < 1: raise ValueError("Kernel size is smaller than 1.") else: raise TypeError("Unknown kernel type.") # convolve w = win / win.sum() if mirror: aux = np.concatenate( (signal[0] * np.ones(size), signal, signal[-1] * np.ones(size))) smoothed = np.convolve(w, aux, mode='same') smoothed = smoothed[size:-size] else: smoothed = np.convolve(w, signal, mode='same') # output params = {'kernel': kernel, 'size': size, 'mirror': mirror} params.update(kwargs) args = (smoothed, params) names = ('signal', 'params') return utils.ReturnTuple(args, names)
def compare_segmentation(reference=None, test=None, sampling_rate=1000., offset=0, minRR=None, tol=0.05): # check inputs if reference is None: raise TypeError("Please specify an input reference list of R-peak \ locations.") if test is None: raise TypeError("Please specify an input test list of R-peak \ locations.") if minRR is None: minRR = np.inf sampling_rate = float(sampling_rate) # ensure numpy reference = np.array(reference) test = np.array(test) # convert to samples minRR = minRR * sampling_rate tol = tol * sampling_rate TP = 0 FP = 0 matchIdx = [] dev = [] for i, r in enumerate(test): # deviation to closest R in reference ref = reference[np.argmin(np.abs(reference - (r + offset)))] error = np.abs(ref - (r + offset)) if error < tol: TP += 1 matchIdx.append(i) dev.append(error) else: if len(matchIdx) > 0: bdf = r - test[matchIdx[-1]] if bdf < minRR: # false positive, but removable with RR interval check pass else: FP += 1 else: FP += 1 # convert deviations to time dev = np.array(dev, dtype='float') dev /= sampling_rate nd = len(dev) if nd == 0: mdev = np.nan sdev = np.nan elif nd == 1: mdev = np.mean(dev) sdev = 0. else: mdev = np.mean(dev) sdev = np.std(dev, ddof=1) # interbeat interval th1 = 1.5 # 40 bpm th2 = 0.3 # 200 bpm rIBI = np.diff(reference) rIBI = np.array(rIBI, dtype='float') rIBI /= sampling_rate good = np.nonzero((rIBI < th1) & (rIBI > th2))[0] rIBI = rIBI[good] nr = len(rIBI) if nr == 0: rIBIm = np.nan rIBIs = np.nan elif nr == 1: rIBIm = np.mean(rIBI) rIBIs = 0. else: rIBIm = np.mean(rIBI) rIBIs = np.std(rIBI, ddof=1) tIBI = np.diff(test[matchIdx]) tIBI = np.array(tIBI, dtype='float') tIBI /= sampling_rate good = np.nonzero((tIBI < th1) & (tIBI > th2))[0] tIBI = tIBI[good] nt = len(tIBI) if nt == 0: tIBIm = np.nan tIBIs = np.nan elif nt == 1: tIBIm = np.mean(tIBI) tIBIs = 0. else: tIBIm = np.mean(tIBI) tIBIs = np.std(tIBI, ddof=1) # output perf = float(TP) / len(reference) acc = float(TP) / (TP + FP) err = float(FP) / (TP + FP) args = (TP, FP, perf, acc, err, matchIdx, dev, mdev, sdev, rIBIm, rIBIs, tIBIm, tIBIs) names = ( 'TP', 'FP', 'performance', 'acc', 'err', 'match', 'deviation', 'mean_deviation', 'std_deviation', 'mean_ref_ibi', 'std_ref_ibi', 'mean_test_ibi', 'std_test_ibi', ) return utils.ReturnTuple(args, names)
def signal_stats(signal=None): """Compute various metrics describing the signal. Parameters ---------- signal : array Input signal. Returns ------- mean : float Mean of the signal. median : float Median of the signal. max : float Maximum signal amplitude. var : float Signal variance (unbiased). std_dev : float Standard signal deviation (unbiased). abs_dev : float Absolute signal deviation. kurtosis : float Signal kurtosis (unbiased). skew : float Signal skewness (unbiased). """ # check inputs if signal is None: raise TypeError("Please specify an input signal.") # ensure numpy signal = np.array(signal) # mean mean = np.mean(signal) # median median = np.median(signal) # maximum amplitude maxAmp = np.abs(signal - mean).max() # variance sigma2 = signal.var(ddof=1) # standard deviation sigma = signal.std(ddof=1) # absolute deviation ad = np.sum(np.abs(signal - median)) # kurtosis kurt = stats.kurtosis(signal, bias=False) # skweness skew = stats.skew(signal, bias=False) # output args = (mean, median, maxAmp, sigma2, sigma, ad, kurt, skew) names = ('mean', 'median', 'max', 'var', 'std_dev', 'abs_dev', 'kurtosis', 'skewness') return utils.ReturnTuple(args, names)
def windower(signal=None, size=None, step=None, fcn=None, fcn_kwargs=None, kernel='boxcar', kernel_kwargs=None): """Apply a function to a signal in sequential windows, with optional overlap. Availabel window kernels: boxcar, triang, blackman, hamming, hann, bartlett, flattop, parzen, bohman, blackmanharris, nuttall, barthann, kaiser (needs beta), gaussian (needs std), general_gaussian (needs power, width), slepian (needs width), chebwin (needs attenuation). Parameters ---------- signal : array Input signal. size : int Size of the signal window. step : int, optional Size of window shift; if None, there is no overlap. fcn : callable Function to apply to each window. fcn_kwargs : dict, optional Additional keyword arguments to pass to 'fcn'. kernel : str, array, optional Type of kernel to use; if array, use directly as the kernel. kernel_kwargs : dict, optional Additional keyword arguments to pass on window creation; ignored if 'kernel' is an array. Returns ------- index : array Indices characterizing window locations (start of the window). values : array Concatenated output of calling 'fcn' on each window. """ # check inputs if signal is None: raise TypeError("Please specify an input signal.") if fcn is None: raise TypeError("Please specify a function to apply to each window.") if kernel_kwargs is None: kernel_kwargs = {} length = len(signal) if isinstance(kernel, basestring): # check size if size > length: raise ValueError("Window size must be smaller than signal length.") win = _get_window(kernel, size, **kernel_kwargs) elif isinstance(kernel, np.ndarray): win = kernel size = len(win) # check size if size > length: raise ValueError("Window size must be smaller than signal length.") if step is None: step = size if step <= 0: raise ValueError("Step size must be at least 1.") # number of windows nb = 1 + (length - size) / step # check signal dimensionality if np.ndim(signal) == 2: # time along 1st dim, tile window nch = np.shape(signal)[1] win = np.tile(np.reshape(win, (size, 1)), nch) index = [] values = [] for i in xrange(nb): start = i * step stop = start + size index.append(start) aux = signal[start:stop] * win # apply function out = fcn(aux, **fcn_kwargs) values.append(out) # transform to numpy index = np.array(index, dtype='int') values = np.array(values) return utils.ReturnTuple((index, values), ('index', 'values'))
def power_spectrum(signal=None, sampling_rate=1000., pad=None, pow2=False, decibel=True): """Compute the power spectrum of a signal (one-sided). Parameters ---------- signal : array Input signal. sampling_rate : int, float, optional Sampling frequency (Hz). pad : int, optional Padding for the Fourier Transform (number of zeros added). pow2 : bool, optional If True, rounds the number of points `N = len(signal) + pad` to the nearest power of 2 greater than N. decibel : bool, optional If True, returns the power in decibels. Returns ------- freqs : array Array of frequencies (Hz) at which the power was computed. power : array Power spectrum. """ # check inputs if signal is None: raise TypeError("Please specify an input signal.") npoints = len(signal) if pad is not None: if pad >= 0: npoints += pad else: raise ValueError("Padding must be a positive integer.") # power of 2 if pow2: npoints = 2**(np.ceil(np.log2(npoints))) Nyq = float(sampling_rate) / 2 hpoints = npoints / 2 freqs = np.linspace(0, Nyq, hpoints) power = np.abs(np.fft.fft(signal, npoints)) / npoints # one-sided power = power[:hpoints] power[1:] *= 2 power = np.power(power, 2) if decibel: power = 10. * np.log10(power) return utils.ReturnTuple((freqs, power), ('freqs', 'power'))
def filter_signal(signal=None, ftype='FIR', band='lowpass', order=None, frequency=None, sampling_rate=1000., **kwargs): """Filter a signal according to the given parameters. Parameters ---------- signal : array Signal to filter. ftype : str Filter type: * Finite Impulse Response filter ('FIR'); * Butterworth filter ('butter'); * Chebyshev filters ('cheby1', 'cheby2'); * Elliptic filter ('ellip'); * Bessel filter ('bessel'). band : str Band type: * Low-pass filter ('lowpass'); * High-pass filter ('highpass'); * Band-pass filter ('bandpass'); * Band-stop filter ('bandstop'). order : int Order of the filter. frequency : int, float, list, array Cutoff frequencies; format depends on type of band: * 'lowpass' or 'bandpass': single frequency; * 'bandpass' or 'bandstop': pair of frequencies. sampling_rate : int, float, optional Sampling frequency (Hz). ``**kwargs`` : dict, optional Additional keyword arguments are passed to the underlying scipy.signal function. Returns ------- signal : array Filtered signal. sampling_rate : float Sampling frequency (Hz). params : dict Filter parameters. Notes ----- * Uses a forward-backward filter implementation. Therefore, the combined filter has linear phase. """ # check inputs if signal is None: raise TypeError("Please specify a signal to filter.") # get filter b, a = get_filter(ftype=ftype, order=order, frequency=frequency, sampling_rate=sampling_rate, band=band, **kwargs) # filter filtered, _ = _filter_signal(b, a, signal, check_phase=True) # output params = { 'ftype': ftype, 'order': order, 'frequency': frequency, 'band': band, } params.update(kwargs) args = (filtered, sampling_rate, params) names = ('signal', 'sampling_rate', 'params') return utils.ReturnTuple(args, names)
def get_filter(ftype='FIR', band='lowpass', order=None, frequency=None, sampling_rate=1000., **kwargs): """Compute digital (FIR or IIR) filter coefficients with the given parameters. Parameters ---------- ftype : str Filter type: * Finite Impulse Response filter ('FIR'); * Butterworth filter ('butter'); * Chebyshev filters ('cheby1', 'cheby2'); * Elliptic filter ('ellip'); * Bessel filter ('bessel'). band : str Band type: * Low-pass filter ('lowpass'); * High-pass filter ('highpass'); * Band-pass filter ('bandpass'); * Band-stop filter ('bandstop'). order : int Order of the filter. frequency : int, float, list, array Cutoff frequencies; format depends on type of band: * 'lowpass' or 'bandpass': single frequency; * 'bandpass' or 'bandstop': pair of frequencies. sampling_rate : int, float, optional Sampling frequency (Hz). ``**kwargs`` : dict, optional Additional keyword arguments are passed to the underlying scipy.signal function. Returns ------- b : array Numerator coefficients. a : array Denominator coefficients. See Also: scipy.signal """ # check inputs if order is None: raise TypeError("Please specify the filter order.") if frequency is None: raise TypeError("Please specify the cutoff frequency.") if band not in ['lowpass', 'highpass', 'bandpass', 'bandstop']: raise ValueError( "Unknown filter type '%r'; choose 'lowpass', 'highpass', \ 'bandpass', or 'bandstop'." % band) # convert frequencies frequency = _norm_freq(frequency, sampling_rate) # get coeffs b, a = [], [] if ftype == 'FIR': # FIR filter if order % 2 == 0: order += 1 a = np.array([1]) if band in ['lowpass', 'bandstop']: b = ss.firwin(numtaps=order, cutoff=frequency, pass_zero=True, **kwargs) elif band in ['highpass', 'bandpass']: b = ss.firwin(numtaps=order, cutoff=frequency, pass_zero=False, **kwargs) elif ftype == 'butter': # Butterworth filter b, a = ss.butter(N=order, Wn=frequency, btype=band, analog=False, output='ba', **kwargs) elif ftype == 'cheby1': # Chebyshev type I filter b, a = ss.cheby1(N=order, Wn=frequency, btype=band, analog=False, output='ba', **kwargs) elif ftype == 'cheby2': # chevyshev type II filter b, a = ss.cheby2(N=order, Wn=frequency, btype=band, analog=False, output='ba', **kwargs) elif ftype == 'ellip': # Elliptic filter b, a = ss.ellip(N=order, Wn=frequency, btype=band, analog=False, output='ba', **kwargs) elif ftype == 'bessel': # Bessel filter b, a = ss.bessel(N=order, Wn=frequency, btype=band, analog=False, output='ba', **kwargs) return utils.ReturnTuple((b, a), ('b', 'a'))
def find_intersection(x1=None, y1=None, x2=None, y2=None, alpha=1.5, xtol=1e-6, ytol=1e-6): """Find the intersection points between two lines using piecewise polynomial interpolation. Parameters ---------- x1 : array Array of x-coordinates of the first line. y1 : array Array of y-coordinates of the first line. x2 : array Array of x-coordinates of the second line. y2 : array Array of y-coordinates of the second line. alpha : float, optional Resolution factor for the x-axis; fraction of total number of x-coordinates. xtol : float, optional Tolerance for the x-axis. ytol : float, optional Tolerance for the y-axis. Returns ------- roots : array Array of x-coordinates of found intersection points. values : array Array of y-coordinates of found intersection points. Notes ----- * If no intersection is found, returns the closest point. """ # check inputs if x1 is None: raise TypeError("Please specify the x-coordinates of the first line.") if y1 is None: raise TypeError("Please specify the y-coordinates of the first line.") if x2 is None: raise TypeError("Please specify the x-coordinates of the second line.") if y2 is None: raise TypeError("Please specify the y-coordinates of the second line.") # ensure numpy x1 = np.array(x1) y1 = np.array(y1) x2 = np.array(x2) y2 = np.array(y2) if x1.shape != y1.shape: raise ValueError( "Input coordinates for the first line must have the same shape.") if x2.shape != y2.shape: raise ValueError( "Input coordinates for the second line must have the same shape.") # interpolate p1 = interpolate.BPoly.from_derivatives(x1, y1[:, np.newaxis]) p2 = interpolate.BPoly.from_derivatives(x2, y2[:, np.newaxis]) # combine x intervals x = np.r_[x1, x2] x_min = x.min() x_max = x.max() npoints = int(len(np.unique(x)) * alpha) x = np.linspace(x_min, x_max, npoints) # initial estimates pd = p1(x) - p2(x) zerocs, = zero_cross(pd) pd_abs = np.abs(pd) zeros = np.nonzero(pd_abs < ytol)[0] ind = np.unique(np.concatenate((zerocs, zeros))) xi = x[ind] # search for solutions roots = set() for v in xi: root, _, ier, _ = optimize.fsolve(_pdiff, v, (p1, p2), full_output=True, xtol=xtol) if ier == 1 and x_min <= root <= x_max: roots.add(root[0]) if len(roots) == 0: # no solution was found => give the best from the initial estimates aux = np.abs(pd) bux = aux.min() * np.ones(npoints, dtype='float') roots, _ = find_intersection(x, aux, x, bux, alpha=1., xtol=xtol, ytol=ytol) # compute values roots = list(roots) roots.sort() roots = np.array(roots) values = np.mean(np.vstack((p1(roots), p2(roots))), axis=0) return utils.ReturnTuple((roots, values), ('roots', 'values'))
def synchronize(signal1=None, signal2=None): """Align two signals based on cross-correlation. Parameters ---------- signal1 : array First input signal. signal2 : array Second input signal. Returns ------- delay : int Delay (number of samples) of 'signal1' in relation to 'signal2'; if 'delay' < 0 , 'signal1' is ahead in relation to 'signal2'; if 'delay' > 0 , 'signal1' is delayed in relation to 'signal2'. corr : float Value of maximum correlation. synch1 : array Biggest possible portion of 'signal1' in synchronization. synch2 : array Biggest possible portion of 'signal2' in synchronization. """ # check inputs if signal1 is None: raise TypeError("Please specify the first input signal.") if signal2 is None: raise TypeError("Please specify the second input signal.") n1 = len(signal1) n2 = len(signal2) # correlate corr = np.correlate(signal1, signal2, mode='full') x = np.arange(-n2 + 1, n1, dtype='int') ind = np.argmax(corr) delay = x[ind] maxCorr = corr[ind] # get synchronization overlap if delay < 0: c = min([n1, len(signal2[-delay:])]) synch1 = signal1[:c] synch2 = signal2[-delay:-delay + c] elif delay > 0: c = min([n2, len(signal1[delay:])]) synch1 = signal1[delay:delay + c] synch2 = signal2[:c] else: c = min([n1, n2]) synch1 = signal1[:c] synch2 = signal2[:c] # output args = (delay, maxCorr, synch1, synch2) names = ('delay', 'corr', 'synch1', 'synch2') return utils.ReturnTuple(args, names)