Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] added PPG quality assessment functionality #987

Merged
merged 7 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you intentionally delete this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably not be deleted

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it back I think, not sure why it’s not showing up here: peterhcharlton@1cd76e4

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Dani! Let's merge then :)

}
)
Expand Down
Loading
Loading