Skip to content

Commit d96c1a5

Browse files
committed
Switched filters and interpolation ops to use peakdet routines.
1 parent b41c04e commit d96c1a5

File tree

1 file changed

+62
-83
lines changed

1 file changed

+62
-83
lines changed

physioqc/metrics/chest_belt.py

Lines changed: 62 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,86 +3,69 @@
33
import matplotlib.pyplot as plt
44
import numpy as np
55
from matplotlib.patches import Polygon
6-
from peakdet import operations
6+
from peakdet import Physio, operations
77
from scipy.signal import resample, welch
88
from scipy.stats import pearsonr
99

1010
from .. import references
1111
from ..due import due
12-
from .utils import butterbpfiltfilt, butterlpfiltfilt, hamming, physio_or_numpy
12+
from .utils import hamming, physio_or_numpy
1313

1414

15-
def respirationprefilter(
16-
rawresp, Fs, lowerpass=0.01, upperpass=2.0, order=1, debug=False
17-
):
15+
def envelopedetect(data, upperpass=0.05, order=5):
1816
"""
19-
Apply a bandpass prefilter to the respiration signal
20-
Parameters
21-
----------
22-
rawresp: ndarray
23-
The raw respiration signal
24-
Fs: float
25-
The sampling frequency of the respiration signal in Hz
26-
lowerpass: float
27-
Lower pass cutoff frequency in Hz
28-
upperpass: float
29-
Upper pass cutoff frequency in Hz
30-
order: int
31-
Butterworth filter order
32-
debug: bool
33-
Print debug information and plot intermediate results
3417
35-
Returns
36-
-------
37-
filteredresp: ndarray
38-
39-
"""
40-
if debug:
41-
print(
42-
f"respirationprefilter: Fs={Fs} order={order}, lowerpass={lowerpass}, upperpass={upperpass}"
43-
)
44-
return butterbpfiltfilt(Fs, lowerpass, upperpass, rawresp, order, debug=debug)
45-
# return operations.filter_physio(rawresp, cutoffs=[lowerpass, upperpass], method='bandpass')
46-
47-
48-
def respenvelopefilter(squarevals, Fs, upperpass=0.05, order=8, debug=False):
49-
"""
50-
Apply a lowepass filter to the respiration envelope signal to get the RMS amplitude
5118
Parameters
5219
----------
53-
squarevals: ndarray
54-
The square of the either the positive or negative portions of the respriation signal
55-
Fs: float
56-
The sampling frequency of the respiration signal in Hz
20+
data
5721
upperpass: float
5822
Upper pass cutoff frequency in Hz
5923
order: int
6024
Butterworth filter order
61-
debug: bool
62-
Print debug information
63-
6425
Returns
6526
-------
66-
filteredenv: ndarray
67-
The filtered respiration envelope signal
27+
einferior: ndarray
28+
The lower edge of the signal envelope
29+
esuperior: ndarray
30+
The upper edge of the signal envelope
6831
6932
"""
70-
if debug:
71-
print(f"respenvelopefilter: Fs={Fs} order={order}, upperpass={upperpass}")
72-
return butterlpfiltfilt(Fs, upperpass, squarevals, order, debug=debug)
33+
npdata = data.data
34+
targetfs = data.fs
35+
esuperior = (
36+
2.0
37+
* operations.filter_physio(
38+
Physio(np.square(np.where(npdata > 0.0, npdata, 0.0)), fs=targetfs),
39+
[upperpass],
40+
"lowpass",
41+
order=order,
42+
).data
43+
)
44+
esuperior = np.sqrt(np.where(esuperior > 0.0, esuperior, 0.0))
45+
einferior = (
46+
2.0
47+
* operations.filter_physio(
48+
Physio(np.square(np.where(npdata < 0.0, npdata, 0.0)), fs=targetfs),
49+
[upperpass],
50+
"lowpass",
51+
order=order,
52+
).data
53+
)
54+
einferior = -np.sqrt(np.where(einferior > 0.0, einferior, 0.0))
55+
return einferior, esuperior
7356

7457

