← cs
$ cat projects/HydRation.md

PPG-Based Heart Rate & Hydration Monitor

Non-invasive physiological monitoring using photoplethysmography signal analysis from video recordings.

2024-02-15
PythonOpenCVSciPyFirebaseNumPy

PPG-Based Heart Rate & Hydration Monitor

Pulls a heart rate out of an ordinary phone video of someone's face or fingertip, then tries to say something about their hydration from the shape of the pulse waveform. No contact, no sensor, just a camera.

Skin brightness changes very slightly with every heartbeat as blood perfuses the tissue underneath. The signal is there in any video; it is just buried under lighting, motion, and compression noise.

Getting a pulse out of RGB

The CHROM method works by exploiting the fact that a blood volume change and a motion artifact do not affect the color channels the same way, so a particular combination of channels cancels most of the motion while keeping the pulse.

  1. Normalize R, G, B
  2. Compute chrominance: Xs = 3R - 2G, Ys = 1.5R + G - 1.5B
  3. Temporally filter to remove DC
  4. Isolate the pulse: S = Xs - (σ(Xs)/σ(Ys)) × Ys

From there it's a bandpass at 0.7 to 4.0 Hz (42 to 240 BPM, the physiologically plausible range) and derivative-based peak detection. Heart rate lands within about ±3 BPM of a reference device.

Video -> Frame Extract -> ROI (center) -> RGB Extract -> CHROM
  -> Bandpass (0.7-4.0 Hz) -> Peak Detect -> HR + TPA/VPA -> Classify

The first and last three seconds get dropped, since autoexposure and autofocus are still settling and they poison the signal.

The hydration part

This is the speculative half. The ratio of total pulse area (the area under one pulse cycle) to valley-to-peak area (the area between the upper envelope and the signal, which tracks vascular compliance) shifts with hydration state:

| TPA/VPA | Reading | |---|---| | < 0.559 | Severe dehydration | | 0.559 - 0.815 | Mild dehydration | | 0.815 - 1.326 | Normal | | > 1.326 | Overhydration |

I have not found this ratio used in a commercial device, and I have also not validated it against urine specific gravity on a real cohort, which is the only thing that would make it a claim rather than a hypothesis. The heart rate numbers I trust. The hydration numbers are interesting and unproven, and the repo says so.

Running it

python -m src.main video.mov --save-trace trace.png
python -m src.main *.mov --output results.json
python -m src.main video.mov --show-plot

There is also a Firebase mode that watches a Storage bucket for uploads, processes each new video, and writes results back to Firestore, which is how it fed a phone app.

Processing a 30-second video takes about 10 seconds on an M1 MacBook Pro. Everything is vectorized NumPy over a streaming frame reader, so a long video never gets fully loaded into memory.

What limits it

Lighting dominates everything. Consistent diffuse light gives a clean trace; a window behind the subject gives noise. The CHROM coefficients are also calibrated for a narrower range of skin tones than they should be, which is a known failure mode of the method and a real fairness problem, not a footnote. Motion beyond about 5mm degrades the signal badly enough that face tracking is the obvious next thing to build.