Sixth← Back to the instrument

How it works, and how well.

Every claim below is either a citation or a number this repository measures on itself. The measurements are produced by the test suite, so they cannot drift away from the code without the build failing.

The physics

Haemoglobin absorbs green light far more strongly than red. With every heartbeat the blood volume in the capillary bed just under your skin rises, and your face reflects fractionally less green. The effect is a few parts per thousand of pixel intensity — one or two least-significant bits of an ordinary 8-bit camera.

That signal sits underneath illumination drift, camera auto-gain, video compression and specular reflection from head motion, all of which are larger than it. Recovering it is the entire problem, and it was solved in the literature between 2008 and 2017. This implements three of those solutions and picks whichever is working best for you right now.

  1. Verkruysse, Svaasand & Nelson (2008) — remote plethysmographic imaging
  2. de Haan & Jeanne (2013) — robust pulse rate from chrominance (CHROM)
  3. Wang, den Brinker, Stuijk & de Haan (2017) — algorithmic principles of remote PPG (POS)
  4. Task Force of the ESC/NASPE (1996) — heart rate variability standards
  5. Orphanidou et al. (2015) — template-matching signal quality for PPG

The signal chain

  1. 01Face landmarks478-point mesh locates forehead and upper cheeks
  2. 02Skin maskevery pixel tested in YCbCr; hair, glasses and shadow excluded
  3. 03Resampleirregular browser frame delivery lifted onto an even grid
  4. 04ExtractionPOS, CHROM and green channel; best spectral SNR wins
  5. 05Band-passzero-phase Butterworth, 0.7 Hz to min(4 Hz, 0.4·fs)
  6. 06Beat timingpeak detection, then cross-correlation against an averaged beat
  7. 07Ectopic rejectionMalik criterion — intervals off the running median by >20%
  8. 08VariabilityRMSSD, SDNN, pNN50; spectral HRV; cardiac coherence
  9. 09Uncertaintypredicted noise floor subtracted in quadrature

None of it uses a machine-learning model. Every step between a camera pixel and a printed number is arithmetic you can read, in about two thousand lines of dependency-free TypeScript.

What we measured on ourselves

A source with perfectly constant beat intervals has an RMSSD of exactly zero. Anything this method reports from such a source is invented. That number is the honest ceiling on what any variability reading here can mean, so we measure it:

ConditionReported RMSSDTruth
Clean pulse, good light≈ 12 ms0 ms
Typical webcam conditions≈ 30–45 ms0 ms
Weak pulse, noisy sensor≈ 90 ms0 ms

A resting adult’s true RMSSD is 20–80 ms. The measurement error is therefore the same size as the thing being measured, which is why every consumer product built on webcam HRV that reports a bare integer is reporting mostly noise.

Because the error is predictable from signal quality, it can be subtracted. Each reading carries a floor estimated from its own beat-template consistency; the true value is recovered in quadrature, and when the measurement fails to exceed its own floor the interface says so instead of printing a number.

Choosing the error model

Three candidate predictors of the noise floor were fitted. The one that fits best is not the one shipped, and the reason is the most important design decision in the project.

PredictorFit (R²)Verdict
Frame rate≈ 0Dropped — no effect once quality is controlled
Spectral SNR0.915Rejected — not blind to real variability
Template consistency0.912Shipped

Heart-rate variability is frequency modulation of the cardiac peak: real variability spreads its energy into respiratory sidebands and therefore lowers spectral SNR. Measured on a synthetic source, adding 100 ms of genuine sinus arrhythmia cost 2.7 dB and would have inflated the predicted error by 36%. An error model built on SNR widens its own uncertainty in proportion to the physiology it is supposed to be detecting — penalising exactly the people whose signal is healthiest.

Template consistency measures whether each detected beat has the shape of a pulse after alignment has removed rate. The same 100 ms of arrhythmia moved it by 0.000.

When it refuses

Two independent conditions must both hold before any number is shown, combined as a geometric mean so neither can compensate for the other:

  • Spectral SNR — is the energy concentrated at a plausible cardiac frequency? Rules out broadband noise.
  • Template consistency — do the detected beats share a pulse shape? Rules out narrowband artefacts, which is what band-passed static looks like.

Band-passing white noise to 0.7–4 Hz produces something locally sinusoidal whose “beats” correlate beautifully with their own average. A system scored on template consistency alone will confidently read a heart rate off a blank wall. Variability is gated more strictly still: it needs 25 seconds of clean beats, since RMSSD is built from squared successive differences and detection jitter enters it directly.

Limits

  • This is not a medical device. It cannot diagnose anything and must not be used to make a health decision.
  • Accuracy depends on light. A soft source in front of you is good; overhead-only light leaves the forehead in shadow and will degrade or refuse the reading.
  • Motion is the dominant error source. The chrominance methods suppress it; they do not eliminate it.
  • Absolute RMSSD from a webcam is not defensible at consumer frame rates, which is why the session reports a change within one sitting — where conditions are fixed and much of the error is common to both readings — rather than an absolute number.
  • Validation so far is against synthetic sources with known ground truth. Agreement against contact PPG on the public rPPG datasets is the obvious next step and has not been done.
  • The session can and does report "no change this instrument can vouch for". That is a correct outcome, not a bug.

Privacy

No video, no frame and no measurement leaves your device — not as a policy, as a property. The page is served with connect-src 'self', so there is no origin it is permitted to open a connection to. The face model, the WebAssembly runtime and the fonts are all served from this origin; no third party is contacted at any point. You can verify all of it from the response headers and the network tab in under a minute.