Portfolio  ›  Projects  ›  SmartFaceGuard
Computer Vision Deep Learning Python Vector Search

SmartFaceGuard,
Face Recognition Pipeline

Detect every face in a frame, turn each one into a 512-dimension vector, and ask a FAISS index who it belongs to. The architecture was sound. The code had never once run end to end — and the evidence for that was sitting in the backup archive.

◆ Research Prototype · runs locally, camera required
Context: Personal research project  ·  Role: Author, then recovery engineer
Starting point: 8 source files totalling 4.2 KB, two 0-byte index files, one 0-byte module
This pass: 7 defects fixed · 3 files written from scratch · 22-check test suite · biometric data exposure closed
7Defects fixed
22/22Checks passing
512-dFaceNet embedding
2Stages still broken
🔧 See the Repairs ⚠️ Read the Limits First
What I Found

A project that described itself as production-grade

The README opened with "a production-grade real-time face recognition system with integrated liveness detection and mask awareness." The directory told a different story: eight Python files adding up to 4.2 KB, an empty camera.py, and a FAISS index of zero bytes.

The interesting evidence was not in the source at all. It was in the compiled bytecode inside the February 2026 backup archive:

📋 What the __pycache__ proved

AI_Pipelined_Projects/smart_face_recognition/app/__pycache__/
    api.cpython-310.pyc            155 bytes
    database.cpython-310.pyc     1,538 bytes
    detection.cpython-310.pyc      755 bytes
    recognition.cpython-310.pyc    978 bytes
    __init__.cpython-310.pyc       160 bytes

    liveness       — no .pyc
    mask_detection — no .pyc
    run_camera     — no .pyc

Python writes a .pyc the first time a module is successfully imported. Four modules had been imported. The two that run_camera.py needs, and run_camera.py itself, never had been. The main loop had never been executed on this machine, not once.

That reframed the job. This was not "fix a regression" — it was "finish a build that had never completed", and then be honest on the portfolio about which parts still do not work.

Technical Approach

Seven stages, five of which work

01 CAPTURECamera, video file or still image
02 DETECTMTCNN, all faces per frame
03 CROPClamp to frame, resize 160×160
04 LIVENESSLaplacian variance filter
05 EMBEDInceptionResnetV1, 512-d, L2-normalised
06 SEARCHFAISS IndexFlatL2 nearest neighbour
07 DECIDEThreshold → identity or Unknown
🎯

Detection — MTCNN

  • Multi-task Cascaded CNN via facenet-pytorch
  • Returns every face box in the frame, with confidence
  • Fixed: boxes are now clamped to frame bounds
🧮

Embedding — FaceNet

  • InceptionResnetV1, VGGFace2 pretrained weights
  • 512-dimension vector per face
  • Fixed: fixed 160×160 input, L2-normalised output
🔍

Search — FAISS

  • IndexFlatL2, exact nearest neighbour
  • Metadata array maps index position → name
  • Fixed: handles the empty-index -1 sentinel

⚠️ Why the normalisation fix mattered more than it looks

The original computed raw FaceNet embeddings and compared them against a hardcoded L2 distance threshold of 0.8. Un-normalised FaceNet vectors have magnitudes around 10, so squared L2 distances between any two faces land far above 0.8. Every query would have returned Unknown, forever, including a photo matched against itself. The threshold was not merely mistuned — it was unreachable. Normalising to unit length puts distances in the range the threshold was clearly written for.

The Repair

Seven defects, each one blocking

#FileDefectEffect if run
1recognition.pycv2 used but never importedNameError on the first call — hard stop
2recognition.pyCrops embedded at whatever size they arrivedEmbeddings not comparable to one another
3recognition.pyEmbeddings not L2-normalised0.8 threshold mathematically unreachable
4detection.pyBox coordinates not clamped to frameFaces at frame edges produced zero-size crops
5database.pyFAISS -1 "no neighbour" not handledIndexError on a sparse index
6database.pyNo embedding-shape validationWrong dimension aborted the process inside C++
7liveness.pyVariance scales with crop resolutionOne threshold meant a different thing on every frame

Written from scratch, and labelled as such

Three files were not repairs, because there was nothing to repair. Each carries a header in the source saying so:

app/camera.py

  • Was 0 bytes, in the backup too
  • Now: capture abstraction over webcam, video file and still image

scripts/enroll_faces.py

  • README pointed at build_augmented_database.py
  • That file never existed, in any archive
  • Now: folder-per-person enrolment with --dry-run

