Inspiration

There is a neck test that takes four minutes and needs no equipment. You sit facing forward, close your eyes, turn your head as far as is comfortable, and return to where you started. How far off you land is your joint position error, and it is a recognised research and clinical measure of cervical proprioception — reported as elevated in whiplash-associated disorder, chronic neck pain, and after concussion.

Plenty of apps will happily estimate your head angle from a webcam. Almost none of them will tell you whether the number they gave you last week and the number they gave you today are actually different.

That is the whole problem. Proprioceptive error is a few degrees. Webcam pose estimation error is also a few degrees. Without a measurement-error model, any app reporting "you improved by 1 degree" is reporting noise with a confident font.

What it does

A guided, on-device cervical JPE test that measures its own uncertainty before it reports yours.

1. It measures its noise floor first. Head pose comes from 468 MediaPipe face landmarks running entirely in the browser. A hold-still calibration estimates the angular standard deviation of the estimator under your actual camera, lighting and posture. If that noise floor exceeds 0.90 degrees, no trial is accepted at all — the app refuses rather than producing a number it cannot stand behind.

2. It gates the geometry. Distance (too near, too far), centering, illumination, excessive head roll and landmark confidence are all checked continuously, and a trial cannot start until every gate is green. Rejected trials are counted and shown, not silently dropped.

3. It runs the real protocol. Eight trials across left rotation, right rotation, extension and flexion. Spoken cues and tones mean the test can genuinely be performed with the eyes closed, which is the entire point of the test. It reports absolute error, constant error and 3D angular error, aggregated per direction with 95% confidence intervals, plus a polar scatter of where your returns actually landed around neutral.

4. It answers the only question that matters. Across sessions it computes within-subject SEM, ICC(2,1) (two-way random, absolute agreement, single measurement, implemented from the formula), and MDC95 = 1.96 x sqrt(2) x SEM. Then it says out loud, for example: "Your mean JPE improved by 1.0 degrees; MDC95 is 1.5 degrees, so this change is NOT distinguishable from measurement noise."

That honest negative is the product. It is what a rep-counter will never tell you.

How we built it

Vanilla JavaScript. MediaPipe Tasks-Vision and the face_landmarker.task model are vendored locally — no CDN at runtime — and every frame is processed on device. No image, no landmark, and no result ever leaves the machine, and the UI states that where the user can see it.

Pose is recovered from the facial transformation matrix and converted to yaw/pitch/roll in degrees. The statistics layer implements SEM, ICC(2,1) with its full mean-squares decomposition, and MDC95, with sessions persisted in IndexedDB.

A validation panel measures the estimator against ground truth by applying known rotations to the canonical face mesh and recovering them through the same pipeline.

For demonstration there are labelled synthetic replay streams that push generated landmark trajectories through the identical estimator, gates and statistics as the live camera. Whenever a replay is active a banner says so, and nothing on screen is presented as a measurement of a real person.

Challenges we ran into

The protocol runner had a phase clock anchored to the first frame's timestamp. If calibration was started before any frame had arrived, that clock was NaN, every elapsed-time comparison silently evaluated false, and the runner hung forever with no error. It looked exactly like a slow camera. Anchoring the phase clock to the first frame actually observed fixed it.

The deeper challenge was resisting the urge to make the numbers look good. Our seeded demo history produces a negative ICC, which is a real result when between-target variance is small relative to error. We show it, with the note explaining what it needs to be meaningful, rather than quietly swapping in a friendlier statistic.

Accomplishments we're proud of

  • Estimator validated against synthetic known-angle ground truth over 693 poses: mean 3D error 2.03 degrees, yaw MAE 1.29, pitch MAE 1.09, p95 3.76.
  • Measured noise floor of 0.31-0.34 degrees on a clean stream, against a hard 0.90 degree acceptance ceiling.
  • A full 8-trial run producing per-direction JPE with CIs, SEM, ICC and MDC95, and a plain-language verdict on whether a change is real.
  • Zero console errors; model loads entirely from local files.

Honest limits

This is not a diagnostic tool. It does not detect, diagnose, rule out or grade whiplash, concussion or anything else, and it is not a substitute for a clinician.

Joint position error is a recognised clinical measure — that is a statement about the test, not about this webcam implementation, which has not been validated against instrumented laboratory measurement or in any patient population. The estimator's 2.03 degree error is measured against synthetic rotations of a canonical mesh, not against a motion-capture rig on a real neck.

The demo runs on a synthetic replay stream because we could not ethically or practically script a real participant, and it is labelled as such throughout. The gates also reject a meaningful fraction of trials on a noisy stream, which is the intended behaviour but does mean a session can end with fewer than eight usable trials.

What we learned

Building the uncertainty model first changed the product. Once MDC95 existed, most of the features we thought we wanted — streaks, scores, progress badges — became indefensible, because we could not show they were measuring anything. What survived is smaller and much more honest.

What's next

Validation against a real goniometer or IMU on the same movements, a clinician-facing export, per-direction MDC instead of a pooled one, and a much larger normative reference before any number here is compared to anything but itself.

Built With

  • computer-vision
  • digital-health
  • indexeddb
  • javascript
  • mediapipe
  • on-device
  • privacy
  • pwa
  • statistics
  • webrtc
Share this project:

Updates