Skip to content

Commit

Permalink
Merge pull request #987 from peterhcharlton/add_ppg_quality
Browse files Browse the repository at this point in the history
[feature] added PPG quality assessment functionality
  • Loading branch information
danibene authored May 21, 2024
2 parents 836f62a + dddc8e0 commit 1dd967b
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 7 deletions.
2 changes: 1 addition & 1 deletion neurokit2/misc/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def summarize_table(signals):
def text_combine(info):
"""Reformat dictionary describing processing methods as strings to be inserted into HTML file."""
preprocessing = '<h2 style="background-color: #FB1CF0">Preprocessing</h1>'
for key in ["text_cleaning", "text_peaks"]:
for key in ["text_cleaning", "text_peaks", "text_quality"]:
if key in info.keys():
preprocessing += info[key] + "<br>"
ref = '<h2 style="background-color: #FBB41C">References</h1>'
Expand Down
2 changes: 2 additions & 0 deletions neurokit2/ppg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .ppg_peaks import ppg_peaks
from .ppg_plot import ppg_plot
from .ppg_process import ppg_process
from .ppg_quality import ppg_quality
from .ppg_segment import ppg_segment
from .ppg_simulate import ppg_simulate

Expand All @@ -23,6 +24,7 @@
"ppg_rate",
"ppg_process",
"ppg_plot",
"ppg_quality",
"ppg_methods",
"ppg_intervalrelated",
"ppg_eventrelated",
Expand Down
60 changes: 56 additions & 4 deletions neurokit2/ppg/ppg_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
from ..misc.report import get_kwargs
from .ppg_clean import ppg_clean
from .ppg_findpeaks import ppg_findpeaks
from .ppg_quality import ppg_quality


def ppg_methods(
sampling_rate=1000,
method="elgendi",
method_cleaning="default",
method_peaks="default",
method_quality="default",
**kwargs,
):
"""**PPG Preprocessing Methods**
Expand Down Expand Up @@ -38,6 +40,11 @@ def ppg_methods(
will be set to the value of ``"method"``. Defaults to ``"default"``.
For more information, see the ``"method"`` argument
of :func:`.ppg_findpeaks`.
method_quality: str
The method used to assess PPG signal quality. If ``"default"``,
will be set to the value of ``"templatematch"``. Defaults to ``"templatematch"``.
For more information, see the ``"method"`` argument
of :func:`.ppg_quality`.
**kwargs
Other arguments to be passed to :func:`.ppg_clean` and
:func:`.ppg_findpeaks`.
Expand All @@ -51,15 +58,17 @@ def ppg_methods(
See Also
--------
ppg_process, ppg_clean, ppg_findpeaks
ppg_process, ppg_clean, ppg_findpeaks, ppg_quality
Examples
--------
.. ipython:: python
import neurokit2 as nk
methods = nk.ppg_methods(sampling_rate=100, method="elgendi", method_cleaning="nabian2018")
methods = nk.ppg_methods(
sampling_rate=100, method="elgendi",
method_cleaning="nabian2018", method_quality="templatematch")
print(methods["text_cleaning"])
print(methods["references"][0])
Expand All @@ -71,7 +80,12 @@ def ppg_methods(
else str(method_cleaning).lower()
)
method_peaks = (
str(method).lower() if method_peaks == "default" else str(method_peaks).lower()
str(method).lower()
if method_peaks == "default"
else str(method_peaks).lower()
)
method_quality = (
str(method_quality).lower()
)

# Create dictionary with all inputs
Expand All @@ -80,16 +94,19 @@ def ppg_methods(
"method": method,
"method_cleaning": method_cleaning,
"method_peaks": method_peaks,
"method_quality": method_quality,
**kwargs,
}

# Get arguments to be passed to cleaning and peak finding functions
# Get arguments to be passed to cleaning, peak finding, and quality assessment functions
kwargs_cleaning, report_info = get_kwargs(report_info, ppg_clean)
kwargs_peaks, report_info = get_kwargs(report_info, ppg_findpeaks)
kwargs_quality, report_info = get_kwargs(report_info, ppg_quality)

# Save keyword arguments in dictionary
report_info["kwargs_cleaning"] = kwargs_cleaning
report_info["kwargs_peaks"] = kwargs_peaks
report_info["kwargs_quality"] = kwargs_quality

# Initialize refs list with NeuroKit2 reference
refs = ["""Makowski, D., Pham, T., Lau, Z. J., Brammer, J. C., Lespinasse, F., Pham, H.,
Expand Down Expand Up @@ -158,5 +175,40 @@ def ppg_methods(
"text_peaks"
] = f"The peak detection was carried out using the method {method_peaks}."

