import numpy as np
from ..misc import progress_bar
from .video_face import video_face
from .video_skin import video_skin
[docs]
def video_ppg(video, sampling_rate=30, verbose=True):
"""**Remote Photoplethysmography (rPPG) from Video**
Extracts the photoplethysmogram (PPG) from a webcam video using the Plane-Orthogonal-to-Skin
(POS) algorithm.
.. note::
This function is experimental and does NOT seem to work at all
(https://github.com/DominiqueMakowski/RemotePhysiology). If you
are interested in helping us improve that aspect of NeuroKit
(e.g., by adding more detection algorithms), please get in touch!
Parameters
----------
video : np.ndarray
A video data numpy array of the shape (frame, channel, height, width).
sampling_rate : int
The sampling rate of the video, by default 30 fps (a common sampling rate for commercial
webcams).
verbose : bool
Whether to print the progress bar.
Returns
-------
np.ndarray
A PPG signal.
Examples
--------
.. ipython:: python
import neurokit2 as nk
# video, sampling_rate = nk.read_video("video.mp4")
# ppg = nk.video_ppg(video)
References
----------
* Wang, W., Den Brinker, A. C., Stuijk, S., & De Haan, G. (2016). Algorithmic principles of
remote PPG. IEEE Transactions on Biomedical Engineering, 64(7), 1479-1491.
"""
# Initialize heart rate
ppg = np.full((len(video)), np.nan)
# Chunk into 8 second segments (5 * 1.6 which is the temporal smoothing window)
chunk_size = int(sampling_rate * 8)
for _, start in progress_bar(np.arange(0, len(video), chunk_size), verbose=verbose):
end = start + chunk_size
if end > len(video):
end = len(video)
ppg[start:end] = _video_ppg(video[start:end, :, :, :], sampling_rate, window=1.6)
return ppg
# ==============================================================================
# Internals
# ==============================================================================
def _video_ppg(video, sampling_rate=30, window=1.6):
# 1. Extract faces
faces = video_face(video, verbose=False)
rgb = np.full((len(faces), 3), np.nan)
for i, face in enumerate(faces):
# 2. Extract skin
mask, masked_face = video_skin(face)
# Extract color
r = np.sum(masked_face[:, :, 0]) / np.sum(mask > 0)
g = np.sum(masked_face[:, :, 1]) / np.sum(mask > 0)
b = np.sum(masked_face[:, :, 2]) / np.sum(mask > 0)
rgb[i, :] = [r, g, b]
# Plane-Orthogonal-to-Skin (POS)
# ==============================
# Calculating window (l)
window = int(sampling_rate * window)
H = np.full(len(rgb), 0)
for t in range(0, (rgb.shape[0] - window)):
# 4. Spatial averaging
C = rgb[t : t + window - 1, :].T
# 5. Temporal normalization
mean_color = np.mean(C, axis=1)
try:
Cn = np.matmul(np.linalg.inv(np.diag(mean_color)), C)
except np.linalg.LinAlgError: # Singular matrix
continue
# 6. Projection
S = np.matmul(np.array([[0, 1, -1], [-2, 1, 1]]), Cn)
# 7. Tuning (2D signal to 1D signal)
std = np.array([1, np.std(S[0, :]) / np.std(S[1, :])])
P = np.matmul(std, S)
# 8. Overlap-Adding
H[t : t + window - 1] = H[t : t + window - 1] + (P - np.mean(P)) / np.std(P)
return H