SecureAttendAI / ARCHITECTURE.md
Nishant Katiyar
Deploy biometric node to HF Spaces
b561839
|
Raw
History Blame Contribute Delete
14.8 kB
# FaceDetection System β€” Architecture Documentation
> **Last Updated:** June 9, 2026
> **Stack:** Python Β· FastAPI Β· OpenCV Β· ONNX Β· SQLite Β· React
---
## Table of Contents
1. [Current Architecture](#1-current-architecture)
2. [Logic Flow (Step-by-Step)](#2-logic-flow-step-by-step)
3. [Core Models](#3-core-models)
4. [Liveness Detection](#4-liveness-detection)
5. [Attendance Logging](#5-attendance-logging)
6. [Proposed Architecture (Alternative Plan)](#6-proposed-architecture-alternative-plan)
7. [Side-by-Side Comparison](#7-side-by-side-comparison)
8. [What's Already Done vs. What Can Be Improved](#8-whats-already-done-vs-what-can-be-improved)
9. [Recommended Next Steps](#9-recommended-next-steps)
---
## 1. Current Architecture
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CAPTURE LAYER β”‚
β”‚ VideoCamera (Singleton) Β· 640Γ—480 Β· ~30 FPS cap β”‚
β”‚ OpenCV VideoCapture β†’ Horizontal Flip (mirror mode) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ DETECTION LAYER β”‚
β”‚ Model : YuNet (face_detection_yunet_2023mar.onnx) β”‚
β”‚ Format: ONNX Β· Input: dynamic (set to frame size per call) β”‚
β”‚ Output: bounding box [x, y, w, h] + 5-point facial landmarks β”‚
β”‚ + detection confidence score β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ LIVENESS LAYER β”‚
β”‚ Method : Landmark Standard-Deviation Analysis β”‚
β”‚ Buffer : Last 15 frames of 5-point landmark coordinates β”‚
β”‚ Logic : If avg. std-dev of all coordinates < 0.7 px β†’ SPOOF β”‚
β”‚ (A real face has micro-tremors; a printed photo is rigid) β”‚
β”‚ Output : is_live = True / False + liveness label β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ (only if is_live == True)
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ EMBEDDING LAYER β”‚
β”‚ Model : SFace (face_recognition_sface_2021dec.onnx) β”‚
β”‚ Format: ONNX Β· ~38 MB on disk β”‚
β”‚ Steps : 1. alignCrop(frame, face_landmarks) β†’ aligned 112Γ—112 β”‚
β”‚ 2. recognizer.feature(aligned_face) β†’ 128-dim vector β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ VERIFICATION LAYER β”‚
β”‚ Method : Cosine Similarity (cv2.FaceRecognizerSF_FR_COSINE) β”‚
β”‚ Threshold : 0.363 (calibrated by OpenCV SFace research paper) β”‚
β”‚ Strategy : Best-of-N β€” takes highest score per employee across β”‚
β”‚ all stored face templates β”‚
β”‚ Output : (employee_id, employee_name, similarity_score) | None β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ATTENDANCE LOGGER β”‚
β”‚ Debounce : 2-minute cooldown per employee_id (in-memory dict) β”‚
β”‚ Logic : check_in β†’ check_out β†’ check_in (toggle per day) β”‚
β”‚ DB : SQLite (attendance.db) β”‚
β”‚ Alerts : Thread-safe Queue β†’ SSE stream β†’ React frontend β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
---
## 2. Logic Flow (Step-by-Step)
```
Camera Frame
β”‚
β”œβ”€β–Ί [YuNet Detector]
β”‚ β”‚
β”‚ β”œβ”€β”€ No faces found ──► Clear liveness buffer, draw scan-line animation
β”‚ β”‚
β”‚ └── Faces found (1..N) ──► For each face:
β”‚ β”‚
β”‚ β”œβ”€β–Ί Parse bbox [x, y, w, h] + 5 landmarks + score
β”‚ β”‚
β”‚ β”œβ”€β–Ί Update landmark buffer (last 15 frames)
β”‚ β”‚
β”‚ β”œβ”€β–Ί [Liveness Check]
β”‚ β”‚ β”œβ”€β”€ Buffer < 8 frames ──► "PENDING..."
β”‚ β”‚ β”œβ”€β”€ std_dev < 0.7 px ──► "SPOOF: STATIC PHOTO" β†’ push SSE alert
β”‚ β”‚ └── std_dev β‰₯ 0.7 px ──► "LIVENESS: VERIFIED"
β”‚ β”‚
β”‚ β”œβ”€β–Ί [SFace Embedding] (only if live)
β”‚ β”‚ └── alignCrop β†’ feature() β†’ 128-d vector
β”‚ β”‚
β”‚ β”œβ”€β–Ί [Cosine Match]
β”‚ β”‚ β”œβ”€β”€ score β‰₯ 0.363 ──► (emp_id, name, score)
β”‚ β”‚ └── score < 0.363 ──► Unknown
β”‚ β”‚
β”‚ └─► [Attendance Trigger] (only if emp_id found)
β”‚ β”œβ”€β”€ In cooldown? ──► Skip
β”‚ β”œβ”€β”€ Determine event_type (check_in / check_out)
β”‚ β”œβ”€β”€ Log to SQLite
β”‚ └── Push notification to SSE Queue
β”‚
└─► [HUD Renderer]
β”œβ”€β”€ Neon bounding box (color: green=match, red=unknown, crimson=spoof)
β”œβ”€β”€ 5-point landmark dots
β”œβ”€β”€ Label banner (name, score %, liveness status)
└── Corner tick HUD overlay + scan-line animation
```
---
## 3. Core Models
| Property | YuNet (Detector) | SFace (Recognizer) |
|---|---|---|
| **File** | `face_detection_yunet_2023mar.onnx` | `face_recognition_sface_2021dec.onnx` |
| **Size** | ~232 KB | ~38 MB |
| **Format** | ONNX | ONNX |
| **Input** | Dynamic (frame resolution) | 112Γ—112 aligned crop |
| **Output** | bbox + 5 landmarks + score | 128-dimensional embedding vector |
| **Runtime** | OpenCV DNN (`cv2.FaceDetectorYN`) | OpenCV DNN (`cv2.FaceRecognizerSF`) |
| **Source** | [HuggingFace: opencv/face_detection_yunet](https://huggingface.co/opencv/face_detection_yunet) | [HuggingFace: opencv/face_recognition_sface](https://huggingface.co/opencv/face_recognition_sface) |
---
## 4. Liveness Detection
**Goal:** Prevent "proxy attendance" using a printed photo or screen-displayed image.
**Method: Landmark Standard-Deviation Analysis**
```python
# Collect last 15 frames of 5-point landmarks
pts = np.array(landmark_buffer) # shape: (15, 5, 2)
std_coords = np.std(pts, axis=0) # shape: (5, 2)
avg_std = np.mean(std_coords)
if avg_std < 0.7:
is_live = False # Coordinates are rigid β†’ static photo / spoof
else:
is_live = True # Natural micro-tremors detected β†’ real face
```
**Why it works:** A real human face held in front of a camera has natural micro-tremors, breathing movements, and small head shifts. A photo or screen holds perfectly still. The 0.7-pixel threshold captures this difference reliably.
**Current Limitation:** This method only catches **perfectly static** spoofs. It won't catch a video of a face played on a phone, or a photo shaken manually.
---
## 5. Attendance Logging
- **Toggle Logic:** The system alternates `check_in` β†’ `check_out` β†’ `check_in` based on the last event stored in the DB for that employee on the current day.
- **Debounce:** A 2-minute in-memory cooldown per `employee_id` prevents duplicate logs from continuous recognition.
- **Real-Time Alerts:** Every attendance event is pushed into a thread-safe `queue.Queue` which is streamed to the React frontend via **Server-Sent Events (SSE)**.
- **Spoof Alerts:** Spoof detection events also push to the SSE queue with a 10-second debounce to avoid flooding the UI.
---
## 6. Proposed Architecture (Alternative Plan)
The following is an alternative architecture plan that was evaluated for this system:
```
Capture Layer
└─► BlazeFace (initial detection)
└─► Correlation Tracker (tracks bounding box between detections)
└─► FaceNet (128-d or 512-d embeddings)
└─► Cosine Similarity (threshold: 0.7)
└─► Attendance Logger
```
**Additional tips proposed:**
- Model Quantization (ONNX / TFLite)
- Frame Skipping (process every other frame)
- Lower detection resolution (320Γ—320)
- Liveness via blink detection / MediaPipe landmarks
---
## 7. Side-by-Side Comparison
| Dimension | Current Implementation | Proposed Plan |
|---|---|---|
| **Detector** | YuNet (ONNX, ~232 KB) | BlazeFace |
| **Tracking** | Per-frame YuNet detection | BlazeFace β†’ Correlation Tracker handoff |
| **Recognizer** | SFace (ONNX, 128-d) via OpenCV | FaceNet (PyTorch / TFLite, 128-d or 512-d) |
| **Similarity** | Cosine via `cv2.FaceRecognizerSF` | Manual cosine similarity |
| **Threshold** | **0.363** (SFace-calibrated) | **0.7** *(incompatible with SFace scale)* |
| **Model Format** | βœ… Already ONNX | Needs conversion to ONNX/TFLite |
| **Liveness** | βœ… Landmark std-dev (static photo guard) | Blink detection (EAR via MediaPipe) |
| **Frame Rate** | ~30 FPS cap (sleep 0.033s) | 15–20 FPS target |
| **Complexity** | Low β€” single pipeline | Higher β€” 2-stage detect+track pipeline |
| **Dependencies** | OpenCV only (no heavy ML framework) | Requires PyTorch or TFLite runtime |
| **Status** | βœ… Production-ready | πŸ”§ Would require significant re-engineering |
### Key Conflicts to Note
> ⚠️ **Threshold Incompatibility:** The proposed threshold of `0.7` is designed for FaceNet's cosine similarity scale. SFace cosine scores use a different calibration where `0.363` is the verified boundary. Applying `0.7` to the current SFace engine would reject almost all legitimate matches.
> ℹ️ **Detector Redundancy:** YuNet already operates at a low-resolution input size and is fast enough for real-time CPU use. Adding a Correlation Tracker on top introduces state complexity with marginal speed gain for an attendance use case (stationary camera, controlled environment).
> βœ… **ONNX is already done:** Both YuNet and SFace are `.onnx` format. The quantization recommendation is already fulfilled.
---
## 8. What's Already Done vs. What Can Be Improved
### βœ… Already Implemented
- [x] ONNX model inference (YuNet + SFace)
- [x] Real-time face detection at 640Γ—480
- [x] 128-d embedding extraction with face alignment
- [x] Cosine similarity matching with calibrated threshold
- [x] In-memory embedding cache (reload on new employee registration)
- [x] Liveness detection (static photo / spoof guard)
- [x] Attendance toggle logic (check-in / check-out)
- [x] 2-minute debounce cooldown per employee
- [x] Real-time SSE alerts to frontend
- [x] Spoof alerts with 10-second debounce
- [x] Premium HUD overlay (neon bounding boxes, corner ticks, scan-line animation)
- [x] Thread-safe singleton camera instance
### πŸ”§ Improvements Worth Adding
| Priority | Improvement | Effort | Impact |
|---|---|---|---|
| ⚑ High | **Frame Skipping** β€” process every 2nd frame | Low | 30–50% CPU reduction |
| ⚑ High | **Downscale for detection** β€” run YuNet on 320Γ—240 copy, scale landmarks back to 640Γ—480 for display | Low | Faster detection pass |
| πŸ‘οΈ Medium | **Blink Detection (EAR)** β€” upgrade to MediaPipe 468-point face mesh for eye-aspect-ratio liveness | Medium | Catches video/moving-photo spoofs |
| πŸ“ Medium | **Multi-angle enrollment** β€” store 3–5 embeddings per employee from different angles | Low | Improves recognition rate |
| πŸš€ Low | **Async embedding extraction** β€” move embedding+matching to a worker thread, keep capture loop purely for frames | Medium | Eliminates blocking in capture loop |
---
## 9. Recommended Next Steps
```
Priority 1 (Quick Wins β€” 1-2 hours):
β”œβ”€β”€ Add frame_count % 2 skip in _capture_loop()
└── Add downscale copy for YuNet detect call
Priority 2 (Accuracy β€” 1 day):
└── Multi-angle face enrollment UI (capture 3-5 samples per person)
Priority 3 (Liveness Upgrade β€” 2-3 days):
└── Integrate MediaPipe FaceMesh for blink-based EAR liveness
```
---
*Generated by Antigravity AI Β· FaceDetection Project Β· June 2026*