@@ -38,76 +38,84 @@ def detect_qrs_static(self):
38
38
self .alignsignals ()
39
39
40
40
41
- # Initialize parameters via the two learning phases
41
+ # Initialize learning parameters via the two learning phases
42
42
self .learnparams ()
43
43
44
- # Loop through every index and detect qrs locations
45
- for i in range (self .siglen ):
46
- pass
44
+ # Loop through every index and detect qrs locations.
45
+ # Start from 200ms after the first qrs detected in the
46
+ # learning phase
47
+ for i in range (self .qrs_inds [0 ]+ 41 , self .siglen ):
47
48
48
- # Go through each index and detect qrs complexes.
49
- for i in range (siglen ):
50
- # Determine whether the current index is a peak
51
- # for each signal
52
- is_peak_F = ispeak (sig_F , siglen , i , 20 )
53
- is_peak_I = ispeak (sig_I , siglen , i , 20 )
54
-
55
- # Whether the current index is a signal peak or noise peak
56
- is_sigpeak_F = False
57
- is_noisepeak_F = False
58
- is_sigpeak_I = False
59
- is_noisepeak_I = False
60
-
61
- # If peaks are detected, classify them as signal or noise
62
- # for their respective channels
63
-
64
- if is_peak_F :
65
- # Satisfied signal peak criteria for the channel
66
- # but not necessarily overall
67
- if sig_F [i ] > thresh_F :
68
- is_sigpeak_F = True
69
- # Did not satisfy signal peak criteria.
70
- # Label as noise peak
71
- else :
72
- is_noisepeak_F = True
73
-
74
- if is_peak_I :
75
- # The
76
- if sig_I [i ] > thresh_I :
77
- is_peak_sig_I = True
78
- else :
79
-
80
- # Check for double signal peak coincidence and at least >200ms (40 samples samples)
81
- # since the previous r peak
82
- is_sigpeak = is_sigpeak_F and is_sigpeak_I and ()
83
-
84
- # Found a signal peak!
85
- if is_sigpeak :
86
-
87
- # BUT WAIT, THERE'S MORE! It could be a T-Wave ...
49
+ # Number of indices from the previous r peak to this index
50
+ last_r_distance = i - qrs_inds [- 1 ]
51
+
52
+ # Determine whether the current index is a peak
53
+ # for each signal
54
+ is_peak_F = ispeak (sig_F , self .siglen , i , 20 )
55
+ is_peak_I = ispeak (sig_I , self .siglen , i , 20 )
88
56
89
- # When an rr interval < 360ms (72 samples), it is checked
90
- # to determine whether it is a T-Wave
91
- if i - prev_r_ind < 360 :
92
-
57
+ # Whether the current index is a signal peak or noise peak
58
+ is_sigpeak_F = False
59
+ is_sigpeak_I = False
60
+ is_noisepeak_F = False
61
+ is_noisepeak_I = False
93
62
94
- # Update running parameters
63
+ # If peaks are detected, classify them as signal or noise
64
+ # for their respective channels
95
65
96
- sigpeak_I = 0.875 * sigpeak_I + 0.125 * sig_I [i ]
97
- sigpeak_F = 0.875 * sigpeak_I + 0.125 * sig_I [i ]
66
+ if is_peak_F :
67
+ # Satisfied signal peak criteria for the channel
68
+ if sig_F [i ] > self .thresh_F :
69
+ is_sigpeak_F = True
70
+ # Did not satisfy signal peak criteria.
71
+ # Label as noise peak
72
+ else :
73
+ is_noisepeak_F = True
74
+
75
+ if is_peak_I :
76
+ # Satisfied signal peak criteria for the channel
77
+ if sig_I [i ] > self .thresh_I :
78
+ is_peak_sig_I = True
79
+ # Did not satisfy signal peak criteria.
80
+ # Label as noise peak
81
+ else :
82
+ is_noisepeak_I = True
98
83
99
- last_r_ind = i
100
- rr_limitavg
101
- rr_limitavg
84
+ # Check for double signal peak coincidence and at least >200ms (40 samples samples)
85
+ # since the previous r peak
86
+ is_sigpeak = is_sigpeak_F and is_sigpeak_I and (last_r_distance > 40 )
87
+
88
+ # The peak crosses thresholds for each channel and >200ms from previous peak
89
+ if is_sigpeak :
90
+
91
+ # If the rr interval < 360ms (72 samples), the peak is checked
92
+ # to determine whether it is a T-Wave. This is the final test
93
+ # to run before the peak can be marked as a qrs complex.
94
+ if last_r_distance < 72 :
95
+
96
+
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"
100
+
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
+
102
115
103
- # Not a signal peak. Update running parameters
104
- # if any other peaks are detected
105
- elif is_peak_F :
116
+ last_r_distance = i - last_r_ind
106
117
107
-
108
- last_r_distance = i - last_r_ind
109
-
110
- if last_r_distance >
118
+ if last_r_distance >
111
119
112
120
113
121
qrs = np .zeros (10 )
@@ -118,14 +126,15 @@ def detect_qrs_static(self):
118
126
qrs = qrs .astype ('int64' )
119
127
120
128
121
- return qrs
129
+ return
122
130
123
131
124
132
125
133
126
134
def resample (self ):
127
135
if self .fs != 200 :
128
136
self .sig = scisig .resample (self .sig , int (self .siglen * 200 / fs ))
137
+ return
129
138
130
139
# Bandpass filter the signal from 5-15Hz
131
140
def bandpass (self , plotsteps ):
@@ -144,6 +153,7 @@ def bandpass(self, plotsteps):
144
153
plt .plot (self .sig_F )
145
154
plt .legend (['After LP' , 'After LP+HP' ])
146
155
plt .show ()
156
+ return
147
157
148
158
# Compute the moving wave integration waveform from the filtered signal
149
159
def mwi (sig , plotsteps ):
@@ -166,17 +176,20 @@ def mwi(sig, plotsteps):
166
176
plt .plot (self .sig_I )
167
177
plt .legend (['deriv' , 'mwi' ])
168
178
plt .show ()
179
+ return
169
180
170
181
# Align the filtered and integrated signal with the original
171
182
def alignsignals (self ):
172
183
self .sig_F = self .sig_F
173
184
174
185
self .sig_I = self .sig_I
175
186
187
+ return
188
+
176
189
177
190
def learnparams (self ):
178
191
"""
179
- Initialize parameters using the start of the waveforms
192
+ Initialize detection parameters using the start of the waveforms
180
193
during the two learning phases described.
181
194
182
195
"Learning phase 1 requires about 2s to initialize
@@ -192,18 +205,19 @@ def learnparams(self):
192
205
This code is not detailed in the Pan-Tompkins
193
206
paper. The PT algorithm requires a threshold to
194
207
categorize peaks as signal or noise, but the
195
- threshold is calculated from noise and signal
196
- peaks. There is a circular dependency when
197
- none of the fields are initialized. Therefore this learning phase will detect signal peaks using a
198
- different method, and estimate the threshold using those peaks.
208
+ threshold is calculated from noise and signal
209
+ peaks. There is a circular dependency when
210
+ none of the fields are initialized. Therefore this
211
+ learning phase will detect initial signal peaks using a
212
+ different method, and estimate the threshold using
213
+ those peaks.
199
214
200
215
This function works as follows:
201
216
- Try to find at least 2 signal peaks (qrs complexes) in the
202
- first N seconds of both signals using simple low order
217
+ first 2 seconds of both signals using simple low order
203
218
moments. Signal peaks are only defined when the same index is
204
- determined to be a peak in both signals. Start with N==2.
205
- If fewer than 2 signal peaks are detected, increment N and
206
- try again.
219
+ determined to be a peak in both signals. If fewer than 2 signal
220
+ peaks are detected, shift to the next 2 window and try again.
207
221
- Using the classified estimated peaks, threshold1 is estimated as
208
222
based on the steady state estimate equation: thres = 0.75*noisepeak + 0.25*sigpeak
209
223
using the mean of the noisepeaks and signalpeaks instead of the
@@ -216,10 +230,12 @@ def learnparams(self):
216
230
radius = 20
217
231
# The signal start duration to use for learning
218
232
learntime = 2
219
-
220
- while :
221
- wavelearn_F = filtsig [:200 * learntime ]
222
- wavelearn_I = mwi [:200 * learntime ]
233
+ # The window number to inspect in the signal
234
+ windownum = 0
235
+
236
+ while (windownum + 1 )* learntime * 200 < self .siglen :
237
+ wavelearn_F = self .sig_F [windownum * learntime * 200 :(windownum + 1 )* learntime * 200 ]
238
+ wavelearn_I = self .sig_I [windownum * learntime * 200 :(windownum + 1 )* learntime * 200 ]
223
239
224
240
# Find peaks in the signals
225
241
peakinds_F = findpeaks_radius (wavelearn_F , radius )
@@ -242,43 +258,51 @@ def learnparams(self):
242
258
noisepeakinds_F = np .setdiff1d (peakinds_F , sigpeakinds )
243
259
noisepeakinds_I = np .setdiff1d (peakinds_I , sigpeakinds )
244
260
245
- if len (sigpeakinds )> 1 :
261
+ # Found at least 2 peaks. Also peak 1 and 2 must be >200ms apart
262
+ if len (sigpeakinds )> 1 and sigpeakinds [1 ]- sigpeakinds [0 ]> 40 :
246
263
break
247
264
248
- # Need to detect at least 2 peaks
249
- learntime = learntime + 1
265
+ # Need to detect at least 2 peaks. Check the next window.
266
+ windownum = windownum + 1
250
267
251
268
# Found at least 2 peaks. Use them to set parameters.
252
-
269
+
253
270
# Set running peak estimates to first values
254
- sigpeak_F = wavelearn_F [sigpeakinds [0 ]]
255
- sigpeak_I = wavelearn_I [sigpeakinds [0 ]]
256
- noisepeak_F = wavelearn_F [noisepeakinds_F [0 ]]
257
- noisepeak_I = wavelearn_I [noisepeakinds_I [0 ]]
271
+ self . sigpeak_F = wavelearn_F [sigpeakinds [0 ]]
272
+ self . sigpeak_I = wavelearn_I [sigpeakinds [0 ]]
273
+ self . noisepeak_F = wavelearn_F [noisepeakinds_F [0 ]]
274
+ self . noisepeak_I = wavelearn_I [noisepeakinds_I [0 ]]
258
275
259
276
# Use all signal and noise peaks in learning window to estimate threshold
260
277
# Based on steady state equation: thres = 0.75*noisepeak + 0.25*sigpeak
261
- thres_F = 0.75 * np .mean (wavelearn_F [noisepeakinds_F ]) + 0.25 * np .mean (wavelearn_F [sigpeakinds_F ])
262
- thres_I = 0.75 * np .mean (wavelearn_I [noisepeakinds_I ]) + 0.25 * np .mean (wavelearn_I [sigpeakinds_I ])
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 ])
263
280
# Alternatively, could skip all of that and do something very simple like thresh_F = max(filtsig[:400])/3
264
281
265
282
# Set the r-r history using the first r-r interval
266
283
# The most recent 8 rr intervals
267
- rr_history_unbound = [wavelearn_F [sigpeakinds [1 ]]- wavelearn_F [sigpeakinds [0 ]]]* 8
284
+ self . rr_history_unbound = [wavelearn_F [sigpeakinds [1 ]]- wavelearn_F [sigpeakinds [0 ]]]* 8
268
285
# The most recent 8 rr intervals that fall within the acceptable low and high rr interval limits
269
- rr_history_bound = [wavelearn_I [sigpeakinds [1 ]]- wavelearn_I [sigpeakinds [0 ]]]* 8
286
+ self . rr_history_bound = [wavelearn_I [sigpeakinds [1 ]]- wavelearn_I [sigpeakinds [0 ]]]* 8
270
287
271
- rr_average_unbound = np .mean (rr_history_unbound )
272
- rr_average_bound = np .mean (rr_history_bound )
288
+ self . rr_average_unbound = np .mean (self . rr_history_unbound )
289
+ self . rr_average_bound = np .mean (self . rr_history_bound )
273
290
274
- # Wait... what is rr_average_unbound for then ?
275
- rr_low_limit = 0.92 * rr_average_bound
276
- rr_high_limit = 1.16 * rr_average_bound
277
- rr_missed_limit = 1.66 * rr_average_bound
291
+ # what is rr_average_unbound ever used for ?
292
+ self . rr_low_limit = 0.92 * rr_average_bound
293
+ self . rr_high_limit = 1.16 * rr_average_bound
294
+ self . rr_missed_limit = 1.66 * rr_average_bound
278
295
279
- return thresh_F , thresh_I , sigpeak_F , sigpeak_I ,
280
- noisepeak_F , noisepeak_I , rr_freeavg_F ,
281
- r_freeavg_I , rr_limitavg_F , rr_limitavg_I
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
+
302
+ # The qrs indices detected
303
+ self .qrs_inds = [sigpeakinds [0 ]]
304
+
305
+ return
282
306
283
307
284
308
0 commit comments