A browser-native facial recognition attendance system that replaces manual roll-calls with a single camera frame — detecting, identifying, and marking every student in the room simultaneously, with no server GPU, no paid APIs, and no heavy Python dependencies.
Built by Alok Deep · Portfolio project demonstrating browser-native AI, REST API design, and full-stack development.
Taking attendance in a classroom of 60 students consumes 5–10 minutes every session — roughly 100+ hours of teaching time lost per year, per class. Biometric devices cost ₹8,000–₹25,000 per unit and require proprietary software. Cloud face recognition APIs charge per-call and send student biometric data to third-party servers.
This project tries a different answer: run the entire recognition pipeline inside the browser using WebAssembly-backed TensorFlow.js, talk to a lightweight Flask backend that only stores vectors and marks dates, and ship the whole thing as a zero-dependency web app a school IT department can run on any existing laptop.
Why browser-side recognition, not a Python model on the server? Because the question a school administrator actually has is: "can I deploy this without a GPU server, a cloud account, or a data-science team to maintain it?" face-api.js answers yes — it runs SSD MobileNet V1 at 3–5 fps on a four-year-old laptop CPU, handles multiple faces in one frame, and never sends a pixel off-device.
| Capability | Where it lives |
|---|---|
| Student registration with face data | /register — form + webcam auto-capture, 5 samples per student |
| 128-d face descriptor extraction | face-api.js → FaceRecognitionNet running in-browser via TensorFlow.js |
| Multi-face simultaneous detection | SsdMobilenetv1Options + detectAllFaces() on a 500 ms setInterval |
| Real-time bounding box overlay | HTML5 <canvas> positioned over <video>, coordinates from faceapi.resizeResults() |
| Identity matching with threshold control | Euclidean distance across all stored descriptors; adjustable 0.30–0.70 slider |
| Duplicate attendance guard | UNIQUE(student_id, date) constraint in SQLite — enforced at the DB layer |
| Batch attendance in one click | POST /api/attendance with all detected student IDs; per-student marked/already-marked response |
| Filtered records + CSV export | /records — class + date filters, Blob → <a download> client-side export |
| Model availability indicator | GET /api/models/status → green dot in sidebar when all 3 model manifests present |
| Zero cloud dependency | All processing on-device; Flask serves locally; no paid APIs, no data leaves the machine |
┌──────────────────────────────────────────────────────────────────────┐
│ PIPELINE 1 · REGISTER STUDENT │
│ Webcam → SSD MobileNet V1 (detect) → Face Landmark 68 Tiny │
│ → ResNet-34 Recognition Net → 5 × 128-d Float32Array descriptors │
│ Student form (name, roll, class) → POST /api/students │
└────────────────────────────┬─────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────────────┐
│ DATA STORE · SQLite │
│ students (id · name · roll · class · descriptors JSON · photo) │
│ attendance (id · student_id · date · time) │
│ UNIQUE(student_id, date) — duplicate attendance blocked at DB layer │
└──────────────┬───────────────────────────────┬──────────────────────┘
▼ ▼
┌──────────────────────────┐ ┌───────────────────────────────────┐
│ PIPELINE 2 · │ │ REST API LAYER │
│ TAKE ATTENDANCE │ │ Flask · app.py │
│ │ │ │
│ Webcam live feed │ │ GET /api/students │
│ detectAllFaces() every │ │ POST /api/students │
│ 500 ms via setInterval │ │ DEL /api/students/<id> │
│ │ │ POST /api/attendance │
│ Euclidean distance vs │ │ GET /api/attendance │
│ all stored descriptors │ │ GET /api/classes │
│ threshold slider 0.3–0.7│ │ GET /api/attendance/dates │
│ │ │ GET /api/models/status │
│ One click → all marked │ └──────────────────┬────────────────┘
└──────────────────────────┘ ▼
┌───────────────────────────────────┐
│ UI LAYER │
│ Jinja2 templates · HTML · CSS │
│ face-api.js 0.22.2 via CDN │
│ Canvas overlay for bbox + names │
│ 4 pages · shared sidebar nav │
└───────────────────────────────────┘
Design decisions worth calling out:
- Face recognition in the browser, not on the server. The Flask backend never touches an image. It stores descriptor vectors and dates. This means no GPU, no OpenCV install headache, no dlib compilation — and no student photos transmitted over a network.
- SQLite, not a separate DB process. Clone the repo, run two commands, open a browser. The
attendance.dbfile is created automatically. For a deployment that needs concurrent writes, the swap to PostgreSQL is a connection-string change. UNIQUE(student_id, date)in the schema, not in application code. Business rules belong at the database layer. The API still returns a per-studentalready_markedstatus so the UI can communicate it, but the constraint cannot be bypassed.setIntervalfor detection, notrequestAnimationFrame. rAF fires at 60 fps — far more than face-api.js can process, causing queued microtask backpressure. A 500 ms interval saturates the model without starving the event loop, keeping the video feed smooth.- All 5 face descriptors stored per student, none averaged. Averaging in embedding space can drift the vector away from any single real capture. Storing all 5 and taking the minimum Euclidean distance at query time is more robust to lighting and pose variation.
| Model | Source | What it answers | Output |
|---|---|---|---|
| SSD MobileNet V1 | face-api.js / TensorFlow.js | "Where are the faces in this frame?" — handles multiple overlapping faces, works at 3–5 fps on CPU | Bounding box + confidence score per face |
| Face Landmark 68 Tiny | face-api.js | "What are the exact positions of eyes, nose, mouth?" — used to align the face crop before descriptor extraction | 68 (x, y) landmark points |
| Face Recognition Net (ResNet-34) | face-api.js / TensorFlow.js | "What is the unique mathematical signature of this face?" — same person across different lighting and pose yields vectors within distance 0.50 | 128-dimensional Float32Array |
| Euclidean Distance Matching | Vanilla JS | "Is this live face the same as any enrolled student?" — compares live descriptor against all stored descriptors, returns the nearest match below threshold | Match identity + distance score (0 = identical) |
Model weights are downloaded once by setup_models.py (~18 MB total) and served locally by Flask from static/models/. The sidebar shows a live green/red status dot via GET /api/models/status.
This system is designed to be deployable on any existing school laptop with zero cost. That trade-off has real constraints worth understanding before you run it in a classroom.
What works reliably:
- Groups of 5–15 students standing within 2–3 metres of the camera, good frontal lighting
- Snap & Detect mode for a still group photo — one thorough pass is more accurate than the live loop
- Controlled environments: a lab, a seminar room, students queuing at a desk
Where it breaks down:
| Limitation | Root cause | Practical impact |
|---|---|---|
| Faces far from the camera | SSD MobileNet V1 accuracy drops sharply for faces smaller than ~80 px in the frame | Back-row students in a 60-seat hall will be missed or misidentified |
| Side profiles and downward-facing heads | Registration captures frontal faces only; the recognition net is sensitive to pose angle | Students looking at their desks, or turned sideways, will not match |
| Laptop webcam field of view | A standard built-in webcam covers ~60–70°, not a full classroom | You can only reliably capture the students directly in front of you |
| Backlit or shadowed faces | Uneven lighting changes descriptor values enough to exceed the match threshold | Students near windows or in dark corners will fail to match |
| CPU speed at scale | Each detected face is compared against all stored descriptors (N faces × S students × 5 descriptors) | Smooth up to ~15 simultaneous faces; noticeable lag beyond 25–30 |
| Threshold sensitivity | A single global threshold applies to all students and lighting conditions | Lower threshold → fewer false positives but more missed students; higher → more matches but more wrong ones |
| Minimum (works, with limits) | Recommended (comfortable classroom use) | |
|---|---|---|
| CPU | Intel Core i5 (8th gen) / Ryzen 5 | Intel Core i7 (10th gen+) / Ryzen 7 |
| RAM | 4 GB | 8 GB |
| Camera | Built-in laptop webcam (720p) | External USB webcam — 1080p, wide-angle (90°+), e.g. Logitech C920 (~₹4,000) |
| Lighting | Overhead fluorescent — acceptable | Diffused front lighting; avoid strong backlighting from windows |
| Browser | Any Chromium-based browser (Chrome, Edge) | Google Chrome — best WebAssembly / TF.js performance |
| Internet | Required once (model download ~18 MB) | Not required after setup |
| GPU | Not required | Not required — all inference runs on CPU via TF.js WASM |
| OS | Windows 10+ · macOS 12+ · Ubuntu 20.04+ | Same |
If you need to cover more students than a laptop webcam comfortably handles, these approaches work without changing any code:
- Row-by-row batching — have students approach in groups of 10–15, click Snap & Detect once per group, then Mark Attendance. Takes ~2 minutes for 60 students instead of 10.
- Dedicated wide-angle USB camera — a 1080p 90° webcam (₹3,000–5,000) roughly triples the usable capture area and resolves the FOV bottleneck.
- Dedicated lighting — a single clip-on LED ring light (~₹800) facing the students eliminates the backlit-window problem entirely.
- Threshold tuning — if your room has consistent lighting, lower the threshold to 0.40 for stricter matching; raise to 0.60 if students are farther away and you're getting too many misses.
When this architecture is the wrong tool: a full lecture hall of 200+ students filmed from a podium requires server-side inference (YOLOv8-face + ArcFace on a basic GPU), a high-resolution IP camera, and frame preprocessing to upscale small face crops before descriptor extraction. The browser-side approach here is the correct trade-off for free, zero-setup, privacy-preserving deployment in small-to-medium classrooms — not for stadium-scale attendance.
Prerequisites: Python 3.9+ · A webcam · Any modern browser (Chrome / Edge recommended)
# 1. Clone the repository
git clone https://github.com/AlokTheDataGuy/mass-attendance.git
cd mass-attendance
# 2. Install the only Python dependency
pip install flask
# 3. Download face-api.js model weights (~18 MB, one-time)
python setup_models.py
# 4. Start the server
python app.pyOpen http://localhost:5000 in your browser.
| Page | URL | Purpose |
|---|---|---|
| Dashboard | http://localhost:5000/ | KPI overview + pipeline shortcuts |
| Register Student | http://localhost:5000/register | Pipeline 1 — enrol a student with face data |
| Take Attendance | http://localhost:5000/attendance | Pipeline 2 — mark whole class in one click |
| Attendance Records | http://localhost:5000/records | Filter, view, and export attendance |
Faster re-run on an existing install:
setup_models.pyskips files already present — it's safe to re-run.
mass-attendance/
├── app.py # Flask backend · REST API · SQLite init
├── setup_models.py # One-time model weight downloader (~18 MB)
├── requirements.txt # Single dependency: Flask
├── attendance.db # SQLite database (auto-created on first run)
│
├── static/
│ ├── models/ # face-api.js weight files (downloaded by setup_models.py)
│ │ ├── ssd_mobilenetv1_model-weights_manifest.json
│ │ ├── ssd_mobilenetv1_model-shard1 / shard2
│ │ ├── face_landmark_68_tiny_model-weights_manifest.json
│ │ ├── face_landmark_68_tiny_model-shard1
│ │ ├── face_recognition_model-weights_manifest.json
│ │ └── face_recognition_model-shard1 / shard2
│ ├── css/
│ │ └── style.css # Full design system · CSS variables · sidebar layout
│ └── js/
│ ├── register.js # Pipeline 1 · webcam auto-capture · descriptor extraction
│ ├── attendance.js # Pipeline 2 · detectAllFaces() · matching · mark API
│ └── records.js # Filter controls · table render · CSV export
│
├── templates/
│ ├── base.html # Jinja2 base layout · sidebar · clock · toast system
│ ├── index.html # Dashboard · stats · pipeline cards · recent table
│ ├── register.html # Registration page · camera + form grid
│ ├── attendance.html # Attendance page · camera + detected panel grid
│ └── records.html # Records page · filter bar + table
│
└── screenshots/
├── dashboard.png
├── register.png
├── take_attendance.png
└── records.png
Legacy scripts (add_faces.py, test.py, face_recogition.py) from the original dlib/KNN prototype are retained for reference but are not used by the new pipeline.
| Layer | Choice | Why |
|---|---|---|
| Database | SQLite | Zero-setup; single file; auto-created at startup; UNIQUE constraints enforce business rules at the schema level |
| Backend | Flask | Minimal surface area for a read/write REST API; Jinja2 templating included; single pip install flask |
| Face Detection | SSD MobileNet V1 (face-api.js) | Best multi-face accuracy in the face-api.js suite; handles overlapping faces in group shots better than Tiny Face Detector |
| Face Alignment | Face Landmark 68 Tiny (face-api.js) | Lightweight 68-point model; necessary for stable descriptor extraction across head poses |
| Face Recognition | ResNet-34 Recognition Net (face-api.js) | Produces 128-d embeddings with < 0.50 intra-class Euclidean distance; open-weight, no API key |
| Runtime | TensorFlow.js (WebAssembly backend) | Runs the above models in-browser on CPU; no GPU, no server-side inference, no data leaves the device |
| Frontend | HTML5 · CSS3 · Vanilla JS | No build step; no npm; loads instantly; <canvas> overlay for bbox drawing |
| Styling | Custom CSS design system | CSS variables for consistent theming; sidebar layout; toast notification system; responsive grid |
| Templates | Jinja2 (via Flask) | Server-side render for initial HTML; JS takes over for all dynamic content |
| API calls | fetch() (native browser) |
Zero dependency; all endpoints return JSON; CSV export via Blob + URL.createObjectURL |
MIT — free to use, modify, and distribute for educational and institutional purposes.
Alok Deep — Full-stack developer (MERN) building toward data science / AI engineering roles.



