SP//LOG

I built a webcam focus tracker, then deleted the focus score

CogniFlow started as a webcam focus tracker, but the score looked precise while measuring the wrong thing.

CogniFlow's three screens, showing the session view, the live state readout, and the insights panel
Published
Reading
7 min
Difficulty
Intermediate
Product
CogniFlow
Status
Experimental

The first version of CogniFlow did what every webcam focus tracker does. It watched my face through the laptop camera and gave me a number out of 100.

Here is the actual formula I shipped:

scoring/engine.ts retired
score = (blinkRate_inv * 0.30)
      + (pupilDelta    * 0.25)
      + (browFurrow    * 0.20)
      + (gazeStability_inv * 0.15)
      + (headMovement_inv  * 0.10)

Five signals, weights that add to one, a nice round 0 to 100 at the end. Under 20 for ten seconds and you were "distracted". Over 80 for thirty seconds and you were in "flow". I had a ring gauge that went red, amber, green. It looked like a real instrument.

It was wrong in a way that took me a while to see, because it was wrong confidently.

The bug that wasn't a bug

I would sit and read documentation for twenty minutes, which is roughly the most focused thing I do all day, and CogniFlow would decide I was drifting.

My first instinct was that I had a threshold wrong somewhere. I nudged the weights. I widened the smoothing window. Nothing helped, because nothing was broken. Reading is calm. Reading is low arousal, low motion, low facial tension. Every input to that formula was reporting the truth, and the formula was turning the truth into a lie.

It got worse when I looked at how the thing was calibrated. On session start you did two anchors: sit still and look at a cross, then do rapid mental arithmetic. The arithmetic anchor set the top of the scale. So the ceiling of my "focus" score was pinned to doing mental arithmetic under time pressure, and ordinary sustained reading naturally sat well below it.

So I went and read the literature, which I should have done first

This is the part I would do differently. I built the scoring engine, then went looking for evidence it was measurable. The evidence is not good.

Blink rate does not have a stable direction. It is one of the better aggregated proxies for cognitive load in the lab, but it is suppressed during reading and visual tasks and increases under auditory load. Worse for my purposes, the suppression during reading is largely invariant to how hard the reading is. It also tracks dopaminergic tone and fatigue rather than task load as such. There is a paper that found a video blink detector that counted blinks accurately and still could not detect the load-driven change in blink rate that a research-grade tracker picked up. Counting blinks is easy. Reading anything into the count is not.

Webcam gaze is mostly noise at this scale. Research-grade infrared trackers work at sub-degree precision. Webcam gaze estimation lands somewhere around 2.7 to 6.95 degrees of error, and WebGazer averages above 4. Head motion makes it worse. A "gaze stability" number computed from a webcam is measuring your hardware at least as much as your attention.

Pupil dilation is not available. True pupillometry wants infrared and a high sampling rate. An RGB webcam cannot reliably separate pupil from iris, and it is worst on dark irises, which is most of the world. The best published webcam pupil models reach roughly 3.2 to 3.6% error against a Tobii reference and under-represent glasses and dark eyes. I had this weighted at 0.25. I have since zeroed it.

Brow furrow is real but small. Brow-lowerer and lid-tightener are the most consistent single facial correlates of strain, at correlations around .29 to .43. And the blendshapes I was reading them from come with a model card that scopes them to AR entertainment, with no FACS validation at all.

Add five weak signals together with weights I picked by feel, put a decimal point on the end, and the output looks far more precise than anything that went into it.

Deleting it

The commits are all there if you want to watch me change my mind:

zsh session
git log --oneline --grep scoreec048e6 state-first live view, retire the visible focus score244c27d drifting is driven by looking-away only, not the weak engagement scored2f2a7b stop pushing the cognitive score to the OS tray/notificationsf39ad9a SessionHistory row headline shows focus time, not score

The one that mattered is the second. Before it, the state machine would flag you as drifting if the engagement score sat below a threshold. That is the bug from the top of this post, encoded. Now drifting is driven only by whether your eyes are on the material. A low score does not get to accuse you of anything.

What is there instead

Four states, chosen because a webcam can actually see them:

  • Focused, eyes on the work surface
  • Drifting, eyes elsewhere for a cumulative ten seconds inside a rolling minute
  • Drowsy, sustained eye closure, the PERCLOS measure borrowed from driver monitoring
  • Away, no face for twenty seconds

The timing constants are lifted from automotive driver-state monitoring, where this problem has had a lot more money and regulation thrown at it than it has in productivity software. Euro NCAP wants a sustained few seconds before something counts as a distraction event, plus a return-and-hold before it clears. That is why a glance at a book does not trip anything, and why a quick look at your phone and back does not either.

The other change I like more than the state machine: on task means looking at the work surface, not the screen. I compute it from horizontal head yaw and ignore vertical gaze entirely. If you are working through something with a pen, looking down at the desk is on task. Every tool I have used that assumes screen equals work penalised me for using paper.

Presence, eye closure, and head direction are the three things a webcam is genuinely good at. That is the product now.

What I still do not know

I have not validated any of this against human ground truth. Not one participant. Everything above about what the signals mean comes from other people's papers, not from my users, because I do not have users.

So the last thing I wrote was not a feature, it was a protocol.

The paper is on GitHub with everything above cited properly, under CC BY. Code is MIT. Neither of them will tell you your focus is 73 out of 100, and that is the point.

Next in this series: why your blink rate lies, and what else the literature says about signals people ship with confidence.

esc