9
9
10
10
class Conf (object ):
11
11
"""
12
- Initial configuration object
12
+ Initial signal configuration object
13
13
"""
14
- def __init__ (self , hr = 75 , hr_max = 200 , hr_min = 25 , qrs_width = 0.1 ):
14
+ def __init__ (self , hr_init = 75 , hr_max = 200 , hr_min = 25 , qrs_width = 0.1 ,
15
+ qrs_thr_init = 0.13 , qrs_thr_min = 0 ):
15
16
"""
16
17
Parameters
17
18
----------
18
19
hr : int or float, optional
19
- Heart rate in beats per minute
20
- qrs_width : int or float, optional
21
- Expected qrs width in seconds
20
+ Heart rate in beats per minute. Wait... what is this for?
21
+ Setting initial hr and rr values.
22
22
hr_max : int or float, optional
23
23
Hard maximum heart rate between two beats, in beats per minute
24
24
hr_min : int or float, optional
25
25
Hard minimum heart rate between two beats, in beats per minute
26
+ qrs_width : int or float, optional
27
+ Expected qrs width in seconds
28
+ qrs_thr_init : int or float, optional
29
+ Initial qrs detection threshold. If learning=True and beats are
30
+ detected, this will be overwritten.
31
+ qrs_thr_min : int or float or string, optional
32
+ Hard minimum detection threshold of qrs wave. Leave as 0 for no
33
+ minimum.
34
+
35
+ Should RT values be specified?
26
36
"""
37
+ if hr_min < 0 :
38
+ raise ValueError ("'hr_min' must be <= 0" )
39
+
40
+ if not hr_min < hr_init < hr_max :
41
+ raise ValueError ("'hr_min' < 'hr_init' < 'hr_max' must be True" )
27
42
28
- self .hr = hr
43
+ if qrs_thr_init < qrs_thr_min :
44
+ raise ValueError ("qrs_thr_min must be <= qrs_thr_init" )
45
+
46
+ self .hr_init = hr_init
29
47
self .hr_max = hr_max
30
48
self .hr_min = hr_min
31
49
self .qrs_width = qrs_width
50
+ self .qrs_thr_init = qrs_thr_init
51
+ self .qrs_thr_min = qrs_thr_min
32
52
33
53
34
54
class CQRS (object ):
@@ -40,23 +60,48 @@ class CQRS(object):
40
60
- Inverted qrs
41
61
42
62
Record 117 is a good challenge. Big twave, weird qrs
63
+
64
+
65
+ Process:
66
+ - Bandpass
67
+ - MWI
68
+ - Learning:
69
+ - Find prominent peaks (using qrs radius)
70
+ - Find N beats. Use ricker wavelet convoluted with filtered signal.
71
+ - Use beats to initialize qrs threshold. Other peak threshold is
72
+ set to half of that. Set min peak and qrs threshold? There is a problem
73
+ with setting this hard limit. gqrs has a hard
74
+ limit, but pantompkins doesn't. Solution: Give options
75
+ 1. Specify limit. Can set to 0 if don't want.
76
+ 2. Set limit from peaks detected while learning. Else 0.
77
+ During learning beat detection, confirm the set is roughly the same size.
78
+ Reject 'beats' detected that are too far off in amplitude.
43
79
"""
44
80
45
81
46
82
def __init__ (self , sig , fs , conf = Conf ()):
47
83
self .sig = sig
48
84
self .fs = fs
49
-
50
85
self .sig_len = len (sig )
51
86
52
- # These values are in samples
53
- self .rr = 60 * self .fs / conf .hr
54
- self .rr_max = 60 * self .fs / conf .hr_min
55
- self .rr_min = 60 * self .fs / conf .hr_max
56
87
57
- self .qrs_width = conf .qrs_width * fs
58
88
89
+ def set_conf (self ):
90
+ """
91
+ Set configuration parameters from the Conf object into the CQRS detector
92
+ object.
93
+
94
+ Time values are in samples, amplitude values are in mV.
95
+ """
96
+ self .rr = 60 * self .fs / self .conf .hr
97
+ self .rr_max = 60 * self .fs / self .conf .hr_min
98
+ self .rr_min = 60 * self .fs / self .conf .hr_max
99
+ self .qrs_width = self .conf .qrs_width * self .fs
59
100
101
+ self .qrs_thr_init = self .conf .qrs_thr_init
102
+ self .qrs_thr_min = self .conf .qrs_thr_min
103
+
104
+
60
105
61
106
62
107
def bandpass (self , fc_low = 5 , fc_high = 20 ):
@@ -71,7 +116,6 @@ def bandpass(self, fc_low=5, fc_high=20):
71
116
self .filter_gain = get_filter_gain (b , a , np .mean (fc_low , fc_high ), fs )
72
117
73
118
74
-
75
119
def mwi (self ):
76
120
"""
77
121
Apply moving wave integration with a ricker (Mexican hat) wavelet onto
@@ -85,10 +129,14 @@ def mwi(self):
85
129
# Save the mwi gain
86
130
self .mwi_gain = get_filter_gain (b , [1 ], np .mean (fc_low , fc_high ), fs )
87
131
88
- def learn_params (self , n_calib_beats = 8 ):
132
+
133
+ def learn_init_params (self , n_calib_beats = 8 ):
89
134
"""
90
- Find a number of beats using cross correlation to determine qrs
91
- detection thresholds.
135
+ Find a number of consecutive beats using cross correlation to determine
136
+ qrs detection thresholds.
137
+
138
+ If the system fails to find enough beats, the default parameters will be
139
+ used instead.
92
140
93
141
Parameters
94
142
----------
@@ -105,38 +153,69 @@ def learn_params(self, n_calib_beats=8):
105
153
int (self .qrs_width / 2 ))
106
154
#dominant_peaks = self.sig_i[self.dominant_peak_inds]
107
155
108
- # Find n qrs peaks and store their amplitudes
156
+ # Go through the dominant peaks and find the qrs peaks
109
157
qrs_peak_amps = []
110
158
111
- peak_num = 0
112
- while len (qrs_peak_amps ) < n_calib_beats
113
- i = dominant_peak_inds [peak_num ]
114
- peak_num += 1
159
+ for peak_num in range (len (self .dominant_peak_inds )):
160
+ i = self .dominant_peak_inds [peak_num ]
161
+
115
162
163
+ if len (qrs_peak_amps ) == n_calib_beats :
164
+ break
116
165
166
+
167
+ if len (qrs_peak_amps ) == n_calib_beats :
168
+ self .qrs_thr = max (np .mean (qrs_peak_amps ) / 2 , self .qrs_thr_min )
117
169
118
- self .qrs_thresh = np .mean (qrs_peak_amps )
170
+ # Failed to find enough calibration beats. Use default values.
171
+ else :
172
+ self .set_init_params
173
+
174
+
175
+
119
176
120
177
121
178
122
179
return
180
+
181
+ def set_init_params (self ):
182
+ self .qrs_thr = self .qrs_thr_init
183
+ self .recent_qrs_inds =
184
+
185
+ self .recent_rr =
123
186
124
187
125
188
126
- def detect (self , sampfrom = 0 , sampto = 'end' ):
189
+ def detect (self , sampfrom = 0 , sampto = 'end' , learn = True ):
127
190
"""
128
191
Detect qrs locations between two samples
129
192
"""
193
+ if sampfrom < 0 :
194
+ raise ValueError ("'sampfrom' cannot be negative" )
130
195
if sampto == 'end' :
131
196
sampto = self .sig_len
197
+ elif sampto > self .sig_len :
198
+ raise ValueError ("'sampto' cannot exceed the signal length" )
132
199
200
+ # Detected qrs indices
133
201
self .qrs_inds = []
202
+ # qrs indices found via backsearch
134
203
self .backsearch_qrs_inds = []
135
204
205
+ # Get signal configuration fields from Conf object
206
+ self .set_conf ()
207
+ # Bandpass filter the signal
136
208
self .bandpass ()
209
+ # Compute moving wave integration of filtered signal
137
210
self .mwi ()
138
211
139
- self .learn_params ()
212
+ # Learn parameters
213
+ if learn :
214
+ self .learn_initial_params ()
215
+ else :
216
+ self .set_initial_params ()
217
+
218
+
140
219
141
220
142
221
@@ -158,9 +237,13 @@ def detect(self, sampfrom=0, sampto='end'):
158
237
159
238
def find_dominant_peaks (sig , radius ):
160
239
"""
161
- Find all dominant peaks in a signal.
162
- A sample is a dominant peak if it is the largest value within the
163
- <radius> samples on its left and right.
240
+ Find all dominant peaks in a signal. A sample is a dominant peak if it is
241
+ the largest value within the <radius> samples on its left and right.
242
+
243
+ In cases where it shares the max value with nearby samples, the earliest
244
+ sample is classified as the dominant peak.
245
+
246
+ TODO: Fix flat mountain scenarios.
164
247
"""
165
248
peak_inds = []
166
249
@@ -205,7 +288,7 @@ def get_filter_gain(b, a, f_gain, fs):
205
288
206
289
207
290
208
- def cqrs_detect (sig , fs , conf = Conf ()):
291
+ def cqrs_detect (sig , fs , conf = Conf (), learn = True ):
209
292
210
293
cqrs = CQRS (sig , fs , conf )
211
294
0 commit comments