This project implements a complete face anti-spoofing pipeline for binary classification (Real vs Fake) on the NUAA dataset. The goal is to distinguish a live face from a spoof attack (printed photo or video replay) using classical machine learning methods: multi-scale LBP texture features, normalized PCA for dimensionality reduction, LDA for discriminability, and two optimized classifiers (SVM and Random Forest).
The NUAA dataset contains grayscale face images split into two classes:
- Real: genuine client faces
- Fake: impostor faces (printed photographs)
| Partition | Real | Fake | Total |
|---|---|---|---|
| Train | 1 743 | 1 748 | 3 491 |
| Test | 3 362 | 5 761 | 9 123 |
The test set is significantly imbalanced (Fake/Real ratio ~1.7), which is why Balanced Accuracy and HTER are used as primary metrics rather than raw accuracy.
The dataset is available on Kaggle: https://www.kaggle.com/datasets/aleksandrpikul222/nuaaaa
Once downloaded, place the dataset under a raw/ folder at the project root. The expected structure is:
raw/
ClientRaw/
ImposterRaw/
client_train_raw.txt
client_test_raw.txt
imposter_train_raw.txt
imposter_test_raw.txt
Raw image
-> Grayscale conversion
-> Resize to 64x64
-> Multi-scale LBP feature extraction (54-dim vector)
-> Normalized PCA (correlation matrix, 80% inertia threshold -> k* components)
-> LDA (1 discriminant component appended)
-> SVM or Random Forest classifier
Local Binary Patterns encode local texture by comparing each pixel to its P neighbors on a circle of radius R. Raw pixels are not used directly because Real and Fake faces share similar facial structure; the discriminative information lies in micro-texture artifacts (print raster, Moiré patterns, screen pixels) that LBP captures effectively.
Three scales are computed and concatenated into a 54-dimensional vector:
| Scale | P | R | Bins | Captures |
|---|---|---|---|---|
| Fine | 8 | 1 | 10 | Micro-textures, print raster, noise |
| Medium | 16 | 2 | 18 | Skin patterns, intermediate structure |
| Coarse | 24 | 3 | 26 | Contours, global regions, reflections |
Images are resized to 64x64 before LBP extraction to preserve high-frequency artifacts that disappear at lower resolutions.
PCA is applied on the correlation matrix R = (1/n) Z^T Z, where Z is the centered and standardized LBP feature matrix. Normalizing by per-feature standard deviation ensures that the three LBP scales (which have very different variances) contribute equally.
The number of retained components k* is chosen by the 80% cumulative inertia threshold, which yields k* = 10 components in our experiments. The Kaiser rule (eigenvalues > 1) is used as a secondary reference.
PCA is unsupervised and does not use class labels. Linear Discriminant Analysis adds one discriminant component (for binary classification, C - 1 = 1) by maximizing the Fisher criterion:
J(w) = (w^T S_B w) / (w^T S_W w)
where S_B is the between-class scatter matrix and S_W is the within-class scatter matrix.
The final feature vector concatenates the k* PCA components and the LDA component:
Z_final = [PC1, PC2, ..., PC_k*, LDA1] (dimension: k* + 1)
The ablation study confirms that adding the LDA component systematically improves Balanced Accuracy for all values of k.
Two supervised classifiers are trained and optimized:
SVM (Support Vector Machine): searches for an optimal separating hyperplane with maximum margin. Kernels explored: RBF, linear, polynomial.
Random Forest: ensemble of decision trees trained on random subsets of data and features, combined by majority vote.
Both classifiers are optimized using a two-phase hyperparameter search with stratified 5-fold cross-validation and Balanced Accuracy as the scoring criterion:
- Phase 1 (Random Search, 60 iterations): broad exploration of the hyperparameter space.
- Phase 2 (Grid Search): refined search around the best configuration found in phase 1. Applied to SVM only; Random Forest uses Random Search alone.
SVM search space: kernels {rbf, linear, poly}, C in [1e-3, 1e3] (log-uniform), gamma in [1e-5, 1]. Random Forest search space: n_estimators in [50, 700], max_depth in {None, 5, 10, 15, 20, 30}, max_features in {sqrt, log2, 0.3, 0.5, 0.7}, criterion in {gini, entropy}.
| Model | Accuracy | Balanced Acc. | F1 macro | F1 weighted | AUC-ROC | FAR | FRR | HTER |
|---|---|---|---|---|---|---|---|---|
| SVM (opt.) | 0.7505 | 0.7042 | 0.7130 | 0.7403 | 0.8507 | 0.4720 | 0.1196 | 0.2958 |
| Random Forest | 0.8163 | 0.8073 | 0.8044 | 0.8171 | 0.8986 | 0.2267 | 0.1587 | 0.1927 |
Random Forest outperforms SVM on every metric. Its HTER of 0.193 versus 0.296 for SVM reflects a significantly better balance between false acceptance rate (fake accepted as real) and false rejection rate (real rejected as fake).
Metrics used:
- Accuracy / Balanced Accuracy: robust to class imbalance.
- F1 macro / weighted.
- AUC-ROC: discriminative capacity of the model.
- HTER = (FAR + FRR) / 2: standard anti-spoofing metric.
.
face_spoof_detection.ipynb Main notebook (full pipeline)
Raport_Deghbar_Dekkiche.pdf Project report (French)
README.md This file
raw/ NUAA dataset (to be downloaded)
ClientRaw/
ImposterRaw/
client_train_raw.txt
client_test_raw.txt
imposter_train_raw.txt
imposter_test_raw.txt
The project runs on CPU and requires the following Python packages:
numpy
pandas
matplotlib
Pillow
tqdm
scipy
scikit-image
scikit-learn
imbalanced-learn (optional, for SMOTE)
Install with:
pip install numpy pandas matplotlib Pillow tqdm scipy scikit-image scikit-learn imbalanced-learn- Download the NUAA dataset from Kaggle and place it under
raw/as described above. - Open
face_spoof_detection.ipynbin Jupyter. - Run all cells in order. The notebook covers: environment setup, dataset loading, EDA, LBP extraction, normalized PCA, LDA, hyperparameter optimization (Random Search then Grid Search), comparative evaluation, and ablation study.
The random seed is fixed to 42 for reproducibility.
Why LBP over raw pixels: Real and Fake faces share the same facial structure, making pixel-space distributions heavily overlapping. LBP captures texture micro-artifacts introduced by the spoofing medium (print raster, Moiré, screen pixels) that are invisible in raw pixel statistics.
Why normalized PCA over standard PCA: The three LBP scales have very different variances. Normalizing by standard deviation before PCA (i.e., working on the correlation matrix rather than the covariance matrix) ensures all scales contribute equally and avoids the coarser scale dominating the principal components.
Why 80% inertia threshold: This criterion provides the best trade-off between dimensionality compression and information preservation, as confirmed by the ablation study comparing k in {2, 5, 10, 20, 28, 35, 40, 50}.
Why append LDA rather than replace PCA: PCA provides denoising and compression; LDA provides class-discriminant direction. Their combination is strictly more powerful than either alone, as the ablation confirms for all values of k. 5. L. Breiman. Random Forests. Machine Learning, 45(1):5–32, 2001. 6. R.A. Fisher. The Use of Multiple Measurements in Taxonomic Problems. Annals of Eugenics, 7(2):179–188, 1936.