The Variability the Camera Invents

Press a fingertip over your phone's rear camera and the light coming back through your skin carries your pulse: each beat drives more blood into the finger, the blood absorbs more of the light, and the level dips. This page reads that, after first showing the same code recovering a known rate from a real smartphone recording whose answer came from an electrocardiogram running alongside it. Then it refuses to give you the second number every pulse app sells, and shows you the arithmetic of the refusal.

This is not a medical device. It performs no diagnosis, no screening and no assessment of your fitness or your health. It will not tell you whether a number is normal, and there is no number it could print that would mean anything about you clinically. It is an instrument for looking at an instrument.

Your fingertip

Cover the rear lens completely with the pad of a finger, firmly enough to seal out the room and gently enough not to squeeze the blood out. This page asks for the torch in the constraints it opens the camera with, and most browsers ignore that. It does not then ask a second time on the camera track itself, which is the other route the API allows and the one WebKit's own test for the feature uses, so where the table below reports no torch, some of that is this page rather than your phone. A bright lamp shining onto the back of your finger is the usual substitute. On a laptop webcam this will almost certainly refuse, because there is no light passing through anything.

Checking the direction of the dip against the shipped recording.

The live measurement

Checking what this browser exposes.

not measured

Nothing has been captured. Everything else on this page has already run.

What your browser actually gave, rather than what was asked for.
nothing captured yet

The one comparison this page does not control

Find your pulse at your wrist or your neck, count the beats for thirty seconds, and type the count in. Everything else here is a number we computed from photons and then checked against numbers we also chose. This one is yours.

30.0

No count entered yet.

Four things this page will not attempt, by name. It will not estimate blood oxygen saturation. It will not look for an irregular rhythm or flag one. It will not estimate blood pressure. It will not produce a stress score, a recovery score, or anything of that shape. Each of those is something readers reasonably expect from a fingertip and each of them needs either a second wavelength, a calibration against a cuff, a clinical rhythm standard, or all three. Saying which four are excluded is more useful than a disclaimer, because it is checkable.

Before it may report yours

A page that measures your own body has a problem no arithmetic page has: you have no second instrument. You cannot tell a working estimator from a plausible one, and a hard coded constant would look exactly like a measurement. So the same estimator, unmodified, first runs on recordings whose answers were established by somebody else, with an electrocardiogram, before this page existed.

Two of the three come from the Brno University of Technology Smartphone PPG Database: real fingertip videos taken on real phones, reduced by its authors to three per frame colour channel means at 30 Hz, each shipped alongside a simultaneous single lead electrocardiogram sampled at 1000 Hz. The reference rate is the median rate over the ten second window, from QRS detection on that electrocardiogram. We did not generate it and could not have influenced it. The third specimen we did generate, and it is there to break things.

Run in your browser just now, by the same estimate() that would measure you.
roleidwhat it isexpectedmeasuredtolerance
running

Running the slate.

And two records it must stay silent about

An estimator that always produces a number is not an instrument, it is a random number generator with good manners. So the slate has a second half: recordings where the correct output is nothing at all. These pass by returning no rate, and the check is for the absence of the value, not for a flag beside it.

The control on the control

All of that would still be theatre if the specimens could not tell a real estimator from a fake one. So here is a deliberately hard coded estimator, one that always returns the anchor's published 54 bpm with a tight error bar, run over the identical slate.

roleidwhat the constant did
running

running

The trap that doubles a heart rate

A fingertip pulse is not a sine wave. It has a sharp systolic upstroke and a second, smaller bump as the aortic valve closes, and a waveform with a bump like that has real energy at twice the heart rate. If that second line is ever the taller one, the tallest line in the spectrum is not the answer, and an estimator that reports it anyway hands a perfectly calm person a rate of 144.

This specimen is built to have exactly that property: the generator was handed a second harmonic larger than its fundamental. It is synthetic, and that is the point. A generated failure is a guarantee and its true answer is exact rather than measured, so it can prove the estimator survives the case. What it cannot tell you is how often the case arises, and the page used to assert that too. So here is that measured instead, over the same 200 records, with each record's own manually verified beat marks giving the rate to look at.

running