# 2. Quality
# ----------
if method_quality in ["templatematch"]:
report_info[
"text_quality"
] = (
"The quality assessment was carried out using template-matching, approximately as described "
+ "in Orphanidou et al. (2015)."
)
refs.append(
"""Orphanidou C, Bonnici T, Charlton P, Clifton D, Vallance D, Tarassenko L (2015)
Signal-quality indices for the electrocardiogram and photoplethysmogram: Derivation
and applications to wireless monitoring
IEEE Journal of Biomedical and Health Informatics 19(3): 832–838. doi:10.1109/JBHI.2014.2338351."""
)
elif method_quality in ["disimilarity"]:
report_info[
"text_quality"
] = (
"The quality assessment was carried out using a disimilarity measure of positive-peaked beats, "
+ "approximately as described in Sabeti et al. (2019)."
)
refs.append(
"""Sabeti E, Reamaroon N, Mathis M, Gryak J, Sjoding M, Najarian K (2019)
Signal quality measure for pulsatile physiological signals using
morphological features: Applications in reliability measure for pulse oximetry
Informatics in Medicine Unlocked 16: 100222. doi:10.1016/j.imu.2019.100222."""
)
elif method_quality in ["none"]:
report_info["text_quality"] = "There was no quality assessment carried out."
else:
report_info[
"text_quality"
] = f"The quality assessment was carried out using the method {method_quality}."

report_info["references"] = list(np.unique(refs))
return report_info
25 changes: 25 additions & 0 deletions neurokit2/ppg/ppg_peaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from ..ecg.ecg_peaks import _ecg_peaks_plot_artefacts
from ..signal import signal_fixpeaks, signal_formatpeaks
from ..stats import rescale
from .ppg_findpeaks import ppg_findpeaks


Expand Down Expand Up @@ -143,6 +144,7 @@ def _ppg_peaks_plot(
info=None,
sampling_rate=1000,
raw=None,
quality=None,
ax=None,
):
x_axis = np.linspace(0, len(ppg_cleaned) / sampling_rate, len(ppg_cleaned))
Expand All @@ -154,6 +156,29 @@ def _ppg_peaks_plot(
ax.set_xlabel("Time (seconds)")
ax.set_title("PPG signal and peaks")

# Quality Area -------------------------------------------------------------
if quality is not None:
quality = rescale(
quality,
to=[
np.min([np.min(raw), np.min(ppg_cleaned)]),
np.max([np.max(raw), np.max(ppg_cleaned)]),
],
)
minimum_line = np.full(len(x_axis), quality.min())

# Plot quality area first
ax.fill_between(
x_axis,
minimum_line,
quality,
alpha=0.12,
zorder=0,
interpolate=True,
facecolor="#4CAF50",
label="Signal quality",
)

# Raw Signal ---------------------------------------------------------------
if raw is not None:
ax.plot(x_axis, raw, color="#B0BEC5", label="Raw signal", zorder=1)
Expand Down
1 change: 1 addition & 0 deletions neurokit2/ppg/ppg_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def ppg_plot(ppg_signals, info=None, static=True):
info=info,
sampling_rate=info["sampling_rate"],
raw=ppg_signals["PPG_Raw"].values,
quality=ppg_signals["PPG_Quality"].values,
ax=ax0,
)

Expand Down
18 changes: 16 additions & 2 deletions neurokit2/ppg/ppg_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
from .ppg_methods import ppg_methods
from .ppg_peaks import ppg_peaks
from .ppg_plot import ppg_plot
from .ppg_quality import ppg_quality


def ppg_process(
ppg_signal, sampling_rate=1000, method="elgendi", report=None, **kwargs
ppg_signal, sampling_rate=1000, method="elgendi", method_quality="templatematch", report=None, **kwargs
):
"""**Process a photoplethysmogram (PPG) signal**
Expand All @@ -26,6 +27,9 @@ def ppg_process(
method : str
The processing pipeline to apply. Can be one of ``"elgendi"``.
Defaults to ``"elgendi"``.
method_quality : str
The quality assessment approach to use. Can be one of ``"templatematch"``, ``"disimilarity"``.
Defaults to ``"templatematch"``.
report : str
The filename of a report containing description and figures of processing
(e.g. ``"myreport.html"``). Needs to be supplied if a report file
Expand Down Expand Up @@ -69,7 +73,7 @@ def ppg_process(
"""
# Sanitize input
ppg_signal = as_vector(ppg_signal)
methods = ppg_methods(sampling_rate=sampling_rate, method=method, **kwargs)
methods = ppg_methods(sampling_rate=sampling_rate, method=method, method_quality=method_quality, **kwargs)

# Clean signal
ppg_cleaned = ppg_clean(
Expand All @@ -94,12 +98,22 @@ def ppg_process(
info["PPG_Peaks"], sampling_rate=sampling_rate, desired_length=len(ppg_cleaned)
)

# Assess signal quality
quality = ppg_quality(
ppg_cleaned,
ppg_pw_peaks=info["PPG_Peaks"],
sampling_rate=sampling_rate,
method=methods["method_quality"],
**methods["kwargs_quality"]
)

# Prepare output
signals = pd.DataFrame(
{
"PPG_Raw": ppg_signal,
"PPG_Clean": ppg_cleaned,
"PPG_Rate": rate,
"PPG_Quality": quality,
"PPG_Peaks": peaks_signal["PPG_Peaks"].values,
}
)
Expand Down
Loading

0 comments on commit 1dd967b

Please sign in to comment.