rapidtide 3.0.11__py3-none-any.whl → 3.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rapidtide/Colortables.py +492 -27
- rapidtide/OrthoImageItem.py +1049 -46
- rapidtide/RapidtideDataset.py +1533 -86
- rapidtide/_version.py +3 -3
- rapidtide/calccoherence.py +196 -29
- rapidtide/calcnullsimfunc.py +188 -40
- rapidtide/calcsimfunc.py +242 -42
- rapidtide/correlate.py +1203 -383
- rapidtide/data/examples/src/testLD +56 -0
- rapidtide/data/examples/src/testalign +1 -1
- rapidtide/data/examples/src/testdelayvar +0 -1
- rapidtide/data/examples/src/testfmri +53 -3
- rapidtide/data/examples/src/testglmfilt +5 -5
- rapidtide/data/examples/src/testhappy +29 -7
- rapidtide/data/examples/src/testppgproc +17 -0
- rapidtide/data/examples/src/testrolloff +11 -0
- rapidtide/data/models/model_cnn_pytorch/best_model.pth +0 -0
- rapidtide/data/models/model_cnn_pytorch/loss.png +0 -0
- rapidtide/data/models/model_cnn_pytorch/loss.txt +1 -0
- rapidtide/data/models/model_cnn_pytorch/model.pth +0 -0
- rapidtide/data/models/model_cnn_pytorch/model_meta.json +68 -0
- rapidtide/decorators.py +91 -0
- rapidtide/dlfilter.py +2226 -110
- rapidtide/dlfiltertorch.py +4842 -0
- rapidtide/externaltools.py +327 -12
- rapidtide/fMRIData_class.py +79 -40
- rapidtide/filter.py +1899 -810
- rapidtide/fit.py +2011 -581
- rapidtide/genericmultiproc.py +93 -18
- rapidtide/happy_supportfuncs.py +2047 -172
- rapidtide/helper_classes.py +584 -43
- rapidtide/io.py +2370 -372
- rapidtide/linfitfiltpass.py +346 -99
- rapidtide/makelaggedtcs.py +210 -24
- rapidtide/maskutil.py +448 -62
- rapidtide/miscmath.py +827 -121
- rapidtide/multiproc.py +210 -22
- rapidtide/patchmatch.py +242 -42
- rapidtide/peakeval.py +31 -31
- rapidtide/ppgproc.py +2203 -0
- rapidtide/qualitycheck.py +352 -39
- rapidtide/refinedelay.py +431 -57
- rapidtide/refineregressor.py +494 -189
- rapidtide/resample.py +671 -185
- rapidtide/scripts/applyppgproc.py +28 -0
- rapidtide/scripts/showxcorr_legacy.py +7 -7
- rapidtide/scripts/stupidramtricks.py +15 -17
- rapidtide/simFuncClasses.py +1052 -77
- rapidtide/simfuncfit.py +269 -69
- rapidtide/stats.py +540 -238
- rapidtide/tests/happycomp +9 -0
- rapidtide/tests/test_cleanregressor.py +1 -2
- rapidtide/tests/test_dlfiltertorch.py +627 -0
- rapidtide/tests/test_findmaxlag.py +24 -8
- rapidtide/tests/test_fullrunhappy_v1.py +0 -2
- rapidtide/tests/test_fullrunhappy_v2.py +0 -2
- rapidtide/tests/test_fullrunhappy_v3.py +11 -4
- rapidtide/tests/test_fullrunhappy_v4.py +10 -2
- rapidtide/tests/test_fullrunrapidtide_v7.py +1 -1
- rapidtide/tests/test_getparsers.py +11 -3
- rapidtide/tests/test_refinedelay.py +0 -1
- rapidtide/tests/test_simroundtrip.py +16 -8
- rapidtide/tests/test_stcorrelate.py +3 -1
- rapidtide/tests/utils.py +9 -8
- rapidtide/tidepoolTemplate.py +142 -38
- rapidtide/tidepoolTemplate_alt.py +165 -44
- rapidtide/tidepoolTemplate_big.py +189 -52
- rapidtide/util.py +1217 -118
- rapidtide/voxelData.py +684 -37
- rapidtide/wiener.py +136 -23
- rapidtide/wiener2.py +113 -7
- rapidtide/workflows/adjustoffset.py +105 -3
- rapidtide/workflows/aligntcs.py +85 -2
- rapidtide/workflows/applydlfilter.py +87 -10
- rapidtide/workflows/applyppgproc.py +540 -0
- rapidtide/workflows/atlasaverage.py +210 -47
- rapidtide/workflows/atlastool.py +100 -3
- rapidtide/workflows/calcSimFuncMap.py +288 -69
- rapidtide/workflows/calctexticc.py +201 -9
- rapidtide/workflows/ccorrica.py +101 -6
- rapidtide/workflows/cleanregressor.py +165 -31
- rapidtide/workflows/delayvar.py +171 -23
- rapidtide/workflows/diffrois.py +81 -3
- rapidtide/workflows/endtidalproc.py +144 -4
- rapidtide/workflows/fdica.py +195 -15
- rapidtide/workflows/filtnifti.py +70 -3
- rapidtide/workflows/filttc.py +74 -3
- rapidtide/workflows/fitSimFuncMap.py +202 -51
- rapidtide/workflows/fixtr.py +73 -3
- rapidtide/workflows/gmscalc.py +113 -3
- rapidtide/workflows/happy.py +801 -199
- rapidtide/workflows/happy2std.py +144 -12
- rapidtide/workflows/happy_parser.py +163 -23
- rapidtide/workflows/histnifti.py +118 -2
- rapidtide/workflows/histtc.py +84 -3
- rapidtide/workflows/linfitfilt.py +117 -4
- rapidtide/workflows/localflow.py +328 -28
- rapidtide/workflows/mergequality.py +79 -3
- rapidtide/workflows/niftidecomp.py +322 -18
- rapidtide/workflows/niftistats.py +174 -4
- rapidtide/workflows/pairproc.py +98 -4
- rapidtide/workflows/pairwisemergenifti.py +85 -2
- rapidtide/workflows/parser_funcs.py +1421 -40
- rapidtide/workflows/physiofreq.py +137 -11
- rapidtide/workflows/pixelcomp.py +207 -5
- rapidtide/workflows/plethquality.py +103 -21
- rapidtide/workflows/polyfitim.py +151 -11
- rapidtide/workflows/proj2flow.py +75 -2
- rapidtide/workflows/rankimage.py +111 -4
- rapidtide/workflows/rapidtide.py +368 -76
- rapidtide/workflows/rapidtide2std.py +98 -2
- rapidtide/workflows/rapidtide_parser.py +109 -9
- rapidtide/workflows/refineDelayMap.py +144 -33
- rapidtide/workflows/refineRegressor.py +675 -96
- rapidtide/workflows/regressfrommaps.py +161 -37
- rapidtide/workflows/resamplenifti.py +85 -3
- rapidtide/workflows/resampletc.py +91 -3
- rapidtide/workflows/retrolagtcs.py +99 -9
- rapidtide/workflows/retroregress.py +176 -26
- rapidtide/workflows/roisummarize.py +174 -5
- rapidtide/workflows/runqualitycheck.py +71 -3
- rapidtide/workflows/showarbcorr.py +149 -6
- rapidtide/workflows/showhist.py +86 -2
- rapidtide/workflows/showstxcorr.py +160 -3
- rapidtide/workflows/showtc.py +159 -3
- rapidtide/workflows/showxcorrx.py +190 -10
- rapidtide/workflows/showxy.py +185 -15
- rapidtide/workflows/simdata.py +264 -38
- rapidtide/workflows/spatialfit.py +77 -2
- rapidtide/workflows/spatialmi.py +250 -27
- rapidtide/workflows/spectrogram.py +305 -32
- rapidtide/workflows/synthASL.py +154 -3
- rapidtide/workflows/tcfrom2col.py +76 -2
- rapidtide/workflows/tcfrom3col.py +74 -2
- rapidtide/workflows/tidepool.py +2971 -130
- rapidtide/workflows/utils.py +19 -14
- rapidtide/workflows/utils_doc.py +293 -0
- rapidtide/workflows/variabilityizer.py +116 -3
- {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/METADATA +10 -8
- {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/RECORD +144 -128
- {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/entry_points.txt +1 -0
- {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/WHEEL +0 -0
- {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/licenses/LICENSE +0 -0
- {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2016-2025 Blaise Frederick
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
#
|
|
18
|
+
#
|
|
19
|
+
import argparse
|
|
20
|
+
from typing import Any, Tuple
|
|
21
|
+
|
|
22
|
+
import matplotlib.pyplot as plt
|
|
23
|
+
import numpy as np
|
|
24
|
+
from numpy.typing import NDArray
|
|
25
|
+
|
|
26
|
+
import rapidtide.filter as tide_filt
|
|
27
|
+
import rapidtide.ppgproc as tide_ppg
|
|
28
|
+
import rapidtide.workflows.parser_funcs as pf
|
|
29
|
+
|
|
30
|
+
DEFAULT_PROCESSNOISE = 0.001
|
|
31
|
+
DEFAULT_HRESTIMATE = 70.0
|
|
32
|
+
DEFAULT_MEASUREMENTNOISE = 0.05
|
|
33
|
+
DEFAULT_QUALTHRESH = 0.5
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_parser() -> Any:
|
|
37
|
+
"""
|
|
38
|
+
Argument parser for applyppgproc.
|
|
39
|
+
|
|
40
|
+
This function constructs and returns an `argparse.ArgumentParser` object configured
|
|
41
|
+
to parse command-line arguments for the `applyppgproc` tool. The tool calculates
|
|
42
|
+
PPG (Photoplethysmography) metrics from a cardiacfromfmri output file.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
argparse.ArgumentParser
|
|
47
|
+
Configured argument parser for the applyppgproc tool.
|
|
48
|
+
|
|
49
|
+
Notes
|
|
50
|
+
-----
|
|
51
|
+
The parser includes both required and optional arguments. Required arguments are:
|
|
52
|
+
- `infileroot`: Root name of the cardiacfromfmri input file (without extension).
|
|
53
|
+
- `outfileroot`: Root name of the output files.
|
|
54
|
+
|
|
55
|
+
Optional arguments include:
|
|
56
|
+
- `--process_noise`: Process noise for the PPG filter (default: `DEFAULT_PROCESSNOISE`).
|
|
57
|
+
- `--hr_estimate`: Starting guess for heart rate in BPM (default: `DEFAULT_HRESTIMATE`).
|
|
58
|
+
- `--qual_thresh`: Quality threshold for PPG, between 0 and 1 (default: `DEFAULT_QUALTHRESH`).
|
|
59
|
+
- `--measurement_noise`: Assumed measurement noise (default: `DEFAULT_MEASUREMENTNOISE`).
|
|
60
|
+
- `--display`: Graph the processed waveforms.
|
|
61
|
+
- `--debug`: Print debugging information.
|
|
62
|
+
|
|
63
|
+
Examples
|
|
64
|
+
--------
|
|
65
|
+
>>> parser = _get_parser()
|
|
66
|
+
>>> args = parser.parse_args()
|
|
67
|
+
"""
|
|
68
|
+
parser = argparse.ArgumentParser(
|
|
69
|
+
prog="applyppgproc",
|
|
70
|
+
description=("Calculate PPG metrics from a happy cardiacfromfmri output file."),
|
|
71
|
+
allow_abbrev=False,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Required arguments
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"infileroot",
|
|
77
|
+
help="The root name of the cardiacfromfmri file (without the json or tsv.gz extension).",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"outfileroot",
|
|
81
|
+
help="The root name of the output files.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# optional arguments
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--process_noise",
|
|
87
|
+
dest="process_noise",
|
|
88
|
+
metavar="NOISE",
|
|
89
|
+
action="store",
|
|
90
|
+
type=lambda x: pf.is_float(parser, x),
|
|
91
|
+
help=f"Process noise for the PPG filter (default is {DEFAULT_PROCESSNOISE}). ",
|
|
92
|
+
default=DEFAULT_PROCESSNOISE,
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--hr_estimate",
|
|
96
|
+
dest="hr_estimate",
|
|
97
|
+
metavar="BPM",
|
|
98
|
+
action="store",
|
|
99
|
+
type=lambda x: pf.is_float(parser, x),
|
|
100
|
+
help=f"Starting guess for heart rate in BPM (default is {DEFAULT_HRESTIMATE}). ",
|
|
101
|
+
default=DEFAULT_HRESTIMATE,
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--qual_thresh",
|
|
105
|
+
dest="qual_thresh",
|
|
106
|
+
metavar="THRESH",
|
|
107
|
+
action="store",
|
|
108
|
+
type=lambda x: pf.is_float(parser, x),
|
|
109
|
+
help=f"Quality threshold for PPG, between 0 and 1 (default is {DEFAULT_QUALTHRESH}). ",
|
|
110
|
+
default=DEFAULT_QUALTHRESH,
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--measurement_noise",
|
|
114
|
+
dest="measurement_noise",
|
|
115
|
+
metavar="NOISE",
|
|
116
|
+
action="store",
|
|
117
|
+
type=lambda x: pf.is_float(parser, x),
|
|
118
|
+
help=f"Assumed measurement noise (default is {DEFAULT_MEASUREMENTNOISE}). ",
|
|
119
|
+
default=DEFAULT_MEASUREMENTNOISE,
|
|
120
|
+
)
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--display",
|
|
123
|
+
dest="display",
|
|
124
|
+
action="store_true",
|
|
125
|
+
help=("Graph the processed waveforms."),
|
|
126
|
+
default=False,
|
|
127
|
+
)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--debug",
|
|
130
|
+
dest="debug",
|
|
131
|
+
action="store_true",
|
|
132
|
+
help=("Print debugging information."),
|
|
133
|
+
default=False,
|
|
134
|
+
)
|
|
135
|
+
return parser
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def procppg(
|
|
139
|
+
args: Any,
|
|
140
|
+
) -> Tuple[
|
|
141
|
+
dict,
|
|
142
|
+
NDArray,
|
|
143
|
+
NDArray,
|
|
144
|
+
NDArray,
|
|
145
|
+
NDArray,
|
|
146
|
+
NDArray,
|
|
147
|
+
NDArray,
|
|
148
|
+
NDArray,
|
|
149
|
+
NDArray,
|
|
150
|
+
NDArray,
|
|
151
|
+
NDArray,
|
|
152
|
+
NDArray,
|
|
153
|
+
NDArray,
|
|
154
|
+
]:
|
|
155
|
+
"""
|
|
156
|
+
Process PPG (Photoplethysmography) signal using a combination of filtering,
|
|
157
|
+
heart rate extraction, and signal quality assessment techniques.
|
|
158
|
+
|
|
159
|
+
This function performs a complete PPG signal processing pipeline including:
|
|
160
|
+
- Reading and preprocessing PPG data
|
|
161
|
+
- Applying Extended Kalman Filter with sinusoidal model
|
|
162
|
+
- Extracting heart rate using FFT, EKF, and peak detection methods
|
|
163
|
+
- Assessing signal quality
|
|
164
|
+
- Computing performance metrics and additional features like HRV and pulse morphology
|
|
165
|
+
- Optionally displaying plots and printing detailed results
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
args : Any
|
|
170
|
+
An object containing the following attributes:
|
|
171
|
+
- infileroot : str
|
|
172
|
+
Root path to the input data file(s)
|
|
173
|
+
- display : bool
|
|
174
|
+
Whether to display plots
|
|
175
|
+
- debug : bool
|
|
176
|
+
Whether to print debug information
|
|
177
|
+
- hr_estimate : float
|
|
178
|
+
Initial heart rate estimate for EKF
|
|
179
|
+
- process_noise : float
|
|
180
|
+
Process noise for EKF
|
|
181
|
+
- measurement_noise : float
|
|
182
|
+
Measurement noise for EKF
|
|
183
|
+
- qual_thresh : float
|
|
184
|
+
Threshold for determining good quality signal segments
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
tuple
|
|
189
|
+
A tuple containing:
|
|
190
|
+
- ppginfo : dict
|
|
191
|
+
Dictionary with various performance metrics and computed features
|
|
192
|
+
- peak_indices : NDArray
|
|
193
|
+
Indices of detected peaks in the filtered signal
|
|
194
|
+
- rri : NDArray
|
|
195
|
+
Inter-beat intervals (RRIs) derived from peak detection
|
|
196
|
+
- hr_waveform_from_peaks : NDArray
|
|
197
|
+
Heart rate waveform computed from peaks
|
|
198
|
+
- hr_times : NDArray
|
|
199
|
+
Time points for heart rate estimates
|
|
200
|
+
- hr_values : NDArray
|
|
201
|
+
Heart rate values (FFT-based)
|
|
202
|
+
- filtered_ekf : NDArray
|
|
203
|
+
Signal filtered using Extended Kalman Filter
|
|
204
|
+
- ekf_heart_rates : NDArray
|
|
205
|
+
Heart rate estimates from EKF
|
|
206
|
+
- cardiacfromfmri_qual_times : NDArray
|
|
207
|
+
Time points for quality scores from raw fMRI signal
|
|
208
|
+
- cardiacfromfmri_qual_scores : NDArray
|
|
209
|
+
Quality scores for raw fMRI signal
|
|
210
|
+
- dlfiltered_qual_times : NDArray
|
|
211
|
+
Time points for quality scores from DL-filtered signal
|
|
212
|
+
- dlfiltered_qual_scores : NDArray
|
|
213
|
+
Quality scores for DL-filtered signal
|
|
214
|
+
|
|
215
|
+
Notes
|
|
216
|
+
-----
|
|
217
|
+
The function uses the `tide_ppg` module for signal processing and analysis.
|
|
218
|
+
It supports visualization via matplotlib when `args.display` is True.
|
|
219
|
+
Heart rate variability (HRV) and pulse morphology features are computed if sufficient peaks are detected.
|
|
220
|
+
|
|
221
|
+
Examples
|
|
222
|
+
--------
|
|
223
|
+
>>> import argparse
|
|
224
|
+
>>> args = argparse.Namespace(
|
|
225
|
+
... infileroot='data/ppg_data',
|
|
226
|
+
... display=True,
|
|
227
|
+
... debug=False,
|
|
228
|
+
... hr_estimate=70.0,
|
|
229
|
+
... process_noise=1e-4,
|
|
230
|
+
... measurement_noise=1e-2,
|
|
231
|
+
... qual_thresh=0.7
|
|
232
|
+
... )
|
|
233
|
+
>>> procppg(args)
|
|
234
|
+
"""
|
|
235
|
+
if args.display:
|
|
236
|
+
import matplotlib as mpl
|
|
237
|
+
|
|
238
|
+
mpl.use("TkAgg")
|
|
239
|
+
import matplotlib.pyplot as plt
|
|
240
|
+
|
|
241
|
+
ppginfo = {}
|
|
242
|
+
|
|
243
|
+
# read in a happy data file
|
|
244
|
+
t, Fs, dlfiltered_ppg, cardiacfromfmri_ppg, pleth_ppg, missing_indices = (
|
|
245
|
+
tide_ppg.read_happy_ppg(args.infileroot, debug=True)
|
|
246
|
+
)
|
|
247
|
+
dlfiltered_ppg /= np.std(dlfiltered_ppg)
|
|
248
|
+
rollofffilter = tide_filt.NoncausalFilter(filtertype="arb")
|
|
249
|
+
rollofffilter.setfreqs(0.0, 0.0, 1.0, 4.0)
|
|
250
|
+
# cardiacfromfmri_ppg = rollofffilter.apply(Fs, cardiacfromfmri_ppg)
|
|
251
|
+
cardiacfromfmri_ppg /= np.std(cardiacfromfmri_ppg)
|
|
252
|
+
|
|
253
|
+
# Apply Extended Kalman filter with sinusoidal model to the dlfiltered timecourse
|
|
254
|
+
ekf = tide_ppg.ExtendedPPGKalmanFilter(
|
|
255
|
+
dt=(1.0 / Fs),
|
|
256
|
+
hr_estimate=args.hr_estimate,
|
|
257
|
+
process_noise=args.process_noise,
|
|
258
|
+
measurement_noise=args.measurement_noise,
|
|
259
|
+
)
|
|
260
|
+
filtered_ekf, ekf_heart_rates = ekf.filter_signal(dlfiltered_ppg, missing_indices)
|
|
261
|
+
|
|
262
|
+
# Extract heart rate using frequency methods
|
|
263
|
+
hr_extractor = tide_ppg.HeartRateExtractor(fs=Fs)
|
|
264
|
+
hr_times, hr_values = hr_extractor.extract_continuous(
|
|
265
|
+
filtered_ekf, window_size=10.0, stride=2.0, method="fft"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Assess signal quality
|
|
269
|
+
quality_assessor = tide_ppg.SignalQualityAssessor(fs=Fs, window_size=5.0)
|
|
270
|
+
cardiacfromfmri_qual_times, cardiacfromfmri_qual_scores = quality_assessor.assess_continuous(
|
|
271
|
+
cardiacfromfmri_ppg, filtered_ekf, stride=1.0
|
|
272
|
+
)
|
|
273
|
+
dlfiltered_qual_times, dlfiltered_qual_scores = quality_assessor.assess_continuous(
|
|
274
|
+
dlfiltered_ppg, filtered_ekf, stride=1.0
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Also get single HR estimate from peaks and beat to beat
|
|
278
|
+
ppginfo["hr_from_peaks"], peak_indices, rri, hr_waveform_from_peaks = (
|
|
279
|
+
hr_extractor.extract_from_peaks(filtered_ekf)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if args.debug:
|
|
283
|
+
print(f"HR from peaks: {ppginfo["hr_from_peaks"]}")
|
|
284
|
+
print(f"Peak indices: {peak_indices}")
|
|
285
|
+
print(f"RRIs: {rri}")
|
|
286
|
+
print(f"hr_waveform_from_peaks: {hr_waveform_from_peaks}")
|
|
287
|
+
|
|
288
|
+
# Plot results
|
|
289
|
+
if args.display:
|
|
290
|
+
fig = plt.figure(figsize=(15, 10))
|
|
291
|
+
gs = fig.add_gridspec(4, 1, hspace=0.3)
|
|
292
|
+
|
|
293
|
+
thissubfig = 0
|
|
294
|
+
# Plot 1: Original and corrupted
|
|
295
|
+
ax1 = fig.add_subplot(gs[thissubfig, 0])
|
|
296
|
+
thissubfig += 1
|
|
297
|
+
ax1.plot(t, dlfiltered_ppg, "g-", label="DL filtered PPG", alpha=0.7, linewidth=1.5)
|
|
298
|
+
ax1.plot(
|
|
299
|
+
t,
|
|
300
|
+
cardiacfromfmri_ppg,
|
|
301
|
+
"r.",
|
|
302
|
+
label="Raw cardiac from fMRI with bad points",
|
|
303
|
+
markersize=3,
|
|
304
|
+
alpha=0.5,
|
|
305
|
+
)
|
|
306
|
+
ax1.set_ylabel("Amplitude")
|
|
307
|
+
ax1.set_title("PPG Signal: Raw cardiac from fmri vs DL filtered")
|
|
308
|
+
ax1.legend(loc="upper right")
|
|
309
|
+
ax1.grid(True, alpha=0.3)
|
|
310
|
+
|
|
311
|
+
# Plot 4: Extended Kalman filter (sinusoidal model)
|
|
312
|
+
ax4 = fig.add_subplot(gs[thissubfig, 0])
|
|
313
|
+
thissubfig += 1
|
|
314
|
+
ax4.plot(t, dlfiltered_ppg, "g-", label="DL filtered PPG", alpha=0.7, linewidth=1)
|
|
315
|
+
ax4.plot(
|
|
316
|
+
t,
|
|
317
|
+
cardiacfromfmri_ppg,
|
|
318
|
+
"r.",
|
|
319
|
+
label="Raw cardiac from fMRI with bad points",
|
|
320
|
+
markersize=3,
|
|
321
|
+
alpha=0.5,
|
|
322
|
+
)
|
|
323
|
+
ax4.plot(t, filtered_ekf, "m-", label="Extended Kalman Filter", linewidth=1.5)
|
|
324
|
+
ax4.plot(
|
|
325
|
+
t[missing_indices],
|
|
326
|
+
filtered_ekf[missing_indices],
|
|
327
|
+
"ro",
|
|
328
|
+
label="Interpolated points",
|
|
329
|
+
markersize=5,
|
|
330
|
+
alpha=0.7,
|
|
331
|
+
)
|
|
332
|
+
# Mark detected peaks
|
|
333
|
+
ax4.plot(
|
|
334
|
+
t[peak_indices],
|
|
335
|
+
filtered_ekf[peak_indices],
|
|
336
|
+
"kx",
|
|
337
|
+
label="Detected peaks",
|
|
338
|
+
markersize=8,
|
|
339
|
+
markeredgewidth=2,
|
|
340
|
+
)
|
|
341
|
+
ax4.set_ylabel("Amplitude")
|
|
342
|
+
ax4.set_title("Extended Kalman Filter (Sinusoidal Model)")
|
|
343
|
+
ax4.legend(loc="upper right")
|
|
344
|
+
ax4.grid(True, alpha=0.3)
|
|
345
|
+
|
|
346
|
+
# Plot 6: Heart rate extraction
|
|
347
|
+
ax6 = fig.add_subplot(gs[thissubfig, 0])
|
|
348
|
+
thissubfig += 1
|
|
349
|
+
ax6.plot(hr_times, hr_values, "b-", label="FFT-based HR", linewidth=2, marker="o")
|
|
350
|
+
ax6.plot(t, ekf_heart_rates, "r-", label="EKF-based HR", linewidth=1.5, alpha=0.7)
|
|
351
|
+
|
|
352
|
+
if hr_waveform_from_peaks is not None:
|
|
353
|
+
ax6.plot(
|
|
354
|
+
t, hr_waveform_from_peaks, "g-", label="Peak-based HR", linewidth=1.5, alpha=0.7
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if ppginfo["hr_from_peaks"] is not None:
|
|
358
|
+
ax6.axhline(
|
|
359
|
+
y=ppginfo["hr_from_peaks"],
|
|
360
|
+
color="g",
|
|
361
|
+
linestyle="--",
|
|
362
|
+
label=f"Peak-based HR: {ppginfo["hr_from_peaks"]:.1f} BPM",
|
|
363
|
+
linewidth=2,
|
|
364
|
+
)
|
|
365
|
+
ax6.set_ylabel("Heart Rate (BPM)")
|
|
366
|
+
ax6.set_title("Heart Rate Extraction")
|
|
367
|
+
ax6.legend(loc="upper right")
|
|
368
|
+
ax6.grid(True, alpha=0.3)
|
|
369
|
+
ax6.set_ylim((40.0, 110.0))
|
|
370
|
+
|
|
371
|
+
# Plot 7: Signal quality assessment
|
|
372
|
+
ax7 = fig.add_subplot(gs[thissubfig, 0])
|
|
373
|
+
thissubfig += 1
|
|
374
|
+
quality_colors = plt.cm.RdYlGn(dlfiltered_qual_scores) # Red=poor, Green=good
|
|
375
|
+
for i in range(len(dlfiltered_qual_times) - 1):
|
|
376
|
+
ax7.axvspan(
|
|
377
|
+
dlfiltered_qual_times[i],
|
|
378
|
+
dlfiltered_qual_times[i + 1],
|
|
379
|
+
alpha=0.3,
|
|
380
|
+
color=quality_colors[i],
|
|
381
|
+
)
|
|
382
|
+
ax7.plot(
|
|
383
|
+
dlfiltered_qual_times, dlfiltered_qual_scores, "k-", linewidth=2, label="Quality Score"
|
|
384
|
+
)
|
|
385
|
+
ax7.axhline(
|
|
386
|
+
y=args.qual_thresh, color="orange", linestyle="--", label="Good quality threshold"
|
|
387
|
+
)
|
|
388
|
+
ax7.set_xlabel("Time (s)")
|
|
389
|
+
ax7.set_ylabel("Quality Score")
|
|
390
|
+
ax7.set_title("Signal Quality Assessment (0=Poor, 1=Excellent)")
|
|
391
|
+
ax7.legend(loc="upper right")
|
|
392
|
+
ax7.grid(True, alpha=0.3)
|
|
393
|
+
ax7.set_ylim((0.0, 1.0))
|
|
394
|
+
|
|
395
|
+
plt.tight_layout()
|
|
396
|
+
plt.show()
|
|
397
|
+
|
|
398
|
+
# Print comprehensive performance metrics
|
|
399
|
+
ppginfo["mse_ekf"] = np.mean((dlfiltered_ppg - filtered_ekf) ** 2)
|
|
400
|
+
ppginfo["mean_fft_hr"] = np.mean(hr_values)
|
|
401
|
+
ppginfo["std_fft_hr"] = np.std(hr_values)
|
|
402
|
+
ppginfo["mean_ekf_hr"] = np.mean(ekf_heart_rates)
|
|
403
|
+
ppginfo["std_ekf_hr"] = np.std(ekf_heart_rates)
|
|
404
|
+
if hr_waveform_from_peaks is not None:
|
|
405
|
+
ppginfo["mean_peaks_hr"] = np.mean(hr_waveform_from_peaks)
|
|
406
|
+
ppginfo["std_peaks_hr"] = np.std(hr_waveform_from_peaks)
|
|
407
|
+
ppginfo["num_detected_peaks"] = len(peak_indices)
|
|
408
|
+
ppginfo["mean_dlfiltered_qual_scores"] = np.mean(dlfiltered_qual_scores)
|
|
409
|
+
ppginfo["mean_cardiacfromfmri_qual_scores"] = np.mean(cardiacfromfmri_qual_scores)
|
|
410
|
+
|
|
411
|
+
print(f"\n{'='*60}")
|
|
412
|
+
print(f"PERFORMANCE METRICS")
|
|
413
|
+
print(f"{'='*60}")
|
|
414
|
+
print(f"\nFiltering Performance:")
|
|
415
|
+
print(f" Extended Kalman MSE: {ppginfo['mse_ekf']:.6f}")
|
|
416
|
+
print(f"\nData Recovery:")
|
|
417
|
+
print(
|
|
418
|
+
f" Missing data points: {len(missing_indices)} ({len(missing_indices)/len(t)*100:.1f}%)"
|
|
419
|
+
)
|
|
420
|
+
print(f"\nHeart Rate Analysis:")
|
|
421
|
+
print(
|
|
422
|
+
f" Peak-based HR: {ppginfo["hr_from_peaks"]:.1f} BPM"
|
|
423
|
+
if ppginfo["hr_from_peaks"]
|
|
424
|
+
else " Peak-based HR: Unable to detect"
|
|
425
|
+
)
|
|
426
|
+
print(f" FFT-based HR (mean): {ppginfo['mean_fft_hr']:.1f} ± {ppginfo['std_fft_hr']:.1f} BPM")
|
|
427
|
+
print(f" EKF-based HR (mean): {ppginfo['mean_ekf_hr']:.1f} ± {ppginfo['std_ekf_hr']:.1f} BPM")
|
|
428
|
+
|
|
429
|
+
if hr_waveform_from_peaks is not None:
|
|
430
|
+
print(
|
|
431
|
+
f" Peak-based HR (mean): {ppginfo['mean_peaks_hr']:.1f} ± {ppginfo['std_peaks_hr']:.1f} BPM"
|
|
432
|
+
)
|
|
433
|
+
print(f" Number of detected peaks: {ppginfo['num_detected_peaks']}")
|
|
434
|
+
print(f"\nSignal Quality:")
|
|
435
|
+
print(f" Mean quality score: {ppginfo['mean_dlfiltered_qual_scores']:.3f}")
|
|
436
|
+
print(
|
|
437
|
+
f" Percentage of good quality signal (>0.7): {np.sum(dlfiltered_qual_scores > 0.7)/len(dlfiltered_qual_scores)*100:.1f}%"
|
|
438
|
+
)
|
|
439
|
+
print(
|
|
440
|
+
f" Percentage of poor quality signal (<0.3): {np.sum(dlfiltered_qual_scores < 0.3)/len(dlfiltered_qual_scores)*100:.1f}%"
|
|
441
|
+
)
|
|
442
|
+
print(f"\n{'='*60}")
|
|
443
|
+
|
|
444
|
+
# Demonstrate the complete processing pipeline
|
|
445
|
+
print(f"\n{'='*60}")
|
|
446
|
+
print(f"COMPLETE PROCESSING PIPELINE DEMO")
|
|
447
|
+
print(f"{'='*60}")
|
|
448
|
+
|
|
449
|
+
processor = tide_ppg.RobustPPGProcessor(
|
|
450
|
+
fs=Fs, method="ekf", hr_estimate=args.hr_estimate, process_noise=args.process_noise
|
|
451
|
+
)
|
|
452
|
+
pipeline_results = processor.process(
|
|
453
|
+
filtered_ekf, missing_indices, quality_threshold=args.qual_thresh
|
|
454
|
+
)
|
|
455
|
+
if args.debug:
|
|
456
|
+
print(pipeline_results)
|
|
457
|
+
|
|
458
|
+
print(f"\nPipeline Results:")
|
|
459
|
+
print(f" Mean quality score: {pipeline_results['mean_quality']:.3f}")
|
|
460
|
+
print(f" Good quality segments: {pipeline_results['good_quality_percentage']:.1f}%")
|
|
461
|
+
if pipeline_results["hr_overall"] is not None:
|
|
462
|
+
print(f" Overall heart rate: {pipeline_results['hr_overall']:.1f} BPM")
|
|
463
|
+
print(f" Detected {len(pipeline_results['peak_indices'])} heartbeats")
|
|
464
|
+
|
|
465
|
+
# Extract additional features
|
|
466
|
+
feature_extractor = tide_ppg.PPGFeatureExtractor(fs=Fs)
|
|
467
|
+
|
|
468
|
+
if len(peak_indices) > 5:
|
|
469
|
+
hrv_features = feature_extractor.extract_hrv_features(peak_indices)
|
|
470
|
+
ppginfo.update(hrv_features)
|
|
471
|
+
|
|
472
|
+
if hrv_features is not None:
|
|
473
|
+
print(f"\nHeart Rate Variability (HRV) Features:")
|
|
474
|
+
print(f" Mean IBI: {hrv_features['mean_ibi']:.1f} ms")
|
|
475
|
+
print(f" SDNN: {hrv_features['sdnn']:.1f} ms")
|
|
476
|
+
print(f" RMSSD: {hrv_features['rmssd']:.1f} ms")
|
|
477
|
+
print(f" pNN50: {hrv_features['pnn50']:.1f}%")
|
|
478
|
+
|
|
479
|
+
if "lf_power" in hrv_features:
|
|
480
|
+
print(f"\n Frequency Domain:")
|
|
481
|
+
print(f" LF Power: {hrv_features['lf_power']:.2f}")
|
|
482
|
+
print(f" HF Power: {hrv_features['hf_power']:.2f}")
|
|
483
|
+
print(f" LF/HF Ratio: {hrv_features['lf_hf_ratio']:.2f}")
|
|
484
|
+
|
|
485
|
+
# Extract morphology from a good segment
|
|
486
|
+
if len(peak_indices) > 0:
|
|
487
|
+
# Find a peak in the middle of the signal
|
|
488
|
+
mid_peak = peak_indices[len(peak_indices) // 2]
|
|
489
|
+
|
|
490
|
+
# Extract segment around this peak
|
|
491
|
+
segment_start = max(0, mid_peak - int(0.4 * 100))
|
|
492
|
+
segment_end = min(len(filtered_ekf), mid_peak + int(0.6 * 100))
|
|
493
|
+
segment = filtered_ekf[segment_start:segment_end]
|
|
494
|
+
peak_in_segment = mid_peak - segment_start
|
|
495
|
+
|
|
496
|
+
morph_features = feature_extractor.extract_morphology_features(segment, peak_in_segment)
|
|
497
|
+
ppginfo.update(morph_features)
|
|
498
|
+
|
|
499
|
+
print(f"\nPulse Morphology Features (single pulse):")
|
|
500
|
+
print(f" Pulse amplitude: {morph_features['pulse_amplitude']:.3f}")
|
|
501
|
+
print(f" Rising time: {morph_features['rising_time']:.3f} s")
|
|
502
|
+
if "augmentation_index" in morph_features:
|
|
503
|
+
print(f" Augmentation index: {morph_features['augmentation_index']:.3f}")
|
|
504
|
+
if "pulse_width" in morph_features:
|
|
505
|
+
print(f" Pulse width (FWHM): {morph_features['pulse_width']:.3f} s")
|
|
506
|
+
|
|
507
|
+
# SpO2 proxy
|
|
508
|
+
spo2_proxy = feature_extractor.compute_spo2_proxy(filtered_ekf)
|
|
509
|
+
ppginfo["spo2_proxy"] = spo2_proxy
|
|
510
|
+
print(f"\nSpO2 Proxy: {spo2_proxy:.1f}% (Note: This is not a real SpO2 measurement!)")
|
|
511
|
+
|
|
512
|
+
print(f"\n{'='*60}")
|
|
513
|
+
print(f"USAGE TIPS")
|
|
514
|
+
print(f"{'='*60}")
|
|
515
|
+
|
|
516
|
+
if args.display:
|
|
517
|
+
plt.plot(
|
|
518
|
+
cardiacfromfmri_qual_times, cardiacfromfmri_qual_scores, label="Cardiac from fMRI"
|
|
519
|
+
)
|
|
520
|
+
plt.plot(dlfiltered_qual_times, dlfiltered_qual_scores, label="DLfiltered")
|
|
521
|
+
plt.show()
|
|
522
|
+
|
|
523
|
+
if args.debug:
|
|
524
|
+
print(f"{ppginfo=}")
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
ppginfo,
|
|
528
|
+
peak_indices,
|
|
529
|
+
rri,
|
|
530
|
+
hr_waveform_from_peaks,
|
|
531
|
+
peak_indices,
|
|
532
|
+
hr_times,
|
|
533
|
+
hr_values,
|
|
534
|
+
filtered_ekf,
|
|
535
|
+
ekf_heart_rates,
|
|
536
|
+
cardiacfromfmri_qual_times,
|
|
537
|
+
cardiacfromfmri_qual_scores,
|
|
538
|
+
dlfiltered_qual_times,
|
|
539
|
+
dlfiltered_qual_scores,
|
|
540
|
+
)
|