Take the 432 colour channels of those 200 records where twice the reference rate still falls inside the search band, and compare the power at the rate with the power at twice it. The second harmonic is the taller of the two on 20 of those channels, and 0 of the 20 belong to a record these quality gates accept. So the bug is real in principle, and rare enough here that the specimen demonstrating it had to be manufactured: on this corpus the condition never once arises in a recording good enough to report. That is a weaker sentence than the one this paragraph replaced, and it is the one the data supports.

The harmonic census has not run yet.

Across all 200 shipped records the same test fired on 27 channels in 20 of them, and it is not free: on 26 of those 27 channels the halved rate was further from the electrocardiogram reference than the unhalved one, because on a poor recording the half rate line it keys on is usually noise rather than a fundamental. Every one of those records was refused by the quality gates anyway, so on this sample the test never changed a number the page would have printed. Insurance against a failure that is certain in principle, was not decisive in these 200 cases, and costs accuracy on the records where it does fire: all three of those are worth saying.

Those three counts have not been rechecked yet.

One word, fourteen beats a minute

Before any spectrum, the slow drift has to come off the signal: a finger settles, a hand warms, the automatic exposure hunts. This page subtracts a two second centred moving average. At the very start and the very end of a short record that average has no data on one side, and what you decide to do about that is a choice nobody writes down.

Reflect the record at the edges and the baseline is still built from real samples. Pretend the record is surrounded by silence, which is what a plain same mode convolution does and what most quick implementations do, and the baseline near each edge is far too small, leaving a large slow swing that lands at the bottom of the search band and gets reported as a pulse.

Record 133007, published reference 54 bpm, same estimator, one word changed.
edge conventionredgreenblue
running

running

What the gate costs, measured

Four things must all hold before this page prints a rate. The three colour channels must agree with each other to within 3.0 bpm, because three independent optical channels looking at the same blood should. The in band signal to noise must reach 4.0. The modulation index, which is the amplitude of the fundamental over the channel's mean level, must stay under 3.0 per cent, because a real fingertip pulse moves the light by tenths of a per cent and anything larger is your finger moving. And the trace must correlate with itself one beat later at 0.35 or better, because a thing that does not repeat at the rate you are about to report is not a pulse.

Before any of that, the calibration the dataset's own authors provide. The database holds 3,888 recordings, and expert consensus judged 830 of them usable for estimating a heart rate. That is not a failure rate for people trying their best, and the dataset's own subject file says why: it is a quality benchmark built to contain bad recordings on purpose. 1,962 of the recordings are of an ear held against the front camera rather than a finger on the rear one, and 1,164 were made while the subject was walking, coughing, laughing, talking, pressing harder, moving the skin across the lens or changing the light, each an artefact the authors induced deliberately. Narrow it to the condition you are about to be in, a finger at rest, and 877 of 1,354 recordings still could not be read to within 5 bpm of the electrocardiogram by a majority of the three or five experts who tried. About two in three. Refusal is the ordinary outcome here, not the failure case.

Counting the shipped annotation and subject files.

Loading the sample.

Running that over the held out half, live, in your browser, right now:

starting the sweep over 100 records

Every record the gate accepted, worst error first. Quality is the dataset's own expert consensus label, which this page never sees before deciding. The two reference columns are the two the dataset ships, and the section below is about the fact that they disagree.
recordqualityref, HR columnref, beat marksthis page error vs columnerror vs marksSNRspreadrepeat
running

Of 100 held out records the gate accepted 15 and refused 85. Of those accepted, 13 were within 5 bpm of the electrocardiogram reference and 2 were not. It also refused 24 records the dataset's annotators had called usable, which is what the refusals cost. One of the fifteen it accepted is record 133007, the anchor this page picked as its known-answer specimen; drop it and the figures are 14 accepted, 12 of them good.

Those hundred are not a hundred of what you are about to do, and the difference cuts in this page's favour. 9 of them are recordings of an ear, and the gate accepted 0 of those; 24 were made under an induced artefact. Take only the 69 that are a finger at rest, which is the condition you will be in: the gate accepted 14 of them and 13 came back within 5 bpm, the single miss by 5.9 bpm. That cuts the other way too, and this is the half the page would rather not have found: it still refused 55 of those 69. Most of the refusals are not the ear recordings or the walking. Most of them are somebody sitting still with a finger on the lens, getting nothing.

running

The figures in the paragraph above have not been rechecked yet.

Two references, and they do not agree

