Face recognition attendance across 20+ camera streams
The problem
A classroom is a room with a door and a timetable. A campus is not. People arrive at gates, hostels and libraries in a continuous stream, at whatever angle they happen to be walking, under whatever light the hour provides, and none of them stop to face a camera.
That rules out the approach used by the PTZ classroom system, which buys coverage by moving one camera between known positions. Here the cameras are fixed at chokepoints and there are more than twenty of them. Coverage is not the problem. Throughput is, and so is the fact that an enrolment photograph taken once, years ago, has to keep matching a face that changes.
The system
A producer process decodes more than twenty live RTSP feeds, applying a region-of-interest crop and a lighting check per frame before pushing into a shared in-memory queue. A consumer batches thirty-two frames per request to an InsightFace-REST service running as two TensorRT replicas on separate GPU partitions behind nginx, where SCRFD detects faces and a w600k_r50 model embeds them.
Identities resolve by cosine search over a GPU-indexed Milvus collection. Matches persist to PostgreSQL, synchronise to the institution’s ERP on a short interval, and stream back over Kafka to displays on site so a person can see they were recognised.
Kafka carries feedback and status only — never frames. Frames stay in shared memory. Putting image data on a broker would have bought durability nobody needed at a cost everybody would have paid.
Two thresholds, and why the weaker one is deliberately crippled
Matching runs in two stages against two collections. A primary match at a strict cosine threshold is treated as identification. A staging collection is searched at a looser threshold, and a hit there is allowed to write an attendance record — but nothing else.
That restriction is the whole design. Only a strict primary match may trigger the on-site feedback display, and only a strict primary match may contribute a new face crop back into the gallery. A weaker match is good enough to mark someone present; it is explicitly not good enough to be shown to a human as confirmed, and not good enough to teach the system what that person looks like.
Without that split, a self-improving gallery eats itself. Every borderline match becomes training data for the next borderline match, confidence drifts, and the drift is invisible because the system’s own scores go up, not down. Separating “confident enough to record” from “confident enough to learn from” is the cheapest available defence against that, and it costs one extra collection.
A gallery that updates itself, and what that costs
High-confidence crops captured during the day land in the staging collection, and a nightly job folds them into the index. The intent is sound: faces change, enrolment photographs do not, and a system that cannot refresh its own reference set degrades quietly from the day it is deployed.
Two costs are worth stating plainly. The nightly update replaces a person’s staging vectors with that night’s, rather than accumulating them — so the enrichment has a one-day memory, not a growing one. And nothing promotes a staging vector into the primary collection, so the strict path never actually benefits from the enrichment. The mechanism works; the pipeline it feeds stops one step short.
Dropping frames on purpose
Once the shared queue passes a set depth, the producer discards frames in blocks rather than blocking. This is the opposite choice from the PTZ system, whose queues are unbounded, and I think it is the right one here.
The reasoning: for attendance, a frame is only useful while the person is still in shot. A backlog does not represent work worth doing later — it represents a system that has already fallen behind reality and is now spending GPU time on people who have walked away. Dropping is not the failure mode; the backlog is. The visible cost is that a sustained overload silently reduces recognition opportunities, which is why queue depth is the first metric this system should expose.
Decisions I would revisit
Acknowledge before persist. The ingestion endpoint returns success once the write is scheduled, not once it has committed. It keeps the recognition path from blocking on the database. It also means a record can be acknowledged and then quietly not exist, because a failed write is logged rather than retried. The same pattern appears in the PTZ system and it is wrong in both; an outbox with retries is the fix.
Kafka topics recreated on every start, with seconds of retention. This makes startup idempotent and keeps the broker tiny. It also means the feedback stream has no history at all, so a display that reconnects has missed whatever it missed. Acceptable for a live display, and it should never be used for anything that needs to be reconstructed.
A lock that busy-waits. Recognition threads coordinate through a lock that polls rather than blocks. At low traffic it burns CPU doing nothing. It is invisible at load and wasteful at night, which is exactly backwards.
Liveness deployed but switched off. A face-liveness service is deployed alongside the stack and is currently disabled. Until it is on, a photograph held up to a gate camera is an open question rather than a closed one — and for a system that controls an attendance record, that is the gap that matters most.
What is not measured
No throughput, latency or recognition-accuracy figures exist for this deployment, and the capacity numbers that do exist are configuration values rather than observations. Batch size, replica count and queue depth describe what the system was told to do, not what it achieved.
In priority order, the instrumentation worth adding: queue depth and drop rate, which together say whether the backpressure decision above is costing anything real; end-to-end lag from frame capture to ERP acknowledgement; and the distribution of cosine distances at both thresholds, which is the only way to tell whether the strict and loose settings are still where they belong.
What this demonstrates
Two systems solving the same problem under opposite constraints, and being explicit about which constraint drove which decision. The PTZ system trades time for coverage because it has one camera per room. This one trades frames for freshness because it has twenty cameras and no ability to ask anyone to wait.
The decision I would defend hardest is the one that looks like a limitation: allowing a weak match to record attendance while forbidding it from teaching the gallery or reaching a human. A system that learns from its own uncertain outputs will become confidently wrong, and it will not tell you.
Client institutions are not named here, and no student data, imagery or configuration appears in this write-up.
Other case studies
Computer vision attendance with a PTZ camera · Production voice AI for drive-thru ordering
Full CV · Download UAE CV (PDF) · Discuss an AI engineering opportunity