33import matplotlib .pyplot as plt
44import numpy as np
55from matplotlib .patches import Polygon
6- from peakdet import operations
6+ from peakdet import Physio , operations
77from scipy .signal import resample , welch
88from scipy .stats import pearsonr
99
1010from .. import references
1111from ..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 )
7659def 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