This document covers:
- Student Face Registration
- Gallery Upload (Phone / Desktop)
- Webcam Capture Across Device Types
- USB Webcam Fallback for Attendance Cameras
- Troubleshooting
Students log in to their portal and navigate to Face Registration. There are two enrollment methods:
| Method | Best For | Notes |
|---|---|---|
| Upload Files | Existing photos on phone/laptop gallery | Supports JPG, PNG, BMP, WebP |
| Use Webcam | Real-time capture | Works on phone, laptop, Pi browser |
Recommended: Upload 5β10 clear, front-facing photos for best recognition accuracy. Avoid sunglasses or face coverings.
Each uploaded image is validated by the backend:
- Must contain exactly one face
- Generates a 128-dimensional dlib embedding stored in the database
- The student is immediately marked as
face_registered = trueupon the first successful sample
The most common cause of gallery upload failures was EXIF orientation. When a phone shoots in portrait mode, it stores the image sideways (landscape) and records the correct rotation in the EXIF metadata. The older code ignored this tag, so the AI received a sideways face and detected 0 faces β "failed to upload" error.
v2.1 fix (ai_engine/embedding_generator.py):
- EXIF auto-rotation β The image is opened via Pillow and rotated/flipped according to its EXIF
Orientationtag before being passed to the HOG face detector. - Upsample retry β If HOG finds 0 faces at the default scale (
upsample_num=1), it retries atupsample_num=2, which catches faces that occupy a smaller portion of the frame (e.g., a wide-angle shot). - RGB mode normalization β Converts RGBA, palette, or grayscale images to RGB automatically.
The EXIF fix relies on Pillow. The Image.Transpose enum (used for rotation) was introduced in Pillow 9.1. The code handles both old and new Pillow builds:
try:
_T = Image.Transpose # Pillow 9.1+
rotation_map = { 3: _T.ROTATE_180, 6: _T.ROTATE_270, 8: _T.ROTATE_90 }
except AttributeError:
rotation_map = { 3: 3, 6: 4, 8: 5 } # Pillow <9.1 integer constantsrequirements.txt now pins Pillow>=9.1.0 explicitly.
| EXIF Value | Meaning | Applied Transform |
|---|---|---|
| 1 | Normal (upright) | None |
| 3 | Rotated 180Β° | ROTATE_180 |
| 6 | Rotated 90Β° CW | ROTATE_270 |
| 8 | Rotated 90Β° CCW | ROTATE_90 |
| 2 | Mirrored horizontal | FLIP_LEFT_RIGHT |
| 4 | Mirrored vertical | FLIP_TOP_BOTTOM |
The original webcam code used getUserMedia({ video: { facingMode: 'user' } }). This works on phones and laptops but fails silently on Raspberry Pi, because the CSI Camera Module does not advertise a facingMode in Chromium's device enumeration β it has no concept of "front" or "rear". The browser would either:
- Fail with
OverconstrainedError(no matching device) - Return an empty stream
frontend/src/components/ui/WebcamCapture.jsx now uses a two-step strategy:
Step 1: Try facingMode: 'user'
β Works on mobile (selects front/selfie camera) β
β Works on laptop (selects built-in webcam) β
β Fails on Pi β OverconstrainedError
Step 2: On OverconstrainedError, retry without facingMode constraint
β Pi browser picks the first available device = /dev/video0 = Camera 0 β
| Device | facingMode: 'user' result | Final camera used |
|---|---|---|
| iPhone / Android | β Selects front/selfie camera | Front camera |
| Laptop | β Selects built-in webcam | Built-in webcam |
| Raspberry Pi (Chromium) | β OverconstrainedError β retries |
/dev/video0 (Camera 0) |
| Desktop with external webcam | β Selects default webcam | Default webcam |
Note: The Retry button in the UI (
startStream()) also uses this same two-step logic.
In previous versions, if Camera 0 (/dev/video0) or Camera 1 (/dev/video2) failed to open, the system would throw a RuntimeError and crash the backend startup.
v2.1 introduces a probe-and-fallback mechanism in camera_service/camera.py. When a preferred camera device cannot be opened, the system automatically tries a configurable list of fallback device indices and uses the first one that opens.
CameraManager now accepts a fallback_indices list. At startup, it probes devices in this order:
preferred_index β fallback[0] β fallback[1] β ... β RuntimeError (if none open)
Example startup log when CSI camera is missing but USB webcam is on /dev/video1:
Trying camera device index 0...
β οΈ Preferred camera /dev/video0 unavailable. Fell back to /dev/video1 (USB webcam).
Set CAMERA_USB_FALLBACK_INDICES in your .env file:
# Single USB webcam on /dev/video1
CAMERA_USB_FALLBACK_INDICES=1
# Try /dev/video1, then /dev/video3, then /dev/video4
CAMERA_USB_FALLBACK_INDICES=1,3,4Default: 1,3,4 (probes the three most common USB webcam indices after the two CSI cameras).
When using a USB webcam, add its device node to docker-compose.yml:
backend:
devices:
- /dev/video0:/dev/video0 # Camera 0 preferred (CSI)
- /dev/video1:/dev/video1 # USB webcam fallback
- /dev/video2:/dev/video2 # Camera 1 preferred (CSI)
- /dev/ttyS0:/dev/ttyS0
- /dev/i2c-1:/dev/i2c-1
privileged: trueCamera 0 and Camera 1 are never running at the same time:
- Attendance mode: Camera 0 runs, Camera 1 is stopped
- Head Count mode: Camera 1 runs, Camera 0 is stopped
This means even if both cameras fall back to the same USB webcam, there is no device conflict β the previous camera is always fully stopped and released before the next one starts.
Check the backend logs:
docker compose logs backend | grep "Camera opened\|Fell back"Expected output:
β
Camera opened on preferred device /dev/video0
β οΈ Preferred camera /dev/video2 unavailable. Fell back to /dev/video1 (USB webcam).
| Cause | Solution |
|---|---|
| Photo taken in portrait mode (EXIF) | Fixed automatically in v2.1 β ensure you are running the latest build |
| Multiple faces in the image | Use a photo with only your face, no other people in the background |
| Image file is corrupt | Try a different image |
| Image format not supported | Use JPG, PNG, BMP, or WebP |
The error message now shows the actual per-file reason from the server (e.g., "could not extract a face"), not just the generic fallback message.
- Ensure you are on HTTPS (or
localhost) β browsers blockgetUserMediaon plain HTTP - On iPhone: you must install and trust the self-signed certificate β see camera_permissions_guide.md
- On Raspberry Pi Chromium: Camera 0 must be detected. Run
ls /dev/video*to confirm
- Check backend logs:
docker compose logs backend | grep "Trying camera\|Fell back\|unavailable" - Verify the device is accessible:
ls -la /dev/video* - Ensure the device is mapped in
docker-compose.ymlunderdevices: - Check
CAMERA_USB_FALLBACK_INDICESin.envincludes the correct index