> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

Reading Heart Rate Through a Webcam: A Deep Dive into Photoplethysmography with Python

[ View on GitHub ]

Reading Heart Rate Through a Webcam: A Deep Dive into Photoplethysmography with Python

Hook

Your webcam can measure your heart rate by detecting color changes in your face invisible to the human eye—variations of less than 2% in the green color channel caused by blood volume changes during each cardiac cycle.

Context

Traditional heart rate monitoring has always required physical contact: fingers on pulse points, chest straps, or optical sensors pressed against the skin. The medical field has long used photoplethysmography (PPG) in pulse oximeters, shining light through fingertips to measure blood oxygen and heart rate. But around 2008, researchers at MIT discovered that standard cameras could detect these same physiological signals remotely by analyzing subtle color changes in human skin.

This breakthrough—remote photoplethysmography (rPPG)—opened possibilities for non-contact vital sign monitoring using commodity hardware. The webcam-pulse-detector project by thearn demonstrates this technique in accessible Python code, targeting developers interested in computer vision, signal processing, or health tech applications. While similar implementations exist in research papers, this repository provides a practical, runnable implementation that visualizes the heartbeat detection process in real-time, making it valuable both as a learning tool and a foundation for experimentation.

Technical Insight

The architecture follows a elegant pipeline: face detection, region of interest isolation, temporal signal collection, frequency domain analysis, and visual feedback. At its core, the system exploits a physiological quirk—oxyhaemoglobin absorbs green light more efficiently than deoxyhaemoglobin, so as blood volume in facial capillaries fluctuates with each heartbeat, the green channel of a camera captures these variations.

The detection starts with OpenCV's Haar cascade classifier to locate faces and extract the forehead region, which provides the strongest PPG signal due to rich vascularization and minimal muscle movement. Here's the key signal extraction logic:

def extract_color(frame, face_rect):
    # Isolate forehead region (upper portion of detected face)
    x, y, w, h = face_rect
    forehead = frame[y:y + h//3, x:x + w]
    
    # Extract mean values for each color channel
    # Green channel contains the strongest PPG signal
    green_val = np.mean(forehead[:, :, 1])
    red_val = np.mean(forehead[:, :, 2])
    blue_val = np.mean(forehead[:, :, 0])
    
    return green_val, red_val, blue_val

These mean values get buffered over time—typically 10-15 seconds worth of frames at 15-20 FPS—creating a one-dimensional time series signal. The raw signal contains the cardiac component mixed with noise from respiration, head movement, and ambient lighting fluctuations. To extract heart rate, the system applies a Fast Fourier Transform to convert the temporal signal into the frequency domain:

def compute_heart_rate(signal_buffer, fps):
    # Normalize and detrend the signal
    normalized = (signal_buffer - np.mean(signal_buffer)) / np.std(signal_buffer)
    
    # Apply FFT to get frequency spectrum
    fft_data = np.fft.rfft(normalized)
    fft_freq = np.fft.rfftfreq(len(normalized), 1.0/fps)
    
    # Bandpass filter: human heart rate typically 0.8-3.0 Hz (48-180 BPM)
    freq_range = (fft_freq >= 0.8) & (fft_freq <= 3.0)
    fft_data_filtered = fft_data[freq_range]
    fft_freq_filtered = fft_freq[freq_range]
    
    # Find dominant frequency (peak power)
    peak_idx = np.argmax(np.abs(fft_data_filtered))
    peak_freq = fft_freq_filtered[peak_idx]
    
    # Convert to beats per minute
    bpm = peak_freq * 60.0
    return bpm

The frequency domain approach is crucial because it separates the cardiac signal (typically 0.8-3.0 Hz or 48-180 BPM) from respiratory artifacts (0.2-0.4 Hz) and high-frequency noise. The dominant frequency in this band corresponds to heart rate.

What makes this implementation particularly clever is the real-time visual feedback mechanism. Rather than just displaying a number, the system computes the phase of the detected frequency and uses it to modulate the brightness of the forehead region, creating a visible pulsing effect synchronized with the actual heartbeat. This phase-locked visualization serves as both user engagement and validation—if the forehead pulses smoothly and consistently, you know the algorithm has locked onto a real physiological signal rather than noise.

The code also implements automatic reset logic when signal quality degrades. If standard deviation spikes beyond thresholds or the buffer detects discontinuities, it clears the temporal buffer and restarts acquisition. This adaptive behavior prevents the system from displaying stale or incorrect readings when the user moves or lighting conditions change abruptly.

One architectural choice worth noting: the system processes only the green channel for heart rate calculation despite collecting RGB data. This follows established rPPG research showing green wavelengths (around 550nm) provide superior signal-to-noise ratio for detecting hemoglobin absorption changes in lighter skin tones. However, this wavelength sensitivity also represents a limitation for diverse populations—melanin absorbs more light across all wavelengths, reducing signal amplitude and making detection more challenging for individuals with darker skin tones. More robust implementations would incorporate multi-channel analysis or adaptive wavelength selection.

Gotcha

The elephant in the room is environmental sensitivity. This tool requires nearly laboratory conditions to function reliably: stable, bright lighting (preferably indirect sunlight or bright artificial light), a stationary subject, and a solid 15-20 seconds of clean data collection before producing meaningful results. Move your head slightly, and the system resets. A shadow crosses your face, it resets. Someone walks past a window and changes the ambient light, it resets again. This makes it impractical for real-world applications like fitness tracking during exercise or casual monitoring while working at a desk.

Accuracy is another concern that the repository doesn't address comprehensively. While the technique can detect heart rate, there's no validation against medical-grade equipment, no discussion of error margins, and no handling of edge cases like arrhythmias or very high/low heart rates. The simple peak-finding approach can occasionally lock onto harmonics (multiples of the true heart rate) or fail entirely in borderline lighting conditions. The code also currently processes only a single face despite detecting multiple faces in frame—a curious limitation given that the infrastructure for multi-person tracking already exists in the OpenCV cascade classifier. For anyone considering medical or research applications, this tool is a starting point for exploration, not a validated measurement device. You'd need extensive calibration, validation studies, and likely regulatory approval before using it in any healthcare context.

Verdict

Use if: You're learning about computer vision and signal processing integration, building proof-of-concept demonstrations of non-contact vital sign monitoring, or need a foundation for rPPG experimentation in controlled research settings. This codebase excels as an educational resource and rapid prototyping platform. Skip if: You need production-ready heart rate monitoring for fitness apps, medical applications, or any scenario with variable lighting and user movement. The 15-20 second stabilization requirement, environmental sensitivity, and lack of validation data make it unsuitable for commercial deployment. For serious health tech development, investigate research-grade rPPG libraries like pyVHR or rPPG-Toolbox, which implement more robust algorithms designed to handle real-world conditions, or consider commercial SDKs with regulatory clearances if medical accuracy is required.