Skip to content

Commit 3e3c945

Browse files
committed
Add peak corrector.
1 parent 70a1a6b commit 3e3c945

File tree

4 files changed

+132
-1
lines changed

4 files changed

+132
-1
lines changed

tests/test_processing.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,21 @@ def test_6(self):
6262
print([a.time for a in annotations])
6363
print(expecting)
6464
assert [a.time for a in annotations] == expecting
65+
66+
def test_7(self):
67+
sig, fields = wfdb.srdsamp('sampledata/100', channels = [0, 1])
68+
ann = wfdb.rdann('sampledata/100', 'atr')
69+
fs = fields['fs']
70+
trained_fs = 360
71+
min_bpm = 10
72+
max_bpm = 350
73+
min_gap = fs*60/min_bpm
74+
max_gap = fs*60/max_bpm
75+
76+
y_idxs = wfdb.processing.correct_peaks(sig[:,0], ann.annsamp, min_gap, max_gap, smooth_window=150)
77+
78+
yz = numpy.zeros(sig.shape[0])
79+
yz[y_idxs] = 1
80+
yz = numpy.where(yz[:10000]==1)[0]
81+
82+
assert numpy.array_equal(yz, [77, 370, 663, 947, 1231, 1515, 1809, 2045, 2403, 2706, 2998, 3283, 3560, 3863, 4171, 4466, 4765, 5061, 5347, 5634, 5919, 6215, 6527, 6824, 7106, 7393, 7670, 7953, 8246, 8539, 8837, 9142, 9432, 9710, 9998])

wfdb/processing/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .basic import resample_ann, resample_sig, resample_singlechan, resample_multichan, normalize
22
from .gqrs import gqrs_detect
3-
from .peaks import find_peaks
3+
from .peaks import find_peaks, correct_peaks

wfdb/processing/basic.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,8 @@ def normalize(x, lb=0, ub=1):
107107
mid_v = max_v - (max_v - min_v) / 2
108108
coef = (ub - lb) / (max_v - min_v)
109109
return x * coef - (mid_v * coef) + mid
110+
111+
112+
def smooth(x, window_size):
113+
box = numpy.ones(window_size)/window_size
114+
return numpy.convolve(x, box, mode='same')

wfdb/processing/peaks.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import copy
22
import numpy
33
from .gqrs import time_to_sample_number, Conf, Peak, Annotation
4+
from .basic import smooth
45

56
def find_peaks(x):
67
# Definitions:
@@ -37,3 +38,110 @@ def find_peaks(x):
3738
i += 1
3839
soft_peaks = numpy.asarray(soft_peaks)+1
3940
return hard_peaks, soft_peaks
41+
42+
def find_peaks(x):
43+
# Definitions:
44+
# * Hard peak: a peak that is either /\ or \/
45+
# * Soft peak: a peak that is either /-*\ or \-*/ (In that cas we define the middle of it as the peak)
46+
47+
# Returns two numpy arrays:
48+
# * hard_peaks contains the indexes of the Hard peaks
49+
# * soft_peaks contains the indexes of the Soft peaks
50+
51+
if len(x) == 0:
52+
return numpy.empty([0]), numpy.empty([0])
53+
54+
tmp = x[1:]
55+
tmp = numpy.append(tmp, [x[-1]])
56+
tmp = x-tmp
57+
tmp[numpy.where(tmp>0)] = +1
58+
tmp[numpy.where(tmp==0)] = 0
59+
tmp[numpy.where(tmp<0)] = -1
60+
tmp2 = tmp[1:]
61+
tmp2 = numpy.append(tmp2, [0])
62+
tmp = tmp-tmp2
63+
hard_peaks = numpy.where(numpy.logical_or(tmp==-2,tmp==+2))[0]+1
64+
soft_peaks = []
65+
for iv in numpy.where(numpy.logical_or(tmp==-1,tmp==+1))[0]:
66+
t = tmp[iv]
67+
i = iv+1
68+
while True:
69+
if i==len(tmp) or tmp[i] == -t or tmp[i] == -2 or tmp[i] == 2:
70+
break
71+
if tmp[i] == t:
72+
soft_peaks.append(int(iv+(i-iv)/2))
73+
break
74+
i += 1
75+
soft_peaks = numpy.asarray(soft_peaks)+1
76+
return hard_peaks, soft_peaks
77+
78+
79+
def correct_peaks(signal, peaks_indexes, min_gap, max_gap, smooth_window):
80+
N = signal.shape[0]
81+
82+
rpeaks = numpy.zeros(N)
83+
rpeaks[peaks_indexes] = 1.0
84+
85+
# Multiple ones '1' side by side:
86+
# in order to prevent that, the following code computes the best peak
87+
rpeaks = rpeaks.astype('int32')
88+
89+
# 1- Extract ranges where we have one or many ones side by side (rpeaks locations predicted by NN)
90+
rpeaks_ranges = []
91+
tmp_idx = 0
92+
for i in range(1, len(rpeaks)):
93+
if rpeaks[i-1] == 1:
94+
if rpeaks[i] == 0:
95+
rpeaks_ranges.append((tmp_idx, i-1))
96+
else:
97+
if rpeaks[i] == 1:
98+
tmp_idx = i
99+
100+
smoothed = smooth(signal, smooth_window)
101+
102+
# Compute signal's peaks
103+
hard_peaks, soft_peaks = find_peaks(x=signal)
104+
all_peak_idxs = numpy.concatenate((hard_peaks, soft_peaks)).astype('int64')
105+
106+
# Replace each range of ones by the index of the best value in it
107+
tmp = set()
108+
for rp_range in rpeaks_ranges:
109+
r = numpy.arange(rp_range[0], rp_range[1]+1, dtype='int64')
110+
vals = signal[r]
111+
smoothed_vals = smoothed[r]
112+
p = r[numpy.argmax(numpy.absolute(numpy.asarray(vals)-smoothed_vals))]
113+
tmp.add(p)
114+
115+
# Replace all peaks by the peak within x-max_gap < x < x+max_gap which have the bigget distance from smooth curve
116+
dist = numpy.absolute(signal-smoothed) # Peak distance from the smoothed mean
117+
rpeaks_indexes = set()
118+
for p in tmp:
119+
a = max(0, p-max_gap)
120+
b = min(N, p+max_gap)
121+
r = numpy.arange(a, b, dtype='int64')
122+
idx_best = r[numpy.argmax(dist[r])]
123+
rpeaks_indexes.add(idx_best)
124+
125+
rpeaks_indexes = list(rpeaks_indexes)
126+
127+
# Prevent multiple peaks to appear in the max bpm range (max_gap)
128+
# If we found more than one peak in this interval, then we choose the peak with the maximum amplitude compared to the mean of the signal
129+
tmp = numpy.asarray(rpeaks_indexes)
130+
to_remove = {}
131+
for idx in rpeaks_indexes:
132+
if idx in to_remove:
133+
continue
134+
r = tmp[numpy.where(numpy.absolute(tmp-idx)<=max_gap)[0]]
135+
if len(r) == 1:
136+
continue
137+
rr = r.astype('int64')
138+
vals = signal[rr]
139+
smoo = smoothed[rr]
140+
the_one = r[numpy.argmax(numpy.absolute(vals-smoo))]
141+
for i in r:
142+
if i != the_one:
143+
to_remove[i] = True
144+
for v, _ in to_remove.items():
145+
rpeaks_indexes.remove(v)
146+
147+
return rpeaks_indexes

0 commit comments

Comments
 (0)