Skip to content

BPM Methods

BVP

CHROM(rgb_signal, **kwargs)

The chrome_pyvhr function receives an RGB signal and returns the Blood Vessel Pulse (BVP) signal calculated from the RGB signal.

Input

rgb_signal: A list of 3 numpy arrays, representing the red (R), green (G), and blue (B) signals, respectively. The arrays must have the same number of elements.

Output

bvp: A numpy array representing the Blood Vessel Pulse (BVP) signal calculated from the RGB signal.

Algorithm

The function first separates the red, green, and blue signals, represented by r, g, and b, respectively. Then, it computes the X and Y components using the formulas:

Xcomp = 3r - 2g #3R - 2G Ycomp = (1.5r) + g - (1.5b)#1.5R + G - 1.5B

Next, the standard deviations of X and Y components, represented by sX and sY, respectively, are calculated. The alpha value is then computed as:

alpha = (sX/sY).reshape(-1,1) alpha = np.repeat(alpha, Xcomp.shape[0])

Finally, the Blood Vessel Pulse (BVP) signal is calculated as:

bvp = Xcomp - np.multiply(alpha, Ycomp)

Returns

The Blood Vessel Pulse (BVP) signal is returned as a numpy array.

De Haan, G., & Jeanne, V. (2013). Robust pulse rate from chrominance-based rPPG. IEEE Transactions on Biomedical Engineering, 60(10), 2878-2886.

Source code in redesign_pipeline/BVP/bvp_methods.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def CHROM(rgb_signal,**kwargs):

    """
    The chrome_pyvhr function receives an RGB signal and returns the Blood Vessel Pulse (BVP) signal calculated from the RGB signal.

    Input
    -----

    rgb_signal: A list of 3 numpy arrays, representing the red (R), green (G), and blue (B) signals, respectively. The arrays must have the same number of elements.

    Output
    ------

    bvp: A numpy array representing the Blood Vessel Pulse (BVP) signal calculated from the RGB signal.

    Algorithm
    ---------

    The function first separates the red, green, and blue signals, represented by r, g, and b, respectively. Then, it computes the X and Y components using the formulas:

    Xcomp = 3*r - 2*g #3R - 2G
    Ycomp = (1.5*r) + g - (1.5*b)#1.5R + G - 1.5B

    Next, the standard deviations of X and Y components, represented by sX and sY, respectively, are calculated.
    The alpha value is then computed as:

    alpha = (sX/sY).reshape(-1,1)
    alpha = np.repeat(alpha, Xcomp.shape[0])

    Finally, the Blood Vessel Pulse (BVP) signal is calculated as:

    bvp = Xcomp - np.multiply(alpha, Ycomp)

    Returns
    -------
    The Blood Vessel Pulse (BVP) signal is returned as a numpy array.

    De Haan, G., & Jeanne, V. (2013). Robust pulse rate from chrominance-based rPPG. 
    IEEE Transactions on Biomedical Engineering, 60(10), 2878-2886.
    """

    X = rgb_signal

    Xcomp = 3*X[:, 0] - 2*X[:, 1]
    Ycomp = (1.5*X[:, 0])+X[:, 1]-(1.5*X[:, 2])

    sX = np.std(Xcomp, axis=1)
    sY = np.std(Ycomp, axis=1)
    alpha = (sX/sY).reshape(-1, 1)
    alpha = np.repeat(alpha, Xcomp.shape[1], 1)
    bvp = Xcomp - np.multiply(alpha, Ycomp)


    return bvp

GREEN(rgb_signal, **kwargs)

GREEN method. Verkruysse, W., Svaasand, L. O., & Nelson, J. S. (2008). Remote plethysmographic imaging using ambient light. Optics express, 16(26), 21434-21445.

Source code in redesign_pipeline/BVP/bvp_methods.py
244
245
246
247
248
249
def GREEN(rgb_signal, **kwargs):
    """
    GREEN method.
    Verkruysse, W., Svaasand, L. O., & Nelson, J. S. (2008). Remote plethysmographic imaging using ambient light. Optics express, 16(26), 21434-21445.
    """
    return rgb_signal[:,1,:]

GREEN_Legacy(rgb_signal, **kwargs)

Input

rgb_signal: A list of length 3 where each element is an array of float values representing red, green and blue values of a video frame.

Output

Returns a 1-D numpy array of the same length as rgb_signal[1] which contains the pulse signal obtained from the video frame.