7558
@due.dcite(references.ROMANO_2023)
7659
def respiratorysqi(
77-
rawresp, Fs, envelopelpffreq=0.05, slidingfilterpctwidth=10.0, debug=False
60+
rawresp, Fs, envelopelpffreq=0.075, slidingfilterpctwidth=10.0, debug=False
7861
):
7962
"""
8063
Implementation of Romano's method from A Signal Quality Index for Improving the Estimation of
8164
Breath-by-Breath Respiratory Rate During Sport and Exercise,
8265
IEEE SENSORS JOURNAL, VOL. 23, NO. 24, 15 DECEMBER 2023
8366
8467
NB: In part A, Romano does not specify the upper pass frequency for the respiratory envelope filter.
85-
0.05Hz looks pretty good.
68+
0.075Hz looks pretty good.
8669
In part B, the width of the sliding window bandpass filter is not specified. We use a range of +/- 5% of the
8770
center frequency.
8871
@@ -109,27 +92,29 @@ def respiratorysqi(
10992
# rawresp = physio_or_numpy(rawresp)
11093

11194
# get the sample frequency down to around 25 Hz for respiratory waveforms
112-
targetfreq = 25.0
113-
if Fs > targetfreq:
114-
dsfac = int(Fs / targetfreq)
115-
print(f"downsampling by a factor of {dsfac}")
116-
rawresp = rawresp[::dsfac] + 0.0
117-
Fs /= dsfac
95+
targetfs = 25.0
96+
rawresp = Physio(rawresp, fs=Fs)
97+
rawresp = operations.interpolate_physio(rawresp, target_fs=targetfs)
11898

11999
# A. Signal Preprocessing
120-
# Apply first order Butterworth bandpass, 0.01-2Hz
121-
prefiltered = respirationprefilter(rawresp, Fs, debug=debug)
100+
# Apply third order Butterworth bandpass, 0.01-2Hz
101+
prefiltered = operations.filter_physio(
102+
rawresp,
103+
[0.01, 2.0],
104+
"bandpass",
105+
order=3,
106+
)
122107
if debug:
123-
plt.plot(rawresp)
124-
plt.plot(prefiltered)
108+
plt.plot(rawresp.data)
109+
plt.plot(prefiltered.data)
125110
plt.title("Raw and prefiltered respiratory signal")
126111
plt.legend(["Raw", "Prefiltered"])
127112
plt.show()
128113
if debug:
129-
print("prefiltered: ", prefiltered)
114+
print("prefiltered: ", prefiltered.data)
130115

131116
# calculate the derivative
132-
derivative = np.gradient(prefiltered, 1.0 / Fs)
117+
derivative = np.gradient(prefiltered.data, 1.0 / targetfs)
133118

134119
# normalize the derivative to the range of ~-1 to 1
135120
derivmax = np.max(derivative)
@@ -145,18 +130,9 @@ def respiratorysqi(
145130
plt.show()
146131

147132
# amplitude correct by flattening the envelope function
148-
esuperior = 2.0 * respenvelopefilter(
149-
np.square(np.where(normderiv > 0.0, normderiv, 0.0)),
150-
Fs,
151-
upperpass=envelopelpffreq,
133+
einferior, esuperior = envelopedetect(
134+
Physio(normderiv, fs=targetfs), upperpass=envelopelpffreq, order=3
152135
)
153-
esuperior = np.sqrt(np.where(esuperior > 0.0, esuperior, 0.0))
154-
einferior = 2.0 * respenvelopefilter(
155-
np.square(np.where(normderiv < 0.0, normderiv, 0.0)),
156-
Fs,
157-
upperpass=envelopelpffreq,
158-
)
159-
einferior = -np.sqrt(np.where(einferior > 0.0, einferior, 0.0))
160136
if debug:
161137
plt.plot(normderiv)
162138
plt.plot(esuperior)
@@ -173,9 +149,9 @@ def respiratorysqi(
173149

174150
# B. Detection of breaths in sliding window
175151
seglength = 12.0
176-
segsamples = int(seglength * Fs)
152+
segsamples = int(seglength * targetfs)
177153
segstep = 2.0
178-
stepsamples = int(segstep * Fs)
154+
stepsamples = int(segstep * targetfs)
179155
totaltclength = len(rawresp)
180156
numsegs = int((totaltclength - segsamples) // stepsamples)
181157
if (totaltclength - segsamples) % segsamples != 0:
@@ -193,7 +169,7 @@ def respiratorysqi(
193169
segment = rmsnormderiv[segstart:segend] + 0.0
194170
segment *= hamming(segsamples)
195171
segment -= np.mean(segment)
196-
thex, they = welch(segment, Fs, nfft=4096)
172+
thex, they = welch(segment, targetfs, nfft=4096)
197173
peakfreqs[i] = thex[np.argmax(they)]
198174
respfilterorder = 1
199175
lowerfac = 1.0 - slidingfilterpctwidth / 200.0
@@ -202,9 +178,12 @@ def respiratorysqi(
202178
upperpass = peakfreqs[i] * upperfac
203179
if debug:
204180
print(peakfreqs[i], lowerfac, lowerpass, upperfac, upperpass)
205-
filteredsegment = butterlpfiltfilt(
206-
Fs, upperpass, segment, respfilterorder, debug=False
207-
)
181+
filteredsegment = operations.filter_physio(
182+
Physio(segment, fs=targetfs),
183+
[upperpass],
184+
"lowpass",
185+
order=respfilterorder,
186+
).data
208187
filteredsegment -= np.mean(filteredsegment)
209188
if i < numsegs - 1:
210189
respfilteredderivs[
@@ -227,7 +206,7 @@ def respiratorysqi(
227206
# C. Breaths segmentation
228207
# The fastest credible breathing rate is 20 breaths/min -> 3 seconds/breath, so set the dist to be 50%
229208
# of that in points
230-
thedist = int((3.0 * Fs) / 2.0)
209+
thedist = int((3.0 * targetfs) / 2.0)
231210
peakinfo = operations.peakfind_physio(respfilteredderivs, thresh=0.05, dist=thedist)
232211
if debug:
233212
ax = operations.plot_physio(peakinfo)
@@ -245,9 +224,9 @@ def respiratorysqi(
245224
breathinfo = {}
246225
startpt = thepeaks[thisbreath]
247226
endpt = thepeaks[thisbreath + 1]
248-
thebreathlocs[thisbreath] = (endpt + startpt) / (Fs * 2.0)
249-
breathinfo["starttime"] = startpt / Fs
250-
breathinfo["endtime"] = endpt / Fs
227+
thebreathlocs[thisbreath] = (endpt + startpt) / (targetfs * 2.0)
228+
breathinfo["starttime"] = startpt / targetfs
229+
breathinfo["endtime"] = endpt / targetfs
251230
breathinfo["centertime"] = thebreathlocs[thisbreath]
252231
thescaledbreaths[thisbreath, :] = resample(
253232
respfilteredderivs[startpt:endpt], scaledpeaklength

0 commit comments

Comments
 (0)