Skip to content

Commit 33361db

Browse files
committed
work on pantompkins
1 parent 1963240 commit 33361db

File tree

1 file changed

+154
-46
lines changed

1 file changed

+154
-46
lines changed

wfdb/processing/ecg/peakdetect.py

Lines changed: 154 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class PanTompkins(object):
1010
Works on static signals. In future update,
1111
will work on streaming ecg signal.
1212
"""
13-
def __init__(self, sig=None, fs=None, streamsig=None):
13+
def __init__(self, sig=None, fs=None, streamsig=None, ):
1414
self.sig = sig
1515
self.fs = fs
1616

@@ -19,6 +19,12 @@ def __init__(self, sig=None, fs=None, streamsig=None):
1919
if sig is not None:
2020
self.siglen = len(sig)
2121

22+
# Feature to add
23+
# "For irregular heart rates, the first threshold
24+
# of each set is reduced by half so as to increase
25+
# the detection sensitivity and to avoid missing beats"
26+
#self.irregular_hr = irregular_hr
27+
2228
def detect_qrs_static(self):
2329
"""
2430
Detect all the qrs locations in the static
@@ -49,16 +55,29 @@ def detect_qrs_static(self):
4955
# Number of indices from the previous r peak to this index
5056
last_r_distance = i - qrs_inds[-1]
5157

58+
# It has been very long since the last r peak
59+
# was found. Search back for common 2 signal
60+
# peaks and test using lower threshold
61+
if last_r_distance > self.rr_missed_limit:
62+
63+
self.backsearch()
64+
# Continue with this index whether or not
65+
# a previous common peak was marked as qrs
66+
last_r_distance = i - qrs_inds[-1]
67+
5268
# Determine whether the current index is a peak
5369
# for each signal
5470
is_peak_F = ispeak(sig_F, self.siglen, i, 20)
5571
is_peak_I = ispeak(sig_I, self.siglen, i, 20)
5672

73+
# Keep track of common peaks that have not been classified as
74+
# signal peaks for future backsearch
75+
if is_peak_F and is_peak_I:
76+
self.recent_commonpeaks.append(i)
77+
5778
# Whether the current index is a signal peak or noise peak
5879
is_sigpeak_F = False
5980
is_sigpeak_I = False
60-
is_noisepeak_F = False
61-
is_noisepeak_I = False
6281

6382
# If peaks are detected, classify them as signal or noise
6483
# for their respective channels
@@ -68,57 +87,50 @@ def detect_qrs_static(self):
6887
if sig_F[i] > self.thresh_F:
6988
is_sigpeak_F = True
7089
# Did not satisfy signal peak criteria.
71-
# Label as noise peak
90+
# Classify as noise peak
7291
else:
73-
is_noisepeak_F = True
92+
self.update_peak_params('nF', i)
7493

7594
if is_peak_I:
7695
# Satisfied signal peak criteria for the channel
7796
if sig_I[i] > self.thresh_I:
78-
is_peak_sig_I = True
97+
is_sigpeak_I = True
7998
# Did not satisfy signal peak criteria.
80-
# Label as noise peak
99+
# Classify as noise peak
81100
else:
82-
is_noisepeak_I = True
101+
self.update_peak_params('nI', i)
83102

84103
# Check for double signal peak coincidence and at least >200ms (40 samples samples)
85104
# since the previous r peak
86105
is_sigpeak = is_sigpeak_F and is_sigpeak_I and (last_r_distance>40)
87106

88107
# The peak crosses thresholds for each channel and >200ms from previous peak
89108
if is_sigpeak:
90-
91109
# If the rr interval < 360ms (72 samples), the peak is checked
92110
# to determine whether it is a T-Wave. This is the final test
93111
# to run before the peak can be marked as a qrs complex.
94112
if last_r_distance < 72:
95-
113+
is_twave = self.istwave(i)
114+
# Classified as likely a t-wave, not a qrs.
115+
if is_twave:
116+
self.update_peak_params('nF', i)
117+
self.update_peak_params('nI', i)
118+
continue
119+
120+
# Finally can classify as a signal peak
121+
# Update running parameters
122+
self.update_peak_params('s', i)
123+
124+
continue
96125