All of that rests on the words the electrocardiogram reference, and the dataset ships that reference twice. One is the HR column of the annotation file: one integer per recording, and the column the dataset's own quality label is defined against. The other is a file of beat marks for each record, which the database's ANNOTATORS file describes as "reference QRS complex annotations from ECG, manually verified". The first is what the figures above are scored against, because it is the published reference. The second ships here too, decoded in your browser from ./specimens/butppg-qrs.bin, and it does not always say the same thing.

running

On 44 of the 200 shipped records the two references differ by more than 5 bpm, which is the whole width of this page's own accept criterion. Scored against the annotation column 13 of the fifteen accepted records are good; scored against the manually verified beat marks it is 14. This page reports both and adjudicates neither: it did not make either reference and has no way to know which one the dataset's authors meant.

running

Those four figures have not been rechecked yet.

So read your own number accordingly. The page prints a rate when its four gates pass, and on the held out records two of the fifteen it accepted were more than 5 bpm from the annotation column, one of them by 57.8 bpm; against the dataset's other reference, its manually verified beat marks, it is one of the fifteen. Both of the two carried a quality label of unusable, written by expert annotators, which the estimator cannot see and neither can you; and the 57.8 bpm miss was recorded while its subject was walking, which you can see perfectly well. Sit still. This is the honest position: here is a rate, and here is why you should not lean on it. A 2024 scoping review of resting heart rate from contact-based smartphone photoplethysmography found correlations against electrocardiography of r = 0.98 to 1, with limits of agreement running from 0.29 bpm in the best case to minus 5.58 and plus 5.52 bpm in the widest. Those come from supervised recordings with cooperative subjects, and they are context for what the technique can achieve rather than a claim about what just happened in your hand.

Green, red, blue, and four numbers instead of an argument

Verkruysse and colleagues showed in 2008 that in ambient light, reflected off a face, the green channel carries the strongest plethysmographic signal, at an absorption peak of haemoglobin, oxygenated and not. Their own wording is "corresponding to an absorption peak by (oxy-) hemoglobin", and the parenthesis is doing real work: near 540 nm both forms absorb, so this is not an oxygen measurement and nothing here becomes one. Contact geometry is a different problem: the light goes through the finger rather than bouncing off it, and red passes through flesh far more easily than green does. The Brno authors annotated from red. Both choices are defensible, and rather than pick a side here are the numbers from the records this page accepted.

Mean over the accepted held out records. DC is the channel's mean level out of 255. AC is the amplitude of the fundamental, read off the windowed spectrum with the window's own gain divided back out, so AC over DC is the fractional depth of the pulse at the heart rate and not a peak to peak swing that would also count the noise. Saying which one it is matters: on the anchor's blue channel the amplitude is 0.267 per cent of the level and the peak to peak swing of the same detrended trace is 0.737, so the two conventions put the modulation gate in different places.
channelmean DCmean AC/DClargest fractional pulse in
running

running

The number this page will not give you

Measuring a heart rate from a phone camera is a fifteen year old consumer feature and there are a thousand apps that do it. Many of them also sell a second number: heart rate variability, usually RMSSD, the root mean square of successive differences between beats, dressed up as recovery or stress or readiness. That number they cannot honestly have, and the reason is arithmetic rather than opinion.

A camera samples on a grid. At 30 frames a second the spacing is h = 33.33 ms, and every beat time you extract is rounded onto it. If those rounding errors are independent and uniform on plus or minus h/2, each has variance h²/12, and the error on a successive difference of differences is −ei + 2ei+1 − ei+2, with variance 6σ². Carry that through and the measured RMSSD is not the true one but

RMSSD_measured = sqrt( RMSSD_true^2 + h^2 / 2 )

so a perfectly steady heart, with no variability whatsoever, reads as sqrt(33.33²/2) = 23.6 ms. Usually. The panel just below is about the word usually, and it is the part of this that took longest to find. For scale: across the 15 studies that reported RMSSD in Nunan's review of 44 studies covering 21,438 healthy adults, the average reported RMSSD was 42 ms and the study averages themselves ran from 19 to 75 ms. That is the whole span of published healthy adult averages, and the number a 30 frame per second camera manufactures out of nothing lands inside it, near the bottom. It is a spread of study averages rather than a range that individual people occupy, which is exactly why it is quoted here as a scale to compare a fabricated 23.6 against and never as a band to place a reader in.

Checking what a ten second window does to that statistic.

Except that 23.6 is a typical value, not a floor

