Back to Articles

Detecting Your Heartbeat Through a Webcam: How Photoplethysmography Works in Python

[ View on GitHub ]

Detecting Your Heartbeat Through a Webcam: How Photoplethysmography Works in Python

Hook

Your webcam can see your heartbeat—not metaphorically, but literally measuring the subtle color changes in your face as blood pulses through capillaries with each cardiac cycle.

Context

Before fitness trackers became ubiquitous, measuring heart rate required either physical contact sensors or specialized medical equipment. The discovery that remote photoplethysmography (rPPG) could extract heart rate from standard video cameras opened fascinating possibilities: imagine telehealth appointments that automatically capture vital signs, or gaming systems that adapt difficulty based on player stress levels.

The webcam-pulse-detector project, created by Tristan Hearn, demonstrates this principle using nothing more than Python, OpenCV, and your laptop’s webcam. It’s a masterclass in applied signal processing—taking the complex science of photoplethysmography and distilling it into roughly 500 lines of accessible code. While commercial implementations now use sophisticated deep learning models and multi-spectral analysis, this project reveals the elegant physics underneath: hemoglobin in your blood absorbs green light differently than surrounding tissue, creating tiny periodic variations in skin color that correlate directly with your pulse.

Technical Insight

Raw Frames

Face ROI

Forehead Pixels

Time-series Signal

Filtered Signal

Peak Frequency

Raw Signal

BPM Value

Augmented Display

Annotated Frame

Webcam Feed

Face Detection

Haar Cascade

Forehead Extraction

60% upper region

Green Channel Sampling

Mean Intensity

Bandpass Filter

0.8-3Hz

FFT Analysis

Frequency Domain

Heart Rate Calculation

BPM

Visualization Engine

Matplotlib

Screen Output

Video + Pulse Graph

System architecture — auto-generated

The architecture follows a classic computer vision pipeline, but the magic happens in how it transforms spatial data (video frames) into temporal signals suitable for frequency analysis. The application starts by using OpenCV’s Haar cascade classifiers to detect faces and isolate the forehead region—chosen specifically because it has minimal muscle movement and strong blood perfusion.

Here’s the core signal extraction logic:

def get_subface_coord(fh_x, fh_y, fh_w, fh_h):
    x = fh_x
    y = int(fh_y + fh_h * 0.5)
    w = fh_w
    h = int(fh_h * 0.6)
    return [x, y, w, h]

# Extract the green channel from forehead region
forehead = frame[y:y+h, x:x+w]
mean_val = np.mean(forehead[:, :, 1])  # Green channel index

The choice of the green channel isn’t arbitrary—hemoglobin’s optical absorption spectrum peaks in green wavelengths (around 540nm). As your heart pumps, blood volume in facial capillaries fluctuates, causing measurable changes in green light absorption. The application samples this mean green intensity value at approximately 20-30 FPS, building a time-series signal.

The real insight comes in the signal processing pipeline. Raw webcam data is incredibly noisy—ambient light flicker, compression artifacts, micro-movements all dwarf the subtle PPG signal. The solution is a carefully tuned bandpass filter:

def butter_bandpass(lowcut, highcut, fs, order=5):
    nyq = 0.5 * fs
    low = lowcut / nyq
    high = highcut / nyq
    b, a = butter(order, [low, high], btype='band')
    return b, a

# Filter to physiological heart rate range (0.8-3 Hz / 48-180 BPM)
filtered = butter_bandpass_filter(raw_signal, 0.8, 3.0, fps)

This bandpass filter (0.8-3 Hz) isolates frequencies corresponding to realistic heart rates while rejecting respiratory artifacts, high-frequency noise, and DC offset. The order-5 Butterworth filter provides a good balance between sharp cutoffs and minimal phase distortion.

Once filtered, the application applies Fast Fourier Transform to convert from time domain to frequency domain. The FFT reveals the dominant periodic component in the signal—your heart rate:

fft_data = np.abs(np.fft.rfft(filtered_signal))
freqs = np.fft.rfftfreq(len(filtered_signal), 1.0/fps)

# Find peak in physiological range
idx = np.where((freqs >= 0.8) & (freqs <= 3.0))
peak_freq = freqs[idx][np.argmax(fft_data[idx])]
bpm = peak_freq * 60.0

The application includes a clever visual feedback mechanism that makes the detected pulse tangible. It calculates the current phase of the heartbeat and modulates the forehead region’s color intensity accordingly—making your face literally pulse in sync with your heart. This isn’t just eye candy; it provides immediate qualitative feedback about signal quality. If the pulsing looks erratic, you know the detection is unreliable.

One sophisticated touch is the spike detection and automatic reset mechanism. The code monitors for sudden discontinuities in the signal that indicate user movement or lighting changes:

if abs(current_val - previous_val) > spike_threshold:
    # Reset buffer and restart collection
    data_buffer = []
    reset_flag = True

This prevents corrupt data from polluting the FFT analysis, though it means you need 15-20 seconds of continuous stable data before getting reliable BPM readings. The application essentially trades real-time responsiveness for accuracy—a reasonable tradeoff given the noise challenges.

Gotcha

The elegant theory crashes hard against reality. Lighting is the primary nemesis—fluorescent bulbs that flicker at 50/60 Hz create interference in exactly the frequency range you’re trying to measure. Direct sunlight causes oversaturation. Shadows from windows create gradual brightness changes that the algorithm interprets as extremely slow heart rates. In practice, you need controlled, diffuse, stable LED lighting to get consistent results.

Motion artifacts are equally problematic. Even breathing causes subtle head movements that dwarf the PPG signal amplitude. The Haar cascade face detector can jump between slightly different bounding boxes frame-to-frame, causing discontinuities in the sampled region. This is why the application includes a manual lock feature (press ‘S’)—you’re essentially admitting that automatic tracking isn’t reliable enough. Professional rPPG systems address this with dense facial landmark tracking (68+ points) and motion compensation algorithms, neither of which are present here.

The lack of validation is concerning for anyone considering adapting this code. There’s no ground truth comparison, no accuracy metrics against reference pulse oximeters, no dataset of test cases. The FFT peak-finding approach can lock onto harmonics or noise peaks, confidently reporting 140 BPM when your actual heart rate is 70. Medical-grade PPG requires calibration curves, individual baseline measurements, and extensive validation across skin tones—dark skin reflects less light, requiring exposure compensation that isn’t implemented here.

Verdict

Use if: You’re learning signal processing, computer vision, or physiological computing and want a hands-on example that demonstrates the complete pipeline from camera to FFT. This is excellent educational code that makes abstract concepts tangible. Also use if you’re prototyping rPPG features and need a simple baseline implementation to build upon—just understand you’ll need to replace most components for production use. Skip if: You need reliable heart rate measurement for any health, fitness, or commercial application. The accuracy is too dependent on environmental conditions, there’s no validation against medical standards, and the code hasn’t been updated for modern OpenCV patterns or deep learning approaches that significantly outperform classical signal processing methods. For production rPPG, look at commercial APIs like Binah.ai or academic frameworks like rPPG-Toolbox that provide benchmarked, validated implementations.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/automation/thearn-webcam-pulse-detector.svg)](https://starlog.is/api/badge-click/automation/thearn-webcam-pulse-detector)