The frame is mirrored on the way in, the way
pipeline/camera.py mirrors its frames before measuring
them — so what you are looking at is exactly what was measured.
It is thrown away immediately afterwards and never leaves the tab.
What is actually being fed to the rules
A real rule engine given a plausible-looking number returns
a real-looking verdict, and there is no way to tell it apart from a true
one. So every input is listed with where it came from. Same
measurement means the definition in pipeline/traits.py,
recomputed here on this page’s own pixels. Approximated means
this project’s formula applied to a different model’s landmarks
— the arithmetic is the same, the inputs are not, and a threshold
tuned against the other model does not mean quite the same thing.
Not measured means exactly that: the value is passed as
null, and guidance.py skips a rule whose input is
None rather than judging on a guess.
| input | value | provenance | how it is obtained |
|---|
The one that is missing
The app’s qualityScore is eDifFIQA, a neural network
scored over the face crop, and it was the fairest of the quality signals
this project measured (1.21× across race groups, against 2.15×
for the brightness gate it replaced). There is no landmark arithmetic that
approximates it, so it is not approximated. It is null, which
means the quality rule never fires on this page and the
“Good quality” checklist row is an absence rather than a
pass — the row says so.
Where the assets come from
The MediaPipe runtime and the landmark model are loaded from a pinned CDN version rather than vendored into this repository. Together they are about 23 MB — roughly 9.4 MB of WebAssembly for each of the SIMD and non-SIMD builds, plus a 3.7 MB model — and committing that much third-party binary to source control to serve one demo page is a worse trade than pinning an exact version. The version is fixed, not floating, so the page cannot change behaviour under you. If the CDN is unreachable the page says so plainly at the top and disables the Start button; it does not fall back to anything, because there is nothing honest to fall back to.
What this is not showing you
The part of this project that is genuinely hard is not on this page and
cannot be. Recognition here is an LBPH model held in a 328 MB
trainer.yml, built from enrolled photographs and matched
against a gallery, with a confidence threshold that has to scale with how
many people are enrolled. None of that runs in a tab, and a page that mimed
it — a box drawn on a face, a name chosen from a list, a confidence
number invented to sit beside it — would be telling you something
untrue about the system it claims to demonstrate.
What is real here is the decision layer. The rules you are watching fire are the ones that run in front of the camera the server owns, in the same order, against the same thresholds, and the measurements they are reading are labelled with exactly how close they are to the ones the real pipeline would hand them.
What the attendance system does
None of it on this page: it needs the camera the server
owns, ~196 MB of pretrained weights and a 328 MB
trainer.yml. This is what python app.py puts on
127.0.0.1:5001, and every item names the endpoint and the
command-line equivalent that does it.
-
Register a person
POST /api/register · cli.register_user
Thirty cropped frames across five head poses — front, turned left, turned right, chin up, chin down — each one past
traits.quality_flags, the same gate the rules on this page read, minus its two pose reasons: the capture plan is deliberately asking for off-axis frames. -
Retrain
POST /api/train
Rebuilds the LBPH model from every sample on disk and reports how many images and how many identities went into it. Registering somebody retrains automatically; this is the manual one.
-
Take attendance
POST /api/attendance/start · cli.attendance
Recognises faces from the live feed and writes one
attendancerow per person per day, with the confidence it was marked at. A second sighting the same day is logged as already marked rather than recorded twice. -
Refuse a photograph
pipeline/liveness.py
MiniFASNet V2, 1.7 MB, passive: it decides from a single frame, so nobody is asked to blink or turn on command. Without it, holding up a phone showing someone’s photo marks them present, and recognition accuracy stops meaning anything.
-
Live trait readout
POST /api/traits
Age estimate and apparent gender beside the face, and a switch to turn them off. Both are weak — 12.4 years mean error, and 43.7% for Black women, worse than chance — which the write-up states rather than hides.
-
Identify a still image
POST /api/identify
Separate from the camera path and deliberately not gated on liveness, because a photograph is refused at the camera and testing recognition against stills would otherwise be impossible. It never writes to the attendance table.
-
Enrollment analysis
GET /api/analysis · cli.analyze_faces
Walks the dataset on a background thread and reports what each person’s samples are actually like — blur, exposure, pose spread — then sweeps the confidence threshold their data justifies. Held out both ways: leave-one-out for SFace, five-fold for LBPH, so no image is scored by a model that had already seen it. Bad enrolment is the usual cause of “it keeps saying Unknown”, and it is invisible until measured.
-
Reports
GET /api/report · cli.view_report
Today and all-time, per person, each row carrying the confidence the mark was made at rather than just a tick.
-
Model inventory
GET /api/models · cli.fetch_models
Names exactly which weights are missing. Detection and attendance run without them; traits, liveness, quality scoring and SFace identification degrade quietly, which looks like the app being broken rather than incomplete.
-
The camera goes off when you stop
POST /api/camera/start · /api/camera/stop
The device is opened and released explicitly rather than held for the lifetime of the process, so leaving the page does not leave the camera light on.
-
Try it without registering anyone
cli.seed_demo --people 40 --samples 10
Enrolls forty identities out of LFW so recognition can be exercised end to end on a machine nobody has been photographed on, and
--removetakes them all back out again. -
Local storage, and nothing else
core/db.py · SQLite
Three tables —
users,attendance,sample_traits— in a file beside the code. Face images live indataset/, the model intrainer.yml, and both are treated as biometric data: git ignores them, and deleting a person means deleting their folder and retraining.
The threshold those matches are compared against is not a constant — it scales with how many people are enrolled, and past about 1,646 there is no sufficient threshold at all. The slider demo plots that curve →