That derivation assumes each beat's rounding error is independent of its neighbours. A perfectly steady heart is the one case where it certainly is not: its beats march around the frame grid in lockstep. If its interval happens to be a whole number of frames, every beat lands at the same point between two of them, every rounding error is identical, the differences are exactly zero, and the invented variability is not 23.6 ms but nothing at all. So rather than assert the floor, here it is computed, at every rate, at whatever frame rate is selected in the panel below.

Left to right, the rate of a heart with no variability at all, 40 to 200 beats a minute. Up and down, the variability a camera at that frame rate would report for it. The dashed line is the closed form. Every cusp that touches the floor of the picture is a rate whose beats fall a whole number of frames apart.

running

running

So an app that takes each beat to be the frame its maximum landed on is biased upward at most rates, by roughly the amount the closed form says, and the exceptions are not rare curiosities: they are the round numbers. An app that interpolates between frames escapes the grid altogether, and runs straight into a second floor that a faster camera does not move. Both are below.

Drive it yourself

A series of 300 beats is generated with an exactly known RMSSD, rendered as a pulse sampled at the frame rate you choose, and measured back two ways. The single humped pulse shape is deliberate: with a realistic dicrotic shoulder the peak picker finds two maxima per beat and what you would be watching is the picker, not the grid.

frame rate

true RMSSD, exact by construction
what the frame grid alone adds, sqrt(h²/2)
closed form prediction
exact beat times, simply rounded onto the grid
measured, beat at the nearest frame
measured, beat refined parabolically
beats found
frame rate

running

running

Two things fall out of that panel, and the second one is the part nobody says out loud.

First, the closed form is right, and only over part of its range. Set the noise to zero and compare the rounded row with the predicted row: they agree closely for a true RMSSD of about 15 ms and above. Below that they part company, and the prediction is too large. The model assumes the rounding errors are independent, and when the real variation is small compared with a third of a frame the successive beats keep landing in the same cell, the errors stop being independent, and the formula overstates the damage. At a true 5 ms and 30 fps the closed form says 24.1 ms and the actual rounding gives about 15.7, averaged over six independent runs of 600 beats. A single run is much noisier than that in this regime, and the panel above runs one series of 300 beats, so its rounded row sits at 18.7 at that setting rather than at the average. Both numbers are far below the 24.1 the formula predicts, which is the point. A limit of the model, stated rather than hidden.

Second, the obvious remedy works, and then stops working. If the frame grid is the problem, do not take the beat to be the frame the maximum landed on: fit a parabola through that sample and its two neighbours and take the vertex, which places the beat between frames. Turn the noise to zero and it works completely, at every frame rate, and the 23.6 ms floor simply disappears. Now turn the noise up. At 10 per cent of the pulse amplitude the parabolic estimate is worse than the floor it was meant to remove, and raising the frame rate hardly helps, because what is left is not grid quantisation but noise on the three samples the parabola is fitted through. There are two floors. Only one of them retreats when you buy a faster camera, which is the part a specification sheet will not tell you.

Parabolic refinement of the fiducial point is the standard remedy for a coarse sampling grid, and the 1996 Task Force report on heart rate variability is the usual citation for it at low sampling rates. We are citing that report rather than quoting it: its full text sits behind publisher paywalls that returned 403 to every retrieval we attempted on 2026-08-17, and again to every retrieval we attempted while writing this sentence. So we have not read it in the original, we cannot tell you what lower bound it puts on a usable sampling rate, and nothing here should be read as that report endorsing this trick at 30 frames a second. The arithmetic above does not depend on it, and the refinement that actually matters on the electrocardiogram below turns out to be a different one.

And on a real person, not a simulation

The anchor recording ships with its own electrocardiogram at 1000 Hz, where a beat can be placed to well under a millisecond. So the true variability of that person, over those ten seconds, is knowable. Three rows below, rather than one, because it would be easy to hand the credit to the wrong step: what the beat detector's own marks give, what the same beats give once each is moved onto the R wave to the nearest sample, and what the parabola through that sample and its neighbours adds. At 1000 Hz the sample grid can only put sqrt(h²/2) = 0.71 ms into a beat time, so the parabola cannot be worth much here, and it is not: the whole of the improvement is the move onto the R wave.

running

running

Two things this arithmetic is not the first to say

