@@ -10,7 +10,7 @@ class PanTompkins(object):
10
10
Works on static signals. In future update,
11
11
will work on streaming ecg signal.
12
12
"""
13
- def __init__ (self , sig = None , fs = None , streamsig = None ):
13
+ def __init__ (self , sig = None , fs = None , streamsig = None , ):
14
14
self .sig = sig
15
15
self .fs = fs
16
16
@@ -19,6 +19,12 @@ def __init__(self, sig=None, fs=None, streamsig=None):
19
19
if sig is not None :
20
20
self .siglen = len (sig )
21
21
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
+
22
28
def detect_qrs_static (self ):
23
29
"""
24
30
Detect all the qrs locations in the static
@@ -49,16 +55,29 @@ def detect_qrs_static(self):
49
55
# Number of indices from the previous r peak to this index
50
56
last_r_distance = i - qrs_inds [- 1 ]
51
57
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
+
52
68
# Determine whether the current index is a peak
53
69
# for each signal
54
70
is_peak_F = ispeak (sig_F , self .siglen , i , 20 )
55
71
is_peak_I = ispeak (sig_I , self .siglen , i , 20 )
56
72
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
+
57
78
# Whether the current index is a signal peak or noise peak
58
79
is_sigpeak_F = False
59
80
is_sigpeak_I = False
60
- is_noisepeak_F = False
61
- is_noisepeak_I = False
62
81
63
82
# If peaks are detected, classify them as signal or noise
64
83
# for their respective channels
@@ -68,57 +87,50 @@ def detect_qrs_static(self):
68
87
if sig_F [i ] > self .thresh_F :
69
88
is_sigpeak_F = True
70
89
# Did not satisfy signal peak criteria.
71
- # Label as noise peak
90
+ # Classify as noise peak
72
91
else :
73
- is_noisepeak_F = True
92
+ self . update_peak_params ( 'nF' , i )
74
93
75
94
if is_peak_I :
76
95
# Satisfied signal peak criteria for the channel
77
96
if sig_I [i ] > self .thresh_I :
78
- is_peak_sig_I = True
97
+ is_sigpeak_I = True
79
98
# Did not satisfy signal peak criteria.
80
- # Label as noise peak
99
+ # Classify as noise peak
81
100
else :
82
- is_noisepeak_I = True
101
+ self . update_peak_params ( 'nI' , i )
83
102
84
103
# Check for double signal peak coincidence and at least >200ms (40 samples samples)
85
104
# since the previous r peak
86
105
is_sigpeak = is_sigpeak_F and is_sigpeak_I and (last_r_distance > 40 )
87
106
88
107
# The peak crosses thresholds for each channel and >200ms from previous peak
89
108
if is_sigpeak :
90
-
91
109
# If the rr interval < 360ms (72 samples), the peak is checked
92
110
# to determine whether it is a T-Wave. This is the final test
93
111
# to run before the peak can be marked as a qrs complex.
94
112
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
96
125
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 )
100
133
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 )
122
134
123
135
# Convert the peak indices back to the original fs if necessary
124
136
if fs != 200 :
@@ -218,7 +230,7 @@ def learnparams(self):
218
230
moments. Signal peaks are only defined when the same index is
219
231
determined to be a peak in both signals. If fewer than 2 signal
220
232
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
222
234
based on the steady state estimate equation: thres = 0.75*noisepeak + 0.25*sigpeak
223
235
using the mean of the noisepeaks and signalpeaks instead of the
224
236
running value.
@@ -262,10 +274,10 @@ def learnparams(self):
262
274
if len (sigpeakinds )> 1 and sigpeakinds [1 ]- sigpeakinds [0 ]> 40 :
263
275
break
264
276
265
- # Need to detect at least 2 peaks. Check the next window.
277
+ # Didn't find 2 satisfactory peaks. Check the next window.
266
278
windownum = windownum + 1
267
279
268
- # Found at least 2 peaks. Use them to set parameters.
280
+ # Found at least 2 satisfactory peaks. Use them to set parameters.
269
281
270
282
# Set running peak estimates to first values
271
283
self .sigpeak_F = wavelearn_F [sigpeakinds [0 ]]
@@ -275,8 +287,8 @@ def learnparams(self):
275
287
276
288
# Use all signal and noise peaks in learning window to estimate threshold
277
289
# 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 ])
280
292
# Alternatively, could skip all of that and do something very simple like thresh_F = max(filtsig[:400])/3
281
293
282
294
# Set the r-r history using the first r-r interval
@@ -292,21 +304,117 @@ def learnparams(self):
292
304
self .rr_low_limit = 0.92 * rr_average_bound
293
305
self .rr_high_limit = 1.16 * rr_average_bound
294
306
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
301
307
302
- # The qrs indices detected
308
+ # The qrs indices detected.
309
+ # Initialize with the first signal peak
310
+ # detected during this learning phase
303
311
self .qrs_inds = [sigpeakinds [0 ]]
304
312
305
313
return
306
314
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 )
307
346
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
308
357
309
358
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 )
310
418
311
419
312
420
0 commit comments