97-
# "If the maximal slope that occurs during this waveform
98-
# is less than half that of the QRS waveform that preceded it,
99-
# it is identified to be a T-wave"
126+
# No double agreement of signal peak.
127+
# Any individual peak that passed its threshold criterial
128+
# will still be classified as noise peak.
129+
elif is_sigpeak_F:
130+
self.update_peak_params('nF', i)
131+
elif is_sigpeak_I:
132+
self.update_peak_params('nI', i)
100133

101-
# Update running parameters
102-
103-
sigpeak_I = 0.875*sigpeak_I + 0.125*sig_I[i]
104-
sigpeak_F = 0.875*sigpeak_I + 0.125*sig_I[i]
105-
106-
last_r_ind = i
107-
rr_limitavg
108-
rr_limitavg
109-
110-
111-
# Not a signal peak. Update running parameters
112-
# if any other peaks are detected
113-
elif is_peak_F:
114-
115-
116-
last_r_distance = i - last_r_ind
117-
118-
if last_r_distance >
119-
120-
121-
qrs = np.zeros(10)
122134

123135
# Convert the peak indices back to the original fs if necessary
124136
if fs!=200:
@@ -218,7 +230,7 @@ def learnparams(self):
218230
moments. Signal peaks are only defined when the same index is
219231
determined to be a peak in both signals. If fewer than 2 signal
220232
peaks are detected, shift to the next 2 window and try again.
221-
- Using the classified estimated peaks, threshold1 is estimated as
233+
- Using the classified estimated peaks, threshold is estimated as
222234
based on the steady state estimate equation: thres = 0.75*noisepeak + 0.25*sigpeak
223235
using the mean of the noisepeaks and signalpeaks instead of the
224236
running value.
@@ -262,10 +274,10 @@ def learnparams(self):
262274
if len(sigpeakinds)>1 and sigpeakinds[1]-sigpeakinds[0]>40:
263275
break
264276

265-
# Need to detect at least 2 peaks. Check the next window.
277+
# Didn't find 2 satisfactory peaks. Check the next window.
266278
windownum = windownum + 1
267279

268-
# Found at least 2 peaks. Use them to set parameters.
280+
# Found at least 2 satisfactory peaks. Use them to set parameters.
269281

270282
# Set running peak estimates to first values
271283
self.sigpeak_F = wavelearn_F[sigpeakinds[0]]
@@ -275,8 +287,8 @@ def learnparams(self):
275287

276288
# Use all signal and noise peaks in learning window to estimate threshold
277289
# Based on steady state equation: thres = 0.75*noisepeak + 0.25*sigpeak
278-
self.thres_F = 0.75*np.mean(wavelearn_F[noisepeakinds_F]) + 0.25*np.mean(wavelearn_F[sigpeakinds_F])
279-
self.thres_I = 0.75*np.mean(wavelearn_I[noisepeakinds_I]) + 0.25*np.mean(wavelearn_I[sigpeakinds_I])
290+
self.thresh_F = 0.75*np.mean(wavelearn_F[noisepeakinds_F]) + 0.25*np.mean(wavelearn_F[sigpeakinds_F])
291+
self.thresh_I = 0.75*np.mean(wavelearn_I[noisepeakinds_I]) + 0.25*np.mean(wavelearn_I[sigpeakinds_I])
280292
# Alternatively, could skip all of that and do something very simple like thresh_F = max(filtsig[:400])/3
281293

282294
# Set the r-r history using the first r-r interval
@@ -292,21 +304,117 @@ def learnparams(self):
292304
self.rr_low_limit = 0.92*rr_average_bound
293305
self.rr_high_limit = 1.16*rr_average_bound
294306
self.rr_missed_limit = 1.66*rr_average_bound
295-
296-
# The previous r index. Used for:
297-
# - refractory period (200ms, 40 samples)
298-
# - t-wave inspection (360ms, 72 samples)
299-
# - triggering searchback for missing r peaks (1.66*rr_average_bound)
300-
self.prev_r_ind = self.firstpeakind
301307

302-
# The qrs indices detected
308+
# The qrs indices detected.
309+
# Initialize with the first signal peak
310+
# detected during this learning phase
303311
self.qrs_inds = [sigpeakinds[0]]
304312

305313
return
306314

