Testing Face Embedding Nearest Neighbor Search with OpenCV SFace
What We Are Verifying
SFace is a face recognition model. It converts a face image into a 128-dimensional embedding. An embedding is a list of numbers that represents image features. The expectation is that faces from the same person will have nearby embeddings.
In this verification, I used two face image datasets and checked the following points:
- Whether the SFace ONNX model can generate 128-dimensional embeddings
- Whether another image of the same person becomes the 1-nearest-neighbor
- Whether cosine similarity differs between same-person pairs and different-person pairs
- How the results change between small grayscale face crops and aligned color face crops
Cosine similarity measures how close the directions of two vectors are. In this test, I L2-normalized the embeddings first, which means adjusting each vector length to 1 before comparison.
Target lab: kiarina/labs/2026/07/10/sface-face-embedding
Reproducing the Environment
You can build the verification environment and run it locally with the commands below. mise and uv are required. The first run also needs an internet connection because it downloads the SFace ONNX model, Olivetti Faces, and LFW.
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/kiarina/labs.git
cd labs
git sparse-checkout set .gitignore .mise/tasks Makefile mise.toml 2026/07/10/sface-face-embedding
mise -C 2026/07/10/sface-face-embedding run
LFW uses about 395 MB after extraction. Including the Python environment and model file, it is safer to reserve roughly 500 MB of free space.
Model and License
The model used here is OpenCV Zoo's face_recognition_sface_2021dec.onnx.
- Model:
face_recognition_sface_2021dec.onnx - Model URL:
https://media.githubusercontent.com/media/opencv/opencv_zoo/47534e27c9851bb1128ccc0102f1145e27f23f98/models/face_recognition_sface/face_recognition_sface_2021dec.onnx - SHA-256:
0ba9fbfa01b5270c96627c4ef784da859931e02f04419c829e83484087c34e79 - Input: 112x112 aligned BGR face crop
- Output: 128-dimensional embedding
- License: Apache-2.0
The SFace directory in OpenCV Zoo explicitly states that all files in that directory are licensed under the Apache 2.0 License.
Datasets
I used two datasets for comparison.
Olivetti Faces
Olivetti Faces contains 40 people, 10 images per person, and 400 total 64x64 grayscale face images. Grayscale means black-and-white images.
- images: 400
- resolution: 64x64 grayscale
- people: 40
- images per person: 10
- dataset SHA-256:
b612fb967f2dc77c9c62d3e1266e0c73d5fca46a4b8906c18e454d41af987794
LFW People, Funneled Color Crop
Labeled Faces in the Wild is a face image dataset closer to real-world conditions. In this test, I used the funneled version through sklearn.datasets.fetch_lfw_people, selected the first 40 people with at least 20 images in a fixed order, and used only the first 10 images for each person. "Funneled" means the faces have been preprocessed so their orientation and position are somewhat aligned.
- selected images: 400
- selected resolution: 125x94 RGB
- selected people: 40
- selected images per person: 10
- archive SHA-256:
b47c8422c8cded889dc5a13418c4bc2abbda121092b3533a83306f90d900100a
Method
For each dataset, I used 40 people x 10 images. For each person, the first 5 images were used as the reference set, and the last 5 images were used as queries.
The flow is:
- Resize the face image to 112x112
- Convert grayscale or RGB images into BGR for OpenCV
- Generate an embedding with
cv2.FaceRecognizerSF.feature - L2-normalize the embedding
- Compute cosine similarity between the query and reference set
- Check top-1 accuracy and top-5 accuracy
Top-1 accuracy is the ratio where the nearest image is from the same person. Top-5 accuracy is the ratio where at least one image from the same person appears in the nearest 5 results.
This test does not perform face detection or additional landmark alignment. Landmark alignment is preprocessing that uses points such as eyes and nose to align the face. Here, I simply resized the dataset crops to 112x112.
Results
The results executed on a Mac Studio (Apple M4 Max) are as follows.
| Dataset | Top-1 | Top-5 | Pos cosine | Neg cosine | sec/image |
|---|---|---|---|---|---|
| Olivetti Faces | 0.800 | 0.920 | 0.722 | 0.577 | 0.0057 |
| LFW people, funneled color crop | 0.820 | 0.945 | 0.463 | 0.228 | 0.0058 |
The verification environment was:
- machine: Mac Studio (Apple M4 Max)
- OS: macOS 26.5.1, arm64
- Python: 3.12.10
- OpenCV: 5.0.0
- NumPy: 2.5.1
- SciPy: 1.18.0
- scikit-learn: 1.9.0
- Pillow: 12.3.0
More detailed results are saved in the lab as output/olivetti_report.json, output/lfw_report.json, and output/summary.json.
Interpretation
The technical data is shown above, but here is the simpler reading.
SFace embeddings grouped same-person images reasonably well
Olivetti Faces reached 80.0% top-1 accuracy and 92.0% top-5 accuracy. Even with small 64x64 grayscale face images, images of the same person were often close in embedding space.
LFW performed slightly better
The funneled color crop version of LFW reached 82.0% top-1 accuracy and 94.5% top-5 accuracy. The condition is closer to what SFace expects: color face crops that are somewhat aligned.
LFW had a clearer gap between same-person and different-person pairs
For Olivetti Faces, the mean cosine similarity was 0.722 for same-person pairs and 0.577 for different-person pairs. The gap was 0.145. For LFW, the mean was 0.463 for same-person pairs and 0.228 for different-person pairs, with a larger gap of 0.235. The absolute values were lower for LFW, but the separation between same-person and different-person pairs was clearer.
This is not complete identification
Even on LFW, 18% of queries had a different person as the top-1 nearest neighbor. This is training-free nearest neighbor search, not a trained classifier. It also does not add face detection or landmark alignment. So this should be read as a technical check under this specific condition, not as a general face recognition benchmark.
Embedding generation was lightweight
Generating embeddings for 400 images took about 2.3 seconds for both datasets. That is about 0.0058 seconds per image. At this scale, it was very manageable even on CPU.
Thoughts After Verification
SFace felt quite practical as a component for turning face images into numbers that are easy to compare. It can be called directly through OpenCV's FaceRecognizerSF, and the lab also fixes the model URL and verifies the SHA-256 hash, which makes the experiment easy to reproduce.
Humans probably do not recognize someone from the face alone either. We also use voice, speaking style, clothing, and the surrounding context to remember who someone might be. Seen that way, if this result is used as one reference signal rather than as a complete face-only identifier, integrating it into an LLM agent feels realistic.
Comments
No comments yet. Start the discussion.