tests/test_pipeline.py

  • No tests existed anywhere in the project
  • Now: 22 checks, runs without a camera
Verification

22 checks, no camera required

The suite runs offline so the pipeline can be verified on a machine with no webcam, which is also what makes it useful in CI. It covers real MTCNN detection against a photograph, unit-norm assertion on the embedding, and a full FAISS write-read-search round trip.

StageCheckedResult
ImportsAll 6 modules import cleanlyPass
CaptureStill-image and video-file sources openPass
DetectionMTCNN finds a face in a real photographPass
CropEdge boxes clamp without zero-size outputPass
EmbeddingShape is (1, 512)Pass
EmbeddingL2 norm == 1.0Pass
FAISSAdd, save, reload, search round tripPass
FAISSEmpty index returns Unknown, not a crashPass
LivenessResolution-independent after fixPass
Total22 checks22 pass

💻 Run it yourself

python -m venv .venv && .venv\Scripts\activate
pip install torch==2.2.2 torchvision==0.17.2 --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt

python -m tests.test_pipeline           # offline, no camera
python run_camera.py                    # webcam
python run_camera.py --source clip.mp4  # video file

FaceNet and MTCNN weights (~107 MB) download on first run. Source is not published — see Privacy.

Limitations

What this system cannot do

This section exists because the original README claimed the opposite. Everything below is a documented gap, not a caveat.

❌ There is no accuracy figure

The system has never been evaluated against a labelled verification set. No FAR, no FRR, no ROC curve, no benchmark. The 0.8 match threshold is inherited from the original code and remains uncalibrated. Any accuracy number attached to this project would be invented, so there is none anywhere on this page.

❌ "Liveness" is not anti-spoofing

The liveness stage computes Laplacian variance — a standard image-sharpness measure. It scores focus, and nothing else. There is no trained anti-spoofing model and no evaluation against any spoof dataset. A sharp photograph, or a replay on a decent screen, passes it. It is a cheap first filter that rejects blurred frames. Calling it a security control would be false.

❌ Mask detection does not work, and was not replaced

The module loaded haarcascade_mcs_nose.xml from cv2.data.haarcascades. That file has never shipped with opencv-python — verified directly: it is not present, and no nose cascade of any kind is bundled. The classifier object was therefore always empty and detectMultiScale raised on it every time.

The module now reports n/a instead of raising. No replacement classifier was substituted. Training or importing one would be building a feature that never existed and filing it as recovered work.

⚠️ Not production biometric security

No deployment, no liveness validation, no threshold calibration, no bias evaluation across demographics, no audit trail. It is a working research pipeline on a laptop. The word "production-grade" has been removed from the project's own README.

Privacy

A biometric project needs a privacy answer

🔐 A live exposure was found and closed

The project directory sits under the website's document root. The parent Irfana/.htaccess carried a recursive rule allowing every .png, .jpg and .pdf through so the awards and research-figure sections could load images.

Because <FilesMatch> applies to a directory and all its subdirectories, that rule was also serving 104 photographs of real people's faces over HTTP, at a guessable path, with no consent record. The same rule was exposing unpublished thesis manuscripts and personal CVs.

Closed on 2026-08-10 with explicit deny rules on the source tree, the thesis directory and the client-work directory. This was the most serious finding of the whole recovery pass, and it had nothing to do with the machine learning.

📸

Face images

  • 104 photographs of real people, no consent record
  • Gitignored, denied at the web server, never published
  • Unlabelled flat dump — not enrollable in any case
🧮

Embeddings

  • Embeddings are not anonymous
  • They identify a specific person and are partially invertible
  • Treated as personal data: gitignored, kept local
📌

Demonstration policy

  • No public demo — that would mean shipping a face database
  • Camera processing stays local by design
  • No biometric data appears on this page or anywhere on this site
Technologies

Actual stack

Detection & embedding
MTCNNInceptionResnetV1 VGGFace2 weightsfacenet-pytorch 2.6
Search
FAISSIndexFlatL2faiss-cpu 1.15
Runtime
Python 3.12PyTorch 2.2.2 CPUOpenCV 4.11NumPy
Practice
22-check test suiteGitignored biometricsServer-side deny rules

The Dockerfile targets a CUDA base image and was not tested in this pass. It is listed here as present, not as verified.

The useful part was the archive, not the model

The fastest way to understand this project was reading compiled bytecode timestamps in a backup ZIP. If you have a research system nobody is sure ever worked, that question is usually answerable from evidence already on disk.