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