Algorithm

  1. Initialize an empty list out_signal.
  2. Loop over the length of rgb_signal[1] to extract red, green and blue values at each index i.
  3. Compute res = g / (r + g + b), where r, g, and b are the red, green and blue values obtained in step 2.
  4. Append res to out_signal list.
  5. Return out_signal as a numpy array.
Source code in redesign_pipeline/BVP/bvp_methods.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def GREEN_Legacy(rgb_signal, **kwargs):

    """
    Input
    -----

    rgb_signal: A list of length 3 where each element is an array of float values representing red, green and blue values of a video frame.

    Output
    ------

    Returns a 1-D numpy array of the same length as rgb_signal[1] which contains the pulse signal obtained from the video frame.

    Algorithm
    ---------

    1. Initialize an empty list out_signal.
    2. Loop over the length of rgb_signal[1] to extract red, green and blue values at each index i.
    3. Compute res = g / (r + g + b), where r, g, and b are the red, green and blue values obtained in step 2.
    4. Append res to out_signal list.
    5. Return out_signal as a numpy array.
    """

    r = rgb_signal[:,0,:]
    g = rgb_signal[:,1,:]
    b = rgb_signal[:,2,:]

    # Compute the pulse signal using the green component divided by the sum of all three RGB components
    res = g/(r+g+b)

    return res

LGI(rgb_signal, **kwargs)

Input

rgb_signal: A list of length 3 where each element is an array of 256 float values representing red, green and blue values of a video frame.

Output

Returns a 1-D numpy array of length 256 which contains the pulse signal obtained from the video frame.

Algorithm

  1. Convert the input list rgb_signal into a 3x256 numpy array X.
  2. Perform a Singular Value Decomposition (SVD) on X. The result is a matrix U which is 3x3, , .
  3. Extract the first column of U and reshape it into a 2D numpy array of shape (1,3).
  4. Compute the projection matrix P as P = identity(3) - transpose(S) * S, where S is the numpy array obtained from step 3 and identity(3) is the 3x3 identity matrix.
  5. Project X onto the subspace spanned by the orthonormal basis obtained in step 4, by computing Y = P * X.
  6. Extract the 2nd row of Y and return it as the pulse signal.

LGI method on CPU using Numpy. Pilz, C. S., Zaunseder, S., Krajewski, J., & Blazek, V. (2018). Local group invariance for heart rate estimation from face videos in the wild. In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition Workshops (pp. 1254-1262).

Source code in redesign_pipeline/BVP/bvp_methods.py
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def LGI(rgb_signal, **kwargs):

    """
    Input
    -----

    rgb_signal: A list of length 3 where each element is an array of 256 float values representing red, green and blue values of a video frame.

    Output
    ------

    Returns a 1-D numpy array of length 256 which contains the pulse signal obtained from the video frame.

    Algorithm
    ---------

    1. Convert the input list rgb_signal into a 3x256 numpy array X.
    2. Perform a Singular Value Decomposition (SVD) on X. The result is a matrix U which is 3x3, _, _.
    3. Extract the first column of U and reshape it into a 2D numpy array of shape (1,3).
    4. Compute the projection matrix P as P = identity(3) - transpose(S) * S, where S is the numpy array obtained from step 3 and identity(3) is the 3x3 identity matrix.
    5. Project X onto the subspace spanned by the orthonormal basis obtained in step 4, by computing Y = P * X.
    6. Extract the 2nd row of Y and return it as the pulse signal.

    LGI method on CPU using Numpy.
    Pilz, C. S., Zaunseder, S., Krajewski, J., & Blazek, V. (2018). Local group invariance for heart rate estimation from face videos in the wild. In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition Workshops (pp. 1254-1262).
    """

    X = rgb_signal

    U, _, _ = np.linalg.svd(X)
    S = U[:, :, 0]
    S = np.expand_dims(S, 2)
    sst = np.matmul(S, np.swapaxes(S, 1, 2))
    p = np.tile(np.identity(3), (S.shape[0], 1, 1))
    P = p - sst
    Y = np.matmul(P, X)
    bvp = Y[:, 1, :]

    return bvp

OMIT(rgb_signal, **kwargs)

OMIT method. Álvarez Casado, C., Bordallo López, M. (2022). Face2PPG: An unsupervised pipeline for blood volume pulse extraction from faces. arXiv (eprint 2202.04101).

Source code in redesign_pipeline/BVP/bvp_methods.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def OMIT(rgb_signal, **kwargs):
    """
    OMIT method.
    Álvarez Casado, C., Bordallo López, M. (2022). Face2PPG: An unsupervised pipeline for blood volume pulse extraction from faces. arXiv (eprint 2202.04101).
    """

    bvp = []
    for i in range(rgb_signal.shape[0]):
        X = rgb_signal[i]
        Q, R = np.linalg.qr(X)
        S = Q[:, 0].reshape(1, -1)
        P = np.identity(3) - np.matmul(S.T, S)
        Y = np.dot(P, X)
        bvp.append(Y[1, :])
    bvp = np.array(bvp)

    return bvp

