Skip to content

Commit f7fe48a

Browse files
committed
Add Resampling and normalization.
1 parent 3822d71 commit f7fe48a

File tree

5 files changed

+154
-1
lines changed

5 files changed

+154
-1
lines changed

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
'matplotlib>=1.5.1',
5858
'requests>=2.10.0',
5959
'pandas>=0.19.1',
60+
'scipy==0.19.0'
6061
],
6162

6263
# List additional groups of dependencies here (e.g. development

tests/test_processing.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import wfdb
2+
import numpy
3+
4+
class test_processing():
5+
6+
def test_1(self):
7+
sig, fields = wfdb.srdsamp('sampledata/100')
8+
ann = wfdb.rdann('sampledata/100', 'atr')
9+
10+
fs = fields['fs']
11+
fs_target = 50
12+
13+
new_sig, new_ann = wfdb.processing.resample_singlechan(sig[:, 0], ann, fs, fs_target)
14+
15+
expected_length = int(sig.shape[0]*fs_target/fs)
16+
17+
assert new_sig.shape[0] == expected_length
18+
19+
def test_2(self):
20+
sig, fields = wfdb.srdsamp('sampledata/100')
21+
ann = wfdb.rdann('sampledata/100', 'atr')
22+
23+
fs = fields['fs']
24+
fs_target = 50
25+
26+
new_sig, new_ann = wfdb.processing.resample_multichan(sig, ann, fs, fs_target)
27+
28+
expected_length = int(sig.shape[0]*fs_target/fs)
29+
30+
assert new_sig.shape[0] == expected_length
31+
assert new_sig.shape[1] == sig.shape[1]
32+
33+
def test_3(self):
34+
sig, _ = wfdb.srdsamp('sampledata/100')
35+
lb = -5
36+
ub = 15
37+
38+
x = wfdb.processing.normalize(sig[:, 0], lb, ub)
39+
assert x.shape[0] == sig.shape[0]
40+
assert numpy.min(x) >= lb
41+
assert numpy.max(x) <= ub

wfdb/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
from .readwrite._signals import estres, wrdatfile
33
from .readwrite.annotations import Annotation, rdann, wrann, showanncodes
44
from .readwrite.downloads import getdblist
5-
from .plot.plots import plotrec, plotann
5+
from .plot.plots import plotrec, plotann
6+
from . import processing

wfdb/processing/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .basic import resample_ann, resample_sig, resample_singlechan, resample_multichan, normalize

wfdb/processing/basic.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import numpy
2+
from scipy import signal
3+
4+
from wfdb import Annotation
5+
6+
7+
def resample_ann(tt, annsamp):
8+
# tt: numpy.array as returned by signal.resample
9+
# annsamp: numpy.array containing indexes of annotations (Annotation.annsamp)
10+
11+
# Compute the new annotation indexes
12+
13+
result = numpy.zeros(len(tt), dtype='bool')
14+
j = 0
15+
tprec = tt[j]
16+
for i, v in enumerate(annsamp):
17+
while True:
18+
d = False
19+
if j+1 == len(tt):
20+
result[j] = 1
21+
break
22+
tnow = tt[j+1]
23+
if tprec <= v and v <= tnow:
24+
if v-tprec < tnow-v:
25+
result[j] = 1
26+
else:
27+
result[j+1] = 1
28+
d = True
29+
j += 1
30+
tprec = tnow
31+
if d:
32+
break
33+
return numpy.where(result==1)[0].astype('int64')
34+
35+
36+
def resample_sig(x, fs, fs_target):
37+
# x: a numpy.array containing the signal
38+
# fs: the current frequency
39+
# fs_target: the target frequency
40+
41+
# Resample a signal
42+
43+
t = numpy.arange(x.shape[0]).astype('float64')
44+
45+
if fs == fs_target:
46+
return x, t
47+
48+
new_length = int(x.shape[0]*fs_target/fs)
49+
xx, tt = signal.resample(x, num=new_length, t=t)
50+
assert xx.shape == tt.shape and xx.shape[0] == new_length
51+
assert numpy.all(numpy.diff(tt) > 0)
52+
return xx, tt
53+
54+
55+
def resample_singlechan(x, ann, fs, fs_target):
56+
# x: a numpy.array containing the signal
57+
# ann: an Annotation object
58+
# fs: the current frequency
59+
# fs_target: the target frequency
60+
61+
# Resample a single-channel signal with its annotations
62+
63+
xx, tt = resample_sig(x, fs, fs_target)
64+
65+
new_annsamp = resample_ann(tt, ann.annsamp)
66+
assert ann.annsamp.shape == new_annsamp.shape
67+
68+
new_ann = Annotation(ann.recordname, ann.annotator, new_annsamp, ann.anntype, ann.num, ann.subtype, ann.chan, ann.aux, ann.fs)
69+
return xx, new_ann
70+
71+
72+
def resample_multichan(xs, ann, fs, fs_target, resamp_ann_chan=0):
73+
# xs: a numpy.ndarray containing the signals as returned by wfdb.srdsamp
74+
# ann: an Annotation object
75+
# fs: the current frequency
76+
# fs_target: the target frequency
77+
# resample_ann_channel: the signal channel that is used to compute new annotation indexes
78+
79+
# Resample multiple channels with their annotations
80+
81+
assert resamp_ann_chan < xs.shape[1]
82+
83+
lx = []
84+
lt = None
85+
for chan in range(xs.shape[1]):
86+
xx, tt = resample_sig(xs[:, chan], fs, fs_target)
87+
lx.append(xx)
88+
if chan == resamp_ann_chan:
89+
lt = tt
90+
91+
new_annsamp = resample_ann(lt, ann.annsamp)
92+
assert ann.annsamp.shape == new_annsamp.shape
93+
94+
new_ann = Annotation(ann.recordname, ann.annotator, new_annsamp, ann.anntype, ann.num, ann.subtype, ann.chan, ann.aux, ann.fs)
95+
return numpy.column_stack(lx), new_ann
96+
97+
98+
def normalize(x, lb=0, ub=1):
99+
# lb: Lower bound
100+
# ub: Upper bound
101+
102+
# Resizes a signal between the lower and upper bound
103+
104+
mid = ub - (ub - lb) / 2
105+
min_v = numpy.min(x)
106+
max_v = numpy.max(x)
107+
mid_v = max_v - (max_v - min_v) / 2
108+
coef = (ub - lb) / (max_v - min_v)
109+
return x * coef - (mid_v * coef) + mid

0 commit comments

Comments
 (0)