Peifeng Xia
Published © GPL3+

FacePulse: Contactless Heart Rate Monitor

A real-time, contact-free heart rate monitoring system using facial video and AI.

IntermediateWork in progressOver 1 day146
FacePulse: Contactless Heart Rate Monitor

Things used in this project

Hardware components

Raspberry Pi 5
Raspberry Pi 5
×1
Sony AI Camera
×1
Raspberry Pi Display Screen
×1
3D Printing Case
×1

Software apps and online services

Raspberry Pi OS (64-bit)
Python 3
OpenCV
OpenCV
Matplotlib

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Extraction Tool, 6 Piece Screw Extractor & Screwdriver Set
Extraction Tool, 6 Piece Screw Extractor & Screwdriver Set

Story

Read more

Custom parts and enclosures

pi-case_E2dWLR8JsA.stl

Sketchfab still processing.

Schematics

Signal Processing in rPPG.png

Code

Untitled file

Python
import os
import cv2
import numpy as np
import datetime
import matplotlib.pyplot as plt
import urllib.parse
import urllib.request
import http.client
from picamera2 import Picamera2
from scipy.fftpack import fft, fftfreq, fftshift
from scipy.signal import butter, filtfilt, savgol_filter

# Force the use of software rendering for OpenGL to avoid GPU issues
os.environ["LIBGL_ALWAYS_SOFTWARE"] = "1"

# ThingSpeak API Key for sending heart rate data
key = "LU7D22MJ8AWQ0XMA"
def send_to_thingspeak(hrate):
    """Send heart rate data to ThingSpeak cloud server."""
    params = urllib.parse.urlencode({'field1': hrate, 'key': key})
    headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain"}
    conn = http.client.HTTPConnection("api.thingspeak.com:80")
    try:
        conn.request("POST", "/update", params, headers)
        response = conn.getresponse()
        print(f"Heart Rate Sent: {hrate:.2f} bpm - Response: {response.status} {response.reason}")
        conn.close()
    except:
        print("Failed to send data to ThingSpeak")

def apply_chrom(rgb_seq, fs):
    """Extracts heart rate signal using the CHROM method."""
    rgb_seq = np.array(rgb_seq)
    B, A = butter(3, [0.7 / (fs / 2), 2.5 / (fs / 2)], btype='band')

    # Define chrominance signals
    X = 3 * rgb_seq[:, 0] - 2 * rgb_seq[:, 1]
    Y = 1.5 * rgb_seq[:, 0] + rgb_seq[:, 1] - 1.5 * rgb_seq[:, 2]

    # Apply bandpass filter
    Xf = filtfilt(B, A, X)
    Yf = filtfilt(B, A, Y)
    alpha = np.std(Xf) / np.std(Yf)
    S = Xf - alpha * Yf
    return S

# Initialize PiCamera settings
picam2 = Picamera2()
picam2.preview_configuration.main.size = (720, 480)
picam2.preview_configuration.main.format = "RGB888"
picam2.preview_configuration.controls.FrameRate = 30
picam2.configure("preview")
picam2.start()

# Load Haar Cascade for face detection
face_cascade = cv2.CascadeClassifier('/usr/share/opencv4/haarcascades/haarcascade_frontalface_default.xml')

# Initialize variables
frame_num = 0
RGB = []
time_stamps = []
bpm = 0
plt.ion()

# Start real-time heart rate detection
while True:
    frame = picam2.capture_array()
    gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
    faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))

# Process heart rate after collecting sufficient frames
if frame_num >= 300:
    N = 300
    fs = frame_num / (time_stamps[-1] - time_stamps[0]).total_seconds()
    T = 1 / fs

    # Extract CHROM signal
    rgb_segment = np.array(RGB[-N:])
    chrom_signal = apply_chrom(rgb_segment, fs)
    chrom_signal = savgol_filter(chrom_signal, window_length=31, polyorder=3)

    # Perform FFT to analyze frequency components
    yf = fft(chrom_signal) / np.sqrt(N)
    xf = fftfreq(N, T)
    xf = fftshift(xf)
    yplot = fftshift(abs(yf))

    # Identify dominant frequency in the valid heart rate range
    valid_range = (xf >= 0.75) & (xf <= 4)
    valid_indices = np.where(valid_range)[0]
    max_idx = valid_indices[np.argmax(yplot[valid_indices])]
    bpm_freq = xf[max_idx]
    bpm = bpm_freq * 60 + 30  # Convert frequency to BPM

Credits

Peifeng Xia
1 project • 0 followers

Comments