PBV(rgb_signal, **kwargs)

PBV method. De Haan, G., & Van Leest, A. (2014). Improved motion robustness of remote-PPG by using the blood volume pulse signature. Physiological measurement, 35(9), 1913.

Source code in redesign_pipeline/BVP/bvp_methods.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def PBV(rgb_signal, **kwargs):
    """
    PBV method.
    De Haan, G., & Van Leest, A. (2014). Improved motion robustness of remote-PPG by using the blood volume pulse signature. Physiological measurement, 35(9), 1913.
    """
    sig_mean = np.mean(rgb_signal, axis = 2)

    signal_norm_r = rgb_signal[:,0,:] / np.expand_dims(sig_mean[:,0],axis=1)
    signal_norm_g = rgb_signal[:,1,:] / np.expand_dims(sig_mean[:,1],axis=1)
    signal_norm_b = rgb_signal[:,2,:] / np.expand_dims(sig_mean[:,2],axis=1)

    pbv_n = np.array([np.std(signal_norm_r, axis = 1), np.std(signal_norm_g, axis = 1), np.std(signal_norm_b, axis = 1)])
    pbv_d = np.sqrt(np.var(signal_norm_r, axis = 1) + np.var(signal_norm_g, axis = 1) + np.var(signal_norm_b, axis = 1))
    pbv = pbv_n / pbv_d

    C = np.swapaxes(np.array([signal_norm_r, signal_norm_g, signal_norm_b]),0,1)
    Ct =np.swapaxes(np.swapaxes(np.transpose(C),0,2),1,2)
    Q = np.matmul(C, Ct)
    W = np.linalg.solve(Q,np.swapaxes(pbv,0,1))

    A = np.matmul(Ct, np.expand_dims(W,axis = 2))
    B =  np.matmul(np.swapaxes(np.expand_dims(pbv.T,axis=2),1,2),np.expand_dims(W,axis = 2))
    bvp = A / B

    return bvp.squeeze(axis=2)

PCA_BVP(rgb_signal, **kwargs)

PCA method. The dictionary parameters are {'component':str}. Where 'component' can be 'second_comp' or 'all_comp'. Lewandowska, M., Rumiński, J., Kocejko, T., & Nowak, J. (2011, September). Measuring pulse rate with a webcam—a non-contact method for evaluating cardiac activity. In 2011 federated conference on computer science and information systems (FedCSIS) (pp. 405-410). IEEE.

Source code in redesign_pipeline/BVP/bvp_methods.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def PCA_BVP(rgb_signal, **kwargs):
    """
    PCA method.
    The dictionary parameters are {'component':str}. Where 'component' can be 'second_comp' or 'all_comp'.
    Lewandowska, M., Rumiński, J., Kocejko, T., & Nowak, J. (2011, September). Measuring pulse rate with a webcam—a non-contact method for evaluating cardiac activity. In 2011 federated conference on computer science and information systems (FedCSIS) (pp. 405-410). IEEE.
    """

    bvp = []
    defined_components = 'second_comp'

    for i in range(rgb_signal.shape[0]):
        X = rgb_signal[i]
        pca = PCA(n_components=3)
        pca.fit(X)

        # selector
        if defined_components == 'all_comp':
            bvp.append(pca.components_[0] * pca.explained_variance_[0])
            bvp.append(pca.components_[1] * pca.explained_variance_[1])
        elif defined_components =='second_comp':
            bvp.append(pca.components_[1] * pca.explained_variance_[1])
    bvp = np.array(bvp)

    return bvp

POS(rgb_signal, **kwargs)

The pos_pyvhr function receives an RGB signal and returns the Blood Volume Pulse (BVP) signal calculated from the RGB signal.

Input

rgb_signal: A list of 3 numpy arrays, representing the red (R), green (G), and blue (B) signals, respectively. The arrays must have the same number of elements, equal to 256.

Output

h: A numpy array representing the Blood Volume Pulse (BVP) signal calculated from the RGB signal.

Algorithm

The function starts by creating a matrix X of shape (3, 256), where the rows represent the red, green, and blue signals, respectively. The values of the red, green, and blue signals are stored in the matrix.

Next, the projection matrix proj is defined:

proj = np.array([[0,1,-1],[-2,1,1]])