315+
# Update parameters when a peak is found
316+
def update_peak_params(self, peaktype, i):
317+
318+
# Noise peak for filtered signal
319+
if peaktype == 'nF':
320+
self.noisepeak_F = 0.875*self.noisepeak_I + 0.125*self.sig_I[i]
321+
# Noise peak for integral signal
322+
elif peaktype == 'nI':
323+
self.noisepeak_I = 0.875*self.noisepeak_I + 0.125*self.sig_I[i]
324+
# Signal peak
325+
else:
326+
new_rr = i - self.qrs_inds[-1]
327+
328+
# The most recent 8 rr intervals
329+
self.rr_history_unbound = self.rr_history_unbound[:-1].append(new_rr)
330+
self.rr_average_unbound = self.rr_average_unbound[:-1]
331+
332+
# The most recent 8 rr intervals that fall within the acceptable low
333+
# and high rr interval limits
334+
if new_rr > self.r_low_limit and new_rr < self.r_high_limit:
335+
self.rr_history_bound = self.rr_history_bound[:-1].append(new_rr)
336+
self.rr_average_bound = np.mean(self.rr_history_bound)
337+
338+
self.rr_low_limit = 0.92*rr_average_bound
339+
self.rr_high_limit = 1.16*rr_average_bound
340+
self.rr_missed_limit = 1.66*rr_average_bound
341+
342+
# Clear the common peaks since last r peak variable
343+
self.recent_commonpeaks = []
344+
345+
self.qrs_inds.append(i)
307346

347+
# Signal peak, regular threshold criteria
348+
if peaktype == 'sr'
349+
self.sigpeak_I = 0.875*self.sigpeak_I + 0.125*self.sig_I[i]
350+
self.sigpeak_F = 0.875*self.sigpeak_F + 0.125*self.sig_F[i]
351+
else:
352+
# Signal peak, searchback criteria
353+
self.sigpeak_I = 0.75*self.sigpeak_I + 0.25*self.sig_I[i]
354+
self.sigpeak_F = 0.75*self.sigpeak_F + 0.25*self.sig_F[i]
355+
356+
return
308357

309358

359+
def backsearch(self):
360+
"""
361+
Search back for common 2 signal
362+
peaks and test for qrs using lower thresholds
363+
364+
"If the program does not find a QRS complex in
365+
the time interval corresponding to 166 percent
366+
of the current average RR interval, the maximal
367+
peak deteted in that time interval that lies
368+
between these two thresholds is considered to be
369+
a possilbe QRS complex, and the lower of the two
370+
thresholds is applied"
371+
372+
Interpreting the above 'the maximal peak':
373+
- A common peak in both sig_F and sig_I.
374+
- The largest sig_F peak.
375+
"""
376+
377+
# No common peaks since the last r
378+
if not self.recent_commonpeaks:
379+
return
380+
381+
recentpeaks_F = self.sig_F[self.recent_commonpeaks]
382+
383+
# Overall signal index to inspect
384+
maxpeak_ind = self.recent_commonpeaks[np.argmax(recentpeaks_F)]
385+
386+
# Test these peak values
387+
sigpeak_F = self.sig_F[maxpeak_ind]
388+
sigpeak_I = self.sig_I[maxpeak_ind]
389+
390+
# Thresholds passed for both signals. Found qrs.
391+
if (sigpeak_F > self.thresh_F/2) and (sigpeak_I > self.thresh_I/2):
392+
self.update_peak_params('ss', maxpeak_ind)
393+
394+
return
395+
396+
def istwave(self, i):
397+
"""
398+
Determine whether the coinciding peak index happens
399+
to be a t-wave instead of a qrs complex.
400+
401+
"If the maximal slope that occurs during this waveform
402+
is less than half that of the QRS waveform that preceded
403+
it, it is identified to be a T wave"
404+
405+
Compare slopes in filtered signal only
406+
"""
407+
408+
# Parameter: Checking width of a qrs complex
409+
# Parameter: Checking width of a t-wave
410+
411+
# Compute 5 point derivative
412+
a_deriv = [1]
413+
b_deriv = [1/4, 1/8, 0, -1/8, -1/4]
414+
sig_F_deriv = scisig.lfilter(b_deriv, a_deriv, self.sig_F)
415+
416+
# Square the derivative
417+
sig_F_deriv = np.square(sig_F_deriv)
310418

311419

312420

0 commit comments

Comments
 (0)