Face-recognition attendance for classrooms — enroll once, scan the room, done.
Paper roll-calls and manual registers cost 5–10 minutes per session and are trivially spoofed. Commercial biometric systems are closed, priced per seat, and send face data to third parties. This project is the open middle ground:
| Manual roll call | Commercial systems | Visual Class Attendance | |
|---|---|---|---|
| Time per session | 5–10 min | ~30 s | ~30 s, hands-free |
| Spoofable by proxy | Trivially | No | Hard (top-k multi-sample matching) |
| Face data leaves the room | — | Usually yes | Never (in-process CPU inference) |
| Runs on | Paper | Vendor hardware | Any laptop / phone browser |
| Cost | Free | Per-seat licenses | Free, MIT |
┌────────┐ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Camera │ → │ YuNet detect │ → │ 5-pt alignment │ → │ ArcFace │
│ stream │ │ (ONNX) │ │ warp → 112×112 │ │ 512-d embed │
└────────┘ └─────────────┘ └──────────────────┘ └──────┬──────┘
↓
┌──────────────────────────────────────┐
│ top-3 mean distance vs class gallery │
│ ≤ tolerance → Present / Late │
└──────────────────────────────────────┘
# Enrollment: 5 jittered samples per person (flip-TTA denoises the embedding)
locs, encodings = detect_and_encode(rgb, scale=1.0, num_jitters=10)
store.enroll("STU-001", "Abebe Kebede", encodings)
# Recognition: mean of the 3 closest samples — one bad photo can neither
# fake a match nor break one.
results = recognize(rgb, class_gallery, tolerance=1.0)
# Marking: duplicate-safe per person/class/day, honors the class late rule.
store.mark("STU-001", "Abebe Kebede", status="Present", class_id="cs2a")Design decisions worth stealing:
- Top-k mean matching, not single-nearest — robust to one corrupted enrollment sample and one lucky impostor frame.
- Class-scoped galleries — recognition only considers the selected class; outsiders and other-class students fall through to Unknown.
- CSV as source of truth, Excel as a derived view — append +
fsyncis crash-safe by construction; atomic-replace JSON/pickle everywhere else. - Backend-stamped embeddings — stored samples carry a
BACKEND_ID; a model upgrade cleanly invalidates old vectors instead of silently cross-matching incompatible spaces.
| Area | What you get |
|---|---|
| Live sessions | |
| Enrollment | Hands-free auto-capture (quality-gated: single face, lighting, sharpness) or manual snapshots; 5 samples per person. |
| Self-enrollment | Per-class invite code + link (?invite=CODE). Students enroll without accounts; regenerate to revoke. |
| Attendance states | Present / Late (per-class threshold) / Excused (excluded from rate denominator, survives bulk-absent) / Absent. Editable in-app per day. |
| Reporting | Dashboard with live metrics + 14-day chart · per-student attendance rates · monthly heatmap (students × days) · CSV/Excel export. |
| Operations | Roles (admin creates teachers; optional guest sandbox), batch roster import from CSV/Excel, timezone setting, dark mode. |
git clone https://github.com/ethioel/Visual-Class-Attendance
cd Visual-Class-Attendance
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
streamlit run app.pyPick the threshold from your own population instead of guessing:
python scripts/calibrate.pygenuine max: 0.87 ← same person, different samples
impostor min: 1.18 ← different people
Good tolerance sits between these two numbers.
ArcFace reference points: genuine ≈ 0.3–0.9, impostor ≈ 1.1–1.5. If the two distributions overlap, enrollment quality is the problem — recapture with varied angle, distance, and lighting.
Streamlit Community Cloud:
- Faces are stored as 512-d embeddings, never images.
- All inference is in-process on CPU — no third-party API sees a face.
- Passwords: PBKDF2-HMAC-SHA256, 200k iterations, per-user salt, uniform verify delay (brute-force throttle + no user enumeration).
- Invite links are bearer credentials — share with the class only; ♻️ Regenerate invalidates leaks.
attendance_db/,demo_db/,models/are gitignored; real deployments belong on self-hosted hardware with institutional consent.
python -m pytest -qCovers matching logic, store flows (duplicate guard, Excused semantics, legacy migration), auth rules, and invite codes. The ML backend is lazily imported — CI runs in seconds with no ONNX/torch.
Issues and PRs welcome. Run python -m pytest -q before submitting; keep the
ML backend lazily imported so the test suite stays dependency-light.
- ArcFace w600k_r50 — recognition ONNX
- OpenCV Zoo YuNet — face detection
- Streamlit and the streamlit-camera-input-live component