A Flask-based exam proctoring application that monitors live camera feeds for suspicious activity using MediaPipe computer vision models. When a face disappears or a hand is raised for a sustained period, an alert is automatically generated, a screenshot is saved to the database, and a video evidence clip is recorded to disk.
- Live MJPEG video stream from multiple cameras simultaneously
- Real-time face and hand landmark overlays drawn on-stream
- Two alert types: No Face Detected and Hand Raised
- Per-camera state machine with configurable thresholds (default: 3 seconds)
- Alert screenshots stored as PNG blobs in SQLite
- MP4 evidence videos saved automatically when suspicious activity ends
- Web UI to view, filter, delete, and download alerts and evidence
- Camera registry supporting local USB cameras and IP/RTSP streams
- Auto-detection of newly plugged USB cameras
CheatGuard AI uses a producer/consumer threading architecture to keep the web server and ML inference fully independent:
_process_camera() thread (one per camera)
├── Captures frames from OpenCV VideoCapture
├── Runs MediaPipe inference every 2nd frame
├── Draws face/hand landmark overlays
├── Encodes frame to JPEG → _jpeg_cache[cam_key]
└── Fires alerts and buffers evidence frames
generate_frames() generator (one per HTTP streaming response)
└── Reads from _jpeg_cache at up to 30 FPS → browser
The generators that serve MJPEG to the browser never touch the camera or the ML models — they only read the latest cached JPEG. This means a slow inference frame never causes the stream to stutter.
Each camera independently tracks one of three states:
| State | Meaning |
|---|---|
IDLE |
Face is present, no suspicious activity |
No Face Detected |
No face visible for ≥ 3 seconds — alert fired |
Hand Raised |
Hand visible while face is present for ≥ 3 seconds — alert fired |
Once an alert fires, the state remains set until conditions return to normal, preventing duplicate alerts during a single sustained incident.
Three MediaPipe models run per inference frame:
| Model | File | Size | Purpose |
|---|---|---|---|
| Face Detector | face_detection_short_range.tflite |
225 KB | Checks whether a face is present (confidence > 0.5) |
| Face Landmarker | face_landmarker.task |
3.6 MB | Extracts a 478-point face mesh for visual annotation |
| Hand Landmarker | hand_landmarker.task |
7.5 MB | Detects up to 2 hands with a 21-point skeleton each |
To reduce CPU load, inference runs only on every 2nd frame. Non-inference frames reuse the most recent result. RGB colour conversion (which is expensive at full resolution) is also skipped on frames where no drawing or inference is needed.
Suspicious condition met (≥ 3 s threshold)
│
├── PNG screenshot of current frame → Alert row in SQLite
│ fields: timestamp, cam_no, alert_type, alert_image (PNG binary)
│
└── Raw frame buffering enabled → evidence_queue[cam_key]
Condition clears (face returns / hand lowers)
└── Daemon thread writes buffered frames → output/evidence_cam{n}_{ts}.mp4
(15 FPS, MP4v codec, original resolution)
Evidence MP4 writing is offloaded to a separate daemon thread so the capture loop is never blocked by disk I/O.
CheatGuard-AI/
├── app.py # Flask app, routes, database model, camera registry
├── camera_processor.py # Background capture, ML inference, MJPEG streaming
├── landmarker.py # MediaPipe model config and landmark drawing helpers
├── cameras.json # Persistent camera registry (auto-created on first run)
├── requirements.txt # Python dependencies
├── .env # SECRET_KEY (create this — see Setup)
├── detection_models/
│ ├── face_detection_short_range.tflite
│ ├── face_landmarker.task
│ └── hand_landmarker.task
├── instance/
│ └── site.db # SQLite alert database (auto-created)
├── output/ # Evidence MP4 files (auto-created)
├── static/
│ └── styles.css
└── templates/
├── index.html # Dashboard with live feeds and camera controls
└── alerts.html # Alert viewer with screenshots and evidence downloads
- Python 3.10 or later
- Windows recommended (camera scanning uses DirectShow); Linux/macOS work for IP streams
pip install -r requirements.txtSECRET_KEY=your-secret-key-here
Replace the value with a long random string. This is used by Flask for session security.
python app.pyThen open http://localhost:5000 in your browser.
On first launch, CheatGuard AI will automatically scan device indices 0–9 for connected cameras and write the results to cameras.json. The SQLite database and output/ directory are also created automatically.
Cameras are identified by their device index (0, 1, 2, …). The Refresh Local Cameras button on the dashboard re-scans the system and adds any newly detected cameras to the registry without removing existing ones.
Use Add Camera on the dashboard and enter the stream URL as the source:
rtsp://192.168.1.100:554/stream
http://192.168.1.100:8080/video
The registry is stored as plain JSON alongside the application rather than in SQLite, so it survives a database reset and can be edited by hand. Each entry has three fields:
[
{ "id": 0, "source": 0, "name": "Desk Camera" },
{ "id": 1, "source": "rtsp://10.0.0.5:554/live", "name": "Exam Hall" }
]- Live MJPEG feed for every registered camera, with FPS and current state overlaid
- Alert count per camera with links to the camera's alert log
- Add camera form (accepts device index or stream URL)
- Remove camera and Refresh Local Cameras buttons
- Filterable by camera
- Each alert shows: timestamp, camera, alert type, and the PNG screenshot captured at the moment of the alert
- Per-alert delete button
- List of downloadable MP4 evidence files
- Download all evidence as a single ZIP archive
| Method | Route | Description |
|---|---|---|
| GET | / |
Dashboard |
| GET | /video_feed/<cam_id> |
MJPEG stream for a camera |
| POST | /add_camera |
Add a new camera (source, optional name) |
| POST | /remove_camera/<cam_id> |
Remove a camera by registry ID |
| POST | /refresh_cameras |
Scan for new local cameras |
| GET | /alerts |
View all alerts |
| GET | /alerts/<cam_no> |
View alerts for one camera |
| POST | /clear_alerts |
Delete all alerts |
| POST | /clear_alerts/<cam_no> |
Delete alerts for one camera |
| POST | /delete_alert/<alert_id> |
Delete a single alert |
| GET | /download/<filepath> |
Download an evidence MP4 |
| GET | /download_all_alerts |
Download all evidence as ZIP |
All tunable settings are controlled via the .env file. Create or edit .env in the project root (alongside app.py) — values are loaded at startup before any camera thread is started.
| Variable | Default | Type | Description |
|---|---|---|---|
SECRET_KEY |
(required) | str |
Flask session secret key — set to a long random string |
NO_FACE_ALERT_SECONDS |
3 |
int |
Seconds without a face before a "No Face Detected" alert fires |
HAND_RAISED_ALERT_SECONDS |
3 |
int |
Seconds a hand must be raised before a "Hand Raised" alert fires |
FACE_DETECTION_CONFIDENCE |
0.5 |
float |
Minimum MediaPipe face-detector confidence score to count as a detection |
HAND_DETECTION_CONFIDENCE |
0.5 |
float |
Minimum MediaPipe hand-landmarker handedness score to count as a detection |
INFERENCE_EVERY_N |
2 |
int |
Run ML inference on every Nth captured frame; other frames reuse the last result |
STREAM_TARGET_FPS |
30 |
int |
Maximum frames per second delivered to the browser via MJPEG |
JPEG_QUALITY |
70 |
int |
JPEG encoding quality for the stream (1–100; lower = smaller, faster) |
EVIDENCE_VIDEO_FPS |
15.0 |
float |
Frame rate used when writing evidence MP4 files to disk |
EVIDENCE_VIDEO_CODEC |
mp4v |
str |
FourCC codec string passed to cv2.VideoWriter_fourcc for evidence videos |
SECRET_KEY=your-secret-key-here
# Alert thresholds
NO_FACE_ALERT_SECONDS=3
HAND_RAISED_ALERT_SECONDS=3
# Detection confidence thresholds
FACE_DETECTION_CONFIDENCE=0.5
HAND_DETECTION_CONFIDENCE=0.5
# Performance
INFERENCE_EVERY_N=2
STREAM_TARGET_FPS=30
JPEG_QUALITY=70
# Evidence recording
EVIDENCE_VIDEO_FPS=15.0
EVIDENCE_VIDEO_CODEC=mp4v
| Package | Purpose |
|---|---|
| Flask | Web framework and HTTP routing |
| Flask-SQLAlchemy | ORM for the SQLite alert database |
| opencv-python | Camera capture, frame processing, JPEG/MP4 encoding |
| mediapipe | Face detection, face landmarking, hand landmarking |
| numpy | Array operations on frames |
| python-dotenv | Loading SECRET_KEY from .env |
See requirements.txt for pinned versions of all transitive dependencies.
See LICENSE.