The Blood Volume Pulse (BVP) signal is stored in the h array. The following loop calculates the BVP signal:

for n in range(X.shape[1]): m = n - wlen + 1 if m >= 0: cn = X[:, m:(n+1)] cn = np.dot(get_normalization_matrix(cn), cn) s = np.dot(proj, cn) hn = np.add(s[0, :], np.std(s[0,:])/np.std(s[1,:])*s[1,:]) h[m:(n+1)] = np.add(h[m:(n+1)], hn - np.mean(hn)) In this loop, the wlen variable, which has a recommended value of 32 for 20 frames per second (fps), is used to calculate the range of values for each iteration of the loop. If m is greater than or equal to 0, then the function calls get_normalization_matrix on cn and stores the result in cn. s is then computed as the dot product of proj and cn. The value of hn is then computed and added to h.

Return

The Blood Volume Pulse (BVP) signal is returned as a numpy array.

POS method. The dictionary parameters are: {'fps':float}. 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.

Source code in redesign_pipeline/BVP/bvp_methods.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def POS(rgb_signal, **kwargs):

    """
    The pos_pyvhr function receives an RGB signal and returns the Blood Volume Pulse (BVP) signal calculated from the RGB signal.

    Input
    -----

    rgb_signal: A list of 3 numpy arrays, representing the red (R), green (G), and blue (B) signals, respectively. The arrays must have the same number of elements, equal to 256.

    Output
    ------

    h: A numpy array representing the Blood Volume Pulse (BVP) signal calculated from the RGB signal.

    Algorithm
    ---------

    The function starts by creating a matrix X of shape (3, 256), where the rows represent the red, green, and blue signals, respectively. The values of the red, green, and blue signals are stored in the matrix.

    Next, the projection matrix proj is defined:

    proj = np.array([[0,1,-1],[-2,1,1]])

    The Blood Volume Pulse (BVP) signal is stored in the h array. The following loop calculates the BVP signal:

    for n in range(X.shape[1]):
        m = n - wlen + 1
        if m >= 0:
            cn = X[:, m:(n+1)]
            cn = np.dot(get_normalization_matrix(cn), cn)
            s = np.dot(proj, cn)
            hn = np.add(s[0, :], np.std(s[0,:])/np.std(s[1,:])*s[1,:])
            h[m:(n+1)] = np.add(h[m:(n+1)], hn - np.mean(hn))
    In this loop, the wlen variable, which has a recommended value of 32 for 20 frames per second (fps), is used to calculate the range of values for each iteration of the loop. If m is greater than or equal to 0, then the function calls get_normalization_matrix on cn and stores the result in cn. s is then computed as the dot product of proj and cn. The value of hn is then computed and added to h.

    Return
    ------

    The Blood Volume Pulse (BVP) signal is returned as a numpy array.

    POS method.
    The dictionary parameters are: {'fps':float}.
    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. 
    """

    # Run the pos algorithm on the RGB color signal c with sliding window length wlen
    # Recommended value for wlen is 32 for a 20 fps camera (1.6 s)
    eps = 10**-9
    X = rgb_signal
    e, c, f = X.shape            # e = #estimators, c = 3 rgb ch., f = #frames

    w = int(1.6 * kwargs['fps'])   # window length

    # stack e times fixed mat P
    P = np.array([[0, 1, -1], [-2, 1, 1]])
    Q = np.stack([P for _ in range(e)], axis=0)

    # Initialize (1)
    H = np.zeros((e, f))
    for n in np.arange(w, f):
        # Start index of sliding window (4)
        m = n - w + 1
        # Temporal normalization (5)
        Cn = X[:, :, m:(n + 1)]
        M = 1.0 / (np.mean(Cn, axis=2)+eps)
        M = np.expand_dims(M, axis=2)  # shape [e, c, w]
        Cn = np.multiply(M, Cn)

        # Projection (6)
        S = np.dot(Q, Cn)
        S = S[0, :, :, :]
        S = np.swapaxes(S, 0, 1)    # remove 3-th dim

        # Tuning (7)
        S1 = S[:, 0, :]
        S2 = S[:, 1, :]
        alpha = np.std(S1, axis=1) / (eps + np.std(S2, axis=1))
        alpha = np.expand_dims(alpha, axis=1)
        Hn = np.add(S1, alpha * S2)
        Hnm = Hn - np.expand_dims(np.mean(Hn, axis=1), axis=1)
        # Overlap-adding (8)
        H[:, m:(n + 1)] = np.add(H[:, m:(n + 1)], Hnm)

    return H