Skip to content

Commit 1915402

Browse files
committed
work on cqrs
1 parent 5a3af94 commit 1915402

File tree

1 file changed

+111
-28
lines changed

1 file changed

+111
-28
lines changed

wfdb/processing/cqrs.py

Lines changed: 111 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,46 @@
99

1010
class Conf(object):
1111
"""
12-
Initial configuration object
12+
Initial signal configuration object
1313
"""
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):
1516
"""
1617
Parameters
1718
----------
1819
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.
2222
hr_max : int or float, optional
2323
Hard maximum heart rate between two beats, in beats per minute
2424
hr_min : int or float, optional
2525
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?
2636
"""
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")
2742

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
2947
self.hr_max = hr_max
3048
self.hr_min = hr_min
3149
self.qrs_width = qrs_width
50+
self.qrs_thr_init = qrs_thr_init
51+
self.qrs_thr_min = qrs_thr_min
3252

3353

3454
class CQRS(object):
@@ -40,23 +60,48 @@ class CQRS(object):
4060
- Inverted qrs
4161
4262
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.
4379
"""
4480

4581

4682
def __init__(self, sig, fs, conf=Conf()):
4783
self.sig = sig
4884
self.fs = fs
49-
5085
self.sig_len = len(sig)
5186

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
5687

57-
self.qrs_width = conf.qrs_width * fs
5888

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
59100

101+
self.qrs_thr_init = self.conf.qrs_thr_init
102+
self.qrs_thr_min = self.conf.qrs_thr_min
103+
104+
60105

61106

62107
def bandpass(self, fc_low=5, fc_high=20):
@@ -71,7 +116,6 @@ def bandpass(self, fc_low=5, fc_high=20):
71116
self.filter_gain = get_filter_gain(b, a, np.mean(fc_low, fc_high), fs)
72117

73118

74-
75119
def mwi(self):
76120
"""
77121
Apply moving wave integration with a ricker (Mexican hat) wavelet onto
@@ -85,10 +129,14 @@ def mwi(self):
85129
# Save the mwi gain
86130
self.mwi_gain = get_filter_gain(b, [1], np.mean(fc_low, fc_high), fs)
87131

88-
def learn_params(self, n_calib_beats=8):
132+
133+
def learn_init_params(self, n_calib_beats=8):
89134
"""
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.
92140
93141
Parameters
94142
----------
@@ -105,38 +153,69 @@ def learn_params(self, n_calib_beats=8):
105153
int(self.qrs_width / 2))
106154
#dominant_peaks = self.sig_i[self.dominant_peak_inds]
107155

108-
# Find n qrs peaks and store their amplitudes
156+
# Go through the dominant peaks and find the qrs peaks
109157
qrs_peak_amps = []
110158

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+
115162

163+
if len(qrs_peak_amps) == n_calib_beats:
164+
break
116165

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)
117169

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+
119176

120177

121178

122179
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 =
123186

124187

125188

126-
def detect(self, sampfrom=0, sampto='end'):
189+
def detect(self, sampfrom=0, sampto='end', learn=True):
127190
"""
128191
Detect qrs locations between two samples
129192
"""
193+
if sampfrom < 0:
194+
raise ValueError("'sampfrom' cannot be negative")
130195
if sampto == 'end':
131196
sampto = self.sig_len
197+
elif sampto > self.sig_len:
198+
raise ValueError("'sampto' cannot exceed the signal length")
132199

200+
# Detected qrs indices
133201
self.qrs_inds = []
202+
# qrs indices found via backsearch
134203
self.backsearch_qrs_inds = []
135204

205+
# Get signal configuration fields from Conf object
206+
self.set_conf()
207+
# Bandpass filter the signal
136208
self.bandpass()
209+
# Compute moving wave integration of filtered signal
137210
self.mwi()
138211

139-
self.learn_params()
212+
# Learn parameters
213+
if learn:
214+
self.learn_initial_params()
215+
else:
216+
self.set_initial_params()
217+
218+
140219

141220

142221

@@ -158,9 +237,13 @@ def detect(self, sampfrom=0, sampto='end'):
158237

159238
def find_dominant_peaks(sig, radius):
160239
"""
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.
164247
"""
165248
peak_inds = []
166249

@@ -205,7 +288,7 @@ def get_filter_gain(b, a, f_gain, fs):
205288

206289

207290

208-
def cqrs_detect(sig, fs, conf=Conf()):
291+
def cqrs_detect(sig, fs, conf=Conf(), learn=True):
209292

210293
cqrs = CQRS(sig, fs, conf)
211294

0 commit comments

Comments
 (0)