paradigma 0.3.2__py3-none-any.whl → 0.4.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.
- paradigma/assets/gait_detection_clf_package.pkl +0 -0
- paradigma/assets/gait_filtering_clf_package.pkl +0 -0
- paradigma/assets/ppg_quality_clf_package.pkl +0 -0
- paradigma/assets/tremor_detection_clf_package.pkl +0 -0
- paradigma/classification.py +115 -0
- paradigma/config.py +314 -0
- paradigma/constants.py +48 -7
- paradigma/feature_extraction.py +811 -547
- paradigma/pipelines/__init__.py +0 -0
- paradigma/pipelines/gait_pipeline.py +727 -0
- paradigma/pipelines/heart_rate_pipeline.py +426 -0
- paradigma/pipelines/heart_rate_utils.py +780 -0
- paradigma/pipelines/tremor_pipeline.py +299 -0
- paradigma/preprocessing.py +363 -0
- paradigma/segmenting.py +396 -0
- paradigma/testing.py +416 -0
- paradigma/util.py +393 -16
- paradigma-0.4.1.dist-info/METADATA +138 -0
- paradigma-0.4.1.dist-info/RECORD +22 -0
- {paradigma-0.3.2.dist-info → paradigma-0.4.1.dist-info}/WHEEL +1 -1
- paradigma/gait_analysis.py +0 -415
- paradigma/gait_analysis_config.py +0 -266
- paradigma/heart_rate_analysis.py +0 -127
- paradigma/heart_rate_analysis_config.py +0 -9
- paradigma/heart_rate_util.py +0 -173
- paradigma/imu_preprocessing.py +0 -232
- paradigma/ppg/classifier/LR_PPG_quality.pkl +0 -0
- paradigma/ppg/classifier/LR_model.mat +0 -0
- paradigma/ppg/feat_extraction/acc_feature.m +0 -20
- paradigma/ppg/feat_extraction/peakdet.m +0 -64
- paradigma/ppg/feat_extraction/ppg_features.m +0 -53
- paradigma/ppg/glob_functions/extract_hr_segments.m +0 -37
- paradigma/ppg/glob_functions/extract_overlapping_segments.m +0 -23
- paradigma/ppg/glob_functions/jsonlab/AUTHORS.txt +0 -41
- paradigma/ppg/glob_functions/jsonlab/ChangeLog.txt +0 -74
- paradigma/ppg/glob_functions/jsonlab/LICENSE_BSD.txt +0 -25
- paradigma/ppg/glob_functions/jsonlab/LICENSE_GPLv3.txt +0 -699
- paradigma/ppg/glob_functions/jsonlab/README.txt +0 -394
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/entries +0 -368
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/demo_jsonlab_basic.m.svn-base +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/demo_ubjson_basic.m.svn-base +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example1.json.svn-base +0 -23
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example2.json.svn-base +0 -22
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example3.json.svn-base +0 -11
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example4.json.svn-base +0 -34
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_basictest.matlab.svn-base +0 -662
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_selftest.m.svn-base +0 -27
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_selftest.matlab.svn-base +0 -144
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_speedtest.m.svn-base +0 -21
- paradigma/ppg/glob_functions/jsonlab/examples/demo_jsonlab_basic.m +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/demo_ubjson_basic.m +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/example1.json +0 -23
- paradigma/ppg/glob_functions/jsonlab/examples/example2.json +0 -22
- paradigma/ppg/glob_functions/jsonlab/examples/example3.json +0 -11
- paradigma/ppg/glob_functions/jsonlab/examples/example4.json +0 -34
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_basictest.matlab +0 -662
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_selftest.m +0 -27
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_selftest.matlab +0 -144
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_speedtest.m +0 -21
- paradigma/ppg/glob_functions/jsonlab/jsonopt.m +0 -32
- paradigma/ppg/glob_functions/jsonlab/loadjson.m +0 -566
- paradigma/ppg/glob_functions/jsonlab/loadubjson.m +0 -528
- paradigma/ppg/glob_functions/jsonlab/mergestruct.m +0 -33
- paradigma/ppg/glob_functions/jsonlab/savejson.m +0 -475
- paradigma/ppg/glob_functions/jsonlab/saveubjson.m +0 -504
- paradigma/ppg/glob_functions/jsonlab/varargin2struct.m +0 -40
- paradigma/ppg/glob_functions/sample_prob_final.m +0 -49
- paradigma/ppg/glob_functions/synchronization.m +0 -76
- paradigma/ppg/glob_functions/tsdf_scan_meta.m +0 -22
- paradigma/ppg/hr_functions/Long_TFD_JOT.m +0 -37
- paradigma/ppg/hr_functions/PPG_TFD_HR.m +0 -59
- paradigma/ppg/hr_functions/TFD toolbox JOT/.gitignore +0 -4
- paradigma/ppg/hr_functions/TFD toolbox JOT/CHANGELOG.md +0 -23
- paradigma/ppg/hr_functions/TFD toolbox JOT/LICENCE.md +0 -27
- paradigma/ppg/hr_functions/TFD toolbox JOT/README.md +0 -251
- paradigma/ppg/hr_functions/TFD toolbox JOT/README.pdf +0 -0
- paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_Doppler_kern.m +0 -142
- paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_Doppler_lag_kern.m +0 -314
- paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_lag_kern.m +0 -123
- paradigma/ppg/hr_functions/TFD toolbox JOT/dec_tfd.m +0 -154
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_di_gdtfd.m +0 -194
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_li_gdtfd.m +0 -200
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_nonsep_gdtfd.m +0 -229
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_sep_gdtfd.m +0 -241
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/di_gdtfd.m +0 -157
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/li_gdtfd.m +0 -190
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/nonsep_gdtfd.m +0 -196
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/sep_gdtfd.m +0 -199
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_tfd.m +0 -144
- paradigma/ppg/hr_functions/TFD toolbox JOT/load_curdir.m +0 -13
- paradigma/ppg/hr_functions/TFD toolbox JOT/pics/decimated_TFDs_examples.png +0 -0
- paradigma/ppg/hr_functions/TFD toolbox JOT/pics/full_TFDs_examples.png +0 -0
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/check_dec_params_seq.m +0 -79
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/dispEE.m +0 -9
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/dispVars.m +0 -26
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/disp_bytes.m +0 -25
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/fold_vector_full.m +0 -40
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/fold_vector_half.m +0 -34
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/gen_LFM.m +0 -29
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/get_analytic_signal.m +0 -76
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/get_window.m +0 -176
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/isreal_fn.m +0 -11
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/padWin.m +0 -97
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/vtfd.m +0 -149
- paradigma/ppg/preprocessing/preprocessing_imu.m +0 -15
- paradigma/ppg/preprocessing/preprocessing_ppg.m +0 -13
- paradigma/ppg_preprocessing.py +0 -313
- paradigma/preprocessing_config.py +0 -69
- paradigma/quantification.py +0 -58
- paradigma/tremor/TremorFeaturesAndClassification.m +0 -345
- paradigma/tremor/feat_extraction/DerivativesExtract.m +0 -22
- paradigma/tremor/feat_extraction/ExtractBandSignalsRMS.m +0 -72
- paradigma/tremor/feat_extraction/MFCCExtract.m +0 -100
- paradigma/tremor/feat_extraction/PSDBandPower.m +0 -52
- paradigma/tremor/feat_extraction/PSDEst.m +0 -63
- paradigma/tremor/feat_extraction/PSDExtrAxis.m +0 -88
- paradigma/tremor/feat_extraction/PSDExtrOpt.m +0 -95
- paradigma/tremor/preprocessing/InterpData.m +0 -32
- paradigma/tremor/weekly_aggregates/WeeklyAggregates.m +0 -295
- paradigma/windowing.py +0 -219
- paradigma-0.3.2.dist-info/METADATA +0 -79
- paradigma-0.3.2.dist-info/RECORD +0 -108
- {paradigma-0.3.2.dist-info → paradigma-0.4.1.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import numpy as np
|
|
3
|
+
import os
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from scipy.signal import welch
|
|
7
|
+
from scipy.signal.windows import hamming, hann
|
|
8
|
+
import tsdf
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
from paradigma.classification import ClassifierPackage
|
|
12
|
+
from paradigma.constants import DataColumns
|
|
13
|
+
from paradigma.config import HeartRateConfig
|
|
14
|
+
from paradigma.feature_extraction import compute_statistics, compute_signal_to_noise_ratio, compute_auto_correlation, \
|
|
15
|
+
compute_dominant_frequency, compute_relative_power, compute_spectral_entropy
|
|
16
|
+
from paradigma.pipelines.heart_rate_utils import assign_sqa_label, extract_hr_segments, extract_hr_from_segment
|
|
17
|
+
from paradigma.segmenting import tabulate_windows, WindowedDataExtractor
|
|
18
|
+
from paradigma.util import read_metadata, aggregate_parameter
|
|
19
|
+
|
|
20
|
+
def extract_signal_quality_features(df_ppg: pd.DataFrame, df_acc: pd.DataFrame, ppg_config: HeartRateConfig, acc_config: HeartRateConfig) -> pd.DataFrame:
|
|
21
|
+
"""
|
|
22
|
+
Extract signal quality features from the PPG signal.
|
|
23
|
+
The features are extracted from the temporal and spectral domain of the PPG signal.
|
|
24
|
+
The temporal domain features include variance, mean, median, kurtosis, skewness, signal-to-noise ratio, and autocorrelation.
|
|
25
|
+
The spectral domain features include the dominant frequency, relative power, spectral entropy.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
df_ppg : pd.DataFrame
|
|
30
|
+
The DataFrame containing the PPG signal.
|
|
31
|
+
df_acc : pd.DataFrame
|
|
32
|
+
The DataFrame containing the accelerometer signal.
|
|
33
|
+
ppg_config: HeartRateConfig
|
|
34
|
+
The configuration for the signal quality feature extraction of the PPG signal.
|
|
35
|
+
acc_config: HeartRateConfig
|
|
36
|
+
The configuration for the signal quality feature extraction of the accelerometer signal.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
df_features : pd.DataFrame
|
|
41
|
+
The DataFrame containing the extracted signal quality features.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
# Group sequences of timestamps into windows
|
|
45
|
+
ppg_windowed_cols = [DataColumns.TIME, ppg_config.ppg_colname]
|
|
46
|
+
ppg_windowed = tabulate_windows(
|
|
47
|
+
df=df_ppg,
|
|
48
|
+
columns=ppg_windowed_cols,
|
|
49
|
+
window_length_s=ppg_config.window_length_s,
|
|
50
|
+
window_step_length_s=ppg_config.window_step_length_s,
|
|
51
|
+
fs=ppg_config.sampling_frequency
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Extract data from the windowed PPG signal
|
|
55
|
+
extractor = WindowedDataExtractor(ppg_windowed_cols)
|
|
56
|
+
idx_time = extractor.get_index(DataColumns.TIME)
|
|
57
|
+
idx_ppg = extractor.get_index(ppg_config.ppg_colname)
|
|
58
|
+
start_time_ppg = np.min(ppg_windowed[:, :, idx_time], axis=1) # Start time of the window is relative to the first datapoint in the PPG data
|
|
59
|
+
ppg_values_windowed = ppg_windowed[:, :, idx_ppg]
|
|
60
|
+
|
|
61
|
+
acc_windowed_cols = [DataColumns.TIME] + acc_config.accelerometer_cols
|
|
62
|
+
acc_windowed = tabulate_windows(
|
|
63
|
+
df=df_acc,
|
|
64
|
+
columns=acc_windowed_cols,
|
|
65
|
+
window_length_s=acc_config.window_length_s,
|
|
66
|
+
window_step_length_s=acc_config.window_step_length_s,
|
|
67
|
+
fs=acc_config.sampling_frequency
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Extract data from the windowed accelerometer signal
|
|
71
|
+
extractor = WindowedDataExtractor(acc_windowed_cols)
|
|
72
|
+
idx_acc = extractor.get_slice(acc_config.accelerometer_cols)
|
|
73
|
+
acc_values_windowed = acc_windowed[:, :, idx_acc]
|
|
74
|
+
|
|
75
|
+
df_features = pd.DataFrame(start_time_ppg, columns=[DataColumns.TIME])
|
|
76
|
+
# Compute features of the temporal domain of the PPG signal
|
|
77
|
+
df_temporal_features = extract_temporal_domain_features(ppg_values_windowed, ppg_config, quality_stats=['var', 'mean', 'median', 'kurtosis', 'skewness'])
|
|
78
|
+
|
|
79
|
+
# Combine temporal features with the start time
|
|
80
|
+
df_features = pd.concat([df_features, df_temporal_features], axis=1)
|
|
81
|
+
|
|
82
|
+
# Compute features of the spectral domain of the PPG signal
|
|
83
|
+
df_spectral_features = extract_spectral_domain_features(ppg_values_windowed, ppg_config)
|
|
84
|
+
|
|
85
|
+
# Combine the spectral features with the previously computed temporal features
|
|
86
|
+
df_features = pd.concat([df_features, df_spectral_features], axis=1)
|
|
87
|
+
|
|
88
|
+
# Compute periodicity feature of the accelerometer signal
|
|
89
|
+
df_accelerometer_feature = extract_accelerometer_feature(acc_values_windowed, ppg_values_windowed, acc_config)
|
|
90
|
+
|
|
91
|
+
# Combine the accelerometer feature with the previously computed features
|
|
92
|
+
df_features = pd.concat([df_features, df_accelerometer_feature], axis=1)
|
|
93
|
+
|
|
94
|
+
return df_features
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def signal_quality_classification(df: pd.DataFrame, config: HeartRateConfig, full_path_to_classifier_package: str | Path) -> pd.DataFrame:
|
|
98
|
+
"""
|
|
99
|
+
Classify the signal quality of the PPG signal using a logistic regression classifier. A probability close to 1 indicates a high-quality signal, while a probability close to 0 indicates a low-quality signal.
|
|
100
|
+
The classifier is trained on features extracted from the PPG signal. The features are extracted using the extract_signal_quality_features function.
|
|
101
|
+
The accelerometer signal is used to determine the signal quality based on the power ratio of the accelerometer signal and returns a binary label based on a threshold.
|
|
102
|
+
A value of 1 on the indicates no/minor periodic motion influence of the accelerometer on the PPG signal, 0 indicates major periodic motion influence.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
df : pd.DataFrame
|
|
107
|
+
The DataFrame containing the PPG features and the accelerometer feature for signal quality classification.
|
|
108
|
+
config : HeartRateConfig
|
|
109
|
+
The configuration for the signal quality classification.
|
|
110
|
+
full_path_to_classifier_package : str | Path
|
|
111
|
+
The path to the directory containing the classifier.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
df_sqa pd.DataFrame
|
|
116
|
+
The DataFrame containing the PPG signal quality predictions (both probabilities of the PPG signal quality classification and the accelerometer label based on the threshold).
|
|
117
|
+
"""
|
|
118
|
+
clf_package = ClassifierPackage.load(full_path_to_classifier_package) # Load the classifier package
|
|
119
|
+
clf = clf_package.classifier # Load the logistic regression classifier
|
|
120
|
+
|
|
121
|
+
# Apply scaling to relevant columns
|
|
122
|
+
scaled_features = clf_package.transform_features(df.loc[:, clf.feature_names_in]) # Apply scaling to the features
|
|
123
|
+
|
|
124
|
+
# Make predictions for PPG signal quality assessment, and assign the probabilities to the DataFrame and drop the features
|
|
125
|
+
df[DataColumns.PRED_SQA_PROBA] = clf.predict_proba(scaled_features)[:, 0]
|
|
126
|
+
df[DataColumns.PRED_SQA_ACC_LABEL] = (df[DataColumns.ACC_POWER_RATIO] < config.threshold_sqa_accelerometer).astype(int) # Assign accelerometer label to the DataFrame based on the threshold
|
|
127
|
+
|
|
128
|
+
return df[[DataColumns.TIME, DataColumns.PRED_SQA_PROBA, DataColumns.PRED_SQA_ACC_LABEL]] # Return only the relevant columns, namely the predicted probabilities for the PPG signal quality and the accelerometer label
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def estimate_heart_rate(df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame, config: HeartRateConfig) -> pd.DataFrame:
|
|
132
|
+
"""
|
|
133
|
+
Estimate the heart rate from the PPG signal using the time-frequency domain method.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
df_sqa : pd.DataFrame
|
|
138
|
+
The DataFrame containing the signal quality assessment predictions.
|
|
139
|
+
df_ppg_preprocessed : pd.DataFrame
|
|
140
|
+
The DataFrame containing the preprocessed PPG signal.
|
|
141
|
+
config : HeartRateConfig
|
|
142
|
+
The configuration for the heart rate estimation.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
df_hr : pd.DataFrame
|
|
147
|
+
The DataFrame containing the heart rate estimations.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
# Extract NumPy arrays for faster operations
|
|
151
|
+
ppg_post_prob = df_sqa[DataColumns.PRED_SQA_PROBA].to_numpy()
|
|
152
|
+
acc_label = df_sqa.loc[:, DataColumns.PRED_SQA_ACC_LABEL].to_numpy() # Adjust later in data columns to get the correct label, should be first intergrated in feature extraction and classification
|
|
153
|
+
ppg_preprocessed = df_ppg_preprocessed.values
|
|
154
|
+
time_idx = df_ppg_preprocessed.columns.get_loc(DataColumns.TIME) # Get the index of the time column
|
|
155
|
+
ppg_idx = df_ppg_preprocessed.columns.get_loc(DataColumns.PPG) # Get the index of the PPG column
|
|
156
|
+
|
|
157
|
+
# Assign window-level probabilities to individual samples
|
|
158
|
+
sqa_label = assign_sqa_label(ppg_post_prob, config, acc_label) # assigns a signal quality label to every individual data point
|
|
159
|
+
v_start_idx, v_end_idx = extract_hr_segments(sqa_label, config.min_hr_samples) # extracts heart rate segments based on the SQA label
|
|
160
|
+
|
|
161
|
+
v_hr_rel = np.array([])
|
|
162
|
+
t_hr_rel = np.array([])
|
|
163
|
+
|
|
164
|
+
edge_add = 2 * config.sampling_frequency # Add 2s on both sides of the segment for HR estimation
|
|
165
|
+
step_size = config.hr_est_samples # Step size for HR estimation
|
|
166
|
+
|
|
167
|
+
# Estimate the maximum size for preallocation
|
|
168
|
+
valid_segments = (v_start_idx >= edge_add) & (v_end_idx <= len(ppg_preprocessed) - edge_add) # check if the segments are valid, e.g. not too close to the edges (2s)
|
|
169
|
+
valid_start_idx = v_start_idx[valid_segments] # get the valid start indices
|
|
170
|
+
valid_end_idx = v_end_idx[valid_segments] # get the valid end indices
|
|
171
|
+
max_size = np.sum((valid_end_idx - valid_start_idx) // step_size) # maximum size for preallocation
|
|
172
|
+
|
|
173
|
+
# Preallocate arrays
|
|
174
|
+
v_hr_rel = np.empty(max_size, dtype=float)
|
|
175
|
+
t_hr_rel = np.empty(max_size, dtype=float)
|
|
176
|
+
|
|
177
|
+
# Track current position
|
|
178
|
+
hr_pos = 0
|
|
179
|
+
|
|
180
|
+
for start_idx, end_idx in zip(valid_start_idx, valid_end_idx):
|
|
181
|
+
# Extract extended PPG segment
|
|
182
|
+
extended_ppg_segment = ppg_preprocessed[start_idx - edge_add : end_idx + edge_add, ppg_idx]
|
|
183
|
+
|
|
184
|
+
# Estimate heart rate
|
|
185
|
+
hr_est = extract_hr_from_segment(
|
|
186
|
+
extended_ppg_segment,
|
|
187
|
+
config.tfd_length,
|
|
188
|
+
config.sampling_frequency,
|
|
189
|
+
config.kern_type,
|
|
190
|
+
config.kern_params,
|
|
191
|
+
)
|
|
192
|
+
n_hr = len(hr_est) # Number of heart rate estimates
|
|
193
|
+
end_idx_time = n_hr * step_size + start_idx # Calculate end index for time, different from end_idx since it is always a multiple of step_size, while end_idx is not
|
|
194
|
+
|
|
195
|
+
# Extract relative time for HR estimates
|
|
196
|
+
hr_time = ppg_preprocessed[start_idx : end_idx_time : step_size, time_idx]
|
|
197
|
+
|
|
198
|
+
# Insert into preallocated arrays
|
|
199
|
+
v_hr_rel[hr_pos:hr_pos + n_hr] = hr_est
|
|
200
|
+
t_hr_rel[hr_pos:hr_pos + n_hr] = hr_time
|
|
201
|
+
hr_pos += n_hr
|
|
202
|
+
|
|
203
|
+
df_hr = pd.DataFrame({"time": t_hr_rel, "heart_rate": v_hr_rel})
|
|
204
|
+
|
|
205
|
+
return df_hr
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def aggregate_heart_rate(hr_values: np.ndarray, aggregates: List[str] = ['mode', '99p']) -> dict:
|
|
209
|
+
"""
|
|
210
|
+
Aggregate the heart rate estimates using the specified aggregation methods.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
hr_values : np.ndarray
|
|
215
|
+
The array containing the heart rate estimates
|
|
216
|
+
aggregates : List[str]
|
|
217
|
+
The list of aggregation methods to be used for the heart rate estimates. The default is ['mode', '99p'].
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
aggregated_results : dict
|
|
222
|
+
The dictionary containing the aggregated results of the heart rate estimates.
|
|
223
|
+
"""
|
|
224
|
+
# Initialize the dictionary for the aggregated results
|
|
225
|
+
aggregated_results = {}
|
|
226
|
+
|
|
227
|
+
# Initialize the dictionary for the aggregated results with the metadata
|
|
228
|
+
aggregated_results = {
|
|
229
|
+
'metadata': {
|
|
230
|
+
'nr_hr_est': len(hr_values)
|
|
231
|
+
},
|
|
232
|
+
'hr_aggregates': {}
|
|
233
|
+
}
|
|
234
|
+
for aggregate in aggregates:
|
|
235
|
+
aggregated_results['hr_aggregates'][f'{aggregate}_{DataColumns.HEART_RATE}'] = aggregate_parameter(hr_values, aggregate)
|
|
236
|
+
|
|
237
|
+
return aggregated_results
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def extract_temporal_domain_features(
|
|
241
|
+
ppg_windowed: np.ndarray,
|
|
242
|
+
config: HeartRateConfig,
|
|
243
|
+
quality_stats: List[str] = ['mean', 'std']
|
|
244
|
+
) -> pd.DataFrame:
|
|
245
|
+
"""
|
|
246
|
+
Compute temporal domain features for the ppg signal. The features are added to the dataframe. Therefore the original dataframe is modified, and the modified dataframe is returned.
|
|
247
|
+
|
|
248
|
+
Parameters
|
|
249
|
+
----------
|
|
250
|
+
ppg_windowed: np.ndarray
|
|
251
|
+
The dataframe containing the windowed accelerometer signal
|
|
252
|
+
|
|
253
|
+
config: HeartRateConfig
|
|
254
|
+
The configuration object containing the parameters for the feature extraction
|
|
255
|
+
|
|
256
|
+
quality_stats: list, optional
|
|
257
|
+
The statistics to be computed for the gravity component of the accelerometer signal (default: ['mean', 'std'])
|
|
258
|
+
|
|
259
|
+
Returns
|
|
260
|
+
-------
|
|
261
|
+
pd.DataFrame
|
|
262
|
+
The dataframe with the added temporal domain features.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
feature_dict = {}
|
|
266
|
+
for stat in quality_stats:
|
|
267
|
+
feature_dict[stat] = compute_statistics(ppg_windowed, stat, abs_stats=True)
|
|
268
|
+
|
|
269
|
+
feature_dict['signal_to_noise'] = compute_signal_to_noise_ratio(ppg_windowed)
|
|
270
|
+
feature_dict['auto_corr'] = compute_auto_correlation(ppg_windowed, config.sampling_frequency)
|
|
271
|
+
return pd.DataFrame(feature_dict)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def extract_spectral_domain_features(
|
|
275
|
+
ppg_windowed: np.ndarray,
|
|
276
|
+
config: HeartRateConfig,
|
|
277
|
+
) -> pd.DataFrame:
|
|
278
|
+
"""
|
|
279
|
+
Calculate the spectral features (dominant frequency, relative power, and spectral entropy)
|
|
280
|
+
for each segment of a PPG signal using a single Welch's method computation. The features are added to the dataframe.
|
|
281
|
+
Therefore the original dataframe is modified, and the modified dataframe is returned.
|
|
282
|
+
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
ppg_windowed: np.ndarray
|
|
286
|
+
The dataframe containing the windowed ppg signal
|
|
287
|
+
|
|
288
|
+
config: HeartRateConfig
|
|
289
|
+
The configuration object containing the parameters for the feature extraction
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
pd.DataFrame
|
|
294
|
+
The dataframe with the added spectral domain features.
|
|
295
|
+
"""
|
|
296
|
+
d_features = {}
|
|
297
|
+
|
|
298
|
+
window = hamming(config.window_length_welch, sym = True)
|
|
299
|
+
|
|
300
|
+
n_samples_window = ppg_windowed.shape[1]
|
|
301
|
+
|
|
302
|
+
freqs, psd = welch(
|
|
303
|
+
ppg_windowed,
|
|
304
|
+
fs=config.sampling_frequency,
|
|
305
|
+
window=window,
|
|
306
|
+
noverlap=config.overlap_welch_window,
|
|
307
|
+
nfft=max(256, 2 ** int(np.log2(n_samples_window))),
|
|
308
|
+
detrend=False,
|
|
309
|
+
axis=1
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Calculate each feature using the computed PSD and frequency array
|
|
313
|
+
d_features['f_dom'] = compute_dominant_frequency(freqs, psd)
|
|
314
|
+
d_features['rel_power'] = compute_relative_power(freqs, psd, config)
|
|
315
|
+
d_features['spectral_entropy'] = compute_spectral_entropy(psd, n_samples_window)
|
|
316
|
+
|
|
317
|
+
return pd.DataFrame(d_features)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def extract_acc_power_feature(
|
|
321
|
+
f1: np.ndarray,
|
|
322
|
+
PSD_acc: np.ndarray,
|
|
323
|
+
f2: np.ndarray,
|
|
324
|
+
PSD_ppg: np.ndarray
|
|
325
|
+
) -> np.ndarray:
|
|
326
|
+
"""
|
|
327
|
+
Extract the accelerometer power feature in the PPG frequency range.
|
|
328
|
+
|
|
329
|
+
Parameters
|
|
330
|
+
----------
|
|
331
|
+
f1: np.ndarray
|
|
332
|
+
The frequency bins of the accelerometer signal.
|
|
333
|
+
PSD_acc: np.ndarray
|
|
334
|
+
The power spectral density of the accelerometer signal.
|
|
335
|
+
f2: np.ndarray
|
|
336
|
+
The frequency bins of the PPG signal.
|
|
337
|
+
PSD_ppg: np.ndarray
|
|
338
|
+
The power spectral density of the PPG signal.
|
|
339
|
+
|
|
340
|
+
Returns
|
|
341
|
+
-------
|
|
342
|
+
np.ndarray
|
|
343
|
+
The accelerometer power feature in the PPG frequency range
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
# Find the index of the maximum PSD value in the PPG signal
|
|
347
|
+
max_PPG_psd_idx = np.argmax(PSD_ppg, axis=1)
|
|
348
|
+
max_PPG_freq_psd = f2[max_PPG_psd_idx]
|
|
349
|
+
|
|
350
|
+
# Find the neighboring indices of the maximum PSD value in the PPG signal
|
|
351
|
+
df_idx = np.column_stack((max_PPG_psd_idx - 1, max_PPG_psd_idx, max_PPG_psd_idx + 1))
|
|
352
|
+
|
|
353
|
+
# Find the index of the closest frequency in the accelerometer signal to the first harmonic of the PPG frequency
|
|
354
|
+
corr_acc_psd_fh_idx = np.argmin(np.abs(f1[:, None] - max_PPG_freq_psd*2), axis=0)
|
|
355
|
+
fh_idx = np.column_stack((corr_acc_psd_fh_idx - 1, corr_acc_psd_fh_idx, corr_acc_psd_fh_idx + 1))
|
|
356
|
+
|
|
357
|
+
# Compute the power in the ranges corresponding to the PPG frequency
|
|
358
|
+
acc_power_PPG_range = (
|
|
359
|
+
np.trapz(PSD_acc[np.arange(PSD_acc.shape[0])[:, None], df_idx], f1[df_idx], axis=1) +
|
|
360
|
+
np.trapz(PSD_acc[np.arange(PSD_acc.shape[0])[:, None], fh_idx], f1[fh_idx], axis=1)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Compute the total power across the entire frequency range
|
|
364
|
+
acc_power_total = np.trapz(PSD_acc, f1)
|
|
365
|
+
|
|
366
|
+
# Compute the power ratio of the accelerometer signal in the PPG frequency range
|
|
367
|
+
acc_power_ratio = acc_power_PPG_range / acc_power_total
|
|
368
|
+
|
|
369
|
+
return acc_power_ratio
|
|
370
|
+
|
|
371
|
+
def extract_accelerometer_feature(
|
|
372
|
+
acc_windowed: np.ndarray,
|
|
373
|
+
ppg_windowed: np.ndarray,
|
|
374
|
+
config: HeartRateConfig
|
|
375
|
+
) -> pd.DataFrame:
|
|
376
|
+
"""
|
|
377
|
+
Extract accelerometer features from the accelerometer signal in the PPG frequency range.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
381
|
+
acc_windowed: np.ndarray
|
|
382
|
+
The dataframe containing the windowed accelerometer signal
|
|
383
|
+
|
|
384
|
+
ppg_windowed: np.ndarray
|
|
385
|
+
The dataframe containing the corresponding windowed ppg signal
|
|
386
|
+
|
|
387
|
+
config: HeartRateConfig
|
|
388
|
+
The configuration object containing the parameters for the feature extraction
|
|
389
|
+
|
|
390
|
+
Returns
|
|
391
|
+
-------
|
|
392
|
+
pd.DataFrame
|
|
393
|
+
The dataframe with the relative power accelerometer feature.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
if config.sensor not in ['imu', 'ppg']:
|
|
397
|
+
raise ValueError("Sensor not recognized.")
|
|
398
|
+
|
|
399
|
+
d_freq = {}
|
|
400
|
+
d_psd = {}
|
|
401
|
+
for sensor in ['imu', 'ppg']:
|
|
402
|
+
config.set_sensor(sensor)
|
|
403
|
+
|
|
404
|
+
if sensor == 'imu':
|
|
405
|
+
windows = acc_windowed
|
|
406
|
+
else:
|
|
407
|
+
windows = ppg_windowed
|
|
408
|
+
|
|
409
|
+
window_type = hann(config.window_length_welch, sym = True)
|
|
410
|
+
d_freq[sensor], d_psd[sensor] = welch(
|
|
411
|
+
windows,
|
|
412
|
+
fs=config.sampling_frequency,
|
|
413
|
+
window=window_type,
|
|
414
|
+
noverlap=config.overlap_welch_window,
|
|
415
|
+
nfft=config.nfft,
|
|
416
|
+
detrend=False,
|
|
417
|
+
axis=1
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
d_psd['imu'] = np.sum(d_psd['imu'], axis=2) # Sum the PSDs of the three axes
|
|
421
|
+
|
|
422
|
+
acc_power_ratio = extract_acc_power_feature(d_freq['imu'], d_psd['imu'], d_freq['ppg'], d_psd['ppg'])
|
|
423
|
+
|
|
424
|
+
return pd.DataFrame(acc_power_ratio, columns=['acc_power_ratio'])
|
|
425
|
+
|
|
426
|
+
|