The first time I tried to measure heart rate with a phone camera, the surprising part was not the algorithm. It was how visible the signal already was once the finger covered the lens and the flash. The preview looked like a soft red blur, but a tiny rhythm was hiding inside it.

MonoPump starts from that rhythm. The app is not recognizing a heart from a photograph. It uses the camera as a cheap optical sensor: shine light into the fingertip, watch how the reflected red channel changes, and estimate the beat interval from that change.
The observation that made it useful
When the flash is on and a fingertip covers the camera, blood absorbs and scatters light differently as vessels expand and relax. One frame is not meaningful. A few hundred frames form a waveform.
This diagram could not be rendered. Its source is still available below.
View diagram source
flowchart LR
A[Flash illuminates fingertip] --> B[Blood volume changes with each beat]
B --> C[Red-channel intensity shifts frame by frame]
C --> D[Build a time-series signal]
D --> E[Detect peaks and estimate BPM]
I expected the hard problem to be advanced signal processing. In practice, the earlier problem was more basic: the signal only exists when the finger covers the lens in a usable way. If the user floats the finger, presses too hard, leaks light from the side, or lets auto-exposure keep hunting, the waveform becomes a story about the camera, not the pulse.
Camera first, algorithm second
The capture path is deliberately plain: back wide-angle camera, BGRA frames, 30 fps, torch on at a moderate level, then exposure locked after the light settles. If exposure keeps adapting during the measurement, it can erase the small brightness changes we are trying to measure.
This diagram could not be rendered. Its source is still available below.
View diagram source
sequenceDiagram
participant UI as Measurement UI
participant C as CameraManager
participant F as Frame Analyzer
UI->>C: request camera frames
C->>C: configure 30 fps BGRA output
C->>C: enable torch and lock exposure
C-->>F: CMSampleBuffer stream
F-->>UI: coverage, waveform, confidence
A frame is not a heart-rate sample by itself. MonoPump samples the center region and computes average red, green, blue, brightness, red ratio, red dominance, and the fraction of pixels where red clearly leads.
redRatio = avgRed / max((avgGreen + avgBlue) / 2, 1)
redDominance = avgRed / max(avgGreen, avgBlue)
redIntensity = redRatio * (avgRed / 255.0)
The important trick is to separate two questions. Is this a plausible fingertip frame? If it is, what scalar value should join the pulse signal? Mixing those questions lets bad coverage frames poison the waveform.
The boundary
Camera PPG is useful, but it is not magic. Skin tone, finger pressure, temperature, camera hardware, flash heat, motion, and ambient leakage all affect the signal. MonoPump treats the result as a descriptive estimate with quality and confidence, not as a medical conclusion.