The direction of this effect is published. Choi and Shin took finger recordings sampled at 10 kHz, decimated them, and measured what the sampling rate did to the variability statistics; their conclusion, verbatim from the abstract, is that pulse rate variability "may be potentially as reliable as that derived from the analysis of the electrocardiogram, provided that fs ≥ 25 Hz sampling frequency is used", with RMSSD among the variables that differed significantly only at 15 Hz and below. That is a real tension with the number above and this page is not going to hide it: theirs is a significance test on 28 subjects, this page's is the size of a term, and the panel above shows how both can hold at once. Turn the noise to zero, where a decimated laboratory waveform lives, and interpolation removes the grid completely. Turn it up to what a fingertip on a phone lens actually gives, and it does not.

And the second one is a limit no frame rate reaches. The intervals a camera measures are pulse intervals, not the heart's own: the time from a beat to the arrival of its pressure wave at your fingertip varies with blood pressure and with breathing, so pulse rate variability is not heart rate variability even with a perfect clock. The review of studies comparing the two concludes that the agreement is good enough at rest but that "short-term variability is somewhat overestimated by PRV, which reflects coupling effects between respiration and the cardiovascular system", and that movement breaks the agreement "often to an inacceptable extent". So the 240 frames a second button above does not lead anywhere better in the end; it just moves the wall.

So this page reports no RMSSD for you, at any frame rate, and that is a design decision rather than a limitation of the code. Even setting the floor aside, the conventional short term standard for a variability measurement is five minutes, this page requires at least 60 seconds and 30 accepted beats before it would consider one, and a twenty second fingertip capture gives neither. A number that is mostly the instrument, quoted against a normal range it was not measured on, would be worse than no number, and it is the number that most often gets sold.

The check

Every figure above was computed in your browser from the files in ./specimens/ by ppg.js, the same module that would measure your fingertip. Nothing on this page is a stored result. The sweep table filled in while you watched, and the paragraph that quotes its totals is audited against it above.

Everything the estimator is free to choose, in one list. These are the knobs. Changing any of them changes the answer, which is why they are printed rather than described.

loading

What is uncertain, named. The interval on a live rate combines half the width of the spectral line with the spread of the estimate across overlapping sub windows; it is a precision, not an accuracy, and the held out sweep above is the accuracy. The anchor's published reference is an integer, so it carries half a beat of rounding before anything else, and that record's own beat to beat rate ranges from 53.7 to 55.5 bpm, so there is no single true value to hit. That anchor also falls inside the held out half, so it is one of the fifteen records the sweep accepted and the paragraph beside that sweep says so. The reference itself is uncertain in a way the page could not have found without shipping the dataset's second copy of it: on 44 of the 200 shipped records the annotation column and the manually verified beat marks disagree by more than 5 bpm, so the accuracy figure above is quoted against both and against neither alone. A ten second record has a raw spectral bin of 6.0 bpm and the estimates are finer than that only because of parabolic interpolation on the line. The RMSSD of the anchor's electrocardiogram rests on eight intervals, so its standard error is around a quarter of it. The four gate thresholds were fitted on the declared fitting split and the numbers quoted are from the held out split, but 100 records is a small sample and the binomial interval is printed with it. The 200 record sample covers only the three channel layout, so nothing here is a statement about the older single channel records or about the database as a whole.

What the harness could not test. The live path is driven end to end in a real browser by scripts/check-live-sensor.mjs, which replaces the camera with a generated video of exactly known rate and checks that the page reports it, and that a second video at a different rate moves the answer. That covers getUserMedia, the frame clock, the pixel reduction and the estimator. It does not cover a torch, an automatic exposure loop fighting the signal, or a real finger, none of which exist in a headless browser.

Recompute it away from this page. node research/ppg-pulse/verify-ppg-pulse.mjs reruns every number above from the shipped files, regenerates the synthesised specimens and compares their hashes, re-derives the dataset counts from the shipped annotation and subject files, decodes the dataset's own beat marks and scores the sweep against both of its references, sweeps the invented variability across every rate to find where the closed form is zero, re-measures where in the beat the light dips, and checks the refusal paths. node scripts/check-live-sensor.mjs --only=ppg-pulse drives the camera path.

Sources. Nemcova A, Smisek R, Vargova E, Marsanova L, Vitek M, Smital L, Filipenska M, Sikorova P, Galik P. Brno University of Technology Smartphone PPG Database (BUT PPG), version 2.0.0, PhysioNet, 2024, doi:10.13026/tn53-8153, and the associated paper in BioMed Research International 2021, article 3453007, doi:10.1155/2021/3453007. The site and motion columns counted above are the dataset's own subject-info.csv, and the meaning of a quality label (a majority of the 3 or 5 annotators landing within 5 bpm of the electrocardiogram) is the definition given in that database's description, read at source on 2026-08-17. The beat marks in ./specimens/butppg-qrs.bin are that database's own per-record .qrs annotation files, unaltered and simply concatenated, described in its ANNOTATORS file (which ships beside them) as "reference QRS complex annotations from ECG, manually verified". The records, the annotation file and the subject information file shipped here are redistributed under Creative Commons Attribution 4.0 International, verified at source on 2026-08-17; the licence text ships beside them in ./specimens/LICENCE. Verkruysse W, Svaasand LO, Nelson JS, "Remote plethysmographic imaging using ambient light", Optics Express 2008;16(26):21434-21445. For which way the light moves, Kamshilin AA, Nippolainen E, Sidorov IS, Vasilev PV, Erofeev NP, Podolian NP, Romashko RV, "A new look at the essence of the imaging photoplethysmography", Scientific Reports 2015;5:10494, doi:10.1038/srep10494, read at source 2026-08-17 via PMC4440202: "the intensity of the back-reflected light is maximal in the end-diastole phase, which is well-established fact". Worth knowing that this particular paper disputes the usual blood volume explanation of the effect and proposes a mechanical one; it does not dispute the direction, which is all this page uses. For the torch, WebKit commit 9c0b105d80212675839a00325b58df55f408f1c0, "[MediaStream] Add support for torch", dated 2023-10-04, read at source 2026-08-17: it adds torch to a video track's capabilities, and its own layout test switches the light on with track.applyConstraints({torch: true}) after the stream is open. Task Force of the European Society of Cardiology and the North American Society of Pacing and Electrophysiology, "Heart rate variability: standards of measurement, physiological interpretation, and clinical use", Circulation 1996;93(5):1043-1065 (cited, not quoted: the full text was not retrievable on 2026-08-17). Shaffer F, Ginsberg JP, "An Overview of Heart Rate Variability Metrics and Norms", Frontiers in Public Health 2017;5:258, doi:10.3389/fpubh.2017.00258, Table 6, reporting Nunan et al.'s review of 44 studies and 21,438 healthy adults. Read at source 2026-08-17. Its columns are "HRV measure, Mean (SD), Range, Studies" and its RMSSD row is "42 (15), 19-75, 15", so every one of those numbers is a statistic over 15 published studies and not over people: 42 ms is the mean of the study means and 19 to 75 ms is their spread. This page quotes it as a scale and never as a normal range. Choi A, Shin H, "Photoplethysmography sampling frequency: pilot assessment of how low can we go to analyze pulse rate variability with reliability?", Physiological Measurement 2017;38(3):586-600, doi:10.1088/1361-6579/aa5efa, whose abstract was read at source on 2026-08-17 through Europe PMC's API, from which the sentence quoted above is taken verbatim; the full text sits behind a paywall and this page quotes only the abstract. Schäfer A, Vagedes J, "How accurate is pulse rate variability as an estimate of heart rate variability? A review on studies comparing photoplethysmographic technology with an electrocardiogram", International Journal of Cardiology 2013;166(1):15-29, doi:10.1016/j.ijcard.2012.03.119, abstract likewise read at source 2026-08-17 through Europe PMC, and the two quoted phrases are from it. Mather JD, Hayes LD, Mair JL, Sculthorpe NF, "Validity of resting heart rate derived from contact-based smartphone photoplethysmography compared with electrocardiography: a scoping review and checklist for optimal acquisition and reporting", Frontiers in Digital Health 2024;6:1326511, doi:10.3389/fdgth.2024.1326511, for the smartphone contact PPG correlations and limits of agreement; read at source 2026-08-17, where the widest limits tabulated are Nam et al.'s minus 5.58 to plus 5.52 bpm.

One dry note, since it is the same arithmetic. Run this on a face at a distance instead of a finger on a lens and it is remote photoplethysmography, which is why the technique has a surveillance literature as well as a clinical one. This page runs it only on a finger you pressed to your own lens, the frames never leave the tab, and nothing is stored.